Skip to content

Commit

Permalink
[enhance](*) introduce alert manager (#909)
Browse files Browse the repository at this point in the history
- [x] introduce alert manager
  - [x] show_msg when there is a ConnectTimeout currently
- [x] show error message in search page
  • Loading branch information
cosven authored Feb 4, 2025
1 parent 8d54432 commit d8ee8dd
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 26 deletions.
48 changes: 48 additions & 0 deletions feeluown/alert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import logging
from typing import TYPE_CHECKING, Optional
from urllib.parse import urlparse

from requests import ConnectTimeout


if TYPE_CHECKING:
from feeluown.app import App

logger = logging.getLogger(__name__)


class AlertManager:
"""Monitor app exceptions and send some alerts."""
def __init__(self):
# Some alerts handling rely on app and some are not.
self._app: Optional['App'] = None

def initialize(self, app: 'App'):
""""""
self._app = app
self._app.player.media_loading_failed.connect(
self.on_media_loading_failed, aioqueue=True)

def on_exception(self, e):
if isinstance(e, ConnectTimeout):
if e.request is not None:
url = e.request.url
hostname = urlparse(url).hostname
else:
hostname = ''
msg = f"链接'{hostname}'超时,请检查你的网络或者代理设置"
self.show_alert(msg)

def on_media_loading_failed(self, *_):
assert self._app is not None
media = self._app.player.current_media
if media and media.url:
proxy = f' {media.http_proxy}' if media.http_proxy else '空'
hostname = urlparse(media.url).hostname
msg = (f'无法播放来自 {hostname} 的资源(资源的 HTTP 代理为{proxy})'
'(注:播放引擎无法使用系统代理)')
self.show_alert(msg)

def show_alert(self, alert):
logger.warning(alert)
self._app.show_msg(alert, timeout=2000)
5 changes: 3 additions & 2 deletions feeluown/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from feeluown.plugin import plugins_mgr
from feeluown.version import VersionManager
from feeluown.task import TaskManager
from feeluown.alert import AlertManager

from .mode import AppMode

Expand Down Expand Up @@ -47,6 +48,7 @@ def __init__(self, args, config, **kwargs):
self.started = Signal() # App is ready to use, for example, UI is available.
self.about_to_shutdown = Signal()

self.alert_mgr = AlertManager()
self.request = Request() # TODO: rename request to http
self.version_mgr = VersionManager(self)
self.task_mgr = TaskManager(self)
Expand Down Expand Up @@ -109,12 +111,11 @@ def __init__(self, args, config, **kwargs):
self.about_to_shutdown.connect(lambda _: self.dump_and_save_state(), weak=False)

def initialize(self):
self.alert_mgr.initialize(self)
self.player_pos_per300ms.initialize()
self.player_pos_per300ms.changed.connect(self.live_lyric.on_position_changed)
self.playlist.song_changed.connect(self.live_lyric.on_song_changed,
aioqueue=True)
self.player.media_loading_failed.connect(
lambda *args: self.show_msg('播放器加载资源失败'), weak=False, aioqueue=True)
self.plugin_mgr.enable_plugins(self)

def run(self):
Expand Down
22 changes: 18 additions & 4 deletions feeluown/gui/components/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def __init__(self, app, **kwargs):
self._layout.addStretch(0)

async def search_and_render(self, q, search_type, source_in):
# pylint: disable=too-many-locals
# pylint: disable=too-many-locals,too-many-statements
view = self
app = self._app

Expand All @@ -88,9 +88,19 @@ async def search_and_render(self, q, search_type, source_in):
succeed = 0
start = datetime.now()
is_first = True # Is first search result.
view.hint.show_msg('正在搜索...')
if source_in is not None:
source_count = len(source_in)
else:
source_count = len(app.library.list())
hint_msgs = [f'正在搜索 {source_count} 个资源提供方...']
view.hint.show_msg('\n'.join(hint_msgs))
async for result in app.library.a_search(
q, type_in=search_type, source_in=source_in):
q, type_in=search_type, source_in=source_in, return_err=True):
if result.err_msg:
hint_msgs.append(f'搜索 {result.source} 的资源出错:{result.err_msg}')
view.hint.show_msg('\n'.join(hint_msgs))
continue

table_container = TableContainer(app, view.accordion)
table_container.layout().setContentsMargins(0, 0, 0, 0)

Expand All @@ -112,6 +122,8 @@ async def search_and_render(self, q, search_type, source_in):
_, search_type, attrname, show_handler = renderer.tabs[tab_index]
objects = getattr(result, attrname) or []
if not objects: # Result is empty.
hint_msgs.append(f'搜索 {result.source} 资源,提供方返回空')
view.hint.show_msg('\n'.join(hint_msgs))
continue

succeed += 1
Expand All @@ -130,7 +142,9 @@ async def search_and_render(self, q, search_type, source_in):
renderer.toolbar.hide()
is_first = False
time_cost = (datetime.now() - start).total_seconds()
view.hint.show_msg(f'搜索完成,共有 {succeed} 个有效的结果,花费 {time_cost:.2f}s')
hint_msgs.pop(0)
hint_msgs.insert(0, f'搜索完成,共有 {succeed} 个有效的结果,花费 {time_cost:.2f}s')
view.hint.show_msg('\n'.join(hint_msgs))


class SearchResultRenderer(Renderer, TabBarRendererMixin):
Expand Down
9 changes: 7 additions & 2 deletions feeluown/gui/widgets/labels.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from PyQt5.QtCore import QTime, Qt
from PyQt5.QtGui import QPalette, QColor
from PyQt5.QtWidgets import QLabel, QSizePolicy

