• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

feeluown / feeluown-netease / 8986919738

07 May 2024 02:11PM UTC coverage: 22.263% (-0.06%) from 22.319%
8986919738

push

github

cosven
support toplist

4 of 24 new or added lines in 4 files covered. (16.67%)

1 existing line in 1 file now uncovered.

362 of 1626 relevant lines covered (22.26%)

0.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

27.86
/fuo_netease/api.py
1
#!/usr/bin/env python
2
# encoding: UTF-8
3

4
import base64
1✔
5
import binascii
1✔
6
import hashlib
1✔
7
import os
1✔
8
import json
1✔
9
import logging
1✔
10

11
from bs4 import BeautifulSoup
1✔
12
import requests
1✔
13
from Crypto.Cipher import AES
1✔
14

15
from .excs import NeteaseIOError
1✔
16

17
site_uri = 'http://music.163.com'
1✔
18
uri = 'http://music.163.com/api'
1✔
19
uri_we = 'http://music.163.com/weapi'
1✔
20
uri_v1 = 'http://music.163.com/weapi/v1'
1✔
21
uri_v3 = 'http://music.163.com/weapi/v3'
1✔
22
uri_e = 'https://music.163.com/eapi'
1✔
23

24
logger = logging.getLogger(__name__)
1✔
25

26

27
class CodeShouldBe200(NeteaseIOError):
1✔
28
    def __init__(self, data):
1✔
29
        self._code = data['code']
×
30
        self._data = data
×
31

32
    def __str__(self):
1✔
33
        return f'json code field should be 200, got {self._code}. data: {self._data}'
×
34

35

36
class API(object):
1✔
37
    def __init__(self):
1✔
38
        super().__init__()
1✔
39
        self.headers = {
1✔
40
            'Host': 'music.163.com',
41
            'Connection': 'keep-alive',
42
            'Referer': 'http://music.163.com/',
43
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2)'
44
                          ' AppleWebKit/537.36 (KHTML, like Gecko)'
45
                          ' Chrome/33.0.1750.152 Safari/537.36'
46
        }
47
        self._cookies = dict(appver="1.2.1", os="osx")
1✔
48
        self._http = None
1✔
49

50
    @property
1✔
51
    def cookies(self):
1✔
52
        return self._cookies
×
53

54
    def load_cookies(self, cookies):
1✔
55
        self._cookies.update(cookies)
×
56
        # 云盘资源发布仅有似乎不支持osx平台
57
        self._cookies.update(dict(appver="7.2.24", os="android"))
×
58

59
    def set_http(self, http):
1✔
60
        self._http = http
×
61

62
    @property
1✔
63
    def http(self):
1✔
64
        return requests if self._http is None else self._http
1✔
65

66
    def request(self, method, action, query=None, timeout=2):
1✔
67
        # logger.info('method=%s url=%s data=%s' % (method, action, query))
68
        if method == "GET":
1✔
69
            res = self.http.get(action, headers=self.headers,
×
70
                                cookies=self._cookies, timeout=timeout)
71
        elif method == "POST":
1✔
72
            res = self.http.post(action, data=query, headers=self.headers,
1✔
73
                                 cookies=self._cookies, timeout=timeout)
74
        elif method == "POST_UPDATE":
×
75
            res = self.http.post(action, data=query, headers=self.headers,
×
76
                                 cookies=self._cookies, timeout=timeout)
77
            self._cookies.update(res.cookies.get_dict())
×
78
        content = res.content
1✔
79
        content_str = content.decode('utf-8')
1✔
80
        content_dict = json.loads(content_str)
1✔
81
        return content_dict
1✔
82

83
    def login(self, country_code, username, pw_encrypt, phone=False):
1✔
84
        action = 'http://music.163.com/api/login/'
×
85
        phone_action = 'http://music.163.com/api/login/cellphone/'
×
86
        data = {
×
87
            'password': pw_encrypt,
88
            'rememberLogin': 'true'
89
        }
90
        if username.isdigit() and (len(username) == 11 or country_code):
×
91
            phone = True
×
92
            data.update({'phone': username, 'countrycode': country_code or '86'})
×
93
        else:
94
            data.update({'username': username})
×
95
        if phone:
×
96
            res_data = self.request("POST_UPDATE", phone_action, data)
×
97
            return res_data
×
98
        else:
99
            res_data = self.request("POST_UPDATE", action, data)
×
100
            return res_data
×
101

102
    def check_cookies(self):
1✔
103
        url = uri + '/push/init'
×
104
        data = self.request("POST_UPDATE", url, {})
×
105
        if data['code'] == 200:
×
106
            return True
×
107
        return False
×
108

109
    def confirm_captcha(self, captcha_id, text):
1✔
110
        action = uri + '/image/captcha/verify/hf?id=' + str(captcha_id) +\
×
111
            '&captcha=' + str(text)
112
        data = self.request('GET', action)
×
113
        return data
×
114

115
    def get_captcha_url(self, captcha_id):
1✔
116
        action = 'http://music.163.com/captcha?id=' + str(captcha_id)
×
117
        return action
×
118

119
    def user_profile(self, user_id):
1✔
120
        """
121
        {'nickname': 'cosven',
122
         'avatarImg': 'xx.jpg',
123
         'userType': 0,
124
         'authStatus': 0,
125
         'expertTags': None,
126
         'backgroundUrl': 'xx.jpg',
127
         'playCount': 2892,
128
         'createdplCnt': 8,
129
         'starPlaylist': {...},
130
         'playlist': [...],
131
         'code': 200
132
        }
133
        """
134
        action = uri_we + '/share/userprofile/info'
×
135
        data = {'userId': user_id}
×
136
        payload = self.encrypt_request(data)
×
137
        res_data = self.request('POST', action, payload)
×
138
        code = res_data['code']
×
139
        if code == 200:
×
140
            return res_data
×
141
        elif code == 400:
×
142
            logger.warn(f'user:{user_id} may be invalid')
×
143
        raise CodeShouldBe200(res_data)
×
144

145
    # 用户歌单
146
    def user_playlists(self, uid, offset=0, limit=200):
1✔
147
        action = uri + '/user/playlist/?offset=' + str(offset) +\
×
148
            '&limit=' + str(limit) + '&uid=' + str(uid)
149
        data = self.request('GET', action)
×
150
        if data['code'] == 200:
×
151
            return data['playlist']
×
152
        return []
×
153

154
    def user_favorite_albums(self, offset=0, limit=30):
1✔
155
        action = uri_we + '/album/sublist'
×
156
        data = {
×
157
            'offset': offset,
158
            'limit': limit,
159
            'csrf_token': self._cookies.get('__csrf')}
160
        payload = self.encrypt_request(data)
×
161
        res_data = self.request('POST', action, payload)
×
162
        if res_data['code'] == 200:
×
163
            return res_data
×
164
        return None
×
165

166
    def user_favorite_artists(self, offset=0, limit=30):
1✔
167
        action = uri_we + '/artist/sublist'
×
168
        data = {
×
169
            'offset': offset,
170
            'limit': limit,
171
            'csrf_token': self._cookies.get('__csrf')}
172
        payload = self.encrypt_request(data)
×
173
        res_data = self.request('POST', action, payload)
×
174
        if res_data['code'] == 200:
×
175
            return res_data
×
176
        return None
×
177

178
    # 搜索单曲(1),歌手(100),专辑(10),歌单(1000),用户(1002) *(type)*
179
    def search(self, s, stype=1, offset=0, total='true', limit=30):
1✔
180
        """get songs list from search keywords"""
181
        action = uri + '/search/get'
×
182
        data = {
×
183
            's': s,
184
            'type': stype,
185
            'offset': offset,
186
            'total': total,
187
            'limit': limit
188
        }
189
        resp = self.request('POST', action, data)
×
190
        if resp['code'] == 200:
×
191
            return resp['result']
×
192
        return []
×
193

194
    def playlist_detail_v3(self, pid, offset=0, limit=200):
1✔
195
        """
196
        该接口返回的 ['playlist']['trackIds'] 字段会包含所有的歌曲
197
        """
198
        action = '/playlist/detail'
1✔
199
        url = uri_v3 + action
1✔
200
        data = dict(id=pid, limit=limit, offset=offset, n=limit)
1✔
201
        payload = self.encrypt_request(data)
1✔
202
        res_data = self.request('POST', url, payload)
1✔
203
        if res_data['code'] == 200:
1✔
204
            return res_data['playlist']
