Compare commits

...

41 commits
v1.3 ... master

Author SHA1 Message Date
Nimesh Ghelani
a97a99ab86
Merge pull request #55 from fracai/mac_support
Mac support
2021-10-02 21:15:45 +01:00
NicoHood
9fea89c787
Merge pull request #53 from NicoHood/changelog
Moved Changelog to a separate file with KeepAChangelog format
2021-09-01 19:15:38 +02:00
Arno Hautala
4a2031a821 mentions macOS say in readme 2021-08-29 10:13:24 -04:00
Arno Hautala
37ba988b73 clarify macOS say provenance 2021-08-28 10:13:03 -04:00
Arno Hautala
6186600e4c use double dash (--) to indicate start of voiceover text parameter 2021-08-25 17:05:11 -04:00
Arno Hautala
8863026157 support for using "say" to generate voiceover files 2021-08-24 22:46:05 -04:00
NicoHood
4fd26bf51c
Moved Changelog to a separate file with KeepAChangelog format 2021-06-05 08:51:57 +02:00
Nimesh Ghelani
589a0cd251
Merge pull request #50 from harjitmoe/master
Fix TypeError when reading playlist files.
2021-06-04 23:12:26 +01:00
HarJIT
64414b6158 Fix TypeError when reading playlist files. 2021-03-13 13:03:19 +00:00
Nimesh Ghelani
410bd863a6
Merge pull request #34 from NicoHood/version15
Release 1.5
2020-06-09 22:49:16 +01:00
NicoHood
ddf4f5be77
Apply fixed suggested by @nims11 review 2020-06-09 23:21:38 +02:00
NicoHood
0529799495
Update Readme for 1.5 2020-06-09 23:20:56 +02:00
Nimesh Ghelani
9b720139ec Merge pull request #42 from nims11/NicoHood-patch-1
Add #41 to Tips and Tricks Readme section
2017-03-03 01:21:36 -05:00
Nico
84a3dbf6e6 Better mkfs command 2017-02-07 22:11:25 +01:00
Nico
fa6e3f15d7 Add #41 to Tips and Tricks Readme section 2017-02-07 22:08:14 +01:00
NicoHood
780268a0b9 Merge remote-tracking branch 'upstream/master' into version15 2016-08-28 00:36:12 +02:00
Nimesh Ghelani
f8838a9fc8 Merge branch 'NicoHood-version14' 2016-08-28 03:53:21 +05:30
Nimesh Ghelani
78ecedd8d5 utf encode playlist names, fixes #35 2016-08-28 03:47:57 +05:30
NicoHood
acf53b5c8f Updated readme instructions 2016-08-27 21:17:37 +02:00
NicoHood
8bfea0be3f removed pylint comments 2016-08-27 21:16:14 +02:00
NicoHood
c22837ba40 Some style cleanup 2016-08-27 21:03:25 +02:00
NicoHood
3ff22a4950 Made mutagen import optional 2016-08-27 21:02:59 +02:00
NicoHood
a21b0a2fe1 Fixed 2to3 conversion errors 2016-08-27 18:08:16 +02:00
NicoHood
bfb4d11027 2to3 autoconvert 2016-08-27 17:42:01 +02:00
NicoHood
406050c382 Inlined inverting of header bytes 2016-08-27 15:34:20 +02:00
NicoHood
e5322ce9f8 Updated readme 2016-08-27 14:57:26 +02:00
NicoHood
938405eb29 Added shortcut parameters (-p, -t, -d, etc.) 2016-08-27 14:49:15 +02:00
NicoHood
30c68e9b41 Renamed script from shuffle.py to ipod-shuffle-4g.py 2016-08-27 14:41:06 +02:00
NicoHood
1b023af3fb Do not force playlist voiceover with auto playlists 2016-08-27 14:39:56 +02:00
NicoHood
08684825b0 Added verbose debug output 2016-08-27 14:39:25 +02:00
NicoHood
957912e64d Added files in extras folder 2016-08-27 14:21:27 +02:00
NicoHood
1d7c3e0ab0 typo 2016-08-27 14:18:31 +02:00
NicoHood
d8eb3871bf Ignore hidden filenames 2016-08-27 14:18:15 +02:00
NicoHood
e189477870 Renamed --voiceover to --track--voiceover 2016-08-27 13:19:21 +02:00
NicoHood
7dbc15b91b Version bump to 1.4 2016-08-27 13:18:51 +02:00
NicoHood
c0c676c05d Catch 'no space left' error #30 2016-08-27 13:16:45 +02:00
Nimesh Ghelani
48909db089 Add Tips for conversion/compression, resolves #11 2016-08-21 02:43:41 +05:30
Nimesh Ghelani
80781517b3 Merge pull request #31 from NicoHood/patch-4
Add AUR repo
2016-08-16 18:49:50 +05:30
Nico
e1bc1c9803 Added AUR package link 2016-07-02 22:00:40 +02:00
Nico
1f28bbd2a5 Update README.md 2016-06-29 21:27:26 +02:00
Nimesh Ghelani
dfbc65ca59 Escape quotes 2016-06-08 05:21:00 +05:30
5 changed files with 334 additions and 180 deletions

