[templates] Clean up how str and subtitles work

This commit is contained in:
2026-06-07 11:06:30 -04:00
parent 4ce3dc03c5
commit c7339fbe31
11 changed files with 72 additions and 61 deletions

View File

@ -86,7 +86,7 @@ fetching and simple saving.
**** Bookmarklet **** Bookmarklet
*** Metadata sources *** Metadata sources
**** Scraper **** Scraper
* Backlog [1/13] :vrobbler:project:personal: * Backlog [2/14] :vrobbler:project:personal:
** 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:
:PROPERTIES: :PROPERTIES:
:ID: 702462cf-d54b-48c6-8a7c-78b8de751deb :ID: 702462cf-d54b-48c6-8a7c-78b8de751deb
@ -498,6 +498,19 @@ needed import celery task. This is how the WebDAV celery task currently works.
This would also be an opporunity to clean up the code around WebDAV imports This would also be an opporunity to clean up the code around WebDAV imports
and make them more re-usable for other import services. and make them more re-usable for other import services.
** DONE [#B] Show team or player images on sport detail and scrobble detail :sports:templates:
:PROPERTIES:
:ID: 68c17383-ee6e-4b5f-b3f5-1b637a0a3ea8
:END:
*** Description
On the sport event detail page, we should show the images of the teams or
players invovled.
Also, those images for the sport event should be shown on the scrobble detail
page for sport event scrobble details.
** DONE [#B] Add fix_metadta method to Video instances :videos:metadata: ** DONE [#B] Add fix_metadta method to Video instances :videos:metadata:
:PROPERTIES: :PROPERTIES:
:ID: 9df5404d-1b60-4eee-b7cf-1f7e6dfade65 :ID: 9df5404d-1b60-4eee-b7cf-1f7e6dfade65

View File

@ -61,9 +61,7 @@ class BirdSightingLogData(BaseLogData, WithPeopleLogData):
@cached_property @cached_property
def bird_list(self) -> str: def bird_list(self) -> str:
if self.birds: if self.birds:
return ", ".join( return ", ".join([BirdSightingEntry(**b).__str__() for b in self.birds])
[BirdSightingEntry(**b).__str__() for b in self.birds]
)
return "" return ""
def as_html(self) -> str: def as_html(self) -> str:
@ -80,9 +78,7 @@ class BirdSightingLogData(BaseLogData, WithPeopleLogData):
) )
if self.area: if self.area:
html_parts.append( html_parts.append(f'<div class="birding-area">Area: {self.area}</div>')
f'<div class="birding-area">Area: {self.area}</div>'
)
if self.party_size: if self.party_size:
html_parts.append( html_parts.append(
@ -105,9 +101,7 @@ class BirdSightingLogData(BaseLogData, WithPeopleLogData):
) )
if self.guide: if self.guide:
html_parts.append( html_parts.append(f'<div class="birding-guide">Guide: {self.guide}</div>')
f'<div class="birding-guide">Guide: {self.guide}</div>'
)
if self.duration_minutes: if self.duration_minutes:
html_parts.append( html_parts.append(
@ -183,9 +177,7 @@ class Bird(TimeStampedModel):
class BirdingLocation(ScrobblableMixin): class BirdingLocation(ScrobblableMixin):
description = models.TextField(**BNULL) description = models.TextField(**BNULL)
geo_location = models.ForeignKey( geo_location = models.ForeignKey(GeoLocation, **BNULL, on_delete=models.DO_NOTHING)
GeoLocation, **BNULL, on_delete=models.DO_NOTHING
)
ebird_hotspot_id = models.CharField(max_length=255, **BNULL) ebird_hotspot_id = models.CharField(max_length=255, **BNULL)
def get_absolute_url(self): def get_absolute_url(self):
@ -193,7 +185,7 @@ class BirdingLocation(ScrobblableMixin):
@property @property
def subtitle(self): def subtitle(self):
return "" return self.geo_location
@property @property
def strings(self) -> ScrobblableConstants: def strings(self) -> ScrobblableConstants:
@ -269,9 +261,7 @@ class BirdingCSVImport(TimeStampedModel):
self.save(update_fields=["process_log", "process_count"]) self.save(update_fields=["process_log", "process_count"])
return return
for count, scrobble in enumerate(scrobbles): for count, scrobble in enumerate(scrobbles):
scrobble_str = ( scrobble_str = f"{scrobble.id}\t{scrobble.timestamp}\t{scrobble.media_obj}"
f"{scrobble.id}\t{scrobble.timestamp}\t{scrobble.media_obj}"
)
log_line = f"{scrobble_str}" log_line = f"{scrobble_str}"
if count > 0: if count > 0:
log_line = "\n" + log_line log_line = "\n" + log_line

View File

@ -266,8 +266,9 @@ class BoardGame(ScrobblableMixin):
"self", **BNULL, on_delete=models.DO_NOTHING "self", **BNULL, on_delete=models.DO_NOTHING
) )
def __str__(self): @property
return self.title def subtitle(self) -> str:
return self.publisher
def get_absolute_url(self): def get_absolute_url(self):
return reverse("boardgames:boardgame_detail", kwargs={"slug": self.uuid}) return reverse("boardgames:boardgame_detail", kwargs={"slug": self.uuid})

