Skip to content

Commit

Permalink
all tests passed!
Browse files Browse the repository at this point in the history
  • Loading branch information
cosven committed Jan 6, 2024
1 parent e24a8f0 commit 84b9a07
Show file tree
Hide file tree
Showing 11 changed files with 50 additions and 252 deletions.
29 changes: 0 additions & 29 deletions examples/model_display.py

This file was deleted.

3 changes: 2 additions & 1 deletion feeluown/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,8 @@ def on_provider_added(self, provider):
if not self._has_nonexistent_models:
return
for i, model in enumerate(self.models.copy()):
if model.state is ModelState.not_exists and model.source == provider.identifier:
if model.state is ModelState.not_exists and \
model.source == provider.identifier:
new_model = resolve(reverse(model, as_line=True))
# TODO: emit data changed signal
self.models[i] = new_model
Expand Down
6 changes: 3 additions & 3 deletions feeluown/gui/components/song_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ def on_metadata_changed(self, metadata):

async def _switch_provider(self, provider_id):
song = self._app.playlist.current_song
songs = await self._app.library.a_list_song_standby(
songs = await self._app.library.a_list_song_standby_v2(
song, source_in=[provider_id])
if songs:
standby = songs[0]
standby, media = songs[0]
assert standby != song
self._app.show_msg(f'使用 {standby} 替换当前歌曲')
self._app.playlist.pure_set_current_song(standby, standby.url)
self._app.playlist.pure_set_current_song(standby, media)
self._app.playlist.remove(song)
else:
self._app.show_msg(f'提供方 “{provider_id}” 没有找到可用的相似歌曲')
2 changes: 1 addition & 1 deletion feeluown/gui/widgets/songs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from feeluown.utils import aio
from feeluown.utils.dispatch import Signal
from feeluown.library import ModelState, ModelFlags
from feeluown.library import ModelState

from feeluown.gui.mimedata import ModelMimeData
from feeluown.gui.helpers import ItemViewNoScrollMixin, ReaderFetchMoreMixin
Expand Down
155 changes: 10 additions & 145 deletions feeluown/library/library.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# mypy: disable-error-code=type-abstract
import logging
import warnings
from functools import partial, lru_cache
from functools import partial
from typing import cast, Optional, Union, TypeVar, Type, Callable, Any

from feeluown.media import Media
from feeluown.utils import aio
from feeluown.utils.dispatch import Signal
from feeluown.utils.utils import log_exectime
from feeluown.utils.reader import create_reader
from .base import SearchType, ModelType
from .provider import AbstractProvider
Expand All @@ -18,8 +17,7 @@
)
from .flags import Flags as PF
from .models import (
ModelFlags as MF, BaseModel, BriefSongModel, UserModel,
get_modelcls_by_type,
ModelFlags as MF, BriefSongModel, UserModel,
)
from .model_protocol import (
BriefVideoProtocol, ModelProtocol, BriefSongProtocol, SongProtocol, UserProtocol,
Expand Down Expand Up @@ -240,69 +238,6 @@ async def a_search(self, keyword, source_in=None, timeout=None,
if result is not None:
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

async def a_list_song_standby_v2(self, song,
audio_select_policy='>>>', source_in=None,
score_fn=None, min_score=MIN_SCORE, limit=1):
Expand Down Expand Up @@ -414,37 +349,6 @@ def check_flags_by_model(self, model: ModelProtocol, flags: PF) -> bool:
ModelType(model.meta.model_type),
flags)

# ---------------------------
# Methods for backward compat
# ---------------------------
def cast_model_to_v1(self, model):
"""Cast a v1/v2 model to v1 (for protocol)
During the model migration from v1 to v2, v2 may lack some ability.
Cast the model to v1 to acquire such ability.
:raises NotSupported: provider doesn't support v1 model
"""
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)
# The source of the default SongModel is None. When ModelCls.source
# is None, it means that the provider does not has its own model class.
if ModelCls.source is None:
model_type_str = repr(ModelType(model.meta.model_type))
emsg = f'provider:{model.source} has no v1 model for {model_type_str}'
e = NotSupported(emsg, provider=provider)
raise e
kv = {}
for field in ModelCls.meta.fields_display:
kv[field] = getattr(model, field)
return ModelCls.create_by_display(identifier=model.identifier, **kv)

