From f14f3f4457419c0cfac2e70f18d1dc8bd47d5990 Mon Sep 17 00:00:00 2001 From: cosven Date: Tue, 16 Jan 2024 21:15:55 +0800 Subject: [PATCH 1/5] library: remove compatibility code for model v1 --- feeluown/gui/components/menu.py | 10 ++-- feeluown/gui/page_containers/table.py | 3 +- feeluown/gui/pages/song_explore.py | 7 +-- feeluown/library/library.py | 84 +++++++++------------------ feeluown/player/playlist.py | 4 +- feeluown/serializers/model_helpers.py | 25 ++++---- 6 files changed, 49 insertions(+), 84 deletions(-) diff --git a/feeluown/gui/components/menu.py b/feeluown/gui/components/menu.py index 79363cdc72..d099a6c2bc 100644 --- a/feeluown/gui/components/menu.py +++ b/feeluown/gui/components/menu.py @@ -5,7 +5,7 @@ from feeluown.excs import ProviderIOError from feeluown.utils.aio import run_fn, run_afn from feeluown.player import SongRadio -from feeluown.library import SongProtocol, VideoModel, SupportsSongMV +from feeluown.library import SongProtocol, VideoModel logger = logging.getLogger(__name__) @@ -108,11 +108,9 @@ def mv_fetched_cb(future): if data['mvs'] is None and self._fetching_mv is False: logger.debug('fetch song.mv for actions') song = data['song'] - provider = self._app.library.get(song.source) - if provider is not None and isinstance(provider, SupportsSongMV): - self._fetching_mv = True - task = run_fn(provider.song_get_mv, song) - task.add_done_callback(mv_fetched_cb) + self._fetching_mv = True + task = run_fn(self._app.library.song_get_mv, song) + task.add_done_callback(mv_fetched_cb) def _hover_artists(self, action, data): # pylint: disable=unnecessary-direct-lambda-call diff --git a/feeluown/gui/page_containers/table.py b/feeluown/gui/page_containers/table.py index c2d391960e..15ab0198d2 100644 --- a/feeluown/gui/page_containers/table.py +++ b/feeluown/gui/page_containers/table.py @@ -13,7 +13,7 @@ from feeluown.utils.reader import wrap from feeluown.media import Media, MediaType from feeluown.excs import ProviderIOError -from feeluown.library import ModelState, NotSupported, ModelFlags, ModelType +from feeluown.library import ModelState, NotSupported, ModelType from feeluown.gui.helpers import BgTransparentMixin, \ disconnect_slots_if_has, fetch_cover_wrapper @@ -502,7 +502,6 @@ async def _on_songs_table_activated(self, index): song = await aio.run_in_executor( None, self._app.library.song_upgrade, song) except NotSupported as e: - assert ModelFlags.v2 & song.meta.flags self._app.show_msg(f'资源提供方不支持该功能: {str(e)}') logger.info(f'provider:{song.source} does not support song_get') song.state = ModelState.cant_upgrade diff --git a/feeluown/gui/pages/song_explore.py b/feeluown/gui/pages/song_explore.py index 9feff46b76..6a8167b22c 100644 --- a/feeluown/gui/pages/song_explore.py +++ b/feeluown/gui/pages/song_explore.py @@ -348,10 +348,9 @@ async def maybe_show_song_pic(self, song, album): album.cover, reverse(album) + '/cover') else: - if ModelFlags.v2 in song.meta.flags: - aio.run_afn(self.cover_label.show_cover, - song.pic_url, - reverse(song) + '/pic_url') + aio.run_afn(self.cover_label.show_cover, + song.pic_url, + reverse(song) + '/pic_url') def resizeEvent(self, e: QResizeEvent) -> None: margins = self.layout().contentsMargins() diff --git a/feeluown/library/library.py b/feeluown/library/library.py index a23e484143..c10b65f100 100644 --- a/feeluown/library/library.py +++ b/feeluown/library/library.py @@ -2,10 +2,11 @@ import logging import warnings from functools import partial -from typing import cast, Optional, Union, TypeVar, Type, Callable, Any +from typing import Optional, Union, TypeVar, Type, Callable, Any from feeluown.media import Media from feeluown.utils import aio +from feeluown.utils.aio import run_fn from feeluown.utils.dispatch import Signal from feeluown.utils.reader import create_reader from .base import SearchType, ModelType @@ -136,7 +137,11 @@ def err_provider_not_support_flag(pid, model_type, op): class Library: - """音乐库,管理资源提供方以及资源""" + """音乐库,管理资源提供方以及资源 + + .. versionchanged:: 4.0 + Never raise ProviderNotFound error. + """ def __init__(self, providers_standby=None): """ @@ -250,9 +255,7 @@ async def a_list_song_standby_v2(self, song, async def prepare_media(standby, policy): media = None try: - media = await aio.run_in_executor(None, - self.song_prepare_media, - standby, policy) + await run_fn(self.song_prepare_media, standby, policy) except MediaNotFound: pass except: # noqa @@ -358,23 +361,12 @@ def song_upgrade(self, song: BriefSongProtocol) -> SongProtocol: def song_prepare_media(self, song: BriefSongProtocol, policy) -> Media: provider = self.get(song.source) if provider is None: - # FIXME: raise ProviderNotfound - raise MediaNotFound(f'provider:{song.source} not found') - if song.meta.flags & MF.v2: - support_or_raise(provider, SupportsSongMultiQuality) - provider = cast(SupportsSongMultiQuality, provider) + raise MediaNotFound(f'provider({song.source}) not found') + media = None + if isinstance(provider, SupportsSongMultiQuality): 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 + raise MediaNotFound('provider returns empty media') return media def song_prepare_mv_media(self, song: BriefSongProtocol, policy) -> Media: @@ -386,15 +378,14 @@ def song_prepare_mv_media(self, song: BriefSongProtocol, policy) -> Media: if mv is not None: media = self.video_prepare_media(mv, policy) return media - raise MediaNotFound + raise MediaNotFound('provider returns empty media') def song_get_mv(self, song: BriefSongProtocol) -> Optional[VideoProtocol]: """Get the MV model of a song. :raises NotSupported: - :raises ProviderNotFound: """ - provider = self.get_or_raise(song.source) + provider = self.get(song.source) if isinstance(provider, SupportsSongMV): mv = provider.song_get_mv(song) else: @@ -571,24 +562,18 @@ def model_get_cover(self, model): :param model: model which has a 'cover' field. :return: cover url if exists, else ''. """ - if MF.v2 in model.meta.flags: - if MF.normal not in model.meta.flags: - try: - um = self._model_upgrade(model) - except (ResourceNotFound, NotSupported): - return '' - else: - um = model - # FIXME: remove this hack lator. - if ModelType(model.meta.model_type) is ModelType.artist: - cover = um.pic_url - else: - cover = um.cover + if MF.normal not in model.meta.flags: + try: + um = self._model_upgrade(model) + except (ResourceNotFound, NotSupported): + return '' else: - cover = model.cover - # Check if cover is a media object. - if cover and not isinstance(cover, str): - cover = cover.url + um = model + # FIXME: remove this hack lator. + if ModelType(model.meta.model_type) is ModelType.artist: + cover = um.pic_url + else: + cover = um.cover return cover def _model_upgrade(self, model): @@ -604,10 +589,6 @@ def _model_upgrade(self, model): Raise ModelNotFound if the model does not exist. Before ModelCannotUpgrade was raised. """ - # Upgrade model in v1 way if it is a v1 model. - if MF.v2 not in model.meta.flags: - return self._model_upgrade_in_v1_way(model) - # Return model directly if it is already a normal model. if MF.normal in model.meta.flags: return model @@ -643,18 +624,11 @@ def video_prepare_media(self, video: BriefVideoProtocol, policy) -> Media: :param video: either a v1 MvModel or a v2 (Brief)VideoModel. """ provider = self.get_or_raise(video.source) - if video.meta.flags & MF.v2: - # provider MUST has multi_quality flag for video - assert isinstance(provider, SupportsVideoMultiQuality) - media, _ = provider.video_select_media(video, policy) - else: - # V1 VideoModel has attribute `media` - if video.meta.support_multi_quality: - media, _ = video.select_media(policy) # type: ignore - else: - media = video.media # type: ignore + # provider MUST has multi_quality flag for video + assert isinstance(provider, SupportsVideoMultiQuality) + media, _ = provider.video_select_media(video, policy) if not media: - raise MediaNotFound + raise MediaNotFound('provider returns empty media') return media # -------- diff --git a/feeluown/player/playlist.py b/feeluown/player/playlist.py index 64b96e7c81..57410d554a 100644 --- a/feeluown/player/playlist.py +++ b/feeluown/player/playlist.py @@ -12,7 +12,7 @@ from feeluown.utils.utils import DedupList from feeluown.player import Metadata, MetadataFields from feeluown.library import ( - MediaNotFound, SongProtocol, ModelType, NotSupported, ResourceNotFound + MediaNotFound, SongProtocol, ModelType, NotSupported, ResourceNotFound, ) from feeluown.media import Media from feeluown.library import reverse @@ -480,7 +480,7 @@ async def a_set_current_song(self, song): await self.a_set_current_song_children(song) return - logger.info(f'{song_str} has no valid media, mark it as bad') + logger.info(f'no media found for {song_str} due to {e}, mark it as bad') self.mark_as_bad(song) except ProviderIOError as e: # FIXME: This may cause infinite loop when the prepare media always fails diff --git a/feeluown/serializers/model_helpers.py b/feeluown/serializers/model_helpers.py index 244d0a70e0..9ab45e21e9 100644 --- a/feeluown/serializers/model_helpers.py +++ b/feeluown/serializers/model_helpers.py @@ -1,7 +1,5 @@ from feeluown.library import AbstractProvider from feeluown.library import ( - ModelFlags, - BaseModel, SongModel, ArtistModel, @@ -23,19 +21,16 @@ def _get_items(self, model): # initialize fields that need to be serialized # if as_line option is set, we always use fields_display if self.opt_as_line or self.opt_brief: - if ModelFlags.v2 in model.meta.flags: - modelcls = type(model) - fields = [field for field in model.__fields__ - if field not in BaseModel.__fields__] - # Include properties. - pydantic_fields = ("__values__", "fields", "__fields_set__", - "model_computed_fields", "model_extra", - "model_fields_set") - fields += [prop for prop in dir(modelcls) - if isinstance(getattr(modelcls, prop), property) - and prop not in pydantic_fields] - else: - fields = model.meta.fields_display + modelcls = type(model) + fields = [field for field in model.__fields__ + if field not in BaseModel.__fields__] + # Include properties. + pydantic_fields = ("__values__", "fields", "__fields_set__", + "model_computed_fields", "model_extra", + "model_fields_set") + fields += [prop for prop in dir(modelcls) + if isinstance(getattr(modelcls, prop), property) + and prop not in pydantic_fields] else: fields = self._declared_fields items = [("provider", model.source), From dd4facf9c40e8b0520a7d6c696f7f37cd8478d24 Mon Sep 17 00:00:00 2001 From: cosven Date: Tue, 16 Jan 2024 22:28:43 +0800 Subject: [PATCH 2/5] remove playlist_add_song/playlist_remove_song from library --- feeluown/gui/pages/model.py | 23 ++++++++++++++++------- feeluown/gui/widgets/playlists.py | 27 ++++++++++++++++++--------- feeluown/gui/widgets/songs.py | 13 +++++++------ feeluown/library/library.py | 22 +--------------------- feeluown/library/models.py | 5 ++++- 5 files changed, 46 insertions(+), 44 deletions(-) diff --git a/feeluown/gui/pages/model.py b/feeluown/gui/pages/model.py index 38a021b384..dfe37a1996 100644 --- a/feeluown/gui/pages/model.py +++ b/feeluown/gui/pages/model.py @@ -1,5 +1,7 @@ from feeluown.library import V2SupportedModelTypes, AlbumModel, NotSupported +from feeluown.library.provider_protocol import SupportsPlaylistRemoveSong from feeluown.utils import aio +from feeluown.utils.aio import run_fn, run_afn from feeluown.utils.reader import create_reader from feeluown.library import ModelType, reverse @@ -170,7 +172,9 @@ async def render(self): self.show_cover(playlist.cover, reverse(playlist) + '/cover')) - self.songs_table.remove_song_func = self.remove_song + provider = self._app.library.get(self.playlist.source) + if isinstance(provider, SupportsPlaylistRemoveSong): + self.songs_table.remove_song_func = self.remove_song async def _show_songs(self): reader = await aio.run_fn(self._app.library.playlist_create_songs_rd, @@ -178,9 +182,14 @@ async def _show_songs(self): self.show_songs(reader=reader, show_count=True) def remove_song(self, song): - # FIXME: this may block the whole app. - if self._app.library.playlist_remove_song(self.playlist, song) is True: - # Re-render songs table so that user can see that the song is removed. - aio.run_afn(self._show_songs) - else: - self._app.show_msg('移除歌曲失败') + + async def do(): + provider = self._app.library.get(self.playlist.source) + if await run_fn(provider.playlist_remove_song, self.playlist, song) is True: + # Re-render songs table so that user can see that the song is removed. + aio.run_afn(self._show_songs) + self._app.show_msg(f'移除歌曲 {song} 成功') + else: + self._app.show_msg(f'移除歌曲 {song} 失败') + + run_afn(do) diff --git a/feeluown/gui/widgets/playlists.py b/feeluown/gui/widgets/playlists.py index b13c24e05e..30675832c6 100644 --- a/feeluown/gui/widgets/playlists.py +++ b/feeluown/gui/widgets/playlists.py @@ -9,6 +9,8 @@ QAbstractItemView, QMenu ) +from feeluown.library import SupportsPlaylistAddSong +from feeluown.utils.aio import run_afn, run_fn from .textlist import TextlistModel, TextlistView @@ -124,16 +126,23 @@ def dropEvent(self, e): playlist = index.data(Qt.UserRole) self._results[index.row] = (index, None) self.viewport().update() - try: - # FIXME: this may block the app. - app = self.parent().parent()._app # type: ignore[attr-defined] - is_success = app.library.playlist_add_song(playlist, song) - except: # noqa, to avoid crash. - logger.exception('add song to playlist failed') + + async def do(): is_success = False - self._results[index.row] = (index, is_success) - self.viewport().update() - self._result_timer.start(2000) + app = self.parent().parent()._app # type: ignore[attr-defined] + try: + provider = app.library.get(playlist.source) + if isinstance(provider, SupportsPlaylistAddSong): + is_success = await run_fn(provider.playlist_add_song, playlist, song) + except: # noqa, to avoid crash. + logger.exception('add song to playlist failed') + is_success = False + app.show_msg(f"添加歌曲 {song} 到播放列表 {'成功' if is_success is True else '失败'}") + self._results[index.row] = (index, is_success) + self.viewport().update() + self._result_timer.start(2000) + + run_afn(do) e.accept() def dragMoveEvent(self, e): diff --git a/feeluown/gui/widgets/songs.py b/feeluown/gui/widgets/songs.py index f3e8a5dbee..d471997d34 100644 --- a/feeluown/gui/widgets/songs.py +++ b/feeluown/gui/widgets/songs.py @@ -642,12 +642,13 @@ def contextMenuEvent(self, event): 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) + remove_song_action = QAction('移除歌曲', menu) + remove_song_action.triggered.connect( + lambda: self._remove_by_indexes(indexes)) + menu.addSeparator() + menu.addAction(remove_song_action) + if self.remove_song_func is None: + remove_song_action.setDisabled(True) model = self.model() models = [model.data(index, Qt.UserRole) for index in indexes] diff --git a/feeluown/library/library.py b/feeluown/library/library.py index c10b65f100..ff942104f5 100644 --- a/feeluown/library/library.py +++ b/feeluown/library/library.py @@ -29,7 +29,7 @@ check_flag as check_flag_impl, SupportsCurrentUser, SupportsAlbumSongsReader, SupportsSongLyric, SupportsSongMV, SupportsSongMultiQuality, - SupportsPlaylistRemoveSong, SupportsPlaylistAddSong, SupportsPlaylistSongsReader, + SupportsPlaylistSongsReader, SupportsArtistSongsReader, SupportsArtistAlbumsReader, SupportsVideoMultiQuality, SupportsArtistContributedAlbumsReader, ) @@ -502,26 +502,6 @@ def playlist_create_songs_rd(self, playlist): playlist, ) - def playlist_remove_song(self, playlist, song) -> bool: - """Remove a song from the playlist - - :return: true if the song is not in playlist anymore. - """ - provider = self.get_or_raise(playlist.source) - if isinstance(provider, SupportsPlaylistRemoveSong): - return provider.playlist_remove_song(playlist, song) - raise NotSupported - - def playlist_add_song(self, playlist, song) -> bool: - """Add a song to the playlist - - :return: true if the song exists in playlist. - """ - provider = self.get_or_raise(playlist.source) - if isinstance(provider, SupportsPlaylistAddSong): - return provider.playlist_add_song(playlist, song) - raise NotSupported - # ------------------------- # generic methods for model # ------------------------- diff --git a/feeluown/library/models.py b/feeluown/library/models.py index fc9aa73f4d..c3a16bb354 100644 --- a/feeluown/library/models.py +++ b/feeluown/library/models.py @@ -230,7 +230,7 @@ class BriefSongModel(BaseBriefModel): duration_ms: str = '' def __str__(self): - return f'{self.title} - {self.artists_name}' + return f'[{self.source}]{self.title}•{self.artists_name}' class BriefVideoModel(BaseBriefModel): @@ -289,6 +289,9 @@ class SongModel(BaseNormalModel): # to fetch a image url of the song. pic_url: str = '' + def __str__(self): + return f'[{self.source}]{self.title}•{self.artists_name}' + @property def artists_name(self): return fmt_artists(self.artists) From 65f7e1b35c8c3969fac4c76bee0ea46d6004ef82 Mon Sep 17 00:00:00 2001 From: cosven Date: Tue, 16 Jan 2024 23:00:53 +0800 Subject: [PATCH 3/5] polish album_songs_rd, song_lyric --- feeluown/gui/pages/model.py | 44 ++++++++++++++---------------- feeluown/gui/pages/song_explore.py | 12 +++----- feeluown/library/library.py | 32 +++------------------- feeluown/player/lyric.py | 10 +++---- 4 files changed, 33 insertions(+), 65 deletions(-) diff --git a/feeluown/gui/pages/model.py b/feeluown/gui/pages/model.py index dfe37a1996..52799abcc8 100644 --- a/feeluown/gui/pages/model.py +++ b/feeluown/gui/pages/model.py @@ -1,6 +1,7 @@ -from feeluown.library import V2SupportedModelTypes, AlbumModel, NotSupported -from feeluown.library.provider_protocol import SupportsPlaylistRemoveSong -from feeluown.utils import aio +from feeluown.library import ( + V2SupportedModelTypes, AlbumModel, + SupportsAlbumSongsReader, SupportsPlaylistRemoveSong, +) from feeluown.utils.aio import run_fn, run_afn from feeluown.utils.reader import create_reader from feeluown.library import ModelType, reverse @@ -20,17 +21,17 @@ async def render(req, **kwargs): # FIXME: handle ProviderIOError and RequestException. if ModelType(model.meta.model_type) in V2SupportedModelTypes: if model.meta.model_type == ModelType.album: - album = await aio.run_fn(app.library.album_upgrade, model) + album = await run_fn(app.library.album_upgrade, model) tab_index = int(req.query.get('tab_index', 1)) al_renderer = AlbumRenderer(album, tab_index) await app.ui.table_container.set_renderer(al_renderer) elif model.meta.model_type == ModelType.artist: - artist = await aio.run_fn(app.library.artist_upgrade, model) + artist = await run_fn(app.library.artist_upgrade, model) tab_index = int(req.query.get('tab_index', 1)) ar_renderer = ArtistRenderer(artist, tab_index) await app.ui.table_container.set_renderer(ar_renderer) elif model.meta.model_type == ModelType.playlist: - playlist = await aio.run_fn(app.library.playlist_upgrade, model) + playlist = await run_fn(app.library.playlist_upgrade, model) pl_renderer = PlaylistRenderer(playlist) await app.ui.table_container.set_renderer(pl_renderer) else: @@ -62,7 +63,7 @@ async def render(self): tab_index = self.tab_index # fetch and render basic metadata - self.meta_widget.title = await aio.run_fn(lambda: artist.name) + self.meta_widget.title = await run_fn(lambda: artist.name) self.meta_widget.source = self._get_source_alias(artist.source) self.meta_widget.show() @@ -76,7 +77,7 @@ async def render(self): contributed = tab_index == 3 self.toolbar.filter_albums_needed.connect( lambda types: self.albums_table.model().filter_by_types(types)) - reader = await aio.run_fn( + reader = await run_fn( self._app.library.artist_create_albums_rd, artist, contributed) self.toolbar.show() self.show_albums(reader) @@ -90,13 +91,13 @@ async def render(self): async def _show_songs(self): artist = self.model - reader = await aio.run_fn(self._app.library.artist_create_songs_rd, artist) + reader = await run_fn(self._app.library.artist_create_songs_rd, artist) async def cb(): - reader = await aio.run_fn(self._app.library.artist_create_songs_rd, artist) + reader = await run_fn(self._app.library.artist_create_songs_rd, artist) self.__show_songs(reader) - self.tabbar.show_songs_needed.connect(lambda: aio.run_afn(cb)) + self.tabbar.show_songs_needed.connect(lambda: run_afn(cb)) self.__show_songs(reader) def __show_songs(self, reader): @@ -137,11 +138,10 @@ async def render(self): if album.song_count == 0: reader = create_reader([]) else: - try: - reader = await aio.run_fn( - self._app.library.album_create_songs_rd, album) - except NotSupported as e: - self._app.show_msg(str(e)) + provider = self._app.library.get(album.source) + if isinstance(provider, SupportsAlbumSongsReader): + reader = await run_fn(provider.album_create_songs_rd, album) + else: reader = create_reader([]) self.meta_widget.songs_count = reader.count self.show_songs(reader, columns_mode=ColumnsMode.album) @@ -149,7 +149,7 @@ async def render(self): # fetch cover and description cover = album.cover if cover: - aio.run_afn(self.show_cover, cover, reverse(album, '/cover')) + run_afn(self.show_cover, cover, reverse(album, '/cover')) class PlaylistRenderer(Renderer): @@ -168,17 +168,15 @@ async def render(self): # show playlist cover if playlist.cover: - aio.create_task( - self.show_cover(playlist.cover, - reverse(playlist) + '/cover')) + run_afn(self.show_cover, playlist.cover, reverse(playlist) + '/cover') provider = self._app.library.get(self.playlist.source) if isinstance(provider, SupportsPlaylistRemoveSong): self.songs_table.remove_song_func = self.remove_song async def _show_songs(self): - reader = await aio.run_fn(self._app.library.playlist_create_songs_rd, - self.playlist) + reader = await run_fn(self._app.library.playlist_create_songs_rd, + self.playlist) self.show_songs(reader=reader, show_count=True) def remove_song(self, song): @@ -187,7 +185,7 @@ async def do(): provider = self._app.library.get(self.playlist.source) if await run_fn(provider.playlist_remove_song, self.playlist, song) is True: # Re-render songs table so that user can see that the song is removed. - aio.run_afn(self._show_songs) + run_afn(self._show_songs) self._app.show_msg(f'移除歌曲 {song} 成功') else: self._app.show_msg(f'移除歌曲 {song} 失败') diff --git a/feeluown/gui/pages/song_explore.py b/feeluown/gui/pages/song_explore.py index 6a8167b22c..e35e9943ee 100644 --- a/feeluown/gui/pages/song_explore.py +++ b/feeluown/gui/pages/song_explore.py @@ -333,14 +333,10 @@ async def maybe_show_song_lyric(self, song): self.lyric_view.on_line_changed, weak=True) return - try: - lyric_model = self._app.library.song_get_lyric(song) - except NotSupported: - pass - else: - if lyric_model is None: - return - self.lyric_view.set_lyric(Lyric.from_content(lyric_model.content)) + lyric_model = self._app.library.song_get_lyric(song) + if lyric_model is None: + return + self.lyric_view.set_lyric(Lyric.from_content(lyric_model.content)) async def maybe_show_song_pic(self, song, album): if album: diff --git a/feeluown/library/library.py b/feeluown/library/library.py index ff942104f5..c9c1751d44 100644 --- a/feeluown/library/library.py +++ b/feeluown/library/library.py @@ -27,7 +27,7 @@ from .model_state import ModelState from .provider_protocol import ( check_flag as check_flag_impl, - SupportsCurrentUser, SupportsAlbumSongsReader, + SupportsCurrentUser, SupportsSongLyric, SupportsSongMV, SupportsSongMultiQuality, SupportsPlaylistSongsReader, SupportsArtistSongsReader, SupportsArtistAlbumsReader, @@ -46,11 +46,6 @@ def raise_(e): raise e -def support_or_raise(provider, protocol_cls): - if not isinstance(provider, protocol_cls): - raise NotSupported(f'{provider} not support {protocol_cls}') from None - - def default_score_fn(origin, standby): # TODO: move this function to utils module @@ -381,30 +376,20 @@ def song_prepare_mv_media(self, song: BriefSongProtocol, policy) -> Media: raise MediaNotFound('provider returns empty media') def song_get_mv(self, song: BriefSongProtocol) -> Optional[VideoProtocol]: - """Get the MV model of a song. - - :raises NotSupported: - """ + """Get the MV model of a song.""" provider = self.get(song.source) if isinstance(provider, SupportsSongMV): - mv = provider.song_get_mv(song) - else: - mv = None - return mv + return provider.song_get_mv(song) def song_get_lyric(self, song: BriefSongModel) -> Optional[LyricProtocol]: """Get the lyric model of a song. Return None when lyric does not exist instead of raising exceptions, because it is predictable. - - :raises NotSupported: - :raises ProviderNotFound: """ - provider = self.get_or_raise(song.source) + provider = self.get(song.source) if isinstance(provider, SupportsSongLyric): return provider.song_get_lyric(song) - raise NotSupported def song_get_web_url(self, song: BriefSongProtocol) -> str: provider = self.getv2_or_raise(song.source) @@ -416,15 +401,6 @@ def song_get_web_url(self, song: BriefSongProtocol) -> str: def album_upgrade(self, album: BriefAlbumProtocol): return self._model_upgrade(album) - def album_create_songs_rd(self, album: BriefAlbumProtocol): - """Create songs reader for album model.""" - return self._handle_protocol_with_model( - SupportsAlbumSongsReader, - lambda p, m: p.album_create_songs_rd(m), - lambda v1_m: create_reader(v1_m.songs), # type: ignore - album, - ) - def _handle_protocol_with_model(self, protocol_cls: Type[T_p], v2_handler: Callable[[T_p, Any], Any], diff --git a/feeluown/player/lyric.py b/feeluown/player/lyric.py index e1d75fe2bc..0f9589c04f 100644 --- a/feeluown/player/lyric.py +++ b/feeluown/player/lyric.py @@ -4,8 +4,8 @@ from typing import Dict, Optional, TYPE_CHECKING from collections import namedtuple, OrderedDict -from feeluown.library import LyricModel, NotSupported -from feeluown.utils import aio +from feeluown.library import LyricModel +from feeluown.utils.aio import run_fn from feeluown.utils.dispatch import Signal if TYPE_CHECKING: @@ -207,17 +207,15 @@ def on_song_changed(self, song): def cb(future): try: lyric = future.result() - except NotSupported: - lyric = None except: # noqa logger.exception('get lyric failed') lyric = None self.set_lyric(lyric) - future = aio.run_fn(self._app.library.song_get_lyric, song) + future = run_fn(self._app.library.song_get_lyric, song) future.add_done_callback(cb) - def set_lyric(self, model: LyricModel): + def set_lyric(self, model: Optional[LyricModel]): if model is None: self._lyric = self._trans_lyric = None elif model.content: From 69cac8bbe00c282888e1b66648a1cf48978c9f90 Mon Sep 17 00:00:00 2001 From: cosven Date: Tue, 16 Jan 2024 23:34:09 +0800 Subject: [PATCH 4/5] remove reader related from library --- feeluown/gui/pages/model.py | 38 +++++++++++---- feeluown/gui/uimain/page_view.py | 5 +- feeluown/library/library.py | 80 ++------------------------------ 3 files changed, 39 insertions(+), 84 deletions(-) diff --git a/feeluown/gui/pages/model.py b/feeluown/gui/pages/model.py index 52799abcc8..eda86e93c6 100644 --- a/feeluown/gui/pages/model.py +++ b/feeluown/gui/pages/model.py @@ -1,6 +1,8 @@ from feeluown.library import ( V2SupportedModelTypes, AlbumModel, SupportsAlbumSongsReader, SupportsPlaylistRemoveSong, + SupportsArtistAlbumsReader, SupportsArtistContributedAlbumsReader, + SupportsPlaylistSongsReader, SupportsArtistSongsReader, ) from feeluown.utils.aio import run_fn, run_afn from feeluown.utils.reader import create_reader @@ -9,6 +11,7 @@ from feeluown.gui.base_renderer import TabBarRendererMixin from feeluown.gui.page_containers.table import Renderer from feeluown.gui.widgets.songs import ColumnsMode +from .template import render_error_message async def render(req, **kwargs): @@ -77,8 +80,19 @@ async def render(self): contributed = tab_index == 3 self.toolbar.filter_albums_needed.connect( lambda types: self.albums_table.model().filter_by_types(types)) - reader = await run_fn( - self._app.library.artist_create_albums_rd, artist, contributed) + + source = artist.source + provider = self._app.library.get(source) + if contributed is False and isinstance(provider, SupportsArtistAlbumsReader): + reader = await run_fn(provider.artist_create_albums_rd, artist) + elif isinstance(provider, SupportsArtistContributedAlbumsReader): + reader = await run_fn(provider.artist_create_contributed_albums_rd, artist) # noqa + else: + if contributed: + await render_error_message(self._app, '资源提供方不支持获取歌手贡献过的专辑') + else: + await render_error_message(self._app, '资源提供方不支持获取歌手专辑') + return self.toolbar.show() self.show_albums(reader) @@ -91,14 +105,17 @@ async def render(self): async def _show_songs(self): artist = self.model - reader = await run_fn(self._app.library.artist_create_songs_rd, artist) + provider = self._app.library.get(artist.source) + if not isinstance(provider, SupportsArtistSongsReader): + await render_error_message(self._app, '资源提供方不支持获取歌手歌曲') + return async def cb(): - reader = await run_fn(self._app.library.artist_create_songs_rd, artist) + reader = await run_fn(provider.artist_create_songs_rd, artist) self.__show_songs(reader) + await cb() self.tabbar.show_songs_needed.connect(lambda: run_afn(cb)) - self.__show_songs(reader) def __show_songs(self, reader): self.show_songs(reader=reader, @@ -142,7 +159,8 @@ async def render(self): if isinstance(provider, SupportsAlbumSongsReader): reader = await run_fn(provider.album_create_songs_rd, album) else: - reader = create_reader([]) + await render_error_message(self._app, '资源提供方不支持获取专辑歌曲') + return self.meta_widget.songs_count = reader.count self.show_songs(reader, columns_mode=ColumnsMode.album) @@ -175,8 +193,12 @@ async def render(self): self.songs_table.remove_song_func = self.remove_song async def _show_songs(self): - reader = await run_fn(self._app.library.playlist_create_songs_rd, - self.playlist) + provider = self._app.library.get(self.playlist.source) + if isinstance(provider, SupportsPlaylistSongsReader): + reader = await run_fn(provider.playlist_create_songs_rd, self.playlist) + else: + await render_error_message(self._app, '资源提供方不支持获取歌单歌曲') + return self.show_songs(reader=reader, show_count=True) def remove_song(self, song): diff --git a/feeluown/gui/uimain/page_view.py b/feeluown/gui/uimain/page_view.py index 71969549bf..fd84081bac 100644 --- a/feeluown/gui/uimain/page_view.py +++ b/feeluown/gui/uimain/page_view.py @@ -197,7 +197,10 @@ def paintEvent(self, e): self._draw_pixmap(painter, draw_width, draw_height, scrolled) self._draw_pixmap_overlay(painter, draw_width, draw_height, scrolled) curve = QEasingCurve(QEasingCurve.OutCubic) - alpha_ratio = min(scrolled / max_scroll_height, 1) + if max_scroll_height == 0: + alpha_ratio = 1 + else: + alpha_ratio = min(scrolled / max_scroll_height, 1) alpha = int(250 * curve.valueForProgress(alpha_ratio)) painter.save() color = self.palette().color(QPalette.Window) diff --git a/feeluown/library/library.py b/feeluown/library/library.py index c9c1751d44..2a9db1487a 100644 --- a/feeluown/library/library.py +++ b/feeluown/library/library.py @@ -2,13 +2,11 @@ import logging import warnings from functools import partial -from typing import Optional, Union, TypeVar, Type, Callable, Any +from typing import Optional, Union, TypeVar from feeluown.media import Media -from feeluown.utils import aio -from feeluown.utils.aio import run_fn +from feeluown.utils.aio import run_fn, as_completed from feeluown.utils.dispatch import Signal -from feeluown.utils.reader import create_reader from .base import SearchType, ModelType from .provider import AbstractProvider from .provider_v2 import ProviderV2 @@ -29,9 +27,7 @@ check_flag as check_flag_impl, SupportsCurrentUser, SupportsSongLyric, SupportsSongMV, SupportsSongMultiQuality, - SupportsPlaylistSongsReader, - SupportsArtistSongsReader, SupportsArtistAlbumsReader, - SupportsVideoMultiQuality, SupportsArtistContributedAlbumsReader, + SupportsVideoMultiQuality, ) @@ -222,12 +218,10 @@ async def a_search(self, keyword, source_in=None, timeout=None, 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_)) + future = run_fn(partial(provider.search, keyword, type_=type_)) fs.append(future) - for future in aio.as_completed(fs, timeout=timeout): + for future in as_completed(fs, timeout=timeout): try: result = await future except: # noqa @@ -401,65 +395,12 @@ def song_get_web_url(self, song: BriefSongProtocol) -> str: def album_upgrade(self, album: BriefAlbumProtocol): return self._model_upgrade(album) - def _handle_protocol_with_model(self, - protocol_cls: Type[T_p], - v2_handler: Callable[[T_p, Any], Any], - v1_handler: Callable[[Any], Any], - model: ModelProtocol): - """A handler helper (experimental). - - :raises ProviderNotFound: - :raises NotSupported: - """ - provider = self.get_or_raise(model.source) - if isinstance(provider, protocol_cls): - return v2_handler(provider, model) - raise NotSupported(f'{protocol_cls} not supported') - # -------- # Artist # -------- def artist_upgrade(self, artist: BriefArtistProtocol): return self._model_upgrade(artist) - def artist_create_songs_rd(self, artist): - """Create songs reader for artist model.""" - return self._handle_protocol_with_model( - SupportsArtistSongsReader, - lambda p, m: p.artist_create_songs_rd(m), - lambda v1_m: (create_reader(v1_m.create_songs_g()) - if v1_m.meta.allow_create_songs_g else - create_reader(v1_m.songs)), - artist, - ) - - def artist_create_albums_rd(self, artist, contributed=False): - """Create albums reader for artist model.""" - source = artist.source - if contributed is False: - protocol_cls = SupportsArtistAlbumsReader - return self._handle_protocol_with_model( - protocol_cls, - lambda p, m: p.artist_create_albums_rd(m), - lambda v1_m: (create_reader(v1_m.create_albums_g()) - if v1_m.meta.allow_create_albums_g else - raise_(NotSupported.create_by_p_p(source, protocol_cls))), - artist - ) - protocol_cls = SupportsArtistContributedAlbumsReader - return self._handle_protocol_with_model( - protocol_cls, - lambda p, m: p.artist_create_contributed_albums_rd(m), - # Old code check if provider supports contributed_albums in this way, - # have to say, it is a little ugly. - lambda v1_m: ( - create_reader(v1_m.create_contributed_albums_g()) - if hasattr(v1_m, 'contributed_albums') and v1_m.contributed_albums else - raise_(NotSupported.create_by_p_p(source, protocol_cls)) - ), - artist - ) - # -------- # Playlist # -------- @@ -467,17 +408,6 @@ def artist_create_albums_rd(self, artist, contributed=False): def playlist_upgrade(self, playlist): return self._model_upgrade(playlist) - def playlist_create_songs_rd(self, playlist): - """Create songs reader for artist model.""" - return self._handle_protocol_with_model( - SupportsPlaylistSongsReader, - lambda p, m: p.playlist_create_songs_rd(m), - lambda v1_m: (create_reader(v1_m.create_songs_g()) - if v1_m.meta.allow_create_songs_g else - create_reader(v1_m.songs)), - playlist, - ) - # ------------------------- # generic methods for model # ------------------------- From feaaf4cc910ce3482b77c8699b2e7cc23ec845ff Mon Sep 17 00:00:00 2001 From: cosven Date: Tue, 16 Jan 2024 23:52:28 +0800 Subject: [PATCH 5/5] fix test --- tests/gui/pages/test_pages.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/gui/pages/test_pages.py b/tests/gui/pages/test_pages.py index 66f7f6cefe..9d2f8eaf96 100644 --- a/tests/gui/pages/test_pages.py +++ b/tests/gui/pages/test_pages.py @@ -1,7 +1,7 @@ import pytest from feeluown.library import BriefArtistModel, BriefAlbumModel, SongModel, \ - PlaylistModel, NotSupported + PlaylistModel from feeluown.utils.router import Request from feeluown.gui.pages.model import render as render_model from feeluown.gui.pages.song_explore import render as render_song_explore @@ -76,7 +76,8 @@ async def test_render_song_v2_with_non_exists_album(guiapp, ekaf_provider): @pytest.mark.asyncio -async def test_render_playlist_v2(guiapp, ekaf_provider): +async def test_render_playlist_v2(guiapp, ekaf_provider, mocker): + mock_error = mocker.patch('feeluown.gui.pages.model.render_error_message') playlist = PlaylistModel( source=ekaf_provider.identifier, identifier='does_not_exist', @@ -88,5 +89,5 @@ async def test_render_playlist_v2(guiapp, ekaf_provider): req = Request('', '', {}, {}, ctx) # ekaf_provider does not support create songs reader for playlist currently. - with pytest.raises(NotSupported): - await render_model(req) + await render_model(req) + assert mock_error.called