View File

@ -173,11 +173,7 @@ class Book(LongPlayScrobblableMixin):
genre = TaggableManager(through=ObjectWithGenres, blank=True, verbose_name="Genre") genre = TaggableManager(through=ObjectWithGenres, blank=True, verbose_name="Genre")
def __str__(self) -> str: def __str__(self) -> str:
if self.issue_number and "Issue" not in str(self.title): return f"{self.title} - {self.subtitle}"
return f"{self.title} - Issue {self.issue_number}"
if self.volume_number and "Volume" not in str(self.title):
return f"{self.title} - Volume {self.volume_number}"
return f"{self.title}"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.pages: if self.pages:
@ -188,7 +184,12 @@ class Book(LongPlayScrobblableMixin):
@property @property
def subtitle(self): def subtitle(self):
return f" by {self.author}" subtitle = self.author
if self.issue_number and "Issue" not in str(self.title):
subtitle += " - Issue {self.issue_number}"
if self.volume_number and "Volume" not in str(self.title):
subtitle += " - Volume {self.volume_number}"
return subtitle
@property @property
def strings(self) -> ScrobblableConstants: def strings(self) -> ScrobblableConstants:

View File

@ -20,6 +20,7 @@ class ScrobbleInline(admin.TabularInline):
extra = 0 extra = 0
raw_id_fields = ( raw_id_fields = (
"video", "video",
"channel",
"podcast_episode", "podcast_episode",
"track", "track",
"video_game", "video_game",
@ -30,6 +31,7 @@ class ScrobbleInline(admin.TabularInline):
"board_game", "board_game",
"geo_location", "geo_location",
"task", "task",
"puzzle",
"mood", "mood",
"brick_set", "brick_set",
"trail", "trail",
@ -59,47 +61,38 @@ class ImportBaseAdmin(admin.ModelAdmin):
@admin.register(AudioScrobblerTSVImport) @admin.register(AudioScrobblerTSVImport)
class AudioScrobblerTSVImportAdmin(ImportBaseAdmin): class AudioScrobblerTSVImportAdmin(ImportBaseAdmin): ...
...
@admin.register(LastFmImport) @admin.register(LastFmImport)
class LastFmImportAdmin(ImportBaseAdmin): class LastFmImportAdmin(ImportBaseAdmin): ...
...
@admin.register(KoReaderImport) @admin.register(KoReaderImport)
class KoReaderImportAdmin(ImportBaseAdmin): class KoReaderImportAdmin(ImportBaseAdmin): ...
...
@admin.register(RetroarchImport) @admin.register(RetroarchImport)
class RetroarchImportAdmin(ImportBaseAdmin): class RetroarchImportAdmin(ImportBaseAdmin): ...
...
class RetroarchImportAdmin(ImportBaseAdmin): class RetroarchImportAdmin(ImportBaseAdmin): ...
...
@admin.register(BGStatsImport) @admin.register(BGStatsImport)
class BGStatsImportAdmin(ImportBaseAdmin): class BGStatsImportAdmin(ImportBaseAdmin): ...
...
@admin.register(EBirdCSVImport) @admin.register(EBirdCSVImport)
class EBirdCSVImportAdmin(ImportBaseAdmin): class EBirdCSVImportAdmin(ImportBaseAdmin): ...
...
@admin.register(ScaleCSVImport) @admin.register(ScaleCSVImport)
class ScaleCSVImportAdmin(ImportBaseAdmin): class ScaleCSVImportAdmin(ImportBaseAdmin): ...
...
@admin.register(TrailGPXImport) @admin.register(TrailGPXImport)
class TrailGPXImportAdmin(ImportBaseAdmin): class TrailGPXImportAdmin(ImportBaseAdmin): ...
...
@admin.register(Genre) @admin.register(Genre)

View File

@ -65,6 +65,9 @@ class ScrobblableMixin(TimeStampedModel):
class Meta: class Meta:
abstract = True abstract = True
def __str__(self) -> str:
return f"{self.title} - {self.subtitle}"
@property @property
def run_time_seconds(self) -> int: def run_time_seconds(self) -> int:
run_time = 900 run_time = 900

View File

@ -184,7 +184,7 @@ class ScrobbleableDetailView(ChartContextMixin, DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs) context_data = super().get_context_data(**kwargs)
scrobbles = [] scrobbles = []
if not self.request.user.is_anonymous: if not self.request.user.is_anonymous and hasattr(self.object, "scrobble_set"):
scrobbles = self.object.scrobble_set.filter( scrobbles = self.object.scrobble_set.filter(
user=self.request.user user=self.request.user
).order_by("-timestamp") ).order_by("-timestamp")
@ -439,8 +439,7 @@ class ScrobbleListView(LoginRequiredMixin, ListView):
full_qs = getattr(self, "_full_queryset", None) full_qs = getattr(self, "_full_queryset", None)
if full_qs is not None and getattr(self, "tag_list", []): if full_qs is not None and getattr(self, "tag_list", []):
total = ( total = (
full_qs.aggregate(total=Sum("playback_position_seconds"))["total"] full_qs.aggregate(total=Sum("playback_position_seconds"))["total"] or 0
or 0
) )
ctx["total_time_seconds"] = total ctx["total_time_seconds"] = total
return ctx return ctx
@ -1245,8 +1244,7 @@ class ScrobbleDetailView(DetailView):
track=media_obj, user=self.object.user track=media_obj, user=self.object.user
).order_by("-timestamp")[:20] ).order_by("-timestamp")[:20]
context["has_mopidy_uri"] = any( context["has_mopidy_uri"] = any(
(s.log or {}).get("raw_data", {}).get("mopidy_uri") (s.log or {}).get("raw_data", {}).get("mopidy_uri") for s in scrobbles
for s in scrobbles
) )
else: else:
context["has_mopidy_uri"] = False context["has_mopidy_uri"] = False

