Add long play infra

This commit is contained in:
2023-03-08 12:11:58 -05:00
parent 788e1ab9e9
commit 960fe3e8d1
11 changed files with 209 additions and 48 deletions

View File

@ -92,6 +92,7 @@ class ScrobbleAdmin(admin.ModelAdmin):
list_filter = ( list_filter = (
"is_paused", "is_paused",
"in_progress", "in_progress",
"long_play_complete",
"source", "source",
) )
ordering = ("-timestamp",) ordering = ("-timestamp",)

View File

@ -1,4 +1,7 @@
JELLYFIN_VIDEO_ITEM_TYPES = ["Episode", "Movie"] JELLYFIN_VIDEO_ITEM_TYPES = ["Episode", "Movie"]
JELLYFIN_AUDIO_ITEM_TYPES = ["Audio"] JELLYFIN_AUDIO_ITEM_TYPES = ["Audio"]
LONG_PLAY_MEDIA = ["VideoGame", "Book"] LONG_PLAY_MEDIA = {
"videogames": "VideoGame",
"books": "Book",
}

View File

@ -1,6 +1,7 @@
import calendar import calendar
import datetime import datetime
import logging import logging
from typing import Optional
from uuid import uuid4 from uuid import uuid4
from books.models import Book from books.models import Book
@ -447,6 +448,25 @@ class Scrobble(TimeStampedModel):
is_stale = True is_stale = True
return is_stale return is_stale
@property
def previous(self):
return (
self.media_obj.scrobble_set.filter(timestamp__lt=self.timestamp)
.order_by("-timestamp")
.last()
)
@property
def long_play_session_seconds(self) -> Optional[int]:
"""Look one scrobble back, if it isn't complete,"""
if self.long_play_complete is not None:
if self.previous:
return int(self.playback_position) - int(
self.previous.playback_position
)
else:
return self.playback_position
@property @property
def percent_played(self) -> int: def percent_played(self) -> int:
if not self.media_obj: if not self.media_obj:
@ -470,7 +490,7 @@ class Scrobble(TimeStampedModel):
@property @property
def can_be_updated(self) -> bool: def can_be_updated(self) -> bool:
updatable = True updatable = True
if self.media_obj.__class__.__name__ in LONG_PLAY_MEDIA: if self.media_obj.__class__.__name__ in LONG_PLAY_MEDIA.values():
logger.info(f"No - Long play media") logger.info(f"No - Long play media")
updatable = False updatable = False
if self.percent_played > 100: if self.percent_played > 100:

View File

@ -67,4 +67,9 @@ urlpatterns = [
views.ChartRecordView.as_view(), views.ChartRecordView.as_view(),
name="charts-home", name="charts-home",
), ),
path(
"long-plays/",
views.ScrobbleLongPlaysView.as_view(),
name="long-plays",
),
] ]

View File

@ -7,6 +7,8 @@ from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import models from django.db import models
from vrobbler.apps.scrobbles.constants import LONG_PLAY_MEDIA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
User = get_user_model() User = get_user_model()
@ -123,3 +125,37 @@ def get_scrobbles_for_media(media_obj, user: User) -> models.QuerySet:
logger.warn("Do not know about media {media_class} 🙍") logger.warn("Do not know about media {media_class} 🙍")
return [] return []
return Scrobble.objects.filter(media_query, user=user) return Scrobble.objects.filter(media_query, user=user)
def get_long_plays_in_progress(user: User) -> list:
"""Find all books where the last scrobble is not marked complete"""
media_list = []
for app, model in LONG_PLAY_MEDIA.items():
media_obj = apps.get_model(app_label=app, model_name=model)
for media in media_obj.objects.all():
if (
media.scrobble_set.all()
and media.scrobble_set.filter(user=user)
.last()
.long_play_complete
== False
):
media_list.append(media)
return media_list
def get_long_plays_completed(user: User) -> list:
"""Find all books where the last scrobble is not marked complete"""
media_list = []
for app, model in LONG_PLAY_MEDIA.items():
media_obj = apps.get_model(app_label=app, model_name=model)
for media in media_obj.objects.all():
if (
media.scrobble_set.all()
and media.scrobble_set.filter(user=user)
.last()
.long_play_complete
== True
):
media_list.append(media)
return media_list

View File

@ -57,9 +57,14 @@ from scrobbles.tasks import (
process_tsv_import, process_tsv_import,
) )
from sports.thesportsdb import lookup_event_from_thesportsdb from sports.thesportsdb import lookup_event_from_thesportsdb
from videos.imdb import lookup_video_from_imdb
from videogames.howlongtobeat import lookup_game_from_hltb from videogames.howlongtobeat import lookup_game_from_hltb
from videos.imdb import lookup_video_from_imdb
from vrobbler.apps.books.openlibrary import lookup_book_from_openlibrary from vrobbler.apps.books.openlibrary import lookup_book_from_openlibrary
from vrobbler.apps.scrobbles.utils import (
get_long_plays_completed,
get_long_plays_in_progress,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -74,6 +79,7 @@ class RecentScrobbleList(ListView):
completed_for_user = Scrobble.objects.filter( completed_for_user = Scrobble.objects.filter(
played_to_completion=True, user=user played_to_completion=True, user=user
) )
data["long_play_in_progress"] = get_long_plays_in_progress(user)
data["video_scrobble_list"] = completed_for_user.filter( data["video_scrobble_list"] = completed_for_user.filter(
video__isnull=False video__isnull=False
).order_by("-timestamp")[:15] ).order_by("-timestamp")[:15]
@ -124,6 +130,18 @@ class RecentScrobbleList(ListView):
).order_by("-timestamp")[:15] ).order_by("-timestamp")[:15]
class ScrobbleLongPlaysView(TemplateView):
template_name = "scrobbles/long_plays_in_progress.html"
def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs)
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
class ScrobbleImportListView(TemplateView): class ScrobbleImportListView(TemplateView):
template_name = "scrobbles/import_list.html" template_name = "scrobbles/import_list.html"
@ -181,28 +199,28 @@ class ManualScrobbleView(FormView):
def form_valid(self, form): def form_valid(self, form):
item_id = form.cleaned_data.get("item_id") key, item_id = form.cleaned_data.get("item_id").split(" ")
data_dict = None data_dict = None
if "-v" in item_id or not data_dict: if key == "-v":
logger.debug(f"Looking for video game with ID {item_id}") logger.debug(f"Looking for video game with ID {item_id}")
data_dict = lookup_game_from_hltb(item_id.replace("-v", "")) data_dict = lookup_game_from_hltb(item_id.replace("-v", ""))
if data_dict: if data_dict:
manual_scrobble_video_game(data_dict, self.request.user.id) manual_scrobble_video_game(data_dict, self.request.user.id)
if "-b" in item_id and not data_dict: if key == "-b":
logger.debug(f"Looking for book with ID {item_id}") logger.debug(f"Looking for book with ID {item_id}")
data_dict = lookup_book_from_openlibrary(item_id.replace("-b", "")) data_dict = lookup_book_from_openlibrary(item_id.replace("-b", ""))
if data_dict: if data_dict:
manual_scrobble_book(data_dict, self.request.user.id) manual_scrobble_book(data_dict, self.request.user.id)
if "-s" in item_id and not data_dict: if key == "-s":
logger.debug(f"Looking for sport event with ID {item_id}") logger.debug(f"Looking for sport event with ID {item_id}")
data_dict = lookup_event_from_thesportsdb(item_id) data_dict = lookup_event_from_thesportsdb(item_id)
if data_dict: if data_dict:
manual_scrobble_event(data_dict, self.request.user.id) manual_scrobble_event(data_dict, self.request.user.id)
if "tt" in item_id: if key == "-i":
data_dict = lookup_video_from_imdb(item_id) data_dict = lookup_video_from_imdb(item_id)
if data_dict: if data_dict:
manual_scrobble_video(data_dict, self.request.user.id) manual_scrobble_video(data_dict, self.request.user.id)