1✔
205
        raise CodeShouldBe200(res_data)
×
206

207
    def update_playlist_name(self, pid, name):
1✔
208
        url = uri + '/playlist/update/name'
×
209
        data = {
×
210
            'id': pid,
211
            'name': name
212
        }
213
        res_data = self.request('POST', url, data)
×
214
        return res_data
×
215

216
    def new_playlist(self, uid, name):
1✔
217
        url = uri + '/playlist/create'
×
218
        data = {
×
219
            'uid': uid,
220
            'name': name
221
        }
222
        res_data = self.request('POST', url, data)
×
223
        if res_data['code'] == 200:
×
224
            return res_data['playlist']
×
225
        raise CodeShouldBe200(res_data)
×
226

227
    def delete_playlist(self, pid):
1✔
228
        url = uri + '/playlist/delete'
×
229
        data = {
×
230
            'id': pid,
231
            'pid': pid
232
        }
233
        res_data = self.request('POST', url, data)
×
234
        if res_data['code'] == 200:
×
235
            return
×
236
        raise CodeShouldBe200(res_data)
×
237

238
    def artist_infos(self, artist_id):
1✔
239
        """
240
        :param artist_id: artist_id
241
        :return: {
242
            code: int,
243
            artist: {artist},
244
            more: boolean,
245
            hotSongs: [songs]
246
        }
247
        """
248
        action = uri + '/artist/' + str(artist_id)
×
249
        data = self.request('GET', action)
×
250
        return data
×
251

252
    def artist_songs(self, artist_id, offset=0, limit=50):
1✔
253
        action = uri_v1 + '/artist/songs'
×
254
        data = dict(id=artist_id,
×
255
                    limit=limit,
256
                    offset=offset,
257
                    order='hot',
258
                    work_type=1,
259
                    private_cloud='true')
260
        payload = self.encrypt_request(data)
×
261
        res_data = self.request('POST', action, payload)
×
262
        if res_data['code'] == 200:
×
263
            return res_data
×
264
        raise CodeShouldBe200(res_data)
×
265

266
    def artist_albums(self, artist_id, offset=0, limit=20):
1✔
267
        action = ('{uri}/artist/albums/{artist_id}?'
×
268
                  'offset={offset}&limit={limit}')
269
        action = action.format(uri=uri,
×
270
                               artist_id=artist_id,
271
                               offset=offset,
272
                               limit=limit)
273
        data = self.request('GET', action)
×
274
        return data
×
275

276
    # album id --> song id set
277
    def album_infos(self, album_id):
1✔
278
        """
279
        :param album_id:
280
        :return: {
281
            code: int,
282
            album: { album }
283
        }
284
        """
285
        action = uri + '/album/' + str(album_id)
×
286
        data = self.request('GET', action)
×
287
        if data['code'] == 200:
×
288
            return data['album']
×
289

290
    def album_desc(self, album_id):
1✔
291
        action = site_uri + '/album'
×
292
        data = {'id': album_id}
×
293
        res = self.http.get(action, data, headers=self.headers)
×
294
        if res is None:
×
295
            return None
×
296
        soup = BeautifulSoup(res.content, 'html.parser')
×
297
        albdescs = soup.select('.n-albdesc')
×
298
        if albdescs:
×
299
            return albdescs[0].prettify()
×
300
        return ''
×
301

302
    def artist_desc(self, artist_id):
1✔
303
        action = site_uri + '/artist/desc'
×
304
        data = {'id': artist_id}
×
305
        res = self.http.get(action, data, headers=self.headers)
×
306
        if res is None:
×
307
            return None
×
308
        soup = BeautifulSoup(res.content, 'html.parser')
×
309
        artdescs = soup.select('.n-artdesc')
×
310
        if artdescs:
×
311
            artdesc = artdescs[0]
×
312
            # FIXME: 艺术家描述是 html 格式的,它有一个 header 为
313
            # ``<h2>{artist_name}简介</h2>``, 而在 FeelUOwn 的 UI 设计中,
314
            # FeelUown 是把艺术家描述显示在艺术家名字下面,
315
            # 而艺术家名字也是用 ``<h2>{artist_name}</h2>`` 来渲染的,
316
            # 这样在视觉上就会出现两个非常相似的文字,非常难看,
317
            # 所以我们在这里把描述中的标题去掉。
318
            # 另外,我们还把描述中所有的 h2 header 替换成 h3 header。
319
            artdesc.h2.decompose()
×
320
            for h2 in artdesc.select('h2'):
×
321
                h2.name = 'h3'
×
322
            return artdesc.prettify()
×
323
        return ''
×
324

325
    # song id --> song url ( details )
326
    def song_detail(self, music_id):
1✔
327
        action = uri + '/song/detail/?id=' + str(music_id) + '&ids=[' +\
×
328
            str(music_id) + ']'
329
        data = self.request('GET', action)
×
330
        if data['code'] == 200:
×
331
            if data['songs']:
×
332
                return data['songs'][0]
×
333
            return
×
334
        raise CodeShouldBe200(data)
×
335

336
    def weapi_songs_url(self, music_ids, bitrate=320000):
1✔
337
        """
338
        When the expected bitrate song url does not exist, server will
339
        return a fallback song url. For example, we request a song
340
        url with bitrate=320000. If there only exists a song url with
341
        bitrate=128000, the server will return it.
342

343
        NOTE(cosven): After some manual testing, we found that the url is
344
        None in following cases:
345
        1. the song is for vip-user and the current user is not vip.
346
        2. the song is a paid song and the current user(may be a vip)
347
           has not bought it.
348
        """
349
        url = uri_we + '/song/enhance/player/url'
×
350
        data = {
×
351
            'ids': music_ids,
352
            'br': bitrate,
353
            'csrf_token': self._cookies.get('__csrf')
354
        }
355
        payload = self.encrypt_request(data)
×
356
        data = self.request('POST', url, payload)
×
357
        if data['code'] == 200:
×
358
            return data['data']
×
359
        return []
×
360

361
    def songs_detail(self, music_ids):
1✔
362
        """批量获取歌曲的详细信息(老版)
363

364
        经过测试 music_ids 不能超过 200 个。
365
        """
366
        music_ids = [str(music_id) for music_id in music_ids]
×
367
        action = uri + '/song/detail?ids=[' +\
×
368
            ','.join(music_ids) + ']'
369
        data = self.request('GET', action)
×
370
        if data['code'] == 200:
×
371
            return data['songs']
×
372
        return []
×
373

374
    def songs_detail_v3(self, music_ids):
1✔
375
        """批量获取歌曲的详细信息
376

377
        经过测试 music_ids 不能超过 1000 个
378
        """
379
        action = '/song/detail'
×
380
        url = uri_v3 + action
×
381
        params = {
×
382
            'c': json.dumps([{'id': id_} for id_ in music_ids]),
383
            'ids': json.dumps(music_ids)
384
        }
385
        payload = self.encrypt_request(params)
×
386
        data = self.request('POST', url, payload)
×
387
        if data['code'] == 200:
×
388
            return data['songs']
×
389
        return []
×
390

391
    def op_music_to_playlist(self, mid, pid, op):
1✔
392
        """
393
        :param op: add or del
394
        """
395
        url_add = uri + '/playlist/manipulate/tracks'
×
396
        trackIds = '["' + str(mid) + '"]'
×
397
        data_add = {
×
398
            'tracks': str(mid),  # music id
399
            'pid': str(pid),    # playlist id
400
            'trackIds': trackIds,  # music id str
401
            'op': op   # opation
402
        }
403
        data = self.request('POST', url_add, data_add)
×
404
        code = data.get('code')
×
405

406
        # 从歌单中成功的移除歌曲时,code 是 200
407
        # 当从歌单中移除一首不存在的歌曲时,code 也是 200
408
        # 当向歌单添加歌曲时,如果歌曲已经在列表当中,返回 code 为 502
409
        # code 为 521 时,可能是因为:绑定手机号后才可操作哦
410
        if code == 200:
×
411
            return 1
×
412
        elif code == 502:
×
413
            return -1
×
414
        else:
415
            return 0
×
416

417
    def set_music_favorite(self, mid, flag):
1✔
418
        url = uri + '/song/like'
×
419
        data = {
×
420
            "trackId": mid,
421
            "like": str(flag).lower(),
422
            "time": 0
423
        }
424
        return self.request("POST", url, data)
×
425

426
    def get_radio_music(self):
1✔
427
        url = uri + '/radio/get'
×
428
        data = self.request('GET', url)
×
429
        if data['code'] == 200:
×
430
            return data['data']
×
431
        return None
×
432

433
    def get_mv_detail(self, mvid):
1✔
434
        """Get mv detail
435
        :param mvid: mv id
436
        :return:
437
        """
438
        url = uri + '/mv/detail?id=' + str(mvid)
×
439
        data = self.request('GET', url)
×
440
        if data['code'] == 200:
×
441
            return data['data']
×
442
        raise CodeShouldBe200(data)
×
443

444
    def get_lyric_by_songid(self, mid):
1✔
445
        """Get song lyric
446
        :param mid: music id
447
        :return: {
448
            lrc: {
449
                version: int,
450
                lyric: str
451
            },
452
            tlyric: {
453
                version: int,
454
                lyric: str
455
            }
456
            sgc: bool,
457
            qfy: bool,
458
            sfy: bool,
459
            transUser: {},
460
            code: int,
461
        }
462
        """
463
        # tv 表示翻译。-1:表示要翻译,1:不要
464
        # lv 为 1 时,对于纯音乐,API 返回的歌词为空
465
        # lv 为 -1 时,纯音乐返回的歌词是:纯音乐,请欣赏
466
        url = uri + '/song/lyric?' + 'id=' + str(mid) + '&lv=-1&kv=1&tv=-1'
×
467
        return self.request('GET', url)
×
468

469
    def get_similar_song(self, mid, offset=0, limit=10):
1✔
470
        url = (f"http://music.163.com/api/discovery/simiSong"
×
471
               f"?songid={mid}&offset={offset}&total=true&limit={limit}")
472
        data = self.request('GET', url)
×
473
        if data['code'] == 200:
×
474
            return data['songs']
×
475
        raise CodeShouldBe200(data)
×
476

477
    def get_recommend_songs(self):
1✔
478
        url = uri_v3 + '/discovery/recommend/songs'
×
479
        payload = self.encrypt_request({})
×
480
        res_data = self.request('POST', url, payload)
×
481
        if res_data['code'] == 200:
×
482
            return res_data['data']['dailySongs']
×
483
        raise CodeShouldBe200(res_data)
×
484

485
    def get_recommend_playlists(self):
1✔
486
        url = uri + '/discovery/recommend/resource'
×
487
        payload = self.encrypt_request({})
×
488
        data = self.request('POST', url, payload)
×
489
        if data['code'] == 200:
×
490
            return data['recommend']
×
491
        raise CodeShouldBe200(data)
×
492

493
    def get_comment(self, comment_id):
1✔
494
        data = {
×
495
            'rid': comment_id,
496
            'offset': '0',
497
            'total': 'true',
498
            'limit': '20',
499
            'csrf_token': self._cookies.get('__csrf')
500
        }
501
        url = uri_v1 + '/resource/comments/' + comment_id
×
502
        payload = self.encrypt_request(data)
×
503
        res_data = self.request('POST', url, payload)
×
504
        if res_data['code'] == 200:
×
505
            return res_data
×
506
        raise CodeShouldBe200(res_data)
×
507

508
    def accumulate_pl_count(self, mid):
1✔
509
        data = {"ids": "[%d]" % mid, "br": 128000,
×
510
                "csrf_token": self._cookies.get('__scrf')}
511
        url = uri_we + '/pl/count'
×
512
        payload = self.encrypt_request(data)
×
513
        return self.request('POST', url, payload)
×
514

515
    def cloud_songs(self, offset=0, limit=30):
1✔
516
        data = dict(limit=limit, offset=offset)
×
517
        url = uri_v1 + '/cloud/get'
×
518
        payload = self.encrypt_request(data)
×
519
        res_data = self.request('POST', url, payload)
×
520
        if res_data['code'] == 200:
×
521
            return res_data
×
522
        raise CodeShouldBe200(res_data)
×
523

524
    def cloud_songs_detail(self, music_ids):
1✔
525
        data = dict(songIds=music_ids.split(","))
×
526
        url = uri_v1 + '/cloud/get/byids'
×
527
        payload = self.encrypt_request(data)
×
528
        return self.request('POST', url, payload)
×
529

530
    def cloud_songs_delete(self, music_ids):
1✔
531
        data = dict(songIds=music_ids.split(","))
×
532
        url = uri_we + '/cloud/del'
×
533
        payload = self.encrypt_request(data)
×
534
        return self.request('POST', url, payload)
×
535

536
    def cloud_song_match(self, sid, asid):
1✔
537
        url = uri + f'/cloud/user/song/match?songId={sid}&adjustSongId={asid}'
×
538
        data = self.request('GET', url)
×
539
        if data['code'] == 200:
×
540
            return data['data']
×
541
        raise CodeShouldBe200(data)
×
542

543
    def cloud_song_upload(self, path):
1✔
544
        def md5sum(file):
×
545
            md5sum = hashlib.md5()
×
546
            with open(file, 'rb') as f:
×
547
                # while chunk := f.read():
548
                #     md5sum.update(chunk)
549
                md5sum.update(f.read())
×
550
            return md5sum
×
551

552
        from .cloud_helpers.cloud_api import Cloud_API
×
553
        cloud_api = Cloud_API(self, uri_e)
×
554

555
        fname = os.path.basename(path)
×
556
        fext = path.split('.')[-1]
×
557
        '''Parsing file names'''
558
        fsize = os.stat(path).st_size
×
559
        md5 = md5sum(path).hexdigest()
×
560
        logger.debug(f'[-] Checking file ( MD5: {md5} )')
×
561
        cresult = cloud_api.GetCheckCloudUpload(md5)
×
562
        if cresult['code'] != 200:
×
563
            return 'UPLOAD_CHECK_FAILED'
×
564

565
        '''网盘资源发布 4 步走:'''
566
        '''1.拿到上传令牌 - 需要文件名,MD5,文件大小'''
567
        token = cloud_api.GetNosToken(fname, md5, fsize, fext)
×
568
        if token['code'] != 200:
×
569
            return 'TOKEN_ALLOC_FAILED'
×
570
        token = token['result']
×
571

572
        '''2. 若文件未曾上传完毕,则完成其上传'''
573
        if cresult['needUpload']:
×
574
            logger.info(f'[+] {fname} needs to be uploaded ( {fsize} B )')
×
575
            try:
×
576
                upload_result = cloud_api.SetUploadObject(
×
577
                    open(path, 'rb'),
578
                    md5, fsize, token['objectKey'], token['token']
579
                )
580
            except Exception:
×
581
                return 'UPLOAD_FAILED'
×
582
            logger.debug(f'[-] Response:\n  {upload_result}')
×
583

584
        '''3. 提交资源'''
585
        songId = cresult['songId']
×
586
        logger.debug(f'''[!] Assuming upload has finished,preparing to submit
×
587
        ID  :   {songId}
588
        MD5 :   {md5}
589
        NAME:   {fname}''')
590
        metadata = cloud_api.GetMetadata(path)
×
591
        submit_result = cloud_api.SetUploadCloudInfo(
×
592
            token['resourceId'], songId, md5, fname,
593
            song=metadata.get('title', '.'),
594
            artist=metadata.get('artist', '.'),
595
            album=metadata.get('album', '.')
596
        )
597
        if submit_result['code'] != 200:
×
598
            return 'SUBMIT_FAILED'
×
599
        logger.debug(f'[-] Response:\n  {submit_result}')
×
600

601
        '''4. 发布资源'''
602
        publish_result = cloud_api.SetPublishCloudResource(submit_result['songId'])
×
603
        if publish_result['code'] != 200:
×
604
            return 'PUBLISH_FAILED'
×
605
        logger.debug(f'[-] Response:\n  {publish_result}')
×
606

607
        return 'STATUS_SUCCEEDED'
×
608

609
    def subscribed_djradio(self, limit=0, offset=0):
1✔
610
        data = dict(limit=100, time=0, needFee=False)
×
611
        url = uri_e + '/djradio/subed/v1'
×
612
        payload = self.eapi_encrypt(b'/api/djradio/subed/v1', data)
×
613
        return self.request('POST', url, {'params': payload})
×
614

615
    def djradio_detail(self, radio_id):
1✔
616
        data = dict(id=radio_id)
×
617
        url = uri_e + '/djradio/v2/get'
×
618
        payload = self.eapi_encrypt(b'/api/djradio/v2/get', data)
×
619
        return self.request('POST', url, {'params': payload})
×
620

621
    def djradio_song_detail(self, id_):
1✔
622
        data = dict(id=id_)
×
623
        url = uri_e + '/dj/program/detail'
×
624
        payload = self.eapi_encrypt(b'/api/dj/program/detail', data)
×
625
        return self.request('POST', url, {'params': payload})
×
626

627
    def djradio_list(self, radio_id, limit=50, offset=0, asc=False):
1✔
628
        data = dict(radioId=radio_id, limit=limit, offset=offset, asc=asc)
×
629
        url = uri_e + '/v1/dj/program/byradio'
×
630
        payload = self.eapi_encrypt(b'/api/v1/dj/program/byradio', data)
×
631
        return self.request('POST', url, {'params': payload})
×
632

633
    def list_toplist(self):
1✔
NEW
634
        url = uri + '/toplist'
×
NEW
635
        data = self.request('GET', url)
×
NEW
636
        if data['code'] == 200:
×
NEW
637
            return data['list']
×
NEW
638
        raise CodeShouldBe200(data)
×
639

640
    def _create_aes_key(self, size):
1✔
641
        return (''.join([hex(b)[2:] for b in os.urandom(size)]))[0:16]
1✔
642

643
    def _aes_encrypt(self, text, key):
1✔
644
        pad = 16 - len(text) % 16
1✔
645
        text = text + pad * chr(pad)
1✔
646
        encryptor = AES.new(bytes(key, 'utf-8'), 2, b'0102030405060708')
1✔
647
        enc_text = encryptor.encrypt(bytes(text, 'utf-8'))
1✔
648
        enc_text_encode = base64.b64encode(enc_text)
1✔
649
        return enc_text_encode
1✔
650

651
    def _rsa_encrypt(self, text):
1✔
652
        e = '010001'
1✔
653
        n = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615'\
1✔
654
            'bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf'\
655
            '695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46'\
656
            'bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b'\
657
            '8e289dc6935b3ece0462db0a22b8e7'
658
        reverse_text = text[::-1]
1✔
659
        encrypted_text = pow(int(binascii.hexlify(reverse_text), 16),
1✔
660
                             int(e, 16), int(n, 16))
661
        return format(encrypted_text, "x").zfill(256)
1✔
662

663
    def eapi_encrypt(self, path, params):
1✔
664
        """
665
        eapi接口参数加密
666
        :param bytes path: 请求的路径
667
        :param params: 请求参数
668
        :return str: 加密结果
669
        """
670
        params = json.dumps(params, separators=(',', ':')).encode()
×
671
        sign_src = b'nobody' + path + b'use' + params + b'md5forencrypt'
×
672
        m = hashlib.md5()
×
673
        m.update(sign_src)
×
674
        sign = m.hexdigest()
×
675
        aes_src = path + b'-36cd479b6b5-' + params + b'-36cd479b6b5-' + sign.encode()
×
676
        pad = 16 - len(aes_src) % 16
×
677
        aes_src = aes_src + bytearray([pad] * pad)
×
678
        crypt = AES.new(b'e82ckenh8dichen8', AES.MODE_ECB)
×
679
        ret = crypt.encrypt(aes_src)
×
680
        return binascii.b2a_hex(ret).upper()
×
681

682
    def encrypt_request(self, data):
1✔
683
        text = json.dumps(data)
1✔
684
        first_aes_key = '0CoJUm6Qyw8W8jud'
1✔
685
        second_aes_key = self._create_aes_key(16)
1✔
686
        enc_text = self._aes_encrypt(
1✔
687
            self._aes_encrypt(text, first_aes_key).decode('ascii'),
688
            second_aes_key).decode('ascii')
689
        enc_aes_key = self._rsa_encrypt(second_aes_key.encode('ascii'))
1✔
690
        payload = {
1✔
691
            'params': enc_text,
692
            'encSecKey': enc_aes_key,
693
        }
694
        return payload
1✔
695

696

697
api = API()
1✔
698

699

700
if __name__ == '__main__':
1✔
701
    from fuo_netease.login_controller import LoginController
×
702
    user = LoginController.load()
×
703
    cookies, _ = user.cache_get('cookies')
×
704
    api.load_cookies(cookies)
×
705
    print(api.djradio_song_detail(1883706033))
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc