diff --git a/vrobbler/apps/music/aggregators.py b/vrobbler/apps/music/aggregators.py index 79e2e2d..4e1a240 100644 --- a/vrobbler/apps/music/aggregators.py +++ b/vrobbler/apps/music/aggregators.py @@ -16,9 +16,7 @@ def scrobble_counts(user=None): now = now_user_timezone(user.profile) user_filter = Q(user=user) - start_of_today = datetime.combine( - now.date(), datetime.min.time(), now.tzinfo - ) + 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 ) @@ -31,9 +29,7 @@ def scrobble_counts(user=None): media_type=Scrobble.MediaType.TRACK, ) data = {} - data["today"] = finished_scrobbles_qs.filter( - timestamp__gte=start_of_today - ).count() + 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() @@ -47,9 +43,7 @@ def scrobble_counts(user=None): return data -def week_of_scrobbles( - user=None, start=None, media: str = "tracks" -) -> dict[str, int]: +def week_of_scrobbles(user=None, start=None, media: str = "tracks") -> dict[str, int]: now = timezone.now() user_filter = Q() @@ -101,9 +95,7 @@ def live_charts( seven_days_ago = now - timedelta(days=7) thirty_days_ago = now - timedelta(days=30) start_of_today = datetime.combine(now, datetime.min.time(), tzinfo) - start_day_of_week = start_of_today - timedelta( - days=now.today().isoweekday() % 7 - ) + start_day_of_week = start_of_today - timedelta(days=now.today().isoweekday() % 7) start_day_of_month = now.replace(day=1) start_day_of_year = now.replace(month=1, day=1) @@ -120,17 +112,13 @@ def live_charts( } time_filter = Q() - completion_filter = Q( - scrobble__user=user, scrobble__played_to_completion=True - ) + completion_filter = Q(scrobble__user=user, scrobble__played_to_completion=True) user_filter = Q(scrobble__user=user) count_field = "scrobble" if media_type == "Artist": for period, query_dict in period_queries.items(): - period_queries[period] = { - "track__" + k: v for k, v in query_dict.items() - } + period_queries[period] = {"track__" + k: v for k, v in query_dict.items()} completion_filter = Q( track__scrobble__user=user, track__scrobble__played_to_completion=True, @@ -175,9 +163,7 @@ def live_tv_charts( seven_days_ago = now - timedelta(days=7) thirty_days_ago = now - timedelta(days=30) start_of_today = datetime.combine(now, datetime.min.time(), tzinfo) - start_day_of_week = start_of_today - timedelta( - days=now.today().isoweekday() % 7 - ) + start_day_of_week = start_of_today - timedelta(days=now.today().isoweekday() % 7) start_day_of_month = now.replace(day=1) start_day_of_year = now.replace(month=1, day=1) @@ -214,3 +200,56 @@ def live_tv_charts( .filter(num_scrobbles__gt=0) .order_by("-num_scrobbles")[:limit] ) + + +def live_youtube_channel_charts( + user: "User", + chart_period: str = "all", + limit: int = 15, +) -> QuerySet: + from django.db.models import OuterRef, Subquery + from videos.models import Video, Channel + + now = timezone.now() + tzinfo = now.tzinfo + now = now_user_timezone(user.profile) + tzinfo = now.tzinfo + + seven_days_ago = now - timedelta(days=7) + thirty_days_ago = now - timedelta(days=30) + start_of_today = datetime.combine(now, datetime.min.time(), tzinfo) + start_day_of_week = start_of_today - timedelta(days=now.today().isoweekday() % 7) + start_day_of_year = now.replace(month=1, day=1) + + period_queries = { + "today": Q(timestamp__gte=start_of_today), + "week": Q(timestamp__gte=start_day_of_week), + "last7": Q(timestamp__gte=seven_days_ago), + "last30": Q(timestamp__gte=thirty_days_ago), + "year": Q(timestamp__gte=start_day_of_year), + "all": Q(), + } + + time_filter = period_queries[chart_period] + completion_filter = Q( + user=user, + played_to_completion=True, + video__video_type=Video.VideoType.YOUTUBE, + ) + + scrobble_counts = ( + Scrobble.objects.filter( + completion_filter, + time_filter, + video__channel=OuterRef("pk"), + ) + .values("video__channel") + .annotate(c=Count("id")) + .values("c") + ) + + return ( + Channel.objects.annotate(num_scrobbles=Subquery(scrobble_counts)) + .filter(num_scrobbles__gt=0) + .order_by("-num_scrobbles")[:limit] + ) diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.py index 13ff7f2..a6a7a40 100644 --- a/vrobbler/apps/scrobbles/views.py +++ b/vrobbler/apps/scrobbles/views.py @@ -22,10 +22,10 @@ from django.views.generic.edit import CreateView from django.views.generic.list import ListView from moods.models import Mood from music.aggregators import ( + artist_scrobble_count, live_charts, live_tv_charts, - scrobble_counts, - week_of_scrobbles, + live_youtube_channel_charts, ) from pendulum.parsing.exceptions import ParserError from rest_framework import status @@ -161,25 +161,17 @@ class RecentScrobbleList(ListView): next_date = date + timedelta(weeks=+2) prev_date = date + timedelta(weeks=-1) if date.isocalendar()[1] < today.isocalendar()[1]: - data[ - "next_link" - ] = f"?date={next_date.strftime('%Y-W%W')}" + data["next_link"] = f"?date={next_date.strftime('%Y-W%W')}" data["prev_link"] = f"?date={prev_date.strftime('%Y-W%W')}" - data[ - "title" - ] = f"Week {date.strftime('%-W')} of {date.year}" + data["title"] = f"Week {date.strftime('%-W')} of {date.year}" data = data | Scrobble.as_dict_by_type( - Scrobble.for_week( - user_id, date.year, date.isocalendar()[1] - ) + Scrobble.for_week(user_id, date.year, date.isocalendar()[1]) ) elif date_str == "this_month" or date_str.count("-") == 1: next_date = date + relativedelta(months=1) prev_date = date + relativedelta(months=-1) if date.month < today.month: - data[ - "next_link" - ] = f"?date={next_date.strftime('%Y-%m')}" + data["next_link"] = f"?date={next_date.strftime('%Y-%m')}" data["title"] = f"{date.strftime('%B %Y')}" data["prev_link"] = f"?date={prev_date.strftime('%Y-%m')}" data = data | Scrobble.as_dict_by_type( @@ -188,24 +180,16 @@ class RecentScrobbleList(ListView): elif date_str == "today" or date_str.count("-") == 2: next_date = date + timedelta(days=1) prev_date = date - timedelta(days=1) - data[ - "prev_link" - ] = f"?date={prev_date.strftime('%Y-%m-%d')}" + data["prev_link"] = f"?date={prev_date.strftime('%Y-%m-%d')}" if date < today: - data[ - "next_link" - ] = f"?date={next_date.strftime('%Y-%m-%d')}" + data["next_link"] = f"?date={next_date.strftime('%Y-%m-%d')}" if date == today: data["title"] = "Today" else: data["title"] = f"{date.strftime('%Y-%m-%d')}" - data[ - "today_link" - ] = f"?date={today.strftime('%Y-%m-%d')}" + data["today_link"] = f"?date={today.strftime('%Y-%m-%d')}" data = data | Scrobble.as_dict_by_type( - Scrobble.for_day( - user_id, date.year, date.month, date.day - ) + Scrobble.for_day(user_id, date.year, date.month, date.day) ) elif date_str == "this_year" or date_str.count("-") == 0: next_date = date + relativedelta(years=+1) @@ -228,9 +212,7 @@ class RecentScrobbleList(ListView): data = data | Scrobble.as_dict_by_type( Scrobble.for_day(user_id, date.year, date.month, date.day) ) - data[ - "today_link" - ] = "" # f"?date={today.strftime('%Y-%m-%d')}" + data["today_link"] = "" # f"?date={today.strftime('%Y-%m-%d')}" data["active_imports"] = AudioScrobblerTSVImport.objects.filter( processing_started__isnull=False, @@ -259,9 +241,7 @@ class ScrobbleLongPlaysView(TemplateView): def get_context_data(self, **kwargs): context_data = super().get_context_data(**kwargs) context_data["view"] = self.request.GET.get("view", "grid") - context_data["in_progress"] = get_long_plays_in_progress( - self.request.user - ) + context_data["in_progress"] = get_long_plays_in_progress(self.request.user) context_data["completed"] = get_long_plays_completed(self.request.user) return context_data @@ -338,9 +318,7 @@ class ManualScrobbleView(FormView): scrobble_fn = MANUAL_SCROBBLE_FNS[key] scrobble = eval(scrobble_fn)(item_id, self.request.user.id) - return HttpResponseRedirect( - scrobble.redirect_url(self.request.user.id) - ) + return HttpResponseRedirect(scrobble.redirect_url(self.request.user.id)) class JsonableResponseMixin: @@ -386,9 +364,7 @@ class AudioScrobblerImportCreateView( return HttpResponseRedirect(self.request.META.get("HTTP_REFERER")) -class KoReaderImportCreateView( - LoginRequiredMixin, JsonableResponseMixin, CreateView -): +class KoReaderImportCreateView(LoginRequiredMixin, JsonableResponseMixin, CreateView): model = KoReaderImport fields = ["sqlite_file"] template_name = "scrobbles/upload_form.html" @@ -447,9 +423,7 @@ def web_scrobbler_webhook(request): playing_state = "started" if request.POST.get("isPlaying") == "false": playing_state = "stopped" - scrobble = web_scrobbler_scrobble_media( - youtube_id, user_id, status=playing_state - ) + scrobble = web_scrobbler_scrobble_media(youtube_id, user_id, status=playing_state) if not scrobble: logger.warning( "[web_scrobbler_webhook] failed", @@ -467,9 +441,7 @@ def web_scrobbler_webhook(request): "[web_scrobbler_webhook] finished", extra={"scrobble_id": scrobble.id}, ) - return JsonResponse( - {"scrobble_id": scrobble.id}, status=status.HTTP_200_OK - ) + return JsonResponse({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK) @csrf_exempt @@ -574,18 +546,12 @@ def import_audioscrobbler_file(request): scrobbles_created = [] # tsv_file = request.FILES[0] - file_serializer = serializers.AudioScrobblerTSVImportSerializer( - data=request.data - ) + file_serializer = serializers.AudioScrobblerTSVImportSerializer(data=request.data) if file_serializer.is_valid(): import_file = file_serializer.save() - return Response( - {"scrobble_ids": scrobbles_created}, status=status.HTTP_200_OK - ) + return Response({"scrobble_ids": scrobbles_created}, status=status.HTTP_200_OK) else: - return Response( - file_serializer.errors, status=status.HTTP_400_BAD_REQUEST - ) + return Response(file_serializer.errors, status=status.HTTP_400_BAD_REQUEST) @permission_classes([IsAuthenticated]) @@ -762,9 +728,7 @@ class ChartRecordView(TemplateView): ).order_by("rank") if charts.count() == 0: - ChartRecord.build( - user=self.request.user, model_str=media_type, **kwargs - ) + ChartRecord.build(user=self.request.user, model_str=media_type, **kwargs) charts = ChartRecord.objects.filter( media_filter, user=self.request.user, **kwargs ).order_by("rank") @@ -796,9 +760,7 @@ class ChartRecordView(TemplateView): media_type = self.request.GET.get("media", "Track") user = self.request.user params = {} - context_data["chart_type"] = self.request.GET.get( - "chart_type", "maloja" - ) + context_data["chart_type"] = self.request.GET.get("chart_type", "maloja") context_data["artist_charts"] = {} if not date: @@ -836,9 +798,7 @@ class ChartRecordView(TemplateView): tzinfo = now.tzinfo now = now.date() start_of_today = datetime.combine(now, datetime.min.time(), tzinfo) - start_day_of_week = start_of_today - timedelta( - days=now.isoweekday() % 7 - ) + start_day_of_week = start_of_today - timedelta(days=now.isoweekday() % 7) start_day_of_month = now.replace(day=1) # TV Series Scrobbles @@ -883,13 +843,27 @@ class ChartRecordView(TemplateView): context_data["tv_show_charts"] = { "today": list(live_tv_charts(user=user, chart_period="today")), "last7": list(live_tv_charts(user=user, chart_period="last7")), - "last30": list( - live_tv_charts(user=user, chart_period="last30") - ), + "last30": list(live_tv_charts(user=user, chart_period="last30")), "year": list(live_tv_charts(user=user, chart_period="year")), "all": list(live_tv_charts(user=user, chart_period="all")), } + context_data["youtube_channel_charts"] = { + "today": list( + live_youtube_channel_charts(user=user, chart_period="today") + ), + "last7": list( + live_youtube_channel_charts(user=user, chart_period="last7") + ), + "last30": list( + live_youtube_channel_charts(user=user, chart_period="last30") + ), + "year": list( + live_youtube_channel_charts(user=user, chart_period="year") + ), + "all": list(live_youtube_channel_charts(user=user, chart_period="all")), + } + return context_data # Date provided, lookup past charts, returning nothing if it's now or in the future. @@ -925,9 +899,7 @@ class ChartRecordView(TemplateView): params["day"] = day month_str = calendar.month_name[month] name = f"Chart for {month_str} {day}, {year}" - in_progress = ( - now.month == month and now.year == year and now.day == day - ) + in_progress = now.month == month and now.year == year and now.day == day media_filter = self.get_media_filter("Track") track_charts = ChartRecord.objects.filter( @@ -939,17 +911,13 @@ class ChartRecordView(TemplateView): ).order_by("rank") if track_charts.count() == 0 and not in_progress: - ChartRecord.build( - user=self.request.user, model_str="Track", **params - ) + ChartRecord.build(user=self.request.user, model_str="Track", **params) media_filter = self.get_media_filter("Track") track_charts = ChartRecord.objects.filter( media_filter, user=self.request.user, **params ).order_by("rank") if artist_charts.count() == 0 and not in_progress: - ChartRecord.build( - user=self.request.user, model_str="Artist", **params - ) + ChartRecord.build(user=self.request.user, model_str="Artist", **params) media_filter = self.get_media_filter("Artist") artist_charts = ChartRecord.objects.filter( media_filter, user=self.request.user, **params @@ -970,38 +938,24 @@ class ScrobbleStatusView(LoginRequiredMixin, TemplateView): def get_context_data(self, **kwargs): data = super().get_context_data(**kwargs) user_scrobble_qs = Scrobble.objects.filter().order_by("-timestamp") - progress_plays = user_scrobble_qs.filter( - in_progress=True, is_paused=False - ) + progress_plays = user_scrobble_qs.filter(in_progress=True, is_paused=False) data["listening"] = progress_plays.filter( Q(track__isnull=False) | Q(podcast_episode__isnull=False) ).first() data["watching"] = progress_plays.filter(video__isnull=False).first() - data["going"] = progress_plays.filter( - geo_location__isnull=False - ).first() - data["playing"] = progress_plays.filter( - board_game__isnull=False - ).first() - data["sporting"] = progress_plays.filter( - sport_event__isnull=False - ).first() - data["browsing"] = progress_plays.filter( - web_page__isnull=False - ).first() - data["participating"] = progress_plays.filter( - life_event__isnull=False - ).first() + data["going"] = progress_plays.filter(geo_location__isnull=False).first() + data["playing"] = progress_plays.filter(board_game__isnull=False).first() + data["sporting"] = progress_plays.filter(sport_event__isnull=False).first() + data["browsing"] = progress_plays.filter(web_page__isnull=False).first() + data["participating"] = progress_plays.filter(life_event__isnull=False).first() data["working"] = progress_plays.filter(task__isnull=False).first() long_plays = user_scrobble_qs.filter( long_play_complete=False, played_to_completion=True ) data["reading"] = long_plays.filter(book__isnull=False).first() - data["sessioning"] = long_plays.filter( - video_game__isnull=False - ).first() + data["sessioning"] = long_plays.filter(video_game__isnull=False).first() return data @@ -1036,9 +990,7 @@ class ScrobbleDetailView(DetailView): data[field_name] = original_value if data.get("with_people_ids", False): - data["with_people_ids"] = [ - p.id for p in data["with_people_ids"] - ] + data["with_people_ids"] = [p.id for p in data["with_people_ids"]] if data.get("platform_id", False): data["platform_id"] = data["platform_id"].id diff --git a/vrobbler/apps/videos/urls.py b/vrobbler/apps/videos/urls.py index 3cfacc4..8d26906 100644 --- a/vrobbler/apps/videos/urls.py +++ b/vrobbler/apps/videos/urls.py @@ -12,6 +12,11 @@ urlpatterns = [ views.SeriesDetailView.as_view(), name="series_detail", ), + path( + "channels//", + views.ChannelDetailView.as_view(), + name="channel_detail", + ), path("videos/", views.VideoListView.as_view(), name="video_list"), path( "video//", diff --git a/vrobbler/apps/videos/views.py b/vrobbler/apps/videos/views.py index e2c046c..57b2f6f 100644 --- a/vrobbler/apps/videos/views.py +++ b/vrobbler/apps/videos/views.py @@ -1,6 +1,6 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.views import generic -from videos.models import Series, Video +from videos.models import Channel, Series, Video from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView @@ -25,9 +25,7 @@ class SeriesDetailView(LoginRequiredMixin, generic.DetailView): context_data = super().get_context_data(**kwargs) context_data["scrobbles"] = self.object.scrobbles_for_user(user_id) - next_episode_id = ( - self.object.last_scrobbled_episode(user_id).next_imdb_id or "" - ) + next_episode_id = self.object.last_scrobbled_episode(user_id).next_imdb_id or "" if self.object.is_episode_playing(user_id): next_episode_id = "" if next_episode_id: @@ -35,6 +33,18 @@ class SeriesDetailView(LoginRequiredMixin, generic.DetailView): return context_data +class ChannelDetailView(LoginRequiredMixin, generic.DetailView): + model = Channel + slug_field = "uuid" + template_name = "videos/channel_detail.html" + + def get_context_data(self, **kwargs): + user_id = self.request.user.id + context_data = super().get_context_data(**kwargs) + context_data["scrobbles"] = self.object.scrobbles_for_user(user_id) + return context_data + + class VideoListView(ScrobbleableListView): model = Video diff --git a/vrobbler/templates/scrobbles/_top_charts.html b/vrobbler/templates/scrobbles/_top_charts.html index 67898f0..99340e7 100644 --- a/vrobbler/templates/scrobbles/_top_charts.html +++ b/vrobbler/templates/scrobbles/_top_charts.html @@ -400,45 +400,60 @@
-
-

TV Shows Watched

+

Top YouTube Channels

+ - {% if series_this_week %} -

This Week

-
- - - {% for scrobble in series_this_week %} - - - - - {% empty %} - - {% endfor %} - -
{{scrobble.video}}{{scrobble.timestamp|date:"N d"}}
No episodes watched this week
+
+ {% for key, channels in youtube_channel_charts.items %} +
+
+ {% if channels.0 %} +
+
+
#1 {{channels.0.name}} ({{channels.0.num_scrobbles}} videos)
+ {% if channels.0.cover_image %} + + {% else %} + + {% endif %} +
+
+ {% endif %} + {% if channels.1 %} +
+
+
+
#2 {{channels.1.name}} ({{channels.1.num_scrobbles}})
+ {% if channels.1.cover_image %} + + {% else %} + + {% endif %} +
+
+
#3 {{channels.2.name}} ({{channels.2.num_scrobbles}})
+ {% if channels.2.cover_image %} + + {% else %} + + {% endif %} +
+
+
+ {% endif %} +
- {% endif %} - - {% if series_this_month %} -

This Month

-
- - - {% for scrobble in series_this_month %} - - - - - {% empty %} - - {% endfor %} - -
{{scrobble.video}}{{scrobble.timestamp|date:"N d"}}
No episodes watched this month
-
- {% endif %} + {% endfor %}
+
{% if videos_this_week or videos_this_month %}
@@ -482,47 +497,4 @@
{% endif %} - - {% if youtue_this_week or youtube_this_month %} -
-

YouTube Watched

- - {% if youtube_this_week %} -

This Week

-
- - - {% for scrobble in youtube_this_week %} - - - - - {% empty %} - - {% endfor %} - -
{{scrobble.video}}{{scrobble.timestamp|date:"N d"}}
No youtube watched this week
-
- {% endif %} - - {% if youtube_this_month %} -

This Month

-
- - - {% for scrobble in youtube_this_month %} - - - - - {% empty %} - - {% endfor %} - -
{{scrobble.video}}{{scrobble.timestamp|date:"N d"}}
No youtube watched this month
-
- {% endif %} - -
- {% endif %}
diff --git a/vrobbler/templates/videos/channel_detail.html b/vrobbler/templates/videos/channel_detail.html new file mode 100644 index 0000000..78f9e05 --- /dev/null +++ b/vrobbler/templates/videos/channel_detail.html @@ -0,0 +1,58 @@ +{% extends "base_list.html" %} +{% load static %} + +{% block title %}{{object.name}}{% endblock %} + +{% block head_extra %} + +{% endblock %} + +{% block lists %} + +
+
+ +
+
+ {% if object.youtube_id %}

View on YouTube

{% endif %} +
+
+
+
+

Last scrobbles

+
+ + + + + + + + + {% for scrobble in scrobbles %} + + + + + {% endfor %} + +
DateTitle
{{scrobble.local_timestamp}}{{scrobble.media_obj.title}}
+
+
+
+{% endblock %}