mirror of
https://github.com/nims11/IPod-Shuffle-4g.git
synced 2025-12-07 07:58:01 +09:00
Merge branch 'autoplaylist' of https://github.com/NicoHood/IPod-Shuffle-4g into NicoHood-autoplaylist
This commit is contained in:
commit
21eba1e2be
1 changed files with 80 additions and 28 deletions
108
shuffle.py
108
shuffle.py
|
|
@ -157,6 +157,7 @@ class Record(object):
|
||||||
self._struct = collections.OrderedDict([])
|
self._struct = collections.OrderedDict([])
|
||||||
self._fields = {}
|
self._fields = {}
|
||||||
self.voiceover = parent.voiceover
|
self.voiceover = parent.voiceover
|
||||||
|
self.playlist_voiceover = parent.playlist_voiceover
|
||||||
self.rename = parent.rename
|
self.rename = parent.rename
|
||||||
self.trackgain = parent.trackgain
|
self.trackgain = parent.trackgain
|
||||||
|
|
||||||
|
|
@ -178,7 +179,7 @@ class Record(object):
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def text_to_speech(self, text, dbid, playlist = False):
|
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
|
# 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")
|
||||||
|
|
@ -327,7 +328,11 @@ class Track(Record):
|
||||||
self["filetype"] = 2
|
self["filetype"] = 2
|
||||||
|
|
||||||
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 = None
|
||||||
|
try:
|
||||||
|
audio = mutagen.File(filename, easy = True)
|
||||||
|
except:
|
||||||
|
print "Error calling mutagen. Possible invalid filename/ID3Tags (hyphen in filename?)"
|
||||||
if audio:
|
if audio:
|
||||||
# Note: Rythmbox IPod plugin sets this value always 0.
|
# 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)
|
||||||
|
|
@ -419,7 +424,7 @@ class Playlist(Record):
|
||||||
def set_master(self, tracks):
|
def set_master(self, tracks):
|
||||||
# By default use "All Songs" builtin voiceover (dbid all zero)
|
# By default use "All Songs" builtin voiceover (dbid all zero)
|
||||||
# Else generate alternative "All Songs" to fit the speaker voice of other playlists
|
# 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["dbid"] = hashlib.md5("masterlist").digest()[:8] #pylint: disable-msg=E1101
|
||||||
self.text_to_speech("All songs", self["dbid"], True)
|
self.text_to_speech("All songs", self["dbid"], True)
|
||||||
self["listtype"] = 1
|
self["listtype"] = 1
|
||||||
|
|
@ -450,28 +455,54 @@ class Playlist(Record):
|
||||||
listtracks = [ x for (_, x) in sorted(sorttracks) ]
|
listtracks = [ x for (_, x) in sorted(sorttracks) ]
|
||||||
return listtracks
|
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()
|
||||||
|
|
||||||
|
# Ignore any hidden directories
|
||||||
|
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"):
|
||||||
|
fullPath = os.path.abspath(os.path.join(dirpath, filename))
|
||||||
|
listtracks.append(fullPath)
|
||||||
|
if not recursive:
|
||||||
|
break
|
||||||
|
return listtracks
|
||||||
|
|
||||||
def remove_relatives(self, relative, filename):
|
def remove_relatives(self, relative, filename):
|
||||||
base = os.path.dirname(os.path.abspath(filename))
|
base = os.path.dirname(os.path.abspath(filename))
|
||||||
if not os.path.exists(relative):
|
if not os.path.exists(relative):
|
||||||
relative = os.path.join(base, relative)
|
relative = os.path.join(base, relative)
|
||||||
fullPath = 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
|
return fullPath
|
||||||
|
|
||||||
def populate(self, filename):
|
def populate(self, filename):
|
||||||
with open(filename, 'rb') as f:
|
# Create a playlist of the folder and all subfolders
|
||||||
data = f.readlines()
|
if os.path.isdir(filename):
|
||||||
|
self.listtracks = self.populate_directory(filename)
|
||||||
|
|
||||||
extension = os.path.splitext(filename)[1].lower()
|
# Read the playlist file
|
||||||
if extension == '.pls':
|
else:
|
||||||
self.listtracks = self.populate_pls(data)
|
with open(filename, 'rb') as f:
|
||||||
elif extension == '.m3u':
|
data = f.readlines()
|
||||||
self.listtracks = self.populate_m3u(data)
|
|
||||||
# Ensure all paths are not relative to the playlist file
|
extension = os.path.splitext(filename)[1].lower()
|
||||||
for i in range(len(self.listtracks)):
|
if extension == '.pls':
|
||||||
self.listtracks[i] = self.remove_relatives(self.listtracks[i], filename)
|
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
|
# Handle the VoiceOverData
|
||||||
text = os.path.splitext(os.path.basename(filename))[0]
|
text = os.path.splitext(os.path.basename(filename))[0]
|
||||||
|
|
@ -502,7 +533,7 @@ class Playlist(Record):
|
||||||
return output + chunks
|
return output + chunks
|
||||||
|
|
||||||
class Shuffler(object):
|
class Shuffler(object):
|
||||||
def __init__(self, path, voiceover=True, rename=False, trackgain=0):
|
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, self.base = self.determine_base(path)
|
||||||
self.tracks = []
|
self.tracks = []
|
||||||
self.albums = []
|
self.albums = []
|
||||||
|
|
@ -510,8 +541,10 @@ class Shuffler(object):
|
||||||
self.lists = []
|
self.lists = []
|
||||||
self.tunessd = None
|
self.tunessd = None
|
||||||
self.voiceover = voiceover
|
self.voiceover = voiceover
|
||||||
|
self.playlist_voiceover = playlist_voiceover
|
||||||
self.rename = rename
|
self.rename = rename
|
||||||
self.trackgain = trackgain
|
self.trackgain = trackgain
|
||||||
|
self.auto_playlists = auto_playlists
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
# remove existing voiceover files (they are either useless or will be overwritten anyway)
|
# remove existing voiceover files (they are either useless or will be overwritten anyway)
|
||||||
|
|
@ -538,15 +571,21 @@ class Shuffler(object):
|
||||||
for (dirpath, dirnames, filenames) in os.walk(self.path):
|
for (dirpath, dirnames, filenames) in os.walk(self.path):
|
||||||
dirnames.sort()
|
dirnames.sort()
|
||||||
# Ignore the speakable directory and any hidden directories
|
# 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()):
|
for filename in sorted(filenames, key = lambda x: x.lower()):
|
||||||
fullPath = os.path.abspath(os.path.join(dirpath, filename))
|
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"):
|
if os.path.splitext(filename)[1].lower() in (".mp3", ".m4a", ".m4b", ".m4p", ".aa", ".wav"):
|
||||||
self.tracks.append(fullPath)
|
self.tracks.append(fullPath)
|
||||||
if os.path.splitext(filename)[1].lower() in (".pls", ".m3u"):
|
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 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:
|
||||||
|
self.lists.append(os.path.abspath(dirpath))
|
||||||
|
|
||||||
def write_database(self):
|
def write_database(self):
|
||||||
with open(os.path.join(self.base, "iPod_Control", "iTunes", "iTunesSD"), "wb") as f:
|
with open(os.path.join(self.base, "iPod_Control", "iTunes", "iTunesSD"), "wb") as f:
|
||||||
|
|
@ -606,10 +645,23 @@ 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(description=
|
||||||
parser.add_argument('--disable-voiceover', action='store_true', help='Disable voiceover feature')
|
'Python script for building the Track and Playlist database '
|
||||||
parser.add_argument('--rename-unicode', action='store_true', help='Rename files causing unicode errors, will do minimal required renaming')
|
'for the newer gen IPod Shuffle. Version 1.3')
|
||||||
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('--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='?',
|
||||||
|
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')
|
parser.add_argument('path', help='Path to the IPod\'s root directory')
|
||||||
result = parser.parse_args()
|
result = parser.parse_args()
|
||||||
|
|
||||||
|
|
@ -618,11 +670,11 @@ if __name__ == '__main__':
|
||||||
if result.rename_unicode:
|
if result.rename_unicode:
|
||||||
check_unicode(result.path)
|
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."
|
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)
|
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.initialize()
|
||||||
shuffle.populate()
|
shuffle.populate()
|
||||||
shuffle.write_database()
|
shuffle.write_database()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue