Skip to content

Commit e9c0171

Browse files
authored
Merge pull request #44 from chenmozhijin/dev
0.9.1
2 parents 209a3c6 + 6c4a3db commit e9c0171

File tree

19 files changed

+552
-307
lines changed

19 files changed

+552
-307
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ jobs:
6464
mkdir -p tests/artifacts/reports
6565
cp -r htmlcov ./tests/artifacts/reports
6666
cp coverage.xml ./tests/artifacts/reports
67+
export log_dir=$(python build_helper.py --task log_dir)
68+
cp -r $log_dir ./tests/artifacts
69+
shell: bash
6770

6871
- name: Upload artifacts
6972
uses: actions/upload-artifact@v4

LDDC/common/data/cache.py

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# SPDX-FileCopyrightText: Copyright (C) 2024-2025 沉默の金 <cmzj@cmzj.org>
22
# SPDX-License-Identifier: GPL-3.0-only
3+
import atexit
34
from collections.abc import Callable
45
from typing import Any, Literal, ParamSpec, TypeVar, overload
56

@@ -8,7 +9,7 @@
89
from LDDC.common.paths import cache_dir
910

1011
cache = Cache(cache_dir, sqlitecache_size=512)
11-
cache_version = 5
12+
cache_version = 6
1213
if "version" not in cache or cache["version"] != cache_version:
1314
cache.clear()
1415
cache["version"] = cache_version
@@ -54,23 +55,42 @@ def cached_call(
5455
cache.set(key, result, expire=expire)
5556
return result
5657

58+
5759
@overload
5860
def get_cached_func(
59-
func: Callable[P, T], typed: bool = True, ignore: set[int | str] | None = None, expire: int | None = None,
60-
) -> Callable[P, T]:...
61+
func: Callable[P, T],
62+
typed: bool = True,
63+
ignore: set[int | str] | None = None,
64+
expire: int | None = None,
65+
) -> Callable[P, T]: ...
66+
6167

6268
@overload
6369
def get_cached_func(
64-
func: Callable[P, T], typed: bool = True, ignore: set[int | str] | None = None, expire: int | None = None, with_status: Literal[False] = False,
65-
) -> Callable[P, T]:...
70+
func: Callable[P, T],
71+
typed: bool = True,
72+
ignore: set[int | str] | None = None,
73+
expire: int | None = None,
74+
with_status: Literal[False] = False,
75+
) -> Callable[P, T]: ...
76+
6677

6778
@overload
6879
def get_cached_func(
69-
func: Callable[P, T], typed: bool = True, ignore: set[int | str] | None = None, expire: int | None = None, with_status: Literal[True] = True,
70-
) -> Callable[P, tuple[T, bool]]:...
80+
func: Callable[P, T],
81+
typed: bool = True,
82+
ignore: set[int | str] | None = None,
83+
expire: int | None = None,
84+
with_status: Literal[True] = True,
85+
) -> Callable[P, tuple[T, bool]]: ...
86+
7187

7288
def get_cached_func(
73-
func: Callable[P, T], typed: bool = True, ignore: set[int | str] | None = None, expire: int | None = None, with_status: bool = False,
89+
func: Callable[P, T],
90+
typed: bool = True,
91+
ignore: set[int | str] | None = None,
92+
expire: int | None = None,
93+
with_status: bool = False,
7494
) -> Callable[P, T] | Callable[P, tuple[T, bool]]:
7595
cache_settings = {"typed": typed, "ignore": ignore if ignore else set(), "expire": expire}
7696
if with_status:
@@ -151,3 +171,12 @@ def _buildcache_key(
151171
key += type_sig
152172

153173
return key
174+
175+
176+
def _atexit() -> None:
177+
cache["version"] = cache_version
178+
cache.expire()
179+
cache.close()
180+
181+
182+
atexit.register(_atexit)

LDDC/common/task_manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from .logger import logger
1414
from .models import P, T
15-
from .thread import in_other_thread, threadpool
15+
from .thread import in_other_thread, is_exited, threadpool
1616

1717

1818
class TaskManager:
@@ -252,7 +252,7 @@ def __init__(self) -> None:
252252

253253
@property
254254
def is_stopped(self) -> bool:
255-
return self.taskmanager.is_finished(self.task_type, self.taskid)
255+
return self.taskmanager.is_finished(self.task_type, self.taskid) or is_exited()
256256

257257
def finished_task(self) -> None:
258258
self.taskmanager.finished_task(self.task_type, self.taskid)

LDDC/common/thread.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,26 @@
22
# SPDX-License-Identifier: GPL-3.0-only
33
from collections.abc import Callable, Iterable
44
from functools import partial, wraps
5-
from threading import Lock
5+
from threading import Event, Lock
66
from typing import Any
77

88
from PySide6.QtCore import QCoreApplication, QEvent, QObject, QRunnable, Qt, QThread, QThreadPool, Signal, Slot
99

1010
from .logger import logger
1111
from .models import P, T
1212

13-
threadpool = QThreadPool()
13+
exit_event = Event()
14+
15+
16+
def is_exited() -> bool:
17+
return exit_event.is_set()
18+
19+
20+
def set_exited() -> None:
21+
exit_event.set()
22+
23+
24+
threadpool = QThreadPool(QCoreApplication.instance())
1425
if threadpool.maxThreadCount() < 8:
1526
threadpool.setMaxThreadCount(8)
1627

@@ -76,8 +87,13 @@ def __init__(self, func: Callable[..., T], emitter: SignalEmitter) -> None:
7687
self.emitter = emitter
7788

7889
def run(self) -> None:
90+
if is_exited():
91+
return
7992
try:
80-
self.emitter.success.emit(self.func()) # 成功时发射信号
93+
result = self.func()
94+
if is_exited():
95+
return
96+
self.emitter.success.emit(result) # 成功时发射信号
8197
except Exception as e:
8298
self.emitter.error.emit(e) # 失败时发射信号
8399
logger.exception(e)

LDDC/common/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
"""版本信息与处理模块"""
55

6-
__version__ = "v0.9.0"
6+
__version__ = "v0.9.1"
77
import re
88
from typing import Literal
99

LDDC/core/api/lyrics/kg.py

Lines changed: 161 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# ruff: noqa: S311 S324
55
import hashlib
66
import json
7+
import random
78
import time
89
from base64 import b64decode, b64encode
910
from datetime import datetime, timedelta, timezone
@@ -137,7 +138,8 @@ def request(
137138
"uuid": "-",
138139
# "uuid": "15e772e1213bdd0718d0c1d10d64e06f",
139140
"mid": mid,
140-
"dfid": self.dfid,
141+
# "dfid": self.dfid,
142+
"dfid": "-",
141143
"clientver": "11070",
142144
"platform": "AndroidFilter",
143145
**params,
@@ -181,7 +183,11 @@ def search(self, keyword: str, search_type: SearchType, page: int = 1) -> APIRes
181183
"page": page,
182184
}
183185
url, module = SEARCH_TYPE_MAPPING[search_type]
184-
data = self.request(url, params, module, headers={"x-router": "complexsearch.kugou.com"})
186+
try:
187+
data = self.request(url, params, module, headers={"x-router": "complexsearch.kugou.com"})
188+
except APIRequestError:
189+
logger.exception("kg API请求错误,尝试使用旧接口")
190+
return self._old_search(keyword, search_type, page)
185191

186192
if not data["data"]["lists"]:
187193
return APIResultList(
@@ -202,7 +208,7 @@ def search(self, keyword: str, search_type: SearchType, page: int = 1) -> APIRes
202208
[
203209
SongInfo(
204210
source=self.source,
205-
id=info["ID"],
211+
id=str(info["ID"]),
206212
hash=info["FileHash"],
207213
title=info["SongName"],
208214
subtitle=info["Auxiliary"],
@@ -232,7 +238,7 @@ def search(self, keyword: str, search_type: SearchType, page: int = 1) -> APIRes
232238
SongListInfo(
233239
source=self.source,
234240
type=SongListType.ALBUM,
235-
id=info["albumid"],
241+
id=str(info["albumid"]),
236242
title=info["albumname"],
237243
imgurl=info["img"],
238244
songcount=info["songcount"],
@@ -267,7 +273,7 @@ def search(self, keyword: str, search_type: SearchType, page: int = 1) -> APIRes
267273
SongListInfo(
268274
source=self.source,
269275
type=SongListType.SONGLIST,
270-
id=info["gid"],
276+
id=str(info["gid"]),
271277
title=info["specialname"],
272278
imgurl=info["img"],
273279
songcount=info["song_count"],
@@ -297,6 +303,155 @@ def search(self, keyword: str, search_type: SearchType, page: int = 1) -> APIRes
297303
case _:
298304
raise NotImplementedError
299305

306+
def _old_search(self, keyword: str, search_type: SearchType, page: int = 1) -> APIResultList[SongInfo] | APIResultList[SongListInfo]:
307+
"""备用搜索API"""
308+
domain = random.choice(["mobiles.kugou.com", "msearchcdn.kugou.com", "mobilecdnbj.kugou.com", "msearch.kugou.com"])
309+
pagesize = 20
310+
311+
match search_type:
312+
case SearchType.SONG:
313+
url = f"http://{domain}/api/v3/search/song"
314+
params = {
315+
"showtype": "14",
316+
"highlight": "",
317+
"pagesize": "30",
318+
"tag_aggr": "1",
319+
"plat": "0",
320+
"sver": "5",
321+
"keyword": keyword,
322+
"correct": "1",
323+
"api_ver": "1",
324+
"version": "9108",
325+
"page": page,
326+
}
327+
case SearchType.SONGLIST:
328+
url = f"http://{domain}/api/v3/search/special"
329+
params = {
330+
"version": "9108",
331+
"highlight": "",
332+
"keyword": keyword,
333+
"pagesize": "20",
334+
"filter": "0",
335+
"page": page,
336+
"sver": "2",
337+
}
338+
case SearchType.ALBUM:
339+
url = f"http://{domain}/api/v3/search/album"
340+
params = {
341+
"version": "9108",
342+
"iscorrection": "1",
343+
"highlight": "",
344+
"plat": "0",
345+
"keyword": keyword,
346+
"pagesize": "20",
347+
"page": page,
348+
"sver": "2",
349+
}
350+
351+
response = self.client.get(url, params=params, timeout=3)
352+
response.raise_for_status()
353+
data = response.json()
354+
start_index = (page - 1) * pagesize
355+
match search_type:
356+
case SearchType.SONG:
357+
return APIResultList(
358+
[
359+
SongInfo(
360+
source=self.source,
361+
id=str(info["album_audio_id"]),
362+
hash=info["hash"],
363+
title=info["songname"],
364+
subtitle=info["topic"],
365+
artist=Artist(info["singername"].split("、")),
366+
album=info["album_name"],
367+
duration=info["duration"] * 1000,
368+
language=LANGUAGE_MAPPING.get(info["trans_param"].get("language"), Language.OTHER),
369+
)
370+
for info in data["data"]["info"]
371+
],
372+
SearchInfo(
373+
source=self.source,
374+
keyword=keyword,
375+
search_type=search_type,
376+
page=page,
377+
),
378+
(
379+
start_index,
380+
start_index + len(data["data"]["info"]) - 1,
381+
data["data"]["total"] if len(data["data"]["info"]) == pagesize else start_index + len(data["data"]["info"]),
382+
),
383+
)
384+
case SearchType.SONGLIST:
385+
return APIResultList(
386+
[
387+
SongListInfo(
388+
source=self.source,
389+
type=SongListType.SONGLIST,
390+
id=str(info["specialid"]),
391+
title=info["specialname"],
392+
imgurl=info["imgurl"],
393+
songcount=info["songcount"],
394+
publishtime=int(
395+
datetime.strptime(info["publishtime"], "%Y-%m-%d %H:%M:%S")
396+
.replace(tzinfo=timezone(timedelta(hours=8)))
397+
.astimezone(timezone.utc)
398+
.timestamp(),
399+
)
400+
if info["publishtime"] != "0000-00-00 00:00:00"
401+
else None,
402+
author=info["nickname"],
403+
)
404+
for info in data["data"]["info"]
405+
],
406+
SearchInfo(
407+
source=self.source,
408+
keyword=keyword,
409+
search_type=search_type,
410+
page=page,
411+
),
412+
(
413+
start_index,
414+
start_index + len(data["data"]["info"]) - 1,
415+
data["data"]["total"] if len(data["data"]["info"]) == pagesize else start_index + len(data["data"]["info"]),
416+
),
417+
)
418+
case SearchType.ALBUM:
419+
return APIResultList(
420+
[
421+
SongListInfo(
422+
source=self.source,
423+
type=SongListType.ALBUM,
424+
id=str(info["albumid"]),
425+
title=info["albumname"],
426+
imgurl=info["imgurl"],
427+
songcount=info["songcount"],
428+
publishtime=int(
429+
datetime.strptime(info["publishtime"], "%Y-%m-%d %H:%M:%S")
430+
.replace(tzinfo=timezone(timedelta(hours=8)))
431+
.astimezone(timezone.utc)
432+
.timestamp(),
433+
)
434+
if info["publishtime"] != "0000-00-00 00:00:00"
435+
else None,
436+
author=info["singer"],
437+
)
438+
for info in data["data"]["info"]
439+
],
440+
SearchInfo(
441+
source=self.source,
442+
keyword=keyword,
443+
search_type=search_type,
444+
page=page,
445+
),
446+
(
447+
start_index,
448+
start_index + len(data["data"]["info"]) - 1,
449+
data["data"]["total"] if len(data["data"]["info"]) == pagesize else start_index + len(data["data"]["info"]),
450+
),
451+
)
452+
case _:
453+
raise NotImplementedError
454+
300455
def get_songlist(self, songlist_info: SongListInfo) -> APIResultList[SongInfo]:
301456
match songlist_info.type:
302457
case SongListType.ALBUM:
@@ -402,7 +557,7 @@ def get_lyricslist(self, song_info: SongInfo) -> APIResultList[LyricInfo]:
402557
[
403558
LyricInfo(
404559
source=self.source,
405-
id=lyric["id"],
560+
id=str(lyric["id"]),
406561
accesskey=lyric["accesskey"],
407562
creator=lyric["nickname"],
408563
duration=lyric["duration"],

0 commit comments

Comments
 (0)