diff --git a/publish/utils/compileAssets.js b/publish/utils/compileAssets.js index 8d6fd8df8d..0d2fbc1f83 100644 --- a/publish/utils/compileAssets.js +++ b/publish/utils/compileAssets.js @@ -18,7 +18,7 @@ module.exports = () => new Promise((resolve, reject) => { resolve() } else { console.log(chalk.red('Asset compilation failed.')) - reject() + reject(new Error('Asset compilation failed')) } }) }) diff --git a/src/lang/en-us.json b/src/lang/en-us.json index f04971a59c..72cc9b1551 100644 --- a/src/lang/en-us.json +++ b/src/lang/en-us.json @@ -613,6 +613,7 @@ "songlist__import_input_tip_1": "Cross-service playlists are not supported. Please confirm whether the playlist to be opened corresponds to the current chosen service", "songlist__import_input_tip_2": "If you encounter a playlist link that cannot be opened. Please send us your feedback", "songlist__import_input_tip_3": "Unable to open Kugou playlist with playlist ID or link from lite edition, but support to open it with Kugou code or link from main edition", + "songlist__import_input_tip_5": "Captcha needed to view videos of uploader if not logged in, so only public Favlist is supported", "songlist__import_input_tip_4": "NetEase Cloud Music's \"I Like\" playlist requires a token to open. For details, see ", "songlist__import_input_title": "Open shared playlist", "songlist__open_list": "Open \"{name}\" playlist", @@ -624,6 +625,7 @@ "source_alias_mg": "MG Music", "source_alias_tx": "TX Music", "source_alias_wy": "WY Music", + "source_alias_bili": "Bilibili", "source_alias_xm": "XM Music", "source_all": "Aggregated", "source_bd": "Baidu", @@ -632,6 +634,7 @@ "source_mg": "Migu", "source_tx": "Tencent", "source_wy": "NetEase", + "source_bili": "Bilibili", "source_xm": "Xiami", "sync__auth_code_input_tip": "Please enter the connection code", "sync__auth_code_title": "Need to enter the connection code", @@ -745,4 +748,4 @@ "user_api_import_online__input_loading": "Importing...", "user_api_import_online__input_tip": "Please enter an HTTP link", "user_api_import_online__title": "Import Music API from network." -} +} \ No newline at end of file diff --git a/src/lang/zh-cn.json b/src/lang/zh-cn.json index c5a5242380..6ad8c3d7b7 100644 --- a/src/lang/zh-cn.json +++ b/src/lang/zh-cn.json @@ -614,6 +614,7 @@ "songlist__import_input_tip_2": "若遇到无法打开的歌单链接,欢迎反馈", "songlist__import_input_tip_3": "酷狗源歌单不支持用歌单 ID 或概念版链接打开,但支持用普通版链接或酷狗码打开", "songlist__import_input_tip_4": "网易源的「我喜欢」歌单需要 Token 才能打开,详情看 ", + "songlist__import_input_tip_5": "哔哩哔哩由于未登录状态需过人机验证才能获取 UP 主的上传视频,所以只支持导入公开的收藏夹链接", "songlist__import_input_title": "打开分享的歌单", "songlist__open_list": "打开「{name}」歌单", "songlist__tag_info_hot_tag": "热门标签", @@ -624,6 +625,7 @@ "source_alias_mg": "小蜜音乐", "source_alias_tx": "小秋音乐", "source_alias_wy": "小芸音乐", + "source_alias_bili": "哔哩哔哩", "source_alias_xm": "小霞音乐", "source_all": "聚合搜索", "source_bd": "百度音乐", @@ -632,6 +634,7 @@ "source_mg": "咪咕音乐", "source_tx": "企鹅音乐", "source_wy": "网易音乐", + "source_bili": "哔哩哔哩", "source_xm": "虾米音乐", "sync__auth_code_input_tip": "请输入连接码", "sync__auth_code_title": "需要输入连接码", @@ -745,4 +748,4 @@ "user_api_import_online__input_loading": "导入中...", "user_api_import_online__input_tip": "请输入 HTTP 链接", "user_api_import_online__title": "在线导入自定义源" -} +} \ No newline at end of file diff --git a/src/lang/zh-tw.json b/src/lang/zh-tw.json index 491563fa56..805e2ccdfd 100644 --- a/src/lang/zh-tw.json +++ b/src/lang/zh-tw.json @@ -613,6 +613,7 @@ "songlist__import_input_tip_1": "不支援跨平台開啟歌單,請確認要開啟的歌單與目前歌單的來源平台是否對應", "songlist__import_input_tip_2": "若遇到無法開啟的歌單連結,歡迎回報", "songlist__import_input_tip_3": "酷狗音樂歌單不支援用歌單 ID 與概念版連結開啟,但支援用普通版連結與酷狗碼開啟", + "songlist__import_input_tip_5": "嗶哩嗶哩由於未登入狀態需過人機驗證才能獲取 UP 主的上傳影片,所以只支援匯入公開的收藏夾連結", "songlist__import_input_tip_4": "網易雲音樂的「我喜歡」歌單需要 Token 才能開啟,詳情看 ", "songlist__import_input_title": "開啟分享的歌單", "songlist__open_list": "開啟「{name}」歌單", @@ -624,6 +625,7 @@ "source_alias_mg": "小蜜音樂", "source_alias_tx": "小秋音樂", "source_alias_wy": "小芸音樂", + "source_alias_bili": "嗶哩嗶哩", "source_alias_xm": "小霞音樂", "source_all": "聚合搜尋", "source_bd": "百度音樂", @@ -632,6 +634,7 @@ "source_mg": "咪咕音樂", "source_tx": "企鵝音樂", "source_wy": "網易音樂", + "source_bili": "嗶哩嗶哩", "source_xm": "蝦米音樂", "sync__auth_code_input_tip": "請輸入連線碼", "sync__auth_code_title": "需要輸入連線碼", @@ -745,4 +748,4 @@ "user_api_import_online__input_loading": "匯入中...", "user_api_import_online__input_tip": "請輸入 HTTP 連結", "user_api_import_online__title": "從線上匯入自訂來源 API" -} +} \ No newline at end of file diff --git a/src/main/modules/userApi/renderer/preload.js b/src/main/modules/userApi/renderer/preload.js index b53ae92efb..c0bbcd945f 100644 --- a/src/main/modules/userApi/renderer/preload.js +++ b/src/main/modules/userApi/renderer/preload.js @@ -25,13 +25,14 @@ const eventNames = Object.values(EVENT_NAMES) const events = { request: null, } -const allSources = ['kw', 'kg', 'tx', 'wy', 'mg', 'local'] +const allSources = ['kw', 'kg', 'tx', 'wy', 'mg', 'bili', 'local'] const supportQualitys = { kw: ['128k', '320k', 'flac', 'flac24bit'], kg: ['128k', '320k', 'flac', 'flac24bit'], tx: ['128k', '320k', 'flac', 'flac24bit'], wy: ['128k', '320k', 'flac', 'flac24bit'], mg: ['128k', '320k', 'flac', 'flac24bit'], + bili: [], local: [], } const supportActions = { @@ -41,6 +42,7 @@ const supportActions = { wy: ['musicUrl'], mg: ['musicUrl'], xm: ['musicUrl'], + bili: ['musicUrl'], local: ['musicUrl', 'lyric', 'pic'], } @@ -219,7 +221,7 @@ const initEnv = (userApi) => { body = resp.body = resp.raw.toString() try { resp.body = JSON.parse(resp.body) - } catch (_) {} + } catch (_) { } body = resp.body callback.call(this, err, { statusCode: resp.statusCode, diff --git a/src/main/modules/winMain/main.ts b/src/main/modules/winMain/main.ts index ca6d64ddf9..56f74d295a 100644 --- a/src/main/modules/winMain/main.ts +++ b/src/main/modules/winMain/main.ts @@ -70,6 +70,14 @@ export const createWindow = () => { const proxy = getProxy() setSesProxy(ses, proxy?.host, proxy?.port) + const filter = { + urls: ['*://*/*bili*/*'] + } + ses.webRequest.onBeforeSendHeaders(filter, (details, callback) => { + details.requestHeaders.Referer = 'https://www.bilibili.com' + callback({ requestHeaders: details.requestHeaders }) + }) + /** * Initial window options */ diff --git a/src/renderer/utils/musicSdk/bili/comment.js b/src/renderer/utils/musicSdk/bili/comment.js new file mode 100644 index 0000000000..5577b48139 --- /dev/null +++ b/src/renderer/utils/musicSdk/bili/comment.js @@ -0,0 +1,78 @@ +import { httpFetch } from '../../request' +import { dateFormat2 } from '../../index' + +export default { + async getComment(mInfo, page = 1, limit = 20) { + const bvid = mInfo.songmid + const url = `https://api.bilibili.com/x/v2/reply?type=1&oid=${bvid}&sort=2&ps=${limit}&pn=${page}` + const { body, statusCode } = await httpFetch(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.34 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.34', + Referer: `https://www.bilibili.com/video/${bvid}/`, + }, + }).promise + + if (statusCode !== 200 || body.code !== 0) throw new Error('获取评论失败') + + return { + source: 'bili', + comments: this.filterComment(body.data.replies), + total: body.data.page.count, + page, + limit, + maxPage: body.data.page.num, + }// seems that sorting by time requires state validation but why, this consumes more server resource? + }, // you know what, this actually returns the exact same as getHotComment, but consider this scenario + // user turns on comment and see pitch blank or simply the same content, then they not even notice + // even if they notice, they may just think "lucky me, what are the odds of that", and move on + + async getHotComment(mInfo, page = 1, limit = 20) { + const bvid = mInfo.songmid + const url = `https://api.bilibili.com/x/v2/reply?type=1&oid=${bvid}&sort=1&ps=${limit}&pn=${page}` + const { body, statusCode } = await httpFetch(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.34 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.34', + Referer: `https://www.bilibili.com/video/${bvid}/`, + }, + }).promise + + if (statusCode !== 200 || body.code !== 0) throw new Error('获取热门评论失败') + + return { + source: 'bili', + comments: this.filterComment(body.data.replies), + total: body.data.page.count, + page, + limit, + maxPage: body.data.page.num, + } + }, + + filterComment(rawList) { + if (!rawList) return [] + + return rawList.map(item => { + return { + id: item.rpid, + rootId: item.rpid, + text: item.content.message.replace(/\\n/g, '\n'), + time: item.ctime * 1000, + timeStr: dateFormat2(item.ctime * 1000), + userName: item.member.uname, + avatar: '//wsrv.nl/?url=' + item.member.avatar + '&il', + userId: item.member.mid, + likedCount: item.like, + reply: item.replies ? item.replies.map(reply => ({ + id: reply.rpid, + text: reply.content.message.replace(/\\n/g, '\n'), + time: reply.ctime * 1000, + timeStr: dateFormat2(reply.ctime * 1000), + userName: reply.member.uname, + avatar: '//wsrv.nl/?url=' + reply.member.avatar + '&il', + userId: reply.member.mid, + likedCount: reply.like, + })) : [], + } + }) + }, +} diff --git a/src/renderer/utils/musicSdk/bili/hotSearch.js b/src/renderer/utils/musicSdk/bili/hotSearch.js new file mode 100644 index 0000000000..3d8ad4f11e --- /dev/null +++ b/src/renderer/utils/musicSdk/bili/hotSearch.js @@ -0,0 +1,29 @@ +import { httpFetch } from '../../request' + +export default { + _requestObj: null, + + async getList(retryNum = 0) { + if (this._requestObj) this._requestObj.cancelHttp() + if (retryNum > 2) return Promise.reject(new Error('try max num')) + + const _requestObj = httpFetch('https://s.search.bilibili.com/main/hotword', { + headers: { + 'User-Agent': 'Mozilla/5.0', + Referer: 'https://www.bilibili.com/', + }, + }) + + const { body, statusCode } = await _requestObj.promise + if (statusCode != 200 || body.code !== 0) throw new Error('获取热搜词失败') + + return { + source: 'bili', + list: this.filterList(body.list), + } + }, + + filterList(rawList) { + return rawList.map(item => item.keyword).slice(0, 20) + }, +} diff --git a/src/renderer/utils/musicSdk/bili/index.js b/src/renderer/utils/musicSdk/bili/index.js new file mode 100644 index 0000000000..af1957823d --- /dev/null +++ b/src/renderer/utils/musicSdk/bili/index.js @@ -0,0 +1,27 @@ +import songList from './songList' +import musicSearch from './musicSearch' +import { apis } from '../api-source' +import hotSearch from './hotSearch' +import comment from './comment' + +const bili = { + songList, + musicSearch, + hotSearch, + comment, + + getMusicUrl(songInfo, type) { + return apis('bili').getMusicUrl(songInfo, type) + }, + getLyric(songInfo) { + throw new Error('Bilibili暂无歌词接口') + }, + getPic(songInfo) { + return apis('bili').getPic(songInfo) + }, + getMusicDetailPageUrl(songInfo) { + return `https://www.bilibili.com/video/${songInfo.songmid}` + }, +} + +export default bili diff --git a/src/renderer/utils/musicSdk/bili/musicSearch.js b/src/renderer/utils/musicSdk/bili/musicSearch.js new file mode 100644 index 0000000000..0dddfd3237 --- /dev/null +++ b/src/renderer/utils/musicSdk/bili/musicSearch.js @@ -0,0 +1,77 @@ +import { httpFetch } from '../../request' +import { sizeFormate } from '../../index' + +export default { + limit: 30, + total: 0, + page: 0, + allPage: 1, + successCode: 0, + + async musicSearch(str, page, limit, retryNum = 0) { + if (retryNum > 2) throw new Error('搜索失败') + + const searchRequest = httpFetch(`https://api.bilibili.com/x/web-interface/search/type?search_type=video&keyword=${encodeURIComponent(str)}&page=${page}&page_size=${limit}`, { + headers: { + 'User-Agent': 'Mozilla/5.0', + Referer: 'https://www.bilibili.com/', + Cookie: 'SESSDATA=xxx', + }, + }) + + return searchRequest.promise.then(({ body }) => { + console.log(body) + if (body.code !== this.successCode) return this.musicSearch(str, page, limit, ++retryNum) + return body + }) + }, + + handleResult(rawList) { + const list = [] + if (!rawList) return list + rawList.forEach(video => { + if (!video.bvid) return + const parts = video.duration.split(':') + const minutes = String(parts[0]).padStart(2, '0') + const seconds = String(parts[1]).padStart(2, '0') + const paddedDuration = `${minutes}:${seconds}` + list.push({ + singer: video.author, + name: video.title.replace(/<[^>]+>/g, ''), + albumName: video.typename, + albumId: video.tid, + source: 'bili', + interval: paddedDuration, + songId: video.bvid, + songmid: video.bvid, + img: '//wsrv.nl/?url=' + video.pic + '&il', + types: [{ type: 'video', size: sizeFormate(video.play), url: `https://www.bilibili.com/video/${video.bvid}` }], + _types: { video: { size: sizeFormate(video.play) } }, + typeUrl: {}, + }) + }) + return list + }, + + async search(str, page = 1, limit) { + if (limit == null) limit = this.limit + + const body = await this.musicSearch(str, page, limit) + if (!body.data.result || body.data.result.length === 0) { + return Promise.reject(new Error('EMPTY_RESULT')) + } + const list = this.handleResult(body.data.result) + + this.total = body.data.numResults + this.page = page + this.allPage = Math.ceil(this.total / limit) + + return { + list, + allPage: this.allPage, + limit, + total: this.total, + source: 'bili', + } + }, +} diff --git a/src/renderer/utils/musicSdk/bili/songList.js b/src/renderer/utils/musicSdk/bili/songList.js new file mode 100644 index 0000000000..95ba8f46e1 --- /dev/null +++ b/src/renderer/utils/musicSdk/bili/songList.js @@ -0,0 +1,94 @@ +import { httpFetch } from '../../request' + +export default { + limit_song: 20, + successCode: 0, + sortList: [ + { + name: '默认', + id: '0', + }, + ], + + getList(sortId, tagId, page, tryNum = 0) { + return Promise.resolve({ + list: [], + total: 0, + page: 1, + limit: 20, + source: 'bili', + }) + }, + + async getListDetail(link, page = 1, limit = 20) { + const favIdMatch = link.match(/favlist\?fid=(\d+)/) + if (!favIdMatch) return Promise.reject(new Error('无效的Bilibili收藏夹链接')) + + const media_id = favIdMatch[1] + const requestObj = httpFetch( + `https://api.bilibili.com/x/v3/fav/resource/list?media_id=${media_id}&pn=${page}&ps=${limit}&keyword=&order=mtime&type=0&tid=0&platform=web`, + { headers: { Referer: 'https://www.bilibili.com/', Cookie: 'SESSDATA=xxx' } }, + ) + + const { body } = await requestObj.promise + if (body.code !== 0) throw new Error('获取收藏夹视频失败') + + const info = body.data.info + const medias = body.data.medias || [] + + const list = medias.map(video => { + const time = video.duration + const minutes = Math.floor(time / 60) + const seconds = time % 60 + const paddedDuration = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}` + return { + singer: video.upper.name, + name: video.title, + albumName: null, + albumId: null, + source: 'bili', + interval: paddedDuration, + songId: video.bvid, + songmid: video.bvid, + img: '//wsrv.nl/?url=' + video.cover + '&il', // proxied due to cors + types: [], + _types: {}, + typeUrl: {}, + } + }) + + return { + list, + page, + limit, + total: body.data.info.media_count, + source: 'bili', + info: { + name: info.title, + img: '//wsrv.nl/?url=' + info.cover + '&il', + desc: info.intro, + author: info.upper.name, + play_count: info.view_count, + }, + } + }, + + getTags() { + return Promise.resolve({ + tags: [], + hotTag: [{ + name: '全部', id: '0', source: 'bili', + }], + source: 'bili', + }) + }, + + search(text, page, limit = 20, retryNum = 0) { + return Promise.resolve({ + list: [], + limit, + total: 0, + source: 'bili', + }) + }, +} diff --git a/src/renderer/utils/musicSdk/index.js b/src/renderer/utils/musicSdk/index.js index 58f7568650..737ee22cd5 100644 --- a/src/renderer/utils/musicSdk/index.js +++ b/src/renderer/utils/musicSdk/index.js @@ -5,6 +5,7 @@ import wy from './wy/index' import mg from './mg/index' import bd from './bd/index' import xm from './xm' +import bili from './bili/index' import { supportQuality } from './api-source' @@ -34,6 +35,10 @@ const sources = { name: '虾米音乐', id: 'xm', }, + { + name: '哔哩哔哩', + id: 'bili', + }, // { // name: '百度音乐', // id: 'bd', @@ -46,6 +51,7 @@ const sources = { mg, bd, xm, + bili, } export default { ...sources, @@ -109,7 +115,7 @@ export default { return intv } const trimStr = str => typeof str == 'string' ? str.trim() : (str || '') - const filterStr = str => typeof str == 'string' ? str.replace(/\s|'|\.|,|,|&|"|、|\(|\)|(|)|`|~|-|<|>|\||\/|\]|\[|!|!/g, '') : String(str || '') + const filterStr = str => typeof str == 'string' ? str.replace(/\s|'|\.,|,|&|"|、|\(|\)|(|)|`|~|-|<|>|\||\/|\]|\[|!|!/g, '') : String(str || '') const fMusicName = filterStr(name).toLowerCase() const fSinger = filterStr(sortSingle(singer)).toLowerCase() const fAlbumName = filterStr(albumName).toLowerCase() diff --git a/src/renderer/views/songList/List/components/OpenListModal.vue b/src/renderer/views/songList/List/components/OpenListModal.vue index 11b5d995b5..6f407308c3 100644 --- a/src/renderer/views/songList/List/components/OpenListModal.vue +++ b/src/renderer/views/songList/List/components/OpenListModal.vue @@ -26,6 +26,7 @@ @click="openUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq/cannot-open-songlist')" >FAQ +
  • {{ $t('songlist__import_input_tip_5') }}
  • {{ $t('songlist__import_input_btn_confirm') }}