hankhank10 / music-screen-api

Display the playing Sonos track in real time on an e-ink display - also includes functionality for last.fm
118 stars 17 forks source link

Improvement: show spotify code of current radio track #61

Open marco79cgn opened 1 year ago

marco79cgn commented 1 year ago

Not really an issue, but I kindly ask if someone would help me to show an additional image (Spotify code) as overlay of the cover (see preview below).

Background: I never wrote any python code before but managed to integrate Spotipy (in sonos_user_data.py). I'm using it to show cover art of the current track on (mostly german) radio stations. I'm reading both artist and track name from the radio text, query Spotify, get the cover url and set it as background image (image_uri). This already works pretty good. As a bonus, since I already have the spotify metadata, I'd like to show the spotify code as well. It's kind of a qr code that can be scanned in the Spotify app to play the track. I already know how to get the code, I just have to put the spotify track uri at the end of this url to download the image with the code: https://scannables.scdn.co/uri/plain/jpeg/368A7D/white/640/spotify:track:5wEoNauEpwOc2rlU0274oT

ToDo: So all there's left to do is to display this image for example in the upper right of the cover art (as overlay). I'm struggling how to do that as I don't really understand this tk mechanism (and how to extend label_albumart_detail). Any help is really appreciated.

This is how it shuold look like:

Cover-SpotifyCode
ashenshugarRET commented 1 year ago

Hello,

Yes, I find tk.inter a bit confusing as well, essentially you'll want to declare a new label and anchor it to the top right (or NE) as follows in display_controller.py:

self.label_spotify_code = tk.Label( self.album_frame, image=None, borderwidth=0, highlightthickness=0, fg="white", bg="black", ) self.label_spotify_code.place(x=10, y=10, anchor='ne')

For ease of reference I would insert this in the same place where all the other tk.inter frames and labels are assigned...

The x= and y= values will indent the graphic a little from the edge. This label is attached to album_frame so it should show whenever album art is displayed.

The final thing you need to do is to populate the label with your Spotify Code graphic, adding the following towards the end of display_controller.py where the similar code to populate the album art should do it:

self.label_spotify_code.configure(image=link to spotify code or variable which contains the graphic), this is the bit I'm struggling with but you mention that you are able to construct the hyperlink so hopefully that will work.

I hope this points in enough of the right direction to get your idea working.

marco79cgn commented 1 year ago

Thank you so much, you really helped me a lot to understand this thing. I managed to implement it now and it works! It took the most time to place this image on the screen so that you can actually see it at all. lol For some reason, West and East are switched and work exactly the other way round on my screen. But at the end I moved it more to the bottom right (relx=0.54, y=600, anchor=tk.SW) with trial & error. ;)

Now I just have to find a solution that looks pleasing. Probably I put the cover back to fullscreen and use the new overlay track information. The thing is, I'm still on a very old version of the music-screen-api since I made quite some changes and didn't want to break anything when trying to merge the latest master. In Java and IntelliJ I'm quite familiar with merging code and there is a really good integrated merge tool to solve conflicts. But in Python, I use vim in the Console and VS Code on the Mac. Didn't manage to connect VS Code remotely via SSH as it always tells me the SSH server on my Pi would be incompatible for reasons unknown.

This is how it looks right now:

Bildschirm­foto 2022-12-19 um 23 24 39 Kopie

So the next thing would be to merge all my stuff with the latest master. Thanks again!

ashenshugarRET commented 1 year ago

Hello,

Great to hear you've been successful, label placement is definitely tricky in tkinter.

I've been trying to get this working as well, and have managed to implement code that allows the user to select whether they want to display the Spotify Code or not through a setting in the sonos_settings.py file.

What I can't do is the Spotipy artist and song name search to return the Spotify URI for songs...

spotify code example

I've uploaded these changes to my upto date repository ashenshugarRET/music-screen-api for this program, perhaps if we can collaborate we can contribute the results back to the Hankhank10's parent project

marco79cgn commented 1 year ago

Sure, let's do it. 👍
At the moment I don't have that much spare time because of christmas. But I'll try it as soon as possible.

The Spotify integration isn't that complicated with the Spotipy library.

In _sonossettings.py I added two additional attributes which are needed for the spotify authentication. You have to create a new client in the Dashboard to obtain them:

spotify_client_id = "33************"
spotify_client_secret = "97df**********"

I imported Spotipy in _sonos_userdata.py at the top:

import spotipy
from spotipy.oauth2 import SpotifyOAuth 
import spotipy.util as util

With these 3 additional functions I'm retrieving the Spotify metadata:

def get_album_cover_from_spotify(self, trackname, artist, spotify_client_id, spotify_client_secret, station):
    current_artist_title = get_artist_title_separated(trackname)
    search_title = ""
    search_artist = ""
    image_uri = ""
    try:
        if len(current_artist_title) > 1:
            search_title = clean_title(current_artist_title[1]) 
        search_artist = current_artist_title[0]
    except:
        search_title = trackname
        search_artist = artist

    try:
        _LOGGER.info("Attempting to get cover url for %s %s..." % (search_title, search_artist))
        spotify = spotipy.Spotify(auth_manager=SpotifyOAuth(username='YOUR_SPOTIFY_USERNAME', client_id=spotify_client_id, client_secret=spotify_client_secret, redirect_uri="https://example.com/callback/", scope="playlist-modify-public"))
        results = spotify.search(q='artist:' + search_artist + ' track:' + search_title, type='album,track', limit=2, market='DE')
        _LOGGER.debug("albums result: %s" % results)
        if len(results) == 0:
            print("Couldn't find entry for track %s from artist %s" % (search_title, search_artist))
        image_uri = results['tracks']['items'][0]['album']['images'][0]['url']
        _LOGGER.info("cover: %s" % image_uri)
        self.spotify_code_uri = "https://scannables.scdn.co/uri/plain/jpeg/368A7D/white/640/" + results['tracks']['items'][0]['uri']
        _LOGGER.info("Spotify Code Uri: %s", self.spotify_code_uri)
    except:
        _LOGGER.info("Couldn't find artwork for %s %s" % (search_title, search_artist))
        _LOGGER.info("Trying to query Spotify again with one search string")
        results = spotify.search(q=search_artist + ' ' + search_title, type='album,track', limit=2, market='DE')
        try:
            image_uri = results['tracks']['items'][0]['album']['images'][0]['url']
            _LOGGER.info("cover: %s" % self.image_uri)
            self.spotify_code_uri = "https://scannables.scdn.co/uri/plain/jpeg/368A7D/white/640/" + results['tracks']['items'][0]['uri']
            _LOGGER.info("Spotify Code Uri: %s", self.spotify_code_uri)
        except:
            _LOGGER.info("Couldn't get a cover, giving up..")
    return image_uri

def get_artist_title_separated(trackname):
    _LOGGER.info("Trying to split trackname: %s", trackname)
    if " von " in trackname: 
        trackname = trackname.replace('"', '')
        return trackname.split(" von ")
    if " - " in trackname: return trackname.split(' - ')
    if " mit " in trackname: return trackname.split(" mit ")
    if " / " in trackname: return trackname.split(" / ")
    if ": " in trackname: return trackname.split(": ")
    return trackname

def clean_title(title):
    cleaned_title = title
    try:
        cleaned_title_array = title.split(' (')
        cleaned_title = cleaned_title_array[0]
        cleaned_title_array = cleaned_title.split(' --')
        cleaned_title = cleaned_title_array[0]
        cleaned_title_array = cleaned_title.split(' |')
        cleaned_title = cleaned_title_array[0]
    except:
        return cleaned_title
    return cleaned_title

First of all, the code needs refactoring or cleanup, I know. The thing is that all german radio stations transmit both artist and title combined as title. So I have to parse both values from the title because you get the best search results from Spotify when asking for both artist and title separated. I also clean the title and remove things like "(Radio Edit) etc. The problem here is, that every radio station shows this information differently, for example "title": "Holiday von Madonna" "title": "Holiday - Madonna" "title": "Madonna / Holiday" "title": "Holiday, Madonna" etc.

So I need a lot of if/else statements to extract the information correctly. On my iPhone in Shorcuts, I'm using a dictionary where I store the radio station as key and an additional dictionary as value where I configure which separator each station is using and whether artist is on the first or second position after splitting.

Bildschirm­foto 2022-12-21 um 17 52 22

Long story short, in sonos_user_data.py I'm calling the function above in the refresh() function like this:

cover = get_album_cover_from_spotify(self, self.trackname, self.artist, self.spotify_client_id, self.spotify_client_secret, self.station)
if "http" in cover: 
    self.image_uri = cover

Please excuse my python skills. I'm "debugging" it with logging. 🙈

ashenshugarRET commented 1 year ago

Thanks,

I have an issue importing spotipy, everytime I insert 'import spotipy' into the top of _sonos_userdata.py the program crashes on booting producing the following errors

Traceback (most recent call last): File "go_sonos_highres.py", line 14, in from aiohttp import ClientError, ClientSession File "/usr/local/lib/python3.7/dist-packages/aiohttp/init.py", line 6, in from .client import ( File "/usr/local/lib/python3.7/dist-packages/aiohttp/client.py", line 35, in from . import hdrs, http, payload File "/usr/local/lib/python3.7/dist-packages/aiohttp/http.py", line 7, in from .http_parser import ( File "/usr/local/lib/python3.7/dist-packages/aiohttp/http_parser.py", line 15, in from .helpers import NO_EXTENSIONS, BaseTimerContext File "/usr/local/lib/python3.7/dist-packages/aiohttp/helpers.py", line 667, in class CeilTimeout(async_timeout.timeout): TypeError: function() argument 1 must be code, not str

It's clearly a local issue form me as you are successfully using Spotipy with your installation

marco79cgn commented 1 year ago

Did you install the lib successfully with

pip install spotipy

? Maybe my version is already a little outdated but it should still work according to the documentation.

ashenshugarRET commented 1 year ago

Yes, I think I installed spotipy successfully, I used the exact instruction you mention. I can import it into standalone scripts and access and search Spotify successfully. The problem only appears when I try importing into sonos_user_data.py, go_sonos_highres.py or display_controller.py...

I will investigate by installing from a fresh Raspberry Pi image, like you I don't have much time available this side of Christmas, so this will be a New Year Project

UPDATE: Fixed it was a permissions issue as I had set go_sonos_highres.py to start as sudo which must have been causing conflicts.

ashenshugarRET commented 1 year ago

To provide an update, I now have the Spotify Code showing reliably when the spotipy search returns a result, when no result is returned no code is displayed. I have just implemented code to override displaying the albumart from the Sonos system with that on Spotify (again if a valid result is returned), I'm going to test this for a few days before commiting the changes to my respository which will then be rolled up to the pull request I requested to this parent respository. Both the displaying of the Spotify Code and Albumart are controlled by variables in sonos_settings.py.

ashenshugarRET commented 1 year ago

Hello,

I've completed my implementation of showing Spotify Codes as well as using Spotify's album art, these should be getting merged into the main repository soon.

I woujld imagine @hankhank10 will close this issue once that is done.

Thanks