├── .gitignore ├── README.md └── listen.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LastFmListenAlong 2 | 3 | ## Description 4 | 5 | LastFmListenAlong is a program made in Python, allowing you to listen along a Last.fm user, by playing the songs the user is listening to on Spotify, in real time. 6 | It uses the Last.fm API to fetch what the user is listening to, and the Spotify API to search for the track on Spotify, and play it. 7 | 8 | ## Configuration 9 | 10 | First, you have to register an app on the [Spotify developper page](https://developer.spotify.com/dashboard/). 11 | Do not forget to whitelist a callback URL for the app (eg. `http://localhost/`). Note the API key and secret and populate it in the code. 12 | 13 | Then you need to get an API key and secret on the [Last.fm API page](https://www.last.fm/api/account/create), and populate it in the code. 14 | 15 | ## Dependencies 16 | 17 | This program relies on the `spotipy` and `pylast` libraries. They also require `requests`. Install them with : 18 | 19 | ```bash 20 | pip install spotipy 21 | pip install pylast 22 | pip install requests 23 | ``` 24 | 25 | Note that, at the date this program was created, the spotipy version installed by pip is a bit outdated and do not contain a method we need to play a song on Spotify. 26 | You may need to get the last version of spotipy yourself here : [plamere/spotipy](https://github.com/plamere/spotipy) and place it in the current directory (renaming it just "spotipy", so that Python can import it from the current dir when it's not installed system-wide). 27 | 28 | 29 | ## Running 30 | 31 | Once everything is configured, you just have to run the program with two parameters: 32 | 33 | ```bash 34 | python listen.py lastfm_user_to_listen_along your_spotify_username 35 | ``` 36 | 37 | ## Blog article about it 38 | 39 | If you're curious you can read a blog article on how I came up with this idea on my blog: [here](https://hexaly.se/2019/02/27/how-to-listen-along-a-last-fm-user-on-spotify/). 40 | 41 | 42 | ## License 43 | 44 | This program is released under no license. Do whatever you want with it. Share it, copy it, play with it, eat it. 45 | -------------------------------------------------------------------------------- /listen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import os 5 | import time 6 | 7 | import pylast 8 | import spotipy 9 | import spotipy.util as util 10 | 11 | API_KEY = "your_lastfm_api_key" 12 | API_SECRET = "your_lastfm_api_secret" 13 | os.environ['SPOTIPY_CLIENT_ID'] = 'your_spotify_api_key' 14 | os.environ['SPOTIPY_CLIENT_SECRET'] = 'your_spotify_api_secret' 15 | # You can use whatever address you want here. 16 | # The spotipy library will ask you to copy/paste the URL you have been redirected to. 17 | # Beware : you must whitelist the url used here, in your app on your Spotify Dashboard 18 | os.environ['SPOTIPY_REDIRECT_URI'] = 'http://localhost/' 19 | 20 | SESSION_KEY_FILE = ".session_key" 21 | 22 | if __name__ == "__main__": 23 | parser = argparse.ArgumentParser( 24 | description="This application allows you to listen the same music a Last.fm user is listening to," 25 | " through Spotify", 26 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 27 | ) 28 | parser.add_argument("user", help="User to follow along") 29 | parser.add_argument("sp_username", help="Your Spotify username") 30 | args = parser.parse_args() 31 | 32 | sp = spotipy.Spotify() 33 | token = util.prompt_for_user_token(args.sp_username, 'streaming user-library-read') 34 | 35 | if token: 36 | sp = spotipy.Spotify(auth=token) 37 | else: 38 | print("Can't get token for", args.sp_username) 39 | exit() 40 | 41 | network = pylast.LastFMNetwork(API_KEY, API_SECRET) 42 | 43 | if not os.path.exists(SESSION_KEY_FILE): 44 | skg = pylast.SessionKeyGenerator(network) 45 | url = skg.get_web_auth_url() 46 | 47 | print("Please authorize the scrobbler to scrobble to your account: {url}\n") 48 | import webbrowser 49 | 50 | webbrowser.open(url) 51 | 52 | while True: 53 | try: 54 | session_key = skg.get_web_auth_session_key(url) 55 | fp = open(SESSION_KEY_FILE, "w") 56 | fp.write(session_key) 57 | fp.close() 58 | break 59 | except pylast.WSError: 60 | time.sleep(1) 61 | else: 62 | session_key = open(SESSION_KEY_FILE).read() 63 | 64 | network.session_key = session_key 65 | user = network.get_user(args.user) 66 | print("Tuned in to", args.user) 67 | 68 | playing_track = None 69 | just_started_playing = False 70 | 71 | while True: 72 | try: 73 | new_track = user.get_now_playing() 74 | if new_track is None: 75 | print("\rUser is not listening to music right now (checking every 1 min).", end="") 76 | # Wait 1 minute if nothing is playing 77 | track_duration = 60 78 | just_started_playing = False 79 | # user is listening to a new song 80 | elif new_track != playing_track: 81 | playing_track = new_track 82 | just_started_playing = True 83 | print("\n{} is now listening to: {}".format(args.user, new_track)) 84 | print("Searching for the track on Spotify...") 85 | # Try searching for the song. If we get an exception, try to renew the token 86 | while True: 87 | try: 88 | results = sp.search(q=new_track, limit=1, type='track') 89 | break 90 | except spotipy.SpotifyException: 91 | token = util.prompt_for_user_token(args.sp_username, 'streaming user-library-read') 92 | if token: 93 | sp = spotipy.Spotify(auth=token) 94 | else: 95 | print("Can't renew token for", args.sp_username) 96 | exit() 97 | # Track found ? 98 | if len(results['tracks']['items']) > 0: 99 | track = results['tracks']['items'][0] 100 | track_duration = int(track['duration_ms']) / 1000 101 | m, s = divmod(track_duration, 60) 102 | print("Playing track on Spotify (duration: {:d}:{:02d}). Enjoy :)".format(int(m), int(s))) 103 | sp.start_playback(uris=[track['uri']]) 104 | else: 105 | print('Track not found :(') 106 | track_duration = 10 107 | else: 108 | just_started_playing = False 109 | except Exception as e: 110 | print("Error: %s" % repr(e)) 111 | # Listen at least for the duration of the song, so that it's not cut before it ends 112 | # we will slowly drift in delay (because of requests time etc) 113 | # but at worst we will miss a song from time to time 114 | # Bonus : If the song just changed and the user decides to change track, we might miss the change in case 115 | # we tuned in right when he started listening to a song. So the first time, wait for 15s and then for the rest 116 | # of the duration of the song afterwards. 117 | if just_started_playing is True: 118 | time.sleep(15) 119 | else: 120 | wait_time = track_duration - 15 121 | wait_time = wait_time if wait_time > 10 else 10 122 | time.sleep(wait_time) 123 | 124 | 125 | # End of file 126 | --------------------------------------------------------------------------------