Skip to content

Commit

Permalink
Merge branch 'NicoHood-NicoHood3'
Browse files Browse the repository at this point in the history
  • Loading branch information
nims11 committed Feb 7, 2016
2 parents e02ef0f + cb5b72d commit 7d628df
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 39 deletions.
99 changes: 92 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
```
9 changes: 5 additions & 4 deletions docs/iTunesSD3gen.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ Here's the general layout of an iTunesSD file:<br>
</td>
<td>4<br>
</td>
<td>?<br>
<td>Version number?<br>
</td>
<td><span style="font-family: 'Courier New',Courier,monospace;">0x03000002</span><br>
<td><span style="font-family: 'Courier New',Courier,monospace;">0x02000003<br>
Old values:<br>0x02010001<br>Gen 2:<br>0x010600<br>0x010800<br></span><br>
</td>
<td><span style="font-family: 'Courier New',Courier,monospace;">03 00 00 02</span><br>
</td>
Expand Down Expand Up @@ -115,7 +116,7 @@ Here's the general layout of an iTunesSD file:<br>
</td>
<td>1<br>
</td>
<td><br>
<td>Only applies for tracks, not for playlists.<br>
</td>
<td><span style="font-family: 'Courier New',Courier,monospace;">1</span><br>
</td>
Expand Down Expand Up @@ -364,7 +365,7 @@ Here's the general layout of an iTunesSD file:<br>
</td>
<td>4<br>
</td>
<td><br>
<td>Rythmbox IPod plugin sets this value always 0.<br>
</td>
<td><span style="font-family: 'Courier New',Courier,monospace;">112169</span><br>
</td>
Expand Down
82 changes: 54 additions & 28 deletions shuffle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -92,6 +111,7 @@ 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 rhvoice(out_wav_path, unicodetext):
Expand All @@ -107,6 +127,7 @@ def rhvoice(out_wav_path, unicodetext):
subprocess.call(["sox", tmp_file.name, out_wav_path, "norm"])

os.remove(tmp_file.name)
return True


class Record(object):
Expand Down Expand Up @@ -141,7 +162,8 @@ def text_to_speech(self, text, dbid, playlist = False):
# 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:
Expand Down Expand Up @@ -189,7 +211,7 @@ def __init__(self, parent):
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)),
Expand Down Expand Up @@ -287,6 +309,7 @@ def populate(self, filename):
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]
Expand Down Expand Up @@ -320,16 +343,10 @@ def __init__(self, parent):
("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
Expand All @@ -349,10 +366,11 @@ def construct(self, tracks): #pylint: disable-msg=W0221
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)
Expand All @@ -379,9 +397,12 @@ def __init__(self, parent):
])

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):
Expand Down Expand Up @@ -443,11 +464,15 @@ def construct(self, tracks): #pylint: disable-msg=W0221

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
Expand Down Expand Up @@ -562,19 +587,20 @@ 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)

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()
Expand Down

0 comments on commit 7d628df

Please sign in to comment.