1
# -*- coding: utf-8 -*-
2
# Copyright 2006 Markus Koller
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License version 2 as
6
# published by the Free Software Foundation
8
# $Id: ipod.py 4047 2007-04-30 03:49:58Z piman $
19
from devices._base import Device
20
from formats._audio import AudioFile
22
# Wraps an itdb_track from libgpod in an AudioFile instance
23
class IPodSong(AudioFile):
26
def __init__(self, track):
27
super(IPodSong, self).__init__()
28
self.sanitize(gpod.itdb_filename_on_ipod(track))
30
for key in ['artist', 'album', 'title', 'genre', 'grouping']:
31
value = getattr(track, key)
33
try: self[key] = unicode(value)
34
except UnicodeDecodeError:
35
self[key] = unicode(value, errors='replace')
36
for key in ['bitrate', 'playcount']:
37
value = getattr(track, key)
39
self['~#'+key] = value
41
try: self["date"] = unicode(track.year)
42
except AttributeError: pass
45
self["discnumber"] = u"%d/%d" % (track.cd_nr, track.cds)
47
self["discnumber"] = u"%d" % track.cd_nr
50
self['tracknumber'] = u"%d/%d" % (track.track_nr, track.tracks)
52
self['tracknumber'] = u"%d" % track.track_nr
55
'~#rating': min(1.0, track.rating / 100.0),
56
'~#length': track.tracklen / 1000.0,
60
self['~format'] = u"iPod: %s" % track.filetype
62
# Disable all tag editing
63
def can_change(self, k=None):
66
class IPodDevice(Device):
76
'title_version': False,
86
gain = gtk.SpinButton()
87
gain.set_range(-20, 20)
89
gain.set_increments(0.1, 1)
90
gain.set_value(float(self['gain']))
91
props.append((_("_Volume Gain (dB):"), gain, 'gain'))
94
['covers', _("Copy _album covers")],
95
['all_tags', _("Combine tags with _multiple values")],
96
['title_version', _("Title includes _version")],
97
['album_part', _("Album includes _part")],
99
check = gtk.CheckButton()
100
check.set_active(self[key])
101
props.append((label, check, key))
103
if self.is_connected():
104
details = self.__get_details()
106
props.append((None, None, None))
107
if 'model' in details:
108
props.append((_("Model:"), details['model'], None))
109
if 'space' in details:
110
props.append((_("Capacity:"), details['space'], None))
111
if 'firmware' in details:
112
props.append((_("Firmware:"), details['firmware'], None))
116
def __get_details(self):
118
sysinfo = os.path.join(self.mountpoint,
119
'iPod_Control', 'Device', 'SysInfo')
121
if os.path.isfile(sysinfo):
124
line = file.readline()
127
if len(parts) < 2: continue
129
parts[0] = parts[0].rstrip(":")
130
if parts[0] == "ModelNumStr" and parts[1] in self.__models:
131
d['model'], d['space'] = self.__models[parts[1]]
132
elif parts[0] == "visibleBuildID":
133
d['firmware'] = parts[2].strip("()")
136
# Assume an iPod shuffle
137
info = os.statvfs(self.mountpoint)
138
space = info.f_bsize * info.f_blocks
139
if space > 512 * 1024 * 1024:
143
if model in self.__models:
144
d['model'], d['space'] = self.__models[model]
152
for track in gpod.sw_get_tracks(self.__itdb):
153
filename = gpod.itdb_filename_on_ipod(track)
155
songs.append(IPodSong(track))
156
else: # Remove orphaned iTunesDB track
158
print _("W: removing orphaned iPod track")
159
self.__remove_track(track)
165
def copy(self, songlist, song):
167
track = gpod.itdb_track_new()
169
# Either combine tags with comma, or only take the first value
170
if self['all_tags']: tag = song.comma
171
else: tag = lambda key: song.list(key)[0]
178
if self['title_version'] and song('version'):
179
title = " - ".join([title, song('version')])
180
track.title = str(title)
187
if self['album_part'] and song('part'):
188
album = " - ".join([album, song('part')])
189
track.album = str(album)
192
for key in ['artist', 'genre', 'grouping']:
193
try: setattr(track, key, str(tag(key)))
194
except IndexError: continue
196
for key in ['bitrate', 'playcount', 'year']:
197
try: setattr(track, key, int(song('~#'+key)))
198
except ValueError: continue
199
# Numeric keys where the names differ
201
'cd_nr': song('~#disc'),
202
'cds': song('~#discs'),
203
'rating': min(100, song('~#rating') * 100),
204
'time_added': self.__mactime(time.time()),
205
'time_modified': self.__mactime(util.mtime(song('~filename'))),
206
'track_nr': song('~#track'),
207
'tracklen': song('~#length') * 1000,
208
'tracks': song('~#tracks'),
209
'size': util.size(song('~filename')),
210
'soundcheck': self.__soundcheck(song),
212
try: setattr(track, key, int(value))
213
except ValueError: continue
215
track.filetype = song('~format')
216
track.comment = song('~filename')
218
# Associate a cover with the track
220
cover = song.find_cover()
222
# libgpod will copy the file later when the iTunesDB
223
# is saved, so we have to keep a reference around in
224
# case the cover is a temporary file.
225
self.__covers.append(cover)
226
gpod.itdb_track_set_thumbnails(track, cover.name)
228
# Add the track to the master playlist
229
gpod.itdb_track_add(self.__itdb, track, -1)
230
master = gpod.itdb_playlist_mpl(self.__itdb)
231
gpod.itdb_playlist_add_track(master, track, -1)
233
# Copy the actual file
234
if gpod.itdb_cp_track_to_ipod(track, song['~filename'], None) == 1:
235
return IPodSong(track)
239
def delete(self, songlist, song):
242
for track in gpod.sw_get_tracks(self.__itdb):
243
if gpod.itdb_filename_on_ipod(track) == song['~filename']:
244
os.remove(song['~filename'])
245
self.__remove_track(track)
250
return str(exc).decode(const.ENCODING, 'replace')
252
def cleanup(self, wlb, action):
254
wlb.set_text("<b>Saving iPod database...</b>")
255
if not self.__save_db():
256
wlb.set_text(_("Unable to save iPod database"))
264
if self.__itdb: return self.__itdb
266
self.__itdb = gpod.itdb_parse(self.mountpoint, None)
267
if not self.__itdb and self.is_connected() and qltk.ConfirmAction(
268
None, _("Uninitialized iPod"),
269
_("Do you want to create an empty database on this iPod?")
271
self.__itdb = self.__create_db()
276
if gpod.itdb_write(self.__itdb, None) == 1 and \
277
gpod.itdb_shuffle_write(self.__itdb, None) == 1:
282
def __create_db(self):
283
db = gpod.itdb_new();
284
gpod.itdb_set_mountpoint(db, self.mountpoint)
286
master = gpod.itdb_playlist_new('iPod', False)
287
gpod.itdb_playlist_set_mpl(master)
288
gpod.itdb_playlist_add(db, master, 0)
292
def __close_db(self):
293
if self.__itdb: gpod.itdb_free(self.__itdb)
296
def __remove_track(self, track):
297
master = gpod.itdb_playlist_mpl(self.__itdb)
298
gpod.itdb_playlist_remove_track(master, track)
299
gpod.itdb_track_remove(track)
301
def __mactime(self, time):
303
if time == 0: return time
304
else: return time + 2082844800
306
# Convert ReplayGain values to Apple Soundcheck values
307
def __soundcheck(self, song):
308
if 'replaygain_album_gain' in song:
309
db = float(song['replaygain_album_gain'].split()[0])
310
elif 'replaygain_track_gain' in song:
311
db = float(song['replaygain_track_gain'].split()[0])
314
soundcheck = int(round(1000 * 10.**(
315
-0.1 * (db + float(self['gain'])))))
318
# This list is taken from
319
# http://en.wikipedia.org/wiki/List_of_iPod_model_numbers
322
'M8513': ('iPod', '5GB'),
323
'M8541': ('iPod', '5GB'),
324
'M8697': ('iPod', '5GB'),
325
'M8709': ('iPod', '10GB'),
327
'M8737': ('iPod', '10GB'),
328
'M8740': ('iPod', '10GB'),
329
'M8738': ('iPod', '20GB'),
330
'M8741': ('iPod', '20GB'),
332
'M8976': ('iPod', '10GB'),
333
'M8946': ('iPod', '15GB'),
334
'M9460': ('iPod', '15GB'),
335
'M9244': ('iPod', '20GB'),
336
'M8948': ('iPod', '30GB'),
337
'M9245': ('iPod', '40GB'),
339
'M9282': ('iPod', '20GB'),
340
'M9787': ('iPod (U2 edition)', '20GB'),
341
'M9268': ('iPod', '40GB'),
342
# Photo / Fourth Generation
343
'MA079': ('iPod photo', '20GB'),
344
'MA127': ('iPod photo (U2 edition)', '20GB'),
345
'M9829': ('iPod photo', '30GB'),
346
'M9585': ('iPod photo', '40GB'),
347
'M9586': ('iPod photo', '60GB'),
348
'M9830': ('iPod photo', '60GB'),
349
# Shuffle / Fourth Generation
350
'M9724': ('iPod shuffle', '512MB'),
351
'M9725': ('iPod shuffle', '1GB'),
352
'MA133': ('iPod shuffle', '512MB'),
353
# Video / Fifth Generation
354
'MA002': ('iPod video white', '30GB'),
355
'MA146': ('iPod video black', '30GB'),
356
'MA003': ('iPod video white', '60GB'),
357
'MA147': ('iPod video black', '60GB'),
358
# Nano / Fifth Generation
359
'MA350': ('iPod nano white', '1GB'),
360
'MA352': ('iPod nano black', '1GB'),
361
'MA004': ('iPod nano white', '2GB'),
362
'MA099': ('iPod nano black', '2GB'),
363
'MA005': ('iPod nano white', '4GB'),
364
'MA107': ('iPod nano black', '4GB'),
365
# First Generation Mini
366
'M9160': ('iPod mini silver', '4GB'),
367
'M9436': ('iPod mini blue', '4GB'),
368
'M9435': ('iPod mini pink', '4GB'),
369
'M9434': ('iPod mini green', '4GB'),
370
'M9437': ('iPod mini gold', '4GB'),
371
# Second Generation Mini
372
'M9800': ('iPod mini silver', '4GB'),
373
'M9802': ('iPod mini blue', '4GB'),
374
'M9804': ('iPod mini pink', '4GB'),
375
'M9806': ('iPod mini green', '4GB'),
376
'M9801': ('iPod mini silver', '6GB'),
377
'M9803': ('iPod mini blue', '6GB'),
378
'M9805': ('iPod mini pink', '6GB'),
379
'M9807': ('iPod mini green', '6GB'),
386
devices = [IPodDevice]