from feeluown.utils.utils import parse_ms
Expand Down Expand Up @@ -83,7 +84,7 @@ class MessageLabel(QLabel):
def __init__(self, text='', level=None, *args, **kwargs):
super().__init__(*args, **kwargs)

self.setTextFormat(Qt.RichText)
self.setWordWrap(True)
self.show_msg(text, level)

def show_msg(self, text, level=None):
Expand All @@ -96,4 +97,8 @@ def show_msg(self, text, level=None):
else:
hint = '️'
color = SOLARIZED_COLORS['blue']
self.setText(f"<span style='color: {color};'>{hint}{text}<span>")
palette = self.palette()
palette.setColor(QPalette.Text, QColor(color))
palette.setColor(QPalette.WindowText, QColor(color))
self.setPalette(palette)
self.setText(f"{hint}{text}")
1 change: 1 addition & 0 deletions feeluown/gui/widgets/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def __init__(self, uri=None, required_cookies_fields=None, domain=None):

self.cookies_text_edit = QTextEdit(self)
self.hint_label = QLabel(self)
self.hint_label.setWordWrap(True)
self.login_btn = QPushButton('登录', self)
self.weblogin_btn = QPushButton('网页登录', self)
self.chrome_btn = QPushButton('从 Chrome 中读取 Cookie')
Expand Down
48 changes: 35 additions & 13 deletions feeluown/library/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import logging
import warnings
from collections import Counter
from functools import partial
from typing import Optional, TypeVar, List, TYPE_CHECKING

from feeluown.media import Media
Expand All @@ -16,7 +15,7 @@
)
from feeluown.library.flags import Flags as PF
from feeluown.library.models import (
ModelFlags as MF, BaseModel,
ModelFlags as MF, BaseModel, SimpleSearchResult,
BriefVideoModel, BriefSongModel, SongModel,
LyricModel, VideoModel, BriefAlbumModel, BriefArtistModel
)
Expand Down Expand Up @@ -135,30 +134,53 @@ def search(self, keyword, type_in=None, source_in=None, **kwargs):
yield result

async def a_search(self, keyword, source_in=None, timeout=None,
type_in=None,
type_in=None, return_err=False,
**_):
"""async version of search
.. versionchanged:: 4.1.9
Add `return_err` parameter.
TODO: add Happy Eyeballs requesting strategy if needed
"""
type_in = SearchType.batch_parse(type_in) if type_in else [SearchType.so]

# Wrap the search function to associate the result with source.
def wrap_search(pvd, kw, t):
def search():
try:
res = pvd.search(kw, type_=t)
except Exception as e: # noqa
if return_err:
logger.exception('One provider search failed')
return SimpleSearchResult(
q=keyword,
source=pvd.identifier, # noqa
err_msg=f'{type(e)}',
)
raise e
# When a provider does not implement search method, it returns None.
if res is not None and (
res.songs or res.albums or
res.artists or res.videos or res.playlists
):
return res
return SimpleSearchResult(
q=keyword, source=pvd.identifier, err_msg='结果为空')
return search

fs = [] # future list
for provider in self._filter(identifier_in=source_in):
for type_ in type_in:
future = run_fn(partial(provider.search, keyword, type_=type_))
future = run_fn(wrap_search(provider, keyword, type_))
fs.append(future)

for future in as_completed(fs, timeout=timeout):
for task_ in as_completed(fs, timeout=timeout):
try:
result = await future
except: # noqa
logger.exception('search task failed')
continue
result = await task_
except Exception as e: # noqa
logger.exception('One search task failed due to asyncio')
else:
# When a provider does not implement search method, it returns None.
if result is not None:
yield result
yield result

async def a_song_prepare_media_no_exc(self, standby, policy):
media = None
Expand Down
2 changes: 2 additions & 0 deletions feeluown/library/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,8 @@ class SimpleSearchResult(_BaseModel):
artists: List[TArtist] = []
playlists: List[TPlaylist] = []
videos: List[TVideo] = []
source: str = ''
err_msg: str = ''


_type_modelcls_mapping = {
Expand Down
7 changes: 4 additions & 3 deletions feeluown/player/playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -750,8 +750,9 @@ async def a_play_model(self, model):
umodel = await aio.run_fn(upgrade_fn, model)
except ModelNotFound:
pass
except: # noqa
logger.exception(f'upgrade model:{model} failed')
except Exception as e: # noqa
logger.exception(f'upgrade model({model}) failed')
self._app.alert_mgr.on_exception(e)
else:
# Replace the brief model with the upgraded model
# when user try to play a brief model that is already in the playlist.
Expand All @@ -766,7 +767,7 @@ async def a_play_model(self, model):
fn, model, name=TASK_SET_CURRENT_MODEL
)
except: # noqa
logger.exception('play model failed')
logger.exception(f'play model({model}) failed')
else:
self._app.player.resume()
logger.info(f'play a model ({model}) succeed')
Expand Down
2 changes: 0 additions & 2 deletions feeluown/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,12 @@ def run_afn_preemptive(self, afn, *args, name=''):
if not name:
name = get_fn_name(afn)
task_spec = self.get_or_create(name)
task_spec.disable_default_cb()
return task_spec.bind_coro(afn(*args))

def run_fn_preemptive(self, fn, *args, name=''):
if not name:
name = get_fn_name(fn)
task_spec = self.get_or_create(name)
task_spec.disable_default_cb()
return task_spec.bind_blocking_io(fn, *args)


Expand Down

0 comments on commit d8ee8dd

Please sign in to comment.