Ejemplo n.º 1
0
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()
Ejemplo n.º 2
0
    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)
Ejemplo n.º 3
0
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()
Ejemplo n.º 4
0
    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)
Ejemplo n.º 5
0
 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")
Ejemplo n.º 6
0
    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()
Ejemplo n.º 7
0
    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 = ''
Ejemplo n.º 8
0
    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
Ejemplo n.º 9
0
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()
Ejemplo n.º 10
0
    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)
Ejemplo n.º 11
0
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
Ejemplo n.º 12
0
    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 = ''
Ejemplo n.º 13
0
    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
Ejemplo n.º 14
0
    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.')
Ejemplo n.º 15
0
    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()
Ejemplo n.º 16
0
 def __init__(self, name, text, symbol, desc):
     # 如果需要,可以支持右键弹出菜单
     self._name = name
     self.text = text
     self.symbol = symbol
     self.desc = desc
     self.clicked = Signal()
Ejemplo n.º 17
0
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()
Ejemplo n.º 18
0
    def test_receiver(self, mock_connect):
        s = Signal()

        @receiver(s)
        def f():
            pass

        self.assertTrue(mock_connect.called)
Ejemplo n.º 19
0
 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()
Ejemplo n.º 20
0
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()
Ejemplo n.º 21
0
    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()
Ejemplo n.º 22
0
    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
Ejemplo n.º 23
0
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()
Ejemplo n.º 24
0
 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')
Ejemplo n.º 25
0
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)
Ejemplo n.º 26
0
    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()
Ejemplo n.º 27
0
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)
Ejemplo n.º 28
0
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='')
Ejemplo n.º 29
0
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
Ejemplo n.º 30
0
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)