From c0c676c05d09be09807eceea5fbe5daab398232b Mon Sep 17 00:00:00 2001 From: NicoHood Date: Sat, 27 Aug 2016 13:16:45 +0200 Subject: [PATCH 01/12] Catch 'no space left' error #30 --- shuffle.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/shuffle.py b/shuffle.py index 29ea499..6fefb6b 100755 --- a/shuffle.py +++ b/shuffle.py @@ -628,7 +628,13 @@ 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 sucessful." # # Read all files from the directory From 7dbc15b91bdc15eec3a1e7d47a5792cb6fc33aed Mon Sep 17 00:00:00 2001 From: NicoHood Date: Sat, 27 Aug 2016 13:18:51 +0200 Subject: [PATCH 02/12] Version bump to 1.4 --- shuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shuffle.py b/shuffle.py index 6fefb6b..9a90b72 100755 --- a/shuffle.py +++ b/shuffle.py @@ -693,7 +693,7 @@ 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', help='Enable track voiceover feature') From e1894778701e23941c0f47766cd7ff9ebd8152d1 Mon Sep 17 00:00:00 2001 From: NicoHood Date: Sat, 27 Aug 2016 13:19:21 +0200 Subject: [PATCH 03/12] Renamed --voiceover to --track--voiceover --- shuffle.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/shuffle.py b/shuffle.py index 9a90b72..8d91fe5 100755 --- a/shuffle.py +++ b/shuffle.py @@ -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 @@ -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") @@ -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)), @@ -572,14 +572,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 @@ -695,7 +695,7 @@ def handle_interrupt(signal, frame): 'Python script for building the Track and Playlist database ' 'for the newer gen IPod Shuffle. Version 1.4') - parser.add_argument('--voiceover', action='store_true', + parser.add_argument('--track-voiceover', action='store_true', help='Enable track voiceover feature') parser.add_argument('--playlist-voiceover', action='store_true', @@ -734,12 +734,12 @@ def handle_interrupt(signal, frame): 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(): + if (result.track_voiceover or result.playlist_voiceover) and 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 - 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() From d8eb3871bfbec4fe5a29692bad60225733412b28 Mon Sep 17 00:00:00 2001 From: NicoHood Date: Sat, 27 Aug 2016 14:18:15 +0200 Subject: [PATCH 04/12] Ignore hidden filenames --- shuffle.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/shuffle.py b/shuffle.py index 8d91fe5..a76c491 100755 --- a/shuffle.py +++ b/shuffle.py @@ -608,11 +608,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. From 1d7c3e0ab0b28ba97cf0960625f8721dc2298e16 Mon Sep 17 00:00:00 2001 From: NicoHood Date: Sat, 27 Aug 2016 14:18:31 +0200 Subject: [PATCH 05/12] typo --- shuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shuffle.py b/shuffle.py index a76c491..ff9cf62 100755 --- a/shuffle.py +++ b/shuffle.py @@ -636,7 +636,7 @@ def write_database(self): print "I/O error({0}): {1}".format(e.errno, e.strerror) print "Error: Writing iPod database failed." sys.exit(1) - print "Database written sucessful." + print "Database written sucessfully." # # Read all files from the directory From 957912e64d3653814025f65c2972dab4e7553a5f Mon Sep 17 00:00:00 2001 From: NicoHood Date: Sat, 27 Aug 2016 14:21:27 +0200 Subject: [PATCH 06/12] Added files in extras folder --- extras/.Trash-1000 | 0 extras/.is_audio_player | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 extras/.Trash-1000 create mode 100644 extras/.is_audio_player diff --git a/extras/.Trash-1000 b/extras/.Trash-1000 new file mode 100644 index 0000000..e69de29 diff --git a/extras/.is_audio_player b/extras/.is_audio_player new file mode 100644 index 0000000..e419f33 --- /dev/null +++ b/extras/.is_audio_player @@ -0,0 +1,2 @@ +name="iPod Shuffle" +audio_folders=iPod_Control/Music/ From 08684825b05cadc69c81577e55939b51eb9168f7 Mon Sep 17 00:00:00 2001 From: NicoHood Date: Sat, 27 Aug 2016 14:39:25 +0200 Subject: [PATCH 07/12] Added verbose debug output --- shuffle.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/shuffle.py b/shuffle.py index ff9cf62..4b1a1dd 100755 --- a/shuffle.py +++ b/shuffle.py @@ -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 @@ -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() @@ -411,7 +411,7 @@ 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)] @@ -419,7 +419,7 @@ def construct(self, tracks): #pylint: disable-msg=W0221 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: @@ -636,7 +636,11 @@ def write_database(self): print "I/O error({0}): {1}".format(e.errno, e.strerror) print "Error: Writing iPod database failed." sys.exit(1) - print "Database written sucessfully." + 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 @@ -724,10 +728,26 @@ def handle_interrupt(signal, frame): 'tracks under one playlist. Similarly \'{genre}\' will group tracks based ' 'on their genre tag. Default template used is \'{artist}\'') + parser.add_argument('--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: From 1b023af3fb9f2019e74059afa4925f12ef3a97e1 Mon Sep 17 00:00:00 2001 From: NicoHood Date: Sat, 27 Aug 2016 14:39:56 +0200 Subject: [PATCH 08/12] Do not force playlist voiceover with auto playlists --- shuffle.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/shuffle.py b/shuffle.py index 4b1a1dd..7233259 100755 --- a/shuffle.py +++ b/shuffle.py @@ -753,13 +753,15 @@ def verboseprint(*args): 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.track_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.track_voiceover = False result.playlist_voiceover = False + else: + verboseprint("Voiceover available.") 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() From 30c68e9b416cd09481de3656ad4ad616d9c6eaf4 Mon Sep 17 00:00:00 2001 From: NicoHood Date: Sat, 27 Aug 2016 14:41:06 +0200 Subject: [PATCH 09/12] Renamed script from shuffle.py to ipod-shuffle-4g.py --- shuffle.py => ipod-shuffle-4g.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename shuffle.py => ipod-shuffle-4g.py (100%) diff --git a/shuffle.py b/ipod-shuffle-4g.py similarity index 100% rename from shuffle.py rename to ipod-shuffle-4g.py From 938405eb29d5f604461d9762e4e04197c2cd0d8e Mon Sep 17 00:00:00 2001 From: NicoHood Date: Sat, 27 Aug 2016 14:49:15 +0200 Subject: [PATCH 10/12] Added shortcut parameters (-p, -t, -d, etc.) --- ipod-shuffle-4g.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ipod-shuffle-4g.py b/ipod-shuffle-4g.py index 7233259..0f0b773 100755 --- a/ipod-shuffle-4g.py +++ b/ipod-shuffle-4g.py @@ -701,26 +701,26 @@ def handle_interrupt(signal, frame): 'Python script for building the Track and Playlist database ' 'for the newer gen IPod Shuffle. Version 1.4') - parser.add_argument('--track-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. ' @@ -728,7 +728,7 @@ def handle_interrupt(signal, frame): 'tracks under one playlist. Similarly \'{genre}\' will group tracks based ' 'on their genre tag. Default template used is \'{artist}\'') - parser.add_argument('--verbose', action='store_true', + 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') From e5322ce9f874b46c5efb79b11e44c0c295a245f9 Mon Sep 17 00:00:00 2001 From: NicoHood Date: Sat, 27 Aug 2016 14:57:26 +0200 Subject: [PATCH 11/12] Updated readme --- README.md | 53 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 8386764..567feaa 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) @@ -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/). @@ -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. @@ -131,6 +134,16 @@ 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.) + 1.3 Release (08.06.2016) * Directory based auto playlist building (--auto-dir-playlists) (#13) * ID3 tags based auto playlist building (--auto-id3-playlists) From 78ecedd8d5333d11053ccfd0aac4fb490362b194 Mon Sep 17 00:00:00 2001 From: Nimesh Ghelani Date: Sun, 28 Aug 2016 03:47:57 +0530 Subject: [PATCH 12/12] utf encode playlist names, fixes #35 --- README.md | 1 + ipod-shuffle-4g.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/README.md b/README.md index 567feaa..8af7eee 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ Original data can be found via [wayback machine](https://web.archive.org/web/201 * 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) diff --git a/ipod-shuffle-4g.py b/ipod-shuffle-4g.py index 0f0b773..717a4d4 100755 --- a/ipod-shuffle-4g.py +++ b/ipod-shuffle-4g.py @@ -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)