Beispiel #1
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')
Beispiel #2
0
 def test_disconnect(self):
     s = Signal()
     s.connect(f)
     s.disconnect(f)
     s.emit(1, 'hello')
Beispiel #3
0
 def test_connect2(self):
     s = Signal()
     s.connect(f)
     s.emit(1, 'hello')
     s.emit(1, 'hello')
Beispiel #4
0
class App:
    """App 基类"""

    DaemonMode = 0x0001  # 开启 daemon
    GuiMode = 0x0010  # 显示 GUI
    CliMode = 0x0100  # 命令行模式

    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 show_msg(self, msg, *args, **kwargs):
        """在程序中显示消息,一般是用来显示程序当前状态"""
        # pylint: disable=no-self-use, unused-argument
        logger.info(msg)

    def get_listen_addr(self):
        return '0.0.0.0' if self.config.ALLOW_LAN_CONNECT else '127.0.0.1'

    def load_state(self):
        playlist = self.playlist
        player = self.player

        try:
            with open(STATE_FILE, 'r') as f:
                state = json.load(f)
        except FileNotFoundError:
            pass
        except json.decoder.JSONDecodeError:
            logger.exception('invalid state file')
        else:
            player.volume = state['volume']
            playlist.playback_mode = PlaybackMode(state['playback_mode'])
            songs = []
            for song in state['playlist']:
                try:
                    song = resolve(song)
                except ResolverNotFound:
                    pass
                else:
                    songs.append(song)
            playlist.init_from(songs)
            if songs and self.mode & App.GuiMode:
                self.browser.goto(page='/player_playlist')

            song = state['song']

            def before_media_change(old_media, media):
                if old_media is not None or playlist.current_song != song:
                    player.media_about_to_changed.disconnect(
                        before_media_change)
                    player.set_play_range()
                    player.resume()

            if song is not None:
                try:
                    song = resolve(state['song'])
                except ResolverNotFound:
                    pass
                else:
                    player.media_about_to_changed.connect(before_media_change,
                                                          weak=False)
                    player.pause()
                    player.set_play_range(start=state['position'])
                    player.load_song(song)

    def dump_state(self):
        logger.info("Dump app state")
        playlist = self.playlist
        player = self.player

        song = self.player.current_song
        if song is not None:
            song = reverse(song, as_line=True)
        # TODO: dump player.media
        state = {
            'playback_mode': playlist.playback_mode.value,
            'volume': player.volume,
            'state': player.state.value,
            'song': song,
            'position': player.position,
            'playlist':
            [reverse(song, as_line=True) for song in playlist.list()],
        }
        with open(STATE_FILE, 'w') as f:
            json.dump(state, f)

    @contextmanager
    def create_action(self, s):  # pylint: disable=no-self-use
        """根据操作描述生成 Action (alpha)

        设计缘由:用户需要知道目前程序正在进行什么操作,进度怎么样,
        结果是失败或者成功。这里将操作封装成 Action。
        """
        show_msg = self.show_msg

        class ActionError(Exception):
            pass

        class Action:
            def set_progress(self, value):
                value = int(value * 100)
                show_msg(s + '...{}%'.format(value), timeout=-1)

            def failed(self, msg=''):
                raise ActionError(msg)

        show_msg(s + '...', timeout=-1)  # doing
        try:
            yield Action()
        except ActionError as e:
            show_msg(s + '...failed\t{}'.format(str(e)))
        except Exception as e:
            show_msg(s + '...error\t{}'.format(str(e)))  # error
            raise
        else:
            show_msg(s + '...done')  # done

    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 exit_player(self):
        self.player.shutdown()  # this cause 'abort trap' on macOS

    def exit(self):
        self.about_to_exit()
        loop = asyncio.get_event_loop()
        loop.stop()
        loop.close()
