Skip to content

Instantly share code, notes, and snippets.

@SixBeeps
Last active January 27, 2025 14:20
Show Gist options
  • Save SixBeeps/1632ae049c93f34ddf0feec4c673066f to your computer and use it in GitHub Desktop.
Save SixBeeps/1632ae049c93f34ddf0feec4c673066f to your computer and use it in GitHub Desktop.
Mixxx Scrobbler
# Mixxx Scrobbler - SixBeeps 2024
# I can't be bothered to write a proper license, so do whatever you want with this code.
# Steps:
#
# 1. Grab an API key and secret, for Last.fm go to https://www.last.fm/api/account/create
# 2. Install the pylast library with `pip install pylast`
# 3. Create a .env file in the same directory as this script with the following contents
# API_KEY=your_api_key
# API_SECRET=your_api_secret
# NETWORK=lastfm OR librefm
# NETWORK_USERNAME=your_username
# NETWORK_PASSWORD=your_password (optional, if not provided, the script will ask for it)
# 4. (Optional) Create a file named regexes.json in the same directory as this script with the following contents
# {
# "artist": {
# "test_regex": "replace_regex"
# },
# "title": {
# "test_regex": "replace_regex"
# }
# }
# This file is used to replace artist and title names with regexes before scrobbling
# 5. Run the script with the cue file as an argument
# python mixxx_scrobbler.py /path/to/cuefile.cue
import getpass
import json
import os
import pylast
import re
import sys
import time
from dotenv import load_dotenv
# Load the environment variables
load_dotenv()
API_KEY = os.getenv("API_KEY")
API_SECRET = os.getenv("API_SECRET")
NETWORK_USERNAME = os.getenv("NETWORK_USERNAME")
USE_NETWORK = os.getenv("NETWORK") or "lastfm"
# Check if the environment variables are set correctly
if API_KEY is None or API_SECRET is None:
print("Please set the API_KEY and API_SECRET environment variables in the .env file.")
exit()
if len(sys.argv) < 2:
print("Please provide the cue file as a command line argument.")
exit()
if USE_NETWORK not in ["lastfm", "librefm"]:
print("NETWORK must be either lastfm or librefm.")
exit()
# Ask for the password
password = pylast.md5(os.getenv("NETWORK_PASSWORD"))
if password is None:
password = pylast.md5(getpass.getpass(f"Please enter the password for {NETWORK_USERNAME} on {USE_NETWORK}: "))
# Connect to the network
network = (pylast.LastFMNetwork(api_key=API_KEY, api_secret=API_SECRET, username=NETWORK_USERNAME, password_hash=password) if USE_NETWORK == "lastfm"
else pylast.LibreFMNetwork(api_key=API_KEY, api_secret=API_SECRET, username=NETWORK_USERNAME, password_hash=password) if USE_NETWORK == "librefm"
else None)
# Load the regexes
regexes = {
"artist": {},
"title": {}
}
if os.path.exists("regexes.json"):
with open("regexes.json", "r") as f:
regexes = json.load(f)
# Find the time when the cue file was created
# This is used to calculate the scrobble time
created = os.path.getctime(sys.argv[1])
# Parse the cue file
cue_file = sys.argv[1]
tracks = []
with open(cue_file, "r") as f:
lines = f.readlines()
title, artist, timestamp = None, None, None
def try_scrobble(title, artist, timestamp):
if title is not None and artist is not None:
# Apply regexes
for key in regexes["artist"]:
artist = re.sub(key, regexes["artist"][key], artist)
for key in regexes["title"]:
title = re.sub(key, regexes["title"][key], title)
# Scrobble the track
print(f"Scrobbling {artist} - {title} at {time.ctime(created + timestamp)}")
tracks.append({
"title": title,
"artist": artist,
"timestamp": int(created + timestamp)
})
for line in lines:
line = line.lstrip()
if line.startswith("TRACK"):
# Scrobble the previous track
try_scrobble(title, artist, timestamp)
title, artist, timestamp = None, None, None
elif line.startswith("TITLE"):
title = ' '.join(line.split(" ")[1:]).strip().replace('"', "")
elif line.startswith("PERFORMER"):
artist = ' '.join(line.split(" ")[1:]).strip().replace('"', "")
elif line.startswith("INDEX"):
# MM:SS:FF to Epoch time
friendly_time = line.split(" ")[2].strip().split(":")
timestamp = int(friendly_time[0]) * 60 + int(friendly_time[1]) + int(friendly_time[2]) / 75
# Scrobble the last track
try_scrobble(title, artist, timestamp)
# Scrobble the tracks
print(f"Scrobbling {len(tracks)} tracks...")
network.scrobble_many(tracks)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment