[scrobbles] Improve query efficency
This commit is contained in:
@ -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,
|
||||||
|
}
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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")),
|
||||||
|
|||||||
Reference in New Issue
Block a user