Add rudimentary manual scrobbling

This commit is contained in:
2023-01-13 16:47:06 -05:00
parent eeee6eea4e
commit e6cf126f5c
13 changed files with 421 additions and 38 deletions

View File

@ -0,0 +1,5 @@
from django import forms
class ScrobbleForm(forms.Form):
imdb_id = forms.CharField(label="IMDB", max_length=30)

View File

@ -0,0 +1,57 @@
import logging
from typing import Optional
from django.utils import timezone
import imdb
from videos.models import Video
imdb_client = imdb.Cinemagoer()
logger = logging.getLogger(__name__)
def lookup_video_from_imdb(imdb_id: str) -> dict:
if 'tt' not in imdb_id:
logger.warning(f"IMDB ID should begin with 'tt' {imdb_id}")
return
lookup_id = imdb_id.strip('tt')
media = imdb_client.get_movie(lookup_id)
run_time_seconds = int(media.get('runtimes')[0]) * 60
# Ticks otherwise known as miliseconds
run_time_ticks = run_time_seconds * 1000 * 1000
item_type = Video.VideoType.MOVIE
if media.get('series title'):
item_type = Video.VideoType.TV_EPISODE
try:
plot = media.get('plot')[0]
except TypeError:
plot = ""
except IndexError:
plot = ""
# Build a rough approximation of a Jellyfin data response
data_dict = {
"ItemType": item_type,
"Name": media.get('title'),
"Overview": plot,
"Tagline": media.get('tagline'),
"Year": media.get('year'),
"Provider_imdb": imdb_id,
"RunTime": run_time_seconds,
"RunTimeTicks": run_time_ticks,
"SeriesName": media.get('series title'),
"EpisodeNumber": media.get('episode'),
"SeasonNumber": media.get('season'),
"PlaybackPositionTicks": 1,
"PlaybackPosition": 1,
"UtcTimestamp": timezone.now().strftime('%Y-%m-%d %H:%M:%S.%f%z'),
"IsPaused": False,
"PlayedToCompletion": False,
}
return data_dict

View File

@ -42,24 +42,27 @@ class Scrobble(TimeStampedModel):
@property
def percent_played(self) -> int:
if (
self.playback_position_ticks
and self.media_obj.run_time_ticks
and self.source != 'Mopidy'
):
return int(
(self.playback_position_ticks / self.media_obj.run_time_ticks)
* 100
)
# If we don't have media_obj.run_time_ticks, let's guess from created time
now = timezone.now()
playback_duration = (now - self.created).seconds
if playback_duration and self.media_obj.run_time:
return int(
(playback_duration / int(self.media_obj.run_time)) * 100
)
playback_ticks = None
percent_played = 100
return 0
if not self.media_obj.run_time_ticks:
logger.warning(
f"{self} has no run_time_ticks value, cannot show percent played"
)
return percent_played
playback_ticks = self.playback_position_ticks
if not playback_ticks:
logger.info(
"No playback_position_ticks, estimating based on creation time"
)
playback_ticks = (timezone.now() - self.timestamp).seconds * 1000
percent = int((playback_ticks / self.media_obj.run_time_ticks) * 100)
if percent > 100:
percent = 100
return percent
@property
def media_obj(self):

View File

@ -104,11 +104,15 @@ def create_jellyfin_scrobble_dict(data_dict: dict, user_id: int) -> dict:
if data_dict.get("PlayedToCompletion"):
jellyfin_status = "stopped"
playback_position_ticks = data_dict.get("PlaybackPositionTicks") // 10000
if playback_position_ticks <= 0:
playback_position_ticks = None
logger.debug(playback_position_ticks)
return {
"user_id": user_id,
"timestamp": parse(data_dict.get("UtcTimestamp")),
"playback_position_ticks": data_dict.get("PlaybackPositionTicks")
// 10000,
"playback_position_ticks": playback_position_ticks,
"playback_position": convert_to_seconds(
data_dict.get("PlaybackPosition")
),
@ -176,3 +180,16 @@ def jellyfin_scrobble_video(data_dict: dict, user_id: Optional[int]):
scrobble_dict = create_jellyfin_scrobble_dict(data_dict, user_id)
return Scrobble.create_or_update_for_video(video, user_id, scrobble_dict)
def manual_scrobble_video(data_dict: dict, user_id: Optional[int]):
if not data_dict.get("Provider_imdb", None):
logger.error(
"No IMDB ID received. This is likely because all metadata is bad, not scrobbling"
)
return
video = Video.find_or_create(data_dict)
scrobble_dict = create_jellyfin_scrobble_dict(data_dict, user_id)
return Scrobble.create_or_update_for_video(video, user_id, scrobble_dict)

View File

@ -10,8 +10,12 @@ logger = logging.getLogger(__name__)
def convert_to_seconds(run_time: str) -> int:
"""Jellyfin sends run time as 00:00:00 string. We want the run time to
actually be in seconds so we'll convert it"""
if ":" in run_time:
actually be in seconds so we'll convert it"
This is actually deprecated, as we now convert to seconds before saving.
But for older videos, we'll leave this here.
"""
if ":" in str(run_time):
run_time_list = run_time.split(":")
hours = int(run_time_list[0])
minutes = int(run_time_list[1])

View File

@ -1,15 +1,14 @@
import json
import logging
from datetime import datetime, timedelta
from dateutil.parser import parse
from django.conf import settings
from django.db.models.fields import timezone
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import FormView
from django.views.generic.list import ListView
from music.constants import JELLYFIN_POST_KEYS as KEYS
from music.models import Track
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
@ -17,16 +16,17 @@ from scrobbles.constants import (
JELLYFIN_AUDIO_ITEM_TYPES,
JELLYFIN_VIDEO_ITEM_TYPES,
)
from scrobbles.forms import ScrobbleForm
from scrobbles.imdb import lookup_video_from_imdb
from scrobbles.models import Scrobble
from scrobbles.scrobblers import (
jellyfin_scrobble_track,
jellyfin_scrobble_video,
manual_scrobble_video,
mopidy_scrobble_podcast,
mopidy_scrobble_track,
)
from scrobbles.serializers import ScrobbleSerializer
from scrobbles.utils import convert_to_seconds
from videos.models import Video
from vrobbler.apps.music.aggregators import (
scrobble_counts,
@ -85,6 +85,20 @@ class RecentScrobbleList(ListView):
).order_by('-timestamp')[:15]
class ManualScrobbleView(FormView):
form_class = ScrobbleForm
template_name = 'scrobbles/manual_form.html'
def form_valid(self, form):
# look for video via IMDB id
data_dict = lookup_video_from_imdb(form.cleaned_data.get('imdb_id'))
manual_scrobble_video(data_dict, self.request.user.id)
return HttpResponseRedirect(reverse("home"))
@csrf_exempt
@api_view(['GET'])
def scrobble_endpoint(request):

View File

@ -96,11 +96,16 @@ class Video(ScrobblableMixin):
video, created = cls.objects.get_or_create(**video_dict)
logger.debug(data_dict)
run_time_ticks = data_dict.get("RunTimeTicks", None)
if run_time_ticks:
run_time_ticks = run_time_ticks // 10000
video_extra_dict = {
"year": data_dict.get("Year", ""),
"overview": data_dict.get("Overview", None),
"tagline": data_dict.get("Tagline", None),
"run_time_ticks": data_dict.get("RunTimeTicks", 0) // 10000,
"run_time_ticks": run_time_ticks,
"run_time": convert_to_seconds(data_dict.get("RunTime", "")),
"tvdb_id": data_dict.get("Provider_tvdb", None),
"tvrage_id": data_dict.get("Provider_tvrage", None),

View File

@ -62,6 +62,8 @@ TMDB_API_KEY = os.getenv("VROBBLER_TMDB_API_KEY", "")
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
TIME_ZONE = os.getenv("VROBBLER_TIME_ZONE", "EST")
ALLOWED_HOSTS = ["*"]
CSRF_TRUSTED_ORIGINS = [
os.getenv("VROBBLER_TRUSTED_ORIGINS", "http://localhost:8000")

View File

@ -199,7 +199,7 @@
{% if scrobble.track %}<em>{{scrobble.track.artist}}</em><br/>{% endif %}
{% if scrobble.podcast_episode%}<em>{{scrobble.podcast_episode.podcast}}</em><br/>{% endif %}
{% if scrobble.video.tv_series %}<em>{{scrobble.video.tv_series }}</em><br/>{% endif %}
<small>{{scrobble.created|naturaltime}}<br/>
<small>{{scrobble.timestamp|naturaltime}}<br/>
from {{scrobble.source}}</small>
<div class="progress-bar" style="margin-right:5px;">
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
@ -243,6 +243,12 @@
</a>
</li>
{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="/manual/">
<span data-feather="key"></span>
Manual scrobble
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/">
<span data-feather="key"></span>

View File

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block content %}
<main class="col-md-4 ms-sm-auto col-lg-10 px-md-4">
<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">Manual scrobble</h1>
<form action="#" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Submit">
</form>
</div>
</main>
{% endblock %}

View File

@ -3,7 +3,7 @@ from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from rest_framework import routers
from scrobbles.views import RecentScrobbleList
from scrobbles.views import RecentScrobbleList, ManualScrobbleView
from videos import urls as video_urls
from scrobbles import urls as scrobble_urls
@ -15,6 +15,7 @@ urlpatterns = [
# path("movies/", include(movies, namespace="movies")),
# path("shows/", include(shows, namespace="shows")),
path("api/v1/scrobbles/", include(scrobble_urls, namespace="scrobbles")),
path('manual/', ManualScrobbleView.as_view(), name='manual-scrobble'),
path("", include(video_urls, namespace="videos")),
path("", RecentScrobbleList.as_view(), name="home"),
]