Kaynağa Gözat

[brickset] Fix manual scrobbling and admin

Colin Powell 5 ay önce
ebeveyn
işleme
e980e3c5c9

+ 9 - 3
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:

+ 5 - 0
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",)

+ 5 - 2
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",

+ 9 - 8
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",
 }
 
 

+ 18 - 0
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",
+        ),
+    ]

+ 3 - 3
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:

+ 39 - 9
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)

+ 22 - 4
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

+ 3 - 1
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

+ 7 - 0
vrobbler/templates/scrobbles/_last_scrobbles.html

@@ -72,6 +72,13 @@
         {% endwith %}
         {% endif %}
 
+        {% if BrickSet %}
+        <h4>Brick sets</h4>
+        {% with scrobbles=BrickSet count=BrickSet_count time=BrickSet_time %}
+        {% include "scrobbles/_scrobble_table.html" %}
+        {% endwith %}
+        {% endif %}
+
         {% if Book %}
         <h4>Books</h4>
         {% with scrobbles=Book count=Book_count time=Book_time %}