Compare commits

...

4 Commits
51.2 ... 51.4

Author SHA1 Message Date
8a28d0675b [release] Bump to version 51.4
All checks were successful
build / test (push) Successful in 1m56s
deploy / test (push) Successful in 1m57s
deploy / build-and-deploy (push) Successful in 38s
- Clean up metadata comicbook enrichment
2026-06-15 12:20:18 -04:00
5f6e75b14e [books] Fix comic book metadata importing
Some checks failed
build / test (push) Has been cancelled
2026-06-15 12:19:54 -04:00
a96a42cdbf [release] Bump to version 51.3
All checks were successful
build / test (push) Successful in 2m12s
deploy / test (push) Successful in 2m6s
deploy / build-and-deploy (push) Successful in 1m5s
- Improve speed of index and chart pages
2026-06-12 13:35:01 -04:00
c7f5d7d384 [charts] Add index to speed things up 2026-06-12 13:34:41 -04:00
11 changed files with 549 additions and 41 deletions

View File

@ -88,7 +88,7 @@ fetching and simple saving.
*** Metadata sources
**** Scraper
* Backlog [0/14] :vrobbler:project:personal:
* Backlog [0/16] :vrobbler:project:personal:
** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :vrobbler:personal:bug:music:scrobbles:
:PROPERTIES:
:ID: 702462cf-d54b-48c6-8a7c-78b8de751deb
@ -518,6 +518,64 @@ favorited media objects.
:PROPERTIES:
:ID: 79f867c3-1288-4143-b6bf-2a452983ee9f
:END:
** TODO [#C] Implement loguru into project :feature:loguru:logging:
:PROPERTIES:
:ID: efcd0c0a-db81-4518-9c23-5505d59e8ef5
:END:
*** Description
Would be great to formalize how we log so we can search for errors and such more
easily. And our exposure to PII is really low at this point in the project,
so we can probably use backtrace=True and diagnose=True to help us root cause
bugs faster.
** TODO [#B] Add a /trends/ page that shows trends based on scrobble data :feature:trends:scrobbles:
*** Description
This project is a bit invovled. But we should add a top level URL `trends` that shows
various trends as defined either in a static settings file, or dynamically via a database table.
Examples of trends:
- How often does the user:
+ watch sports while doing a task?
+ do a task while watching a video?
* how often do I do
- trail_scrobble__average_heartrate per trail
- ...
* Version 51.4 [1/1]
** DONE [#A] Clean up metadata comicbook enrichment :bug:comics:books:metadata:
:PROPERTIES:
:ID: cd875450-7117-78ca-8be4-9c8b73037dba
:END:
*** Description
Still getting wonky results with some comicbooks. Would be nice to be able to
tag a Book as a comicbook, and also gather volume information. I also noticed
that some books that are found in OL never get their comicvine_id populated. We
should make sure we always have comicvine_ids if available.
* Version 51.3 [1/1]
** DONE [#A] Improve speed of index and chart pages :bug:scrobbles:perf:
:PROPERTIES:
:ID: 031a23f8-7c02-4926-9884-6654ceca16c2
:END:
*** Description
Over the last few releases, the home page and charts pages have gotten really
slow.
We should look into what's causing the slowness and maybe do more agressive
query optimization or caching.
* Version 51.2 [2/2]
** DONE [#A] Fix bug where last page of book gets separate scrobble :bug:books:importers:koreader:
:PROPERTIES:

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "vrobbler"
version = "51.2"
version = "51.4"
description = ""
authors = ["Colin Powell <colin@unbl.ink>"]

View File

@ -160,7 +160,10 @@ class Command(BaseCommand):
)
def _enrich_book(self, book, sleep_secs):
from books.sources.comicvine import lookup_comic_from_comicvine
from books.sources.comicvine import (
lookup_comic_from_comicvine,
lookup_issue_by_comicvine_id,
)
from books.sources.google import lookup_book_from_google
from books.sources.openlibrary import lookup_book_from_openlibrary as lookup_book_from_ol
@ -168,13 +171,13 @@ class Command(BaseCommand):
author_name = book.author.name if book.author else None
book_dict = {}
is_comic = bool(book.readcomics_url) or (
book.issue_number is not None or book.volume_number is not None
)
if is_comic and READCOMICSONLINE_URL in (book.readcomics_url or ""):
cv_data = None
if book.comicvine_id:
cv_data = lookup_issue_by_comicvine_id(str(book.comicvine_id))
if not cv_data:
cv_data = lookup_comic_from_comicvine(title)
if cv_data:
book_dict.update(cv_data)
if cv_data:
book_dict.update(cv_data)
ol_data = lookup_book_from_ol(title, author=author_name)
time.sleep(sleep_secs)
@ -261,6 +264,14 @@ class Command(BaseCommand):
book.volume_number = data["volume_number"]
update_fields.append("volume_number")
if data.get("volume") and not book.volume:
book.volume = data["volume"]
update_fields.append("volume")
if data.get("volume_comicvine_id") and not book.volume_comicvine_id:
book.volume_comicvine_id = data["volume_comicvine_id"]
update_fields.append("volume_comicvine_id")
if update_fields:
book.save(update_fields=update_fields)
self.stdout.write(f" [ENRICHED] {book}{', '.join(update_fields)}")
@ -279,4 +290,12 @@ class Command(BaseCommand):
book.genre.add(*new_genres)
self.stdout.write(f" [GENRES] {book} — added {len(new_genres)} genres")
tags = data.pop("tags", [])
if tags:
existing_tags = set(book.tags.names())
new_tags = [t for t in tags if t not in existing_tags]
if new_tags:
book.tags.add(*new_tags)
self.stdout.write(f" [TAGS] {book} — added {', '.join(new_tags)}")
return changed if any(changed.values()) else None

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.29 on 2026-06-15 16:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("books", "0036_alter_book_genre_alter_paper_genre"),
]
operations = [
migrations.AddField(
model_name="book",
name="volume",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name="book",
name="volume_comicvine_id",
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@ -20,6 +20,7 @@ from books.openlibrary import (
from books.sources.comicvine import (
ComicVineClient,
lookup_comic_from_comicvine,
lookup_issue_by_comicvine_id,
)
from books.sources.google import lookup_book_from_google
from books.sources.openlibrary import (
@ -150,6 +151,8 @@ class Book(LongPlayScrobblableMixin):
first_sentence = models.TextField(**BNULL)
# ComicVine
comicvine_id = models.CharField(max_length=255, **BNULL)
volume = models.CharField(max_length=255, **BNULL)
volume_comicvine_id = models.CharField(max_length=255, **BNULL)
readcomics_url = models.CharField(max_length=255, **BNULL)
next_readcomics_url = models.CharField(max_length=255, **BNULL)
issue_number = models.IntegerField(**BNULL)
@ -236,13 +239,17 @@ class Book(LongPlayScrobblableMixin):
if not book_dict:
return book
tags = book_dict.pop("tags", [])
genres = book_dict.pop("genres", book_dict.pop("generes", []))
for k, v in book_dict.items():
setattr(book, k, v)
book.save()
genres = book_dict.get("genres", [])
if genres:
book.genre.add(*genres)
if tags:
book.tags.add(*tags)
return book
@classmethod
@ -276,8 +283,10 @@ class Book(LongPlayScrobblableMixin):
book_dict = None
source_tag = None
tried_comicvine = False
if READCOMICSONLINE_URL in url:
book_dict = lookup_comic_from_comicvine(title)
tried_comicvine = True
if book_dict:
source_tag = MediaSourceTag.COMICVINE
book_dict["readcomics_url"] = get_comic_issue_url(url)
@ -302,6 +311,16 @@ class Book(LongPlayScrobblableMixin):
if ol_data and ol_data.get("cover_url"):
book_dict["cover_url"] = ol_data["cover_url"]
# Always try ComicVine as a fallback — it may recognize books that
# OL/Google don't flag as comics
if not tried_comicvine:
cv_data = lookup_comic_from_comicvine(title)
if cv_data:
for k, v in cv_data.items():
if v:
book_dict.setdefault(k, v)
source_tag = MediaSourceTag.COMICVINE
if not book_dict:
logger.warning(
"No book found in any source, using data as is",
@ -312,6 +331,7 @@ class Book(LongPlayScrobblableMixin):
authors = book_dict.pop("authors", [])
cover_url = book_dict.pop("cover_url", "")
genres = book_dict.pop("genres", book_dict.pop("generes", []))
tags = book_dict.pop("tags", [])
if authors:
for author_str in authors:
@ -331,6 +351,8 @@ class Book(LongPlayScrobblableMixin):
book.save_image_from_url(cover_url)
if genres:
book.genre.add(*genres)
if tags:
book.tags.add(*tags)
book.authors.add(*author_list)
if source_tag:
book.tags.add(source_tag.value)
@ -368,8 +390,14 @@ class Book(LongPlayScrobblableMixin):
data = lookup_comic_from_locg(str(self.title))
if not data and COMICVINE_API_KEY:
logger.warn(f"Checking ComicVine for {self.title}")
data = lookup_comic_from_comicvine(str(self.title))
if self.comicvine_id:
logger.warn(
f"Checking ComicVine by ID for {self.title}"
)
data = lookup_issue_by_comicvine_id(str(self.comicvine_id))
if not data:
logger.warn(f"Checking ComicVine for {self.title}")
data = lookup_comic_from_comicvine(str(self.title))
if not data:
logger.warn(f"Book not found in any sources: {self.title}")
@ -407,10 +435,10 @@ class Book(LongPlayScrobblableMixin):
)
data.pop("pages")
# Pop this, so we can look it up later
# Pop these so they don't get passed to update()
cover_url = data.pop("cover_url", "")
subject_key_list = data.pop("subject_key_list", [])
tags = data.pop("tags", [])
# Fun trick for updating all fields at once
Book.objects.filter(pk=self.id).update(**data)
@ -418,6 +446,8 @@ class Book(LongPlayScrobblableMixin):
if subject_key_list:
self.genre.add(*subject_key_list)
if tags:
self.tags.add(*tags)
if cover_url:
r = requests.get(cover_url)

View File

@ -17,8 +17,10 @@ class ComicVineClient(object):
account on https://comicvine.gamespot.com/ in order to obtain an API key.
"""
# All API requests made by this client will be made to this URL.
# All API requests made by this client will be made to these URLs.
API_URL = "https://comicvine.gamespot.com/api/search/"
ISSUE_API_URL = "https://comicvine.gamespot.com/api/issue/4000-{issue_id}/"
VOLUME_API_URL = "https://comicvine.gamespot.com/api/volume/4050-{volume_id}/"
# A valid User-Agent header must be set in order for our API requests to
# be accepted, otherwise our request will be rejected with a
@ -197,6 +199,74 @@ class ComicVineClient(object):
raise exception(message)
def get_issue(self, issue_id: str) -> dict:
"""
Fetch a single issue by its ComicVine ID directly from the issue detail
endpoint, which returns richer data than the search endpoint.
:param issue_id: The ComicVine numeric ID for the issue (e.g. "538480")
:type issue_id: str
:return: The full JSON response for the issue, or empty dict on failure.
:rtype: dict
"""
params = {
"api_key": self.api_key,
"format": "json",
}
url = self.ISSUE_API_URL.format(issue_id=issue_id)
response = requests.get(url, headers=self.HEADERS, params=params)
if not response.ok:
self._handle_http_error(response)
json_data = response.json()
if json_data.get("status_code") != 1:
error_msg = json_data.get("error", "Unknown ComicVine API error")
logger.error(
"ComicVine API returned status_code %s: %s",
json_data.get("status_code"),
error_msg,
)
return {}
return json_data.get("results", {})
def get_volume(self, volume_id: str) -> dict:
"""
Fetch a single volume by its ComicVine ID from the volume detail
endpoint. Used to get publisher info and other volume-level metadata.
:param volume_id: The ComicVine numeric ID for the volume (e.g. "91273")
:type volume_id: str
:return: The full JSON response for the volume, or empty dict on failure.
:rtype: dict
"""
params = {
"api_key": self.api_key,
"format": "json",
}
url = self.VOLUME_API_URL.format(volume_id=volume_id)
response = requests.get(url, headers=self.HEADERS, params=params)
if not response.ok:
self._handle_http_error(response)
json_data = response.json()
if json_data.get("status_code") != 1:
error_msg = json_data.get("error", "Unknown ComicVine API error")
logger.error(
"ComicVine API returned status_code %s: %s",
json_data.get("status_code"),
error_msg,
)
return {}
return json_data.get("results", {})
def lookup_comic_from_comicvine(title: str) -> dict:
original_title = title
@ -238,26 +308,113 @@ def lookup_comic_from_comicvine(title: str) -> dict:
if not found_result:
found_result = results[0]
title = found_result.get("name")
data_dict = _build_data_dict_from_issue(found_result, original_title)
_enrich_with_volume_data(client, data_dict)
return data_dict
if found_result.get("volume"):
title = found_result.get("volume").get("name")
def lookup_issue_by_comicvine_id(comicvine_id: str) -> dict:
"""
Look up an issue directly by its ComicVine ID using the issue detail
endpoint. Returns richer data than the search-based lookup.
:param comicvine_id: The ComicVine numeric ID for the issue (e.g. "538480")
:type comicvine_id: str
:return: A dict of extracted book metadata, or empty dict on failure.
:rtype: dict
"""
if not comicvine_id:
return {}
api_key = getattr(settings, "COMICVINE_API_KEY", "")
if not api_key:
logger.warning("No ComicVine API key configured, not looking anything up")
return {}
client = ComicVineClient(api_key=api_key)
issue_data = client.get_issue(comicvine_id)
if not issue_data:
logger.warning("No issue found on ComicVine for ID %s", comicvine_id)
return {}
data_dict = _build_data_dict_from_issue(issue_data, issue_data.get("name", ""))
_enrich_with_volume_data(client, data_dict)
return data_dict
def _build_data_dict_from_issue(issue_data: dict, original_title: str = "") -> dict:
"""
Build a book metadata dict from a ComicVine issue resource (either from
search results or issue detail endpoint). Both return the same shape of
issue data.
:param issue_data: The issue resource dict from ComicVine.
:param original_title: The original search term, if any.
:return: A dict of extracted book metadata.
:rtype: dict
"""
title = issue_data.get("name")
if issue_data.get("volume"):
title = issue_data.get("volume").get("name")
cover_url = None
if found_result.get("image"):
cover_url = found_result["image"].get("original_url")
if issue_data.get("image"):
cover_url = issue_data["image"].get("original_url")
volume_name = None
volume_cv_id = None
publisher_name = None
volume_data = issue_data.get("volume")
if volume_data:
volume_name = volume_data.get("name")
volume_cv_id = volume_data.get("id")
publisher_data = volume_data.get("publisher")
if publisher_data:
publisher_name = publisher_data.get("name")
data_dict = {
"title": title,
"original_title": original_title,
"issue_number": found_result.get("issue_number"),
"volume_number": found_result.get("volume_number"),
"issue_number": issue_data.get("issue_number"),
"volume_number": issue_data.get("volume_number"),
"volume": volume_name,
"volume_comicvine_id": volume_cv_id,
"publisher": publisher_name,
"cover_url": cover_url,
"comicvine_id": found_result.get("id"),
"comicvine_data": found_result,
"summary": found_result.get("description"),
"publish_date": found_result.get("cover_date"),
"first_publish_year": (found_result.get("cover_date") or "")[:4],
"comicvine_id": issue_data.get("id"),
"summary": issue_data.get("description"),
"publish_date": issue_data.get("cover_date"),
"first_publish_year": (issue_data.get("cover_date") or "")[:4],
"tags": ["comicbook"],
}
return data_dict
def _enrich_with_volume_data(client: ComicVineClient, data_dict: dict) -> None:
"""
Follow-up a successful issue lookup by fetching the volume detail and
filling in publisher and other volume-level metadata that the issue
endpoint doesn't provide.
:param client: An initialised ComicVineClient instance.
:param data_dict: The data dict from an issue lookup (mutated in place).
"""
volume_cv_id = data_dict.get("volume_comicvine_id")
if not volume_cv_id:
return
volume_data = client.get_volume(str(volume_cv_id))
if not volume_data:
return
publisher_data = volume_data.get("publisher")
if publisher_data:
publisher_name = publisher_data.get("name")
if publisher_name and not data_dict.get("publisher"):
data_dict["publisher"] = publisher_name
if not data_dict.get("volume"):
data_dict["volume"] = volume_data.get("name")

View File

@ -0,0 +1,146 @@
# Generated by Django 4.2.29 on 2026-06-12 16:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("charts", "0001_initial"),
]
operations = [
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "month", "album", "rank"],
name="charts_char_user_id_1adcde_idx",
),
),
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "month", "track", "rank"],
name="charts_char_user_id_d18aab_idx",
),
),
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "month", "video", "rank"],
name="charts_char_user_id_de9f0a_idx",
),
),
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "month", "board_game", "rank"],
name="charts_char_user_id_d5d58f_idx",
),
),
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "month", "book", "rank"],
name="charts_char_user_id_e877cf_idx",
),
),
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "month", "food", "rank"],
name="charts_char_user_id_a0ad71_idx",
),
),
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "month", "podcast", "rank"],
name="charts_char_user_id_846b80_idx",
),
),
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "month", "trail", "rank"],
name="charts_char_user_id_54feba_idx",
),
),
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "week", "album", "rank"],
name="charts_char_user_id_a3dc49_idx",
),
),
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "week", "track", "rank"],
name="charts_char_user_id_4b01ab_idx",
),
),
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "week", "video", "rank"],
name="charts_char_user_id_2ac9d2_idx",
),
),
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "week", "board_game", "rank"],
name="charts_char_user_id_ba968a_idx",
),
),
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "week", "book", "rank"],
name="charts_char_user_id_e66751_idx",
),
),
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "week", "food", "rank"],
name="charts_char_user_id_d23f06_idx",
),
),
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "week", "podcast", "rank"],
name="charts_char_user_id_be8122_idx",
),
),
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "week", "trail", "rank"],
name="charts_char_user_id_b94ea9_idx",
),
),
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "month", "day", "artist", "rank"],
name="charts_char_user_id_406e0e_idx",
),
),
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "month", "day", "album", "rank"],
name="charts_char_user_id_322b0d_idx",
),
),
migrations.AddIndex(
model_name="chartrecord",
index=models.Index(
fields=["user", "year", "month", "day", "tv_series", "rank"],
name="charts_char_user_id_aa44b7_idx",
),
),
]

View File

@ -60,10 +60,29 @@ class ChartRecord(TimeStampedModel):
models.Index(fields=["user", "year", "geo_location", "rank"]),
models.Index(fields=["user", "year", "food", "rank"]),
models.Index(fields=["user", "year", "book", "rank"]),
models.Index(fields=["user", "year", "week", "artist", "rank"]),
models.Index(fields=["user", "year", "week", "tv_series", "rank"]),
models.Index(fields=["user", "year", "month", "artist", "rank"]),
models.Index(fields=["user", "year", "month", "album", "rank"]),
models.Index(fields=["user", "year", "month", "track", "rank"]),
models.Index(fields=["user", "year", "month", "tv_series", "rank"]),
models.Index(fields=["user", "year", "month", "video", "rank"]),
models.Index(fields=["user", "year", "month", "board_game", "rank"]),
models.Index(fields=["user", "year", "month", "book", "rank"]),
models.Index(fields=["user", "year", "month", "food", "rank"]),
models.Index(fields=["user", "year", "month", "podcast", "rank"]),
models.Index(fields=["user", "year", "month", "trail", "rank"]),
models.Index(fields=["user", "year", "week", "artist", "rank"]),
models.Index(fields=["user", "year", "week", "album", "rank"]),
models.Index(fields=["user", "year", "week", "track", "rank"]),
models.Index(fields=["user", "year", "week", "tv_series", "rank"]),
models.Index(fields=["user", "year", "week", "video", "rank"]),
models.Index(fields=["user", "year", "week", "board_game", "rank"]),
models.Index(fields=["user", "year", "week", "book", "rank"]),
models.Index(fields=["user", "year", "week", "food", "rank"]),
models.Index(fields=["user", "year", "week", "podcast", "rank"]),
models.Index(fields=["user", "year", "week", "trail", "rank"]),
models.Index(fields=["user", "year", "month", "day", "artist", "rank"]),
models.Index(fields=["user", "year", "month", "day", "album", "rank"]),
models.Index(fields=["user", "year", "month", "day", "tv_series", "rank"]),
]
@property

View File

@ -438,12 +438,14 @@ class ChartRecordView(TemplateView):
return context
def get_available_years(self, user):
return list(
ChartRecord.objects.filter(user=user)
.values_list("year", flat=True)
.distinct()
.order_by("-year")
)
if not hasattr(self, "_available_years"):
self._available_years = list(
ChartRecord.objects.filter(user=user)
.values_list("year", flat=True)
.distinct()
.order_by("-year")
)
return self._available_years
def get_period_type(self):
date_param = self.request.GET.get("date")

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.29 on 2026-06-12 16:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("scrobbles", "0096_convert_book_page_data_to_dict"),
]
operations = [
migrations.AddIndex(
model_name="scrobble",
index=models.Index(
fields=["user", "-timestamp"], name="scrobbles_s_user_id_d367a7_idx"
),
),
]

View File

@ -612,6 +612,30 @@ class EBirdCSVImport(BaseFileImportMixin):
self.mark_finished()
TYPE_FK_PREFETCHES: dict[str, tuple[str, ...]] = {
"Video": ("video",),
"Track": ("track", "track__artist_fk"),
"PodcastEpisode": ("podcast_episode", "podcast_episode__podcast"),
"SportEvent": ("sport_event",),
"Book": ("book",),
"Paper": ("paper",),
"VideoGame": ("video_game",),
"BoardGame": ("board_game",),
"GeoLocation": ("geo_location",),
"Trail": ("trail",),
"Beer": ("beer",),
"Puzzle": ("puzzle",),
"Food": ("food",),
"Task": ("task",),
"WebPage": ("web_page",),
"LifeEvent": ("life_event",),
"Mood": ("mood",),
"BrickSet": ("brick_set",),
"Channel": ("channel",),
"BirdingLocation": ("birding_location",),
}
class ScrobbleQuerySet(models.QuerySet):
def with_related(self):
return self.select_related("user").prefetch_related(
@ -638,6 +662,13 @@ class ScrobbleQuerySet(models.QuerySet):
"birding_location",
)
def with_related_for_types(self, media_types: list[str]):
prefetches = []
for t in media_types:
if t in TYPE_FK_PREFETCHES:
prefetches.extend(TYPE_FK_PREFETCHES[t])
return self.select_related("user").prefetch_related(*prefetches)
class ShareViewLog(TimeStampedModel):
scrobble = models.ForeignKey(
@ -778,6 +809,7 @@ class Scrobble(TimeStampedModel):
"is_paused",
]
),
models.Index(fields=["user", "-timestamp"]),
]
@classmethod
@ -810,11 +842,14 @@ class Scrobble(TimeStampedModel):
@classmethod
def as_dict_by_type(cls, scrobble_qs: models.QuerySet) -> dict:
scrobbles_by_type = defaultdict(list)
scrobbles = (
scrobble_qs.with_related()
if hasattr(scrobble_qs, "with_related")
else scrobble_qs
)
if hasattr(scrobble_qs, "with_related"):
media_types_present = list(
scrobble_qs.values_list("media_type", flat=True).distinct()
)
scrobbles = scrobble_qs.with_related_for_types(media_types_present)
else:
scrobbles = scrobble_qs
for scrobble in scrobbles:
scrobbles_by_type[scrobble.media_type].append(scrobble)
@ -829,7 +864,7 @@ class Scrobble(TimeStampedModel):
# Remove any locations without titles
if "GeoLocation" in scrobbles_by_type.keys():
for loc_scrobble in scrobbles_by_type["GeoLocation"]:
for loc_scrobble in list(scrobbles_by_type["GeoLocation"]):
if not loc_scrobble.media_obj.title:
scrobbles_by_type["GeoLocation"].remove(loc_scrobble)
scrobbles_by_type["GeoLocation_count"] -= 1