Add timezone support and an authenticated view

This commit is contained in:
2023-01-18 00:39:12 -05:00
parent a0af0bce05
commit e75c22d583
6 changed files with 185 additions and 81 deletions

View File

@ -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(

View File

@ -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))

View File

@ -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

View File

@ -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",

View File

@ -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 %}

View File

@ -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 %}