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