Skip to content

Commit

Permalink
player: slightly refactor metadata manager
Browse files Browse the repository at this point in the history
  • Loading branch information
cosven committed Jul 24, 2024
1 parent 8921ae2 commit 90c01ea
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 80 deletions.
3 changes: 2 additions & 1 deletion feeluown/player/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from .playlist import PlaybackMode, PlaylistRepeatMode, PlaylistShuffleMode
from .base_player import State
from .mpvplayer import MpvPlayer as Player
from .playlist import PlaylistMode, Playlist
from .playlist import PlaylistMode, Playlist, MetadataManager
from .fm import FM
from .radio import SongRadio
from .lyric import LiveLyric, parse_lyric_text, Line as LyricLine, Lyric
Expand All @@ -24,6 +24,7 @@
'Playlist',
'PlayerPositionDelegate',

'MetadataManager',
'Metadata',
'MetadataFields',

Expand Down
144 changes: 76 additions & 68 deletions feeluown/player/playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def __init__(self, app: 'App', songs=None, playback_mode=PlaybackMode.loop,
:param playback_mode: :class:`feeluown.player.PlaybackMode`
"""
self._app = app
self._metadata_mgr = MetadataManager(app)

#: init playlist mode normal
self._mode = PlaylistMode.normal
Expand Down Expand Up @@ -526,7 +527,7 @@ async def a_set_current_song(self, song):
self.mark_as_bad(song)
target_song, media = await self.find_and_use_standby(song)

metadata = await self._prepare_metadata_for_song(target_song)
metadata = await self._metadata_mgr.prepare_for_song(target_song)
self.pure_set_current_song(target_song, media, metadata)

async def a_set_current_song_children(self, song):
Expand Down Expand Up @@ -590,72 +591,6 @@ def pure_set_current_song(self, song, media, metadata=None):
else:
self._app.player.stop()

async def _prepare_metadata_for_song(self, song):
metadata = Metadata({
MetadataFields.uri: reverse(song),
MetadataFields.source: song.source,
MetadataFields.title: song.title_display or '',
# The song.artists_name should return a list of strings
MetadataFields.artists: [song.artists_name_display or ''],
MetadataFields.album: song.album_name_display or '',
})
try:
song: SongModel = await aio.wait_for(
aio.run_fn(self._app.library.song_upgrade, song),
timeout=1,
)
except ResourceNotFound:
return metadata
except: # noqa
logger.exception(f"fetching song's meta failed, song:'{song.title_display}'")
return metadata

artwork = song.pic_url
released = song.date
if not (artwork and released) and song.album is not None:
try:
album = await aio.wait_for(
aio.run_fn(self._app.library.album_upgrade, song.album),
timeout=1
)
except ResourceNotFound:
pass
except: # noqa
logger.warning(
f"fetching song's album meta failed, song:{song.title_display}")
else:
artwork = album.cover or artwork
released = album.released or released
# For model v1, the cover can be a Media object.
# For example, in fuo_local plugin, the album.cover is a Media
# object with url set to fuo://local/songs/{identifier}/data/cover.
if isinstance(artwork, Media):
artwork = artwork.url

# Try to use album meta first.
if artwork and released:
metadata[MetadataFields.artwork] = artwork
metadata[MetadataFields.released] = released
else:
metadata[MetadataFields.artwork] = song.pic_url or artwork
metadata[MetadataFields.released] = song.date or released
return metadata

async def _prepare_metadata_for_video(self, video):
metadata = Metadata({
# The value of model v1 title_display may be None.
MetadataFields.title: video.title_display or '',
MetadataFields.source: video.source,
MetadataFields.uri: reverse(video),
})
try:
video = await aio.run_fn(self._app.library.video_upgrade, video)
except ModelNotFound as e:
logger.warning(f"can't get cover of video due to {str(e)}")
else:
metadata[MetadataFields.artwork] = video.cover
return metadata

async def _prepare_media(self, song):
task_spec = self._app.task_mgr.get_or_create('prepare-media')
# task_spec.disable_default_cb()
Expand Down Expand Up @@ -710,7 +645,7 @@ async def a_set_current_model(self, model):
except MediaNotFound:
self._app.show_msg('没有可用的播放链接')
else:
metadata = await self._prepare_metadata_for_video(video)
metadata = await self._metadata_mgr.prepare_for_video(video)
kwargs = {}
if not self._app.has_gui:
kwargs['video'] = False
Expand Down Expand Up @@ -741,3 +676,76 @@ def cb(future):
self._app.player.resume()
logger.info(f'play a model ({model}) succeed')
task.add_done_callback(cb)


class MetadataManager:
def __init__(self, app: 'App'):
self._app = app

def _prepare_basic_metadata_for_song(self, song):
return Metadata({
MetadataFields.uri: reverse(song),
MetadataFields.source: song.source,
MetadataFields.title: song.title_display or '',
# The song.artists_name should return a list of strings
MetadataFields.artists: [song.artists_name_display or ''],
MetadataFields.album: song.album_name_display or '',
})

async def fetch_from_song(self, song):
empty_result = ('', '', None)
try:
usong: SongModel = await aio.wait_for(
aio.run_fn(self._app.library.song_upgrade, song),
timeout=1,
)
except ResourceNotFound:
return empty_result
except: # noqa
logger.exception(f"fetching song's meta failed, song:'{song.title_display}'")
return empty_result
return (usong.pic_url, usong.date, usong.album)

async def fetch_from_album(self, album):
empty_result = ('', '')
try:
album = await aio.wait_for(
aio.run_fn(self._app.library.album_upgrade, album),
timeout=1
)
except ResourceNotFound:
return empty_result
except: # noqa
logger.warning(
f"fetching album meta failed, album:{album.name}")
return empty_result
return (album.cover, album.released)

async def prepare_for_song(self, song):
metadata = self._prepare_basic_metadata_for_song(song)

artwork, released, album = await self.fetch_from_song(song)
if not (artwork and released) and album is not None:
album_cover, album_released = await self.fetch_from_album(album)
# Try to use album meta first.
artwork = album_cover or artwork
released = album_released or released
metadata[MetadataFields.artwork] = artwork
metadata[MetadataFields.released] = released

return metadata

async def prepare_for_video(self, video):
metadata = Metadata({
# The value of model v1 title_display may be None.
MetadataFields.title: video.title_display or '',
MetadataFields.source: video.source,
MetadataFields.uri: reverse(video),
})
try:
video = await aio.run_fn(self._app.library.video_upgrade, video)
except ModelNotFound as e:
logger.warning(f"can't get cover of video due to {str(e)}")
else:
metadata[MetadataFields.artwork] = video.cover
return metadata
4 changes: 2 additions & 2 deletions tests/player/test_fm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest

from feeluown.excs import ProviderIOError
from feeluown.player import Playlist, PlaylistMode, FM
from feeluown.player import Playlist, PlaylistMode, FM, MetadataManager
from feeluown.task import TaskManager


Expand Down Expand Up @@ -76,7 +76,7 @@ async def test_multiple_eof_reached_signal(app_mock, song, mocker):
async def test_reactivate_fm_mode_after_playing_other_songs(
app_mock, song, song1, mocker):

mocker.patch.object(Playlist, '_prepare_metadata_for_song')
mocker.patch.object(MetadataManager, 'prepare_for_song')

def f(*args, **kwargs): return [song1]

Expand Down
17 changes: 8 additions & 9 deletions tests/player/test_playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
import pytest

from feeluown.library.excs import MediaNotFound
from feeluown.media import Media
from feeluown.player import (
Playlist, PlaylistMode, Player, PlaybackMode,
PlaylistRepeatMode, PlaylistShuffleMode,
PlaylistRepeatMode, PlaylistShuffleMode, MetadataManager
)
from feeluown.utils.dispatch import Signal

Expand Down Expand Up @@ -116,7 +115,7 @@ async def test_set_current_song_with_bad_song_1(
mock_pure_set_current_song = mocker.patch.object(Playlist, 'pure_set_current_song')
mock_mark_as_bad = mocker.patch.object(Playlist, 'mark_as_bad')
sentinal = object()
mocker.patch.object(Playlist, '_prepare_metadata_for_song', return_value=sentinal)
mocker.patch.object(MetadataManager, 'prepare_for_song', return_value=sentinal)
await pl.a_set_current_song(song2)
# A song that has no valid media should be marked as bad
assert mock_mark_as_bad.called
Expand All @@ -132,7 +131,7 @@ async def test_set_current_song_with_bad_song_2(
mock_pure_set_current_song = mocker.patch.object(Playlist, 'pure_set_current_song')
mock_mark_as_bad = mocker.patch.object(Playlist, 'mark_as_bad')
sentinal = object()
mocker.patch.object(Playlist, '_prepare_metadata_for_song', return_value=sentinal)
mocker.patch.object(MetadataManager, 'prepare_for_song', return_value=sentinal)
await pl.a_set_current_song(song2)
# A song that has no valid media should be marked as bad
assert mock_mark_as_bad.called
Expand All @@ -150,7 +149,7 @@ async def test_set_current_song_with_bad_song_3(
mock_pure_set_current_song = mocker.patch.object(Playlist, 'pure_set_current_song')
mock_prepare_mv_media = mocker.patch.object(Playlist, '_prepare_mv_media',
return_value=media)
mocker.patch.object(Playlist, '_prepare_metadata_for_song', return_value=metadata)
mocker.patch.object(MetadataManager, 'prepare_for_song', return_value=metadata)

app_mock.config.ENABLE_MV_AS_STANDBY = 1
pl = Playlist(app_mock)
Expand Down Expand Up @@ -181,7 +180,7 @@ async def test_set_an_existing_bad_song_as_current_song(
song1 is bad, standby is [song2]
play song1, song2 should be insert after song1 instead of song
"""
mocker.patch.object(Playlist, '_prepare_metadata_for_song')
mocker.patch.object(MetadataManager, 'prepare_for_song')
await pl.a_set_current_song(song1)
assert pl.list().index(song2) == 2

Expand Down Expand Up @@ -299,7 +298,7 @@ async def test_play_next_bad_song(app_mock, song, song1, mocker):
be marked as bad. Besides, it should try to find standby.
"""
mock_pure_set_current_song = mocker.patch.object(Playlist, 'pure_set_current_song')
mocker.patch.object(Playlist, '_prepare_metadata_for_song', return_value=object())
mocker.patch.object(MetadataManager, 'prepare_for_song', return_value=object())
mock_standby = mocker.patch.object(Playlist,
'find_and_use_standby',
return_value=(song1, None))
Expand Down Expand Up @@ -358,12 +357,12 @@ def test_playlist_next_should_call_set_current_song(app_mock, mocker, song):
async def test_playlist_prepare_metadata_for_song(
app_mock, library, pl, ekaf_brief_song0, mocker):
class Album:
cover = Media('fuo://')
cover = 'http://'
released = '2018-01-01'

app_mock.library = library
album = Album()
mocker.patch.object(library, 'album_upgrade', return_value=album)
# app_mock.library.album_upgrade.return_value = album
# When cover is a media object, prepare_metadata should also succeed.
await pl._prepare_metadata_for_song(ekaf_brief_song0)
await pl._metadata_mgr.prepare_for_song(ekaf_brief_song0)

0 comments on commit 90c01ea

Please sign in to comment.