Add timezone support and an authenticated view
This commit is contained in:
@ -1,12 +1,13 @@
|
|||||||
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
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
from django.db.models import Count, Q, Sum
|
||||||
|
from django.utils import timezone
|
||||||
|
from music.models import Artist, Track
|
||||||
|
from scrobbles.models import Scrobble
|
||||||
|
from videos.models import Video
|
||||||
|
from vrobbler.apps.profiles.utils import now_user_timezone
|
||||||
|
|
||||||
NOW = timezone.now()
|
NOW = timezone.now()
|
||||||
START_OF_TODAY = datetime.combine(NOW.date(), datetime.min.time(), NOW.tzinfo)
|
START_OF_TODAY = datetime.combine(NOW.date(), datetime.min.time(), NOW.tzinfo)
|
||||||
@ -17,58 +18,103 @@ STARTING_DAY_OF_CURRENT_MONTH = NOW.date().replace(day=1)
|
|||||||
STARTING_DAY_OF_CURRENT_YEAR = NOW.date().replace(month=1, day=1)
|
STARTING_DAY_OF_CURRENT_YEAR = NOW.date().replace(month=1, day=1)
|
||||||
|
|
||||||
|
|
||||||
def scrobble_counts():
|
def scrobble_counts(user):
|
||||||
finished_scrobbles_qs = Scrobble.objects.filter(played_to_completion=True)
|
|
||||||
|
now = timezone.now()
|
||||||
|
user_filter = Q()
|
||||||
|
if user.is_authenticated:
|
||||||
|
now = now_user_timezone(user.profile)
|
||||||
|
user_filter = Q(user=user)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
finished_scrobbles_qs = Scrobble.objects.filter(
|
||||||
|
user_filter, played_to_completion=True
|
||||||
|
)
|
||||||
data = {}
|
data = {}
|
||||||
data['today'] = finished_scrobbles_qs.filter(
|
data['today'] = finished_scrobbles_qs.filter(
|
||||||
timestamp__gte=START_OF_TODAY
|
timestamp__gte=start_of_today
|
||||||
).count()
|
).count()
|
||||||
data['week'] = finished_scrobbles_qs.filter(
|
data['week'] = finished_scrobbles_qs.filter(
|
||||||
timestamp__gte=STARTING_DAY_OF_CURRENT_WEEK
|
timestamp__gte=starting_day_of_current_week
|
||||||
).count()
|
).count()
|
||||||
data['month'] = finished_scrobbles_qs.filter(
|
data['month'] = finished_scrobbles_qs.filter(
|
||||||
timestamp__gte=STARTING_DAY_OF_CURRENT_MONTH
|
timestamp__gte=starting_day_of_current_month
|
||||||
).count()
|
).count()
|
||||||
data['year'] = finished_scrobbles_qs.filter(
|
data['year'] = finished_scrobbles_qs.filter(
|
||||||
timestamp__gte=STARTING_DAY_OF_CURRENT_YEAR
|
timestamp__gte=starting_day_of_current_year
|
||||||
).count()
|
).count()
|
||||||
data['alltime'] = finished_scrobbles_qs.count()
|
data['alltime'] = finished_scrobbles_qs.count()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def week_of_scrobbles(media: str = 'tracks') -> dict[str, int]:
|
def week_of_scrobbles(user=None, media: str = 'tracks') -> dict[str, int]:
|
||||||
|
|
||||||
|
now = timezone.now()
|
||||||
|
user_filter = Q()
|
||||||
|
if user.is_authenticated:
|
||||||
|
now = now_user_timezone(user.profile)
|
||||||
|
user_filter = Q(user=user)
|
||||||
|
|
||||||
|
start_of_today = datetime.combine(
|
||||||
|
now.date(), datetime.min.time(), now.tzinfo
|
||||||
|
)
|
||||||
|
|
||||||
scrobble_day_dict = {}
|
scrobble_day_dict = {}
|
||||||
|
base_qs = Scrobble.objects.filter(user_filter, played_to_completion=True)
|
||||||
|
|
||||||
media_filter = Q(track__isnull=False)
|
media_filter = Q(track__isnull=False)
|
||||||
|
if media == 'movies':
|
||||||
|
media_filter = Q(video__video_type=Video.VideoType.MOVIE)
|
||||||
|
if media == 'series':
|
||||||
|
media_filter = Q(video__video_type=Video.VideoType.TV_EPISODE)
|
||||||
|
|
||||||
for day in range(6, -1, -1):
|
for day in range(6, -1, -1):
|
||||||
start = START_OF_TODAY - timedelta(days=day)
|
start = start_of_today - timedelta(days=day)
|
||||||
end = datetime.combine(start, datetime.max.time(), NOW.tzinfo)
|
end = datetime.combine(start, datetime.max.time(), now.tzinfo)
|
||||||
day_of_week = start.strftime('%A')
|
day_of_week = start.strftime('%A')
|
||||||
if media == 'movies':
|
|
||||||
media_filter = Q(video__videotype=Video.VideoType.MOVIE)
|
scrobble_day_dict[day_of_week] = base_qs.filter(
|
||||||
if media == 'series':
|
media_filter,
|
||||||
media_filter = Q(video__videotype=Video.VideoType.TV_EPISODE)
|
timestamp__gte=start,
|
||||||
scrobble_day_dict[day_of_week] = (
|
timestamp__lte=end,
|
||||||
Scrobble.objects.filter(media_filter)
|
played_to_completion=True,
|
||||||
.filter(
|
).count()
|
||||||
timestamp__gte=start,
|
|
||||||
timestamp__lte=end,
|
|
||||||
played_to_completion=True,
|
|
||||||
)
|
|
||||||
.count()
|
|
||||||
)
|
|
||||||
|
|
||||||
return scrobble_day_dict
|
return scrobble_day_dict
|
||||||
|
|
||||||
|
|
||||||
def top_tracks(filter: str = "today", limit: int = 15) -> List["Track"]:
|
def top_tracks(
|
||||||
time_filter = Q(scrobble__timestamp__gte=START_OF_TODAY)
|
user: "User", filter: str = "today", limit: int = 15
|
||||||
|
) -> List["Track"]:
|
||||||
|
|
||||||
|
now = timezone.now()
|
||||||
|
if user.is_authenticated:
|
||||||
|
now = now_user_timezone(user.profile)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
time_filter = Q(scrobble__timestamp__gte=start_of_today)
|
||||||
if filter == "week":
|
if filter == "week":
|
||||||
time_filter = Q(scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_WEEK)
|
time_filter = Q(scrobble__timestamp__gte=starting_day_of_current_week)
|
||||||
if filter == "month":
|
if filter == "month":
|
||||||
time_filter = Q(scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_MONTH)
|
time_filter = Q(scrobble__timestamp__gte=starting_day_of_current_month)
|
||||||
if filter == "year":
|
if filter == "year":
|
||||||
time_filter = Q(scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_YEAR)
|
time_filter = Q(scrobble__timestamp__gte=starting_day_of_current_year)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
Track.objects.filter(time_filter)
|
Track.objects.filter(time_filter)
|
||||||
@ -77,7 +123,9 @@ def top_tracks(filter: str = "today", limit: int = 15) -> List["Track"]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def top_artists(filter: str = "today", limit: int = 15) -> List["Artist"]:
|
def top_artists(
|
||||||
|
user: "User", filter: str = "today", limit: int = 15
|
||||||
|
) -> List["Artist"]:
|
||||||
time_filter = Q(track__scrobble__timestamp__gte=START_OF_TODAY)
|
time_filter = Q(track__scrobble__timestamp__gte=START_OF_TODAY)
|
||||||
if filter == "week":
|
if filter == "week":
|
||||||
time_filter = Q(
|
time_filter = Q(
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import settings
|
|
||||||
import pytz
|
import pytz
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
# need to translate to a non-naive timezone, even if timezone == settings.TIME_ZONE, so we can compare two dates
|
# need to translate to a non-naive timezone, even if timezone == settings.TIME_ZONE, so we can compare two dates
|
||||||
def to_user_timezone(date, profile):
|
def to_user_timezone(date, profile):
|
||||||
@ -17,7 +20,12 @@ def to_system_timezone(date, profile):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def now_timezone():
|
def now_user_timezone(profile):
|
||||||
|
timezone.activate(pytz.timezone(profile.timezone))
|
||||||
|
return timezone.localtime(timezone.now())
|
||||||
|
|
||||||
|
|
||||||
|
def now_system_timezone():
|
||||||
return (
|
return (
|
||||||
datetime.datetime.now()
|
datetime.datetime.now()
|
||||||
.replace(tzinfo=pytz.timezone(settings.TIME_ZONE))
|
.replace(tzinfo=pytz.timezone(settings.TIME_ZONE))
|
||||||
|
|||||||
@ -9,6 +9,7 @@ from django.utils import timezone
|
|||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
from django.views.generic.list import ListView
|
from django.views.generic.list import ListView
|
||||||
|
import pytz
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import api_view
|
from rest_framework.decorators import api_view
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -45,31 +46,44 @@ class RecentScrobbleList(ListView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
data = super().get_context_data(**kwargs)
|
data = super().get_context_data(**kwargs)
|
||||||
|
user = self.request.user
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
data['now_playing_list'] = Scrobble.objects.filter(
|
if self.request.user.is_authenticated:
|
||||||
in_progress=True,
|
timezone.activate(pytz.timezone(user.profile.timezone))
|
||||||
is_paused=False,
|
now = timezone.localtime(timezone.now())
|
||||||
timestamp__lte=now,
|
data['now_playing_list'] = Scrobble.objects.filter(
|
||||||
)
|
in_progress=True,
|
||||||
data['video_scrobble_list'] = Scrobble.objects.filter(
|
is_paused=False,
|
||||||
video__isnull=False, played_to_completion=True
|
timestamp__lte=now,
|
||||||
).order_by('-timestamp')[:15]
|
user=user,
|
||||||
data['podcast_scrobble_list'] = Scrobble.objects.filter(
|
)
|
||||||
podcast_episode__isnull=False, played_to_completion=True
|
|
||||||
).order_by('-timestamp')[:15]
|
|
||||||
data['sport_scrobble_list'] = Scrobble.objects.filter(
|
|
||||||
sport_event__isnull=False, played_to_completion=True
|
|
||||||
).order_by('-timestamp')[:15]
|
|
||||||
# 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()
|
completed_for_user = Scrobble.objects.filter(
|
||||||
data['top_weekly_artists'] = top_artists(filter='week')
|
played_to_completion=True, user=user
|
||||||
data['top_monthly_artists'] = top_artists(filter='month')
|
)
|
||||||
|
data['video_scrobble_list'] = completed_for_user.filter(
|
||||||
|
video__isnull=False
|
||||||
|
).order_by('-timestamp')[:15]
|
||||||
|
|
||||||
data["weekly_data"] = week_of_scrobbles()
|
data['podcast_scrobble_list'] = completed_for_user.filter(
|
||||||
data['counts'] = scrobble_counts()
|
podcast_episode__isnull=False
|
||||||
|
).order_by('-timestamp')[:15]
|
||||||
|
|
||||||
|
data['sport_scrobble_list'] = completed_for_user.filter(
|
||||||
|
sport_event__isnull=False
|
||||||
|
).order_by('-timestamp')[:15]
|
||||||
|
|
||||||
|
# data['top_daily_tracks'] = top_tracks()
|
||||||
|
data['top_weekly_tracks'] = top_tracks(user, filter='week')
|
||||||
|
data['top_monthly_tracks'] = top_tracks(user, filter='month')
|
||||||
|
|
||||||
|
# data['top_daily_artists'] = top_artists()
|
||||||
|
data['top_weekly_artists'] = top_artists(user, filter='week')
|
||||||
|
data['top_monthly_artists'] = top_artists(user, filter='month')
|
||||||
|
|
||||||
|
data["weekly_data"] = week_of_scrobbles(user=user)
|
||||||
|
|
||||||
|
data['counts'] = scrobble_counts(user)
|
||||||
data['imdb_form'] = ScrobbleForm
|
data['imdb_form'] = ScrobbleForm
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|||||||
@ -53,7 +53,7 @@ TMDB_API_KEY = os.getenv("VROBBLER_TMDB_API_KEY", "")
|
|||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
TIME_ZONE = os.getenv("VROBBLER_TIME_ZONE", "EST")
|
TIME_ZONE = os.getenv("VROBBLER_TIME_ZONE", "US/Eastern")
|
||||||
|
|
||||||
ALLOWED_HOSTS = ["*"]
|
ALLOWED_HOSTS = ["*"]
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
@ -61,16 +61,9 @@ CSRF_TRUSTED_ORIGINS = [
|
|||||||
]
|
]
|
||||||
X_FRAME_OPTIONS = "SAMEORIGIN"
|
X_FRAME_OPTIONS = "SAMEORIGIN"
|
||||||
|
|
||||||
|
|
||||||
CACHALOT_TIMEOUT = os.getenv("VROBBLER_CACHALOT_TIMEOUT", 3600)
|
CACHALOT_TIMEOUT = os.getenv("VROBBLER_CACHALOT_TIMEOUT", 3600)
|
||||||
REDIS_URL = os.getenv("VROBBLER_REDIS_URL", None)
|
REDIS_URL = os.getenv("VROBBLER_REDIS_URL", None)
|
||||||
|
|
||||||
CELERY_TASK_ALWAYS_EAGER = os.getenv("VROBBLER_SKIP_CELERY", False)
|
|
||||||
CELERY_BROKER_URL = REDIS_URL if REDIS_URL else "memory://localhost/"
|
|
||||||
CELERY_RESULT_BACKEND = "django-db"
|
|
||||||
CELERY_TIMEZONE = os.getenv("VROBBLER_TIME_ZONE", "EST")
|
|
||||||
CELERY_TASK_TRACK_STARTED = True
|
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="shortcut icon" type="image/png" href="{% static 'images/favicon.ico' %}"/>
|
<link rel="shortcut icon" type="image/png" href="{% static 'images/favicon.ico' %}"/>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" 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/popper.js@1.12.9/dist/umd/popper.min.js" crossorigin="anonymous"></script>
|
||||||
<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://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://code.jquery.com/jquery-3.3.1.js" integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.10/clipboard.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.10/clipboard.min.js"></script>
|
||||||
@ -175,16 +176,18 @@
|
|||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{% if user.is_authenticated %}
|
||||||
<form id="scrobble-form" action="{% url 'imdb-manual-scrobble' %}" method="post">
|
<form id="scrobble-form" action="{% url 'imdb-manual-scrobble' %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ imdb_form }}
|
{{ imdb_form }}
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
<div class="navbar-nav">
|
<div class="navbar-nav">
|
||||||
<div class="nav-item text-nowrap">
|
<div class="nav-item text-nowrap">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<a class="nav-link px-3" href="{% url "account_logout" %}">Sign out</a>
|
<a class="nav-link px-3" href="{% url "account_logout" %}">Sign out</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="nav-link px-3" href="{% url "account_login" %}">Sign in</a>
|
<a class="nav-link px-3" href="{% url "account_login" %}">Sign in</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -194,7 +197,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
|
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
|
||||||
<div class="position-sticky pt-3">
|
<div class="position-sticky pt-3">
|
||||||
{% if now_playing_list %}
|
{% if now_playing_list and user.is_authenticated %}
|
||||||
<ul style="padding-right:10px;">
|
<ul style="padding-right:10px;">
|
||||||
<b>Now playing</b>
|
<b>Now playing</b>
|
||||||
{% for scrobble in now_playing_list %}
|
{% for scrobble in now_playing_list %}
|
||||||
|
|||||||
@ -7,15 +7,21 @@
|
|||||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
<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>
|
<h1 class="h2">Dashboard</h1>
|
||||||
<div class="btn-toolbar mb-2 mb-md-0">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<div class="btn-group me-2">
|
||||||
<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">Share</button>
|
<button type="button" class="btn btn-sm btn-outline-secondary">Export</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary">Export</button>
|
</div>
|
||||||
</div>
|
<div class="dropdown">
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle">
|
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" id="graphDateButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<span data-feather="calendar"></span>
|
<span data-feather="calendar"></span>
|
||||||
This week
|
This week
|
||||||
</button>
|
</button>
|
||||||
|
<div class="dropdown-menu" aria-labelledby="graphDateButton">
|
||||||
|
<a class="dropdown-item" href="#">Action</a>
|
||||||
|
<a class="dropdown-item" href="#">Another action</a>
|
||||||
|
<a class="dropdown-item" href="#">Something else here</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -23,6 +29,7 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
|
{% if user.is_authenticated %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<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>
|
<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>
|
</div>
|
||||||
@ -35,6 +42,9 @@
|
|||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link" id="artist-month-tab" data-bs-toggle="tab" data-bs-target="#artists-month" type="button" role="tab" aria-controls="home" aria-selected="true">Monthly Artists</button>
|
<button class="nav-link" id="artist-month-tab" data-bs-toggle="tab" data-bs-target="#artists-month" type="button" role="tab" aria-controls="home" aria-selected="true">Monthly Artists</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#tracks-week" type="button" role="tab" aria-controls="profile" aria-selected="false">Weekly Tracks</button>
|
||||||
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#tracks-month" type="button" role="tab" aria-controls="profile" aria-selected="false">Monthly Tracks</button>
|
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#tracks-month" type="button" role="tab" aria-controls="profile" aria-selected="false">Monthly Tracks</button>
|
||||||
</li>
|
</li>
|
||||||
@ -63,6 +73,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane fade show" id="tracks-week" role="tabpanel" aria-labelledby="tracks-week-tab">
|
||||||
|
<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 class="tab-pane fade show" id="tracks-month" role="tabpanel" aria-labelledby="tracks-month-tab">
|
<div class="tab-pane fade show" id="tracks-month" role="tabpanel" aria-labelledby="tracks-month-tab">
|
||||||
<h2>Top tracks this month</h2>
|
<h2>Top tracks this month</h2>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@ -226,6 +261,9 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user