feat(播放): 添加支持直接使用playUrl播放歌曲的功能
- 在SearchResultTable和SearchResultList组件中添加playTheSongWithPlayUrl方法 - 修改播放逻辑,优先使用playUrl进行播放 - 在Mobile.vue中添加playUrl处理逻辑 - 新增tunehub服务的getSongInfo和searchSongs接口 - 重构歌曲搜索处理逻辑,使用tunehub服务替代原有实现
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<van-col span="23" offset="1">
|
||||
<van-row>
|
||||
<van-col span="22">
|
||||
<van-row @click="play(null, item.url, i)">
|
||||
<van-row @click="play(item, i)">
|
||||
<van-col style="font-size: 16px">
|
||||
<i
|
||||
v-if="item.resourceForbidden"
|
||||
@@ -115,6 +115,10 @@ export default {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
playTheSongWithPlayUrl: {
|
||||
type: Function,
|
||||
required: false,
|
||||
},
|
||||
suggestMatchSongId: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -137,11 +141,15 @@ export default {
|
||||
const playTheSong = (songMeta, pageUrl, suggestMatchSongId) => {
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
<el-tooltip content="播放歌曲" placement="top" v-else>
|
||||
<el-button
|
||||
@click="play(null, scope.row.url)"
|
||||
@click="play(scope.row)"
|
||||
type="primary"
|
||||
circle
|
||||
:disabled="scope.row.url.indexOf('youtube') >= 0"
|
||||
@@ -94,7 +94,7 @@
|
||||
<el-button
|
||||
type="success"
|
||||
circle
|
||||
@click="uploadToCloud(scope.row.url)"
|
||||
@click="uploadToCloud(scope.row)"
|
||||
:disabled="!wyAccount"
|
||||
class="operation-btn"
|
||||
>
|
||||
@@ -113,7 +113,7 @@
|
||||
<el-button
|
||||
type="primary"
|
||||
circle
|
||||
@click="downloadToLocalService(scope.row.url)"
|
||||
@click="downloadToLocalService(scope.row)"
|
||||
:disabled="!globalConfig.downloadPathExisted"
|
||||
class="operation-btn"
|
||||
>
|
||||
@@ -126,7 +126,7 @@
|
||||
type="warning"
|
||||
circle
|
||||
class="operation-btn"
|
||||
@click="openSourceUrl(scope.row.url)"
|
||||
@click="openSourceUrl(scope.row)"
|
||||
>
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
</el-button>
|
||||
@@ -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");
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
<van-row style="margin-top: 10px">
|
||||
<SearchResultList
|
||||
:playTheSong="playTheSong"
|
||||
:playTheSongWithPlayUrl="playTheSongWithPlayUrl"
|
||||
:suggestMatchSongId="suggestMatchSongId"
|
||||
:searchResult="searchResult"
|
||||
>
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<van-row style="margin-top: 30px" v-if="searchResult.length > 0">
|
||||
<SearchResultList
|
||||
:playTheSong="playTheSong"
|
||||
:playTheSongWithPlayUrl="playTheSongWithPlayUrl"
|
||||
:suggestMatchSongId="suggestMatchSongId"
|
||||
:searchResult="searchResult"
|
||||
>
|
||||
|
||||
@@ -215,6 +215,7 @@
|
||||
<el-row v-if="searchResult.length > 0" class="search-result-container">
|
||||
<SearchResultTable
|
||||
:playTheSong="playTheSong"
|
||||
:playTheSongWithPlayUrl="playTheSongWithPlayUrl"
|
||||
:abortTheSong="abortTheSong"
|
||||
:suggestMatchSongId="suggestMatchSongId"
|
||||
:searchResult="searchResult"
|
||||
@@ -269,6 +270,10 @@ export default {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
playTheSongWithPlayUrl: {
|
||||
type: Function,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.wyAccount = storage.get("wyAccount");
|
||||
@@ -286,12 +291,16 @@ 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 {
|
||||
abortTheSong,
|
||||
playTheSong,
|
||||
playTheSongWithPlayUrl,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
</p>
|
||||
<SearchResultTable
|
||||
:playTheSong="playTheSong"
|
||||
:playTheSongWithPlayUrl="playTheSongWithPlayUrl"
|
||||
:abortTheSong="abortTheSong"
|
||||
:suggestMatchSongId="suggestMatchSongId"
|
||||
:searchResult="searchResult"
|
||||
|
||||
Reference in New Issue
Block a user