From a27a98e245405fc82cc330dd300f008f58d0df53 Mon Sep 17 00:00:00 2001 From: b606 <7627533+b606@users.noreply.github.com> Date: Sun, 15 Jan 2023 21:47:43 +0100 Subject: [PATCH 01/11] Define function convert_to_mp3 and avoid using global file variables - move the .WAV file to a temp filename - Use multiprocessing.Process for convert_to_mp3 That still does not solve all the problem of corrupt .MP3 file for the track just before advertisement track. The bug still exists but occurrence seems to be reduced. The source of the bug might be deeper in pydub.AudioSegment --- spotifyripper.py | 53 +++++++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/spotifyripper.py b/spotifyripper.py index 490fb8f..14f0f27 100755 --- a/spotifyripper.py +++ b/spotifyripper.py @@ -13,6 +13,7 @@ from dbus.mainloop.glib import DBusGMainLoop from gi.repository import GLib from pydub import AudioSegment +from multiprocessing import Process pre_subprocess = None r = None @@ -65,6 +66,36 @@ def download_cover(art_url, file_cover): open(file_cover, 'wb').write(r.content) +def convert_to_mp3(a_file_input, a_file_cover, a_album, a_artist, a_title, a_track_number): + #print("convert_to_mp3: start converting " + a_file_input) + # Use internal filename to Protect from successive conversion + a_temp_file_input = a_file_input.replace(".wav", "-tmp.wav") + a_file_output = a_file_input.replace(".wav", ".mp3") + os.rename(a_file_input, a_temp_file_input) + + sound = AudioSegment.from_wav(a_temp_file_input) + sound.export(a_file_output, format="mp3", bitrate="160k", cover=a_file_cover, tags={ + "album": a_album, + "artist": a_artist, + "title": a_title, + "track": int(a_track_number) + } + ) + + a_file_output_size = os.stat(a_file_output).st_size + if a_file_output_size < 1048576: + print('\033[33m' + "Warning: small file " + a_file_output + " \033[0m\n") + + if a_file_output_size > 10485760: + print('\033[33m' + "Warning: large file " + a_file_output + " \033[0m\n") + + # print("DELETE " + a_file_cover) + os.remove(a_file_cover) + # print("DELETE " + a_file_input) + os.remove(a_temp_file_input) + #print("convert_to_mp3: done converting " + a_file_input) + + def spotify_handler(*args): global pre_album global pre_artist @@ -134,27 +165,7 @@ def spotify_handler(*args): # convert previous file if os.path.isfile(pre_file_input): - pre_file_output = pre_file_input.replace(".wav", ".mp3") - sound = AudioSegment.from_wav(pre_file_input) - sound.export(pre_file_output, format="mp3", bitrate="160k", cover=pre_file_cover, tags={ - "album": pre_album, - "artist": pre_artist, - "title": pre_title, - "track": int(pre_track_number) - } - ) - - pre_file_output_size = os.stat(pre_file_output).st_size - if pre_file_output_size < 1048576: - print('\033[33m' + "Warning: small file " + pre_file_output + " \033[0m\n") - - if pre_file_output_size > 10485760: - print('\033[33m' + "Warning: large file " + pre_file_output + " \033[0m\n") - - # print("DELETE " + pre_file_cover) - os.remove(pre_file_cover) - # print("DELETE " + pre_file_input) - os.remove(pre_file_input) + Process(target=convert_to_mp3, args=(pre_file_input, pre_file_cover, pre_album, pre_artist, pre_title, pre_track_number)).start() # download cover file_cover = file_input.replace(".wav", ".jpg") From a3a089332058901c1861cd7deee632c4cf43b226 Mon Sep 17 00:00:00 2001 From: b606 <7627533+b606@users.noreply.github.com> Date: Sun, 15 Jan 2023 22:00:26 +0100 Subject: [PATCH 02/11] Define system independent function "get_download_path" NB. win32 not tested --- spotifyripper.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/spotifyripper.py b/spotifyripper.py index 14f0f27..4dd9dbc 100755 --- a/spotifyripper.py +++ b/spotifyripper.py @@ -5,6 +5,7 @@ import dbus import os +import sys import pprint import pulsectl import re @@ -25,6 +26,7 @@ pre_file_input = "" pre_file_cover = "" file_cover = "" +download_path = "" spotify_sink_index = 0 @@ -54,6 +56,22 @@ def create_directory(path_album): return path_album +def get_download_path(): + if sys.platform == "win32": + command = r'reg query "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders" /v "Downloads"' + result = subprocess.run(command, stdout=subprocess.PIPE, text = True) + download_path = result.stdout.splitlines()[2].split()[2] + else: + download_path = subprocess.check_output(['xdg-user-dir', 'DOWNLOAD'], text = True).rstrip('\n') + + if download_path == "": + download_path = os.path.expanduser("~/Downloads") + + download_path = os.path.join(download_path, 'spotifyripper') + print("download_path = %s" % download_path) + return download_path + + def download_cover(art_url, file_cover): global r @@ -105,6 +123,7 @@ def spotify_handler(*args): global pre_track_number global pre_file_input global pre_file_cover + global download_path global r global spotify_sink_index @@ -141,9 +160,8 @@ def spotify_handler(*args): print() - # create dir - path_base = os.path.expanduser("~/Downloads/spotifyripper") + path_base = os.path.expanduser(download_path) if disc_number > 1: disc_number_str = str(disc_number) + " " else: @@ -187,6 +205,7 @@ def spotify_handler(*args): pre_track_number = track_number +download_path = get_download_path() spotify_sink_index = get_spotify_sink_index() DBusGMainLoop(set_as_default=True) From deae791c27bb38b8f08857cd80410c2102db1e83 Mon Sep 17 00:00:00 2001 From: b606 <7627533+b606@users.noreply.github.com> Date: Sun, 15 Jan 2023 12:09:50 +0100 Subject: [PATCH 03/11] Show AlbumArtist --- spotifyripper.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spotifyripper.py b/spotifyripper.py index 4dd9dbc..cd4ee0e 100755 --- a/spotifyripper.py +++ b/spotifyripper.py @@ -129,13 +129,12 @@ def spotify_handler(*args): metadata = args[1]["Metadata"] # debug - # pprint.pprint(metadata) - + #pprint.pprint(metadata) artist = metadata["xesam:artist"][0] - # print("Artist: " + artist) - #albumArtist = metadata["xesam:albumArtist"][0] - # print("AArtist " + albumArtist) + #print("Artist: " + artist) + albumArtist = metadata["xesam:albumArtist"][0] + #print("AArtist " + albumArtist) title = metadata["xesam:title"] album = metadata["xesam:album"] track_number = metadata["xesam:trackNumber"] @@ -153,6 +152,7 @@ def spotify_handler(*args): if title != pre_title: print("Artist: " + artist) + print("AlbumArtist: " + albumArtist) print("Album: " + album) print("Title: " + str(track_number) + " - " + title) # print("Cover: " + art_url) From 3340d733d0a2509f921008f591baf31ba9e67788 Mon Sep 17 00:00:00 2001 From: b606 <7627533+b606@users.noreply.github.com> Date: Sun, 15 Jan 2023 12:29:32 +0100 Subject: [PATCH 04/11] Increase bitrate to 320k and reduce debugging verbosity --- spotifyripper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spotifyripper.py b/spotifyripper.py index cd4ee0e..4469cda 100755 --- a/spotifyripper.py +++ b/spotifyripper.py @@ -92,7 +92,7 @@ def convert_to_mp3(a_file_input, a_file_cover, a_album, a_artist, a_title, a_tra os.rename(a_file_input, a_temp_file_input) sound = AudioSegment.from_wav(a_temp_file_input) - sound.export(a_file_output, format="mp3", bitrate="160k", cover=a_file_cover, tags={ + sound.export(a_file_output, format="mp3", bitrate="320k", cover=a_file_cover, tags={ "album": a_album, "artist": a_artist, "title": a_title, @@ -104,7 +104,7 @@ def convert_to_mp3(a_file_input, a_file_cover, a_album, a_artist, a_title, a_tra if a_file_output_size < 1048576: print('\033[33m' + "Warning: small file " + a_file_output + " \033[0m\n") - if a_file_output_size > 10485760: + if a_file_output_size > 25485760: print('\033[33m' + "Warning: large file " + a_file_output + " \033[0m\n") # print("DELETE " + a_file_cover) From 2b02258beb2dfb9c1860d08fa199bacbc8f068ed Mon Sep 17 00:00:00 2001 From: b606 <7627533+b606@users.noreply.github.com> Date: Sun, 15 Jan 2023 21:11:07 +0100 Subject: [PATCH 05/11] Clarify with error message when the Spotify client is not regstered --- spotifyripper.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/spotifyripper.py b/spotifyripper.py index 4469cda..cb10a26 100755 --- a/spotifyripper.py +++ b/spotifyripper.py @@ -27,7 +27,7 @@ pre_file_cover = "" file_cover = "" download_path = "" -spotify_sink_index = 0 +spotify_sink_index = -1 def get_spotify_sink_index(): @@ -38,7 +38,7 @@ def get_spotify_sink_index(): if (sink.name == "Spotify") and (sink.corked == False): return sink.index - return 0 + return -1 def create_directory(path_album): @@ -174,9 +174,15 @@ def spotify_handler(*args): pre_subprocess.terminate() file_input = path_album + "/" + str(track_number) + " - " + artist + " - " + title + ".wav" - if spotify_sink_index == 0: + if spotify_sink_index == -1: spotify_sink_index = get_spotify_sink_index() print("spotify_sink_index: " + str(spotify_sink_index)) + # If Spotify not found, do nothing + if spotify_sink_index == -1: + print("the Spotify client not found.") + print("It has to be registered with the audio server by playing a sound.") + return + if (artist != "") or (album != ""): pre_subprocess = subprocess.Popen(["parec", "--monitor-stream=" + str(spotify_sink_index), "--file-format=wav", file_input]) From 1f82d07ca530f5bc09c9481d48c52d35d92b8ebc Mon Sep 17 00:00:00 2001 From: b606 <7627533+b606@users.noreply.github.com> Date: Mon, 16 Jan 2023 14:10:47 +0100 Subject: [PATCH 06/11] Typo --- spotifyripper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spotifyripper.py b/spotifyripper.py index cb10a26..643a0af 100755 --- a/spotifyripper.py +++ b/spotifyripper.py @@ -179,7 +179,7 @@ def spotify_handler(*args): print("spotify_sink_index: " + str(spotify_sink_index)) # If Spotify not found, do nothing if spotify_sink_index == -1: - print("the Spotify client not found.") + print("the Spotify client is not found.") print("It has to be registered with the audio server by playing a sound.") return From d21fa1bde59180b54ff87e2020334ea374300d7a Mon Sep 17 00:00:00 2001 From: b606 <7627533+b606@users.noreply.github.com> Date: Mon, 16 Jan 2023 14:12:27 +0100 Subject: [PATCH 07/11] Add extra delay for parec after advertisement track. --- spotifyripper.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spotifyripper.py b/spotifyripper.py index 643a0af..bbc289c 100755 --- a/spotifyripper.py +++ b/spotifyripper.py @@ -6,6 +6,7 @@ import dbus import os import sys +import time import pprint import pulsectl import re @@ -27,6 +28,7 @@ pre_file_cover = "" file_cover = "" download_path = "" +advertisement_detected = False spotify_sink_index = -1 @@ -125,6 +127,7 @@ def spotify_handler(*args): global pre_file_cover global download_path global r + global advertisement_detected global spotify_sink_index metadata = args[1]["Metadata"] @@ -185,7 +188,12 @@ def spotify_handler(*args): if (artist != "") or (album != ""): + # parec starts too soon after the advertisement bits. + # Add 1s delay in order to skip the end of the advertisement. + if advertisement_detected == True: + time.sleep(1) pre_subprocess = subprocess.Popen(["parec", "--monitor-stream=" + str(spotify_sink_index), "--file-format=wav", file_input]) + advertisement_detected = False # convert previous file if os.path.isfile(pre_file_input): @@ -209,6 +217,8 @@ def spotify_handler(*args): pre_title = title if track_number != "": pre_track_number = track_number + if (artist == "") and (album == ""): + advertisement_detected = True download_path = get_download_path() From cd261eb37a236149a901b0f60388043decba1fc8 Mon Sep 17 00:00:00 2001 From: b606 <7627533+b606@users.noreply.github.com> Date: Thu, 2 Mar 2023 12:57:02 +0100 Subject: [PATCH 08/11] Use the tag AlbumArtist for the folder name --- spotifyripper.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spotifyripper.py b/spotifyripper.py index bbc289c..bf35afa 100755 --- a/spotifyripper.py +++ b/spotifyripper.py @@ -147,6 +147,9 @@ def spotify_handler(*args): # if albumArtist != "": # artist = albumArtist + if albumArtist == "": + albumArtist = artist + if title != "": title = title.replace("/", "-") @@ -169,7 +172,7 @@ def spotify_handler(*args): disc_number_str = str(disc_number) + " " else: disc_number_str = "" - path_album = create_directory(path_base + "/" + artist + "/" + disc_number_str + album) + path_album = create_directory(path_base + "/" + albumArtist + "/" + disc_number_str + album) # print("path_album: " + path_album) # record stream From ea808e6bdee1bc87a5540157bd42b98bab874a4d Mon Sep 17 00:00:00 2001 From: b606 <7627533+b606@users.noreply.github.com> Date: Thu, 2 Mar 2023 12:48:35 +0100 Subject: [PATCH 09/11] spotifyripper.py: generate cover.jpg as external file --- spotifyripper.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/spotifyripper.py b/spotifyripper.py index bf35afa..38d2118 100755 --- a/spotifyripper.py +++ b/spotifyripper.py @@ -109,8 +109,13 @@ def convert_to_mp3(a_file_input, a_file_cover, a_album, a_artist, a_title, a_tra if a_file_output_size > 25485760: print('\033[33m' + "Warning: large file " + a_file_output + " \033[0m\n") - # print("DELETE " + a_file_cover) - os.remove(a_file_cover) + external_cover_file = os.path.dirname(a_file_cover) + "/cover.jpg" + if not os.path.isfile(external_cover_file): + os.rename(a_file_cover, external_cover_file) + else: + # print("DELETE " + a_file_cover) + os.remove(a_file_cover) + # print("DELETE " + a_file_input) os.remove(a_temp_file_input) #print("convert_to_mp3: done converting " + a_file_input) From 9960a73bb830c75b80c89972e22c7288bf574866 Mon Sep 17 00:00:00 2001 From: b606 <7627533+b606@users.noreply.github.com> Date: Thu, 2 Mar 2023 12:50:51 +0100 Subject: [PATCH 10/11] spotifyripper.py: convert_to_opus (smaller file size) --- spotifyripper.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/spotifyripper.py b/spotifyripper.py index 38d2118..2dd7bcd 100755 --- a/spotifyripper.py +++ b/spotifyripper.py @@ -121,6 +121,40 @@ def convert_to_mp3(a_file_input, a_file_cover, a_album, a_artist, a_title, a_tra #print("convert_to_mp3: done converting " + a_file_input) +def convert_to_opus(a_file_input, a_file_cover, a_album, a_artist, a_title, a_track_number): + # Use internal filename to Protect from successive conversion + a_temp_file_input = a_file_input.replace(".wav", "-tmp.wav") + a_file_output = a_file_input.replace(".wav", ".opus") + os.rename(a_file_input, a_temp_file_input) + + sound = AudioSegment.from_wav(a_temp_file_input) + # Recommended: Opus at 128 KB/s (VBR) is pretty much transparent + sound.export(a_file_output, format="opus", codec="libopus", bitrate="192k", tags={ + "album": a_album, + "artist": a_artist, + "title": a_title, + "track": int(a_track_number) + } + ) + + a_file_output_size = os.stat(a_file_output).st_size + if a_file_output_size < 1048576: + print('\033[33m' + "Warning: small file " + a_file_output + " \033[0m\n") + + if a_file_output_size > 25485760: + print('\033[33m' + "Warning: large file " + a_file_output + " \033[0m\n") + + external_cover_file = os.path.dirname(a_file_cover) + "/cover.jpg" + if not os.path.isfile(external_cover_file): + os.rename(a_file_cover, external_cover_file) + else: + # print("DELETE " + a_file_cover) + os.remove(a_file_cover) + + # print("DELETE " + a_file_input) + os.remove(a_temp_file_input) + + def spotify_handler(*args): global pre_album global pre_artist @@ -206,6 +240,7 @@ def spotify_handler(*args): # convert previous file if os.path.isfile(pre_file_input): Process(target=convert_to_mp3, args=(pre_file_input, pre_file_cover, pre_album, pre_artist, pre_title, pre_track_number)).start() + # Process(target=convert_to_opus, args=(pre_file_input, pre_file_cover, pre_album, pre_artist, pre_title, pre_track_number)).start() # download cover file_cover = file_input.replace(".wav", ".jpg") From 9b6785a21138bc00a368df28f6c97e04f08b1e7c Mon Sep 17 00:00:00 2001 From: b606 <7627533+b606@users.noreply.github.com> Date: Thu, 9 Mar 2023 02:08:19 +0100 Subject: [PATCH 11/11] Refresh spotify_sink_index regularly --- spotifyripper.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/spotifyripper.py b/spotifyripper.py index 2dd7bcd..8a8c308 100755 --- a/spotifyripper.py +++ b/spotifyripper.py @@ -35,8 +35,9 @@ def get_spotify_sink_index(): with pulsectl.Pulse('spotify') as pulse: for sink in pulse.sink_input_list(): - # print("sink.name:" + sink.name) - # print("sink.corked:" + str(sink.corked)) + # if (sink.name == "Spotify"): + # print("sink.name:" + sink.name) + # print("sink.corked:" + str(sink.corked)) if (sink.name == "Spotify") and (sink.corked == False): return sink.index @@ -171,7 +172,7 @@ def spotify_handler(*args): metadata = args[1]["Metadata"] # debug - #pprint.pprint(metadata) + # pprint.pprint(metadata) artist = metadata["xesam:artist"][0] #print("Artist: " + artist) @@ -219,14 +220,17 @@ def spotify_handler(*args): pre_subprocess.terminate() file_input = path_album + "/" + str(track_number) + " - " + artist + " - " + title + ".wav" + # refresh spotify_sink_index + old_spotify_sink_index = spotify_sink_index + spotify_sink_index = get_spotify_sink_index() + if (old_spotify_sink_index != spotify_sink_index): + print("(info) spotify_sink_index: " + str(spotify_sink_index)) + + # If Spotify not found, do nothing if spotify_sink_index == -1: - spotify_sink_index = get_spotify_sink_index() - print("spotify_sink_index: " + str(spotify_sink_index)) - # If Spotify not found, do nothing - if spotify_sink_index == -1: - print("the Spotify client is not found.") - print("It has to be registered with the audio server by playing a sound.") - return + print("the Spotify client is not found.") + print("It has to be registered with the audio server by playing a sound.") + return if (artist != "") or (album != ""):