Add rudimentary manual scrobbling
This commit is contained in:
5
vrobbler/apps/scrobbles/forms.py
Normal file
5
vrobbler/apps/scrobbles/forms.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django import forms
|
||||
|
||||
|
||||
class ScrobbleForm(forms.Form):
|
||||
imdb_id = forms.CharField(label="IMDB", max_length=30)
|
||||
57
vrobbler/apps/scrobbles/imdb.py
Normal file
57
vrobbler/apps/scrobbles/imdb.py
Normal 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
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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):
|
||||
|
||||
Reference in New Issue
Block a user