Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f0d5ad7f4 | |||
| 2b81b28bff | |||
| d0c88ce271 | |||
| f8c9df3b9a | |||
| 8b61dab1bc | |||
| 3655cd7934 | |||
| 602d1e0ddb | |||
| 457828e04d | |||
| 49889ae297 | |||
| 4d573bc934 | |||
| bdd0f19161 |
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "vrobbler"
|
||||
version = "0.1.8"
|
||||
version = "0.2.2"
|
||||
description = ""
|
||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ from django.contrib import admin
|
||||
|
||||
from music.models import Artist, Album, Track
|
||||
|
||||
|
||||
@admin.register(Album)
|
||||
class AlbumAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
@ -9,12 +10,14 @@ class AlbumAdmin(admin.ModelAdmin):
|
||||
list_filter = ("year",)
|
||||
ordering = ("name",)
|
||||
|
||||
|
||||
@admin.register(Artist)
|
||||
class ArtistAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
list_display = ("name", "musicbrainz_id")
|
||||
ordering = ("name",)
|
||||
|
||||
|
||||
@admin.register(Track)
|
||||
class TrackAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
|
||||
105
vrobbler/apps/music/aggregators.py
Normal file
105
vrobbler/apps/music/aggregators.py
Normal file
@ -0,0 +1,105 @@
|
||||
from django.db.models import Q, Count, Sum
|
||||
from typing import List, Optional
|
||||
from scrobbles.models import Scrobble
|
||||
from music.models import Track, Artist
|
||||
from videos.models import Video
|
||||
|
||||
from django.utils import timezone
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
NOW = timezone.now()
|
||||
START_OF_TODAY = datetime.combine(NOW.date(), datetime.min.time(), NOW.tzinfo)
|
||||
STARTING_DAY_OF_CURRENT_WEEK = NOW.date() - timedelta(
|
||||
days=NOW.today().isoweekday() % 7
|
||||
)
|
||||
STARTING_DAY_OF_CURRENT_MONTH = NOW.date().replace(day=1)
|
||||
STARTING_DAY_OF_CURRENT_YEAR = NOW.date().replace(month=1, day=1)
|
||||
|
||||
|
||||
def scrobble_counts():
|
||||
finished_scrobbles_qs = Scrobble.objects.filter(in_progress=False)
|
||||
data = {}
|
||||
data['today'] = finished_scrobbles_qs.filter(
|
||||
timestamp__gte=START_OF_TODAY
|
||||
).count()
|
||||
data['week'] = finished_scrobbles_qs.filter(
|
||||
timestamp__gte=STARTING_DAY_OF_CURRENT_WEEK
|
||||
).count()
|
||||
data['month'] = finished_scrobbles_qs.filter(
|
||||
timestamp__gte=STARTING_DAY_OF_CURRENT_MONTH
|
||||
).count()
|
||||
data['year'] = finished_scrobbles_qs.filter(
|
||||
timestamp__gte=STARTING_DAY_OF_CURRENT_YEAR
|
||||
).count()
|
||||
data['alltime'] = finished_scrobbles_qs.count()
|
||||
return data
|
||||
|
||||
|
||||
def week_of_scrobbles(media: str = 'tracks') -> dict[str, int]:
|
||||
scrobble_day_dict = {}
|
||||
media_filter = Q(track__isnull=False)
|
||||
|
||||
for day in range(6, -1, -1):
|
||||
start = START_OF_TODAY - timedelta(days=day)
|
||||
end = datetime.combine(start, datetime.max.time(), NOW.tzinfo)
|
||||
day_of_week = start.strftime('%A')
|
||||
if media == 'movies':
|
||||
media_filter = Q(video__videotype=Video.VideoType.MOVIE)
|
||||
if media == 'series':
|
||||
media_filter = Q(video__videotype=Video.VideoType.TV_EPISODE)
|
||||
scrobble_day_dict[day_of_week] = (
|
||||
Scrobble.objects.filter(media_filter)
|
||||
.filter(
|
||||
timestamp__gte=start,
|
||||
timestamp__lte=end,
|
||||
in_progress=False,
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
return scrobble_day_dict
|
||||
|
||||
|
||||
def top_tracks(filter: str = "today", limit: int = 15) -> List["Track"]:
|
||||
time_filter = Q(scrobble__timestamp__gte=START_OF_TODAY)
|
||||
if filter == "week":
|
||||
time_filter = Q(scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_WEEK)
|
||||
if filter == "month":
|
||||
time_filter = Q(scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_MONTH)
|
||||
if filter == "year":
|
||||
time_filter = Q(scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_YEAR)
|
||||
|
||||
return (
|
||||
Track.objects.annotate(num_scrobbles=Count("scrobble", distinct=True))
|
||||
.filter(time_filter)
|
||||
.order_by("-num_scrobbles")[:limit]
|
||||
)
|
||||
|
||||
|
||||
def top_artists(filter: str = "today", limit: int = 15) -> List["Artist"]:
|
||||
time_filter = Q(track__scrobble__timestamp__gte=START_OF_TODAY)
|
||||
if filter == "week":
|
||||
time_filter = Q(
|
||||
track__scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_WEEK
|
||||
)
|
||||
if filter == "month":
|
||||
time_filter = Q(
|
||||
track__scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_MONTH
|
||||
)
|
||||
if filter == "year":
|
||||
time_filter = Q(
|
||||
track__scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_YEAR
|
||||
)
|
||||
|
||||
return (
|
||||
Artist.objects.annotate(
|
||||
num_scrobbles=Count("track__scrobble", distinct=True)
|
||||
)
|
||||
.filter(time_filter)
|
||||
.order_by("-num_scrobbles")[:limit]
|
||||
)
|
||||
|
||||
|
||||
def artist_scrobble_count(artist_id: int, filter: str = "today") -> int:
|
||||
return Scrobble.objects.filter(track__artist=artist_id).count()
|
||||
@ -0,0 +1,35 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-11 03:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('music', '0003_album_uuid_artist_uuid_track_uuid'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='artist',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='album',
|
||||
name='musicbrainz_id',
|
||||
field=models.CharField(
|
||||
blank=True, max_length=255, null=True, unique=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='track',
|
||||
name='musicbrainz_id',
|
||||
field=models.CharField(
|
||||
blank=True, max_length=255, null=True, unique=True
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='artist',
|
||||
unique_together={('name', 'musicbrainz_id')},
|
||||
),
|
||||
]
|
||||
@ -16,7 +16,7 @@ class Album(TimeStampedModel):
|
||||
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||
name = models.CharField(max_length=255)
|
||||
year = models.IntegerField(**BNULL)
|
||||
musicbrainz_id = models.CharField(max_length=255, **BNULL)
|
||||
musicbrainz_id = models.CharField(max_length=255, unique=True, **BNULL)
|
||||
musicbrainz_releasegroup_id = models.CharField(max_length=255, **BNULL)
|
||||
musicbrainz_albumartist_id = models.CharField(max_length=255, **BNULL)
|
||||
|
||||
@ -33,6 +33,9 @@ class Artist(TimeStampedModel):
|
||||
name = models.CharField(max_length=255)
|
||||
musicbrainz_id = models.CharField(max_length=255, **BNULL)
|
||||
|
||||
class Meta:
|
||||
unique_together=[['name', 'musicbrainz_id']]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@ -42,13 +45,19 @@ class Artist(TimeStampedModel):
|
||||
|
||||
|
||||
class Track(TimeStampedModel):
|
||||
class Opinion(models.IntegerChoices):
|
||||
DOWN = -1, 'Thumbs down'
|
||||
NEUTRAL = 0, 'No opinion'
|
||||
UP = 1, 'Thumbs up'
|
||||
|
||||
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||
title = models.CharField(max_length=255, **BNULL)
|
||||
artist = models.ForeignKey(Artist, on_delete=models.DO_NOTHING)
|
||||
album = models.ForeignKey(Album, on_delete=models.DO_NOTHING, **BNULL)
|
||||
musicbrainz_id = models.CharField(max_length=255, **BNULL)
|
||||
musicbrainz_id = models.CharField(max_length=255, unique=True, **BNULL)
|
||||
run_time = models.CharField(max_length=8, **BNULL)
|
||||
run_time_ticks = models.PositiveBigIntegerField(**BNULL)
|
||||
# thumbs = models.IntegerField(default=Opinion.NEUTRAL, choices=Opinion.choices)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title} by {self.artist}"
|
||||
|
||||
@ -13,7 +13,7 @@ class ScrobbleAdmin(admin.ModelAdmin):
|
||||
"playback_position",
|
||||
"in_progress",
|
||||
)
|
||||
list_filter = ("in_progress", "source")
|
||||
list_filter = ("in_progress", "source", "track__artist")
|
||||
ordering = ("-timestamp",)
|
||||
|
||||
|
||||
|
||||
@ -99,19 +99,10 @@ class Scrobble(TimeStampedModel):
|
||||
f"Creating or updating scrobble for video {video} with data {jellyfin_data}"
|
||||
)
|
||||
scrobble = (
|
||||
Scrobble.objects.filter(video=video, user_id=user_id)
|
||||
cls.objects.filter(video=video, user_id=user_id)
|
||||
.order_by('-modified')
|
||||
.first()
|
||||
)
|
||||
# Check if playback_position_ticks has changed from this scrobble
|
||||
scrobble_changed = (
|
||||
scrobble
|
||||
and scrobble.playback_position_ticks
|
||||
!= jellyfin_data['playback_position_ticks']
|
||||
)
|
||||
if not scrobble_changed:
|
||||
logger.info('Scrobble playback has not changed, not scrobbling')
|
||||
return
|
||||
|
||||
# Backoff is how long until we consider this a new scrobble
|
||||
backoff = timezone.now() + timedelta(minutes=VIDEO_BACKOFF)
|
||||
@ -127,7 +118,7 @@ class Scrobble(TimeStampedModel):
|
||||
) -> "Scrobble":
|
||||
scrobble_data['track_id'] = track.id
|
||||
scrobble = (
|
||||
Scrobble.objects.filter(track=track, user_id=user_id)
|
||||
cls.objects.filter(track=track, user_id=user_id)
|
||||
.order_by('-modified')
|
||||
.first()
|
||||
)
|
||||
|
||||
@ -21,6 +21,12 @@ from scrobbles.models import Scrobble
|
||||
from scrobbles.serializers import ScrobbleSerializer
|
||||
from scrobbles.utils import convert_to_seconds
|
||||
from videos.models import Video
|
||||
from vrobbler.apps.music.aggregators import (
|
||||
scrobble_counts,
|
||||
top_artists,
|
||||
top_tracks,
|
||||
week_of_scrobbles,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -43,19 +49,33 @@ class RecentScrobbleList(ListView):
|
||||
def get_context_data(self, **kwargs):
|
||||
data = super().get_context_data(**kwargs)
|
||||
now = timezone.now()
|
||||
last_three_minutes = timezone.now() - timedelta(minutes=3)
|
||||
last_eight_minutes = timezone.now() - timedelta(minutes=8)
|
||||
# Find scrobbles from the last 10 minutes
|
||||
data['now_playing_list'] = Scrobble.objects.filter(
|
||||
in_progress=True,
|
||||
timestamp__gte=last_three_minutes,
|
||||
is_paused=False,
|
||||
timestamp__gte=last_eight_minutes,
|
||||
timestamp__lte=now,
|
||||
)
|
||||
data['video_scrobble_list'] = Scrobble.objects.filter(
|
||||
video__isnull=False, in_progress=False
|
||||
).order_by('-timestamp')[:10]
|
||||
data['top_daily_tracks'] = top_tracks()
|
||||
data['top_weekly_tracks'] = top_tracks(filter='week')
|
||||
data['top_monthly_tracks'] = top_tracks(filter='month')
|
||||
|
||||
data['top_daily_artists'] = top_artists()
|
||||
data['top_weekly_artists'] = top_artists(filter='week')
|
||||
data['top_monthly_artists'] = top_artists(filter='month')
|
||||
|
||||
data["weekly_data"] = week_of_scrobbles()
|
||||
data['counts'] = scrobble_counts()
|
||||
return data
|
||||
|
||||
def get_queryset(self):
|
||||
return Scrobble.objects.filter(in_progress=False).order_by(
|
||||
'-timestamp'
|
||||
)
|
||||
return Scrobble.objects.filter(
|
||||
track__isnull=False, in_progress=False
|
||||
).order_by('-timestamp')[:25]
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-11 03:23
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('videos', '0004_series_uuid_video_uuid'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='video',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='video',
|
||||
unique_together={('title', 'imdb_id')},
|
||||
),
|
||||
]
|
||||
@ -58,7 +58,8 @@ class Video(TimeStampedModel):
|
||||
imdb_id = models.CharField(max_length=20, **BNULL)
|
||||
tvrage_id = models.CharField(max_length=20, **BNULL)
|
||||
|
||||
# Metadata fields from TMDB
|
||||
class Meta:
|
||||
unique_together = [['title', 'imdb_id']]
|
||||
|
||||
def __str__(self):
|
||||
if self.video_type == self.VideoType.TV_EPISODE:
|
||||
@ -107,6 +108,7 @@ class Video(TimeStampedModel):
|
||||
video_dict["season_number"] = data_dict.get("SeasonNumber", "")
|
||||
|
||||
video, created = cls.objects.get_or_create(**video_dict)
|
||||
|
||||
if created:
|
||||
logger.debug(f"Created new video: {video}")
|
||||
else:
|
||||
|
||||
@ -6,15 +6,18 @@ from os import environ as env
|
||||
|
||||
if not 'DJANGO_SETTINGS_MODULE' in env:
|
||||
from vrobbler import settings
|
||||
|
||||
env.setdefault('DJANGO_SETTINGS_MODULE', settings.__name__)
|
||||
|
||||
|
||||
import django
|
||||
|
||||
django.setup()
|
||||
|
||||
# this line must be after django.setup() for logging configure
|
||||
logger = logging.getLogger('vrobbler')
|
||||
|
||||
|
||||
def main():
|
||||
# to get configured settings
|
||||
from django.conf import settings
|
||||
|
||||
@ -89,6 +89,7 @@ INSTALLED_APPS = [
|
||||
"rest_framework",
|
||||
"allauth",
|
||||
"allauth.account",
|
||||
"allauth.socialaccount",
|
||||
"django_celery_results",
|
||||
]
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
{% load static %}
|
||||
{% load humanize %}
|
||||
<!doctype html>
|
||||
<html class="no-js" lang="">
|
||||
<head>
|
||||
@ -7,11 +8,10 @@
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.js" integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.10/clipboard.min.js"></script>
|
||||
|
||||
<style type="text/css">
|
||||
dl {
|
||||
display: flex;
|
||||
@ -48,76 +48,272 @@
|
||||
border-radius: 3px;
|
||||
transition: width 500ms ease-in-out;
|
||||
}
|
||||
.bd-placeholder-img {
|
||||
font-size: 1.125rem;
|
||||
text-anchor: middle;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.bd-placeholder-img-lg {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
}
|
||||
body {
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
||||
.feather {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sidebar
|
||||
*/
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
/* rtl:raw:
|
||||
right: 0;
|
||||
*/
|
||||
bottom: 0;
|
||||
/* rtl:remove */
|
||||
left: 0;
|
||||
z-index: 100; /* Behind the navbar */
|
||||
padding: 48px 0 0; /* Height of navbar */
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.sidebar {
|
||||
top: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-sticky {
|
||||
position: relative;
|
||||
top: 0;
|
||||
height: calc(100vh - 48px);
|
||||
padding-top: .5rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link .feather {
|
||||
margin-right: 4px;
|
||||
color: #727272;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #2470dc;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover .feather,
|
||||
.sidebar .nav-link.active .feather {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.sidebar-heading {
|
||||
font-size: .75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/*
|
||||
* Navbar
|
||||
*/
|
||||
|
||||
.navbar-brand {
|
||||
padding-top: .75rem;
|
||||
padding-bottom: .75rem;
|
||||
font-size: 1rem;
|
||||
background-color: rgba(0, 0, 0, .25);
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
|
||||
}
|
||||
|
||||
.navbar .navbar-toggler {
|
||||
top: .25rem;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.navbar .form-control {
|
||||
padding: .75rem 1rem;
|
||||
border-width: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.form-control-dark {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
border-color: rgba(255, 255, 255, .1);
|
||||
}
|
||||
|
||||
.form-control-dark:focus {
|
||||
border-color: transparent;
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
|
||||
}
|
||||
|
||||
</style>
|
||||
{% block head_extra %}{% endblock %}
|
||||
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<!-- Place favicon.ico in the root directory -->
|
||||
|
||||
<script>
|
||||
function checkUpdate(){
|
||||
$.get('/library/update/status/', function(data) {
|
||||
$('#library-update-status').html("");
|
||||
console.log('Checking for task');
|
||||
setTimeout(checkUpdate,5000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<!--[if lt IE 8]>
|
||||
<p class="browserupgrade">
|
||||
You are using an <strong>outdated</strong> browser. Please
|
||||
<a href="http://browsehappy.com/">upgrade your browser</a> to improve
|
||||
your experience.
|
||||
</p>
|
||||
<![endif]-->
|
||||
<div class="container">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<a class="navbar-brand" href="/">Vrobbler</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
|
||||
<a class="navbar-brand col-md-3 col-lg-2 me-0 px-3" href="#">Vrobbler</a>
|
||||
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'home' %}">Recent<span class="sr-only"></span></a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Movies</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<a class="dropdown-item" href="{% url "videos:movie_list" %}">All</a>
|
||||
{% for movie in movie_list %}
|
||||
<a class="dropdown-item" href="{{movie.get_absolute_url}}">{{movie.title}}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Shows</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<a class="dropdown-item" href="{ url "games:gamecollection_list" %}">All</a>
|
||||
{% for series in series_list %}
|
||||
<a class="dropdown-item" href="{{series.get_absolute_url}}">{{series.name}}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
|
||||
<div class="navbar-nav">
|
||||
<div class="nav-item text-nowrap">
|
||||
{% if user.is_authenticated %}
|
||||
<a class="nav-link px-3" href="{% url "account_logout" %}">Sign out</a>
|
||||
{% else %}
|
||||
<a class="nav-link px-3" href="{% url "account_login" %}">Sign in</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if request.user.is_authenticated %}
|
||||
<a class="nav-link" href="{% url 'account_logout' %}">Logout<span class="sr-only"></span></a>
|
||||
{% else %}
|
||||
<a class="nav-link" href="{% url 'account_login' %}">Login<span class="sr-only"></span></a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<h1>{% block title %}{% endblock %}</h1>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
|
||||
<div class="position-sticky pt-3">
|
||||
{% if now_playing_list %}
|
||||
<ul style="padding-right:10px;">
|
||||
<b>Now playing</b>
|
||||
{% for scrobble in now_playing_list %}
|
||||
{% if scrobble.video %}
|
||||
<div>
|
||||
{{scrobble.video.title}}<br/>
|
||||
<small>{{scrobble.created|naturaltime}}<br/>
|
||||
from {{scrobble.source}}</small>
|
||||
<div class="progress-bar">
|
||||
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if scrobble.track %}
|
||||
<div>
|
||||
{{scrobble.track.title}}<br/>
|
||||
<em>{{scrobble.track.artist}}</em><br/>
|
||||
<small>{{scrobble.created|naturaltime}}<br/>
|
||||
from {{scrobble.source}}</small>
|
||||
<div class="progress-bar" style="margin-right:5px;">
|
||||
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<hr/>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="#">
|
||||
<span data-feather="music"></span>
|
||||
Tracks
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">
|
||||
<span data-feather="user"></span>
|
||||
Artists
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">
|
||||
<span data-feather="film"></span>
|
||||
Movies
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">
|
||||
<span data-feather="tv"></span>
|
||||
Series
|
||||
</a>
|
||||
</li>
|
||||
{% if user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/">
|
||||
<span data-feather="key"></span>
|
||||
Admin
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% block extra_nav %}
|
||||
{% endblock %}
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% block extra_js %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script>
|
||||
<script><!-- comment ------------------------------------------------->
|
||||
/* globals Chart:false, feather:false */
|
||||
(function () {
|
||||
'use strict'
|
||||
|
||||
feather.replace({ 'aria-hidden': 'true' })
|
||||
|
||||
// Graphs
|
||||
var ctx = document.getElementById('myChart')
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
var myChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [
|
||||
{% for day in weekly_data.keys %}
|
||||
"{{day}}"{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
],
|
||||
datasets: [{
|
||||
data: [
|
||||
{% for count in weekly_data.values %}
|
||||
{{count}}{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
],
|
||||
lineTension: 0,
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: '#007bf0',
|
||||
borderWidth: 4,
|
||||
pointBackgroundColor: '#007bff'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
beginAtZero: false
|
||||
}
|
||||
}]
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
})
|
||||
})()
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,50 +1,128 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load humanize %}
|
||||
|
||||
{% block title %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if now_playing_list %}
|
||||
<h2>Now playing</h2>
|
||||
{% for scrobble in now_playing_list %}
|
||||
{% if scrobble.video %}
|
||||
<dl class="latest-scrobble">
|
||||
<dt>{{scrobble.video.title}} - {{scrobble.video}}</dt>
|
||||
<dd>
|
||||
Started {{scrobble.created|naturaltime}} from {{scrobble.source}}
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">Dashboard</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary">Share</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary">Export</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle">
|
||||
<span data-feather="calendar"></span>
|
||||
This week
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-bar">
|
||||
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
|
||||
</div>
|
||||
</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
{% if scrobble.track %}
|
||||
<dl class="latest-scrobble">
|
||||
<dt>{{scrobble.track.title}} by {{scrobble.track.artist}} from {{scrobble.track.album}}</dt>
|
||||
<dd>
|
||||
Started {{scrobble.created|naturaltime}} from {{scrobble.source}}
|
||||
<div class="progress-bar">
|
||||
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
|
||||
</div>
|
||||
</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
<br />
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<h2>Last scrobbles</h2>
|
||||
<ul>
|
||||
{% for scrobble in object_list %}
|
||||
<li>
|
||||
{{scrobble.timestamp|naturaltime}}:
|
||||
{% if scrobble.video %}
|
||||
🎥 watched <a href="{{scrobble.video.imdb_link}}">{{scrobble.video}}{% if scrobble.video.video_type == 'E' %} - {{scrobble.video.title}}{% endif %}</a></li>
|
||||
{% endif %}
|
||||
{% if scrobble.track %}
|
||||
🎶 listened to <a href="{{scrobble.track.mb_link}}">{{scrobble.track.title}}</a> by <a href="{{scrobble.track.artist.mb_link}}">{{scrobble.track.artist}}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<canvas class="my-4 w-100" id="myChart" width="900" height="380"></canvas>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<h2>Top artists this week</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Artist</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for artist in top_weekly_artists %}
|
||||
<tr>
|
||||
<td>{{artist.num_scrobbles}}</td>
|
||||
<td>{{artist.name}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg">
|
||||
<h2>Top tracks this week</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Track</th>
|
||||
<th scope="col">Artist</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for track in top_weekly_tracks %}
|
||||
<tr>
|
||||
<td>{{track.num_scrobbles}}</td>
|
||||
<td>{{track.title}}</td>
|
||||
<td>{{track.artist.name}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
<h2>Latest listened</h2>
|
||||
<p>Today <b>{{counts.today}}</b> | This Week <b>{{counts.week}}</b> | This Month <b>{{counts.month}}</b> | This Year <b>{{counts.year}}</b> | All Time <b>{{counts.alltime}}</b></p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Time</th>
|
||||
<th scope="col">Track</th>
|
||||
<th scope="col">Artist</th>
|
||||
<th scope="col">Source</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scrobble in object_list %}
|
||||
<tr>
|
||||
<td>{{scrobble.timestamp|naturaltime}}</td>
|
||||
<td>{{scrobble.track.title}}</td>
|
||||
<td>{{scrobble.track.artist.name}}</td>
|
||||
<td>{{scrobble.source}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg">
|
||||
<h2>Latest watched</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Time</th>
|
||||
<th scope="col">Title</th>
|
||||
<th scope="col">Series</th>
|
||||
<th scope="col">Source</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scrobble in video_scrobble_list %}
|
||||
<tr>
|
||||
<td>{{scrobble.timestamp|naturaltime}}</td>
|
||||
<td>{% if scrobble.video.tv_series %}E{{scrobble.video.season_number}}S{{scrobble.video.season_number}} -{% endif %} {{scrobble.video.title}}</td>
|
||||
<td>{% if scrobble.video.tv_series %}{{scrobble.video.tv_series}}{% endif %}</td>
|
||||
<td>{{scrobble.source}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user