Skip to content

Commit

Permalink
Merge branch 'NicoHood-version14'
Browse files Browse the repository at this point in the history
  • Loading branch information
nims11 committed Aug 27, 2016
2 parents 48909db + 78ecedd commit f8838a9
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 48 deletions.
54 changes: 34 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
# IPod Shuffle 4g Scripts
# IPod Shuffle 4g Script

##shuffle.py
## ipod-shuffle-4g.py

Python script for building the Track and Playlist database for the newer gen IPod Shuffle.
Forked from the [shuffle-db-ng project](https://code.google.com/p/shuffle-db-ng/)

Just put your audio files into the mass storage of your IPod and shuffle.py will do the rest
```bash
$ python shuffle.py -h
usage: shuffle.py [-h] [--voiceover] [--playlist-voiceover] [--rename-unicode]
[--track-gain TRACK_GAIN]
[--auto-dir-playlists [AUTO_DIR_PLAYLISTS]]
[--auto-id3-playlists [ID3_TEMPLATE]]
path
Just put your audio files into the mass storage of your IPod and shuffle.py will do the rest.
```
$ ./ipod-shuffle-4g.py --help
usage: ipod-shuffle-4g.py [-h] [-t] [-p] [-u] [-g TRACK_GAIN]
[-d [AUTO_DIR_PLAYLISTS]] [-i [ID3_TEMPLATE]] [-v]
path
Python script for building the Track and Playlist database for the newer gen
IPod Shuffle. Version 1.3
IPod Shuffle. Version 1.4
positional arguments:
path Path to the IPod\'s root directory
path Path to the IPod's root directory
optional arguments:
-h, --help show this help message and exit
--voiceover Enable track voiceover feature
--playlist-voiceover Enable playlist voiceover feature
--rename-unicode Rename files causing unicode errors, will do minimal
-t, --track-voiceover
Enable track voiceover feature
-p, --playlist-voiceover
Enable playlist voiceover feature
-u, --rename-unicode Rename files causing unicode errors, will do minimal
required renaming
--track-gain TRACK_GAIN
-g TRACK_GAIN, --track-gain TRACK_GAIN
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
--auto-dir-playlists [AUTO_DIR_PLAYLISTS]
-d [AUTO_DIR_PLAYLISTS], --auto-dir-playlists [AUTO_DIR_PLAYLISTS]
Generate automatic playlists for each folder
recursively inside "IPod_Control/Music/". You can
optionally limit the depth: 0=root, 1=artist, 2=album,
n=subfoldername, default=-1 (No Limit).
--auto-id3-playlists [ID3_TEMPLATE]
-i [ID3_TEMPLATE], --auto-id3-playlists [ID3_TEMPLATE]
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
Expand All @@ -44,17 +44,18 @@ optional arguments:
group tracks under one playlist. Similarly '{genre}'
will group tracks based on their genre tag. Default
template used is '{artist}'
-v, --verbose Show verbose output of database generation.
```

#### Dependencies

This script requires:
* [Python 2.7](http://www.python.org/download/releases/2.7/)
* [Mutagen](https://code.google.com/p/mutagen/)

Optional Voiceover support
* [PicoSpeaker](http://picospeaker.tk/readme.php) or espeak -- (English files)
* [eSpeak](http://espeak.sourceforge.net/)
* [PicoSpeaker](http://picospeaker.tk/readme.php)
* [RHVoice (master branch, 3e31edced402a08771d2c48c73213982cbe9333e)](https://github.com/Olga-Yakovleva/RHVoice) -- (Russian files only)
* [SoX](http://sox.sourceforge.net) -- (Russian files)

Expand Down Expand Up @@ -84,6 +85,7 @@ References to the overlays above: [ikelos](http://git.overlays.gentoo.org/gitweb
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.
The file can be found in the [extras](extras) folder.

#### Compress/Convert your music files
([#11](https://github.com/nims11/IPod-Shuffle-4g/issues/11)) Shuffle is short on storage, and you might want to squeeze in more of your collection by sacrificing some bitrate off your files. In rarer cases, you might also possess music in formats not supported by your ipod. Although `ffmpeg` can handle almost all your needs, if you are looking for a friendly alternative, try [Soundconverter](http://soundconverter.org/).
Expand All @@ -98,6 +100,7 @@ Simply place a file called `.is_audio_player` into the root directory of your IP
name="Name's IPOD"
audio_folders=iPod_Control/Music/
```
The file can be found in the [extras](extras) folder.

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.
Expand Down Expand Up @@ -131,6 +134,17 @@ Original data can be found via [wayback machine](https://web.archive.org/web/201
# Version History

```
1.4 Release (27.08.2016)
* Catch "no space left" error #30
* Renamed --voiceover to --track-voiceover
* Added optional --verbose output
* Renamed script from shuffle.py to ipod-shuffle-4g.py
* Added files to `extras` folder
* Ignore hidden filenames
* Do not force playlist voiceover with auto playlists
* Added shortcut parameters (-p, -t, -d, etc.)
* Fix UnicodeEncodeError for non-ascii playlist names (#35)
1.3 Release (08.06.2016)
* Directory based auto playlist building (--auto-dir-playlists) (#13)
* ID3 tags based auto playlist building (--auto-id3-playlists)
Expand Down
Empty file added extras/.Trash-1000
Empty file.
2 changes: 2 additions & 0 deletions extras/.is_audio_player
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
name="iPod Shuffle"
audio_folders=iPod_Control/Music/
88 changes: 60 additions & 28 deletions shuffle.py → ipod-shuffle-4g.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ 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
verboseprint("Using existing", out_wav_path)
return True

# ensure we deal with unicode later
Expand Down Expand Up @@ -190,7 +190,7 @@ def __init__(self, parent):
self.parent = parent
self._struct = collections.OrderedDict([])
self._fields = {}
self.voiceover = parent.voiceover
self.track_voiceover = parent.track_voiceover
self.playlist_voiceover = parent.playlist_voiceover
self.rename = parent.rename
self.trackgain = parent.trackgain
Expand All @@ -213,7 +213,7 @@ def construct(self):
return output

def text_to_speech(self, text, dbid, playlist = False):
if self.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
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")
Expand Down Expand Up @@ -272,7 +272,7 @@ def __init__(self, parent):
("total_number_of_playlists", ("I", 0)),
("unknown2", ("Q", 0)),
("max_volume", ("B", 0)),
("voiceover_enabled", ("B", int(self.voiceover))),
("voiceover_enabled", ("B", int(self.track_voiceover))),
("unknown3", ("H", 0)),
("total_tracks_without_podcasts", ("I", 0)),
("track_header_offset", ("I", 64)),
Expand Down Expand Up @@ -317,7 +317,7 @@ def construct(self):
track_chunk = ""
for i in self.tracks:
track = Track(self)
print "[*] Adding track", i
verboseprint("[*] Adding track", i)
track.populate(i)
output += struct.pack("I", self.base_offset + self["total_length"] + len(track_chunk))
track_chunk += track.construct()
Expand Down Expand Up @@ -411,15 +411,15 @@ def __init__(self, parent):
def construct(self, tracks): #pylint: disable-msg=W0221
# Build the master list
masterlist = Playlist(self)
print "[+] Adding master playlist"
verboseprint("[+] Adding master playlist")
masterlist.set_master(tracks)
chunks = [masterlist.construct(tracks)]

# Build all the remaining playlists
playlistcount = 1
for i in self.lists:
playlist = Playlist(self)
print "[+] Adding playlist", (i[0] if type(i) == type(()) else i)
verboseprint("[+] Adding playlist", (i[0] if type(i) == type(()) else i))
playlist.populate(i)
construction = playlist.construct(tracks)
if playlist["number_of_songs"] > 0:
Expand Down Expand Up @@ -545,6 +545,8 @@ def populate(self, obj):
text = os.path.splitext(os.path.basename(filename))[0]

# Handle the VoiceOverData
if isinstance(text, unicode):
text = text.encode('utf-8', 'ignore')
self["dbid"] = hashlib.md5(text).digest()[:8] #pylint: disable-msg=E1101
self.text_to_speech(text, self["dbid"], True)

Expand Down Expand Up @@ -572,14 +574,14 @@ def construct(self, tracks): #pylint: disable-msg=W0221
return output + chunks

class Shuffler(object):
def __init__(self, path, 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.tracks = []
self.albums = []
self.artists = []
self.lists = []
self.tunessd = None
self.voiceover = voiceover
self.track_voiceover = track_voiceover
self.playlist_voiceover = playlist_voiceover
self.rename = rename
self.trackgain = trackgain
Expand Down Expand Up @@ -608,11 +610,13 @@ def populate(self):
# Ignore the speakable directory and any hidden directories
if not is_path_prefix("iPod_Control/Speakable", relpath) and "/." not in dirpath:
for filename in sorted(filenames, key = lambda x: x.lower()):
fullPath = os.path.abspath(os.path.join(dirpath, filename))
if os.path.splitext(filename)[1].lower() in (".mp3", ".m4a", ".m4b", ".m4p", ".aa", ".wav"):
self.tracks.append(fullPath)
if os.path.splitext(filename)[1].lower() in (".pls", ".m3u"):
self.lists.append(fullPath)
# Ignore hidden files
if not filename.startswith("."):
fullPath = os.path.abspath(os.path.join(dirpath, filename))
if os.path.splitext(filename)[1].lower() in (".mp3", ".m4a", ".m4b", ".m4p", ".aa", ".wav"):
self.tracks.append(fullPath)
if os.path.splitext(filename)[1].lower() in (".pls", ".m3u"):
self.lists.append(fullPath)

# Create automatic playlists in music directory.
# Ignore the (music) root and any hidden directories.
Expand All @@ -628,7 +632,17 @@ def populate(self):

def write_database(self):
with open(os.path.join(self.path, "iPod_Control", "iTunes", "iTunesSD"), "wb") as f:
f.write(self.tunessd.construct())
try:
f.write(self.tunessd.construct())
except IOError as e:
print "I/O error({0}): {1}".format(e.errno, e.strerror)
print "Error: Writing iPod database failed."
sys.exit(1)
print "Database written successfully:"
print "Tracks", len(self.tracks)
print "Albums", len(self.albums)
print "Artists", len(self.artists)
print "Playlists", len(self.lists)

#
# Read all files from the directory
Expand Down Expand Up @@ -687,53 +701,71 @@ def handle_interrupt(signal, frame):

parser = argparse.ArgumentParser(description=
'Python script for building the Track and Playlist database '
'for the newer gen IPod Shuffle. Version 1.3')
'for the newer gen IPod Shuffle. Version 1.4')

parser.add_argument('--voiceover', action='store_true',
parser.add_argument('-t', '--track-voiceover', action='store_true',
help='Enable track voiceover feature')

parser.add_argument('--playlist-voiceover', action='store_true',
parser.add_argument('-p', '--playlist-voiceover', action='store_true',
help='Enable playlist voiceover feature')

parser.add_argument('--rename-unicode', action='store_true',
parser.add_argument('-u', '--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',
parser.add_argument('-g', '--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('--auto-dir-playlists', type=int, default=None, const=-1, nargs='?',
parser.add_argument('-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: '
'0=root, 1=artist, 2=album, n=subfoldername, default=-1 (No Limit).')

parser.add_argument('--auto-id3-playlists', type=str, default=None, metavar='ID3_TEMPLATE', const='{artist}', nargs='?',
parser.add_argument('-i', '--auto-id3-playlists', type=str, default=None, metavar='ID3_TEMPLATE', 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',
help='Show verbose output of database generation.')

parser.add_argument('path', help='Path to the IPod\'s root directory')

result = parser.parse_args()

# Enable verbose printing if desired
# Smaller version for python3 available.
# See https://stackoverflow.com/questions/5980042/how-to-implement-the-verbose-or-v-option-into-a-script
if result.verbose:
def verboseprint(*args):
# Print each argument separately so caller doesn't need to
# stuff everything to be printed into a single string
for arg in args:
print arg,
print
else:
verboseprint = lambda *a: None # do-nothing function

checkPathValidity(result.path)

if result.rename_unicode:
check_unicode(result.path)

if result.auto_id3_playlists != None or result.auto_dir_playlists != None:
result.playlist_voiceover = True

if (result.voiceover or result.playlist_voiceover) and not Text2Speech.check_support():
verboseprint("Playlist voiceover requested:", result.playlist_voiceover)
verboseprint("Track voiceover requested:", result.track_voiceover)
if (result.track_voiceover or result.playlist_voiceover):
if not Text2Speech.check_support():
print "Error: Did not find any voiceover program. Voiceover disabled."
result.voiceover = False
result.track_voiceover = False
result.playlist_voiceover = False
else:
verboseprint("Voiceover available.")

shuffle = Shuffler(result.path, voiceover=result.voiceover, playlist_voiceover=result.playlist_voiceover, rename=result.rename_unicode, trackgain=result.track_gain, auto_dir_playlists=result.auto_dir_playlists, auto_id3_playlists=result.auto_id3_playlists)
shuffle = Shuffler(result.path, track_voiceover=result.track_voiceover, playlist_voiceover=result.playlist_voiceover, rename=result.rename_unicode, trackgain=result.track_gain, auto_dir_playlists=result.auto_dir_playlists, auto_id3_playlists=result.auto_id3_playlists)
shuffle.initialize()
shuffle.populate()
shuffle.write_database()

0 comments on commit f8838a9

Please sign in to comment.