Files
vrobbler/vrobbler/apps/scrobbles/importers/lastfm.py
Colin Powell a7551ef162 [music] Weird hack to get timezone for LFM scrobbles
Last.fm seems to send timestamps for scrobbles with a timezone of UTC
but the actual timezone is already localized. But that means we can't
extract the timezone we want, even though the timestamp is already in
the right timezone for storage.
2025-07-28 10:52:02 -04:00

170 lines
5.9 KiB
Python

import logging
from datetime import datetime, timedelta
import pylast
import pytz
from django.conf import settings
from music.models import Track
logger = logging.getLogger(__name__)
PYLAST_ERRORS = tuple(
getattr(pylast, exc_name)
for exc_name in (
"ScrobblingError",
"NetworkError",
"MalformedResponseError",
"WSError",
)
if hasattr(pylast, exc_name)
)
class LastFM:
def __init__(self, user):
try:
self.client = pylast.LastFMNetwork(
api_key=getattr(settings, "LASTFM_API_KEY"),
api_secret=getattr(settings, "LASTFM_SECRET_KEY"),
username=user.profile.lastfm_username,
password_hash=pylast.md5(user.profile.lastfm_password),
)
self.user = self.client.get_user(user.profile.lastfm_username)
self.vrobbler_user = user
except PYLAST_ERRORS as e:
logger.error(f"Error during Last.fm setup: {e}")
def import_from_lastfm(self, last_processed=None):
"""Given a last processed time, import all scrobbles from LastFM since then"""
from scrobbles.models import Scrobble
new_scrobbles = []
source = "Last.fm"
lastfm_scrobbles = self.get_last_scrobbles(time_from=last_processed)
for lfm_scrobble in lastfm_scrobbles:
track = Track.find_or_create(
title=lfm_scrobble.get("title"),
artist_name=lfm_scrobble.get("artist"),
album_name=lfm_scrobble.get("album"),
enrich=True,
)
tz_timestamp = self.vrobbler_user.profile.get_timestamp_with_tz(
lfm_scrobble.get("timestamp")
)
timestamp = lfm_scrobble.get("timestamp")
stop_timestamp = timestamp + timedelta(
seconds=track.run_time_seconds
)
new_scrobble = Scrobble(
user=self.vrobbler_user,
timestamp=timestamp,
stop_timestamp=stop_timestamp,
source=source,
track=track,
played_to_completion=True,
in_progress=False,
media_type=Scrobble.MediaType.TRACK,
timezone=tz_timestamp.tzinfo.name,
)
# Vrobbler scrobbles on finish, LastFM scrobbles on start
seconds_eariler = timestamp - timedelta(seconds=20)
seconds_later = timestamp + timedelta(seconds=20)
existing = Scrobble.objects.filter(
created__gte=seconds_eariler,
created__lte=seconds_later,
track=track,
).first()
if existing:
logger.debug(f"Skipping existing scrobble {new_scrobble}")
continue
logger.debug(f"Queued scrobble {new_scrobble} for creation")
new_scrobbles.append(new_scrobble)
created = Scrobble.objects.bulk_create(new_scrobbles)
# TODO Add a notification for users that their import is complete
logger.info(
f"Last.fm import fnished",
extra={
"scrobbles_created": len(created),
"user_id": self.vrobbler_user,
"lastfm_user": self.user,
},
)
return created
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
tracks"""
lfm_params = {}
scrobbles = []
if time_from:
lfm_params["time_from"] = int(time_from.timestamp())
if time_to:
lfm_params["time_to"] = int(time_to.timestamp())
# if not time_from and not time_to:
lfm_params["limit"] = None
found_scrobbles = self.user.get_recent_tracks(**lfm_params)
# 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:
logger.info(f"Processing {scrobble}")
run_time = None
mbid = None
artist = None
log_dict = {"scrobble": scrobble}
try:
run_time = int(scrobble.track.get_duration() / 1000)
mbid = scrobble.track.get_mbid()
artist = scrobble.track.get_artist().name
log_dict["artist"] = artist
log_dict["mbid"] = mbid
log_dict["run_time"] = run_time
except pylast.MalformedResponseError as e:
logger.warning(e)
except pylast.WSError as e:
logger.info(
"LastFM barfed trying to get the track for {scrobble.track}",
extra=log_dict,
)
except pylast.NetworkError as e:
logger.info(
"LastFM barfed trying to get the track for {scrobble.track}",
extra=log_dict,
)
if not artist:
logger.info(
f"Silly LastFM, no artist found for scrobble",
extra=log_dict,
)
continue
# TODO figure out if this will actually work
# timestamp = datetime.fromtimestamp(int(scrobble.timestamp), UTC)
timestamp = datetime.utcfromtimestamp(
int(scrobble.timestamp)
).replace(tzinfo=pytz.utc)
logger.info(
f"Scrobble appended to list for bulk create", extra=log_dict
)
scrobbles.append(
{
"artist": artist,
"album": scrobble.album,
"title": scrobble.track.title,
"mbid": mbid,
"run_time_seconds": run_time,
"timestamp": timestamp,
}
)
return scrobbles