[charts] Add youtube aggregator
All checks were successful
build & deploy / test (push) Successful in 1m41s
build & deploy / deploy (push) Successful in 52s

This commit is contained in:
2026-03-12 00:26:28 -04:00
parent 5934dcdf8e
commit f506fa465f
6 changed files with 238 additions and 202 deletions

View File

@ -16,9 +16,7 @@ def scrobble_counts(user=None):
now = now_user_timezone(user.profile) now = now_user_timezone(user.profile)
user_filter = Q(user=user) user_filter = Q(user=user)
start_of_today = datetime.combine( start_of_today = datetime.combine(now.date(), datetime.min.time(), now.tzinfo)
now.date(), datetime.min.time(), now.tzinfo
)
starting_day_of_current_week = now.date() - timedelta( starting_day_of_current_week = now.date() - timedelta(
days=now.today().isoweekday() % 7 days=now.today().isoweekday() % 7
) )
@ -31,9 +29,7 @@ def scrobble_counts(user=None):
media_type=Scrobble.MediaType.TRACK, media_type=Scrobble.MediaType.TRACK,
) )
data = {} data = {}
data["today"] = finished_scrobbles_qs.filter( data["today"] = finished_scrobbles_qs.filter(timestamp__gte=start_of_today).count()
timestamp__gte=start_of_today
).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()
@ -47,9 +43,7 @@ def scrobble_counts(user=None):
return data return data
def week_of_scrobbles( def week_of_scrobbles(user=None, start=None, media: str = "tracks") -> dict[str, int]:
user=None, start=None, media: str = "tracks"
) -> dict[str, int]:
now = timezone.now() now = timezone.now()
user_filter = Q() user_filter = Q()
@ -101,9 +95,7 @@ def live_charts(
seven_days_ago = now - timedelta(days=7) seven_days_ago = now - timedelta(days=7)
thirty_days_ago = now - timedelta(days=30) thirty_days_ago = now - timedelta(days=30)
start_of_today = datetime.combine(now, datetime.min.time(), tzinfo) start_of_today = datetime.combine(now, datetime.min.time(), tzinfo)
start_day_of_week = start_of_today - timedelta( start_day_of_week = start_of_today - timedelta(days=now.today().isoweekday() % 7)
days=now.today().isoweekday() % 7
)
start_day_of_month = now.replace(day=1) start_day_of_month = now.replace(day=1)
start_day_of_year = now.replace(month=1, day=1) start_day_of_year = now.replace(month=1, day=1)
@ -120,17 +112,13 @@ def live_charts(
} }
time_filter = Q() time_filter = Q()
completion_filter = Q( completion_filter = Q(scrobble__user=user, scrobble__played_to_completion=True)
scrobble__user=user, scrobble__played_to_completion=True
)
user_filter = Q(scrobble__user=user) user_filter = Q(scrobble__user=user)
count_field = "scrobble" count_field = "scrobble"
if media_type == "Artist": if media_type == "Artist":
for period, query_dict in period_queries.items(): for period, query_dict in period_queries.items():
period_queries[period] = { period_queries[period] = {"track__" + k: v for k, v in query_dict.items()}
"track__" + k: v for k, v in query_dict.items()
}
completion_filter = Q( completion_filter = Q(
track__scrobble__user=user, track__scrobble__user=user,
track__scrobble__played_to_completion=True, track__scrobble__played_to_completion=True,
@ -175,9 +163,7 @@ def live_tv_charts(
seven_days_ago = now - timedelta(days=7) seven_days_ago = now - timedelta(days=7)
thirty_days_ago = now - timedelta(days=30) thirty_days_ago = now - timedelta(days=30)
start_of_today = datetime.combine(now, datetime.min.time(), tzinfo) start_of_today = datetime.combine(now, datetime.min.time(), tzinfo)
start_day_of_week = start_of_today - timedelta( start_day_of_week = start_of_today - timedelta(days=now.today().isoweekday() % 7)
days=now.today().isoweekday() % 7
)
start_day_of_month = now.replace(day=1) start_day_of_month = now.replace(day=1)
start_day_of_year = now.replace(month=1, 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) .filter(num_scrobbles__gt=0)
.order_by("-num_scrobbles")[:limit] .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]
)

View File

@ -22,10 +22,10 @@ from django.views.generic.edit import CreateView
from django.views.generic.list import ListView from django.views.generic.list import ListView
from moods.models import Mood from moods.models import Mood
from music.aggregators import ( from music.aggregators import (
artist_scrobble_count,
live_charts, live_charts,
live_tv_charts, live_tv_charts,
scrobble_counts, live_youtube_channel_charts,
week_of_scrobbles,
) )
from pendulum.parsing.exceptions import ParserError from pendulum.parsing.exceptions import ParserError
from rest_framework import status from rest_framework import status
@ -161,25 +161,17 @@ class RecentScrobbleList(ListView):
next_date = date + timedelta(weeks=+2) next_date = date + timedelta(weeks=+2)
prev_date = date + timedelta(weeks=-1) prev_date = date + timedelta(weeks=-1)
if date.isocalendar()[1] < today.isocalendar()[1]: if date.isocalendar()[1] < today.isocalendar()[1]:
data[ data["next_link"] = f"?date={next_date.strftime('%Y-W%W')}"
"next_link"
] = f"?date={next_date.strftime('%Y-W%W')}"
data["prev_link"] = f"?date={prev_date.strftime('%Y-W%W')}" data["prev_link"] = f"?date={prev_date.strftime('%Y-W%W')}"
data[ data["title"] = f"Week {date.strftime('%-W')} of {date.year}"
"title"
] = f"Week {date.strftime('%-W')} of {date.year}"
data = data | Scrobble.as_dict_by_type( data = data | Scrobble.as_dict_by_type(
Scrobble.for_week( Scrobble.for_week(user_id, date.year, date.isocalendar()[1])
user_id, date.year, date.isocalendar()[1]
)
) )
elif date_str == "this_month" or date_str.count("-") == 1: elif date_str == "this_month" or date_str.count("-") == 1:
next_date = date + relativedelta(months=1) next_date = date + relativedelta(months=1)
prev_date = date + relativedelta(months=-1) prev_date = date + relativedelta(months=-1)
if date.month < today.month: if date.month < today.month:
data[ data["next_link"] = f"?date={next_date.strftime('%Y-%m')}"
"next_link"
] = f"?date={next_date.strftime('%Y-%m')}"
data["title"] = f"{date.strftime('%B %Y')}" data["title"] = f"{date.strftime('%B %Y')}"
data["prev_link"] = f"?date={prev_date.strftime('%Y-%m')}" data["prev_link"] = f"?date={prev_date.strftime('%Y-%m')}"
data = data | Scrobble.as_dict_by_type( data = data | Scrobble.as_dict_by_type(
@ -188,24 +180,16 @@ class RecentScrobbleList(ListView):
elif date_str == "today" or date_str.count("-") == 2: elif date_str == "today" or date_str.count("-") == 2:
next_date = date + timedelta(days=1) next_date = date + timedelta(days=1)
prev_date = date - timedelta(days=1) prev_date = date - timedelta(days=1)
data[ data["prev_link"] = f"?date={prev_date.strftime('%Y-%m-%d')}"
"prev_link"
] = f"?date={prev_date.strftime('%Y-%m-%d')}"
if date < today: if date < today:
data[ data["next_link"] = f"?date={next_date.strftime('%Y-%m-%d')}"
"next_link"
] = f"?date={next_date.strftime('%Y-%m-%d')}"
if date == today: if date == today:
data["title"] = "Today" data["title"] = "Today"
else: else:
data["title"] = f"{date.strftime('%Y-%m-%d')}" data["title"] = f"{date.strftime('%Y-%m-%d')}"
data[ data["today_link"] = f"?date={today.strftime('%Y-%m-%d')}"
"today_link"
] = f"?date={today.strftime('%Y-%m-%d')}"
data = data | Scrobble.as_dict_by_type( data = data | Scrobble.as_dict_by_type(
Scrobble.for_day( Scrobble.for_day(user_id, date.year, date.month, date.day)
user_id, date.year, date.month, date.day
)
) )
elif date_str == "this_year" or date_str.count("-") == 0: elif date_str == "this_year" or date_str.count("-") == 0:
next_date = date + relativedelta(years=+1) next_date = date + relativedelta(years=+1)
@ -228,9 +212,7 @@ class RecentScrobbleList(ListView):
data = data | Scrobble.as_dict_by_type( 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)
) )
data[ data["today_link"] = "" # f"?date={today.strftime('%Y-%m-%d')}"
"today_link"
] = "" # f"?date={today.strftime('%Y-%m-%d')}"
data["active_imports"] = AudioScrobblerTSVImport.objects.filter( data["active_imports"] = AudioScrobblerTSVImport.objects.filter(
processing_started__isnull=False, processing_started__isnull=False,
@ -259,9 +241,7 @@ class ScrobbleLongPlaysView(TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs) context_data = super().get_context_data(**kwargs)
context_data["view"] = self.request.GET.get("view", "grid") context_data["view"] = self.request.GET.get("view", "grid")
context_data["in_progress"] = get_long_plays_in_progress( context_data["in_progress"] = get_long_plays_in_progress(self.request.user)
self.request.user
)
context_data["completed"] = get_long_plays_completed(self.request.user) context_data["completed"] = get_long_plays_completed(self.request.user)
return context_data return context_data
@ -338,9 +318,7 @@ class ManualScrobbleView(FormView):
scrobble_fn = MANUAL_SCROBBLE_FNS[key] scrobble_fn = MANUAL_SCROBBLE_FNS[key]
scrobble = eval(scrobble_fn)(item_id, self.request.user.id) scrobble = eval(scrobble_fn)(item_id, self.request.user.id)
return HttpResponseRedirect( return HttpResponseRedirect(scrobble.redirect_url(self.request.user.id))
scrobble.redirect_url(self.request.user.id)
)
class JsonableResponseMixin: class JsonableResponseMixin:
@ -386,9 +364,7 @@ class AudioScrobblerImportCreateView(
return HttpResponseRedirect(self.request.META.get("HTTP_REFERER")) return HttpResponseRedirect(self.request.META.get("HTTP_REFERER"))
class KoReaderImportCreateView( class KoReaderImportCreateView(LoginRequiredMixin, JsonableResponseMixin, CreateView):
LoginRequiredMixin, JsonableResponseMixin, CreateView
):
model = KoReaderImport model = KoReaderImport
fields = ["sqlite_file"] fields = ["sqlite_file"]
template_name = "scrobbles/upload_form.html" template_name = "scrobbles/upload_form.html"
@ -447,9 +423,7 @@ def web_scrobbler_webhook(request):
playing_state = "started" playing_state = "started"
if request.POST.get("isPlaying") == "false": if request.POST.get("isPlaying") == "false":
playing_state = "stopped" playing_state = "stopped"
scrobble = web_scrobbler_scrobble_media( scrobble = web_scrobbler_scrobble_media(youtube_id, user_id, status=playing_state)
youtube_id, user_id, status=playing_state
)
if not scrobble: if not scrobble:
logger.warning( logger.warning(
"[web_scrobbler_webhook] failed", "[web_scrobbler_webhook] failed",
@ -467,9 +441,7 @@ def web_scrobbler_webhook(request):
"[web_scrobbler_webhook] finished", "[web_scrobbler_webhook] finished",
extra={"scrobble_id": scrobble.id}, extra={"scrobble_id": scrobble.id},
) )
return JsonResponse( return JsonResponse({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK)
{"scrobble_id": scrobble.id}, status=status.HTTP_200_OK
)
@csrf_exempt @csrf_exempt
@ -574,18 +546,12 @@ def import_audioscrobbler_file(request):
scrobbles_created = [] scrobbles_created = []
# tsv_file = request.FILES[0] # tsv_file = request.FILES[0]
file_serializer = serializers.AudioScrobblerTSVImportSerializer( file_serializer = serializers.AudioScrobblerTSVImportSerializer(data=request.data)
data=request.data
)
if file_serializer.is_valid(): if file_serializer.is_valid():
import_file = file_serializer.save() import_file = file_serializer.save()
return Response( return Response({"scrobble_ids": scrobbles_created}, status=status.HTTP_200_OK)
{"scrobble_ids": scrobbles_created}, status=status.HTTP_200_OK
)
else: else:
return Response( return Response(file_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
file_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
@permission_classes([IsAuthenticated]) @permission_classes([IsAuthenticated])
@ -762,9 +728,7 @@ class ChartRecordView(TemplateView):
).order_by("rank") ).order_by("rank")
if charts.count() == 0: if charts.count() == 0:
ChartRecord.build( ChartRecord.build(user=self.request.user, model_str=media_type, **kwargs)
user=self.request.user, model_str=media_type, **kwargs
)
charts = ChartRecord.objects.filter( charts = ChartRecord.objects.filter(
media_filter, user=self.request.user, **kwargs media_filter, user=self.request.user, **kwargs
).order_by("rank") ).order_by("rank")
@ -796,9 +760,7 @@ class ChartRecordView(TemplateView):
media_type = self.request.GET.get("media", "Track") media_type = self.request.GET.get("media", "Track")
user = self.request.user user = self.request.user
params = {} params = {}
context_data["chart_type"] = self.request.GET.get( context_data["chart_type"] = self.request.GET.get("chart_type", "maloja")
"chart_type", "maloja"
)
context_data["artist_charts"] = {} context_data["artist_charts"] = {}
if not date: if not date:
@ -836,9 +798,7 @@ class ChartRecordView(TemplateView):
tzinfo = now.tzinfo tzinfo = now.tzinfo
now = now.date() now = now.date()
start_of_today = datetime.combine(now, datetime.min.time(), tzinfo) start_of_today = datetime.combine(now, datetime.min.time(), tzinfo)
start_day_of_week = start_of_today - timedelta( start_day_of_week = start_of_today - timedelta(days=now.isoweekday() % 7)
days=now.isoweekday() % 7
)
start_day_of_month = now.replace(day=1) start_day_of_month = now.replace(day=1)
# TV Series Scrobbles # TV Series Scrobbles
@ -883,13 +843,27 @@ class ChartRecordView(TemplateView):
context_data["tv_show_charts"] = { context_data["tv_show_charts"] = {
"today": list(live_tv_charts(user=user, chart_period="today")), "today": list(live_tv_charts(user=user, chart_period="today")),
"last7": list(live_tv_charts(user=user, chart_period="last7")), "last7": list(live_tv_charts(user=user, chart_period="last7")),
"last30": list( "last30": list(live_tv_charts(user=user, chart_period="last30")),
live_tv_charts(user=user, chart_period="last30")
),
"year": list(live_tv_charts(user=user, chart_period="year")), "year": list(live_tv_charts(user=user, chart_period="year")),
"all": list(live_tv_charts(user=user, chart_period="all")), "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 return context_data
# Date provided, lookup past charts, returning nothing if it's now or in the future. # 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 params["day"] = day
month_str = calendar.month_name[month] month_str = calendar.month_name[month]
name = f"Chart for {month_str} {day}, {year}" name = f"Chart for {month_str} {day}, {year}"
in_progress = ( in_progress = now.month == month and now.year == year and now.day == day
now.month == month and now.year == year and now.day == day
)
media_filter = self.get_media_filter("Track") media_filter = self.get_media_filter("Track")
track_charts = ChartRecord.objects.filter( track_charts = ChartRecord.objects.filter(
@ -939,17 +911,13 @@ class ChartRecordView(TemplateView):
).order_by("rank") ).order_by("rank")
if track_charts.count() == 0 and not in_progress: if track_charts.count() == 0 and not in_progress:
ChartRecord.build( ChartRecord.build(user=self.request.user, model_str="Track", **params)
user=self.request.user, model_str="Track", **params
)
media_filter = self.get_media_filter("Track") media_filter = self.get_media_filter("Track")
track_charts = ChartRecord.objects.filter( track_charts = ChartRecord.objects.filter(
media_filter, user=self.request.user, **params media_filter, user=self.request.user, **params
).order_by("rank") ).order_by("rank")
if artist_charts.count() == 0 and not in_progress: if artist_charts.count() == 0 and not in_progress:
ChartRecord.build( ChartRecord.build(user=self.request.user, model_str="Artist", **params)
user=self.request.user, model_str="Artist", **params
)
media_filter = self.get_media_filter("Artist") media_filter = self.get_media_filter("Artist")
artist_charts = ChartRecord.objects.filter( artist_charts = ChartRecord.objects.filter(
media_filter, user=self.request.user, **params media_filter, user=self.request.user, **params
@ -970,38 +938,24 @@ class ScrobbleStatusView(LoginRequiredMixin, TemplateView):
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_scrobble_qs = Scrobble.objects.filter().order_by("-timestamp") user_scrobble_qs = Scrobble.objects.filter().order_by("-timestamp")
progress_plays = user_scrobble_qs.filter( progress_plays = user_scrobble_qs.filter(in_progress=True, is_paused=False)
in_progress=True, is_paused=False
)
data["listening"] = progress_plays.filter( data["listening"] = progress_plays.filter(
Q(track__isnull=False) | Q(podcast_episode__isnull=False) Q(track__isnull=False) | Q(podcast_episode__isnull=False)
).first() ).first()
data["watching"] = progress_plays.filter(video__isnull=False).first() data["watching"] = progress_plays.filter(video__isnull=False).first()
data["going"] = progress_plays.filter( data["going"] = progress_plays.filter(geo_location__isnull=False).first()
geo_location__isnull=False data["playing"] = progress_plays.filter(board_game__isnull=False).first()
).first() data["sporting"] = progress_plays.filter(sport_event__isnull=False).first()
data["playing"] = progress_plays.filter( data["browsing"] = progress_plays.filter(web_page__isnull=False).first()
board_game__isnull=False data["participating"] = progress_plays.filter(life_event__isnull=False).first()
).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() data["working"] = progress_plays.filter(task__isnull=False).first()
long_plays = user_scrobble_qs.filter( long_plays = user_scrobble_qs.filter(
long_play_complete=False, played_to_completion=True long_play_complete=False, played_to_completion=True
) )
data["reading"] = long_plays.filter(book__isnull=False).first() data["reading"] = long_plays.filter(book__isnull=False).first()
data["sessioning"] = long_plays.filter( data["sessioning"] = long_plays.filter(video_game__isnull=False).first()
video_game__isnull=False
).first()
return data return data
@ -1036,9 +990,7 @@ class ScrobbleDetailView(DetailView):
data[field_name] = original_value data[field_name] = original_value
if data.get("with_people_ids", False): if data.get("with_people_ids", False):
data["with_people_ids"] = [ data["with_people_ids"] = [p.id for p in data["with_people_ids"]]
p.id for p in data["with_people_ids"]
]
if data.get("platform_id", False): if data.get("platform_id", False):
data["platform_id"] = data["platform_id"].id data["platform_id"] = data["platform_id"].id

View File

@ -12,6 +12,11 @@ urlpatterns = [
views.SeriesDetailView.as_view(), views.SeriesDetailView.as_view(),
name="series_detail", name="series_detail",
), ),
path(
"channels/<slug:slug>/",
views.ChannelDetailView.as_view(),
name="channel_detail",
),
path("videos/", views.VideoListView.as_view(), name="video_list"), path("videos/", views.VideoListView.as_view(), name="video_list"),
path( path(
"video/<slug:slug>/", "video/<slug:slug>/",

View File

@ -1,6 +1,6 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.views import generic from django.views import generic
from videos.models import Series, Video from videos.models import Channel, Series, Video
from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView
@ -25,9 +25,7 @@ class SeriesDetailView(LoginRequiredMixin, generic.DetailView):
context_data = super().get_context_data(**kwargs) context_data = super().get_context_data(**kwargs)
context_data["scrobbles"] = self.object.scrobbles_for_user(user_id) context_data["scrobbles"] = self.object.scrobbles_for_user(user_id)
next_episode_id = ( next_episode_id = self.object.last_scrobbled_episode(user_id).next_imdb_id or ""
self.object.last_scrobbled_episode(user_id).next_imdb_id or ""
)
if self.object.is_episode_playing(user_id): if self.object.is_episode_playing(user_id):
next_episode_id = "" next_episode_id = ""
if next_episode_id: if next_episode_id:
@ -35,6 +33,18 @@ class SeriesDetailView(LoginRequiredMixin, generic.DetailView):
return context_data 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): class VideoListView(ScrobbleableListView):
model = Video model = Video

View File

@ -400,45 +400,60 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-md"> <h2>Top YouTube Channels</h2>
<h2>TV Shows Watched</h2> <ul class="nav nav-tabs" id="youtubeChannelTab" role="tablist">
{% for key, name in chart_keys.items %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if forloop.counter == 2 %}active{% endif %}"
id="youtubeChannel-{{key}}-tab" data-bs-toggle="tab" data-bs-target="#youtubeChannel-{{key}}"
type="button" role="tab" aria-controls="home" aria-selected="true">{{name}}</button>
</li>
{% endfor %}
</ul>
{% if series_this_week %} <div class="tab-content" id="youtubeChannelTabContent" class="maloja-chart">
<h3>This Week</h3> {% for key, channels in youtube_channel_charts.items %}
<div class="table-responsive"> <div class="tab-pane fade {% if forloop.counter == 2 %}show active{% endif %}" id="youtubeChannel-{{key}}" role="tabpanel" aria-labelledby="youtubeChannel-{{key}}-tab">
<table class="table table-striped table-sm"> <div style="display:block">
<tbody> {% if channels.0 %}
{% for scrobble in series_this_week %} <div style="float:left;">
<tr> <div class="image-wrapper" style="display:flex; flex-wrap: wrap; margin:0">
<td><a href="{{scrobble.video.get_absolute_url}}">{{scrobble.video}}</a></td> <div class="caption">#1 {{channels.0.name}} ({{channels.0.num_scrobbles}} videos)</div>
<td>{{scrobble.timestamp|date:"N d"}}</td> {% if channels.0.cover_image %}
</tr> <a href="{{channels.0.get_absolute_url}}"><img lt="{{channels.0.name}}" src="{{channels.0.cover_medium.url}}" width="300px"></a>
{% empty %} {% else %}
<tr><td>No episodes watched this week</td></tr> <a href="{{channels.0.get_absolute_url}}"><img lt="{{channels.0.name}}" src="{% static "images/not-found.jpg" %}" width="300px"></a>
{% endfor %} {% endif %}
</tbody> </div>
</table> </div>
{% endif %}
{% if channels.1 %}
<div style="float:left; width:300px;">
<div style="display:flex; flex-wrap: wrap;">
<div class="image-wrapper" style="width:50%">
<div class="caption-medium">#2 {{channels.1.name}} ({{channels.1.num_scrobbles}})</div>
{% if channels.1.cover_image %}
<a href="{{channels.1.get_absolute_url}}"><img lt="{{channels.1.name}}" src="{{channels.1.cover_small.url}}" width="150px"></a>
{% else %}
<a href="{{channels.1.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="150px"></a>
{% endif %}
</div>
<div class="image-wrapper" style="width:50%">
<div class="caption-medium">#3 {{channels.2.name}} ({{channels.2.num_scrobbles}})</div>
{% if channels.2.cover_image %}
<a href="{{channels.2.get_absolute_url}}"><img lt="{{channels.2.name}}" src="{{channels.2.cover_small.url}}" width="150px"></a>
{% else %}
<a href="{{channels.2.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="150px"></a>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
</div> </div>
{% endif %} {% endfor %}
{% if series_this_month %}
<h3>This Month</h3>
<div class="table-responsive">
<table class="table table-striped table-sm">
<tbody>
{% for scrobble in series_this_month %}
<tr>
<td><a href="{{scrobble.video.get_absolute_url}}">{{scrobble.video}}</a></td>
<td>{{scrobble.timestamp|date:"N d"}}</td>
</tr>
{% empty %}
<tr><td>No episodes watched this month</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div> </div>
</div>
{% if videos_this_week or videos_this_month %} {% if videos_this_week or videos_this_month %}
<div class="col-md"> <div class="col-md">
@ -482,47 +497,4 @@
</div> </div>
{% endif %} {% endif %}
{% if youtue_this_week or youtube_this_month %}
<div class="col-md">
<h2>YouTube Watched</h2>
{% if youtube_this_week %}
<h3>This Week</h3>
<div class="table-responsive">
<table class="table table-striped table-sm">
<tbody>
{% for scrobble in youtube_this_week %}
<tr>
<td><a href="{{scrobble.video.get_absolute_url}}">{{scrobble.video}}</a></td>
<td>{{scrobble.timestamp|date:"N d"}}</td>
</tr>
{% empty %}
<tr><td>No youtube watched this week</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if youtube_this_month %}
<h3>This Month</h3>
<div class="table-responsive">
<table class="table table-striped table-sm">
<tbody>
{% for scrobble in youtube_this_month %}
<tr>
<td><a href="{{scrobble.video.get_absolute_url}}">{{scrobble.video}}</a></td>
<td>{{scrobble.timestamp|date:"N d"}}</td>
</tr>
{% empty %}
<tr><td>No youtube watched this month</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
{% endif %}
</div> </div>

View File

@ -0,0 +1,58 @@
{% extends "base_list.html" %}
{% load static %}
{% block title %}{{object.name}}{% endblock %}
{% block head_extra %}
<style>
.cover {float:left; width:412px; margin-right:10px; padding-bottom:15px;}
.summary {float:left; width:600px; margin-left:10px;}
.header {padding-bottom:15px;}
.image-wrapper { contain: content; }
.caption {
position: fixed;
top: 15px;
left: 30px;
padding: 5px;
font-size: 110%;
font-weight: bold;
color:white;
background:rgba(0,0,0,0.4);
}
</style>
{% endblock %}
{% block lists %}
<div class="row header">
<div class="cover image-wrapper">
<img src="{% if object.cover_image %}{{object.cover_image.url}}{% else %}{% static 'images/no-video-cover.jpg' %}{% endif %}" width="400px" />
</div>
<div class="summary">
{% if object.youtube_id %}<p><a href="{{object.youtube_url}}" target="_blank">View on YouTube</a></p>{% endif %}
</div>
</div>
<div class="row">
<div class="col-md">
<h3>Last scrobbles</h3>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Title</th>
</tr>
</thead>
<tbody>
{% for scrobble in scrobbles %}
<tr>
<td><a href={{scrobble.get_absolute_url}}>{{scrobble.local_timestamp}}</a></td>
<td><a href="{{scrobble.media_obj.get_absolute_url}}">{{scrobble.media_obj.title}}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}