Compare commits

..

3 Commits
35 ... 36

Author SHA1 Message Date
8c865fe008 [release] Release version 36 2025-11-17 18:15:21 -05:00
572dbf7a88 [videos] Clean up utilities 2025-11-17 18:13:40 -05:00
7addd50577 [videos] Refactor lookup to use new library 2025-11-17 17:56:15 -05:00
6 changed files with 112 additions and 140 deletions

View File

@ -92,7 +92,7 @@ fetching and simple saving.
:LOGBOOK:
CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20
:END:
* Backlog [2/25]
* Backlog [1/23]
** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :vrobbler:personal:bug:music:scrobbles:
** TODO [#C] Move to using more robust mopidy-webhooks pacakge form pypi :utility:improvement:
:PROPERTIES:
@ -445,6 +445,11 @@ https://life.lab.unbl.ink/scrobble/e39779c8-62a5-46a6-bdef-fb7662810dc6/start/
This may have already been resolved ... need to just confirm it.
** TODO [#A] Find page numbers for comic books from ComicVine :vrobbler:feature:books:personal:project:
* Version 36.0 [1/1]
** DONE [#A] Refactor how videos are scrobbled :vrobbler:vidoes:feature:personal:project:
:PROPERTIES:
:ID: 6034a11d-5376-994d-9a4b-e1640e258cfa
:END:
* Version 35.0 [3/3]
** DONE [#B] Add youtube link in place of IMDB on video detail page :vrobbler:feature:videos:personal:project:
:PROPERTIES:

View File

@ -123,9 +123,8 @@ def jellyfin_scrobble_media(
/ 10000000
)
if media_type == Scrobble.MediaType.VIDEO:
media_obj = Video.get_from_imdb_id(
post_data.get("Provider_imdb", "").replace("tt", "")
)
imdb_id = post_data.get("Provider_imdb", "")
media_obj = Video.find_or_create(imdb_id)
else:
media_obj = Track.find_or_create(
title=post_data.get("Name", ""),
@ -162,18 +161,14 @@ def jellyfin_scrobble_media(
def web_scrobbler_scrobble_media(
youtube_id: str, user_id: int, status: str = "started"
) -> Optional[Scrobble]:
video = Video.get_from_youtube_id(youtube_id)
video = Video.find_or_create(youtube_id)
return video.scrobble_for_user(user_id, status, source="Web Scrobbler")
def manual_scrobble_video(
video_id: str, user_id: int, source: str = "IMDb", action: Optional[str] = None
):
if "tt" in video_id:
video = Video.get_from_imdb_id(video_id)
else:
video = Video.get_from_youtube_id(video_id)
video = Video.find_or_create(video_id)
# When manually scrobbling, try finding a source from the series
if video.tv_series:

View File

@ -59,8 +59,11 @@ class VideoMetadata:
def as_dict_with_cover_and_genres(self) -> tuple:
video_dict = vars(self)
series_id = ""
cover = None
if "cover_url" in video_dict.keys():
cover = video_dict.pop("cover_url", "")
genres = video_dict.pop("genres", [])
return video_dict, cover, genres
if "tv_series_imdb_id" in video_dict.keys():
series_id = video_dict.pop("tv_series_imdb_id")
return video_dict, series_id, cover, genres

View File

@ -1,4 +1,5 @@
from dataclasses import dataclass
import re
import logging
from typing import Optional
from uuid import uuid4
@ -27,7 +28,9 @@ from vrobbler.apps.scrobbles.dataclasses import BaseLogData, WithPeopleLogData
YOUTUBE_VIDEO_URL = "https://www.youtube.com/watch?v="
YOUTUBE_CHANNEL_URL = "https://www.youtube.com/channel/"
IMDB_VIDEO_URL = "https://www.imdb.com/title/tt"
YOUTUBE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{11}$')
IMDB_VIDEO_URL = "https://www.imdb.com/title/"
logger = logging.getLogger(__name__)
BNULL = {"blank": True, "null": True}
@ -136,6 +139,13 @@ class Series(TimeStampedModel):
url = self.cover_image_medium.url
return url
def save_image_from_url(self, url: str, force_update: bool = False):
if not self.cover_image or (force_update and url):
r = requests.get(url)
if r.status_code == 200:
fname = f"{self.name}_{self.uuid}.jpg"
self.cover_image.save(fname, ContentFile(r.content), save=True)
def scrobbles_for_user(self, user_id: int, include_playing=False):
from scrobbles.models import Scrobble
@ -184,6 +194,32 @@ class Series(TimeStampedModel):
if genres := imdb_dict.get("genres"):
self.genre.add(*genres)
@classmethod
def find_or_create(cls, imdb_id: str, overwrite: bool = True):
series, created = cls.objects.get_or_create(imdb_id=imdb_id)
if not created and not overwrite:
logger.info("Series not created and overwrite=False, returning")
return series
vdict, _, cover, genres = lookup_video_from_imdb(
imdb_id
).as_dict_with_cover_and_genres()
vdict.pop("video_type")
vdict["name"] = vdict.pop("title")
for k, v in vdict.items():
setattr(series, k, v)
series.save()
if cover:
series.save_image_from_url(cover)
if genres:
series.genre.add(*genres)
return series
class Video(ScrobblableMixin):
COMPLETION_PERCENT = getattr(settings, "VIDEO_COMPLETION_PERCENT", 90)
@ -304,7 +340,7 @@ class Video(ScrobblableMixin):
if not created and not overwrite:
return video
vdict, cover, genres = lookup_video_from_youtube(
vdict, _, cover, genres = lookup_video_from_youtube(
youtube_id
).as_dict_with_cover_and_genres()
if created or overwrite:
@ -320,28 +356,38 @@ class Video(ScrobblableMixin):
def get_from_imdb_id(
cls, imdb_id: str, overwrite: bool = False
) -> "Video":
if "tt" in imdb_id:
imdb_id = imdb_id[2:]
video, created = cls.objects.get_or_create(imdb_id=imdb_id)
if not created and not overwrite:
return video
vdict, cover, genres = lookup_video_from_tmdb(
vdict, series_id, cover, genres = lookup_video_from_imdb(
imdb_id
).as_dict_with_cover_and_genres()
if created or overwrite:
for k, v in vdict.items():
setattr(video, k, v)
if series_id:
video.tv_series = Series.find_or_create(imdb_id=series_id)
video.save()
video.save_image_from_url(cover)
video.genre.add(*genres)
if cover:
video.save_image_from_url(cover)
if genres:
video.genre.add(*genres)
return video
@classmethod
def find_or_create(
cls, data_dict: dict, post_keys: dict = JELLYFIN_POST_KEYS
) -> Optional["Video"]:
"""Thes smallest of wrappers around our actual get or create utility."""
imdb_key = post_keys.get("IMDB_ID", "").replace("tt", "")
return cls.get_from_imdb_id(data_dict.get(imdb_key))
def find_or_create(cls, source_id: str, overwrite: bool = True) -> "Video":
if "tt" in source_id:
return cls.get_from_imdb_id(source_id, overwrite)
if bool(YOUTUBE_ID_PATTERN.match(source_id)):
return cls.get_from_youtube_id(source_id, overwrite)
#TODO scrobble but without a video obj?
logger.warning("Video ID not recognized, not scrobbling")
return

View File

@ -6,55 +6,17 @@ from videos.metadata import VideoMetadata, VideoType
logger = logging.getLogger(__name__)
def lookup_video_from_imdb(
name_or_id: str, kind: str = "movie"
) -> VideoMetadata:
from videos.models import Series
# Very few video titles start with tt, but IMDB IDs often come in with it
if name_or_id.startswith("tt"):
name_or_id = name_or_id[2:]
imdb_id = None
try:
imdb_id = int(name_or_id)
except ValueError:
pass
def lookup_video_from_imdb(imdb_id: str) -> VideoMetadata:
if not imdb_id.startswith("tt"):
logger.warning("This method requires an IMDB ID starting with 'tt'")
return VideoMetadata()
video_metadata = VideoMetadata(imdb_id=imdb_id)
imdb_result: dict = {}
imdb_result = imdb.get_title(name_or_id)
imdb_result = imdb.get_title(imdb_id)
logger.debug(f"Found result from IMDB: {imdb_result.title}")
if not imdb_result:
imdb_result = {}
imdb_results: list = imdb.search_movie(name_or_id)
if len(imdb_results) > 1:
for result in imdb_results:
if result["kind"] == kind:
imdb_client.update(
result,
info=[
"plot",
"synopsis",
"taglines",
"next_episode",
"genres",
],
)
imdb_result = result
break
if len(imdb_results) == 1:
imdb_result = imdb_results[0]
imdb_client.update(
imdb_result,
info=["plot", "synopsis", "taglines", "next_episode", "genres"],
)
if not imdb_result:
logger.info(
f"[lookup_video_from_imdb] no video found on imdb",
@ -62,39 +24,27 @@ def lookup_video_from_imdb(
)
return None
video_metadata.cover_url = imdb_result.get("cover url")
if video_metadata.cover_url:
video_metadata.cover_url = helpers.resizeImage(
video_metadata.cover_url, width=800
)
video_metadata.imdb_id = imdb_id
video_metadata.title = imdb_result.title
video_metadata.base_run_time_seconds = (
imdb_result.runtime * 60
)
video_metadata.year = imdb_result.year
video_metadata.plot = imdb_result.plot.get("en-US", "")
video_metadata.imdb_rating = imdb_result.rating
video_metadata.genres = imdb_result.genres
video_metadata.cover_url = imdb_result.primary_image
video_metadata.video_type = VideoType.MOVIE.value
series_name = None
if imdb_result.get("kind") == "episode":
try:
series_name = imdb_result.get("episode of", None).data.get(
"title", None
)
except IndexError:
series_name = None
if series_name:
series, _ = Series.objects.get_or_create(name=series_name)
video_metadata.video_type = VideoType.TV_EPISODE.value
video_metadata.tv_series_id = series.id
if imdb_result.type_id == "tvEpisode":
video_metadata.video_type = VideoType.TV_EPISODE.value
if imdb_result.get("runtimes"):
video_metadata.base_run_time_seconds = (
int(imdb_result.get("runtimes")[0]) * 60
)
series = imdb_result.series
video_metadata.tv_series_imdb_id = series.imdb_id
video_metadata.tv_series_title = series.title
video_metadata.episode_number = imdb_result.episode
video_metadata.season_number = imdb_result.season
video_metadata.next_imdb_id = imdb_result.next_episode_id
video_metadata.imdb_id = name_or_id
video_metadata.title = imdb_result.get("title", "")
video_metadata.episode_number = imdb_result.get("episode", None)
video_metadata.season_number = imdb_result.get("season", None)
video_metadata.next_imdb_id = imdb_result.get("next episode", None)
video_metadata.year = imdb_result.get("year", None)
video_metadata.plot = imdb_result.get("plot outline", "")
video_metadata.imdb_rating = imdb_result.get("rating", None)
video_metadata.genres = imdb_result.get("genres", [])
return video_metadata

View File

@ -1,51 +1,24 @@
import logging
from scrobbles.utils import convert_to_seconds
from videos.imdb import lookup_video_from_imdb
from videos.models import Series, Video
from videos.skatevideosite import lookup_video_from_skatevideosite
from videos.models import Video
from django.db import IntegrityError
#from videos.skatevideosite import lookup_video_from_skatevideosite
logger = logging.getLogger(__name__)
def get_or_create_video(data_dict: dict, post_keys: dict, force_update=False):
name_or_id = data_dict.get(post_keys.get("IMDB_ID"), "") or data_dict.get(
post_keys.get("VIDEO_TITLE"), ""
)
def clean_up_videos():
videos = Video.objects.filter(imdb_id__isnull=False).exclude(imdb_id__icontains="tt")
video = Video.objects.filter(imdb_id=name_or_id).first()
if video:
return video
for video in videos:
logger.info(f"Fixing imdb_id for {video}")
video.imdb_id = "tt" + video.imdb_id
try:
video.save(update_fields=["imdb_id"])
except IntegrityError:
new_video = Video.objects.filter(imdb_id="tt" + video.imdb_id).first()
video.scrobble_set.all().update(video=new_video)
video.delete()
imdb_metadata = lookup_video_from_imdb(name_or_id)
# skatevideosite_metadata = lookup_video_from_skatevideosite(name_or_id)
# youtube_metadata = {} # TODO lookup_video_from_youtube(name_or_id)
video_dict = imdb_metadata
if not video_dict:
logger.info(
"No video found on imdb, skatevideosite or youtube, cannot scrobble",
extra={"name_or_id": name_or_id},
)
return
video = Video.get_from_imdb_id(video_dict.get("imdb_id")
if not "overview" in video_dict.keys():
video_dict["overview"] = data_dict.get(
post_keys.get("OVERVIEW"), None
)
if not "tagline" in video_dict.keys():
video_dict["tagline"] = data_dict.get(
post_keys.get("TAGLINE"), None
)
if not "tmdb_id" in video_dict.keys():
video_dict["tmdb_id"] = data_dict.get(
post_keys.get("TMDB_ID"), None
)
return video
def get_or_create_video_from_skatevideosite(title: str, force_update: bool=True):
return
videos = Video.objects.filter(scrobble__isnull=True)
videos.delete()