Skip to content

Commit 472b9d1

Browse files
committed
0.0.1
1 parent 996b939 commit 472b9d1

3 files changed

Lines changed: 266 additions & 2 deletions

File tree

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22
本项目是 [SimMusic](https://github.com/Simsv-Software/SimMusic2024) 的扩展,用于接入 AHA-Music 实现听歌识曲功能。
33

44
## 安装
5-
**开发中,暂不提供下载。**
5+
请确保 SimMusic 主程序版本 >= 0.2.1。
66

77
前往 [最新 Release](https://github.com/simsv-software/SimMusic-Recognition-Ext/releases/latest) 页面,下载 `extension.zip`
88

9-
然后,将它拖入 SimMusic 扩展页面,即可完成安装。
9+
然后,将它拖入 SimMusic 扩展页面,即可完成安装。
10+
11+
## 接口
12+
- 听歌识曲:https://aha-music.com/
13+
- 专辑封面:https://covers.musichoarders.xyz/
14+
15+
非常感谢以上接口提供方,如有侵权,请联系我们删除。

src/index.js

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
2+
3+
const {navbarDiv, pageDiv} = ExtensionFunctions.insertNavigationItem({
4+
pageId: "recognition",
5+
icon: "F298",
6+
text: "识曲",
7+
appendBefore: "downloadPage",
8+
});
9+
10+
ExtensionFunctions.insertStyle(`
11+
#recognition .title{font-size:1.75em;margin-bottom:30px;}
12+
#recognition .title>small{opacity:.8;font-size:.7em;display:block;}
13+
14+
#recognition .source{background:white;border-radius:25px;height:50px;display:flex;align-items:center;padding:5px;margin:35px 0;position:relative;}
15+
#recognition .source>div:not(.indicator){width:130px;text-align:center;transition:color .2s;}
16+
#recognition .source>div:not(.indicator):hover{color:#1E9FFF;}
17+
#recognition .source>div.indicator{position:absolute;top:5px;left:5px;bottom:5px;width:130px;transition:left .2s;color:#1E9FFF;border-radius:20px;background:#DAEFFF;}
18+
#recognition .source>div{display:flex;align-items:center;justify-content:center;}
19+
#recognition .source>div>i{margin-right:5px;margin-top:1px;}
20+
#recognition:not(.idle) .source{pointer-events:none;}
21+
22+
#recognition #recEntryPage{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;}
23+
#recognition #recEntryPage .recognitionBtn{width:200px;height:200px;border-radius:50%;position:relative;transition:all .2s;}
24+
#recognition #recEntryPage .recognitionBtn:active{transform:scale(.95);}
25+
26+
#recognition #recEntryPage .recognitionBtn .animationDots{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;}
27+
#recognition #recEntryPage .recognitionBtn .animationDots>div{width:12px;background:white;height:40px;border-radius:6px;transition:all .2s;margin:0 6px;}
28+
#recognition #recEntryPage .recognitionBtn .text{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:2em;transition:all .2s;opacity:0;}
29+
30+
#recognition.idle #recEntryPage .recognitionBtn .animationDots>div:nth-child(2),#recognition.idle #recEntryPage .recognitionBtn .animationDots>div:nth-child(4){height:70px;}
31+
#recognition.idle #recEntryPage .recognitionBtn .animationDots>div:nth-child(3){height:100px;}
32+
33+
#recognition.recording #recEntryPage .recognitionBtn,#recognition.loading #recEntryPage .recognitionBtn{animation:recBtnAnimation 1s ease infinite;animation-delay:.2s;}
34+
#recognition.recording #recEntryPage .recognitionBtn .animationDots>div,#recognition.loading #recEntryPage .recognitionBtn .animationDots>div:not(.centerDot){animation:recDotAnimation 1s ease-in-out infinite;}
35+
#recognition.recording #recEntryPage .recognitionBtn .animationDots>div:nth-child(2){animation-delay:.1s;}
36+
#recognition.recording #recEntryPage .recognitionBtn .animationDots>div:nth-child(3){animation-delay:.2s;}
37+
#recognition.recording #recEntryPage .recognitionBtn .animationDots>div:nth-child(4){animation-delay:.3s;}
38+
#recognition.recording #recEntryPage .recognitionBtn .animationDots>div:nth-child(5){animation-delay:.4s;}
39+
@keyframes recBtnAnimation{from{box-shadow:0 0 0 0 #1E9FFF33}100%{box-shadow:0 0 0 40px #1E9FFF00}}
40+
@keyframes recDotAnimation{50%{height:80px}100%{height:40px}}
41+
42+
#recognition.loading #recEntryPage .recognitionBtn{filter:none!important;pointer-events:none;}
43+
#recognition.loading #recEntryPage .recognitionBtn .animationDots>div{opacity:0;}
44+
#recognition.loading #recEntryPage .recognitionBtn .text{opacity:1;animation:recTextAnimation 1s linear infinite;animation-delay:.2s;}
45+
@keyframes recTextAnimation{50%{opacity:.7}100%{opacity:1}}
46+
47+
#recognition #recResultPage,#recognition #recEntryPage{transition:all .3s;}
48+
#recognition:not(.result) #recResultPage,#recognition.result #recEntryPage{transform:translateY(50vh);opacity:0;pointer-events:none;}
49+
#recognition #recResultPage{position:absolute;inset:0 0 50px 0;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:left;}
50+
#recognition #recResultPage>img{box-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);width:300px;height:300px;border-radius:10px;}
51+
#recognition #recResultPage .title>i{transition:all .2s;}
52+
#recognition #recResultPage .title>i:hover{color:#1E9FFF;}
53+
#recognition #recResultPage .title>i:active{color:#1E9FFF;opacity:.8;}
54+
#recognition #recResultPage .songInfo{width:300px;margin:20px 0 25px 0;}
55+
#recognition #recResultPage .songInfo>*{display:block;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;width:100%;}
56+
#recognition #recResultPage .songInfo>b{font-size:1.3em;}
57+
#recognition #recResultPage .songInfo>div{font-size:.9em;opacity:.8;}
58+
`);
59+
60+
pageDiv.innerHTML = `
61+
<div id="recEntryPage">
62+
<div class="title">听歌识曲<small>识别您正在播放的歌曲</small></div>
63+
<button class="recognitionBtn">
64+
<div class="animationDots"><div></div><div></div><div></div><div></div><div></div></div>
65+
<div class="text">正在识别</div>
66+
</button>
67+
<div class="source">
68+
<div class="indicator"></div>
69+
<div><i>&#xEBCA;</i>系统音频</div>
70+
<div><i>&#xEF4E;</i>环境音频</div>
71+
</div>
72+
</div>
73+
<div id="recResultPage">
74+
<div class="title"><i>&#xEA58;</i> 识别结果</div>
75+
<img src="assets/placeholder.svg">
76+
<div>
77+
<div class="songInfo">
78+
<b class="musicTitle">未知曲目</b>
79+
<div>
80+
<i>&#xF264;</i> <span class="artist">未知艺术家</span>&nbsp;&nbsp;&nbsp;
81+
<i>&#xEA1F;</i> <span class="album">未知专辑</span>
82+
</div>
83+
</div>
84+
</div>
85+
<div class="buttonGroup">
86+
<button class="searchBtn"><i>&#xF0D1;</i> 在「<span class="searchSource"></span>」搜索</button>
87+
<button class="sub square searchOptionBtn"><i>&#xEA4D;</i></button>
88+
</div>
89+
</div>
90+
`;
91+
92+
93+
94+
95+
let recSource = 0;
96+
let recRecording = false;
97+
let mediaRecorder,stream,audioChunks;
98+
function recSwitchSource (id) {
99+
recSource = id;
100+
const indicator = pageDiv.querySelector(".source>div.indicator");
101+
indicator.style.left = `${id * 130 + 5}px`;
102+
setTimeout(() => {indicator.innerHTML = pageDiv.querySelector(`.source>div:nth-child(${id + 2})`).innerHTML;}, 150);
103+
};
104+
recSwitchSource(0);
105+
pageDiv.querySelector(`.source>div:nth-child(2)`).onclick = () => {recSwitchSource(0);};
106+
pageDiv.querySelector(`.source>div:nth-child(3)`).onclick = () => {recSwitchSource(1);};
107+
function resetStatus() {
108+
pageDiv.classList.add("idle");
109+
pageDiv.classList.remove("recording");
110+
pageDiv.classList.remove("loading");
111+
pageDiv.classList.remove("result");
112+
recRecording = false;
113+
try {
114+
mediaRecorder.stop();
115+
stream.getTracks().forEach(track => track.stop());
116+
} catch {}
117+
}
118+
resetStatus();
119+
pageDiv.querySelector(".title>i").onclick = resetStatus;
120+
121+
pageDiv.querySelector(".recognitionBtn").onclick = async () => {
122+
if (!recRecording) {
123+
pageDiv.classList.remove("idle");
124+
pageDiv.classList.add("recording");
125+
if (!document.getElementById("audio").paused) SimAPControls.togglePlay();
126+
recRecording = true;
127+
// 开始录音逻辑
128+
try {
129+
// 说明:getDisplayMedia无法设置只读音频源(video must be requested) 只能拉小视频码率
130+
stream = recSource ? await navigator.mediaDevices.getUserMedia({audio: true, video: false}) : await navigator.mediaDevices.getDisplayMedia({audio: true});
131+
audioChunks = [];
132+
mediaRecorder = new MediaRecorder(stream, {mimeType: "video/mp4", videoBitsPerSecond: 1});
133+
mediaRecorder.ondataavailable = e => { if (e.data.size > 0) audioChunks.push(e.data); };
134+
mediaRecorder.start();
135+
} catch {
136+
alert("录音时出现错误,请检查相关权限配置。");
137+
resetStatus();
138+
}
139+
} else {
140+
pageDiv.classList.remove("recording");
141+
pageDiv.classList.add("loading");
142+
recRecording = false;
143+
// 停止录音逻辑
144+
try {
145+
mediaRecorder.onstop = async () => {
146+
const audioBlob = new Blob(audioChunks, { type: "video/mp4" });
147+
const audioFile = new File([audioBlob], "recording.mp4", { type: "video/mp4" });
148+
const formData = new FormData();
149+
formData.append("file", audioFile, "recording.mp4");
150+
const res = await fetch("https://api.doreso.com/upload", { method: "POST", body: formData });
151+
if (res.ok) {
152+
const result = await res.json();
153+
queryResult(result.data.id);
154+
} else {
155+
alert(`今日调用过于频繁 (${res.status}),请稍后再试。`);
156+
resetStatus();
157+
}
158+
};
159+
mediaRecorder.stop();
160+
} catch {
161+
alert("出现未知错误,请稍后再试。");
162+
resetStatus();
163+
} finally {
164+
stream.getTracks().forEach(track => track.stop());
165+
}
166+
}
167+
};
168+
169+
async function queryResult(id) {
170+
const res = await fetch(`https://api.doreso.com/file/${id}`);
171+
const json = await res.json();
172+
if (!json?.data[0]?.results) {
173+
if (json?.data[0]?.state == 0) return setTimeout(() => {queryResult(id)}, 500);
174+
else {
175+
alert("暂未匹配到相似的曲目。");
176+
return resetStatus();
177+
}
178+
}
179+
const title = json.data[0].results?.music[0]?.result?.title;
180+
const artist = json.data[0].results?.music[0]?.result?.artists?.map(item => item.name).join(", ") ?? "未知艺术家";
181+
const album = json.data[0].results?.music[0]?.result?.album?.name ?? "未知专辑";
182+
if (!title) {
183+
alert("暂未匹配到相似的曲目。");
184+
return resetStatus();
185+
}
186+
pageDiv.querySelector(".musicTitle").innerText = title;
187+
pageDiv.querySelector(".artist").innerText = artist;
188+
pageDiv.querySelector(".album").innerText = album;
189+
pageDiv.querySelector("#recResultPage>img").src = "assets/placeholder.svg";
190+
pageDiv.classList.add("result");
191+
searchKeyword = `${title} - ${artist}`
192+
const data = {
193+
"artist": artist,
194+
"album": album,
195+
"country": "cn",
196+
"sources": [ "kugou" ],
197+
};
198+
fetch("https://covers.musichoarders.xyz/api/search", {
199+
method: "POST",
200+
headers: { "Content-Type": "application/json", },
201+
body: JSON.stringify(data),
202+
})
203+
.then(res => res.text())
204+
.then(text => {
205+
let imageFound;
206+
text.split("\n").forEach(line => {
207+
if (imageFound) return;
208+
try {
209+
let json = JSON.parse(line);
210+
if (json.type == "cover") {
211+
pageDiv.querySelector("#recResultPage>img").src = json.bigCoverUrl;
212+
imageFound = true;
213+
}
214+
} catch {}
215+
});
216+
});
217+
}
218+
219+
220+
221+
let currentSource,searchKeyword;
222+
function loadSearchList() {
223+
ExtensionRuntime.getExtData().then(extData => {
224+
let sources = {};
225+
currentSource = config.getItem("recognition.searchSource");
226+
for (const name in ExtensionConfig) {
227+
if (ExtensionConfig[name].search) sources[name] = extData[name].uiName;
228+
}
229+
if (!sources[currentSource]) currentSource = Object.keys(sources)[0];
230+
pageDiv.querySelector(".searchSource").innerText = sources[currentSource];
231+
let menuItems = [];
232+
for (const name in sources) {
233+
menuItems.push({
234+
label: sources[name],
235+
icon: currentSource == name ? "EB7B" : " ",
236+
click() { config.setItem("recognition.searchSource", name); loadSearchList(); },
237+
});
238+
}
239+
pageDiv.querySelector(".searchOptionBtn").onclick = e => {
240+
new ContextMenu(menuItems).popup([e.clientX, e.clientY]);
241+
};
242+
});
243+
}
244+
loadSearchList();
245+
pageDiv.querySelector(".searchBtn").onclick = async () => {
246+
await Search.switchSearch();
247+
document.getElementById("searchSource").value = currentSource;
248+
document.getElementById("searchInput").value = searchKeyword;
249+
Search.submit();
250+
}

src/manifest.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extName": "听歌识曲",
3+
"entries":[
4+
"index.js"
5+
],
6+
"packageId":"recognition",
7+
"version":"0.0.1"
8+
}

0 commit comments

Comments
 (0)