Skip to content

Commit

Permalink
player: some improvements about the player
Browse files Browse the repository at this point in the history
  • Loading branch information
cosven committed Dec 10, 2023
1 parent b8c29b6 commit 1c575b2
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 37 deletions.
54 changes: 40 additions & 14 deletions feeluown/gui/drawers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from typing import Optional

from PyQt5.QtCore import Qt, QRect, QPoint, QPointF
from PyQt5.QtGui import QPainter, QBrush, QPixmap, QImage, QColor, QPolygonF
from PyQt5.QtWidgets import QWidget

from feeluown.gui.helpers import random_solarized_color


class PixmapDrawer:
"""Draw pixmap on a widget with radius.
Expand All @@ -12,25 +16,30 @@ def __init__(self, img, widget: QWidget, radius: int = 0):
"""
:param widget: a object which has width() and height() method.
"""
self._img = img
self._widget_last_width = widget.width()

new_img = img.scaledToWidth(self._widget_last_width, Qt.SmoothTransformation)
self._pixmap = QPixmap(new_img)

self._widget = widget
self._radius = radius

if img is None:
self._color = random_solarized_color()
self._img = None
self._pixmap = None
else:
self._img = img
self._color = None
new_img = img.scaledToWidth(self._widget_last_width, Qt.SmoothTransformation)
self._pixmap = QPixmap(new_img)

@classmethod
def from_img_data(cls, img_data, *args, **kwargs):
img = QImage()
img.loadFromData(img_data)
return cls(img, *args, **kwargs)

def get_img(self) -> QImage:
def get_img(self) -> Optional[QImage]:
return self._img

def get_pixmap(self) -> QPixmap:
def get_pixmap(self) -> Optional[QPixmap]:
return self._pixmap

def maybe_update_pixmap(self):
Expand All @@ -41,21 +50,39 @@ def maybe_update_pixmap(self):
self._pixmap = QPixmap(new_img)

def draw(self, painter):
if self._pixmap is None:
return

self.maybe_update_pixmap()

painter.save()
painter.setRenderHint(QPainter.Antialiasing)
painter.setRenderHint(QPainter.SmoothPixmapTransform)
if self._pixmap is None:
self._draw_random_color(painter)
else:
self._draw_pixmap(painter)
painter.restore()

def _get_radius(self):
return self._radius if self._radius >= 1 else self._widget.width() * self._radius

def _draw_random_color(self, painter: QPainter):
brush = QBrush(self._color)
painter.setBrush(brush)
painter.setPen(Qt.NoPen)
rect = self._widget.rect()
if self._radius == 0:
painter.drawRect(rect)
else:
radius = self._get_radius()
painter.drawRoundedRect(rect, radius, radius)

def _draw_pixmap(self, painter: QPainter):
assert self._pixmap is not None

self.maybe_update_pixmap()
brush = QBrush(self._pixmap)
painter.setBrush(brush)
painter.setPen(Qt.NoPen)
radius = self._radius
size = self._pixmap.size()
y = (size.height() - self._widget.height()) // 2

painter.save()
painter.translate(0, -y)
rect = QRect(0, y, self._widget.width(), self._widget.height())
Expand All @@ -65,7 +92,6 @@ def draw(self, painter):
radius = radius if self._radius >= 1 else self._widget.width() * self._radius
painter.drawRoundedRect(rect, radius, radius)
painter.restore()
painter.restore()


class AvatarIconDrawer:
Expand Down
4 changes: 4 additions & 0 deletions feeluown/gui/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,10 @@ async def fetch_model_cover(model, cb):
return fetch_model_cover


def random_solarized_color():
return QColor(random.choice(list(SOLARIZED_COLORS.values())))


# https://ethanschoonover.com/solarized/
SOLARIZED_COLORS = {
'yellow': '#b58900',
Expand Down
6 changes: 3 additions & 3 deletions feeluown/gui/uimain/player_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,7 @@ def _setup_ui(self):
self._layout.setContentsMargins(0, 0, 0, 0)

def on_metadata_changed(self, metadata):
if not metadata:
return

metadata = metadata or {}
released = metadata.get('released', '')
if released:
self.cover_label.setToolTip(f'专辑发行日期:{released}')
Expand All @@ -180,6 +178,8 @@ def on_metadata_changed(self, metadata):
artwork_uid = metadata.get('uri', artwork)
if artwork:
run_afn(self.cover_label.show_cover, artwork, artwork_uid)
else:
self.cover_label.show_img(None)


class TopPanel(QFrame):
Expand Down
21 changes: 10 additions & 11 deletions feeluown/gui/widgets/cover_label.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import warnings
from typing import Optional

from PyQt5.QtCore import QSize
from PyQt5.QtGui import QPainter, QImage
Expand All @@ -13,7 +14,7 @@ def __init__(self, parent=None, pixmap=None, radius=3):
super().__init__(parent=parent)

self._radius = radius
self.drawer = None
self.drawer = PixmapDrawer(None, self, self._radius)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.MinimumExpanding)

def show_pixmap(self, pixmap):
Expand All @@ -24,12 +25,11 @@ def show_pixmap(self, pixmap):
self.updateGeometry()
self.update() # Schedule a repaint to refresh the UI ASAP.

def show_img(self, img: QImage):
def show_img(self, img: Optional[QImage]):
if not img or img.isNull():
self.drawer = None
return

self.drawer = PixmapDrawer(img, self, self._radius)
self.drawer = PixmapDrawer(None, self, self._radius)
else:
self.drawer = PixmapDrawer(img, self, self._radius)
self.updateGeometry()
self.update()

Expand All @@ -41,12 +41,11 @@ def paintEvent(self, e):
one is as follow, the other way is using bitmap mask,
but in our practice, the mask way has poor render effects
"""
if self.drawer:
painter = QPainter(self)
self.drawer.draw(painter)
painter = QPainter(self)
self.drawer.draw(painter)

def contextMenuEvent(self, e):
if self.drawer is None:
if self.drawer.get_img() is None:
return
menu = QMenu()
action = menu.addAction('查看原图')
Expand All @@ -60,7 +59,7 @@ def resizeEvent(self, e):

def sizeHint(self):
super_size = super().sizeHint()
if self.drawer is None:
if self.drawer.get_pixmap() is None:
return super_size
h = (self.width() * self.drawer.get_pixmap().height()) \
// self.drawer.get_pixmap().width()
Expand Down
3 changes: 3 additions & 0 deletions feeluown/library/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ class BriefSongModel(BaseBriefModel):
album_name: str = ''
duration_ms: str = ''

def __str__(self):
return f'{self.title} - {self.artists_name}'


class BriefVideoModel(BaseBriefModel):
meta: Any = ModelMeta.create(ModelType.video, is_brief=True)
Expand Down
32 changes: 23 additions & 9 deletions feeluown/player/playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import logging
import random
from enum import IntEnum, Enum
from typing import Optional
from typing import Optional, TYPE_CHECKING

from feeluown.excs import ProviderIOError
from feeluown.utils import aio
from feeluown.utils.aio import run_fn
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
Expand All @@ -17,6 +17,9 @@
from feeluown.media import Media
from feeluown.models.uri import reverse

if TYPE_CHECKING:
from feeluown.app import App

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -71,7 +74,7 @@ class PlaylistMode(IntEnum):


class Playlist:
def __init__(self, app, songs=None, playback_mode=PlaybackMode.loop,
def __init__(self, app: 'App', songs=None, playback_mode=PlaybackMode.loop,
audio_select_policy='hq<>'):
"""
:param songs: list of :class:`feeluown.models.SongModel`
Expand Down Expand Up @@ -397,6 +400,9 @@ def previous_song(self):
previous_song = self._get_good_song(base=current_index - 1, direction=-1)
return previous_song

async def a_next(self):
self.next()

def next(self) -> Optional[asyncio.Task]:
if self.next_song is None:
self.eof_reached.emit()
Expand Down Expand Up @@ -473,7 +479,7 @@ async def a_set_current_song(self, song):
self.batch_add(song.children)
await self.a_set_current_song(song.children[0])
else:
self.next()
run_afn(self.a_next)
return

logger.info(f'{song_str} has no valid media, mark it as bad')
Expand All @@ -482,7 +488,7 @@ async def a_set_current_song(self, song):
# if mode is fm mode, do not find standby song,
# just skip the song
if self.mode is PlaylistMode.fm:
self.next()
run_afn(self.a_next)
return

self._app.show_msg(f'{song_str} is invalid, try to find standby')
Expand All @@ -493,15 +499,19 @@ async def a_set_current_song(self, song):
)
if standby_candidates:
standby, media = standby_candidates[0]
self._app.show_msg(f'Song standby was found in {standby.source} ✅')
msg = f'Song standby was found in {standby.source} ✅'
logger.info(msg)
self._app.show_msg(msg)
# Insert the standby song after the song
if song in self._songs and standby not in self._songs:
index = self._songs.index(song)
self._songs.insert(index + 1, standby)
self.songs_added.emit(index + 1, 1)
target_song = standby
else:
self._app.show_msg('Song standby not found')
msg = 'Song standby not found'
logger.info(msg)
self._app.show_msg(msg)
except ProviderIOError as e:
# FIXME: This may cause infinite loop when the prepare media always fails
logger.error(f'prepare media failed: {e}, try next song')
Expand All @@ -511,7 +521,7 @@ async def a_set_current_song(self, song):
logger.exception('prepare media failed due to unknown error, '
'so we mark the song as a bad one')
self.mark_as_bad(song)
self.next()
run_afn(self.a_next)
return
else:
assert media, "media must not be empty"
Expand All @@ -534,7 +544,7 @@ def pure_set_current_song(self, song, media, metadata=None):

if song is not None:
if media is None:
self.next()
run_afn(self.a_next)
else:
# Note that the value of model v1 {}_display may be None.
kwargs = {}
Expand Down Expand Up @@ -607,6 +617,7 @@ async def _prepare_metadata_for_video(self, video):

async def _prepare_media(self, song):
task_spec = self._app.task_mgr.get_or_create('prepare-media')
task_spec.disable_default_cb()
if self.watch_mode is True:
try:
mv_media = await task_spec.bind_blocking_io(
Expand Down Expand Up @@ -671,6 +682,8 @@ def play_model(self, model):
.. versionadded: 3.7.14
"""
# Stop the player so that user know the action is working.
self._app.player.stop()
task = self.set_current_model(model)
if task is not None:
def cb(future):
Expand All @@ -680,4 +693,5 @@ def cb(future):
logger.exception('play model failed')
else:
self._app.player.resume()
logger.info(f'play a model ({model}) succeed')
task.add_done_callback(cb)
1 change: 1 addition & 0 deletions tests/player/test_playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ async def test_play_next_bad_song(app_mock, song, song1, mocker):
pl._current_song = song
await pl.a_set_current_song(pl.next_song)
assert mock_mark_as_bad.called
await asyncio.sleep(0.1)
assert mock_next.called


Expand Down

0 comments on commit 1c575b2

Please sign in to comment.