def __init__(self, audio_device=b'auto', winid=None, *args, **kwargs): super(MpvPlayer, self).__init__(*args, **kwargs) # https://github.com/cosven/FeelUOwn/issues/246 locale.setlocale(locale.LC_NUMERIC, 'C') mpvkwargs = {} if winid is not None: mpvkwargs['wid'] = winid mpvkwargs['vo'] = 'opengl-cb' # set log_handler if you want to debug # mpvkwargs['log_handler'] = self.__log_handler # mpvkwargs['msg_level'] = 'all=v' logger.info('libmpv version %s', _mpv_client_api_version()) 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()
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 create_app(config): mode = config.MODE if mode & App.GuiMode: from PyQt5.QtCore import Qt from PyQt5.QtGui import QIcon, QPixmap from PyQt5.QtWidgets import QApplication, QWidget from feeluown.compat import QEventLoop q_app = QApplication(sys.argv) q_app.setQuitOnLastWindowClosed(not config.ENABLE_TRAY) q_app.setApplicationName('FeelUOwn') app_event_loop = QEventLoop(q_app) asyncio.set_event_loop(app_event_loop) class GuiApp(QWidget): mode = App.GuiMode def __init__(self): super().__init__() self.setObjectName('app') QApplication.setWindowIcon(QIcon(QPixmap(APP_ICON))) def closeEvent(self, _): if not self.config.ENABLE_TRAY: self.exit() def exit(self): self.ui.mpv_widget.close() event_loop = asyncio.get_event_loop() event_loop.stop() def mouseReleaseEvent(self, e): if not self.rect().contains(e.pos()): return if e.button() == Qt.BackButton: self.browser.back() elif e.button() == Qt.ForwardButton: self.browser.forward() class FApp(App, GuiApp): def __init__(self, config): App.__init__(self, config) GuiApp.__init__(self) else: FApp = App Signal.setup_aio_support() Resolver.setup_aio_support() app = FApp(config) attach_attrs(app) Resolver.library = app.library return app
def setup_app(args, config): if config.DEBUG: verbose = 3 else: verbose = args.verbose or 0 logger_config(verbose=verbose, to_file=config.LOG_TO_FILE) Signal.setup_aio_support() app = create_app(config) return app
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, 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)
class LiveLyric(object): """live lyric LiveLyric listens to song changed signal and position changed signal and emit sentence changed signal. It also has a ``current_sentence`` property. Usage:: live_lyric = LiveLyric() player.song_changed.connect(live_lyric.on_song_changed) player.position_change.connect(live_lyric.on_position_changed) """ 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 = '' @property def current_sentence(self): """get current lyric sentence""" return self._current_sentence @current_sentence.setter def current_sentence(self, value): self._current_sentence = value self.sentence_changed.emit(value) # TODO: performance optimization? def on_position_changed(self, position): """bind position changed signal with this""" if position is None or not self._lyric: return pos = find_previous(position * 1000 + 300, self._pos_list) if pos is not None and pos != self._pos: self.current_sentence = self._pos_s_map[pos] self._pos = pos def on_song_changed(self, song): """bind song changed signal with this""" if song is None or song.lyric is None: self._lyric = None self._pos_s_map = {} else: self._lyric = song.lyric.content self._pos_s_map = parse(self._lyric) self._pos_list = sorted(list(self._pos_s_map.keys())) self._pos = None self.current_sentence = ''
def __init__(self, playlist=None, **kwargs): self._position = 0 # seconds self._volume = 100 # (0, 100) self._playlist = Playlist() if playlist is None else playlist self._state = State.stopped self._duration = None self._current_media = None #: player position changed signal self.position_changed = Signal() #: player state changed signal self.state_changed = Signal() #: current song finished signal self.song_finished = Signal() #: duration changed signal self.duration_changed = Signal() #: media changed signal self.media_changed = Signal() #: volume changed signal: (int) self.volume_changed = Signal()
def main(): # 让程序能正确的找到图标等资源 os.chdir(os.path.join(os.path.dirname(__file__), '..')) sys.excepthook = excepthook parser = setup_argparse() args = parser.parse_args() if args.version: print('feeluown {}, fuocore {}'.format(feeluown_version, fuocore_version)) return check_ports() ensure_dirs() config = create_config() load_rcfile(config) map_args_to_config(args, config) logger_config(config.DEBUG, to_file=args.log_to_file) if config.MODE & App.GuiMode: try: import PyQt5 # noqa except ImportError: logger.warning('PyQt5 is not installed,can only use CLI mode.') config.MODE = App.CliMode if config.MODE & App.GuiMode: from PyQt5.QtWidgets import QApplication from quamash import QEventLoop q_app = QApplication(sys.argv) q_app.setQuitOnLastWindowClosed(True) q_app.setApplicationName('FeelUOwn') app_event_loop = QEventLoop(q_app) asyncio.set_event_loop(app_event_loop) event_loop = asyncio.get_event_loop() Signal.setup_aio_support(loop=event_loop) app = create_app(config) bind_signals(app) if sys.platform.lower() == 'darwin': enable_mac_hotkey(force=config.FORCE_MAC_HOTKEY) try: event_loop.run_forever() except KeyboardInterrupt: # NOTE: gracefully shutdown? pass finally: event_loop.stop() app.shutdown() event_loop.close()
def __init__(self, songs=None, playback_mode=PlaybackMode.loop): """ :param songs: list of :class:`fuocore.models.SongModel` :param playback_mode: :class:`fuocore.player.PlaybackMode` """ self._current_song = None self._songs = songs or [] self._playback_mode = playback_mode # signals self.playback_mode_changed = Signal() self.song_changed = Signal()
def __init__(self, app): """ :type app: feeluown.app.App """ self._tasks = [] self._task_queue = asyncio.Queue() #: emit List[DownloadTask] self.tasks_changed = Signal() self.downloader: Downloader = AioRequestsDownloader() self._path = SONG_DIR
def test_receiver(self, mock_connect): s = Signal() @receiver(s) def f(): pass self.assertTrue(mock_connect.called)
def __init__(self, songs=None, playback_mode=PlaybackMode.loop): """ :param songs: list of :class:`fuocore.models.SongModel` :param playback_mode: :class:`fuocore.player.PlaybackMode` """ self._current_song = None self._bad_songs = [] # songs whose url is invalid self._songs = songs or [] self._playback_mode = playback_mode #: playback mode changed signal self.playback_mode_changed = Signal() #: current song changed signal self.song_changed = Signal()
def __init__(self, name, text, symbol, desc): # 如果需要,可以支持右键弹出菜单 self._name = name self.text = text self.symbol = symbol self.desc = desc self.clicked = Signal()
def create_app(config): mode = config.MODE if mode & App.GuiMode: from quamash import QEventLoop from PyQt5.QtCore import QSize from PyQt5.QtGui import QIcon, QPixmap from PyQt5.QtWidgets import QApplication, QWidget q_app = QApplication(sys.argv) q_app.setQuitOnLastWindowClosed(True) q_app.setApplicationName('FeelUOwn') app_event_loop = QEventLoop(q_app) asyncio.set_event_loop(app_event_loop) class GuiApp(QWidget): mode = App.GuiMode def __init__(self): super().__init__() self.setObjectName('app') QApplication.setWindowIcon(QIcon(QPixmap(APP_ICON))) def closeEvent(self, e): self.ui.mpv_widget.close() event_loop = asyncio.get_event_loop() event_loop.stop() def sizeHint(self): # pylint: disable=no-self-use return QSize(1000, 618) class FApp(App, GuiApp): def __init__(self, config): App.__init__(self, config) GuiApp.__init__(self) else: FApp = App Signal.setup_aio_support() Resolver.setup_aio_support() app = FApp(config) attach_attrs(app) Resolver.library = app.library return app
def __init__(self, audio_device=b'auto', winid=None, *args, **kwargs): super(MpvPlayer, self).__init__(*args, **kwargs) # https://github.com/cosven/FeelUOwn/issues/246 locale.setlocale(locale.LC_NUMERIC, 'C') mpvkwargs = {} if winid is not None: mpvkwargs['wid'] = winid mpvkwargs['vo'] = 'opengl-cb' self._mpv = MPV(ytdl=True, input_default_bindings=True, input_vo_keyboard=True, **mpvkwargs) _mpv_set_property_string(self._mpv.handle, b'audio-device', audio_device) # TODO: 之后可以考虑将这个属性加入到 AbstractPlayer 中 self.video_format_changed = Signal()
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.activated.connect(self._on_activated) self.about_to_show_menu = Signal() self._setup_ui()
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): """ :param songs: list of :class:`fuocore.models.SongModel` :param playback_mode: :class:`fuocore.player.PlaybackMode` """ #: store value for ``current_song`` property self._current_song = None #: songs whose url is invalid self._bad_songs = [] #: store value for ``songs`` property self._songs = songs or [] #: 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
def __init__(self, songs=None, playback_mode=PlaybackMode.loop, audio_select_policy='hq<>'): """ :param songs: list of :class:`fuocore.models.SongModel` :param playback_mode: :class:`fuocore.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 __init__(self, audio_device=b'auto', winid=None, *args, **kwargs): super(MpvPlayer, self).__init__(*args, **kwargs) # https://github.com/cosven/FeelUOwn/issues/246 locale.setlocale(locale.LC_NUMERIC, 'C') mpvkwargs = {} if winid is not None: mpvkwargs['wid'] = winid mpvkwargs['vo'] = 'opengl-cb' # 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._version = _mpv_client_api_version() 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 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')
def __init__(self, playlist=Playlist(), **kwargs): self._position = 0 # seconds self._volume = 100 # (0, 100) self._playlist = playlist self._state = State.stopped self._duration = None self.position_changed = Signal() self.state_changed = Signal() self.song_finished = Signal() self.duration_changed = Signal() self.media_changed = Signal()
class PluginsManager: """在 App 初始化完成之后,加载插件 TODO: 以后可能需要支持在 App 初始化完成之前加载插件 """ scan_finished = Signal() def __init__(self, app): super().__init__() self._app = app self._plugins = {} def load(self, plugin): plugin.enable(self._app) def unload(self, plugin): plugin.disable(self._app) def scan(self): logger.debug('Scaning plugins...') modules_name = [ p for p in os.listdir(PLUGINS_DIR) if os.path.isdir(os.path.join(PLUGINS_DIR, p)) ] modules_name.extend([ p for p in os.listdir(USER_PLUGINS_DIR) if os.path.isdir(os.path.join(USER_PLUGINS_DIR)) ]) for module_name in modules_name: try: module = importlib.import_module(module_name) plugin_alias = module.__alias__ plugin = Plugin(module, alias=plugin_alias) plugin.enable(self._app) logger.info('detect plugin: %s.' % plugin_alias) except: # noqa logger.exception('detect a bad plugin %s' % module_name) else: self._plugins[plugin.name] = plugin logger.debug('Scaning plugins...done') self.scan_finished.emit(list(self._plugins.values()))
class PluginsManager: """在 App 初始化完成之后,加载插件 TODO: 以后可能需要支持在 App 初始化完成之前加载插件 """ scan_finished = Signal() def __init__(self, app): super().__init__() self._app = app self._plugins = {} def enable(self, plugin): plugin.enable(self._app) def disable(self, plugin): plugin.disable(self._app) def scan(self): """扫描并加载插件""" with action_log('Scaning plugins'): self._scan_dirs() self._scan_entry_points() self.scan_finished.emit(list(self._plugins.values())) def load_module(self, module): """加载插件模块并启用插件""" plugin = None with action_log('Creating plugin from module:%s' % module.__name__): try: plugin = Plugin.create(module) except InvalidPluginError as e: raise ActionError(str(e)) if plugin is None: return with action_log('Enabling plugin:%s' % plugin.name): self._plugins[plugin.name] = plugin try: self.enable(plugin) except Exception as e: raise ActionError(str(e)) def _scan_dirs(self): """扫描插件目录中的插件""" modules_name = ([ p for p in os.listdir(USER_PLUGINS_DIR) if os.path.isdir(os.path.join(USER_PLUGINS_DIR)) ]) for module_name in modules_name: try: module = importlib.import_module(module_name) except Exception as e: logger.exception('Failed to import module %s', module_name) else: self.load_module(module) def _scan_entry_points(self): """扫描通过 setuptools 机制注册的插件 https://packaging.python.org/guides/creating-and-discovering-plugins/ """ for entry_point in pkg_resources.iter_entry_points('fuo.plugins_v1'): try: module = entry_point.load() except Exception as e: # noqa logger.exception('Failed to load module %s', entry_point.name) else: self.load_module(module)
class Playlist: """player playlist provide a list of song model to play """ def __init__(self, songs=None, playback_mode=PlaybackMode.loop): """ :param songs: list of :class:`fuocore.models.SongModel` :param playback_mode: :class:`fuocore.player.PlaybackMode` """ #: store value for ``current_song`` property self._current_song = None #: songs whose url is invalid self._bad_songs = [] #: store value for ``songs`` property self._songs = songs or [] #: 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. """ 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: self.current_song = self.next_song self._songs.remove(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 = 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 对象调用。 """ self._last_song = self.current_song if song is None: self._current_song = None # add it to playlist if song not in playlist elif song in self._songs: self._current_song = song else: self.insert(song) self._current_song = song self.song_changed.emit(song) @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): """从播放列表中获取一首可以播放的歌曲 :param base: base index :param random: random strategy or not :param direction: forward if > 0 else backword >>> 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: song_list = self._songs[base:] + self._songs[0:base] else: song_list = self._songs[base::-1] + self._songs[:base:-1] for song in song_list: if song not in self._bad_songs: good_songs.append(song) 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) 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
def test_disconnect(self): s = Signal() s.connect(f) s.disconnect(f) s.emit(1, 'hello')
def test_connect2(self): s = Signal() s.connect(f) s.emit(1, 'hello') s.emit(1, 'hello')