This commit is contained in:
Avi Herman 2024-07-07 10:05:57 -04:00 committed by GitHub
commit a97a2aa474
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,19 +1,21 @@
#!/usr/bin/env python3
# Builtin libraries
import sys
import struct
import urllib.request, urllib.parse, urllib.error
import os
import hashlib
import subprocess
import argparse
import collections
import errno
import argparse
import shutil
import hashlib
import os
import re
import tempfile
import shutil
import signal
import struct
import subprocess
import sys
import tempfile
import urllib.error
import urllib.parse
import urllib.request
# External libraries
try:
@ -23,6 +25,8 @@ except ImportError:
audio_ext = (".mp3", ".m4a", ".m4b", ".m4p", ".aa", ".wav")
list_ext = (".pls", ".m3u")
def make_dir_if_absent(path):
try:
os.makedirs(path)
@ -30,19 +34,27 @@ def make_dir_if_absent(path):
if exc.errno != errno.EEXIST:
raise
def raises_unicode_error(str):
try:
str.encode('latin-1')
str.encode("latin-1")
return False
except (UnicodeEncodeError, UnicodeDecodeError):
return True
def hash_error_unicode(item):
item_bytes = item.encode('utf-8')
return "".join(["{0:02X}".format(ord(x)) for x in reversed(hashlib.md5(item_bytes).hexdigest()[:8])])
item_bytes = item.encode("utf-8")
return "".join(
[
"{0:02X}".format(ord(x))
for x in reversed(hashlib.md5(item_bytes).hexdigest()[:8])
]
)
def validate_unicode(path):
path_list = path.split('/')
path_list = path.split("/")
last_raise = False
for i in range(len(path_list)):
if raises_unicode_error(path_list[i]):
@ -51,30 +63,43 @@ def validate_unicode(path):
else:
last_raise = False
extension = os.path.splitext(path)[1].lower()
return "/".join(path_list) + (extension if last_raise and extension in audio_ext else '')
return "/".join(path_list) + (
extension if last_raise and extension in audio_ext else ""
)
def exec_exists_in_path(command):
with open(os.devnull, 'w') as FNULL:
with open(os.devnull, "w") as FNULL:
try:
with open(os.devnull, 'r') as RFNULL:
subprocess.call([command], stdout=FNULL, stderr=subprocess.STDOUT, stdin=RFNULL)
with open(os.devnull, "r") as RFNULL:
subprocess.call(
[command], stdout=FNULL, stderr=subprocess.STDOUT, stdin=RFNULL
)
return True
except OSError as e:
return False
def splitpath(path):
return path.split(os.sep)
def get_relpath(path, basepath):
commonprefix = os.sep.join(os.path.commonprefix(list(map(splitpath, [path, basepath]))))
commonprefix = os.sep.join(
os.path.commonprefix(list(map(splitpath, [path, basepath])))
)
return os.path.relpath(path, commonprefix)
def is_path_prefix(prefix, path):
return prefix == os.sep.join(os.path.commonprefix(list(map(splitpath, [prefix, path]))))
return prefix == os.sep.join(
os.path.commonprefix(list(map(splitpath, [prefix, path])))
)
def group_tracks_by_id3_template(tracks, template):
grouped_tracks_dict = {}
template_vars = set(re.findall(r'{.*?}', template))
template_vars = set(re.findall(r"{.*?}", template))
for track in tracks:
try:
id3_dict = mutagen.File(track, easy=True)
@ -84,7 +109,7 @@ def group_tracks_by_id3_template(tracks, template):
key = template
single_var_present = False
for var in template_vars:
val = id3_dict.get(var[1:-1], [''])[0]
val = id3_dict.get(var[1:-1], [""])[0]
if len(val) > 0:
single_var_present = True
key = key.replace(var, val)
@ -96,8 +121,9 @@ def group_tracks_by_id3_template(tracks, template):
return sorted(grouped_tracks_dict.items())
class Text2Speech(object):
valid_tts = {'pico2wave': True, 'RHVoice': True, 'espeak': True, 'say': True}
valid_tts = {"pico2wave": True, "RHVoice": True, "espeak": True, "say": True}
@staticmethod
def check_support():
@ -105,28 +131,28 @@ class Text2Speech(object):
# Check for macOS say voiceover
if not exec_exists_in_path("say"):
Text2Speech.valid_tts['say'] = False
Text2Speech.valid_tts["say"] = False
print("Warning: macOS say not found, voicever won't be generated using it.")
else:
voiceoverAvailable = True
# Check for pico2wave voiceover
if not exec_exists_in_path("pico2wave"):
Text2Speech.valid_tts['pico2wave'] = False
Text2Speech.valid_tts["pico2wave"] = False
print("Warning: pico2wave not found, voicever won't be generated using it.")
else:
voiceoverAvailable = True
# Check for espeak voiceover
if not exec_exists_in_path("espeak"):
Text2Speech.valid_tts['espeak'] = False
Text2Speech.valid_tts["espeak"] = False
print("Warning: espeak not found, voicever won't be generated using it.")
else:
voiceoverAvailable = True
# Check for Russian RHVoice voiceover
if not exec_exists_in_path("RHVoice"):
Text2Speech.valid_tts['RHVoice'] = False
Text2Speech.valid_tts["RHVoice"] = False
print("Warning: RHVoice not found, Russian voicever won't be generated.")
else:
voiceoverAvailable = True
@ -145,7 +171,7 @@ class Text2Speech(object):
# ensure we deal with unicode later
if not isinstance(text, str):
text = str(text, 'utf-8')
text = str(text, "utf-8")
lang = Text2Speech.guess_lang(text)
if lang == "ru-RU":
return Text2Speech.rhvoice(out_wav_path, text)
@ -162,42 +188,76 @@ class Text2Speech(object):
# guess-language seems like an overkill for now
@staticmethod
def guess_lang(unicodetext):
lang = 'en-GB'
lang = "en-GB"
if re.search("[А-Яа-я]", unicodetext) is not None:
lang = 'ru-RU'
lang = "ru-RU"
return lang
@staticmethod
def pico2wave(out_wav_path, unicodetext):
if not Text2Speech.valid_tts['pico2wave']:
if not Text2Speech.valid_tts["pico2wave"]:
return False
subprocess.call(["pico2wave", "-l", "en-GB", "-w", out_wav_path, '--', unicodetext])
subprocess.call(
["pico2wave", "-l", "en-GB", "-w", out_wav_path, " ", unicodetext]
)
return True
@staticmethod
def say(out_wav_path, unicodetext):
if not Text2Speech.valid_tts['say']:
if not Text2Speech.valid_tts["say"]:
return False
subprocess.call(["say", "-o", out_wav_path, '--data-format=LEI16', '--file-format=WAVE', '--', unicodetext])
subprocess.call(
[
"say",
"-o",
out_wav_path,
"--data-format=LEI16",
"--file-format=WAVE",
"--",
unicodetext,
]
)
return True
@staticmethod
def espeak(out_wav_path, unicodetext):
if not Text2Speech.valid_tts['espeak']:
if not Text2Speech.valid_tts["espeak"]:
return False
subprocess.call(["espeak", "-v", "english_rp", "-s", "150", "-w", out_wav_path, '--', unicodetext])
subprocess.call(
[
"espeak",
"-v",
"english_rp",
"-s",
"150",
"-w",
out_wav_path,
"--",
unicodetext,
]
)
return True
@staticmethod
def rhvoice(out_wav_path, unicodetext):
if not Text2Speech.valid_tts['RHVoice']:
if not Text2Speech.valid_tts["RHVoice"]:
return False
tmp_file = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
tmp_file.close()
proc = subprocess.Popen(["RHVoice", "--voice=Elena", "--variant=Russian", "--volume=100", "-o", tmp_file.name], stdin=subprocess.PIPE)
proc.communicate(input=unicodetext.encode('utf-8'))
proc = subprocess.Popen(
[
"RHVoice",
"--voice=Elena",
"--variant=Russian",
"--volume=100",
"-o",
tmp_file.name,
],
stdin=subprocess.PIPE,
)
proc.communicate(input=unicodetext.encode("utf-8"))
# make a little bit louder to be comparable with pico2wave
subprocess.call(["sox", tmp_file.name, out_wav_path, "norm"])
@ -231,17 +291,30 @@ class Record(object):
output += struct.pack("<" + fmt, self._fields.get(i, default))
return output
def text_to_speech(self, text, dbid, playlist = False):
if self.track_voiceover and not playlist or self.playlist_voiceover and playlist:
def text_to_speech(self, text, dbid, playlist=False):
if (
self.track_voiceover
and not playlist
or self.playlist_voiceover
and playlist
):
# Create the voiceover wav file
fn = ''.join(format(x, '02x') for x in reversed(dbid))
path = os.path.join(self.base, "iPod_Control", "Speakable", "Tracks" if not playlist else "Playlists", fn + ".wav")
fn = "".join(format(x, "02x") for x in reversed(dbid))
path = os.path.join(
self.base,
"iPod_Control",
"Speakable",
"Tracks" if not playlist else "Playlists",
fn + ".wav",
)
return Text2Speech.text2speech(path, text)
return False
def path_to_ipod(self, filename):
if os.path.commonprefix([os.path.abspath(filename), self.base]) != self.base:
raise IOError("Cannot get Ipod filename, since file is outside the IPOD path")
raise IOError(
"Cannot get Ipod filename, since file is outside the IPOD path"
)
baselen = len(self.base)
if self.base.endswith(os.path.sep):
baselen -= 1
@ -249,7 +322,9 @@ class Record(object):
return ipodname
def ipod_to_path(self, ipodname):
return os.path.abspath(os.path.join(self.base, os.path.sep.join(ipodname.split("/"))))
return os.path.abspath(
os.path.join(self.base, os.path.sep.join(ipodname.split("/")))
)
@property
def shuffledb(self):
@ -278,12 +353,14 @@ class Record(object):
def lists(self):
return self.shuffledb.lists
class TunesSD(Record):
def __init__(self, parent):
Record.__init__(self, parent)
self.track_header = TrackHeader(self)
self.play_header = PlaylistHeader(self)
self._struct = collections.OrderedDict([
self._struct = collections.OrderedDict(
[
("header_id", ("4s", b"bdhs")), # shdb
("unknown1", ("I", 0x02000003)),
("total_length", ("I", 64)),
@ -297,7 +374,8 @@ class TunesSD(Record):
("track_header_offset", ("I", 64)),
("playlist_header_offset", ("I", 0)),
("unknown4", ("20s", b"\x00" * 20)),
])
]
)
def construct(self):
# The header is a fixed length, so no need to calculate it
@ -316,16 +394,19 @@ class TunesSD(Record):
output = Record.construct(self)
return output + track_header + play_header
class TrackHeader(Record):
def __init__(self, parent):
self.base_offset = 0
Record.__init__(self, parent)
self._struct = collections.OrderedDict([
self._struct = collections.OrderedDict(
[
("header_id", ("4s", b"hths")), # shth
("total_length", ("I", 0)),
("number_of_tracks", ("I", 0)),
("unknown1", ("Q", 0)),
])
]
)
def construct(self):
self["number_of_tracks"] = len(self.tracks)
@ -338,15 +419,19 @@ class TrackHeader(Record):
track = Track(self)
verboseprint("[*] Adding track", i)
track.populate(i)
output += struct.pack("I", self.base_offset + self["total_length"] + len(track_chunk))
output += struct.pack(
"I", self.base_offset + self["total_length"] + len(track_chunk)
)
track_chunk += track.construct()
return output + track_chunk
class Track(Record):
def __init__(self, parent):
Record.__init__(self, parent)
self._struct = collections.OrderedDict([
self._struct = collections.OrderedDict(
[
("header_id", ("4s", b"rths")), # shtr
("header_length", ("I", 0x174)),
("start_at_pos_ms", ("I", 0)),
@ -372,10 +457,11 @@ class Track(Record):
("dbid", ("8s", 0)),
("artistid", ("I", 0)),
("unknown5", ("32s", b"\x00" * 32)),
])
]
)
def populate(self, filename):
self["filename"] = self.path_to_ipod(filename).encode('utf-8')
self["filename"] = self.path_to_ipod(filename).encode("utf-8")
if os.path.splitext(filename)[1].lower() in (".m4a", ".m4b", ".m4p", ".aa"):
self["filetype"] = 2
@ -386,9 +472,11 @@ class Track(Record):
if mutagen:
audio = None
try:
audio = mutagen.File(filename, easy = True)
audio = mutagen.File(filename, easy=True)
except:
print("Error calling mutagen. Possible invalid filename/ID3Tags (hyphen in filename?)")
print(
"Error calling mutagen. Possible invalid filename/ID3Tags (hyphen in filename?)"
)
if audio:
# Note: Rythmbox IPod plugin sets this value always 0.
self["stop_at_pos_ms"] = int(audio.info.length * 1000)
@ -412,23 +500,26 @@ class Track(Record):
# Handle the VoiceOverData
if isinstance(text, str):
text = text.encode('utf-8', 'ignore')
text = text.encode("utf-8", "ignore")
self["dbid"] = hashlib.md5(text).digest()[:8]
self.text_to_speech(text, self["dbid"])
class PlaylistHeader(Record):
def __init__(self, parent):
self.base_offset = 0
Record.__init__(self, parent)
self._struct = collections.OrderedDict([
("header_id", ("4s", b"hphs")), #shph
self._struct = collections.OrderedDict(
[
("header_id", ("4s", b"hphs")), # shph
("total_length", ("I", 0)),
("number_of_playlists", ("I", 0)),
("number_of_non_podcast_lists", ("2s", b"\xFF\xFF")),
("number_of_master_lists", ("2s", b"\x01\x00")),
("number_of_non_audiobook_lists", ("2s", b"\xFF\xFF")),
("unknown2", ("2s", b"\x00" * 2)),
])
]
)
def construct(self, tracks):
# Build the master list
@ -448,7 +539,9 @@ class PlaylistHeader(Record):
playlistcount += 1
chunks += [construction]
else:
print("Error: Playlist does not contain a single track. Skipping playlist.")
print(
"Error: Playlist does not contain a single track. Skipping playlist."
)
self["number_of_playlists"] = playlistcount
self["total_length"] = 0x14 + (self["number_of_playlists"] * 4)
@ -463,24 +556,31 @@ class PlaylistHeader(Record):
return output + b"".join(chunks)
class Playlist(Record):
def __init__(self, parent):
self.listtracks = []
Record.__init__(self, parent)
self._struct = collections.OrderedDict([
self._struct = collections.OrderedDict(
[
("header_id", ("4s", b"lphs")), # shpl
("total_length", ("I", 0)),
("number_of_songs", ("I", 0)),
("number_of_nonaudio", ("I", 0)),
("dbid", ("8s", b"\x00" * 8)),
("listtype", ("I", 2)),
("unknown1", ("16s", b"\x00" * 16))
])
("unknown1", ("16s", b"\x00" * 16)),
]
)
def set_master(self, tracks):
# By default use "All Songs" builtin voiceover (dbid all zero)
# Else generate alternative "All Songs" to fit the speaker voice of other playlists
if self.playlist_voiceover and (Text2Speech.valid_tts['pico2wave'] or Text2Speech.valid_tts['espeak'] or Text2Speech.valid_tts['say']):
if self.playlist_voiceover and (
Text2Speech.valid_tts["pico2wave"]
or Text2Speech.valid_tts["espeak"]
or Text2Speech.valid_tts["say"]
):
self["dbid"] = hashlib.md5(b"masterlist").digest()[:8]
self.text_to_speech("All songs", self["dbid"], True)
self["listtype"] = 1
@ -503,28 +603,35 @@ class Playlist(Record):
if dataarr[0].lower().startswith("file"):
num = int(dataarr[0][4:])
filename = urllib.parse.unquote(dataarr[1]).strip()
if filename.lower().startswith('file://'):
if filename.lower().startswith("file://"):
filename = filename[7:]
if self.rename:
filename = validate_unicode(filename)
sorttracks.append((num, filename))
listtracks = [ x for (_, x) in sorted(sorttracks) ]
listtracks = [x for (_, x) in sorted(sorttracks)]
return listtracks
def populate_directory(self, playlistpath, recursive = True):
def populate_directory(self, playlistpath, recursive=True):
# Add all tracks inside the folder and its subfolders recursively.
# Folders containing no music and only a single Album
# would generate duplicated playlists. That is intended and "wont fix".
# Empty folders (inside the music path) will generate an error -> "wont fix".
listtracks = []
for (dirpath, dirnames, filenames) in os.walk(playlistpath):
for dirpath, dirnames, filenames in os.walk(playlistpath):
dirnames.sort()
# Ignore any hidden directories
if "/." not in dirpath:
for filename in sorted(filenames, key = lambda x: x.lower()):
for filename in sorted(filenames, key=lambda x: x.lower()):
# Only add valid music files to playlist
if os.path.splitext(filename)[1].lower() in (".mp3", ".m4a", ".m4b", ".m4p", ".aa", ".wav"):
if os.path.splitext(filename)[1].lower() in (
".mp3",
".m4a",
".m4b",
".m4p",
".aa",
".wav",
):
fullPath = os.path.abspath(os.path.join(dirpath, filename))
listtracks.append(fullPath)
if not recursive:
@ -550,24 +657,26 @@ class Playlist(Record):
text = os.path.splitext(os.path.basename(filename))[0]
else:
# Read the playlist file
with open(filename, 'r', errors="replace") as f:
with open(filename, "r", errors="replace") as f:
data = f.readlines()
extension = os.path.splitext(filename)[1].lower()
if extension == '.pls':
if extension == ".pls":
self.listtracks = self.populate_pls(data)
elif extension == '.m3u':
elif extension == ".m3u":
self.listtracks = self.populate_m3u(data)
else:
raise
# Ensure all paths are not relative to the playlist file
for i in range(len(self.listtracks)):
self.listtracks[i] = self.remove_relatives(self.listtracks[i], filename)
self.listtracks[i] = self.remove_relatives(
self.listtracks[i], filename
)
text = os.path.splitext(os.path.basename(filename))[0]
# Handle the VoiceOverData
self["dbid"] = hashlib.md5(text.encode('utf-8')).digest()[:8]
self["dbid"] = hashlib.md5(text.encode("utf-8")).digest()[:8]
self.text_to_speech(text, self["dbid"], True)
def construct(self, tracks):
@ -583,8 +692,10 @@ class Playlist(Record):
except:
# Print an error if no track was found.
# Empty playlists are handeled in the PlaylistHeader class.
print("Error: Could not find track \"" + path + "\".")
print("Maybe its an invalid FAT filesystem name. Please fix your playlist. Skipping track.")
print('Error: Could not find track "' + path + '".')
print(
"Maybe its an invalid FAT filesystem name. Please fix your playlist. Skipping track."
)
if position > -1:
chunks += struct.pack("I", position)
self["number_of_songs"] += 1
@ -593,8 +704,18 @@ class Playlist(Record):
output = Record.construct(self)
return output + chunks
class Shuffler(object):
def __init__(self, path, track_voiceover=False, playlist_voiceover=False, rename=False, trackgain=0, auto_dir_playlists=None, auto_id3_playlists=None):
def __init__(
self,
path,
track_voiceover=False,
playlist_voiceover=False,
rename=False,
trackgain=0,
auto_dir_playlists=None,
auto_id3_playlists=None,
):
self.path = os.path.abspath(path)
self.tracks = []
self.albums = []
@ -610,9 +731,17 @@ class Shuffler(object):
def initialize(self):
# remove existing voiceover files (they are either useless or will be overwritten anyway)
for dirname in ('iPod_Control/Speakable/Playlists', 'iPod_Control/Speakable/Tracks'):
for dirname in (
"iPod_Control/Speakable/Playlists",
"iPod_Control/Speakable/Tracks",
):
shutil.rmtree(os.path.join(self.path, dirname), ignore_errors=True)
for dirname in ('iPod_Control/iTunes', 'iPod_Control/Music', 'iPod_Control/Speakable/Playlists', 'iPod_Control/Speakable/Tracks'):
for dirname in (
"iPod_Control/iTunes",
"iPod_Control/Music",
"iPod_Control/Speakable/Playlists",
"iPod_Control/Speakable/Tracks",
):
make_dir_if_absent(os.path.join(self.path, dirname))
def dump_state(self):
@ -624,31 +753,49 @@ class Shuffler(object):
def populate(self):
self.tunessd = TunesSD(self)
for (dirpath, dirnames, filenames) in os.walk(self.path):
for dirpath, dirnames, filenames in os.walk(self.path):
dirnames.sort()
relpath = get_relpath(dirpath, self.path)
# Ignore the speakable directory and any hidden directories
if not is_path_prefix("iPod_Control/Speakable", relpath) and "/." not in dirpath:
for filename in sorted(filenames, key = lambda x: x.lower()):
if (
not is_path_prefix("iPod_Control/Speakable", relpath)
and "/." not in dirpath
):
for filename in sorted(filenames, key=lambda x: x.lower()):
# Ignore hidden files
if not filename.startswith("."):
fullPath = os.path.abspath(os.path.join(dirpath, filename))
if os.path.splitext(filename)[1].lower() in (".mp3", ".m4a", ".m4b", ".m4p", ".aa", ".wav"):
if os.path.splitext(filename)[1].lower() in (
".mp3",
".m4a",
".m4b",
".m4p",
".aa",
".wav",
):
self.tracks.append(fullPath)
if os.path.splitext(filename)[1].lower() in (".pls", ".m3u"):
self.lists.append(fullPath)
# Create automatic playlists in music directory.
# Ignore the (music) root and any hidden directories.
if self.auto_dir_playlists and "iPod_Control/Music/" in dirpath and "/." not in dirpath:
if (
self.auto_dir_playlists
and "iPod_Control/Music/" in dirpath
and "/." not in dirpath
):
# Only go to a specific depth. -1 is unlimted, 0 is ignored as there is already a master playlist.
depth = dirpath[len(self.path) + len(os.path.sep):].count(os.path.sep) - 1
depth = (
dirpath[len(self.path) + len(os.path.sep) :].count(os.path.sep) - 1
)
if self.auto_dir_playlists < 0 or depth <= self.auto_dir_playlists:
self.lists.append(os.path.abspath(dirpath))
if self.auto_id3_playlists != None:
if mutagen:
for grouped_list in group_tracks_by_id3_template(self.tracks, self.auto_id3_playlists):
for grouped_list in group_tracks_by_id3_template(
self.tracks, self.auto_id3_playlists
):
self.lists.append(grouped_list)
else:
print("Error: No mutagen found. Cannot generate auto-id3-playlists.")
@ -656,7 +803,9 @@ class Shuffler(object):
def write_database(self):
print("Writing database. This may take a while...")
with open(os.path.join(self.path, "iPod_Control", "iTunes", "iTunesSD"), "wb") as f:
with open(
os.path.join(self.path, "iPod_Control", "iTunes", "iTunesSD"), "wb"
) as f:
try:
f.write(self.tunessd.construct())
except IOError as e:
@ -670,6 +819,7 @@ class Shuffler(object):
print("Artists", len(self.artists))
print("Playlists", len(self.lists))
#
# Read all files from the directory
# Construct the appropriate iTunesDB file
@ -678,27 +828,32 @@ class Shuffler(object):
# Use SVOX pico2wave and RHVoice to produce voiceover data
#
def check_unicode(path):
ret_flag = False # True if there is a recognizable file within this level
for item in os.listdir(path):
if os.path.isfile(os.path.join(path, item)):
if os.path.splitext(item)[1].lower() in audio_ext+list_ext:
if os.path.splitext(item)[1].lower() in audio_ext + list_ext:
ret_flag = True
if raises_unicode_error(item):
src = os.path.join(path, item)
dest = os.path.join(path, hash_error_unicode(item)) + os.path.splitext(item)[1].lower()
print('Renaming %s -> %s' % (src, dest))
dest = (
os.path.join(path, hash_error_unicode(item))
+ os.path.splitext(item)[1].lower()
)
print("Renaming %s -> %s" % (src, dest))
os.rename(src, dest)
else:
ret_flag = (check_unicode(os.path.join(path, item)) or ret_flag)
ret_flag = check_unicode(os.path.join(path, item)) or ret_flag
if ret_flag and raises_unicode_error(item):
src = os.path.join(path, item)
new_name = hash_error_unicode(item)
dest = os.path.join(path, new_name)
print('Renaming %s -> %s' % (src, dest))
print("Renaming %s -> %s" % (src, dest))
os.rename(src, dest)
return ret_flag
def nonnegative_int(string):
try:
intval = int(string)
@ -709,57 +864,97 @@ def nonnegative_int(string):
raise argparse.ArgumentTypeError("Track gain value should be in range 0-99")
return intval
def checkPathValidity(path):
if not os.path.isdir(result.path):
print("Error finding IPod directory. Maybe it is not connected or mounted?")
sys.exit(1)
if not os.access(result.path, os.W_OK):
print('Unable to get write permissions in the IPod directory')
print("Unable to get write permissions in the IPod directory")
sys.exit(1)
def handle_interrupt(signal, frame):
print("Interrupt detected, exiting...")
sys.exit(1)
if __name__ == '__main__':
if __name__ == "__main__":
signal.signal(signal.SIGINT, handle_interrupt)
parser = argparse.ArgumentParser(description=
'Python script for building the Track and Playlist database '
'for the newer gen IPod Shuffle. Version 1.5')
parser = argparse.ArgumentParser(
description="Python script for building the Track and Playlist database "
"for the newer gen IPod Shuffle. Version 1.5"
)
parser.add_argument('-t', '--track-voiceover', action='store_true',
help='Enable track voiceover feature')
parser.add_argument(
"-t",
"--track-voiceover",
action="store_true",
help="Enable track voiceover feature",
)
parser.add_argument('-p', '--playlist-voiceover', action='store_true',
help='Enable playlist voiceover feature')
parser.add_argument(
"-p",
"--playlist-voiceover",
action="store_true",
help="Enable playlist voiceover feature",
)
parser.add_argument('-u', '--rename-unicode', action='store_true',
help='Rename files causing unicode errors, will do minimal required renaming')
parser.add_argument(
"-u",
"--rename-unicode",
action="store_true",
help="Rename files causing unicode errors, will do minimal required renaming",
)
parser.add_argument('-g', '--track-gain', type=nonnegative_int, default='0',
help='Specify volume gain (0-99) for all tracks; '
'0 (default) means no gain and is usually fine; '
'e.g. 60 is very loud even on minimal player volume')
parser.add_argument(
"-g",
"--track-gain",
type=nonnegative_int,
default="0",
help="Specify volume gain (0-99) for all tracks; "
"0 (default) means no gain and is usually fine; "
"e.g. 60 is very loud even on minimal player volume",
)
parser.add_argument('-d', '--auto-dir-playlists', type=int, default=None, const=-1, nargs='?',
help='Generate automatic playlists for each folder recursively inside '
parser.add_argument(
"-d",
"--auto-dir-playlists",
type=int,
default=None,
const=-1,
nargs="?",
help="Generate automatic playlists for each folder recursively inside "
'"IPod_Control/Music/". You can optionally limit the depth: '
'0=root, 1=artist, 2=album, n=subfoldername, default=-1 (No Limit).')
"0=root, 1=artist, 2=album, n=subfoldername, default=-1 (No Limit).",
)
parser.add_argument('-i', '--auto-id3-playlists', type=str, default=None, metavar='ID3_TEMPLATE', const='{artist}', nargs='?',
help='Generate automatic playlists based on the id3 tags of any music '
'added to the iPod. You can optionally specify a template string '
'based on which id3 tags are used to generate playlists. For eg. '
'\'{artist} - {album}\' will use the pair of artist and album to group '
'tracks under one playlist. Similarly \'{genre}\' will group tracks based '
'on their genre tag. Default template used is \'{artist}\'')
parser.add_argument(
"-i",
"--auto-id3-playlists",
type=str,
default=None,
metavar="ID3_TEMPLATE",
const="{artist}",
nargs="?",
help="Generate automatic playlists based on the id3 tags of any music "
"added to the iPod. You can optionally specify a template string "
"based on which id3 tags are used to generate playlists. For eg. "
"'{artist} - {album}' will use the pair of artist and album to group "
"tracks under one playlist. Similarly '{genre}' will group tracks based "
"on their genre tag. Default template used is '{artist}'",
)
parser.add_argument('-v', '--verbose', action='store_true',
help='Show verbose output of database generation.')
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Show verbose output of database generation.",
)
parser.add_argument('path', help='Path to the IPod\'s root directory')
parser.add_argument("path", help="Path to the IPod's root directory")
result = parser.parse_args()
@ -772,11 +967,13 @@ if __name__ == '__main__':
check_unicode(result.path)
if not mutagen:
print("Warning: No mutagen found. Database will not contain any album nor artist information.")
print(
"Warning: No mutagen found. Database will not contain any album nor artist information."
)
verboseprint("Playlist voiceover requested:", result.playlist_voiceover)
verboseprint("Track voiceover requested:", result.track_voiceover)
if (result.track_voiceover or result.playlist_voiceover):
if result.track_voiceover or result.playlist_voiceover:
if not Text2Speech.check_support():
print("Error: Did not find any voiceover program. Voiceover disabled.")
result.track_voiceover = False
@ -784,13 +981,15 @@ if __name__ == '__main__':
else:
verboseprint("Voiceover available.")
shuffle = Shuffler(result.path,
shuffle = Shuffler(
result.path,
track_voiceover=result.track_voiceover,
playlist_voiceover=result.playlist_voiceover,
rename=result.rename_unicode,
trackgain=result.track_gain,
auto_dir_playlists=result.auto_dir_playlists,
auto_id3_playlists=result.auto_id3_playlists)
auto_id3_playlists=result.auto_id3_playlists,
)
shuffle.initialize()
shuffle.populate()
shuffle.write_database()