Skip to content

Commit

Permalink
player: slightly refactor metadata assembler (#856)
Browse files Browse the repository at this point in the history
  • Loading branch information
cosven authored Jul 27, 2024
1 parent 710d4e7 commit 1f6496e
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 85 deletions.
2 changes: 2 additions & 0 deletions feeluown/player/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .base_player import State
from .mpvplayer import MpvPlayer as Player
from .playlist import PlaylistMode, Playlist
from .metadata_assembler import MetadataAssembler
from .fm import FM
from .radio import SongRadio
from .lyric import LiveLyric, parse_lyric_text, Line as LyricLine, Lyric
Expand All @@ -26,6 +27,7 @@

'Metadata',
'MetadataFields',
'MetadataAssembler',

'LiveLyric',
'parse_lyric_text',
Expand Down
87 changes: 87 additions & 0 deletions feeluown/player/metadata_assembler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import logging
from typing import TYPE_CHECKING

from feeluown.excs import ResourceNotFound, ModelNotFound
from feeluown.library import reverse, SongModel
from feeluown.player import Metadata, MetadataFields
from feeluown.utils import aio

if TYPE_CHECKING:
from feeluown.app import App

logger = logging.getLogger(__name__)


class MetadataAssembler:
"""Cook and fetch metadata for songs and videos"""
def __init__(self, app: 'App'):
self._app = app

@staticmethod
def cook_basic_metadata_for_song(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.cook_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
82 changes: 8 additions & 74 deletions feeluown/player/playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@
from enum import IntEnum, Enum
from typing import Optional, TYPE_CHECKING

from feeluown.excs import ModelNotFound, ProviderIOError
from feeluown.excs import ProviderIOError
from feeluown.utils import aio
from feeluown.utils.aio import run_fn, run_afn
from feeluown.utils.dispatch import Signal
from feeluown.utils.utils import DedupList
from feeluown.player import Metadata, MetadataFields
from feeluown.library import (
MediaNotFound, SongModel, ModelType, ResourceNotFound, VideoModel,
MediaNotFound, SongModel, ModelType, VideoModel,
)
from feeluown.media import Media
from feeluown.library import reverse
from .metadata_assembler import MetadataAssembler

if TYPE_CHECKING:
from feeluown.app import App
Expand All @@ -27,7 +26,7 @@ class PlaybackMode(IntEnum):
"""
Playlist playback mode.
.. versiondeprecated:: 3.8.12
.. deprecated:: 3.8.12
Please use PlaylistRepeatMode and PlaylistShuffleMode instead.
"""
one_loop = 0 #: One Loop
Expand Down Expand Up @@ -81,6 +80,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 = MetadataAssembler(app)

#: init playlist mode normal
self._mode = PlaylistMode.normal
Expand Down Expand Up @@ -327,7 +327,7 @@ def _get_good_song(self, base=0, random_=False, direction=1, loop=True):
"""从播放列表中获取一首可以播放的歌曲
:param base: base index
:param random: random strategy or not
:param random_: random strategy or not
:param direction: forward if > 0 else backward
:param loop: regard the song list as a loop
Expand Down Expand Up @@ -526,7 +526,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 +590,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 +644,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
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, MetadataAssembler
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(MetadataAssembler, '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, MetadataAssembler
)
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(MetadataAssembler, '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(MetadataAssembler, '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(MetadataAssembler, '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(MetadataAssembler, '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(MetadataAssembler, '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 1f6496e

Please sign in to comment.