View File

@ -123,17 +123,17 @@ class SportEvent(ScrobblableMixin):
super(SportEvent, self).save(*args, **kwargs) super(SportEvent, self).save(*args, **kwargs)
def __str__(self) -> str: def __str__(self) -> str:
league = self.league return f"{self.title} - {self.subtitle}"
if self.league and self.league.abbreviation_str:
league = self.league.abbreviation_str
return f"{self.title} - {league}"
def get_absolute_url(self): def get_absolute_url(self):
return reverse("sports:event_detail", kwargs={"slug": self.uuid}) return reverse("sports:event_detail", kwargs={"slug": self.uuid})
@property @property
def subtitle(self) -> str: def subtitle(self) -> str:
return self.comp_str league = self.league
if self.league and self.league.abbreviation_str:
league = self.league.abbreviation_str
return f"{league} {self.get_event_type_display()}"
@property @property
def comp_str(self) -> str: def comp_str(self) -> str:

View File

@ -163,12 +163,9 @@ class VideoGame(LongPlayScrobblableMixin):
platforms = models.ManyToManyField(VideoGamePlatform) platforms = models.ManyToManyField(VideoGamePlatform)
retroarch_name = models.CharField(max_length=255, **BNULL) retroarch_name = models.CharField(max_length=255, **BNULL)
def __str__(self):
return self.title
@property @property
def subtitle(self): def subtitle(self):
return f" On {self.platforms.first()}" return f"{self.platforms.first()}"
@property @property
def strings(self) -> ScrobblableConstants: def strings(self) -> ScrobblableConstants:

View File

