From 237a869ba9fb33cd1518a7f9f46dce8ad5c88b24 Mon Sep 17 00:00:00 2001 From: stark81 <849966181@qq.com> Date: Thu, 10 Oct 2024 14:09:23 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E6=9B=B4=E6=96=B0=E4=BA=86API=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=EF=BC=8C=E8=A7=A3=E5=86=B3=E4=BA=86=E9=A2=91=E7=B9=81?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E4=BF=AE=E6=94=B9=E5=AF=86=E7=A0=81=E4=BB=A5?= =?UTF-8?q?=E5=8F=8A=E5=B0=81=E5=8F=B7=E9=97=AE=E9=A2=98=EF=BC=9B=202.=20?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E9=9F=B3=E4=B9=90=E7=9A=84=E4=B8=93=E8=BE=91?= =?UTF-8?q?=E3=80=81=E8=89=BA=E4=BA=BA=E4=BF=AE=E6=94=B9=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E8=99=9A=E6=8B=9F=E5=88=97=E8=A1=A8=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E5=87=BA=E7=8E=B0=E6=9C=AC=E5=9C=B0=E6=AD=8C=E6=9B=B2=E8=BF=87?= =?UTF-8?q?=E5=A4=9A=E6=97=B6=E5=AF=BC=E8=87=B4=E9=A1=B5=E9=9D=A2=E5=8D=A1?= =?UTF-8?q?=E9=A1=BF=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=9B=203.=20=E4=BD=BF?= =?UTF-8?q?=E7=94=A8unblock-serve=E6=9D=A5=E6=9B=BF=E6=8D=A2unblock-rust?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E4=B8=8D=E8=A7=A6=E5=8F=91unblock?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- src/background.js | 2 +- src/components/Cover.vue | 12 +- src/components/TrackList.vue | 5 + src/components/VirtualCoverRow.vue | 226 +++++++++++++++++++++++++++++ src/components/VirtualScroll.vue | 77 +++++----- src/electron/ipcMain.js | 179 ++++++++++++----------- src/store/actions.js | 34 +++-- src/utils/Player.js | 2 +- src/views/explore.vue | 2 +- src/views/library.vue | 5 +- src/views/localMusic.vue | 18 ++- vue.config.js | 4 +- yarn.lock | 125 +++++++++++++++- 14 files changed, 538 insertions(+), 156 deletions(-) create mode 100644 src/components/VirtualCoverRow.vue diff --git a/package.json b/package.json index 40c2638..07b83fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "yesplaymusic", - "version": "0.4.16", + "version": "0.4.17", "private": true, "description": "A third party music player for Netease Music", "author": "qier222", @@ -34,6 +34,7 @@ "@fastify/cookie": "^9.3.1", "@fastify/static": "^7.0.4", "@unblockneteasemusic/rust-napi": "^0.4.0", + "@unblockneteasemusic/server": "^0.27.8", "NeteaseCloudMusicApi": "^4.15.6", "axios": "^0.26.1", "change-case": "^4.1.2", diff --git a/src/background.js b/src/background.js index 5c6a4eb..594b124 100644 --- a/src/background.js +++ b/src/background.js @@ -106,7 +106,7 @@ class Background { log('initializing'); // Make sure the app is singleton. - if (release().startsWith("6.1") && type() == 'Windows_NT') + if (release().startsWith('6.1') && type() == 'Windows_NT') app.disableHardwareAcceleration(); if (process.platform === 'win32') app.setAppUserModelId(app.getName()); if (!app.requestSingleInstanceLock()) return app.quit(); diff --git a/src/components/Cover.vue b/src/components/Cover.vue index c1c6694..7b85cfc 100644 --- a/src/components/Cover.vue +++ b/src/components/Cover.vue @@ -2,8 +2,9 @@
@@ -33,6 +34,7 @@ export default { props: { id: { type: Number, required: true }, type: { type: String, required: true }, + matched: { type: Boolean, default: true }, imageUrl: { type: String, required: true }, fixedSize: { type: Number, default: 0 }, playButtonSize: { type: Number, default: 22 }, @@ -73,6 +75,7 @@ export default { }, methods: { play() { + if (!this.matched) return; const player = this.$store.state.player; const playActions = { album: player.playAlbumByID, @@ -96,8 +99,13 @@ export default { } }, goTo() { + if (!this.matched) return; this.$router.push({ name: this.type, params: { id: this.id } }); }, + mouseOper(focus) { + if (!this.matched) return; + this.focus = focus; + }, }, }; diff --git a/src/components/TrackList.vue b/src/components/TrackList.vue index b2aa6c6..1f7a4ef 100644 --- a/src/components/TrackList.vue +++ b/src/components/TrackList.vue @@ -103,6 +103,7 @@ :list="tracks" :column-number="columnNumber" :show-position="showPosition" + :item-size="itemSize" :type="type" :enabled="enabled" > @@ -202,6 +203,10 @@ export default { type: String, default: 'id', }, + itemSize: { + type: Number, + default: 64, + }, }, data() { return { diff --git a/src/components/VirtualCoverRow.vue b/src/components/VirtualCoverRow.vue new file mode 100644 index 0000000..683ce5c --- /dev/null +++ b/src/components/VirtualCoverRow.vue @@ -0,0 +1,226 @@ + + + + + diff --git a/src/components/VirtualScroll.vue b/src/components/VirtualScroll.vue index 8c3e4df..77a20f4 100644 --- a/src/components/VirtualScroll.vue +++ b/src/components/VirtualScroll.vue @@ -3,7 +3,7 @@ ref="listRef" class="virtual-scroll" :style="{ height: containerHeight + 'px' }" - @scroll="scrollEvent(false)" + @scroll="scrollEvent" >
[] }, }, data() { return { screenHeight: 0, start: 0, - end: 0, startOffset: 0, position: [], - containerHeight: 100, oneHeight: 0, lastScrollTop: 0, scrollTop: 0, @@ -103,10 +105,14 @@ export default { return this.position[this.position.length - 1]?.bottom || 0; }, aboveRow() { - return Math.min(this.start, 5); + return Math.min(this.start, this.aboveValue); }, belowRow() { - return Math.min(this.totalRow - this.end, 5); + return Math.min(this.totalRow - this.end, this.belowValue); + }, + containerHeight() { + const windowHeight = window.innerHeight - 64; + return Math.min(windowHeight, this.listHeight, this.propsHeight); }, visibleRow() { return Math.ceil(this.containerHeight / this.oneHeight); @@ -118,12 +124,18 @@ export default { ); }, anchorPoint() { - return this.position.length ? this.position[this.start] : null; + return this.position.length + ? this.position[this.start * this.columnNumber] + : null; + }, + end() { + return this.start + this.visibleRow; }, listStyles() { const listHeight = this.totalRow * this.oneHeight; const windowHeight = window.innerHeight - 64; return { + gap: `0 ${this.gap}px`, gridTemplateColumns: `repeat(${this.columnNumber}, 1fr)`, transform: `translateY(${this.startOffset}px)`, paddingBottom: `${ @@ -149,10 +161,6 @@ export default { }, _listData() { this.initPosition(); - this.containerHeight = Math.min( - this.totalRow * this.oneHeight, - window.innerHeight - 64 - ); }, }, created() { @@ -162,11 +170,6 @@ export default { this.oneHeight = this.$refs.itemsRef ? this.$refs.itemsRef[0]?.getBoundingClientRect()?.height : 64; - this.containerHeight = Math.min( - this.totalRow * this.oneHeight, - window.innerHeight - 64 - ); - this.end = this.start + this.visibleRow; if (this.enabled) { this.aber = this.observer(); this.aber.observe(this.$refs.listRef); @@ -217,18 +220,18 @@ export default { this.position = this._listData.map((d, index) => ({ index, height: this.itemSize, - top: Math.ceil(index / this.columnNumber) * this.itemSize, - bottom: Math.ceil(index / this.columnNumber + 1) * this.itemSize, + top: Math.floor(index / this.columnNumber) * this.itemSize, + bottom: Math.floor(index / this.columnNumber + 1) * this.itemSize, })); }, getStartIndex(scrollTop = 0) { let start = 0; - let end = this.position.length - 1; + let end = Math.ceil(this.position.length / this.columnNumber) - 1; let tempIndex = null; while (start <= end) { const midIndex = Math.floor((start + end) / 2); - const midValue = this.position[midIndex].bottom; + const midValue = this.position[midIndex * this.columnNumber].bottom; if (midValue === scrollTop) { return midIndex; @@ -312,24 +315,28 @@ export default { scrollTop < this.anchorPoint.top ) { this.start = this.getStartIndex(scrollTop); - this.end = this.start + this.visibleRow; + this.setStartOffset(); } }, updateItemsSize() { this.$refs.itemsRef?.forEach(node => { - const height = node.getBoundingClientRect().height; - const index = +node.id; - const oldHeight = this.position[index].height; - const diff = oldHeight - height; + if (node.id % this.columnNumber === 0) { + const height = node.getBoundingClientRect().height; + const index = +node.id; + const oldHeight = this.position[index].height; + const diff = oldHeight - height; - if (diff) { - this.position[index].bottom -= diff; - this.position[index].height = height; - this.position[index].over = true; + if (diff) { + this.position[index].bottom -= diff; + this.position[index].height = height; + this.position[index].over = true; - for (let k = index + 1; k < this.position.length; k++) { - this.position[k].top = this.position[k - 1].bottom; - this.position[k].bottom -= diff; + for (let k = index + 1; k < this.position.length; k++) { + if (k % this.columnNumber !== 0) break; + this.position[k].top = + this.position[k - this.columnNumber].bottom; + this.position[k].bottom -= diff; + } } } }); @@ -337,9 +344,11 @@ export default { setStartOffset() { if (this.start >= 1) { const size = - this.position[this.start].top - - (this.position[this.start - this.aboveRow].top || 0); - this.startOffset = this.position[this.start - 1].bottom - size; + this.position[this.start * this.columnNumber].top - + (this.position[(this.start - this.aboveRow) * this.columnNumber] + .top || 0); + this.startOffset = + this.position[(this.start - 1) * this.columnNumber].bottom - size; } else { this.startOffset = 0; } diff --git a/src/electron/ipcMain.js b/src/electron/ipcMain.js index 3f35341..a135f15 100644 --- a/src/electron/ipcMain.js +++ b/src/electron/ipcMain.js @@ -1,5 +1,5 @@ import { app, dialog, globalShortcut, ipcMain } from 'electron'; -import UNM from '@unblockneteasemusic/rust-napi'; +// import UNM from '@unblockneteasemusic/rust-napi'; import { registerGlobalShortcut } from '@/electron/globalShortcut'; import cloneDeep from 'lodash/cloneDeep'; import shortcuts from '@/utils/shortcuts'; @@ -81,13 +81,13 @@ const client = require('discord-rich-presence')('818936529484906596'); * @param {?} data The data to convert. * @returns {import("buffer").Buffer} The converted data. */ -function toBuffer(data) { - if (data instanceof Buffer) { - return data; - } else { - return Buffer.from(data); - } -} +// function toBuffer(data) { +// if (data instanceof Buffer) { +// return data; +// } else { +// return Buffer.from(data); +// } +// } /** * Get the file base64 data from bilivideo. @@ -95,21 +95,21 @@ function toBuffer(data) { * @param {string} url The URL to fetch. * @returns {Promise} The file base64 data. */ -async function getBiliVideoFile(url) { - const axios = await import('axios').then(m => m.default); - const response = await axios.get(url, { - headers: { - Referer: 'https://www.bilibili.com/', - 'User-Agent': 'okhttp/3.4.1', - }, - responseType: 'arraybuffer', - }); - - const buffer = toBuffer(response.data); - const encodedData = buffer.toString('base64'); - - return encodedData; -} +// async function getBiliVideoFile(url) { +// const axios = await import('axios').then(m => m.default); +// const response = await axios.get(url, { +// headers: { +// Referer: 'https://www.bilibili.com/', +// 'User-Agent': 'okhttp/3.4.1', +// }, +// responseType: 'arraybuffer', +// }); + +// const buffer = toBuffer(response.data); +// const encodedData = buffer.toString('base64'); + +// return encodedData; +// } /** * Parse the source string (`a, b`) to source list `['a', 'b']`. @@ -118,27 +118,27 @@ async function getBiliVideoFile(url) { * @param {string} sourceString The source string. * @returns {string[]} The source list. */ -function parseSourceStringToList(executor, sourceString) { - const availableSource = executor.list(); +// function parseSourceStringToList(executor, sourceString) { +// const availableSource = executor.list(); - return sourceString - .split(',') - .map(s => s.trim().toLowerCase()) - .filter(s => { - const isAvailable = availableSource.includes(s); +// return sourceString +// .split(',') +// .map(s => s.trim().toLowerCase()) +// .filter(s => { +// const isAvailable = availableSource.includes(s); - if (!isAvailable) { - log(`This source is not one of the supported source: ${s}`); - } +// if (!isAvailable) { +// log(`This source is not one of the supported source: ${s}`); +// } - return isAvailable; - }); -} +// return isAvailable; +// }); +// } export function initIpcMain(win, store, tray, lrc) { // WIP: Do not enable logging as it has some issues in non-blocking I/O environment. // UNM.enableLogging(UNM.LoggingType.ConsoleEnv); - const unmExecutor = new UNM.Executor(); + // const unmExecutor = new UNM.Executor(); ipcMain.handle( 'unblock-music', @@ -149,55 +149,64 @@ export function initIpcMain(win, store, tray, lrc) { * @param {Record} ncmTrack * @param {UNM.Context} context */ - async (_, sourceListString, ncmTrack, context) => { - // Formt the track input - // FIXME: Figure out the structure of Track - const song = { - id: ncmTrack.id && ncmTrack.id.toString(), - name: ncmTrack.name, - duration: ncmTrack.dt, - album: ncmTrack.al && { - id: ncmTrack.al.id && ncmTrack.al.id.toString(), - name: ncmTrack.al.name, - }, - artists: ncmTrack.ar - ? ncmTrack.ar.map(({ id, name }) => ({ - id: id && id.toString(), - name, - })) - : [], - }; - - const sourceList = - typeof sourceListString === 'string' - ? parseSourceStringToList(unmExecutor, sourceListString) - : ['ytdl', 'bilibili', 'pyncm', 'kugou']; - log(`[UNM] using source: ${sourceList.join(', ')}`); - log(`[UNM] using configuration: ${JSON.stringify(context)}`); - - try { - // TODO: tell users to install yt-dlp. - const matchedAudio = await unmExecutor.search( - sourceList, - song, - context - ); - const retrievedSong = await unmExecutor.retrieve(matchedAudio, context); - - // bilibili's audio file needs some special treatment - if (retrievedSong.url.includes('bilivideo.com')) { - retrievedSong.url = await getBiliVideoFile(retrievedSong.url); - } - - log(`respond with retrieve song…`); - log(JSON.stringify(matchedAudio)); - return retrievedSong; - } catch (err) { - const errorMessage = err instanceof Error ? `${err.message}` : `${err}`; - log(`UnblockNeteaseMusic failed: ${errorMessage}`); - return null; - } + async (_, sourceListString, ncmTrack) => { + const sourceList = sourceListString + .split(',') + .map(s => s.trim().toLowerCase()); + // console.log(sourceList, ncmTrack, context); + const match = require('@unblockneteasemusic/server'); + const result = await match(ncmTrack.id, sourceList); + return result; } + // async (_, sourceListString, ncmTrack, context) => { + // // Formt the track input + // // FIXME: Figure out the structure of Track + // const song = { + // id: ncmTrack.id && ncmTrack.id.toString(), + // name: ncmTrack.name, + // duration: ncmTrack.dt, + // album: ncmTrack.al && { + // id: ncmTrack.al.id && ncmTrack.al.id.toString(), + // name: ncmTrack.al.name, + // }, + // artists: ncmTrack.ar + // ? ncmTrack.ar.map(({ id, name }) => ({ + // id: id && id.toString(), + // name, + // })) + // : [], + // }; + + // const sourceList = + // typeof sourceListString === 'string' + // ? parseSourceStringToList(unmExecutor, sourceListString) + // : ['ytdl', 'bilibili', 'pyncm', 'kugou']; + // log(`[UNM] using source: ${sourceList.join(', ')}`); + // log(`[UNM] using configuration: ${JSON.stringify(context)}`); + + // try { + // // TODO: tell users to install yt-dlp. + // const matchedAudio = await unmExecutor.search( + // sourceList, + // song, + // context + // ); + // const retrievedSong = await unmExecutor.retrieve(matchedAudio, context); + + // // bilibili's audio file needs some special treatment + // if (retrievedSong.url.includes('bilivideo.com')) { + // retrievedSong.url = await getBiliVideoFile(retrievedSong.url); + // } + + // log(`respond with retrieve song…`); + // log(JSON.stringify(matchedAudio)); + // return retrievedSong; + // } catch (err) { + // const errorMessage = err instanceof Error ? `${err.message}` : `${err}`; + // log(`UnblockNeteaseMusic failed: ${errorMessage}`); + // return null; + // } + // } ); ipcMain.on('close', e => { diff --git a/src/store/actions.js b/src/store/actions.js index d36d851..8cf5784 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -374,23 +374,27 @@ export default { let like = true; if (state.liked.songs.includes(id)) like = false; likeATrack({ id, like }) - .then(() => { - if (like === false) { - commit('updateLikedXXX', { - name: 'songs', - data: state.liked.songs.filter(d => d !== id), - }); + .then(res => { + if (res.code === 200) { + if (like === false) { + commit('updateLikedXXX', { + name: 'songs', + data: state.liked.songs.filter(d => d !== id), + }); + } else { + let newLikeSongs = state.liked.songs; + newLikeSongs.push(id); + commit('updateLikedXXX', { + name: 'songs', + data: newLikeSongs, + }); + } + dispatch('fetchLikedSongsWithDetails'); + const { ipcRenderer } = require('electron'); + ipcRenderer.send('updateTrayLikeState', like); } else { - let newLikeSongs = state.liked.songs; - newLikeSongs.push(id); - commit('updateLikedXXX', { - name: 'songs', - data: newLikeSongs, - }); + dispatch('showToast', '操作失败,专辑下架或版权锁定'); } - dispatch('fetchLikedSongsWithDetails'); - const { ipcRenderer } = require('electron'); - ipcRenderer.send('updateTrayLikeState', like); }) .catch(() => { dispatch('showToast', '操作失败,专辑下架或版权锁定'); diff --git a/src/utils/Player.js b/src/utils/Player.js index 33f257e..78778fb 100644 --- a/src/utils/Player.js +++ b/src/utils/Player.js @@ -645,7 +645,7 @@ export default class { this._localPic = null; } let artists = track.ar.map(a => a.name); - const useLocal = track.isLocal !== false && track.matched !== true; + const useLocal = track.isLocal && !track.matched; if (useLocal) { const blob = await fetch(`atom://get-pic/${track.filePath}`).then(res => res.blob() diff --git a/src/views/explore.vue b/src/views/explore.vue index ef190df..197813b 100644 --- a/src/views/explore.vue +++ b/src/views/explore.vue @@ -1,6 +1,6 @@