fuo-qqmusic 1.0.5__tar.gz → 1.0.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fuo-qqmusic might be problematic. Click here for more details.
- {fuo_qqmusic-1.0.5 → fuo_qqmusic-1.0.7}/PKG-INFO +12 -2
- {fuo_qqmusic-1.0.5 → fuo_qqmusic-1.0.7}/README.md +7 -0
- {fuo_qqmusic-1.0.5 → fuo_qqmusic-1.0.7}/fuo_qqmusic/api.py +109 -1
- fuo_qqmusic-1.0.7/fuo_qqmusic/login.py +24 -0
- {fuo_qqmusic-1.0.5 → fuo_qqmusic-1.0.7}/fuo_qqmusic/provider.py +89 -2
- {fuo_qqmusic-1.0.5 → fuo_qqmusic-1.0.7}/fuo_qqmusic/provider_ui.py +5 -32
- {fuo_qqmusic-1.0.5 → fuo_qqmusic-1.0.7}/fuo_qqmusic.egg-info/PKG-INFO +13 -3
- {fuo_qqmusic-1.0.5 → fuo_qqmusic-1.0.7}/fuo_qqmusic.egg-info/SOURCES.txt +4 -1
- {fuo_qqmusic-1.0.5 → fuo_qqmusic-1.0.7}/fuo_qqmusic.egg-info/requires.txt +1 -1
- {fuo_qqmusic-1.0.5 → fuo_qqmusic-1.0.7}/setup.py +2 -2
- fuo_qqmusic-1.0.7/tests/test_api.py +56 -0
- fuo_qqmusic-1.0.7/tests/test_provider.py +26 -0
- {fuo_qqmusic-1.0.5 → fuo_qqmusic-1.0.7}/fuo_qqmusic/__init__.py +0 -0
- {fuo_qqmusic-1.0.5 → fuo_qqmusic-1.0.7}/fuo_qqmusic/assets/icon.svg +0 -0
- {fuo_qqmusic-1.0.5 → fuo_qqmusic-1.0.7}/fuo_qqmusic/consts.py +0 -0
- {fuo_qqmusic-1.0.5 → fuo_qqmusic-1.0.7}/fuo_qqmusic/excs.py +0 -0
- {fuo_qqmusic-1.0.5 → fuo_qqmusic-1.0.7}/fuo_qqmusic/schemas.py +0 -0
- {fuo_qqmusic-1.0.5 → fuo_qqmusic-1.0.7}/fuo_qqmusic.egg-info/dependency_links.txt +0 -0
- {fuo_qqmusic-1.0.5 → fuo_qqmusic-1.0.7}/fuo_qqmusic.egg-info/entry_points.txt +0 -0
- {fuo_qqmusic-1.0.5 → fuo_qqmusic-1.0.7}/fuo_qqmusic.egg-info/top_level.txt +0 -0
- {fuo_qqmusic-1.0.5 → fuo_qqmusic-1.0.7}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: fuo_qqmusic
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.7
|
|
4
4
|
Summary: feeluown qqmusic plugin
|
|
5
5
|
Home-page: https://github.com/feeluown/feeluown-qqmusic
|
|
6
6
|
Author: Cosven
|
|
@@ -13,3 +13,13 @@ Classifier: Programming Language :: Python :: 3.7
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.8
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.9
|
|
15
15
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Requires-Dist: feeluown>=4.1.3
|
|
17
|
+
Requires-Dist: requests
|
|
18
|
+
Requires-Dist: marshmallow<4.0.0,>=3.0
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: keywords
|
|
24
|
+
Dynamic: requires-dist
|
|
25
|
+
Dynamic: summary
|
|
@@ -20,6 +20,13 @@ pip3 install fuo-qqmusic
|
|
|
20
20
|
[操作示例](https://github.com/feeluown/feeluown-qqmusic/issues/6)。
|
|
21
21
|
|
|
22
22
|
## changelog
|
|
23
|
+
### 1.0.7 (2025-05-31)
|
|
24
|
+
- 支持音乐黑名单
|
|
25
|
+
|
|
26
|
+
### 1.0.6 (2025-05-08)
|
|
27
|
+
- 支持每日推荐
|
|
28
|
+
- 支持自动登录
|
|
29
|
+
|
|
23
30
|
### 1.0.5 (2024-06-03)
|
|
24
31
|
- 修复搜索接口,支持搜索歌单、视频、专辑
|
|
25
32
|
- 支持提供歌单的播放次数
|
|
@@ -8,6 +8,7 @@ import math
|
|
|
8
8
|
import json
|
|
9
9
|
import random
|
|
10
10
|
import time
|
|
11
|
+
from enum import Enum
|
|
11
12
|
|
|
12
13
|
import requests
|
|
13
14
|
from .excs import QQIOError
|
|
@@ -58,7 +59,7 @@ class API(object):
|
|
|
58
59
|
Please http capture request from (mobile) qqmusic mobile web page
|
|
59
60
|
"""
|
|
60
61
|
|
|
61
|
-
def __init__(self, timeout=
|
|
62
|
+
def __init__(self, timeout=2):
|
|
62
63
|
# TODO: 暂时无脑统一一个 timeout
|
|
63
64
|
# 正确的应该是允许不同接口有不同的超时时间
|
|
64
65
|
self._timeout = timeout
|
|
@@ -518,6 +519,33 @@ class API(object):
|
|
|
518
519
|
js = self.rpc(payload)
|
|
519
520
|
return js['songlist']['data']['tracks']
|
|
520
521
|
|
|
522
|
+
def get_diss_info(self, dissid, offset=0, limit=50):
|
|
523
|
+
"""获取歌单的详情
|
|
524
|
+
|
|
525
|
+
这是一种特殊的歌单,它的内容是动态的,由官方生成。比如“百万收藏”。
|
|
526
|
+
|
|
527
|
+
:param dissid: int, 举个例子 211111 是百万收藏。
|
|
528
|
+
:return: 参考 fixtures/get_diss_info.json
|
|
529
|
+
"""
|
|
530
|
+
payload = {
|
|
531
|
+
'req': {
|
|
532
|
+
"module": "music.srfDissInfo.aiDissInfo",
|
|
533
|
+
"method":"uniform_get_Dissinfo",
|
|
534
|
+
"param": {
|
|
535
|
+
"disstid":dissid,
|
|
536
|
+
"userinfo":1, # 不懂啥意思
|
|
537
|
+
"tag":1, # 不懂啥意思
|
|
538
|
+
"orderlist":1,
|
|
539
|
+
"song_begin": offset,
|
|
540
|
+
"song_num": limit,
|
|
541
|
+
"onlysonglist":0,
|
|
542
|
+
"enc_host_uin":"" # 注:即使登录了,这个也是空
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
js = self.rpc(payload)
|
|
547
|
+
return js['req']['data']
|
|
548
|
+
|
|
521
549
|
def get_song_url(self, song_mid):
|
|
522
550
|
uin = self._uin
|
|
523
551
|
songvkey = str(random.random()).replace("0.", "")
|
|
@@ -690,5 +718,85 @@ class API(object):
|
|
|
690
718
|
return 'http://isure.stream.qqmusic.qq.com/{}'.format(midurlinfo[0]['purl'])
|
|
691
719
|
return ''
|
|
692
720
|
|
|
721
|
+
class DislikeListType(Enum):
|
|
722
|
+
singer = 2
|
|
723
|
+
song = 3
|
|
724
|
+
_style_unsupported = 4 # TODO: 这是什么?似乎是不喜欢的风格列表,不确定,暂时不支持
|
|
725
|
+
|
|
726
|
+
def get_dislike_list(self, page=1, type_=DislikeListType.song, last_id=0):
|
|
727
|
+
payload = {
|
|
728
|
+
"req_0": {
|
|
729
|
+
"module": "music.feedback.FeedbackBlack",
|
|
730
|
+
"method": "GetDislikeList",
|
|
731
|
+
"param": {
|
|
732
|
+
"Cmd": type_.value,
|
|
733
|
+
"Page": page,
|
|
734
|
+
"SongLastid": last_id if type_ == API.DislikeListType.song else 0,
|
|
735
|
+
"SingersLastid": (
|
|
736
|
+
last_id if type_ == API.DislikeListType.singer else 0
|
|
737
|
+
),
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
}
|
|
741
|
+
js = self.rpc(payload)
|
|
742
|
+
if type_ == API.DislikeListType.song:
|
|
743
|
+
return js["req_0"]["data"]["Songs"]
|
|
744
|
+
elif type_ == API.DislikeListType.singer:
|
|
745
|
+
return js["req_0"]["data"]["Singers"]
|
|
746
|
+
else:
|
|
747
|
+
raise QQIOError(f"Unknown dislike list type: {type_}")
|
|
748
|
+
|
|
749
|
+
def add_to_dislike_list(self, items, type_=DislikeListType.song):
|
|
750
|
+
req_param = {
|
|
751
|
+
"Singers": [],
|
|
752
|
+
"Songs": [],
|
|
753
|
+
"Styles": [],
|
|
754
|
+
"OnlyAdd": 1,
|
|
755
|
+
}
|
|
756
|
+
if type_ == API.DislikeListType.song:
|
|
757
|
+
req_param["Songs"] = items
|
|
758
|
+
elif type_ == API.DislikeListType.singer:
|
|
759
|
+
req_param["Singers"] = items
|
|
760
|
+
else:
|
|
761
|
+
raise QQIOError(f"Unknown dislike list type: {type_}")
|
|
762
|
+
|
|
763
|
+
payload = {
|
|
764
|
+
"req_0": {
|
|
765
|
+
"module": "music.feedback.FeedbackBlack",
|
|
766
|
+
"method": "AddDislike",
|
|
767
|
+
"param": req_param,
|
|
768
|
+
},
|
|
769
|
+
}
|
|
770
|
+
js = self.rpc(payload)
|
|
771
|
+
# Response example, {'code': 0, 'data': {'Retcode': 0, 'Msg': '', 'Token': ''}}
|
|
772
|
+
CodeShouldBe0.check(js['req_0'])
|
|
773
|
+
return js['req_0']['data']
|
|
774
|
+
|
|
775
|
+
def remove_from_dislike_list(self, items, type_=DislikeListType.song):
|
|
776
|
+
req_param = {
|
|
777
|
+
"Singers": [],
|
|
778
|
+
"Songs": [],
|
|
779
|
+
"Styles": [],
|
|
780
|
+
"OnlyAdd": 0,
|
|
781
|
+
}
|
|
782
|
+
if type_ == API.DislikeListType.song:
|
|
783
|
+
req_param["Songs"] = items
|
|
784
|
+
elif type_ == API.DislikeListType.singer:
|
|
785
|
+
req_param["Singers"] = items
|
|
786
|
+
else:
|
|
787
|
+
raise QQIOError(f"Unknown dislike list type: {type_}")
|
|
788
|
+
|
|
789
|
+
payload = {
|
|
790
|
+
"req_0": {
|
|
791
|
+
"module": "music.feedback.FeedbackBlack",
|
|
792
|
+
"method": "CancelDislike",
|
|
793
|
+
"param": req_param,
|
|
794
|
+
},
|
|
795
|
+
}
|
|
796
|
+
js = self.rpc(payload)
|
|
797
|
+
CodeShouldBe0.check(js)
|
|
798
|
+
CodeShouldBe0.check(js['req_0'])
|
|
799
|
+
return js['req_0']['data']
|
|
800
|
+
|
|
693
801
|
|
|
694
802
|
api = API()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from feeluown.consts import DATA_DIR
|
|
5
|
+
|
|
6
|
+
USER_INFO_FILE = DATA_DIR + '/qqmusic_user_info.json'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def read_cookies():
|
|
10
|
+
if os.path.exists(USER_INFO_FILE):
|
|
11
|
+
# if the file is broken, just raise error
|
|
12
|
+
with open(USER_INFO_FILE) as f:
|
|
13
|
+
return json.load(f).get('cookies', None)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def write_cookies(user, cookies):
|
|
17
|
+
js = {
|
|
18
|
+
'identifier': user.identifier,
|
|
19
|
+
'name': user.name,
|
|
20
|
+
'cookies': cookies
|
|
21
|
+
}
|
|
22
|
+
with open(USER_INFO_FILE, 'w') as f:
|
|
23
|
+
json.dump(js, f, indent=2)
|
|
24
|
+
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import List, Optional, Protocol
|
|
2
|
+
from typing import List, Optional, Protocol, Tuple
|
|
3
3
|
from feeluown.excs import ModelNotFound
|
|
4
4
|
from feeluown.library import (
|
|
5
5
|
AbstractProvider,
|
|
@@ -23,13 +23,21 @@ from feeluown.library import (
|
|
|
23
23
|
SupportsPlaylistGet,
|
|
24
24
|
SupportsPlaylistSongsReader,
|
|
25
25
|
SupportsRecACollectionOfSongs,
|
|
26
|
+
SupportsCurrentUserDislikeSongsReader,
|
|
27
|
+
SupportsCurrentUserDislikeAddSong,
|
|
28
|
+
SupportsCurrentUserDislikeRemoveSong,
|
|
29
|
+
SupportsCurrentUserChanged,
|
|
26
30
|
SimpleSearchResult,
|
|
27
31
|
SearchType,
|
|
28
32
|
ModelType,
|
|
33
|
+
UserModel,
|
|
29
34
|
)
|
|
30
35
|
from feeluown.media import Media, Quality
|
|
36
|
+
from feeluown.utils.dispatch import Signal
|
|
31
37
|
from feeluown.utils.reader import create_reader, SequentialReader
|
|
32
38
|
from .api import API
|
|
39
|
+
from .login import read_cookies
|
|
40
|
+
from .excs import QQIOError
|
|
33
41
|
|
|
34
42
|
|
|
35
43
|
logger = logging.getLogger(__name__)
|
|
@@ -50,6 +58,9 @@ class Supports(
|
|
|
50
58
|
SupportsPlaylistSongsReader,
|
|
51
59
|
SupportsRecACollectionOfSongs,
|
|
52
60
|
SupportsAlbumSongsReader,
|
|
61
|
+
SupportsCurrentUserDislikeSongsReader,
|
|
62
|
+
SupportsCurrentUserDislikeAddSong,
|
|
63
|
+
SupportsCurrentUserDislikeRemoveSong,
|
|
53
64
|
Protocol,
|
|
54
65
|
):
|
|
55
66
|
pass
|
|
@@ -67,6 +78,7 @@ class QQProvider(AbstractProvider, ProviderV2):
|
|
|
67
78
|
def __init__(self):
|
|
68
79
|
super().__init__()
|
|
69
80
|
self.api = API()
|
|
81
|
+
self.current_user_changed = Signal()
|
|
70
82
|
|
|
71
83
|
def _(self) -> Supports:
|
|
72
84
|
return self
|
|
@@ -79,6 +91,32 @@ class QQProvider(AbstractProvider, ProviderV2):
|
|
|
79
91
|
def name(self):
|
|
80
92
|
return "QQ 音乐"
|
|
81
93
|
|
|
94
|
+
def auto_login(self):
|
|
95
|
+
cookies = read_cookies()
|
|
96
|
+
user, err = self.try_get_user_from_cookies(cookies)
|
|
97
|
+
if user:
|
|
98
|
+
self.auth(user)
|
|
99
|
+
else:
|
|
100
|
+
logger.info(f'Auto login failed: {err}')
|
|
101
|
+
self.current_user_changed.emit(user)
|
|
102
|
+
|
|
103
|
+
def try_get_user_from_cookies(self, cookies) -> Tuple[Optional[UserModel], str]:
|
|
104
|
+
if not cookies: # is None or empty
|
|
105
|
+
return None, 'empty cookies'
|
|
106
|
+
|
|
107
|
+
uin = provider.api.get_uin_from_cookies(cookies)
|
|
108
|
+
if uin is None:
|
|
109
|
+
return None, "can't extract user info from cookies"
|
|
110
|
+
|
|
111
|
+
provider.api.set_cookies(cookies)
|
|
112
|
+
# try to extract current user
|
|
113
|
+
try:
|
|
114
|
+
user = provider.user_get(uin)
|
|
115
|
+
except QQIOError:
|
|
116
|
+
provider.api.set_cookies(None)
|
|
117
|
+
return None, 'get user info with cookies failed, expired cookies?'
|
|
118
|
+
return user, ''
|
|
119
|
+
|
|
82
120
|
def use_model_v2(self, mtype):
|
|
83
121
|
return mtype in (
|
|
84
122
|
ModelType.song,
|
|
@@ -259,6 +297,27 @@ class QQProvider(AbstractProvider, ProviderV2):
|
|
|
259
297
|
pl["logo"] = pl["cover"]
|
|
260
298
|
return [_deserialize(playlist, QQPlaylistSchema) for playlist in playlists]
|
|
261
299
|
|
|
300
|
+
def rec_list_daily_songs(self):
|
|
301
|
+
# TODO: cache API result
|
|
302
|
+
feed = self.api.get_recommend_feed()
|
|
303
|
+
card = None
|
|
304
|
+
for shelf_ in feed['v_shelf']:
|
|
305
|
+
if 'moduleID' not in shelf_['extra_info']:
|
|
306
|
+
for batch in shelf_['v_niche']:
|
|
307
|
+
for card in batch['v_card']:
|
|
308
|
+
if (
|
|
309
|
+
card['extra_info'].get('moduleID', '').startswith('recforyou')
|
|
310
|
+
and card['jumptype'] == 10014 # 10014->playlist
|
|
311
|
+
):
|
|
312
|
+
card = card
|
|
313
|
+
break
|
|
314
|
+
if card is None:
|
|
315
|
+
logger.warning("No daily songs found")
|
|
316
|
+
return []
|
|
317
|
+
playlist_id = card['id']
|
|
318
|
+
playlist = self.playlist_get(playlist_id)
|
|
319
|
+
return self.playlist_create_songs_rd(playlist).readall()
|
|
320
|
+
|
|
262
321
|
def rec_list_daily_playlists(self):
|
|
263
322
|
# TODO: cache API result
|
|
264
323
|
feed = self.api.get_recommend_feed()
|
|
@@ -273,7 +332,6 @@ class QQProvider(AbstractProvider, ProviderV2):
|
|
|
273
332
|
playlists = []
|
|
274
333
|
for batch in shelf['v_niche']:
|
|
275
334
|
for card in batch['v_card']:
|
|
276
|
-
print(card['title'], card['jumptype'])
|
|
277
335
|
if card['jumptype'] == 10014: # 10014->playlist
|
|
278
336
|
playlists.append(
|
|
279
337
|
PlaylistModel(identifier=str(card['id']),
|
|
@@ -369,6 +427,35 @@ class QQProvider(AbstractProvider, ProviderV2):
|
|
|
369
427
|
data_songs = self.api.song_similar(int(song.identifier))
|
|
370
428
|
return [_deserialize(data_song, QQSongSchema) for data_song in data_songs]
|
|
371
429
|
|
|
430
|
+
def current_user_dislike_create_songs_rd(self):
|
|
431
|
+
user = self.get_current_user()
|
|
432
|
+
if user is None:
|
|
433
|
+
return create_reader([])
|
|
434
|
+
# FIXME: 如果用户的黑名单歌曲数量较多的话,这样处理则是不够的
|
|
435
|
+
items = self.api.get_dislike_list(1, API.DislikeListType.song, 0)
|
|
436
|
+
songs = []
|
|
437
|
+
for item in items:
|
|
438
|
+
name = item['Name']
|
|
439
|
+
title, artists_name = name.split(' - ')
|
|
440
|
+
song = BriefSongModel(
|
|
441
|
+
source=SOURCE,
|
|
442
|
+
identifier=item['ID'],
|
|
443
|
+
title=title,
|
|
444
|
+
artists_name=artists_name,
|
|
445
|
+
)
|
|
446
|
+
songs.append(song)
|
|
447
|
+
return create_reader(songs)
|
|
448
|
+
|
|
449
|
+
def current_user_dislike_add_song(self, song):
|
|
450
|
+
items = [{'ID': song.identifier}]
|
|
451
|
+
js = self.api.add_to_dislike_list(items, API.DislikeListType.song)
|
|
452
|
+
return js.get('Retcode') == 0
|
|
453
|
+
|
|
454
|
+
def current_user_dislike_remove_song(self, song):
|
|
455
|
+
items = [{'ID': song.identifier}]
|
|
456
|
+
js = self.api.remove_from_dislike_list(items, API.DislikeListType.song)
|
|
457
|
+
return js.get('Retcode') == 0
|
|
458
|
+
|
|
372
459
|
|
|
373
460
|
def _deserialize(data, schema_cls):
|
|
374
461
|
schema = schema_cls()
|
|
@@ -11,20 +11,11 @@ from feeluown.app.gui_app import GuiApp
|
|
|
11
11
|
|
|
12
12
|
from .provider import provider
|
|
13
13
|
from .excs import QQIOError
|
|
14
|
+
from .login import read_cookies, write_cookies
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
USER_INFO_FILE = DATA_DIR + '/qqmusic_user_info.json'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def read_cookies():
|
|
22
|
-
if os.path.exists(USER_INFO_FILE):
|
|
23
|
-
# if the file is broken, just raise error
|
|
24
|
-
with open(USER_INFO_FILE) as f:
|
|
25
|
-
return json.load(f).get('cookies', None)
|
|
26
|
-
|
|
27
|
-
|
|
28
19
|
class ProviderUI(AbstractProviderUi):
|
|
29
20
|
def __init__(self, app: GuiApp):
|
|
30
21
|
self._app = app
|
|
@@ -72,31 +63,13 @@ class LoginDialog(CookiesLoginDialog):
|
|
|
72
63
|
provider._user = user
|
|
73
64
|
|
|
74
65
|
async def user_from_cookies(self, cookies):
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
uin = provider.api.get_uin_from_cookies(cookies)
|
|
79
|
-
if uin is None:
|
|
80
|
-
raise InvalidCookies("can't extract user info from cookies")
|
|
81
|
-
|
|
82
|
-
provider.api.set_cookies(cookies)
|
|
83
|
-
# try to extract current user
|
|
84
|
-
try:
|
|
85
|
-
user = await run_fn(provider.user_get, uin)
|
|
86
|
-
except QQIOError:
|
|
87
|
-
provider.api.set_cookies(None)
|
|
88
|
-
raise InvalidCookies('get user info with cookies failed, expired cookies?')
|
|
89
|
-
else:
|
|
66
|
+
user, err = await run_fn(provider.try_get_user_from_cookies, cookies)
|
|
67
|
+
if user:
|
|
90
68
|
return user
|
|
69
|
+
raise InvalidCookies(err)
|
|
91
70
|
|
|
92
71
|
def load_user_cookies(self):
|
|
93
72
|
return read_cookies()
|
|
94
73
|
|
|
95
74
|
def dump_user_cookies(self, user, cookies):
|
|
96
|
-
|
|
97
|
-
'identifier': user.identifier,
|
|
98
|
-
'name': user.name,
|
|
99
|
-
'cookies': cookies
|
|
100
|
-
}
|
|
101
|
-
with open(USER_INFO_FILE, 'w') as f:
|
|
102
|
-
json.dump(js, f, indent=2)
|
|
75
|
+
write_cookies(user, cookies)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
2
|
-
Name:
|
|
3
|
-
Version: 1.0.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fuo_qqmusic
|
|
3
|
+
Version: 1.0.7
|
|
4
4
|
Summary: feeluown qqmusic plugin
|
|
5
5
|
Home-page: https://github.com/feeluown/feeluown-qqmusic
|
|
6
6
|
Author: Cosven
|
|
@@ -13,3 +13,13 @@ Classifier: Programming Language :: Python :: 3.7
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.8
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.9
|
|
15
15
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Requires-Dist: feeluown>=4.1.3
|
|
17
|
+
Requires-Dist: requests
|
|
18
|
+
Requires-Dist: marshmallow<4.0.0,>=3.0
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: keywords
|
|
24
|
+
Dynamic: requires-dist
|
|
25
|
+
Dynamic: summary
|
|
@@ -4,6 +4,7 @@ fuo_qqmusic/__init__.py
|
|
|
4
4
|
fuo_qqmusic/api.py
|
|
5
5
|
fuo_qqmusic/consts.py
|
|
6
6
|
fuo_qqmusic/excs.py
|
|
7
|
+
fuo_qqmusic/login.py
|
|
7
8
|
fuo_qqmusic/provider.py
|
|
8
9
|
fuo_qqmusic/provider_ui.py
|
|
9
10
|
fuo_qqmusic/schemas.py
|
|
@@ -13,4 +14,6 @@ fuo_qqmusic.egg-info/dependency_links.txt
|
|
|
13
14
|
fuo_qqmusic.egg-info/entry_points.txt
|
|
14
15
|
fuo_qqmusic.egg-info/requires.txt
|
|
15
16
|
fuo_qqmusic.egg-info/top_level.txt
|
|
16
|
-
fuo_qqmusic/assets/icon.svg
|
|
17
|
+
fuo_qqmusic/assets/icon.svg
|
|
18
|
+
tests/test_api.py
|
|
19
|
+
tests/test_provider.py
|
|
@@ -5,7 +5,7 @@ from setuptools import setup
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name='fuo_qqmusic',
|
|
8
|
-
version='1.0.
|
|
8
|
+
version='1.0.7',
|
|
9
9
|
description='feeluown qqmusic plugin',
|
|
10
10
|
author='Cosven',
|
|
11
11
|
author_email='yinshaowen241@gmail.com',
|
|
@@ -29,7 +29,7 @@ setup(
|
|
|
29
29
|
install_requires=[
|
|
30
30
|
'feeluown>=4.1.3',
|
|
31
31
|
'requests',
|
|
32
|
-
'marshmallow>=3.0'
|
|
32
|
+
'marshmallow>=3.0,<4.0.0'
|
|
33
33
|
],
|
|
34
34
|
entry_points={
|
|
35
35
|
'fuo.plugins_v1': [
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from fuo_qqmusic.api import API
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def parse_cookie_to_dict(cookie_str):
|
|
6
|
+
# 初始化结果字典
|
|
7
|
+
result = {}
|
|
8
|
+
|
|
9
|
+
# 按分号分割字符串,得到每个键值对
|
|
10
|
+
pairs = cookie_str.split(";")
|
|
11
|
+
|
|
12
|
+
for pair in pairs:
|
|
13
|
+
# 去除两端空格
|
|
14
|
+
pair = pair.strip()
|
|
15
|
+
|
|
16
|
+
# 按等号分割键和值
|
|
17
|
+
if "=" in pair:
|
|
18
|
+
key, value = pair.split("=", 1)
|
|
19
|
+
key = key.strip()
|
|
20
|
+
value = value.strip()
|
|
21
|
+
|
|
22
|
+
# 如果值为空字符串,则设置为 None
|
|
23
|
+
if value == "":
|
|
24
|
+
value = None
|
|
25
|
+
|
|
26
|
+
# 存入字典
|
|
27
|
+
result[key] = value
|
|
28
|
+
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
cookie_str = "Your cookie string here"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.skip(reason="need valid cookies")
|
|
36
|
+
def test_api():
|
|
37
|
+
api = API()
|
|
38
|
+
api.set_cookies(parse_cookie_to_dict(cookie_str))
|
|
39
|
+
# You can also use the following code to load cookies
|
|
40
|
+
# from fuo_qqmusic.provider import provider
|
|
41
|
+
# provider.auto_login()
|
|
42
|
+
# api = provider.api
|
|
43
|
+
items = [
|
|
44
|
+
{
|
|
45
|
+
"ID": "238159921",
|
|
46
|
+
"Name": "无人区-Vacuum Track#ADD8E6- - 米缐p.",
|
|
47
|
+
"IdType": 0,
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
print(api.add_to_dislike_list(items, type_=API.DislikeListType.song))
|
|
51
|
+
print(api.get_dislike_list(type_=API.DislikeListType.song))
|
|
52
|
+
print(api.remove_from_dislike_list(items, type_=API.DislikeListType.song))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
test_api()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from fuo_qqmusic import provider
|
|
8
|
+
from fuo_qqmusic.api import API
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _read_json_fixture(path):
|
|
12
|
+
path = os.path.join('data/fixtures', path)
|
|
13
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
14
|
+
data = json.load(f)
|
|
15
|
+
return data
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def album_3913679():
|
|
20
|
+
return _read_json_fixture('album_3913679.json')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_provider_album_get(album_3913679):
|
|
24
|
+
patch.object(API, 'album_detail', return_value=album_3913679)
|
|
25
|
+
album = provider.album_get('3913679')
|
|
26
|
+
assert album.identifier == '3913679'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|