├── .gitignore ├── README.md ├── main.py ├── mysql3 └── mysql3.py ├── requirement.txt ├── server ├── __init__.py ├── kugoulitemusic.py ├── kugoumusic.py ├── qishuimusic.py ├── qqmusic.py ├── wangyiyunmusic.py ├── wufanyouxiting.py └── xuelang.py ├── start.sh └── utils ├── os.py ├── tc_tea.py └── util.py /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 项目介绍 2 | 此项目旨在从你的手机里导出音乐。 3 | ## 使用环境 4 | 1. 你的手机必须已经root 5 | 6 | 2. 手机和电脑必须在同一个局域网 7 | 8 | ## 使用指南 9 | 10 | 1. 确保您的设备满足所需的环境。 11 | 12 | 2. 在手机上使用 Root 权限开启一个可访问整个手机目录的http服务: 13 | 14 | 1. 前往 [F-Droid](https://f-droid.org/) 下载并安装 [Termux](https://f-droid.org/zh_Hans/packages/com.termux/) 。 15 | 16 | 2. 使用下列两个命令,更新 Termux 的软件源和软件 17 | ```bash 18 | pkg update 19 | pkg upgrade 20 | ``` 21 | 22 | 3. 安装 Python 23 | ```bash 24 | pkg install python 25 | ``` 26 | 27 | 4. 获取Root权限 28 | ```bash 29 | su 30 | ``` 31 | 32 | 5. 设置环境变量或进入指定目录(二选一) 33 | 1. 设置环境变量(推荐) 34 | 执行如下指令: 35 | ```bash 36 | export PATH=/data/data/com.termux/files/usr/bin:$PATH 37 | ``` 38 | 2. 进入指定路径(不推荐) 39 | 路径为 `/data/data/com.termux/files/usr/bin` 。不推荐方法不给出相应指令,请自行解决。 40 | 41 | 6. 查看局域网地址 42 | 1. 在 Termux 内查看 43 | 执行 `ifconfig` 。可能会有多个地址,请一一尝试。 44 | 2. 在 系统设置 查看 45 | 如果使用的是 Wi-Fi ,则可查看 Wi-Fi 的详细信息以找到手机的局域网地址。 46 | 7. 启动 http 服务 47 | 1. 若你设置了环境变量,则直接执行如下命令 48 | ```bash 49 | python -m http.server -d / 50 | ``` 51 | 2. 若你进入指定文件夹:该方法不推荐,请自行修改命令。 52 | 53 | 3. 克隆或下载此项目的源代码。 54 | 55 | 4. 安装 `Python 3` 56 | 57 | 5. 安装项目依赖 58 | ```bash 59 | pip install -r requirements.txt 60 | ``` 61 | 62 | 6. 运行 63 | ```bash 64 | python main.py 65 | ``` 66 | 并按提示输入你获取到的手机的IP地址。 67 | 68 | # 更新日志 69 | 70 | - 2022.11.15 71 | 1.初版上线 -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | from server import * 3 | 4 | 5 | def main(): 6 | if not os.path.exists('./db'): 7 | os.makedirs('./db') 8 | if not os.path.exists('./song'): 9 | os.makedirs('./song') 10 | print("""1.qq音乐-11.11.0.10 11 | 2.酷狗音乐-11.3.8 12 | 3.酷狗音乐概念版-3.0.0 13 | 4.网易云音乐-9.0.25 14 | 5.汽水音乐-6.0.0 15 | 6.学浪-6.7.0 16 | 7.悟饭游戏厅-5.0.4.9""") 17 | input_type = input('请输入导出类型:') 18 | if input_type == '1': 19 | qqmusic.main(ip) 20 | elif input_type == '2': 21 | kugoumusic.main(ip) 22 | elif input_type == '3': 23 | kugoulitemusic.main(ip) 24 | elif input_type == '4': 25 | wangyiyunmusic.main(ip) 26 | elif input_type == '5': 27 | qishuimusic.main(ip) 28 | elif input_type == '6': 29 | xuelang.main(ip) 30 | elif input_type == '7': 31 | wufanyouxiting.main(ip) 32 | else: 33 | input('选择类型错误') 34 | 35 | 36 | if __name__ == '__main__': 37 | ip_addr = str(input("请输入手机的IP地址:\n")) 38 | ip = ip_addr + ":8000" 39 | main() 40 | -------------------------------------------------------------------------------- /mysql3/mysql3.py: -------------------------------------------------------------------------------- 1 | 2 | import sqlite3 3 | 4 | 5 | class SQL3(object): 6 | 7 | def __init__(self, file_path): 8 | self.conn = sqlite3.connect(file_path) 9 | # self.conn.execute('PRAGMA journal_mode = OFF') 10 | 11 | def query(self, sql): 12 | cur = self.conn.cursor() 13 | cur.execute(sql) 14 | values = cur.fetchall() 15 | cur.close() 16 | return values 17 | 18 | def search_text(self, text): 19 | table_list = set() 20 | cur = self.conn.cursor() 21 | cur.execute("select name from sqlite_master where type='table' order by name;") 22 | values = cur.fetchall() 23 | for table_name, *_ in values: 24 | cur.execute("select * from " + table_name + ';') 25 | values2 = cur.fetchall() 26 | for each in values2: 27 | for item in each: 28 | if text in str(item): 29 | table_list.add(table_name) 30 | break 31 | cur.close() 32 | return table_list 33 | 34 | def __del__(self): 35 | self.conn.close() 36 | -------------------------------------------------------------------------------- /requirement.txt: -------------------------------------------------------------------------------- 1 | requests 2 | pycryptodome 3 | mutagen -------------------------------------------------------------------------------- /server/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from server import qqmusic, kugoumusic, kugoulitemusic, wangyiyunmusic, qishuimusic, xuelang, wufanyouxiting 3 | 4 | 5 | __all__ = [ 6 | 'qqmusic', 7 | 'kugoumusic', 8 | 'kugoulitemusic', 9 | 'wangyiyunmusic', 10 | 'qishuimusic', 11 | 'xuelang', 12 | 'wufanyouxiting' 13 | ] 14 | -------------------------------------------------------------------------------- /server/kugoulitemusic.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import traceback 4 | import requests 5 | from mysql3.mysql3 import SQL3 6 | from utils.os import use_song_list, android_exists, android_download 7 | 8 | 9 | def export_file(song_name, song_suffix, file_url): 10 | if os.path.exists('./song/' + song_name + '.' + song_suffix): 11 | print('文件已存在:./song/' + song_name + '.' + song_suffix) 12 | return 13 | response = requests.get(file_url).content 14 | with open('./song/' + song_name + '.' + song_suffix, 'wb') as f: 15 | f.write(response) 16 | print('导出成功:./song/' + song_name + '.' + song_suffix) 17 | 18 | 19 | def main(ip): 20 | # 首先下载db数据 21 | android_download(ip, '/data/data/com.kugou.android.lite/databases/kugou_music_phone_v7.db', './db/kugou_music_phone_v7_lite.db') 22 | if android_exists(ip, '/data/data/com.kugou.android.lite/databases/kugou_music_phone_v7.db-wal'): 23 | android_download(ip, '/data/data/com.kugou.android.lite/databases/kugou_music_phone_v7.db-wal', './db/kugou_music_phone_v7_lite.db-wal') 24 | android_download(ip, '/data/data/com.kugou.android.lite/databases/kugou_music_phone_v7.db-shm', './db/kugou_music_phone_v7_lite.db-shm') 25 | sql3 = SQL3('./db/kugou_music_phone_v7_lite.db') 26 | values = sql3.query('''SELECT downloadurl, temppath FROM file_downloading where temppath like '%.kgm%' or temppath like '%.flac%';''') 27 | song_list = [] 28 | for file_url, file_path in values: 29 | song_name, song_suffix = sql3.query(f'SELECT musicname, extname FROM file where filepath="{file_path}";')[0] 30 | song_list.append([song_name, song_suffix, file_url]) 31 | song_list = use_song_list(song_list) 32 | for song_name, song_suffix, file_url in song_list: 33 | try: 34 | export_file(song_name, song_suffix, file_url) 35 | except: 36 | print(traceback.format_exc()) 37 | -------------------------------------------------------------------------------- /server/kugoumusic.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import traceback 4 | import requests 5 | from mysql3.mysql3 import SQL3 6 | from utils.os import use_song_list, android_exists, android_download 7 | 8 | 9 | def export_file(song_name, song_suffix, file_url): 10 | if os.path.exists('./song/' + song_name + '.' + song_suffix): 11 | print('文件已存在:./song/' + song_name + '.' + song_suffix) 12 | return 13 | response = requests.get(file_url).content 14 | with open('./song/' + song_name + '.' + song_suffix, 'wb') as f: 15 | f.write(response) 16 | print('导出成功:./song/' + song_name + '.' + song_suffix) 17 | 18 | 19 | def main(ip): 20 | # 首先下载db数据 21 | android_download(ip, '/data/data/com.kugou.android/databases/kugou_music_phone_v7.db', './db/kugou_music_phone_v7.db') 22 | if android_exists(ip, '/data/data/com.kugou.android/databases/kugou_music_phone_v7.db-wal'): 23 | android_download(ip, '/data/data/com.kugou.android/databases/kugou_music_phone_v7.db-wal', './db/kugou_music_phone_v7.db-wal') 24 | android_download(ip, '/data/data/com.kugou.android/databases/kugou_music_phone_v7.db-shm', './db/kugou_music_phone_v7.db-shm') 25 | sql3 = SQL3('./db/kugou_music_phone_v7.db') 26 | values = sql3.query('''SELECT downloadurl, temppath FROM file_downloading where temppath like '%.kgm%' or temppath like '%.flac%';''') 27 | song_list = [] 28 | for file_url, file_path in values: 29 | song_name, song_suffix = sql3.query(f'SELECT musicname, extname FROM file where filepath="{file_path}";')[0] 30 | song_list.append([song_name, song_suffix, file_url]) 31 | song_list = use_song_list(song_list) 32 | for song_name, song_suffix, file_url in song_list: 33 | try: 34 | export_file(song_name, song_suffix, file_url) 35 | except: 36 | print(traceback.format_exc()) 37 | -------------------------------------------------------------------------------- /server/qishuimusic.py: -------------------------------------------------------------------------------- 1 | 2 | import base64 3 | import re 4 | import os 5 | import traceback 6 | import requests 7 | import json 8 | from mysql3.mysql3 import SQL3 9 | from utils.util import decrypt_spade_a 10 | from utils.os import android_listdir, use_song_list, safe_title 11 | 12 | 13 | def export_file(song_name, song_suffix, song_url, song_encrypt, song_spadea): 14 | song_data = requests.get(song_url).content 15 | with open('./song/' + song_name + '.' + song_suffix, 'wb') as f: 16 | f.write(song_data) 17 | if song_encrypt: 18 | key = decrypt_spade_a(song_spadea) 19 | os.rename('./song/' + song_name + '.' + song_suffix, './song/' + song_name + 'en.' + song_suffix) 20 | os.system('ffmpeg.exe -decryption_key ' + key + ' -i "' + './song/' + song_name + 'en.' + song_suffix + '" -c copy -copyts -movflags +faststart -movflags +use_metadata_tags -y "' + './song/' + song_name + '.' + song_suffix + '"') 21 | os.remove('./song/' + song_name + 'en.' + song_suffix) 22 | 23 | 24 | def main(ip): 25 | # 首先下载db数据 26 | file_path = '/data/data/com.luna.music/databases' 27 | for file_name in android_listdir(ip, file_path): 28 | if file_name.startswith('download_') and file_name.endswith('.db') and len(re.findall('\d+', file_name)[0]) > 2: 29 | url = f'http://{ip}{file_path}/{file_name}' 30 | response = requests.get(url).content 31 | with open('./db/download.db', 'wb') as f: 32 | f.write(response) 33 | break 34 | sql3 = SQL3('./db/download.db') 35 | values = sql3.query('''SELECT * FROM downloaded_track;''') 36 | song_list = [] 37 | for item in values: 38 | item_info = json.loads(item[1]) 39 | song_name = item_info['name'] + '-' + ','.join([each['name'] for each in item_info['artists']]) 40 | item_info = json.loads(item[3]) 41 | song_suffix = item_info['m_codec_type'] 42 | song_url = item_info['m_main_url'] 43 | song_spadea = item_info.get('m_spadea') 44 | song_encrypt = item_info['m_encrypt'] 45 | song_list.append([song_name, song_suffix, song_url, song_encrypt, song_spadea]) 46 | song_list = use_song_list(song_list) 47 | for song_name, song_suffix, song_url, song_encrypt, song_spadea in song_list: 48 | try: 49 | export_file(safe_title(song_name), song_suffix, song_url, song_encrypt, song_spadea) 50 | except: 51 | print(traceback.format_exc()) 52 | 53 | 54 | if __name__ == '__main__': 55 | print('5011945309e6465581fd0d6971c0e002' == decrypt_spade_a('kLwe+la9BvZLvwzvcaI97HOnPO9ClAroRbgK8Xa5CPBCuAqDgw==')) 56 | -------------------------------------------------------------------------------- /server/qqmusic.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import traceback 4 | import re 5 | import base64 6 | import struct 7 | from mysql3.mysql3 import SQL3 8 | from utils.tc_tea import tc_tea_decrypt 9 | from utils.os import android_listdir, use_song_list 10 | 11 | 12 | FIRST_SEGMENT_SIZE = 0x80 13 | SEGMENT_SIZE = 0x1400 14 | music_suffix = { 15 | 'flac': 'flac', 16 | 'mflac0': 'flac', 17 | 'qmcflac': 'flac' 18 | } 19 | 20 | 21 | def createInstWidthEKey(ekey): 22 | 23 | def isEncV2(key): 24 | return key.startswith(b'QQMusic EncV2,Key:') 25 | 26 | def DecryptV2Key(key): 27 | MixKey1 = [0x33, 0x38, 0x36, 0x5A, 0x4A, 0x59, 0x21, 0x40, 0x23, 0x2A, 0x24, 0x25, 0x5E, 0x26, 0x29, 0x28] 28 | MixKey2 = [0x2A, 0x2A, 0x23, 0x21, 0x28, 0x23, 0x24, 0x25, 0x26, 0x5E, 0x61, 0x31, 0x63, 0x5A, 0x2C, 0x54] 29 | decode_key_1 = tc_tea_decrypt(bytes(MixKey1), key[18:]) 30 | decode_key_2 = tc_tea_decrypt(bytes(MixKey2), decode_key_1) 31 | return base64.b64decode(decode_key_2) 32 | 33 | simple_key_buf = bytes.fromhex('695646382b20150b') 34 | if isEncV2(ekey): 35 | dec_ekey = DecryptV2Key(ekey) 36 | else: 37 | dec_ekey = ekey 38 | 39 | tea_key = list() 40 | for i in range(0, 16, 2): 41 | tea_key.append(simple_key_buf[i // 2]) 42 | tea_key.append(dec_ekey[i // 2]) 43 | 44 | return dec_ekey[:8] + tc_tea_decrypt(bytes(tea_key), dec_ekey[8:]) 45 | 46 | 47 | def rotate(value, bits): 48 | rotate = (bits + 4) % 8 49 | left = value << rotate 50 | right = value >> rotate 51 | return (left | right) & 0xFF 52 | 53 | 54 | def mapL(offset, key): 55 | if offset > 0x7FFF: 56 | offset %= 0x7FFF 57 | index = (offset * offset + 71214) % len(key) 58 | value = key[index] 59 | return rotate(value, index & 0b0111) 60 | 61 | 62 | def EncFirstSegment(offset, buf, len_size, key, key_hash, decrypted_data): 63 | for i in range(len_size): 64 | next_key = key[offset % len(key)] 65 | decrypted_data.append(buf[i] ^ key[GetSegmentKey(offset, next_key, key_hash) % len(key)]) 66 | offset += 1 67 | 68 | 69 | def GetSegmentKey(offset, next_key, key_hash): 70 | return int(key_hash / ((offset + 1) * next_key) * 100) 71 | 72 | 73 | def EncASegment(S, offset, buf, len_segment, key, key_hash, decrypt_data): 74 | S = S[:] 75 | segment_id = int(offset / SEGMENT_SIZE) & 0x1FF 76 | skip_len = GetSegmentKey(int(offset / SEGMENT_SIZE), key[segment_id], key_hash) & 0x1FF 77 | skip_len += offset % SEGMENT_SIZE 78 | N = len(key) 79 | j = 0 80 | k = 0 81 | for i in range(skip_len): 82 | j = (j + 1) % N 83 | k = (S[j] + k) % N 84 | S[j], S[k] = S[k], S[j] 85 | for i in range(len_segment): 86 | j = (j + 1) % N 87 | k = (S[j] + k) % N 88 | S[j], S[k] = S[k], S[j] 89 | decrypt_data.append(buf[i] ^ S[(S[j] + S[k]) % N]) 90 | 91 | 92 | def decrypt_file(song_file_buffer, key, out_file): 93 | decrypted_file = bytearray() 94 | if len(key) < 300: 95 | for i in range(len(song_file_buffer)): 96 | decrypted_file.append(song_file_buffer[i] ^ mapL(i, key)) 97 | out_file.write(decrypted_file) 98 | else: 99 | N = len(key) 100 | S = [i & 0xFF for i in range(N)] 101 | j = 0 102 | for i in range(N): 103 | j = (S[i] + j + key[i % N]) % N 104 | S[i], S[j] = S[j], S[i] 105 | key_hash = 1 106 | for i in range(N): 107 | value = key[i] 108 | # // ignore if key char is '\x00' 109 | if not value: 110 | continue 111 | next_hash = (key_hash * value) & 0xFFFFFFFF 112 | if next_hash == 0 or next_hash <= key_hash: 113 | break 114 | key_hash = next_hash 115 | offset = 0 116 | buf = song_file_buffer 117 | size = len(song_file_buffer) 118 | len_size = size 119 | len_segment = min(size, FIRST_SEGMENT_SIZE) 120 | EncFirstSegment(offset, buf, len_segment, key, key_hash, decrypted_file) 121 | len_size -= len_segment 122 | buf = buf[len_segment:] 123 | offset += len_segment 124 | if offset % SEGMENT_SIZE != 0: 125 | len_segment = min(SEGMENT_SIZE - (offset % SEGMENT_SIZE), len_size) 126 | EncASegment(S, offset, buf, len_segment, key, key_hash, decrypted_file) 127 | len_size -= len_segment 128 | buf = buf[len_segment:] 129 | offset += len_segment 130 | while len_size > SEGMENT_SIZE: 131 | len_segment = min(SEGMENT_SIZE, len_size) 132 | EncASegment(S, offset, buf, len_segment, key, key_hash, decrypted_file) 133 | len_size -= len_segment 134 | buf = buf[len_segment:] 135 | offset += len_segment 136 | if len_size > 0: 137 | EncASegment(S, offset, buf, len_size, key, key_hash, decrypted_file) 138 | out_file.write(decrypted_file) 139 | 140 | 141 | def decrypt_file_from_qmcflac(song_file_buffer, out_file): 142 | decrypted_file = bytearray() 143 | staticCipherBox = [119, 72, 50, 115, 222, 242, 192, 200, 149, 236, 48, 178, 81, 195, 225, 160, 158, 230, 157, 207, 144 | 250, 127, 20, 209, 206, 184, 220, 195, 74, 103, 147, 214, 40, 194, 145, 112, 202, 141, 162, 164, 145 | 240, 8, 97, 144, 126, 111, 162, 224, 235, 174, 62, 182, 103, 199, 146, 244, 145, 181, 246, 108, 146 | 94, 132, 64, 247, 243, 27, 2, 127, 213, 171, 65, 137, 40, 244, 37, 204, 82, 17, 173, 67, 104, 147 | 166, 65, 139, 132, 181, 255, 44, 146, 74, 38, 216, 71, 106, 124, 149, 97, 204, 230, 203, 187, 63, 148 | 71, 88, 137, 117, 195, 117, 161, 217, 175, 204, 8, 115, 23, 220, 170, 154, 162, 22, 65, 216, 162, 149 | 6, 198, 139, 252, 102, 52, 159, 207, 24, 35, 160, 10, 116, 231, 43, 39, 112, 146, 233, 175, 55, 150 | 230, 140, 167, 188, 98, 101, 156, 194, 8, 201, 136, 179, 243, 67, 172, 116, 44, 15, 212, 175, 151 | 161, 195, 1, 100, 149, 78, 72, 159, 244, 53, 120, 149, 122, 57, 214, 106, 160, 109, 64, 232, 79, 152 | 168, 239, 17, 29, 243, 27, 63, 63, 7, 221, 111, 91, 25, 48, 25, 251, 239, 14, 55, 240, 14, 205, 153 | 22, 73, 254, 83, 71, 19, 26, 189, 164, 241, 64, 25, 96, 14, 237, 104, 9, 6, 95, 77, 207, 61, 26, 154 | 254, 32, 119, 228, 217, 218, 249, 164, 43, 118, 28, 113, 219, 0, 188, 253, 12, 108, 165, 71, 247, 155 | 246, 0, 121, 74, 17] 156 | 157 | def getMask(t): 158 | if t > 32767: 159 | t %= 32767 160 | return staticCipherBox[t * t + 27 & 255] 161 | 162 | for i in range(len(song_file_buffer)): 163 | decrypted_file.append(song_file_buffer[i] ^ getMask(i)) 164 | 165 | out_file.write(decrypted_file) 166 | 167 | 168 | def export_file(song_name, song_suffix, file_name, file_path, sql3, ip): 169 | if song_suffix in music_suffix.keys(): 170 | song_name = song_name + '.' + music_suffix[song_suffix] 171 | else: 172 | print('未支持的文件后缀: ' + song_suffix) 173 | return 174 | if os.path.exists('./song/' + song_name): 175 | return 176 | out_file = open('./song/' + song_name, 'wb') 177 | 178 | song_file_buffer = requests.get(f'http://{ip}{file_path}/{file_name}').content 179 | if song_suffix == 'flac': 180 | out_file.write(song_file_buffer) 181 | elif song_suffix == 'mflac0': 182 | assert song_file_buffer[-4:] == b'STag' 183 | song_file_buffer = song_file_buffer[:(struct.unpack('>i', song_file_buffer[-8:-4])[0] + 8) * -1] 184 | # 从数据库获取ekey 185 | ekey = sql3.query(f'SELECT ekey FROM audio_file_ekey_table where file_path="{file_path}/{file_name}";')[0][0] 186 | key = createInstWidthEKey(base64.b64decode(ekey)) 187 | decrypt_file(song_file_buffer, key, out_file) 188 | elif song_suffix == 'qmcflac': 189 | decrypt_file_from_qmcflac(song_file_buffer, out_file) 190 | 191 | out_file.close() 192 | 193 | 194 | def main(ip): 195 | # 首先下载db数据 196 | url = f'http://{ip}/data/data/com.tencent.qqmusic/databases/player_process_db' 197 | response = requests.get(url).content 198 | with open('./db/player_process_db', 'wb') as f: 199 | f.write(response) 200 | sql3 = SQL3('./db/player_process_db') 201 | values = sql3.query('''SELECT * FROM audio_file_ekey_table limit 1;''') 202 | if values: 203 | file_path = os.path.dirname(values[0][0]) 204 | else: 205 | file_path = '/storage/emulated/0/qqmusic/song' 206 | song_list = [] 207 | for file_name in android_listdir(ip, file_path): 208 | song_name, song_suffix = re.search('(.+?) \[.+?\]\.(.+)', file_name).groups() 209 | song_list.append([song_name, song_suffix, file_name]) 210 | song_list = use_song_list(song_list) 211 | for song_name, song_suffix, file_name in song_list: 212 | try: 213 | export_file(song_name, song_suffix, file_name, file_path, sql3, ip) 214 | except: 215 | print(traceback.format_exc()) 216 | -------------------------------------------------------------------------------- /server/wangyiyunmusic.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import traceback 4 | import requests 5 | import struct 6 | import json 7 | import base64 8 | from mutagen.flac import FLAC 9 | from Crypto.Cipher import AES 10 | from Crypto.Util.Padding import unpad 11 | from utils.os import android_listdir, use_song_list 12 | 13 | 14 | def getKeyData(dataView, offset): 15 | keyLen = struct.unpack(").+(?=)', re.findall('(?<=)', response, re.S)[0]): 26 | file_name = re.sub('<.+?>', '', li) 27 | if file_name[-1] == '/': 28 | file_name = file_name[:-1] 29 | file_list.append(file_name) 30 | return file_list 31 | 32 | 33 | def use_song_list(song_list): 34 | i = 0 35 | return_list = [] 36 | print('序号 歌曲') 37 | for each_song in song_list: 38 | print(str(i) + ' ' + each_song[0]) 39 | i += 1 40 | xuhaolist = input('请输入要下载视频的序号:') 41 | if xuhaolist: 42 | if '@' in xuhaolist: 43 | return [] 44 | if ',' in xuhaolist: 45 | for each1 in xuhaolist.split(','): 46 | if '-' in each1: 47 | sta, ena = each1.split('-') 48 | for each2 in range(int(sta), int(ena) + 1): 49 | return_list.append(song_list[each2]) 50 | else: 51 | return_list.append(song_list[int(each1)]) 52 | else: 53 | if '-' in xuhaolist: 54 | sta, ena = xuhaolist.split('-') 55 | for each in range(int(sta), int(ena) + 1): 56 | return_list.append(song_list[each]) 57 | else: 58 | return_list.append(song_list[int(xuhaolist)]) 59 | print('你选择的是如下歌曲') 60 | i = 1 61 | for each in return_list: 62 | print(str(i) + '. ' + each[0]) 63 | i += 1 64 | return return_list 65 | else: 66 | print('你选择的是全部歌曲') 67 | return song_list 68 | 69 | 70 | def safe_title(title): 71 | title = re.sub('[\\\/\:\*\?\"\<\>\|]', '', title) 72 | for i in ['\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', '\x09', '\x0a', '\x0b', '\x0c', 73 | '\x0d', '\x0e', '\x0f', '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', 74 | '\x1a', '\x1b', '\x1c', '\x1d', '\x1e', '\x1f', '\x7F']: 75 | if i in title: 76 | title = title.replace(i, '') 77 | return title 78 | -------------------------------------------------------------------------------- /utils/tc_tea.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import base64 3 | import ctypes 4 | import random 5 | 6 | DELTA = 0x9e3779b9 7 | ROUNDS = 16 8 | LOG_ROUNDS = 4 9 | SALT_LEN = 2 10 | ZERO_LEN = 7 11 | 12 | 13 | class Size_t(object): 14 | value = 0 15 | 16 | def __init__(self, value): 17 | self.value = value 18 | 19 | 20 | def encrypt(key: bytes, sIn: bytes, iLength: int, buffer: bytearray) -> None: 21 | outlen: Size_t = Size_t(oi_symmetry_encrypt2_len(iLength)) 22 | 23 | oi_symmetry_encrypt2(sIn, iLength, key, buffer, outlen) 24 | 25 | while len(buffer) > outlen.value: 26 | buffer.pop() 27 | 28 | 29 | def decrypt(key: bytes, sIn: bytes, iLength: int, buffer: bytearray) -> bool: 30 | outlen: Size_t = Size_t(iLength) 31 | 32 | if not oi_symmetry_decrypt2(sIn, iLength, key, buffer, outlen): 33 | return False 34 | 35 | while len(buffer) > outlen.value: 36 | buffer.pop() 37 | return True 38 | 39 | 40 | # pOutBuffer、pInBuffer均为8byte, pKey为16byte 41 | def TeaEncryptECB(pInBuf: bytes, pKey: bytes, pOutBuf: bytearray) -> None: 42 | k = list() 43 | pOutBuf.clear() 44 | # plain-text is TCP/IP-endian 45 | # GetBlockBigEndian(in, y, z) 46 | y, z = struct.unpack("!II", pInBuf[:8]) 47 | 48 | # TCP/IP network byte order (which is big-endian) 49 | 50 | for i in struct.unpack("!IIII", pKey): 51 | # now key is TCP/IP-endian 52 | k.append(i) 53 | 54 | sum = 0 55 | for i in range(ROUNDS): 56 | sum += DELTA 57 | sum = ctypes.c_uint32(sum).value 58 | y += ((z << 4) + k[0]) ^ (z + sum) ^ ((z >> 5) + k[1]) 59 | y = ctypes.c_uint32(y).value 60 | z += ((y << 4) + k[2]) ^ (y + sum) ^ ((y >> 5) + k[3]) 61 | z = ctypes.c_uint32(z).value 62 | 63 | for i in struct.pack("!II", y, z): 64 | pOutBuf.append(i) 65 | 66 | # now encrypted buf is TCP/IP-endian 67 | 68 | 69 | # pOutBuffer、pInBuffer均为8byte, pKey为16byte 70 | def TeaDecryptECB(pInBuf: bytes, pKey: bytes, pOutBuf: bytearray) -> None: 71 | k = list() 72 | pOutBuf.clear() 73 | # now encrypted buf is TCP/IP-endian 74 | # TCP/IP network byte order (which is big-endian). 75 | y, z = struct.unpack("!II", pInBuf[:8]) 76 | 77 | for i in struct.unpack("!IIII", pKey): 78 | # key is TCP/IP-endian; 79 | k.append(i) 80 | 81 | sum = ctypes.c_uint32(DELTA << LOG_ROUNDS).value 82 | for i in range(ROUNDS): 83 | z -= ((y << 4) + k[2]) ^ (y + sum) ^ ((y >> 5) + k[3]) 84 | z = ctypes.c_uint32(z).value 85 | y -= ((z << 4) + k[0]) ^ (z + sum) ^ ((z >> 5) + k[1]) 86 | y = ctypes.c_uint32(y).value 87 | sum -= DELTA 88 | 89 | for i in struct.pack("!II", y, z): 90 | pOutBuf.append(i) 91 | # now plain-text is TCP/IP-endian; 92 | 93 | 94 | # pKey为16byte 95 | # 输入:nInBufLen为需加密的明文部分(Body)长度 96 | # 输出:返回为加密后的长度(是8byte的倍数) 97 | # TEA加密算法,CBC模式 98 | # 密文格式:PadLen(1byte)+Padding(var,0-7byte)+Salt(2byte)+Body(var byte)+Zero(7byte) 99 | def oi_symmetry_encrypt2_len(nInBufLen: int) -> int: 100 | # nPadSaltBodyZeroLen # PadLen(1byte)+Salt+Body+Zero的长度 101 | # 根据Body长度计算PadLen,最小必需长度必需为8byte的整数倍 102 | nPadSaltBodyZeroLen = nInBufLen # /*Body长度*/ 103 | nPadSaltBodyZeroLen += 1 + SALT_LEN + ZERO_LEN # PadLen(1byte)+Salt(2byte)+Zero(7byte) 104 | nPadlen = nPadSaltBodyZeroLen % 8 105 | if nPadlen: # len=nSaltBodyZeroLen%8 106 | # 模8余0需补0,余1补7,余2补6,...,余7补1 107 | nPadlen = 8 - nPadlen 108 | return nPadSaltBodyZeroLen + nPadlen 109 | 110 | 111 | # pKey为16byte 112 | # 输入:pInBuf为需加密的明文部分(Body),nInBufLen为pInBuf长度 113 | # 输出:pOutBuf为密文格式,pOutBufLen为pOutBuf的长度是8byte的倍数 114 | # TEA加密算法,CBC模式 115 | # 密文格式:PadLen(1byte)+Padding(var,0-7byte)+Salt(2byte)+Body(var byte)+Zero(7byte) 116 | def oi_symmetry_encrypt2(pInBuf: bytes, nInBufLen: int, pKey: bytes, pOutBuf: bytearray, pOutBufLen: Size_t) -> None: 117 | # /*PadLen(1byte)+Salt+Body+Zero的长度*/ 118 | # 根据Body长度计算PadLen,最小必需长度必需为8byte的整数倍 119 | nPadSaltBodyZeroLen = nInBufLen # Body长度 120 | nPadSaltBodyZeroLen = nPadSaltBodyZeroLen + 1 + SALT_LEN + ZERO_LEN # PadLen(1byte)+Salt(2byte)+Zero(7byte) 121 | nPadlen = nPadSaltBodyZeroLen % 8 122 | if nPadlen: # len=nSaltBodyZeroLen%8 123 | # 模8余0需补0,余1补7,余2补6,...,余7补1 124 | nPadlen = 8 - nPadlen 125 | 126 | # srand( (unsigned)time( NULL ) ); 初始化随机数 127 | # 加密第一块数据(8byte),取前面10byte 128 | src_buf = bytearray([0] * 8) 129 | src_buf[0] = (random.randint(0, 255) & 0xf8) | nPadlen # 最低三位存PadLen,清零 130 | src_i = 1 # src_i指向src_buf下一个位置 131 | 132 | while nPadlen: 133 | src_buf[src_i] = random.randint(0, 255) # Padding 134 | src_i += 1 135 | nPadlen -= 1 136 | 137 | # come here, src_i must <= 8 138 | 139 | iv_plain = bytearray() 140 | for i in range(8): 141 | iv_plain.append(0) 142 | 143 | iv_crypt = bytearray(iv_plain) # make zero iv 144 | 145 | pOutBufLen.value = 0 # init OutBufLen 146 | 147 | i = 1 148 | while i <= SALT_LEN: # Salt(2byte) 149 | if src_i < 8: 150 | src_buf[src_i] = random.randint(0, 255) 151 | src_i += 1 152 | i += 1 # i inc in here 153 | if src_i == 8: 154 | # src_i==8 155 | 156 | for j in range(8): # 加密前异或前8个byte的密文(iv_crypt指向的) 157 | src_buf[j] ^= iv_crypt[j] 158 | 159 | # pOutBuffer、pInBuffer均为8byte, pKey为16byte 160 | # 加密 161 | temp_pOutBuf = bytearray() 162 | TeaEncryptECB(src_buf, pKey, temp_pOutBuf) 163 | 164 | for j in range(8): # 加密后异或前8个byte的明文(iv_plain指向的) 165 | temp_pOutBuf[j] ^= iv_plain[j] 166 | 167 | # 保存当前的iv_plain 168 | for j in range(8): 169 | iv_plain[j] = src_buf[j] 170 | 171 | # 更新iv_crypt 172 | src_i = 0 173 | iv_crypt = bytearray(temp_pOutBuf) 174 | pOutBufLen.value += 8 175 | pOutBuf += temp_pOutBuf 176 | 177 | # src_i指向src_buf下一个位置 178 | pInBufIndex = 0 179 | while nInBufLen: 180 | if src_i < 8: 181 | src_buf[src_i] = pInBuf[pInBufIndex] 182 | pInBufIndex += 1 183 | src_i += 1 184 | nInBufLen -= 1 185 | if src_i == 8: 186 | # src_i==8 187 | for j in range(8): # 加密前异或前8个byte的密文(iv_crypt指向的) 188 | src_buf[j] ^= iv_crypt[j] 189 | # pOutBuffer、pInBuffer均为8byte, pKey为16byte 190 | temp_pOutBuf = bytearray() 191 | TeaEncryptECB(src_buf, pKey, temp_pOutBuf) 192 | 193 | for j in range(8): # 加密后异或前8个byte的明文(iv_plain指向的) 194 | temp_pOutBuf[j] ^= iv_plain[j] 195 | 196 | # 保存当前的iv_plain 197 | for j in range(8): 198 | iv_plain[j] = src_buf[j] 199 | 200 | src_i = 0 201 | iv_crypt = bytearray(temp_pOutBuf) 202 | pOutBufLen.value += 8 203 | pOutBuf += temp_pOutBuf 204 | 205 | # src_i指向src_buf下一个位置 206 | i = 1 207 | while i <= ZERO_LEN: 208 | if src_i < 8: 209 | src_buf[src_i] = 0 210 | src_i += 1 211 | i += 1 # i inc in here 212 | if src_i == 8: 213 | # src_i==8 214 | 215 | for j in range(8): # 加密前异或前8个byte的密文(iv_crypt指向的) 216 | src_buf[j] ^= iv_crypt[j] 217 | 218 | # pOutBuffer、pInBuffer均为8byte, pKey为16byte 219 | temp_pOutBuf = bytearray() 220 | TeaEncryptECB(src_buf, pKey, temp_pOutBuf) 221 | 222 | for j in range(8): # 加密后异或前8个byte的明文(iv_plain指向的) 223 | temp_pOutBuf[j] ^= iv_plain[j] 224 | 225 | # 保存当前的iv_plain 226 | for j in range(8): 227 | iv_plain[j] = src_buf[j] 228 | 229 | src_i = 0 230 | iv_crypt = temp_pOutBuf 231 | pOutBufLen.value += 8 232 | pOutBuf += temp_pOutBuf 233 | 234 | 235 | # pKey为16byte 236 | # 输入: pInBuf为密文格式, nInBufLen为pInBuf的长度是8byte的倍数; 237 | # *pOutBufLen为接收缓冲区的长度 238 | # 特别注意 * pOutBufLen应预置接收缓冲区的长度! 239 | # 输出: pOutBuf为明文(Body), pOutBufLen为pOutBuf的长度, 至少应预留nInBufLen - 10; 240 | # 返回值: 如果格式正确返回true; 241 | # TEA解密算法, CBC模式 242 | # 密文格式: PadLen(1byte)+Padding(var, 0 - 7byte)+Salt(2byte)+Body(varbyte)+Zero(7byte) 243 | def oi_symmetry_decrypt2(pInBuf: bytes, nInBufLen: int, pKey: bytes, pOutBuf: bytearray, pOutBufLen: Size_t) -> bool: 244 | dest_buf = bytearray() 245 | zero_buf = bytearray() 246 | 247 | # const char * pInBufBoundary; 248 | nBufPos = 0 249 | 250 | if (nInBufLen % 8) or (nInBufLen < 16): 251 | return False 252 | 253 | TeaDecryptECB(pInBuf, pKey, dest_buf) 254 | 255 | nPadLen = dest_buf[0] & 0x7 # 只要最低三位 256 | 257 | # 密文格式: PadLen(1byte)+Padding(var, 0-7byte)+Salt(2byte)+Body(var byte)+Zero(7byte) 258 | i = nInBufLen - 1 # PadLen(1byte) 259 | i = i - nPadLen - SALT_LEN - ZERO_LEN # 明文长度 260 | 261 | if (pOutBufLen.value < i) or (i < 0): 262 | return False 263 | 264 | pOutBufLen.value = i 265 | 266 | # pInBufBoundary = pInBuf + nInBufLen; 输入缓冲区的边界,下面不能pInBuf >= pInBufBoundary 267 | 268 | for i in range(8): 269 | zero_buf.append(0) 270 | 271 | iv_pre_crypt = bytearray(zero_buf) 272 | iv_cur_crypt = bytearray(pInBuf) # init iv 273 | 274 | pInBuf = pInBuf[8:] 275 | nBufPos += 8 276 | dest_i = 1 # dest_i指向dest_buf下一个位置 277 | 278 | # 把Padding滤掉 279 | dest_i += nPadLen 280 | 281 | # dest_i must <= 8 282 | 283 | # 把Salt滤掉 284 | i = 1 285 | while i <= SALT_LEN: 286 | if dest_i < 8: 287 | dest_i += 1 288 | i += 1 289 | elif dest_i == 8: 290 | # 解开一个新的加密块 291 | # 改变前一个加密块的指针 292 | iv_pre_crypt = bytearray(iv_cur_crypt) 293 | iv_cur_crypt = bytearray(pInBuf) 294 | 295 | # 异或前一块明文(在dest_buf[]中) 296 | for j in range(8): 297 | if nBufPos + j >= nInBufLen: 298 | return False 299 | dest_buf[j] ^= pInBuf[j] 300 | 301 | # dest_i == 8 302 | TeaDecryptECB(bytes(dest_buf), pKey, dest_buf) 303 | 304 | # 在取出的时候才异或前一块密文(iv_pre_crypt) 305 | 306 | pInBuf = pInBuf[8:] 307 | nBufPos += 8 308 | dest_i = 0 # dest_i指向dest_buf下一个位置 309 | 310 | # 还原明文 311 | nPlainLen = pOutBufLen.value 312 | while nPlainLen: 313 | if dest_i < 8: 314 | pOutBuf.append(dest_buf[dest_i] ^ iv_pre_crypt[dest_i]) 315 | dest_i += 1 316 | nPlainLen -= 1 317 | elif dest_i == 8: 318 | # dest_i == 8 319 | # 改变前一个加密块的指针 320 | iv_pre_crypt = bytearray(iv_cur_crypt) 321 | iv_cur_crypt = bytearray(pInBuf) 322 | 323 | # 解开一个新的加密块 324 | # 异或前一块明文(在dest_buf[]中) 325 | for j in range(8): 326 | if nBufPos + j >= nInBufLen: 327 | return False 328 | dest_buf[j] ^= pInBuf[j] 329 | TeaDecryptECB(bytes(dest_buf), pKey, dest_buf) 330 | # 在取出的时候才异或前一块密文(iv_pre_crypt) 331 | pInBuf = pInBuf[8:] 332 | nBufPos += 8 333 | dest_i = 0 # dest_i指向dest_buf下一个位置 334 | 335 | # 校验Zero 336 | i = 1 337 | while i <= ZERO_LEN: 338 | if dest_i < 8: 339 | if dest_buf[dest_i] ^ iv_pre_crypt[dest_i]: 340 | return False 341 | dest_i += 1 342 | i += 1 343 | elif dest_i == 8: 344 | # 改变前一个加密块的指针 345 | iv_pre_crypt = bytearray(iv_cur_crypt) 346 | iv_cur_crypt = bytearray(pInBuf) 347 | 348 | # 解开一个新的加密块 349 | # 异或前一块明文(在dest_buf[]中) 350 | for j in range(8): 351 | if nBufPos + j >= nInBufLen: 352 | return False 353 | dest_buf[j] ^= pInBuf[j] 354 | 355 | TeaDecryptECB(bytes(dest_buf), pKey, dest_buf) 356 | 357 | # 在取出的时候才异或前一块密文(iv_pre_crypt) 358 | pInBuf += 8 359 | nBufPos += 8 360 | dest_i = 0 # dest_i指向dest_buf下一个位置 361 | return True 362 | 363 | 364 | def tc_tea_encrypt(keys: bytes, message: bytes) -> bytes: 365 | # 封装python语法风格 366 | data = bytearray() 367 | encrypt(keys, message, len(message), data) 368 | return bytes(data) 369 | 370 | 371 | def tc_tea_decrypt(keys: bytes, message: bytes) -> bytes: 372 | # 封装python语法风格 373 | data = bytearray() 374 | if decrypt(keys, message, len(message), data): 375 | return bytes(data) 376 | else: 377 | raise Exception('解密失败') 378 | 379 | 380 | if __name__ == '__main__': 381 | key = base64.b64decode('aXVWdEY2OEgrMiA2FTgLbw=='.encode()) 382 | ekey = "dXQ2SDI2OG+Vj36p0kK1UsUfEbSTSUF0LpRSH/fQp6GNloSGsB0OhoG7OIHa2pKPmYgnZhGIbp09sZx8+R+Ges/2BgQ8OnL0yuY3oEtc9AVviDdHjQxayEK5ojleedvA9Qjr9UthKJJMcmAmL17DSieI0WH7FUQo6/OEgrYLoteEYZgajrBJbJu34BEIIMA9rl6FF8ROZR05GtI2eOooiJkNVr/GjdzajzaptHUKZ0S7KfaRnA/MsGfb9C5pjKaZAm0n4eCkBiLUH4PnqvRj2Yt8HKkzUBpIH0n2ENSJs+3Hj0cCSk0arxIckZtygACV8zxNF26j8NuwW+tN/JTC7M6qACFe9k4ph8MwgwvX/AM=" 383 | ekey = base64.b64decode(ekey.encode()) 384 | print(ekey) 385 | decrypt_data = tc_tea_decrypt(key, ekey[8:]) 386 | print('解密成功') 387 | print(decrypt_data.decode()) 388 | decrypt_data = bytes(decrypt_data) 389 | encrypt_data = tc_tea_encrypt(key, decrypt_data) 390 | decrypt_data_2 = tc_tea_decrypt(key, encrypt_data) 391 | assert decrypt_data_2 == decrypt_data 392 | print('加密成功') 393 | 394 | 395 | 396 | 397 | -------------------------------------------------------------------------------- /utils/util.py: -------------------------------------------------------------------------------- 1 | 2 | import base64 3 | 4 | 5 | def popcount(number): 6 | return len(bin(number).split('1')) - 1 7 | 8 | 9 | def decrypt_spade_a(spade): 10 | spade = base64.b64decode(spade.encode()) 11 | slat = spade[0] ^ spade[1] ^ spade[2] 12 | key_len = len(spade) - slat + 47 13 | xor_list = bytes([250, 85]) + spade[1: 1 + key_len] 14 | key = bytearray() 15 | for i in range(key_len): 16 | key.append((xor_list[i] ^ xor_list[i + 2]) - 21 - popcount(i)) 17 | return key[1:-1].decode() 18 | --------------------------------------------------------------------------------