Skip to content

Instantly share code, notes, and snippets.

@0x8008
Created May 20, 2025 08:38
Show Gist options
  • Select an option

  • Save 0x8008/4ac766fcf20f64841b1128ab66f5d209 to your computer and use it in GitHub Desktop.

Select an option

Save 0x8008/4ac766fcf20f64841b1128ab66f5d209 to your computer and use it in GitHub Desktop.
Copy last.fm scrobbles to your account indefinitely
import time
import requests
import hashlib
import os
from datetime import datetime
# Last.fm API configuration
API_KEY = "" # Replace with your Last.fm API key
API_SECRET = "" # Replace with your Last.fm API secret
API_ROOT = "http://ws.audioscrobbler.com/2.0/"
# User configuration
SOURCE_USERNAME = "source_username" # Username to get scrobbles from
TARGET_AUTH_TOKEN = "your_auth_token" # Your authentication token
STORAGE_FILE = "last_scrobble.txt" # File to store the timestamp of the last scrobbled track
def get_auth_token():
"""
Instructions to get authentication token for the target account.
You need to manually follow these steps once.
"""
print(f"1. Visit: http://www.last.fm/api/auth?api_key={API_KEY}&cb=http://localhost:5555")
print("2. Allow access in your browser")
print("3. Copy the token from the resulting URL (http://localhost:5555/?token=YOUR_TOKEN)")
print("4. Use this token to authorize your script")
token = input("Enter the token: ")
# Get session key using the token
signature = f"api_key{API_KEY}methodauth.getSessiontoken{token}{API_SECRET}"
api_sig = hashlib.md5(signature.encode('utf-8')).hexdigest()
params = {
'method': 'auth.getSession',
'api_key': API_KEY,
'token': token,
'api_sig': api_sig,
'format': 'json'
}
response = requests.get(API_ROOT, params=params)
data = response.json()
if 'session' in data and 'key' in data['session']:
return data['session']['key']
else:
print("Failed to get session key:", data)
return None
def get_recent_tracks(username, limit=200, page=1, from_timestamp=None):
"""
Get recent tracks for a user with pagination and timestamp filtering
"""
params = {
'method': 'user.getRecentTracks',
'user': username,
'api_key': API_KEY,
'format': 'json',
'limit': limit,
'page': page
}
if from_timestamp:
params['from'] = from_timestamp
response = requests.get(API_ROOT, params=params)
return response.json()
def normalize_tracks(data):
"""
Normalize the 'track' field to always be a list
"""
if 'recenttracks' in data and 'track' in data['recenttracks']:
tracks = data['recenttracks']['track']
if isinstance(tracks, dict):
# Convert single track dict to a list with one item
data['recenttracks']['track'] = [tracks]
elif isinstance(tracks, str):
# Handle the case where 'track' is a string
print(f"Warning: 'track' field is a string: {tracks}")
data['recenttracks']['track'] = []
return data
def scrobble_track(session_key, artist, track, album="", timestamp=None):
"""
Scrobble a track to Last.fm
"""
if timestamp is None:
timestamp = int(time.time())
params = {
'method': 'track.scrobble',
'artist': artist,
'track': track,
'timestamp': timestamp,
'api_key': API_KEY,
'sk': session_key,
'format': 'json'
}
if album:
params['album'] = album
# Create API signature
signature = ''.join([f"{k}{params[k]}" for k in sorted(params.keys()) if k != 'format'])
signature = f"{signature}{API_SECRET}"
api_sig = hashlib.md5(signature.encode('utf-8')).hexdigest()
params['api_sig'] = api_sig
response = requests.post(API_ROOT, data=params)
return response.json()
def get_last_timestamp():
"""
Get the timestamp of the last scrobbled track from storage.
If no previous timestamp exists, use the current time as starting point.
"""
if os.path.exists(STORAGE_FILE):
with open(STORAGE_FILE, 'r') as f:
try:
return int(f.read().strip())
except:
# Use current time if file exists but is invalid
current_time = int(time.time())
save_last_timestamp(current_time)
return current_time
# Use current time as starting point if no file exists
current_time = int(time.time())
save_last_timestamp(current_time)
print(f"No previous timestamp found. Starting from current time: {datetime.fromtimestamp(current_time)}")
return current_time
def save_last_timestamp(timestamp):
"""
Save the timestamp of the last scrobbled track to storage
"""
with open(STORAGE_FILE, 'w') as f:
f.write(str(timestamp))
def continuous_transfer(source_username, session_key, poll_interval=60):
"""
Continuously monitor and transfer new scrobbles from source account to target account
"""
last_timestamp = get_last_timestamp()
scrobbles_today = 0
day_start = int(time.time()) // 86400 * 86400 # Start of current day
print(f"Starting continuous scrobble transfer from {source_username}...")
print(f"Only scrobbling tracks newer than: {datetime.fromtimestamp(last_timestamp)}")
while True:
try:
# Reset daily counter if we're in a new day
current_day_start = int(time.time()) // 86400 * 86400
if current_day_start > day_start:
day_start = current_day_start
scrobbles_today = 0
print(f"New day started. Scrobble counter reset.")
# Check if we've hit the daily limit
if scrobbles_today >= 2800:
wait_time = day_start + 86400 - int(time.time())
print(f"Daily scrobble limit reached (2800). Waiting {wait_time} seconds until midnight...")
time.sleep(wait_time)
continue
print(f"Checking for new scrobbles since {datetime.fromtimestamp(last_timestamp)}...")
data = get_recent_tracks(source_username, limit=200, from_timestamp=last_timestamp)
data = normalize_tracks(data) # Normalize the tracks data structure
if 'recenttracks' not in data or 'track' not in data['recenttracks']:
print("No recent tracks found or API error. Retrying in a minute...")
time.sleep(poll_interval)
continue
tracks = data['recenttracks']['track']
if not isinstance(tracks, list):
print(f"Unexpected type for 'track': {type(tracks)}. Retrying in a minute...")
time.sleep(poll_interval)
continue
if not tracks:
print("No new tracks to scrobble. Checking again in a minute...")
time.sleep(poll_interval)
continue
# Filter out currently playing track
tracks = [t for t in tracks if not (isinstance(t, dict) and '@attr' in t and 'nowplaying' in t['@attr'])]
if not tracks:
print("Only found currently playing track. Waiting for completed scrobbles...")
time.sleep(poll_interval)
continue
# Sort tracks by timestamp ascending
try:
tracks.sort(key=lambda t: int(t['date']['uts']) if isinstance(t, dict) and 'date' in t and 'uts' in t['date'] else 0)
except Exception as e:
print(f"Error sorting tracks: {e}")
print(f"Track data structure: {tracks}")
time.sleep(poll_interval)
continue
for track in tracks:
try:
if not isinstance(track, dict):
print(f"Skipping invalid track format: {track}")
continue
artist = track['artist']['#text'] if isinstance(track['artist'], dict) and '#text' in track['artist'] else str(track['artist'])
title = track['name']
album = track['album']['#text'] if 'album' in track and isinstance(track['album'], dict) and '#text' in track['album'] else ""
if 'date' in track and 'uts' in track['date']:
timestamp = int(track['date']['uts'])
else:
print("Track missing timestamp, skipping...")
continue
if timestamp <= last_timestamp:
continue
print(f"Scrobbling: {artist} - {title} ({album}) from {datetime.fromtimestamp(timestamp)}")
result = scrobble_track(session_key, artist, title, album, timestamp)
if 'scrobbles' in result:
print("Scrobble successful!")
last_timestamp = timestamp
save_last_timestamp(last_timestamp)
scrobbles_today += 1
else:
print(f"Scrobble failed: {result}")
# Be nice to the API
time.sleep(1)
except Exception as e:
print(f"Error processing track: {e}")
print(f"Track data: {track}")
continue
print(f"Processed all new tracks. Waiting {poll_interval} seconds before checking again...")
print(f"Scrobbles today: {scrobbles_today}/2800")
time.sleep(poll_interval)
except Exception as e:
print(f"Error occurred: {e}")
print("Waiting 5 minutes before retrying...")
time.sleep(300)
if __name__ == "__main__":
print("Last.fm Continuous Scrobble Transfer Tool")
print("---------------------------------------")
# Check if we need to get a new auth token
if not TARGET_AUTH_TOKEN or TARGET_AUTH_TOKEN == "your_auth_token":
print("You need to get an authentication token first.")
session_key = get_auth_token()
if session_key:
print(f"Your session key is: {session_key}")
print("Update the TARGET_AUTH_TOKEN in the script with this value.")
exit(0)
# Ask for source username if not set
if SOURCE_USERNAME == "source_username":
SOURCE_USERNAME = input("Enter the username to get scrobbles from: ")
# Ask for poll interval
poll_interval = input("How often to check for new scrobbles (in seconds, default: 60): ")
poll_interval = int(poll_interval) if poll_interval.isdigit() else 60
# Start the continuous transfer
continuous_transfer(SOURCE_USERNAME, TARGET_AUTH_TOKEN, poll_interval)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment