-
-
Save daryltucker/3d1129289a93462fcd703b7c2916a058 to your computer and use it in GitHub Desktop.
| #!/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) | |
| ############################################################################## |
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.
how do you hash the password?
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'
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....
Awesome! Glad to know it still works and you were able to figure it out.
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.