[brickset] Fix manual scrobbling and admin
This commit is contained in:
@ -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":
|
def find_or_create(cls, brickset_id: str) -> "BrickSet":
|
||||||
return cls.objects.filter(title=title).first()
|
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:
|
||||||
|
|||||||
@ -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",)
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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/",
|
"-i": ["https://www.imdb.com/title/", "https://www.youtube.com/watch?v="],
|
||||||
"-s": "https://www.thesportsdb.com/event/",
|
"-s": ["https://www.thesportsdb.com/event/"],
|
||||||
"-g": "https://boardgamegeek.com/boardgame/",
|
"-g": ["https://boardgamegeek.com/boardgame/"],
|
||||||
"-u": "https://untappd.com/",
|
"-u": ["https://untappd.com/"],
|
||||||
"-b": "https://www.amazon.com/",
|
"-b": ["https://www.amazon.com/"],
|
||||||
"-t": "https://app.todoist.com/app/task/{id}",
|
"-t": ["https://app.todoist.com/app/task/{id}"],
|
||||||
"-i": "https://www.youtube.com/watch?v=",
|
"-p": ["https://www.ipdb.plus/IPDb/puzzle.php?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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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:
|
if self.brick_set:
|
||||||
media_obj = self.brickset
|
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:
|
||||||
|
|||||||
@ -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 = extract_domain(url)
|
||||||
domain = url.split("//")[-1].split("/")[0]
|
|
||||||
except IndexError:
|
|
||||||
domain = None
|
|
||||||
|
|
||||||
for key, content_url in SCROBBLE_CONTENT_URLS.items():
|
for key, content_urls in SCROBBLE_CONTENT_URLS.items():
|
||||||
if domain in content_url:
|
for content_url in content_urls:
|
||||||
content_key = key
|
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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user