~ubuntu-branches/ubuntu/karmic/quodlibet/karmic

« back to all changes in this revision

Viewing changes to devices/ipod.py

  • Committer: Bazaar Package Importer
  • Author(s): Luca Falavigna
  • Date: 2009-01-30 23:55:34 UTC
  • mfrom: (1.1.12 upstream)
  • Revision ID: james.westby@ubuntu.com-20090130235534-l4e72ulw0vqfo17w
Tags: 2.0-1ubuntu1
* Merge from Debian experimental (LP: #276856), remaining Ubuntu changes:
  + debian/patches/40-use-music-profile.patch:
    - Use the "Music and Movies" pipeline per default.
* Refresh the above patch for new upstream release.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
# Copyright 2006 Markus Koller
3
 
#
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
7
 
#
8
 
# $Id: ipod.py 4047 2007-04-30 03:49:58Z piman $
9
 
 
10
 
import os
11
 
import time
12
 
import gtk
13
 
 
14
 
import const
15
 
import qltk
16
 
import stock
17
 
import util
18
 
 
19
 
from devices._base import Device
20
 
from formats._audio import AudioFile
21
 
 
22
 
# Wraps an itdb_track from libgpod in an AudioFile instance
23
 
class IPodSong(AudioFile):
24
 
    is_file = False
25
 
 
26
 
    def __init__(self, track):
27
 
        super(IPodSong, self).__init__()
28
 
        self.sanitize(gpod.itdb_filename_on_ipod(track))
29
 
 
30
 
        for key in ['artist', 'album', 'title', 'genre', 'grouping']:
31
 
            value = getattr(track, key)
32
 
            if value:
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)
38
 
            if value:
39
 
                self['~#'+key] = value
40
 
 
41
 
        try: self["date"] = unicode(track.year)
42
 
        except AttributeError: pass
43
 
 
44
 
        if track.cds:
45
 
            self["discnumber"] = u"%d/%d" % (track.cd_nr, track.cds)
46
 
        elif track.cd_nr:
47
 
            self["discnumber"] = u"%d" % track.cd_nr
48
 
 
49
 
        if track.tracks:
50
 
            self['tracknumber'] = u"%d/%d" % (track.track_nr, track.tracks)
51
 
        elif track.track_nr:
52
 
            self['tracknumber'] = u"%d" % track.track_nr
53
 
 
54
 
        for key, value in {
55
 
            '~#rating': min(1.0, track.rating / 100.0),
56
 
            '~#length': track.tracklen / 1000.0,
57
 
        }.items():
58
 
            if value != 0:
59
 
                self[key] = value
60
 
        self['~format'] = u"iPod: %s" % track.filetype
61
 
 
62
 
    # Disable all tag editing
63
 
    def can_change(self, k=None):
64
 
        return []
65
 
 
66
 
class IPodDevice(Device):
67
 
    icon = stock.IPOD
68
 
    type = "ipod"
69
 
 
70
 
    ordered = True
71
 
 
72
 
    defaults = {
73
 
        'gain': 0.0,
74
 
        'covers': True,
75
 
        'all_tags': False,
76
 
        'title_version': False,
77
 
        'album_part': False,
78
 
    }
79
 
 
80
 
    __itdb = None
81
 
    __covers = []
82
 
 
83
 
    def Properties(self):
84
 
        props = []
85
 
 
86
 
        gain = gtk.SpinButton()
87
 
        gain.set_range(-20, 20)
88
 
        gain.set_digits(1)
89
 
        gain.set_increments(0.1, 1)
90
 
        gain.set_value(float(self['gain']))
91
 
        props.append((_("_Volume Gain (dB):"), gain, 'gain'))
92
 
 
93
 
        for key, label in [
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")],
98
 
        ]:
99
 
            check = gtk.CheckButton()
100
 
            check.set_active(self[key])
101
 
            props.append((label, check, key))
102
 
 
103
 
        if self.is_connected():
104
 
            details = self.__get_details()
105
 
            if len(details) > 0:
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))
113
 
 
114
 
        return props
115
 
 
116
 
    def __get_details(self):
117
 
        d = {}
118
 
        sysinfo = os.path.join(self.mountpoint,
119
 
            'iPod_Control', 'Device', 'SysInfo')
120
 
 
121
 
        if os.path.isfile(sysinfo):
122
 
            file = open(sysinfo)
123
 
            while True:
124
 
                line = file.readline()
125
 
                if not line: break
126
 
                parts = line.split()
127
 
                if len(parts) < 2: continue
128
 
 
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("()")
134
 
            file.close()
135
 
        else:
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:
140
 
                model = 'M9725'
141
 
            else:
142
 
                model = 'M9724'
143
 
            if model in self.__models:
144
 
                d['model'], d['space'] = self.__models[model]
145
 
 
146
 
        return d
147
 
 
148
 
    def list(self, wlb):
149
 
        self.__load_db()
150
 
        songs = []
151
 
        orphaned = False
152
 
        for track in gpod.sw_get_tracks(self.__itdb):
153
 
            filename = gpod.itdb_filename_on_ipod(track)
154
 
            if filename:
155
 
                songs.append(IPodSong(track))
156
 
            else: # Remove orphaned iTunesDB track
157
 
                orphaned = True
158
 
                print _("W: removing orphaned iPod track")
