forked from upstream/IPod-Shuffle-4g
Merge branch 'NicoHood-NicoHood3'
This commit is contained in:
commit
7d628df3e8
3 changed files with 151 additions and 39 deletions
99
README.md
99
README.md
|
|
@ -26,11 +26,6 @@ optional arguments:
|
||||||
is very loud even on minimal player volume
|
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
|
#### Dependencies
|
||||||
|
|
||||||
This script requires:
|
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)
|
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
|
* Last.fm Scrobbler
|
||||||
* Qt frontend
|
* Qt frontend
|
||||||
|
|
||||||
##EXTRA READING
|
## EXTRA READING
|
||||||
* [shuffle3db specification](docs/iTunesSD3gen.md)
|
* [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/)
|
* [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.
|
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).
|
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.
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,10 @@ Here's the general layout of an iTunesSD file:<br>
|
||||||
</td>
|
</td>
|
||||||
<td>4<br>
|
<td>4<br>
|
||||||
</td>
|
</td>
|
||||||
<td>?<br>
|
<td>Version number?<br>
|
||||||
</td>
|
</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>
|
||||||
<td><span style="font-family: 'Courier New',Courier,monospace;">03 00 00 02</span><br>
|
<td><span style="font-family: 'Courier New',Courier,monospace;">03 00 00 02</span><br>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -115,7 +116,7 @@ Here's the general layout of an iTunesSD file:<br>
|
||||||
</td>
|
</td>
|
||||||
<td>1<br>
|
<td>1<br>
|
||||||
</td>
|
</td>
|
||||||
<td><br>
|
<td>Only applies for tracks, not for playlists.<br>
|
||||||
</td>
|
</td>
|
||||||
<td><span style="font-family: 'Courier New',Courier,monospace;">1</span><br>
|
<td><span style="font-family: 'Courier New',Courier,monospace;">1</span><br>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -364,7 +365,7 @@ Here's the general layout of an iTunesSD file:<br>
|
||||||
</td>
|
</td>
|
||||||
<td>4<br>
|
<td>4<br>
|
||||||
</td>
|
</td>
|
||||||
<td><br>
|
<td>Rythmbox IPod plugin sets this value always 0.<br>
|
||||||
</td>
|
</td>
|
||||||
<td><span style="font-family: 'Courier New',Courier,monospace;">112169</span><br>
|
<td><span style="font-family: 'Courier New',Courier,monospace;">112169</span><br>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
80
shuffle.py
80
shuffle.py
|
|
@ -61,23 +61,42 @@ class Text2Speech(object):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_support():
|
def check_support():
|
||||||
|
voiceoverAvailable = False
|
||||||
|
|
||||||
|
# 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 "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"):
|
if not exec_exists_in_path("RHVoice"):
|
||||||
Text2Speech.valid_tts['RHVoice'] = False
|
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
|
@staticmethod
|
||||||
def text2speech(out_wav_path, text):
|
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
|
# ensure we deal with unicode later
|
||||||
if not isinstance(text, unicode):
|
if not isinstance(text, unicode):
|
||||||
text = unicode(text, 'utf-8')
|
text = unicode(text, 'utf-8')
|
||||||
lang = Text2Speech.guess_lang(text)
|
lang = Text2Speech.guess_lang(text)
|
||||||
if lang == "ru-RU":
|
if lang == "ru-RU":
|
||||||
Text2Speech.rhvoice(out_wav_path, text)
|
return Text2Speech.rhvoice(out_wav_path, text)
|
||||||
else:
|
else:
|
||||||
Text2Speech.pico2wave(out_wav_path, text)
|
return Text2Speech.pico2wave(out_wav_path, text)
|
||||||
|
|
||||||
# guess-language seems like an overkill for now
|
# guess-language seems like an overkill for now
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -92,6 +111,7 @@ class Text2Speech(object):
|
||||||
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
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def rhvoice(out_wav_path, unicodetext):
|
def rhvoice(out_wav_path, unicodetext):
|
||||||
|
|
@ -107,6 +127,7 @@ class Text2Speech(object):
|
||||||
subprocess.call(["sox", tmp_file.name, out_wav_path, "norm"])
|
subprocess.call(["sox", tmp_file.name, out_wav_path, "norm"])
|
||||||
|
|
||||||
os.remove(tmp_file.name)
|
os.remove(tmp_file.name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class Record(object):
|
class Record(object):
|
||||||
|
|
@ -141,7 +162,8 @@ class Record(object):
|
||||||
# Create the voiceover wav file
|
# Create the voiceover wav file
|
||||||
fn = "".join(["{0:02X}".format(ord(x)) for x in reversed(dbid)])
|
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")
|
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):
|
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:
|
||||||
|
|
@ -189,7 +211,7 @@ class TunesSD(Record):
|
||||||
self.play_header = PlaylistHeader(self)
|
self.play_header = PlaylistHeader(self)
|
||||||
self._struct = collections.OrderedDict([
|
self._struct = collections.OrderedDict([
|
||||||
("header_id", ("4s", "shdb")),
|
("header_id", ("4s", "shdb")),
|
||||||
("unknown1", ("I", 0x02010001)),
|
("unknown1", ("I", 0x02000003)),
|
||||||
("total_length", ("I", 64)),
|
("total_length", ("I", 64)),
|
||||||
("total_number_of_tracks", ("I", 0)),
|
("total_number_of_tracks", ("I", 0)),
|
||||||
("total_number_of_playlists", ("I", 0)),
|
("total_number_of_playlists", ("I", 0)),
|
||||||
|
|
@ -287,6 +309,7 @@ class Track(Record):
|
||||||
text = os.path.splitext(os.path.basename(filename))[0]
|
text = os.path.splitext(os.path.basename(filename))[0]
|
||||||
audio = mutagen.File(filename, easy = True)
|
audio = mutagen.File(filename, easy = True)
|
||||||
if audio:
|
if audio:
|
||||||
|
# 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)
|
||||||
|
|
||||||
artist = audio.get("artist", [u"Unknown"])[0]
|
artist = audio.get("artist", [u"Unknown"])[0]
|
||||||
|
|
@ -320,16 +343,10 @@ class PlaylistHeader(Record):
|
||||||
("header_id", ("4s", "shph")),
|
("header_id", ("4s", "shph")),
|
||||||
("total_length", ("I", 0)),
|
("total_length", ("I", 0)),
|
||||||
("number_of_playlists", ("I", 0)),
|
("number_of_playlists", ("I", 0)),
|
||||||
("number_of_podcast_lists", ("I", 0xffffffff)),
|
("number_of_non_podcast_lists", ("2s", "\xFF\xFF")),
|
||||||
("number_of_master_lists", ("I", 0)),
|
("number_of_master_lists", ("2s", "\x01\x00")),
|
||||||
("number_of_audiobook_lists", ("I", 0xffffffff)),
|
("number_of_non_audiobook_lists", ("2s", "\xFF\xFF")),
|
||||||
("unknown1", ("I", 0)),
|
("unknown2", ("2s", "\x00" * 2)),
|
||||||
("unknown2", ("I", 0xffffffff)),
|
|
||||||
("unknown3", ("I", 0)),
|
|
||||||
("unknown4", ("I", 0xffffffff)),
|
|
||||||
("unknown5", ("I", 0)),
|
|
||||||
("unknown6", ("I", 0xffffffff)),
|
|
||||||
("unknown7", ("20s", "\x00" * 20)),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
def construct(self, tracks): #pylint: disable-msg=W0221
|
def construct(self, tracks): #pylint: disable-msg=W0221
|
||||||
|
|
@ -349,10 +366,11 @@ class PlaylistHeader(Record):
|
||||||
if playlist["number_of_songs"] > 0:
|
if playlist["number_of_songs"] > 0:
|
||||||
playlistcount += 1
|
playlistcount += 1
|
||||||
chunks += [construction]
|
chunks += [construction]
|
||||||
|
else:
|
||||||
|
print "Error: Playlist does not contain a single track. Skipping playlist."
|
||||||
|
|
||||||
self["number_of_playlists"] = playlistcount
|
self["number_of_playlists"] = playlistcount
|
||||||
self["number_of_master_lists"] = 0
|
self["total_length"] = 0x14 + (self["number_of_playlists"] * 4)
|
||||||
self["total_length"] = 0x44 + (self["number_of_playlists"] * 4)
|
|
||||||
# Start the header
|
# Start the header
|
||||||
|
|
||||||
output = Record.construct(self)
|
output = Record.construct(self)
|
||||||
|
|
@ -379,9 +397,12 @@ class Playlist(Record):
|
||||||
])
|
])
|
||||||
|
|
||||||
def set_master(self, tracks):
|
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.voiceover and Text2Speech.valid_tts['pico2wave']:
|
||||||
self["dbid"] = hashlib.md5("masterlist").digest()[:8] #pylint: disable-msg=E1101
|
self["dbid"] = hashlib.md5("masterlist").digest()[:8] #pylint: disable-msg=E1101
|
||||||
self["listtype"] = 1
|
|
||||||
self.text_to_speech("All songs", self["dbid"], True)
|
self.text_to_speech("All songs", self["dbid"], True)
|
||||||
|
self["listtype"] = 1
|
||||||
self.listtracks = tracks
|
self.listtracks = tracks
|
||||||
|
|
||||||
def populate_m3u(self, data):
|
def populate_m3u(self, data):
|
||||||
|
|
@ -443,11 +464,15 @@ class Playlist(Record):
|
||||||
|
|
||||||
chunks = ""
|
chunks = ""
|
||||||
for i in self.listtracks:
|
for i in self.listtracks:
|
||||||
|
path = self.ipod_to_path(i)
|
||||||
|
position = -1
|
||||||
try:
|
try:
|
||||||
position = tracks.index(self.ipod_to_path(i))
|
position = tracks.index(path)
|
||||||
except:
|
except:
|
||||||
print tracks
|
# Print an error if no track was found.
|
||||||
raise
|
# 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:
|
if position > -1:
|
||||||
chunks += struct.pack("I", position)
|
chunks += struct.pack("I", position)
|
||||||
self["number_of_songs"] += 1
|
self["number_of_songs"] += 1
|
||||||
|
|
@ -562,10 +587,10 @@ def handle_interrupt(signal, frame):
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
signal.signal(signal.SIGINT, handle_interrupt)
|
signal.signal(signal.SIGINT, handle_interrupt)
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('--disable-voiceover', action='store_true', help='Disable Voiceover Feature')
|
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('--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('--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()
|
result = parser.parse_args()
|
||||||
|
|
||||||
checkPathValidity(result.path)
|
checkPathValidity(result.path)
|
||||||
|
|
@ -573,8 +598,9 @@ if __name__ == '__main__':
|
||||||
if result.rename_unicode:
|
if result.rename_unicode:
|
||||||
check_unicode(result.path)
|
check_unicode(result.path)
|
||||||
|
|
||||||
if not result.disable_voiceover:
|
if not result.disable_voiceover and not Text2Speech.check_support():
|
||||||
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 = Shuffler(result.path, voiceover=not result.disable_voiceover, rename=result.rename_unicode, trackgain=result.track_gain)
|
||||||
shuffle.initialize()
|
shuffle.initialize()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue