[scrobbles] Improve query efficency
All checks were successful
build & deploy / test (push) Successful in 2m1s
build & deploy / deploy (push) Successful in 23s

This commit is contained in:
2026-03-21 14:05:30 -04:00
parent 6a2cb4a881
commit 3a02bcad9d
3 changed files with 243 additions and 183 deletions

View File

@ -16,9 +16,7 @@ def scrobble_counts(user=None):
now = now_user_timezone(user.profile) now = now_user_timezone(user.profile)
user_filter = Q(user=user) user_filter = Q(user=user)
start_of_today = datetime.combine( start_of_today = datetime.combine(now.date(), datetime.min.time(), now.tzinfo)
now.date(), datetime.min.time(), now.tzinfo
)
starting_day_of_current_week = now.date() - timedelta( starting_day_of_current_week = now.date() - timedelta(
days=now.today().isoweekday() % 7 days=now.today().isoweekday() % 7
) )
@ -31,9 +29,7 @@ def scrobble_counts(user=None):
media_type=Scrobble.MediaType.TRACK, media_type=Scrobble.MediaType.TRACK,
) )
data = {} data = {}
data["today"] = finished_scrobbles_qs.filter( data["today"] = finished_scrobbles_qs.filter(timestamp__gte=start_of_today).count()
timestamp__gte=start_of_today
).count()
data["week"] = finished_scrobbles_qs.filter( data["week"] = finished_scrobbles_qs.filter(
timestamp__gte=starting_day_of_current_week timestamp__gte=starting_day_of_current_week
).count() ).count()
@ -47,9 +43,7 @@ def scrobble_counts(user=None):
return data return data
def week_of_scrobbles( def week_of_scrobbles(user=None, start=None, media: str = "tracks") -> dict[str, int]:
user=None, start=None, media: str = "tracks"
) -> dict[str, int]:
now = timezone.now() now = timezone.now()
user_filter = Q() user_filter = Q()
@ -101,9 +95,7 @@ def live_charts(
seven_days_ago = now - timedelta(days=7) seven_days_ago = now - timedelta(days=7)
thirty_days_ago = now - timedelta(days=30) thirty_days_ago = now - timedelta(days=30)
start_of_today = datetime.combine(now, datetime.min.time(), tzinfo) start_of_today = datetime.combine(now, datetime.min.time(), tzinfo)
start_day_of_week = start_of_today - timedelta( start_day_of_week = start_of_today - timedelta(days=now.today().isoweekday() % 7)
days=now.today().isoweekday() % 7
)
start_day_of_month = now.replace(day=1) start_day_of_month = now.replace(day=1)
start_day_of_year = now.replace(month=1, day=1) start_day_of_year = now.replace(month=1, day=1)
@ -120,17 +112,13 @@ def live_charts(
} }
time_filter = Q() time_filter = Q()
completion_filter = Q( completion_filter = Q(scrobble__user=user, scrobble__played_to_completion=True)
scrobble__user=user, scrobble__played_to_completion=True
)
user_filter = Q(scrobble__user=user) user_filter = Q(scrobble__user=user)
count_field = "scrobble" count_field = "scrobble"
if media_type == "Artist": if media_type == "Artist":
for period, query_dict in period_queries.items(): for period, query_dict in period_queries.items():
period_queries[period] = { period_queries[period] = {"track__" + k: v for k, v in query_dict.items()}
"track__" + k: v for k, v in query_dict.items()
}
completion_filter = Q( completion_filter = Q(
track__scrobble__user=user, track__scrobble__user=user,
track__scrobble__played_to_completion=True, track__scrobble__played_to_completion=True,
@ -175,9 +163,7 @@ def live_tv_charts(
seven_days_ago = now - timedelta(days=7) seven_days_ago = now - timedelta(days=7)
thirty_days_ago = now - timedelta(days=30) thirty_days_ago = now - timedelta(days=30)
start_of_today = datetime.combine(now, datetime.min.time(), tzinfo) start_of_today = datetime.combine(now, datetime.min.time(), tzinfo)
start_day_of_week = start_of_today - timedelta( start_day_of_week = start_of_today - timedelta(days=now.today().isoweekday() % 7)
days=now.today().isoweekday() % 7
)
start_day_of_month = now.replace(day=1) start_day_of_month = now.replace(day=1)
start_day_of_year = now.replace(month=1, day=1) start_day_of_year = now.replace(month=1, day=1)
@ -232,9 +218,7 @@ def live_youtube_channel_charts(
seven_days_ago = now - timedelta(days=7) seven_days_ago = now - timedelta(days=7)
thirty_days_ago = now - timedelta(days=30) thirty_days_ago = now - timedelta(days=30)
start_of_today = datetime.combine(now, datetime.min.time(), tzinfo) start_of_today = datetime.combine(now, datetime.min.time(), tzinfo)
start_day_of_week = start_of_today - timedelta( start_day_of_week = start_of_today - timedelta(days=now.today().isoweekday() % 7)
days=now.today().isoweekday() % 7
)
start_day_of_year = now.replace(month=1, day=1) start_day_of_year = now.replace(month=1, day=1)
period_queries = { period_queries = {
@ -269,3 +253,113 @@ def live_youtube_channel_charts(
.filter(num_scrobbles__gt=0) .filter(num_scrobbles__gt=0)
.order_by("-num_scrobbles")[:limit] .order_by("-num_scrobbles")[:limit]
) )
def batch_live_charts(
user: "User",
media_type: str = "Track",
limit: int = 15,
app_label: str = "music",
) -> dict[str, list]:
from django.db.models import Count, Q
now = timezone.now()
tzinfo = now.tzinfo
now = now.date()
if user.is_authenticated:
now = now_user_timezone(user.profile)
tzinfo = now.tzinfo
seven_days_ago = now - timedelta(days=7)
thirty_days_ago = now - timedelta(days=30)
start_of_today = datetime.combine(now, datetime.min.time(), tzinfo)
start_day_of_year = now.replace(month=1, day=1)
media_model = apps.get_model(app_label=app_label, model_name=media_type)
if media_type == "Artist":
base_filter = Q(
track__scrobble__user=user, track__scrobble__played_to_completion=True
)
count_field = "track__scrobble"
else:
base_filter = Q(scrobble__user=user, scrobble__played_to_completion=True)
count_field = "scrobble"
scrobble_filter_prefix = (
"track__scrobble__" if media_type == "Artist" else "scrobble__"
)
queryset = media_model.objects.filter(
Q(track__scrobble__user=user)
if media_type == "Artist"
else Q(scrobble__user=user)
).annotate(
num_scrobbles_today=Count(
count_field,
filter=Q(
**{
f"{scrobble_filter_prefix}played_to_completion": True,
f"{scrobble_filter_prefix}timestamp__gte": start_of_today,
}
),
distinct=True,
),
num_scrobbles_last7=Count(
count_field,
filter=Q(
**{
f"{scrobble_filter_prefix}played_to_completion": True,
f"{scrobble_filter_prefix}timestamp__gte": seven_days_ago,
}
),
distinct=True,
),
num_scrobbles_last30=Count(
count_field,
filter=Q(
**{
f"{scrobble_filter_prefix}played_to_completion": True,
f"{scrobble_filter_prefix}timestamp__gte": thirty_days_ago,
}
),
distinct=True,
),
num_scrobbles_year=Count(
count_field,
filter=Q(
**{
f"{scrobble_filter_prefix}played_to_completion": True,
f"{scrobble_filter_prefix}timestamp__gte": start_day_of_year,
}
),
distinct=True,
),
num_scrobbles=Count(
count_field,
filter=Q(
**{
f"{scrobble_filter_prefix}played_to_completion": True,
}
),
distinct=True,
),
)
items = list(queryset.order_by("-num_scrobbles")[:limit])
for item in items:
item.count = item.num_scrobbles
def filter_by_count_attr(qs, attr_name):
return sorted(
[item for item in items if getattr(item, attr_name) > 0],
key=lambda x: -getattr(x, attr_name),
)[:limit]
return {
"today": filter_by_count_attr(items, "num_scrobbles_today"),
"last7": filter_by_count_attr(items, "num_scrobbles_last7"),
"last30": filter_by_count_attr(items, "num_scrobbles_last30"),
"year": filter_by_count_attr(items, "num_scrobbles_year"),
"all": items,
}

View File

@ -62,9 +62,7 @@ logger = logging.getLogger(__name__)
User = get_user_model() User = get_user_model()
BNULL = {"blank": True, "null": True} BNULL = {"blank": True, "null": True}
POINTS_FOR_MOVEMENT_HISTORY = int( POINTS_FOR_MOVEMENT_HISTORY = int(getattr(settings, "POINTS_FOR_MOVEMENT_HISTORY", 3))
getattr(settings, "POINTS_FOR_MOVEMENT_HISTORY", 3)
)
class BaseFileImportMixin(TimeStampedModel): class BaseFileImportMixin(TimeStampedModel):
@ -107,9 +105,7 @@ class BaseFileImportMixin(TimeStampedModel):
scrobble_id = line.split("\t")[0] scrobble_id = line.split("\t")[0]
scrobble = Scrobble.objects.filter(id=scrobble_id).first() scrobble = Scrobble.objects.filter(id=scrobble_id).first()
if not scrobble: if not scrobble:
logger.warning( logger.warning(f"Could not find scrobble {scrobble_id} to undo")
f"Could not find scrobble {scrobble_id} to undo"
)
continue continue
logger.info(f"Removing scrobble {scrobble_id}") logger.info(f"Removing scrobble {scrobble_id}")
if not dryrun: if not dryrun:
@ -152,9 +148,7 @@ class BaseFileImportMixin(TimeStampedModel):
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
@ -176,9 +170,7 @@ class KoReaderImport(BaseFileImportMixin):
return "KOReader" return "KOReader"
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse("scrobbles:koreader-import-detail", kwargs={"slug": self.uuid})
"scrobbles:koreader-import-detail", kwargs={"slug": self.uuid}
)
def get_path(instance, filename): def get_path(instance, filename):
extension = filename.split(".")[-1] extension = filename.split(".")[-1]
@ -214,15 +206,11 @@ class KoReaderImport(BaseFileImportMixin):
fix_profile_historic_timezones(self.user.profile) fix_profile_historic_timezones(self.user.profile)
if self.processed_finished and not force: if self.processed_finished and not force:
logger.info( logger.info(f"{self} already processed on {self.processed_finished}")
f"{self} already processed on {self.processed_finished}"
)
return return
self.mark_started() self.mark_started()
scrobbles = process_koreader_sqlite_file( scrobbles = process_koreader_sqlite_file(self.upload_file_path, self.user.id)
self.upload_file_path, self.user.id
)
self.record_log(scrobbles) self.record_log(scrobbles)
self.mark_finished() self.mark_finished()
@ -236,9 +224,7 @@ class AudioScrobblerTSVImport(BaseFileImportMixin):
return "AudiosScrobbler" return "AudiosScrobbler"
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse("scrobbles:tsv-import-detail", kwargs={"slug": self.uuid})
"scrobbles:tsv-import-detail", kwargs={"slug": self.uuid}
)
def get_path(instance, filename): def get_path(instance, filename):
extension = filename.split(".")[-1] extension = filename.split(".")[-1]
@ -262,16 +248,12 @@ class AudioScrobblerTSVImport(BaseFileImportMixin):
fix_profile_historic_timezones(self.user.profile) fix_profile_historic_timezones(self.user.profile)
if self.processed_finished and not force: if self.processed_finished and not force:
logger.info( logger.info(f"{self} already processed on {self.processed_finished}")
f"{self} already processed on {self.processed_finished}"
)
return return
self.mark_started() self.mark_started()
scrobbles = import_audioscrobbler_tsv_file( scrobbles = import_audioscrobbler_tsv_file(self.upload_file_path, self.user.id)
self.upload_file_path, self.user.id
)
self.record_log(scrobbles) self.record_log(scrobbles)
self.mark_finished() self.mark_finished()
@ -285,9 +267,7 @@ class LastFmImport(BaseFileImportMixin):
return "LastFM" return "LastFM"
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse("scrobbles:lastfm-import-detail", kwargs={"slug": self.uuid})
"scrobbles:lastfm-import-detail", kwargs={"slug": self.uuid}
)
def process(self, import_all=False): def process(self, import_all=False):
"""Import scrobbles found on LastFM""" """Import scrobbles found on LastFM"""
@ -296,9 +276,7 @@ class LastFmImport(BaseFileImportMixin):
fix_profile_historic_timezones(self.user.profile) fix_profile_historic_timezones(self.user.profile)
if self.processed_finished: if self.processed_finished:
logger.info( logger.info(f"{self} already processed on {self.processed_finished}")
f"{self} already processed on {self.processed_finished}"
)
return return
last_import = None last_import = None
@ -336,9 +314,7 @@ class RetroarchImport(BaseFileImportMixin):
return "Retroarch" return "Retroarch"
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse("scrobbles:retroarch-import-detail", kwargs={"slug": self.uuid})
"scrobbles:retroarch-import-detail", kwargs={"slug": self.uuid}
)
def process(self, import_all=False, force=False): def process(self, import_all=False, force=False):
"""Import scrobbles found on Retroarch""" """Import scrobbles found on Retroarch"""
@ -346,9 +322,7 @@ class RetroarchImport(BaseFileImportMixin):
fix_profile_historic_timezones(self.user.profile) fix_profile_historic_timezones(self.user.profile)
if self.processed_finished and not force: if self.processed_finished and not force:
logger.info( logger.info(f"{self} already processed on {self.processed_finished}")
f"{self} already processed on {self.processed_finished}"
)
return return
if force: if force:
@ -504,9 +478,37 @@ class ChartRecord(TimeStampedModel):
return cls.objects.filter(year=year, week=week, user=user) return cls.objects.filter(year=year, week=week, user=user)
class ScrobbleQuerySet(models.QuerySet):
def with_related(self):
return self.select_related("user").prefetch_related(
"video",
"track",
"track__artist",
"podcast_episode",
"podcast_episode__podcast",
"sport_event",
"book",
"paper",
"video_game",
"board_game",
"geo_location",
"beer",
"puzzle",
"food",
"trail",
"task",
"web_page",
"life_event",
"mood",
"brick_set",
)
class Scrobble(TimeStampedModel): class Scrobble(TimeStampedModel):
"""A scrobble tracks played media items by a user.""" """A scrobble tracks played media items by a user."""
objects = ScrobbleQuerySet.as_manager()
class MediaType(models.TextChoices): class MediaType(models.TextChoices):
"""Enum mapping a media model type to a string""" """Enum mapping a media model type to a string"""
@ -539,39 +541,25 @@ class Scrobble(TimeStampedModel):
podcast_episode = models.ForeignKey( podcast_episode = models.ForeignKey(
PodcastEpisode, on_delete=models.DO_NOTHING, **BNULL PodcastEpisode, on_delete=models.DO_NOTHING, **BNULL
) )
sport_event = models.ForeignKey( sport_event = models.ForeignKey(SportEvent, on_delete=models.DO_NOTHING, **BNULL)
SportEvent, on_delete=models.DO_NOTHING, **BNULL
)
book = models.ForeignKey(Book, on_delete=models.DO_NOTHING, **BNULL) book = models.ForeignKey(Book, on_delete=models.DO_NOTHING, **BNULL)
paper = models.ForeignKey(Paper, on_delete=models.DO_NOTHING, **BNULL) paper = models.ForeignKey(Paper, on_delete=models.DO_NOTHING, **BNULL)
video_game = models.ForeignKey( video_game = models.ForeignKey(VideoGame, on_delete=models.DO_NOTHING, **BNULL)
VideoGame, on_delete=models.DO_NOTHING, **BNULL board_game = models.ForeignKey(BoardGame, on_delete=models.DO_NOTHING, **BNULL)
) geo_location = models.ForeignKey(GeoLocation, on_delete=models.DO_NOTHING, **BNULL)
board_game = models.ForeignKey(
BoardGame, on_delete=models.DO_NOTHING, **BNULL
)
geo_location = models.ForeignKey(
GeoLocation, on_delete=models.DO_NOTHING, **BNULL
)
beer = models.ForeignKey(Beer, on_delete=models.DO_NOTHING, **BNULL) beer = models.ForeignKey(Beer, on_delete=models.DO_NOTHING, **BNULL)
puzzle = models.ForeignKey(Puzzle, on_delete=models.DO_NOTHING, **BNULL) puzzle = models.ForeignKey(Puzzle, on_delete=models.DO_NOTHING, **BNULL)
food = models.ForeignKey(Food, on_delete=models.DO_NOTHING, **BNULL) food = models.ForeignKey(Food, on_delete=models.DO_NOTHING, **BNULL)
trail = models.ForeignKey(Trail, on_delete=models.DO_NOTHING, **BNULL) trail = models.ForeignKey(Trail, on_delete=models.DO_NOTHING, **BNULL)
task = models.ForeignKey(Task, on_delete=models.DO_NOTHING, **BNULL) task = models.ForeignKey(Task, on_delete=models.DO_NOTHING, **BNULL)
web_page = models.ForeignKey(WebPage, on_delete=models.DO_NOTHING, **BNULL) web_page = models.ForeignKey(WebPage, on_delete=models.DO_NOTHING, **BNULL)
life_event = models.ForeignKey( life_event = models.ForeignKey(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)
brick_set = 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(
max_length=14, choices=MediaType.choices, default=MediaType.VIDEO max_length=14, choices=MediaType.choices, default=MediaType.VIDEO
) )
user = models.ForeignKey( user = models.ForeignKey(User, blank=True, null=True, on_delete=models.DO_NOTHING)
User, blank=True, null=True, on_delete=models.DO_NOTHING
)
# Time keeping # Time keeping
timestamp = models.DateTimeField(**BNULL) timestamp = models.DateTimeField(**BNULL)
@ -599,9 +587,7 @@ class Scrobble(TimeStampedModel):
upload_to="scrobbles/videogame_save_data/", **BNULL upload_to="scrobbles/videogame_save_data/", **BNULL
) )
gpx_file = models.FileField(upload_to="scrobbles/gpx_file/", **BNULL) gpx_file = models.FileField(upload_to="scrobbles/gpx_file/", **BNULL)
screenshot = models.ImageField( screenshot = models.ImageField(upload_to="scrobbles/videogame_screenshot/", **BNULL)
upload_to="scrobbles/videogame_screenshot/", **BNULL
)
screenshot_small = ImageSpecField( screenshot_small = ImageSpecField(
source="screenshot", source="screenshot",
processors=[ResizeToFit(100, 100)], processors=[ResizeToFit(100, 100)],
@ -662,8 +648,13 @@ class Scrobble(TimeStampedModel):
@classmethod @classmethod
def as_dict_by_type(cls, scrobble_qs: models.QuerySet) -> dict: def as_dict_by_type(cls, scrobble_qs: models.QuerySet) -> dict:
scrobbles_by_type = defaultdict(list) scrobbles_by_type = defaultdict(list)
scrobbles = (
scrobble_qs.with_related()
if hasattr(scrobble_qs, "with_related")
else scrobble_qs
)
for scrobble in scrobble_qs: for scrobble in scrobbles:
scrobbles_by_type[scrobble.media_type].append(scrobble) scrobbles_by_type[scrobble.media_type].append(scrobble)
if not scrobbles_by_type.get(scrobble.media_type + "_count"): if not scrobbles_by_type.get(scrobble.media_type + "_count"):
scrobbles_by_type[scrobble.media_type + "_count"] = 0 scrobbles_by_type[scrobble.media_type + "_count"] = 0
@ -697,9 +688,7 @@ class Scrobble(TimeStampedModel):
from scrobbles.models import Scrobble from scrobbles.models import Scrobble
if self.logdata and self.logdata.serial_scrobble_id: if self.logdata and self.logdata.serial_scrobble_id:
return Scrobble.objects.filter( return Scrobble.objects.filter(id=self.logdata.serial_scrobble_id).first()
id=self.logdata.serial_scrobble_id
).first()
@property @property
def finish_url(self) -> str: def finish_url(self) -> str:
@ -722,8 +711,7 @@ class Scrobble(TimeStampedModel):
self.media_type = self.MediaType(self.media_obj.__class__.__name__) self.media_type = self.MediaType(self.media_obj.__class__.__name__)
if (self.timestamp and self.stop_timestamp) and ( if (self.timestamp and self.stop_timestamp) and (
not self.playback_position_seconds not self.playback_position_seconds or self.playback_position_seconds <= 0
or self.playback_position_seconds <= 0
): ):
self.playback_position_seconds = ( self.playback_position_seconds = (
self.stop_timestamp - self.timestamp self.stop_timestamp - self.timestamp
@ -738,9 +726,9 @@ class Scrobble(TimeStampedModel):
return reverse("scrobbles:detail", kwargs={"uuid": self.uuid}) return reverse("scrobbles:detail", kwargs={"uuid": self.uuid})
def push_to_archivebox(self): def push_to_archivebox(self):
pushable_media = hasattr( pushable_media = hasattr(self.media_obj, "push_to_archivebox") and callable(
self.media_obj, "push_to_archivebox" self.media_obj.push_to_archivebox
) and callable(self.media_obj.push_to_archivebox) )
if pushable_media and self.user.profile.archivebox_url: if pushable_media and self.user.profile.archivebox_url:
try: try:
@ -810,10 +798,7 @@ class Scrobble(TimeStampedModel):
logger.info(f"Redirecting to {self.media_obj.url}") logger.info(f"Redirecting to {self.media_obj.url}")
redirect_url = self.media_obj.url redirect_url = self.media_obj.url
if ( if self.media_type == self.MediaType.VIDEO and self.media_obj.youtube_id:
self.media_type == self.MediaType.VIDEO
and self.media_obj.youtube_id
):
redirect_url = self.media_obj.youtube_link redirect_url = self.media_obj.youtube_link
return redirect_url return redirect_url
@ -829,9 +814,7 @@ class Scrobble(TimeStampedModel):
@property @property
def local_stop_timestamp(self): def local_stop_timestamp(self):
if self.stop_timestamp: if self.stop_timestamp:
return timezone.localtime( return timezone.localtime(self.stop_timestamp, timezone=self.tzinfo)
self.stop_timestamp, timezone=self.tzinfo
)
@property @property
def scrobble_media_key(self) -> str: def scrobble_media_key(self) -> str:
@ -959,9 +942,7 @@ class Scrobble(TimeStampedModel):
long_play_secs = 0 long_play_secs = 0
if self.previous and not self.previous.long_play_complete: if self.previous and not self.previous.long_play_complete:
long_play_secs = self.previous.long_play_seconds or 0 long_play_secs = self.previous.long_play_seconds or 0
percent = int( percent = int(((playback_seconds + long_play_secs) / run_time_secs) * 100)
((playback_seconds + long_play_secs) / run_time_secs) * 100
)
return percent return percent
@ -1006,9 +987,7 @@ class Scrobble(TimeStampedModel):
expected_end = self.timestamp + datetime.timedelta( expected_end = self.timestamp + datetime.timedelta(
seconds=self.media_obj.run_time_seconds seconds=self.media_obj.run_time_seconds
) )
expected_end_padded = expected_end + datetime.timedelta( expected_end_padded = expected_end + datetime.timedelta(seconds=padding_seconds)
seconds=padding_seconds
)
# Take our start time, add our media length and an extra 30 min (1800s) is it still in the future? keep going # Take our start time, add our media length and an extra 30 min (1800s) is it still in the future? keep going
is_in_progress = expected_end_padded > pendulum.now() is_in_progress = expected_end_padded > pendulum.now()
logger.info( logger.info(
@ -1071,11 +1050,9 @@ class Scrobble(TimeStampedModel):
@classmethod @classmethod
def by_date(cls, media_type: str = "Track"): def by_date(cls, media_type: str = "Track"):
cls.objects.filter(media_type=media_type).values( cls.objects.filter(media_type=media_type).values("timestamp__date").annotate(
"timestamp__date" count=models.Count("id")
).annotate(count=models.Count("id")).values( ).values("timestamp__date", "count").order_by(
"timestamp__date", "count"
).order_by(
"-count", "-count",
) )
@ -1209,9 +1186,7 @@ class Scrobble(TimeStampedModel):
logger.warning( logger.warning(
f"[create_or_update] geoloc requires create_or_update_location" f"[create_or_update] geoloc requires create_or_update_location"
) )
scrobble = cls.create_or_update_location( scrobble = cls.create_or_update_location(media, scrobble_data, user_id)
media, scrobble_data, user_id
)
return scrobble return scrobble
if not skip_in_progress_check or read_log_page: if not skip_in_progress_check or read_log_page:
@ -1224,9 +1199,7 @@ class Scrobble(TimeStampedModel):
"scrobble_data": scrobble_data, "scrobble_data": scrobble_data,
}, },
) )
scrobble_data["playback_status"] = scrobble_data.pop( scrobble_data["playback_status"] = scrobble_data.pop("status", None)
"status", None
)
# If it's marked as stopped, send it through our update mechanism, which will complete it # If it's marked as stopped, send it through our update mechanism, which will complete it
if scrobble and ( if scrobble and (
scrobble.can_be_updated scrobble.can_be_updated
@ -1238,12 +1211,8 @@ class Scrobble(TimeStampedModel):
if page_list: if page_list:
for page in page_list: for page in page_list:
if not page.get("end_ts", None): if not page.get("end_ts", None):
page["end_ts"] = int( page["end_ts"] = int(timezone.now().timestamp())
timezone.now().timestamp() page["duration"] = page["end_ts"] - page.get("start_ts")
)
page["duration"] = page["end_ts"] - page.get(
"start_ts"
)
page_list.append( page_list.append(
BookPageLogData( BookPageLogData(
@ -1279,9 +1248,9 @@ class Scrobble(TimeStampedModel):
"source": source, "source": source,
}, },
) )
if mtype == cls.MediaType.FOOD and not scrobble_data.get( if mtype == cls.MediaType.FOOD and not scrobble_data.get("log", {}).get(
"log", {} "calories", None
).get("calories", None): ):
if media.calories: if media.calories:
scrobble_data["log"] = FoodLogData(calories=media.calories) scrobble_data["log"] = FoodLogData(calories=media.calories)
@ -1368,9 +1337,7 @@ class Scrobble(TimeStampedModel):
if existing_locations := location.in_proximity(named=True): if existing_locations := location.in_proximity(named=True):
existing_location = existing_locations.first() existing_location = existing_locations.first()
ts = int(pendulum.now().timestamp()) ts = int(pendulum.now().timestamp())
scrobble.log[ scrobble.log[ts] = f"Location {location.id} too close to this scrobble"
ts
] = f"Location {location.id} too close to this scrobble"
scrobble.save(update_fields=["log"]) scrobble.save(update_fields=["log"])
logger.info( logger.info(
f"[scrobbling] finished - found existing named location", f"[scrobbling] finished - found existing named location",
@ -1549,9 +1516,7 @@ class Scrobble(TimeStampedModel):
"""Returns true if our media is beyond our completion percent, unless """Returns true if our media is beyond our completion percent, unless
our type is geolocation in which case we always return false our type is geolocation in which case we always return false
""" """
beyond_completion = ( beyond_completion = self.percent_played >= self.media_obj.COMPLETION_PERCENT
self.percent_played >= self.media_obj.COMPLETION_PERCENT
)
if self.media_type == "GeoLocation": if self.media_type == "GeoLocation":
logger.info( logger.info(

View File

@ -29,7 +29,7 @@ from django.views.generic.list import ListView
from moods.models import Mood from moods.models import Mood
from music.aggregators import ( from music.aggregators import (
artist_scrobble_count, artist_scrobble_count,
live_charts, batch_live_charts,
live_tv_charts, live_tv_charts,
live_youtube_channel_charts, live_youtube_channel_charts,
scrobble_counts, scrobble_counts,
@ -794,22 +794,14 @@ class ChartRecordView(TemplateView):
"year": "This year", "year": "This year",
"all": "All time", "all": "All time",
} }
context_data["current_artist_charts"] = { context_data["current_artist_charts"] = batch_live_charts(
"today": list(live_charts(**artist, chart_period="today")), user=user, media_type="Artist", limit=limit
"last7": list(live_charts(**artist, chart_period="last7")), )
"last30": list(live_charts(**artist, chart_period="last30")),
"year": list(live_charts(**artist, chart_period="year")),
"all": list(live_charts(**artist)),
}
track = {"user": user, "media_type": "Track", "limit": limit} track = {"user": user, "media_type": "Track", "limit": limit}
context_data["current_track_charts"] = { context_data["current_track_charts"] = batch_live_charts(
"today": list(live_charts(**track, chart_period="today")), user=user, media_type="Track", limit=limit
"last7": list(live_charts(**track, chart_period="last7")), )
"last30": list(live_charts(**track, chart_period="last30")),
"year": list(live_charts(**track, chart_period="year")),
"all": list(live_charts(**track)),
}
now = timezone.now() now = timezone.now()
tzinfo = now.tzinfo tzinfo = now.tzinfo
@ -821,44 +813,53 @@ class ChartRecordView(TemplateView):
start_day_of_week = start_of_today - timedelta(days=now.isoweekday() % 7) start_day_of_week = start_of_today - timedelta(days=now.isoweekday() % 7)
start_day_of_month = now.replace(day=1) start_day_of_month = now.replace(day=1)
# TV Series Scrobbles # TV Series Scrobbles - fetch all and filter in Python
series_scrobbles = Scrobble.objects.filter( series_all = list(
user=user, Scrobble.objects.with_related()
video__video_type=Video.VideoType.TV_EPISODE, .filter(
played_to_completion=True, user=user,
video__video_type=Video.VideoType.TV_EPISODE,
played_to_completion=True,
timestamp__gte=start_day_of_month,
)
.order_by("-timestamp")
) )
context_data["series_this_week"] = series_scrobbles.filter( context_data["series_this_week"] = [
timestamp__gte=start_day_of_week s for s in series_all if s.timestamp >= start_day_of_week
).order_by("-timestamp") ]
context_data["series_this_month"] = series_scrobbles.filter( context_data["series_this_month"] = series_all
timestamp__gte=start_day_of_month
).order_by("-timestamp")
# Movie Scrobbles # Movie Scrobbles - fetch all and filter in Python
movie_scrobbles = Scrobble.objects.filter( movies_all = list(
user=user, Scrobble.objects.with_related()
video__video_type=Video.VideoType.MOVIE, .filter(
played_to_completion=True, user=user,
video__video_type=Video.VideoType.MOVIE,
played_to_completion=True,
timestamp__gte=start_day_of_month,
)
.order_by("-timestamp")
) )
context_data["videos_this_week"] = movie_scrobbles.filter( context_data["videos_this_week"] = [
timestamp__gte=start_day_of_week v for v in movies_all if v.timestamp >= start_day_of_week
).order_by("-timestamp") ]
context_data["videos_this_month"] = movie_scrobbles.filter( context_data["videos_this_month"] = movies_all
timestamp__gte=start_day_of_month
).order_by("-timestamp")
# YouTube Scrobbles # YouTube Scrobbles - fetch all and filter in Python
youtube_scrobbles = Scrobble.objects.filter( youtube_all = list(
user=user, Scrobble.objects.with_related()
video__video_type=Video.VideoType.YOUTUBE, .filter(
played_to_completion=True, user=user,
video__video_type=Video.VideoType.YOUTUBE,
played_to_completion=True,
timestamp__gte=start_day_of_month,
)
.order_by("-timestamp")
) )
context_data["youtube_this_week"] = youtube_scrobbles.filter( context_data["youtube_this_week"] = [
timestamp__gte=start_day_of_week y for y in youtube_all if y.timestamp >= start_day_of_week
).order_by("-timestamp") ]
context_data["youtube_this_month"] = youtube_scrobbles.filter( context_data["youtube_this_month"] = youtube_all
timestamp__gte=start_day_of_month
).order_by("-timestamp")
context_data["tv_show_charts"] = { context_data["tv_show_charts"] = {
"today": list(live_tv_charts(user=user, chart_period="today")), "today": list(live_tv_charts(user=user, chart_period="today")),