diff --git a/README.md b/README.md
index 3b9d1ce..8e1d133 100644
--- a/README.md
+++ b/README.md
@@ -26,11 +26,6 @@ optional arguments:
is very loud even on minimal player volume
```
-#### Additions to the original
-* Option to disable voiceover
-* Initialize the IPod Directory tree
-* Using the --rename-unicode flag, filenames with strange characters and different language are renamed which avoids the script to crash with a Unicode Error
-
#### Dependencies
This script requires:
@@ -59,13 +54,103 @@ ACCEPT_KEYWORDS="~amd64" emerge -av app-accessibility/svox app-accessibility/rhv
```
References to the overlays above: [ikelos](http://git.overlays.gentoo.org/gitweb/?p=dev/ikelos.git;a=summary), [ahippo-rhvoice-overlay](https://github.com/ahippo/rhvoice-gentoo-overlay)
-##TODO
+##Tips and Tricks
+
+#### Disable trash for IPod
+To avoid that linux moves deleted files into trash you can create an empty file `.Trash-1000`.
+This forces linux to delete the files permanently instead of moving them to the trash.
+Of course you can also use `shift + delete` to permanently delete files without this trick.
+
+#### Use Rhythmbox to manage your music and playlists
+As described [in the blog post](https://nims11.wordpress.com/2013/10/12/ipod-shuffle-4g-under-linux/)
+you can use Rythmbox to sync your personal music library to your IPod
+but still make use of the additional features this script provides (such as voiceover).
+
+Simply place a file called `.is_audio_player` into the root directory of your IPod and add the following content:
+```
+name="Name's IPOD"
+audio_folders=iPod_Control/Music/
+```
+
+Now disable the IPod plugin of Rhythmbox and enable the MTP plugin instead.
+You can use Rythmbox now to generate playlists and sync them to your IPod.
+The script will recognize the .pls playlists and generate a proper iTunesSD file.
+
+##### Known Rhythmbox syncing issues
+* Creating playlists with names like `K.I.Z.` will fail, because the FAT Filesystem does not support a dot `.` at the end of a directory/file.
+* Sometimes bad ID3 tags can also cause corrupted playlists.
+
+In all cases you can try to update Rythmbox to the latest version, sync again or fix the wrong filenames yourself.
+
+#### Carry the script with your IPod
+If you want to use this script on different computers it makes sense
+to simply copy the script into the IPod's root directory.
+
+## TODO
* Last.fm Scrobbler
* Qt frontend
-##EXTRA READING
+## EXTRA READING
* [shuffle3db specification](docs/iTunesSD3gen.md)
* [Using shuffle.py and Rhythmbox for easy syncing of playlists and songs](http://nims11.wordpress.com/2013/10/12/ipod-shuffle-4g-under-linux/)
+* [gtkpod](http://www.gtkpod.org/wiki/Home)
+* [German Ubuntu IPod tutorial](https://wiki.ubuntuusers.de/iPod/)
+* [IPod management apps](https://wiki.archlinux.org/index.php/IPod#iPod_management_apps)
The original shuffle3db website went offline. This repository contains a copy of the information inside the `docs` folder.
Original data can be found via [wayback machine](https://web.archive.org/web/20131016014401/http://shuffle3db.wikispaces.com/iTunesSD3gen).
+
+
+# Version History
+
+```
+1.2 Release (04.02.2016)
+* Additional fixes from NicoHood
+* Fixed "All Songs" and "Playlist N" sounds when voiceover is disabled #17
+* Better handle broken playlist paths #16
+* Skip existing voiceover files with the same name (e.g. "Track 1.mp3")
+* Only use voiceover if dependencies are installed
+* Added Path help entry
+* Made help message lower case
+* Improved Readme
+* Improved docs
+* Added MIT License
+* Added this changelog
+
+1.1 Release (11.10.2013 - 23.01.2016)
+* Fixes from nims11 fork
+* Option to disable voiceover
+* Initialize the IPod Directory tree
+* Using the --rename-unicode flag
+ filenames with strange characters and different language are renamed
+ which avoids the script to crash with a Unicode Error
+* Other small fixes
+
+1.0 Release (15.08.2012 - 17.10.2012)
+* Original release by ikelos
+```
+
+# License and Copyright
+
+```
+Copyright (c) 2012-2016 ikelos, nims11, NicoHood
+See the readme for credit to other people.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
diff --git a/docs/iTunesSD3gen.md b/docs/iTunesSD3gen.md
index 8580603..b959fc8 100644
--- a/docs/iTunesSD3gen.md
+++ b/docs/iTunesSD3gen.md
@@ -41,9 +41,10 @@ Here's the general layout of an iTunesSD file:
4
|
- ?
+ | Version number?
|
- 0x03000002
+ | 0x02000003
+ Old values: 0x02010001 Gen 2: 0x010600 0x010800
|
03 00 00 02
|
@@ -115,7 +116,7 @@ Here's the general layout of an iTunesSD file:
1
|
-
+ | Only applies for tracks, not for playlists.
|
1
|
@@ -364,7 +365,7 @@ Here's the general layout of an iTunesSD file:
4
|
-
+ | Rythmbox IPod plugin sets this value always 0.
|
112169
|
diff --git a/shuffle.py b/shuffle.py
index 20e73f4..d1cc202 100755
--- a/shuffle.py
+++ b/shuffle.py
@@ -61,23 +61,42 @@ class Text2Speech(object):
@staticmethod
def check_support():
+ voiceoverAvailable = False
+
+ # Check for pico2wave voiceover
if not exec_exists_in_path("pico2wave"):
Text2Speech.valid_tts['pico2wave'] = False
- print "Error executing pico2wave, voicever won't be generated using it"
+ print "Error executing pico2wave, 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 "Error executing RHVoice, voicever won't be generated using it"
+ print "Warning: Error executing RHVoice, 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):
+ 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 +111,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 +127,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 +162,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 +211,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)),
@@ -287,6 +309,7 @@ class Track(Record):
text = os.path.splitext(os.path.basename(filename))[0]
audio = mutagen.File(filename, easy = True)
if audio:
+ # Note: Rythmbox IPod plugin sets this value always 0.
self["stop_at_pos_ms"] = int(audio.info.length * 1000)
artist = audio.get("artist", [u"Unknown"])[0]
@@ -320,16 +343,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", "\xFF\xFF")),
+ ("number_of_master_lists", ("2s", "\x01\x00")),
+ ("number_of_non_audiobook_lists", ("2s", "\xFF\xFF")),
+ ("unknown2", ("2s", "\x00" * 2)),
])
def construct(self, tracks): #pylint: disable-msg=W0221
@@ -349,10 +366,11 @@ class PlaylistHeader(Record):
if playlist["number_of_songs"] > 0:
playlistcount += 1
chunks += [construction]
+ else:
+ print "Error: Playlist does not contain a single track. Skipping playlist."
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 +397,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 and Text2Speech.valid_tts['pico2wave']:
+ 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):
@@ -443,11 +464,15 @@ class Playlist(Record):
chunks = ""
for i in self.listtracks:
+ path = self.ipod_to_path(i)
+ position = -1
try:
- position = tracks.index(self.ipod_to_path(i))
+ position = tracks.index(path)
except:
- print tracks
- raise
+ # 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
@@ -562,10 +587,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)
@@ -573,8 +598,9 @@ if __name__ == '__main__':
if result.rename_unicode:
check_unicode(result.path)
- if not result.disable_voiceover:
- Text2Speech.check_support()
+ if not result.disable_voiceover and not Text2Speech.check_support():
+ print "Error: Did not find any voiceover program. Voiceover disabled."
+ result.disable_voiceover = True
shuffle = Shuffler(result.path, voiceover=not result.disable_voiceover, rename=result.rename_unicode, trackgain=result.track_gain)
shuffle.initialize()