diff --git a/docs/iTunesSD3gen.md b/docs/iTunesSD3gen.md index 8580603..61b2b47 100644 --- a/docs/iTunesSD3gen.md +++ b/docs/iTunesSD3gen.md @@ -115,7 +115,7 @@ Here's the general layout of an iTunesSD file:
1
-
+ Only applies for tracks, not for playlists.
1
diff --git a/shuffle.py b/shuffle.py index 20e73f4..0f57b47 100755 --- a/shuffle.py +++ b/shuffle.py @@ -70,14 +70,20 @@ class Text2Speech(object): @staticmethod def text2speech(out_wav_path, text): + # Skip voiceover geneartion 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): + print "Using existing", out_wav_path + return True + # ensure we deal with unicode later if not isinstance(text, unicode): text = unicode(text, 'utf-8') lang = Text2Speech.guess_lang(text) if lang == "ru-RU": - Text2Speech.rhvoice(out_wav_path, text) + return Text2Speech.rhvoice(out_wav_path, text) else: - Text2Speech.pico2wave(out_wav_path, text) + return Text2Speech.pico2wave(out_wav_path, text) # guess-language seems like an overkill for now @staticmethod @@ -92,6 +98,7 @@ class Text2Speech(object): if not Text2Speech.valid_tts['pico2wave']: return False subprocess.call(["pico2wave", "-l", "en-GB", "-w", out_wav_path, unicodetext]) + return True @staticmethod def rhvoice(out_wav_path, unicodetext): @@ -107,6 +114,7 @@ class Text2Speech(object): subprocess.call(["sox", tmp_file.name, out_wav_path, "norm"]) os.remove(tmp_file.name) + return True class Record(object): @@ -141,7 +149,8 @@ class Record(object): # Create the voiceover wav file fn = "".join(["{0:02X}".format(ord(x)) for x in reversed(dbid)]) path = os.path.join(self.base, "iPod_Control", "Speakable", "Tracks" if not playlist else "Playlists", fn + ".wav") - Text2Speech.text2speech(path, text) + 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: @@ -189,7 +198,7 @@ class TunesSD(Record): self.play_header = PlaylistHeader(self) self._struct = collections.OrderedDict([ ("header_id", ("4s", "shdb")), - ("unknown1", ("I", 0x02010001)), + ("unknown1", ("I", 0x02000003)), ("total_length", ("I", 64)), ("total_number_of_tracks", ("I", 0)), ("total_number_of_playlists", ("I", 0)), @@ -320,16 +329,10 @@ class PlaylistHeader(Record): ("header_id", ("4s", "shph")), ("total_length", ("I", 0)), ("number_of_playlists", ("I", 0)), - ("number_of_podcast_lists", ("I", 0xffffffff)), - ("number_of_master_lists", ("I", 0)), - ("number_of_audiobook_lists", ("I", 0xffffffff)), - ("unknown1", ("I", 0)), - ("unknown2", ("I", 0xffffffff)), - ("unknown3", ("I", 0)), - ("unknown4", ("I", 0xffffffff)), - ("unknown5", ("I", 0)), - ("unknown6", ("I", 0xffffffff)), - ("unknown7", ("20s", "\x00" * 20)), + ("number_of_non_podcast_lists", ("2s", "\x03\x00")), #TODO check if really ffff is okay + ("number_of_master_lists", ("2s", "\x01\x00")), + ("number_of_non_audiobook_lists", ("2s", "\x03\x00")), #TODO as above + ("unknown2", ("2s", "\x00" * 2)), ]) def construct(self, tracks): #pylint: disable-msg=W0221 @@ -351,8 +354,7 @@ class PlaylistHeader(Record): chunks += [construction] self["number_of_playlists"] = playlistcount - self["number_of_master_lists"] = 0 - self["total_length"] = 0x44 + (self["number_of_playlists"] * 4) + self["total_length"] = 0x14 + (self["number_of_playlists"] * 4) # Start the header output = Record.construct(self) @@ -379,9 +381,12 @@ class Playlist(Record): ]) def set_master(self, tracks): - self["dbid"] = hashlib.md5("masterlist").digest()[:8] #pylint: disable-msg=E1101 + # 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.voiceover: + self["dbid"] = hashlib.md5("masterlist").digest()[:8] #pylint: disable-msg=E1101 + self.text_to_speech("All songs", self["dbid"], True) self["listtype"] = 1 - self.text_to_speech("All songs", self["dbid"], True) self.listtracks = tracks def populate_m3u(self, data): @@ -562,10 +567,10 @@ def handle_interrupt(signal, frame): if __name__ == '__main__': signal.signal(signal.SIGINT, handle_interrupt) parser = argparse.ArgumentParser() - parser.add_argument('--disable-voiceover', action='store_true', help='Disable Voiceover Feature') - parser.add_argument('--rename-unicode', action='store_true', help='Rename Files Causing Unicode Errors, will do minimal required renaming') + parser.add_argument('--disable-voiceover', action='store_true', help='Disable voiceover feature') + parser.add_argument('--rename-unicode', action='store_true', help='Rename files causing unicode errors, will do minimal required renaming') parser.add_argument('--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('path') + parser.add_argument('path', help='Path to the IPod\'s root directory') result = parser.parse_args() checkPathValidity(result.path)