@ -50,7 +50,11 @@
<h1 class="d-flex align-items-center gap-2"> <h1 class="d-flex align-items-center gap-2">
{% if object.media_type == "Video" %}🎬{% elif object.media_type == "Track" %}🎵{% elif object.media_type == "PodcastEpisode" %}🎙️{% elif object.media_type == "SportEvent" %}⚽{% elif object.media_type == "Book" %}📚{% elif object.media_type == "Paper" %}📄{% elif object.media_type == "VideoGame" %}🎮{% elif object.media_type == "BoardGame" %}🎲{% elif object.media_type == "GeoLocation" %}📍{% elif object.media_type == "Trail" %}🥾{% elif object.media_type == "Beer" %}🍺{% elif object.media_type == "Puzzle" %}🧩{% elif object.media_type == "Food" %}🍔{% elif object.media_type == "Task" %}✅{% elif object.media_type == "WebPage" %}🌐{% elif object.media_type == "LifeEvent" %}🎉{% elif object.media_type == "Mood" %}😊{% elif object.media_type == "BrickSet" %}🧱{% elif object.media_type == "Channel" %}📺{% endif %} {% if object.media_type == "Video" %}🎬{% elif object.media_type == "Track" %}🎵{% elif object.media_type == "PodcastEpisode" %}🎙️{% elif object.media_type == "SportEvent" %}⚽{% elif object.media_type == "Book" %}📚{% elif object.media_type == "Paper" %}📄{% elif object.media_type == "VideoGame" %}🎮{% elif object.media_type == "BoardGame" %}🎲{% elif object.media_type == "GeoLocation" %}📍{% elif object.media_type == "Trail" %}🥾{% elif object.media_type == "Beer" %}🍺{% elif object.media_type == "Puzzle" %}🧩{% elif object.media_type == "Food" %}🍔{% elif object.media_type == "Task" %}✅{% elif object.media_type == "WebPage" %}🌐{% elif object.media_type == "LifeEvent" %}🎉{% elif object.media_type == "Mood" %}😊{% elif object.media_type == "BrickSet" %}🧱{% elif object.media_type == "Channel" %}📺{% endif %}
{% if object.media_obj.get_absolute_url %}<a href="{{ object.media_obj.get_absolute_url }}">{% endif %}{{ object.media_obj }}{% if object.media_obj.get_absolute_url %}</a>{% endif %} {% if object.media_obj.get_absolute_url %}
<a href="{{ object.media_obj.get_absolute_url }}">{% endif %}
{{ object.media_obj.title }}
{% if object.media_obj.get_absolute_url %}</a>
{% endif %}
{% if user.is_authenticated and object.media_obj %} {% if user.is_authenticated and object.media_obj %}
<button id="favorite-btn" <button id="favorite-btn"
data-url="{% url 'scrobbles:toggle-favorite' object.media_type object.media_obj.id %}" data-url="{% url 'scrobbles:toggle-favorite' object.media_type object.media_obj.id %}"
@ -62,7 +66,15 @@
</button> </button>
{% endif %} {% endif %}
</h1> </h1>
<h2>{{ object.media_obj.subtitle }}</h2>
<p>
{% if object.media_type == "SportEvent" %}
{% for team in object.media_obj.teams.all %}
<img src="{{team.logo.url}}" width=150 />
{% endfor %}
{% endif %}
{% if object.media_type == "Task" and object.logdata.title %} {% if object.media_type == "Task" and object.logdata.title %}
</p>
<h2>{{ object.logdata.title }}</h2> <h2>{{ object.logdata.title }}</h2>
{% endif %} {% endif %}
<h3 class="text-muted">{{ object.local_timestamp }}</h3> <h3 class="text-muted">{{ object.local_timestamp }}</h3>

View File

@ -1,13 +1,16 @@
{% extends "base_detail.html" %} {% extends "base_detail.html" %}
{% block title %}{{object}}{% endblock %} {% block title %}{{object.title}}{% endblock %}
{% block details %} {% block details %}
<h2>{{object.subtitle}}</h2> <h2>{{object.league}} {{object.get_event_type_display}}</h2>
<div class="row"> <div class="row">
<h2>{{object.tv_series}}</h2>
<div class="col-md"> <div class="col-md">
{% for team in object.teams.all %}
<img src="{{team.logo.url}}" width=150 />
{% endfor %}
<hr />
<h3>Last scrobbles</h3> <h3>Last scrobbles</h3>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-sm"> <table class="table table-striped table-sm">