瀏覽代碼

[brickset] Fix manual scrobbling and admin

Colin Powell 5 月之前
父節點
當前提交
e980e3c5c9

+ 9 - 3
vrobbler/apps/bricksets/models.py

@@ -16,7 +16,9 @@ class BrickSet(LongPlayScrobblableMixin):
     number = models.CharField(max_length=10, **BNULL)
     number = models.CharField(max_length=10, **BNULL)
     release_year = models.IntegerField(**BNULL)
     release_year = models.IntegerField(**BNULL)
     piece_count = 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)
     lego_item_number = models.CharField(max_length=10, **BNULL)
     box_image = models.ImageField(upload_to="brickset/boxes/", **BNULL)
     box_image = models.ImageField(upload_to="brickset/boxes/", **BNULL)
     box_image_small = ImageSpecField(
     box_image_small = ImageSpecField(
@@ -53,8 +55,12 @@ class BrickSet(LongPlayScrobblableMixin):
         return BrickSetLogData
         return BrickSetLogData
 
 
     @classmethod
     @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
     @property
     def primary_image_url(self) -> str:
     def primary_image_url(self) -> str:

+ 5 - 0
vrobbler/apps/music/admin.py

@@ -16,6 +16,7 @@ class AlbumAdmin(admin.ModelAdmin):
         "theaudiodb_mood",
         "theaudiodb_mood",
         "musicbrainz_id",
         "musicbrainz_id",
     )
     )
+    raw_id_fields = ("album_artist",)
     list_filter = (
     list_filter = (
         "theaudiodb_score",
         "theaudiodb_score",
         "theaudiodb_genre",
         "theaudiodb_genre",
@@ -53,6 +54,10 @@ class TrackAdmin(admin.ModelAdmin):
         "artist",
         "artist",
         "musicbrainz_id",
         "musicbrainz_id",
     )
     )
+    raw_id_fields = (
+        "album",
+        "artist",
+    )
     list_filter = ("album", "artist")
     list_filter = ("album", "artist")
     search_fields = ("title",)
     search_fields = ("title",)
     ordering = ("-created",)
     ordering = ("-created",)

+ 5 - 2
vrobbler/apps/scrobbles/admin.py

@@ -27,7 +27,7 @@ class ScrobbleInline(admin.TabularInline):
         "geo_location",
         "geo_location",
         "task",
         "task",
         "mood",
         "mood",
-        "brickset",
+        "brick_set",
         "trail",
         "trail",
         "beer",
         "beer",
         "web_page",
         "web_page",
@@ -122,9 +122,12 @@ class ScrobbleAdmin(admin.ModelAdmin):
         "video_game",
         "video_game",
         "board_game",
         "board_game",
         "geo_location",
         "geo_location",
+        "puzzle",
+        "paper",
+        "food",
         "task",
         "task",
         "mood",
         "mood",
-        "brickset",
+        "brick_set",
         "trail",
         "trail",
         "beer",
         "beer",
         "web_page",
         "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}"
 TODOIST_TASK_URL = "https://app.todoist.com/app/task/{id}"
 
 
 SCROBBLE_CONTENT_URLS = {
 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",)
 EXCLUDE_FROM_NOW_PLAYING = ("GeoLocation",)
@@ -49,6 +49,7 @@ MANUAL_SCROBBLE_FNS = {
     "-w": "manual_scrobble_webpage",
     "-w": "manual_scrobble_webpage",
     "-t": "manual_scrobble_task",
     "-t": "manual_scrobble_task",
     "-p": "manual_scrobble_puzzle",
     "-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
         LifeEvent, on_delete=models.DO_NOTHING, **BNULL
     )
     )
     mood = models.ForeignKey(Mood, 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
         BrickSet, on_delete=models.DO_NOTHING, **BNULL
     )
     )
     media_type = models.CharField(
     media_type = models.CharField(
@@ -1019,8 +1019,8 @@ class Scrobble(TimeStampedModel):
             media_obj = self.life_event
             media_obj = self.life_event
         if self.mood:
         if self.mood:
             media_obj = 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:
         if self.trail:
             media_obj = self.trail
             media_obj = self.trail
         if self.beer:
         if self.beer:

+ 39 - 9
vrobbler/apps/scrobbles/scrobblers.py

@@ -8,6 +8,7 @@ import pytz
 from beers.models import Beer
 from beers.models import Beer
 from boardgames.models import BoardGame
 from boardgames.models import BoardGame
 from books.models import Book
 from books.models import Book
+from bricksets.models import BrickSet
 from dateutil.parser import parse
 from dateutil.parser import parse
 from django.utils import timezone
 from django.utils import timezone
 from locations.constants import LOCATION_PROVIDERS
 from locations.constants import LOCATION_PROVIDERS
@@ -23,7 +24,7 @@ from scrobbles.constants import (
     SCROBBLE_CONTENT_URLS,
     SCROBBLE_CONTENT_URLS,
 )
 )
 from scrobbles.models import Scrobble
 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.models import SportEvent
 from sports.thesportsdb import lookup_event_from_thesportsdb
 from sports.thesportsdb import lookup_event_from_thesportsdb
 from tasks.models import Task
 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
     we know about the content type, and routes it to the appropriate media
     scrobbler. Otherwise, return nothing."""
     scrobbler. Otherwise, return nothing."""
     content_key = ""
     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
     item_id = None
     if not content_key:
     if not content_key:
@@ -343,8 +342,10 @@ def manual_scrobble_from_url(
         except IndexError:
         except IndexError:
             pass
             pass
 
 
-    if content_key == "-i":
+    if content_key == "-i" and "v=" in url:
         item_id = url.split("v=")[1].split("&")[0]
         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]
     scrobble_fn = MANUAL_SCROBBLE_FNS[content_key]
     return eval(scrobble_fn)(item_id, user_id, action=action)
     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
     # 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)
     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
 import re
 from datetime import datetime, timedelta, tzinfo
 from datetime import datetime, timedelta, tzinfo
 from sqlite3 import IntegrityError
 from sqlite3 import IntegrityError
+from urllib.parse import urlparse
 
 
 import pytz
 import pytz
 from django.apps import apps
 from django.apps import apps
@@ -17,9 +18,10 @@ from scrobbles.tasks import (
     process_lastfm_import,
     process_lastfm_import,
     process_retroarch_import,
     process_retroarch_import,
 )
 )
-from vrobbler.apps.scrobbles.notifications import NtfyNotification
 from webdav.client import get_webdav_client
 from webdav.client import get_webdav_client
 
 
+from vrobbler.apps.scrobbles.notifications import NtfyNotification
+
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 User = get_user_model()
 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)
     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:
 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):
 def import_from_webdav_for_all_users(restart=False):
     """Grab a list of all users with WebDAV enabled and kickoff imports for them"""
     """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 books.koreader import fetch_file_from_webdav
+    from scrobbles.models import KoReaderImport
 
 
     # LastFmImport = apps.get_model("scrobbles", "LastFMImport")
     # LastFmImport = apps.get_model("scrobbles", "LastFMImport")
     webdav_enabled_user_ids = UserProfile.objects.filter(
     webdav_enabled_user_ids = UserProfile.objects.filter(
@@ -271,6 +274,7 @@ def get_file_md5_hash(file_path: str) -> str:
             file_hash.update(chunk)
             file_hash.update(chunk)
     return file_hash.hexdigest()
     return file_hash.hexdigest()
 
 
+
 def deduplicate_tracks(commit=False) -> int:
 def deduplicate_tracks(commit=False) -> int:
     from music.models import Track
     from music.models import Track
 
 
@@ -293,7 +297,11 @@ def deduplicate_tracks(commit=False) -> int:
                     try:
                     try:
                         other.delete()
                         other.delete()
                     except IntegrityError as e:
                     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)
     return len(dups)
 
 
 
 
@@ -318,3 +326,13 @@ def send_stop_notifications_for_in_progress_scrobbles() -> int:
             notifications_sent += 1
             notifications_sent += 1
 
 
     return notifications_sent
     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:
     def as_dict_with_cover_and_genres(self) -> tuple:
         video_dict = vars(self)
         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")
         genres = video_dict.pop("genres")
         return video_dict, cover, genres
         return video_dict, cover, genres

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

@@ -72,6 +72,13 @@
         {% endwith %}
         {% endwith %}
         {% endif %}
         {% 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 %}
         {% if Book %}
         <h4>Books</h4>
         <h4>Books</h4>
         {% with scrobbles=Book count=Book_count time=Book_time %}
         {% with scrobbles=Book count=Book_count time=Book_time %}