diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 0665084..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,111 +0,0 @@ -# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -This changlog uses the [ISO 8601 date format](https://www.iso.org/iso-8601-date-and-time-format.html) of (YYYY-MM-DD). - -## [Unreleased] - -## [1.5.1] - 2021-06-05 - -### Changed - -* Moved Changelog from Readme to Changelog.md file - -### Fixed - -* Fix TypeError when reading playlist files [#50](https://github.com/nims11/IPod-Shuffle-4g/pull/50) - -## [1.5.0] - 2020-06-10 - -### Changed - -* Port Script to Python3 -* Mutagen support is now optional - -## [1.4.0] - 2016-08-28 - -### Added - -* Added optional `--verbose` output -* Added files to `extras` folder -* Added shortcut parameters (`-p`, `-t`, `-d`, etc.) - -### Changed - -* Renamed `--voiceover` to `--track-voiceover` -* Renamed script from `shuffle.py` to `ipod-shuffle-4g.py` -* Ignore hidden filenames -* Do not force playlist voiceover with auto playlists - -### Fixed - -* Catch "no space left" error [#30](https://github.com/nims11/IPod-Shuffle-4g/issues/30) -* Fix UnicodeEncodeError for non-ascii playlist names [#35](https://github.com/nims11/IPod-Shuffle-4g/issues/35) - -## [1.3.0] - 2016-06-08 - -### Added - -* Directory based auto playlist building (`--auto-dir-playlists`) [#13](https://github.com/nims11/IPod-Shuffle-4g/issues/13) -* ID3 tags based auto playlist building (`--auto-id3-playlists`) -* Added short program description -* Differentiate track and playlist voiceover [#26](https://github.com/nims11/IPod-Shuffle-4g/issues/26) - -### Changed - -* Voiceover disabled by default [#26](https://github.com/nims11/IPod-Shuffle-4g/issues/26) (Playlist voiceover enabled with auto playlist generation) - - -### Fixed - -* Fix hyphen in filename [#4](https://github.com/nims11/IPod-Shuffle-4g/issues/4) -* Fixed mutagen bug [#5](https://github.com/nims11/IPod-Shuffle-4g/issues/5) - -## [1.2.0] - 2016-02-04 - -### Added - -* Added Path help entry -* Added MIT License -* Added this changelog - -### Changed - -* Skip existing voiceover files with the same name (e.g. "Track 1.mp3") -* Made help message lower case -* Improved Readme -* Improved docs - -### Fixed - -* Additional fixes from NicoHood -* Fixed "All Songs" and "Playlist N" sounds when voiceover is disabled [#17](https://github.com/nims11/IPod-Shuffle-4g/issues/17) -* Better handle broken playlist paths [#16](https://github.com/nims11/IPod-Shuffle-4g/issues/16) -* Only use voiceover if dependencies are installed - -## [1.1.0] - 2016-01-23 - -### Added - -* 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.0] - 2012-10-17 - -### Added - -* Original release by ikelos - -[Unreleased]: https://github.com/nims11/IPod-Shuffle-4g/compare/1.5.1...HEAD -[1.5.1]: https://github.com/nims11/IPod-Shuffle-4g/compare/v1.5...1.5.1 -[1.5.0]: https://github.com/nims11/IPod-Shuffle-4g/compare/v1.4...v1.5 -[1.4.0]: https://github.com/nims11/IPod-Shuffle-4g/compare/v1.3...v1.4 -[1.3.0]: https://github.com/nims11/IPod-Shuffle-4g/compare/v1.2...v1.3 -[1.2.0]: https://github.com/nims11/IPod-Shuffle-4g/compare/v1.1...v1.2 -[1.1.0]: https://github.com/nims11/IPod-Shuffle-4g/compare/646b7def4c498c59b063e535a5b64695d8d87e6b...v1.1 -[1.0.0]: https://github.com/nims11/IPod-Shuffle-4g/commit/646b7def4c498c59b063e535a5b64695d8d87e6b diff --git a/README.md b/README.md index ba6c8c0..bcf1c80 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ 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/) -and improved my nims11 and NicoHood. Just put your audio files into the mass storage of your IPod and shuffle.py will do the rest. ``` @@ -14,7 +13,7 @@ usage: ipod-shuffle-4g.py [-h] [-t] [-p] [-u] [-g TRACK_GAIN] path Python script for building the Track and Playlist database for the newer gen -IPod Shuffle. Version 1.5 +IPod Shuffle. Version 1.4 positional arguments: path Path to the IPod's root directory @@ -51,9 +50,7 @@ optional arguments: #### Dependencies This script requires: -* [Python 3](https://www.python.org/download/releases/3.0/) - -Optional album/artist and auto-id3-playlists support: +* [Python 2.7](http://www.python.org/download/releases/2.7/) * [Mutagen](https://code.google.com/p/mutagen/) Optional Voiceover support @@ -61,29 +58,28 @@ Optional Voiceover support * [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) -* say (macOS) ##### Ubuntu -`apt-get install python3 python-mutagen libttspico*` +`apt-get install python-mutagen libttspico*` ##### Arch Linux -From the **Extra** repository: `pacman -S python` and optional `pacman -S python-mutagen espeak` or from the AUR: `svox-pico-bin` ([link](https://aur.archlinux.org/packages/svox-pico-bin/)) +From the **Extra** repository: `pacman -S python2 mutagen` and optional `pacman -S espeak` or from the AUR: `svox-pico-bin` ([link](https://aur.archlinux.org/packages/svox-pico-bin/)) You can also [install the script from AUR](https://aur.archlinux.org/packages/ipod-shuffle-4g/). ##### Gentoo Linux ```bash -PYTHON_TARGETS="python3" emerge -av media-libs/mutagen +PYTHON_TARGETS="python2_7" emerge -av media-libs/mutagen layman --add=ikelos layman --overlays="https://raw.githubusercontent.com/ahippo/rhvoice-gentoo-overlay/master/repositories.xml" --fetch --add=ahippo-rhvoice-overlay ACCEPT_KEYWORDS="~amd64" emerge -av app-accessibility/svox app-accessibility/rhvoice ``` 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) -## Tips and Tricks +##Tips and Tricks #### Disable trash for IPod To avoid that linux moves deleted files into trash you can create an empty file `.Trash-1000`. @@ -144,3 +140,53 @@ Your IPod should work and play music again now. 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). + + +# 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) +* 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 +* 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 +``` diff --git a/ipod-shuffle-4g.py b/ipod-shuffle-4g.py index 8225a56..717a4d4 100755 --- a/ipod-shuffle-4g.py +++ b/ipod-shuffle-4g.py @@ -1,11 +1,12 @@ -#!/usr/bin/env python3 - -# Builtin libraries +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- import sys import struct -import urllib.request, urllib.parse, urllib.error +import urllib import os import hashlib +import mutagen +import binascii import subprocess import collections import errno @@ -15,12 +16,6 @@ import re import tempfile import signal -# External libraries -try: - import mutagen -except ImportError: - mutagen = None - audio_ext = (".mp3", ".m4a", ".m4b", ".m4p", ".aa", ".wav") list_ext = (".pls", ".m3u") def make_dir_if_absent(path): @@ -32,19 +27,19 @@ def make_dir_if_absent(path): def raises_unicode_error(str): try: - str.encode('latin-1') + str.decode('utf-8').encode('latin-1') return False except (UnicodeEncodeError, UnicodeDecodeError): return True def hash_error_unicode(item): - item_bytes = item.encode('utf-8') - return "".join(["{0:02X}".format(ord(x)) for x in reversed(hashlib.md5(item_bytes).hexdigest()[:8])]) + return "".join(["{0:02X}".format(ord(x)) for x in reversed(hashlib.md5(item).digest()[:8])]) + pass def validate_unicode(path): path_list = path.split('/') last_raise = False - for i in range(len(path_list)): + for i in xrange(len(path_list)): if raises_unicode_error(path_list[i]): path_list[i] = hash_error_unicode(path_list[i]) last_raise = True @@ -66,11 +61,11 @@ def splitpath(path): return path.split(os.sep) def get_relpath(path, basepath): - commonprefix = os.sep.join(os.path.commonprefix(list(map(splitpath, [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(list(map(splitpath, [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 = {} @@ -97,37 +92,30 @@ def group_tracks_by_id3_template(tracks, template): return sorted(grouped_tracks_dict.items()) class Text2Speech(object): - valid_tts = {'pico2wave': True, 'RHVoice': True, 'espeak': True, 'say': True} + valid_tts = {'pico2wave': True, 'RHVoice': True, 'espeak': True} @staticmethod def check_support(): voiceoverAvailable = False - # Check for macOS say voiceover - if not exec_exists_in_path("say"): - Text2Speech.valid_tts['say'] = False - print("Warning: macOS say not found, voicever won't be generated using it.") - else: - voiceoverAvailable = True - # Check for pico2wave voiceover if not exec_exists_in_path("pico2wave"): Text2Speech.valid_tts['pico2wave'] = False - print("Warning: pico2wave not found, voicever won't be generated using it.") + print "Warning: pico2wave not found, voicever won't be generated using it." else: voiceoverAvailable = True # Check for espeak voiceover if not exec_exists_in_path("espeak"): Text2Speech.valid_tts['espeak'] = False - print("Warning: espeak not found, voicever won't be generated using it.") + print "Warning: espeak not found, voicever won't be generated using it." else: voiceoverAvailable = True # Check for Russian RHVoice voiceover if not exec_exists_in_path("RHVoice"): Text2Speech.valid_tts['RHVoice'] = False - print("Warning: RHVoice not found, Russian voicever won't be generated.") + print "Warning: RHVoice not found, Russian voicever won't be generated." else: voiceoverAvailable = True @@ -144,8 +132,8 @@ class Text2Speech(object): return True # ensure we deal with unicode later - if not isinstance(text, str): - text = str(text, 'utf-8') + if not isinstance(text, unicode): + text = unicode(text, 'utf-8') lang = Text2Speech.guess_lang(text) if lang == "ru-RU": return Text2Speech.rhvoice(out_wav_path, text) @@ -154,8 +142,6 @@ class Text2Speech(object): return True elif Text2Speech.espeak(out_wav_path, text): return True - elif Text2Speech.say(out_wav_path, text): - return True else: return False @@ -163,7 +149,7 @@ class Text2Speech(object): @staticmethod def guess_lang(unicodetext): lang = 'en-GB' - if re.search("[А-Яа-я]", unicodetext) is not None: + if re.search(u"[А-Яа-я]", unicodetext) is not None: lang = 'ru-RU' return lang @@ -171,21 +157,14 @@ class Text2Speech(object): def pico2wave(out_wav_path, unicodetext): if not Text2Speech.valid_tts['pico2wave']: return False - subprocess.call(["pico2wave", "-l", "en-GB", "-w", out_wav_path, '--', unicodetext]) - return True - - @staticmethod - def say(out_wav_path, unicodetext): - if not Text2Speech.valid_tts['say']: - return False - subprocess.call(["say", "-o", out_wav_path, '--data-format=LEI16', '--file-format=WAVE', '--', unicodetext]) + subprocess.call(["pico2wave", "-l", "en-GB", "-w", out_wav_path, unicodetext]) return True @staticmethod def espeak(out_wav_path, unicodetext): if not Text2Speech.valid_tts['espeak']: return False - subprocess.call(["espeak", "-v", "english_rp", "-s", "150", "-w", out_wav_path, '--', unicodetext]) + subprocess.call(["espeak", "-v", "english_rp", "-s", "150", "-w", out_wav_path, unicodetext]) return True @staticmethod @@ -217,7 +196,7 @@ class Record(object): self.trackgain = parent.trackgain def __getitem__(self, item): - if item not in list(self._struct.keys()): + if item not in self._struct.keys(): raise KeyError return self._fields.get(item, self._struct[item][1]) @@ -225,16 +204,18 @@ class Record(object): self._fields[item] = value def construct(self): - output = bytes() - for i in list(self._struct.keys()): + output = "" + for i in self._struct.keys(): (fmt, default) = self._struct[i] + if fmt == "4s": + fmt, default = "I", int(binascii.hexlify(default), 16) output += struct.pack("<" + fmt, self._fields.get(i, default)) return output def text_to_speech(self, text, dbid, playlist = False): if self.track_voiceover and not playlist or self.playlist_voiceover and playlist: # Create the voiceover wav file - fn = ''.join(format(x, '02x') 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") return Text2Speech.text2speech(path, text) return False @@ -284,7 +265,7 @@ class TunesSD(Record): self.track_header = TrackHeader(self) self.play_header = PlaylistHeader(self) self._struct = collections.OrderedDict([ - ("header_id", ("4s", b"bdhs")), # shdb + ("header_id", ("4s", "shdb")), ("unknown1", ("I", 0x02000003)), ("total_length", ("I", 64)), ("total_number_of_tracks", ("I", 0)), @@ -296,7 +277,7 @@ class TunesSD(Record): ("total_tracks_without_podcasts", ("I", 0)), ("track_header_offset", ("I", 64)), ("playlist_header_offset", ("I", 0)), - ("unknown4", ("20s", b"\x00" * 20)), + ("unknown4", ("20s", "\x00" * 20)), ]) def construct(self): @@ -321,7 +302,7 @@ class TrackHeader(Record): self.base_offset = 0 Record.__init__(self, parent) self._struct = collections.OrderedDict([ - ("header_id", ("4s", b"hths")), # shth + ("header_id", ("4s", "shth")), ("total_length", ("I", 0)), ("number_of_tracks", ("I", 0)), ("unknown1", ("Q", 0)), @@ -333,7 +314,7 @@ class TrackHeader(Record): output = Record.construct(self) # Construct the underlying tracks - track_chunk = bytes() + track_chunk = "" for i in self.tracks: track = Track(self) verboseprint("[*] Adding track", i) @@ -347,13 +328,13 @@ class Track(Record): def __init__(self, parent): Record.__init__(self, parent) self._struct = collections.OrderedDict([ - ("header_id", ("4s", b"rths")), # shtr + ("header_id", ("4s", "shtr")), ("header_length", ("I", 0x174)), ("start_at_pos_ms", ("I", 0)), ("stop_at_pos_ms", ("I", 0)), ("volume_gain", ("I", int(self.trackgain))), ("filetype", ("I", 1)), - ("filename", ("256s", b"\x00" * 256)), + ("filename", ("256s", "\x00" * 256)), ("bookmark", ("I", 0)), ("dontskip", ("B", 1)), ("remember", ("B", 0)), @@ -371,49 +352,46 @@ class Track(Record): ("unknown4", ("Q", 0)), ("dbid", ("8s", 0)), ("artistid", ("I", 0)), - ("unknown5", ("32s", b"\x00" * 32)), + ("unknown5", ("32s", "\x00" * 32)), ]) def populate(self, filename): - self["filename"] = self.path_to_ipod(filename).encode('utf-8') + self["filename"] = self.path_to_ipod(filename) if os.path.splitext(filename)[1].lower() in (".m4a", ".m4b", ".m4p", ".aa"): self["filetype"] = 2 text = os.path.splitext(os.path.basename(filename))[0] + 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) - # Try to get album and artist information with mutagen - if mutagen: - 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) + artist = audio.get("artist", [u"Unknown"])[0] + if artist in self.artists: + self["artistid"] = self.artists.index(artist) + else: + self["artistid"] = len(self.artists) + self.artists.append(artist) - artist = audio.get("artist", ["Unknown"])[0] - if artist in self.artists: - self["artistid"] = self.artists.index(artist) - else: - self["artistid"] = len(self.artists) - self.artists.append(artist) + album = audio.get("album", [u"Unknown"])[0] + if album in self.albums: + self["albumid"] = self.albums.index(album) + else: + self["albumid"] = len(self.albums) + self.albums.append(album) - album = audio.get("album", ["Unknown"])[0] - if album in self.albums: - self["albumid"] = self.albums.index(album) - else: - self["albumid"] = len(self.albums) - self.albums.append(album) - - if audio.get("title", "") and audio.get("artist", ""): - text = " - ".join(audio.get("title", "") + audio.get("artist", "")) + if audio.get("title", "") and audio.get("artist", ""): + text = u" - ".join(audio.get("title", u"") + audio.get("artist", u"")) # Handle the VoiceOverData - if isinstance(text, str): + if isinstance(text, unicode): text = text.encode('utf-8', 'ignore') - self["dbid"] = hashlib.md5(text).digest()[:8] + self["dbid"] = hashlib.md5(text).digest()[:8] #pylint: disable-msg=E1101 self.text_to_speech(text, self["dbid"]) class PlaylistHeader(Record): @@ -421,16 +399,16 @@ class PlaylistHeader(Record): self.base_offset = 0 Record.__init__(self, parent) self._struct = collections.OrderedDict([ - ("header_id", ("4s", b"hphs")), #shph + ("header_id", ("4s", "shph")), ("total_length", ("I", 0)), ("number_of_playlists", ("I", 0)), - ("number_of_non_podcast_lists", ("2s", b"\xFF\xFF")), - ("number_of_master_lists", ("2s", b"\x01\x00")), - ("number_of_non_audiobook_lists", ("2s", b"\xFF\xFF")), - ("unknown2", ("2s", b"\x00" * 2)), + ("number_of_non_podcast_lists", ("2s", "\xFF\xFF")), + ("number_of_master_lists", ("2s", "\x01\x00")), + ("number_of_non_audiobook_lists", ("2s", "\xFF\xFF")), + ("unknown2", ("2s", "\x00" * 2)), ]) - def construct(self, tracks): + def construct(self, tracks): #pylint: disable-msg=W0221 # Build the master list masterlist = Playlist(self) verboseprint("[+] Adding master playlist") @@ -448,7 +426,7 @@ class PlaylistHeader(Record): playlistcount += 1 chunks += [construction] else: - print("Error: Playlist does not contain a single track. Skipping playlist.") + print "Error: Playlist does not contain a single track. Skipping playlist." self["number_of_playlists"] = playlistcount self["total_length"] = 0x14 + (self["number_of_playlists"] * 4) @@ -461,27 +439,27 @@ class PlaylistHeader(Record): output += struct.pack("I", offset) offset += len(chunks[i]) - return output + b"".join(chunks) + return output + "".join(chunks) class Playlist(Record): def __init__(self, parent): self.listtracks = [] Record.__init__(self, parent) self._struct = collections.OrderedDict([ - ("header_id", ("4s", b"lphs")), # shpl + ("header_id", ("4s", "shpl")), ("total_length", ("I", 0)), ("number_of_songs", ("I", 0)), ("number_of_nonaudio", ("I", 0)), - ("dbid", ("8s", b"\x00" * 8)), + ("dbid", ("8s", "\x00" * 8)), ("listtype", ("I", 2)), - ("unknown1", ("16s", b"\x00" * 16)) + ("unknown1", ("16s", "\x00" * 16)) ]) 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.playlist_voiceover and (Text2Speech.valid_tts['pico2wave'] or Text2Speech.valid_tts['espeak'] or Text2Speech.valid_tts['say']): - self["dbid"] = hashlib.md5(b"masterlist").digest()[:8] + 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 self.listtracks = tracks @@ -502,7 +480,7 @@ class Playlist(Record): dataarr = i.strip().split("=", 1) if dataarr[0].lower().startswith("file"): num = int(dataarr[0][4:]) - filename = urllib.parse.unquote(dataarr[1]).strip() + filename = urllib.unquote(dataarr[1]).strip() if filename.lower().startswith('file://'): filename = filename[7:] if self.rename: @@ -550,7 +528,7 @@ class Playlist(Record): text = os.path.splitext(os.path.basename(filename))[0] else: # Read the playlist file - with open(filename, 'r', errors="replace") as f: + with open(filename, 'rb') as f: data = f.readlines() extension = os.path.splitext(filename)[1].lower() @@ -567,14 +545,16 @@ class Playlist(Record): text = os.path.splitext(os.path.basename(filename))[0] # Handle the VoiceOverData - self["dbid"] = hashlib.md5(text.encode('utf-8')).digest()[:8] + 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) - def construct(self, tracks): + def construct(self, tracks): #pylint: disable-msg=W0221 self["total_length"] = 44 + (4 * len(self.listtracks)) self["number_of_songs"] = 0 - chunks = bytes() + chunks = "" for i in self.listtracks: path = self.ipod_to_path(i) position = -1 @@ -583,8 +563,8 @@ class Playlist(Record): except: # Print an error if no track was found. # 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.") + print "Error: Could not find track \"" + path + "\"." + print "Maybe its an invalid FAT filesystem name. Please fix your playlist. Skipping track." if position > -1: chunks += struct.pack("I", position) self["number_of_songs"] += 1 @@ -616,11 +596,11 @@ class Shuffler(object): make_dir_if_absent(os.path.join(self.path, dirname)) def dump_state(self): - print("Shuffle DB state") - print("Tracks", self.tracks) - print("Albums", self.albums) - print("Artists", self.artists) - print("Playlists", self.lists) + print "Shuffle DB state" + print "Tracks", self.tracks + print "Albums", self.albums + print "Artists", self.artists + print "Playlists", self.lists def populate(self): self.tunessd = TunesSD(self) @@ -647,28 +627,22 @@ class Shuffler(object): self.lists.append(os.path.abspath(dirpath)) if self.auto_id3_playlists != None: - if mutagen: - for grouped_list in group_tracks_by_id3_template(self.tracks, self.auto_id3_playlists): - self.lists.append(grouped_list) - else: - print("Error: No mutagen found. Cannot generate auto-id3-playlists.") - sys.exit(1) + for grouped_list in group_tracks_by_id3_template(self.tracks, self.auto_id3_playlists): + self.lists.append(grouped_list) def write_database(self): - print("Writing database. This may take a while...") with open(os.path.join(self.path, "iPod_Control", "iTunes", "iTunesSD"), "wb") as f: 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.") + 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)) + 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 @@ -687,7 +661,7 @@ def check_unicode(path): if raises_unicode_error(item): src = os.path.join(path, item) dest = os.path.join(path, hash_error_unicode(item)) + os.path.splitext(item)[1].lower() - print('Renaming %s -> %s' % (src, dest)) + print 'Renaming %s -> %s' % (src, dest) os.rename(src, dest) else: ret_flag = (check_unicode(os.path.join(path, item)) or ret_flag) @@ -695,7 +669,7 @@ def check_unicode(path): src = os.path.join(path, item) new_name = hash_error_unicode(item) dest = os.path.join(path, new_name) - print('Renaming %s -> %s' % (src, dest)) + print 'Renaming %s -> %s' % (src, dest) os.rename(src, dest) return ret_flag @@ -711,15 +685,15 @@ def nonnegative_int(string): def checkPathValidity(path): if not os.path.isdir(result.path): - print("Error finding IPod directory. Maybe it is not connected or mounted?") + print "Error finding IPod directory. Maybe it is not connected or mounted?" sys.exit(1) if not os.access(result.path, os.W_OK): - print('Unable to get write permissions in the IPod directory') + print 'Unable to get write permissions in the IPod directory' sys.exit(1) def handle_interrupt(signal, frame): - print("Interrupt detected, exiting...") + print "Interrupt detected, exiting..." sys.exit(1) if __name__ == '__main__': @@ -727,7 +701,7 @@ if __name__ == '__main__': parser = argparse.ArgumentParser(description= 'Python script for building the Track and Playlist database ' - 'for the newer gen IPod Shuffle. Version 1.5') + 'for the newer gen IPod Shuffle. Version 1.4') parser.add_argument('-t', '--track-voiceover', action='store_true', help='Enable track voiceover feature') @@ -764,33 +738,34 @@ if __name__ == '__main__': result = parser.parse_args() # Enable verbose printing if desired - verboseprint = print if result.verbose else lambda *a, **k: None + # 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 not mutagen: - print("Warning: No mutagen found. Database will not contain any album nor artist information.") - 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.") + 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 = 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()