1# Copyright (C) 2009-2010 Aren Olson
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2, or (at your option)
6# any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
16#
17#
18# The developers of the Exaile media player hereby grant permission
19# for non-GPL compatible GStreamer and Exaile plugins to be used and
20# distributed together with GStreamer and Exaile. This permission is
21# above and beyond the permissions granted by the GPL license by which
22# Exaile is covered. If you modify this code, you may extend this
23# exception to your version of the code, but you are not obligated to
24# do so. If you do not wish to do so, delete this exception statement
25# from your version.
26
27import dbus
28from fcntl import ioctl
29import logging
30import os
31import struct
32
33from xl.nls import gettext as _
34from xl import providers, event
35from xl.hal import Handler, UDisksProvider
36from xl.devices import Device, KeyedDevice
37
38from xl import playlist, trax, common
39import os.path
40
41from . import cdprefs
42
43try:
44    import DiscID
45    import CDDB
46
47    CDDB_AVAIL = True
48except Exception:
49    CDDB_AVAIL = False
50
51logger = logging.getLogger(__name__)
52
53
54TOC_HEADER_FMT = 'BB'
55TOC_ENTRY_FMT = 'BBBix'
56ADDR_FMT = 'BBB' + 'x' * (struct.calcsize('i') - 3)
57CDROMREADTOCHDR = 0x5305
58CDROMREADTOCENTRY = 0x5306
59CDROM_LEADOUT = 0xAA
60CDROM_MSF = 0x02
61CDROM_DATA_TRACK = 0x04
62
63
64class CdPlugin:
65    def enable(self, exaile):
66        self.__exaile = exaile
67        self.__udisks2 = None
68
69    def on_exaile_loaded(self):
70        # verify that hal/whatever is loaded, load correct provider
71        if self.__exaile.udisks2 is not None:
72            self.__udisks2 = UDisks2CdProvider()
73            providers.register('udisks2', self.__udisks2)
74
75    def teardown(self, exaile):
76        if self.__udisks2 is not None:
77            providers.unregister('udisks2', self.__udisks2)
78        self.__udisks2 = None
79        self.__exaile = None
80
81    def disable(self, exaile):
82        self.teardown(exaile)
83
84    def get_preferences_pane(self):
85        return cdprefs
86
87
88plugin_class = CdPlugin
89
90
91class _CDTrack:
92    """
93    @ivar track: Track number. Starts with 1, which is used for the TOC and contains data.
94    @ivar data: `True` if this "track" contains data, `False` if it is audio
95    @ivar minutes: Minutes from begin of CD
96    @ivar seconds: Seconds after `minutes`, from begin of CD
97    @ivar frames: Frames after `seconds`, from begin of CD
98    """
99
100    def __init__(self, entry):
101        self.track, adrctrl, _format, addr = struct.unpack(TOC_ENTRY_FMT, entry)
102        self.minutes, self.seconds, self.frames = struct.unpack(
103            ADDR_FMT, struct.pack('i', addr)
104        )
105
106        # adr = adrctrl & 0xf
107        ctrl = (adrctrl & 0xF0) >> 4
108
109        self.data = False
110        if ctrl & CDROM_DATA_TRACK:
111            self.data = True
112
113    def get_frame_count(self):
114        return (self.minutes * 60 + self.seconds) * 75 + self.frames
115
116
117class CDTocParser:
118    # based on code from http://carey.geek.nz/code/python-cdrom/cdtoc.py
119
120    def __init__(self, device):
121        self.__raw_tracks = []
122        self.__read_toc(device)
123
124    def __read_toc(self, device):
125        fd = os.open(device, os.O_RDONLY)
126        try:
127            toc_header = struct.pack(TOC_HEADER_FMT, 0, 0)
128            toc_header = ioctl(fd, CDROMREADTOCHDR, toc_header)
129            start, end = struct.unpack(TOC_HEADER_FMT, toc_header)
130
131            # All tracks plus leadout
132            for trnum in list(range(start, end + 1)) + [CDROM_LEADOUT]:
133                entry = struct.pack(TOC_ENTRY_FMT, trnum, 0, CDROM_MSF, 0)
134                entry = ioctl(fd, CDROMREADTOCENTRY, entry)
135                self.__raw_tracks.append(_CDTrack(entry))
136        finally:
137            os.close(fd)
138
139    def _get_track_lengths(self):
140        """ returns track length in seconds """
141        track = self.__raw_tracks[0]
142        offset = track.get_frame_count()
143        lengths = []
144        for track in self.__raw_tracks[1:]:
145            frame_end = track.get_frame_count()
146            lengths.append((frame_end - offset) // 75)
147            offset = frame_end
148        return lengths
149
150
151class CDPlaylist(playlist.Playlist):
152    def __init__(self, name=_("Audio Disc"), device=None):
153        playlist.Playlist.__init__(self, name=name)
154
155        if not device:
156            self.__device = "/dev/cdrom"
157        else:
158            self.__device = device
159
160        self.open_disc()
161
162    def open_disc(self):
163
164        toc = CDTocParser(self.__device)
165        lengths = toc._get_track_lengths()
166
167        songs = []
168
169        for count, length in enumerate(lengths):
170            count += 1
171            song = trax.Track("cdda://%d/#%s" % (count, self.__device))
172            song.set_tags(
173                title="Track %d" % count, tracknumber=str(count), __length=length
174            )
175            songs.append(song)
176
177        self.extend(songs)
178
179        if CDDB_AVAIL:
180            self.get_cddb_info()
181
182    @common.threaded
183    def get_cddb_info(self):
184        try:
185            disc = DiscID.open(self.__device)
186            self.__device = DiscID.disc_id(disc)
187            status, info = CDDB.query(self.__device)
188        except IOError:
189            return
190
191        if status in (210, 211):
192            info = info[0]
193            status = 200
194        if status != 200:
195            return
196
197        (status, info) = CDDB.read(info['category'], info['disc_id'])
198
199        title = info['DTITLE'].split(" / ")
200        for i in range(self.__device[1]):
201            tr = self[i]
202            tr.set_tags(
203                title=info['TTITLE' + str(i)].decode('iso-8859-15', 'replace'),
204                album=title[1].decode('iso-8859-15', 'replace'),
205                artist=title[0].decode('iso-8859-15', 'replace'),
206                year=info['EXTD'].replace("YEAR: ", ""),
207                genre=info['DGENRE'],
208            )
209
210        self.name = title[1].decode('iso-8859-15', 'replace')
211        event.log_event('cddb_info_retrieved', self, True)
212
213
214class CDDevice(KeyedDevice):
215    """
216    represents a CD
217    """
218
219    class_autoconnect = True
220
221    def __init__(self, dev):
222        Device.__init__(self, dev)
223        self.name = _("Audio Disc")
224        self.dev = dev
225
226    def _get_panel_type(self):
227        import imp
228
229        try:
230            _cdguipanel = imp.load_source(
231                "_cdguipanel", os.path.join(os.path.dirname(__file__), "_cdguipanel.py")
232            )
233            return _cdguipanel.CDPanel
234        except Exception:
235            logger.exception("Could not import cd gui panel")
236            return 'flatplaylist'
237
238    panel_type = property(_get_panel_type)
239
240    def connect(self):
241        cdpl = CDPlaylist(device=self.dev)
242        self.playlists.append(cdpl)
243        self.connected = True
244
245    def disconnect(self):
246        self.playlists = []
247        self.connected = False
248        CDDevice.destroy(self)
249
250
251class UDisks2CdProvider(UDisksProvider):
252    name = 'cd'
253    PRIORITY = UDisksProvider.NORMAL
254
255    def _get_num_tracks(self, obj, udisks):
256        if obj.iface_type != 'org.freedesktop.UDisks2.Block':
257            return
258
259        try:
260            drive = udisks.get_object_by_path(obj.props.Get('Drive'))
261        except KeyError:
262            return
263
264        # Use number of audio tracks to identify supported media
265        ntracks = drive.props.Get('OpticalNumAudioTracks')
266        if ntracks > 0:
267            return ntracks
268
269    def get_priority(self, obj, udisks):
270        ntracks = self._get_num_tracks(obj, udisks)
271        if ntracks is not None:
272            return self.PRIORITY
273
274    def get_device(self, obj, udisks):
275        device = obj.props.Get('Device', byte_arrays=True).decode('utf-8')
276        device = device.strip('\0')
277        return CDDevice(device)
278
279    def on_device_changed(self, obj, udisks, device):
280        if self._get_num_tracks(obj, udisks) is None:
281            return 'remove'
282
283
284# vim: et sts=4 sw=4
285