Skip to content

Commit d8ee8dd

Browse files
authored
[enhance](*) introduce alert manager (#909)
- [x] introduce alert manager - [x] show_msg when there is a ConnectTimeout currently - [x] show error message in search page
1 parent 8d54432 commit d8ee8dd

File tree

9 files changed

+118
-26
lines changed

9 files changed

+118
-26
lines changed

feeluown/alert.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import logging
2+
from typing import TYPE_CHECKING, Optional
3+
from urllib.parse import urlparse
4+
5+
from requests import ConnectTimeout
6+
7+
8+
if TYPE_CHECKING:
9+
from feeluown.app import App
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
class AlertManager:
15+
"""Monitor app exceptions and send some alerts."""
16+
def __init__(self):
17+
# Some alerts handling rely on app and some are not.
18+
self._app: Optional['App'] = None
19+
20+
def initialize(self, app: 'App'):
21+
""""""
22+
self._app = app
23+
self._app.player.media_loading_failed.connect(
24+
self.on_media_loading_failed, aioqueue=True)
25+
26+
def on_exception(self, e):
27+
if isinstance(e, ConnectTimeout):
28+
if e.request is not None:
29+
url = e.request.url
30+
hostname = urlparse(url).hostname
31+
else:
32+
hostname = ''
33+
msg = f"链接'{hostname}'超时,请检查你的网络或者代理设置"
34+
self.show_alert(msg)
35+
36+
def on_media_loading_failed(self, *_):
37+
assert self._app is not None
38+
media = self._app.player.current_media
39+
if media and media.url:
40+
proxy = f' {media.http_proxy}' if media.http_proxy else '空'
41+
hostname = urlparse(media.url).hostname
42+
msg = (f'无法播放来自 {hostname} 的资源(资源的 HTTP 代理为{proxy})'
43+
'(注:播放引擎无法使用系统代理)')
44+
self.show_alert(msg)
45+
46+
def show_alert(self, alert):
47+
logger.warning(alert)
48+
self._app.show_msg(alert, timeout=2000)

feeluown/app/app.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from feeluown.plugin import plugins_mgr
1919
from feeluown.version import VersionManager
2020
from feeluown.task import TaskManager
21+
from feeluown.alert import AlertManager
2122

2223
from .mode import AppMode
2324

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

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

111113
def initialize(self):
114+
self.alert_mgr.initialize(self)
112115
self.player_pos_per300ms.initialize()
113116
self.player_pos_per300ms.changed.connect(self.live_lyric.on_position_changed)
114117
self.playlist.song_changed.connect(self.live_lyric.on_song_changed,
115118
aioqueue=True)
116-
self.player.media_loading_failed.connect(
117-
lambda *args: self.show_msg('播放器加载资源失败'), weak=False, aioqueue=True)
118119
self.plugin_mgr.enable_plugins(self)
119120

120121
def run(self):

feeluown/gui/components/search.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def __init__(self, app, **kwargs):
7878
self._layout.addStretch(0)
7979

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

@@ -88,9 +88,19 @@ async def search_and_render(self, q, search_type, source_in):
8888
succeed = 0
8989
start = datetime.now()
9090
is_first = True # Is first search result.
91-
view.hint.show_msg('正在搜索...')
91+
if source_in is not None:
92+
source_count = len(source_in)
93+
else:
94+
source_count = len(app.library.list())
95+
hint_msgs = [f'正在搜索 {source_count} 个资源提供方...']
96+
view.hint.show_msg('\n'.join(hint_msgs))
9297
async for result in app.library.a_search(
93-
q, type_in=search_type, source_in=source_in):
98+
q, type_in=search_type, source_in=source_in, return_err=True):
99+
if result.err_msg:
100+
hint_msgs.append(f'搜索 {result.source} 的资源出错:{result.err_msg}')
101+
view.hint.show_msg('\n'.join(hint_msgs))
102+
continue
103+
94104
table_container = TableContainer(app, view.accordion)
95105
table_container.layout().setContentsMargins(0, 0, 0, 0)
96106

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

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

135149

136150
class SearchResultRenderer(Renderer, TabBarRendererMixin):

feeluown/gui/widgets/labels.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from PyQt5.QtCore import QTime, Qt
2+
from PyQt5.QtGui import QPalette, QColor
23
from PyQt5.QtWidgets import QLabel, QSizePolicy
34

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

86-
self.setTextFormat(Qt.RichText)
87+
self.setWordWrap(True)
8788
self.show_msg(text, level)
8889

8990
def show_msg(self, text, level=None):
@@ -96,4 +97,8 @@ def show_msg(self, text, level=None):
9697
else:
9798
hint = '️'
9899
color = SOLARIZED_COLORS['blue']
99-
self.setText(f"<span style='color: {color};'>{hint}{text}<span>")
100+
palette = self.palette()
101+
palette.setColor(QPalette.Text, QColor(color))
102+
palette.setColor(QPalette.WindowText, QColor(color))
103+
self.setPalette(palette)
104+
self.setText(f"{hint}{text}")

feeluown/gui/widgets/login.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def __init__(self, uri=None, required_cookies_fields=None, domain=None):
9090

9191
self.cookies_text_edit = QTextEdit(self)
9292
self.hint_label = QLabel(self)
93+
self.hint_label.setWordWrap(True)
9394
self.login_btn = QPushButton('登录', self)
9495
self.weblogin_btn = QPushButton('网页登录', self)
9596
self.chrome_btn = QPushButton('从 Chrome 中读取 Cookie')

feeluown/library/library.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import logging
33
import warnings
44
from collections import Counter
5-
from functools import partial
65
from typing import Optional, TypeVar, List, TYPE_CHECKING
76

87
from feeluown.media import Media
@@ -16,7 +15,7 @@
1615
)
1716
from feeluown.library.flags import Flags as PF
1817
from feeluown.library.models import (
19-
ModelFlags as MF, BaseModel,
18+
ModelFlags as MF, BaseModel, SimpleSearchResult,
2019
BriefVideoModel, BriefSongModel, SongModel,
2120
LyricModel, VideoModel, BriefAlbumModel, BriefArtistModel
2221
)
@@ -135,30 +134,53 @@ def search(self, keyword, type_in=None, source_in=None, **kwargs):
135134
yield result
136135

137136
async def a_search(self, keyword, source_in=None, timeout=None,
138-
type_in=None,
137+
type_in=None, return_err=False,
139138
**_):
140139
"""async version of search
141140
141+
.. versionchanged:: 4.1.9
142+
Add `return_err` parameter.
143+
142144
TODO: add Happy Eyeballs requesting strategy if needed
143145
"""
144146
type_in = SearchType.batch_parse(type_in) if type_in else [SearchType.so]
145147

148+
# Wrap the search function to associate the result with source.
149+
def wrap_search(pvd, kw, t):
150+
def search():
151+
try:
152+
res = pvd.search(kw, type_=t)
153+
except Exception as e: # noqa
154+
if return_err:
155+
logger.exception('One provider search failed')
156+
return SimpleSearchResult(
157+
q=keyword,
158+
source=pvd.identifier, # noqa
159+
err_msg=f'{type(e)}',
160+
)
161+
raise e
162+
# When a provider does not implement search method, it returns None.
163+
if res is not None and (
164+
res.songs or res.albums or
165+
res.artists or res.videos or res.playlists
166+
):
167+
return res
168+
return SimpleSearchResult(
169+
q=keyword, source=pvd.identifier, err_msg='结果为空')
170+
return search
171+
146172
fs = [] # future list
147173
for provider in self._filter(identifier_in=source_in):
148174
for type_ in type_in:
149-
future = run_fn(partial(provider.search, keyword, type_=type_))
175+
future = run_fn(wrap_search(provider, keyword, type_))
150176
fs.append(future)
151-
152-
for future in as_completed(fs, timeout=timeout):
177+
for task_ in as_completed(fs, timeout=timeout):
153178
try:
154-
result = await future
155-
except: # noqa
156-
logger.exception('search task failed')
157-
continue
179+
result = await task_
180+
except Exception as e: # noqa
181+
logger.exception('One search task failed due to asyncio')
158182
else:
159-
# When a provider does not implement search method, it returns None.
160-
if result is not None:
161-
yield result
183+
yield result
162184

163185
async def a_song_prepare_media_no_exc(self, standby, policy):
164186
media = None

feeluown/library/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,8 @@ class SimpleSearchResult(_BaseModel):
450450
artists: List[TArtist] = []
451451
playlists: List[TPlaylist] = []
452452
videos: List[TVideo] = []
453+
source: str = ''
454+
err_msg: str = ''
453455

454456

455457
_type_modelcls_mapping = {

feeluown/player/playlist.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -750,8 +750,9 @@ async def a_play_model(self, model):
750750
umodel = await aio.run_fn(upgrade_fn, model)
751751
except ModelNotFound:
752752
pass
753-
except: # noqa
754-
logger.exception(f'upgrade model:{model} failed')
753+
except Exception as e: # noqa
754+
logger.exception(f'upgrade model({model}) failed')
755+
self._app.alert_mgr.on_exception(e)
755756
else:
756757
# Replace the brief model with the upgraded model
757758
# when user try to play a brief model that is already in the playlist.
@@ -766,7 +767,7 @@ async def a_play_model(self, model):
766767
fn, model, name=TASK_SET_CURRENT_MODEL
767768
)
768769
except: # noqa
769-
logger.exception('play model failed')
770+
logger.exception(f'play model({model}) failed')
770771
else:
771772
self._app.player.resume()
772773
logger.info(f'play a model ({model}) succeed')

feeluown/task.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,12 @@ def run_afn_preemptive(self, afn, *args, name=''):
129129
if not name:
130130
name = get_fn_name(afn)
131131
task_spec = self.get_or_create(name)
132-
task_spec.disable_default_cb()
133132
return task_spec.bind_coro(afn(*args))
134133

135134
def run_fn_preemptive(self, fn, *args, name=''):
136135
if not name:
137136
name = get_fn_name(fn)
138137
task_spec = self.get_or_create(name)
139-
task_spec.disable_default_cb()
140138
return task_spec.bind_blocking_io(fn, *args)
141139

142140

0 commit comments

Comments
 (0)