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