diff --git a/vrobbler/apps/bricksets/models.py b/vrobbler/apps/bricksets/models.py index 338b953..29f9a6f 100644 --- a/vrobbler/apps/bricksets/models.py +++ b/vrobbler/apps/bricksets/models.py @@ -16,7 +16,9 @@ class BrickSet(LongPlayScrobblableMixin): number = models.CharField(max_length=10, **BNULL) release_year = models.IntegerField(**BNULL) piece_count = models.IntegerField(**BNULL) - brickset_rating = models.DecimalField(max_digits=3, decimal_places=1, **BNULL) + brickset_rating = models.DecimalField( + max_digits=3, decimal_places=1, **BNULL + ) lego_item_number = models.CharField(max_length=10, **BNULL) box_image = models.ImageField(upload_to="brickset/boxes/", **BNULL) box_image_small = ImageSpecField( @@ -53,8 +55,12 @@ class BrickSet(LongPlayScrobblableMixin): return BrickSetLogData @classmethod - def find_or_create(cls, title: str) -> "BrickSet": - return cls.objects.filter(title=title).first() + def find_or_create(cls, brickset_id: str) -> "BrickSet": + brickset = cls.objects.filter(number=brickset_id).first() + if not brickset: + # TODO: enrich this from the website + brickset = cls.objects.create(number=brickset_id) + return brickset @property def primary_image_url(self) -> str: diff --git a/vrobbler/apps/music/admin.py b/vrobbler/apps/music/admin.py index a3ebead..9174287 100644 --- a/vrobbler/apps/music/admin.py +++ b/vrobbler/apps/music/admin.py @@ -16,6 +16,7 @@ class AlbumAdmin(admin.ModelAdmin): "theaudiodb_mood", "musicbrainz_id", ) + raw_id_fields = ("album_artist",) list_filter = ( "theaudiodb_score", "theaudiodb_genre", @@ -53,6 +54,10 @@ class TrackAdmin(admin.ModelAdmin): "artist", "musicbrainz_id", ) + raw_id_fields = ( + "album", + "artist", + ) list_filter = ("album", "artist") search_fields = ("title",) ordering = ("-created",) diff --git a/vrobbler/apps/scrobbles/admin.py b/vrobbler/apps/scrobbles/admin.py index 57bb459..75a4197 100644 --- a/vrobbler/apps/scrobbles/admin.py +++ b/vrobbler/apps/scrobbles/admin.py @@ -27,7 +27,7 @@ class ScrobbleInline(admin.TabularInline): "geo_location", "task", "mood", - "brickset", + "brick_set", "trail", "beer", "web_page", @@ -122,9 +122,12 @@ class ScrobbleAdmin(admin.ModelAdmin): "video_game", "board_game", "geo_location", + "puzzle", + "paper", + "food", "task", "mood", - "brickset", + "brick_set", "trail", "beer", "web_page", diff --git a/vrobbler/apps/scrobbles/constants.py b/vrobbler/apps/scrobbles/constants.py index e978917..d176879 100644 --- a/vrobbler/apps/scrobbles/constants.py +++ b/vrobbler/apps/scrobbles/constants.py @@ -27,14 +27,14 @@ MEDIA_END_PADDING_SECONDS = { TODOIST_TASK_URL = "https://app.todoist.com/app/task/{id}" SCROBBLE_CONTENT_URLS = { - "-i": "https://www.imdb.com/title/", - "-s": "https://www.thesportsdb.com/event/", - "-g": "https://boardgamegeek.com/boardgame/", - "-u": "https://untappd.com/", - "-b": "https://www.amazon.com/", - "-t": "https://app.todoist.com/app/task/{id}", - "-i": "https://www.youtube.com/watch?v=", - "-p": "https://www.ipdb.plus/IPDb/puzzle.php?id=", + "-i": ["https://www.imdb.com/title/", "https://www.youtube.com/watch?v="], + "-s": ["https://www.thesportsdb.com/event/"], + "-g": ["https://boardgamegeek.com/boardgame/"], + "-u": ["https://untappd.com/"], + "-b": ["https://www.amazon.com/"], + "-t": ["https://app.todoist.com/app/task/{id}"], + "-p": ["https://www.ipdb.plus/IPDb/puzzle.php?id="], + "-l": ["https://brickset.com/sets/"], } EXCLUDE_FROM_NOW_PLAYING = ("GeoLocation",) @@ -49,6 +49,7 @@ MANUAL_SCROBBLE_FNS = { "-w": "manual_scrobble_webpage", "-t": "manual_scrobble_task", "-p": "manual_scrobble_puzzle", + "-l": "manual_scrobble_brickset", } diff --git a/vrobbler/apps/scrobbles/migrations/0070_rename_brickset_scrobble_brick_set.py b/vrobbler/apps/scrobbles/migrations/0070_rename_brickset_scrobble_brick_set.py new file mode 100644 index 0000000..7585d34 --- /dev/null +++ b/vrobbler/apps/scrobbles/migrations/0070_rename_brickset_scrobble_brick_set.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.19 on 2025-06-09 14:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("scrobbles", "0069_scrobble_puzzle_alter_scrobble_media_type"), + ] + + operations = [ + migrations.RenameField( + model_name="scrobble", + old_name="brickset", + new_name="brick_set", + ), + ] diff --git a/vrobbler/apps/scrobbles/models.py b/vrobbler/apps/scrobbles/models.py index 13bb19f..6b6d936 100644 --- a/vrobbler/apps/scrobbles/models.py +++ b/vrobbler/apps/scrobbles/models.py @@ -549,7 +549,7 @@ class Scrobble(TimeStampedModel): LifeEvent, on_delete=models.DO_NOTHING, **BNULL ) mood = models.ForeignKey(Mood, on_delete=models.DO_NOTHING, **BNULL) - brickset = models.ForeignKey( + brick_set = models.ForeignKey( BrickSet, on_delete=models.DO_NOTHING, **BNULL ) media_type = models.CharField( @@ -1019,8 +1019,8 @@ class Scrobble(TimeStampedModel): media_obj = self.life_event if self.mood: media_obj = self.mood - if self.brickset: - media_obj = self.brickset + if self.brick_set: + media_obj = self.brick_set if self.trail: media_obj = self.trail if self.beer: diff --git a/vrobbler/apps/scrobbles/scrobblers.py b/vrobbler/apps/scrobbles/scrobblers.py index fd6def5..bd1ffb2 100644 --- a/vrobbler/apps/scrobbles/scrobblers.py +++ b/vrobbler/apps/scrobbles/scrobblers.py @@ -8,6 +8,7 @@ import pytz from beers.models import Beer from boardgames.models import BoardGame from books.models import Book +from bricksets.models import BrickSet from dateutil.parser import parse from django.utils import timezone from locations.constants import LOCATION_PROVIDERS @@ -23,7 +24,7 @@ from scrobbles.constants import ( SCROBBLE_CONTENT_URLS, ) from scrobbles.models import Scrobble -from scrobbles.utils import convert_to_seconds +from scrobbles.utils import convert_to_seconds, extract_domain from sports.models import SportEvent from sports.thesportsdb import lookup_event_from_thesportsdb from tasks.models import Task @@ -322,14 +323,12 @@ def manual_scrobble_from_url( we know about the content type, and routes it to the appropriate media scrobbler. Otherwise, return nothing.""" content_key = "" - try: - domain = url.split("//")[-1].split("/")[0] - except IndexError: - domain = None + domain = extract_domain(url) - for key, content_url in SCROBBLE_CONTENT_URLS.items(): - if domain in content_url: - content_key = key + for key, content_urls in SCROBBLE_CONTENT_URLS.items(): + for content_url in content_urls: + if domain in content_url: + content_key = key item_id = None if not content_key: @@ -343,8 +342,10 @@ def manual_scrobble_from_url( except IndexError: pass - if content_key == "-i": + if content_key == "-i" and "v=" in url: item_id = url.split("v=")[1].split("&")[0] + elif content_key == "-i" and "title/tt" in url: + item_id = "tt" + str(item_id) scrobble_fn = MANUAL_SCROBBLE_FNS[content_key] return eval(scrobble_fn)(item_id, user_id, action=action) @@ -853,3 +854,32 @@ def manual_scrobble_puzzle( # TODO Kick out a process to enrich the media here, and in every scrobble event return Scrobble.create_or_update(puzzle, user_id, scrobble_dict) + + +def manual_scrobble_brickset( + brickset_id: str, user_id: int, action: Optional[str] = None +): + brickset = BrickSet.find_or_create(brickset_id) + + if not brickset: + logger.error(f"No brickset found for Brickset ID {brickset_id}") + return + + scrobble_dict = { + "user_id": user_id, + "timestamp": timezone.now(), + "playback_position_seconds": 0, + "source": "Vrobbler", + } + logger.info( + "[vrobbler-scrobble] brickset scrobble request received", + extra={ + "brickset_id": brickset.id, + "user_id": user_id, + "scrobble_dict": scrobble_dict, + "media_type": Scrobble.MediaType.BRICKSET, + }, + ) + + # TODO Kick out a process to enrich the media here, and in every scrobble event + return Scrobble.create_or_update(brickset, user_id, scrobble_dict) diff --git a/vrobbler/apps/scrobbles/utils.py b/vrobbler/apps/scrobbles/utils.py index f86fdc4..bf17b36 100644 --- a/vrobbler/apps/scrobbles/utils.py +++ b/vrobbler/apps/scrobbles/utils.py @@ -3,6 +3,7 @@ import logging import re from datetime import datetime, timedelta, tzinfo from sqlite3 import IntegrityError +from urllib.parse import urlparse import pytz from django.apps import apps @@ -17,9 +18,10 @@ from scrobbles.tasks import ( process_lastfm_import, process_retroarch_import, ) -from vrobbler.apps.scrobbles.notifications import NtfyNotification from webdav.client import get_webdav_client +from vrobbler.apps.scrobbles.notifications import NtfyNotification + logger = logging.getLogger(__name__) User = get_user_model() @@ -67,7 +69,8 @@ def get_scrobbles_for_media(media_obj, user: User) -> models.QuerySet: return Scrobble.objects.filter(media_query, user=user) -def get_recently_played_board_games(user: User) -> dict: ... +def get_recently_played_board_games(user: User) -> dict: + ... def get_long_plays_in_progress(user: User) -> dict: @@ -187,8 +190,8 @@ def delete_zombie_scrobbles(dry_run=True): def import_from_webdav_for_all_users(restart=False): """Grab a list of all users with WebDAV enabled and kickoff imports for them""" - from scrobbles.models import KoReaderImport from books.koreader import fetch_file_from_webdav + from scrobbles.models import KoReaderImport # LastFmImport = apps.get_model("scrobbles", "LastFMImport") webdav_enabled_user_ids = UserProfile.objects.filter( @@ -271,6 +274,7 @@ def get_file_md5_hash(file_path: str) -> str: file_hash.update(chunk) return file_hash.hexdigest() + def deduplicate_tracks(commit=False) -> int: from music.models import Track @@ -293,7 +297,11 @@ def deduplicate_tracks(commit=False) -> int: try: other.delete() except IntegrityError as e: - print("could not delete ", other.id, f": IntegrityError {e}") + print( + "could not delete ", + other.id, + f": IntegrityError {e}", + ) return len(dups) @@ -318,3 +326,13 @@ def send_stop_notifications_for_in_progress_scrobbles() -> int: notifications_sent += 1 return notifications_sent + + +def extract_domain(url): + parsed_url = urlparse(url) + domain = ( + parsed_url.netloc.split(".")[-2] + + "." + + parsed_url.netloc.split(".")[-1] + ) + return domain diff --git a/vrobbler/apps/videos/metadata.py b/vrobbler/apps/videos/metadata.py index 3bdc02d..80c3ca0 100644 --- a/vrobbler/apps/videos/metadata.py +++ b/vrobbler/apps/videos/metadata.py @@ -57,6 +57,8 @@ class VideoMetadata: def as_dict_with_cover_and_genres(self) -> tuple: video_dict = vars(self) - cover = video_dict.pop("cover_url") + 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 diff --git a/vrobbler/templates/scrobbles/_last_scrobbles.html b/vrobbler/templates/scrobbles/_last_scrobbles.html index 3eaaca4..13fc631 100644 --- a/vrobbler/templates/scrobbles/_last_scrobbles.html +++ b/vrobbler/templates/scrobbles/_last_scrobbles.html @@ -72,6 +72,13 @@ {% endwith %} {% endif %} + {% if BrickSet %} +