111
CHANGELOG.md Normal file
View file

@ -0,0 +1,111 @@
# 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

106
README.md
View file

@ -1,41 +1,42 @@
# 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. 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/) 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 Just put your audio files into the mass storage of your IPod and shuffle.py will do the rest.
```bash ```
$ python shuffle.py -h $ ./ipod-shuffle-4g.py --help
usage: shuffle.py [-h] [--voiceover] [--playlist-voiceover] [--rename-unicode] usage: ipod-shuffle-4g.py [-h] [-t] [-p] [-u] [-g TRACK_GAIN]
[--track-gain TRACK_GAIN] [-d [AUTO_DIR_PLAYLISTS]] [-i [ID3_TEMPLATE]] [-v]
[--auto-dir-playlists [AUTO_DIR_PLAYLISTS]]
[--auto-id3-playlists [ID3_TEMPLATE]]
path path
Python script for building the Track and Playlist database for the newer gen Python script for building the Track and Playlist database for the newer gen
IPod Shuffle. Version 1.3 IPod Shuffle. Version 1.5
positional arguments: positional arguments:
path Path to the IPod's root directory path Path to the IPod's root directory
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
--voiceover Enable track voiceover feature -t, --track-voiceover
--playlist-voiceover Enable playlist voiceover feature Enable track voiceover feature
--rename-unicode Rename files causing unicode errors, will do minimal -p, --playlist-voiceover
Enable playlist voiceover feature
-u, --rename-unicode Rename files causing unicode errors, will do minimal
required renaming required renaming
--track-gain TRACK_GAIN -g TRACK_GAIN, --track-gain TRACK_GAIN
Specify volume gain (0-99) for all tracks; 0 (default) Specify volume gain (0-99) for all tracks; 0 (default)
means no gain and is usually fine; e.g. 60 is very means no gain and is usually fine; e.g. 60 is very
loud even on minimal player volume 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 Generate automatic playlists for each folder
recursively inside "IPod_Control/Music/". You can recursively inside "IPod_Control/Music/". You can
optionally limit the depth: 0=root, 1=artist, 2=album, optionally limit the depth: 0=root, 1=artist, 2=album,
n=subfoldername, default=-1 (No Limit). 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 Generate automatic playlists based on the id3 tags of
any music added to the iPod. You can optionally any music added to the iPod. You can optionally
specify a template string based on which id3 tags are specify a template string based on which id3 tags are
@ -44,44 +45,54 @@ optional arguments:
group tracks under one playlist. Similarly '{genre}' group tracks under one playlist. Similarly '{genre}'
will group tracks based on their genre tag. Default will group tracks based on their genre tag. Default
template used is '{artist}' template used is '{artist}'
-v, --verbose Show verbose output of database generation.
``` ```
#### Dependencies #### Dependencies
This script requires: This script requires:
* [Python 3](https://www.python.org/download/releases/3.0/)
* [Python 2.7](http://www.python.org/download/releases/2.7/) Optional album/artist and auto-id3-playlists support:
* [Mutagen](https://code.google.com/p/mutagen/) * [Mutagen](https://code.google.com/p/mutagen/)
Optional Voiceover support 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) * [RHVoice (master branch, 3e31edced402a08771d2c48c73213982cbe9333e)](https://github.com/Olga-Yakovleva/RHVoice) -- (Russian files only)
* [SoX](http://sox.sourceforge.net) -- (Russian files) * [SoX](http://sox.sourceforge.net) -- (Russian files)
* say (macOS)
##### Ubuntu ##### Ubuntu
`apt-get install python-mutagen libttspico*` `apt-get install python3 python-mutagen libttspico*`
##### Arch Linux ##### Arch Linux
From the **Extra** repository: `pacman -S python2 mutagen` and from the AUR: `svox-pico-bin` ([link](https://aur.archlinux.org/packages/svox-pico-bin/)) 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/))
You can also [install the script from AUR](https://aur.archlinux.org/packages/ipod-shuffle-4g/).
##### Gentoo Linux ##### Gentoo Linux
```bash ```bash
PYTHON_TARGETS="python2_7" emerge -av media-libs/mutagen PYTHON_TARGETS="python3" emerge -av media-libs/mutagen
layman --add=ikelos layman --add=ikelos
layman --overlays="https://raw.githubusercontent.com/ahippo/rhvoice-gentoo-overlay/master/repositories.xml" --fetch --add=ahippo-rhvoice-overlay 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 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) 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 #### Disable trash for IPod
To avoid that linux moves deleted files into trash you can create an empty file `.Trash-1000`. 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. 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. 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/).
#### Use Rhythmbox to manage your music and playlists #### Use Rhythmbox to manage your music and playlists
As described [in the blog post](https://nims11.wordpress.com/2013/10/12/ipod-shuffle-4g-under-linux/) As described [in the blog post](https://nims11.wordpress.com/2013/10/12/ipod-shuffle-4g-under-linux/)
@ -93,6 +104,7 @@ Simply place a file called `.is_audio_player` into the root directory of your IP
name="Name's IPOD" name="Name's IPOD"
audio_folders=iPod_Control/Music/ 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. 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. You can use Rythmbox now to generate playlists and sync them to your IPod.
@ -108,6 +120,17 @@ In all cases you can try to update Rythmbox to the latest version, sync again or
If you want to use this script on different computers it makes sense If you want to use this script on different computers it makes sense
to simply copy the script into the IPod's root directory. to simply copy the script into the IPod's root directory.
#### Format/Restore/Recover IPod
([#41](https://github.com/nims11/IPod-Shuffle-4g/issues/41)) If you formatted your IPod wrong and lost all data you can still recover it.
It is important to **not use MBR/GPT**. You need to directly create a **Fat16 Filesystem**:
```bash
sudo mkfs.vfat -I -F 16 -n IPOD /dev/sdX
```
Run this script to generate the new database. All missing sound files should be regenrated by the IPod on next use.
Your IPod should work and play music again now.
## TODO ## TODO
* Last.fm Scrobbler * Last.fm Scrobbler
* Qt frontend * Qt frontend
@ -121,42 +144,3 @@ to simply copy the script into the IPod's root directory.
The original shuffle3db website went offline. This repository contains a copy of the information inside the `docs` folder. 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). Original data can be found via [wayback machine](https://web.archive.org/web/20131016014401/http://shuffle3db.wikispaces.com/iTunesSD3gen).
# 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
* 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
```

0
extras/.Trash-1000 Normal file
View file

2
extras/.is_audio_player Normal file
View file

@ -0,0 +1,2 @@
name="iPod Shuffle"
audio_folders=iPod_Control/Music/

View file

@ -1,12 +1,11 @@
#!/usr/bin/env python2.7 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Builtin libraries
import sys import sys
import struct import struct
import urllib import urllib.request, urllib.parse, urllib.error
import os import os
import hashlib import hashlib
import mutagen
import binascii
import subprocess import subprocess
import collections import collections
import errno import errno
@ -16,6 +15,12 @@ import re
import tempfile import tempfile
import signal import signal
# External libraries
try:
import mutagen
except ImportError:
mutagen = None
audio_ext = (".mp3", ".m4a", ".m4b", ".m4p", ".aa", ".wav") audio_ext = (".mp3", ".m4a", ".m4b", ".m4p", ".aa", ".wav")
list_ext = (".pls", ".m3u") list_ext = (".pls", ".m3u")
def make_dir_if_absent(path): def make_dir_if_absent(path):
@ -27,19 +32,19 @@ def make_dir_if_absent(path):
def raises_unicode_error(str): def raises_unicode_error(str):
try: try:
str.decode('utf-8').encode('latin-1') str.encode('latin-1')
return False return False
except (UnicodeEncodeError, UnicodeDecodeError): except (UnicodeEncodeError, UnicodeDecodeError):
return True return True
def hash_error_unicode(item): def hash_error_unicode(item):
return "".join(["{0:02X}".format(ord(x)) for x in reversed(hashlib.md5(item).digest()[:8])]) item_bytes = item.encode('utf-8')
pass return "".join(["{0:02X}".format(ord(x)) for x in reversed(hashlib.md5(item_bytes).hexdigest()[:8])])
def validate_unicode(path): def validate_unicode(path):
path_list = path.split('/') path_list = path.split('/')
last_raise = False last_raise = False
for i in xrange(len(path_list)): for i in range(len(path_list)):
if raises_unicode_error(path_list[i]): if raises_unicode_error(path_list[i]):
path_list[i] = hash_error_unicode(path_list[i]) path_list[i] = hash_error_unicode(path_list[i])
last_raise = True last_raise = True
@ -61,11 +66,11 @@ def splitpath(path):
return path.split(os.sep) return path.split(os.sep)
def get_relpath(path, basepath): def get_relpath(path, basepath):
commonprefix = os.sep.join(os.path.commonprefix(map(splitpath, [path, basepath]))) commonprefix = os.sep.join(os.path.commonprefix(list(map(splitpath, [path, basepath]))))
return os.path.relpath(path, commonprefix) return os.path.relpath(path, commonprefix)
def is_path_prefix(prefix, path): def is_path_prefix(prefix, path):
return prefix == os.sep.join(os.path.commonprefix(map(splitpath, [prefix, path]))) return prefix == os.sep.join(os.path.commonprefix(list(map(splitpath, [prefix, path]))))
def group_tracks_by_id3_template(tracks, template): def group_tracks_by_id3_template(tracks, template):
grouped_tracks_dict = {} grouped_tracks_dict = {}
@ -92,30 +97,37 @@ def group_tracks_by_id3_template(tracks, template):
return sorted(grouped_tracks_dict.items()) return sorted(grouped_tracks_dict.items())
class Text2Speech(object): class Text2Speech(object):
valid_tts = {'pico2wave': True, 'RHVoice': True, 'espeak': True} valid_tts = {'pico2wave': True, 'RHVoice': True, 'espeak': True, 'say': True}
@staticmethod @staticmethod
def check_support(): def check_support():
voiceoverAvailable = False 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 # Check for pico2wave voiceover
if not exec_exists_in_path("pico2wave"): if not exec_exists_in_path("pico2wave"):
Text2Speech.valid_tts['pico2wave'] = False 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: else:
voiceoverAvailable = True voiceoverAvailable = True
# Check for espeak voiceover # Check for espeak voiceover
if not exec_exists_in_path("espeak"): if not exec_exists_in_path("espeak"):
Text2Speech.valid_tts['espeak'] = False 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: else:
voiceoverAvailable = True voiceoverAvailable = True
# Check for Russian RHVoice voiceover # Check for Russian RHVoice voiceover
if not exec_exists_in_path("RHVoice"): if not exec_exists_in_path("RHVoice"):
Text2Speech.valid_tts['RHVoice'] = False 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: else:
voiceoverAvailable = True voiceoverAvailable = True
@ -128,12 +140,12 @@ class Text2Speech(object):
# Skip voiceover generation if a track with the same name is used. # Skip voiceover generation if a track with the same name is used.
# This might happen with "Track001" or "01. Intro" names for example. # This might happen with "Track001" or "01. Intro" names for example.
if os.path.isfile(out_wav_path): if os.path.isfile(out_wav_path):
print "Using existing", out_wav_path verboseprint("Using existing", out_wav_path)
return True return True
# ensure we deal with unicode later # ensure we deal with unicode later
if not isinstance(text, unicode): if not isinstance(text, str):
text = unicode(text, 'utf-8') text = str(text, 'utf-8')
lang = Text2Speech.guess_lang(text) lang = Text2Speech.guess_lang(text)
if lang == "ru-RU": if lang == "ru-RU":
return Text2Speech.rhvoice(out_wav_path, text) return Text2Speech.rhvoice(out_wav_path, text)
@ -142,6 +154,8 @@ class Text2Speech(object):
return True return True
elif Text2Speech.espeak(out_wav_path, text): elif Text2Speech.espeak(out_wav_path, text):
return True return True
elif Text2Speech.say(out_wav_path, text):
return True
else: else:
return False return False
@ -149,7 +163,7 @@ class Text2Speech(object):
@staticmethod @staticmethod
def guess_lang(unicodetext): def guess_lang(unicodetext):
lang = 'en-GB' lang = 'en-GB'
if re.search(u"[А-Яа-я]", unicodetext) is not None: if re.search("[А-Яа-я]", unicodetext) is not None:
lang = 'ru-RU' lang = 'ru-RU'
return lang return lang
@ -157,14 +171,21 @@ class Text2Speech(object):
def pico2wave(out_wav_path, unicodetext): def pico2wave(out_wav_path, unicodetext):
if not Text2Speech.valid_tts['pico2wave']: if not Text2Speech.valid_tts['pico2wave']:
return False return False
subprocess.call(["pico2wave", "-l", "en-GB", "-w", out_wav_path, unicodetext]) 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])
return True return True
@staticmethod @staticmethod
def espeak(out_wav_path, unicodetext): def espeak(out_wav_path, unicodetext):
if not Text2Speech.valid_tts['espeak']: if not Text2Speech.valid_tts['espeak']:
return False 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 return True
@staticmethod @staticmethod
@ -190,13 +211,13 @@ class Record(object):
self.parent = parent self.parent = parent
self._struct = collections.OrderedDict([]) self._struct = collections.OrderedDict([])
self._fields = {} self._fields = {}
self.voiceover = parent.voiceover self.track_voiceover = parent.track_voiceover
self.playlist_voiceover = parent.playlist_voiceover self.playlist_voiceover = parent.playlist_voiceover
self.rename = parent.rename self.rename = parent.rename
self.trackgain = parent.trackgain self.trackgain = parent.trackgain
def __getitem__(self, item): def __getitem__(self, item):
if item not in self._struct.keys(): if item not in list(self._struct.keys()):
raise KeyError raise KeyError
return self._fields.get(item, self._struct[item][1]) return self._fields.get(item, self._struct[item][1])
@ -204,18 +225,16 @@ class Record(object):
self._fields[item] = value self._fields[item] = value
def construct(self): def construct(self):
output = "" output = bytes()
for i in self._struct.keys(): for i in list(self._struct.keys()):
(fmt, default) = self._struct[i] (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)) output += struct.pack("<" + fmt, self._fields.get(i, default))
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 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 # Create the voiceover wav file
fn = "".join(["{0:02X}".format(ord(x)) for x in reversed(dbid)]) fn = ''.join(format(x, '02x') 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")
return Text2Speech.text2speech(path, text) return Text2Speech.text2speech(path, text)
return False return False
@ -265,19 +284,19 @@ class TunesSD(Record):
self.track_header = TrackHeader(self) self.track_header = TrackHeader(self)
self.play_header = PlaylistHeader(self) self.play_header = PlaylistHeader(self)
self._struct = collections.OrderedDict([ self._struct = collections.OrderedDict([
("header_id", ("4s", "shdb")), ("header_id", ("4s", b"bdhs")), # shdb
("unknown1", ("I", 0x02000003)), ("unknown1", ("I", 0x02000003)),
("total_length", ("I", 64)), ("total_length", ("I", 64)),
("total_number_of_tracks", ("I", 0)), ("total_number_of_tracks", ("I", 0)),
("total_number_of_playlists", ("I", 0)), ("total_number_of_playlists", ("I", 0)),
("unknown2", ("Q", 0)), ("unknown2", ("Q", 0)),
("max_volume", ("B", 0)), ("max_volume", ("B", 0)),
("voiceover_enabled", ("B", int(self.voiceover))), ("voiceover_enabled", ("B", int(self.track_voiceover))),
("unknown3", ("H", 0)), ("unknown3", ("H", 0)),
("total_tracks_without_podcasts", ("I", 0)), ("total_tracks_without_podcasts", ("I", 0)),
("track_header_offset", ("I", 64)), ("track_header_offset", ("I", 64)),
("playlist_header_offset", ("I", 0)), ("playlist_header_offset", ("I", 0)),
("unknown4", ("20s", "\x00" * 20)), ("unknown4", ("20s", b"\x00" * 20)),
]) ])
def construct(self): def construct(self):
@ -302,7 +321,7 @@ class TrackHeader(Record):
self.base_offset = 0 self.base_offset = 0
Record.__init__(self, parent) Record.__init__(self, parent)
self._struct = collections.OrderedDict([ self._struct = collections.OrderedDict([
("header_id", ("4s", "shth")), ("header_id", ("4s", b"hths")), # shth
("total_length", ("I", 0)), ("total_length", ("I", 0)),
("number_of_tracks", ("I", 0)), ("number_of_tracks", ("I", 0)),
("unknown1", ("Q", 0)), ("unknown1", ("Q", 0)),
@ -314,10 +333,10 @@ class TrackHeader(Record):
output = Record.construct(self) output = Record.construct(self)
# Construct the underlying tracks # Construct the underlying tracks
track_chunk = "" track_chunk = bytes()
for i in self.tracks: for i in self.tracks:
track = Track(self) track = Track(self)
print "[*] Adding track", i verboseprint("[*] Adding track", i)
track.populate(i) track.populate(i)
output += struct.pack("I", self.base_offset + self["total_length"] + len(track_chunk)) output += struct.pack("I", self.base_offset + self["total_length"] + len(track_chunk))
track_chunk += track.construct() track_chunk += track.construct()
@ -328,13 +347,13 @@ class Track(Record):
def __init__(self, parent): def __init__(self, parent):
Record.__init__(self, parent) Record.__init__(self, parent)
self._struct = collections.OrderedDict([ self._struct = collections.OrderedDict([
("header_id", ("4s", "shtr")), ("header_id", ("4s", b"rths")), # shtr
("header_length", ("I", 0x174)), ("header_length", ("I", 0x174)),
("start_at_pos_ms", ("I", 0)), ("start_at_pos_ms", ("I", 0)),
("stop_at_pos_ms", ("I", 0)), ("stop_at_pos_ms", ("I", 0)),
("volume_gain", ("I", int(self.trackgain))), ("volume_gain", ("I", int(self.trackgain))),
("filetype", ("I", 1)), ("filetype", ("I", 1)),
("filename", ("256s", "\x00" * 256)), ("filename", ("256s", b"\x00" * 256)),
("bookmark", ("I", 0)), ("bookmark", ("I", 0)),
("dontskip", ("B", 1)), ("dontskip", ("B", 1)),
("remember", ("B", 0)), ("remember", ("B", 0)),
@ -352,33 +371,36 @@ class Track(Record):
("unknown4", ("Q", 0)), ("unknown4", ("Q", 0)),
("dbid", ("8s", 0)), ("dbid", ("8s", 0)),
("artistid", ("I", 0)), ("artistid", ("I", 0)),
("unknown5", ("32s", "\x00" * 32)), ("unknown5", ("32s", b"\x00" * 32)),
]) ])
def populate(self, filename): def populate(self, filename):
self["filename"] = self.path_to_ipod(filename) self["filename"] = self.path_to_ipod(filename).encode('utf-8')
if os.path.splitext(filename)[1].lower() in (".m4a", ".m4b", ".m4p", ".aa"): if os.path.splitext(filename)[1].lower() in (".m4a", ".m4b", ".m4p", ".aa"):
self["filetype"] = 2 self["filetype"] = 2
text = os.path.splitext(os.path.basename(filename))[0] text = os.path.splitext(os.path.basename(filename))[0]
# Try to get album and artist information with mutagen
if mutagen:
audio = None audio = None
try: try:
audio = mutagen.File(filename, easy = True) audio = mutagen.File(filename, easy = True)
except: except:
print "Error calling mutagen. Possible invalid filename/ID3Tags (hyphen in filename?)" 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)
artist = audio.get("artist", [u"Unknown"])[0] artist = audio.get("artist", ["Unknown"])[0]
if artist in self.artists: if artist in self.artists:
self["artistid"] = self.artists.index(artist) self["artistid"] = self.artists.index(artist)
else: else:
self["artistid"] = len(self.artists) self["artistid"] = len(self.artists)
self.artists.append(artist) self.artists.append(artist)
album = audio.get("album", [u"Unknown"])[0] album = audio.get("album", ["Unknown"])[0]
if album in self.albums: if album in self.albums:
self["albumid"] = self.albums.index(album) self["albumid"] = self.albums.index(album)
else: else:
@ -386,12 +408,12 @@ class Track(Record):
self.albums.append(album) self.albums.append(album)
if audio.get("title", "") and audio.get("artist", ""): if audio.get("title", "") and audio.get("artist", ""):
text = u" - ".join(audio.get("title", u"") + audio.get("artist", u"")) text = " - ".join(audio.get("title", "") + audio.get("artist", ""))
# Handle the VoiceOverData # Handle the VoiceOverData
if isinstance(text, unicode): if isinstance(text, str):
text = text.encode('utf-8', 'ignore') text = text.encode('utf-8', 'ignore')
self["dbid"] = hashlib.md5(text).digest()[:8] #pylint: disable-msg=E1101 self["dbid"] = hashlib.md5(text).digest()[:8]
self.text_to_speech(text, self["dbid"]) self.text_to_speech(text, self["dbid"])
class PlaylistHeader(Record): class PlaylistHeader(Record):
@ -399,19 +421,19 @@ class PlaylistHeader(Record):
self.base_offset = 0 self.base_offset = 0
Record.__init__(self, parent) Record.__init__(self, parent)
self._struct = collections.OrderedDict([ self._struct = collections.OrderedDict([
("header_id", ("4s", "shph")), ("header_id", ("4s", b"hphs")), #shph
("total_length", ("I", 0)), ("total_length", ("I", 0)),
("number_of_playlists", ("I", 0)), ("number_of_playlists", ("I", 0)),
("number_of_non_podcast_lists", ("2s", "\xFF\xFF")), ("number_of_non_podcast_lists", ("2s", b"\xFF\xFF")),
("number_of_master_lists", ("2s", "\x01\x00")), ("number_of_master_lists", ("2s", b"\x01\x00")),
("number_of_non_audiobook_lists", ("2s", "\xFF\xFF")), ("number_of_non_audiobook_lists", ("2s", b"\xFF\xFF")),
("unknown2", ("2s", "\x00" * 2)), ("unknown2", ("2s", b"\x00" * 2)),
]) ])
def construct(self, tracks): #pylint: disable-msg=W0221 def construct(self, tracks):
# Build the master list # Build the master list
masterlist = Playlist(self) masterlist = Playlist(self)
print "[+] Adding master playlist" verboseprint("[+] Adding master playlist")
masterlist.set_master(tracks) masterlist.set_master(tracks)
chunks = [masterlist.construct(tracks)] chunks = [masterlist.construct(tracks)]
@ -419,14 +441,14 @@ class PlaylistHeader(Record):
playlistcount = 1 playlistcount = 1
for i in self.lists: for i in self.lists:
playlist = Playlist(self) 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) playlist.populate(i)
construction = playlist.construct(tracks) construction = playlist.construct(tracks)
if playlist["number_of_songs"] > 0: if playlist["number_of_songs"] > 0:
playlistcount += 1 playlistcount += 1
chunks += [construction] chunks += [construction]
else: 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["number_of_playlists"] = playlistcount
self["total_length"] = 0x14 + (self["number_of_playlists"] * 4) self["total_length"] = 0x14 + (self["number_of_playlists"] * 4)
@ -439,27 +461,27 @@ class PlaylistHeader(Record):
output += struct.pack("I", offset) output += struct.pack("I", offset)
offset += len(chunks[i]) offset += len(chunks[i])
return output + "".join(chunks) return output + b"".join(chunks)
class Playlist(Record): class Playlist(Record):
def __init__(self, parent): def __init__(self, parent):
self.listtracks = [] self.listtracks = []
Record.__init__(self, parent) Record.__init__(self, parent)
self._struct = collections.OrderedDict([ self._struct = collections.OrderedDict([
("header_id", ("4s", "shpl")), ("header_id", ("4s", b"lphs")), # shpl
("total_length", ("I", 0)), ("total_length", ("I", 0)),
("number_of_songs", ("I", 0)), ("number_of_songs", ("I", 0)),
("number_of_nonaudio", ("I", 0)), ("number_of_nonaudio", ("I", 0)),
("dbid", ("8s", "\x00" * 8)), ("dbid", ("8s", b"\x00" * 8)),
("listtype", ("I", 2)), ("listtype", ("I", 2)),
("unknown1", ("16s", "\x00" * 16)) ("unknown1", ("16s", b"\x00" * 16))
]) ])
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.playlist_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'] or Text2Speech.valid_tts['say']):
self["dbid"] = hashlib.md5("masterlist").digest()[:8] #pylint: disable-msg=E1101 self["dbid"] = hashlib.md5(b"masterlist").digest()[:8]
self.text_to_speech("All songs", self["dbid"], True) self.text_to_speech("All songs", self["dbid"], True)
self["listtype"] = 1 self["listtype"] = 1
self.listtracks = tracks self.listtracks = tracks
@ -480,7 +502,7 @@ class Playlist(Record):
dataarr = i.strip().split("=", 1) dataarr = i.strip().split("=", 1)
if dataarr[0].lower().startswith("file"): if dataarr[0].lower().startswith("file"):
num = int(dataarr[0][4:]) num = int(dataarr[0][4:])
filename = urllib.unquote(dataarr[1]).strip() filename = urllib.parse.unquote(dataarr[1]).strip()
if filename.lower().startswith('file://'): if filename.lower().startswith('file://'):
filename = filename[7:] filename = filename[7:]
if self.rename: if self.rename:
@ -528,7 +550,7 @@ class Playlist(Record):
text = os.path.splitext(os.path.basename(filename))[0] text = os.path.splitext(os.path.basename(filename))[0]
else: else:
# Read the playlist file # Read the playlist file
with open(filename, 'rb') as f: with open(filename, 'r', errors="replace") as f:
data = f.readlines() data = f.readlines()
extension = os.path.splitext(filename)[1].lower() extension = os.path.splitext(filename)[1].lower()
@ -545,14 +567,14 @@ class Playlist(Record):
text = os.path.splitext(os.path.basename(filename))[0] text = os.path.splitext(os.path.basename(filename))[0]
# Handle the VoiceOverData # Handle the VoiceOverData
self["dbid"] = hashlib.md5(text).digest()[:8] #pylint: disable-msg=E1101 self["dbid"] = hashlib.md5(text.encode('utf-8')).digest()[:8]
self.text_to_speech(text, self["dbid"], True) self.text_to_speech(text, self["dbid"], True)
def construct(self, tracks): #pylint: disable-msg=W0221 def construct(self, tracks):
self["total_length"] = 44 + (4 * len(self.listtracks)) self["total_length"] = 44 + (4 * len(self.listtracks))
self["number_of_songs"] = 0 self["number_of_songs"] = 0
chunks = "" chunks = bytes()
for i in self.listtracks: for i in self.listtracks:
path = self.ipod_to_path(i) path = self.ipod_to_path(i)
position = -1 position = -1
@ -561,8 +583,8 @@ class Playlist(Record):
except: except:
# Print an error if no track was found. # Print an error if no track was found.
# Empty playlists are handeled in the PlaylistHeader class. # Empty playlists are handeled in the PlaylistHeader class.
print "Error: Could not find track \"" + path + "\"." print("Error: Could not find track \"" + path + "\".")
print "Maybe its an invalid FAT filesystem name. Please fix your playlist. Skipping track." print("Maybe its an invalid FAT filesystem name. Please fix your playlist. Skipping track.")
if position > -1: if position > -1:
chunks += struct.pack("I", position) chunks += struct.pack("I", position)
self["number_of_songs"] += 1 self["number_of_songs"] += 1
@ -572,14 +594,14 @@ class Playlist(Record):
return output + chunks return output + chunks
class Shuffler(object): 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.path = os.path.abspath(path)
self.tracks = [] self.tracks = []
self.albums = [] self.albums = []
self.artists = [] self.artists = []
self.lists = [] self.lists = []
self.tunessd = None self.tunessd = None
self.voiceover = voiceover self.track_voiceover = track_voiceover
self.playlist_voiceover = playlist_voiceover self.playlist_voiceover = playlist_voiceover
self.rename = rename self.rename = rename
self.trackgain = trackgain self.trackgain = trackgain
@ -594,11 +616,11 @@ class Shuffler(object):
make_dir_if_absent(os.path.join(self.path, dirname)) make_dir_if_absent(os.path.join(self.path, dirname))
def dump_state(self): def dump_state(self):
print "Shuffle DB state" print("Shuffle DB state")
print "Tracks", self.tracks print("Tracks", self.tracks)
print "Albums", self.albums print("Albums", self.albums)
print "Artists", self.artists print("Artists", self.artists)
print "Playlists", self.lists print("Playlists", self.lists)
def populate(self): def populate(self):
self.tunessd = TunesSD(self) self.tunessd = TunesSD(self)
@ -608,6 +630,8 @@ class Shuffler(object):
# Ignore the speakable directory and any hidden directories # Ignore the speakable directory and any hidden directories
if not is_path_prefix("iPod_Control/Speakable", relpath) 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()): for filename in sorted(filenames, key = lambda x: x.lower()):
# Ignore hidden files
if not filename.startswith("."):
fullPath = os.path.abspath(os.path.join(dirpath, filename)) fullPath = os.path.abspath(os.path.join(dirpath, filename))
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)
@ -623,12 +647,28 @@ class Shuffler(object):
self.lists.append(os.path.abspath(dirpath)) self.lists.append(os.path.abspath(dirpath))
if self.auto_id3_playlists != None: if self.auto_id3_playlists != None:
if mutagen:
for grouped_list in group_tracks_by_id3_template(self.tracks, self.auto_id3_playlists): for grouped_list in group_tracks_by_id3_template(self.tracks, self.auto_id3_playlists):
self.lists.append(grouped_list) self.lists.append(grouped_list)
else:
print("Error: No mutagen found. Cannot generate auto-id3-playlists.")
sys.exit(1)
def write_database(self): 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: with open(os.path.join(self.path, "iPod_Control", "iTunes", "iTunesSD"), "wb") as f:
try:
f.write(self.tunessd.construct()) f.write(self.tunessd.construct())
except IOError as e:
print("I/O error({0}): {1}".format(e.errno, e.strerror))
print("Error: Writing iPod database failed.")
sys.exit(1)
print("Database written successfully:")
print("Tracks", len(self.tracks))
print("Albums", len(self.albums))
print("Artists", len(self.artists))
print("Playlists", len(self.lists))
# #
# Read all files from the directory # Read all files from the directory
@ -647,7 +687,7 @@ def check_unicode(path):
if raises_unicode_error(item): if raises_unicode_error(item):
src = os.path.join(path, item) src = os.path.join(path, item)
dest = os.path.join(path, hash_error_unicode(item)) + os.path.splitext(item)[1].lower() 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) os.rename(src, dest)
else: else:
ret_flag = (check_unicode(os.path.join(path, item)) or ret_flag) ret_flag = (check_unicode(os.path.join(path, item)) or ret_flag)
@ -655,7 +695,7 @@ def check_unicode(path):
src = os.path.join(path, item) src = os.path.join(path, item)
new_name = hash_error_unicode(item) new_name = hash_error_unicode(item)
dest = os.path.join(path, new_name) dest = os.path.join(path, new_name)
print 'Renaming %s -> %s' % (src, dest) print('Renaming %s -> %s' % (src, dest))
os.rename(src, dest) os.rename(src, dest)
return ret_flag return ret_flag
@ -671,15 +711,15 @@ def nonnegative_int(string):
def checkPathValidity(path): def checkPathValidity(path):
if not os.path.isdir(result.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) sys.exit(1)
if not os.access(result.path, os.W_OK): 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) sys.exit(1)
def handle_interrupt(signal, frame): def handle_interrupt(signal, frame):
print "Interrupt detected, exiting..." print("Interrupt detected, exiting...")
sys.exit(1) sys.exit(1)
if __name__ == '__main__': if __name__ == '__main__':
@ -687,28 +727,28 @@ if __name__ == '__main__':
parser = argparse.ArgumentParser(description= parser = argparse.ArgumentParser(description=
'Python script for building the Track and Playlist database ' '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.5')
parser.add_argument('--voiceover', action='store_true', parser.add_argument('-t', '--track-voiceover', action='store_true',
help='Enable track voiceover feature') 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') 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') 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; ' help='Specify volume gain (0-99) for all tracks; '
'0 (default) means no gain and is usually fine; ' '0 (default) means no gain and is usually fine; '
'e.g. 60 is very loud even on minimal player volume') '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 ' help='Generate automatic playlists for each folder recursively inside '
'"IPod_Control/Music/". You can optionally limit the depth: ' '"IPod_Control/Music/". You can optionally limit the depth: '
'0=root, 1=artist, 2=album, n=subfoldername, default=-1 (No Limit).') '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 ' help='Generate automatic playlists based on the id3 tags of any music '
'added to the iPod. You can optionally specify a template string ' 'added to the iPod. You can optionally specify a template string '
'based on which id3 tags are used to generate playlists. For eg. ' 'based on which id3 tags are used to generate playlists. For eg. '
@ -716,24 +756,41 @@ if __name__ == '__main__':
'tracks under one playlist. Similarly \'{genre}\' will group tracks based ' 'tracks under one playlist. Similarly \'{genre}\' will group tracks based '
'on their genre tag. Default template used is \'{artist}\'') 'on their genre tag. Default template used is \'{artist}\'')
parser.add_argument('-v', '--verbose', action='store_true',
help='Show verbose output of database generation.')
parser.add_argument('path', help='Path to the IPod\'s root directory') parser.add_argument('path', help='Path to the IPod\'s root directory')
result = parser.parse_args() result = parser.parse_args()
# Enable verbose printing if desired
verboseprint = print if result.verbose else lambda *a, **k: None
checkPathValidity(result.path) checkPathValidity(result.path)
if result.rename_unicode: if result.rename_unicode:
check_unicode(result.path) check_unicode(result.path)
if result.auto_id3_playlists != None or result.auto_dir_playlists != None: if not mutagen:
result.playlist_voiceover = True print("Warning: No mutagen found. Database will not contain any album nor artist information.")
if (result.voiceover or result.playlist_voiceover) and not Text2Speech.check_support(): verboseprint("Playlist voiceover requested:", result.playlist_voiceover)
print "Error: Did not find any voiceover program. Voiceover disabled." verboseprint("Track voiceover requested:", result.track_voiceover)
result.voiceover = False 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 result.playlist_voiceover = False
else:
verboseprint("Voiceover available.")
shuffle = Shuffler(result.path, voiceover=result.voiceover, playlist_voiceover=result.playlist_voiceover, rename=result.rename_unicode, trackgain=result.track_gain, auto_dir_playlists=result.auto_dir_playlists, auto_id3_playlists=result.auto_id3_playlists) shuffle = Shuffler(result.path,
track_voiceover=result.track_voiceover,
playlist_voiceover=result.playlist_voiceover,
rename=result.rename_unicode,
trackgain=result.track_gain,
auto_dir_playlists=result.auto_dir_playlists,
auto_id3_playlists=result.auto_id3_playlists)
shuffle.initialize() shuffle.initialize()
shuffle.populate() shuffle.populate()
shuffle.write_database() shuffle.write_database()