# -----
# Songs
# -----
Expand Down Expand Up @@ -493,10 +397,6 @@ def song_get_mv(self, song: BriefSongProtocol) -> Optional[VideoProtocol]:
provider = self.get_or_raise(song.source)
if isinstance(provider, SupportsSongMV):
mv = provider.song_get_mv(song)
elif not self.check_flags(song.source, song.meta.model_type, PF.model_v2):
song_v1 = self.cast_model_to_v1(song)
mv = song_v1.mv
mv = cast(Optional[VideoProtocol], mv)
else:
mv = None
return mv
Expand All @@ -512,12 +412,8 @@ def song_get_lyric(self, song: BriefSongModel) -> Optional[LyricProtocol]:
"""
provider = self.get_or_raise(song.source)
if isinstance(provider, SupportsSongLyric):
lyric = provider.song_get_lyric(song)
else:
song_v1 = self.cast_model_to_v1(song)
lyric = song_v1.lyric
lyric = cast(Optional[LyricProtocol], lyric)
return lyric
return provider.song_get_lyric(song)
raise NotSupported

def song_get_web_url(self, song: BriefSongProtocol) -> str:
provider = self.getv2_or_raise(song.source)
Expand Down Expand Up @@ -551,17 +447,7 @@ def _handle_protocol_with_model(self,
provider = self.get_or_raise(model.source)
if isinstance(provider, protocol_cls):
return v2_handler(provider, model)

try:
v1model = self.cast_model_to_v1(model)
except NotSupported as e:
# Make the error message more informative.
if e.provider is not None:
pid = e.provider.identifier
msg = f'provider:{pid} does not support {protocol_cls.__name__}'
raise NotSupported(msg)
raise # This branch should not be reached.
return v1_handler(v1model)
raise NotSupported(f'{protocol_cls} not supported')

# --------
# Artist
Expand Down Expand Up @@ -632,11 +518,8 @@ def playlist_remove_song(self, playlist, song) -> bool:
"""
provider = self.get_or_raise(playlist.source)
if isinstance(provider, SupportsPlaylistRemoveSong):
ok = provider.playlist_remove_song(playlist, song)
else:
playlist = self.cast_model_to_v1(playlist)
ok = playlist.remove(song.identifier)
return ok
return provider.playlist_remove_song(playlist, song)
raise NotSupported

def playlist_add_song(self, playlist, song) -> bool:
"""Add a song to the playlist
Expand All @@ -645,11 +528,8 @@ def playlist_add_song(self, playlist, song) -> bool:
"""
provider = self.get_or_raise(playlist.source)
if isinstance(provider, SupportsPlaylistAddSong):
ok = provider.playlist_add_song(playlist, song)
else:
playlist = self.cast_model_to_v1(playlist)
ok = playlist.add(song.identifier)
return ok
return provider.playlist_add_song(playlist, song)
raise NotSupported

# -------------------------
# generic methods for model
Expand Down Expand Up @@ -749,22 +629,7 @@ def _model_upgrade(self, model):
model.state = ModelState.not_exists
raise ModelNotFound(f'provider:{provider} return an empty model')
return upgraded_model

# Fallback to v1 way if the provider does not support PF.get.
# For example, provider X may support 'get' for SongModel and it
# does not support 'get' for ArtistModel temporarily. It returns
# a SongModel and the song.artists returns list of BriefArtistModel,
# in this condition, BriefArtistModel should be upgraded in v1 way.
return self._model_upgrade_in_v1_way(model)

def _model_upgrade_in_v1_way(self, model):
v1model = self.cast_model_to_v1(model)
modelcls = get_modelcls_by_type(ModelType(model.meta.model_type))
fields = [f for f in list(modelcls.__fields__)
if f not in list(BaseModel.__fields__)]
for field in fields:
getattr(v1model, field)
return v1model
raise NotSupported

# --------
# Video
Expand Down
2 changes: 1 addition & 1 deletion feeluown/library/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ class SongModel(BaseNormalModel):
"""
meta: Any = ModelMeta.create(ModelType.song, is_normal=True)
title: str
album: Optional[BriefAlbumModel] = None
album: Optional[TAlbum] = None
artists: List[BriefArtistModel]
duration: int # milliseconds
# A playlist can consist of multiple songs and a song can have many children.
Expand Down
28 changes: 5 additions & 23 deletions feeluown/library/uri.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@

from .base import ModelType
from .model_state import ModelState
from .models import (
BriefSongModel, BriefArtistModel, BriefAlbumModel,
BriefPlaylistModel, BriefUserModel, BriefVideoModel
)
from .models import get_modelcls_by_type


logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -60,23 +59,6 @@ class NoReverseMatch(Exception):
}


def get_model_cls(type_):
if type_ is ModelType.song:
return BriefSongModel
elif type_ is ModelType.artist:
return BriefArtistModel
elif type_ is ModelType.album:
return BriefAlbumModel
elif type_ is ModelType.playlist:
return BriefPlaylistModel
elif type_ is ModelType.user:
return BriefUserModel
elif type_ is ModelType.video:
return BriefVideoModel
else:
raise ValueError('invalid model type')


class Resolver:
loop = None
library = None
Expand Down Expand Up @@ -248,7 +230,7 @@ def parse_line(line):
raise ResolveFailed('invalid line: {}'.format(line))
source, ns, identifier = m.groups()
path = uri[m.end():]
Model = get_model_cls(NS_TYPE_MAP[ns])
Model = get_modelcls_by_type(NS_TYPE_MAP[ns], brief=True)
if ns == 'songs':
parse_func = parse_song_str
elif ns == 'albums':
Expand All @@ -261,7 +243,7 @@ def parse_line(line):
parse_func = parse_unknown
data = parse_func(model_str.strip())
data['source'] = source
model = Model.create_by_display(identifier=identifier, **data)
model = Model(identifier=identifier, **data)
return model, path


Expand Down
2 changes: 1 addition & 1 deletion feeluown/serializers/model_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class SearchSerializerMixin:
"""

class Meta:
types = ( )
types = ()

def _get_items(self, result):
fields = ('songs', 'albums', 'artists', 'playlists',)
Expand Down
Loading

0 comments on commit 84b9a07

Please sign in to comment.