Compare commits

...

235 Commits

Author SHA1 Message Date
ikun0014
3fb2718cdf feat: zzc sign 2025-03-28 16:51:41 +08:00
ikun0014
0677c0b5ad
Merge pull request #99 from luren-dc/main
feat: 刷新失活 qqmusic_key
2025-03-27 16:55:56 +08:00
Luren
2400602aae fix: 修复报错 2025-03-27 04:44:24 +00:00
Luren
77737d18e6 feat: 刷新失活 qqmusic_key 2025-03-27 04:38:10 +00:00
006lp
e13b7e3831
fix: attempt to fix quality level comparison logic in wy module (#98)
Main improvements:
1. Added explicit server response level comparison logic
2. Fixed data field reference error in NCMAPI path
3. Unified quality validation process between official and NCMAPI interfaces
2025-03-22 01:27:21 +08:00
lerdb
c175312307
chore: [release] release v2.0.0
此项目可能长期处于停更状态,但是如果你有好的想法也欢迎提交pr,我会在review后合并
2025-03-18 19:35:00 +08:00
dependabot[bot]
b4049ceb69
build(deps): bump pycryptodome from 3.21.0 to 3.22.0 (#97)
Bumps [pycryptodome](https://github.com/Legrandin/pycryptodome) from 3.21.0 to 3.22.0.
- [Release notes](https://github.com/Legrandin/pycryptodome/releases)
- [Changelog](https://github.com/Legrandin/pycryptodome/blob/master/Changelog.rst)
- [Commits](https://github.com/Legrandin/pycryptodome/compare/v3.21.0...v3.22.0)

---
updated-dependencies:
- dependency-name: pycryptodome
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-18 19:28:00 +08:00
dependabot[bot]
031834d40f
build(deps): bump pygments from 2.18.0 to 2.19.1 (#93)
Bumps [pygments](https://github.com/pygments/pygments) from 2.18.0 to 2.19.1.
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.18.0...2.19.1)

---
updated-dependencies:
- dependency-name: pygments
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-02 20:11:09 +08:00
dependabot[bot]
daf34ace39
Merge pull request #92 from MeoProject/dependabot/pip/ruamel-yaml-0.18.10 2025-01-07 04:31:28 +00:00
dependabot[bot]
3b6ee6e9b2
build(deps): bump ruamel-yaml from 0.18.7 to 0.18.10
Bumps ruamel-yaml from 0.18.7 to 0.18.10.

---
updated-dependencies:
- dependency-name: ruamel-yaml
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-06 23:33:14 +00:00
dependabot[bot]
edaec2a3d2
Merge pull request #91 from MeoProject/dependabot/pip/ruamel-yaml-0.18.7 2024-12-31 02:43:36 +00:00
dependabot[bot]
30c0eaa23e
build(deps): bump ruamel-yaml from 0.18.6 to 0.18.7
Bumps ruamel-yaml from 0.18.6 to 0.18.7.

---
updated-dependencies:
- dependency-name: ruamel-yaml
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-30 23:49:10 +00:00
ikun0014
412f692ba1
Update default_config.py 2024-12-18 19:55:43 +08:00
ikun0014
c84c2acbb9
Update player.py 2024-12-18 19:55:26 +08:00
ikun0014
3e5c1cd94b
你好今年是哪年 2024-12-18 19:55:02 +08:00
ikun0014
3375f0beba
Merge pull request #88 from 006lp/main
Add: More detailed log output
2024-12-17 23:45:43 +08:00
ikun0014
82be199636
Update build.py 2024-12-17 23:09:20 +08:00
006lp
f7069fc6f8
Fix: Map the server-returned level to a standardized value 2024-12-17 20:44:07 +08:00
ikun0014
ed47e928dd
feat: 支持NeteaseCloudMusicApi 2024-12-17 20:37:34 +08:00
006lp
56d19ee671
Add: Use third-party Netease Cloud Music API server to send GET request for the link (temporary workaround) 2024-12-17 19:23:56 +08:00
dependabot[bot]
0433d7a4d1
Merge pull request #87 from MeoProject/dependabot/pip/hiredis-3.1.0 2024-12-10 08:38:46 +00:00
dependabot[bot]
c63beb983f
build(deps): bump hiredis from 3.0.0 to 3.1.0
Bumps [hiredis](https://github.com/redis/hiredis-py) from 3.0.0 to 3.1.0.
- [Release notes](https://github.com/redis/hiredis-py/releases)
- [Changelog](https://github.com/redis/hiredis-py/blob/master/CHANGELOG.md)
- [Commits](https://github.com/redis/hiredis-py/compare/v3.0.0...v3.1.0)

---
updated-dependencies:
- dependency-name: hiredis
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-10 08:36:22 +00:00
dependabot[bot]
6d247dbeac
Merge pull request #86 from MeoProject/dependabot/pip/redis-5.2.1 2024-12-10 08:34:53 +00:00
dependabot[bot]
d4cb252aa4
build(deps): bump redis from 5.2.0 to 5.2.1
Bumps [redis](https://github.com/redis/redis-py) from 5.2.0 to 5.2.1.
- [Release notes](https://github.com/redis/redis-py/releases)
- [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES)
- [Commits](https://github.com/redis/redis-py/compare/v5.2.0...v5.2.1)

---
updated-dependencies:
- dependency-name: redis
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 23:25:52 +00:00
lerdb
1ea53d2ca9
chore: 补两个括号 2024-11-19 23:05:26 +08:00
ikun0014
497f21d2d4
Merge pull request #85 from Folltoshe/main
fix: 修复使用redis缓存的时候过期时间错误
2024-11-17 22:44:52 +08:00
Folltoshe
8e361ff14b deps: 更新依赖 2024-11-17 22:42:12 +08:00
Folltoshe
d3ab5f910d fix: 修复使用redis缓存的时候过期时间错误 2024-11-17 22:34:14 +08:00
ikun0014
6b08683b66
Merge pull request #84 from Folltoshe/main
feat: 支持使用redis缓存数据
2024-11-17 22:03:47 +08:00
Folltoshe
196732ab7d feat: 支持使用redis缓存数据 2024-11-17 21:58:42 +08:00
ikun0014
dcfe8a4472
Merge pull request #83 from Folltoshe/main
chore: 优化启动脚本
2024-11-17 21:29:10 +08:00
Folltoshe
b45c3a7765 fix: 修复报错 2024-11-17 20:58:12 +08:00
Folltoshe
5442e3340b chore: 优化启动脚本 2024-11-17 20:57:16 +08:00
dependabot[bot]
92a69f048d
Merge pull request #82 from MeoProject/dependabot/pip/colorlog-6.9.0 2024-11-05 10:31:32 +00:00
dependabot[bot]
0fd9fe1c3e
build(deps): bump colorlog from 6.8.2 to 6.9.0
Bumps [colorlog](https://github.com/borntyping/python-colorlog) from 6.8.2 to 6.9.0.
- [Release notes](https://github.com/borntyping/python-colorlog/releases)
- [Commits](https://github.com/borntyping/python-colorlog/compare/v6.8.2...v6.9.0)

---
updated-dependencies:
- dependency-name: colorlog
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-04 23:14:51 +00:00
ikun0014
3533a72681
feat: 支持关闭某个平台的服务 2024-10-28 18:07:25 +08:00
dependabot[bot]
88937e4917
build(deps): bump xmltodict from 0.14.1 to 0.14.2 (#80)
Bumps [xmltodict](https://github.com/martinblech/xmltodict) from 0.14.1 to 0.14.2.
- [Changelog](https://github.com/martinblech/xmltodict/blob/master/CHANGELOG.md)
- [Commits](https://github.com/martinblech/xmltodict/compare/v0.14.1...v0.14.2)

---
updated-dependencies:
- dependency-name: xmltodict
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-27 19:33:57 +08:00
ikun0014
6c6ef01681
chore: 统一为refresh_login 2024-10-16 20:49:49 +08:00
dependabot[bot]
f67de8f43b
build(deps): bump aiohttp from 3.10.9 to 3.10.10 (#79)
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.9 to 3.10.10.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.10.9...v3.10.10)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-16 14:50:01 +08:00
dependabot[bot]
7dbafb958c
build(deps): bump xmltodict from 0.13.0 to 0.14.1 (#78)
Bumps [xmltodict](https://github.com/martinblech/xmltodict) from 0.13.0 to 0.14.1.
- [Changelog](https://github.com/martinblech/xmltodict/blob/master/CHANGELOG.md)
- [Commits](https://github.com/martinblech/xmltodict/compare/v0.13.0...v0.14.1)

---
updated-dependencies:
- dependency-name: xmltodict
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-16 09:43:40 +08:00
lerdb
f7988a48b1
chore: 合并log输出 2024-10-10 17:09:10 +08:00
lerdb
52420696f8
fix: 修复一个逻辑漏洞 2024-10-10 17:08:23 +08:00
lerdb
aa1894b66f
chore: [release] v2.0.0.beta-12 2024-10-09 19:45:09 +08:00
lerdb
a177ebb3e6
fix: 生成changelog时未获取到正确的tag 2024-10-09 19:32:00 +08:00
dependabot[bot]
94778abece
build(deps): bump urllib3 from 2.2.1 to 2.2.2 (#76)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.2.1...2.2.2)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-09 14:10:14 +08:00
dependabot[bot]
4709eea052
build(deps): bump certifi from 2024.2.2 to 2024.7.4 (#75)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2024.2.2 to 2024.7.4.
- [Commits](https://github.com/certifi/python-certifi/compare/2024.02.02...2024.07.04)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-09 14:05:45 +08:00
lerdb
2370f73fa8
docs: 更新文档 2024-10-08 22:57:59 +08:00
lerdb
f58d1b2069
chore: 优化代码 2024-10-08 22:22:36 +08:00
dependabot[bot]
b13c7d77ab
build(deps): bump pycryptodome from 3.20.0 to 3.21.0 (#74)
Bumps [pycryptodome](https://github.com/Legrandin/pycryptodome) from 3.20.0 to 3.21.0.
- [Release notes](https://github.com/Legrandin/pycryptodome/releases)
- [Changelog](https://github.com/Legrandin/pycryptodome/blob/master/Changelog.rst)
- [Commits](https://github.com/Legrandin/pycryptodome/compare/v3.20.0...v3.21.0)

---
updated-dependencies:
- dependency-name: pycryptodome
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-08 15:26:33 +08:00
dependabot[bot]
f54b3d38dd
build(deps): bump aiohttp from 3.10.5 to 3.10.9 (#73)
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.5 to 3.10.9.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.10.5...v3.10.9)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-08 15:22:33 +08:00
lerdb
edfc6396d8
fix: 修复默认生成的配置文件换行被转义 2024-10-07 17:37:01 +08:00
lerdb
6dee331484
fix: 修复网易保活 2024-10-07 17:35:51 +08:00
lerdb
bb1df10f1c
feat: 网易增加保活 2024-10-06 23:11:59 +08:00
sukimon_qwq
39fa65c2b6
chore: 移除print 2024-10-04 12:42:21 +08:00
sukimon_qwq
9adc778c05
style: 少量日志stack_error格式优化 2024-10-04 12:36:11 +08:00
sukimon_qwq
3a2da26245
fix: 修复tx源歌词与翻译错位的问题 2024-10-04 12:34:51 +08:00
ikun
193618b34e
fix: 修复vkey api部分文件404的问题 2024-09-17 20:56:21 +08:00
lerdb
e62ef1ce82
Merge pull request #71 from MeoProject/dependabot/pip/pybind11-2.13.6
build(deps): bump pybind11 from 2.13.5 to 2.13.6
2024-09-17 08:58:32 +08:00
dependabot[bot]
fe942f4ed9
build(deps): bump pybind11 from 2.13.5 to 2.13.6
Bumps [pybind11](https://github.com/pybind/pybind11) from 2.13.5 to 2.13.6.
- [Release notes](https://github.com/pybind/pybind11/releases)
- [Changelog](https://github.com/pybind/pybind11/blob/master/docs/changelog.rst)
- [Commits](https://github.com/pybind/pybind11/compare/v2.13.5...v2.13.6)

---
updated-dependencies:
- dependency-name: pybind11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-16 23:29:28 +00:00
ikun
c9d5df7fe8
feat: 支持接入第三方提供的vkey api 2024-09-16 14:52:12 +08:00
dependabot[bot]
15bfed2625
Merge pull request #68 from MeoProject/dependabot/pip/aiohttp-3.10.5 2024-09-15 01:38:21 +00:00
dependabot[bot]
2e6c49e2fe
build(deps): bump aiohttp from 3.10.3 to 3.10.5
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.3 to 3.10.5.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.10.3...v3.10.5)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-15 01:36:15 +00:00
dependabot[bot]
9bcfd581d9
Merge pull request #70 from MeoProject/dependabot/pip/pybind11-2.13.5 2024-09-15 01:34:52 +00:00
dependabot[bot]
4bf675ee3b
build(deps): bump pybind11 from 2.13.1 to 2.13.5
Bumps [pybind11](https://github.com/pybind/pybind11) from 2.13.1 to 2.13.5.
- [Release notes](https://github.com/pybind/pybind11/releases)
- [Changelog](https://github.com/pybind/pybind11/blob/master/docs/changelog.rst)
- [Commits](https://github.com/pybind/pybind11/compare/v2.13.1...v2.13.5)

---
updated-dependencies:
- dependency-name: pybind11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-26 23:13:04 +00:00
ikun
089d1c2cde chore: 优化一下 2024-08-18 18:49:39 +08:00
ikun
8919767eb5 feat: 给ck池加上kg刷新登录
测试时不知道为什么ck不变,估计要等(
2024-08-18 17:00:41 +08:00
dependabot[bot]
c254555ee7 build(deps): bump aiohttp from 3.10.1 to 3.10.3
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.1 to 3.10.3.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.10.1...v3.10.3)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-16 17:41:34 +08:00
lerdb
b82795eb13
Merge pull request #66 from MeoProject/dependabot/pip/aiohttp-3.10.1
build(deps): bump aiohttp from 3.9.5 to 3.10.1
2024-08-06 12:24:27 +08:00
dependabot[bot]
f0ecd9f269
build(deps): bump aiohttp from 3.9.5 to 3.10.1
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.9.5 to 3.10.1.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.5...v3.10.1)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 23:41:01 +00:00
ikun
d1e68719ba docs: 更新README 2024-07-11 09:47:34 +08:00
dependabot[bot]
67c11df74c
Merge pull request #62 from MeoProject/dependabot/pip/pybind11-2.13.1 2024-07-06 04:02:31 +00:00
dependabot[bot]
c4b38a47bb
build(deps): bump pybind11 from 2.12.0 to 2.13.1
Bumps [pybind11](https://github.com/pybind/pybind11) from 2.12.0 to 2.13.1.
- [Release notes](https://github.com/pybind/pybind11/releases)
- [Changelog](https://github.com/pybind/pybind11/blob/master/docs/changelog.rst)
- [Commits](https://github.com/pybind/pybind11/compare/v2.12.0...v2.13.1)

---
updated-dependencies:
- dependency-name: pybind11
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-06 04:00:00 +00:00
dependabot[bot]
cca000e318
Merge pull request #61 from MeoProject/dependabot/pip/pillow-10.4.0 2024-07-06 03:59:02 +00:00
dependabot[bot]
097b82c0c2
build(deps): bump pillow from 10.3.0 to 10.4.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.3.0 to 10.4.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/10.3.0...10.4.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 23:58:12 +00:00
ikun
2e67b1967a docs: 为什么不改README😡 2024-06-15 17:23:32 +08:00
lerdb
bdf03dbfdd
fix: 修复Python3.10以下无法启动 2024-06-12 19:56:30 +08:00
lerdb
10dcfdb8aa
fix: 修复源脚本检查更新 2024-06-12 19:27:17 +08:00
lerdb
3ea3c75859
feat: 新增源脚本检查更新功能
当客户端检查更新时,如果服务器的脚本与客户端的脚本不一致时会向客户端发送更新源脚本事件
2024-06-12 16:38:52 +08:00
lerdb
faeea6c42a
chore: 格式化yaml配置 2024-06-12 16:18:34 +08:00
helloplhm-qwq
1f36aecff4
fix: 删一个状态码
Some checks failed
build beta / build (ubuntu-latest) (push) Failing after 18s
Build Release / build (ubuntu-latest) (push) Has been skipped
build beta / build (macos-latest) (push) Has been cancelled
build beta / build (ubuntu-20.04) (push) Has been cancelled
build beta / build (windows-latest) (push) Has been cancelled
Build Release / build (macos-latest) (push) Has been cancelled
Build Release / build (windows-latest) (push) Has been cancelled
build beta / post (push) Has been cancelled
Build Release / release (push) Has been cancelled
2024-06-08 12:07:56 +08:00
helloplhm-qwq
a70a646a06
fix: 补一个await 2024-06-08 11:59:55 +08:00
helloplhm-qwq
e78599de70
Merge branch 'main' of https://github.com/MeoProject/lx-music-api-server 2024-06-08 11:52:23 +08:00
helloplhm-qwq
33bfdf1928
fix: 尝试修复gcsp 2024-06-08 11:50:39 +08:00
dependabot[bot]
2141b7d665 build(deps): bump requests from 2.32.2 to 2.32.3
Some checks failed
build beta / build (macos-latest) (push) Waiting to run
build beta / build (ubuntu-20.04) (push) Waiting to run
build beta / build (windows-latest) (push) Waiting to run
build beta / post (push) Blocked by required conditions
Build Release / build (macos-latest) (push) Waiting to run
Build Release / build (windows-latest) (push) Waiting to run
Build Release / release (push) Blocked by required conditions
build beta / build (ubuntu-latest) (push) Failing after 33s
Build Release / build (ubuntu-latest) (push) Has been skipped
Bumps [requests](https://github.com/psf/requests) from 2.32.2 to 2.32.3.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.2...v2.32.3)

---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-07 17:41:22 +08:00
helloplhm-qwq
c014d8a58a
Merge branch 'main' of https://github.com/helloplhm-qwq/lx-music-api-server 2024-06-07 15:55:44 +08:00
helloplhm-qwq
c22aa35d30
fix: NameError 2024-06-07 15:53:50 +08:00
helloplhm-qwq
f8b0126066
feat: 久违的更新awa 2024-06-07 15:46:40 +08:00
dependabot[bot]
4ac0633d8e ---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-26 14:55:01 +08:00
dependabot[bot]
622344516d
Merge pull request #57 from MeoProject/dependabot/pip/ujson-5.10.0 2024-05-22 09:33:22 +00:00
dependabot[bot]
30dcf0a5be
---
updated-dependencies:
- dependency-name: ujson
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-20 23:42:54 +00:00
dependabot[bot]
2dbb84ff28
Merge pull request #53 from MeoProject/dependabot/pip/pygments-2.18.0 2024-05-12 08:50:24 +00:00
dependabot[bot]
f2e3f8335b
build(deps): bump pygments from 2.17.2 to 2.18.0
Bumps [pygments](https://github.com/pygments/pygments) from 2.17.2 to 2.18.0.
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.17.2...2.18.0)

---
updated-dependencies:
- dependency-name: pygments
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 23:16:05 +00:00
dependabot[bot]
f93a888259
Merge pull request #52 from MeoProject/dependabot/pip/aiohttp-3.9.5 2024-04-23 17:29:05 +00:00
dependabot[bot]
c11718e2a8
build(deps): bump aiohttp from 3.9.4 to 3.9.5
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.9.4 to 3.9.5.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.4...v3.9.5)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-22 23:28:21 +00:00
梓澄qwq
1da6ddaab5
fix: init failed 2024-04-21 20:12:40 +08:00
梓澄qwq
5d687e6573
chore: 标记app对象为全局对象 2024-04-21 20:07:21 +08:00
梓澄qwq
f1640760af
fix: 修复意外的检查逻辑 x2 2024-04-21 20:02:20 +08:00
梓澄qwq
9066a251a1
fix: 意外的逻辑判断 2024-04-21 19:53:37 +08:00
lerdb
0502286ac8
chore: 移除webui
已经使用更加可视化的yaml进行config配置
2024-04-21 16:36:07 +08:00
helloplhm-qwq
bf16930c70 chore (pyqdes): 合并代码仓库 x2 2024-04-20 23:12:12 +08:00
helloplhm-qwq
56a8c2d18c chore (pyqdes): 合并代码仓库 2024-04-20 23:08:49 +08:00
ikun
5076086b0e docs: Update README
修改Docker部署教程
2024-04-19 00:00:55 +08:00
梓澄qwq
d22b434b4a
revert: 9d985d5 2024-04-18 01:35:04 +08:00
ikun
9d985d58cb chore: 移除无效导入 2024-04-17 23:32:56 +08:00
梓澄qwq
2463660e90
fix: 修复允许公网转发时报错
Close #51
2024-04-16 01:49:22 +08:00
helloplhm-qwq
acb84a5cb9
build ([release]): 更新版本号 2024-04-15 03:04:50 +08:00
helloplhm-qwq
1c939b5f23
chore: 阿巴阿巴 2024-04-15 03:03:44 +08:00
helloplhm-qwq
17332fa751
chore: 阿巴阿巴 2024-04-15 02:58:26 +08:00
梓澄qwq
d6c113c293
fix: 修复yaml不支持的方法 2024-04-15 01:29:47 +08:00
helloplhm-qwq
afd39e1c46
chore: git pull 2024-04-14 19:14:19 +08:00
helloplhm-qwq
667d420499
feat: 更新太大了不想总结自己去看提交记录吧(已知把配置文件换成了yaml 2024-04-14 19:06:36 +08:00
helloplhm-qwq
45e2e7147d
feat: 支持npm run xxx (纯属poetry run难用) 2024-04-14 15:51:24 +08:00
helloplhm-qwq
3f4e921e2a
feat (build): 修改更新日志的生成逻辑 2024-04-14 15:48:44 +08:00
dependabot[bot]
facb6d81e6 build(deps): bump aiohttp from 3.9.3 to 3.9.4
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.9.3 to 3.9.4.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.3...v3.9.4)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-14 13:52:43 +08:00
ikun
a769b23cdf
feat: 增加kg token报活
feat: 增加kg token报活
2024-04-07 00:14:48 +08:00
ikun
6412003d89 Merge branch 'main' of https://github.com/Huibq/lx-music-api-server-python into pr/47 2024-04-07 00:10:28 +08:00
Huibq
c42d30f737 feat: 增加kg token报活 2024-04-07 00:10:20 +08:00
Huibq
4d6ff2626b 酷狗token保活,未完成cookie池集体保活 2024-04-06 16:36:24 +08:00
梓澄qwq
7e10d98c47
fix: 修复开启ckpool时mg账号保活导致的init failure 2024-04-06 14:21:54 +08:00
梓澄qwq
6062e23e52
chore: update 2024-04-03 23:47:35 +08:00
dependabot[bot]
2436852848
Merge pull request #46 from lxmusics/dependabot/pip/pillow-10.3.0 2024-04-02 08:45:55 +00:00
dependabot[bot]
576ee65820
build(deps): bump pillow from 10.2.0 to 10.3.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.2.0 to 10.3.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/10.2.0...10.3.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 00:03:54 +00:00
ikun0014
d19c343615 docs: README 2024-03-27 18:06:42 +08:00
梓澄qwq
27cf9d51a9
fix: 修复波点uid
Close #43
2024-03-09 01:44:50 +08:00
helloplhm-qwq
1a2f7e2e55 chore: [release] 更新版本号(v2.0.0.beta-10) 2024-03-03 18:39:21 +08:00
helloplhm-qwq
e6115b555b
fix: 修复咪咕集体无版权 & feat: 更换咪咕API为网页版接口 & fix: 修复网络库潜在报错问题
Close #39
2024-03-03 18:37:20 +08:00
helloplhm-qwq
64a1b2ffac
fix: 日志记录失败 * n 2024-03-03 17:11:52 +08:00
helloplhm-qwq
5e70560df9
chore: 更新git ignore 2024-03-03 16:51:12 +08:00
helloplhm-qwq
bf2cd1dd36
fix: 语法错误 2024-03-03 16:50:12 +08:00
helloplhm-qwq
0c5e405c31
fix: 打包文件日志日常 2024-03-03 16:45:18 +08:00
lerdb
f06b6e8710
chore: 移除flask文件 2024-03-03 16:11:18 +08:00
helloplhm-qwq
cd34deca63
Merge branch 'main' of https://github.com/lxmusics/lx-music-api-server-python 2024-03-03 14:08:34 +08:00
helloplhm-qwq
1b13a8e845
revert: chore: (deps) 更新依赖 2024-03-03 14:08:22 +08:00
helloplhm-qwq
f806ffc32e
chore: (deps) 更新依赖 2024-03-03 14:08:00 +08:00
helloplhm-qwq
fa53fdb4e1
fix: 修复某些时候因错误无法格式化导致程序崩溃的问题 2024-03-03 14:03:30 +08:00
ikun0014
b4ba6f5d86 docs: 修改Docker搭建教程 2024-03-03 13:25:18 +08:00
ikun
de5043607d
Merge pull request #42 from chaos-zhu/patch-1
docs: 添加容器自动重启指令
2024-03-03 13:16:26 +08:00
helloplhm-qwq
7ea698e147
feat: 优化报错输出 2024-03-02 20:07:39 +08:00
chaoszhu
267cf81425
添加容器自动重启指令 2024-02-29 15:51:17 +08:00
lerdb
546d94ab9a
fix: 使用webui重命名的config.bak未保存在config目录下 2024-02-25 10:45:06 +08:00
lerdb
5645bf8c72
chore: 移除changelog 2024-02-25 10:39:56 +08:00
lerdb
e9e6f327e6
fix: 修复构建无法获取changelog 2024-02-25 10:38:53 +08:00
梓澄qwq
f24762ecb7 revert: chore: update 2024-02-22 21:02:31 +08:00
梓澄qwq
3f346f79b6 chore: update 2024-02-22 21:02:31 +08:00
梓澄qwq
72b7840819 chore: update 2024-02-22 21:02:31 +08:00
ikun0014
1d640f8dd4 feat: 兼容已生成的配置文件 2024-02-22 21:02:31 +08:00
ikun0014
27e2f6677d fix: WebUI不可用 2024-02-22 21:02:31 +08:00
ikun0014
715206ea6a fix: 纯属意外(确信) 2024-02-22 21:02:31 +08:00
ikun
f88496023f chore: 把配置文件生成的地方换个位置,对Docker用户更友好
ps: Docker只能挂载目录,所以更友好
2024-02-22 21:02:31 +08:00
lerdb
de3e8da790
build: Update build_release 2024-02-19 09:48:44 +08:00
helloplhm-qwq
7c5b8f2be0 fix: unknown 2024-02-18 15:33:41 +08:00
helloplhm-qwq
ef77cdf7db feat: kw & wy 源支持使用raw quality获取歌曲 2024-02-18 15:20:22 +08:00
helloplhm-qwq
48d8434e95 revert: fix: 修复wy源 sky 音质的获取 2024-02-18 15:19:00 +08:00
helloplhm-qwq
24b7082d0e fix: 修复wy源 sky 音质的获取 2024-02-18 15:17:09 +08:00
梓澄qwq
bbdb3eea7a
chore: merge #38
docs: 更新Docker部署教程
2024-02-14 15:14:07 +08:00
梓澄qwq
6c33f5971a
docs: improve translation 2024-02-14 15:12:55 +08:00
ikun0014
da9be3da49 docs: 更新Docker部署教程 2024-02-14 15:01:01 +08:00
lerdb
3e55a86669
docs: 更新文档 2024-02-14 12:37:25 +08:00
lerdb
d7dcc4401e
feat: 更新项目图标 2024-02-14 12:16:25 +08:00
lerdb
d4b5c06592
build: 构建可执行文件增加图标 2024-02-14 11:25:41 +08:00
lerdb
f5f35bd8e0
fix: webui 2024-02-09 00:05:25 +08:00
lerdb
41443f6325
feat: 初步支持webui 2024-02-08 23:56:09 +08:00
lerdb
bd7abc9a12
build: 更新构建 2024-02-08 10:34:48 +08:00
helloplhm-qwq
00de8745b5
fix: 下载脚本的文件名异常 2024-02-07 09:45:07 +08:00
helloplhm-qwq
6ef26f864c
feat: 支持多个请求key 2024-02-06 21:54:10 +08:00
helloplhm-qwq
46a270f4f4
revert: fix: 修复MP3内嵌歌词读取 2024-02-06 11:52:38 +08:00
helloplhm-qwq
d2bedcfd73
fix: 修复MP3内嵌歌词读取 2024-02-06 11:42:39 +08:00
helloplhm-qwq
b9a1e530dd
feat: (localMusic) 优化读取本地音乐meta信息,修复重复读取的问题 2024-02-04 16:27:36 +08:00
helloplhm-qwq
35f1fe513a
fix: 修复LRC文件读取歌词 2024-02-04 16:17:54 +08:00
helloplhm-qwq
a55da25de5
docs: 画新的饼 2024-02-04 16:13:08 +08:00
helloplhm-qwq
b965fbfb37
chore: 移除未使用的import 2024-02-04 15:24:12 +08:00
helloplhm-qwq
4fb209dd94
build: [release] 更新beta9 2024-02-04 12:13:32 +08:00
helloplhm-qwq
2d290e05d9
docs: 更新README 2024-02-04 12:08:58 +08:00
helloplhm-qwq
1d0309d5d1
build: (deps) 更新依赖 2024-02-04 12:07:12 +08:00
helloplhm-qwq
98e6a3280d
docs: 更新README 2024-02-04 11:58:31 +08:00
helloplhm-qwq
339e5edf3d
feat: 支持客户端播放服务端音乐 2024-02-04 11:51:52 +08:00
lerdb
a8e4d8ac69
docs: Update README_EN.md 2024-02-03 23:46:41 +08:00
lerdb
de0b033884
docs: Update README 2024-02-03 23:44:16 +08:00
lerdb
0997464d39
docs: Update README 2024-02-03 23:20:32 +08:00
lerdb
bdd49824aa
docs: Update README_EN.md 2024-02-03 20:53:00 +08:00
lerdb
7a0bf4932d
docs: Update README.md 2024-02-03 20:49:20 +08:00
lerdb
0e3cd085eb
docs: Create an English version of README.md 2024-02-03 20:43:59 +08:00
lerdb
567eadcbe1
build: [release] 主要是想触发Actions 2024-02-02 23:21:09 +08:00
lerdb
761f2fc862
chore: [release] beta-8 2024-02-02 23:16:45 +08:00
lerdb
068b58a619
fix: [release] 修复release乱码问题 2024-02-02 23:12:52 +08:00
lerdb
ff276cb38a
chore: [release] 更新beta-8 2024-02-02 23:08:52 +08:00
lerdb
827c5c59b4
chore: 提交更新日志 2024-02-02 23:06:31 +08:00
lerdb
47d4bb3d3a
chore: 更新版本号 2024-02-02 23:04:33 +08:00
lerdb
af1c69a6e3
build: 修复构建 2024-02-02 23:02:47 +08:00
helloplhm-qwq
61f426e915
fix: 修复非大陆服务器酷狗歌曲搜索 2024-02-01 11:04:30 +08:00
helloplhm-qwq
0af0515d9c
fix: 支持海外???? 2024-02-01 11:02:44 +08:00
helloplhm-qwq
f927f153a8
build: [release] 我来刷版本号辣 2024-01-31 23:57:58 +08:00
helloplhm-qwq
8457dd4cef
build: [release] 我来刷版本号辣 2024-01-31 23:53:04 +08:00
helloplhm-qwq
31ec95bcc0
build: [release] 我来刷版本号辣 2024-01-31 23:48:51 +08:00
helloplhm-qwq
7d1047ea34
build: [release] 我来刷版本号辣 2024-01-31 23:45:28 +08:00
helloplhm-qwq
878a9f8c0d
build: [release] 我来刷版本号辣 2024-01-31 23:43:02 +08:00
helloplhm-qwq
896dd6f698
build: [release] 我来刷版本号辣 2024-01-31 23:35:51 +08:00
helloplhm-qwq
859beddfaa
build: [release] 我来刷版本号辣 2024-01-31 23:34:44 +08:00
helloplhm-qwq
6ac5d8c388
chore: 测试提交6 2024-01-31 23:34:18 +08:00
helloplhm-qwq
b202d92b10
chore: 测试提交7 2024-01-31 23:34:16 +08:00
helloplhm-qwq
50990fed63
chore: 测试提交5 2024-01-31 23:34:12 +08:00
helloplhm-qwq
56cc849614
chore: 测试提交4 2024-01-31 23:34:11 +08:00
helloplhm-qwq
5a1a8c4e3a
chore: 测试提交3 2024-01-31 23:34:09 +08:00
helloplhm-qwq
e1d0e16d33
chore: 测试提交2 2024-01-31 23:34:07 +08:00
helloplhm-qwq
ae729ba66e
chore: 测试提交1 2024-01-31 23:34:05 +08:00
helloplhm-qwq
27a02d0709
build: [release] 我来刷版本号辣 2024-01-31 23:30:07 +08:00
helloplhm-qwq
67571e37c1
build: [release] 我来刷版本号辣 2024-01-31 23:24:06 +08:00
helloplhm-qwq
de46ab907f
build: [release] 我来刷版本号辣 2024-01-31 23:19:57 +08:00
helloplhm-qwq
7e35fee479
chore: 测试提交 2024-01-31 23:19:25 +08:00
helloplhm-qwq
6d798d807b
build: [release] 尝试修复构建 2024-01-31 23:11:39 +08:00
helloplhm-qwq
220bf6831b
build: [release] 尝试修复构建 2024-01-31 22:59:03 +08:00
helloplhm-qwq
b615f42a19
build: [release] 尝试修复构建 2024-01-31 22:56:32 +08:00
helloplhm-qwq
5c167b911c
build: [release] 更新版本号(2.0.0.beta-5) 2024-01-31 22:50:31 +08:00
lerdb
8f84af70ac
chore: 过滤所有测试文件 2024-01-31 22:29:37 +08:00
lerdb
73d95b6032
fix: kuwo源配置kuwodes时报错 #37 2024-01-31 22:27:47 +08:00
lerdb
eca696c79b
build: fix typo 2024-01-31 21:08:26 +08:00
lerdb
d1e23d94c6
Merge branch 'main' of github.com:lxmusics/lx-music-api-server-python 2024-01-31 20:53:36 +08:00
lerdb
42fd54389e
build: 重写构建 2024-01-31 20:53:08 +08:00
梓澄qwq
09f34fd2b9
chore: fix typo 2024-01-31 14:49:55 +08:00
helloplhm-qwq
39caeeb816
Merge branch 'main' of https://github.com/lxmusics/lx-music-api-server-python 2024-01-31 11:25:21 +08:00
helloplhm-qwq
54222f89ce
chore: (git) 更新.gitignore 2024-01-31 11:24:50 +08:00
helloplhm-qwq
610343c8e1
chore: no title 2024-01-31 11:23:43 +08:00
lerdb
2899cac40c
build: 重写构建流程 2024-01-31 00:12:19 +08:00
helloplhm-qwq
ee0c894d05
feat: 优化脚本下载 2024-01-30 22:58:45 +08:00
dependabot[bot]
08375b9b10
Merge pull request #34 from lxmusics/dependabot/pip/aiohttp-3.9.3 2024-01-30 01:51:41 +00:00
dependabot[bot]
c11cacd8c0
build(deps): bump aiohttp from 3.9.1 to 3.9.3
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.9.1 to 3.9.3.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.1...v3.9.3)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 01:49:59 +00:00
dependabot[bot]
3d2361ea85
Merge pull request #35 from lxmusics/dependabot/pip/colorlog-6.8.2 2024-01-30 01:49:05 +00:00
dependabot[bot]
1e87fc7194
build(deps): bump colorlog from 6.8.0 to 6.8.2
Bumps [colorlog](https://github.com/borntyping/python-colorlog) from 6.8.0 to 6.8.2.
- [Release notes](https://github.com/borntyping/python-colorlog/releases)
- [Commits](https://github.com/borntyping/python-colorlog/compare/v6.8.0...v6.8.2)

---
updated-dependencies:
- dependency-name: colorlog
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-29 23:56:29 +00:00
helloplhm-qwq
f3b0a4d5a6
feat: 优化端口监听逻辑,遇到某个端口监听错误时不重复监听 2024-01-25 00:52:59 +08:00
helloplhm-qwq
e0f96333d0
fix: other method 2024-01-24 18:35:29 +08:00
dependabot[bot]
5b1f741845
Merge pull request #31 from lxmusics/dependabot/pip/flask-3.0.1 2024-01-23 15:47:58 +00:00
dependabot[bot]
1d246115fc
build(deps-dev): bump flask from 3.0.0 to 3.0.1
Bumps [flask](https://github.com/pallets/flask) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/3.0.0...3.0.1)

---
updated-dependencies:
- dependency-name: flask
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 23:12:55 +00:00
lerdb
e1d30cfbbf
fix: 修复下载script时错误信息乱码的问题 2024-01-22 13:20:39 +08:00
lerdb
9a9e64ac19
fix: 修复无法下载script的bug 2024-01-22 12:44:00 +08:00
lerdb
2f869694d5
docs: 更新文档 2024-01-22 12:42:06 +08:00
52 changed files with 5098 additions and 2236 deletions

View File

@ -1,16 +1,20 @@
# this workflow using github actions to build a binary exe file for windows users
name: build beta
on:
push:
branches: [main]
paths-ignore:
- '**.md'
- '.gitignore'
pull_request:
branches: [main]
jobs:
build-windows:
runs-on: windows-latest
build:
strategy:
matrix:
runs-on: [windows-latest, macos-latest, ubuntu-latest, ubuntu-20.04]
runs-on: ${{ matrix.runs-on }}
steps:
- name: Checkout git repo
uses: actions/checkout@v2
@ -26,37 +30,37 @@ jobs:
- name: Install dependencies
run: python -m pip install -r ./requirements.txt
- name: Build EXE
- name: Build
run: python build.py build test
- name: Upload
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: windows
name: dist-${{ matrix.runs-on }}
path: ./dist
build-linux:
post:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout git repo
uses: actions/checkout@v2
- name: Setup Python 3.11
uses: actions/setup-python@v2
- name: Download Build Files
uses: actions/download-artifact@v4
with:
python-version: '3.11'
path: dist
pattern: dist-*
merge-multiple: false
- name: Install pyinstaller
run: python -m pip install pyinstaller
- name: Get File Address
run: |
echo "windowsFile=./dist/dist-windows-latest/lx-music-api-server_$(git rev-parse --short HEAD).exe" >> $GITHUB_ENV
echo "macosFile=./dist/dist-macos-latest/lx-music-api-server_$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "linuxFile=./dist/dist-ubuntu-latest/lx-music-api-server_$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Install dependencies
run: python -m pip install -r ./requirements.txt
- name: Build Executable
run: python build.py build test
- name: Upload
uses: actions/upload-artifact@v2
with:
name: linux
path: ./dist
- name: Post to Channel
run: |
echo ${{ secrets.SCRIPT_BASE64 }} | base64 -d > test.py
ESCAPED=`python test.py`
curl -s -v "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMediaGroup?chat_id=${{ secrets.TELEGRAM_ID }}&media=%5B%7B%22type%22:%22document%22,%20%22media%22:%22attach://windows%22,%22parse_mode%22:%22MarkdownV2%22,%22caption%22:${ESCAPED}%7D%5D" -F windows=@${{ env.windowsFile }}

View File

@ -1,12 +1,23 @@
name: Build Release
on:
workflow_dispatch:
push:
branches: [main]
paths-ignore:
- '**.md'
- '.gitignore'
permissions:
contents: write
pull-requests: write
jobs:
build-windows:
runs-on: windows-latest
build:
if: contains(github.event.head_commit.message, '[release]')
strategy:
matrix:
runs-on: [windows-latest, macos-latest, ubuntu-latest]
runs-on: ${{ matrix.runs-on }}
steps:
- name: Checkout git repo
uses: actions/checkout@v2
@ -22,37 +33,70 @@ jobs:
- name: Install dependencies
run: python -m pip install -r ./requirements.txt
- name: Build EXE
- name: Build
run: python build.py build release
- name: Upload
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: windows
name: dist-${{ matrix.runs-on }}
path: ./dist
build-linux:
release:
if: contains(github.event.head_commit.message, '[release]')
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout git repo
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Setup Python 3.11
uses: actions/setup-python@v2
with:
python-version: '3.11'
- name: Install pyinstaller
run: python -m pip install pyinstaller
- name: Get version
run: |
python -m pip install toml
echo PACKAGE_VERSION=`python -c 'import toml; print(toml.load("./pyproject.toml")["tool"]["poetry"]["version"])'` >> $GITHUB_ENV
- name: Install dependencies
run: python -m pip install -r ./requirements.txt
- name: Generate Changelog
run: |
git fetch --prune --unshallow --tags
python build.py changelog >> ./changelog.md
- name: Build Executable
run: python build.py build release
- name: Upload
uses: actions/upload-artifact@v2
- name: Create git tag
uses: pkgdeps/git-tag-action@v2
with:
name: linux
path: ./dist
github_token: ${{ github.token }}
github_repo: ${{ github.repository }}
version: ${{ env.PACKAGE_VERSION }}
git_commit_sha: ${{ github.sha }}
git_tag_prefix: "v"
- name: Download Build Files
uses: actions/download-artifact@v4
with:
path: dist
pattern: dist-*
merge-multiple: false
- name: Rename Files
run: |
mv ./dist/dist-windows-latest/lx-music-api-server_${{ env.PACKAGE_VERSION }}.exe ./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_windows.exe
mv ./dist/dist-macos-latest/lx-music-api-server_${{ env.PACKAGE_VERSION }} ./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_macos
mv ./dist/dist-ubuntu-latest/lx-music-api-server_${{ env.PACKAGE_VERSION }} ./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_ubuntu
- name: Release
uses: softprops/action-gh-release@v1
with:
body_path: ./changelog.md
prerelease: false
draft: false
tag_name: v${{ env.PACKAGE_VERSION }}
files: |
./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_windows.exe
./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_macos
./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_ubuntu
env:
GITHUB_TOKEN: ${{ github.token }}

12
.gitignore vendored
View File

@ -18,11 +18,16 @@ build
# project
cache.db
data.db
test.py
*/test.py
test.*
*/test.*
logs
config.json
config.yml
/config/config.json
/config/data.db
*.log
*.bak
*.tmp
# VSCode
.history
@ -37,3 +42,6 @@ config.json
# temp script
lx-music-source-example.js
# dumprecord
dumprecord_*.txt

View File

@ -1,13 +0,0 @@
FROM python:3.10-alpine
WORKDIR /app
COPY ./main.py .
COPY ./common ./common
COPY ./modules ./modules
COPY ./requirements.txt .
# 指定源, 如果后期源挂了, 更换个源就可以.
RUN pip install --no-cache -i https://pypi.mirrors.ustc.edu.cn/simple/ -r requirements.txt
CMD [ "python", "main.py" ]

295
README.md
View File

@ -1,90 +1,110 @@
简体中文 | [English](README_EN.md)
<div align="center">
![lx-music-api-server-python](https://socialify.git.ci/lxmusics/lx-music-api-server-python/image?description=1&font=Inter&forks=1&issues=1&language=1&name=1&owner=1&pulls=1&stargazers=1&theme=Auto)
![lx-music-api-server](https://socialify.git.ci/MeoProject/lx-music-api-server/image?description=1&forks=1&issues=1&logo=https%3A%2F%2Fraw.githubusercontent.com%2FMeoProject%2Flx-music-api-server%2Fmain%2Ficon.png&owner=1&pulls=1&stargazers=1&theme=Auto)
![GitHub Repo Size](https://img.shields.io/github/repo-size/lxmusics/lx-music-api-server-python?style=for-the-badge)
[![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/lxmusics/lx-music-api-server-python/build_beta.yml?style=for-the-badge)](https://github.com/lxmusics/lx-music-api-server-python/actions/workflows/build_beta.yml)
[![GitHub Release (with filter)](https://img.shields.io/github/v/release/lxmusics/lx-music-api-server-python?style=for-the-badge)](https://github.com/lxmusics/lx-music-api-server-python/releases/latest)
[![GitHub All Releases](https://img.shields.io/github/downloads/lxmusics/lx-music-api-server-python/total?style=for-the-badge&color=violet)](https://github.com/lxmusics/lx-music-api-server-python/releases)
[![GitHub License](https://img.shields.io/github/license/lxmusics/lx-music-api-server-python?style=for-the-badge)](https://github.com/lxmusics/lx-music-api-server/blob/main/LICENSE)
![GitHub Repo Size](https://img.shields.io/github/repo-size/MeoProject/lx-music-api-server?style=for-the-badge)
[![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/MeoProject/lx-music-api-server/build_beta.yml?style=for-the-badge)](https://github.com/MeoProject/lx-music-api-server/actions/workflows/build_beta.yml)
[![GitHub Release (with filter)](https://img.shields.io/github/v/release/MeoProject/lx-music-api-server?style=for-the-badge)](https://github.com/MeoProject/lx-music-api-server/releases/latest)
[![GitHub All Releases](https://img.shields.io/github/downloads/MeoProject/lx-music-api-server/total?style=for-the-badge&color=violet)](https://github.com/MeoProject/lx-music-api-server/releases)
[![GitHub License](https://img.shields.io/github/license/MeoProject/lx-music-api-server?style=for-the-badge)](https://github.com/MeoProject/lx-music-api-server/blob/main/LICENSE)
</div>
原仓库:[lx-music-api-server](https://github.com/lxmusics/lx-music-api-server)
你可以在原仓库中找到对应的可用源脚本
本项目[文档](https://apidoc.zcmonety.xyz)
**由于使用此项目导致的封号等情况与本项目无关**
由于使用此项目导致的**封号**等情况**与本项目无关**
**主开发是高一住校学生,只有周末有时间回复,也欢迎所有人来贡献代码,我们在这里万分感谢**
本项目不接受私人定制,非**本项目Github发布**所出现问题**与本项目无关**
**临近期末考试,更新会放缓,新功能请不要催得太紧**
## 💡特点
## 部署方法
- [ ] 功能
- [ ] 完整性API歌单搜索
- [x] 网易云刷新登录
- [ ] 服务器向webdav
- [x] 客户端播放服务器上的本地音乐
- [x] Cookie池
- [x] https监听多端口监听
- [x] 反代兼容性
- [x] 获取更高的音质
- [x] QRC解密
- [ ] 本地化支持(目前仅支持简体中文)
- [x] 多端部署(`Windows` `Linux` `MacOS`
### poetry部署
## 💻部署方法
### Release 部署(推荐)
1. 从 [Releases](https://github.com/MeoProject/lx-music-api-server/releases)
或 [Actions](https://github.com/MeoProject/lx-music-api-server/actions)
下载对应你系统的可执行文件 (从 GitHub Actions 下载需要登录 GitHub 账号)
2. 运行可执行文件(如果下载的文件是压缩包请先解压)
---
### Poetry 部署
环境要求: Python 3.8+
1. 安装 poetry
```bash
pip install poetry
```
2. clone本项目
```bash
git clone https://github.com/lxmusics/lx-music-api-server-python.git
cd lx-music-api-server-python
```
1. 安装 poetry
```bash
pip install poetry
```
2. clone本项目并进入项目目录
```bash
git clone https://github.com/MeoProject/lx-music-api-server.git
cd lx-music-api-server
```
3. 安装依赖
```bash
poetry install
```
```bash
poetry install
```
4. 启动
```bash
poetry shell # 进入poetry环境
python main.py # 运行项目
```
```bash
poetry shell # 进入poetry环境
python main.py # 运行项目
```
---
### 直接部署
环境要求Python 3.6+, 建议Python 3.8+
环境要求Python 3.6 - 3.11, 建议Python 3.8+
Python 3.12 及以上会装不上依赖
没有其他限制,能用 Python 理论上就能跑起来
flask版本即将停止支持不建议使用
测试版本部署linux 命令如果为 python3 请自行替换:
linux 命令如果为 python3 请自行替换
```bash
git clone https://github.com/lxmusics/lx-music-api-server-python.git # clone本项目
cd lx-music-api-server-python # 进入项目目录
python -m pip install -r ./requirements.txt # 安装依赖
python main.py # 启动服务
```
1. clone本项目并进入项目目录
对于 release 的部署和上方类似,这里不再赘述
```bash
git clone https://github.com/MeoProject/lx-music-api-server.git
cd lx-music-api-server
```
### Docker 部署
2. 安装依赖
环境要求Docker
```bash
python -m pip install -r ./requirements.txt
```
**该方法未经测试,不知道需要的 Docker 版本,可以自己尝试现有的 Docker 版本是否可以使用**
3. 启动
```bash
# 更新软件包
sudo apt-get update
```bash
python main.py
```
# 安装Docker已有跳过
sudo apt-get install -y docker.io
# 创建容器
docker run --name lx-music-api-server-python -p 9763:9763 -d ikun0014/lx-music-api-server-python:latest
# 获取容器目录
docker inspect lx-music-api-server-python
```
**配置文件填写需要到容器目录里的 /app 目录填写 config.json**
## 返回码说明
## 📖返回码说明
接口返回值中`body.code`字段值中的代码含义
@ -107,100 +127,105 @@ docker inspect lx-music-api-server-python
| 429 | 请求过于频繁 |
| 500 | 服务器内部错误(对应 body.code 4 |
## 备注
## 🔖备注
### 本项目中可能会出现以下优秀代码
1. 三角形具有稳定性
```python
for a in xxx:
if (xxx):
if (xxx):
```python
for a in xxx:
if (xxx):
for b in xxx:
if (xxx):
if (xxx):
while (xxx):
for b in xxx:
if (xxx):
while (xxx):
pass
pass
pass
pass
pass
pass
pass
pass
pass
```
```
2. 能一行写完那就坚决不多行
```python
sys.stdout.write('\r|'+'=' * (int(dd['pares'].index(ds) / total * 50)) + ' ' * (49 - int(dd['pares'].index(ds) / total * 50)) + f'''|{int(dd['pares'].index(ds) / total * 100)}% xx''' + ds['title']+' ' * 20)
```
```python
sys.stdout.write('\r|'+'=' * (int(dd['pares'].index(ds) / total * 50)) + ' ' * (49 - int(dd['pares'].index(ds) / total * 50)) + f'''|{int(dd['pares'].index(ds) / total * 100)}% xx''' + ds['title']+' ' * 20)
```
3. 不复用重复部分
```python
async def other(method, source, songid, _):
try:
func = require('modules.' + source + '.' + method)
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
}
try:
result = await func(songid)
return {
'code': 0,
'msg': 'success',
'data': result
}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
}
async def other_with_query(method, source, t, _, query):
try:
func = require('modules.' + source + '.' + method)
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
}
try:
result = await func(t, query)
return {
'code': 0,
'msg': 'success',
'data': result
}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
}
```
```python
async def other(method, source, songid, _):
try:
func = require('modules.' + source + '.' + method)
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
}
try:
result = await func(songid)
return {
'code': 0,
'msg': 'success',
'data': result
}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
}
async def other_with_query(method, source, t, _, query):
try:
func = require('modules.' + source + '.' + method)
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
}
try:
result = await func(t, query)
return {
'code': 0,
'msg': 'success',
'data': result
}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
}
```
4. 模块不拆分
详见[config.py](https://github.com/lxmusics/lx-music-api-server-python/tree/main/common/config.py)
详见[config.py](https://github.com/MeoProject/lx-music-api-server/tree/main/common/config.py)
5. 不明所以的变量名
```python
a = '小明'
b = 1
c = 2
d = b''
def e(a, b, c):
c = xxx
d = xxx
f = e(c, b, a)
```
## 项目协议
```python
a = '小明'
b = 1
c = 2
d = b''
def e(a, b, c):
c = xxx
d = xxx
f = e(c, b, a)
```
本项目基于 [MIT](https://github.com/lxmusics/lx-music-api-server/blob/main/LICENSE) 许可证发行,以下协议是对于 MIT 原协议的补充,如有冲突,以以下协议为准。
## 📄项目协议
本项目基于 [MIT](https://github.com/MeoProject/lx-music-api-server/blob/main/LICENSE) 许可证发行,以下协议是对于 MIT 原协议的补充,如有冲突,以以下协议为准。
词语约定:本协议中的“本项目”指本音源项目;“使用者”指签署本协议的使用者;“官方音乐平台”指对本项目内置的包括酷我、酷狗、咪咕等音乐源的官方平台统称;“版权数据”指包括但不限于图像、音频、名字等在内的他人拥有所属版权的数据。
@ -218,12 +243,10 @@ helloplhm-qwq+outlook.com
folltoshe+foxmail.com
(请将`+`替换成`@`)
## Star 趋势图
## Star 趋势图
[![Stargazers over time](https://starchart.cc/lxmusics/lx-music-api-server-python.svg)](https://starchart.cc/lxmusics/lx-music-api-server-python)
[![Stargazers over time](https://starchart.cc/MeoProject/lx-music-api-server.svg)](https://starchart.cc/MeoProject/lx-music-api-server)
## 贡献者
## ⚙️贡献者
<a href="https://github.com/lxmusics/lx-music-api-server-python/graphs/contributors">
<img src="https://contrib.rocks/image?repo=lxmusics/lx-music-api-server-python" />
</a>
[![Contributor](https://contrib.rocks/image?repo=MeoProject/lx-music-api-server)](https://github.com/MeoProject/lx-music-api-server/graphs/contributors)

249
README_EN.md Normal file
View File

@ -0,0 +1,249 @@
English | [简体中文](README.md)
<div align="center">
![lx-music-api-server](https://socialify.git.ci/MeoProject/lx-music-api-server/image?description=1&forks=1&issues=1&logo=https%3A%2F%2Fraw.githubusercontent.com%2FMeoProject%2Flx-music-api-server%2Fmain%2Ficon.png&owner=1&pulls=1&stargazers=1&theme=Auto)
![GitHub Repo Size](https://img.shields.io/github/repo-size/MeoProject/lx-music-api-server?style=for-the-badge)
[![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/MeoProject/lx-music-api-server/build_beta.yml?style=for-the-badge)](https://github.com/MeoProject/lx-music-api-server/actions/workflows/build_beta.yml)
[![GitHub Release (with filter)](https://img.shields.io/github/v/release/MeoProject/lx-music-api-server?style=for-the-badge)](https://github.com/MeoProject/lx-music-api-server/releases/latest)
[![GitHub All Releases](https://img.shields.io/github/downloads/MeoProject/lx-music-api-server/total?style=for-the-badge&color=violet)](https://github.com/MeoProject/lx-music-api-server/releases)
[![GitHub License](https://img.shields.io/github/license/MeoProject/lx-music-api-server?style=for-the-badge)](https://github.com/MeoProject/lx-music-api-server/blob/main/LICENSE)
</div>
This project [Document](https://apidoc.zcmonety.xyz)
**Ban** and other situations caused by the use of this project have **nothing** to do with this project.
This project does not accept private customization, and the problems that occur in the non-**Github release of this project** have nothing to do with this project**
## 💡Feature
- [ ] Functions
- [ ] All APIs (playlist, search)
- [x] Netease Cloud Refresh Login
- [ ] Client plays music on webdav
- [x] Client plays local music on the server
- [x] Cookie pool
- [x] HTTPS listening, multi port listening
- [x] Reverse proxy compatibility
- [x] Get higher quality
- [x] QRC decrypt
- [ ] Localization (currently only Simplified Chinese is supported)
- [x] Platform support: `Windows` `Linux` `MacOS`
## 💻How to deploy
### Use Release (recommended)
1. Download the executable file corresponding to your system from [Releases](https://github.com/MeoProject/lx-music-api-server/releases) or [Actions](https://github.com/MeoProject/lx-music-api-server/actions)
2. Run the downloaded executable file (maybe you need to unzip the downloaded file if it is a compressed files)
---
### Use Poetry
Required environment: Python 3.8+
1. Install poetry
```bash
pip install poetry
```
2. Clone this project and enter the project directory
```bash
git clone https://github.com/MeoProject/lx-music-api-server.git
cd lx-music-api-server
```
3. Install requirements
```bash
poetry install --no-root
```
4. Run it
```bash
poetry shell # enter poetry environment
python main.py # run project
```
---
### Directly deploy
Required environment: Python 3.6 - 3.11, Python 3.8+ is better.
Python 3.12 or higher maybe install requirements failed.
Without other restrictions, you can run only with Python.
If you are using linux, you command maybe python3, please replace it yourself.
1. Clone this project and enter the project directory
```bash
git clone https://github.com/MeoProject/lx-music-api-server.git
cd lx-music-api-server
```
2. Install requirements
```bash
python -m pip install -r ./requirements.txt
```
3. Run it
```bash
python main.py
```
## 📖Return code description
The code meaning in the `body.code` field value in the interface return value.
| Value | Meaning |
| ---- | --------------------------------------------------- |
| 0 | Success |
| 1 | IP is banned or does not support anti-generation |
| 2 | Fail to obtain. |
| 4 | Server internal error (corresponding to statuscode 500) |
| 5 | Too frequent requests |
| 6 | Parameter error |
The code meaning of `statuscode` returned by the interface.
| Value | Meaning |
| ---- | -------------------------------------------------- |
| 200 | Success |
| 403 | IP is banned |
| 400 | Parameter error |
| 429 | Too frequent requests |
| 500 | Server internal error (corresponding to body.code 4) |
## 🔖Remarks
### The following excellent codes may appear in this project.
1. Triangle has stability.
```python
for a in xxx:
if (xxx):
if (xxx):
if (xxx):
for b in xxx:
if (xxx):
while (xxx):
pass
pass
pass
pass
pass
pass
pass
```
2. If you can finish it in one line, then don't write many lines.
```python
sys.stdout.write('\r|'+'=' * (int(dd['pares'].index(ds) / total * 50)) + ' ' * (49 - int(dd['pares'].index(ds) / total * 50)) + f'''|{int(dd['pares'].index(ds) / total * 100)}% xx''' + ds['title']+' ' * 20)
```
3. Do not reuse duplicate parts
```python
async def other(method, source, songid, _):
try:
func = require('modules.' + source + '.' + method)
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
}
try:
result = await func(songid)
return {
'code': 0,
'msg': 'success',
'data': result
}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
}
async def other_with_query(method, source, t, _, query):
try:
func = require('modules.' + source + '.' + method)
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
}
try:
result = await func(t, query)
return {
'code': 0,
'msg': 'success',
'data': result
}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
}
```
4. Module does not split
Details at [config.py](https://github.com/MeoProject/lx-music-api-server/tree/main/common/config.py)
5. Unknown variable name
```python
a = '小明'
b = 1
c = 2
d = b''
def e(a, b, c):
c = xxx
d = xxx
f = e(c, b, a)
```
## 📄Project agreement
This project is issued under [MIT](https://github.com/MeoProject/lx-music-api-server/blob/main/LICENSE) license. The following agreement is a supplement to the original MIT agreement. In case of conflict, the following agreement shall prevail.
Word agreement: "this project" in this agreement refers to this audio source project; "User" means the user who signed this Agreement; "Official Music Platform" refers to the official platforms built in this project, including Cool Me, Cool Dog, Mi Gu and other music sources; "Copyright data" refers to data of which others have copyright, including but not limited to images, audio, names, etc.
1. The data source principle of this project is to pull data from the public servers of official music platforms, and display the data after simple screening and merging, so this project is not responsible for the accuracy of the data.
2. Copyright data may be generated during the use of this project, and this project does not own the copyright data. In order to avoid infringement, users must clear the copyright data generated during the use of this project within **24 hours**.
3. Any direct, indirect, special, accidental or consequential damages of any nature arising from the use of this project (including but not limited to damages caused by loss of goodwill, shutdown, computer failure or malfunction, or any and all other commercial damages or losses) shall be borne by the user.
4. This project is completely free of charge, and the open source is published on GitHub for people all over the world to learn and exchange technology. This project does not guarantee that the technology in the project may violate local laws and regulations. **It is forbidden to use this project in violation of local laws and regulations.** The user shall bear any illegal acts caused by the user knowing or not knowing that the local laws and regulations do not allow it, and this project will not bear any direct, indirect, special, accidental or consequential responsibilities.
If you use this project, you will accept the above agreement on your behalf.
Music platform is not easy, please respect copyright and support genuine.
This project is only used for the exploration and research of technical feasibility, and does not accept any commercial (including but not limited to advertising) cooperation and donation.
If you have any questions about this, please mail to:
helloplhm-qwq+outlook.com
folltoshe+foxmail.com
(please replace `+` to `@`)
## ✨Star trend chart
[![Stargazers over time](https://starchart.cc/MeoProject/lx-music-api-server.svg)](https://starchart.cc/MeoProject/lx-music-api-server)
## ⚙Contributor
[![Contributor](https://contrib.rocks/image?repo=MeoProject/lx-music-api-server)](https://github.com/MeoProject/lx-music-api-server/graphs/contributors)

View File

@ -1,16 +1,26 @@
import subprocess
import toml
import sys
import re
import os
def get_latest_tag():
return subprocess.check_output(['git', 'describe', '--abbrev=0', '--tags']).decode('utf-8').strip()
t = subprocess.check_output(['git', 'tag', '--sort=v:refname']).decode('utf-8').strip().split("\n")
return t[-1] if (t[-1] != toml.load("./pyproject.toml")["tool"]["poetry"]["version"]) else t[-2]
def get_specified_tag(index):
r = subprocess.check_output(['git', 'tag', '--sort=v:refname']).decode('utf-8').strip().split("\n")
n = []
for i in r:
if (i):
n.append(i.strip())
return n[index]
def get_changelog():
res = subprocess.check_output(
['git', 'log', f'{get_latest_tag()}..HEAD', '--pretty=format:"%h %s"']).decode('utf-8').strip()
cmd = ['git', 'log', f'{get_specified_tag(-1)}..HEAD', '--pretty=format:"%h %s"']
# print(cmd)
res = subprocess.check_output(cmd).decode('utf-8').strip()
res = res.split('\n')
featMsg = []
fixMsg = []
@ -20,17 +30,17 @@ def get_changelog():
noticeMsg = []
unknownMsg = []
for msg in res:
if (re.match('[a-f0-9]*.(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|notice)\:', msg[1:-1])):
if (re.match('[a-f0-9]*.(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|notice).*?\(?.*?\)?\:', msg[1:-1])):
msg = msg[1:-1]
if msg[8:].startswith('notice:'):
if msg[8:].startswith('notice'):
noticeMsg.append(msg)
elif msg[8:].startswith('feat:'):
elif msg[8:].startswith('feat'):
featMsg.append(msg)
elif msg[8:].startswith('fix:'):
elif msg[8:].startswith('fix'):
fixMsg.append(msg)
elif msg[8:].startswith('docs:'):
elif msg[8:].startswith('docs'):
docsMsg.append(msg)
elif msg[8:].startswith('build:'):
elif msg[8:].startswith('build'):
buildMsg.append(msg)
else:
otherMsg.append(msg)
@ -94,6 +104,8 @@ def build_test(fileName):
'-m',
'PyInstaller',
'-F',
'-i',
'res/icon.ico',
'--name',
fileName if fileName else f'lx-music-api-server_{sha}',
'main.py'])
@ -121,12 +133,14 @@ def build_release(fileName = ''):
'Please check if you forgetting to activate the virtualenv.', sep='\n')
sys.exit(1)
vername = get_latest_tag()
vername = toml.load("./pyproject.toml")["tool"]["poetry"]["version"]
popen = subprocess.Popen([get_python_path(),
'-m',
'PyInstaller',
'-F',
'-i',
'res/icon.ico',
'--name',
fileName if fileName else f'lx-music-api-server_{vername}',
'main.py'])

View File

@ -21,15 +21,16 @@ from . import config
from . import utils
from . import variable
def is_valid_utf8(text) -> bool:
try:
if isinstance(text, bytes):
text = text.decode('utf-8')
text = text.decode("utf-8")
# 判断是否为有效的utf-8字符串
if "\ufffe" in text:
return False
try:
text.encode('utf-8').decode('utf-8')
text.encode("utf-8").decode("utf-8")
return True
except UnicodeDecodeError:
return False
@ -37,42 +38,48 @@ def is_valid_utf8(text) -> bool:
logger.error(traceback.format_exc())
return False
def is_plain_text(text) -> bool:
# 判断是否为纯文本
pattern = re.compile(r'[^\x00-\x7F]')
pattern = re.compile(r"[^\x00-\x7F]")
return not bool(pattern.search(text))
def convert_dict_to_form_string(dic: dict) -> str:
# 将字典转换为表单字符串
return '&'.join([f'{k}={v}' for k, v in dic.items()])
return "&".join([f"{k}={v}" for k, v in dic.items()])
def log_plaintext(text: str) -> str:
if (text.startswith('{') and text.endswith('}')):
if text.startswith("{") and text.endswith("}"):
try:
text = json.loads(text)
except:
pass
elif (text.startswith('<xml') and text.endswith('>')): # xml data
elif text.startswith("<xml") and text.endswith(">"): # xml data
try:
text = f'xml: {utils.load_xml(text)}'
text = f"xml: {utils.load_xml(text)}"
except:
pass
return text
# 内置的UA列表
ua_list = [ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.39',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0 uacq',
'Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5666.197 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 uacq',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36'
]
ua_list = [
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.39",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0 uacq",
"Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5666.197 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 uacq",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36",
]
# 日志记录器
logger = log.log('http_utils')
logger = log.log("http_utils")
def request(url: str, options = {}) -> requests.Response:
'''
def request(url: str, options={}) -> requests.Response:
"""
Http请求主函数, 用于发送网络请求
- url: 需要请求的URL地址(必填)
- options: 请求的配置参数(可选, 留空时为GET请求, 总体与nodejs的请求的options填写差不多)
@ -86,13 +93,13 @@ def request(url: str, options = {}) -> requests.Response:
- cache-ignore: <list> 缓存忽略关键字
@ return: requests.Response类型的响应数据
'''
"""
# 缓存读取
cache_key = f'{url}{options}'
if (isinstance(options.get('cache-ignore'), list)):
for i in options.get('cache-ignore'):
cache_key = cache_key.replace(str(i), '')
options.pop('cache-ignore')
cache_key = f"{url}{options}"
if isinstance(options.get("cache-ignore"), list):
for i in options.get("cache-ignore"):
cache_key = cache_key.replace(str(i), "")
options.pop("cache-ignore")
cache_key = utils.createMD5(cache_key)
if options.get("cache") and options["cache"] != "no-cache":
cache = config.getCache("httpx", cache_key)
@ -105,76 +112,83 @@ def request(url: str, options = {}) -> requests.Response:
else:
cache_info = None
# 获取请求方法没有则默认为GET请求
try:
method = options['method']
options.pop('method')
method = options["method"].upper()
options.pop("method")
except Exception as e:
method = 'GET'
method = "GET"
# 获取User-Agent没有则从ua_list中随机选择一个
try:
d_lower = {k.lower(): v for k, v in options['headers'].items()}
useragent = d_lower['user-agent']
d_lower = {k.lower(): v for k, v in options["headers"].items()}
useragent = d_lower["user-agent"]
except:
try:
options['headers']['User-Agent'] = random.choice(ua_list)
options["headers"]["User-Agent"] = random.choice(ua_list)
except:
options['headers'] = {}
options['headers']['User-Agent'] = random.choice(ua_list)
options["headers"] = {}
options["headers"]["User-Agent"] = random.choice(ua_list)
# 检查是否在国内
if ((not variable.iscn) and (not options["headers"].get("X-Forwarded-For"))):
if (not variable.iscn) and (not options["headers"].get("X-Forwarded-For")):
options["headers"]["X-Forwarded-For"] = variable.fakeip
# 获取请求主函数
try:
reqattr = getattr(requests, method.lower())
except AttributeError:
raise AttributeError('Unsupported method: '+method)
raise AttributeError("Unsupported method: " + method)
# 请求前记录
logger.debug(f'HTTP Request: {url}\noptions: {options}')
logger.debug(f"HTTP Request: {url}\noptions: {options}")
# 转换body/form参数为原生的data参数并为form请求追加Content-Type头
if (method == 'POST') or (method == 'PUT'):
if options.get('body'):
options['data'] = options['body']
options.pop('body')
if options.get('form'):
options['data'] = convert_dict_to_form_string(options['form'])
options.pop('form')
options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
if (isinstance(options['data'], dict)):
options['data'] = json.dumps(options['data'])
if (method == "POST") or (method == "PUT"):
if options.get("body"):
options["data"] = options["body"]
options.pop("body")
if options.get("form"):
options["data"] = convert_dict_to_form_string(options["form"])
options.pop("form")
options["headers"]["Content-Type"] = "application/x-www-form-urlencoded"
if isinstance(options["data"], dict):
options["data"] = json.dumps(options["data"])
# 进行请求
try:
logger.info("-----start----- " + url)
req = reqattr(url, **options)
except Exception as e:
logger.error(f'HTTP Request runs into an Error: {log.highlight_error(traceback.format_exc())}')
logger.error(f"HTTP Request runs into an Error: {log.highlight_error(traceback.format_exc())}")
raise e
# 请求后记录
logger.debug(f'Request to {url} succeed with code {req.status_code}')
if (req.content.startswith(b'\x78\x9c') or req.content.startswith(b'\x78\x01')): # zlib headers
logger.debug(f"Request to {url} succeed with code {req.status_code}")
if req.content.startswith(b"\x78\x9c") or req.content.startswith(b"\x78\x01"): # zlib headers
try:
decompressed = zlib.decompress(req.content)
if (is_valid_utf8(decompressed)):
if is_valid_utf8(decompressed):
logger.debug(log_plaintext(decompressed.decode("utf-8")))
else:
logger.debug('response is not text binary, ignore logging it')
logger.debug("response is not text binary, ignore logging it")
except:
logger.debug('response is not text binary, ignore logging it')
logger.debug("response is not text binary, ignore logging it")
else:
if (is_valid_utf8(req.content)):
if is_valid_utf8(req.content):
logger.debug(log_plaintext(req.content.decode("utf-8")))
else:
logger.debug('response is not text binary, ignore logging it')
logger.debug("response is not text binary, ignore logging it")
# 缓存写入
if (cache_info and cache_info != "no-cache"):
if cache_info and cache_info != "no-cache":
cache_data = pickle.dumps(req)
expire_time = (cache_info if isinstance(cache_info, int) else 3600) + int(time.time())
config.updateCache("httpx", cache_key, {"expire": True, "time": expire_time, "data": utils.createBase64Encode(cache_data)})
expire_time = cache_info if isinstance(cache_info, int) else 3600
expire_at = int((time.time()) + expire_time)
config.updateCache(
"httpx",
cache_key,
{"expire": True, "time": expire_at, "data": utils.createBase64Encode(cache_data)},
expire_time,
)
logger.debug("缓存已更新: " + url)
def _json():
return json.loads(req.content)
setattr(req, 'json', _json)
setattr(req, "json", _json)
# 返回请求
return req
@ -184,21 +198,24 @@ def checkcn():
req = request("https://mips.kugou.com/check/iscn?&format=json")
body = utils.CreateObject(req.json())
variable.iscn = bool(body.flag)
if (not variable.iscn):
variable.fakeip = config.read_config('common.fakeip')
if not variable.iscn:
variable.fakeip = config.read_config("common.fakeip")
logger.info(f"您在非中国大陆服务器({body.country})上启动了项目已自动开启ip伪装")
logger.warning("此方式无法解决咪咕音乐的链接获取问题,您可以配置代理,服务器地址可在下方链接中找到\nhttps://hidemy.io/cn/proxy-list/?country=CN#list")
logger.warning(
"此方式无法解决咪咕音乐的链接获取问题,您可以配置代理,服务器地址可在下方链接中找到\nhttps://hidemy.io/cn/proxy-list/?country=CN#list"
)
except Exception as e:
logger.warning('检查服务器位置失败,已忽略')
logger.warning("检查服务器位置失败,已忽略")
logger.warning(traceback.format_exc())
class ClientResponse:
# 这个类为了方便aiohttp响应与requests响应的跨类使用也为了解决pickle无法缓存的问题
def __init__(self, status, content, headers):
self.status = status
self.content = content
self.headers = headers
self.text = content.decode("utf-8", errors='ignore')
self.text = content.decode("utf-8", errors="ignore")
def json(self):
return json.loads(self.content)
@ -211,8 +228,9 @@ async def convert_to_requests_response(aiohttp_response) -> ClientResponse:
return ClientResponse(status_code, content, headers)
async def AsyncRequest(url, options = {}) -> ClientResponse:
'''
async def AsyncRequest(url, options={}) -> ClientResponse:
"""
Http异步请求主函数, 用于发送网络请求
- url: 需要请求的URL地址(必填)
- options: 请求的配置参数(可选, 留空时为GET请求, 总体与nodejs的请求的options填写差不多)
@ -226,15 +244,15 @@ async def AsyncRequest(url, options = {}) -> ClientResponse:
- cache-ignore: <list> 缓存忽略关键字
@ return: common.Httpx.ClientResponse类型的响应数据
'''
if (not variable.aioSession):
"""
if not variable.aioSession:
variable.aioSession = aiohttp.ClientSession(trust_env=True)
# 缓存读取
cache_key = f'{url}{options}'
if (isinstance(options.get('cache-ignore'), list)):
for i in options.get('cache-ignore'):
cache_key = cache_key.replace(str(i), '')
options.pop('cache-ignore')
cache_key = f"{url}{options}"
if isinstance(options.get("cache-ignore"), list):
for i in options.get("cache-ignore"):
cache_key = cache_key.replace(str(i), "")
options.pop("cache-ignore")
cache_key = utils.createMD5(cache_key)
if options.get("cache") and options["cache"] != "no-cache":
cache = config.getCache("httpx_async", cache_key)
@ -248,75 +266,80 @@ async def AsyncRequest(url, options = {}) -> ClientResponse:
else:
cache_info = None
# 获取请求方法没有则默认为GET请求
try:
method = options['method']
options.pop('method')
method = options["method"]
options.pop("method")
except Exception as e:
method = 'GET'
method = "GET"
# 获取User-Agent没有则从ua_list中随机选择一个
try:
d_lower = {k.lower(): v for k, v in options['headers'].items()}
useragent = d_lower['user-agent']
d_lower = {k.lower(): v for k, v in options["headers"].items()}
useragent = d_lower["user-agent"]
except:
try:
options['headers']['User-Agent'] = random.choice(ua_list)
options["headers"]["User-Agent"] = random.choice(ua_list)
except:
options['headers'] = {}
options['headers']['User-Agent'] = random.choice(ua_list)
options["headers"] = {}
options["headers"]["User-Agent"] = random.choice(ua_list)
# 检查是否在国内
if ((not variable.iscn) and (not options["headers"].get("X-Forwarded-For"))):
if (not variable.iscn) and (not options["headers"].get("X-Forwarded-For")):
options["headers"]["X-Forwarded-For"] = variable.fakeip
# 获取请求主函数
try:
reqattr = getattr(variable.aioSession, method.lower())
except AttributeError:
raise AttributeError('Unsupported method: '+method)
raise AttributeError("Unsupported method: " + method)
# 请求前记录
logger.debug(f'HTTP Request: {url}\noptions: {options}')
logger.debug(f"HTTP Request: {url}\noptions: {options}")
# 转换body/form参数为原生的data参数并为form请求追加Content-Type头
if (method == 'POST') or (method == 'PUT'):
if options.get('body'):
options['data'] = options['body']
options.pop('body')
if options.get('form'):
options['data'] = convert_dict_to_form_string(options['form'])
options.pop('form')
options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
if (isinstance(options['data'], dict)):
options['data'] = json.dumps(options['data'])
if (method == "POST") or (method == "PUT"):
if options.get("body") is not None:
options["data"] = options["body"]
options.pop("body")
if options.get("form") is not None:
options["data"] = convert_dict_to_form_string(options["form"])
options.pop("form")
options["headers"]["Content-Type"] = "application/x-www-form-urlencoded"
if isinstance(options.get("data"), dict):
options["data"] = json.dumps(options["data"])
# 进行请求
try:
logger.info("-----start----- " + url)
req_ = await reqattr(url, **options)
except Exception as e:
logger.error(f'HTTP Request runs into an Error: {log.highlight_error(traceback.format_exc())}')
logger.error(f"HTTP Request runs into an Error: {log.highlight_error(traceback.format_exc())}")
raise e
# 请求后记录
logger.debug(f'Request to {url} succeed with code {req_.status}')
logger.debug(f"Request to {url} succeed with code {req_.status}")
# 为懒人提供的不用改代码移植的方法
# 才不是梓澄呢
req = await convert_to_requests_response(req_)
if (req.content.startswith(b'\x78\x9c') or req.content.startswith(b'\x78\x01')): # zlib headers
if req.content.startswith(b"\x78\x9c") or req.content.startswith(b"\x78\x01"): # zlib headers
try:
decompressed = zlib.decompress(req.content)
if (is_valid_utf8(decompressed)):
if is_valid_utf8(decompressed):
logger.debug(log_plaintext(decompressed.decode("utf-8")))
else:
logger.debug('response is not text binary, ignore logging it')
logger.debug("response is not text binary, ignore logging it")
except:
logger.debug('response is not text binary, ignore logging it')
logger.debug("response is not text binary, ignore logging it")
else:
if (is_valid_utf8(req.content)):
if is_valid_utf8(req.content):
logger.debug(log_plaintext(req.content.decode("utf-8")))
else:
logger.debug('response is not text binary, ignore logging it')
logger.debug("response is not text binary, ignore logging it")
# 缓存写入
if (cache_info and cache_info != "no-cache"):
if cache_info and cache_info != "no-cache":
cache_data = pickle.dumps(req)
expire_time = (cache_info if isinstance(cache_info, int) else 3600) + int(time.time())
config.updateCache("httpx_async", cache_key, {"expire": True, "time": expire_time, "data": utils.createBase64Encode(cache_data)})
expire_time = cache_info if isinstance(cache_info, int) else 3600
expire_at = int((time.time()) + expire_time)
config.updateCache(
"httpx_async",
cache_key,
{"expire": True, "time": expire_at, "data": utils.createBase64Encode(cache_data)},
expire_time,
)
logger.debug("缓存已更新: " + url)
# 返回请求
return req

View File

@ -13,297 +13,70 @@ import os
import traceback
import sys
import sqlite3
import shutil
import ruamel.yaml as yaml_
from . import variable
from .log import log
from . import default_config
import threading
import redis
logger = log('config_manager')
logger = log("config_manager")
# 创建线程本地存储对象
local_data = threading.local()
local_cache = threading.local()
local_redis = threading.local()
def get_data_connection():
# 检查线程本地存储对象是否存在连接对象,如果不存在则创建一个新的连接对象
if not hasattr(local_data, 'connection'):
local_data.connection = sqlite3.connect('data.db')
return local_data.connection
# 创建线程本地存储对象
local_cache = threading.local()
def get_cache_connection():
# 检查线程本地存储对象是否存在连接对象,如果不存在则创建一个新的连接对象
if not hasattr(local_cache, 'connection'):
local_cache.connection = sqlite3.connect('cache.db')
return local_cache.connection
def get_redis_connection():
return local_redis.connection
def handle_connect_db():
try:
local_data.connection = sqlite3.connect("./config/data.db")
if read_config("common.cache.adapter") == "redis":
host = read_config("common.cache.redis.host")
port = read_config("common.cache.redis.port")
user = read_config("common.cache.redis.user")
password = read_config("common.cache.redis.password")
db = read_config("common.cache.redis.db")
client = redis.Redis(host=host, port=port, username=user, password=password, db=db)
if not client.ping():
raise
local_redis.connection = client
else:
local_cache.connection = sqlite3.connect("./cache.db")
except:
logger.error("连接数据库失败")
sys.exit(1)
class ConfigReadException(Exception):
pass
default = {
"common": {
"host": "0.0.0.0",
"_host-desc": "服务器启动时所使用的HOST地址",
"ports": [ 9763 ],
"_ports-desc": "服务器启动时所使用的端口",
"ssl_info": {
"desc": "服务器https配置is_https是这个服务器是否是https服务器如果你使用了反向代理来转发这个服务器如果它使用了https也请将它设置为true",
"is_https": False,
"enable": False,
"ssl_ports": [ 443 ],
"path": {
"desc": "ssl证书的文件地址",
"cert": "/path/to/your/cer",
"privkey": "/path/to/your/private/key",
},
},
"reverse_proxy": {
"desc": "针对类似于nginx一类的反代的配置",
"allow_proxy": True,
"_allow_proxy-desc": "是否允许反代",
"proxy_whitelist_remote": [
"反代时允许的ip来源列表通常为127.0.0.1",
"127.0.0.1"
],
"real_ip_header": 'X-Real-IP',
"_real_ip_header-desc": "反代来源ip的来源头不懂请保持默认",
},
"debug_mode": False,
"_debug_mode-desc": "是否开启调试模式",
"log_length_limit": 500,
"_log_length_limit-desc": "单条日志长度限制",
"fakeip": "1.0.1.114",
"_fakeip-desc": "服务器在海外时的IP伪装值",
"proxy": {
"enable": False,
"http_value": "http://127.0.0.1:7890",
"https_value": "http://127.0.0.1:7890",
},
"_proxy-desc": "代理配置HTTP与HTTPS协议需分开配置",
"log_file": True,
"_log_file-desc": "是否开启日志文件",
'cookiepool': False,
'_cookiepool-desc': '是否开启cookie池这将允许用户配置多个cookie并在请求时随机使用一个启用后请在module.cookiepool中配置cookie在user处配置的cookie会被忽略cookiepool中格式统一为列表嵌套user处的cookie的字典',
"allow_download_script": True,
'_allow_download_script-desc': '是否允许直接从服务端下载脚本,开启后可以直接访问 /script?key=你的请求key 下载脚本',
"download_config": {
"desc": "源脚本的相关配置dev为是否启用开发模式",
"name": "修改为你的源脚本名称",
"intro": "修改为你的源脚本描述",
"author": "修改为你的源脚本作者",
"version": "修改为你的源版本",
"filename": "lx-music-source.js",
"_filename-desc": "客户端保存脚本时的文件名(可能因浏览器不同出现不一样的情况)",
"dev": True,
"quality": {
"kw": ["128k"],
"kg": ["128k"],
"tx": ["128k"],
"wy": ["128k"],
"mg": ["128k"],
}
},
},
"security": {
"rate_limit": {
"global": 0,
"ip": 0,
"desc": "请求速率限制global为全局ip为单个ip填入的值为至少间隔多久才能进行一次请求单位不限制请填为0"
},
"key": {
"enable": False,
"_enable-desc": "是否开启请求key开启后只有请求头中包含key且值一样时可以访问API",
"ban": True,
"value": "114514",
},
"whitelist_host": [
"localhost",
"0.0.0.0",
"127.0.0.1",
],
"_whitelist_host-desc": "强制白名单HOST不需要加端口号即不受其他安全设置影响的HOST",
"check_lxm": False,
"_check_lxm-desc": "是否检查lxm请求头正常的LX Music请求时都会携带这个请求头",
"lxm_ban": True,
"_lxm_ban-desc": "lxm请求头不存在或不匹配时是否将用户IP加入黑名单",
"allowed_host": {
"desc": "HOST允许列表启用后只允许列表内的HOST访问服务器不需要加端口号",
"enable": False,
"blacklist": {
"desc": "当用户访问的HOST并不在允许列表中时是否将请求IP加入黑名单长度单位",
"enable": False,
"length": 0,
},
"list": [
"localhost",
"0.0.0.0",
"127.0.0.1",
],
},
"banlist": {
"desc": "是否启用黑名单(全局设置,关闭后已存储的值并不受影响,但不会再检查)",
"enable": True,
"expire": {
"desc": "是否启用黑名单IP过期关闭后其他地方的配置会失效",
"enable": True,
"length": 86400 * 7, # 七天
},
},
},
"module": {
"kg": {
"desc": "酷狗音乐相关配置",
"client": {
"desc": "客户端请求配置,不懂请保持默认,修改请统一为字符串格式",
"appid": "1005",
"_appid-desc": "酷狗音乐的appid官方安卓为1005官方PC为1001",
"signatureKey": "OIlwieks28dk2k092lksi2UIkp",
"_signatureKey-desc": "客户端signature采用的key值需要与appid对应",
"clientver": "12029",
"_clientver-desc": "客户端versioncodepidversionsecret可能随此值而变化",
"pidversionsecret": "57ae12eb6890223e355ccfcb74edf70d",
"_pidversionsecret-desc": "获取URL时所用的key值计算验证值",
},
"tracker": {
"desc": "trackerapi请求配置不懂请保持默认修改请统一为字符串格式",
"host": "https://gateway.kugou.com",
"path": "/v5/url",
"version": "v5",
"x-router": {
"desc": "当host为gateway.kugou.com时需要追加此头为tracker类地址时则不需要",
"enable": True,
"value": "tracker.kugou.com",
},
"extra_params": {},
"_extra_params-desc": "自定义添加的param优先级大于默认填写类型为普通的JSON数据会自动转换为请求param",
},
"user": {
"desc": "此处内容请统一抓包获取需要vip账号来获取会员歌曲如果没有请留为空值mid必填可以瞎填一段数字",
"token": "",
"userid": "0",
"mid": "114514",
}
},
"tx": {
"desc": "QQ音乐相关配置",
"vkeyserver": {
"desc": "请求官方api时使用的guiduin等信息不需要与cookie中信息一致",
"guid": "114514",
"uin": "10086",
},
"user": {
"desc": "用户数据可以通过浏览器获取需要vip账号来获取会员歌曲如果没有请留为空值qqmusic_key可以从Cookie中/客户端的请求体中comm.authst获取",
"qqmusic_key": "",
"uin": "",
"_uin-desc": "key对应的QQ号",
'refresh_login': {
'desc': '刷新登录相关配置enable是否启动interval刷新间隔',
'enable': False,
'interval': 86000
}
},
"cdnaddr": "http://ws.stream.qqmusic.qq.com/",
},
"wy": {
"desc": "网易云音乐相关配置",
"user": {
"desc": "账号cookie数据可以通过浏览器获取需要vip账号来获取会员歌曲如果没有请留为空值",
"cookie": ""
},
},
"mg": {
"desc": "咪咕音乐相关配置",
"user": {
"desc": "研究不深后两项自行抓包获取在header里",
"aversionid": "",
"token": "",
"osversion": "10",
"useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36",
},
},
"kw": {
"desc": "酷我音乐相关配置proto支持值['bd-api', 'kuwodes']",
"proto": "bd-api",
"user": {
"uid": "0",
"token": "",
"device_id": "0",
},
"des": {
"desc": "kuwodes接口mobi, nmobi一类的加密相关配置",
"f": "kuwo",
"need_encrypt": True,
"param填写注释": "{songId}为歌曲id, {map_quality}为map后的歌曲音质酷我规范, {raw_quality}为请求时的歌曲音质LX规范, {ext}为歌曲文件扩展名",
"params": "type=convert_url_with_sign&rid={songId}&quality={map_quality}&ext={ext}",
"host": "nmobi.kuwo.cn",
"path": "mobi.s",
"response_types": ['这里是reponse_type的所有支持值当设置为json时会使用到下面的两个值来获取url/bitrate如果为text则为传统的逐行解析方式', 'json', 'text'],
"response_type": "json",
"url_json_path": "data.url",
"bitrate_json_path": "data.bitrate",
"headers": {
"User-Agent": 'okhttp/3.10.0'
}
}
},
'cookiepool': {
'kg': [
{
'userid': '0',
'token': '',
'mid': '114514',
},
],
'tx': [
{
'qqmusic_key': '',
'uin': '',
'refresh_login': {
'desc': 'cookie池中对于此账号刷新登录的配置账号间互不干扰',
'enable': False,
'interval': 86000,
}
}
],
'wy': [
{
'cookie': '',
}
],
'mg': [
{
'aversionid': '',
'token': '',
'osversion': '10',
'useragent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36',
}
],
'kw': [
{
"uid": "0",
"token": "",
"device_id": "0",
},
]
},
},
}
yaml = yaml_.YAML()
default_str = default_config.default
default = yaml.load(default_str)
def handle_default_config():
with open("./config.json", "w", encoding="utf-8") as f:
f.write(json.dumps(default, indent=2, ensure_ascii=False,
escape_forward_slashes=False))
f.close()
if (not os.getenv('build')):
logger.info('首次启动或配置文件被删除,已创建默认配置文件')
with open("./config/config.yml", "w", encoding="utf-8") as f:
f.write(default_str)
if not os.getenv("build"):
logger.info(
f'\n建议您到{variable.workdir + os.path.sep}config.json修改配置后重新启动服务器')
f"首次启动或配置文件被删除,已创建默认配置文件\n建议您到{variable.workdir + os.path.sep}config.yml修改配置后重新启动服务器"
)
return default
@ -344,8 +117,7 @@ def save_data(config_data):
# Insert the new configuration data into the 'data' table
for key, value in config_data.items():
cursor.execute(
"INSERT INTO data (key, value) VALUES (?, ?)", (key, json.dumps(value)))
cursor.execute("INSERT INTO data (key, value) VALUES (?, ?)", (key, json.dumps(value)))
conn.commit()
@ -354,51 +126,69 @@ def save_data(config_data):
logger.error(traceback.format_exc())
def handleBuildRedisKey(module, key):
prefix = read_config("common.cache.redis.key_prefix")
return f"{prefix}:{module}:{key}"
def getCache(module, key):
try:
# 连接到数据库(如果数据库不存在,则会自动创建)
conn = get_cache_connection()
# 创建一个游标对象
cursor = conn.cursor()
cursor.execute("SELECT data FROM cache WHERE module=? AND key=?",
(module, key))
result = cursor.fetchone()
if result:
cache_data = json.loads(result[0])
cache_data["time"] = int(cache_data["time"])
if (not cache_data['expire']):
return cache_data
if (int(time.time()) < int(cache_data['time'])):
if read_config("common.cache.adapter") == "redis":
redis = get_redis_connection()
key = handleBuildRedisKey(module, key)
result = redis.get(key)
if result:
cache_data = json.loads(result)
return cache_data
else:
# 连接到数据库(如果数据库不存在,则会自动创建)
conn = get_cache_connection()
# 创建一个游标对象
cursor = conn.cursor()
cursor.execute("SELECT data FROM cache WHERE module=? AND key=?", (module, key))
result = cursor.fetchone()
if result:
cache_data = json.loads(result[0])
cache_data["time"] = int(cache_data["time"])
if not cache_data["expire"]:
return cache_data
if int(time.time()) < int(cache_data["time"]):
return cache_data
except:
pass
# traceback.print_exc()
return False
return None
def updateCache(module, key, data):
def updateCache(module, key, data, expire=None):
try:
# 连接到数据库(如果数据库不存在,则会自动创建)
conn = get_cache_connection()
# 创建一个游标对象
cursor = conn.cursor()
cursor.execute(
"SELECT data FROM cache WHERE module=? AND key=?", (module, key))
result = cursor.fetchone()
if result:
cursor.execute(
"UPDATE cache SET data = ? WHERE module = ? AND key = ?", (json.dumps(data), module, key))
if read_config("common.cache.adapter") == "redis":
redis = get_redis_connection()
key = handleBuildRedisKey(module, key)
redis.set(key, json.dumps(data), ex=expire if expire and expire > 0 else None)
else:
cursor.execute(
"INSERT INTO cache (module, key, data) VALUES (?, ?, ?)", (module, key, json.dumps(data)))
conn.commit()
# 连接到数据库(如果数据库不存在,则会自动创建)
conn = get_cache_connection()
# 创建一个游标对象
cursor = conn.cursor()
cursor.execute("SELECT data FROM cache WHERE module=? AND key=?", (module, key))
result = cursor.fetchone()
if result:
cursor.execute(
"UPDATE cache SET data = ? WHERE module = ? AND key = ?", (json.dumps(data), module, key)
)
else:
cursor.execute(
"INSERT INTO cache (module, key, data) VALUES (?, ?, ?)", (module, key, json.dumps(data))
)
conn.commit()
except:
logger.error('缓存写入遇到错误…')
logger.error("缓存写入遇到错误…")
logger.error(traceback.format_exc())
@ -406,13 +196,13 @@ def resetRequestTime(ip):
config_data = load_data()
try:
try:
config_data['requestTime'][ip] = 0
config_data["requestTime"][ip] = 0
except KeyError:
config_data['requestTime'] = {}
config_data['requestTime'][ip] = 0
config_data["requestTime"] = {}
config_data["requestTime"][ip] = 0
save_data(config_data)
except:
logger.error('配置写入遇到错误…')
logger.error("配置写入遇到错误…")
logger.error(traceback.format_exc())
@ -420,20 +210,20 @@ def updateRequestTime(ip):
try:
config_data = load_data()
try:
config_data['requestTime'][ip] = time.time()
config_data["requestTime"][ip] = time.time()
except KeyError:
config_data['requestTime'] = {}
config_data['requestTime'][ip] = time.time()
config_data["requestTime"] = {}
config_data["requestTime"][ip] = time.time()
save_data(config_data)
except:
logger.error('配置写入遇到错误...')
logger.error("配置写入遇到错误...")
logger.error(traceback.format_exc())
def getRequestTime(ip):
config_data = load_data()
try:
value = config_data['requestTime'][ip]
value = config_data["requestTime"][ip]
except:
value = 0
return value
@ -441,7 +231,7 @@ def getRequestTime(ip):
def read_data(key):
config = load_data()
keys = key.split('.')
keys = key.split(".")
value = config
for k in keys:
if k not in value and keys.index(k) != len(keys) - 1:
@ -456,7 +246,7 @@ def read_data(key):
def write_data(key, value):
config = load_data()
keys = key.split('.')
keys = key.split(".")
current = config
for k in keys[:-1]:
if k not in current:
@ -471,7 +261,7 @@ def write_data(key, value):
def push_to_list(key, obj):
config = load_data()
keys = key.split('.')
keys = key.split(".")
current = config
for k in keys[:-1]:
if k not in current:
@ -488,10 +278,10 @@ def push_to_list(key, obj):
def write_config(key, value):
config = None
with open('config.json', 'r', encoding='utf-8') as f:
config = json.load(f)
with open("./config/config.yml", "r", encoding="utf-8") as f:
config = yaml_.YAML().load(f)
keys = key.split('.')
keys = key.split(".")
current = config
for k in keys[:-1]:
if k not in current:
@ -499,17 +289,21 @@ def write_config(key, value):
current = current[k]
current[keys[-1]] = value
variable.config = config
with open('config.json', 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False,
escape_forward_slashes=False)
f.close()
# 设置保留注释和空行的参数
y = yaml_.YAML()
y.preserve_quotes = True
y.preserve_blank_lines = True
# 写入配置并保留注释和空行
with open("./config/config.yml", "w", encoding="utf-8") as f:
y.dump(config, f)
def read_default_config(key):
try:
config = default
keys = key.split('.')
keys = key.split(".")
value = config
for k in keys:
if isinstance(value, dict):
@ -530,7 +324,7 @@ def read_default_config(key):
def _read_config(key):
try:
config = variable.config
keys = key.split('.')
keys = key.split(".")
value = config
for k in keys:
if isinstance(value, dict):
@ -551,7 +345,7 @@ def _read_config(key):
def read_config(key):
try:
config = variable.config
keys = key.split('.')
keys = key.split(".")
value = config
for k in keys:
if isinstance(value, dict):
@ -567,23 +361,23 @@ def read_config(key):
return value
except:
default_value = read_default_config(key)
if (isinstance(default_value, type(None))):
logger.warning(f'配置文件{key}不存在')
if isinstance(default_value, type(None)):
logger.warning(f"配置文件{key}不存在")
else:
for i in range(len(keys)):
tk = '.'.join(keys[:(i + 1)])
tk = ".".join(keys[: (i + 1)])
tkvalue = _read_config(tk)
logger.debug(f'configfix: 读取配置文件{tk}的值:{tkvalue}')
if ((tkvalue is None) or (tkvalue == {})):
logger.debug(f"configfix: 读取配置文件{tk}的值:{tkvalue}")
if (tkvalue is None) or (tkvalue == {}):
write_config(tk, read_default_config(tk))
logger.info(f'配置文件{tk}不存在,已创建')
logger.info(f"配置文件{tk}不存在,已创建")
return default_value
def write_data(key, value):
config = load_data()
keys = key.split('.')
keys = key.split(".")
current = config
for k in keys[:-1]:
if k not in current:
@ -595,146 +389,166 @@ def write_data(key, value):
save_data(config)
def initConfig():
def init_config():
if not os.path.exists("./config"):
os.mkdir("config")
if os.path.exists("./config.json"):
shutil.move("config.json", "./config")
if os.path.exists("./data.db"):
shutil.move("./data.db", "./config")
if os.path.exists("./config/config.json"):
os.rename("./config/config.json", "./config/config.json.bak")
handle_default_config()
logger.warning("json配置文件已不再使用已将其重命名为config.json.bak")
logger.warning("配置文件不会自动更新(因为变化太大),请手动修改配置文件重启服务器")
sys.exit(0)
try:
with open("./config.json", "r", encoding="utf-8") as f:
with open("./config/config.yml", "r", encoding="utf-8") as f:
try:
variable.config = json.loads(f.read())
if (not isinstance(variable.config, dict)):
logger.warning('配置文件并不是一个有效的字典,使用默认值')
variable.config = yaml.load(f.read())
if not isinstance(variable.config, dict):
logger.warning("配置文件并不是一个有效的字典,使用默认值")
variable.config = default
with open("./config.json", "w", encoding="utf-8") as f:
f.write(json.dumps(variable.config, indent=2,
ensure_ascii=False, escape_forward_slashes=False))
with open("./config/config.yml", "w", encoding="utf-8") as f:
yaml.dump(variable.config, f)
f.close()
except:
if os.path.getsize("./config.json") != 0:
logger.error("配置文件加载失败,请检查是否遵循JSON语法规范")
if os.path.getsize("./config/config.yml") != 0:
logger.error("配置文件加载失败,请检查是否遵循YAML语法规范")
sys.exit(1)
else:
variable.config = handle_default_config()
except FileNotFoundError:
variable.config = handle_default_config()
# print(variable.config)
variable.log_length_limit = read_config('common.log_length_limit')
variable.debug_mode = read_config('common.debug_mode')
variable.log_length_limit = read_config("common.log_length_limit")
variable.debug_mode = read_config("common.debug_mode")
logger.debug("配置文件加载成功")
conn = sqlite3.connect('cache.db')
# 尝试连接数据库
handle_connect_db()
conn = sqlite3.connect("./cache.db")
# 创建一个游标对象
cursor = conn.cursor()
# 创建一个表来存储缓存数据
cursor.execute('''CREATE TABLE IF NOT EXISTS cache
cursor.execute(
"""CREATE TABLE IF NOT EXISTS cache
(id INTEGER PRIMARY KEY AUTOINCREMENT,
module TEXT NOT NULL,
key TEXT NOT NULL,
data TEXT NOT NULL)''')
data TEXT NOT NULL)"""
)
conn.close()
conn2 = sqlite3.connect('data.db')
conn2 = sqlite3.connect("./config/data.db")
# 创建一个游标对象
cursor2 = conn2.cursor()
cursor2.execute('''CREATE TABLE IF NOT EXISTS data
cursor2.execute(
"""CREATE TABLE IF NOT EXISTS data
(key TEXT PRIMARY KEY,
value TEXT)''')
value TEXT)"""
)
conn2.close()
logger.debug('数据库初始化成功')
logger.debug("数据库初始化成功")
# handle data
all_data_keys = {'banList': [], 'requestTime': {}, 'banListRaw': []}
all_data_keys = {"banList": [], "requestTime": {}, "banListRaw": []}
data = load_data()
if (data == {}):
write_data('banList', [])
write_data('requestTime', {})
logger.info('数据库内容为空,已写入默认值')
if data == {}:
write_data("banList", [])
write_data("requestTime", {})
logger.info("数据库内容为空,已写入默认值")
for k, v in all_data_keys.items():
if (k not in data):
if k not in data:
write_data(k, v)
logger.info(f'数据库中不存在{k},已创建')
logger.info(f"数据库中不存在{k},已创建")
# 处理代理配置
if (read_config('common.proxy.enable')):
if (read_config('common.proxy.http_value')):
os.environ['http_proxy'] = read_config('common.proxy.http_value')
logger.info('HTTP协议代理地址: ' +
read_config('common.proxy.http_value'))
if (read_config('common.proxy.https_value')):
os.environ['https_proxy'] = read_config('common.proxy.https_value')
logger.info('HTTPS协议代理地址: ' +
read_config('common.proxy.https_value'))
logger.info('代理功能已开启,请确保代理地址正确,否则无法连接网络')
if read_config("common.proxy.enable"):
if read_config("common.proxy.http_value"):
os.environ["http_proxy"] = read_config("common.proxy.http_value")
logger.info("HTTP协议代理地址: " + read_config("common.proxy.http_value"))
if read_config("common.proxy.https_value"):
os.environ["https_proxy"] = read_config("common.proxy.https_value")
logger.info("HTTPS协议代理地址: " + read_config("common.proxy.https_value"))
logger.info("代理功能已开启,请确保代理地址正确,否则无法连接网络")
# cookie池
if (read_config('common.cookiepool')):
logger.info('已启用cookie池功能请确定配置的cookie都能正确获取链接')
logger.info('传统的源 - 单用户cookie配置将被忽略')
logger.info('所以即使某个源你只有一个cookie也请填写到cookiepool对应的源中否则将无法使用该cookie')
if read_config("common.cookiepool"):
logger.info("已启用cookie池功能请确定配置的cookie都能正确获取链接")
logger.info("传统的源 - 单用户cookie配置将被忽略")
logger.info("所以即使某个源你只有一个cookie也请填写到cookiepool对应的源中否则将无法使用该cookie")
variable.use_cookie_pool = True
# 移除已经过期的封禁数据
banlist = read_data('banList')
banlistRaw = read_data('banListRaw')
banlist = read_data("banList")
banlistRaw = read_data("banListRaw")
count = 0
for b in banlist:
if (b['expire'] and (time.time() > b['expire_time'])):
if b["expire"] and (time.time() > b["expire_time"]):
count += 1
banlist.remove(b)
if (b['ip'] in banlistRaw):
banlistRaw.remove(b['ip'])
write_data('banList', banlist)
write_data('banListRaw', banlistRaw)
if (count != 0):
logger.info(f'已移除{count}条过期封禁数据')
if b["ip"] in banlistRaw:
banlistRaw.remove(b["ip"])
write_data("banList", banlist)
write_data("banListRaw", banlistRaw)
if count != 0:
logger.info(f"已移除{count}条过期封禁数据")
# 处理旧版数据库的banListRaw
banlist = read_data('banList')
banlistRaw = read_data('banListRaw')
if (banlist != [] and banlistRaw == []):
banlist = read_data("banList")
banlistRaw = read_data("banListRaw")
if banlist != [] and banlistRaw == []:
for b in banlist:
banlistRaw.append(b['ip'])
banlistRaw.append(b["ip"])
return
def ban_ip(ip_addr, ban_time=-1):
if read_config('security.banlist.enable'):
banList = read_data('banList')
banList.append({
'ip': ip_addr,
'expire': read_config('security.banlist.expire.enable'),
'expire_time': read_config('security.banlist.expire.length') if (ban_time == -1) else ban_time,
})
write_data('banList', banList)
banListRaw = read_data('banListRaw')
if (ip_addr not in banListRaw):
if read_config("security.banlist.enable"):
banList = read_data("banList")
banList.append(
{
"ip": ip_addr,
"expire": read_config("security.banlist.expire.enable"),
"expire_time": read_config("security.banlist.expire.length") if (ban_time == -1) else ban_time,
}
)
write_data("banList", banList)
banListRaw = read_data("banListRaw")
if ip_addr not in banListRaw:
banListRaw.append(ip_addr)
write_data('banListRaw', banListRaw)
write_data("banListRaw", banListRaw)
else:
if (variable.banList_suggest < 10):
if variable.banList_suggest < 10:
variable.banList_suggest += 1
logger.warning('黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求')
logger.warning("黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求")
def check_ip_banned(ip_addr):
if read_config('security.banlist.enable'):
banList = read_data('banList')
banlistRaw = read_data('banListRaw')
if (ip_addr in banlistRaw):
if read_config("security.banlist.enable"):
banList = read_data("banList")
banlistRaw = read_data("banListRaw")
if ip_addr in banlistRaw:
for b in banList:
if (b['ip'] == ip_addr):
if (b['expire']):
if (b['expire_time'] > int(time.time())):
if b["ip"] == ip_addr:
if b["expire"]:
if b["expire_time"] > int(time.time()):
return True
else:
banList.remove(b)
banlistRaw.remove(b['ip'])
write_data('banListRaw', banlistRaw)
write_data('banList', banList)
banlistRaw.remove(b["ip"])
write_data("banListRaw", banlistRaw)
write_data("banList", banList)
return False
else:
return True
@ -744,10 +558,10 @@ def check_ip_banned(ip_addr):
else:
return False
else:
if (variable.banList_suggest <= 10):
if variable.banList_suggest <= 10:
variable.banList_suggest += 1
logger.warning('黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求')
logger.warning("黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求")
return False
initConfig()
init_config()

236
common/default_config.py Normal file
View File

@ -0,0 +1,236 @@
default = """\
common:
hosts: # 服务器监听地址
- 0.0.0.0
# - '::' # 取消这一行的注释,启用 ipv6 监听
ports: # 服务器启动时所使用的端口
- 9763
ssl_info: # 服务器https配置
# 这个服务器是否是https服务器如果你使用了反向代理来转发这个服务器如果它使用了https也请将它设置为true
is_https: false
# python原生https监听
enable: false
ssl_ports:
- 443
path: # ssl证书的文件地址
cert: /path/to/your/cer
privkey: /path/to/your/private/key
reverse_proxy: # 针对类似于nginx一类的反代的配置
allow_public_ip: false # 允许来自公网的转发
allow_proxy: true # 是否允许反代
real_ip_header: X-Real-IP # 反代来源ip的来源头不懂请保持默认
debug_mode: false # 是否开启调试模式
log_length_limit: 500 # 单条日志长度限制
fakeip: 1.0.1.114 # 服务器在海外时的IP伪装值
proxy: # 代理配置HTTP与HTTPS协议需分开配置
enable: false
http_value: http://127.0.0.1:7890
https_value: http://127.0.0.1:7890
log_file: true # 是否存储日志文件
cookiepool: false # 是否开启cookie池这将允许用户配置多个cookie并在请求时随机使用一个启用后请在module.cookiepool中配置cookie在user处配置的cookie会被忽略cookiepool中格式统一为列表嵌套user处的cookie的字典
allow_download_script: true # 是否允许直接从服务端下载脚本,开启后可以直接访问 /script?key=你的请求key 下载脚本
download_config: # 源脚本的相关配置
name: 修改为你的源脚本名称
intro: 修改为你的源脚本描述
author: 修改为你的源脚本作者
version: 修改为你的源版本
filename: lx-music-source.js # 客户端保存脚本时的文件名(可能因浏览器不同出现不一样的情况)
dev: true # 是否启用开发模式
update: true # 是否开启脚本更新提醒
# 可用参数
# {updateUrl}为更新地址(带请求key)
# {url}为请求时的url(不带请求的param)
# {key}为请求时携带的key
updateMsg: "源脚本有更新啦,更新地址:\\n{updateUrl}"
quality:
kw: [128k]
kg: [128k]
tx: [128k]
wy: [128k]
mg: [128k]
local_music: # 服务器侧本地音乐相关配置,如果需要使用此功能请确保你的带宽足够
audio_path: ./audio
temp_path: ./temp
# 缓存配置
cache:
# 适配器 [redis,sql]
adapter: sql
# redis 配置
redis:
host: 127.0.0.1
port: 6379
db: 0
user: ""
password: ""
key_prefix: "LXAPISERVER"
security:
rate_limit: # 请求速率限制 填入的值为至少间隔多久才能进行一次请求单位不限制请填为0
global: 0 # 全局
ip: 0 # 单个IP
key:
enable: false # 是否开启请求key开启后只有请求头中包含key且值一样时可以访问API
ban: true
values: # 填自己所有的请求key
- "114514"
whitelist_host: # 强制白名单HOST不需要加端口号即不受其他安全设置影响的HOST
- localhost
- 0.0.0.0
- 127.0.0.1
check_lxm: false # 是否检查lxm请求头正常的LX Music在内置源请求时都会携带这个请求头
lxm_ban: true # lxm请求头不存在或不匹配时是否将用户IP加入黑名单
allowed_host: # HOST允许列表启用后只允许列表内的HOST访问服务器不需要加端口号
enable: false
blacklist: # 当用户访问的HOST并不在允许列表中时是否将请求IP加入黑名单长度单位
enable: false
length: 0
list:
- localhost
- 0.0.0.0
- 127.0.0.1
banlist: # 是否启用黑名单(全局设置,关闭后已存储的值并不受影响,但不会再检查)
enable: true
expire: # 是否启用黑名单IP过期关闭后其他地方的配置会失效
enable: true
length: 604800
module:
kg: # 酷狗音乐相关配置
enable: true # 是否开启本平台服务
client: # 客户端请求配置,不懂请保持默认,修改请统一为字符串格式
appid: "1005" # 酷狗音乐的appid官方安卓为1005官方PC为1001
signatureKey: OIlwieks28dk2k092lksi2UIkp # 客户端signature采用的key值需要与appid对应
clientver: "12029" # 客户端versioncodepidversionsecret可能随此值而变化
pidversionsecret: 57ae12eb6890223e355ccfcb74edf70d # 获取URL时所用的key值计算验证值
pid: "2" # url接口的pid
tracker: # trackerapi请求配置不懂请保持默认修改请统一为字符串格式
host: https://gateway.kugou.com
path: /v5/url
version: v5
x-router: # 当host为gateway.kugou.com时需要追加此头为tracker类地址时则不需要
enable: true
value: tracker.kugou.com
extra_params: {} # 自定义添加的param优先级大于默认填写类型为普通的JSON数据会自动转换为请求param
user: # 此处内容请统一抓包获取(/v5/url需要vip账号来获取会员歌曲如果没有请留为空值mid必填可以瞎填一段数字
token: ""
userid: "0"
mid: "114514"
lite_sign_in: # 是否启用概念版自动签到仅在appid=3116时运行
enable: false
interval: 86400
mixsongmid: # mix_songmid的获取方式, 默认auto, 可以改成一个数字手动
value: auto
refresh_login: # 酷狗token保活相关配置30天不刷新token会失效enable是否启动interval刷新间隔。默认appid=1005时有效3116需要更换signatureKey
enable: false
interval: 86000
login_url: http://login.user.kugou.com/v4/login_by_token
tx: # QQ音乐相关配置
enable: true # 是否开启本平台服务
vkeyserver: # 请求官方api时使用的guiduin等信息不需要与cookie中信息一致
guid: "114514"
uin: "10086"
user: # 用户数据可以通过浏览器获取需要vip账号来获取会员歌曲如果没有请留为空值qqmusic_key可以从Cookie中/客户端的请求体中comm.authst获取
qqmusic_key: ""
uin: "" # key对应的QQ号
refresh_key: "" # 刷新失活 qqmusic_key
refresh_login: # 刷新登录相关配置enable是否启动interval刷新间隔
enable: false
interval: 86000
cdnaddr: http://ws.stream.qqmusic.qq.com/
wy: # 网易云音乐相关配置, proto支持值: ['offcial', 'ncmapi']
enable: true # 是否开启本平台服务
proto: offcial
user:
cookie: "" # 账号cookie数据可以通过浏览器获取需要vip账号来获取会员歌曲如果没有请留为空值
refresh_login:
enable: false
interval: 86400
ncmapi:
api_url: "" # NeteaseCloudMusicApi的URL, 自行参考https://gitlab.com/Binaryify/neteasecloudmusicapi搭建
mg: # 咪咕音乐相关配置
enable: true # 是否开启本平台服务
user: # 研究不深后两项自行抓包获取网页端cookie
by: ""
session: ""
useragent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36
refresh_login: # cookie保活配置
enable: false
interval: 86400
kw: # 酷我音乐相关配置proto支持值['bd-api', 'kuwodes']
enable: true # 是否开启本平台服务
proto: bd-api
user:
uid: "0"
token: ""
device_id: "0"
des: # kuwodes接口mobi, nmobi一类的加密相关配置
f: kuwo
need_encrypt: true # 是否开启kuwodes
# {songId}为歌曲id
# {map_quality}为map后的歌曲音质酷我规范
# {raw_quality}为请求时的歌曲音质LX规范
# {ext}为歌曲文件扩展名
params: type=convert_url_with_sign&rid={songId}&quality={map_quality}&ext={ext}
host: nmobi.kuwo.cn
path: mobi.s
# 这里是reponse_type的所有支持值当设置为json时会使用到下面的两个值来获取url/bitrate如果为text则为传统的逐行解析方式
response_type: json
url_json_path: data.url
bitrate_json_path: data.bitrate
headers:
User-Agent: okhttp/3.10.0
gcsp: # 歌词适配后端配置
# 请注意只允许私用,不要给原作者带来麻烦,谢谢
enable: false # 是否启用歌词适配后端
path: /client/cgi-bin/api.fcg # 后端接口地址
enable_verify: false # 是否启用后端验证
package_md5: "" # apk包的md5值用于验证
salt_1: "NDRjZGIzNzliNzEe" # 后端验证参数1
salt_2: "6562653262383463363633646364306534333668" # 后端验证参数2
cookiepool:
kg:
- userid: "0"
token: ""
mid: "114514"
lite_sign_in: # 是否启用概念版自动签到仅在appid=3116时运行
enable: false
interval: 86400
mixsongmid: # mix_songmid的获取方式, 默认auto, 可以改成一个数字手动
value: auto
refresh_login: # cookie池中对于此账号刷新登录的配置账号间互不干扰
enable: false
login_url: http://login.user.kugou.com/v4/login_by_token
tx:
- qqmusic_key: ""
uin: ""
refresh_key: ""
refresh_login: # cookie池中对于此账号刷新登录的配置账号间互不干扰
enable: false
interval: 86000
wy:
- cookie: ""
refresh_login: # cookie池中对于此账号刷新登录的配置账号间互不干扰
enable: false
interval: 86400
mg:
- by: ""
session: ""
useragent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36
refresh_login:
enable: false
interval: 86400
kw:
- uid: "0"
token: ""
device_id: "0"
"""

93
common/gcsp.py Normal file
View File

@ -0,0 +1,93 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: gcsp.py -
# - project: moonsea_api(private) -
# - license: MIT -
# ----------------------------------------
# This file is part of the "moonsea_api" project and featured for the "lx-music-api-server" project.
import zlib
import binascii
import time
import ujson as json
import modules
from .utils import createMD5 as hashMd5
from . import config
from aiohttp.web import Response, Request
PACKAGE = config.read_config("module.gcsp.package_md5") # pkg md5
SALT_1 = config.read_config("module.gcsp.salt_1") # salt 1
SALT_2 = config.read_config("module.gcsp.salt_2") # salt 2
NEED_VERIFY = config.read_config("module.gcsp.enable_verify") # need verify
qm = {
'mp3': '128k',
'hq': '320k',
'sq': 'flac',
"hr": "flac24bit",
'hires': 'flac24bit'
}
pm = {
'qq': 'tx',
'wyy': 'wy',
'kugou': 'kg',
"kuwo": "kw",
"mgu": "mg"
}
internal_trans = {
"time": "请求检验失败,请检查系统时间是否为标准时间",
"sign": "请求检验失败,请检查应用是否被修改或更新到最新版本",
}
def decode(indata):
return json.loads(binascii.unhexlify(zlib.decompress(indata)))
def verify(data):
if (not NEED_VERIFY):
return "success"
sign_1 = hashMd5(PACKAGE + data["time"] + SALT_2)
sign_2 = hashMd5((json.dumps(data["text_1"]) + json.dumps(data["text_2"]) + sign_1 + data["time"] + SALT_1).replace("\\", "").replace("}\"", "}").replace("\"{", "{"))
if (data["sign_1"] != sign_1 or data["sign_2"] != sign_2):
return "sign"
if int(time.time()) - int(data["time"]) > 10:
return "time"
return "success"
async def handleGcspBody(body):
data = decode(body)
result = verify(data)
if (result != "success"):
return zlib.compress(json.dumps({"code": "403", "error_msg": internal_trans[result], "data": None}, ensure_ascii = False).encode("utf-8"))
data["te"] = json.loads(data["text_1"])
body = await modules.url(pm[data["te"]["platform"]], data["te"]["t1"], qm[data["te"]["t2"]])
if (body["code"] == 0):
return zlib.compress(json.dumps({"code": "200", "error_msg": "success", "data": body["data"] if (pm[data["te"]["platform"]] != "kw") else {"bitrate": "123", "url": body["data"]}}, ensure_ascii = False).encode("utf-8"))
else:
return zlib.compress(json.dumps({"code": "403", "error_msg": "内部系统错误,请稍后再试", "data": None}, ensure_ascii = False).encode("utf-8"))
async def handle_request(request: Request):
if (request.method == "POST"):
content_size = request.content_length
if (content_size > 5 * 1024): # 5kb
return Response(
body = "Request Entity Too Large",
status = 413
)
body = await request.read()
return Response(
body = await handleGcspBody(body),
content_type = "application/octet-stream",
status = 200
)
else:
return Response(
body = "Method Not Allowed",
status = 405
)

396
common/localMusic.py Normal file
View File

@ -0,0 +1,396 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: localMusic.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
import platform
import subprocess
import sys
from PIL import Image
import aiohttp
from common.utils import createFileMD5, createMD5, timeLengthFormat
from . import log, config
import ujson as json
import traceback
import mutagen
import os
logger = log.log('local_music_handler')
audios = []
map = {}
AUDIO_PATH = config.read_config("common.local_music.audio_path")
TEMP_PATH = config.read_config("common.local_music.temp_path")
FFMPEG_PATH = None
def convertCover(input_bytes):
if (input_bytes.startswith(b'\xff\xd8\xff\xe0')): # jpg object do not need convert
return input_bytes
temp = TEMP_PATH + '/' + createMD5(input_bytes) + '.img'
with open(temp, 'wb') as f:
f.write(input_bytes)
f.close()
img = Image.open(temp)
img = img.convert('RGB')
with open(temp + 'crt', 'wb') as f:
img.save(f, format='JPEG')
f.close()
data = None
with open(temp + 'crt', 'rb') as f:
data = f.read()
f.close()
try:
os.remove(temp)
except:
pass
try:
os.remove(temp + 'crt')
except:
pass
return data
def check_ffmpeg():
logger.info('正在检查ffmpeg')
devnull = open(os.devnull, 'w')
linux_bin_path = '/usr/bin/ffmpeg'
environ_ffpmeg_path = os.environ.get('FFMPEG_PATH')
if (platform.system() == 'Windows' or platform.system() == 'Cygwin'):
if (environ_ffpmeg_path and (not environ_ffpmeg_path.endswith('.exe'))):
environ_ffpmeg_path += '/ffmpeg.exe'
else:
if (environ_ffpmeg_path and os.path.isdir(environ_ffpmeg_path)):
environ_ffpmeg_path += '/ffmpeg'
if (environ_ffpmeg_path):
try:
subprocess.Popen([environ_ffpmeg_path, '-version'], stdout=devnull, stderr=devnull)
devnull.close()
return environ_ffpmeg_path
except:
pass
if (os.path.isfile(linux_bin_path)):
try:
subprocess.Popen([linux_bin_path, '-version'], stdout=devnull, stderr=devnull)
devnull.close()
return linux_bin_path
except:
pass
try:
subprocess.Popen(['ffmpeg', '-version'], stdout=devnull, stderr=devnull)
return 'ffmpeg'
except:
logger.warning('无法找到ffmpeg对于本地音乐的一些扩展功能无法使用如果您不需要请忽略本条提示')
logger.warning('如果您已经安装,请将 FFMPEG_PATH 环境变量设置为您的ffmpeg安装路径或者将其添加到PATH中')
return None
def getAudioCoverFromFFMpeg(path):
if (not FFMPEG_PATH):
return None
cmd = [FFMPEG_PATH, '-i', path, TEMP_PATH + '/_tmp.jpg']
popen = subprocess.Popen(cmd, stdout=sys.stdout, stderr=sys.stdout)
popen.wait()
if (os.path.exists(TEMP_PATH + '/_tmp.jpg')):
with open(TEMP_PATH + '/_tmp.jpg', 'rb') as f:
data = f.read()
f.close()
try:
os.remove(TEMP_PATH + '/_tmp.jpg')
except:
pass
return data
def readFileCheckCover(path):
with open(path, 'rb') as f: # read the first 1MB audio
data = f.read(1024 * 1024)
return b'image/' in data
def checkLyricValid(lyric_content):
if (lyric_content is None):
return False
if (lyric_content == ''):
return False
lines = lyric_content.split('\n')
for line in lines:
line = line.strip()
if (line == ''):
continue
if (line.startswith('[')):
continue
if (not line.startswith('[')):
return False
return True
def filterLyricLine(lyric_content: str) -> str:
lines = lyric_content.split('\n')
completed = []
for line in lines:
line = line.strip()
if (line.startswith('[')):
completed.append(line)
continue
return '\n'.join(completed)
def getAudioMeta(filepath):
if not os.path.exists(filepath):
return None
try:
audio = mutagen.File(filepath)
if not audio:
return None
logger.debug(audio.items())
if (filepath.lower().endswith('.mp3')):
cover = audio.get('APIC:')
if (cover):
cover = convertCover(cover.data)
lrc_key = None
for k in list(audio.keys()):
if (k.startswith('USLT')):
lrc_key = k
break
title = audio.get('TIT2')
artist = audio.get('TPE1')
album = audio.get('TALB')
if (lrc_key):
lyric = audio.get(lrc_key)
else:
lyric = None
if (title):
title = title.text
if (artist):
artist = artist.text
if (album):
album = album.text
if (lyric):
lyric = [lyric.text]
if (not lyric):
if (os.path.isfile(os.path.splitext(filepath)[0] + '.lrc')):
with open(os.path.splitext(filepath)[0] + '.lrc', 'r', encoding='utf-8') as f:
t = f.read().replace('\ufeff', '')
logger.debug(t)
lyric = filterLyricLine(t)
logger.debug(lyric)
if (not checkLyricValid(lyric)):
lyric = [None]
else:
lyric = [lyric]
f.close()
else:
lyric = [None]
else:
cover = audio.get('cover')
if (cover):
cover = convertCover(cover[0])
else:
if (readFileCheckCover(filepath)):
cover = getAudioCoverFromFFMpeg(filepath)
else:
cover = None
title = audio.get('title')
artist = audio.get('artist')
album = audio.get('album')
lyric = audio.get('lyrics')
if (not lyric):
if (os.path.isfile(os.path.splitext(filepath)[0] + '.lrc')):
with open(os.path.splitext(filepath)[0] + '.lrc', 'r', encoding='utf-8') as f:
lyric = filterLyricLine(f.read())
if (not checkLyricValid(lyric)):
lyric = [None]
else:
lyric = [lyric]
f.close()
else:
lyric = [None]
return {
"filepath": filepath,
"title": title[0] if title else '',
"artist": ''.join(artist) if artist else '',
"album": album[0] if album else '',
"cover_path": extractCover({
"filepath": filepath,
"cover": cover,
}, TEMP_PATH),
"lyrics": lyric[0],
'length': audio.info.length,
'format_length': timeLengthFormat(audio.info.length),
'md5': createFileMD5(filepath),
}
except:
logger.error(f"get audio meta error: {filepath}")
logger.error(traceback.format_exc())
return None
def checkAudioValid(path):
if not os.path.exists(path):
return False
try:
audio = mutagen.File(path)
if not audio:
return False
return True
except:
logger.error(f"check audio valid error: {path}")
logger.error(traceback.format_exc())
return False
def extractCover(audio_info, temp_path):
if (not audio_info['cover']):
return None
path = os.path.join(temp_path + '/' + createMD5(audio_info['filepath']) + '_cover.jpg')
with open(path, 'wb') as f:
f.write(audio_info['cover'])
return path
def findAudios(cache):
available_exts = [
'mp3',
'wav',
'flac',
'ogg',
'm4a',
]
files = os.listdir(AUDIO_PATH)
if (files == []):
return []
audios = []
_map = {}
for c in cache:
_map[c['filepath']] = c
for file in files:
if (not file.endswith(tuple(available_exts))):
continue
path = os.path.join(AUDIO_PATH, file)
if (not checkAudioValid(path)):
continue
logger.info(f"found audio: {path}")
if (not (_map.get(path) and _map[path]['md5'] == createFileMD5(path))):
meta = getAudioMeta(path)
audios = audios + [meta]
else:
audios = audios + [_map[path]]
return audios
def getAudioCover(filepath):
if not os.path.exists(filepath):
return None
try:
audio = mutagen.File(filepath)
if not audio:
return None
if (filepath.lower().endswith('mp3')):
return audio.get('APIC:').data
else:
if (readFileCheckCover(filepath)):
return getAudioCoverFromFFMpeg(filepath)
else:
return None
except:
logger.error(f"get audio cover error: {filepath}")
logger.error(traceback.format_exc())
return None
def writeAudioCover(filepath):
s = getAudioCover(filepath)
path = os.path.join(TEMP_PATH + '/' + createMD5(filepath) + '_cover.jpg')
with open(path, 'wb') as f:
f.write(s)
f.close()
return path
def writeLocalCache(audios):
with open(TEMP_PATH + '/meta.json', 'w', encoding='utf-8') as f:
f.write(json.dumps({
"file_list": os.listdir(AUDIO_PATH),
"audios": audios
}, ensure_ascii = False, indent = 2))
f.close()
def dumpLocalCache():
try:
TEMP_PATH = config.read_config("common.local_music.temp_path")
with open(TEMP_PATH + '/meta.json', 'r', encoding='utf-8') as f:
d = json.loads(f.read())
return d
except:
return {
"file_list": [],
"audios": []
}
def initMain():
global FFMPEG_PATH
FFMPEG_PATH = check_ffmpeg()
logger.debug('找到的ffmpeg命令: ' + str(FFMPEG_PATH))
if (not os.path.exists(AUDIO_PATH)):
os.mkdir(AUDIO_PATH)
logger.info(f"创建本地音乐文件夹 {AUDIO_PATH}")
if (not os.path.exists(TEMP_PATH)):
os.mkdir(TEMP_PATH)
logger.info(f"创建本地音乐临时文件夹 {TEMP_PATH}")
global audios
cache = dumpLocalCache()
if (cache['file_list'] == os.listdir(AUDIO_PATH)):
audios = cache['audios']
else:
audios = findAudios(cache['audios'])
writeLocalCache(audios)
for a in audios:
map[a['filepath']] = a
logger.info("初始化本地音乐成功")
logger.debug(f'本地音乐列表: {audios}')
logger.debug(f'本地音乐map: {map}')
async def generateAudioFileResonse(path):
try:
w = map[path]
return aiohttp.web.FileResponse(w['filepath'])
except:
return {
'code': 2,
'msg': '未找到文件',
'data': None
}, 404
async def generateAudioCoverResonse(path):
try:
w = map[path]
if (not os.path.exists(w['cover_path'])):
p = writeAudioCover(w['filepath'])
logger.debug(f"生成音乐封面文件 {w['cover_path']} 成功")
return aiohttp.web.FileResponse(p)
return aiohttp.web.FileResponse(w['cover_path'])
except:
logger.debug(traceback.format_exc())
return {
'code': 2,
'msg': '未找到封面',
'data': None
}, 404
async def generateAudioLyricResponse(path):
try:
w = map[path]
return w['lyrics']
except:
return {
'code': 2,
'msg': '未找到歌词',
'data': None
}, 404
def checkLocalMusic(path):
return {
'file': os.path.exists(path),
'cover': os.path.exists(map[path]['cover_path']),
'lyric': bool(map[path]['lyrics'])
}

View File

@ -10,11 +10,20 @@
import logging
import colorlog
import os
import sys
import re
import io
import traceback
import time
from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalFormatter
from .utils import filterFileName, addToGlobalNamespace
from .variable import debug_mode, log_length_limit, log_file
from .utils import filterFileName, setGlobal, require
from .variable import debug_mode, log_length_limit, log_file, log_files
from colorama import Fore, Style
from colorama import init as clinit
clinit() # 初始化 colorama
if ((not os.path.exists("logs")) and log_file):
try:
@ -22,15 +31,158 @@ if ((not os.path.exists("logs")) and log_file):
except:
pass
class Color:
"""
彩色文字处理器
"""
def highlight_error(error):
# 对堆栈跟踪进行语法高亮
highlighted_traceback = highlight(
error, PythonLexer(), TerminalFormatter())
def __getattr__(self, k):
return lambda x: f"{getattr(Fore, k.upper())}{x}{Style.RESET_ALL}"
# 返回语法高亮后的堆栈跟踪字符串
return str(highlighted_traceback)
color = Color()
# purple_parentheses = color.purple('(') + '{}' + color.purple(')')
def is_rubbish(input_string):
return bool(re.match(r'^(\~|\^)*$', input_string))
def stack_error(exception):
stack_trace = traceback.format_exception(type(exception), exception, exception.__traceback__)
return ''.join(stack_trace)
def python_highlight(code):
return highlight(code, PythonLexer(), TerminalFormatter())
def read_code(file_path, target_line_number):
try:
with open(file_path, 'r', encoding='utf-8') as file:
lines = file.readlines()
start = max(0, target_line_number - 4)
end = min(target_line_number + 4, len(lines))
lineMap = {
'current': lines[target_line_number - 1],
'result': lines[start:end]
}
# print(lineMap)
return lineMap
except FileNotFoundError:
sys.stderr.write("日志模块出错,本次日志可能无法记录,请报告给开发者: 处理错误语法高亮时找不到源文件")
except Exception as e:
sys.stderr.write(f"日志模块出错,本次日志可能无法记录,请报告给开发者: \n" + traceback.format_exc())
def stack_info(stack_trace_line):
try:
parts = stack_trace_line.split(', ')
file_path = ' '.join(parts[0].split(' ')[1:]) # 提取文件路径
line_number = int(parts[1].split(' ')[1]) # 提取行号
function_name = parts[2].split(' ')[1] # 提取函数名
return file_path, line_number, function_name
except Exception as e:
sys.stderr.write(f"日志模块出错,本次日志可能无法记录,请报告给开发者: \n" + traceback.format_exc())
def highlight_error(e):
try:
if (isinstance(e, Exception)):
error = stack_error(e)
else:
error = e
lines = [i.strip() for i in error.split("\n") if i.strip()]
final = []
ign = False
for i in lines:
if (ign):
ign = False
continue
if (i.startswith("Traceback (most recent call last):")):
final.append(color.cyan(i))
elif (i.startswith("During handling of the above exception, another exception occurred:")):
final.append(color.cyan(i))
elif (i.startswith("The above exception was the direct cause of the following exception:")):
final.append(color.cyan(i))
elif (i.startswith("File")):
ign = True
p, l, f = stack_info(i)
p = p[1:-1]
if (p.startswith('<') or not os.path.isfile(p)):
final.append(i)
final.append(" " if (lines[lines.index(l) + 1].startswith("File")) else (" " + lines[lines.index(l) + 1]))
continue
code = read_code(p, l)
cc = []
viewed = False
firstLineNumber = max(l - 3, 1)
line_number = firstLineNumber
for i in range(len(code['result'])):
c = code["result"][i]
if (c.startswith(code['current']) and (not viewed) and (i <= 3)):
viewed = True
line_number = line_number + 1
# print(line_number)
cc.append((' ' * (10 - len(str(l))) + f'{color.red(str(l))} >|' + python_highlight(c)))
else:
cc.append((' ' * (10 - len(str(line_number))) + f'{color.blue(str(line_number))} |' + python_highlight(c)))
line_number = line_number + 1
# print(line_number)
code = "\n".join(cc)
p = '"' + p + '"'
final.append(f" File {color.yellow(f'{p}')} in {color.cyan(f) + '()' if (not f.startswith('<')) else ''}\n\n\n{code}\n")
else:
try:
if (is_rubbish(i)):
continue
if (issubclass(require(("builtins." if ("." not in i.split(":")[0]) else "") + i.split(":")[0]), Exception)):
exc = i.split(":")[0]
desc = "" if (len(i.split(":")) == 1) else ':'.join(i.split(":")[1:]).strip()
final.append(color.red(exc) + (": " + color.yellow(desc)) if (desc) else "")
else:
final.append(color.cyan(i))
except:
# traceback.print_exc()
final.append(i)
return "\n".join(final).replace('\n\n', '\n')
'''
lines = [i for i in error.split("\n") if (i.strip() and not is_rubbish(i))]
final = []
ispass = False # pass handle next line
for index in range(len(lines)):
i = lines[index]
i2 = i.strip()
if (ispass):
ispass = False
continue
if (i2.startswith("Traceback (most recent call last):")):
final.append(color.cyan(i))
elif (i2.startswith("During handling of the above exception, another exception occurred:")):
final.append(color.cyan(i))
elif (i2.startswith("The above exception was the direct cause of the following exception:")):
final.append(color.cyan(i))
elif (i2.startswith("File")):
p, l, f = stack_info(i2)
p = p[1:-1]
if (p.startswith('<') or not os.path.isfile(p)):
final.append(f' File {color.yellow("\"" + p + "\"")} in {color.cyan(f)}() [line {color.blue(l)}]\n')
_probably_err_name = lines[index + 1].split(':')[0]
if not (lines.get(index + 1, '').strip().startswith("File") or require(
('builtins.' + _probably_err_name) if ("." not in _probably_err_name) else _probably_err_name
) is Exception):
final.append(f" > {color.red(l)} |" + python_highlight(lines[index + 1]))
ispass = True
continue
codeMap = read_code(p, l)
line_length = len(codeMap['result'])
'''
except:
sys.stderr.write('格式化错误失败,使用默认格式\n' + traceback.format_exc())
if (isinstance(e, Exception)):
return stack_error(e)
else:
return e
class LogHelper(logging.Handler):
# 日志转接器
@ -43,10 +195,19 @@ class LogHelper(logging.Handler):
log_message = self.format(record)
self.custom_logger.info(log_message)
class fileWriter(logging.Handler):
def __init__(self, f: io.TextIOWrapper, f2: logging.Formatter):
self.file = f
self.formatter = f2
def emit(self, record: logging.LogRecord):
self.file.write(self.format(record) + '\n')
self.file.flush()
class log:
# 主类
def __init__(self, module_name='Not named logger', output_level='INFO', filename=''):
self.name = module_name
self._logger = logging.getLogger(module_name)
if not output_level.upper() in dir(logging):
raise NameError('Unknown loglevel: '+output_level)
@ -65,21 +226,12 @@ class log:
'CRITICAL': 'red,bg_white',
})
if log_file:
file_formatter = logging.Formatter(
'%(asctime)s|[%(name)s/%(levelname)s]|%(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
if filename:
filename = filterFileName(filename)
else:
filename = './logs/' + module_name + '.log'
file_handler = logging.FileHandler(filename, encoding="utf-8")
file_handler.setFormatter(file_formatter)
file_handler_ = logging.FileHandler(
"./logs/console_full.log", encoding="utf-8")
file_handler_.setFormatter(file_formatter)
self._logger.addHandler(file_handler_)
self._logger.addHandler(file_handler)
self.file = open(filename, 'a+', encoding='utf-8')
log_files.append(self.file)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
self.module_name = module_name
@ -88,60 +240,55 @@ class log:
debug_handler.setFormatter(formatter)
def debug(self, message, allow_hidden=True):
if self.module_name == "flask" and "\n" in message:
if message.startswith("Error"):
return self._logger.error(message)
for m in message.split("\n"):
if "WARNING" in m:
self._logger.warning(m)
else:
self._logger.info(m)
return
if (log_file):
self.file.write('{time}|[{name}/DEBUG]{msg}'.format(time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), name = self.module_name, msg = message) + '\n')
if len(str(message)) > log_length_limit and allow_hidden:
message = str(message)[:log_length_limit] + " ..."
self._logger.debug(message)
def log(self, message, allow_hidden=True):
if self.module_name == "flask" and "\n" in message:
if message.startswith("Error"):
return self._logger.error(message)
for m in message.split("\n"):
if "WARNING" in m:
self._logger.warning(m)
else:
self._logger.info(m)
return
if (log_file):
self.file.write('{time}|[{name}/INFO]{msg}'.format(time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), name = self.module_name, msg = message) + '\n')
if len(str(message)) > log_length_limit and allow_hidden:
message = str(message)[:log_length_limit] + " ..."
self._logger.info(message)
def info(self, message, allow_hidden=True):
if self.module_name == "flask" and "\n" in message:
if message.startswith("Error"):
return self._logger.error(message)
for m in message.split("\n"):
if "WARNING" in m:
self._logger.warning(m)
else:
self._logger.info(m)
return
if (log_file):
self.file.write('{time}|[{name}/INFO]{msg}'.format(time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), name = self.module_name, msg = message) + '\n')
if len(str(message)) > log_length_limit and allow_hidden:
message = str(message)[:log_length_limit] + "..."
self._logger.info(message)
def warning(self, message):
if (message.startswith('Traceback')):
self._logger.error('\n' + highlight_error(message))
self._logger.warning(message)
if (log_file):
self.file.write('{time}|[{name}/WARNING]{msg}'.format(time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), name = self.module_name, msg = message) + '\n')
if (message.strip().startswith('Traceback')):
self._logger.warning('\n' + highlight_error(message))
else:
self._logger.warning(message)
def error(self, message):
if (log_file):
self.file.write('{time}|[{name}/ERROR]{msg}'.format(time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), name = self.module_name, msg = message) + '\n')
if (message.startswith('Traceback')):
self._logger.error('\n' + highlight_error(message))
else:
self._logger.error(message)
def critical(self, message):
self._logger.critical(message)
if (log_file):
self.file.write('{time}|[{name}/CRITICAL]{msg}'.format(time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), name = self.module_name, msg = message) + '\n')
if (message.startswith('Traceback')):
self._logger.critical('\n' + highlight_error(message))
else:
self._logger.critical(message)
def set_level(self, loglevel):
loglevel_upper = loglevel.upper()
@ -163,4 +310,4 @@ def logprint(*args, sep=' ', end='', file=None, flush=None):
printlogger.info(sep.join(str(arg) for arg in args), allow_hidden=False)
addToGlobalNamespace('print', logprint)
setGlobal(logprint, 'print')

View File

@ -10,11 +10,11 @@
from . import Httpx
from . import config
from . import scheduler
from .variable import iscn
from .log import log
from aiohttp.web import Response
import ujson as json
import re
from common.utils import createMD5
logger = log('lx_script')
@ -38,8 +38,8 @@ async def get_response(retry = 0):
if (retry > 21):
logger.warning('请求源脚本内容失败')
return
baseurl = '/lxmusics/lx-music-api-server/main/lx-music-source-example.js'
jsdbaseurl = '/gh/lxmusics/lx-music-api-server@main/lx-music-source-example.js'
baseurl = '/MeoProject/lx-music-api-server/main/lx-music-source-example.js'
jsdbaseurl = '/gh/MeoProject/lx-music-api-server@main/lx-music-source-example.js'
try:
i = retry
if (i > 10):
@ -69,13 +69,13 @@ async def get_script():
logger.warning('请求源脚本内容失败')
async def generate_script_response(request):
if (request.query.get('key') != config.read_config('security.key.value') and config.read_config('security.key.enable')):
return 'key验证失败'
if (request.query.get('key') not in config.read_config('security.key.values') and config.read_config('security.key.enable')):
return {'code': 6, 'msg': 'key验证失败', 'data': None}, 403
try:
with open('./lx-music-source-example.js', 'r') as f:
with open('./lx-music-source-example.js', 'r', encoding='utf-8') as f:
script = f.read()
except:
return '本地无源脚本'
return {'code': 4, 'msg': '本地无源脚本', 'data': None}, 400
scriptLines = script.split('\n')
newScriptLines = []
for line in scriptLines:
@ -84,29 +84,43 @@ async def generate_script_response(request):
if (line.startswith('const API_URL')):
newScriptLines.append(f'''const API_URL = "{'https' if config.read_config('common.ssl_info.is_https') else 'http'}://{request.host}"''')
elif (line.startswith('const API_KEY')):
newScriptLines.append(f'const API_KEY = "{config.read_config("security.key.value")}"')
newScriptLines.append(f"""const API_KEY = `{request.query.get("key") if request.query.get("key") else ''''''}`""")
elif (line.startswith("* @name")):
newScriptLines.append(" * @name " + config.read_config("common.download_config.name"))
elif (line.startswith("* @description")):
newScriptLines.append(" * @description " + config.read_config("common.download_config.intro"))
elif (line.startswith("* @author")):
newScriptLines.append((" * @author helloplhm-qwq & Folltoshe & " + config.read_config("common.download_config.author")) if config.read_config("common.download_config.author") else " * @author helloplhm-qwq & Folltoshe")
newScriptLines.append(" * @author " + config.read_config("common.download_config.author"))
elif (line.startswith("* @version")):
newScriptLines.append(" * @version " + config.read_config("common.download_config.version"))
elif (line.startswith("const DEV_ENABLE ")):
newScriptLines.append("const DEV_ENABLE = " + str(config.read_config("common.download_config.dev")).lower())
elif (line.startswith("const UPDATE_ENABLE ")):
newScriptLines.append("const UPDATE_ENABLE = " + str(config.read_config("common.download_config.update")).lower())
else:
newScriptLines.append(oline)
r = '\n'.join(newScriptLines)
r = re.sub(r'const MUSIC_QUALITY = {[^}]+}', f'const MUSIC_QUALITY = JSON.parse(\'{json.dumps(config.read_config("common.download_config.quality"))}\')', r)
# 用于检查更新
if (config.read_config("common.download_config.update")):
md5 = createMD5(r)
r = r.replace(r"const SCRIPT_MD5 = ''", f"const SCRIPT_MD5 = '{md5}'")
if (request.query.get('checkUpdate')):
if (request.query.get('checkUpdate') == md5):
return {'code': 0, 'msg': 'success', 'data': None}, 200
url = f"{'https' if config.read_config('common.ssl_info.is_https') else 'http'}://{request.host}/script"
updateUrl = f"{url}{('?key=' + request.query.get('key')) if request.query.get('key') else ''}"
updateMsg = config.read_config('common.download_config.updateMsg').format(updateUrl = updateUrl, url = url, key = request.query.get('key')).replace('\\n', '\n')
return {'code': 0, 'msg': 'success', 'data': {'updateMsg': updateMsg, 'updateUrl': updateUrl}}, 200
return Response(text = r, content_type = 'text/javascript',
headers = {
'Content-Disposition': f'''attachment; filename={
config.read_config("common.download_config.filename")
if config.read_config("common.download_config.filename").endswith(".js")
else (config.read_config("common.download_config.filename" + ".js"))}'''
else (config.read_config("common.download_config.filename") + ".js")}'''
})
if (config.read_config('common.allow_download_script')):

View File

@ -40,6 +40,9 @@ class taskWrapper:
logger.error(f"task {self.name} run failed, waiting for next execute...")
logger.error(traceback.format_exc())
def __str__(self):
return f'SchedulerTaskWrapper(name="{self.name}", interval={self.interval}, function={self.function}, args={self.args}, latest_execute={self.latest_execute})'
def append(name, task, interval = 86400, args = {}):
global tasks
wrapper = taskWrapper(name, task, interval, args)

View File

@ -7,6 +7,7 @@
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
import hashlib
import platform
import binascii
import builtins
@ -15,8 +16,9 @@ import zlib
import time
import re
import xmltodict
import ipaddress
from urllib.parse import quote, unquote, urlparse
from hashlib import md5 as handleCreateMD5
from typing import Union
def createBase64Encode(data_bytes):
encoded_data = base64.b64encode(data_bytes)
@ -51,8 +53,8 @@ def require(module):
index += 1
return _module
def addToGlobalNamespace(key, data):
setattr(builtins, key, data)
def setGlobal(obj, key = ''):
setattr(builtins, obj.__name__ if (not key) else key, obj)
def filterFileName(filename):
if platform.system() == 'Windows' or platform.system() == 'Cygwin':
@ -64,8 +66,17 @@ def filterFileName(filename):
# 将不合法字符替换为下划线
return re.sub(illegal_chars, '_', filename)
def createMD5(s: str):
return handleCreateMD5(s.encode("utf-8")).hexdigest()
def createMD5(s: Union[str, bytes]):
if (isinstance(s, str)):
s = s.encode("utf-8")
return hashlib.md5(s).hexdigest()
def createFileMD5(path):
with open(path, 'rb') as f:
md5 = hashlib.md5()
for chunk in iter(lambda: f.read(4096), b""):
md5.update(chunk)
return md5.hexdigest()
def readFile(path, mode = "text"):
try:
@ -189,5 +200,11 @@ def timestamp_format(t):
t = int(t)
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(t))
addToGlobalNamespace('require', require)
def is_local_ip(ip):
try:
i = ipaddress.ip_address(ip)
return i.is_private
except:
return False
setGlobal(require)

View File

@ -8,40 +8,43 @@
# This file is part of the "lx-music-api-server" project.
import os as _os
import ujson as _json
import ruamel.yaml as _yaml
yaml = _yaml.YAML()
def _read_config_file():
try:
with open("./config.json", "r", encoding = "utf-8") as f:
return _json.load(f)
with open(f"./config/config.yml", "r", encoding="utf-8") as f:
return yaml.load(f.read())
except:
pass
return []
def _read_config(key):
try:
config = _read_config_file()
keys = key.split('.')
value = config
for k in keys:
if isinstance(value, dict):
if k not in value and keys.index(k) != len(keys) - 1:
value[k] = {}
elif k not in value and keys.index(k) == len(keys) - 1:
value = None
value = value[k]
else:
config = _read_config_file()
keys = key.split('.')
value = config
for k in keys:
if isinstance(value, dict):
if k not in value and keys.index(k) != len(keys) - 1:
value[k] = []
elif k not in value and keys.index(k) == len(keys) - 1:
value = None
break
value = value[k]
else:
value = None
break
return value
return value
except:
return None
_dm = _read_config("common.debug_mode")
_lm = _read_config("common.log_file")
_ll = _read_config("common.log_length_limit")
debug_mode = _dm if (_dm) else False
debug_mode = True if (_os.getenv('CURRENT_ENV') ==
'development') else (_dm if (_dm) else False)
log_length_limit = _ll if (_ll) else 500
log_file = _lm if (not isinstance(_lm , type(None))) else True
log_file = _lm if (isinstance(_lm, bool)) else True
running = True
config = {}
workdir = _os.getcwd()
@ -51,3 +54,8 @@ fake_ip = None
aioSession = None
qdes_lib_loaded = False
use_cookie_pool = False
running_ports = []
use_proxy = False
http_proxy = ''
https_proxy = ''
log_files = []

13
deps/pyqdes/README.md vendored Normal file
View File

@ -0,0 +1,13 @@
## pyqdes QRC解密库
构建:
```bash
python setup.py bdist_wheel
```
到dist目录pip安装那个whl文件
```bash
pip install xxx.whl
```
api服务器会自行识别

266
deps/pyqdes/des.cpp vendored Normal file
View File

@ -0,0 +1,266 @@
/*********************************************************************
* Filename: des.cpp
* Author: Brad Conte (brad AT radconte.com)
* Copyright:
* Disclaimer: This code is presented "as is" without any guarantees.
* Details: Implementation of the DES encryption algorithm.
Modes of operation (such as CBC) are not included.
The formal NIST algorithm specification can be found here:
* http://csrc.nist.gov/publications/fips/fips46-3/fips46-3.pdf
*********************************************************************/
/*************************** HEADER FILES ***************************/
#include <stdlib.h>
#include <memory.h>
#include "des.h"
/****************************** MACROS ******************************/
// Obtain bit "b" from the left and shift it "c" places from the right
#define BITNUM(a, b, c) (((a[(b) / 32 * 4 + 3 - (b) % 32 / 8] >> (7 - (b % 8))) & 0x01) << (c))
#define BITNUMINTR(a, b, c) ((((a) >> (31 - (b))) & 0x00000001) << (c))
#define BITNUMINTL(a, b, c) ((((a) << (b)) & 0x80000000) >> (c))
// This macro converts a 6 bit block with the S-Box row defined as the first and last
// bits to a 6 bit block with the row defined by the first two bits.
#define SBOXBIT(a) (((a)&0x20) | (((a)&0x1f) >> 1) | (((a)&0x01) << 4))
/**************************** VARIABLES *****************************/
static const BYTE sbox1[64] = {
14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7,
0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8,
4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0,
15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13};
static const BYTE sbox2[64] = {
15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10,
3, 13, 4, 7, 15, 2, 8, 15, 12, 0, 1, 10, 6, 9, 11, 5,
0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15,
13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9};
static const BYTE sbox3[64] = {
10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8,
13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1,
13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7,
1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12};
static const BYTE sbox4[64] = {
7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15,
13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9,
10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4,
3, 15, 0, 6, 10, 10, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14};
static const BYTE sbox5[64] = {
2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9,
14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6,
4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14,
11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3};
static const BYTE sbox6[64] = {
12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11,
10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8,
9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6,
4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13};
static const BYTE sbox7[64] = {
4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1,
13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6,
1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2,
6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12};
static const BYTE sbox8[64] = {
13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7,
1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2,
7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8,
2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11};
/*********************** FUNCTION DEFINITIONS ***********************/
// Initial (Inv)Permutation step
void IP(WORD state[], const BYTE in[])
{
state[0] = BITNUM(in, 57, 31) | BITNUM(in, 49, 30) | BITNUM(in, 41, 29) | BITNUM(in, 33, 28) |
BITNUM(in, 25, 27) | BITNUM(in, 17, 26) | BITNUM(in, 9, 25) | BITNUM(in, 1, 24) |
BITNUM(in, 59, 23) | BITNUM(in, 51, 22) | BITNUM(in, 43, 21) | BITNUM(in, 35, 20) |
BITNUM(in, 27, 19) | BITNUM(in, 19, 18) | BITNUM(in, 11, 17) | BITNUM(in, 3, 16) |
BITNUM(in, 61, 15) | BITNUM(in, 53, 14) | BITNUM(in, 45, 13) | BITNUM(in, 37, 12) |
BITNUM(in, 29, 11) | BITNUM(in, 21, 10) | BITNUM(in, 13, 9) | BITNUM(in, 5, 8) |
BITNUM(in, 63, 7) | BITNUM(in, 55, 6) | BITNUM(in, 47, 5) | BITNUM(in, 39, 4) |
BITNUM(in, 31, 3) | BITNUM(in, 23, 2) | BITNUM(in, 15, 1) | BITNUM(in, 7, 0);
state[1] = BITNUM(in, 56, 31) | BITNUM(in, 48, 30) | BITNUM(in, 40, 29) | BITNUM(in, 32, 28) |
BITNUM(in, 24, 27) | BITNUM(in, 16, 26) | BITNUM(in, 8, 25) | BITNUM(in, 0, 24) |
BITNUM(in, 58, 23) | BITNUM(in, 50, 22) | BITNUM(in, 42, 21) | BITNUM(in, 34, 20) |
BITNUM(in, 26, 19) | BITNUM(in, 18, 18) | BITNUM(in, 10, 17) | BITNUM(in, 2, 16) |
BITNUM(in, 60, 15) | BITNUM(in, 52, 14) | BITNUM(in, 44, 13) | BITNUM(in, 36, 12) |
BITNUM(in, 28, 11) | BITNUM(in, 20, 10) | BITNUM(in, 12, 9) | BITNUM(in, 4, 8) |
BITNUM(in, 62, 7) | BITNUM(in, 54, 6) | BITNUM(in, 46, 5) | BITNUM(in, 38, 4) |
BITNUM(in, 30, 3) | BITNUM(in, 22, 2) | BITNUM(in, 14, 1) | BITNUM(in, 6, 0);
}
void InvIP(WORD state[], BYTE in[])
{
in[3] = BITNUMINTR(state[1], 7, 7) | BITNUMINTR(state[0], 7, 6) | BITNUMINTR(state[1], 15, 5) |
BITNUMINTR(state[0], 15, 4) | BITNUMINTR(state[1], 23, 3) | BITNUMINTR(state[0], 23, 2) |
BITNUMINTR(state[1], 31, 1) | BITNUMINTR(state[0], 31, 0);
in[2] = BITNUMINTR(state[1], 6, 7) | BITNUMINTR(state[0], 6, 6) | BITNUMINTR(state[1], 14, 5) |
BITNUMINTR(state[0], 14, 4) | BITNUMINTR(state[1], 22, 3) | BITNUMINTR(state[0], 22, 2) |
BITNUMINTR(state[1], 30, 1) | BITNUMINTR(state[0], 30, 0);
in[1] = BITNUMINTR(state[1], 5, 7) | BITNUMINTR(state[0], 5, 6) | BITNUMINTR(state[1], 13, 5) |
BITNUMINTR(state[0], 13, 4) | BITNUMINTR(state[1], 21, 3) | BITNUMINTR(state[0], 21, 2) |
BITNUMINTR(state[1], 29, 1) | BITNUMINTR(state[0], 29, 0);
in[0] = BITNUMINTR(state[1], 4, 7) | BITNUMINTR(state[0], 4, 6) | BITNUMINTR(state[1], 12, 5) |
BITNUMINTR(state[0], 12, 4) | BITNUMINTR(state[1], 20, 3) | BITNUMINTR(state[0], 20, 2) |
BITNUMINTR(state[1], 28, 1) | BITNUMINTR(state[0], 28, 0);
in[7] = BITNUMINTR(state[1], 3, 7) | BITNUMINTR(state[0], 3, 6) | BITNUMINTR(state[1], 11, 5) |
BITNUMINTR(state[0], 11, 4) | BITNUMINTR(state[1], 19, 3) | BITNUMINTR(state[0], 19, 2) |
BITNUMINTR(state[1], 27, 1) | BITNUMINTR(state[0], 27, 0);
in[6] = BITNUMINTR(state[1], 2, 7) | BITNUMINTR(state[0], 2, 6) | BITNUMINTR(state[1], 10, 5) |
BITNUMINTR(state[0], 10, 4) | BITNUMINTR(state[1], 18, 3) | BITNUMINTR(state[0], 18, 2) |
BITNUMINTR(state[1], 26, 1) | BITNUMINTR(state[0], 26, 0);
in[5] = BITNUMINTR(state[1], 1, 7) | BITNUMINTR(state[0], 1, 6) | BITNUMINTR(state[1], 9, 5) |
BITNUMINTR(state[0], 9, 4) | BITNUMINTR(state[1], 17, 3) | BITNUMINTR(state[0], 17, 2) |
BITNUMINTR(state[1], 25, 1) | BITNUMINTR(state[0], 25, 0);
in[4] = BITNUMINTR(state[1], 0, 7) | BITNUMINTR(state[0], 0, 6) | BITNUMINTR(state[1], 8, 5) |
BITNUMINTR(state[0], 8, 4) | BITNUMINTR(state[1], 16, 3) | BITNUMINTR(state[0], 16, 2) |
BITNUMINTR(state[1], 24, 1) | BITNUMINTR(state[0], 24, 0);
}
WORD f(WORD state, const BYTE key[])
{
BYTE lrgstate[6]; //,i;
WORD t1, t2;
// Expantion Permutation
t1 = BITNUMINTL(state, 31, 0) | ((state & 0xf0000000) >> 1) | BITNUMINTL(state, 4, 5) |
BITNUMINTL(state, 3, 6) | ((state & 0x0f000000) >> 3) | BITNUMINTL(state, 8, 11) |
BITNUMINTL(state, 7, 12) | ((state & 0x00f00000) >> 5) | BITNUMINTL(state, 12, 17) |
BITNUMINTL(state, 11, 18) | ((state & 0x000f0000) >> 7) | BITNUMINTL(state, 16, 23);
t2 = BITNUMINTL(state, 15, 0) | ((state & 0x0000f000) << 15) | BITNUMINTL(state, 20, 5) |
BITNUMINTL(state, 19, 6) | ((state & 0x00000f00) << 13) | BITNUMINTL(state, 24, 11) |
BITNUMINTL(state, 23, 12) | ((state & 0x000000f0) << 11) | BITNUMINTL(state, 28, 17) |
BITNUMINTL(state, 27, 18) | ((state & 0x0000000f) << 9) | BITNUMINTL(state, 0, 23);
lrgstate[0] = (t1 >> 24) & 0x000000ff;
lrgstate[1] = (t1 >> 16) & 0x000000ff;
lrgstate[2] = (t1 >> 8) & 0x000000ff;
lrgstate[3] = (t2 >> 24) & 0x000000ff;
lrgstate[4] = (t2 >> 16) & 0x000000ff;
lrgstate[5] = (t2 >> 8) & 0x000000ff;
// Key XOR
lrgstate[0] ^= key[0];
lrgstate[1] ^= key[1];
lrgstate[2] ^= key[2];
lrgstate[3] ^= key[3];
lrgstate[4] ^= key[4];
lrgstate[5] ^= key[5];
// S-Box Permutation
state = (sbox1[SBOXBIT(lrgstate[0] >> 2)] << 28) |
(sbox2[SBOXBIT(((lrgstate[0] & 0x03) << 4) | (lrgstate[1] >> 4))] << 24) |
(sbox3[SBOXBIT(((lrgstate[1] & 0x0f) << 2) | (lrgstate[2] >> 6))] << 20) |
(sbox4[SBOXBIT(lrgstate[2] & 0x3f)] << 16) |
(sbox5[SBOXBIT(lrgstate[3] >> 2)] << 12) |
(sbox6[SBOXBIT(((lrgstate[3] & 0x03) << 4) | (lrgstate[4] >> 4))] << 8) |
(sbox7[SBOXBIT(((lrgstate[4] & 0x0f) << 2) | (lrgstate[5] >> 6))] << 4) |
sbox8[SBOXBIT(lrgstate[5] & 0x3f)];
// P-Box Permutation
state = BITNUMINTL(state, 15, 0) | BITNUMINTL(state, 6, 1) | BITNUMINTL(state, 19, 2) |
BITNUMINTL(state, 20, 3) | BITNUMINTL(state, 28, 4) | BITNUMINTL(state, 11, 5) |
BITNUMINTL(state, 27, 6) | BITNUMINTL(state, 16, 7) | BITNUMINTL(state, 0, 8) |
BITNUMINTL(state, 14, 9) | BITNUMINTL(state, 22, 10) | BITNUMINTL(state, 25, 11) |
BITNUMINTL(state, 4, 12) | BITNUMINTL(state, 17, 13) | BITNUMINTL(state, 30, 14) |
BITNUMINTL(state, 9, 15) | BITNUMINTL(state, 1, 16) | BITNUMINTL(state, 7, 17) |
BITNUMINTL(state, 23, 18) | BITNUMINTL(state, 13, 19) | BITNUMINTL(state, 31, 20) |
BITNUMINTL(state, 26, 21) | BITNUMINTL(state, 2, 22) | BITNUMINTL(state, 8, 23) |
BITNUMINTL(state, 18, 24) | BITNUMINTL(state, 12, 25) | BITNUMINTL(state, 29, 26) |
BITNUMINTL(state, 5, 27) | BITNUMINTL(state, 21, 28) | BITNUMINTL(state, 10, 29) |
BITNUMINTL(state, 3, 30) | BITNUMINTL(state, 24, 31);
// Return the final state value
return (state);
}
void des_key_setup(const BYTE key[], BYTE schedule[][6], DES_MODE mode)
{
WORD i, j, to_gen, C, D;
const WORD key_rnd_shift[16] = {1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1};
const WORD key_perm_c[28] = {56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17,
9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35};
const WORD key_perm_d[28] = {62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21,
13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3};
const WORD key_compression[48] = {13, 16, 10, 23, 0, 4, 2, 27, 14, 5, 20, 9,
22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1,
40, 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 47,
43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31};
// Permutated Choice #1 (copy the key in, ignoring parity bits).
for (i = 0, j = 31, C = 0; i < 28; ++i, --j)
C |= BITNUM(key, key_perm_c[i], j);
for (i = 0, j = 31, D = 0; i < 28; ++i, --j)
D |= BITNUM(key, key_perm_d[i], j);
// Generate the 16 subkeys.
for (i = 0; i < 16; ++i)
{
C = ((C << key_rnd_shift[i]) | (C >> (28 - key_rnd_shift[i]))) & 0xfffffff0;
D = ((D << key_rnd_shift[i]) | (D >> (28 - key_rnd_shift[i]))) & 0xfffffff0;
// Decryption subkeys are reverse order of encryption subkeys so
// generate them in reverse if the key schedule is for decryption useage.
if (mode == DES_DECRYPT)
to_gen = 15 - i;
else /*(if mode == DES_ENCRYPT)*/
to_gen = i;
// Initialize the array
for (j = 0; j < 6; ++j)
schedule[to_gen][j] = 0;
for (j = 0; j < 24; ++j)
schedule[to_gen][j / 8] |= BITNUMINTR(C, key_compression[j], 7 - (j % 8));
for (; j < 48; ++j)
schedule[to_gen][j / 8] |= BITNUMINTR(D, key_compression[j] - 27, 7 - (j % 8));
}
}
void des_crypt(const BYTE in[], BYTE out[], const BYTE key[][6])
{
WORD state[2], idx, t;
IP(state, in);
for (idx = 0; idx < 15; ++idx)
{
t = state[1];
state[1] = f(state[1], key[idx]) ^ state[0];
state[0] = t;
}
// Perform the final loop manually as it doesn't switch sides
state[0] = f(state[1], key[15]) ^ state[0];
InvIP(state, out);
}
/*
void three_des_key_setup(const BYTE key[], BYTE schedule[][16][6], DES_MODE mode)
{
if (mode == DES_ENCRYPT)
{
des_key_setup(&key[0], schedule[0], mode);
des_key_setup(&key[8], schedule[1], DES_DECRYPT);
des_key_setup(&key[16], schedule[2], mode);
}
else // if (mode == DES_DECRYPT
{
des_key_setup(&key[16], schedule[0], mode);
des_key_setup(&key[8], schedule[1], DES_ENCRYPT);
des_key_setup(&key[0], schedule[2], mode);
}
}
void three_des_crypt(const BYTE in[], BYTE out[], const BYTE key[][16][6])
{
des_crypt(in, out, key[0]);
des_crypt(out, out, key[1]);
des_crypt(out, out, key[2]);
}
*/

26
deps/pyqdes/des.h vendored Normal file
View File

@ -0,0 +1,26 @@
#ifndef DES_H
#define DES_H
/*************************** HEADER FILES ***************************/
#include <stddef.h>
/****************************** MACROS ******************************/
#define DES_BLOCK_SIZE 8 // DES operates on 8 bytes at a time
/**************************** DATA TYPES ****************************/
typedef unsigned char BYTE; // 8-bit byte
typedef unsigned int WORD; // 32-bit word, change to "long" for 16-bit machines
typedef enum {
DES_ENCRYPT,
DES_DECRYPT
} DES_MODE;
/*********************** FUNCTION DECLARATIONS **********************/
void des_key_setup(const BYTE key[], BYTE schedule[][6], DES_MODE mode);
void des_crypt(const BYTE in[], BYTE out[], const BYTE key[][6]);
// void three_des_key_setup(const BYTE key[], BYTE schedule[][16][6], DES_MODE mode);
// void three_des_crypt(const BYTE in[], BYTE out[], const BYTE key[][16][6]);
#endif // DES_H

56
deps/pyqdes/main.cpp vendored Normal file
View File

@ -0,0 +1,56 @@
#include "des.cpp"
#include <pybind11/pybind11.h>
unsigned char KEY1[] = "!@#)(NHLiuy*$%^&";
unsigned char KEY2[] = "123ZXC!@#)(*$%^&";
unsigned char KEY3[] = "!@#)(*$%^&abcDEF";
int func_des(unsigned char *buff, unsigned char *key, int len)
{
BYTE schedule[16][6];
des_key_setup(key, schedule, DES_ENCRYPT);
for (int i = 0; i < len; i += 8)
des_crypt(buff + i, buff + i, schedule);
return 0;
}
int func_ddes(unsigned char *buff, unsigned char *key, int len)
{
BYTE schedule[16][6];
des_key_setup(key, schedule, DES_DECRYPT);
for (int i = 0; i < len; i += 8)
des_crypt(buff + i, buff + i, schedule);
return 0;
}
void LyricDecode_(unsigned char *content, int len)
{
func_ddes(content, KEY1, len);
func_des(content, KEY2, len);
func_ddes(content, KEY3, len);
}
namespace py = pybind11;
// 修改 LyricDecode 函数,接受和返回字节数组
py::bytes LyricDecode(py::bytes input)
{
// 获取输入字节数组的指针和长度
const char *input_ptr = PyBytes_AsString(input.ptr());
Py_ssize_t input_len = PyBytes_Size(input.ptr());
// 复制输入数据以便修改
std::vector<unsigned char> data(input_ptr, input_ptr + input_len);
// 调用 LyricDecode 函数进行解密
LyricDecode_(data.data(), data.size());
// 创建输出字节数组
py::bytes output(reinterpret_cast<const char *>(data.data()), data.size());
return output;
}
PYBIND11_MODULE(qdes, m) {
m.def("LyricDecode", &LyricDecode, "Decrypt a string");
}

16
deps/pyqdes/setup.py vendored Normal file
View File

@ -0,0 +1,16 @@
from setuptools import setup, Extension
import pybind11
module = Extension(
'qdes',
sources=['main.cpp'],
include_dirs=[pybind11.get_include(), pybind11.get_include(user=True)],
extra_compile_args=["-std=c++11"], # 添加编译器选项
)
setup(
name='qdes',
version='1.0',
description='QDES encryption/decryption module',
ext_modules=[module],
)

View File

@ -1,15 +0,0 @@
version: "3.8"
services:
lx:
container_name: lx-server
build: .
ports:
- "9763:9763"
volumes:
- .:/app
environment:
TZ: 'Asia/Shanghai'
restart: always
networks:
default:

288
lx-music-source-example.js Normal file
View File

@ -0,0 +1,288 @@
/*!
* @name 替换为你的音乐源名称
* @description 替换为你的音乐源介绍
* @version v2.0.1
* @author Folltoshe & helloplhm-qwq & lerdb
* @repository https://github.com/lxmusics/lx-music-api-server
*/
// 是否开启开发模式
const DEV_ENABLE = true
// 是否开启更新提醒
const UPDATE_ENABLE = true
// 服务端地址
const API_URL = 'http://127.0.0.1:9763'
// 服务端配置的请求key
const API_KEY = ''
// 音质配置(key为音源名称,不要乱填.如果你账号为VIP可以填写到hires)
// 全部的支持值: ['128k', '320k', 'flac', 'flac24bit']
const MUSIC_QUALITY = {
kw: ['128k'],
kg: ['128k'],
tx: ['128k'],
wy: ['128k'],
mg: ['128k'],
}
// 音源配置(默认为自动生成,可以修改为手动)
const MUSIC_SOURCE = Object.keys(MUSIC_QUALITY)
MUSIC_SOURCE.push('local')
/**
* 下面的东西就不要修改了
*/
const { EVENT_NAMES, request, on, send, utils, env, version } = globalThis.lx
// MD5值,用来检查更新
const SCRIPT_MD5 = ''
/**
* URL请求
*
* @param {string} url - 请求的地址
* @param {object} options - 请求的配置文件
* @return {Promise} 携带响应体的Promise对象
*/
const httpFetch = (url, options = { method: 'GET' }) => {
return new Promise((resolve, reject) => {
console.log('--- start --- ' + url)
request(url, options, (err, resp) => {
if (err) return reject(err)
console.log('API Response: ', resp)
resolve(resp)
})
})
}
/**
* Encodes the given data to base64.
*
* @param {type} data - the data to be encoded
* @return {string} the base64 encoded string
*/
const handleBase64Encode = (data) => {
var data = utils.buffer.from(data, 'utf-8')
return utils.buffer.bufToString(data, 'base64')
}
/**
*
* @param {string} source - 音源
* @param {object} musicInfo - 歌曲信息
* @param {string} quality - 音质
* @returns {Promise<string>} 歌曲播放链接
* @throws {Error} - 错误消息
*/
const handleGetMusicUrl = async (source, musicInfo, quality) => {
if (source == 'local') {
if (!musicInfo.songmid.startsWith('server_')) throw new Error('upsupported local file')
const songId = musicInfo.songmid
const requestBody = {
p: songId.replace('server_', ''),
}
var t = 'c'
var b = handleBase64Encode(JSON.stringify(requestBody)) /* url safe*/.replace(/\+/g, '-').replace(/\//g, '_')
const targetUrl = `${API_URL}/local/${t}?q=${b}`
const request = await httpFetch(targetUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'User-Agent': `${env ? `lx-music-${env}/${version}` : `lx-music-request/${version}`}`,
'X-Request-Key': API_KEY,
},
follow_max: 5,
})
const { body } = request
if (body.code == 0 && body.data && body.data.file) {
var t = 'u'
var b = handleBase64Encode(JSON.stringify(requestBody)) /* url safe*/.replace(/\+/g, '-').replace(/\//g, '_')
return `${API_URL}/local/${t}?q=${b}`
}
throw new Error('404 Not Found')
}
const songId = musicInfo.hash ?? musicInfo.songmid
const request = await httpFetch(`${API_URL}/url/${source}/${songId}/${quality}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'User-Agent': `${env ? `lx-music-${env}/${version}` : `lx-music-request/${version}`}`,
'X-Request-Key': API_KEY,
},
follow_max: 5,
})
const { body } = request
if (!body || isNaN(Number(body.code))) throw new Error('unknow error')
if (env != 'mobile') console.groupEnd()
switch (body.code) {
case 0:
console.log(`handleGetMusicUrl(${source}_${musicInfo.songmid}, ${quality}) success, URL: ${body.data}`)
return body.data
case 1:
console.log(`handleGetMusicUrl(${source}_${musicInfo.songmid}, ${quality}) failed: ip被封禁`)
throw new Error('block ip')
case 2:
console.log(`handleGetMusicUrl(${source}_${musicInfo.songmid}, ${quality}) failed, ${body.msg}`)
throw new Error('get music url failed')
case 4:
console.log(`handleGetMusicUrl(${source}_${musicInfo.songmid}, ${quality}) failed, 远程服务器错误`)
throw new Error('internal server error')
case 5:
console.log(`handleGetMusicUrl(${source}_${musicInfo.songmid}, ${quality}) failed, 请求过于频繁,请休息一下吧`)
throw new Error('too many requests')
case 6:
console.log(`handleGetMusicUrl(${source}_${musicInfo.songmid}, ${quality}) failed, 请求参数错误`)
throw new Error('param error')
default:
console.log(`handleGetMusicUrl(${source}_${musicInfo.songmid}, ${quality}) failed, ${body.msg ? body.msg : 'unknow error'}`)
throw new Error(body.msg ?? 'unknow error')
}
}
const handleGetMusicPic = async (source, musicInfo) => {
switch (source) {
case 'local':
// 先从服务器检查是否有对应的类型,再响应链接
if (!musicInfo.songmid.startsWith('server_')) throw new Error('upsupported local file')
const songId = musicInfo.songmid
const requestBody = {
p: songId.replace('server_', ''),
}
var t = 'c'
var b = handleBase64Encode(JSON.stringify(requestBody))/* url safe*/.replace(/\+/g, '-').replace(/\//g, '_')
const targetUrl = `${API_URL}/local/${t}?q=${b}`
const request = await httpFetch(targetUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'User-Agent': `${env ? `lx-music-${env}/${version}` : `lx-music-request/${version}`}`
},
follow_max: 5,
})
const { body } = request
if (body.code === 0 && body.data.cover) {
var t = 'p'
var b = handleBase64Encode(JSON.stringify(requestBody))/* url safe*/.replace(/\+/g, '-').replace(/\//g, '_')
return `${API_URL}/local/${t}?q=${b}`
}
throw new Error('get music pic failed')
default:
throw new Error('action(pic) does not support source(' + source + ')')
}
}
const handleGetMusicLyric = async (source, musicInfo) => {
switch (source) {
case 'local':
if (!musicInfo.songmid.startsWith('server_')) throw new Error('upsupported local file')
const songId = musicInfo.songmid
const requestBody = {
p: songId.replace('server_', ''),
}
var t = 'c'
var b = handleBase64Encode(JSON.stringify(requestBody))/* url safe*/.replace(/\+/g, '-').replace(/\//g, '_')
const targetUrl = `${API_URL}/local/${t}?q=${b}`
const request = await httpFetch(targetUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'User-Agent': `${env ? `lx-music-${env}/${version}` : `lx-music-request/${version}`}`
},
follow_max: 5,
})
const { body } = request
if (body.code === 0 && body.data.lyric) {
var t = 'l'
var b = handleBase64Encode(JSON.stringify(requestBody))/* url safe*/.replace(/\+/g, '-').replace(/\//g, '_')
const request2 = await httpFetch(`${API_URL}/local/${t}?q=${b}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'User-Agent': `${env ? `lx-music-${env}/${version}` : `lx-music-request/${version}`}`
},
follow_max: 5,
})
if (request2.body.code === 0) {
return {
lyric: request2.body.data ?? "",
tlyric: "",
rlyric: "",
lxlyric: ""
}
}
throw new Error('get music lyric failed')
}
throw new Error('get music lyric failed')
default:
throw new Error('action(lyric) does not support source(' + source + ')')
}
}
// 检查源脚本是否有更新
const checkUpdate = async () => {
const request = await httpFetch(`${API_URL}/script?key=${API_KEY}&checkUpdate=${SCRIPT_MD5}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'User-Agent': `${env ? `lx-music-${env}/${version}` : `lx-music-request/${version}`}`
},
})
const { body } = request
if (!body || body.code !== 0) console.log('checkUpdate failed')
else {
console.log('checkUpdate success')
if (body.data != null) {
globalThis.lx.send(lx.EVENT_NAMES.updateAlert, { log: body.data.updateMsg, updateUrl: body.data.updateUrl })
}
}
}
// 生成歌曲信息
const musicSources = {}
MUSIC_SOURCE.forEach(item => {
musicSources[item] = {
name: item,
type: 'music',
actions: (item == 'local') ? ['musicUrl', 'pic', 'lyric'] : ['musicUrl'],
qualitys: (item == 'local') ? [] : MUSIC_QUALITY[item],
}
})
// 监听 LX Music 请求事件
on(EVENT_NAMES.request, ({ action, source, info }) => {
switch (action) {
case 'musicUrl':
if (env != 'mobile') {
console.group(`Handle Action(musicUrl)`)
console.log('source', source)
console.log('quality', info.type)
console.log('musicInfo', info.musicInfo)
} else {
console.log(`Handle Action(musicUrl)`)
console.log('source', source)
console.log('quality', info.type)
console.log('musicInfo', info.musicInfo)
}
return handleGetMusicUrl(source, info.musicInfo, info.type)
.then(data => Promise.resolve(data))
.catch(err => Promise.reject(err))
case 'pic':
return handleGetMusicPic(source, info.musicInfo)
.then(data => Promise.resolve(data))
.catch(err => Promise.reject(err))
case 'lyric':
return handleGetMusicLyric(source, info.musicInfo)
.then(data => Promise.resolve(data))
.catch(err => Promise.reject(err))
default:
console.error(`action(${action}) not support`)
return Promise.reject('action not support')
}
})
// 检查更新
if (UPDATE_ENABLE) checkUpdate()
// 向 LX Music 发送初始化成功事件
send(EVENT_NAMES.inited, { status: true, openDevTools: DEV_ENABLE, sources: musicSources })

View File

236
main.py
View File

@ -9,59 +9,72 @@
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
import time
import aiohttp
import asyncio
import traceback
import threading
import ujson as json
from aiohttp.web import Response, FileResponse, StreamResponse, Application
from io import TextIOWrapper
import sys
import os
if ((sys.version_info.major == 3 and sys.version_info.minor < 6) or sys.version_info.major == 2):
if sys.version_info < (3, 6):
print('Python版本过低请使用Python 3.6+ ')
sys.exit(1)
from common import config
# fix: module not found: common/modules
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from common import utils
from common import config, localMusic
from common import lxsecurity
from common import log
from common import Httpx
from common import variable
from common import scheduler
from common import lx_script
from aiohttp.web import Response
import ujson as json
import threading
import traceback
from common import gcsp
import modules
import asyncio
import aiohttp
import time
import os
def handleResult(dic, status = 200) -> Response:
return Response(body = json.dumps(dic, indent=2, ensure_ascii=False), content_type='application/json', status = status)
def handleResult(dic, status=200) -> Response:
if (not isinstance(dic, dict)):
dic = {
'code': 0,
'msg': 'success',
'data': dic
}
return Response(body=json.dumps(dic, indent=2, ensure_ascii=False), content_type='application/json', status=status)
logger = log.log("main")
aiologger = log.log('aiohttp_web')
stopEvent = None
if (sys.version_info.minor < 8 and sys.version_info.major == 3):
if sys.version_info < (3, 8):
logger.warning('您使用的Python版本已经停止更新不建议继续使用')
import concurrent
stopEvent = concurrent.futures._base.CancelledError
else:
stopEvent = asyncio.exceptions.CancelledError
def start_checkcn_thread() -> None:
threading.Thread(target=Httpx.checkcn).start()
# check request info before start
async def handle_before_request(app, handler):
async def handle_request(request):
try:
if (config.read_config('common.reverse_proxy.allow_proxy')):
if (request.headers.get(config.read_config('common.reverse_proxy.real_ip_header'))):
# proxy header
if (request.remote in config.read_config('common.reverse_proxy.proxy_whitelist_remote')):
request.remote_addr = request.headers.get(config.read_config('common.reverse_proxy.real_ip_header'))
else:
return handleResult({"code": 1, "msg": "反代客户端远程地址不在反代ip白名单中", "data": None}, 403)
else:
request.remote_addr = request.remote
if config.read_config("common.reverse_proxy.allow_proxy") and request.headers.get(
config.read_config("common.reverse_proxy.real_ip_header")):
if not (config.read_config("common.reverse_proxy.allow_public_ip") or utils.is_local_ip(request.remote)):
return handleResult({"code": 1, "msg": "不允许的公网ip转发", "data": None}, 403)
# proxy header
request.remote_addr = request.headers.get(config.read_config("common.reverse_proxy.real_ip_header"))
else:
request.remote_addr = request.remote
# check ip
@ -72,13 +85,13 @@ async def handle_before_request(app, handler):
(time.time() - config.getRequestTime('global'))
<
(config.read_config("security.rate_limit.global"))
):
):
return handleResult({"code": 5, "msg": "全局限速", "data": None}, 429)
if (
(time.time() - config.getRequestTime(request.remote_addr))
<
(config.read_config("security.rate_limit.ip"))
):
):
return handleResult({"code": 5, "msg": "IP限速", "data": None}, 429)
# update request time
config.updateRequestTime('global')
@ -87,23 +100,32 @@ async def handle_before_request(app, handler):
if (config.read_config("security.allowed_host.enable")):
if request.host.split(":")[0] not in config.read_config("security.allowed_host.list"):
if config.read_config("security.allowed_host.blacklist.enable"):
config.ban_ip(request.remote_addr, int(config.read_config("security.allowed_host.blacklist.length")))
config.ban_ip(request.remote_addr, int(
config.read_config("security.allowed_host.blacklist.length")))
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
resp = await handler(request)
if (isinstance(resp, str)):
resp = Response(body = resp, content_type='text/plain', status = 200)
elif (isinstance(resp, dict)):
if (isinstance(resp, (str, list, dict))):
resp = handleResult(resp)
elif (not isinstance(resp, Response)):
resp = Response(body = str(resp), content_type='text/plain', status = 200)
aiologger.info(f'{request.remote_addr + ("" if (request.remote == request.remote_addr) else f"|proxy@{request.remote}")} - {request.method} "{request.path}", {resp.status}')
elif (isinstance(resp, tuple) and len(resp) == 2): # flask like response
body, status = resp
if (isinstance(body, (str, list, dict))):
resp = handleResult(body, status)
else:
resp = Response(
body=str(body), content_type='text/plain', status=status)
elif (not isinstance(resp, (Response, FileResponse, StreamResponse))):
resp = Response(
body=str(resp), content_type='text/plain', status=200)
aiologger.info(
f'{request.remote_addr + ("" if (request.remote == request.remote_addr) else f"|proxy@{request.remote}")} - {request.method} "{request.path}", {resp.status}')
return resp
except:
logger.error(traceback.format_exc())
return {"code": 4, "msg": "内部服务器错误", "data": None}
return handle_request
async def main(request):
return handleResult({"code": 0, "msg": "success", "data": None})
@ -114,7 +136,7 @@ async def handle(request):
songId = request.match_info.get('songId')
quality = request.match_info.get('quality')
if (config.read_config("security.key.enable") and request.host.split(':')[0] not in config.read_config('security.whitelist_host')):
if (request.headers.get("X-Request-Key")) != config.read_config("security.key.value"):
if (request.headers.get("X-Request-Key")) not in config.read_config("security.key.values"):
if (config.read_config("security.key.ban")):
config.ban_ip(request.remote_addr)
return handleResult({"code": 1, "msg": "key验证失败", "data": None}, 403)
@ -128,47 +150,114 @@ async def handle(request):
try:
query = dict(request.query)
if (method in dir(modules)):
source_enable = config.read_config(f'module.{source}.enable')
if not source_enable:
return handleResult({
'code': 4,
'msg': '此平台已停止服务',
'data': None,
"Your IP": request.remote_addr
}, 404)
return handleResult(await getattr(modules, method)(source, songId, quality, query))
else:
return handleResult(await modules.other(source, songId, quality, query))
return handleResult(await modules.other(method, source, songId, quality, query))
except:
logger.error(traceback.format_exc())
return handleResult({'code': 4, 'msg': '内部服务器错误', 'data': None}, 500)
async def handle_404(request):
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
app = aiohttp.web.Application(middlewares=[handle_before_request])
async def handle_local(request):
try:
query = dict(request.query)
data = query.get('q')
data = utils.createBase64Decode(
data.replace('-', '+').replace('_', '/'))
data = json.loads(data)
t = request.match_info.get('type')
data['t'] = t
except:
logger.info(traceback.format_exc())
return handleResult({'code': 6, 'msg': '请求参数有错', 'data': None}, 404)
if (data['t'] == 'u'):
if (data['p'] in list(localMusic.map.keys())):
return await localMusic.generateAudioFileResonse(data['p'])
else:
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
if (data['t'] == 'l'):
if (data['p'] in list(localMusic.map.keys())):
return await localMusic.generateAudioLyricResponse(data['p'])
else:
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
if (data['t'] == 'p'):
if (data['p'] in list(localMusic.map.keys())):
return await localMusic.generateAudioCoverResonse(data['p'])
else:
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
if (data['t'] == 'c'):
if (not data['p'] in list(localMusic.map.keys())):
return {
'code': 0,
'msg': 'success',
'data': {
'file': False,
'cover': False,
'lyric': False
}
}
return {
'code': 0,
'msg': 'success',
'data': localMusic.checkLocalMusic(data['p'])
}
app = Application(middlewares=[handle_before_request])
utils.setGlobal(app, "app")
# mainpage
app.router.add_get('/', main)
# api
app.router.add_get('/{method}/{source}/{songId}/{quality}', handle)
app.router.add_get('/{method}/{source}/{songId}', handle)
app.router.add_get('/local/{type}', handle_local)
if (config.read_config('common.allow_download_script')):
app.router.add_get('/script', lx_script.generate_script_response)
if (config.read_config('module.gcsp.enable')):
app.router.add_route('*', config.read_config('module.gcsp.path'), gcsp.handle_request)
# 404
app.router.add_route('*', '/{tail:.*}', handle_404)
async def run_app():
while True:
try:
host = config.read_config('common.host')
ports = [int(port) for port in config.read_config('common.ports')]
ssl_ports = [int(port) for port in config.read_config('common.ssl_info.ssl_ports')]
async def run_app_host(host):
retries = 0
while True:
if (retries > 4):
logger.warning("重试次数已达上限,但仍有部分端口未能完成监听,已自动进行忽略")
break
try:
ports = [int(port)
for port in config.read_config('common.ports')]
ssl_ports = [int(port) for port in config.read_config(
'common.ssl_info.ssl_ports')]
final_ssl_ports = []
final_ports = []
for p in ports:
if (p not in ssl_ports):
if (p not in ssl_ports and f'{host}_{p}' not in variable.running_ports):
final_ports.append(p)
else:
final_ssl_ports.append(p)
if (p not in variable.running_ports):
final_ssl_ports.append(p)
# 读取证书和私钥路径
cert_path = config.read_config('common.ssl_info.path.cert')
privkey_path = config.read_config('common.ssl_info.path.privkey')
privkey_path = config.read_config(
'common.ssl_info.path.privkey')
# 创建 HTTP AppRunner
http_runner = aiohttp.web.AppRunner(app)
@ -176,15 +265,22 @@ async def run_app():
# 启动 HTTP 端口监听
for port in final_ports:
http_site = aiohttp.web.TCPSite(http_runner, host, port)
await http_site.start()
logger.info(f"监听 -> http://{host}:{port}")
if (port not in variable.running_ports):
http_site = aiohttp.web.TCPSite(
http_runner, host, port)
await http_site.start()
variable.running_ports.append(f'{host}_{port}')
logger.info(f"""监听 -> http://{
host if (':' not in host)
else '[' + host + ']'
}:{port}""")
if (config.read_config("common.ssl_info.enable") and final_ssl_ports != []):
if (os.path.exists(cert_path) and os.path.exists(privkey_path)):
import ssl
# 创建 SSL 上下文,加载配置文件中指定的证书和私钥
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context = ssl.create_default_context(
ssl.Purpose.CLIENT_AUTH)
ssl_context.load_cert_chain(cert_path, privkey_path)
# 创建 HTTPS AppRunner
@ -193,24 +289,37 @@ async def run_app():
# 启动 HTTPS 端口监听
for port in ssl_ports:
https_site = aiohttp.web.TCPSite(https_runner, host, port, ssl_context=ssl_context)
await https_site.start()
logger.info(f"监听 -> https://{host}:{port}")
return
if (port not in variable.running_ports):
https_site = aiohttp.web.TCPSite(
https_runner, host, port, ssl_context=ssl_context)
await https_site.start()
variable.running_ports.append(f'{host}_{port}')
logger.info(f"""监听 -> https://{
host if (':' not in host)
else '[' + host + ']'
}:{port}""")
logger.debug(f"HOST({host}) 已完成监听")
break
except OSError as e:
if str(e).startswith("[Errno 98]"):
if (str(e).startswith("[Errno 98]") or str(e).startswith('[Errno 10048]')):
logger.error("端口已被占用,请检查\n" + str(e))
logger.info('服务器将在10s后再次尝试启动...')
await asyncio.sleep(10)
logger.info('重新尝试启动...')
retries += 1
else:
raise
logger.error("未知错误,请检查\n" + traceback.format_exc())
async def run_app():
for host in config.read_config('common.hosts'):
await run_app_host(host)
async def initMain():
await scheduler.run()
variable.aioSession = aiohttp.ClientSession(trust_env=True)
localMusic.initMain()
try:
await run_app()
logger.info("服务器启动成功请按下Ctrl + C停止")
@ -238,5 +347,20 @@ if __name__ == "__main__":
except KeyboardInterrupt:
pass
except:
logger.error('初始化出错,请检查日志')
logger.error(traceback.format_exc())
logger.critical('初始化出错,请检查日志')
logger.critical(traceback.format_exc())
with open('dumprecord_{}.txt'.format(int(time.time())), 'w', encoding='utf-8') as f:
f.write(traceback.format_exc())
e = '\n\nGlobal variable object:\n\n'
for k in dir(variable):
e += (k + ' = ' + str(getattr(variable, k)) + '\n') if (not k.startswith('_')) else ''
f.write(e)
e = '\n\nsys.modules:\n\n'
for k in sys.modules:
e += (k + ' = ' + str(sys.modules[k]) + '\n') if (not k.startswith('_')) else ''
f.write(e)
logger.critical('dumprecord_{}.txt 已保存至当前目录'.format(int(time.time())))
finally:
for f in variable.log_files:
if (f and isinstance(f, TextIOWrapper)):
f.close()

View File

@ -11,6 +11,7 @@ from common.exceptions import FailedException
from common.utils import require
from common import log
from common import config
# 从.引入的包并没有在代码中直接使用但是是用require在请求时进行引入的不要动
from . import kw
from . import mg
@ -20,194 +21,195 @@ from . import wy
import traceback
import time
logger = log.log('api_handler')
logger = log.log("api_handler")
sourceExpirationTime = {
'tx': {
"tx": {
"expire": True,
"time": 80400, # 不知道tx为什么要取一个这么不对劲的数字当过期时长
},
'kg': {
"kg": {
"expire": True,
"time": 24 * 60 * 60, # 24 hours
},
'kw': {
"expire": True,
"time": 60 * 60 # 60 minutes
},
'wy': {
"kw": {"expire": True, "time": 60 * 60}, # 60 minutes
"wy": {
"expire": True,
"time": 20 * 60, # 20 minutes
},
'mg': {
"mg": {
"expire": False,
"time": 0,
}
},
}
async def url(source, songId, quality, query):
if (not quality):
async def url(source, songId, quality, query={}):
if not quality:
return {
'code': 2,
'msg': '需要参数"quality"',
'data': None,
"code": 2,
"msg": '需要参数"quality"',
"data": None,
}
if (source == "kg"):
if source == "kg":
songId = songId.lower()
try:
cache = config.getCache('urls', f'{source}_{songId}_{quality}')
cache = config.getCache("urls", f"{source}_{songId}_{quality}")
if cache:
logger.debug(f'使用缓存的{source}_{songId}_{quality}数据URL{cache["url"]}')
return {
'code': 0,
'msg': 'success',
'data': cache['url'],
'extra': {
'cache': True,
'quality': {
'target': quality,
'result': quality,
"code": 0,
"msg": "success",
"data": cache["url"],
"extra": {
"cache": True,
"quality": {
"target": quality,
"result": quality,
},
'expire': {
"expire": {
# 在更新缓存的时候把有效期的75%作为链接可用时长,现在加回来
'time': int(cache['time'] + (sourceExpirationTime[source]['time'] * 0.25)) if cache['expire'] else None,
'canExpire': cache['expire'],
}
"time": (
int(cache["time"] + (sourceExpirationTime[source]["time"] * 0.25))
if cache["expire"]
else None
),
"canExpire": cache["expire"],
},
},
}
except:
logger.error(traceback.format_exc())
try:
func = require('modules.' + source + '.url')
func = require("modules." + source + ".url")
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
"code": 1,
"msg": "未知的源或不支持的方法",
"data": None,
}
try:
result = await func(songId, quality)
logger.info(f'获取{source}_{songId}_{quality}成功URL{result["url"]}')
canExpire = sourceExpirationTime[source]['expire']
expireTime = sourceExpirationTime[source]['time'] + int(time.time())
config.updateCache('urls', f'{source}_{songId}_{quality}', {
"expire": canExpire,
# 取有效期的75%作为链接可用时长
"time": int(expireTime - sourceExpirationTime[source]['time'] * 0.25),
"url": result['url'],
})
canExpire = sourceExpirationTime[source]["expire"]
expireTime = int(sourceExpirationTime[source]["time"] * 0.75)
expireAt = int(expireTime + time.time())
config.updateCache(
"urls",
f"{source}_{songId}_{quality}",
{
"expire": canExpire,
# 取有效期的75%作为链接可用时长
"time": expireAt,
"url": result["url"],
},
expireTime if canExpire else None,
)
logger.debug(f'缓存已更新:{source}_{songId}_{quality}, URL{result["url"]}, expire: {expireTime}')
return {
'code': 0,
'msg': 'success',
'data': result['url'],
'extra': {
'cache': False,
'quality': {
'target': quality,
'result': result['quality'],
"code": 0,
"msg": "success",
"data": result["url"],
"extra": {
"cache": False,
"quality": {
"target": quality,
"result": result["quality"],
},
'expire': {
'time': expireTime if canExpire else None,
'canExpire': canExpire,
"expire": {
"time": expireAt if canExpire else None,
"canExpire": canExpire,
},
},
}
except FailedException as e:
logger.info(f'获取{source}_{songId}_{quality}失败,原因:' + e.args[0])
logger.info(f"获取{source}_{songId}_{quality}失败,原因:" + e.args[0])
return {
'code': 2,
'msg': e.args[0],
'data': None,
"code": 2,
"msg": e.args[0],
"data": None,
}
async def lyric(source, songId, _, query):
cache = config.getCache('lyric', f'{source}_{songId}')
cache = config.getCache("lyric", f"{source}_{songId}")
if cache:
return {
'code': 0,
'msg': 'success',
'data': cache['data']
}
return {"code": 0, "msg": "success", "data": cache["data"]}
try:
func = require('modules.' + source + '.lyric')
func = require("modules." + source + ".lyric")
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
"code": 1,
"msg": "未知的源或不支持的方法",
"data": None,
}
try:
result = await func(songId)
config.updateCache('lyric', f'{source}_{songId}', {
"data": result,
"time": int(time.time() + (86400 * 3)), # 歌词缓存3天
"expire": True,
})
logger.debug(f'缓存已更新:{source}_{songId}, lyric: {result}')
return {
'code': 0,
'msg': 'success',
'data': result
}
expireTime = 86400 * 3
expireAt = int(time.time() + expireTime)
config.updateCache(
"lyric",
f"{source}_{songId}",
{
"data": result,
"time": expireAt, # 歌词缓存3天
"expire": True,
},
expireTime,
)
logger.debug(f"缓存已更新:{source}_{songId}, lyric: {result}")
return {"code": 0, "msg": "success", "data": result}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
"code": 2,
"msg": e.args[0],
"data": None,
}
async def search(source, songid, _, query):
try:
func = require('modules.' + source + '.search')
func = require("modules." + source + ".search")
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
"code": 1,
"msg": "未知的源或不支持的方法",
"data": None,
}
try:
result = await func(songid, query)
return {
'code': 0,
'msg': 'success',
'data': result
}
return {"code": 0, "msg": "success", "data": result}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
"code": 2,
"msg": e.args[0],
"data": None,
}
async def other(method, source, songid, _, query):
try:
func = require('modules.' + source + '.' + method)
func = require("modules." + source + "." + method)
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
"code": 1,
"msg": "未知的源或不支持的方法",
"data": None,
}
try:
result = await func(songid)
return {
'code': 0,
'msg': 'success',
'data': result
}
return {"code": 0, "msg": "success", "data": result}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
"code": 2,
"msg": e.args[0],
"data": None,
}
async def info_with_query(source, songid, _, query):
return await other('info', source, songid, None)
return await other("info", source, songid, None)

View File

@ -17,10 +17,12 @@ from .lyric import lyricSearchByHash as _lyricSearch
from .mv import getMvInfo as _getMvInfo
from .mv import getMvPlayURL as _getMvUrl
from .search import getSongSearchResult as _songsearch
from . import lite_signin
from common.exceptions import FailedException
from common import Httpx
from common import utils
import asyncio
from . import refresh_login
async def info(hash_):
tasks = []

168
modules/kg/lite_signin.py Normal file
View File

@ -0,0 +1,168 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: lite_signin.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from common.exceptions import FailedException
from .utils import buildRequestParams, sign
from common import Httpx, config, utils, variable, scheduler, log
import random
import binascii
import time
logger = log.log('kugou_lite_sign_in')
async def randomMixSongMid():
'''
通过TOP500榜单获取随机歌曲的mixsongmid
'''
# 声明榜单url
rankUrl = 'http://mobilecdnbj.kugou.com/api/v3/rank/song?version=9108&ranktype=1&plat=0&pagesize=100&area_code=1&page=1&rankid=8888&with_res_tag=0&show_portrait_mv=1'
# 请求
res = await Httpx.AsyncRequest(rankUrl, {
"method": 'GET'
})
data = res.json()
if (data.get('status') != 1):
raise FailedException('排行榜获取失败')
# 随机选择一首歌曲
randomSong = random.choice(data['data']['info'])
# 因为排行榜api不会返回mixsongmid
# 所以需要进行一次搜索接口来获取
search_req = await Httpx.AsyncRequest(utils.encodeURI(f'https://songsearch.kugou.com/song_search_v2?' + buildRequestParams({
"keyword": randomSong['filename'],
"area_code": 1,
"page": 1,
"pagesize": 1,
"userid": 0,
"clientver": "",
"platform": "WebFilter",
"filter": 2,
"iscorrection": 1,
"privilege_filter": 0
})), {
"headers": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.142.86 Safari/537.36",
"Referer": "https://www.kugou.com",
}
})
body = search_req.json()
if (body.get('status') != 1):
raise FailedException('歌曲搜索失败')
if (body['data']['total'] == 0 or body['data']['lists'] == []):
raise FailedException('歌曲搜索失败')
return body['data']['lists'][0]['MixSongID']
async def do_account_signin(user_info):
'''
签到主函数传入userinfo响应None就是成功报错即为不成功
'''
# 检查用户配置文件获取mixsongmid
mixid = user_info['lite_sign_in']['mixsongmid']['value']
if (mixid == 'auto'):
mixid = await randomMixSongMid()
# 声明变量
headers = {
'User-Agent': f'Android712-AndroidPhone-{config.read_config("module.kg.client.clientver")}-18-0-NetMusic-wifi',
'KG-THash': '3e5ec6b',
'KG-Rec': '1',
'KG-RC': '1',
"x-router": "youth.kugou.com"
}
body = """{"mixsongid":__id__}""".replace("__id__", str(mixid))
# params = "userid={}&token={}&appid=3116&clientver=10518&clienttime={}&mid={}&uuid={}&dfid=-".format(read_config("common.kg.userid"), read_config("common.kg.token"), int(time.time()), read_config("common.kg.mid"), str(binascii.hexlify(random.randbytes(16)), encoding = "utf-8"))
params = {
"userid": user_info['userid'],
"token": user_info['token'],
"appid": 3116,
"clientver": config.read_config('module.kg.client.clientver'),
"clienttime": int(time.time()),
"mid": user_info['mid'],
"uuid": str(binascii.hexlify(random.randbytes(16)), encoding="utf-8"),
"dfid": "-"
}
params['signature'] = sign(
params, body, config.read_config('module.kg.client.signatureKey'))
# 发送请求
req = await Httpx.AsyncRequest(f"https://gateway.kugou.com/v2/report/listen_song?" +
buildRequestParams(params), {
"method": "POST",
"body": body,
"headers": headers
})
req = req.json()
if req['status'] == 1:
return
else:
raise FailedException(req['error_msg'])
def task_handler():
# not lite client configure
if (int(config.read_config('module.kg.client.appid')) != 3116):
return
# no user
if ((not variable.use_cookie_pool) and (not config.read_config('module.kg.user.token'))):
return
# devide cookiepool
if (variable.use_cookie_pool):
pool = config.read_config('module.cookiepool.kg')
for user in pool:
index = pool.index(user)
if (user.get('lite_sign_in') is None):
user['lite_sign_in'] = {
"desc": "是否启用概念版自动签到仅在appid=3116时运行",
"enable": False,
"interval": 86400,
"mixsongmid": {
"desc": "mix_songmid的获取方式, 默认auto, 可以改成一个数字手动",
"value": "auto"
}
}
pool[index] = user
config.write_config('module.cookiepool.kg', pool)
logger.info(f'用户池用户(index = {index})配置缺失lite_sign_in字段已自动写入')
# refresh
pool = config.read_config('module.cookiepool.kg')
# add signin schedule task
for user in pool:
if (user.get('lite_sign_in').get('enable')):
scheduler.append(f'kugou_lite_sign_in_{user["userid"]}', do_account_signin, user['lite_sign_in']['interval'], {'user_info': user})
else:
user_info = config.read_config('module.kg.user')
if (user_info.get('lite_sign_in') is None):
user_info['lite_sign_in'] = {
"desc": "是否启用概念版自动签到仅在appid=3116时运行",
"enable": False,
"interval": 86400,
"mixsongmid": {
"desc": "mix_songmid的获取方式, 默认auto, 可以改成一个数字手动",
"value": "auto"
}
}
config.write_config('module.kg.user', user_info)
logger.info('用户配置缺失lite_sign_in字段已自动写入')
if (user_info.get('lite_sign_in').get('enable')):
scheduler.append(f'kugou_lite_sign_in', do_account_signin, user_info['lite_sign_in']['interval'], {'user_info': user_info})
task_handler()

View File

@ -46,7 +46,7 @@ async def url(songId, quality):
'album_audio_id': albumaudioid,
'behavior': 'play',
'clienttime': int(time.time()),
'pid': 2,
'pid': tools.pid,
'key': getKey(thash, user_info),
'dfid': '-',
'pidversion': 3001

209
modules/kg/refresh_login.py Normal file
View File

@ -0,0 +1,209 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq - (feat. Huibq and ikun0014)
# - name: refresh_login.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
import time
from common import variable
from common import scheduler
from common import config
from common import log
from .utils import signRequest, tools, aes_sign
import ujson as json
logger = log.log('kg_refresh_login')
async def refresh():
if (not config.read_config('module.kg.user.token')):
return
if (not config.read_config('module.kg.user.refresh_login.enable')):
return
user_id = config.read_config('module.kg.user.userid')
token = config.read_config('module.kg.user.token')
if (config.read_config('module.kg.client.appid') == '1005'):
ts = int(time.time() * 1000)
p3 = aes_sign(json.dumps({'clienttime': ts // 1000, 'token': token}))
data = {
'p3': p3,
'clienttime_ms': ts,
't1': 0,
't2': 0,
'userid': user_id
}
params = {
'dfid': '-',
'appid': tools.appid,
'mid': tools.mid,
'clientver': tools.clientver,
'clienttime': ts // 1000
}
headers = {
'User-Agent': 'Android712-AndroidPhone-8983-18-0-NetMusic-wifi',
'KG-THash': '3e5ec6b',
'KG-Rec': '1',
'KG-RC': '1',
}
login_url = config.read_config('module.kg.user.refresh_login.login_url')
req = await signRequest(login_url, params, {'method': 'POST', 'json': data, 'headers': headers})
body = req.json()
if body['error_code'] != 0:
logger.warning(f'酷狗音乐账号(UID_{user_id})刷新登录失败, code: ' +
str(body['error_code']) + f'\n响应体: {body}')
return
else:
logger.info(f'酷狗音乐账号(UID_{user_id})刷新登录成功')
config.write_config('module.kg.user.userid',
str(body['data']['userid']))
config.write_config('module.kg.user.token',
body['data']['token'])
logger.info(f'为酷狗音乐账号(UID_{user_id})数据更新完毕')
return
elif (config.read_config('module.kg.client.appid') == '3116'):
ts = int(time.time() * 1000)
p3 = aes_sign(json.dumps({'clienttime': ts // 1000, 'token': token}), key=b'c24f74ca2820225badc01946dba4fdf7', iv=b'adc01946dba4fdf7')
data = {
'p3': p3,
'clienttime_ms': ts,
't1': 0,
't2': 0,
'userid': user_id
}
params = {
'dfid': '-',
'appid': tools.appid,
'mid': tools.mid,
'clientver': tools.clientver,
'clienttime': ts // 1000
}
headers = {
'User-Agent': 'Android712-AndroidPhone-8983-18-0-NetMusic-wifi',
'KG-THash': '3e5ec6b',
'KG-Rec': '1',
'KG-RC': '1',
}
login_url = config.read_config('module.kg.user.refresh_login.login_url')
req = await signRequest(login_url, params, {'method': 'POST', 'json': data, 'headers': headers})
body = req.json()
if body['error_code'] != 0:
logger.warning(f'酷狗音乐账号(UID_{user_id})刷新登录失败, code: ' +
str(body['error_code']) + f'\n响应体: {body}')
return
else:
logger.info(f'酷狗音乐账号(UID_{user_id})刷新登录成功')
config.write_config('module.kg.user.userid',
str(body['data']['userid']))
config.write_config('module.kg.user.token',
body['data']['token'])
logger.info(f'为酷狗音乐账号(UID_{user_id})数据更新完毕')
return
if (not variable.use_cookie_pool):
kgconfig = config.read_config('module.kg')
refresh_login_info = kgconfig.get('refresh_login')
if (refresh_login_info):
kgconfig['user']['refresh_login'] = refresh_login_info
kgconfig.pop('refresh_login')
config.write_config('module.kg', kgconfig)
if (config.read_config('module.kg.user.refresh_login.enable') and not variable.use_cookie_pool):
scheduler.append('kg_refresh_login', refresh,
config.read_config('module.kg.user.refresh_login.interval'))
async def refresh_login_for_pool(user_info):
user_id = user_info["userid"]
token = user_info["token"]
if (config.read_config('module.kg.client.appid') == '1005'):
ts = int(time.time() * 1000)
p3 = aes_sign(json.dumps({'clienttime': ts // 1000, 'token': token}))
data = {
'p3': p3,
'clienttime_ms': ts,
't1': 0,
't2': 0,
'userid': user_id
}
params = {
'dfid': '-',
'appid': tools.appid,
'mid': tools.mid,
'clientver': tools.clientver,
'clienttime': ts // 1000
}
headers = {
'User-Agent': 'Android712-AndroidPhone-8983-18-0-NetMusic-wifi',
'KG-THash': '3e5ec6b',
'KG-Rec': '1',
'KG-RC': '1',
}
login_url = user_info["refresh_login"]["login_url"]
req = await signRequest(login_url, params, {'method': 'POST', 'json': data, 'headers': headers})
body = req.json()
if body['error_code'] != 0:
logger.warning(f'酷狗音乐账号(UID_{user_id})刷新登录失败, code: ' +
str(body['error_code']) + f'\n响应体: {body}')
return
else:
logger.info(f'为酷狗音乐账号(UID_{user_id})刷新登录成功')
user_list = config.read_config('module.cookiepool.kg')
user_list[user_list.index(
user_info)]['token'] = body['data']['token']
user_list[user_list.index(
user_info)]['userid'] = str(body['data']['userid'])
config.write_config('module.cookiepool.kg', user_list)
logger.info(f'为酷狗音乐账号(UID_{user_id})数据更新完毕')
elif (config.read_config('module.kg.client.appid') == '3116'):
ts = int(time.time() * 1000)
p3 = aes_sign(json.dumps({'clienttime': ts // 1000, 'token': token}), key=b'c24f74ca2820225badc01946dba4fdf7', iv=b'adc01946dba4fdf7')
data = {
'p3': p3,
'clienttime_ms': ts,
't1': 0,
't2': 0,
'userid': user_id
}
params = {
'dfid': '-',
'appid': tools.appid,
'mid': tools.mid,
'clientver': tools.clientver,
'clienttime': ts // 1000
}
headers = {
'User-Agent': 'Android712-AndroidPhone-8983-18-0-NetMusic-wifi',
'KG-THash': '3e5ec6b',
'KG-Rec': '1',
'KG-RC': '1',
}
login_url = user_info["refresh_login"]["login_url"]
req = await signRequest(login_url, params, {'method': 'POST', 'json': data, 'headers': headers})
body = req.json()
if body['error_code'] != 0:
logger.warning(f'酷狗音乐账号(UID_{user_id})刷新登录失败, code: ' +
str(body['error_code']) + f'\n响应体: {body}')
return
else:
logger.info(f'为酷狗音乐账号(UID_{user_id})刷新登录成功')
user_list = config.read_config('module.cookiepool.kg')
user_list[user_list.index(
user_info)]['token'] = body['data']['token']
user_list[user_list.index(
user_info)]['userid'] = str(body['data']['userid'])
config.write_config('module.cookiepool.kg', user_list)
logger.info(f'为酷狗音乐账号(UID_{user_id})数据更新完毕')
return
def reg_refresh_login_pool_task():
user_info_pool = config.read_config('module.cookiepool.kg')
for user_info in user_info_pool:
if (user_info['refresh_login'].get('enable')):
scheduler.append(
f'kgmusic_refresh_login_pooled_{user_info["userid"]}', refresh_login_for_pool, int(604800), args = {'user_info': user_info})
if (variable.use_cookie_pool):
reg_refresh_login_pool_task()

View File

@ -66,6 +66,7 @@ async def getSongSearchResult(query, page = 1, size = 20):
"keyword": query,
"page": page,
"pagesize": size,
"area_code": 1,
"userid": 0,
"clientver": "",
"platform": "WebFilter",

View File

@ -6,10 +6,12 @@
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from common import utils
from common import config
from common import Httpx
import json
createObject = utils.CreateObject
@ -23,6 +25,8 @@ tools = createObject({
"version": config.read_config("module.kg.tracker.version"),
"extra_params": config.read_config("module.kg.tracker.extra_params"),
"appid": config.read_config("module.kg.client.appid"),
'mid': config.read_config('module.kg.user.mid'),
"pid": config.read_config("module.kg.client.pid"),
'qualityHashMap': {
'128k': 'hash_128',
'320k': 'hash_320',
@ -43,19 +47,26 @@ def buildSignatureParams(dictionary, body = ""):
joined_str = ''.join([f'{k}={v}' for k, v in dictionary.items()])
return joined_str + body
def buildRequestParams(dictionary):
def buildRequestParams(dictionary: dict):
joined_str = '&'.join([f'{k}={v}' for k, v in dictionary.items()])
return joined_str
def sign(params, body = "", signkey = tools["signkey"]):
if (isinstance(body, dict)):
body = json.dumps(body)
params = utils.sortDict(params)
params = buildSignatureParams(params, body)
return utils.createMD5(signkey + params + signkey)
async def signRequest(url, params, options, signkey = tools["signkey"]):
params['signature'] = sign(params, options.get("body") if options.get("body") else (options.get("data") if options.get("data") else ""), signkey)
params['signature'] = sign(params, options.get("body") if options.get("body") else (options.get("data") if options.get("data") else (options.get("json") if options.get("json") else "")), signkey)
url = url + "?" + buildRequestParams(params)
return await Httpx.AsyncRequest(url, options)
def getKey(hash_, user_info):
return utils.createMD5(hash_.lower() + tools.pidversec + tools.appid + user_info['mid'] + user_info['userid'])
def aes_sign(plain_text, key=b'90b8382a1bb4ccdcf063102053fd75b8', iv=b'f063102053fd75b8'):
cipher = AES.new(key, AES.MODE_CBC, iv)
crypto = cipher.encrypt(pad(plain_text.encode(), AES.block_size))
return crypto.hex()

View File

@ -19,6 +19,10 @@ tools = {
'320k': '320kmp3',
'flac': '2000kflac',
'flac24bit': '4000kflac',
'128kmp3': '128kmp3',
'320kmp3': '320kmp3',
"2000kflac": "2000kflac",
"4000kflac": "4000kflac",
},
'qualityMapReverse': {
128: '128k',
@ -38,7 +42,7 @@ async def url(songId, quality):
proto = config.read_config('module.kw.proto')
if (proto == 'bd-api'):
user_info = config.read_config('module.kw.user') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.kw'))
target_url = f'''https://bd-api.kuwo.cn/api/service/music/downloadInfo/{songId}?isMv=0&format={tools['extMap'][quality]}&br={tools['qualityMap'][quality]}&uin={user_info['uid']}&token={user_info['token']}'''
target_url = f'''https://bd-api.kuwo.cn/api/service/music/downloadInfo/{songId}?isMv=0&format={tools['extMap'][quality]}&br={tools['qualityMap'][quality]}&uid={user_info['uid']}&token={user_info['token']}'''
req = await Httpx.AsyncRequest(target_url, {
'method': 'GET',
'headers': {
@ -80,13 +84,14 @@ async def url(songId, quality):
url = ''
bitrate = 1
if (des_info["response_type"] == 'json'):
body = req.json()
url = req.json()
for p in des_info['url_json_path'].split('.'):
url = body.get(p)
url = url.get(p)
if (url == None):
raise FailedException('failed')
bitrate = req.json()
for p in des_info['bitrate_json_path'].split('.'):
bitrate = body.get(p)
bitrate = bitrate.get(p)
if (bitrate == None):
raise FailedException('failed')
elif (des_info['response_type'] == 'text'):

View File

@ -12,48 +12,52 @@ from common import Httpx
from common import config
from common import variable
from common.exceptions import FailedException
from . import refresh_login # 删了这个定时任务会寄掉
tools = {
'url': 'https://app.c.nf.migu.cn/MIGUM2.0/strategy/listen-url/v2.4?toneFlag=__quality__&songId=__songId__&resourceType=2',
'qualityMap': {
'128k': 'PQ',
'320k': 'HQ',
'flac': 'SQ',
'flac24bit': 'ZQ',
'128k': '1',
'320k': '2',
'flac': '3',
'flac24bit': '4',
"master": "5"
},
'qualityMapReverse': {
'PQ': '128k',
'HQ': '320k',
'SQ': 'flac',
'ZQ': 'flac24bit',
'000009': '128k',
'020010': '320k',
'011002': 'flac',
'011005': 'flac24bit',
},
}
async def url(songId, quality):
async def url(songmid, quality):
info_url = f"http://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/resourceinfo.do?resourceType=2&copyrightId=" + songmid
info_request = await Httpx.AsyncRequest(info_url, {"method": "POST", "cache": 259200})
infobody = info_request.json()
if infobody["code"] != "000000":
raise FailedException("failed to fetch song info")
user_info = config.read_config('module.mg.user') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.mg'))
req = await Httpx.AsyncRequest(tools['url'].replace('__quality__', tools['qualityMap'][quality]).replace('__songId__', songId), {
req = await Httpx.AsyncRequest(f'https://m.music.migu.cn/migumusic/h5/play/auth/getSongPlayInfo?type={tools["qualityMap"][quality]}&copyrightId={infobody["resource"][0]["copyrightId"]}', {
'method': 'GET',
'headers': {
'User-Agent': user_info['useragent'],
'aversionid': user_info['aversionid'],
'token': user_info['token'],
'channel': '0146832',
'language': 'Chinese',
'ua': 'Android_migu',
'mode': 'android',
'os': 'Android ' + user_info['osversion'],
"by": user_info["by"],
"Cookie": "SESSION=" + user_info["session"],
"Referer": "https://m.music.migu.cn/v4/",
"Origin": "https://m.music.migu.cn",
},
})
try:
body = req.json()
data = body['data']
if ((not int(body['code']) == 0) or ( not data['url'])):
raise FailedException('failed')
if (int(body['code']) != 200 or (not body.get("data")) or (not body["data"]["playUrl"])):
raise FailedException(body.get("msg") if body.get("msg") else "failed")
data = body["data"]
return {
'url': data['url'].split('?')[0],
'quality': tools['qualityMapReverse'][data['audioFormatType']],
'url': body["data"]["playUrl"].split("?")[0] if body["data"]["playUrl"].split("?")[0].startswith("http") else "http:" + body["data"]["playUrl"].split("?")[0],
'quality': tools['qualityMapReverse'].get(data['formatId']) if (tools['qualityMapReverse'].get(data['formatId'])) else "unknown",
}
except:
raise FailedException('failed')

View File

@ -0,0 +1,51 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: refresh_login.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from common import Httpx
from common import config
from common.exceptions import FailedException
from common import scheduler
from common import variable
from common import log
logger = log.log("migu_refresh_login")
async def do_account_refresh(user_info):
req = await Httpx.AsyncRequest("https://m.music.migu.cn/migumusic/h5/user/auth/userActiveNotice", {
"method": "POST",
"body": "",
"headers": {
"User-Agent": user_info["useragent"],
"by": user_info["by"],
"Cookie": "SESSION=" + user_info["session"],
"Referer": "https://m.music.migu.cn/v4/my",
"Origin": "https://m.music.migu.cn",
},
})
body = req.json()
if (int(body["code"]) != 200):
raise FailedException("咪咕session保活失败: " + str(body["msg"]))
return logger.info("咪咕session保活成功")
if (variable.use_cookie_pool):
users = config.read_config("module.cookiepool.mg")
for u in users:
ref = u.get("refresh_login") if u.get("refresh_login") else {
"enable": False,
"interval": 86400
}
if (ref["enable"]):
scheduler.append("migu_refresh_login_pooled_" + u["by"], do_account_refresh, ref["interval"], {"user_info": u})
else:
u = config.read_config("module.mg.user")
ref = config.read_config("module.mg.user.refresh_login")
if (ref["enable"]):
scheduler.append("migu_refresh_login", do_account_refresh, ref["interval"], {"user_info": u})

View File

@ -1,104 +1,55 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: QMWSign.py -
# - project: lx-music-api-server -
# - author: jixunmoe -
# - name: zzc_sign.py -
# - project: qmweb-sign -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
# This file is part of the "qmweb-sign" project.
from common.utils import createMD5
import re as _re
import sys
import re
def v(b):
res = []
p = [21, 4, 9, 26, 16, 20, 27, 30]
for x in p:
res.append(b[x])
return ''.join(res)
from hashlib import sha1
from base64 import b64encode
def c(b):
res = []
p = [18, 11, 3, 2, 1, 7, 6, 25]
for x in p:
res.append(b[x])
return ''.join(res)
PART_1_INDEXES = [23, 14, 6, 36, 16, 40, 7, 19]
PART_2_INDEXES = [16, 1, 32, 12, 19, 27, 8, 5]
SCRAMBLE_VALUES = [
89,
39,
179,
150,
218,
82,
58,
252,
177,
52,
186,
123,
120,
64,
242,
133,
143,
161,
121,
179,
]
def y(a, b, c):
e = []
r25 = a >> 2
if b is not None and c is not None:
r26 = a & 3
r26_2 = r26 << 4
r26_3 = b >> 4
r26_4 = r26_2 | r26_3
r27 = b & 15
r27_2 = r27 << 2
r27_3 = r27_2 | (c >> 6)
r28 = c & 63
e.append(r25)
e.append(r26_4)
e.append(r27_3)
e.append(r28)
else:
r10 = a >> 2
r11 = a & 3
r11_2 = r11 << 4
e.append(r10)
e.append(r11_2)
return e
PART_1_INDEXES = filter(lambda x: x < 40, PART_1_INDEXES)
def n(ls):
e = []
for i in range(0, len(ls), 3):
if i < len(ls) - 2:
e += y(ls[i], ls[i + 1], ls[i + 2])
else:
e += y(ls[i], None, None)
res = []
b64all = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
for i in e:
res.append(b64all[i])
return ''.join(res)
def t(b):
zd = {
"0": 0,
"1": 1,
"2": 2,
"3": 3,
"4": 4,
"5": 5,
"6": 6,
"7": 7,
"8": 8,
"9": 9,
"A": 10,
"B": 11,
"C": 12,
"D": 13,
"E": 14,
"F": 15
}
ol = [212, 45, 80, 68, 195, 163, 163, 203, 157, 220, 254, 91, 204, 79, 104, 6]
res = []
j = 0
for i in range(0, len(b), 2):
one = zd[b[i]]
two = zd[b[i + 1]]
r = one * 16 ^ two
res.append(r ^ ol[j])
j += 1
return res
def sign(payload: str) -> str:
hash = sha1(payload.encode("utf-8")).hexdigest().upper()
def sign(params):
md5Str = createMD5(params).upper()
h = v(md5Str)
e = c(md5Str)
ls = t(md5Str)
m = n(ls)
res = 'zzb' + h + m + e
res = res.lower()
r = _re.compile(r'[\\/+]')
res = _re.sub(r, '', res)
return res
part1 = "".join(map(lambda i: hash[i], PART_1_INDEXES))
part2 = "".join(map(lambda i: hash[i], PART_2_INDEXES))
part3 = bytearray(20)
for i, v in enumerate(SCRAMBLE_VALUES):
value = v ^ int(hash[i * 2 : i * 2 + 2], 16)
part3[i] = value
b64_part = re.sub(rb"[\\/+=]", b"", b64encode(part3)).decode("utf-8")
return f"zzc{part1}{b64_part}{part2}".lower()

View File

@ -26,70 +26,66 @@ class ParseTools:
'timeLabelFixRxp': re.compile(r'(?:\.0+|0+)$'),
}
def ms_format(self, time_ms):
if not time_ms:
def msFormat(self, timeMs):
if isinstance(timeMs, float) and timeMs.is_nan():
return ''
ms = time_ms % 1000
time_ms /= 1000
m = str(int(time_ms / 60)).zfill(2)
time_ms %= 60
s = str(int(time_ms)).zfill(2)
return f"[{m}:{s}.{str(ms).zfill(3)}]"
ms = timeMs % 1000
timeMs //= 1000
m = str(int(timeMs // 60)).zfill(2)
s = str(int(timeMs % 60)).zfill(2)
return f'[{m}:{s}.{str(ms).zfill(3)}]'
def parse_lyric(self, lrc):
lrc = lrc.strip()
lrc = lrc.replace('\r', '')
def parseLyric(self, lrc):
lrc = lrc.strip().replace('\r', '')
if not lrc:
return {'lyric': '', 'lxlyric': ''}
lines = lrc.split('\n')
# print(lrc)
lxlrc_lines = []
lyric_lines = []
lines = lrc.split('\n')
lxlrcLines = []
lrcLines = []
for line in lines:
line = line.strip()
result = self.rxps['lineTime'].match(line)
if not result:
if line.startswith('[offset'):
lxlrc_lines.append(line)
lyric_lines.append(line)
if self.rxps['lineTime2'].search(line):
lyric_lines.append(line)
lxlrcLines.append(line)
lrcLines.append(line)
if self.rxps['lineTime2'].match(line):
lrcLines.append(line)
continue
start_ms_time = int(result.group(1))
start_time_str = self.ms_format(start_ms_time)
if not start_time_str:
startMsTime = int(result.group(1))
startTimeStr = self.msFormat(startMsTime)
if not startTimeStr:
continue
words = re.sub(self.rxps['lineTime'], '', line)
lyric_lines.append(f"{start_time_str}{re.sub(self.rxps['wordTimeAll'], '', words)}")
lrcLines.append(f'{startTimeStr}{re.sub(self.rxps["wordTimeAll"], "", words)}')
times = re.findall(self.rxps['wordTimeAll'], words)
if not times:
continue
times = [
f"<{max(int(match.group(1)) - start_ms_time, 0)},{match.group(2)}>"
for match in re.finditer(r'\((\d+),(\d+)\)', words)
]
word_arr = re.split(self.rxps['wordTime'], words)
new_words = ''.join([f"{time}{word}" for time, word in zip(times, word_arr)])
lxlrc_lines.append(f"{start_time_str}{new_words}")
_rxp = r"\((\d+),(\d+)\)"
times = [f'''<{max(int(re.search(_rxp, time).group(1)) - startMsTime, 0)},{re.search(_rxp, time).group(2)}>''' for time in times]
wordArr = re.split(self.rxps['wordTime'], words)
newWords = ''.join([f'{time}{wordArr[index]}' for index, time in enumerate(times)])
lxlrcLines.append(f'{startTimeStr}{newWords}')
return {
'lyric': '\n'.join(lyric_lines),
'lxlyric': '\n'.join(lxlrc_lines),
'lyric': '\n'.join(lrcLines),
'lxlyric': '\n'.join(lxlrcLines),
}
def parse_rlyric(self, lrc):
lrc = lrc.strip()
lrc = lrc.replace('\r', '')
def parseRlyric(self, lrc):
lrc = lrc.strip().replace('\r', '')
if not lrc:
return {'lyric': '', 'lxlyric': ''}
lines = lrc.split('\n')
lyric_lines = []
lines = lrc.split('\n')
lrcLines = []
for line in lines:
line = line.strip()
@ -97,91 +93,104 @@ class ParseTools:
if not result:
continue
start_ms_time = int(result.group(1))
start_time_str = self.ms_format(start_ms_time)
if not start_time_str:
startMsTime = int(result.group(1))
startTimeStr = self.msFormat(startMsTime)
if not startTimeStr:
continue
words = re.sub(self.rxps['lineTime'], '', line)
lrcLines.append(f'{startTimeStr}{re.sub(self.rxps["wordTimeAll"], "", words)}')
lyric_lines.append(f"{start_time_str}{re.sub(self.rxps['wordTimeAll'], '', words)}")
return '\n'.join(lrcLines)
return '\n'.join(lyric_lines)
def remove_tag(self, string):
def removeTag(self, string):
return re.sub(r'^[\S\s]*?LyricContent="', '', string).replace('"\/>[\S\s]*?$', '')
def get_intv(self, interval):
def getIntv(self, interval):
if not interval:
return 0
if '.' not in interval:
interval += '.0'
arr = re.split(':|\.', interval.ljust(8, '0'))[:3]
m, s, ms = map(int, arr)
return m * 3600000 + s * 1000 + ms
arr = re.split(r':|\.', interval)
while len(arr) < 3:
arr.insert(0, '0')
m, s, ms = arr
return int(m) * 3600000 + int(s) * 1000 + int(ms)
def fix_rlrc_time_tag(self, rlrc, lrc):
rlrc_lines = rlrc.split('\n')
lrc_lines = lrc.split('\n')
new_lrc = []
for line in rlrc_lines:
result = self.rxps['lineTime2'].search(line)
def fixRlrcTimeTag(self, rlrc, lrc):
rlrcLines = rlrc.split('\n')
lrcLines = lrc.split('\n')
newLrc = []
for line in rlrcLines:
result = self.rxps['lineTime2'].match(line)
if not result:
continue
words = re.sub(self.rxps['lineTime2'], '', line)
if not words.strip():
continue
t1 = self.get_intv(result.group(1))
while lrc_lines:
lrc_line = lrc_lines.pop(0)
lrc_line_result = self.rxps['lineTime2'].search(lrc_line)
if not lrc_line_result:
t1 = self.getIntv(result.group(1))
while lrcLines:
lrcLine = lrcLines.pop(0)
lrcLineResult = self.rxps['lineTime2'].match(lrcLine)
if not lrcLineResult:
continue
t2 = self.get_intv(lrc_line_result.group(1))
t2 = self.getIntv(lrcLineResult.group(1))
if abs(t1 - t2) < 100:
new_lrc.append(re.sub(self.rxps['lineTime2'], lrc_line_result.group(0), line))
newLrc.append(re.sub(self.rxps['lineTime2'], lrcLineResult.group(0), line))
break
return '\n'.join(new_lrc)
def fix_tlrc_time_tag(self, tlrc, lrc):
tlrc_lines = tlrc.split('\n')
lrc_lines = lrc.split('\n')
new_lrc = []
time_tag_rxp = r'^\[[\d:.]+\]'
return '\n'.join(newLrc)
for line in tlrc_lines:
result = re.match(time_tag_rxp, line)
def fixTlrcTimeTag(self, tlrc, lrc):
tlrcLines = tlrc.split('\n')
lrcLines = lrc.split('\n')
newLrc = []
for line in tlrcLines:
result = self.rxps['lineTime2'].match(line)
if not result:
continue
words = re.sub(time_tag_rxp, '', line)
words = re.sub(self.rxps['lineTime2'], '', line)
if not words.strip():
continue
tag = re.sub(r'\[\d+:\d+\.\d+\]', '', result.group(0))
time = result.group(1)
if '.' in time:
time += '0' * (3 - len(time.split('.')[1]))
while lrc_lines:
lrc_line = lrc_lines.pop(0)
lrc_line_result = re.match(time_tag_rxp, lrc_line)
if not lrc_line_result:
t1 = self.getIntv(time)
while lrcLines:
lrcLine = lrcLines.pop(0)
lrcLineResult = self.rxps['lineTime2'].match(lrcLine)
if not lrcLineResult:
continue
if tag in lrc_line_result.group(0):
new_lrc.append(re.sub(time_tag_rxp, lrc_line_result.group(0), line))
t2 = self.getIntv(lrcLineResult.group(1))
if abs(t1 - t2) < 100:
newLrc.append(re.sub(self.rxps['lineTime2'], lrcLineResult.group(0), line))
break
return '\n'.join(new_lrc)
return '\n'.join(newLrc)
def parse(self, lrc, tlrc, rlrc):
def parse(self, lrc, tlrc=None, rlrc=None):
info = {
'lyric': '',
'tlyric': '',
'rlyric': '',
'lxlyric': '',
}
if lrc:
lyric_info = self.parse_lyric(self.remove_tag(lrc))
info['lyric'] = lyric_info['lyric']
info['lxlyric'] = lyric_info['lxlyric']
parsed_lrc = self.parseLyric(self.removeTag(lrc))
info['lyric'] = parsed_lrc['lyric']
info['lxlyric'] = parsed_lrc['lxlyric']
if rlrc:
info['rlyric'] = self.fix_rlrc_time_tag(self.parse_rlyric(self.remove_tag(rlrc)), info['lyric'])
info['rlyric'] = self.fixRlrcTimeTag(self.parseRlyric(self.removeTag(rlrc)), info['lyric'])
if tlrc:
info['tlyric'] = self.fix_tlrc_time_tag(tlrc, info['lyric'])
info['tlyric'] = self.fixTlrcTimeTag(tlrc, info['lyric'])
return info

View File

@ -21,22 +21,22 @@ async def url(songId, quality):
strMediaMid = infoBody['track_info']['file']['media_mid']
user_info = config.read_config('module.tx.user') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.tx'))
requestBody = {
'req_0': {
'module': 'vkey.GetVkeyServer',
'method': 'CgiGetVkey',
'param': {
'filename': [f"{tools.fileInfo[quality]['h']}{strMediaMid}{tools.fileInfo[quality]['e']}"],
'guid': config.read_config('module.tx.vkeyserver.guid'),
'songmid': [songId],
'songtype': [0],
'uin': str(user_info['uin']),
'loginflag': 1,
'platform': '20',
"req": {
"module": "music.vkey.GetVkey",
"method": "UrlGetVkey",
"param": {
"filename": [f"{tools.fileInfo[quality]['h']}{strMediaMid}{tools.fileInfo[quality]['e']}"],
"guid": config.read_config("module.tx.vkeyserver.guid"),
"songmid": [songId],
"songtype": [0],
"uin": str(user_info["uin"]),
"loginflag": 1,
"platform": "20",
},
},
'comm': {
"qq": str(user_info['uin']),
"authst": user_info['qqmusic_key'],
"comm": {
"qq": str(user_info["uin"]),
"authst": user_info["qqmusic_key"],
"ct": "26",
"cv": "2010101",
"v": "2010101"
@ -44,7 +44,7 @@ async def url(songId, quality):
}
req = await signRequest(requestBody)
body = createObject(req.json())
data = body.req_0.data.midurlinfo[0]
data = body.req.data.midurlinfo[0]
url = data['purl']
if (not url):

View File

@ -7,205 +7,151 @@
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from common import Httpx, variable
from common import scheduler
from common import config
from common import log
from common import Httpx, variable, scheduler, config, log
from .utils import sign
import ujson as json
from typing import Dict, Any, Optional
logger = log.log('qqmusic_refresh_login')
logger = log.log("qqmusic_refresh_login")
async def refresh():
if (not config.read_config('module.tx.user.qqmusic_key')):
return
if (not config.read_config('module.tx.user.refresh_login.enable')):
return
if (config.read_config('module.tx.user.qqmusic_key').startswith('W_X')):
options = {
'method': 'POST',
'body': json.dumps({
"comm": {
"fPersonality": "0",
"tmeLoginType": "1",
"tmeLoginMethod": "1",
"qq": "",
"authst": "",
"ct": "11",
"cv": "12080008",
"v": "12080008",
"tmeAppID": "qqmusic"
},
"req1": {
"module": "music.login.LoginServer",
"method": "Login",
"param": {
"code": "",
"openid": "",
"refresh_token": "",
"str_musicid": str(config.read_config('module.tx.user.uin')),
"musickey": config.read_config('module.tx.user.qqmusic_key'),
"unionid": "",
"refresh_key": "",
"loginMode": 2
}
}
})
}
signature = sign(options['body'])
req = await Httpx.AsyncRequest(f'https://u.y.qq.com/cgi-bin/musics.fcg?sign={signature}', options)
body = req.json()
if (body['req1']['code'] != 0):
logger.warning('刷新登录失败, code: ' +
str(body['req1']['code']) + f'\n响应体: {body}')
return
else:
logger.info('刷新登录成功')
config.write_config('module.tx.user.uin',
str(body['req1']['data']['musicid']))
logger.info('已通过相应数据更新uin')
config.write_config('module.tx.user.qqmusic_key',
body['req1']['data']['musickey'])
logger.info('已通过相应数据更新qqmusic_key')
elif (config.read_config('module.tx.user.qqmusic_key').startswith('Q_H_L')):
options = {
'method': 'POST',
'body': json.dumps({
'req1': {
'module': 'QQConnectLogin.LoginServer',
'method': 'QQLogin',
'param': {
'expired_in': 7776000,
'musicid': int(config.read_config('module.tx.user.uin')),
'musickey': config.read_config('module.tx.user.qqmusic_key')
}
}
})
}
signature = sign(options['body'])
req = await Httpx.AsyncRequest(f'https://u6.y.qq.com/cgi-bin/musics.fcg?sign={signature}', options)
body = req.json()
if (body['req1']['code'] != 0):
logger.warning('刷新登录失败, code: ' +
str(body['req1']['code']) + f'\n响应体: {body}')
return
else:
logger.info('刷新登录成功')
config.write_config('module.tx.user.uin',
str(body['req1']['data']['musicid']))
logger.info('已通过相应数据更新uin')
config.write_config('module.tx.user.qqmusic_key',
body['req1']['data']['musickey'])
logger.info('已通过相应数据更新qqmusic_key')
def _build_request_body(user_info: Dict[str, Any]) -> Dict[str, Any]:
"""构建统一请求体结构"""
return {
"comm": {
"fPersonality": "0",
"tmeLoginType": "2"
if user_info["qqmusic_key"].startswith("Q_H_L")
else "1",
"qq": str(user_info["uin"]),
"authst": user_info["qqmusic_key"],
"ct": "11",
"cv": "12080008",
"v": "12080008",
"tmeAppID": "qqmusic",
},
"req1": {
"module": "music.login.LoginServer",
"method": "Login",
"param": {
"str_musicid": str(user_info["uin"]),
"musickey": user_info["qqmusic_key"],
"refresh_key": user_info.get("refresh_key", ""),
},
},
}
async def _update_user_config(
user_info: Dict[str, Any], new_data: Dict[str, Any]
) -> None:
"""统一更新用户配置"""
updates = {
"uin": str(new_data.get("musicid", user_info["uin"])),
"qqmusic_key": new_data.get("musickey", user_info["qqmusic_key"]),
"refresh_key": new_data.get("refresh_key", user_info.get("refresh_key", "")),
}
if variable.use_cookie_pool:
user_list = config.read_config("module.cookiepool.tx")
target_user = next((u for u in user_list if u["uin"] == user_info["uin"]), None)
if target_user:
target_user.update(updates)
config.write_config("module.cookiepool.tx", user_list)
else:
logger.error('未知的qqmusic_key格式')
for key, value in updates.items():
config.write_config(f"module.tx.user.{key}", value)
if (not variable.use_cookie_pool):
# changed refresh login config path
txconfig = config.read_config('module.tx')
refresh_login_info = txconfig.get('refresh_login')
if (refresh_login_info):
txconfig['user']['refresh_login'] = refresh_login_info
txconfig.pop('refresh_login')
config.write_config('module.tx', txconfig)
if (config.read_config('module.tx.user.refresh_login.enable') and not variable.use_cookie_pool):
scheduler.append('qqmusic_refresh_login', refresh,
config.read_config('module.tx.user.refresh_login.interval'))
async def _process_refresh(user_info: Dict[str, Any]) -> Optional[bool]:
"""统一处理刷新逻辑"""
try:
# 构建请求参数
request_body = _build_request_body(user_info)
signature = sign(json.dumps(request_body))
async def refresh_login_for_pool(user_info):
if (user_info['qqmusic_key'].startswith('W_X')):
options = {
'method': 'POST',
'body': json.dumps({
"comm": {
"fPersonality": "0",
"tmeLoginType": "1",
"tmeLoginMethod": "1",
"qq": "",
"authst": "",
"ct": "11",
"cv": "12080008",
"v": "12080008",
"tmeAppID": "qqmusic"
# 发送请求
response = await Httpx.AsyncRequest(
f"https://u.y.qq.com/cgi-bin/musics.fcg?sign={signature}",
{
"method": "POST",
"body": json.dumps(request_body),
"headers": {
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
"req1": {
"module": "music.login.LoginServer",
"method": "Login",
"param": {
"code": "",
"openid": "",
"refresh_token": "",
"str_musicid": str(user_info['uin']),
"musickey": user_info['qqmusic_key'],
"unionid": "",
"refresh_key": "",
"loginMode": 2
}
}
})
}
signature = sign(options['body'])
req = await Httpx.AsyncRequest(f'https://u.y.qq.com/cgi-bin/musics.fcg?sign={signature}', options)
body = req.json()
if (body['req1']['code'] != 0):
logger.warning(f'为QQ音乐账号({user_info["uin"]})刷新登录失败, code: ' +
str(body['req1']['code']) + f'\n响应体: {body}')
return
else:
logger.info(f'为QQ音乐账号(WeChat_{user_info["uin"]})刷新登录成功')
user_list = config.read_config('module.cookiepool.tx')
user_list[user_list.index(
user_info)]['qqmusic_key'] = body['req1']['data']['musickey']
user_list[user_list.index(
user_info)]['uin'] = str(body['req1']['data']['musicid'])
config.write_config('module.cookiepool.tx', user_list)
logger.info(f'为QQ音乐账号(WeChat_{user_info["uin"]})数据更新完毕')
return
elif (user_info['qqmusic_key'].startswith('Q_H_L')):
options = {
'method': 'POST',
'body': json.dumps({
'req1': {
'module': 'QQConnectLogin.LoginServer',
'method': 'QQLogin',
'param': {
'expired_in': 7776000,
'musicid': int(user_info['uin']),
'musickey': user_info['qqmusic_key']
}
}
})
}
signature = sign(options['body'])
req = await Httpx.AsyncRequest(f'https://u6.y.qq.com/cgi-bin/musics.fcg?sign={signature}', options)
body = req.json()
if (body['req1']['code'] != 0):
},
)
response_data = response.json()
if response_data.get("req1", {}).get("code") != 0:
logger.warning(
f'为QQ音乐账号({user_info["uin"]})刷新登录失败, code: ' + str(body['req1']['code']) + f'\n响应体: {body}')
return
else:
logger.info(f'为QQ音乐账号(QQ_{user_info["uin"]})刷新登录成功')
user_list = config.read_config('module.cookiepool.tx')
user_list[user_list.index(
user_info)]['qqmusic_key'] = body['req1']['data']['musickey']
user_list[user_list.index(
user_info)]['uin'] = str(body['req1']['data']['musicid'])
config.write_config('module.cookiepool.tx', user_list)
logger.info(f'为QQ音乐账号(QQ_{user_info["uin"]})数据更新完毕')
return
else:
logger.warning(f'为QQ音乐账号({user_info["uin"]})刷新登录失败: 未知或不支持的key类型')
f"刷新失败 [账号: {user_info['uin']} 代码: {response_data['req1']['code']}]"
)
return False
# 更新配置
await _update_user_config(user_info, response_data["req1"]["data"])
logger.info(f"刷新成功 [账号: {user_info['uin']}]")
return True
except json.JSONDecodeError:
logger.error(
"响应解析失败 [账号: %s] 原始响应: %s",
user_info["uin"],
response.text[:100],
)
except KeyError as e:
logger.error(
"响应数据格式异常 [账号: %s] 缺失字段: %s", user_info["uin"], str(e)
)
except Exception as e:
logger.error(
"刷新过程异常 [账号: %s] 错误信息: %s",
user_info["uin"],
str(e),
)
return False
async def refresh() -> None:
"""主刷新入口非Cookie池模式"""
if not config.read_config("module.tx.user.refresh_login.enable"):
return
def reg_refresh_login_pool_task():
user_info_pool = config.read_config('module.cookiepool.tx')
for user_info in user_info_pool:
if (user_info['refresh_login'].get('enable')):
scheduler.append(
f'qqmusic_refresh_login_pooled_{user_info["uin"]}', refresh_login_for_pool, user_info['refresh_login']['interval'], args = {'user_info': user_info})
await _process_refresh(
{
"uin": config.read_config("module.tx.user.uin"),
"qqmusic_key": config.read_config("module.tx.user.qqmusic_key"),
"refresh_key": config.read_config("module.tx.user.refresh_key"),
}
)
if (variable.use_cookie_pool):
reg_refresh_login_pool_task()
async def refresh_login_for_pool(user_info: Dict[str, Any]) -> None:
"""Cookie池刷新入口"""
if user_info.get("refresh_login", {}).get("enable", False):
await _process_refresh(user_info)
def _setup_scheduler() -> None:
"""初始化定时任务"""
if variable.use_cookie_pool:
user_list = config.read_config("module.cookiepool.tx")
for user in user_list:
if user.get("refresh_login", {}).get("enable", False):
scheduler.append(
f"qq_refresh_{user['uin']}",
refresh_login_for_pool,
user["refresh_login"].get("interval", 3600),
args={"user_info": user},
)
elif config.read_config("module.tx.user.refresh_login.enable"):
scheduler.append(
"qqmusic_main_refresh",
refresh,
config.read_config("module.tx.user.refresh_login.interval"),
)
# 初始化定时任务
_setup_scheduler()

View File

@ -13,51 +13,107 @@ from common import config
from common.exceptions import FailedException
from .encrypt import eapiEncrypt
import ujson as json
from . import refresh_login
PROTO = config.read_config("module.wy.proto")
API_URL = config.read_config("module.wy.ncmapi.api_url")
tools = {
'qualityMap': {
'128k': 'standard',
"192k": "higher",
'320k': 'exhigh',
'flac': 'lossless',
'flac24bit': 'hires',
"dolby": "jyeffect",
"sky": "jysky",
"sky": "sky",
"master": "jymaster",
"standard": "standard",
"higher": "higher",
"exhigh": "exhigh",
"lossless": "lossless",
"hires": "hires",
"jyeffect": "jyeffect",
"jymaster": "jymaster",
},
'qualityMapReverse': {
'standard': '128k',
"higher": "192k",
'exhigh': '320k',
'lossless': 'flac',
'hires': 'flac24bit',
"jyeffect": "dolby",
"jysky": "sky",
"sky": "sky",
"jymaster": "master",
},
}
async def url(songId, quality):
path = '/api/song/enhance/player/url/v1'
requestUrl = 'https://interface.music.163.com/eapi/song/enhance/player/url/v1'
req = await Httpx.AsyncRequest(requestUrl, {
'method': 'POST',
'headers': {
'Cookie': config.read_config('module.wy.user.cookie') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.wy'))['cookie'],
},
'form': eapiEncrypt(path, json.dumps({
if PROTO == "offcial":
path = '/api/song/enhance/player/url/v1'
requestUrl = 'https://interface.music.163.com/eapi/song/enhance/player/url/v1'
requestBody = {
"ids": json.dumps([songId]),
"level": tools["qualityMap"][quality],
"encodeType": "flac",
}))
})
body = req.json()
if (not body.get("data") or (not body.get("data")) or (not body.get("data")[0].get("url"))):
raise FailedException("failed")
}
if (quality == "sky"):
requestBody["immerseType"] = "c51"
req = await Httpx.AsyncRequest(requestUrl, {
'method': 'POST',
'headers': {
'Cookie': config.read_config('module.wy.user.cookie') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.wy'))['cookie'],
},
'form': eapiEncrypt(path, json.dumps(requestBody))
})
body = req.json()
if (not body.get("data") or (not body.get("data")) or (not body.get("data")[0].get("url"))):
raise FailedException("失败")
data = body["data"][0]
if (data['level'] != tools['qualityMap'][quality]):
raise FailedException("reject unmatched quality")
data = body["data"][0]
return {
'url': data["url"].split("?")[0],
'quality': tools['qualityMapReverse'][data['level']]
}
# 修正:映射服务器返回的 level 为标准化值
data_level = data['level']
expected_level = tools["qualityMap"][quality]
# 检查客户端请求的 quality 与服务器返回的 level 是否匹配
if data_level != expected_level:
raise FailedException(
f"reject unmatched quality: expected={expected_level}, got={data_level}"
)
return {
'url': data["url"].split("?")[0],
'quality': tools['qualityMapReverse'][data['level']]
}
elif (PROTO == "ncmapi") and (API_URL):
requestUrl = f"{API_URL}/song/url/v1"
requestBody = {
"ids": songId,
"level": tools["qualityMap"][quality],
"cookie": config.read_config('module.wy.user.cookie') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.wy'))['cookie']
}
req = await Httpx.AsyncRequest(requestUrl, {
"method": "GET",
"params": requestBody
})
body = req.json()
if (body["code"] != 200) or (not body.get("data")):
raise FailedException("失败")
data = body["data"][0]
# 修正:映射服务器返回的 level 为标准化值
data_level = data['level']
expected_level = tools["qualityMap"][quality]
# 检查客户端请求的 quality 与服务器返回的 level 是否匹配
if data_level != expected_level:
raise FailedException(
f"reject unmatched quality: expected={expected_level}, got={data_level}"
)
return {
'url': data["url"].split("?")[0],
'quality': quality
}

100
modules/wy/refresh_login.py Normal file
View File

@ -0,0 +1,100 @@
# ----------------------------------------
# - mode: python -
# - author: lerdb -
# - name: refresh_login.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from common import Httpx, variable
from common import scheduler
from common import config
from common import log
from common.exceptions import FailedException
from time import time
from random import randint
from .encrypt import eapiEncrypt
import ujson as json
logger = log.log("wy_refresh_login")
def cookieStr2Dict(cookieStr):
cookieDict = {}
for line in cookieStr.split(";"):
if line.strip() == "":
continue
try:
name, value = line.strip().split("=", 1)
cookieDict[name] = value
except:
continue
return cookieDict
def cookieDict2Str(cookieDict):
cookieStr = ""
for name, value in cookieDict.items():
cookieStr += f"{name}={value}; "
return cookieStr
async def refresh(cookie:str):
"""
网易云刷新登录
@param cookie: 网易云音乐cookie
"""
cookie = cookieStr2Dict(cookie)
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0"
}
baseUrl = "http://interface.music.163.com/eapi/"
path = "/api/login/token/refresh"
header = {
"osver": cookie.get("osver", "17.4.1"),
"deviceId": cookie.get("deviceId",""),
"os": cookie.get("os","ios"),
"appver": cookie.get("appver", ("9.0.65" if cookie.get("os") != "pc" else "")),
"versioncode": cookie.get("versioncode", "140"),
"mobilename": cookie.get("mobilename", ""),
"buildver": cookie.get("buildver", str(time())[:10]),
"resolution": cookie.get("resolution", "1920x1080"),
"__csrf": cookie.get("__csrf", ""),
"channel": cookie.get("channel", ""),
"requestId": str(time() * 1000)[:13] + "_" + f"{randint(0, 9999):0>4}",
}
if cookie.get("MUSIC_U"):
header["MUSIC_U"] = cookie.get("MUSIC_U")
if cookie.get("MUSIC_A"):
header["MUSIC_A"] = cookie.get("MUSIC_A")
headers["Cookie"] = cookieDict2Str(header)
req = await Httpx.AsyncRequest(
baseUrl + path[5:],
{
"method": "POST",
"headers": headers,
"form": eapiEncrypt(path, json.dumps({"header": header, "e_r": False})),
}
)
body = req.json()
if int(body["code"]) != 200:
raise FailedException("网易云刷新登录失败(code: " + body["code"] + ")")
return logger.info("网易云刷新登录成功")
if (variable.use_cookie_pool):
cookies = config.read_config("module.cookiepool.wy")
for c in cookies:
ref = c.get("refresh_login") if c.get("refresh_login") else {
"enable": False,
"interval": 86400
}
if (ref["enable"]):
scheduler.append("wy_refresh_login_pooled_" + c["cookie"][:32], refresh, ref["interval"], {"cookie": c["cookie"]})
else:
c = config.read_config("module.wy.user.cookie")
ref = config.read_config("module.wy.user.refresh_login")
if (ref["enable"]):
scheduler.append("wy_refresh_login", refresh, ref["interval"], {"cookie": c})

13
package.json Normal file
View File

@ -0,0 +1,13 @@
{
"name": "lx-music-api-server",
"version": "2.0.0",
"description": "一个适配 LX Music 的 API 后端实现",
"scripts": {
"dev": "python run.py development",
"prod": "python run.py production",
"install": "pip install -r requirements.txt",
"poetry:install": "poetry install",
"poetry:development": "poetry run python run.py development",
"poetry:production": "poetry run python run.py production"
}
}

1896
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,28 @@
[tool.poetry]
name = "lx-music-api-server-python"
version = "2.0.0.beta-4"
name = "lx_music_api_server"
version = "2.0.0"
description = "一个适配 LX Music 的 API 后端实现"
authors = ["helloplhm-qwq"]
authors = ["helloplhm-qwq", "lerdb", "Folltoshe"]
license = "MIT"
readme = "README.md"
package-mode = false
[tool.poetry.dependencies]
python = "^3.8"
aiohttp = "^3.9.1"
pycryptodome = "^3.20.0"
ujson = "^5.9.0"
requests = "^2.31.0"
colorlog = "^6.8.0"
Pygments = "^2.17.2"
xmltodict = "^0.13.0"
[tool.poetry.dev-dependencies]
Flask = "^3.0.0"
aiohttp = "^3.10.10"
pycryptodome = "^3.22.0"
ujson = "^5.10.0"
requests = "^2.32.3"
colorlog = "^6.9.0"
Pygments = "^2.19.1"
xmltodict = "^0.14.2"
pillow = "^10.4.0"
mutagen = "^1.47.0"
colorama = "^0.4.6"
ruamel-yaml = "^0.18.10"
pybind11 = "^2.13.6"
redis = "^5.2.1"
hiredis = "^3.1.0"
[build-system]
requires = ["poetry-core"]

View File

@ -5,3 +5,10 @@ requests
colorlog
pygments
xmltodict
toml
mutagen
pillow
colorama
ruamel-yaml
redis
hiredis

BIN
res/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
res/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

21
run.py Normal file
View File

@ -0,0 +1,21 @@
import os
import argparse
import subprocess
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Script to set environment variables and run different environments.")
parser.add_argument(
"environment",
choices=["production", "development"],
nargs="?",
default="production",
help="Specify the environment to run.",
)
args = parser.parse_args()
try:
if args.environment:
os.environ["CURRENT_ENV"] = args.environment
subprocess.run(["python", "main.py"])
except KeyboardInterrupt:
pass

11
script.py Normal file
View File

@ -0,0 +1,11 @@
# https://github.com/python-poetry/poetry/issues/241#issuecomment-445434646
import sys
import subprocess
def __getattr__(name): # python 3.7+, otherwise define each script manually
name = name.replace('_', '-')
subprocess.run(
['python', '-u', '-m', name] + sys.argv[1:]
) # run whatever you like based on 'name'

24
setup.py Normal file
View File

@ -0,0 +1,24 @@
from setuptools import setup
import toml
try:
version = toml.load("./pyproject.toml")["tool"]["poetry"]["version"]
description = toml.load("./pyproject.toml")["tool"]["poetry"]["description"]
except Exception:
version = "1.0.0"
description = "Description not available"
setup(
name="lx_music_api_server_setup",
version=version,
scripts=["run.py"],
author="helloplhm-qwq",
author_email="helloplhm-qwq@outlook.com",
description=description,
url="https://github.com/helloplhm-qwq/lx-music-api-server",
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
)

24
termux.py Normal file
View File

@ -0,0 +1,24 @@
# for installing the dependencies on Termux.
# ruamel-yaml-clib need to build from source, but it will throw an error during
# the build process in default configuration. To fix this, we need to set
# CFLAGS environment variable to "-Wno-incompatible-function-pointer-types".
# Resolution from: https://github.com/termux/termux-packages/issues/16746
import os
import subprocess
def is_termux():
return "termux" in os.environ.get("HOME", "")
def install_ruamel_clib():
if (is_termux()):
# https://github.com/termux/termux-packages/issues/16746
os.environ["CFLAGS"] = "-Wno-incompatible-function-pointer-types"
subprocess.run(["poetry", "run", "pip", "install", "ruamel-yaml-clib"])
def install():
install_ruamel_clib()
subprocess.run(["poetry", "install"])
if __name__ == "__main__":
install()