IPod-Shuffle-4g/ipod-shuffle-4g.py
2021-08-27 22:34:15 -04:00

814 lines
32 KiB
Python
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# Builtin libraries
import sys
import struct
import urllib.request, urllib.parse, urllib.error
import os
import hashlib
import subprocess
import collections
import errno
import argparse
import shutil
import re
import tempfile
import signal
# External libraries
try:
import mutagen
except ImportError:
mutagen = None
audio_ext = (".mp3", ".m4a", ".m4b", ".m4p", ".aa", ".wav")
list_ext = (".pls", ".m3u")
def make_dir_if_absent(path):
try:
os.makedirs(path)
except OSError as exc:
if exc.errno != errno.EEXIST:
raise
def raises_unicode_error(str):
try:
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])])
def validate_unicode(path):
path_list = path.split('/')
last_raise = False
for i in range(len(path_list)):
if raises_unicode_error(path_list[i]):
path_list[i] = hash_error_unicode(path_list[i])
last_raise = True
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 '')
def exec_exists_in_path(command):
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)
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]))))
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]))))
def group_tracks_by_id3_template(tracks, template):
grouped_tracks_dict = {}
template_vars = set(re.findall(r'{.*?}', template))
for track in tracks:
try:
id3_dict = mutagen.File(track, easy=True)
except:
id3_dict = {}
key = template
single_var_present = False
for var in template_vars:
val = id3_dict.get(var[1:-1], [''])[0]
if len(val) > 0:
single_var_present = True
key = key.replace(var, val)
if single_var_present:
if key not in grouped_tracks_dict:
grouped_tracks_dict[key] = []
grouped_tracks_dict[key].append(track)
return sorted(grouped_tracks_dict.items())
class Text2Speech(object):
valid_tts = {'pico2wave': True, 'RHVoice': True, 'espeak': True}
@staticmethod
def check_support():
voiceoverAvailable = False
# Check for pico2wave voiceover
if not exec_exists_in_path("pico2wave"):
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
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
print("Warning: RHVoice not found, Russian voicever won't be generated.")
else:
voiceoverAvailable = True
# Return if we at least found one voiceover program.
# Otherwise this will result in silent voiceover for tracks and "Playlist N" for playlists.
return voiceoverAvailable
@staticmethod
def text2speech(out_wav_path, text):
# Skip voiceover generation if a track with the same name is used.
# This might happen with "Track001" or "01. Intro" names for example.
if os.path.isfile(out_wav_path):
verboseprint("Using existing", out_wav_path)
return True
# ensure we deal with unicode later
if not isinstance(text, str):
text = str(text, 'utf-8')
lang = Text2Speech.guess_lang(text)
if lang == "ru-RU":
return Text2Speech.rhvoice(out_wav_path, text)
else:
if Text2Speech.pico2wave(out_wav_path, text):
return True
elif Text2Speech.espeak(out_wav_path, text):
return True
else:
return False
# guess-language seems like an overkill for now
@staticmethod
def guess_lang(unicodetext):
lang = 'en-GB'
if re.search("[А-Яа-я]", unicodetext) is not None:
lang = 'ru-RU'
return lang
@staticmethod
def pico2wave(out_wav_path, unicodetext):
if not Text2Speech.valid_tts['pico2wave']:
return False
subprocess.call(["pico2wave", "-l", "en-GB", "-w", out_wav_path, unicodetext])
return True
@staticmethod
def espeak(out_wav_path, unicodetext):
if not Text2Speech.valid_tts['espeak']:
return False
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']:
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'))
# make a little bit louder to be comparable with pico2wave
subprocess.call(["sox", tmp_file.name, out_wav_path, "norm"])
os.remove(tmp_file.name)
return True
class Record(object):
def __init__(self, parent):
self.parent = parent
self._struct = collections.OrderedDict([])
self._fields = {}
self.track_voiceover = parent.track_voiceover
self.playlist_voiceover = parent.playlist_voiceover
self.rename = parent.rename
self.trackgain = parent.trackgain
def __getitem__(self, item):
if item not in list(self._struct.keys()):
raise KeyError
return self._fields.get(item, self._struct[item][1])
def __setitem__(self, item, value):
self._fields[item] = value
def construct(self):
output = bytes()
for i in list(self._struct.keys()):
(fmt, default) = self._struct[i]
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:
# 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")
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")
baselen = len(self.base)
if self.base.endswith(os.path.sep):
baselen -= 1
ipodname = "/".join(os.path.abspath(filename)[baselen:].split(os.path.sep))
return ipodname
def ipod_to_path(self, ipodname):
return os.path.abspath(os.path.join(self.base, os.path.sep.join(ipodname.split("/"))))
@property
def shuffledb(self):
parent = self.parent
while parent.__class__ != Shuffler:
parent = parent.parent
return parent
@property
def base(self):
return self.shuffledb.path
@property
def tracks(self):
return self.shuffledb.tracks
@property
def albums(self):
return self.shuffledb.albums
@property
def artists(self):
return self.shuffledb.artists
@property
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([
("header_id", ("4s", b"bdhs")), # shdb
("unknown1", ("I", 0x02000003)),
("total_length", ("I", 64)),
("total_number_of_tracks", ("I", 0)),
("total_number_of_playlists", ("I", 0)),
("unknown2", ("Q", 0)),
("max_volume", ("B", 0)),
("voiceover_enabled", ("B", int(self.track_voiceover))),
("unknown3", ("H", 0)),
("total_tracks_without_podcasts", ("I", 0)),
("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
self.track_header.base_offset = 64
track_header = self.track_header.construct()
# The playlist offset will depend on the number of tracks
self.play_header.base_offset = self.track_header.base_offset + len(track_header)
play_header = self.play_header.construct(self.track_header.tracks)
self["playlist_header_offset"] = self.play_header.base_offset
self["total_number_of_tracks"] = self.track_header["number_of_tracks"]
self["total_tracks_without_podcasts"] = self.track_header.total_tracks_without_podcasts()
self["total_number_of_playlists"] = self.play_header["number_of_playlists"]
output = Record.construct(self)
return output + track_header + play_header
class TrackHeader(Record):
def __init__(self, parent):
self.base_offset = 0
self.total_podcasts = 0
Record.__init__(self, parent)
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)
self["total_length"] = 20 + (len(self.tracks) * 4)
output = Record.construct(self)
# Construct the underlying tracks
track_chunk = bytes()
for i in self.tracks:
track = Track(self)
verboseprint("[*] Adding track", i)
track.populate(i)
if track.is_podcast:
self.total_podcasts += 1
output += struct.pack("I", self.base_offset + self["total_length"] + len(track_chunk))
track_chunk += track.construct()
return output + track_chunk
def total_tracks_without_podcasts(self):
return self["number_of_tracks"] - self.total_podcasts
class Track(Record):
def __init__(self, parent):
Record.__init__(self, parent)
self.is_podcast = False
self._struct = collections.OrderedDict([
("header_id", ("4s", b"rths")), # shtr
("header_length", ("I", 0x174)),
("start_at_pos_ms", ("I", 0)),
("stop_at_pos_ms", ("I", 0)),
("volume_gain", ("I", int(self.trackgain))),
("filetype", ("I", 1)),
("filename", ("256s", b"\x00" * 256)),
("bookmark", ("I", 0)),
("dontskip", ("B", 1)),
("remember", ("B", 0)),
("unintalbum", ("B", 0)),
("unknown", ("B", 0)),
("pregap", ("I", 0x200)),
("postgap", ("I", 0x200)),
("numsamples", ("I", 0)),
("unknown2", ("I", 0)),
("gapless", ("I", 0)),
("unknown3", ("I", 0)),
("albumid", ("I", 0)),
("track", ("H", 1)),
("disc", ("H", 0)),
("unknown4", ("Q", 0)),
("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')
if os.path.splitext(filename)[1].lower() in (".m4a", ".m4b", ".m4p", ".aa"):
self["filetype"] = 2
if "/iPod_Control/Podcasts/" in filename:
self.is_podcast = True
self["dontskip"] = 0
text = os.path.splitext(os.path.basename(filename))[0]
# Try to get album and artist information with mutagen
if mutagen:
audio = None
try:
audio = mutagen.File(filename, easy = True)
except:
print("Error calling mutagen. Possible invalid filename/ID3Tags (hyphen in filename?)")
if audio:
if "Podcast" in audio.get("genre", ["Unknown"]):
self.is_podcast = True
self["dontskip"] = 0
# Note: Rythmbox IPod plugin sets this value always 0.
self["stop_at_pos_ms"] = int(audio.info.length * 1000)
artist = audio.get("artist", ["Unknown"])[0]
if artist in self.artists:
self["artistid"] = self.artists.index(artist)
else:
self["artistid"] = len(self.artists)
self.artists.append(artist)
album = audio.get("album", ["Unknown"])[0]
if album in self.albums:
self["albumid"] = self.albums.index(album)
else:
self["albumid"] = len(self.albums)
self.albums.append(album)
if audio.get("title", "") and audio.get("artist", ""):
text = " - ".join(audio.get("title", "") + audio.get("artist", ""))
# Handle the VoiceOverData
if isinstance(text, str):
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
("total_length", ("I", 0)),
("number_of_playlists", ("I", 0)),
("number_of_non_podcast_lists", ("H", 65535)),
("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
masterlist = Playlist(self)
verboseprint("[+] Adding master playlist")
masterlist.set_master(tracks)
chunks = [masterlist.construct(tracks)]
# Build all the remaining playlists
playlistcount = 1
podcastlistcount = 0
for i in self.lists:
playlist = Playlist(self)
verboseprint("[+] Adding playlist", (i[0] if type(i) == type(()) else i))
playlist.populate(i)
construction = playlist.construct(tracks)
if playlist["number_of_songs"] > 0:
if playlist["listtype"] == 3:
podcastlistcount += 1
playlistcount += 1
chunks += [construction]
else:
print("Error: Playlist does not contain a single track. Skipping playlist.")
self["number_of_playlists"] = playlistcount
if podcastlistcount > 0:
self["number_of_non_podcast_lists"] = playlistcount - podcastlistcount
self["total_length"] = 0x14 + (self["number_of_playlists"] * 4)
# Start the header
output = Record.construct(self)
offset = self.base_offset + self["total_length"]
for i in range(len(chunks)):
output += struct.pack("I", offset)
offset += len(chunks[i])
return output + b"".join(chunks)
class Playlist(Record):
def __init__(self, parent):
self.listtracks = []
Record.__init__(self, parent)
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))
])
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']):
self["dbid"] = hashlib.md5(b"masterlist").digest()[:8]
self.text_to_speech("All songs", self["dbid"], True)
self["listtype"] = 1
self.listtracks = tracks
def set_podcast(self):
self["listtype"] = 3
def populate_m3u(self, data):
listtracks = []
for i in data:
if not i.startswith("#"):
path = i.strip()
if self.rename:
path = validate_unicode(path)
listtracks.append(path)
return listtracks
def populate_pls(self, data):
sorttracks = []
for i in data:
dataarr = i.strip().split("=", 1)
if dataarr[0].lower().startswith("file"):
num = int(dataarr[0][4:])
filename = urllib.parse.unquote(dataarr[1]).strip()
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) ]
return listtracks
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".
if "/iPod_Control/Podcasts/" in playlistpath:
self.set_podcast()
listtracks = []
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()):
# Only add valid music files to playlist
if os.path.splitext(filename)[1].lower() in (".mp3", ".m4a", ".m4b", ".m4p", ".aa", ".wav"):
fullPath = os.path.abspath(os.path.join(dirpath, filename))
if os.path.islink(fullPath):
fullPath = os.path.realpath(fullPath)
listtracks.append(fullPath)
if not recursive:
break
return listtracks
def remove_relatives(self, relative, filename):
base = os.path.dirname(os.path.abspath(filename))
if not os.path.exists(relative):
relative = os.path.join(base, relative)
fullPath = relative
return fullPath
def populate(self, obj):
# Create a playlist of the folder and all subfolders
if type(obj) == type(()):
self.listtracks = obj[1]
text = obj[0]
else:
filename = obj
if os.path.isdir(filename):
self.listtracks = self.populate_directory(filename)
text = os.path.splitext(os.path.basename(filename))[0]
else:
# Read the playlist file
with open(filename, 'r', errors="replace") as f:
data = f.readlines()
extension = os.path.splitext(filename)[1].lower()
if extension == '.pls':
self.listtracks = self.populate_pls(data)
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)
text = os.path.splitext(os.path.basename(filename))[0]
# Handle the VoiceOverData
self["dbid"] = hashlib.md5(text.encode('utf-8')).digest()[:8]
self.text_to_speech(text, self["dbid"], True)
def construct(self, tracks):
self["total_length"] = 44 + (4 * len(self.listtracks))
self["number_of_songs"] = 0
chunks = bytes()
for i in self.listtracks:
path = self.ipod_to_path(i)
position = -1
if self["listtype"] == 1 and "/iPod_Control/Podcasts/" in path:
print ('not including podcast in master playlist: {}'.format(path))
continue
try:
position = tracks.index(path)
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.")
if position > -1:
chunks += struct.pack("I", position)
self["number_of_songs"] += 1
self["number_of_nonaudio"] = self["number_of_songs"]
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):
self.path = os.path.abspath(path)
self.tracks = []
self.albums = []
self.artists = []
self.lists = []
self.tunessd = None
self.track_voiceover = track_voiceover
self.playlist_voiceover = playlist_voiceover
self.rename = rename
self.trackgain = trackgain
self.auto_dir_playlists = auto_dir_playlists
self.auto_id3_playlists = auto_id3_playlists
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'):
shutil.rmtree(os.path.join(self.path, dirname), ignore_errors=True)
for dirname in ('iPod_Control/iTunes', 'iPod_Control/Music', 'iPod_Control/Podcasts', 'iPod_Control/Speakable/Playlists', 'iPod_Control/Speakable/Tracks'):
make_dir_if_absent(os.path.join(self.path, dirname))
def dump_state(self):
print("Shuffle DB state")
print("Tracks", self.tracks)
print("Albums", self.albums)
print("Artists", self.artists)
print("Playlists", self.lists)
def populate(self):
self.tunessd = TunesSD(self)
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()):
# Ignore hidden files
if not filename.startswith("."):
fullPath = os.path.abspath(os.path.join(dirpath, filename))
if os.path.islink(fullPath):
fullPath = os.path.realpath(fullPath)
if os.path.splitext(filename)[1].lower() in (".mp3", ".m4a", ".m4b", ".m4p", ".aa", ".wav"):
if fullPath not in self.tracks:
self.tracks.append(fullPath)
if os.path.splitext(filename)[1].lower() in (".pls", ".m3u"):
if fullPath not in self.lists:
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 or "iPod_Control/Podcasts/" 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
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):
self.lists.append(grouped_list)
else:
print("Error: No mutagen found. Cannot generate auto-id3-playlists.")
sys.exit(1)
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:
try:
f.write(self.tunessd.construct())
except IOError as e:
print("I/O error({0}): {1}".format(e.errno, e.strerror))
print("Error: Writing iPod database failed.")
sys.exit(1)
print("Database written successfully:")
print("Tracks", len(self.tracks))
print("Albums", len(self.albums))
print("Artists", len(self.artists))
print("Playlists", len(self.lists))
#
# Read all files from the directory
# Construct the appropriate iTunesDB file
# Construct the appropriate iTunesSD file
# http://shuffle3db.wikispaces.com/iTunesSD3gen
# 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:
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))
os.rename(src, dest)
else:
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))
os.rename(src, dest)
return ret_flag
def nonnegative_int(string):
try:
intval = int(string)
except ValueError:
raise argparse.ArgumentTypeError("'%s' must be an integer" % string)
if intval < 0 or intval > 99:
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')
sys.exit(1)
def handle_interrupt(signal, frame):
print("Interrupt detected, exiting...")
sys.exit(1)
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.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('-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('-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).')
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('path', help='Path to the IPod\'s root directory')
result = parser.parse_args()
# Enable verbose printing if desired
verboseprint = print if result.verbose else lambda *a, **k: None
checkPathValidity(result.path)
if result.rename_unicode:
check_unicode(result.path)
if not mutagen:
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 not Text2Speech.check_support():
print("Error: Did not find any voiceover program. Voiceover disabled.")
result.track_voiceover = False
result.playlist_voiceover = False
else:
verboseprint("Voiceover available.")
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)
shuffle.initialize()
shuffle.populate()
shuffle.write_database()