From 6fa4f1a72ebb700e4e8f0d63c64dcfaf9bec3279 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=8F=B2=E6=82=A6?=
Date: Wed, 7 Jan 2026 17:10:32 +0800
Subject: [PATCH] =?UTF-8?q?feat(=E6=92=AD=E6=94=BE):=20=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E6=94=AF=E6=8C=81=E7=9B=B4=E6=8E=A5=E4=BD=BF=E7=94=A8playUrl?=
=?UTF-8?q?=E6=92=AD=E6=94=BE=E6=AD=8C=E6=9B=B2=E7=9A=84=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在SearchResultTable和SearchResultList组件中添加playTheSongWithPlayUrl方法
- 修改播放逻辑,优先使用playUrl进行播放
- 在Mobile.vue中添加playUrl处理逻辑
- 新增tunehub服务的getSongInfo和searchSongs接口
- 重构歌曲搜索处理逻辑,使用tunehub服务替代原有实现
---
backend/src/handler/songs.js | 112 ++++++++++++++----
backend/src/service/music_platform/tunehub.js | 36 ++++++
frontend/src/Mobile.vue | 7 ++
.../components/SearchResultListForMobile.vue | 69 ++++++++---
frontend/src/components/SearchResultTable.vue | 49 ++++++--
frontend/src/views/mobile/Home.vue | 9 ++
frontend/src/views/mobile/Playlist.vue | 1 +
frontend/src/views/pc/Home.vue | 9 ++
frontend/src/views/pc/Playlist.vue | 1 +
9 files changed, 242 insertions(+), 51 deletions(-)
diff --git a/backend/src/handler/songs.js b/backend/src/handler/songs.js
index b168f90..4bb05c9 100644
--- a/backend/src/handler/songs.js
+++ b/backend/src/handler/songs.js
@@ -1,8 +1,51 @@
-const logger = require('consola');
-const { searchSongsWithKeyword, searchSongsWithSongMeta } = require('../service/search_songs');
const { getPlayUrlWithOptions } = require('../service/songs_info');
-const { getMetaWithUrl } = require('../service/media_fetcher');
const { matchUrlFromStr } = require('../utils/regex');
+const { searchSongs, getSongInfo, buildSongUrl } = require('../service/music_platform/tunehub');
+const configManager = require('../service/config_manager');
+
+function buildPageUrl(source, songId) {
+ if (!source || !songId) {
+ return '';
+ }
+ if (source === 'netease') {
+ return `https://music.163.com/song?id=${songId}`;
+ }
+ if (source === 'qq') {
+ return `https://y.qq.com/n/ryqq/songDetail/${songId}`;
+ }
+ return '';
+}
+
+function mapTunehubResult(item) {
+ const playUrl = item.url || buildSongUrl(item.platform, item.id);
+ const pageUrl = buildPageUrl(item.platform, item.id);
+ return {
+ songName: item.name || '',
+ artist: item.artist || '',
+ album: item.album || '',
+ duration: 0,
+ url: pageUrl || playUrl || '',
+ playUrl: playUrl || '',
+ pageUrl: pageUrl || '',
+ coverUrl: item.pic || '',
+ resourceForbidden: false,
+ source: item.platform || '',
+ fromMusicPlatform: true,
+ score: 0,
+ };
+}
+
+function parseNeteaseSongId(url) {
+ const match = url.match(/song\\?id=(\\d+)/);
+ if (match && match[1]) {
+ return match[1];
+ }
+ const altMatch = url.match(/\\bid=(\\d+)/);
+ if (altMatch && altMatch[1]) {
+ return altMatch[1];
+ }
+ return '';
+}
async function search(req, res) {
const query = req.query;
@@ -16,31 +59,50 @@ async function search(req, res) {
});
return;
}
- let songs = [];
+ let keyword = keywordOrUrl;
const url = matchUrlFromStr(keywordOrUrl);
- if (!url) {
- songs = await searchSongsWithKeyword(keywordOrUrl);
- } else {
- const songMeta = await getMetaWithUrl(url);
- if (!songMeta) {
- res.send({
- status: 2,
- message: "can not get song meta with this url",
- });
- return;
+ if (url) {
+ const neteaseId = parseNeteaseSongId(url);
+ if (neteaseId) {
+ const info = await getSongInfo('netease', neteaseId);
+ if (info && info.name && info.artist) {
+ keyword = `${info.name} ${info.artist}`;
+ }
}
- songs = await searchSongsWithSongMeta({
- songName: songMeta.songName,
- artist: songMeta.artist,
- album: songMeta.album,
- duration: songMeta.duration,
- }, {
- expectArtistAkas: [],
- allowSongsJustMatchDuration: true,
- allowSongsNotMatchMeta: true,
- });
}
+ const limit = query.limit ? parseInt(query.limit, 10) : 20;
+ const requestSource = query.source || '';
+ const searchData = await searchSongs(keyword, {
+ source: requestSource,
+ limit: Number.isNaN(limit) ? 20 : limit,
+ aggregate: !requestSource,
+ });
+ if (searchData === false || !searchData.results) {
+ res.send({
+ status: 0,
+ data: {
+ songs: [],
+ }
+ });
+ return;
+ }
+
+ const globalConfig = await configManager.getGlobalConfig();
+ const enabledSources = globalConfig && Array.isArray(globalConfig.sources) ? globalConfig.sources : [];
+ const songs = searchData.results
+ .filter(item => {
+ if (!item.platform) {
+ return false;
+ }
+ if (enabledSources.length === 0) {
+ return true;
+ }
+ return enabledSources.includes(item.platform);
+ })
+ .map(mapTunehubResult)
+ .filter(song => song.songName.length > 0);
+
res.send({
status: 0,
data: {
@@ -73,4 +135,4 @@ async function getPlayUrl(req, res) {
module.exports = {
search: search,
getPlayUrl: getPlayUrl
-}
\ No newline at end of file
+}
diff --git a/backend/src/service/music_platform/tunehub.js b/backend/src/service/music_platform/tunehub.js
index 0c23658..0f4a565 100644
--- a/backend/src/service/music_platform/tunehub.js
+++ b/backend/src/service/music_platform/tunehub.js
@@ -34,6 +34,40 @@ async function getPlaylistDetail(source, playlistId) {
return response.data;
}
+async function getSongInfo(source, songId) {
+ const response = await fetchJson({
+ source,
+ id: songId,
+ type: 'info',
+ });
+ if (!response || response.code !== 200 || !response.data) {
+ return false;
+ }
+ return response.data;
+}
+
+async function searchSongs(keyword, {
+ source = '',
+ limit = 20,
+ aggregate = true,
+} = {}) {
+ const params = {
+ keyword,
+ limit,
+ };
+ if (aggregate) {
+ params.type = 'aggregateSearch';
+ } else {
+ params.type = 'search';
+ params.source = source;
+ }
+ const response = await fetchJson(params);
+ if (!response || response.code !== 200 || !response.data) {
+ return false;
+ }
+ return response.data;
+}
+
function buildSongUrl(source, songId, br) {
const params = {
source,
@@ -48,5 +82,7 @@ function buildSongUrl(source, songId, br) {
module.exports = {
getPlaylistDetail,
+ getSongInfo,
+ searchSongs,
buildSongUrl,
};
diff --git a/frontend/src/Mobile.vue b/frontend/src/Mobile.vue
index 3f60c58..fccfc25 100644
--- a/frontend/src/Mobile.vue
+++ b/frontend/src/Mobile.vue
@@ -187,6 +187,13 @@ export default {
}
playOption.playUrl = playUrlRet.data.playUrl;
}
+ if (playOption.playUrl) {
+ playOption.playUrl = getProperPlayUrl(
+ playOption.source,
+ playOption.playUrl,
+ playOption.pageUrl
+ );
+ }
this.songInfos.push(playOption);
this.currentSongIndex = this.songInfos.length - 1;
this.changedTime = new Date().getTime();
diff --git a/frontend/src/components/SearchResultListForMobile.vue b/frontend/src/components/SearchResultListForMobile.vue
index 016fc16..125bc14 100644
--- a/frontend/src/components/SearchResultListForMobile.vue
+++ b/frontend/src/components/SearchResultListForMobile.vue
@@ -9,7 +9,7 @@
-
+
{
props.playTheSong(songMeta, pageUrl, suggestMatchSongId);
};
+ const playTheSongWithPlayUrl = (playOption) => {
+ props.playTheSongWithPlayUrl && props.playTheSongWithPlayUrl(playOption);
+ };
const showPopover = ref([]);
return {
playTheSong,
+ playTheSongWithPlayUrl,
showPopover,
ellipsis,
};
@@ -158,6 +166,12 @@ export default {
},
},
methods: {
+ getSourceUrl(item) {
+ if (!item) {
+ return "";
+ }
+ return item.pageUrl || item.url || "";
+ },
async uploadToCloud(pageUrl) {
const ret = await createSyncSongFromUrlJob(
pageUrl,
@@ -180,37 +194,60 @@ export default {
startTaskListener(ret.data.jobId);
}
},
- play(songMeta, pageUrl, index) {
+ play(item, index) {
if (this.currentSongIndex === index) {
return;
}
this.currentSongIndex = index;
- this.playTheSong(songMeta, pageUrl, this.suggestMatchSongId);
+ const pageUrl = this.getSourceUrl(item);
+ if (this.playTheSongWithPlayUrl && item && item.playUrl) {
+ this.playTheSongWithPlayUrl({
+ playUrl: item.playUrl,
+ coverUrl: item.coverUrl,
+ songName: item.songName,
+ artist: item.artist,
+ pageUrl,
+ source: item.source,
+ });
+ return;
+ }
+ this.playTheSong(null, pageUrl, this.suggestMatchSongId);
},
async onSelect(actionItem) {
const currentSong = this.searchResult[actionItem.songIndex];
+ const pageUrl = this.getSourceUrl(currentSong);
console.log(currentSong);
switch (actionItem.action) {
case ActionUpload:
- this.uploadToCloud(currentSong.url);
+ this.uploadToCloud(pageUrl);
break;
case ActionDownloadToLocalService:
- this.downloadToLocalService(currentSong.url);
+ this.downloadToLocalService(pageUrl);
break;
case ActionDownload:
- const ret = await getSongsMeta({ url: currentSong.url });
- const info = ret.data.songMeta;
- console.log(ret);
- const a = document.createElement("a");
- a.href = info.audios[0].url;
- a.download = `${currentSong.songName}-${currentSong.artist}.mp3`;
- a.style.display = "none";
- document.body.appendChild(a);
- a.click();
- a.remove();
+ if (currentSong.playUrl) {
+ const a = document.createElement("a");
+ a.href = currentSong.playUrl;
+ a.download = `${currentSong.songName}-${currentSong.artist}.mp3`;
+ a.style.display = "none";
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ } else {
+ const ret = await getSongsMeta({ url: pageUrl });
+ const info = ret.data.songMeta;
+ console.log(ret);
+ const a = document.createElement("a");
+ a.href = info.audios[0].url;
+ a.download = `${currentSong.songName}-${currentSong.artist}.mp3`;
+ a.style.display = "none";
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ }
break;
case ActionOpenRef:
- window.open(currentSong.url, "_blank").focus();
+ window.open(pageUrl || currentSong.url, "_blank").focus();
break;
}
},
diff --git a/frontend/src/components/SearchResultTable.vue b/frontend/src/components/SearchResultTable.vue
index b516a8a..42ee60e 100644
--- a/frontend/src/components/SearchResultTable.vue
+++ b/frontend/src/components/SearchResultTable.vue
@@ -73,7 +73,7 @@
@@ -113,7 +113,7 @@
@@ -126,7 +126,7 @@
type="warning"
circle
class="operation-btn"
- @click="openSourceUrl(scope.row.url)"
+ @click="openSourceUrl(scope.row)"
>
@@ -229,6 +229,10 @@ export default {
type: Function,
required: true,
},
+ playTheSongWithPlayUrl: {
+ type: Function,
+ required: false,
+ },
abortTheSong: {
type: Function,
required: true,
@@ -250,11 +254,15 @@ export default {
const playTheSong = (songMeta, pageUrl, suggestMatchSongId) => {
props.playTheSong(songMeta, pageUrl, suggestMatchSongId);
};
+ const playTheSongWithPlayUrl = (playOption) => {
+ props.playTheSongWithPlayUrl && props.playTheSongWithPlayUrl(playOption);
+ };
const abortTheSong = () => {
props.abortTheSong();
};
return {
playTheSong,
+ playTheSongWithPlayUrl,
abortTheSong,
};
},
@@ -267,7 +275,14 @@ export default {
},
},
methods: {
- async uploadToCloud(pageUrl) {
+ getSourceUrl(row) {
+ if (!row) {
+ return "";
+ }
+ return row.pageUrl || row.url || "";
+ },
+ async uploadToCloud(row) {
+ const pageUrl = this.getSourceUrl(row);
const ret = await createSyncSongFromUrlJob(
pageUrl,
this.suggestMatchSongId
@@ -278,7 +293,8 @@ export default {
startTaskListener(ret.data.jobId);
}
},
- async downloadToLocalService(pageUrl) {
+ async downloadToLocalService(row) {
+ const pageUrl = this.getSourceUrl(row);
const ret = await createDownloadSongFromUrlJob(
pageUrl,
this.suggestMatchSongId
@@ -295,15 +311,28 @@ export default {
this.globalConfig = globalConfig.data;
}
},
- play(songMeta, pageUrl) {
- this.currentSongUrl = pageUrl;
- this.playTheSong(songMeta, pageUrl, this.suggestMatchSongId);
+ play(row) {
+ const pageUrl = this.getSourceUrl(row);
+ this.currentSongUrl = row && row.url ? row.url : pageUrl;
+ if (this.playTheSongWithPlayUrl && row && row.playUrl) {
+ this.playTheSongWithPlayUrl({
+ playUrl: row.playUrl,
+ coverUrl: row.coverUrl,
+ songName: row.songName,
+ artist: row.artist,
+ pageUrl,
+ source: row.source,
+ });
+ return;
+ }
+ this.playTheSong(null, pageUrl, this.suggestMatchSongId);
},
abort() {
this.currentSongUrl = -1;
this.abortTheSong();
},
- openSourceUrl(url) {
+ openSourceUrl(row) {
+ const url = this.getSourceUrl(row);
try {
if (typeof window !== "undefined" && window?.open) {
window.open(url, "_blank", "noopener,noreferrer");
diff --git a/frontend/src/views/mobile/Home.vue b/frontend/src/views/mobile/Home.vue
index 4eff0e3..c920dab 100644
--- a/frontend/src/views/mobile/Home.vue
+++ b/frontend/src/views/mobile/Home.vue
@@ -128,6 +128,7 @@
@@ -412,6 +413,10 @@ export default {
type: Function,
required: true,
},
+ playTheSongWithPlayUrl: {
+ type: Function,
+ required: false,
+ },
},
mounted() {
this.wyAccount = storage.get("wyAccount");
@@ -425,8 +430,12 @@ export default {
const playTheSong = (songMeta, pageUrl) => {
props.playTheSong(songMeta, pageUrl);
};
+ const playTheSongWithPlayUrl = (playOption) => {
+ props.playTheSongWithPlayUrl && props.playTheSongWithPlayUrl(playOption);
+ };
return {
playTheSong,
+ playTheSongWithPlayUrl,
ellipsis,
};
},
diff --git a/frontend/src/views/mobile/Playlist.vue b/frontend/src/views/mobile/Playlist.vue
index 67f63c3..7d129c2 100644
--- a/frontend/src/views/mobile/Playlist.vue
+++ b/frontend/src/views/mobile/Playlist.vue
@@ -18,6 +18,7 @@
diff --git a/frontend/src/views/pc/Home.vue b/frontend/src/views/pc/Home.vue
index 6dfa6af..a898602 100644
--- a/frontend/src/views/pc/Home.vue
+++ b/frontend/src/views/pc/Home.vue
@@ -215,6 +215,7 @@
{
props.playTheSong(songMeta, pageUrl, suggestMatchSongId);
};
+ const playTheSongWithPlayUrl = (playOption) => {
+ props.playTheSongWithPlayUrl && props.playTheSongWithPlayUrl(playOption);
+ };
const abortTheSong = () => {
props.abortTheSong();
};
return {
abortTheSong,
playTheSong,
+ playTheSongWithPlayUrl,
};
},
components: {
diff --git a/frontend/src/views/pc/Playlist.vue b/frontend/src/views/pc/Playlist.vue
index 65b660a..b093b63 100644
--- a/frontend/src/views/pc/Playlist.vue
+++ b/frontend/src/views/pc/Playlist.vue
@@ -28,6 +28,7 @@