Spaces:
Running
Running
admin
commited on
Commit
·
b02246d
1
Parent(s):
262568e
sync ms
Browse files- app.py +26 -13
- config.py +0 -16
- fail.mp3 +0 -0
- kuwo.py → modules/kuwo.py +49 -55
- lizhi.py → modules/lizhi.py +45 -29
- meta.py → modules/meta.py +80 -34
- netease.py → modules/netease.py +111 -48
- qq.py → modules/qq.py +66 -57
- requirements.txt +1 -3
- utils.py +82 -26
app.py
CHANGED
@@ -1,28 +1,41 @@
|
|
1 |
import gradio as gr
|
2 |
-
from netease import parser163
|
3 |
-
from qq import qmusic_parser
|
4 |
-
from kuwo import kuwo_parser
|
5 |
-
from lizhi import lizhifm_parser
|
6 |
-
from meta import music_meta_editor
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
|
8 |
if __name__ == "__main__":
|
9 |
with gr.Blocks() as demo:
|
10 |
-
gr.Markdown(
|
11 |
-
|
12 |
-
)
|
13 |
-
with gr.Tab("Music163"):
|
14 |
parser163()
|
15 |
|
16 |
-
with gr.Tab("QQ"):
|
17 |
qmusic_parser()
|
18 |
|
19 |
-
with gr.Tab("
|
20 |
kuwo_parser()
|
21 |
|
22 |
-
with gr.Tab("
|
23 |
lizhifm_parser()
|
24 |
|
25 |
-
with gr.Tab("
|
26 |
music_meta_editor()
|
27 |
|
28 |
demo.launch()
|
|
|
1 |
import gradio as gr
|
2 |
+
from modules.netease import parser163
|
3 |
+
from modules.qq import qmusic_parser
|
4 |
+
from modules.kuwo import kuwo_parser
|
5 |
+
from modules.lizhi import lizhifm_parser
|
6 |
+
from modules.meta import music_meta_editor
|
7 |
+
from utils import LANG
|
8 |
+
|
9 |
+
ZH2EN = {
|
10 |
+
"本站不提供任何音频存储服务,仅提供最基本的解析服务,请勿滥用": "This site does not provide any audio storage services, only provide the most basic parsing services, please DO NOT abuse",
|
11 |
+
"网易云音乐": "Music163",
|
12 |
+
"QQ音乐": "QQ",
|
13 |
+
"酷我音乐": "Kuwo",
|
14 |
+
"荔枝FM": "LizhiFM",
|
15 |
+
"元信息编辑器": "MetaEditor",
|
16 |
+
}
|
17 |
+
|
18 |
+
|
19 |
+
def _L(zh_txt: str):
|
20 |
+
return ZH2EN[zh_txt] if LANG else zh_txt
|
21 |
+
|
22 |
|
23 |
if __name__ == "__main__":
|
24 |
with gr.Blocks() as demo:
|
25 |
+
gr.Markdown(_L("本站不提供任何音频存储服务,仅提供最基本的解析服务,请勿滥用"))
|
26 |
+
with gr.Tab(_L("网易云音乐")):
|
|
|
|
|
27 |
parser163()
|
28 |
|
29 |
+
with gr.Tab(_L("QQ音乐")):
|
30 |
qmusic_parser()
|
31 |
|
32 |
+
with gr.Tab(_L("酷我音乐")):
|
33 |
kuwo_parser()
|
34 |
|
35 |
+
with gr.Tab(_L("荔枝FM")):
|
36 |
lizhifm_parser()
|
37 |
|
38 |
+
with gr.Tab(_L("元信息编辑器")):
|
39 |
music_meta_editor()
|
40 |
|
41 |
demo.launch()
|
config.py
DELETED
@@ -1,16 +0,0 @@
|
|
1 |
-
import os
|
2 |
-
|
3 |
-
API_163 = os.getenv("api_music163")
|
4 |
-
API_KUWO = os.getenv("api_kuwo")
|
5 |
-
API_QQ_2 = os.getenv("api_qmusic_2")
|
6 |
-
API_QQ_1 = os.getenv("api_qmusic_1")
|
7 |
-
KEY_QQ_1 = os.getenv("apikey_qmusic_1")
|
8 |
-
if not (API_163 and API_KUWO and API_QQ_2 and API_QQ_1 and KEY_QQ_1):
|
9 |
-
print("Please check your environment var!")
|
10 |
-
exit()
|
11 |
-
|
12 |
-
TIMEOUT = None
|
13 |
-
TMP_DIR = "./__pycache__"
|
14 |
-
HEADER = {
|
15 |
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36"
|
16 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fail.mp3
DELETED
Binary file (6.48 kB)
|
|
kuwo.py → modules/kuwo.py
RENAMED
@@ -2,38 +2,26 @@ import os
|
|
2 |
import re
|
3 |
import requests
|
4 |
import gradio as gr
|
5 |
-
from
|
6 |
-
from utils import insert_meta, clean_dir, timestamp
|
7 |
-
from config import API_KUWO, TIMEOUT, TMP_DIR
|
8 |
-
|
9 |
-
|
10 |
-
def download_file(
|
11 |
-
id, url: str, title, artist, album, lyric, cover, cache=f"{TMP_DIR}/kuwo"
|
12 |
-
):
|
13 |
-
clean_dir(cache)
|
14 |
-
fmt = url.split(".")[-1]
|
15 |
-
local_file = f"{cache}/{id}.{fmt}"
|
16 |
-
response = requests.get(url, stream=True)
|
17 |
-
if response.status_code == 200:
|
18 |
-
total_size = int(response.headers.get("Content-Length", 0)) + 1
|
19 |
-
time_stamp = timestamp()
|
20 |
-
progress_bar = tqdm(
|
21 |
-
total=total_size,
|
22 |
-
unit="B",
|
23 |
-
unit_scale=True,
|
24 |
-
desc=f"[{time_stamp}] {local_file}",
|
25 |
-
)
|
26 |
-
with open(local_file, "wb") as f:
|
27 |
-
for chunk in response.iter_content(chunk_size=8192):
|
28 |
-
if chunk:
|
29 |
-
f.write(chunk)
|
30 |
-
progress_bar.update(len(chunk))
|
31 |
-
|
32 |
-
insert_meta(local_file, title, artist, album, lyric, cover)
|
33 |
-
return local_file
|
34 |
|
35 |
-
|
36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
|
38 |
|
39 |
def parse_id(url: str):
|
@@ -52,37 +40,38 @@ def parse_id(url: str):
|
|
52 |
|
53 |
def get_file_size(file_path):
|
54 |
size_in_bytes = os.path.getsize(file_path)
|
55 |
-
units = ["B", "
|
56 |
-
unit_index = 0
|
57 |
-
|
|
|
58 |
unit_index = 3
|
59 |
size = size_in_bytes / (1024**3)
|
60 |
|
61 |
-
elif size_in_bytes >= 1024**2:
|
62 |
unit_index = 2
|
63 |
size = size_in_bytes / (1024**2)
|
64 |
|
65 |
-
elif size_in_bytes >= 1024:
|
66 |
unit_index = 1
|
67 |
size = size_in_bytes / 1024
|
68 |
|
69 |
else:
|
70 |
size = size_in_bytes
|
71 |
-
|
72 |
if unit_index == 0:
|
73 |
-
return f"{size}
|
74 |
else:
|
75 |
-
return f"{size:.2f}
|
76 |
|
77 |
|
78 |
-
# outer func
|
79 |
def infer(url: str):
|
|
|
80 |
song = title = cover = artist = album = quality = size = lyric = None
|
81 |
try:
|
82 |
song_id = parse_id(url)
|
83 |
if not song_id:
|
84 |
-
|
85 |
-
return song, title, artist, album, lyric, cover, song_id, quality, size
|
86 |
|
87 |
response = requests.get(API_KUWO, params={"id": song_id}, timeout=TIMEOUT)
|
88 |
if response.status_code == 200:
|
@@ -108,13 +97,17 @@ def infer(url: str):
|
|
108 |
album,
|
109 |
lyric,
|
110 |
cover,
|
|
|
111 |
)
|
112 |
size = size.upper() if size else get_file_size(song)
|
113 |
|
|
|
|
|
|
|
114 |
except Exception as e:
|
115 |
-
|
116 |
|
117 |
-
return song, title, artist, album, lyric, cover, song_id, quality, size
|
118 |
|
119 |
|
120 |
def kuwo_parser():
|
@@ -122,26 +115,27 @@ def kuwo_parser():
|
|
122 |
fn=infer,
|
123 |
inputs=[
|
124 |
gr.Textbox(
|
125 |
-
label="
|
126 |
placeholder="https://kuwo.cn/play_detail/*",
|
127 |
)
|
128 |
],
|
129 |
outputs=[
|
|
|
130 |
gr.Audio(
|
131 |
-
label="
|
132 |
show_download_button=True,
|
133 |
show_share_button=False,
|
134 |
),
|
135 |
-
gr.Textbox(label="
|
136 |
-
gr.Textbox(label="
|
137 |
-
gr.Textbox(label="
|
138 |
-
gr.TextArea(label="
|
139 |
-
gr.Image(label="
|
140 |
-
gr.Textbox(label="
|
141 |
-
gr.Textbox(label="
|
142 |
-
gr.Textbox(label="
|
143 |
],
|
144 |
-
title="
|
145 |
flagging_mode="never",
|
146 |
examples=[
|
147 |
"237668532",
|
|
|
2 |
import re
|
3 |
import requests
|
4 |
import gradio as gr
|
5 |
+
from utils import download_file, TIMEOUT, API_KUWO, TMP_DIR, LANG
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
|
7 |
+
ZH2EN = {
|
8 |
+
"请输入酷我音乐 ID 或 URL 链接": "Please enter Kuwo music ID or URL Link",
|
9 |
+
"含元信息音频下载": "Audio with metadata",
|
10 |
+
"歌名": "Title",
|
11 |
+
"作者": "Artist",
|
12 |
+
"专辑": "Album",
|
13 |
+
"歌词": "Lyrics",
|
14 |
+
"歌曲图片": "Cover",
|
15 |
+
"歌曲 ID": "Song ID",
|
16 |
+
"音质": "Quality",
|
17 |
+
"大小": "Size",
|
18 |
+
"酷我音乐无损解析": "Kuwo Music URL Parser without Loss",
|
19 |
+
"状态栏": "Status",
|
20 |
+
}
|
21 |
+
|
22 |
+
|
23 |
+
def _L(zh_txt: str):
|
24 |
+
return ZH2EN[zh_txt] if LANG else zh_txt
|
25 |
|
26 |
|
27 |
def parse_id(url: str):
|
|
|
40 |
|
41 |
def get_file_size(file_path):
|
42 |
size_in_bytes = os.path.getsize(file_path)
|
43 |
+
units = ["B", "KB", "MB", "GB"]
|
44 |
+
unit_index = 0 # 初始单位为字节
|
45 |
+
# 根据文件大小选择合适的单位
|
46 |
+
if size_in_bytes >= 1024**3: # 大于等于 1 GB
|
47 |
unit_index = 3
|
48 |
size = size_in_bytes / (1024**3)
|
49 |
|
50 |
+
elif size_in_bytes >= 1024**2: # 大于等于 1 MB
|
51 |
unit_index = 2
|
52 |
size = size_in_bytes / (1024**2)
|
53 |
|
54 |
+
elif size_in_bytes >= 1024: # 大于等于 1 KB
|
55 |
unit_index = 1
|
56 |
size = size_in_bytes / 1024
|
57 |
|
58 |
else:
|
59 |
size = size_in_bytes
|
60 |
+
# 格式化输出,保留两位小数
|
61 |
if unit_index == 0:
|
62 |
+
return f"{size}{units[unit_index]}"
|
63 |
else:
|
64 |
+
return f"{size:.2f}{units[unit_index]}"
|
65 |
|
66 |
|
67 |
+
# outer func requires try except
|
68 |
def infer(url: str):
|
69 |
+
status = "Success"
|
70 |
song = title = cover = artist = album = quality = size = lyric = None
|
71 |
try:
|
72 |
song_id = parse_id(url)
|
73 |
if not song_id:
|
74 |
+
raise ValueError("请输入有效的网址或ID!")
|
|
|
75 |
|
76 |
response = requests.get(API_KUWO, params={"id": song_id}, timeout=TIMEOUT)
|
77 |
if response.status_code == 200:
|
|
|
97 |
album,
|
98 |
lyric,
|
99 |
cover,
|
100 |
+
f"{TMP_DIR}/kuwo",
|
101 |
)
|
102 |
size = size.upper() if size else get_file_size(song)
|
103 |
|
104 |
+
else:
|
105 |
+
raise ConnectionError(f"HTTP {response.status_code}")
|
106 |
+
|
107 |
except Exception as e:
|
108 |
+
status = f"{e}"
|
109 |
|
110 |
+
return status, song, title, artist, album, lyric, cover, song_id, quality, size
|
111 |
|
112 |
|
113 |
def kuwo_parser():
|
|
|
115 |
fn=infer,
|
116 |
inputs=[
|
117 |
gr.Textbox(
|
118 |
+
label=_L("请输入酷我音乐 ID 或 URL 链接"),
|
119 |
placeholder="https://kuwo.cn/play_detail/*",
|
120 |
)
|
121 |
],
|
122 |
outputs=[
|
123 |
+
gr.Textbox(label=_L("状态栏"), show_copy_button=True),
|
124 |
gr.Audio(
|
125 |
+
label=_L("含元信息音频下载"),
|
126 |
show_download_button=True,
|
127 |
show_share_button=False,
|
128 |
),
|
129 |
+
gr.Textbox(label=_L("歌名"), show_copy_button=True),
|
130 |
+
gr.Textbox(label=_L("作者"), show_copy_button=True),
|
131 |
+
gr.Textbox(label=_L("专辑"), show_copy_button=True),
|
132 |
+
gr.TextArea(label=_L("歌词"), show_copy_button=True),
|
133 |
+
gr.Image(label=_L("歌曲图片"), show_share_button=False),
|
134 |
+
gr.Textbox(label=_L("歌曲 ID"), show_copy_button=True),
|
135 |
+
gr.Textbox(label=_L("音质"), show_copy_button=True),
|
136 |
+
gr.Textbox(label=_L("大小"), show_copy_button=True),
|
137 |
],
|
138 |
+
title=_L("酷我音乐无损解析"),
|
139 |
flagging_mode="never",
|
140 |
examples=[
|
141 |
"237668532",
|
lizhi.py → modules/lizhi.py
RENAMED
@@ -3,8 +3,20 @@ import gradio as gr
|
|
3 |
from tqdm import tqdm
|
4 |
from pydub import AudioSegment
|
5 |
from datetime import datetime, timedelta
|
6 |
-
from utils import rm_dir, mk_dir,
|
7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
|
9 |
|
10 |
def get_prev_day(date_str):
|
@@ -14,15 +26,20 @@ def get_prev_day(date_str):
|
|
14 |
return previous_day.strftime(date_format)
|
15 |
|
16 |
|
17 |
-
def rm_end_seconds(input_file: str, output_file
|
|
|
|
|
|
|
|
|
18 |
audio = AudioSegment.from_file(input_file)
|
19 |
remove_ms = seconds * 1000
|
20 |
new_audio = audio[:-remove_ms]
|
21 |
new_audio.export(output_file, format="mp3")
|
|
|
22 |
|
23 |
|
24 |
def download_mp3(url: str, local_file: str):
|
25 |
-
response = requests.get(url)
|
26 |
retcode = response.status_code
|
27 |
if retcode == 200:
|
28 |
total_size = int(response.headers.get("Content-Length", 0)) + 1
|
@@ -35,45 +52,45 @@ def download_mp3(url: str, local_file: str):
|
|
35 |
)
|
36 |
with open(local_file, "wb") as f:
|
37 |
for chunk in response.iter_content(chunk_size=8192):
|
38 |
-
if chunk:
|
39 |
-
f.write(chunk)
|
40 |
progress_bar.update(len(chunk))
|
41 |
|
42 |
-
rm_end_seconds(local_file
|
43 |
|
44 |
elif retcode == 404:
|
45 |
bad_date = "/".join(url.split("/audio/")[-1].split("/")[:-1])
|
46 |
fixed_date = get_prev_day(bad_date)
|
47 |
fixed_url = url.replace(bad_date, fixed_date)
|
48 |
-
download_mp3(fixed_url, local_file)
|
49 |
|
50 |
else:
|
51 |
-
raise ConnectionError(f"
|
52 |
|
53 |
|
54 |
-
# outer func
|
55 |
def infer(page_url: str, date: str, cache=f"{TMP_DIR}/lizhi"):
|
56 |
-
|
|
|
57 |
try:
|
58 |
rm_dir(cache)
|
59 |
if not page_url:
|
60 |
-
raise ValueError("
|
61 |
|
62 |
if ("http" in page_url and ".lizhi" in page_url) or page_url.isdigit():
|
63 |
sound_id = extract_fst_int(page_url.split("/")[-1])
|
64 |
else:
|
65 |
-
raise ValueError("
|
66 |
|
67 |
voice_time = date.strip().replace("-", "/")
|
68 |
mp3_url = f"http://cdn5.lizhi.fm/audio/{voice_time}/{sound_id}_hd.mp3"
|
69 |
-
outpath = f"{cache}/{sound_id}.mp3"
|
70 |
mk_dir(cache)
|
71 |
-
download_mp3(mp3_url,
|
72 |
-
return outpath
|
73 |
|
74 |
except Exception as e:
|
75 |
-
|
76 |
-
|
|
|
77 |
|
78 |
|
79 |
def lizhifm_parser():
|
@@ -81,21 +98,20 @@ def lizhifm_parser():
|
|
81 |
fn=infer,
|
82 |
inputs=[
|
83 |
gr.Textbox(
|
84 |
-
label="
|
85 |
placeholder="https://www.lizhi.fm/*/*",
|
86 |
),
|
87 |
-
gr.Textbox(
|
88 |
-
|
89 |
-
|
90 |
-
),
|
|
|
91 |
],
|
92 |
-
outputs=gr.Audio(
|
93 |
-
label="Download MP3",
|
94 |
-
show_download_button=True,
|
95 |
-
),
|
96 |
-
title="Lizhi FM Audio Direct URL Parsing Tool",
|
97 |
flagging_mode="never",
|
98 |
-
|
|
|
|
|
|
|
99 |
examples=[
|
100 |
["https://www.lizhi.fm/voice/3136401036767886342", "2025-04-05"],
|
101 |
["https://www.lizhifm.com/voice/3136401036767886342", "2025-04-05"],
|
|
|
3 |
from tqdm import tqdm
|
4 |
from pydub import AudioSegment
|
5 |
from datetime import datetime, timedelta
|
6 |
+
from utils import timestamp, extract_fst_int, rm_dir, mk_dir, TMP_DIR, LANG
|
7 |
+
|
8 |
+
ZH2EN = {
|
9 |
+
"输入声音页 URL": "Enter the sound page URL",
|
10 |
+
"按格式输入声音发布日期": "Enter sound publication date in format",
|
11 |
+
"下载 MP3": "Download MP3",
|
12 |
+
"荔枝FM音频解析下载": "Lizhi FM Audio Direct URL Parsing Tool",
|
13 |
+
"推荐辅助工具 <a href='https://tool.lu/datecalc' target='_blank'>日期计算器</a>": "The <a href='https://tool.lu/en_US/datecalc' target='_blank'>datecalc</a> is highly recommanded.",
|
14 |
+
"状态栏": "Status",
|
15 |
+
}
|
16 |
+
|
17 |
+
|
18 |
+
def _L(zh_txt: str):
|
19 |
+
return ZH2EN[zh_txt] if LANG else zh_txt
|
20 |
|
21 |
|
22 |
def get_prev_day(date_str):
|
|
|
26 |
return previous_day.strftime(date_format)
|
27 |
|
28 |
|
29 |
+
def rm_end_seconds(input_file: str, output_file="", seconds=3.1):
|
30 |
+
print("移除音频水印...")
|
31 |
+
if not output_file:
|
32 |
+
output_file = input_file
|
33 |
+
|
34 |
audio = AudioSegment.from_file(input_file)
|
35 |
remove_ms = seconds * 1000
|
36 |
new_audio = audio[:-remove_ms]
|
37 |
new_audio.export(output_file, format="mp3")
|
38 |
+
return output_file
|
39 |
|
40 |
|
41 |
def download_mp3(url: str, local_file: str):
|
42 |
+
response = requests.get(url, stream=True)
|
43 |
retcode = response.status_code
|
44 |
if retcode == 200:
|
45 |
total_size = int(response.headers.get("Content-Length", 0)) + 1
|
|
|
52 |
)
|
53 |
with open(local_file, "wb") as f:
|
54 |
for chunk in response.iter_content(chunk_size=8192):
|
55 |
+
if chunk: # 确保 chunk 不为空
|
56 |
+
f.write(chunk) # 更新进度条
|
57 |
progress_bar.update(len(chunk))
|
58 |
|
59 |
+
return rm_end_seconds(local_file)
|
60 |
|
61 |
elif retcode == 404:
|
62 |
bad_date = "/".join(url.split("/audio/")[-1].split("/")[:-1])
|
63 |
fixed_date = get_prev_day(bad_date)
|
64 |
fixed_url = url.replace(bad_date, fixed_date)
|
65 |
+
return download_mp3(fixed_url, local_file)
|
66 |
|
67 |
else:
|
68 |
+
raise ConnectionError(f"错误: {retcode}, {response.text}")
|
69 |
|
70 |
|
71 |
+
# outer func requires try except
|
72 |
def infer(page_url: str, date: str, cache=f"{TMP_DIR}/lizhi"):
|
73 |
+
status = "Success"
|
74 |
+
outpath = None
|
75 |
try:
|
76 |
rm_dir(cache)
|
77 |
if not page_url:
|
78 |
+
raise ValueError("声音链接或ID为空")
|
79 |
|
80 |
if ("http" in page_url and ".lizhi" in page_url) or page_url.isdigit():
|
81 |
sound_id = extract_fst_int(page_url.split("/")[-1])
|
82 |
else:
|
83 |
+
raise ValueError("无效的声音链接或ID")
|
84 |
|
85 |
voice_time = date.strip().replace("-", "/")
|
86 |
mp3_url = f"http://cdn5.lizhi.fm/audio/{voice_time}/{sound_id}_hd.mp3"
|
|
|
87 |
mk_dir(cache)
|
88 |
+
outpath = download_mp3(mp3_url, f"{cache}/{sound_id}.mp3")
|
|
|
89 |
|
90 |
except Exception as e:
|
91 |
+
status = f"{e}"
|
92 |
+
|
93 |
+
return status, outpath
|
94 |
|
95 |
|
96 |
def lizhifm_parser():
|
|
|
98 |
fn=infer,
|
99 |
inputs=[
|
100 |
gr.Textbox(
|
101 |
+
label=_L("输入声音页 URL"),
|
102 |
placeholder="https://www.lizhi.fm/*/*",
|
103 |
),
|
104 |
+
gr.Textbox(label=_L("按格式输入声音发布日期"), placeholder="YYYY-MM-DD"),
|
105 |
+
],
|
106 |
+
outputs=[
|
107 |
+
gr.Textbox(label=_L("状态栏"), show_copy_button=True),
|
108 |
+
gr.Audio(label=_L("下载 MP3"), show_download_button=True),
|
109 |
],
|
|
|
|
|
|
|
|
|
|
|
110 |
flagging_mode="never",
|
111 |
+
title=_L("荔枝FM音频解析下载"),
|
112 |
+
description=_L(
|
113 |
+
"推荐辅助工具 <a href='https://tool.lu/datecalc' target='_blank'>日期计算器</a>"
|
114 |
+
),
|
115 |
examples=[
|
116 |
["https://www.lizhi.fm/voice/3136401036767886342", "2025-04-05"],
|
117 |
["https://www.lizhifm.com/voice/3136401036767886342", "2025-04-05"],
|
meta.py → modules/meta.py
RENAMED
@@ -4,18 +4,36 @@ import gradio as gr
|
|
4 |
from mutagen.mp3 import MP3
|
5 |
from mutagen.flac import FLAC
|
6 |
from mutagen.id3 import USLT, PictureType
|
7 |
-
from utils import insert_meta, clean_dir
|
8 |
-
from config import TMP_DIR
|
9 |
|
10 |
CACHE_DIR = f"{TMP_DIR}/meta"
|
11 |
-
|
12 |
-
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
def extract_metadata(file_path: str):
|
|
|
15 |
title = fname = artist = album = lyrics = cover = None
|
16 |
try:
|
17 |
if not file_path:
|
18 |
-
raise ValueError("
|
19 |
|
20 |
fname = os.path.splitext(os.path.basename(file_path))[0]
|
21 |
file_ext = os.path.splitext(file_path)[1].lower()
|
@@ -25,9 +43,9 @@ def extract_metadata(file_path: str):
|
|
25 |
title, artist, album, lyrics, cover = extract_flac_meta(file_path)
|
26 |
|
27 |
except Exception as e:
|
28 |
-
|
29 |
|
30 |
-
return title, fname, artist, album, lyrics, cover
|
31 |
|
32 |
|
33 |
def extract_mp3_meta(file_path):
|
@@ -41,9 +59,9 @@ def extract_mp3_meta(file_path):
|
|
41 |
lyrics = tag.text
|
42 |
|
43 |
if tag.FrameID == "APIC":
|
44 |
-
|
|
|
45 |
img_file.write(tag.data)
|
46 |
-
cover = f"{CACHE_DIR}/cover.jpg"
|
47 |
|
48 |
return title, artist, album, lyrics, cover
|
49 |
|
@@ -57,82 +75,110 @@ def extract_flac_meta(file_path):
|
|
57 |
lyrics = audio.get("LYRICS")[0]
|
58 |
for picture in audio.pictures:
|
59 |
if picture.type == PictureType.COVER_FRONT:
|
60 |
-
|
|
|
61 |
img_file.write(picture.data)
|
62 |
-
cover = f"{CACHE_DIR}/cover.jpg"
|
63 |
|
64 |
break
|
65 |
|
66 |
return title, artist, album, lyrics, cover
|
67 |
|
68 |
|
69 |
-
# outer func
|
70 |
-
def upd_meta(
|
|
|
|
|
71 |
try:
|
72 |
-
file_ext = os.path.splitext(
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
77 |
|
|
|
|
|
|
|
|
|
|
|
78 |
except Exception as e:
|
79 |
-
|
80 |
-
|
|
|
81 |
|
82 |
|
83 |
def music_meta_editor():
|
84 |
clean_dir(CACHE_DIR)
|
85 |
with gr.Blocks() as meta:
|
86 |
-
gr.Markdown("#
|
87 |
with gr.Row():
|
88 |
with gr.Column():
|
89 |
-
input_audio = gr.Audio(
|
|
|
|
|
|
|
90 |
fname = gr.Textbox(
|
91 |
-
label="
|
92 |
-
placeholder="
|
93 |
interactive=True,
|
94 |
show_copy_button=True,
|
95 |
)
|
96 |
title = gr.Textbox(
|
97 |
-
label="
|
98 |
interactive=True,
|
99 |
show_copy_button=True,
|
100 |
)
|
101 |
artist = gr.Textbox(
|
102 |
-
label="
|
103 |
interactive=True,
|
104 |
show_copy_button=True,
|
105 |
)
|
106 |
album = gr.Textbox(
|
107 |
-
label="
|
108 |
interactive=True,
|
109 |
show_copy_button=True,
|
110 |
)
|
111 |
lyrics = gr.TextArea(
|
112 |
-
label="
|
113 |
interactive=True,
|
114 |
show_copy_button=True,
|
115 |
)
|
116 |
cover = gr.Image(
|
117 |
-
label="
|
118 |
interactive=True,
|
119 |
type="filepath",
|
120 |
)
|
121 |
-
submit = gr.Button("
|
122 |
|
123 |
with gr.Column():
|
124 |
-
|
|
|
125 |
|
126 |
input_audio.upload(
|
127 |
fn=extract_metadata,
|
128 |
inputs=input_audio,
|
129 |
-
outputs=[title, fname, artist, album, lyrics, cover],
|
|
|
|
|
|
|
|
|
|
|
|
|
130 |
)
|
131 |
|
132 |
submit.click(
|
133 |
fn=upd_meta,
|
134 |
inputs=[input_audio, fname, title, artist, album, lyrics, cover],
|
135 |
-
outputs=output_audio,
|
136 |
)
|
137 |
|
138 |
return meta
|
|
|
4 |
from mutagen.mp3 import MP3
|
5 |
from mutagen.flac import FLAC
|
6 |
from mutagen.id3 import USLT, PictureType
|
7 |
+
from utils import insert_meta, clean_dir, TMP_DIR, LANG
|
|
|
8 |
|
9 |
CACHE_DIR = f"{TMP_DIR}/meta"
|
10 |
+
ZH2EN = {
|
11 |
+
"# 音频 Meta 信息编辑器": "# Music Metadata Editor",
|
12 |
+
"上传待编辑音乐 (mp3/flac)": "Upload music (mp3/flac)",
|
13 |
+
"编辑文件名 (不含后缀)": "Edit filename (excluding suffix)",
|
14 |
+
"请确保文件名有效": "Ensure the filename is valid",
|
15 |
+
"编辑歌名": "Edit title",
|
16 |
+
"编辑作者": "Edit artist",
|
17 |
+
"编辑专辑": "Edit album",
|
18 |
+
"编辑歌词": "Edit lyrics",
|
19 |
+
"编辑封面 (留空意味着清空)": "Edit cover (leaving blank means deletion)",
|
20 |
+
"提交": "Submit",
|
21 |
+
"下载输出音频": "Download output audio",
|
22 |
+
"状态栏": "Status",
|
23 |
+
}
|
24 |
+
|
25 |
+
|
26 |
+
def _L(zh_txt: str):
|
27 |
+
return ZH2EN[zh_txt] if LANG else zh_txt
|
28 |
+
|
29 |
+
|
30 |
+
# outer func requires try except
|
31 |
def extract_metadata(file_path: str):
|
32 |
+
status = "Success"
|
33 |
title = fname = artist = album = lyrics = cover = None
|
34 |
try:
|
35 |
if not file_path:
|
36 |
+
raise ValueError("文件路径为空!")
|
37 |
|
38 |
fname = os.path.splitext(os.path.basename(file_path))[0]
|
39 |
file_ext = os.path.splitext(file_path)[1].lower()
|
|
|
43 |
title, artist, album, lyrics, cover = extract_flac_meta(file_path)
|
44 |
|
45 |
except Exception as e:
|
46 |
+
status = f"{e}"
|
47 |
|
48 |
+
return status, title, fname, artist, album, lyrics, cover
|
49 |
|
50 |
|
51 |
def extract_mp3_meta(file_path):
|
|
|
59 |
lyrics = tag.text
|
60 |
|
61 |
if tag.FrameID == "APIC":
|
62 |
+
cover = f"{CACHE_DIR}/cover.jpg"
|
63 |
+
with open(cover, "wb") as img_file:
|
64 |
img_file.write(tag.data)
|
|
|
65 |
|
66 |
return title, artist, album, lyrics, cover
|
67 |
|
|
|
75 |
lyrics = audio.get("LYRICS")[0]
|
76 |
for picture in audio.pictures:
|
77 |
if picture.type == PictureType.COVER_FRONT:
|
78 |
+
cover = f"{CACHE_DIR}/cover.jpg"
|
79 |
+
with open(cover, "wb") as img_file:
|
80 |
img_file.write(picture.data)
|
|
|
81 |
|
82 |
break
|
83 |
|
84 |
return title, artist, album, lyrics, cover
|
85 |
|
86 |
|
87 |
+
# outer func requires try except
|
88 |
+
def upd_meta(audio_in: str, fname, title, artist, album, lyric, cover):
|
89 |
+
status = "Success"
|
90 |
+
audio_out = None
|
91 |
try:
|
92 |
+
file_ext = os.path.splitext(audio_in)[1].lower()
|
93 |
+
audio_out = insert_meta(
|
94 |
+
audio_in,
|
95 |
+
title,
|
96 |
+
artist,
|
97 |
+
album,
|
98 |
+
lyric,
|
99 |
+
cover,
|
100 |
+
f"{CACHE_DIR}/{fname}{file_ext}",
|
101 |
+
)
|
102 |
+
|
103 |
+
except Exception as e:
|
104 |
+
status = f"{e}"
|
105 |
+
|
106 |
+
return status, audio_out
|
107 |
|
108 |
+
|
109 |
+
def clear_inputs():
|
110 |
+
status = None
|
111 |
+
try:
|
112 |
+
clean_dir(CACHE_DIR)
|
113 |
except Exception as e:
|
114 |
+
status = f"{e}"
|
115 |
+
|
116 |
+
return status, None, None, None, None, None, None
|
117 |
|
118 |
|
119 |
def music_meta_editor():
|
120 |
clean_dir(CACHE_DIR)
|
121 |
with gr.Blocks() as meta:
|
122 |
+
gr.Markdown(_L("# 音频 Meta 信息编辑器"))
|
123 |
with gr.Row():
|
124 |
with gr.Column():
|
125 |
+
input_audio = gr.Audio(
|
126 |
+
label=_L("上传待编辑音乐 (mp3/flac)"),
|
127 |
+
type="filepath",
|
128 |
+
)
|
129 |
fname = gr.Textbox(
|
130 |
+
label=_L("编辑文件名 (不含后缀)"),
|
131 |
+
placeholder=_L("请确保文件名有效"),
|
132 |
interactive=True,
|
133 |
show_copy_button=True,
|
134 |
)
|
135 |
title = gr.Textbox(
|
136 |
+
label=_L("编辑歌名"),
|
137 |
interactive=True,
|
138 |
show_copy_button=True,
|
139 |
)
|
140 |
artist = gr.Textbox(
|
141 |
+
label=_L("编辑作者"),
|
142 |
interactive=True,
|
143 |
show_copy_button=True,
|
144 |
)
|
145 |
album = gr.Textbox(
|
146 |
+
label=_L("编辑专辑"),
|
147 |
interactive=True,
|
148 |
show_copy_button=True,
|
149 |
)
|
150 |
lyrics = gr.TextArea(
|
151 |
+
label=_L("编辑歌词"),
|
152 |
interactive=True,
|
153 |
show_copy_button=True,
|
154 |
)
|
155 |
cover = gr.Image(
|
156 |
+
label=_L("编辑封面 (留空意味着清空)"),
|
157 |
interactive=True,
|
158 |
type="filepath",
|
159 |
)
|
160 |
+
submit = gr.Button(_L("提交"))
|
161 |
|
162 |
with gr.Column():
|
163 |
+
status_bar = gr.Textbox(label=_L("状态栏"), show_copy_button=True)
|
164 |
+
output_audio = gr.Audio(label=_L("下载输出音频"))
|
165 |
|
166 |
input_audio.upload(
|
167 |
fn=extract_metadata,
|
168 |
inputs=input_audio,
|
169 |
+
outputs=[status_bar, title, fname, artist, album, lyrics, cover],
|
170 |
+
)
|
171 |
+
|
172 |
+
input_audio.clear(
|
173 |
+
fn=clear_inputs,
|
174 |
+
inputs=None,
|
175 |
+
outputs=[status_bar, title, fname, artist, album, lyrics, cover],
|
176 |
)
|
177 |
|
178 |
submit.click(
|
179 |
fn=upd_meta,
|
180 |
inputs=[input_audio, fname, title, artist, album, lyrics, cover],
|
181 |
+
outputs=[status_bar, output_audio],
|
182 |
)
|
183 |
|
184 |
return meta
|
netease.py → modules/netease.py
RENAMED
@@ -2,12 +2,57 @@ import re
|
|
2 |
import requests
|
3 |
import gradio as gr
|
4 |
from tqdm import tqdm
|
5 |
-
from
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
):
|
12 |
clean_dir(cache)
|
13 |
fmt = "mp3" if " MP3" in quality else "flac"
|
@@ -24,15 +69,14 @@ def download_file(
|
|
24 |
)
|
25 |
with open(local_file, "wb") as f:
|
26 |
for chunk in response.iter_content(chunk_size=8192):
|
27 |
-
if chunk:
|
28 |
-
f.write(chunk)
|
29 |
progress_bar.update(len(chunk))
|
30 |
|
31 |
-
insert_meta(local_file, title, artist, album, lyric, cover)
|
32 |
-
return local_file
|
33 |
|
34 |
else:
|
35 |
-
raise ConnectionError(f"
|
36 |
|
37 |
|
38 |
def parse_id(url: str):
|
@@ -56,24 +100,28 @@ def parse_id(url: str):
|
|
56 |
return None
|
57 |
|
58 |
|
59 |
-
# outer func
|
60 |
def infer(url: str, level: str):
|
|
|
61 |
if not level:
|
62 |
-
level = "
|
63 |
|
64 |
song = title = cover = artist = album = quality = size = lyric = None
|
65 |
try:
|
66 |
song_id = parse_id(url)
|
67 |
if "playlist?" in url:
|
68 |
-
raise ValueError("
|
69 |
|
70 |
if not song_id or not level:
|
71 |
-
|
72 |
-
return song, title, artist, album, lyric, cover, song_id, quality, size
|
73 |
|
74 |
response = requests.get(
|
75 |
API_163,
|
76 |
-
params={
|
|
|
|
|
|
|
|
|
77 |
timeout=TIMEOUT,
|
78 |
)
|
79 |
|
@@ -82,23 +130,33 @@ def infer(url: str, level: str):
|
|
82 |
url = data["url"]
|
83 |
if extract_fst_url(url):
|
84 |
title = data["name"]
|
85 |
-
cover = data["pic"]
|
86 |
artist = data["artist"]
|
87 |
album = data["album"]
|
|
|
|
|
88 |
quality = data["format"]
|
89 |
size = data["size"]
|
90 |
-
|
91 |
-
|
92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
93 |
)
|
94 |
|
95 |
else:
|
96 |
-
raise ValueError(f"{url}
|
|
|
|
|
|
|
97 |
|
98 |
except Exception as e:
|
99 |
-
|
100 |
|
101 |
-
return song, title, artist, album, lyric, cover, song_id, quality, size
|
102 |
|
103 |
|
104 |
def parser163():
|
@@ -106,45 +164,50 @@ def parser163():
|
|
106 |
fn=infer,
|
107 |
inputs=[
|
108 |
gr.Textbox(
|
109 |
-
label="
|
110 |
placeholder="https://music.163.com/#/song?id=",
|
111 |
),
|
112 |
gr.Dropdown(
|
113 |
choices=[
|
114 |
-
"
|
115 |
-
"
|
116 |
-
"
|
117 |
-
"
|
118 |
-
"
|
119 |
-
"
|
120 |
-
"
|
121 |
],
|
122 |
-
value="
|
123 |
-
label="
|
124 |
),
|
125 |
],
|
126 |
outputs=[
|
|
|
127 |
gr.Audio(
|
128 |
-
label="
|
129 |
show_download_button=True,
|
130 |
show_share_button=False,
|
131 |
),
|
132 |
-
gr.Textbox(label="
|
133 |
-
gr.Textbox(label="
|
134 |
-
gr.Textbox(label="
|
135 |
-
gr.TextArea(label="
|
136 |
-
gr.Image(label="
|
137 |
-
gr.Textbox(label="
|
138 |
-
gr.Textbox(label="
|
139 |
-
gr.Textbox(label="
|
140 |
],
|
141 |
-
title="
|
142 |
flagging_mode="never",
|
143 |
examples=[
|
144 |
-
["36990266", "
|
145 |
-
["
|
146 |
-
["https://music.163.com/#/song?id=36990266", "
|
147 |
-
["
|
|
|
|
|
|
|
|
|
148 |
],
|
149 |
cache_examples=False,
|
150 |
)
|
|
|
2 |
import requests
|
3 |
import gradio as gr
|
4 |
from tqdm import tqdm
|
5 |
+
from utils import (
|
6 |
+
extract_fst_url,
|
7 |
+
get_real_url,
|
8 |
+
insert_meta,
|
9 |
+
clean_dir,
|
10 |
+
timestamp,
|
11 |
+
API_163,
|
12 |
+
TIMEOUT,
|
13 |
+
TMP_DIR,
|
14 |
+
LANG,
|
15 |
+
)
|
16 |
+
|
17 |
+
|
18 |
+
ZH2EN = {
|
19 |
+
"请输入网易云音乐 ID 或 URL 链接": "Please input music163 song ID or URL",
|
20 |
+
"选择音质": "Sound quality",
|
21 |
+
"标准音质": "standard",
|
22 |
+
"极高音质": "exhigh",
|
23 |
+
"无损音质": "lossless",
|
24 |
+
"Hires音质": "hires",
|
25 |
+
"沉浸环绕声": "sky",
|
26 |
+
"高清环绕声": "jyeffect",
|
27 |
+
"超清母带": "jymaster",
|
28 |
+
"含元信息音频下载": "Audio with metadata",
|
29 |
+
"歌名": "Title",
|
30 |
+
"作者": "Artist",
|
31 |
+
"专辑": "Album",
|
32 |
+
"歌词": "Lyrics",
|
33 |
+
"歌曲图片": "Cover",
|
34 |
+
"歌曲 ID": "Song ID",
|
35 |
+
"音质": "Quality",
|
36 |
+
"大小": "Size",
|
37 |
+
"网易云音乐无损解析": "Parse Music163 Songs without Loss",
|
38 |
+
"状态栏": "Status",
|
39 |
+
}
|
40 |
+
|
41 |
+
|
42 |
+
def _L(zh_txt: str):
|
43 |
+
return ZH2EN[zh_txt] if LANG else zh_txt
|
44 |
+
|
45 |
+
|
46 |
+
def download_audio(
|
47 |
+
id: int,
|
48 |
+
quality: str,
|
49 |
+
url: str,
|
50 |
+
title: str,
|
51 |
+
artist: str,
|
52 |
+
album: str,
|
53 |
+
lyric: str,
|
54 |
+
cover: str,
|
55 |
+
cache=f"{TMP_DIR}/163",
|
56 |
):
|
57 |
clean_dir(cache)
|
58 |
fmt = "mp3" if " MP3" in quality else "flac"
|
|
|
69 |
)
|
70 |
with open(local_file, "wb") as f:
|
71 |
for chunk in response.iter_content(chunk_size=8192):
|
72 |
+
if chunk: # 确保 chunk 不为空
|
73 |
+
f.write(chunk) # 更新进度条
|
74 |
progress_bar.update(len(chunk))
|
75 |
|
76 |
+
return insert_meta(local_file, title, artist, album, lyric, cover)
|
|
|
77 |
|
78 |
else:
|
79 |
+
raise ConnectionError(f"下载失败,状态码:{response.status_code}")
|
80 |
|
81 |
|
82 |
def parse_id(url: str):
|
|
|
100 |
return None
|
101 |
|
102 |
|
103 |
+
# outer func requires try except
|
104 |
def infer(url: str, level: str):
|
105 |
+
status = "Success"
|
106 |
if not level:
|
107 |
+
level = _L("无损音质")
|
108 |
|
109 |
song = title = cover = artist = album = quality = size = lyric = None
|
110 |
try:
|
111 |
song_id = parse_id(url)
|
112 |
if "playlist?" in url:
|
113 |
+
raise ValueError("请输入歌曲链接而非歌单链接!")
|
114 |
|
115 |
if not song_id or not level:
|
116 |
+
raise ValueError("请输入有效的网址或 ID, 并选择一个声音质量水平!")
|
|
|
117 |
|
118 |
response = requests.get(
|
119 |
API_163,
|
120 |
+
params={
|
121 |
+
"id": song_id,
|
122 |
+
"type": "json",
|
123 |
+
"level": level if LANG else ZH2EN[level],
|
124 |
+
},
|
125 |
timeout=TIMEOUT,
|
126 |
)
|
127 |
|
|
|
130 |
url = data["url"]
|
131 |
if extract_fst_url(url):
|
132 |
title = data["name"]
|
|
|
133 |
artist = data["artist"]
|
134 |
album = data["album"]
|
135 |
+
lyric = data["lyric"]
|
136 |
+
cover = data["pic"]
|
137 |
quality = data["format"]
|
138 |
size = data["size"]
|
139 |
+
song = download_audio(
|
140 |
+
data["id"],
|
141 |
+
quality,
|
142 |
+
url,
|
143 |
+
title,
|
144 |
+
artist,
|
145 |
+
album,
|
146 |
+
lyric,
|
147 |
+
cover,
|
148 |
)
|
149 |
|
150 |
else:
|
151 |
+
raise ValueError(f"{url}或歌曲不存在")
|
152 |
+
|
153 |
+
else:
|
154 |
+
raise ConnectionError(f"HTTP {response.status_code}")
|
155 |
|
156 |
except Exception as e:
|
157 |
+
status = f"{e}"
|
158 |
|
159 |
+
return status, song, title, artist, album, lyric, cover, song_id, quality, size
|
160 |
|
161 |
|
162 |
def parser163():
|
|
|
164 |
fn=infer,
|
165 |
inputs=[
|
166 |
gr.Textbox(
|
167 |
+
label=_L("请输入网易云音乐 ID 或 URL 链接"),
|
168 |
placeholder="https://music.163.com/#/song?id=",
|
169 |
),
|
170 |
gr.Dropdown(
|
171 |
choices=[
|
172 |
+
_L("标准音质"),
|
173 |
+
_L("极高音质"),
|
174 |
+
_L("无损音质"),
|
175 |
+
_L("Hires音质"),
|
176 |
+
_L("沉浸环绕声"),
|
177 |
+
_L("高清环绕声"),
|
178 |
+
_L("超清母带"),
|
179 |
],
|
180 |
+
value=_L("无损音质"),
|
181 |
+
label=_L("选择音质"),
|
182 |
),
|
183 |
],
|
184 |
outputs=[
|
185 |
+
gr.Textbox(label=_L("状态栏"), show_copy_button=True),
|
186 |
gr.Audio(
|
187 |
+
label=_L("含元信息音频下载"),
|
188 |
show_download_button=True,
|
189 |
show_share_button=False,
|
190 |
),
|
191 |
+
gr.Textbox(label=_L("歌名"), show_copy_button=True),
|
192 |
+
gr.Textbox(label=_L("作者"), show_copy_button=True),
|
193 |
+
gr.Textbox(label=_L("专辑"), show_copy_button=True),
|
194 |
+
gr.TextArea(label=_L("歌词"), show_copy_button=True),
|
195 |
+
gr.Image(label=_L("歌曲图片"), show_share_button=False),
|
196 |
+
gr.Textbox(label=_L("歌曲 ID"), show_copy_button=True),
|
197 |
+
gr.Textbox(label=_L("音质"), show_copy_button=True),
|
198 |
+
gr.Textbox(label=_L("大小"), show_copy_button=True),
|
199 |
],
|
200 |
+
title=_L("网易云音乐无损解析"),
|
201 |
flagging_mode="never",
|
202 |
examples=[
|
203 |
+
["36990266", _L("标准音质")],
|
204 |
+
["http://163cn.tv/CmHfO44", _L("标准音质")],
|
205 |
+
["https://music.163.com/#/song?id=36990266", _L("标准音质")],
|
206 |
+
["https://y.music.163.com/m/song?id=36990266", _L("极高音质")],
|
207 |
+
[
|
208 |
+
"分享Alan Walker的单曲《Faded》http://163cn.tv/CmHfO44 (@网易云音乐)",
|
209 |
+
_L("无损音质"),
|
210 |
+
],
|
211 |
],
|
212 |
cache_examples=False,
|
213 |
)
|
qq.py → modules/qq.py
RENAMED
@@ -1,35 +1,36 @@
|
|
1 |
import re
|
2 |
import requests
|
3 |
import gradio as gr
|
4 |
-
from
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
|
|
33 |
|
34 |
|
35 |
def parse_id(url: str):
|
@@ -55,36 +56,37 @@ def parse_id(url: str):
|
|
55 |
|
56 |
def parse_size(size_in_bytes):
|
57 |
units = ["B", "KB", "MB", "GB"]
|
58 |
-
unit_index = 0
|
59 |
-
|
|
|
60 |
unit_index = 3
|
61 |
size = size_in_bytes / (1024**3)
|
62 |
|
63 |
-
elif size_in_bytes >= 1024**2:
|
64 |
unit_index = 2
|
65 |
size = size_in_bytes / (1024**2)
|
66 |
|
67 |
-
elif size_in_bytes >= 1024:
|
68 |
unit_index = 1
|
69 |
size = size_in_bytes / 1024
|
70 |
|
71 |
else:
|
72 |
size = size_in_bytes
|
73 |
-
|
74 |
if unit_index == 0:
|
75 |
return f"{size}{units[unit_index]}"
|
76 |
else:
|
77 |
return f"{size:.2f}{units[unit_index]}"
|
78 |
|
79 |
|
80 |
-
# outer func
|
81 |
def infer(url: str):
|
|
|
82 |
song = title = artist = album = lyric = cover = song_id = quality = size = None
|
83 |
try:
|
84 |
song_mid = parse_id(url)
|
85 |
if not song_mid:
|
86 |
-
|
87 |
-
return song, title, artist, album, lyric, cover, song_id, quality, size
|
88 |
|
89 |
# title, artist, album, cover
|
90 |
response = requests.get(
|
@@ -112,21 +114,27 @@ def infer(url: str):
|
|
112 |
response = requests.get(API_QQ_2, params={"mid": song_mid}, timeout=TIMEOUT)
|
113 |
if response.status_code == 200 and response.json()["code"] == 200:
|
114 |
data = response.json()["data"]
|
115 |
-
song = download_file(
|
116 |
-
song_mid, data["urls"][0]["url"], title, artist, album, lyric, cover
|
117 |
-
)
|
118 |
song_id = data["id"]
|
119 |
quality = data["urls"][0]["type"]
|
120 |
size = parse_size(int(data["urls"][0]["size"]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
121 |
|
122 |
else:
|
123 |
-
|
124 |
-
raise ConnectionError(response.json()["msg"] + ", mid or URL may be wrong!")
|
125 |
|
126 |
except Exception as e:
|
127 |
-
|
128 |
|
129 |
-
return song, title, artist, album, lyric, cover, song_id, quality, size
|
130 |
|
131 |
|
132 |
def qmusic_parser():
|
@@ -134,31 +142,32 @@ def qmusic_parser():
|
|
134 |
fn=infer,
|
135 |
inputs=[
|
136 |
gr.Textbox(
|
137 |
-
label="
|
138 |
placeholder="https://y.qq.com/n/ryqq/songDetail/*",
|
139 |
-
)
|
140 |
],
|
141 |
outputs=[
|
|
|
142 |
gr.Audio(
|
143 |
-
label="
|
144 |
show_download_button=True,
|
145 |
show_share_button=False,
|
146 |
),
|
147 |
-
gr.Textbox(label="
|
148 |
-
gr.Textbox(label="
|
149 |
-
gr.Textbox(label="
|
150 |
-
gr.TextArea(label="
|
151 |
-
gr.Image(label="
|
152 |
-
gr.Textbox(label="
|
153 |
-
gr.Textbox(label="
|
154 |
-
gr.Textbox(label="
|
155 |
],
|
156 |
-
title="QQ
|
157 |
flagging_mode="never",
|
158 |
examples=[
|
159 |
"000ZjUoy0DeHRO",
|
160 |
"https://y.qq.com/n/ryqq/songDetail/000ZjUoy0DeHRO",
|
161 |
-
"https://c6.y.qq.com/base/fcgi-bin/u?__=fY3GHnkxtiHJ",
|
162 |
],
|
163 |
cache_examples=False,
|
164 |
)
|
|
|
1 |
import re
|
2 |
import requests
|
3 |
import gradio as gr
|
4 |
+
from utils import (
|
5 |
+
extract_fst_url,
|
6 |
+
get_real_url,
|
7 |
+
download_file,
|
8 |
+
API_QQ_2,
|
9 |
+
API_QQ_1,
|
10 |
+
KEY_QQ_1,
|
11 |
+
TIMEOUT,
|
12 |
+
TMP_DIR,
|
13 |
+
LANG,
|
14 |
+
)
|
15 |
+
|
16 |
+
ZH2EN = {
|
17 |
+
"请输入QQ音乐 mid 或 URL 链接": "Please enter the mid or URL of QQ music song",
|
18 |
+
"含元信息音频下载": "Audio with metadata",
|
19 |
+
"歌名": "Title",
|
20 |
+
"作者": "Artist",
|
21 |
+
"专辑": "Album",
|
22 |
+
"歌词": "Lyrics",
|
23 |
+
"歌曲图片": "Cover",
|
24 |
+
"歌曲 ID": "Song ID",
|
25 |
+
"音质": "Quality",
|
26 |
+
"大小": "Size",
|
27 |
+
"QQ音乐直链解析": "QQ Music Parser",
|
28 |
+
"状态栏": "Status",
|
29 |
+
}
|
30 |
+
|
31 |
+
|
32 |
+
def _L(zh_txt: str):
|
33 |
+
return ZH2EN[zh_txt] if LANG else zh_txt
|
34 |
|
35 |
|
36 |
def parse_id(url: str):
|
|
|
56 |
|
57 |
def parse_size(size_in_bytes):
|
58 |
units = ["B", "KB", "MB", "GB"]
|
59 |
+
unit_index = 0 # 初始单位为字节
|
60 |
+
# 根据文件大小选择合适的单位
|
61 |
+
if size_in_bytes >= 1024**3: # 大于等于 1 GB
|
62 |
unit_index = 3
|
63 |
size = size_in_bytes / (1024**3)
|
64 |
|
65 |
+
elif size_in_bytes >= 1024**2: # 大于等于 1 MB
|
66 |
unit_index = 2
|
67 |
size = size_in_bytes / (1024**2)
|
68 |
|
69 |
+
elif size_in_bytes >= 1024: # 大于等于 1 KB
|
70 |
unit_index = 1
|
71 |
size = size_in_bytes / 1024
|
72 |
|
73 |
else:
|
74 |
size = size_in_bytes
|
75 |
+
# 格式化输出,保留两位小数
|
76 |
if unit_index == 0:
|
77 |
return f"{size}{units[unit_index]}"
|
78 |
else:
|
79 |
return f"{size:.2f}{units[unit_index]}"
|
80 |
|
81 |
|
82 |
+
# outer func requires try except
|
83 |
def infer(url: str):
|
84 |
+
status = "Success"
|
85 |
song = title = artist = album = lyric = cover = song_id = quality = size = None
|
86 |
try:
|
87 |
song_mid = parse_id(url)
|
88 |
if not song_mid:
|
89 |
+
raise ValueError("请输入有效的网址或mid!")
|
|
|
90 |
|
91 |
# title, artist, album, cover
|
92 |
response = requests.get(
|
|
|
114 |
response = requests.get(API_QQ_2, params={"mid": song_mid}, timeout=TIMEOUT)
|
115 |
if response.status_code == 200 and response.json()["code"] == 200:
|
116 |
data = response.json()["data"]
|
|
|
|
|
|
|
117 |
song_id = data["id"]
|
118 |
quality = data["urls"][0]["type"]
|
119 |
size = parse_size(int(data["urls"][0]["size"]))
|
120 |
+
song = download_file(
|
121 |
+
song_mid,
|
122 |
+
data["urls"][0]["url"],
|
123 |
+
title,
|
124 |
+
artist,
|
125 |
+
album,
|
126 |
+
lyric,
|
127 |
+
cover,
|
128 |
+
f"{TMP_DIR}/qq",
|
129 |
+
)
|
130 |
|
131 |
else:
|
132 |
+
raise ConnectionError(response.json()["msg"] + ", 可能是歌曲mid或URL有误")
|
|
|
133 |
|
134 |
except Exception as e:
|
135 |
+
status = f"{e}"
|
136 |
|
137 |
+
return status, song, title, artist, album, lyric, cover, song_id, quality, size
|
138 |
|
139 |
|
140 |
def qmusic_parser():
|
|
|
142 |
fn=infer,
|
143 |
inputs=[
|
144 |
gr.Textbox(
|
145 |
+
label=_L("请输入QQ音乐 mid 或 URL 链接"),
|
146 |
placeholder="https://y.qq.com/n/ryqq/songDetail/*",
|
147 |
+
)
|
148 |
],
|
149 |
outputs=[
|
150 |
+
gr.Textbox(label=_L("状态栏"), show_copy_button=True),
|
151 |
gr.Audio(
|
152 |
+
label=_L("含元信息音频下载"),
|
153 |
show_download_button=True,
|
154 |
show_share_button=False,
|
155 |
),
|
156 |
+
gr.Textbox(label=_L("歌名"), show_copy_button=True),
|
157 |
+
gr.Textbox(label=_L("作者"), show_copy_button=True),
|
158 |
+
gr.Textbox(label=_L("专辑"), show_copy_button=True),
|
159 |
+
gr.TextArea(label=_L("歌词"), show_copy_button=True),
|
160 |
+
gr.Image(label=_L("歌曲图片"), show_share_button=False),
|
161 |
+
gr.Textbox(label=_L("歌曲 ID"), show_copy_button=True),
|
162 |
+
gr.Textbox(label=_L("音质"), show_copy_button=True),
|
163 |
+
gr.Textbox(label=_L("大小"), show_copy_button=True),
|
164 |
],
|
165 |
+
title=_L("QQ音乐直链解析"),
|
166 |
flagging_mode="never",
|
167 |
examples=[
|
168 |
"000ZjUoy0DeHRO",
|
169 |
"https://y.qq.com/n/ryqq/songDetail/000ZjUoy0DeHRO",
|
170 |
+
"DJMOUN《一氧化碳 (监狱兔概念神) (DJ版)》 https://c6.y.qq.com/base/fcgi-bin/u?__=fY3GHnkxtiHJ @QQ音乐",
|
171 |
],
|
172 |
cache_examples=False,
|
173 |
)
|
requirements.txt
CHANGED
@@ -1,4 +1,2 @@
|
|
1 |
-
gradio
|
2 |
mutagen
|
3 |
-
tzlocal
|
4 |
-
requests
|
|
|
|
|
1 |
mutagen
|
2 |
+
tzlocal
|
|
utils.py
CHANGED
@@ -2,13 +2,29 @@ import os
|
|
2 |
import re
|
3 |
import shutil
|
4 |
import requests
|
|
|
5 |
from datetime import datetime
|
6 |
from zoneinfo import ZoneInfo
|
7 |
from tzlocal import get_localzone
|
8 |
from mutagen.mp3 import MP3
|
9 |
from mutagen.flac import FLAC, Picture
|
10 |
from mutagen.id3 import TIT2, TPE1, TALB, USLT, APIC
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
|
14 |
def timestamp(naive_time: datetime = None, target_tz=ZoneInfo("Asia/Shanghai")):
|
@@ -20,7 +36,12 @@ def timestamp(naive_time: datetime = None, target_tz=ZoneInfo("Asia/Shanghai")):
|
|
20 |
return aware_local.astimezone(target_tz).strftime("%Y-%m-%d %H:%M:%S")
|
21 |
|
22 |
|
23 |
-
def insert_meta(
|
|
|
|
|
|
|
|
|
|
|
24 |
if cover:
|
25 |
if cover.startswith("http"):
|
26 |
cover = requests.get(cover).content
|
@@ -28,26 +49,8 @@ def insert_meta(file_path: str, title, artist, album, lyric, cover: str):
|
|
28 |
with open(cover, "rb") as file:
|
29 |
cover = file.read()
|
30 |
|
31 |
-
if
|
32 |
-
audio =
|
33 |
-
audio.tags["TITLE"] = title
|
34 |
-
audio.tags["ARTIST"] = artist
|
35 |
-
audio.tags["ALBUM"] = album
|
36 |
-
audio.tags["LYRICS"] = lyric
|
37 |
-
if cover:
|
38 |
-
picture = Picture()
|
39 |
-
picture.type = 3 # cover
|
40 |
-
picture.mime = "image/jpeg"
|
41 |
-
picture.data = cover
|
42 |
-
audio.add_picture(picture)
|
43 |
-
|
44 |
-
else:
|
45 |
-
audio.clear_pictures()
|
46 |
-
|
47 |
-
audio.save()
|
48 |
-
|
49 |
-
elif file_path.endswith(".mp3"):
|
50 |
-
audio = MP3(file_path)
|
51 |
if audio.tags:
|
52 |
for tag_id in list(audio.tags.keys()):
|
53 |
del audio.tags[tag_id]
|
@@ -62,9 +65,9 @@ def insert_meta(file_path: str, title, artist, album, lyric, cover: str):
|
|
62 |
if cover:
|
63 |
audio.tags.add(
|
64 |
APIC(
|
65 |
-
encoding=3, # UTF-8
|
66 |
-
mime="image/jpeg", #
|
67 |
-
type=3, #
|
68 |
desc="Cover",
|
69 |
data=cover,
|
70 |
)
|
@@ -72,8 +75,26 @@ def insert_meta(file_path: str, title, artist, album, lyric, cover: str):
|
|
72 |
|
73 |
audio.save()
|
74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
|
76 |
-
|
|
|
|
|
|
|
|
|
|
|
77 |
match = re.search(r'(https?://[^\s"]+)', text)
|
78 |
if match:
|
79 |
return match.group(1)
|
@@ -111,3 +132,38 @@ def rm_dir(dirpath: str):
|
|
111 |
def clean_dir(dirpath: str):
|
112 |
rm_dir(dirpath)
|
113 |
os.makedirs(dirpath)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
import re
|
3 |
import shutil
|
4 |
import requests
|
5 |
+
from tqdm import tqdm
|
6 |
from datetime import datetime
|
7 |
from zoneinfo import ZoneInfo
|
8 |
from tzlocal import get_localzone
|
9 |
from mutagen.mp3 import MP3
|
10 |
from mutagen.flac import FLAC, Picture
|
11 |
from mutagen.id3 import TIT2, TPE1, TALB, USLT, APIC
|
12 |
+
|
13 |
+
LANG = os.getenv("language")
|
14 |
+
API_163 = os.getenv("api_music163")
|
15 |
+
API_KUWO = os.getenv("api_kuwo")
|
16 |
+
API_QQ_2 = os.getenv("api_qmusic_2")
|
17 |
+
API_QQ_1 = os.getenv("api_qmusic_1")
|
18 |
+
KEY_QQ_1 = os.getenv("apikey_qmusic_1")
|
19 |
+
if not (API_163 and API_KUWO and API_QQ_2 and API_QQ_1 and KEY_QQ_1):
|
20 |
+
print("请检查环境变量!")
|
21 |
+
exit()
|
22 |
+
|
23 |
+
TIMEOUT = None
|
24 |
+
TMP_DIR = "./__pycache__"
|
25 |
+
HEADER = {
|
26 |
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36"
|
27 |
+
}
|
28 |
|
29 |
|
30 |
def timestamp(naive_time: datetime = None, target_tz=ZoneInfo("Asia/Shanghai")):
|
|
|
36 |
return aware_local.astimezone(target_tz).strftime("%Y-%m-%d %H:%M:%S")
|
37 |
|
38 |
|
39 |
+
def insert_meta(audio_in: str, title, artist, album, lyric, cover: str, audio_out=""):
|
40 |
+
if audio_out:
|
41 |
+
shutil.copyfile(audio_in, audio_out)
|
42 |
+
else:
|
43 |
+
audio_out = audio_in
|
44 |
+
|
45 |
if cover:
|
46 |
if cover.startswith("http"):
|
47 |
cover = requests.get(cover).content
|
|
|
49 |
with open(cover, "rb") as file:
|
50 |
cover = file.read()
|
51 |
|
52 |
+
if audio_out.endswith(".mp3"):
|
53 |
+
audio = MP3(audio_out)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
if audio.tags:
|
55 |
for tag_id in list(audio.tags.keys()):
|
56 |
del audio.tags[tag_id]
|
|
|
65 |
if cover:
|
66 |
audio.tags.add(
|
67 |
APIC(
|
68 |
+
encoding=3, # UTF-8 编码
|
69 |
+
mime="image/jpeg", # 图片的 MIME 类型
|
70 |
+
type=3, # 封面图片
|
71 |
desc="Cover",
|
72 |
data=cover,
|
73 |
)
|
|
|
75 |
|
76 |
audio.save()
|
77 |
|
78 |
+
elif audio_out.endswith(".flac"):
|
79 |
+
audio = FLAC(audio_out)
|
80 |
+
audio.tags["TITLE"] = title
|
81 |
+
audio.tags["ARTIST"] = artist
|
82 |
+
audio.tags["ALBUM"] = album
|
83 |
+
audio.tags["LYRICS"] = lyric
|
84 |
+
audio.clear_pictures()
|
85 |
+
if cover:
|
86 |
+
picture = Picture()
|
87 |
+
picture.type = 3 # 封面图片
|
88 |
+
picture.mime = "image/jpeg"
|
89 |
+
picture.data = cover
|
90 |
+
audio.add_picture(picture)
|
91 |
|
92 |
+
audio.save()
|
93 |
+
|
94 |
+
return audio_out
|
95 |
+
|
96 |
+
|
97 |
+
def extract_fst_url(text):
|
98 |
match = re.search(r'(https?://[^\s"]+)', text)
|
99 |
if match:
|
100 |
return match.group(1)
|
|
|
132 |
def clean_dir(dirpath: str):
|
133 |
rm_dir(dirpath)
|
134 |
os.makedirs(dirpath)
|
135 |
+
|
136 |
+
|
137 |
+
def download_file(
|
138 |
+
id: int,
|
139 |
+
url: str,
|
140 |
+
title: str,
|
141 |
+
artist: str,
|
142 |
+
album: str,
|
143 |
+
lyric: str,
|
144 |
+
cover: str,
|
145 |
+
cache: str,
|
146 |
+
):
|
147 |
+
clean_dir(cache)
|
148 |
+
fmt = url.split(".")[-1]
|
149 |
+
local_file = f"{cache}/{id}.{fmt}"
|
150 |
+
response = requests.get(url, stream=True)
|
151 |
+
if response.status_code == 200:
|
152 |
+
total_size = int(response.headers.get("Content-Length", 0)) + 1
|
153 |
+
time_stamp = timestamp()
|
154 |
+
progress_bar = tqdm(
|
155 |
+
total=total_size,
|
156 |
+
unit="B",
|
157 |
+
unit_scale=True,
|
158 |
+
desc=f"[{time_stamp}] {local_file}",
|
159 |
+
)
|
160 |
+
with open(local_file, "wb") as f:
|
161 |
+
for chunk in response.iter_content(chunk_size=8192):
|
162 |
+
if chunk: # 确保 chunk 不为空
|
163 |
+
f.write(chunk) # 更新进度条
|
164 |
+
progress_bar.update(len(chunk))
|
165 |
+
|
166 |
+
return insert_meta(local_file, title, artist, album, lyric, cover)
|
167 |
+
|
168 |
+
else:
|
169 |
+
raise ConnectionError(f"下载失败,状态码:{response.status_code}")
|