Compare commits

...

14 Commits
18.2 ... 18.8

23 changed files with 242 additions and 106 deletions

View File

@ -79,20 +79,7 @@ fetching and simple saving.
:LOGBOOK: :LOGBOOK:
CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20 CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20
:END: :END:
* Backlog [4/27] * Backlog [1/22]
** STRT Only create a LastFM import if there are files to import :vrobbler:feature:lastfm:importers:project:personal:
:PROPERTIES:
:ID: 7a1456af-c5f1-6385-b34f-0be24a6b65b0
:END:
- Note taken on [2025-07-20 Sun 16:21]
This thing is kicking my butt. As it stands it works, but the scrobbles are not assigned to the tracks properly.
** TODO Add timezone awarness to IMAP importer :personal:project:vrobbler:feature:importer:imap:timezones:
** DONE Track timezone changes for profiles :vrobbler:feature:profiles:personal:project:
:PROPERTIES:
:ID: 89ec867f-29fd-82f1-be17-b49dddc30c78
:END:
[2025-07-11 14:23]
** TODO [#A] Add classmethod for metadata fetching to tracks :vrobbler:feature:music:personal:project: ** TODO [#A] Add classmethod for metadata fetching to tracks :vrobbler:feature:music:personal:project:
:PROPERTIES: :PROPERTIES:
:ID: bc4b45e5-4c65-13c5-ab7b-1937d3fbf5c2 :ID: bc4b45e5-4c65-13c5-ab7b-1937d3fbf5c2
@ -456,6 +443,34 @@ it's annoying.
** TODO [#C] Allow users to see tasks on calendar view :vrobbler:personal:project:templates:feature: ** TODO [#C] Allow users to see tasks on calendar view :vrobbler:personal:project:templates:feature:
https://codepen.io/oliviale/pen/QYqybo https://codepen.io/oliviale/pen/QYqybo
** TODO [#C] Come up with a possible flow using WebDAV and super-productivity for tasks :personal:feature:project:vrobbler:tasks: ** TODO [#C] Come up with a possible flow using WebDAV and super-productivity for tasks :personal:feature:project:vrobbler:tasks:
* Version 18.7
** DONE Use the timezone history log to fix old Scrobbles that fall into those timezone blocks :vrobbler:chore:scrobbles:project:personal:
:PROPERTIES:
:ID: 9d055ac1-584b-20c8-7ad9-9ce36b329dc7
:END:
* Version 18.4
** DONE Track timezone changes for profiles :vrobbler:feature:profiles:personal:project:
:PROPERTIES:
:ID: 89ec867f-29fd-82f1-be17-b49dddc30c78
:END:
[2025-07-11 14:23]
** DONE Only create a LastFM import if there are files to import :vrobbler:feature:lastfm:importers:project:personal:
:PROPERTIES:
:ID: 7a1456af-c5f1-6385-b34f-0be24a6b65b0
:END:
- Note taken on [2025-07-20 Sun 16:21]
This thing is kicking my butt. As it stands it works, but the scrobbles are not assigned to the tracks properly.
* Version 18.3
** DONE Add timezone awarness to IMAP importer :personal:project:vrobbler:feature:importer:imap:timezones:
:PROPERTIES:
:ID: 05837b7c-96aa-6190-3678-e2ae7c7cac75
:END:
* Version 18
** DONE Condense tracks of the same title by the same artist with multiple albums :vrobbler:feature:music:project:personal:
:PROPERTIES:
:ID: b39fcec8-59fd-eab0-5809-b8144c7d2708
:END:
** DONE Import from BG stats a "learning" log field when "Learning to play" is in the comment :vrobbler:feature:boardgames:project:personal: ** DONE Import from BG stats a "learning" log field when "Learning to play" is in the comment :vrobbler:feature:boardgames:project:personal:
:PROPERTIES: :PROPERTIES:
:ID: fda59fab-4349-e99e-54c6-9f1392a1c474 :ID: fda59fab-4349-e99e-54c6-9f1392a1c474

View File

@ -3,16 +3,14 @@ import re
import sqlite3 import sqlite3
from datetime import datetime, timedelta from datetime import datetime, timedelta
from enum import Enum from enum import Enum
import pendulum
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import requests import requests
from books.constants import BOOKS_TITLES_TO_IGNORE from books.constants import BOOKS_TITLES_TO_IGNORE
from django.apps import apps from django.apps import apps
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from stream_sqlite import stream_sqlite
from scrobbles.notifications import NtfyNotification from scrobbles.notifications import NtfyNotification
from vrobbler.apps.profiles.utils import one_off_fix_colins_profile from stream_sqlite import stream_sqlite
from webdav.client import get_webdav_client from webdav.client import get_webdav_client
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -287,9 +285,6 @@ def build_scrobbles_from_book_map(
datetime.fromtimestamp(int(last_page.get("end_ts"))) datetime.fromtimestamp(int(last_page.get("end_ts")))
) )
if user.id == 1 and not user.profile.timezone_change_log:
one_off_fix_colins_profile(user.profile)
# Adjust for Daylight Saving Time # Adjust for Daylight Saving Time
if timestamp.dst() == timedelta( if timestamp.dst() == timedelta(
0 0

View File

@ -7,6 +7,7 @@ from profiles.models import UserProfile
class UserProfileAdmin(admin.ModelAdmin): class UserProfileAdmin(admin.ModelAdmin):
date_hierarchy = "created" date_hierarchy = "created"
ordering = ("-created",) ordering = ("-created",)
readonly_fields = ("timezone_change_log",)
exclude = ( exclude = (
"twitch_token", "twitch_token",
"twitch_client_secret", "twitch_client_secret",

View File

@ -16,6 +16,7 @@ BNULL = {"blank": True, "null": True}
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class UserProfile(TimeStampedModel): class UserProfile(TimeStampedModel):
user = models.OneToOneField( user = models.OneToOneField(
User, on_delete=models.CASCADE, related_name="profile" User, on_delete=models.CASCADE, related_name="profile"
@ -72,7 +73,10 @@ class UserProfile(TimeStampedModel):
old_instance = UserProfile.objects.get(pk=self.pk) old_instance = UserProfile.objects.get(pk=self.pk)
is_timezone_change = self.timezone != old_instance.timezone is_timezone_change = self.timezone != old_instance.timezone
if is_timezone_change: if is_timezone_change:
logger.info("Updating timezone changelog for user", extra={"profile_id": self.id}) logger.info(
"Updating timezone changelog for user",
extra={"profile_id": self.id},
)
previous_changes = old_instance.timezone_change_log previous_changes = old_instance.timezone_change_log
now = timezone.now().replace(microsecond=0) now = timezone.now().replace(microsecond=0)
new_log = f"{self.timezone} - {now}" new_log = f"{self.timezone} - {now}"
@ -98,7 +102,7 @@ class UserProfile(TimeStampedModel):
change_list = self.historic_timezone_changes change_list = self.historic_timezone_changes
for idx, start in enumerate(change_list): for idx, start in enumerate(change_list):
try: try:
end = change_list[idx+1] end = change_list[idx + 1]
except IndexError: except IndexError:
end = None end = None
@ -111,6 +115,28 @@ class UserProfile(TimeStampedModel):
return timestamp.replace(tzinfo=timezone) return timestamp.replace(tzinfo=timezone)
def adjust_timezone_of_scrobbles(self, commit=False):
current_dt = None
scrobbles_to_change_qs_list = []
for boundry_dt in self.historic_timezone_changes:
if current_dt and boundry_dt:
logger.info(
f"Checking for scrobbles between {current_dt} and {boundry_dt} to update to {current_dt.tzinfo.name}"
)
scrobbles = self.user.scrobble_set.filter(
timestamp__gte=current_dt,
timestamp__lt=boundry_dt,
).exclude(timezone=current_dt.tzinfo.name)
scrobbles_to_change_qs_list.append(scrobbles)
logger.info(
f"Updating {scrobbles.count()} scrobble timezones to {current_dt.tzinfo.name}"
)
if commit:
scrobbles.update(timezone=current_dt.tzinfo.name)
current_dt = boundry_dt
return scrobbles_to_change_qs_list
@cached_property @cached_property
def task_context_tags(self) -> list[str]: def task_context_tags(self) -> list[str]:
tag_list = [ tag_list = [

View File

@ -59,25 +59,27 @@ def start_of_year(dt, profile) -> datetime:
return start_of_day(dt, profile).replace(month=1, day=1) return start_of_day(dt, profile).replace(month=1, day=1)
def one_off_fix_colins_profile(profile): def fix_profile_historic_timezones(profile):
home_tz = "America/New_York" home_tz = "America/New_York"
europe = "2022-10-15" europe = "2023-10-15 06:00:00"
europe_end = "2023-12-16 12:00:00"
europe_tz = "Europe/Paris" europe_tz = "Europe/Paris"
europe_end = "2023-12-15"
washington = "2023-04-28" washington = "2024-04-28 06:00:00"
washington_end = "2024-05-04 12:00:00"
washington_tz = "America/Los_Angeles" washington_tz = "America/Los_Angeles"
washington_end = "2023-05-04"
camp = "2024-08-04" camp = "2024-08-04 17:00:00"
camp_end = "2024-08-10" camp_end = "2024-08-10 12:00:00"
camp_tz = "America/Halifax" camp_tz = "America/Halifax"
summer = "2025-07-09 12:00:00" summer = "2025-07-09 06:00:00"
summer_end = "2025-07-13 20:30:00" summer_end = "2025-07-11 23:30:00"
summer_tz = "America/Los_Angeles" summer_tz = "America/Los_Angeles"
profile.timezone_change_log = None
profile.timezone_change_log = "" profile.timezone_change_log = ""
profile.timezone_change_log += f"{europe_tz} - {pendulum.parse(europe)}\n" profile.timezone_change_log += f"{europe_tz} - {pendulum.parse(europe)}\n"
profile.timezone_change_log += ( profile.timezone_change_log += (

View File

@ -102,7 +102,7 @@ class ChartRecordAdmin(admin.ModelAdmin):
@admin.register(Scrobble) @admin.register(Scrobble)
class ScrobbleAdmin(admin.ModelAdmin): class ScrobbleAdmin(admin.ModelAdmin):
# date_hierarchy = "timestamp" date_hierarchy = "timestamp"
list_display = ( list_display = (
"timestamp", "timestamp",
"media_name", "media_name",
@ -112,6 +112,7 @@ class ScrobbleAdmin(admin.ModelAdmin):
"in_progress", "in_progress",
"is_paused", "is_paused",
"played_to_completion", "played_to_completion",
"user",
) )
raw_id_fields = ( raw_id_fields = (
"video", "video",
@ -140,6 +141,7 @@ class ScrobbleAdmin(admin.ModelAdmin):
"long_play_complete", "long_play_complete",
"source", "source",
"timezone", "timezone",
"user",
) )
ordering = ("-timestamp",) ordering = ("-timestamp",)
@ -148,3 +150,7 @@ class ScrobbleAdmin(admin.ModelAdmin):
def playback_percent(self, obj): def playback_percent(self, obj):
return obj.percent_played return obj.percent_played
def get_queryset(self, request):
qs = super().get_queryset(request).exclude(timestamp__year=None)
return qs

View File

@ -50,20 +50,22 @@ class LastFM:
enrich=True, enrich=True,
) )
timezone = settings.TIME_ZONE timestamp = self.vrobbler_user.profile.get_timestamp_with_tz(
if self.vrobbler_user.profile: lfm_scrobble.get("timestamp")
timezone = self.vrobbler_user.profile.timezone )
stop_timestamp = timestamp + timedelta(
timestamp = lfm_scrobble.get("timestamp") seconds=track.run_time_seconds
)
new_scrobble = Scrobble( new_scrobble = Scrobble(
user=self.vrobbler_user, user=self.vrobbler_user,
timestamp=timestamp, timestamp=timestamp,
stop_timestamp=stop_timestamp,
source=source, source=source,
track=track, track=track,
timezone=timezone,
played_to_completion=True, played_to_completion=True,
in_progress=False, in_progress=False,
media_type=Scrobble.MediaType.TRACK, media_type=Scrobble.MediaType.TRACK,
timezone=timestamp.tzinfo.name,
) )
# Vrobbler scrobbles on finish, LastFM scrobbles on start # Vrobbler scrobbles on finish, LastFM scrobbles on start
seconds_eariler = timestamp - timedelta(seconds=20) seconds_eariler = timestamp - timedelta(seconds=20)
@ -91,7 +93,7 @@ class LastFM:
) )
return created return created
def get_last_scrobbles(self, time_from=None, time_to=None): def get_last_scrobbles(self, time_from=None, time_to=None, check=False):
"""Given a user, Last.fm api key, and secret key, grab a list of scrobbled """Given a user, Last.fm api key, and secret key, grab a list of scrobbled
tracks""" tracks"""
lfm_params = {} lfm_params = {}
@ -107,6 +109,9 @@ class LastFM:
found_scrobbles = self.user.get_recent_tracks(**lfm_params) found_scrobbles = self.user.get_recent_tracks(**lfm_params)
# TOOD spin this out into a celery task over certain threshold of found scrobbles? # TOOD spin this out into a celery task over certain threshold of found scrobbles?
if check and found_scrobbles:
return True
for scrobble in found_scrobbles: for scrobble in found_scrobbles:
logger.info(f"Processing {scrobble}") logger.info(f"Processing {scrobble}")
run_time = None run_time = None

View File

@ -1,9 +1,11 @@
import logging
from books.koreader import fetch_file_from_webdav from books.koreader import fetch_file_from_webdav
from profiles.models import UserProfile from profiles.models import UserProfile
from scrobbles.models import KoReaderImport from scrobbles.models import KoReaderImport
from scrobbles.tasks import process_koreader_import from scrobbles.tasks import process_koreader_import
from scrobbles.utils import get_file_md5_hash
from webdav.client import get_webdav_client from webdav.client import get_webdav_client
import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -11,7 +13,7 @@ logger = logging.getLogger(__name__)
def import_from_webdav_for_all_users(restart=False): def import_from_webdav_for_all_users(restart=False):
"""Grab a list of all users with WebDAV enabled and kickoff imports for them""" """Grab a list of all users with WebDAV enabled and kickoff imports for them"""
# LastFmImport = apps.get_model("scrobbles", "LastFMImport") # WebDavImport = apps.get_model("scrobbles", "WebDavImport")
webdav_enabled_user_ids = UserProfile.objects.filter( webdav_enabled_user_ids = UserProfile.objects.filter(
webdav_url__isnull=False, webdav_url__isnull=False,
webdav_user__isnull=False, webdav_user__isnull=False,
@ -42,7 +44,7 @@ def import_from_webdav_for_all_users(restart=False):
KoReaderImport.objects.filter( KoReaderImport.objects.filter(
user_id=user_id, processed_finished__isnull=False user_id=user_id, processed_finished__isnull=False
) )
.order_by("Processed_finished") .order_by("processed_finished")
.last() .last()
) )

View File

@ -33,6 +33,7 @@ from profiles.utils import (
end_of_day, end_of_day,
end_of_month, end_of_month,
end_of_week, end_of_week,
fix_profile_historic_timezones,
start_of_day, start_of_day,
start_of_month, start_of_month,
start_of_week, start_of_week,
@ -205,6 +206,9 @@ class KoReaderImport(BaseFileImportMixin):
def process(self, force=False): def process(self, force=False):
if self.user.id == 1:
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}"
@ -250,6 +254,9 @@ class AudioScrobblerTSVImport(BaseFileImportMixin):
def process(self, force=False): def process(self, force=False):
from scrobbles.importers.tsv import import_audioscrobbler_tsv_file from scrobbles.importers.tsv import import_audioscrobbler_tsv_file
if self.user.id == 1:
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}"
@ -280,6 +287,10 @@ class LastFmImport(BaseFileImportMixin):
def process(self, import_all=False): def process(self, import_all=False):
"""Import scrobbles found on LastFM""" """Import scrobbles found on LastFM"""
if self.user.id == 1:
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}"
@ -327,6 +338,9 @@ class RetroarchImport(BaseFileImportMixin):
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"""
if self.user.id == 1:
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}"
@ -1087,6 +1101,7 @@ class Scrobble(TimeStampedModel):
key = media_class_to_foreign_key(media.__class__.__name__) key = media_class_to_foreign_key(media.__class__.__name__)
media_query = models.Q(**{key: media}) media_query = models.Q(**{key: media})
scrobble_data[key + "_id"] = media.id scrobble_data[key + "_id"] = media.id
skip_in_progress_check = kwargs.get("skip_in_progress_check", False)
# Find our last scrobble of this media item (track, video, etc) # Find our last scrobble of this media item (track, video, etc)
scrobble = ( scrobble = (
@ -1110,27 +1125,31 @@ class Scrobble(TimeStampedModel):
) )
return scrobble return scrobble
logger.info( if not skip_in_progress_check:
f"[create_or_update] check for existing scrobble to update ", logger.info(
extra={ f"[create_or_update] check for existing scrobble to update ",
"scrobble_id": scrobble.id if scrobble else None, extra={
"media_type": mtype, "scrobble_id": scrobble.id if scrobble else None,
"media_id": media.id, "media_type": mtype,
"scrobble_data": scrobble_data, "media_id": media.id,
}, "scrobble_data": scrobble_data,
) },
scrobble_data["playback_status"] = scrobble_data.pop("status", None) )
# If it's marked as stopped, send it through our update mechanism, which will complete it scrobble_data["playback_status"] = scrobble_data.pop(
if scrobble and ( "status", None
scrobble.can_be_updated )
or scrobble_data["playback_status"] == "stopped" # If it's marked as stopped, send it through our update mechanism, which will complete it
): if scrobble and (
if "log" in scrobble_data.keys() and scrobble.log: scrobble.can_be_updated
scrobble_data["log"] = scrobble.log | scrobble_data["log"] or scrobble_data["playback_status"] == "stopped"
return scrobble.update(scrobble_data) ):
if "log" in scrobble_data.keys() and scrobble.log:
scrobble_data["log"] = scrobble.log | scrobble_data["log"]
return scrobble.update(scrobble_data)
# Discard status before creating
scrobble_data.pop("playback_status")
# Discard status before creating
scrobble_data.pop("playback_status")
logger.info( logger.info(
f"[scrobbling] creating new scrobble", f"[scrobbling] creating new scrobble",
extra={ extra={

View File

@ -18,6 +18,7 @@ from music.models import Track
from people.models import Person from people.models import Person
from podcasts.models import PodcastEpisode from podcasts.models import PodcastEpisode
from podcasts.utils import parse_mopidy_uri from podcasts.utils import parse_mopidy_uri
from profiles.models import UserProfile
from puzzles.models import Puzzle from puzzles.models import Puzzle
from scrobbles.constants import ( from scrobbles.constants import (
JELLYFIN_AUDIO_ITEM_TYPES, JELLYFIN_AUDIO_ITEM_TYPES,
@ -25,6 +26,7 @@ from scrobbles.constants import (
SCROBBLE_CONTENT_URLS, SCROBBLE_CONTENT_URLS,
) )
from scrobbles.models import Scrobble from scrobbles.models import Scrobble
from scrobbles.notifications import NtfyNotification
from scrobbles.utils import convert_to_seconds, extract_domain from scrobbles.utils import convert_to_seconds, extract_domain
from sports.models import SportEvent from sports.models import SportEvent
from sports.thesportsdb import lookup_event_from_thesportsdb from sports.thesportsdb import lookup_event_from_thesportsdb
@ -390,10 +392,25 @@ def email_scrobble_board_game(
locations[location_dict.get("id")] = location locations[location_dict.get("id")] = location
scrobbles_created = [] scrobbles_created = []
second = 0
for play_dict in bgstat_data.get("plays", []): for play_dict in bgstat_data.get("plays", []):
hour = None
minute = None
second = None
if "comments" in play_dict.keys(): if "comments" in play_dict.keys():
if "Learning to play" in play_dict.get("comments"): for line in play_dict.get("comments", "").split("\n"):
log_data["learning"] = True if "Learning to play" in line:
log_data["learning"] = True
if "Start time:" in line:
start_time = line.split(": ")[1]
pieces = start_time.split(":")
hour = int(pieces[0])
minute = int(pieces[1])
try:
second = int(pieces[2])
except IndexError:
second = 0
log_data["details"] = play_dict.get("comments") log_data["details"] = play_dict.get("comments")
log_data["expansion_ids"] = [] log_data["expansion_ids"] = []
try: try:
@ -402,7 +419,6 @@ def email_scrobble_board_game(
try: try:
base_game = expansions[play_dict.get("gameRefId")] base_game = expansions[play_dict.get("gameRefId")]
except KeyError: except KeyError:
print(play_dict)
logger.info( logger.info(
"Skipping scrobble of play, can't find game", "Skipping scrobble of play, can't find game",
extra={"play_dict": play_dict}, extra={"play_dict": play_dict},
@ -440,35 +456,56 @@ def email_scrobble_board_game(
} }
) )
start = parse(play_dict.get("playDate")) timestamp = parse(play_dict.get("playDate"))
if hour and minute:
logger.info(f"Scrobble playDate has manual start time {timestamp}")
timestamp = timestamp.replace(
hour=hour, minute=minute, second=second or 0
)
logger.info(f"Update to {timestamp}")
profile = UserProfile.objects.filter(user_id=user_id).first()
timestamp = profile.get_timestamp_with_tz(timestamp)
if play_dict.get("durationMin") > 0: if play_dict.get("durationMin") > 0:
duration_seconds = play_dict.get("durationMin") * 60 duration_seconds = play_dict.get("durationMin") * 60
else: else:
duration_seconds = base_game.run_time_seconds duration_seconds = base_game.run_time_seconds
stop = start + timedelta(seconds=duration_seconds) stop_timestamp = timestamp + timedelta(seconds=duration_seconds)
logger.info(f"Creating scrobble for {base_game} at {timestamp}")
scrobble_dict = { scrobble_dict = {
"user_id": user_id, "user_id": user_id,
"timestamp": start, "timestamp": timestamp,
"playback_position_seconds": duration_seconds, "playback_position_seconds": duration_seconds,
"source": "BG Stats", "source": "BG Stats",
"log": log_data, "log": log_data,
} }
scrobble = Scrobble.objects.filter( scrobble = None
board_game=base_game, user_id=user_id, timestamp=start if timestamp.year > 2023:
).first() logger.info(
"Scrobbles older than 2024 likely have no time associated just create it"
)
scrobble = Scrobble.objects.filter(
board_game=base_game, user_id=user_id, timestamp=timestamp
).first()
if scrobble: if scrobble:
logger.info( logger.info(
"Scrobble already exists, skipping", "Scrobble already exists, skipping",
extra={"scrobble_dict": scrobble_dict, "user_id": user_id}, extra={"scrobble_dict": scrobble_dict, "user_id": user_id},
) )
continue continue
scrobble = Scrobble.create_or_update(base_game, user_id, scrobble_dict) scrobble = Scrobble.create_or_update(
scrobble.stop_timestamp = stop base_game, user_id, scrobble_dict, skip_in_progress_check=True
)
scrobble.timezone = timestamp.tzinfo.name
scrobble.stop_timestamp = stop_timestamp
scrobble.in_progress = False scrobble.in_progress = False
scrobble.played_to_completion = True scrobble.played_to_completion = True
scrobble.save() scrobble.save()
scrobbles_created.append(scrobble) scrobbles_created.append(scrobble)
NtfyNotification(scrobble).send()
return scrobbles_created return scrobbles_created

View File

@ -114,6 +114,8 @@ def get_long_plays_completed(user: User) -> list:
def import_lastfm_for_all_users(restart=False): def import_lastfm_for_all_users(restart=False):
"""Grab a list of all users with LastFM enabled and kickoff imports for them""" """Grab a list of all users with LastFM enabled and kickoff imports for them"""
from scrobbles.importers.lastfm import LastFM
LastFmImport = apps.get_model("scrobbles", "LastFMImport") LastFmImport = apps.get_model("scrobbles", "LastFMImport")
lastfm_enabled_user_ids = UserProfile.objects.filter( lastfm_enabled_user_ids = UserProfile.objects.filter(
lastfm_username__isnull=False, lastfm_username__isnull=False,
@ -124,6 +126,31 @@ def import_lastfm_for_all_users(restart=False):
lastfm_import_count = 0 lastfm_import_count = 0
for user_id in lastfm_enabled_user_ids: for user_id in lastfm_enabled_user_ids:
lfm_import = LastFmImport.objects.filter(
user_id=user_id, processed_finished__isnull=False
).last()
if lfm_import:
last_processed = lfm_import.processed_finished
else:
logger.info(
f"Not resuming failed LastFM import {lfm_import.id} for user {user_id}, use restart=True to restart"
"No existing LastFM import, we should start a monthly parsing of lastFm for this user going back to 2002"
)
continue
lfm_client = LastFM(
user=get_user_model().objects.filter(id=user_id).first()
)
has_scrobbles = lfm_client.get_last_scrobbles(
time_from=last_processed, check=True
)
if not has_scrobbles:
logger.info("No new scrobbles to import from LastFM")
continue
lfm_import, created = LastFmImport.objects.get_or_create( lfm_import, created = LastFmImport.objects.get_or_create(
user_id=user_id, processed_finished__isnull=True user_id=user_id, processed_finished__isnull=True
) )

View File

@ -64,15 +64,16 @@ class ScrobbleableListView(ListView):
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
user_filter = Q()
if not self.request.user.is_anonymous: if not self.request.user.is_anonymous:
queryset = queryset.annotate( user_filter = Q(scrobble__user=self.request.user)
queryset = (
queryset.annotate(
scrobble_count=Count("scrobble"), scrobble_count=Count("scrobble"),
filter=Q(scrobble__user=self.request.user), )
).order_by("-scrobble_count") .filter(user_filter, scrobble_count__gt=0)
else: .order_by("-scrobble_count")
queryset = queryset.annotate( )
scrobble_count=Count("scrobble")
).order_by("-scrobble_count")
return queryset return queryset
@ -86,7 +87,7 @@ class ScrobbleableDetailView(DetailView):
if not self.request.user.is_anonymous: if not self.request.user.is_anonymous:
context_data["scrobbles"] = self.object.scrobble_set.filter( context_data["scrobbles"] = self.object.scrobble_set.filter(
user=self.request.user user=self.request.user
) ).order_by("-timestamp")
return context_data return context_data
@ -201,7 +202,7 @@ class RecentScrobbleList(ListView):
processed_finished__isnull=True, processed_finished__isnull=True,
user=self.request.user, user=self.request.user,
) )
data["counts"] = [] #scrobble_counts(user) data["counts"] = [] # scrobble_counts(user)
else: else:
data["weekly_data"] = week_of_scrobbles() data["weekly_data"] = week_of_scrobbles()
data["counts"] = scrobble_counts() data["counts"] = scrobble_counts()

View File

@ -39,7 +39,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<p>{{object.scrobble_set.count}} scrobbles</p> <p>{{scrobbles.count}} scrobbles</p>
<p> <p>
<a href="{{object.start_url}}">Drink again</a> <a href="{{object.start_url}}">Drink again</a>
</p> </p>
@ -55,7 +55,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %} {% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %}
<tr> <tr>
<td>{{scrobble.local_timestamp}}</td> <td>{{scrobble.local_timestamp}}</td>
</tr> </tr>

View File

@ -42,7 +42,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<p>{{object.scrobble_set.count}} scrobbles</p> <p>{{scrobbles.count}} scrobbles</p>
<p> <p>
<a href="{{object.start_url}}">Play again</a> <a href="{{object.start_url}}">Play again</a>
</p> </p>
@ -60,7 +60,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %} {% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %}
<tr> <tr>
<td>{{scrobble.local_timestamp}}</td> <td>{{scrobble.local_timestamp}}</td>
<td>{{scrobble.media_obj.publisher}}</td> <td>{{scrobble.media_obj.publisher}}</td>

View File

@ -26,10 +26,10 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<p>{{object.scrobble_set.count}} scrobbles</p> <p>{{scrobbles.count}} scrobbles</p>
<p>Read {{object.scrobble_set.last.book_pages_read}} pages{% if object.scrobble_set.last.long_play_complete %} and completed{% else %}{% endif %}</p> <p>Read {{scrobbles.last.book_pages_read}} pages{% if scrobbles.last.long_play_complete %} and completed{% else %}{% endif %}</p>
<p> <p>
{% if object.scrobble_set.last.long_play_complete == True %} {% if scrobbles.last.long_play_complete == True %}
<a href="">Read again</a> <a href="">Read again</a>
{% else %} {% else %}
<a href="">Resume reading</a> <a href="">Resume reading</a>
@ -50,7 +50,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %} {% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %}
<tr> <tr>
<td>{{scrobble.local_timestamp}}</td> <td>{{scrobble.local_timestamp}}</td>
<td>{% if scrobble.long_play_complete == True %}Yes{% endif %}</td> <td>{% if scrobble.long_play_complete == True %}Yes{% endif %}</td>

View File

@ -48,7 +48,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<p>{{object.scrobble_set.count}} scrobbles</p> <p>{{scrobbles.count}} scrobbles</p>
<div class="row"> <div class="row">
<div class="col-md"> <div class="col-md">
<h3>Last scrobbles</h3> <h3>Last scrobbles</h3>
@ -60,7 +60,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %} {% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %}
<tr> <tr>
<td>{{scrobble.local_timestamp|naturaltime}}</td> <td>{{scrobble.local_timestamp|naturaltime}}</td>
</tr> </tr>

View File

@ -26,7 +26,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for scrobble in object.scrobble_set.all %} {% for scrobble in scrobbles.all %}
<tr> <tr>
<td>{{scrobble.local_timestamp}}</td> <td>{{scrobble.local_timestamp}}</td>
</tr> </tr>

View File

@ -9,7 +9,7 @@
{% endif %} {% endif %}
</div> </div>
<div class="row"> <div class="row">
<p>{{object.scrobble_set.count}} scrobbles</p> <p>{{scrobbles.count}} scrobbles</p>
{% if charts %} {% if charts %}
<p>{% for chart in charts %}<em><a href="{{chart.link}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p> <p>{% for chart in charts %}<em><a href="{{chart.link}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
{% endif %} {% endif %}
@ -26,7 +26,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for scrobble in object.scrobble_set.all %} {% for scrobble in scrobbles.all %}
<tr> <tr>
<td>{{scrobble.local_timestamp}}</td> <td>{{scrobble.local_timestamp}}</td>
<td><a href="{{scrobble.track.get_absolute_url}}">{{scrobble.track.title}}</a></td> <td><a href="{{scrobble.track.get_absolute_url}}">{{scrobble.track.title}}</a></td>

View File

@ -18,7 +18,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for scrobble in object.scrobble_set.all %} {% for scrobble in scrobbles.all %}
<tr> <tr>
<td>{{scrobble.local_timestamp}}</td> <td>{{scrobble.local_timestamp}}</td>
<td>{{scrobble.media_obj.round.season.name}}</td> <td>{{scrobble.media_obj.round.season.name}}</td>

View File

@ -39,7 +39,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<p>{{object.scrobble_set.count}} scrobbles</p> <p>{{scrobbles.count}} scrobbles</p>
<p> <p>
<a href="{{object.start_url}}">Play again</a> <a href="{{object.start_url}}">Play again</a>
</p> </p>
@ -57,7 +57,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %} {% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %}
<tr> <tr>
<td>{{scrobble.local_timestamp}}</td> <td>{{scrobble.local_timestamp}}</td>
<td><a href="{{scrobble.get_media_source_url}}">{{scrobble.logdata.description}}</a></td> <td><a href="{{scrobble.get_media_source_url}}">{{scrobble.logdata.description}}</a></td>

View File

@ -59,12 +59,12 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<p>{{object.scrobble_set.count}} scrobbles</p> <p>{{scrobbles.count}} scrobbles</p>
{% if object.scrobble_set.last.long_play_seconds %} {% if scrobbles.last.long_play_seconds %}
<p>{{object.scrobble_set.last.long_play_seconds|natural_duration}}{% if object.scrobble_set.last.long_play_complete %} and completed{% else %} spent playing{% endif %}</p> <p>{{scrobbles.last.long_play_seconds|natural_duration}}{% if scrobbles.last.long_play_complete %} and completed{% else %} spent playing{% endif %}</p>
{% endif %} {% endif %}
<p> <p>
{% if object.scrobble_set.last.long_play_complete == True %} {% if scrobbles.last.long_play_complete == True %}
<a href="">Play again</a> <a href="">Play again</a>
{% else %} {% else %}
<a href="{{object.start_url}}">Resume playing</a> <a href="{{object.start_url}}">Resume playing</a>
@ -86,7 +86,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %} {% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %}
<tr> <tr>
<td>{{scrobble.local-timestamp}}</td> <td>{{scrobble.local-timestamp}}</td>
<td>{% if scrobble.long_play_complete == True %}Yes{% else %}Not yet{% endif %}</td> <td>{% if scrobble.long_play_complete == True %}Yes{% else %}Not yet{% endif %}</td>

View File

@ -85,7 +85,7 @@ dd {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for scrobble in object.scrobble_set.all %} {% for scrobble in scrobbles.all %}
<tr> <tr>
<td>{{scrobble.local_timestamp}}</td> <td>{{scrobble.local_timestamp}}</td>
</tr> </tr>

View File

@ -51,7 +51,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for scrobble in object.scrobble_set.all %} {% for scrobble in scrobbles.all %}
<tr> <tr>
<td>{{scrobble.local_timestamp}}</td> <td>{{scrobble.local_timestamp}}</td>
</tr> </tr>