Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c865fe008 | |||
| 572dbf7a88 | |||
| 7addd50577 |
@ -92,7 +92,7 @@ fetching and simple saving.
|
|||||||
:LOGBOOK:
|
:LOGBOOK:
|
||||||
CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20
|
CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20
|
||||||
:END:
|
: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] 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:
|
** TODO [#C] Move to using more robust mopidy-webhooks pacakge form pypi :utility:improvement:
|
||||||
:PROPERTIES:
|
: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.
|
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:
|
** 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]
|
* Version 35.0 [3/3]
|
||||||
** DONE [#B] Add youtube link in place of IMDB on video detail page :vrobbler:feature:videos:personal:project:
|
** DONE [#B] Add youtube link in place of IMDB on video detail page :vrobbler:feature:videos:personal:project:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
|
|||||||
@ -123,9 +123,8 @@ def jellyfin_scrobble_media(
|
|||||||
/ 10000000
|
/ 10000000
|
||||||
)
|
)
|
||||||
if media_type == Scrobble.MediaType.VIDEO:
|
if media_type == Scrobble.MediaType.VIDEO:
|
||||||
media_obj = Video.get_from_imdb_id(
|
imdb_id = post_data.get("Provider_imdb", "")
|
||||||
post_data.get("Provider_imdb", "").replace("tt", "")
|
media_obj = Video.find_or_create(imdb_id)
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
media_obj = Track.find_or_create(
|
media_obj = Track.find_or_create(
|
||||||
title=post_data.get("Name", ""),
|
title=post_data.get("Name", ""),
|
||||||
@ -162,18 +161,14 @@ def jellyfin_scrobble_media(
|
|||||||
def web_scrobbler_scrobble_media(
|
def web_scrobbler_scrobble_media(
|
||||||
youtube_id: str, user_id: int, status: str = "started"
|
youtube_id: str, user_id: int, status: str = "started"
|
||||||
) -> Optional[Scrobble]:
|
) -> 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")
|
return video.scrobble_for_user(user_id, status, source="Web Scrobbler")
|
||||||
|
|
||||||
|
|
||||||
def manual_scrobble_video(
|
def manual_scrobble_video(
|
||||||
video_id: str, user_id: int, source: str = "IMDb", action: Optional[str] = None
|
video_id: str, user_id: int, source: str = "IMDb", action: Optional[str] = None
|
||||||
):
|
):
|
||||||
if "tt" in video_id:
|
video = Video.find_or_create(video_id)
|
||||||
video = Video.get_from_imdb_id(video_id)
|
|
||||||
|
|
||||||
else:
|
|
||||||
video = Video.get_from_youtube_id(video_id)
|
|
||||||
|
|
||||||
# When manually scrobbling, try finding a source from the series
|
# When manually scrobbling, try finding a source from the series
|
||||||
if video.tv_series:
|
if video.tv_series:
|
||||||
|
|||||||
@ -59,8 +59,11 @@ class VideoMetadata:
|
|||||||
|
|
||||||
def as_dict_with_cover_and_genres(self) -> tuple:
|
def as_dict_with_cover_and_genres(self) -> tuple:
|
||||||
video_dict = vars(self)
|
video_dict = vars(self)
|
||||||
|
series_id = ""
|
||||||
cover = None
|
cover = None
|
||||||
if "cover_url" in video_dict.keys():
|
if "cover_url" in video_dict.keys():
|
||||||
cover = video_dict.pop("cover_url", "")
|
cover = video_dict.pop("cover_url", "")
|
||||||
genres = video_dict.pop("genres", [])
|
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
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import re
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import uuid4
|
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_VIDEO_URL = "https://www.youtube.com/watch?v="
|
||||||
YOUTUBE_CHANNEL_URL = "https://www.youtube.com/channel/"
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
BNULL = {"blank": True, "null": True}
|
BNULL = {"blank": True, "null": True}
|
||||||
@ -136,6 +139,13 @@ class Series(TimeStampedModel):
|
|||||||
url = self.cover_image_medium.url
|
url = self.cover_image_medium.url
|
||||||
return 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):
|
def scrobbles_for_user(self, user_id: int, include_playing=False):
|
||||||
from scrobbles.models import Scrobble
|
from scrobbles.models import Scrobble
|
||||||
|
|
||||||
@ -184,6 +194,32 @@ class Series(TimeStampedModel):
|
|||||||
if genres := imdb_dict.get("genres"):
|
if genres := imdb_dict.get("genres"):
|
||||||
self.genre.add(*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):
|
class Video(ScrobblableMixin):
|
||||||
COMPLETION_PERCENT = getattr(settings, "VIDEO_COMPLETION_PERCENT", 90)
|
COMPLETION_PERCENT = getattr(settings, "VIDEO_COMPLETION_PERCENT", 90)
|
||||||
@ -304,7 +340,7 @@ class Video(ScrobblableMixin):
|
|||||||
if not created and not overwrite:
|
if not created and not overwrite:
|
||||||
return video
|
return video
|
||||||
|
|
||||||
vdict, cover, genres = lookup_video_from_youtube(
|
vdict, _, cover, genres = lookup_video_from_youtube(
|
||||||
youtube_id
|
youtube_id
|
||||||
).as_dict_with_cover_and_genres()
|
).as_dict_with_cover_and_genres()
|
||||||
if created or overwrite:
|
if created or overwrite:
|
||||||
@ -320,28 +356,38 @@ class Video(ScrobblableMixin):
|
|||||||
def get_from_imdb_id(
|
def get_from_imdb_id(
|
||||||
cls, imdb_id: str, overwrite: bool = False
|
cls, imdb_id: str, overwrite: bool = False
|
||||||
) -> "Video":
|
) -> "Video":
|
||||||
if "tt" in imdb_id:
|
|
||||||
imdb_id = imdb_id[2:]
|
|
||||||
video, created = cls.objects.get_or_create(imdb_id=imdb_id)
|
video, created = cls.objects.get_or_create(imdb_id=imdb_id)
|
||||||
if not created and not overwrite:
|
if not created and not overwrite:
|
||||||
return video
|
return video
|
||||||
|
|
||||||
vdict, cover, genres = lookup_video_from_tmdb(
|
vdict, series_id, cover, genres = lookup_video_from_imdb(
|
||||||
imdb_id
|
imdb_id
|
||||||
).as_dict_with_cover_and_genres()
|
).as_dict_with_cover_and_genres()
|
||||||
|
|
||||||
if created or overwrite:
|
if created or overwrite:
|
||||||
for k, v in vdict.items():
|
for k, v in vdict.items():
|
||||||
setattr(video, k, v)
|
setattr(video, k, v)
|
||||||
|
|
||||||
|
if series_id:
|
||||||
|
video.tv_series = Series.find_or_create(imdb_id=series_id)
|
||||||
|
|
||||||
video.save()
|
video.save()
|
||||||
|
|
||||||
video.save_image_from_url(cover)
|
if cover:
|
||||||
video.genre.add(*genres)
|
video.save_image_from_url(cover)
|
||||||
|
if genres:
|
||||||
|
video.genre.add(*genres)
|
||||||
|
|
||||||
return video
|
return video
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_or_create(
|
def find_or_create(cls, source_id: str, overwrite: bool = True) -> "Video":
|
||||||
cls, data_dict: dict, post_keys: dict = JELLYFIN_POST_KEYS
|
if "tt" in source_id:
|
||||||
) -> Optional["Video"]:
|
return cls.get_from_imdb_id(source_id, overwrite)
|
||||||
"""Thes smallest of wrappers around our actual get or create utility."""
|
if bool(YOUTUBE_ID_PATTERN.match(source_id)):
|
||||||
imdb_key = post_keys.get("IMDB_ID", "").replace("tt", "")
|
return cls.get_from_youtube_id(source_id, overwrite)
|
||||||
return cls.get_from_imdb_id(data_dict.get(imdb_key))
|
|
||||||
|
#TODO scrobble but without a video obj?
|
||||||
|
logger.warning("Video ID not recognized, not scrobbling")
|
||||||
|
|
||||||
|
return
|
||||||
|
|||||||
@ -6,55 +6,17 @@ from videos.metadata import VideoMetadata, VideoType
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def lookup_video_from_imdb(
|
def lookup_video_from_imdb(imdb_id: str) -> VideoMetadata:
|
||||||
name_or_id: str, kind: str = "movie"
|
if not imdb_id.startswith("tt"):
|
||||||
) -> VideoMetadata:
|
logger.warning("This method requires an IMDB ID starting with 'tt'")
|
||||||
from videos.models import Series
|
return VideoMetadata()
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
video_metadata = VideoMetadata(imdb_id=imdb_id)
|
video_metadata = VideoMetadata(imdb_id=imdb_id)
|
||||||
imdb_result: dict = {}
|
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}")
|
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:
|
if not imdb_result:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[lookup_video_from_imdb] no video found on imdb",
|
f"[lookup_video_from_imdb] no video found on imdb",
|
||||||
@ -62,39 +24,27 @@ def lookup_video_from_imdb(
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
video_metadata.cover_url = imdb_result.get("cover url")
|
video_metadata.imdb_id = imdb_id
|
||||||
if video_metadata.cover_url:
|
video_metadata.title = imdb_result.title
|
||||||
video_metadata.cover_url = helpers.resizeImage(
|
video_metadata.base_run_time_seconds = (
|
||||||
video_metadata.cover_url, width=800
|
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
|
video_metadata.video_type = VideoType.MOVIE.value
|
||||||
series_name = None
|
if imdb_result.type_id == "tvEpisode":
|
||||||
if imdb_result.get("kind") == "episode":
|
video_metadata.video_type = VideoType.TV_EPISODE.value
|
||||||
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.get("runtimes"):
|
series = imdb_result.series
|
||||||
video_metadata.base_run_time_seconds = (
|
video_metadata.tv_series_imdb_id = series.imdb_id
|
||||||
int(imdb_result.get("runtimes")[0]) * 60
|
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
|
return video_metadata
|
||||||
|
|||||||
@ -1,51 +1,24 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from scrobbles.utils import convert_to_seconds
|
from videos.models import Video
|
||||||
from videos.imdb import lookup_video_from_imdb
|
from django.db import IntegrityError
|
||||||
from videos.models import Series, Video
|
#from videos.skatevideosite import lookup_video_from_skatevideosite
|
||||||
from videos.skatevideosite import lookup_video_from_skatevideosite
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_video(data_dict: dict, post_keys: dict, force_update=False):
|
def clean_up_videos():
|
||||||
name_or_id = data_dict.get(post_keys.get("IMDB_ID"), "") or data_dict.get(
|
videos = Video.objects.filter(imdb_id__isnull=False).exclude(imdb_id__icontains="tt")
|
||||||
post_keys.get("VIDEO_TITLE"), ""
|
|
||||||
)
|
|
||||||
|
|
||||||
video = Video.objects.filter(imdb_id=name_or_id).first()
|
for video in videos:
|
||||||
if video:
|
logger.info(f"Fixing imdb_id for {video}")
|
||||||
return 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)
|
videos = Video.objects.filter(scrobble__isnull=True)
|
||||||
# skatevideosite_metadata = lookup_video_from_skatevideosite(name_or_id)
|
videos.delete()
|
||||||
# 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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user