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