diff --git a/feeluown/collection.py b/feeluown/collection.py index 082e36637c..3bd7cb1d90 100644 --- a/feeluown/collection.py +++ b/feeluown/collection.py @@ -124,9 +124,9 @@ def create_empty(cls, fpath, title=''): doc = tomlkit.document() if title: - doc.add('title', title) - doc.add('created', datetime.now()) - doc.add('updated', datetime.now()) + doc.add('title', title) # type: ignore[arg-type] + doc.add('created', datetime.now()) # type: ignore[arg-type] + doc.add('updated', datetime.now()) # type: ignore[arg-type] with open(fpath, 'w', encoding='utf-8') as f: f.write(TOML_DELIMLF) f.write(tomlkit.dumps(doc)) diff --git a/feeluown/gui/components/btns.py b/feeluown/gui/components/btns.py index afb3aa2dc5..5839d2e465 100644 --- a/feeluown/gui/components/btns.py +++ b/feeluown/gui/components/btns.py @@ -1,20 +1,23 @@ import logging +from typing import TYPE_CHECKING from PyQt5.QtCore import QEvent from PyQt5.QtWidgets import QPushButton, QWidget, QHBoxLayout -from feeluown.app import App from feeluown.player import State from feeluown.excs import ProviderIOError from feeluown.utils.aio import run_fn from feeluown.gui.widgets.textbtn import TextButton from feeluown.gui.helpers import resize_font +if TYPE_CHECKING: + from feeluown.app.gui_app import GuiApp + logger = logging.getLogger(__name__) class LyricButton(TextButton): - def __init__(self, app: App, **kwargs): + def __init__(self, app: 'GuiApp', **kwargs): kwargs.setdefault('height', 16) super().__init__('词', **kwargs) self._app = app @@ -45,7 +48,7 @@ def eventFilter(self, _, event): class WatchButton(TextButton): - def __init__(self, app: App, *args, **kwargs): + def __init__(self, app: 'GuiApp', *args, **kwargs): super().__init__('♨', *args, **kwargs) self._app = app @@ -75,7 +78,7 @@ def showEvent(self, e): class LikeButton(QPushButton): - def __init__(self, app: App, size=(15, 15), parent=None): + def __init__(self, app: 'GuiApp', size=(15, 15), parent=None): super().__init__(parent=parent) self._app = app self.setCheckable(True) @@ -113,7 +116,7 @@ def is_song_liked(self, song): class SongMVTextButton(TextButton): - def __init__(self, app: App, song=None, text='MV', **kwargs): + def __init__(self, app: 'GuiApp', song=None, text='MV', **kwargs): super().__init__(text, **kwargs) self._app = app self._song = None @@ -152,7 +155,7 @@ async def get_mv(self): class MVButton(SongMVTextButton): - def __init__(self, app: App, parent=None, **kwargs): + def __init__(self, app: 'GuiApp', parent=None, **kwargs): super().__init__(app, song=None, parent=parent, **kwargs) self.setObjectName('mv_btn') @@ -169,7 +172,7 @@ async def update_mv_btn_status(self, song): class MediaButtons(QWidget): - def __init__(self, app: App, spacing=8, button_width=30, parent=None): + def __init__(self, app: 'GuiApp', spacing=8, button_width=30, parent=None): super().__init__(parent=parent) self._app = app diff --git a/feeluown/gui/components/collections.py b/feeluown/gui/components/collections.py index f323b61b99..22cd820505 100644 --- a/feeluown/gui/components/collections.py +++ b/feeluown/gui/components/collections.py @@ -30,9 +30,11 @@ def __init__(self, app: 'GuiApp', **kwargs): self._app.coll_mgr.scan_finished.connect(self.on_scan_finished) def on_scan_finished(self): - self.model().clear() + model = self.model() + assert isinstance(model, TextlistModel) + model.clear() for coll in self._app.coll_mgr.listall(): - self.model().add(coll) + model.add(coll) def _on_clicked(self, index): collection = index.data(role=Qt.UserRole) diff --git a/feeluown/gui/components/menu.py b/feeluown/gui/components/menu.py index d099a6c2bc..8bc2516065 100644 --- a/feeluown/gui/components/menu.py +++ b/feeluown/gui/components/menu.py @@ -1,11 +1,13 @@ import logging -from typing import Optional +from typing import Optional, TYPE_CHECKING -from feeluown.app import App 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 +from feeluown.library import SongModel, VideoModel + +if TYPE_CHECKING: + from feeluown.app.gui_app import GuiApp logger = logging.getLogger(__name__) @@ -15,7 +17,7 @@ class SongMenuInitializer: - def __init__(self, app: App, song): + def __init__(self, app: 'GuiApp', song): """ :type app: feeluown.app.App """ @@ -46,7 +48,7 @@ def goto_song_explore(song): app.browser.goto(model=song, path='/explore') async def goto_song_album(song): - usong: SongProtocol = await run_fn(self._app.library.song_upgrade, song) + usong: SongModel = await run_fn(self._app.library.song_upgrade, song) if usong.album is not None: self._app.browser.goto(model=usong.album) else: diff --git a/feeluown/gui/drawers.py b/feeluown/gui/drawers.py index 539285b8b2..4dfeb23e70 100644 --- a/feeluown/gui/drawers.py +++ b/feeluown/gui/drawers.py @@ -46,6 +46,7 @@ def get_pixmap(self) -> Optional[QPixmap]: def maybe_update_pixmap(self): if self._widget.width() != self._widget_last_width: self._widget_last_width = self._widget.width() + assert self._img is not None new_img = self._img.scaledToWidth(self._widget_last_width, Qt.SmoothTransformation) self._pixmap = QPixmap(new_img) diff --git a/feeluown/gui/pages/search.py b/feeluown/gui/pages/search.py index 62967c21d0..d7ab9ba00a 100644 --- a/feeluown/gui/pages/search.py +++ b/feeluown/gui/pages/search.py @@ -1,4 +1,4 @@ -from PyQt5.QtWidgets import QFrame, QVBoxLayout +from PyQt5.QtWidgets import QAbstractItemView, QFrame, QVBoxLayout from feeluown.library import SearchType from feeluown.gui.page_containers.table import TableContainer, Renderer @@ -58,9 +58,11 @@ async def render(req, **kwargs): # pylint: disable=too-many-locals,too-many-bra # HACK: set fixed row for tables. # pylint: disable=protected-access for table in table_container._tables: + assert isinstance(table, QAbstractItemView) delegate = table.itemDelegate() if isinstance(delegate, ImgCardListDelegate): - table._fixed_row_count = 2 + # FIXME: set fixed_row_count in better way. + table._fixed_row_count = 2 # type: ignore[attr-defined] delegate.update_settings("card_min_width", 100) elif isinstance(table, SongsTableView): table._fixed_row_count = 8 diff --git a/feeluown/gui/provider_ui.py b/feeluown/gui/provider_ui.py index 60c400f713..7d71915553 100644 --- a/feeluown/gui/provider_ui.py +++ b/feeluown/gui/provider_ui.py @@ -103,7 +103,7 @@ def __init__(self, app: 'GuiApp'): self._store: Dict[str, AbstractProviderUi] = {} # {name: provider_ui} - self._items = {} # name:model mapping + self._items: Dict[str, ProviderUiItem] = {} # name:model mapping self.model = ProvidersModel(self._app.library, self._app) def register(self, provider_ui: AbstractProviderUi): diff --git a/feeluown/gui/uimain/floating_box.py b/feeluown/gui/uimain/floating_box.py index 3fcf88f794..b191222ded 100644 --- a/feeluown/gui/uimain/floating_box.py +++ b/feeluown/gui/uimain/floating_box.py @@ -135,7 +135,7 @@ def __init__(self, app, padding=6, *args, **kwargs): super().__init__(app, *args, **kwargs) self._padding = padding - self._angle = 0 + self._angle: float = 0 self._timer = QTimer() self._timer.timeout.connect(self.on_timeout) self._timer.start(16) @@ -163,12 +163,13 @@ def paintEvent(self, e): painter.setBrush(QBrush(color)) painter.drawRoundedRect(self.rect(), radius, radius) - if self._pixmap is not None: - size = self._pixmap.size() + pixmap = self.drawer.get_pixmap() + if pixmap is not None: + size = pixmap.size() y = (size.height() - self.height()) // 2 rect = QRect(self._padding, y+self._padding, self.width()-self._padding*2, self.height()-self._padding*2) - brush = QBrush(self._pixmap) + brush = QBrush(pixmap) painter.setBrush(brush) painter.drawRoundedRect(rect, radius, radius) diff --git a/feeluown/gui/uimain/page_view.py b/feeluown/gui/uimain/page_view.py index fd84081bac..50f0339616 100644 --- a/feeluown/gui/uimain/page_view.py +++ b/feeluown/gui/uimain/page_view.py @@ -198,9 +198,9 @@ def paintEvent(self, e): self._draw_pixmap_overlay(painter, draw_width, draw_height, scrolled) curve = QEasingCurve(QEasingCurve.OutCubic) if max_scroll_height == 0: - alpha_ratio = 1 + alpha_ratio = 1.0 else: - alpha_ratio = min(scrolled / max_scroll_height, 1) + alpha_ratio = min(scrolled / max_scroll_height, 1.0) alpha = int(250 * curve.valueForProgress(alpha_ratio)) painter.save() color = self.palette().color(QPalette.Window) diff --git a/feeluown/gui/widgets/cover_label.py b/feeluown/gui/widgets/cover_label.py index a13c49fbf4..2d5b05c617 100644 --- a/feeluown/gui/widgets/cover_label.py +++ b/feeluown/gui/widgets/cover_label.py @@ -59,10 +59,10 @@ def resizeEvent(self, e): def sizeHint(self): super_size = super().sizeHint() - if self.drawer.get_pixmap() is None: + pixmap = self.drawer.get_pixmap() + if pixmap is None: return super_size - h = (self.width() * self.drawer.get_pixmap().height()) \ - // self.drawer.get_pixmap().width() + h = (self.width() * pixmap.height()) // pixmap.width() # cover label height hint can be as large as possible, since the # parent width has been set maximumHeigh w = self.width() diff --git a/feeluown/library/__init__.py b/feeluown/library/__init__.py index 614b633b61..2bf6ab4bca 100644 --- a/feeluown/library/__init__.py +++ b/feeluown/library/__init__.py @@ -3,7 +3,7 @@ from .provider import AbstractProvider, ProviderV2, Provider from .flags import Flags as ProviderFlags from .model_state import ModelState -from .model_protocol import ( +from .model_protocol import ( # deprecated BriefSongProtocol, BriefVideoProtocol, BriefArtistProtocol, diff --git a/feeluown/library/library.py b/feeluown/library/library.py index 243c12306b..1d5f20b383 100644 --- a/feeluown/library/library.py +++ b/feeluown/library/library.py @@ -2,7 +2,7 @@ import logging import warnings from functools import partial -from typing import Optional, TypeVar +from typing import Optional, TypeVar, List from feeluown.media import Media from feeluown.utils.aio import run_fn, as_completed @@ -15,11 +15,9 @@ ) from .flags import Flags as PF from .models import ( - ModelFlags as MF, BriefSongModel, -) -from .model_protocol import ( - BriefVideoProtocol, ModelProtocol, BriefSongProtocol, SongProtocol, - LyricProtocol, VideoProtocol, BriefAlbumProtocol, BriefArtistProtocol + ModelFlags as MF, BaseModel, + BriefVideoModel, BriefSongModel, SongModel, + LyricModel, VideoModel, BriefAlbumModel, BriefArtistModel ) from .model_state import ModelState from .provider_protocol import ( @@ -70,53 +68,6 @@ def duration_ms_to_duration(ms): return score -def _sort_song_standby(song, standby_list): - """sort song standby list by similarity""" - - def get_score(standby): - """ - score strategy - - 1. title + album > artist - 2. artist > title > album - """ - - score = 10 - if song.artists_name_display != standby.artists_name_display: - score -= 4 - if song.title_display != standby.title_display: - score -= 3 - if song.album_name_display != standby.album_name_display: - score -= 2 - return score - - sorted_standby_list = sorted( - standby_list, - key=lambda standby: get_score(standby), - reverse=True - ) - - return sorted_standby_list - - -def _extract_and_sort_song_standby_list(song, result_g): - standby_list = [] - for result in result_g: - for standby in result.songs[:2]: - standby_list.append(standby) - sorted_standby_list = _sort_song_standby(song, standby_list) - return sorted_standby_list - - -def _get_display_property_or_raise(model, attr): - """Get property with no IO operation - - I hope we need not use this function in other module because - it is tightly coupled with display_property. - """ - return getattr(model, f'_display_store_{attr}') - - def err_provider_not_support_flag(pid, model_type, op): op_str = str(op) if op is PF.get: @@ -172,7 +123,7 @@ def get(self, identifier) -> Optional[Provider]: return provider return None - def list(self): + def list(self) -> List[Provider]: """列出所有资源提供方""" return list(self._providers) @@ -205,7 +156,7 @@ def search(self, keyword, type_in=None, source_in=None, **kwargs): 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 @@ -312,7 +263,7 @@ def check_flags(self, source: str, model_type: ModelType, flags: PF) -> bool: return False return True - def check_flags_by_model(self, model: ModelProtocol, flags: PF) -> bool: + def check_flags_by_model(self, model: BaseModel, flags: PF) -> bool: """Alias for check_flags""" warnings.warn('please use isinstance(provider, protocol_cls)') return self.check_flags(model.source, @@ -322,10 +273,10 @@ def check_flags_by_model(self, model: ModelProtocol, flags: PF) -> bool: # ----- # Songs # ----- - def song_upgrade(self, song: BriefSongProtocol) -> SongProtocol: + def song_upgrade(self, song: BriefSongModel) -> SongModel: return self._model_upgrade(song) # type: ignore - def song_prepare_media(self, song: BriefSongProtocol, policy) -> Media: + def song_prepare_media(self, song: BriefSongModel, policy) -> Media: provider = self.get(song.source) if provider is None: raise MediaNotFound(f'provider({song.source}) not found') @@ -336,7 +287,7 @@ def song_prepare_media(self, song: BriefSongProtocol, policy) -> Media: raise MediaNotFound('provider returns empty media') return media - def song_prepare_mv_media(self, song: BriefSongProtocol, policy) -> Media: + def song_prepare_mv_media(self, song: BriefSongModel, policy) -> Media: """ .. versionadded:: 3.7.5 @@ -347,13 +298,14 @@ def song_prepare_mv_media(self, song: BriefSongProtocol, policy) -> Media: return media raise MediaNotFound('provider returns empty media') - def song_get_mv(self, song: BriefSongProtocol) -> Optional[VideoProtocol]: + def song_get_mv(self, song: BriefSongModel) -> Optional[VideoModel]: """Get the MV model of a song.""" provider = self.get(song.source) if isinstance(provider, SupportsSongMV): return provider.song_get_mv(song) + return None - def song_get_lyric(self, song: BriefSongModel) -> Optional[LyricProtocol]: + def song_get_lyric(self, song: BriefSongModel) -> Optional[LyricModel]: """Get the lyric model of a song. Return None when lyric does not exist instead of raising exceptions, @@ -362,17 +314,18 @@ def song_get_lyric(self, song: BriefSongModel) -> Optional[LyricProtocol]: provider = self.get(song.source) if isinstance(provider, SupportsSongLyric): return provider.song_get_lyric(song) + return None # -------- # Album # -------- - def album_upgrade(self, album: BriefAlbumProtocol): + def album_upgrade(self, album: BriefAlbumModel): return self._model_upgrade(album) # -------- # Artist # -------- - def artist_upgrade(self, artist: BriefArtistProtocol): + def artist_upgrade(self, artist: BriefArtistModel): return self._model_upgrade(artist) # -------- @@ -457,7 +410,7 @@ def _model_upgrade(self, model): def video_upgrade(self, video): return self._model_upgrade(video) - def video_prepare_media(self, video: BriefVideoProtocol, policy) -> Media: + def video_prepare_media(self, video: BriefVideoModel, policy) -> Media: """Prepare media for video. :param video: either a v1 MvModel or a v2 (Brief)VideoModel. diff --git a/feeluown/library/models.py b/feeluown/library/models.py index e2821eb9ed..854376dcd7 100644 --- a/feeluown/library/models.py +++ b/feeluown/library/models.py @@ -63,7 +63,7 @@ except ImportError: # pydantic<2.0 from pydantic import validator - identifier_validator = validator('identifier', pre=True) + identifier_validator = validator('identifier', pre=True) # type: ignore pydantic_version = 1 from feeluown.utils.utils import elfhash @@ -101,6 +101,15 @@ def fmt_artists(artists: List['BriefArtistModel']) -> str: return fmt_artists_names([artist.name for artist in artists]) +def get_duration_ms(duration): + if duration is not None: + seconds = duration / 1000 + m, s = seconds / 60, seconds % 60 + else: + m, s = 0, 0 + return '{:02}:{:02}'.format(int(m), int(s)) + + # When a model is fully supported (with v2 design), it means # the library has implemented all features(functions) for this model. # You can do anything with model v2 without model v1. @@ -262,7 +271,7 @@ class BriefUserModel(BaseBriefModel): name: str = '' -class SongModel(BaseNormalModel): +class SongModel(BriefSongModel, BaseNormalModel): """ ..versionadded: 3.8.11 The `pic_url` field. @@ -289,28 +298,17 @@ class SongModel(BaseNormalModel): # to fetch a image url of the song. pic_url: str = '' + def model_post_init(self, _): + super().model_post_init(_) + self.artists_name = fmt_artists(self.artists) + self.album_name = self.album.name if self.album else '' + self.duration_ms = get_duration_ms(self.duration) + def __str__(self): return f'{self.source}:{self.title}•{self.artists_name}' - @property - def artists_name(self): - return fmt_artists(self.artists) - - @property - def album_name(self): - return self.album.name if self.album else '' - @property - def duration_ms(self): - if self.duration is not None: - seconds = self.duration / 1000 - m, s = seconds / 60, seconds % 60 - else: - m, s = 0, 0 - return '{:02}:{:02}'.format(int(m), int(s)) - - -class UserModel(BaseNormalModel): +class UserModel(BriefUserModel, BaseNormalModel): meta: Any = ModelMeta.create(ModelType.user, is_normal=True) name: str = '' avatar_url: str = '' @@ -336,7 +334,7 @@ class CommentModel(BaseNormalModel): root_comment_id: Optional[str] = None -class ArtistModel(BaseNormalModel): +class ArtistModel(BriefArtistModel, BaseNormalModel): meta: Any = ModelMeta.create(ModelType.artist, is_normal=True) name: str pic_url: str @@ -345,7 +343,7 @@ class ArtistModel(BaseNormalModel): description: str -class AlbumModel(BaseNormalModel): +class AlbumModel(BriefAlbumModel, BaseNormalModel): """ .. versionadded:: 3.8.12 The `song_count` field. @@ -371,9 +369,9 @@ class AlbumModel(BaseNormalModel): description: str released: str = '' # format: 2000-12-27. - @property - def artists_name(self): - return fmt_artists(self.artists) + def model_post_init(self, _): + super().model_post_init(_) + self.artists_name = fmt_artists(self.artists) class LyricModel(BaseNormalModel): @@ -382,28 +380,20 @@ class LyricModel(BaseNormalModel): trans_content: str = '' -class VideoModel(BaseNormalModel): +class VideoModel(BriefVideoModel, BaseNormalModel): meta: Any = ModelMeta.create(ModelType.video, is_normal=True) title: str artists: List[BriefArtistModel] duration: int cover: str - @property - def artists_name(self): - return fmt_artists(self.artists) - - @property - def duration_ms(self): - if self.duration is not None: - seconds = self.duration / 1000 - m, s = seconds / 60, seconds % 60 - else: - m, s = 0, 0 - return '{:02}:{:02}'.format(int(m), int(s)) + def model_post_init(self, _): + super().model_post_init(_) + self.artists_name = fmt_artists(self.artists) + self.duration_ms = get_duration_ms(self.duration) -class PlaylistModel(BaseBriefModel): +class PlaylistModel(BriefPlaylistModel, BaseNormalModel): meta: Any = ModelMeta.create(ModelType.playlist, is_normal=True) # Since modelv1 playlist does not have creator field, it is set to optional. creator: Optional[BriefUserModel] = None diff --git a/feeluown/library/provider.py b/feeluown/library/provider.py index 9d642c5ae2..68ac05ed4b 100644 --- a/feeluown/library/provider.py +++ b/feeluown/library/provider.py @@ -27,6 +27,7 @@ class Provider: class meta: identifier: str = '' name: str = '' + flags: dict = {} def __init__(self): self._user = None @@ -65,36 +66,7 @@ def search(self, *args, **kwargs): pass def use_model_v2(self, model_type: ModelType) -> bool: - """Check whether model v2 is used for the specified model_type. - - For feeluown developer, there are three things you should know. - - 1. Both v2 model and v1 model implement BriefXProtocol, which means - model.{attirbute}_display works for both models. For exmample, - SongModel(v2), BriefSongModel(v2) and SongModel(v1) all implement - BriefSongProtocol. So no matter which version the `song` is, it is - always safe to access `song.title_display`. - - 2. When model v2 is used, it means the way of accessing model's attributes - becomes different. So you should always check which version - the model is before accessing some attributes. - - For model v1, you can access all model's attributes by {model}.{attribute}, - and IO(network) operations may be performed implicitly. For example, - the code `song.url` *may* trigger a network request to fetch the - url when `song.url` is currently None. Tips: you can check the - `BaseModel.__getattribute__` implementation in `feeluown.library` package - for more details. - - For model v2, everything are explicit. Basic attributes of model can be - accessed by {model}.{attribute} and there will be no IO operations. - Other attributes can only be accessed with methods of library. For example, - you can access song url/media info by `library.song_prepare_media`. - - 3. When deserializing model from a text line, the model version is important. - If provider does not declare it uses model v2, feeluown just use model v1 - to do deserialization to keep backward compatibility. - """ + """Check whether model v2 is used for the specified model_type.""" return Flags.model_v2 in self.meta.flags.get(model_type, Flags.none) def model_get(self, model_type, model_id): diff --git a/feeluown/library/provider_protocol.py b/feeluown/library/provider_protocol.py index 6c16724f7e..8721d72f77 100644 --- a/feeluown/library/provider_protocol.py +++ b/feeluown/library/provider_protocol.py @@ -4,13 +4,10 @@ from feeluown.excs import NoUserLoggedIn from .models import ( BriefCommentModel, SongModel, VideoModel, AlbumModel, ArtistModel, - PlaylistModel, UserModel, ModelType, -) -from .model_protocol import ( - BriefArtistProtocol, BriefSongProtocol, SongProtocol, - BriefVideoProtocol, VideoProtocol, - LyricProtocol, + PlaylistModel, UserModel, ModelType, BriefArtistModel, BriefSongModel, + LyricModel, BriefVideoModel, ) + from .flags import Flags as PF @@ -62,7 +59,7 @@ def song_get(self, identifier: ID) -> SongModel: @runtime_checkable class SupportsSongSimilar(Protocol): @abstractmethod - def song_list_similar(self, song: BriefSongProtocol) -> List[BriefSongProtocol]: + def song_list_similar(self, song: BriefSongModel) -> List[BriefSongModel]: """List similar songs """ raise NotImplementedError @@ -72,7 +69,7 @@ def song_list_similar(self, song: BriefSongProtocol) -> List[BriefSongProtocol]: @runtime_checkable class SupportsSongMultiQuality(Protocol): @abstractmethod - def song_list_quality(self, song: BriefSongProtocol) -> List[Quality.Audio]: + def song_list_quality(self, song: BriefSongModel) -> List[Quality.Audio]: """List all possible qualities Please ensure all the qualities are valid. `song_get_media(song, quality)` @@ -82,7 +79,7 @@ def song_list_quality(self, song: BriefSongProtocol) -> List[Quality.Audio]: @abstractmethod def song_select_media( - self, song: BriefSongProtocol, policy=None + self, song: BriefSongModel, policy=None ) -> Tuple[Media, Quality.Audio]: """Select a media by the quality sorting policy @@ -92,7 +89,7 @@ def song_select_media( @abstractmethod def song_get_media( - self, song: BriefVideoProtocol, quality: Quality.Audio + self, song: BriefVideoModel, quality: Quality.Audio ) -> Optional[Media]: """Get song's media by a specified quality @@ -104,21 +101,21 @@ def song_get_media( @eq(ModelType.song, PF.hot_comments) @runtime_checkable class SupportsSongHotComments(Protocol): - def song_list_hot_comments(self, song: BriefSongProtocol) -> List[BriefCommentModel]: + def song_list_hot_comments(self, song: BriefSongModel) -> List[BriefCommentModel]: raise NotImplementedError @eq(ModelType.song, PF.web_url) @runtime_checkable class SupportsSongWebUrl(Protocol): - def song_get_web_url(self, song: BriefSongProtocol) -> str: + def song_get_web_url(self, song: BriefSongModel) -> str: raise NotImplementedError @eq(ModelType.song, PF.lyric) @runtime_checkable class SupportsSongLyric(Protocol): - def song_get_lyric(self, song: BriefSongProtocol) -> Optional[LyricProtocol]: + def song_get_lyric(self, song: BriefSongModel) -> Optional[LyricModel]: """Get music video of the song """ raise NotImplementedError @@ -127,7 +124,7 @@ def song_get_lyric(self, song: BriefSongProtocol) -> Optional[LyricProtocol]: @eq(ModelType.song, PF.mv) @runtime_checkable class SupportsSongMV(Protocol): - def song_get_mv(self, song: BriefSongProtocol) -> Optional[VideoProtocol]: + def song_get_mv(self, song: BriefSongModel) -> Optional[VideoModel]: """Get music video of the song """ @@ -154,7 +151,7 @@ def album_get(self, identifier: ID) -> AlbumModel: @runtime_checkable class SupportsAlbumSongsReader(Protocol): @abstractmethod - def album_create_songs_rd(self, album) -> List[SongProtocol]: + def album_create_songs_rd(self, album) -> List[SongModel]: raise NotImplementedError @@ -178,7 +175,7 @@ def artist_get(self, identifier: ID) -> ArtistModel: @runtime_checkable class SupportsArtistSongsReader(Protocol): @abstractmethod - def artist_create_songs_rd(self, artist: BriefArtistProtocol): + def artist_create_songs_rd(self, artist: BriefArtistModel): """Create songs reader of the artist """ raise NotImplementedError @@ -188,7 +185,7 @@ def artist_create_songs_rd(self, artist: BriefArtistProtocol): @runtime_checkable class SupportsArtistAlbumsReader(Protocol): @abstractmethod - def artist_create_albums_rd(self, artist: BriefArtistProtocol): + def artist_create_albums_rd(self, artist: BriefArtistModel): """Create albums reader of the artist """ raise NotImplementedError @@ -197,7 +194,7 @@ def artist_create_albums_rd(self, artist: BriefArtistProtocol): @runtime_checkable class SupportsArtistContributedAlbumsReader(Protocol): @abstractmethod - def artist_create_contributed_albums_rd(self, artist: BriefArtistProtocol): + def artist_create_contributed_albums_rd(self, artist: BriefArtistModel): """Create contributed albums reader of the artist """ raise NotImplementedError @@ -223,16 +220,16 @@ def video_get(self, identifier: ID) -> VideoModel: @runtime_checkable class SupportsVideoMultiQuality(Protocol): @abstractmethod - def video_list_quality(self, video: BriefVideoProtocol) -> List[Quality.Video]: + def video_list_quality(self, video: BriefVideoModel) -> List[Quality.Video]: raise NotImplementedError @abstractmethod def video_select_media( - self, video: BriefVideoProtocol, policy=None) -> Tuple[Media, Quality.Video]: + self, video: BriefVideoModel, policy=None) -> Tuple[Media, Quality.Video]: raise NotImplementedError @abstractmethod - def video_get_media(self, video: BriefVideoProtocol, quality) -> Optional[Media]: + def video_get_media(self, video: BriefVideoModel, quality) -> Optional[Media]: raise NotImplementedError diff --git a/feeluown/player/playlist.py b/feeluown/player/playlist.py index 9eae420af2..a641b490d2 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, SongModel, ModelType, NotSupported, ResourceNotFound, ) from feeluown.media import Media from feeluown.library import reverse @@ -425,7 +425,7 @@ def previous(self) -> Optional[asyncio.Task]: return self.set_current_song(self.previous_song) @property - def current_song(self) -> Optional[SongProtocol]: + def current_song(self) -> Optional[SongModel]: """Current song return None if there is no current song @@ -433,7 +433,7 @@ def current_song(self) -> Optional[SongProtocol]: return self._current_song @current_song.setter - def current_song(self, song: Optional[SongProtocol]): + def current_song(self, song: Optional[SongModel]): self.set_current_song(song) def set_current_song(self, song) -> Optional[asyncio.Task]: diff --git a/feeluown/server/handlers/base.py b/feeluown/server/handlers/base.py index 0bfe5b7d31..b133180697 100644 --- a/feeluown/server/handlers/base.py +++ b/feeluown/server/handlers/base.py @@ -1,7 +1,11 @@ -from typing import Dict, Type, TypeVar, Optional +from typing import Dict, Type, TypeVar, Optional, TYPE_CHECKING from feeluown.server.session import SessionLike +if TYPE_CHECKING: + from feeluown.app.server_app import ServerApp + + T = TypeVar('T', bound='HandlerMeta') cmd_handler_mapping: Dict[str, 'HandlerMeta'] = {} @@ -22,7 +26,7 @@ def __new__(cls: Type[T], name, bases, attrs) -> T: class AbstractHandler(metaclass=HandlerMeta): support_aio_handle = False - def __init__(self, app, session: Optional[SessionLike] = None): + def __init__(self, app: 'ServerApp', session: Optional[SessionLike] = None): """ 暂时不确定 session 应该设计为什么样的结构。当前主要是为了将它看作一个 subscriber。大部分 handler 不需要使用到 session 对像,目前只有 SubHandler diff --git a/setup.cfg b/setup.cfg index 9b57c4fb72..3c0a77b550 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,5 +34,3 @@ addopts = -q --doctest-modules [mypy-mpv] ignore_errors = True -[mypy-feeluown.models.*] -ignore_errors = True diff --git a/tests/library/test_model_v2.py b/tests/library/test_model_v2.py index a6491025ed..939a4969d8 100644 --- a/tests/library/test_model_v2.py +++ b/tests/library/test_model_v2.py @@ -23,6 +23,10 @@ def test_create_song_model_basic(): # check song's attribute value assert song.artists_name == 'Audrey' + # song model cache get/set + song.cache_set('count', 1) + assert song.cache_get('count') == (1, True) + def test_create_model_with_extra_field(): with pytest.raises(pydantic.ValidationError):