def register_provider(self, servicename, provider, target=None): """ Registers a provider for a service. The provider object is used by consumers of the service. Services can be targeted for a specific use. For example, if you have a widget that uses a service 'foo', if your object can perform a service only for a specific type of widget, then target would be set to the widget type. If you had a service that could perform 'foo' for all widgets, then target would be set to None, and all widgets could use your service. It is intended that most services should set target to None, with some narrow exceptions. :param servicename: the name of the service [string] :type servicename: string :param provider: the object that is the provider [object] :type provider: object :param target: a specific target for the service [object] :type target: object """ service = self.services.setdefault(servicename, {}) providers = service.setdefault(target, []) if provider not in providers: providers.append(provider) logger.debug( "Provider %(provider)s registered for service %(service)s " "with target %(target)s" % {'provider': provider.name, 'service': servicename, 'target': target} ) event.log_event("%s_provider_added" % servicename, self, (provider, target))
def rescan(self, notify_interval=None, force_update=False): ''' Called when a library needs to refresh its track list. The force_update parameter is not applicable and is ignored. ''' if self.collection is None: return True if self.scanning: return t = time.time() logger.info('Scanning library: %s' % self.daap_share.name) self.scanning = True db = self.collection # DAAP gives us all the tracks in one dump self.daap_share.reload() if self.daap_share.all: count = len(self.daap_share.all) else: count = 0 if count > 0: logger.info('Adding %d tracks from %s. (%f s)' % (count, self.daap_share.name, time.time()-t)) self.collection.add_tracks(self.daap_share.all) if notify_interval is not None: event.log_event('tracks_scanned', self, count) # track removal? self.scanning = False
def set_tag_raw(self, tag, values, notify_changed=True): """ Set the raw value of a tag. :param tag: The name of the tag to set. :param values: The value or values to set the tag to. :param notify_changed: whether to send a signal to let other parts of Exaile know there has been an update. Only set this to False if you know that no other parts of Exaile need to be updated. """ # handle values that aren't lists if not isinstance(values, list): if not tag.startswith("__"): # internal tags dont have to be lists values = [values] # TODO: is this needed? why? # for lists, filter out empty values and convert to unicode if isinstance(values, list): values = [common.to_unicode(x, self.__tags.get('__encoding')) for x in values if x not in (None, '')] # save some memory by not storing null values. if not values: try: del self.__tags[tag] except KeyError: pass else: self.__tags[tag] = values self._dirty = True if notify_changed: event.log_event("track_tags_changed", self, tag)
def transfer(self): """ Tranfer the queued tracks to the library. This is NOT asynchronous """ self.transferring = True self.current_pos += 1 try: while self.current_pos < len(self.queue) and not self._stop: track = self.queue[self.current_pos] loc = track.get_loc_for_io() self.library.add(loc) # TODO: make this be based on filesize not count progress = self.current_pos * 100 / len(self.queue) event.log_event('track_transfer_progress', self, progress) self.current_pos += 1 finally: self.queue = [] self.transferring = False self.current_pos = -1 self._stop = False event.log_event('track_transfer_progress', self, 100)
def _on_message(self, bus, message, reading_tag=False): handled = self._handle_message(bus, message, reading_tag) if handled: pass elif message.type == gst.MESSAGE_TAG: """ Update track length and optionally metadata from gstreamer's parser. Useful for streams and files mutagen doesn't understand. """ parsed = message.parse_tag() event.log_event('tags_parsed', self, (self.current, parsed)) if self.current and not self.current.get_tag_raw('__length'): try: raw_duration = self._pipe.query_duration(gst.FORMAT_TIME, None)[0] except gst.QueryError: logger.error("Couldn't query duration") raw_duration = 0 duration = float(raw_duration)/gst.SECOND if duration > 0: self.current.set_tag_raw('__length', duration) elif message.type == gst.MESSAGE_EOS and not self.is_paused(): self._eos_func() elif message.type == gst.MESSAGE_ERROR: logger.error("%s %s" %(message, dir(message)) ) message_text = message.parse_error()[1] # The most readable part is always the last.. message_text = message_text[message_text.rfind(':') + 1:] # .. unless there's nothing in it. if ' ' not in message_text: if message_text.startswith('playsink'): message_text += _(': Possible audio device error, is it plugged in?') event.log_event('playback_error', self, message_text) self._error_func() return True
def get_cddb_info(self): try: disc = DiscID.open(self.device) self.info = DiscID.disc_id(disc) status, info = CDDB.query(self.info) except IOError: return if status in (210, 211): info = info[0] status = 200 if status != 200: return (status, info) = CDDB.read(info['category'], info['disc_id']) title = info['DTITLE'].split(" / ") for i in range(self.info[1]): tr = self[i] tr.set_tag_raw('title', info['TTITLE' + `i`].decode('iso-8859-15', 'replace')) tr.set_tag_raw('album', title[1].decode('iso-8859-15', 'replace')) tr.set_tag_raw('artist', title[0].decode('iso-8859-15', 'replace')) tr.set_tag_raw('year', info['EXTD'].replace("YEAR: ", "")) tr.set_tag_raw('genre', info['DGENRE']) self.name = title[1].decode('iso-8859-15', 'replace') event.log_event('cddb_info_retrieved', self, True)
def _restore_player_state(self, location): if not settings.get_option("%s/resume_playback" % self.player._name, True): return try: f = open(location, 'rb') state = pickle.load(f) f.close() except: return for req in ['state', 'position', '_playtime_stamp']: if req not in state: return if state['state'] in ['playing', 'paused']: event.log_event("playback_player_resume", self.player, None) vol = self.player._get_volume() self.player._set_volume(0) self.play(self.get_current()) if self.player.current: self.player.seek(state['position']) if state['state'] == 'paused' or \ settings.get_option("%s/resume_paused" % self.player._name, False): self.player.toggle_pause() self.player._playtime_stamp = state['_playtime_stamp'] self.player._set_volume(vol)
def set_option(self, option, value, save=True): """ Set an option (in ``section/key`` syntax) to the specified value :param option: the full path to an option :type option: string :param value: the value the option should be assigned :type value: any :param save: If True, cause the settings to be written to file """ value = self._val_to_str(value) splitvals = option.split('/') section, key = "/".join(splitvals[:-1]), splitvals[-1] try: self.set(section, key, value) except NoSectionError: self.add_section(section) self.set(section, key, value) self._dirty = True if save: self.delayed_save() section = section.replace('/', '_') event.log_event('option_set', self, option) event.log_event('%s_option_set' % section, self, option)
def get_cddb_info(self): try: disc = DiscID.open(self.device) self.info = DiscID.disc_id(disc) status, info = CDDB.query(self.info) except IOError: return if status in (210, 211): info = info[0] status = 200 if status != 200: return (status, info) = CDDB.read(info["category"], info["disc_id"]) title = info["DTITLE"].split(" / ") for i in range(self.info[1]): tr = self[i] tr.set_tag_raw("title", info["TTITLE" + str(i)].decode("iso-8859-15", "replace")) tr.set_tag_raw("album", title[1].decode("iso-8859-15", "replace")) tr.set_tag_raw("artist", title[0].decode("iso-8859-15", "replace")) tr.set_tag_raw("year", info["EXTD"].replace("YEAR: ", "")) tr.set_tag_raw("genre", info["DGENRE"]) self.name = title[1].decode("iso-8859-15", "replace") event.log_event("cddb_info_retrieved", self, True)
def unregister_provider(self, servicename, provider, target=None): """ Unregisters a provider. :param servicename: the name of the service :type servicename: string :param provider: the provider to be removed :type provider: object :param target: a specific target for the service [object] :type target: object """ if servicename not in self.services: return try: service = self.services[servicename] if provider in service[target]: service[target].remove(provider) logger.debug( "Provider %(provider)s unregistered from " "service %(service)s with target %(target)s" % { 'provider' : provider.name, 'service' : servicename, 'target' : target } ) event.log_event("%s_provider_removed" % servicename, self, (provider, target)) if not service[target]: #no values for target key then del it del service[target] except KeyError: return
def play(self, track): """ plays the specified track, overriding any currently playing track if the track cannot be played, playback stops completely """ if track is None: self.stop() return False else: self.stop(fire=False) playing = self.is_playing() self._current = track uri = track.get_loc_for_io() logger.info("Playing %s" % uri) self.reset_playtime_stamp() self.playbin.set_property("uri", uri) if urlparse.urlsplit(uri)[0] == "cdda": self.notify_id = self.playbin.connect('notify::source', self.__notify_source) self.playbin.set_state(gst.STATE_PLAYING) if not playing: event.log_event('playback_player_start', self, track) event.log_event('playback_track_start', self, track) return True
def update_field(self, name, *params): try: self[name] = getattr(self, '_'+name.replace('-', '_'))(*params) except: self[name] = '' if name in [f[0] for f in self.get_template_fields()]: event.log_event('field_refresh', self, (name, self[name]))
def remove_library(self, library): """ Remove a library from the collection :param library: the library to remove :type library: :class:`Library` """ for k, v in self.libraries.iteritems(): if v == library: del self.libraries[k] break to_rem = [] if not "://" in library.location: location = u"file://" + library.location else: location = library.location for tr in self.tracks: if tr.startswith(location): to_rem.append(self.tracks[tr]._track) self.remove_tracks(to_rem) self.serialize_libraries() self._dirty = True if self._frozen: self._libraries_dirty = True else: event.log_event('libraries_modified', self, None)
def seek(self, value): """ seek to the given position in the current stream """ self.streams[self._current_stream].seek(value) self._reset_crossfade_timer() event.log_event('playback_seeked', self, value)
def remove_device(self, device): try: if device.connected: device.disconnect() del self.devices[device.get_name()] except KeyError: pass event.log_event("device_removed", self, device)
def __set_connected(self, val): prior = self._connected self._connected = val if prior != val: if val: event.log_event("device_connected", self, self) else: event.log_event("device_disconnected", self, self)
def link_clicked(self, link): if link[0] == 'rate': self.track.set_rating(int(link[1])) self.refresh_rating() for field in ['rating', 'track-info']: if field in self.get_template_fields(): event.log_event('field_refresh', self, (field, str(self[field]))) return True
def do_rating_changed(self, rating): """ Updates the rating of the currently playing track """ if player.PLAYER.current is not None: player.PLAYER.current.set_rating(rating) maximum = settings.get_option('rating/maximum', 5) event.log_event('rating_changed', self, rating / maximum * 100)
def SetRating(self, value): """ Sets the current track's rating :param value: the new rating :type value: int """ self.SetTrackAttr('__rating', value) event.log_event('rating_changed', self, value)
def __set_queue_has_tracks(self, value): if value != self.__queue_has_tracks_val: oldpos = self.current_position self.__queue_has_tracks_val = value event.log_event( "playlist_current_position_changed", self, (self.current_position, oldpos), )
def engine_notify_error(self, msg): ''' Notification that some kind of error has occurred. If the error is not recoverable, the engine is expected to stop playback and reset itself to a state where playback can begin again. .. note:: Only to be called from engine ''' event.log_event('playback_error', self, msg)
def seek(self, value): """ Seek to a position in the currently playing stream :param value: the position in seconds :type value: int """ if self._engine.seek(value): event.log_event('playback_seeked', self, value)
def _settle_state(self): self._settle_flag = 1 if self._settle_trap > 10: self._settle_trap = 0 self._settle_flag = 0 logger.debug("Failed to settle state on %s."%self) gst.Bin.set_state(self, gst.STATE_NULL) event.log_event("stream_settled", self, None) return gobject.idle_add(self._settle_state_sub)
def pause(self): """ pause playback. DOES NOT TOGGLE """ if self.is_playing(): self.pipe.set_state(gst.STATE_PAUSED) self._reset_crossfade_timer() event.log_event('playback_player_pause', self, self.current) return True return False
def pause(self): """ pause playback. DOES NOT TOGGLE """ if self.is_playing(): self.update_playtime() self.playbin.set_state(gst.STATE_PAUSED) self.reset_playtime_stamp() event.log_event('playback_player_pause', self, self.current) return True return False
def add_device(self, device): # make sure we don't overwrite existing devices count = 3 if device.get_name() in self.devices: device.name += " (2)" while device.get_name() in self.devices: device.name = device.name[:-4] + " (%s)" % count count += 1 self.devices[device.get_name()] = device event.log_event("device_added", self, device)
def on_rating_changed(self, widget, rating): """ Updates the rating of the selected tracks """ tracks = self.get_selected_tracks() for track in tracks: track.set_rating(rating) maximum = settings.get_option('rating/maximum', 5) event.log_event('rating_changed', self, rating / maximum * 100)
def set_loc(self, loc): """ Sets the location. :param loc: the location, as either a uri or a file path. """ self.__unregister() gloc = Gio.File.new_for_commandline_arg(loc) self.__tags['__loc'] = gloc.get_uri() self.__register() event.log_event('track_tags_changed', self, '__loc')
def engine_notify_track_start(self, track): ''' Called when a track has just entered the playing state :param track: Track that is being played now .. note:: Only to be called from engine ''' self._reset_playtime_stamp() event.log_event('playback_track_start', self, track)
def disable(self, exaile): logger.debug('Disabling Preview Device') event.log_event('preview_device_disabling', self, None) self._destroy_gui_hooks() self._destroy_gui() self.player.destroy() self.player = None self.queue = None logger.debug('Preview Device Disabled')
def _handle_message(self, bus, message, reading_tag=False): if message.type == gst.MESSAGE_BUFFERING: percent = message.parse_buffering() if not percent < 100: logger.info('Buffering complete') if percent % 5 == 0: event.log_event('playback_buffering', self, percent) elif message.type == gst.MESSAGE_ELEMENT and \ message.src == self._pipe and \ message.structure.get_name() == 'playbin2-stream-changed' and \ self._buffered_track is not None: self.queue.next(autoplay=False) self._next_track(self._buffered_track, already_playing=True) else: return False return True
def connect(self): try: self.bus = dbus.SystemBus() hal_obj = self.bus.get_object('org.freedesktop.Hal', '/org/freedesktop/Hal/Manager') self.hal = dbus.Interface(hal_obj, 'org.freedesktop.Hal.Manager') logger.debug("HAL Providers: %s" % repr(self.get_providers())) for p in self.get_providers(): try: self.on_provider_added(p) except: logger.exception("Failed to load HAL devices for %s", p.name) self.setup_device_events() logger.debug("Connected to HAL") event.log_event("hal_connected", self, None) except: logger.warning("Failed to connect to HAL, " \ "autodetection of devices will be disabled.")
def connect(self): assert self._state == 'init' logger.debug("Connecting to %s", self.name) try: self.obj = self._connect() logger.info("Connected to %s", self.name) event.log_event("hal_connected", self, None) except Exception: logger.warning("Failed to connect to %s, " \ "autodetection of devices will be disabled.", self.name) common.log_exception() return False self._state = 'addremove' logger.debug("%s: state = addremove", self.name) self._add_all(self.obj) self._state = 'listening' logger.debug("%s: state = listening", self.name) return True
def add_tracks(self, tracks): """ Like add(), but takes a list of :class:`xl.trax.Track` """ locations = [] now = time() for tr in tracks: if not tr.get_tag_raw('__date_added'): tr.set_tags(__date_added=now) location = tr.get_loc_for_io() # Don't add duplicates -- track URLs are unique if location in self.tracks: continue locations += [location] self.tracks[location] = TrackHolder(tr, self._key) self._key += 1 if locations: event.log_event('tracks_added', self, locations) self._dirty = True
def set_cover(self, track, db_string, data=None): """ Sets the cover for a track. This will overwrite any existing entry. :param track: The track to set the cover for :param db_string: the string identifying the source of the cover, in "method:key" format. :param data: The raw cover data to store for the track. Will only be stored if the method has use_cache=True """ name = db_string.split(":", 1)[0] method = self.methods.get(name) if method and method.use_cache and data: db_string = "cache:%s" % self.__cache.add(data) key = self._get_track_key(track) if key: self.db[key] = db_string self.timeout_save() event.log_event('cover_set', self, track)
def _set_direct(self, option, value): """ Sets the option directly to the value, only for use in copying settings. :param option: the option path :type option: string :param value: the value to set :type value: any """ splitvals = option.split('/') section, key = "/".join(splitvals[:-1]), splitvals[-1] try: self.set(section, key, value) except NoSectionError: self.add_section(section) self.set(section, key, value) event.log_event('option_set', self, option)
def _progress_update(self, type, library, count): """ Called when a progress update should be emitted while scanning tracks """ self._running_count = count count = count + self._running_total_count if self.file_count < 0: event.log_event('scan_progress_update', self, 0) return try: event.log_event( 'scan_progress_update', self, count / self.file_count * 100, ) except ZeroDivisionError: pass
def set_current_playlist(self, playlist): """ Sets the playlist to be processed in the queue :param playlist: the playlist to process :type playlist: :class:`xl.playlist.Playlist` .. note:: The following :doc:`events </xl/event>` will be emitted by this method: * `queue_current_playlist_changed`: indicates that the queue playlist has been changed """ if playlist is self.__current_playlist: return elif playlist is None: playlist = self if playlist is self: self.__queue_has_tracks = True self.__current_playlist = playlist event.log_event('queue_current_playlist_changed', self, playlist)
def engine_notify_track_end(self, track, done): """ Called when a track has been stopped. Either: - stop() was called - play() was called and the prior track was stopped :param track: Must be the track that was just playing, and must never be None :param done: If True, no further tracks will be played .. note:: Only to be called from engine """ self._update_playtime(track) event.log_event('playback_track_end', self, track) if done: self._cancel_delayed_start() event.log_event('playback_player_end', self, track)
def on_message(self, bus, message, reading_tag = False): """ Called when a message is received from gstreamer """ if message.type == gst.MESSAGE_TAG and self.tag_func: self.tag_func(message.parse_tag()) if not self.current.get_tag_raw('__length'): try: duration = float(self.playbin.query_duration( gst.FORMAT_TIME, None)[0])/1000000000 if duration > 0: self.current.set_tag_raw('__length', duration) except gst.QueryError: logger.error("Couldn't query duration") elif message.type == gst.MESSAGE_EOS and not self.is_paused(): self.eof_func() elif message.type == gst.MESSAGE_ERROR: logger.error("%s %s" %(message, dir(message)) ) a = message.parse_error()[0] gobject.idle_add(self._on_playback_error, a.message) # TODO: merge this into stop() and make it engine-agnostic somehow curr = self.current self._current = None self.playbin.set_state(gst.STATE_NULL) self.setup_pipe() event.log_event("playback_track_end", self, curr) event.log_event("playback_player_end", self, curr) elif message.type == gst.MESSAGE_BUFFERING: percent = message.parse_buffering() if not percent < 100: logger.info('Buffering complete') if percent % 5 == 0: event.log_event('playback_buffering', self, percent) return True
def play(self, track, start_at=None, paused=False): """ Starts the playback with the provided track or stops the playback it immediately if none :param track: the track to play or None :type track: :class:`xl.trax.Track` :param start_at: The offset to start playback at, in seconds :param paused: If True, start the track in 'paused' mode .. note:: The following :doc:`events </xl/event>` will be emitted by this method: * `playback_player_start`: indicates the start of playback overall * `playback_track_start`: indicates playback start of a track """ if track is None: self.stop() else: if self.is_stopped(): event.log_event('playback_player_start', self, track) play_args = self._get_play_params(track, start_at, paused, False) self._engine.play(*play_args) if play_args[2]: event.log_event('playback_player_pause', self, track) event.log_event("playback_toggle_pause", self, track)
def stop(self, _fire=True, **kwargs): """ Stops the playback :param fire: Send the 'playback_player_end' event. Used by engines to avoid spurious playback_end events. Not public API. .. note:: The following :doc:`events </xl/event>` will be emitted by this method: * `playback_player_end`: indicates the end of playback overall * `playback_track_end`: indicates playback end of a track """ self._cancel_delayed_start() self._cancel_stop_offset() if self.is_playing() or self.is_paused(): prev_current = self._stop(**kwargs) if _fire: event.log_event('playback_player_end', self, prev_current) return True return False
def _stop(self, _onlyfire=False): """ Stops playback. The following parameters are for internal use only and are not public API. :param onlyfire: Only send the _end event(s), don't actually halt playback. This is used at the end of a playlist, because the gapless mechanism will fire to tell us to load the next track for buffering, but since there isn't one if we actually halt the player the last few moments of the prior track will be cut off. """ self._buffered_track = None current = self.current if not _onlyfire: self._pipe.set_state(gst.STATE_NULL) self._update_playtime() self._current = None event.log_event('playback_track_end', self, current) return current
def unpause(self): """ Resumes the playback if it is paused, does not toggle it :returns: True if paused, False otherwise .. note:: The following :doc:`events </xl/event>` will be emitted by this method: * `playback_player_resume`: indicates that the playback has been resumed * `playback_toggle_pause`: indicates that the playback has been paused or resumed """ self._cancel_delayed_start() if self.is_paused(): self._reset_playtime_stamp() self._engine.unpause() current = self.current event.log_event('playback_player_resume', self, current) event.log_event("playback_toggle_pause", self, current) return True return False
def next(self, autoplay=True, track=None): """ Goes to the next track, either in the queue, or in the current playlist. If a track is passed in, that track is played :param autoplay: play the track in addition to returning it :type autoplay: bool :param track: if passed, play this track :type track: :class:`xl.trax.Track` .. note:: The following :doc:`events </xl/event>` will be emitted by this method: * `playback_playlist_end`: indicates that the end of the queue has been reached """ if track is None: if self.__queue_has_tracks: if not self.__remove_item_on_playback: track = super().next() else: try: track = self.pop(0) except IndexError: pass # reached the end of the internal queue, don't repeat if track is None: self.__queue_has_tracks = False if track is None and self.current_playlist is not self: track = self.current_playlist.next() if autoplay: self.player.play(track) if not track: event.log_event("playback_playlist_end", self, self.current_playlist) return track
def unlink_stream(self, stream): try: current = stream.get_track() pad = stream.get_static_pad("src").get_peer() stream.unlink(self.adder) try: self.adder.release_request_pad(pad) except TypeError: pass gobject.idle_add(stream.set_state, gst.STATE_NULL) try: self.pipe.remove(stream) except gst.RemoveError: logger.debug("Failed to remove stream %s"%stream) if stream in self.streams: self.streams[self.streams.index(stream)] = None event.log_event("playback_track_end", self, current) return True except AttributeError: return True except: common.log_exception(log=logger) return False
def play(self, track, stop_last=True): """ plays the specified track, overriding any currently playing track if the track cannot be played, playback stops completely """ if track is None: self.stop() return False elif stop_last: self.stop(fire=False) else: self.stop(fire=False, onlyfire=True) playing = self.is_playing() if not playing: event.log_event_sync('playback_reconfigure_bins', self, None) self._current = track uri = track.get_loc_for_io() logger.info("Playing %s" % uri) self.reset_playtime_stamp() self.playbin.set_property("uri", uri) if urlparse.urlsplit(uri)[0] == "cdda": self.notify_id = self.playbin.connect('notify::source', self.__notify_source) self.playbin.set_state(gst.STATE_PLAYING) if not playing: event.log_event('playback_player_start', self, track) event.log_event('playback_track_start', self, track) return True
def set_tag_raw(self, tag, values, notify_changed=True): """ Set the raw value of a tag. :param tag: The name of the tag to set. :param values: The value or values to set the tag to. :param notify_changed: whether to send a signal to let other parts of Exaile know there has been an update. Only set this to False if you know that no other parts of Exaile need to be updated. """ # handle values that aren't lists if not isinstance(values, list): if not tag.startswith("__"): # internal tags dont have to be lists values = [values] # TODO: is this needed? why? # for lists, filter out empty values and convert to unicode if isinstance(values, list): values = [ common.to_unicode(x, self.__tags.get('__encoding')) for x in values if x not in (None, '') ] # save some memory by not storing null values. if not values: try: del self.__tags[tag] except KeyError: pass else: self.__tags[tag] = values self._dirty = True if notify_changed: event.log_event("track_tags_changed", self, tag)
def register_provider(self, servicename, provider, target=None): """ Registers a provider for a service. The provider object is used by consumers of the service. Services can be targeted for a specific use. For example, if you have a widget that uses a service 'foo', if your object can perform a service only for a specific type of widget, then target would be set to the widget type. If you had a service that could perform 'foo' for all widgets, then target would be set to None, and all widgets could use your service. It is intended that most services should set target to None, with some narrow exceptions. :param servicename: the name of the service [string] :type servicename: string :param provider: the object that is the provider [object] :type provider: object :param target: a specific target for the service [object] :type target: object """ service = self.services.setdefault(servicename, {}) providers = service.setdefault(target, []) if provider not in providers: providers.append(provider) logger.debug( "Provider %(provider)s registered for service %(service)s " "with target %(target)s" % { 'provider': provider.name, 'service': servicename, 'target': target }) event.log_event("%s_provider_added" % servicename, self, (provider, target))
def _next_track(self, track, already_playing=False): """ internal api: advances the track to the next track """ if track is None: self.stop() return False elif not already_playing: self.stop(_fire=False) else: self.stop(_fire=False, _onlyfire=True) playing = self.is_playing() if not playing: event.log_event('playback_reconfigure_bins', self, None) self._current = track uri = track.get_loc_for_io() logger.info("Playing %s" % common.sanitize_url(uri)) self._reset_playtime_stamp() if not already_playing: self._pipe.set_property("uri", uri) if urlparse.urlsplit(uri)[0] == "cdda": self.notify_id = self._pipe.connect('notify::source', self.__notify_source) self._pipe.set_state(gst.STATE_PLAYING) if not playing: event.log_event('playback_player_start', self, track) event.log_event('playback_track_start', self, track) if playing and self._should_delay_start(): self._last_position = 0 self._setup_startstop_offsets(track) return True
def _run(): on_ui_thread[0] = False event.log_event('test', ucb, None)
def quit(self, restart=False): """ Exits Exaile normally. Takes care of saving preferences, databases, etc. :param restart: Whether to directly restart :type restart: bool """ if self.quitting: return self.quitting = True logger.info("Exaile is shutting down...") logger.info("Disabling plugins...") for k, plugin in self.plugins.enabled_plugins.iteritems(): if hasattr(plugin, 'teardown'): try: plugin.teardown(self) except Exception: pass from xl import event # this event should be used by modules that dont need # to be saved in any particular order. modules that might be # touched by events triggered here should be added statically # below. event.log_event("quit_application", self, None) logger.info("Saving state...") self.plugins.save_enabled() if self.gui: self.gui.quit() from xl import covers covers.MANAGER.save() self.collection.save_to_location() # Save order of custom playlists self.playlists.save_order() self.stations.save_order() # save player, queue from xl import player player.QUEUE._save_player_state( os.path.join(xdg.get_data_dir(), 'player.state')) player.QUEUE.save_to_location( os.path.join(xdg.get_data_dir(), 'queue.state')) player.PLAYER.stop() from xl import settings settings.MANAGER.save() if restart: logger.info("Restarting...") logger_setup.stop_logging() python = sys.executable if sys.platform == 'win32': # Python Win32 bug: it does not quote individual command line # arguments. Here we do it ourselves and pass the whole thing # as one string. # See https://bugs.python.org/issue436259 (closed wontfix). import subprocess cmd = [python] + sys.argv cmd = subprocess.list2cmdline(cmd) os.execl(python, cmd) else: os.execl(python, python, *sys.argv) logger.info("Bye!") logger_setup.stop_logging() sys.exit(0)
def on_provider_removed(self, station): if station.name in self.stations: del self.stations[station.name] event.log_event('station_removed', self, station)
def on_provider_added(self, station): if not station.name in self.stations: self.stations[station.name] = station event.log_event('station_added', self, station)
def set_rating(self, rating): prev_rating = trax.Track.get_rating(self) trax.Track.set_rating(self, rating) event.log_event('doubanfm_track_rating_change', self, (prev_rating, rating))
def play(self, track, user=True): if not track: return # we cant play nothing playing = self.is_playing() logger.debug("Attmepting to play \"%s\""%track) next = 1-self._current_stream if self.streams[next]: self.unlink_stream(self.streams[next]) fading = False duration = 0 if user: if settings.get_option("player/user_fade_enabled", False): fading = True duration = settings.get_option("player/user_fade", 1000) else: self.unlink_stream(self.streams[self._current_stream]) else: if settings.get_option("player/crossfading", False): fading = True duration = settings.get_option( "player/crossfade_duration", 3000) else: self.unlink_stream(self.streams[self._current_stream]) self.streams[next] = AudioStream("Stream%s"%(next), caps=self.caps) self.streams[next].dec.connect("drained", self._on_drained, self.streams[next]) if not self.link_stream(self.streams[next], track): return False if fading: self.streams[next].set_volume(0) self.pipe.set_state(gst.STATE_PLAYING) self.streams[next]._settle_flag = 1 gobject.idle_add(self.streams[next].set_state, gst.STATE_PLAYING) gobject.idle_add(self._set_state, self.pipe, gst.STATE_PLAYING) if fading: timeout = int(float(duration)/float(100)) if self.streams[next]: gobject.timeout_add(timeout, self._fade_stream, self.streams[next], 1) if self.streams[self._current_stream]: gobject.timeout_add(timeout, self._fade_stream, self.streams[self._current_stream], -1, True) if settings.get_option("player/crossfading", False): time = int(track.get_tag_raw("__length")*1000 - duration) gobject.timer_id = gobject.timeout_add(time, self._start_crossfade) self._current_stream = next if not playing: event.log_event('playback_player_start', self, track) event.log_event('playback_track_start', self, track) return True
def on_message(self, bus, message): ''' This is called on the main thread ''' if message.type == Gst.MessageType.BUFFERING: percent = message.parse_buffering() if not percent < 100: self.logger.info('Buffering complete') if percent % 5 == 0: event.log_event('playback_buffering', self.engine.player, percent) elif message.type == Gst.MessageType.TAG: """ Update track length and optionally metadata from gstreamer's parser. Useful for streams and files mutagen doesn't understand. """ current = self.current_track if not current.is_local(): gst_utils.parse_stream_tags(current, message.parse_tag()) if current and not current.get_tag_raw('__length'): res, raw_duration = self.playbin.query_duration( Gst.Format.TIME) if not res: self.logger.error("Couldn't query duration") raw_duration = 0 duration = float(raw_duration) / Gst.SECOND if duration > 0: current.set_tag_raw('__length', duration) elif (message.type == Gst.MessageType.EOS and not self.get_gst_state() == Gst.State.PAUSED): self.engine._eos_func(self) elif (message.type == Gst.MessageType.STREAM_START and message.src == self.playbin and self.buffered_track is not None): # This handles starting the next track during gapless transition buffered_track = self.buffered_track self.buffered_track = None play_args = self.engine.player.engine_autoadvance_notify_next( buffered_track) + (True, True) self.engine._next_track(*play_args) elif message.type == Gst.MessageType.STATE_CHANGED: # This idea from quodlibet: pulsesink will not notify us when # volume changes if the stream is paused, so do it when the # state changes. if message.src == self.audio_sink: self.playbin.notify("volume") elif message.type == Gst.MessageType.ERROR: self.__handle_error_message(message) elif message.type == Gst.MessageType.ELEMENT: if not missing_plugin.handle_message(message, self.engine): logger.debug( "Unexpected element-specific GstMessage received from %s: %s", message.src, message, ) elif message.type == Gst.MessageType.WARNING: # TODO there might be some useful warnings we ignore for now. gerror, debug_text = Gst.Message.parse_warning(message) logger.warn( "Unhandled GStreamer warning received:\n\tGError: %s\n\tDebug text: %s", gerror, debug_text, ) else: # TODO there might be some useful messages we ignore for now. logger.debug("Unhandled GstMessage of type %s received: %s", message.type, message)
def __notify_user_on_error(message_text, engine): event.log_event('playback_error', engine.player, message_text)
def __init(self): """ Initializes Exaile """ # pylint: disable-msg=W0201 logger.info("Loading Exaile %s on Python %s..." % (__version__, platform.python_version())) logger.info("Loading settings...") try: from xl import settings except common.VersionError: logger.exception("Error loading settings") sys.exit(1) logger.debug("Settings loaded from %s" % settings.location) # display locale information if available try: import locale lc, enc = locale.getlocale() if enc is not None: logger.info("Using %s %s locale" % (lc, enc)) else: logger.info("Using unknown locale") except Exception: pass splash = None if self.options.StartGui: if settings.get_option('gui/use_splash', True): from xlgui.widgets.info import Splash splash = Splash() splash.show() firstrun = settings.get_option("general/first_run", True) if not self.options.NoImport and \ (firstrun or self.options.ForceImport): try: sys.path.insert(0, xdg.get_data_path("migrations")) import migration_200907100931 as migrator del sys.path[0] migrator.migrate(force=self.options.ForceImport) del migrator except Exception: logger.exception("Failed to migrate from 0.2.14") # Migrate old rating options from xl.migrations.settings import rating rating.migrate() # Migrate builtin OSD to plugin from xl.migrations.settings import osd osd.migrate() # Migrate engines from xl.migrations.settings import engine engine.migrate() # TODO: enable audio plugins separately from normal # plugins? What about plugins that use the player? # Gstreamer doesn't initialize itself automatically, and fails # miserably when you try to inherit from something and GST hasn't # been initialized yet. So this is here. from gi.repository import Gst Gst.init(None) # Initialize plugin manager from xl import plugins self.plugins = plugins.PluginsManager(self) if not self.options.SafeMode: logger.info("Loading plugins...") self.plugins.load_enabled() else: logger.info("Safe mode enabled, not loading plugins.") # Initialize the collection logger.info("Loading collection...") from xl import collection try: self.collection = collection.Collection("Collection", location=os.path.join( xdg.get_data_dir(), 'music.db')) except common.VersionError: logger.exception("VersionError loading collection") sys.exit(1) from xl import event # Set up the player and playback queue from xl import player event.log_event("player_loaded", player.PLAYER, None) # Initalize playlist manager from xl import playlist self.playlists = playlist.PlaylistManager() self.smart_playlists = playlist.SmartPlaylistManager( 'smart_playlists', collection=self.collection) if firstrun: self._add_default_playlists() event.log_event("playlists_loaded", self, None) # Initialize dynamic playlist support from xl import dynamic dynamic.MANAGER.collection = self.collection # Initalize device manager logger.info("Loading devices...") from xl import devices self.devices = devices.DeviceManager() event.log_event("device_manager_ready", self, None) # Initialize dynamic device discovery interface # -> if initialized and connected, then the object is not None self.udisks2 = None self.udisks = None self.hal = None if self.options.Hal: from xl import hal udisks2 = hal.UDisks2(self.devices) if udisks2.connect(): self.udisks2 = udisks2 else: udisks = hal.UDisks(self.devices) if udisks.connect(): self.udisks = udisks else: self.hal = hal.HAL(self.devices) self.hal.connect() else: self.hal = None # Radio Manager from xl import radio self.stations = playlist.PlaylistManager('radio_stations') self.radio = radio.RadioManager() self.gui = None # Setup GUI if self.options.StartGui: logger.info("Loading interface...") import xlgui self.gui = xlgui.Main(self) self.gui.main.window.show_all() event.log_event("gui_loaded", self, None) if splash is not None: splash.destroy() if firstrun: settings.set_option("general/first_run", False) self.loading = False Exaile._exaile = self event.log_event("exaile_loaded", self, None) restore = True if self.gui: # Find out if the user just passed in a list of songs # TODO: find a better place to put this songs = [ Gio.File.new_for_path(arg).get_uri() for arg in self.options.locs ] if len(songs) > 0: restore = False self.gui.open_uri(songs[0], play=True) for arg in songs[1:]: self.gui.open_uri(arg) # kick off autoscan of libraries # -> don't do it in command line mode, since that isn't expected self.gui.rescan_collection_with_progress(True) if restore: player.QUEUE._restore_player_state( os.path.join(xdg.get_data_dir(), 'player.state'))
def rescan(self, notify_interval: Optional[int] = None, force_update: bool = False): # TODO: What return type? """ Rescan the associated folder and add the contained files to the Collection """ # TODO: use gio's cancellable support if self.collection is None: return True if self.scanning: return logger.info("Scanning library: %s", self.location) self.scanning = True libloc = Gio.File.new_for_uri(self.location) count = 0 dirtracks = deque() compilations = deque() ccheck = {} for fil in common.walk(libloc): count += 1 type = fil.query_info("standard::type", Gio.FileQueryInfoFlags.NONE, None).get_file_type() if type == Gio.FileType.DIRECTORY: if dirtracks: for tr in dirtracks: self._check_compilation(ccheck, compilations, tr) for (basedir, album) in compilations: base = basedir.replace('"', '\\"') alb = album.replace('"', '\\"') items = [ tr for tr in dirtracks if tr.get_tag_raw('__basedir') == base and # FIXME: this is ugly alb in "".join(tr.get_tag_raw('album') or []).lower() ] for item in items: item.set_tag_raw('__compilation', (basedir, album)) dirtracks = deque() compilations = deque() ccheck = {} elif type == Gio.FileType.REGULAR: tr = self.update_track(fil, force_update=force_update) if not tr: continue if dirtracks is not None: dirtracks.append(tr) # do this so that if we have, say, a 4000-song folder # we dont get bogged down trying to keep track of them # for compilation detection. Most albums have far fewer # than 110 tracks anyway, so it is unlikely that this # restriction will affect the heuristic's accuracy. # 110 was chosen to accomodate "top 100"-style # compilations. if len(dirtracks) > 110: logger.debug( "Too many files, skipping " "compilation detection heuristic for %s", fil.get_uri(), ) dirtracks = None if self.collection and self.collection._scan_stopped: self.scanning = False logger.info("Scan canceled") return # progress update if notify_interval is not None and count % notify_interval == 0: event.log_event('tracks_scanned', self, count) # final progress update if notify_interval is not None: event.log_event('tracks_scanned', self, count) removals = deque() for tr in self.collection.tracks.values(): tr = tr._track loc = tr.get_loc_for_io() if not loc: continue gloc = Gio.File.new_for_uri(loc) try: if not gloc.has_prefix(libloc): continue except UnicodeDecodeError: logger.exception("Error decoding file location") continue if not gloc.query_exists(None): removals.append(tr) for tr in removals: logger.debug("Removing %s", tr) self.collection.remove(tr) logger.info("Scan completed: %s", self.location) self.scanning = False