Skip to content

Instantly share code, notes, and snippets.

@daryltucker
Created December 30, 2018 22:37
Show Gist options
  • Save daryltucker/3d1129289a93462fcd703b7c2916a058 to your computer and use it in GitHub Desktop.
Save daryltucker/3d1129289a93462fcd703b7c2916a058 to your computer and use it in GitHub Desktop.
Scrobble [cold media] to Last.fm
#!/usr/bin/env python3
# Neo-Retro Group
# @daryltucker
'''
Scrobble albums, waiting the duration of each track and setting Now Playing.
Scrobble single track.
Helps automate Scrobbling of cold media (CD Player/Turntable)
'''
import sys
import os
import select
import time
import pylast
import argparse
import json
##############################################################################
# argparse
##############################################################################
parser = argparse.ArgumentParser(description="Scrobble [cold media] to Last.fm")
parser.add_argument('artist', help="Artist", nargs='?')
parser.add_argument('target', help="Track or Album", nargs='?')
# parser.add_argument('--service', help="Select Last.fm or Libre.fm")
parser.add_argument(
'--not-playing', help="Disable Now Playing",
dest="now_playing", action='store_false'
)
parser.add_argument(
'--debug', help="Noisy, Debug Output",
dest="debug", action='store_true'
)
parser.add_argument(
'--justdoit',
help="Instead of waiting track duration, immediately Scrobble track.",
dest="justdoit", action='store_true'
)
parser.add_argument(
'--love', help="Love Artist - Track/Album",
dest='love', action='store_true'
)
# Scrobble Track, Load Album
group = parser.add_mutually_exclusive_group()
group.add_argument(
'-s', '--scrobble', help="Scrobble Artist - Track",
dest='scrobble', action='store_true'
)
group.add_argument(
'-a', '--album', help="Load Artist - Album for interactive Scrobbling",
dest='interactive', action='store_true'
)
args = parser.parse_args()
''''''
##############################################################################
# Configuration / Initialization
##############################################################################
# ${HOME}/.bashrc Last.fm Configuration
# https://www.last.fm/api/authentication
# https://www.last.fm/api/account/create
'''
export LASTFM_API_KEY=""
export LASTFM_API_SECRET=""
export LASTFM_USERNAME=""
export LASTFM_PASSHASH=""
'''
# Pull LastFM details from Environment
LASTFM_API_KEY = os.environ.get('LASTFM_API_KEY')
LASTFM_API_SECRET = os.environ.get('LASTFM_API_SECRET')
LASTFM_USERNAME = os.environ.get('LASTFM_USERNAME')
LASTFM_PASSHASH = os.environ.get('LASTFM_PASSHASH')
# Tag added to Scrobble'd tracks
SCROBBLE_TAG = "cold-media"
# SCROBBLE_TAG = "" # Disable tagging
# Storage for Loved Tracks
LOVED_TRACKS = []
# TODO Allow selection of Libre.fm
session = pylast.LastFMNetwork(
api_key=LASTFM_API_KEY, api_secret=LASTFM_API_SECRET,
username=LASTFM_USERNAME, password_hash=LASTFM_PASSHASH
)
# Provide access to arguments
artist = args.artist
target = args.target
now_playing = args.now_playing
justdoit = args.justdoit
autolove = args.love
# Skip Now Playing if Just Do It is active
# 1) Why would we? 2) Causes lag 3) Leaves User Now Playing final track
if justdoit:
now_playing = False
if args.debug:
print(args)
''''''
##############################################################################
# Functions
##############################################################################
def isLoved(trackObj=None, pullLovedTracks=False):
''' Determine if a track is part of the User's Loved tracks '''
# This was written because I cannot figure out / there are issues
# with trackObj.get_userloved() always returning None
# No guarantee we receive all Loved tracks
global LOVED_TRACKS
global LOVED_MAP
result = False
# Try to use library method, skipping inefficient workaround
untrusted = trackObj.get_userloved()
if untrusted is not None:
return True
# Refresh LOVED_ variables
if pullLovedTracks or len(LOVED_TRACKS) < 1:
LOVED_TRACKS = session.get_authenticated_user().get_loved_tracks(
limit=None, cacheable=False
)
LOVED_MAP = list(map(
lambda t: str(t[0]), LOVED_TRACKS
))
# Look for Track in Loved Tracks
if trackObj:
q = "%s - %s" % (
trackObj.artist, trackObj.title
)
if q in LOVED_MAP:
result = True
return result
def make_track_payload(trackObj):
''' Scrobble Payload '''
# Retrieve Artist, Title and Album for Scrobble Payload
result = {
'artist': str(trackObj.get_artist()),
'title': str(trackObj.get_title()),
'album': str(trackObj.get_album()),
'mbid': trackObj.get_mbid()
}
# Pop mbid if it doesn't exist (causes problems otherwise)
if not result.get('mbid'):
del result['mbid']
if args.debug:
print(result)
return result
def loveTrack(trackObj, love=True):
''' Love or remove Love from track '''
result = None
if love:
msg = "Loved track!"
result = trackObj.love()
if not love:
msg = "Removed Love from track!"
result = trackObj.unlove()
print(msg)
return result
def wait_or_scrobble(trackObj):
''' Wait Track duration, or User input '''
then = time.time() # Track start timehack
duration = trackObj.get_duration()
seconds = int(duration / 1000)
msg = "Scrobble? (%s, %s, %s, or wait %.2fm for auto Scrobble): " % (
"s to skip", "l to Love", "u to Un-Love", seconds / 60
)
print(msg)
# Logic for honoring arguments ...
if now_playing:
msg = "Now Playing!"
session.update_now_playing(**make_track_payload(trackObj))
print(msg)
if autolove:
loveTrack(trackObj)
# Refresh Loved Tracks
isLoved(trackObj=None, pullLovedTracks=True)
doLoop = True
doScrobble = None
while not justdoit and (doLoop and doScrobble is None):
x, y, z = select.select([sys.stdin], [], [], 0)
if x:
line = sys.stdin.readline().strip()
else:
line = None
# Manual, Interactive
if line in [""]:
doScrobble = True
elif line in ["s"]:
doScrobble = False
elif line in ["l"]:
loveTrack(trackObj, love=True)
elif line in ["u"]:
loveTrack(trackObj, love=False)
# Scrobble after track has played
dx = time.time() - then
if dx >= seconds:
doScrobble = True
# Short sleep to reduce cycles
else:
if args.debug:
print("%ss / %.000f...s = %s%%" % (
dx, seconds, (dx / seconds * 100)
))
time.sleep(1)
if doScrobble or justdoit:
session.scrobble(
timestamp=time.time(),
**make_track_payload(trackObj)
)
if SCROBBLE_TAG:
trackObj.add_tag(SCROBBLE_TAG)
msg = "Scrobble'd!"
else:
msg = "Skipped!"
print(msg)
return doScrobble
''''''
##############################################################################
# Runtime Functions/Mains
##############################################################################
def interactive(artist=artist, album=target):
''' Load an Album to Scrobble interactively '''
albumObj = session.get_album(artist, album)
# TODO Allow selection and store when multiple albums are discovered
albumInfo = {
'release_date': 'Unknown Date'
}
artistObj = albumObj.get_artist()
artistStr = artistObj.get_name()
print("-- Interactive Scrobbling -----")
print("Album: %s - %s [%s]" % (
artistStr,
albumObj.get_title(),
albumInfo.get('release_date')
))
print("------")
track_no = 0
for trackObj in albumObj.get_tracks():
msg = "No Action."
try:
track_no += 1
track_payload = make_track_payload(trackObj)
track_loved = "Loved" if isLoved(trackObj) else "Un-Loved"
print("#%02d| %s - %s [%s]" % (
track_no,
track_payload.get('artist'),
track_payload.get('title'),
track_loved
))
trackObj.add_tag("nrg_scrobbler")
wait_or_scrobble(trackObj)
except Exception as e:
msg = "Could not Scrobble track #%s! %s" % (track_no, e)
print(msg)
def scrobble(artist=artist, track=target, mbid=None):
''' Scrobble single Track '''
trackObj = session.get_track(artist, track)
return wait_or_scrobble(trackObj)
##############################################################################
# Runtime/Main
##############################################################################
if __name__ in ["__main__"]:
# Cache User Loved Tracks
isLoved(trackObj=None, pullLovedTracks=True)
if args.interactive:
interactive()
elif args.scrobble:
scrobble()
else:
sys.exit(2)
##############################################################################
@bonelifer
Copy link

Does this still work? I tried it but got this error. Just need something to love tracks on last.fm via command line to use with bash script and mpc.

Traceback (most recent call last):
  File "./lastfmlove.py", line 330, in <module>
    isLoved(trackObj=None, pullLovedTracks=True)
  File "./lastfmlove.py", line 138, in isLoved
    untrusted = trackObj.get_userloved()
AttributeError: 'NoneType' object has no attribute 'get_userloved'

@daryltucker
Copy link
Author

daryltucker commented Jun 23, 2022

Seems to be working for me. You'll need the env vars as well.

export LASTFM_API_KEY=""
export LASTFM_API_SECRET=""
export LASTFM_USERNAME=""
export LASTFM_PASSHASH=""
~/scripts/nrg_scrobbler.py -a "Led Zeppelin" "Houses of the Holy"

The goal of this script was to help me scrobble records as I play them on my turn-table. Definitely could have more checks, like to make sure these env vars are set, etc.

@bonelifer
Copy link

how do you hash the password?

@bonelifer
Copy link

bonelifer commented Jun 23, 2022

What version of python do you have? Also which version of pylast? Still getting this, md5'd my hash:

william@william:~/bin$ bash love.sh 
Traceback (most recent call last):
  File "/home/william/bin/nrg_scrobbler.py", line 330, in <module>
    isLoved(trackObj=None, pullLovedTracks=True)
  File "/home/william/bin/nrg_scrobbler.py", line 138, in isLoved
    untrusted = trackObj.get_userloved()
AttributeError: 'NoneType' object has no attribute 'get_userloved'

@bonelifer
Copy link

NVM. Tried the example on PyLast's github readme, it didn't work. Opened an issue and apparently LastFM is at it again as the login code no longer worked. They provided different code for the login portion that worked. Thanks for replying. Sorry for the hassle. Now off to love my scrobbles. Scrobble Scrobble....

@daryltucker
Copy link
Author

Awesome! Glad to know it still works and you were able to figure it out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment