Thursday, April 25, 2024
HomePythonChallenge Write-up: Show Spotify lyrics on exterior show

Challenge Write-up: Show Spotify lyrics on exterior show


Hello guys! 👋

It has been some time since I final posted something. I’ve been busy however I’m again with a enjoyable article. I got here throughout a job posting on Upwork the place somebody was in search of a software program that performs the lyrics of a Spotify tune on twin screens. One display screen will show the English lyrics and the opposite one will show Hungarian lyrics. I used to be actually intrigued so I made a decision to present it a go. On this article I’m going to speak about the entire thought course of I went via whereas attempting to determine an answer as I feel that half is commonly lacking from programming tutorials.

Job posting

I discovered about just a few enjoyable issues whereas implementing this challenge. The primary one is MPRIS (Media Participant Distant Interfacing Specification). It’s a normal D-Bus interface that gives a typical programmatic API for controlling media gamers. Spotify helps MPRIS nonetheless, so far as I’m conscious, MPRIS is principally a Unix-specific know-how. It’s theoretically supported on Mac however I couldn’t discover a lot helpful details about it.

I didn’t find yourself utilizing MPRIS for this challenge however I wished to say it right here for my future self. And in case you are working with multimedia on a Unix-based system, it’s best to undoubtedly test it out!

Sufficient with the prelude, let’s dive proper in.

Ultimate product

That is what I ended up making:

As you’ll be able to see, the lyrics are all synced up with the tune. Solely the presently taking part in stanza is confirmed up on the display screen. The lyrics are loaded from native textual content information the place every tune has an accompanying textual content file containing the lyrics.

Doing the analysis

As quickly as I learn the issue assertion, I made a decision to flex my Google-fu and run some searches. You’ll be shocked how usually there’s an open supply challenge doing precisely what you are attempting to do and you may repurpose it to your use case. Nonetheless, my searches didn’t end in a lot success. I suppose the primary purpose for it was that that is such a distinct segment use case. Spotify already supplies completely synced lyrics for songs and most of the people don’t want them translated. And even when they do, they will merely use Google translate.

Lyrics inside Spotify

I did come throughout two initiatives on GitHub:

Nonetheless, neither of those initiatives catered to my particular wants. The primary challenge was not cross-platform. It solely works on Linux and so I dominated it out immediately.

The second challenge was a bit extra helpful. It offers the identify of the presently taking part in tune and the artist for that tune. Most significantly, it was multi-platform. I figured that if I can get this to work on my machine, I can use the identify of the tune and the artist to find an area textual content file containing the lyrics of the tune and show them in a browser window utilizing Flask.

The one remaining difficulty was that the tune and artist identify weren’t sufficient for me to show the lyrics. I wanted a solution to show solely the present stanza that was taking part in. SwSpotify didn’t have an API for me to get the present location of the playhead. I wanted that to determine which stanza to play. Fortunately, SwSpotify confirmed me a technique that I might use to get this data. I noticed that it was utilizing Apple Script to question Spotify for the tune data.

That is what the related code part regarded like:

apple_script_code = """
# Verify if Spotify is presently operating
if utility "Spotify" is operating then
    # If this executes it means Spotify is presently operating
    getCurrentlyPlayingTrack()
finish if
on getCurrentlyPlayingTrack()
    inform utility "Spotify"
        set isPlaying to participant state as string
        set currentArtist to artist of present observe as string
        set currentTrack to call of present observe as string
        return {currentArtist, currentTrack, isPlaying}
    finish inform
finish getCurrentlyPlayingTrack
"""

Sadly I had no concept which queries had been supported by the Spotify utility. I made a decision to run some Google searches once more and got here throughout this StackOverflow query. Somebody had helpfully talked about a file that contained all of the out there queries.

/Functions/Spotify.app/Contents/Assets/Spotify.sdef

I rapidly opened it up and noticed participant place. I examined it out within the Apple Script Editor and fortunately it labored immediately:

Apple Script Editor

That is the ultimate Apple Script that I ended up with:

if utility "Spotify" is operating then
    # If this executes it means Spotify is presently operating
    getCurrentTrackPosition()
finish if
on getCurrentTrackPosition()
    inform utility "Spotify"
        set trackPosition to participant place as string
        return trackPosition
    finish inform
finish getCurrentTrackPosition

After determining the Apple Script, I made a decision that I used to be going to organize the lyrics information such that there was a timecode earlier than every stanza. I can then course of these lyrics information in Python and match the observe place with the suitable stanza.

That is what the related part of one among these information ended up wanting like:

[00:08.62]
First issues first
I'ma say all of the phrases inside my head
I am fired up and uninterested in the way in which that issues have been, oh ooh
The way in which that issues have been, oh ooh

[00:22.63]
Second factor second
Do not you inform me what you suppose that I will be
I am the one on the sail, I am the grasp of my sea, oh ooh
The grasp of my sea, oh ooh

At this level, the one lacking piece was to determine the best way to stream lyrics from Flask to the browser. I wished it to be so simple as potential so I didn’t wish to use net sockets. Fortunately I had used streaming responses in Flask earlier than so I knew I might use them for this objective. I searched on-line for a ready-made instance and got here throughout this StackOverflow reply that contained some pattern code for me to make use of.

Ultimate code

I used the code from that reply and ended up with this closing code:

