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.
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.
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:
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:
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! 👋