159
 
                self.__remove_track(track)
160
 
        if orphaned:
161
 
            self.__save_db()
162
 
        self.__close_db()
163
 
        return songs
164
 
 
165
 
    def copy(self, songlist, song):
166
 
        self.__load_db()
167
 
        track = gpod.itdb_track_new()
168
 
 
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]
172
 
 
173
 
        try:
174
 
            title = tag('title')
175
 
        except IndexError:
176
 
            pass
177
 
        else:
178
 
            if self['title_version'] and song('version'):
179
 
                title = " - ".join([title, song('version')])
180
 
            track.title = str(title)
181
 
 
182
 
        try:
183
 
            album = tag('album')
184
 
        except IndexError:
185
 
            pass
186
 
        else:
187
 
            if self['album_part'] and song('part'):
188
 
                album = " - ".join([album, song('part')])
189
 
            track.album = str(album)
190
 
 
191
 
        # String keys
192
 
        for key in ['artist', 'genre', 'grouping']:
193
 
            try: setattr(track, key, str(tag(key)))
194
 
            except IndexError: continue
195
 
        # Numeric keys
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
200
 
        for key, value in {
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),
211
 
        }.items():
212
 
            try: setattr(track, key, int(value))
213
 
            except ValueError: continue
214
 
 
215
 
        track.filetype = song('~format')
216
 
        track.comment = song('~filename')
217
 
 
218
 
        # Associate a cover with the track
219
 
        if self['covers']:
220
 
            cover = song.find_cover()
221
 
            if 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)
227
 
 
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)
232
 
 
233
 
        # Copy the actual file
234
 
        if gpod.itdb_cp_track_to_ipod(track, song['~filename'], None) == 1:
235
 
            return IPodSong(track)
236
 
        else:
237
 
            return False
238
 
 
239
 
    def delete(self, songlist, song):
240
 
        self.__load_db()
241
 
        try:
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)
246
 
                    return True
247
 
            else:
248
 
                return False
249
 
        except IOError, exc:
250
 
            return str(exc).decode(const.ENCODING, 'replace')
251
 
 
252
 
    def cleanup(self, wlb, action):
253
 
        try:
254
 
            wlb.set_text("<b>Saving iPod database...</b>")
255
 
            if not self.__save_db():
256
 
                wlb.set_text(_("Unable to save iPod database"))
257
 
                return False
258
 
            return True
259
 
        finally:
260
 
            self.__close_db()
261
 
            self.__covers = []
262
 
 
263
 
    def __load_db(self):
264
 
        if self.__itdb: return self.__itdb
265
 
 
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?")
270
 
            ).run():
271
 
            self.__itdb = self.__create_db()
272
 
 
273
 
        return self.__itdb
274
 
 
275
 
    def __save_db(self):
276
 
        if gpod.itdb_write(self.__itdb, None) == 1 and \
277
 
           gpod.itdb_shuffle_write(self.__itdb, None) == 1:
278
 
            return True
279
 
        else:
280
 
            return False
281
 
 
282
 
    def __create_db(self):
283
 
        db = gpod.itdb_new();
284
 
        gpod.itdb_set_mountpoint(db, self.mountpoint)
285
 
 
286
 
        master = gpod.itdb_playlist_new('iPod', False)
287
 
        gpod.itdb_playlist_set_mpl(master)
288
 
        gpod.itdb_playlist_add(db, master, 0)
289
 
 
290
 
        return db
291
 
 
292
 
    def __close_db(self):
293
 
        if self.__itdb: gpod.itdb_free(self.__itdb)
294
 
        self.__itdb = None
295
 
 
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)
300
 
 
301
 
    def __mactime(self, time):
302
 
        time = int(time)
303
 
        if time == 0: return time
304
 
        else: return time + 2082844800
305
 
 
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])
312
 
        else: db = 0.0
313
 
 
314
 
        soundcheck = int(round(1000 * 10.**(
315
 
            -0.1 * (db + float(self['gain'])))))
316
 
        return soundcheck
317
 
 
318
 
    # This list is taken from
319
 
    # http://en.wikipedia.org/wiki/List_of_iPod_model_numbers
320
 
    __models = {
321
 
        # First Generation
322
 
        'M8513': ('iPod', '5GB'),
323
 
        'M8541': ('iPod', '5GB'),
324
 
        'M8697': ('iPod', '5GB'),
325
 
        'M8709': ('iPod', '10GB'),
326
 
        # Second Generation
327
 
        'M8737': ('iPod', '10GB'),
328
 
        'M8740': ('iPod', '10GB'),
329
 
        'M8738': ('iPod', '20GB'),
330
 
        'M8741': ('iPod', '20GB'),
331
 
        # Third Generation
332
 
        'M8976': ('iPod', '10GB'),
333
 
        'M8946': ('iPod', '15GB'),
334
 
        'M9460': ('iPod', '15GB'),
335
 
        'M9244': ('iPod', '20GB'),
336
 
        'M8948': ('iPod', '30GB'),
337
 
        'M9245': ('iPod', '40GB'),
338
 
        # Fourth Generation
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'),
380
 
    }
381
 
 
382
 
try: import gpod
383
 
except ImportError:
384
 
    devices = []
385
 
else:
386
 
    devices = [IPodDevice]