View File

@ -5,6 +5,8 @@ register = template.Library()
@register.filter @register.filter
def natural_duration(value): def natural_duration(value):
if not value:
return
value = int(value) value = int(value)
total_minutes = int(value / 60) total_minutes = int(value / 60)
hours = int(total_minutes / 60) hours = int(total_minutes / 60)

View File

@ -32,6 +32,8 @@
min-height: 3em; min-height: 3em;
border-right: 1px solid #777; border-right: 1px solid #777;
} }
.now-playing p { margin:0; }
.now-playing .right { float:right; margin-right:10px; }
.latest-scrobble { .latest-scrobble {
width: 50%; width: 50%;
} }
@ -220,33 +222,9 @@
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" aria-current="page" href="/tracks/"> <a class="nav-link" href="/long-plays/">
<span data-feather="music"></span> <span data-feather="playv"></span>
Tracks Long plays
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/artists/">
<span data-feather="user"></span>
Artists
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/albums/">
<span data-feather="music"></span>
Albums
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/movies/">
<span data-feather="film"></span>
Movies
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/series/">
<span data-feather="tv"></span>
TV Shows
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
@ -262,28 +240,30 @@
<hr/> <hr/>
{% if now_playing_list and user.is_authenticated %} {% if now_playing_list and user.is_authenticated %}
<ul style="padding-right:10px;"> <ul>
<b>Now playing</b> <b>Now playing</b>
{% for scrobble in now_playing_list %} {% for scrobble in now_playing_list %}
<div> <div class="now-playing">
{% if scrobble.media_obj.album.cover_image %} {% if scrobble.media_obj.album.cover_image %}
<td><img src="{{scrobble.track.album.cover_image.url}}" width=120 height=120 style="border:1px solid black; " /></td><br/> <div style="float:left;padding-right:5px;"><img src="{{scrobble.track.album.cover_image.url}}" width=75 height=75 style="border:1px solid black; " /></div>
{% endif %} {% endif %}
<a href="{{scrobble.media_obj.get_absolute_url}}">{{scrobble.media_obj.title}}</a><br/> <p><a href="{{scrobble.media_obj.get_absolute_url}}">{{scrobble.media_obj.title}}</a></p>
{% if scrobble.media_obj.subtitle %} {% if scrobble.media_obj.subtitle %}
<em><a href="{{scrobble.media_obj.subtitle.get_absolute_url}}">{{scrobble.media_obj.subtitle}}</a></em><br/> <p><em><a href="{{scrobble.media_obj.subtitle.get_absolute_url}}">{{scrobble.media_obj.subtitle}}</a></em></p>
{% endif %} {% endif %}
<small>{{scrobble.timestamp|naturaltime}}<br/> from {{scrobble.source}}</small> <br/>
<p><small>{{scrobble.timestamp|naturaltime}} from {{scrobble.source}}</small></p>
<div class="progress-bar" style="margin-right:5px;"> <div class="progress-bar" style="margin-right:5px;">
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span> <span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
</div> </div>
<a href="{% url "scrobbles:cancel" scrobble.uuid %}">Cancel</a> <p class="action-buttons">
<a href="{% url "scrobbles:finish" scrobble.uuid %}">Finish</a> <a href="{% url "scrobbles:cancel" scrobble.uuid %}">Cancel</a>
<hr /> <a class="right" href="{% url "scrobbles:finish" scrobble.uuid %}">Finish</a>
</p>
</div> </div>
{% endfor %} {% endfor %}
</ul> </ul>
<hr/> {% if now_playing_list|length > 1 %}<hr/>{% endif %}
{% endif %} {% endif %}
{% if active_imports %} {% if active_imports %}

View File

@ -45,7 +45,7 @@
{% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %} {% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %}
<tr> <tr>
<td>{{scrobble.timestamp}}</td> <td>{{scrobble.timestamp}}</td>
<td>{% if scrobble.in_progress %}Now playing{% else %}{{scrobble.playback_position|natural_duration}}{% endif %}</td> <td>{% if scrobble.in_progress %}Now playing{% else %}{{scrobble.long_play_session_seconds|natural_duration}}{% endif %}</td>
<td>{% for author in scrobble.book.authors.all %}<a href="{{author.get_absolute_url}}">{{author}}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</td> <td>{% for author in scrobble.book.authors.all %}<a href="{{author.get_absolute_url}}">{{author}}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -0,0 +1,95 @@
{% extends "base_list.html" %}
{% load urlreplace %}
{% block title %}Long Plays{% endblock %}
{% block lists %}
<div class="row">
<p class="view">
<span class="view-links">
{% if view == 'grid' %}
View as <a href="?{% urlreplace view='list' %}">List</a>
{% else %}
View as <a href="?{% urlreplace view='grid' %}">Grid</a>
{% endif %}
</span>
</p>
<hr />
<h2>In progress</h2>
{% if view == 'grid' %}
<div>
{% for media in in_progress %}
{% if media.cover %}
<dl style="width: 130px; float: left; margin-right:10px;">
<dd><img src="{{media.cover.url}}" width=120 height=120 /></dd>
</dl>
{% elif media.hltb_cover %}
<dl style="width: 130px; float: left; margin-right:10px;">
<dd><img src="{{media.htlb_cover.url}}" width=120 height=120 /></dd>
</dl>
{% endif %}
{% endfor %}
</div>
{% else %}
<div class="col-md">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Scrobbles</th>
<th scope="col">Title</th>
</tr>
</thead>
<tbody>
{% for media in in_progress %}
<tr>
<td>{{media.scrobble_set.count}}</td>
<td><a href="{{media.get_absolute_url}}">{{media}}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<h2>Completed</h2>
{% if view == 'grid' %}
<div>
{% for media in completed %}
{% if media.cover %}
<dl style="width: 130px; float: left; margin-right:10px;">
<dd><img src="{{media.cover.url}}" width=120 height=120 /></dd>
</dl>
{% elif media.hltb_cover %}
<dl style="width: 130px; float: left; margin-right:10px;">
<dd><img src="{{media.htlb_cover.url}}" width=120 height=120 /></dd>
</dl>
{% endif %}
{% endfor %}
</div>
{% else %}
<div class="col-md">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Scrobbles</th>
<th scope="col">Title</th>
</tr>
</thead>
<tbody>
{% for media in completed %}
<tr>
<td>{{media.scrobble_set.count}}</td>
<td><a href="{{media.get_absolute_url}}">{{media}}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -27,7 +27,8 @@
</div> </div>
<div class="row"> <div class="row">
<p>{{object.scrobble_set.count}} scrobbles</p> <p>{{object.scrobble_set.count}} scrobbles</p>
<p><a href="">Start playing</a></p> <p>{{object.scrobble_set.last.playback_position|natural_duration}}{% if object.scrobble_set.last.long_play_complete %} and completed{% else %} spent playing{% endif %}</p>
<p><a href="">Resume long play</a></p>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md"> <div class="col-md">
@ -45,7 +46,7 @@
{% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %} {% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %}
<tr> <tr>
<td>{{scrobble.timestamp}}</td> <td>{{scrobble.timestamp}}</td>
<td>{% if scrobble.in_progress %}Now playing{% else %}{{scrobble.playback_position|natural_duration}}{% endif %}</td> <td>{% if scrobble.in_progress %}Now playing{% else %}{{scrobble.long_play_session_seconds|natural_duration}}{% endif %}</td>
<td>{% for platform in scrobble.video_game.platforms.all %}<a href="{{platform.get_absolute_url}}">{{platform}}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</td> <td>{% for platform in scrobble.video_game.platforms.all %}<a href="{{platform.get_absolute_url}}">{{platform}}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</td>
</tr> </tr>
{% endfor %} {% endfor %}