from SwSpotify import spotify, SpotifyNotRunning, SpotifyPaused
from flask import Flask, render_template
import subprocess
import time
import re

app = Flask(__name__)


def get_current_time():
    apple_script_code = """
    # Verify if Spotify is presently operating
    if utility "Spotify" is operating then
        # If this executes it means Spotify is presently operating
        getCurrentTrackPosition()
    finish if
    on getCurrentTrackPosition()
        inform utility "Spotify"
            set trackPosition to participant place as string
            return trackPosition
        finish inform
    finish getCurrentTrackPosition
    """

    outcome = subprocess.run(
        ["osascript", "-e", apple_script_code],
        capture_output=True,
        encoding="utf-8",
    )
    print(outcome.stdout)
    return outcome.stdout or ""


def get_sec(time_str):
    """Get seconds from time."""
    m, s = time_str.break up(":")
    return int(m) * 60 + float(s)


def get_lyrics(song_name, language):
    strive:
        with open(f"./{language}_lyrics/{song_name}.txt", "r") as f:
            lyrics = f.learn()
    besides FileNotFoundError:
        return
    sample = re.compile("[(.+)]((?:n.+)+)", re.MULTILINE)
    splitted = re.findall(sample, lyrics)
    time_stanza = {}
    for (time, stanza) in splitted:
        time_stanza[get_sec(time)] = stanza.strip()
    return time_stanza


@app.route("/")
def index():
    return render_template("index.html")


@app.route("/stream")
def stream():
    def generate():
        whereas True:
            strive:
                title, artist = spotify.present()
            besides (SpotifyNotRunning, SpotifyPaused) as e:
                print(e)
            else:
                print(f"{title} - {artist}")
                current_time = float(get_current_time())
                print(current_time)
                time_stanza = get_lyrics(title, "english")
                current_stanza = ""
                if time_stanza:
                    time_list = listing(time_stanza.keys())
                    for index, stanza_start_time in enumerate(time_list):
                        if (
                            stanza_start_time < current_time
                            and time_list[index + 1] > current_time
                        ):
                            current_stanza = time_stanza[stanza_start_time]
                            break
                yield f"{title.title()} - {artist.title()} ##{current_stanza}n----"

            time.sleep(1)

    return app.response_class(generate(), mimetype="textual content/plain")


app.run()

That is the accompanying HTML template:

<!DOCTYPE html>
<html>

<head>

</head>

<physique>
    <div class="song-meta">
        <img src={{ url_for('static', filename="pictures/music.png" ) }} width="50" alt="tune identify" />
        <span id="songName">Loading..</span>
    </div>
    <div class="lyrics">
        <pre id="stanza">
        </pre>
    </div>
    <script>

        var songNameSpan = doc.getElementById('songName');
        var stanzaSpan = doc.getElementById('stanza');
        var xhr = new XMLHttpRequest();
        xhr.open('GET', '{{ url_for('stream') }}');

        xhr.onreadystatechange = perform () {
            var all_lines = xhr.responseText.break up('n----');
            last_line = all_lines.size - 2
            var songName_stanza = all_lines[last_line]
            if (songName_stanza){
                songName_stanza = songName_stanza.break up('##')
                console.log(songName_stanza)
                songNameSpan.textContent = songName_stanza[0]
                stanzaSpan.textContent = songName_stanza[1]
            }

            if (xhr.readyState == XMLHttpRequest.DONE) {
                /*Typically it stops working when the stream is completed (tune modifications)
                so I simply refresh the web page. It virtually all the time solves the difficulty*/
                doc.location.reload()
                songNameSpan.textContent = "The Finish of Stream"
            }
        }

        xhr.ship();

    </script>

    <model>
        html,
        physique {
            peak: 100%;
        }

        physique {
            margin: 0;
            background-color: #272727;
        }

        .song-meta {
            place: absolute;
            high: 40px;
            left: 40px;
            show: flex;
            align-items: middle;
        }

        #songName {
            font-size: 2rem;
            margin-left: 20px;
            background-color: white;
            padding: 10px 20px;
            border-radius: 20px;
        }

        .lyrics {
            peak: 100%;
            padding: 0;
            margin: 0;
            show: flex;
            align-items: middle;
            justify-content: middle;
        }

        #stanza {
            width: auto;
            font-family: Helvetica, sans-serif;
            padding: 5px;
            margin: 10px;
            line-height: 1.5em;
            coloration: white;
            font-size: 4rem;
            text-align: middle;
        }
    </model>
</physique>

</html>

My closing listing construction regarded like this:

.
├── app.py
├── english_lyrics
│   └── Believer.txt
├── static
│   └── pictures
│       └── music.png
└── templates
    └── index.html

And that is what the ultimate utility web site regarded like:

Final web app

Conclusion

There may be numerous stuff that’s not optimized within the Python code. This can be a fast and soiled resolution to this downside nevertheless it works completely tremendous. I didn’t work with the customer so I had no purpose to enhance it. I simply wished to check out the concept as a result of the challenge appeared enjoyable 😃

I had numerous enjoyable whereas engaged on this challenge. I hope you discovered about my thought course of on this article and noticed how one can go from level 0 to a completely working challenge and put totally different items collectively. In case you like studying about this type of stuff please remark under. I really like to listen to from you guys!

Until subsequent time! 👋

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments