From 64617c4d8ccf7a27d61491f384c480539b966ddd Mon Sep 17 00:00:00 2001 From: NicoHood Date: Tue, 5 Apr 2016 20:55:44 +0200 Subject: [PATCH 01/16] Added script description --- shuffle.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shuffle.py b/shuffle.py index 274f0e0..d2f94f3 100755 --- a/shuffle.py +++ b/shuffle.py @@ -606,7 +606,9 @@ def handle_interrupt(signal, frame): if __name__ == '__main__': signal.signal(signal.SIGINT, handle_interrupt) - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser(description= + 'Python script for building the Track and Playlist database ' + 'for the newer gen IPod Shuffle.') 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') From df97b876b83808ea8aa4ce2ea5bd78f6f556ca0e Mon Sep 17 00:00:00 2001 From: NicoHood Date: Tue, 5 Apr 2016 20:57:34 +0200 Subject: [PATCH 02/16] Made argument parser functions better readable in script --- shuffle.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/shuffle.py b/shuffle.py index d2f94f3..e02a879 100755 --- a/shuffle.py +++ b/shuffle.py @@ -609,9 +609,14 @@ def handle_interrupt(signal, frame): parser = argparse.ArgumentParser(description= 'Python script for building the Track and Playlist database ' 'for the newer gen IPod Shuffle.') - 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('--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', help='Path to the IPod\'s root directory') result = parser.parse_args() From bcc374df130f2830c0fc04138f7a0d4b17116675 Mon Sep 17 00:00:00 2001 From: NicoHood Date: Tue, 5 Apr 2016 20:58:35 +0200 Subject: [PATCH 03/16] Added Auto Playlists --- shuffle.py | 76 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 13 deletions(-) diff --git a/shuffle.py b/shuffle.py index e02a879..6d5c57b 100755 --- a/shuffle.py +++ b/shuffle.py @@ -450,6 +450,33 @@ def populate_pls(self, data): listtracks = [ x for (_, x) in sorted(sorttracks) ] return listtracks + def populate_directory(self, playlistpath, recursive = True): + # Add all tracks inside the folder and its subfolders recursively. + # Folders containing no music and only a single Album + # would generate duplicated playlists. That is intended and "wont fix". + # Empty folders (inside the music path) will generate an error -> "wont fix". + listtracks = [] + for (dirpath, dirnames, filenames) in os.walk(playlistpath): + dirnames.sort() + + for filename in sorted(filenames, key = lambda x: x.lower()): + # Only add valid music files to playlist + if os.path.splitext(filename)[1].lower() in (".mp3", ".m4a", ".m4b", ".m4p", ".aa", ".wav"): + # Reformat fullPath so that the basepath is lower/upper and the rest lower. + # This is required to get the correct position (track index) inside Playlist.construct() + # /media/username/USER'S IPOD/IPod_Control/Music/Artist/Album/Track.mp3 + fullPath = os.path.abspath(os.path.join(dirpath, filename)) + # /media/username/USER'S IPOD/ + basepath = self.base + # ipod_control/music/artist/album/track.mp3 + ipodpath = self.path_to_ipod(fullPath)[1:].lower() + # /media/username/USER'S IPOD/ipod_control/music/artist/album/track.mp3 + fullPath = os.path.abspath(os.path.join(basepath, ipodpath)) + listtracks.append(fullPath) + if not recursive: + break + return listtracks + def remove_relatives(self, relative, filename): base = os.path.dirname(os.path.abspath(filename)) if not os.path.exists(relative): @@ -461,17 +488,26 @@ def remove_relatives(self, relative, filename): return fullPath def populate(self, filename): - with open(filename, 'rb') as f: - data = f.readlines() - - extension = os.path.splitext(filename)[1].lower() - if extension == '.pls': - self.listtracks = self.populate_pls(data) - elif extension == '.m3u': - self.listtracks = self.populate_m3u(data) - # Ensure all paths are not relative to the playlist file - for i in range(len(self.listtracks)): - self.listtracks[i] = self.remove_relatives(self.listtracks[i], filename) + # Create a playlist of the folder and all subfolders + if os.path.isdir(filename): + self.listtracks = self.populate_directory(filename) + + # Read the playlist file + else: + with open(filename, 'rb') as f: + data = f.readlines() + + extension = os.path.splitext(filename)[1].lower() + if extension == '.pls': + self.listtracks = self.populate_pls(data) + elif extension == '.m3u': + self.listtracks = self.populate_m3u(data) + else: + raise + + # Ensure all paths are not relative to the playlist file + for i in range(len(self.listtracks)): + self.listtracks[i] = self.remove_relatives(self.listtracks[i], filename) # Handle the VoiceOverData text = os.path.splitext(os.path.basename(filename))[0] @@ -502,7 +538,7 @@ def construct(self, tracks): #pylint: disable-msg=W0221 return output + chunks class Shuffler(object): - def __init__(self, path, voiceover=True, rename=False, trackgain=0): + def __init__(self, path, voiceover=True, rename=False, trackgain=0, auto_playlists=None): self.path, self.base = self.determine_base(path) self.tracks = [] self.albums = [] @@ -512,6 +548,7 @@ def __init__(self, path, voiceover=True, rename=False, trackgain=0): self.voiceover = voiceover self.rename = rename self.trackgain = trackgain + self.auto_playlists = auto_playlists def initialize(self): # remove existing voiceover files (they are either useless or will be overwritten anyway) @@ -548,6 +585,15 @@ def populate(self): if os.path.splitext(filename)[1].lower() in (".pls", ".m3u"): self.lists.append(os.path.abspath(os.path.join(dirpath, filename))) + # Create automatic playlists in music directory. + # Ignore the (music) root and any hidden directories. + if self.auto_playlists and "ipod_control/music/" in dirpath.lower() and "/." not in dirpath.lower(): + # Only go to a specific depth. -1 is unlimted, 0 is ignored as there is already a master playlist. + depth = dirpath[len(self.path) + len(os.path.sep):].count(os.path.sep) - 1 + if self.auto_playlists < 0 or depth <= self.auto_playlists: + print "Adding folder", depth, " ", dirpath + self.lists.append(os.path.abspath(dirpath)) + def write_database(self): with open(os.path.join(self.base, "iPod_Control", "iTunes", "iTunesSD"), "wb") as f: f.write(self.tunessd.construct()) @@ -617,6 +663,10 @@ def handle_interrupt(signal, frame): 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-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('path', help='Path to the IPod\'s root directory') result = parser.parse_args() @@ -629,7 +679,7 @@ def handle_interrupt(signal, frame): 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, auto_playlists=result.auto_playlists) shuffle.initialize() shuffle.populate() shuffle.write_database() From 6e919eca3db7e4fee341425333bd3ff22747832b Mon Sep 17 00:00:00 2001 From: NicoHood Date: Tue, 5 Apr 2016 20:58:52 +0200 Subject: [PATCH 04/16] Minor typo --- shuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shuffle.py b/shuffle.py index 6d5c57b..4e95d43 100755 --- a/shuffle.py +++ b/shuffle.py @@ -579,7 +579,7 @@ def populate(self): for filename in sorted(filenames, key = lambda x: x.lower()): fullPath = os.path.abspath(os.path.join(dirpath, filename)) relPath = fullPath[fullPath.index(self.path)+len(self.path)+1:].lower() - fullPath = os.path.abspath(os.path.join(self.path, relPath)); + fullPath = os.path.abspath(os.path.join(self.path, relPath)) 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"): From 8dff7e8d5e15521e41a02b91d346f81e2b30441d Mon Sep 17 00:00:00 2001 From: NicoHood Date: Tue, 5 Apr 2016 22:03:10 +0200 Subject: [PATCH 05/16] Skip hidden directories for auto playlists --- shuffle.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/shuffle.py b/shuffle.py index 4e95d43..acb347c 100755 --- a/shuffle.py +++ b/shuffle.py @@ -459,20 +459,22 @@ def populate_directory(self, playlistpath, recursive = True): for (dirpath, dirnames, filenames) in os.walk(playlistpath): dirnames.sort() - for filename in sorted(filenames, key = lambda x: x.lower()): - # Only add valid music files to playlist - if os.path.splitext(filename)[1].lower() in (".mp3", ".m4a", ".m4b", ".m4p", ".aa", ".wav"): - # Reformat fullPath so that the basepath is lower/upper and the rest lower. - # This is required to get the correct position (track index) inside Playlist.construct() - # /media/username/USER'S IPOD/IPod_Control/Music/Artist/Album/Track.mp3 - fullPath = os.path.abspath(os.path.join(dirpath, filename)) - # /media/username/USER'S IPOD/ - basepath = self.base - # ipod_control/music/artist/album/track.mp3 - ipodpath = self.path_to_ipod(fullPath)[1:].lower() - # /media/username/USER'S IPOD/ipod_control/music/artist/album/track.mp3 - fullPath = os.path.abspath(os.path.join(basepath, ipodpath)) - listtracks.append(fullPath) + # Ignore any hidden directories + if "/." not in dirpath.lower(): + for filename in sorted(filenames, key = lambda x: x.lower()): + # Only add valid music files to playlist + if os.path.splitext(filename)[1].lower() in (".mp3", ".m4a", ".m4b", ".m4p", ".aa", ".wav"): + # Reformat fullPath so that the basepath is lower/upper and the rest lower. + # This is required to get the correct position (track index) inside Playlist.construct() + # /media/username/USER'S IPOD/IPod_Control/Music/Artist/Album/Track.mp3 + fullPath = os.path.abspath(os.path.join(dirpath, filename)) + # /media/username/USER'S IPOD/ + basepath = self.base + # ipod_control/music/artist/album/track.mp3 + ipodpath = self.path_to_ipod(fullPath)[1:].lower() + # /media/username/USER'S IPOD/ipod_control/music/artist/album/track.mp3 + fullPath = os.path.abspath(os.path.join(basepath, ipodpath)) + listtracks.append(fullPath) if not recursive: break return listtracks From d71be4f9fb46c3290fc4879ee4313b14e4cc02cf Mon Sep 17 00:00:00 2001 From: NicoHood Date: Tue, 5 Apr 2016 22:06:53 +0200 Subject: [PATCH 06/16] Removed debug output --- shuffle.py | 1 - 1 file changed, 1 deletion(-) diff --git a/shuffle.py b/shuffle.py index acb347c..c4cff6c 100755 --- a/shuffle.py +++ b/shuffle.py @@ -593,7 +593,6 @@ def populate(self): # Only go to a specific depth. -1 is unlimted, 0 is ignored as there is already a master playlist. depth = dirpath[len(self.path) + len(os.path.sep):].count(os.path.sep) - 1 if self.auto_playlists < 0 or depth <= self.auto_playlists: - print "Adding folder", depth, " ", dirpath self.lists.append(os.path.abspath(dirpath)) def write_database(self): From 5b2a4a2a3637aed2ec0bc4ed58e10345ae968951 Mon Sep 17 00:00:00 2001 From: NicoHood Date: Tue, 5 Apr 2016 23:11:48 +0200 Subject: [PATCH 07/16] Fix hyphen in filename #4 --- shuffle.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/shuffle.py b/shuffle.py index c4cff6c..502cbc6 100755 --- a/shuffle.py +++ b/shuffle.py @@ -327,7 +327,11 @@ def populate(self, filename): self["filetype"] = 2 text = os.path.splitext(os.path.basename(filename))[0] - audio = mutagen.File(filename, easy = True) + audio = None + try: + audio = mutagen.File(filename, easy = True) + except: + print "Error calling mutagen. Possible invalid filename/ID3Tags (hyphen in filename?)" if audio: # Note: Rythmbox IPod plugin sets this value always 0. self["stop_at_pos_ms"] = int(audio.info.length * 1000) From 4134e93cd3c56680a893140bcd24898781bfb467 Mon Sep 17 00:00:00 2001 From: NicoHood Date: Wed, 6 Apr 2016 18:10:24 +0200 Subject: [PATCH 08/16] Removed lower case from script (fix issue #5) --- shuffle.py | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/shuffle.py b/shuffle.py index 502cbc6..809a26d 100755 --- a/shuffle.py +++ b/shuffle.py @@ -464,20 +464,11 @@ def populate_directory(self, playlistpath, recursive = True): dirnames.sort() # Ignore any hidden directories - if "/." not in dirpath.lower(): + if "/." not in dirpath: for filename in sorted(filenames, key = lambda x: x.lower()): # Only add valid music files to playlist if os.path.splitext(filename)[1].lower() in (".mp3", ".m4a", ".m4b", ".m4p", ".aa", ".wav"): - # Reformat fullPath so that the basepath is lower/upper and the rest lower. - # This is required to get the correct position (track index) inside Playlist.construct() - # /media/username/USER'S IPOD/IPod_Control/Music/Artist/Album/Track.mp3 fullPath = os.path.abspath(os.path.join(dirpath, filename)) - # /media/username/USER'S IPOD/ - basepath = self.base - # ipod_control/music/artist/album/track.mp3 - ipodpath = self.path_to_ipod(fullPath)[1:].lower() - # /media/username/USER'S IPOD/ipod_control/music/artist/album/track.mp3 - fullPath = os.path.abspath(os.path.join(basepath, ipodpath)) listtracks.append(fullPath) if not recursive: break @@ -488,9 +479,6 @@ def remove_relatives(self, relative, filename): if not os.path.exists(relative): relative = os.path.join(base, relative) fullPath = relative - ipodpath = self.parent.parent.parent.path - relPath = fullPath[fullPath.index(ipodpath)+len(ipodpath)+1:].lower() - fullPath = os.path.abspath(os.path.join(ipodpath, relPath)) return fullPath def populate(self, filename): @@ -581,19 +569,17 @@ def populate(self): for (dirpath, dirnames, filenames) in os.walk(self.path): dirnames.sort() # Ignore the speakable directory and any hidden directories - if "ipod_control/speakable" not in dirpath.lower() and "/." not in dirpath.lower(): + if "iPod_Control/Speakable" not in dirpath and "/." not in dirpath: for filename in sorted(filenames, key = lambda x: x.lower()): fullPath = os.path.abspath(os.path.join(dirpath, filename)) - relPath = fullPath[fullPath.index(self.path)+len(self.path)+1:].lower() - fullPath = os.path.abspath(os.path.join(self.path, relPath)) 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(os.path.abspath(os.path.join(dirpath, filename))) + self.lists.append(fullPath) # Create automatic playlists in music directory. # Ignore the (music) root and any hidden directories. - if self.auto_playlists and "ipod_control/music/" in dirpath.lower() and "/." not in dirpath.lower(): + if self.auto_playlists and "iPod_Control/Music/" in dirpath and "/." not in dirpath: # Only go to a specific depth. -1 is unlimted, 0 is ignored as there is already a master playlist. depth = dirpath[len(self.path) + len(os.path.sep):].count(os.path.sep) - 1 if self.auto_playlists < 0 or depth <= self.auto_playlists: From 96a0d35dc86e5eb57d632f04624296135e486fb6 Mon Sep 17 00:00:00 2001 From: NicoHood Date: Wed, 6 Apr 2016 22:02:54 +0200 Subject: [PATCH 09/16] Use switch to enable voiceover --- shuffle.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/shuffle.py b/shuffle.py index 809a26d..e5b4db5 100755 --- a/shuffle.py +++ b/shuffle.py @@ -646,8 +646,8 @@ def handle_interrupt(signal, frame): parser = argparse.ArgumentParser(description= 'Python script for building the Track and Playlist database ' 'for the newer gen IPod Shuffle.') - parser.add_argument('--disable-voiceover', action='store_true', - help='Disable voiceover feature') + parser.add_argument('--voiceover', action='store_true', + help='Enable 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', @@ -666,11 +666,11 @@ def handle_interrupt(signal, frame): if result.rename_unicode: check_unicode(result.path) - if not result.disable_voiceover and not Text2Speech.check_support(): + if result.voiceover and not Text2Speech.check_support(): print "Error: Did not find any voiceover program. Voiceover disabled." - result.disable_voiceover = True + result.voiceover = False - shuffle = Shuffler(result.path, voiceover=not result.disable_voiceover, rename=result.rename_unicode, trackgain=result.track_gain, auto_playlists=result.auto_playlists) + shuffle = Shuffler(result.path, voiceover=result.voiceover, rename=result.rename_unicode, trackgain=result.track_gain, auto_playlists=result.auto_playlists) shuffle.initialize() shuffle.populate() shuffle.write_database() From 7129c05e99a48bd0dfbd4cb6cc6a5b436097f13b Mon Sep 17 00:00:00 2001 From: NicoHood Date: Wed, 6 Apr 2016 22:03:18 +0200 Subject: [PATCH 10/16] Add version number to description --- shuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shuffle.py b/shuffle.py index e5b4db5..9fd7f92 100755 --- a/shuffle.py +++ b/shuffle.py @@ -645,7 +645,7 @@ def handle_interrupt(signal, frame): signal.signal(signal.SIGINT, handle_interrupt) parser = argparse.ArgumentParser(description= 'Python script for building the Track and Playlist database ' - 'for the newer gen IPod Shuffle.') + 'for the newer gen IPod Shuffle. Version 1.3') parser.add_argument('--voiceover', action='store_true', help='Enable voiceover feature') parser.add_argument('--rename-unicode', action='store_true', From a1cebe9d0beaab17c025601b47c3f0c4d4d35711 Mon Sep 17 00:00:00 2001 From: NicoHood Date: Wed, 6 Apr 2016 22:08:28 +0200 Subject: [PATCH 11/16] Differentiate track and playlist voiceover --- shuffle.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/shuffle.py b/shuffle.py index 9fd7f92..4880802 100755 --- a/shuffle.py +++ b/shuffle.py @@ -157,6 +157,7 @@ def __init__(self, parent): self._struct = collections.OrderedDict([]) self._fields = {} self.voiceover = parent.voiceover + self.playlist_voiceover = parent.playlist_voiceover self.rename = parent.rename self.trackgain = parent.trackgain @@ -178,7 +179,7 @@ def construct(self): return output def text_to_speech(self, text, dbid, playlist = False): - if self.voiceover: + if self.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") @@ -423,7 +424,7 @@ def __init__(self, parent): 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'] or Text2Speech.valid_tts['espeak']): + if self.playlist_voiceover and (Text2Speech.valid_tts['pico2wave'] or Text2Speech.valid_tts['espeak']): self["dbid"] = hashlib.md5("masterlist").digest()[:8] #pylint: disable-msg=E1101 self.text_to_speech("All songs", self["dbid"], True) self["listtype"] = 1 @@ -532,7 +533,7 @@ def construct(self, tracks): #pylint: disable-msg=W0221 return output + chunks class Shuffler(object): - def __init__(self, path, voiceover=True, rename=False, trackgain=0, auto_playlists=None): + def __init__(self, path, voiceover=False, playlist_voiceover=False, rename=False, trackgain=0, auto_playlists=None): self.path, self.base = self.determine_base(path) self.tracks = [] self.albums = [] @@ -540,6 +541,7 @@ def __init__(self, path, voiceover=True, rename=False, trackgain=0, auto_playlis self.lists = [] self.tunessd = None self.voiceover = voiceover + self.playlist_voiceover = playlist_voiceover self.rename = rename self.trackgain = trackgain self.auto_playlists = auto_playlists @@ -647,7 +649,9 @@ def handle_interrupt(signal, frame): 'Python script for building the Track and Playlist database ' 'for the newer gen IPod Shuffle. Version 1.3') parser.add_argument('--voiceover', action='store_true', - help='Enable voiceover feature') + help='Enable track voiceover feature') + parser.add_argument('--playlist-voiceover', action='store_true', + help='Enable playlist 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', @@ -670,7 +674,7 @@ def handle_interrupt(signal, frame): print "Error: Did not find any voiceover program. Voiceover disabled." result.voiceover = False - shuffle = Shuffler(result.path, voiceover=result.voiceover, rename=result.rename_unicode, trackgain=result.track_gain, auto_playlists=result.auto_playlists) + shuffle = Shuffler(result.path, voiceover=result.voiceover, playlist_voiceover=result.playlist_voiceover, rename=result.rename_unicode, trackgain=result.track_gain, auto_playlists=result.auto_playlists) shuffle.initialize() shuffle.populate() shuffle.write_database() From e6303ad9648bd5702df234c447f7d2ef873c1695 Mon Sep 17 00:00:00 2001 From: Nimesh Ghelani Date: Wed, 8 Jun 2016 02:57:04 +0530 Subject: [PATCH 12/16] Remove redundant self.base --- shuffle.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/shuffle.py b/shuffle.py index 4880802..7409f1d 100755 --- a/shuffle.py +++ b/shuffle.py @@ -207,7 +207,7 @@ def shuffledb(self): @property def base(self): - return self.shuffledb.base + return self.shuffledb.path @property def tracks(self): @@ -534,7 +534,7 @@ def construct(self, tracks): #pylint: disable-msg=W0221 class Shuffler(object): def __init__(self, path, voiceover=False, playlist_voiceover=False, rename=False, trackgain=0, auto_playlists=None): - self.path, self.base = self.determine_base(path) + self.path = os.path.abspath(path) self.tracks = [] self.albums = [] self.artists = [] @@ -560,12 +560,6 @@ def dump_state(self): print "Artists", self.artists print "Playlists", self.lists - def determine_base(self, path): - base = os.path.abspath(path) - # while not os.path.ismount(base): - # base = os.path.dirname(base) - return base, base - def populate(self): self.tunessd = TunesSD(self) for (dirpath, dirnames, filenames) in os.walk(self.path): @@ -588,7 +582,7 @@ def populate(self): self.lists.append(os.path.abspath(dirpath)) def write_database(self): - with open(os.path.join(self.base, "iPod_Control", "iTunes", "iTunesSD"), "wb") as f: + with open(os.path.join(self.path, "iPod_Control", "iTunes", "iTunesSD"), "wb") as f: f.write(self.tunessd.construct()) # From 06ce8cb4033bfad072c55ea8822bfa2502614027 Mon Sep 17 00:00:00 2001 From: Nimesh Ghelani Date: Wed, 8 Jun 2016 03:43:32 +0530 Subject: [PATCH 13/16] Add failsafe path operations --- shuffle.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/shuffle.py b/shuffle.py index 7409f1d..a2c632d 100755 --- a/shuffle.py +++ b/shuffle.py @@ -57,6 +57,16 @@ def exec_exists_in_path(command): except OSError as e: return False +def splitpath(path): + return path.split(os.sep) + +def get_relpath(path, basepath): + commonprefix = os.sep.join(os.path.commonprefix(map(splitpath, [path, basepath]))) + return os.path.relpath(path, commonprefix) + +def is_path_prefix(prefix, path): + return prefix == os.sep.join(os.path.commonprefix(map(splitpath, [prefix, path]))) + class Text2Speech(object): valid_tts = {'pico2wave': True, 'RHVoice': True, 'espeak': True} @@ -564,8 +574,9 @@ def populate(self): self.tunessd = TunesSD(self) for (dirpath, dirnames, filenames) in os.walk(self.path): dirnames.sort() + relpath = get_relpath(dirpath, self.path) # Ignore the speakable directory and any hidden directories - if "iPod_Control/Speakable" not in dirpath and "/." not in dirpath: + 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"): From 44fc42a2e42094cd57430278f49f7347150ee35f Mon Sep 17 00:00:00 2001 From: Nimesh Ghelani Date: Wed, 8 Jun 2016 05:14:03 +0530 Subject: [PATCH 14/16] Update README.md for v1.3 --- README.md | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1a21b8e..19cbf81 100644 --- a/README.md +++ b/README.md @@ -8,22 +8,42 @@ 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] [--disable-voiceover] [--rename-unicode] +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 +Python script for building the Track and Playlist database for the newer gen +IPod Shuffle. Version 1.3 + positional arguments: - path + path Path to the IPod's root directory optional arguments: -h, --help show this help message and exit - --disable-voiceover Disable Voiceover Feature - --rename-unicode Rename Files Causing Unicode Errors, will do minimal + --voiceover Enable track voiceover feature + --playlist-voiceover Enable playlist voiceover feature + --rename-unicode Rename files causing unicode errors, will do minimal required renaming --track-gain TRACK_GAIN - Store this 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 + 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] + 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] + 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}' ``` #### Dependencies @@ -106,6 +126,15 @@ Original data can be found via [wayback machine](https://web.archive.org/web/201 # Version History ``` +1.3 Release (08.06.2016) +* Directory based auto playlist building (--auto-dir-playlists) (#13) +* ID3 tags based auto playlist building (--auto-id3-playlists) +* Added short program description +* Fix hyphen in filename #4 +* Fixed mutagen bug #5 +* Voiceover disabled by default #26 (Playlist voiceover enabled with auto playlist generation) +* Differentiate track and playlist voiceover #26 + 1.2 Release (04.02.2016) * Additional fixes from NicoHood * Fixed "All Songs" and "Playlist N" sounds when voiceover is disabled #17 From f22fdee04266e27efb8de17c7a1600e6933dad1f Mon Sep 17 00:00:00 2001 From: Nimesh Ghelani Date: Wed, 8 Jun 2016 05:14:58 +0530 Subject: [PATCH 15/16] Add id3 based auto playlist generation --- shuffle.py | 100 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 75 insertions(+), 25 deletions(-) diff --git a/shuffle.py b/shuffle.py index a2c632d..58db65c 100755 --- a/shuffle.py +++ b/shuffle.py @@ -67,6 +67,30 @@ def get_relpath(path, basepath): def is_path_prefix(prefix, path): return prefix == os.sep.join(os.path.commonprefix(map(splitpath, [prefix, path]))) +def group_tracks_by_id3_template(tracks, template): + grouped_tracks_dict = {} + template_vars = set(re.findall(r'{.*?}', template)) + for track in tracks: + try: + id3_dict = mutagen.File(track, easy=True) + except: + id3_dict = {} + + key = template + single_var_present = False + for var in template_vars: + val = id3_dict.get(var[1:-1], [''])[0] + if len(val) > 0: + single_var_present = True + key = key.replace(var, val) + + if single_var_present: + if key not in grouped_tracks_dict: + grouped_tracks_dict[key] = [] + grouped_tracks_dict[key].append(track) + + return sorted(grouped_tracks_dict.items()) + class Text2Speech(object): valid_tts = {'pico2wave': True, 'RHVoice': True, 'espeak': True} @@ -395,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 + print "[+] Adding playlist", (i[0] if type(i) == type(()) else i) playlist.populate(i) construction = playlist.construct(tracks) if playlist["number_of_songs"] > 0: @@ -492,30 +516,35 @@ def remove_relatives(self, relative, filename): fullPath = relative return fullPath - def populate(self, filename): + def populate(self, obj): # Create a playlist of the folder and all subfolders - if os.path.isdir(filename): - self.listtracks = self.populate_directory(filename) - - # Read the playlist file + if type(obj) == type(()): + self.listtracks = obj[1] + text = obj[0] else: - with open(filename, 'rb') as f: - data = f.readlines() - - extension = os.path.splitext(filename)[1].lower() - if extension == '.pls': - self.listtracks = self.populate_pls(data) - elif extension == '.m3u': - self.listtracks = self.populate_m3u(data) + filename = obj + if os.path.isdir(filename): + self.listtracks = self.populate_directory(filename) + text = os.path.splitext(os.path.basename(filename))[0] else: - raise - - # Ensure all paths are not relative to the playlist file - for i in range(len(self.listtracks)): - self.listtracks[i] = self.remove_relatives(self.listtracks[i], filename) + # Read the playlist file + with open(filename, 'rb') as f: + data = f.readlines() + + extension = os.path.splitext(filename)[1].lower() + if extension == '.pls': + self.listtracks = self.populate_pls(data) + elif extension == '.m3u': + self.listtracks = self.populate_m3u(data) + else: + raise + + # Ensure all paths are not relative to the playlist file + for i in range(len(self.listtracks)): + self.listtracks[i] = self.remove_relatives(self.listtracks[i], filename) + text = os.path.splitext(os.path.basename(filename))[0] # Handle the VoiceOverData - text = os.path.splitext(os.path.basename(filename))[0] self["dbid"] = hashlib.md5(text).digest()[:8] #pylint: disable-msg=E1101 self.text_to_speech(text, self["dbid"], True) @@ -543,7 +572,7 @@ 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_playlists=None): + def __init__(self, path, 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 = [] @@ -554,7 +583,8 @@ def __init__(self, path, voiceover=False, playlist_voiceover=False, rename=False self.playlist_voiceover = playlist_voiceover self.rename = rename self.trackgain = trackgain - self.auto_playlists = auto_playlists + self.auto_dir_playlists = auto_dir_playlists + self.auto_id3_playlists = auto_id3_playlists def initialize(self): # remove existing voiceover files (they are either useless or will be overwritten anyway) @@ -650,24 +680,40 @@ def handle_interrupt(signal, frame): if __name__ == '__main__': signal.signal(signal.SIGINT, handle_interrupt) + parser = argparse.ArgumentParser(description= 'Python script for building the Track and Playlist database ' 'for the newer gen IPod Shuffle. Version 1.3') + parser.add_argument('--voiceover', action='store_true', help='Enable track voiceover feature') + parser.add_argument('--playlist-voiceover', action='store_true', help='Enable playlist 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('--auto-playlists', type=int, default=None, const=-1, nargs='?', + + parser.add_argument('--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='?', + 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('path', help='Path to the IPod\'s root directory') + result = parser.parse_args() checkPathValidity(result.path) @@ -675,11 +721,15 @@ def handle_interrupt(signal, frame): if result.rename_unicode: check_unicode(result.path) - if result.voiceover and not Text2Speech.check_support(): + 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(): print "Error: Did not find any voiceover program. Voiceover disabled." result.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_playlists=result.auto_playlists) + 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.initialize() shuffle.populate() shuffle.write_database() From d3e5c767be7b0a34e065812883d988a4a6d8e311 Mon Sep 17 00:00:00 2001 From: Nimesh Ghelani Date: Wed, 8 Jun 2016 05:15:16 +0530 Subject: [PATCH 16/16] Add better handling of filenames and directories --- shuffle.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/shuffle.py b/shuffle.py index 58db65c..29ea499 100755 --- a/shuffle.py +++ b/shuffle.py @@ -616,12 +616,16 @@ def populate(self): # Create automatic playlists in music directory. # Ignore the (music) root and any hidden directories. - if self.auto_playlists and "iPod_Control/Music/" in dirpath and "/." not in dirpath: + if self.auto_dir_playlists and "iPod_Control/Music/" in dirpath and "/." not in dirpath: # Only go to a specific depth. -1 is unlimted, 0 is ignored as there is already a master playlist. depth = dirpath[len(self.path) + len(os.path.sep):].count(os.path.sep) - 1 - if self.auto_playlists < 0 or depth <= self.auto_playlists: + if self.auto_dir_playlists < 0 or depth <= self.auto_dir_playlists: self.lists.append(os.path.abspath(dirpath)) + if self.auto_id3_playlists != None: + for grouped_list in group_tracks_by_id3_template(self.tracks, self.auto_id3_playlists): + self.lists.append(grouped_list) + def write_database(self): with open(os.path.join(self.path, "iPod_Control", "iTunes", "iTunesSD"), "wb") as f: f.write(self.tunessd.construct())