feat(播放): 添加支持直接使用playUrl播放歌曲的功能

- 在SearchResultTable和SearchResultList组件中添加playTheSongWithPlayUrl方法
- 修改播放逻辑,优先使用playUrl进行播放
- 在Mobile.vue中添加playUrl处理逻辑
- 新增tunehub服务的getSongInfo和searchSongs接口
- 重构歌曲搜索处理逻辑,使用tunehub服务替代原有实现
This commit is contained in:
史悦
2026-01-07 17:10:32 +08:00
parent 0dbb36be9d
commit 6fa4f1a72e
9 changed files with 242 additions and 51 deletions

View File

@@ -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
}
}

View File

@@ -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,
};