Beispiel #5
0
class AbstractPlayer(metaclass=ABCMeta):
    """Player abstrace base class"""
    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 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()

        #: volume changed signal: (int)
        self.volume_changed = Signal()

        self._playlist.song_changed_v2.connect(self._on_song_changed)
        self.media_finished.connect(self._on_media_finished)

    @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_song(self):
        """alias of playlist.current_song"""
        return self._playlist.current_song

    @property
    def playlist(self):
        """player playlist

        :return: :class:`.Playlist`
        """
        return self._playlist

    @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):
        if value is not None and value != self._duration:
            self._duration = value
            self.duration_changed.emit(value)

    @abstractmethod
    def play(self, url, video=True):
        """play media

        :param url: a local file absolute path, or a http url that refers to a
            media file
        :param video: show video or not
        """

    @abstractmethod
    def set_play_range(self, start=None, end=None):
        pass

    def load_song(self, song):
        """加载歌曲

        如果目标歌曲与当前歌曲不相同,则修改播放列表当前歌曲,
        播放列表会发出 song_changed 信号,player 监听到信号后调用 play 方法,
        到那时才会真正的播放新的歌曲。如果和当前播放歌曲相同,则忽略。

        .. note::

            调用方不应该直接调用 playlist.current_song = song 来切换歌曲
        """
        if song is not None and song != self.current_song:
            self._playlist.current_song = song

    def play_song(self, song):
        """加载并播放指定歌曲"""
        self.load_song(song)
        self.resume()

    def play_songs(self, songs):
        """(alpha) play list of songs"""
        self.playlist.init_from(songs)
        self.playlist.next()
        self.resume()

    @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"""

    def _on_song_changed(self, song, media):
        """播放列表 current_song 发生变化后的回调

        判断变化后的歌曲是否有效的,有效则播放,否则将它标记为无效歌曲。
        如果变化后的歌曲是 None,则停止播放。
        """
        if song is not None:
            if media is None:
                self._playlist.mark_as_bad(song)
                self._playlist.next()
            else:
                self.play(media)
        else:
            self.stop()

    def _on_media_finished(self):
        if self._playlist.playback_mode == PlaybackMode.one_loop:
            self.playlist.current_song = self.playlist.current_song
        else:
            self._playlist.next()
Beispiel #6
0
class App:
    """App 基类"""
    _instance = None

    # .. deprecated:: 3.8
    #    Use :class:`AppMode` instead.
    DaemonMode = AppMode.server.value
    GuiMode = AppMode.gui.value
    CliMode = AppMode.cli.value

    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)

    def initialize(self):
        self.player.position_changed.connect(
            self.live_lyric.on_position_changed)
        self.playlist.song_changed.connect(self.live_lyric.on_song_changed,
                                           aioqueue=True)
        self.plugin_mgr.scan()

    def run(self):
        pass

    @property
    def instance(self) -> Optional['App']:
        """App running instance.

        .. versionadded:: 3.8
        """
        return App._instance

    @property
    def has_server(self) -> bool:
        """
        .. versionadded:: 3.8
        """
        return AppMode.server in AppMode(self.config.MODE)

    @property
    def has_gui(self) -> bool:
        """
        .. versionadded:: 3.8
        """
        return AppMode.gui in AppMode(self.config.MODE)

    def show_msg(self, msg, *args, **kwargs):
        """在程序中显示消息,一般是用来显示程序当前状态"""
        # pylint: disable=no-self-use, unused-argument
        logger.info(msg)

    def get_listen_addr(self):
        return '0.0.0.0' if self.config.ALLOW_LAN_CONNECT else '127.0.0.1'

    def load_state(self):
        playlist = self.playlist
        player = self.player

        try:
            with open(STATE_FILE, 'r', encoding='utf-8') as f:
                state = json.load(f)
        except FileNotFoundError:
            pass
        except json.decoder.JSONDecodeError:
            logger.exception('invalid state file')
        else:
            player.volume = state['volume']
            playlist.playback_mode = PlaybackMode(state['playback_mode'])
            songs = []
            for song in state['playlist']:
                try:
                    song = resolve(song)
                except ResolverNotFound:
                    pass
                else:
                    songs.append(song)
            playlist.set_models(songs)
            song = state['song']

            def before_media_change(old_media, media):
                # When the song has no media or preparing media is failed,
                # the current_song is not the song we set.
                #
                # When user play a new media directly through player.play interface,
                # the old media is not None.
                if old_media is not None or playlist.current_song != song:
                    player.media_about_to_changed.disconnect(
                        before_media_change)
                    player.set_play_range()

            if song is not None:
                try:
                    song = resolve(state['song'])
                except ResolverNotFound:
                    pass
                else:
                    player.media_about_to_changed.connect(before_media_change,
                                                          weak=False)
                    player.pause()
                    player.set_play_range(start=state['position'])
                    playlist.set_current_song(song)

    def dump_state(self):
        logger.info("Dump app state")
        playlist = self.playlist
        player = self.player

        song = self.player.current_song
        if song is not None:
            song = reverse(song, as_line=True)
        # TODO: dump player.media
        state = {
            'playback_mode': playlist.playback_mode.value,
            'volume': player.volume,
            'state': player.state.value,
            'song': song,
            'position': player.position,
            'playlist':
            [reverse(song, as_line=True) for song in playlist.list()],
        }
        with open(STATE_FILE, 'w', encoding='utf-8') as f:
            json.dump(state, f)

    @contextmanager
    def create_action(self, s):  # pylint: disable=no-self-use
        """根据操作描述生成 Action (alpha)

        设计缘由:用户需要知道目前程序正在进行什么操作,进度怎么样,
        结果是失败或者成功。这里将操作封装成 Action。
        """
        show_msg = self.show_msg

        class ActionError(Exception):
            pass

        class Action:
            def set_progress(self, value):
                value = int(value * 100)
                show_msg(s + f'...{value}%', timeout=-1)

            def failed(self, msg=''):
                raise ActionError(msg)

        show_msg(s + '...', timeout=-1)  # doing
        try:
            yield Action()
        except ActionError as e:
            show_msg(s + f'...failed\t{str(e)}')
        except Exception as e:
            show_msg(s + f'...error\t{str(e)}')  # error
            raise
        else:
            show_msg(s + '...done')  # done

    def about_to_exit(self):
        logger.info('Do graceful shutdown')
        try:
            self.about_to_shutdown.emit(self)
            self.player.stop()
            self.exit_player()
        except:  # noqa, pylint: disable=bare-except
            logger.exception("about-to-exit failed")
        logger.info('Ready for shutdown')

    def exit_player(self):
        self.player.shutdown()  # this cause 'abort trap' on macOS

    def exit(self):
        self.about_to_exit()