async def test_add_and_remove_symbol(app_mock, signal_mgr, signal, mocker): """ test add slot symbol """ signal_mgr.initialize(app_mock) Signal.setup_aio_support() app_mock.test_signal = signal func = mock.MagicMock() func.__name__ = 'fake_func' mock_F = mocker.patch('feeluown.fuoexec.signal_manager.fuoexec_F', return_value=func) signal_mgr.add('app.test_signal', func, use_symbol=True) signal.emit() await asyncio.sleep(0.1) mock_F.assert_called_once_with('fake_func') # fuoexec_F should be called once. assert func.called assert func.call_count == 1 signal_mgr.remove('app.test_signal', func, use_symbol=True) signal.emit() await asyncio.sleep(0.1) # fuoexec_F should not be called anymore assert func.call_count == 1 Signal.teardown_aio_support()
def __init__(self, args, config): self.mode = config.MODE # DEPRECATED: use app.config.MODE instead self.config = config self.args = args self.initialized = Signal() self.about_to_shutdown = Signal() self.plugin_mgr = PluginsManager(self) self.request = Request() # TODO: rename request to http self.version_mgr = VersionManager(self) self.task_mgr = TaskManager(self) # Library. self.library = Library(config.PROVIDERS_STANDBY) # TODO: initialization should be moved into initialize Resolver.library = self.library # Player. self.player = Player( audio_device=bytes(config.MPV_AUDIO_DEVICE, 'utf-8')) self.playlist = Playlist( self, audio_select_policy=config.AUDIO_SELECT_POLICY) self.live_lyric = LiveLyric(self) self.fm = FM(self) # TODO: initialization should be moved into initialize self.player.set_playlist(self.playlist) self.about_to_shutdown.connect(lambda _: self.dump_state(), weak=False)
async def test_signals_slots_mgr(app_mock, signal): signals_slots_mgr = SignalsSlotsManager() Signal.setup_aio_support() func = mock.MagicMock() app_mock.test_signal = signal # test if add method works signals_slots_mgr.add('app.test_signal', func) signals_slots_mgr.initialize(app_mock) signal.emit() await asyncio.sleep(0.1) # schedule signal callbacks with aioqueue enabled assert func.called is True # test if add method works after initialized func_lator = mock.MagicMock() signals_slots_mgr.add('app.test_signal', func_lator) signal.emit() await asyncio.sleep(0.1) assert func_lator.called is True # test if remove method works signals_slots_mgr.remove('app.test_signal', func_lator) signal.emit() await asyncio.sleep(0.1) assert func_lator.call_count == 1 assert func.call_count == 3 Signal.teardown_aio_support()
def __init__(self, config): self.mode = config.MODE # DEPRECATED: use app.config.MODE instead self.config = config self.initialized = Signal() self.about_to_shutdown = Signal() self.initialized.connect(lambda _: self.load_state(), weak=False) self.about_to_shutdown.connect(lambda _: self.dump_state(), weak=False)
def about_to_exit(self): try: logger.info('Do graceful shutdown') self.about_to_shutdown.emit(self) Signal.teardown_aio_support() self.player.stop() self.exit_player() except: # noqa logger.exception("about-to-exit failed")
def __init__(self, app): super().__init__() self._app = app self._plugins = {} #: A plugin is about to enable. # The payload is the plugin object `(Plugin)`. # .. versionadded: 3.7.15 self.about_to_enable = Signal()
def __init__(self): self.sentence_changed = Signal(str) self._lyric = None self._pos_s_map = {} # position sentence map self._pos_list = [] # position list self._pos = None self._current_sentence = ''
def __init__(self, app, songs=None, playback_mode=PlaybackMode.loop, audio_select_policy='hq<>'): """ :param songs: list of :class:`feeluown.models.SongModel` :param playback_mode: :class:`feeluown.player.PlaybackMode` """ self._app = app #: mainthread asyncio loop ref # We know that feeluown is a asyncio-app, and we can assume # that the playlist is inited in main thread. self._loop = asyncio.get_event_loop() #: init playlist mode normal self._mode = PlaylistMode.normal #: playlist eof signal # playlist have no enough songs self.eof_reached = Signal() #: playlist mode changed signal self.mode_changed = Signal() #: store value for ``current_song`` property self._current_song = None #: songs whose url is invalid self._bad_songs = DedupList() #: store value for ``songs`` property self._songs = DedupList(songs or []) self.audio_select_policy = audio_select_policy #: store value for ``playback_mode`` property self._playback_mode = playback_mode #: playback mode changed signal self.playback_mode_changed = Signal() self.song_changed = Signal() """current song changed signal The player will play the song after it receive the signal, when song is None, the player will stop playback. """ self.song_changed_v2 = Signal() """current song chagned signal, v2 emit(song, media) """ #: When watch mode is on, playlist try to play the mv/video of the song self.watch_mode = False
async def start_app(args, config, sentinal=None): """ The param sentinal is currently only used for unittest. """ Signal.setup_aio_support() app = create_app(args, config) # Do fuoexec initialization before app initialization. fuoexec_init(app) # Initialize app with config. # # all objects can do initialization here. some objects may emit signal, # some objects may connect with others signals. app.initialize() app.initialized.emit(app) # Load last state. app.load_state() def sighanlder(signum, _): logger.info('Signal %d is received', signum) app.exit() # Handle signals. signal.signal(signal.SIGTERM, sighanlder) signal.signal(signal.SIGINT, sighanlder) if sentinal is None: sentinal: asyncio.Future = asyncio.Future() def shutdown(_): # Since about_to_shutdown signal may emit multiple times # (QApplication.aboutToQuit emits multiple times), # we should check if it is already done firstly. if not sentinal.done(): sentinal.set_result(0) app.about_to_shutdown.connect(shutdown, weak=False) # App can exit in several ways. # # GUI mode: # 1. QApplication.quit. QApplication.quit can be called under several circumstances # 1. User press CMD-Q on macOS. # 2. User clicks the tray icon exit button. # 2. SIGTERM is received. # # Daemon mode: # 1. Ctrl-C # 2. SIGTERM app.run() await sentinal Signal.teardown_aio_support()
def __init__(self, providers_standby=None): """ :type app: feeluown.app.App """ self._providers_standby = providers_standby self._providers = set() self.provider_added = Signal() # emit(AbstractProvider) self.provider_removed = Signal() # emit(AbstractProvider)
async def test_add_and_remove(app_mock, signal_mgr, signal): Signal.setup_aio_support() func = mock.MagicMock() app_mock.test_signal = signal # test if add method works signal_mgr.add('app.test_signal', func, use_symbol=False) signal_mgr.initialize(app_mock) signal.emit() await asyncio.sleep(0.1) # schedule signal callbacks with aioqueue enabled assert func.called is True
def __init__(self, app): """ :type app: feeluown.app.App """ self._app = app self.sentence_changed = Signal(str) self._lyric = None self._pos_s_map: Dict[int, str] = {} # position sentence map self._pos_list: List[int] = [] # position list self._pos = None self._current_sentence = ''
def __init__(self, app, config): """ :type app: feeluown.app.App """ self._tasks = [] self._task_queue = asyncio.Queue() #: emit List[DownloadTask] self.tasks_changed = Signal() self.download_finished = Signal() self.downloader: Downloader = AioRequestsDownloader() self._path = config.DOWNLOAD_DIR or DEFAULT_DOWNLOAD_DIR
def __init__(self, _=None, audio_device=b'auto', winid=None, **kwargs): """ :param _: keep this arg to keep backward compatibility """ super().__init__(**kwargs) # https://github.com/cosven/FeelUOwn/issues/246 locale.setlocale(locale.LC_NUMERIC, 'C') mpvkwargs = {} if winid is not None: mpvkwargs['wid'] = winid self._version = _mpv_client_api_version() # old version libmpv can use opengl-cb if self._version < (1, 107): mpvkwargs['vo'] = 'opengl-cb' self.use_opengl_cb = True else: self.use_opengl_cb = False # set log_handler if you want to debug # mpvkwargs['log_handler'] = self.__log_handler # mpvkwargs['msg_level'] = 'all=v' # the default version of libmpv on Ubuntu 18.04 is (1, 25) self._mpv = MPV(ytdl=False, input_default_bindings=True, input_vo_keyboard=True, **mpvkwargs) _mpv_set_property_string(self._mpv.handle, b'audio-device', audio_device) # old version libmpv(for example: (1, 20)) should set option by using # _mpv_set_option_string, while newer version can use _mpv_set_property_string _mpv_set_option_string(self._mpv.handle, b'user-agent', b'Mozilla/5.0 (Windows NT 10.0; Win64; x64)') #: if video_format changes to None, there is no video available self.video_format_changed = Signal() self._mpv.observe_property( 'time-pos', lambda name, position: self._on_position_changed(position)) self._mpv.observe_property( 'duration', lambda name, duration: self._on_duration_changed(duration)) self._mpv.observe_property( 'video-format', lambda name, vformat: self._on_video_format_changed(vformat)) # self._mpv.register_event_callback(lambda event: self._on_event(event)) self._mpv._event_callbacks.append(self._on_event) logger.debug('Player initialize finished.')
def __init__(self, parent=None): super().__init__(parent) QTableView.__init__(self, parent) # override ItemViewNoScrollMixin variables self._least_row_count = 6 self._row_height = 40 # slot functions self.remove_song_func = None self.delegate = SongsTableDelegate(self) self.setItemDelegate(self.delegate) self.about_to_show_menu = Signal() self._setup_ui()
def __init__(self, name, text, symbol, desc): # 如果需要,可以支持右键弹出菜单 self._name = name self.text = text self.symbol = symbol self.desc = desc self.clicked = Signal()
async def test_add_and_remove_after_initialization(app_mock, signal_mgr, signal): Signal.setup_aio_support() # test if add method works after initialized func_lator = mock.MagicMock() app_mock.test_signal = signal signal_mgr.initialize(app_mock) signal_mgr.add('app.test_signal', func_lator, use_symbol=False) signal.emit() await asyncio.sleep(0.1) assert func_lator.called is True # test if remove method works signal_mgr.remove('app.test_signal', func_lator, use_symbol=False) signal.emit() await asyncio.sleep(0.1) assert func_lator.call_count == 1 Signal.teardown_aio_support()
def test_receiver(self, mock_connect): s = Signal() @receiver(s) def f(): pass self.assertTrue(mock_connect.called)
def __init__(self, name, text, symbol, desc, colorful_svg=None, provider=None): # 如果需要,可以支持右键弹出菜单 self._name = name self.text = text self.symbol = symbol self.desc = desc self.colorful_svg = colorful_svg self.provider = provider self.clicked = Signal()
async def test_signals_slots_mgr_add_slot_symbol(app_mock, signal, mocker): """ test add slot symbol """ signals_slots_mgr = SignalsSlotsManager() signals_slots_mgr.initialize(app_mock) Signal.setup_aio_support() app_mock.test_signal = signal func = mock.MagicMock() func.__name__ = 'fake_func' mock_F = mocker.patch('feeluown.fuoexec.fuoexec_F', return_value=func) signals_slots_mgr.add('app.test_signal', 'fake_func') signal.emit() await asyncio.sleep(0.1) mock_F.assert_called_once_with('fake_func') assert func.called is True Signal.teardown_aio_support()
def __init__(self, app, *args, **kwargs): super().__init__(*args, **kwargs) self._app = app #: mainthread asyncio loop ref # We know that feeluown is a asyncio-app, and we can assume # that the playlist is inited in main thread. self._loop = asyncio.get_event_loop() #: find-song-standby task self._task = None #: init playlist mode normal self._mode = PlaylistMode.normal #: playlist eof signal # playlist have no enough songs self.eof_reached = Signal() #: playlist mode changed signal self.mode_changed = Signal()
def __init__(self, songs=None, playback_mode=PlaybackMode.loop, audio_select_policy='hq<>'): """ :param songs: list of :class:`feeluown.models.SongModel` :param playback_mode: :class:`feeluown.player.PlaybackMode` """ #: store value for ``current_song`` property self._current_song = None #: songs whose url is invalid self._bad_songs = DedupList() #: store value for ``songs`` property self._songs = DedupList(songs or []) self.audio_select_policy = audio_select_policy #: store value for ``playback_mode`` property self._playback_mode = playback_mode #: playback mode changed signal self.playback_mode_changed = Signal() self.song_changed = Signal() """current song changed signal The player will play the song after it receive the signal, when song is None, the player will stop playback. """ self.song_changed_v2 = Signal() """current song chagned signal, v2
def reinit_fuoexec(app_mock): """ fuoexec can be only initialized once, but we should initialize it for each test case. This function does this trick. """ g = fuoexec_get_globals().copy() app_mock.initialized = Signal() fuoexec_init(app_mock) yield fuoexec_get_globals().clear() fuoexec_get_globals().update(g) signal_mgr.initialized = False signal_mgr.signal_connectors.clear()
def test_connect1(self): with mock.patch.object(A, 'f', return_value=None) as mock_method_f: s = Signal() # pay attention self.assertTrue(self.a1.f == self.a2.f == mock_method_f) s.connect(self.a1.f) s.emit(1, 'hello') mock_method_f.assert_called_once_with(1, 'hello')
class SongsTableView(ItemViewNoScrollMixin, QTableView): show_artist_needed = pyqtSignal([object]) show_album_needed = pyqtSignal([object]) play_song_needed = pyqtSignal([object]) add_to_playlist_needed = pyqtSignal(list) def __init__(self, parent=None): super().__init__(parent) QTableView.__init__(self, parent) # override ItemViewNoScrollMixin variables self._least_row_count = 6 self._row_height = 40 # slot functions self.remove_song_func = None self.delegate = SongsTableDelegate(self) self.setItemDelegate(self.delegate) self.about_to_show_menu = Signal() self._setup_ui() def _setup_ui(self): self.horizontalHeader().setSectionResizeMode( QHeaderView.ResizeToContents) # FIXME: PyQt5 seg fault # self.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setFrameShape(QFrame.NoFrame) self.horizontalHeader().setStretchLastSection(True) self.verticalHeader().hide() self.horizontalHeader().hide() self.verticalHeader().setDefaultSectionSize(self._row_height) self.setWordWrap(False) self.setTextElideMode(Qt.ElideRight) self.setMouseTracking(True) self.setEditTriggers(QAbstractItemView.SelectedClicked) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setShowGrid(False) self.setDragEnabled(True) self.setDragDropMode(QAbstractItemView.DragOnly) def show_artists_by_index(self, index): self.edit(index) def contextMenuEvent(self, event): indexes = self.selectionModel().selectedIndexes() if len(indexes) <= 0: return menu = QMenu() # add to playlist action add_to_playlist_action = QAction('添加到播放队列', menu) add_to_playlist_action.triggered.connect( lambda: self._add_to_playlist(indexes)) menu.addAction(add_to_playlist_action) # remove song action if self.remove_song_func is not None: remove_song_action = QAction('移除歌曲', menu) remove_song_action.triggered.connect( lambda: self._remove_by_indexes(indexes)) menu.addSeparator() menu.addAction(remove_song_action) model = self.model() models = [model.data(index, Qt.UserRole) for index in indexes] def add_action(text, callback): action = QAction(text, menu) menu.addSeparator() menu.addAction(action) action.triggered.connect(lambda: callback(models)) # .. versionadded: v3.7 # The context key *models* self.about_to_show_menu.emit({ 'add_action': add_action, 'models': models }) menu.exec(event.globalPos()) def _add_to_playlist(self, indexes): model = self.model() songs = [] for index in indexes: song = model.data(index, Qt.UserRole) songs.append(song) self.add_to_playlist_needed.emit(songs) def _remove_by_indexes(self, indexes): model = self.model() source_model = model.sourceModel() distinct_rows = set() for index in indexes: row = index.row() if row not in distinct_rows: song = model.data(index, Qt.UserRole) self.remove_song_func(song) distinct_rows.add(row) source_model.removeRows(indexes[0].row(), len(distinct_rows)) def mouseReleaseEvent(self, e): if e.button() in (Qt.BackButton, Qt.ForwardButton): e.ignore() else: super().mouseReleaseEvent(e)
def __init__(self, _=None, **kwargs): """ :param _: keep this arg to keep backward compatibility """ self._position = 0 # seconds self._volume = 100 # (0, 100) self._playlist = None self._state = State.stopped self._duration = None self._current_media = None self._current_metadata = Metadata() #: player position changed signal self.position_changed = Signal() self.seeked = Signal() #: player state changed signal self.state_changed = Signal() #: current media finished signal self.media_finished = Signal() #: duration changed signal self.duration_changed = Signal() #: media about to change: (old_media, media) self.media_about_to_changed = Signal() #: media changed signal self.media_changed = Signal() self.media_loaded = Signal() self.metadata_changed = Signal() #: volume changed signal: (int) self.volume_changed = Signal()
class AbstractPlayer(metaclass=ABCMeta): """Player abstrace base class""" def __init__(self, _=None, **kwargs): """ :param _: keep this arg to keep backward compatibility """ self._position = 0 # seconds self._volume = 100 # (0, 100) self._playlist = None self._state = State.stopped self._duration = None self._current_media = None self._current_metadata = Metadata() #: player position changed signal self.position_changed = Signal() self.seeked = Signal() #: player state changed signal self.state_changed = Signal() #: current media finished signal self.media_finished = Signal() #: duration changed signal self.duration_changed = Signal() #: media about to change: (old_media, media) self.media_about_to_changed = Signal() #: media changed signal self.media_changed = Signal() self.media_loaded = Signal() self.metadata_changed = Signal() #: volume changed signal: (int) self.volume_changed = Signal() @property def state(self): """Player state :rtype: State """ return self._state @state.setter def state(self, value): """set player state, emit state changed signal outer object should not set state directly, use ``pause`` / ``resume`` / ``stop`` / ``play`` method instead. """ self._state = value self.state_changed.emit(value) @property def current_media(self): return self._current_media @property def current_metadata(self) -> Metadata: """Metadata for the current media Check `MetadataFields` for all possible fields. Note that some fields can be missed if them are unknown. For example, a video's metadata may have no genre info. """ return self._current_metadata @property def position(self): """player position, the units is seconds""" return self._position @position.setter def position(self, position): """set player position, the units is seconds""" @property def volume(self): return self._volume @volume.setter def volume(self, value): value = 0 if value < 0 else value value = 100 if value > 100 else value self._volume = value self.volume_changed.emit(value) @property def duration(self): """player media duration, the units is seconds""" return self._duration @duration.setter def duration(self, value): value = value or 0 if value != self._duration: self._duration = value self.duration_changed.emit(value) @abstractmethod def play(self, media, video=True, metadata=None): """play media :param media: a local file absolute path, or a http url that refers to a media file :param video: show video or not :param metadata: metadata for the media """ @abstractmethod def set_play_range(self, start=None, end=None): pass @abstractmethod def resume(self): """play playback""" @abstractmethod def pause(self): """pause player""" @abstractmethod def toggle(self): """toggle player state""" @abstractmethod def stop(self): """stop player""" @abstractmethod def shutdown(self): """shutdown player, do some clean up here""" # ------------------ # Deprecated methods # ------------------ @property def playlist(self): """(DEPRECATED) player playlist Player SHOULD not know the existence of playlist. However, in the very beginning, the player depends on playlist and listen playlist's signal. Other programs may depends on the playlist property and we keep it for backward compatibility. TODO: maybe add a DeprecationWarning in v3.8. :return: :class:`.Playlist` """ return self._playlist def set_playlist(self, playlist): self._playlist = playlist @property def current_song(self): """(Deprecated) alias of playlist.current_song Please use playlist.current_song instead. """ warnings.warn('use playlist.current_model instead', DeprecationWarning) return self._playlist.current_song def load_song(self, song) -> asyncio.Task: """加载歌曲 如果目标歌曲与当前歌曲不相同,则修改播放列表当前歌曲, 播放列表会发出 song_changed 信号,player 监听到信号后调用 play 方法, 到那时才会真正的播放新的歌曲。如果和当前播放歌曲相同,则忽略。 .. note:: 调用方应该直接调用 playlist.current_song = song 来切换歌曲 """ assert song is not None warnings.warn( 'use playlist.set_current_model instead, this will be removed in v3.8', DeprecationWarning) return self._playlist.set_current_song(song) def play_song(self, song): """加载并播放指定歌曲""" warnings.warn( 'use playlist.set_current_model instead, this will be removed in v3.8', DeprecationWarning) return self._playlist.set_current_song(song) def play_songs(self, songs): """(alpha) play list of songs""" warnings.warn( 'use playlist.init_from instead, this will be removed in v3.8', DeprecationWarning) self.playlist.set_models(songs, next_=True)
class Library: """音乐库,管理资源提供方以及资源""" def __init__(self, providers_standby=None): """ :type app: feeluown.app.App """ self._providers_standby = providers_standby self._providers = set() self.provider_added = Signal() # emit(AbstractProvider) self.provider_removed = Signal() # emit(AbstractProvider) def register(self, provider): """register provider :raises ProviderAlreadyExists: :raises ValueError: >>> from feeluown.library import dummy_provider >>> library = Library(None) >>> library.register(dummy_provider) >>> library.register(dummy_provider) Traceback (most recent call last): ... feeluown.library.ProviderAlreadyExists """ if not isinstance(provider, AbstractProvider): raise ValueError('invalid provider instance') for _provider in self._providers: if _provider.identifier == provider.identifier: raise ProviderAlreadyExists self._providers.add(provider) self.provider_added.emit(provider) def deregister(self, provider): """deregister provider""" try: self._providers.remove(provider) except ValueError: raise ProviderNotFound from None else: self.provider_removed.emit(provider) def get(self, identifier): """通过资源提供方唯一标识获取提供方实例""" for provider in self._providers: if provider.identifier == identifier: return provider return None def list(self): """列出所有资源提供方""" return list(self._providers) def _filter(self, identifier_in=None): if identifier_in is None: return iter(self._providers) return filter(lambda p: p.identifier in identifier_in, self.list()) def search(self, keyword, type_in=None, source_in=None, **kwargs): """search song/artist/album/playlist by keyword please use a_search method if you can. :param keyword: search keyword :param type_in: search type :param source_in: None or provider identifier list - TODO: support search with filters(by artist or by source) """ type_in = SearchType.batch_parse(type_in) if type_in else [ SearchType.so ] for provider in self._filter(identifier_in=source_in): for type_ in type_in: try: result = provider.search(keyword=keyword, type_=type_, **kwargs) except Exception: # pylint: disable=broad-except logger.exception('Search %s in %s failed.', keyword, provider) else: if result is not None: yield result async def a_search(self, keyword, source_in=None, timeout=None, type_in=None, **kwargs): """async version of search TODO: add Happy Eyeballs requesting strategy if needed """ type_in = SearchType.batch_parse(type_in) if type_in else [ SearchType.so ] fs = [] # future list for provider in self._filter(identifier_in=source_in): for type_ in type_in: future = aio.run_in_executor( None, partial(provider.search, keyword, type_=type_)) fs.append(future) for future in aio.as_completed(fs, timeout=timeout): try: result = await future except: # noqa logger.exception('search task failed') continue else: yield result @log_exectime def list_song_standby(self, song, onlyone=True): """try to list all valid standby Search a song in all providers. The typical usage scenario is when a song is not available in one provider, we can try to acquire it from other providers. FIXME: this method will send several network requests, which may block the caller. :param song: song model :param onlyone: return only one element in result :return: list of songs (maximum count: 2) """ valid_sources = [ pvd.identifier for pvd in self.list() if pvd.identifier != song.source ] q = '{} {}'.format(song.title, song.artists_name) result_g = self.search(q, source_in=valid_sources) sorted_standby_list = _extract_and_sort_song_standby_list( song, result_g) # choose one or two valid standby result = [] for standby in sorted_standby_list: if standby.url: # this may trigger network request result.append(standby) if onlyone or len(result) >= 2: break return result async def a_list_song_standby(self, song, onlyone=True, source_in=None): """async version of list_song_standby .. versionadded:: 3.7.5 The *source_in* paramter. """ if source_in is None: pvd_ids = self._providers_standby or [ pvd.identifier for pvd in self.list() ] else: pvd_ids = [ pvd.identifier for pvd in self._filter(identifier_in=source_in) ] # FIXME(cosven): the model return from netease is new model, # and it does not has url attribute valid_providers = [ pvd_id for pvd_id in pvd_ids if pvd_id != song.source and pvd_id != 'netease' ] q = '{} {}'.format(song.title_display, song.artists_name_display) result_g = [] async for result in self.a_search(q, source_in=valid_providers): if result is not None: result_g.append(result) sorted_standby_list = _extract_and_sort_song_standby_list( song, result_g) # choose one or two valid standby result = [] for standby in sorted_standby_list: try: url = await aio.run_in_executor(None, lambda: standby.url) except: # noqa logger.exception('get standby url failed') else: if url: result.append(standby) if onlyone or len(result) >= 2: break return result # # methods for v2 # # provider common def get_or_raise(self, identifier) -> ProviderV2: """ :raises ProviderNotFound: """ provider = self.get(identifier) if provider is None: raise ProviderNotFound(f'provider {identifier} not found') return provider def check_flags(self, source: str, model_type: ModelType, flags: PF) -> bool: """Check if a provider satisfies the specific ability for a model type .. note:: Currently, we use ProviderFlags to define which ability a provider has. In the future, we may use typing.Protocol. So you should use :meth:`check_flags` method to check ability instead of compare provider flags directly. """ provider = self.get(source) if provider is None: return False if isinstance(provider, ProviderV2): return provider.check_flags(model_type, flags) return False def check_flags_by_model(self, model: ModelProtocol, flags: PF) -> bool: """Alias for check_flags""" return self.check_flags(model.source, ModelType(model.meta.model_type), flags) # --------------------------- # Methods for backward compat # --------------------------- def cast_model_to_v1(self, model): """Cast a v1/v2 model to v1 During the model migration from v1 to v2, v2 may lack some ability. Cast the model to v1 to acquire such ability. """ if isinstance(model, BaseModel) and (model.meta.flags & MF.v2): return self._cast_model_to_v1_impl(model) return model @lru_cache(maxsize=1024) def _cast_model_to_v1_impl(self, model): provider = self.get_or_raise(model.source) ModelCls = provider.get_model_cls(model.meta.model_type) kv = {} for field in ModelCls.meta.fields_display: kv[field] = getattr(model, field) return ModelCls.create_by_display(identifier=model.identifier, **kv) # ----- # Songs # ----- def song_upgrade(self, song: BriefSongProtocol) -> SongProtocol: if song.meta.flags & MF.v2: if MF.normal in song.meta.flags: upgraded_song = cast(SongProtocol, song) else: provider = self.get_or_raise(song.source) if self.check_flags_by_model(song, PF.get): upgraded_song = provider.song_get(song.identifier) else: raise NotSupported( "provider has not flag 'get' for 'song'") else: fields = [ f for f in list(SongModel.__fields__) if f not in list(BaseModel.__fields__) ] for field in fields: getattr(song, field) upgraded_song = cast(SongProtocol, song) return upgraded_song def song_list_similar(self, song: BriefSongProtocol) -> List[BriefSongProtocol]: provider = self.get_or_raise(song.source) return provider.song_list_similar(song) def song_list_hot_comments(self, song: BriefSongProtocol): provider = self.get_or_raise(song.source) return provider.song_list_hot_comments(song) def song_prepare_media(self, song: BriefSongProtocol, policy) -> Media: provider = self.get(song.source) if provider is None: raise MediaNotFound(f'provider:{song.source} not found') if song.meta.flags & MF.v2: # provider MUST has multi_quality flag for song assert self.check_flags_by_model(song, PF.multi_quality) media, _ = provider.song_select_media(song, policy) else: if song.meta.support_multi_quality: media, _ = song.select_media(policy) # type: ignore else: url = song.url # type: ignore if url: media = Media(url) else: raise MediaNotFound if not media: raise MediaNotFound return media def song_prepare_mv_media(self, song: BriefSongProtocol, policy) -> Media: """ .. versionadded:: 3.7.5 """ provider = self.get(song.source) if provider is None: raise MediaNotFound(f'provider:{song.source} not found') song_v1 = self.cast_model_to_v1(song) mv = song_v1.mv if mv.meta.support_multi_quality: media, _ = mv.select_media(policy) else: media = mv.media if media: media = Media(media) else: media = None if not media: raise MediaNotFound return media def song_get_lyric(self, song: BriefSongModel): pass def song_get_mv(self, song: BriefSongModel): pass # -------- # Provider # -------- def provider_has_current_user(self, source: str) -> bool: """Check if a provider has a logged in user No IO operation is triggered. .. versionadded:: 3.7.6 """ provider = self.get_or_raise(source) if self.check_flags(source, ModelType.none, PF.current_user): return provider.has_current_user() try: user_v1 = getattr(provider, '_user') except AttributeError: logger.warn( "We can't determine if the provider has a current user") return False else: return user_v1 is not None def provider_get_current_user(self, source: str) -> UserProtocol: """Get provider current logged in user :raises NotSupported: :raises ProviderNotFound: :raises NoUserLoggedIn: .. versionadded:: 3.7.6 """ provider = self.get_or_raise(source) if self.check_flags(source, ModelType.none, PF.current_user): return provider.get_current_user() user_v1 = getattr(provider, '_user', None) if user_v1 is None: raise NoUserLoggedIn return UserModel(identifier=user_v1.identifier, source=source, name=user_v1.name_display, avatar_url='')
class Playlist: """player playlist provide a list of song model to play """ def __init__(self, songs=None, playback_mode=PlaybackMode.loop, audio_select_policy='hq<>'): """ :param songs: list of :class:`feeluown.models.SongModel` :param playback_mode: :class:`feeluown.player.PlaybackMode` """ #: store value for ``current_song`` property self._current_song = None #: songs whose url is invalid self._bad_songs = DedupList() #: store value for ``songs`` property self._songs = DedupList(songs or []) self.audio_select_policy = audio_select_policy #: store value for ``playback_mode`` property self._playback_mode = playback_mode #: playback mode changed signal self.playback_mode_changed = Signal() self.song_changed = Signal() """current song changed signal The player will play the song after it receive the signal, when song is None, the player will stop playback. """ self.song_changed_v2 = Signal() """current song chagned signal, v2 emit(song, media) """ def __len__(self): return len(self._songs) def __getitem__(self, index): """overload [] operator""" return self._songs[index] def mark_as_bad(self, song): if song in self._songs and song not in self._bad_songs: self._bad_songs.append(song) def add(self, song): """往播放列表末尾添加一首歌曲""" if song in self._songs: return self._songs.append(song) logger.debug('Add %s to player playlist', song) def insert(self, song): """在当前歌曲后插入一首歌曲""" if song in self._songs: return if self._current_song is None: self._songs.append(song) else: index = self._songs.index(self._current_song) self._songs.insert(index + 1, song) def remove(self, song): """Remove song from playlist. O(n) If song is current song, remove the song and play next. Otherwise, just remove it. """ if song in self._songs: if self._current_song is None: self._songs.remove(song) elif song == self._current_song: next_song = self.next_song # 随机模式下或者歌单只剩一首歌曲,下一首可能和当前歌曲相同 if next_song == self.current_song: self.current_song = None self._songs.remove(song) self.current_song = self.next_song else: next_song = self.next_song self._songs.remove(song) self.current_song = next_song else: self._songs.remove(song) logger.debug('Remove {} from player playlist'.format(song)) else: logger.debug('Remove failed: {} not in playlist'.format(song)) if song in self._bad_songs: self._bad_songs.remove(song) def init_from(self, songs): """(alpha) temporarily, should only called by player.play_songs""" self.clear() # since we will call songs.clear method during playlist clearing, # we need to deepcopy songs object here. self._songs = DedupList(copy.deepcopy(songs)) def clear(self): """remove all songs from playlists""" if self.current_song is not None: self.current_song = None self._songs.clear() self._bad_songs.clear() def list(self): """get all songs in playlists""" return self._songs @property def current_song(self): """ current playing song, return None if there is no current song """ return self._current_song @current_song.setter def current_song(self, song): """设置当前歌曲,将歌曲加入到播放列表,并发出 song_changed 信号 .. note:: 该方法理论上只应该被 Player 对象调用。 """ media = None if song is not None: media = self.prepare_media(song) self._set_current_song(song, media) def _set_current_song(self, song, media): if song is None: self._current_song = None else: # add it to playlist if song not in playlist if song in self._songs: self._current_song = song else: self.insert(song) self._current_song = song self.song_changed.emit(song) self.song_changed_v2.emit(song, media) def prepare_media(self, song): """prepare media data """ warnings.warn('use library.song_prepare_media please', DeprecationWarning) if song.meta.support_multi_quality: media, quality = song.select_media(self.audio_select_policy) else: media = song.url # maybe a empty string return Media(media) if media else None @property def playback_mode(self): return self._playback_mode @playback_mode.setter def playback_mode(self, playback_mode): self._playback_mode = playback_mode self.playback_mode_changed.emit(playback_mode) def _get_good_song(self, base=0, random_=False, direction=1, loop=True): """从播放列表中获取一首可以播放的歌曲 :param base: base index :param random: random strategy or not :param direction: forward if > 0 else backward :param loop: regard the song list as a loop >>> pl = Playlist([1, 2, 3]) >>> pl._get_good_song() 1 >>> pl._get_good_song(base=1) 2 >>> pl._bad_songs = [2] >>> pl._get_good_song(base=1, direction=-1) 1 >>> pl._get_good_song(base=1) 3 >>> pl._bad_songs = [1, 2, 3] >>> pl._get_good_song() """ if not self._songs or len(self._songs) <= len(self._bad_songs): logger.debug('No good song in playlist.') return None good_songs = [] if direction > 0: if loop is True: song_list = self._songs[base:] + self._songs[0:base] else: song_list = self._songs[base:] else: if loop is True: song_list = self._songs[base::-1] + self._songs[:base:-1] else: song_list = self._songs[base::-1] for song in song_list: if song not in self._bad_songs: good_songs.append(song) if not good_songs: return None if random_: return random.choice(good_songs) else: return good_songs[0] @property def next_song(self): """next song for player, calculated based on playback_mode""" # 如果没有正在播放的歌曲,找列表里面第一首能播放的 if self.current_song is None: return self._get_good_song() if self.playback_mode == PlaybackMode.random: next_song = self._get_good_song(random_=True) else: current_index = self._songs.index(self.current_song) if current_index == len(self._songs) - 1: if self.playback_mode in (PlaybackMode.loop, PlaybackMode.one_loop): next_song = self._get_good_song() elif self.playback_mode == PlaybackMode.sequential: next_song = None else: next_song = self._get_good_song(base=current_index + 1, loop=False) return next_song @property def previous_song(self): """previous song for player to play NOTE: not the last played song """ if self.current_song is None: return self._get_good_song(base=-1, direction=-1) if self.playback_mode == PlaybackMode.random: previous_song = self._get_good_song(direction=-1) else: current_index = self._songs.index(self.current_song) previous_song = self._get_good_song(base=current_index - 1, direction=-1) return previous_song def next(self): """advance to the next song in playlist""" self.current_song = self.next_song def previous(self): """return to the previous song in playlist""" self.current_song = self.previous_song
class DownloadManager: def __init__(self, app, config): """ :type app: feeluown.app.App """ self._tasks = [] self._task_queue = asyncio.Queue() #: emit List[DownloadTask] self.tasks_changed = Signal() self.download_finished = Signal() self.downloader: Downloader = AioRequestsDownloader() self._path = config.DOWNLOAD_DIR or DEFAULT_DOWNLOAD_DIR def initialize(self): aio.create_task(self.worker()) def list_tasks(self) -> List[DownloadTask]: return self._tasks async def get(self, url, filename, headers=None, cookies=None): """download and save a file :param url: file url :param filename: target file name """ # check if there exists same task for task in self.list_tasks(): if task.filename == filename: logger.warning( f'task: {filename} has already been put into queue') return filepath = self._getpath(filename) dirpath = os.path.dirname(filepath) if not os.path.exists(dirpath): os.makedirs(dirpath) task = DownloadTask(url, filename, self.downloader) self._tasks.append(task) await self._task_queue.put(task) self.tasks_changed.emit(self.list_tasks()) logger.info(f'task: {filename} has been put into queue') return filepath async def worker(self): while True: task = await self._task_queue.get() await self.run_task(task) # task done and remove task self._task_queue.task_done() self._tasks.remove(task) self.tasks_changed.emit(self.list_tasks()) path = self._getpath(task.filename) logger.info(f'content has been saved into {path}') self.download_finished.emit(task.filename, True) async def run_task(self, task): task.status = DownloadStatus.running filename = task.filename downloader = task.downloader filepath = self._getpath(filename) try: ok = await downloader.run(task.url, filepath) except asyncio.CancelledError: task.status = DownloadStatus.failed except Exception: logger.exception('download failed') task.status = DownloadStatus.failed else: if ok: task.status = DownloadStatus.ok else: task.status = DownloadStatus.failed # clean up the temp file if needed if task.status is DownloadStatus.failed: downloader.clean(filepath) self.download_finished.emit(filename, False) def is_file_downloaded(self, filename): return os.path.exists(self._getpath(filename)) def _getpath(self, filename): return os.path.join(self._path, filename)