[charts] Add TV charts

This commit is contained in:
2026-03-11 23:53:08 -04:00
parent 118e208a36
commit 0e5d8e6b2f
4 changed files with 313 additions and 2 deletions

View File

@ -89,6 +89,7 @@ def live_charts(
media_type: str = "Track", media_type: str = "Track",
chart_period: str = "all", chart_period: str = "all",
limit: int = 15, limit: int = 15,
app_label: str = "music",
) -> QuerySet: ) -> QuerySet:
now = timezone.now() now = timezone.now()
tzinfo = now.tzinfo tzinfo = now.tzinfo
@ -106,7 +107,7 @@ def live_charts(
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)
media_model = apps.get_model(app_label="music", model_name=media_type) media_model = apps.get_model(app_label=app_label, model_name=media_type)
period_queries = { period_queries = {
"today": {"scrobble__timestamp__gte": start_of_today}, "today": {"scrobble__timestamp__gte": start_of_today},
@ -154,3 +155,60 @@ def live_charts(
def artist_scrobble_count(artist_id: int, filter: str = "today") -> int: def artist_scrobble_count(artist_id: int, filter: str = "today") -> int:
return Scrobble.objects.filter(track__artist=artist_id).count() return Scrobble.objects.filter(track__artist=artist_id).count()
def live_tv_charts(
user: "User",
chart_period: str = "all",
limit: int = 15,
) -> QuerySet:
from django.db.models import OuterRef, Subquery
from videos.models import Video, Series
now = timezone.now()
tzinfo = now.tzinfo
now = now.date()
if user.is_authenticated:
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_month = now.replace(day=1)
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),
"month": Q(timestamp__gte=start_day_of_month),
"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.TV_EPISODE,
)
scrobble_counts = (
Scrobble.objects.filter(
completion_filter,
time_filter,
video__tv_series=OuterRef("pk"),
)
.values("video__tv_series")
.annotate(c=Count("id"))
.values("c")
)
return (
Series.objects.annotate(num_scrobbles=Subquery(scrobble_counts))
.filter(num_scrobbles__gt=0)
.order_by("-num_scrobbles")[:limit]
)

View File

@ -21,7 +21,12 @@ from django.views.generic import DetailView, FormView, TemplateView
from django.views.generic.edit import CreateView 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 live_charts, scrobble_counts, week_of_scrobbles from music.aggregators import (
live_charts,
live_tv_charts,
scrobble_counts,
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
from rest_framework.decorators import ( from rest_framework.decorators import (
@ -59,6 +64,7 @@ from scrobbles.utils import (
get_long_plays_completed, get_long_plays_completed,
get_long_plays_in_progress, get_long_plays_in_progress,
) )
from profiles.utils import now_user_timezone
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -822,6 +828,68 @@ class ChartRecordView(TemplateView):
"year": list(live_charts(**track, chart_period="year")), "year": list(live_charts(**track, chart_period="year")),
"all": list(live_charts(**track)), "all": list(live_charts(**track)),
} }
now = timezone.now()
tzinfo = now.tzinfo
if user.is_authenticated:
now = now_user_timezone(user.profile)
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_month = now.replace(day=1)
# TV Series Scrobbles
series_scrobbles = Scrobble.objects.filter(
user=user,
video__video_type=Video.VideoType.TV_EPISODE,
played_to_completion=True,
)
context_data["series_this_week"] = series_scrobbles.filter(
timestamp__gte=start_day_of_week
).order_by("-timestamp")
context_data["series_this_month"] = series_scrobbles.filter(
timestamp__gte=start_day_of_month
).order_by("-timestamp")
# Movie Scrobbles
movie_scrobbles = Scrobble.objects.filter(
user=user,
video__video_type=Video.VideoType.MOVIE,
played_to_completion=True,
)
context_data["videos_this_week"] = movie_scrobbles.filter(
timestamp__gte=start_day_of_week
).order_by("-timestamp")
context_data["videos_this_month"] = movie_scrobbles.filter(
timestamp__gte=start_day_of_month
).order_by("-timestamp")
# YouTube Scrobbles
youtube_scrobbles = Scrobble.objects.filter(
user=user,
video__video_type=Video.VideoType.YOUTUBE,
played_to_completion=True,
)
context_data["youtube_this_week"] = youtube_scrobbles.filter(
timestamp__gte=start_day_of_week
).order_by("-timestamp")
context_data["youtube_this_month"] = youtube_scrobbles.filter(
timestamp__gte=start_day_of_month
).order_by("-timestamp")
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")
),
"year": list(live_tv_charts(user=user, chart_period="year")),
"all": list(live_tv_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.

View File

@ -342,3 +342,187 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<div class="row">
<h2>Top TV Shows</h2>
<ul class="nav nav-tabs" id="tvshowTab" 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="tvshow-{{key}}-tab" data-bs-toggle="tab" data-bs-target="#tvshow-{{key}}"
type="button" role="tab" aria-controls="home" aria-selected="true">{{name}}</button>
</li>
{% endfor %}
</ul>
<div class="tab-content" id="tvshowTabContent" class="maloja-chart">
{% for key, shows in tv_show_charts.items %}
<div class="tab-pane fade {% if forloop.counter == 2 %}show active{% endif %}" id="tvshow-{{key}}" role="tabpanel" aria-labelledby="tvshow-{{key}}-tab">
<div style="display:block">
{% if shows.0 %}
<div style="float:left;">
<div class="image-wrapper" style="display:flex; flex-wrap: wrap; margin:0">
<div class="caption">#1 {{shows.0.name}} ({{shows.0.num_scrobbles}} episodes)</div>
{% if shows.0.cover_image %}
<a href="{{shows.0.get_absolute_url}}"><img lt="{{shows.0.name}}" src="{{shows.0.cover_medium.url}}" width="300px"></a>
{% else %}
<a href="{{shows.0.get_absolute_url}}"><img lt="{{shows.0.name}}" src="{% static "images/not-found.jpg" %}" width="300px"></a>
{% endif %}
</div>
</div>
{% endif %}
{% if shows.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 {{shows.1.name}} ({{shows.1.num_scrobbles}})</div>
{% if shows.1.cover_image %}
<a href="{{shows.1.get_absolute_url}}"><img lt="{{shows.1.name}}" src="{{shows.1.cover_small.url}}" width="150px"></a>
{% else %}
<a href="{{shows.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 {{shows.2.name}} ({{shows.2.num_scrobbles}})</div>
{% if shows.2.cover_image %}
<a href="{{shows.2.get_absolute_url}}"><img lt="{{shows.2.name}}" src="{{shows.2.cover_small.url}}" width="150px"></a>
{% else %}
<a href="{{shows.2.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="150px"></a>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
<div class="row">
<div class="col-md">
<h2>TV Shows Watched</h2>
{% if series_this_week %}
<h3>This Week</h3>
<div class="table-responsive">
<table class="table table-striped table-sm">
<tbody>
{% for scrobble in series_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 episodes watched this week</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% 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>
{% if videos_this_week or videos_this_month %}
<div class="col-md">
<h2>Movies Watched</h2>
{% if videos_this_week %}
<h3>This Week</h3>
<div class="table-responsive">
<table class="table table-striped table-sm">
<tbody>
{% for scrobble in videos_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 movies watched this week</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if videos_this_month %}
<h3>This Month</h3>
<div class="table-responsive">
<table class="table table-striped table-sm">
<tbody>
{% for scrobble in videos_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 movies watched this month</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
{% 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>

View File

@ -95,6 +95,7 @@
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal"
data-bs-target="#exportModal">Export</button> data-bs-target="#exportModal">Export</button>
</div> </div>
<a href="{% url 'scrobbles:charts-home' %}" class="btn btn-sm btn-outline-secondary">Charts</a>
{% endif %} {% endif %}
<div class="dropdown"> <div class="dropdown">
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" id="graphDateButton" <button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" id="graphDateButton"