diff --git a/package.json b/package.json index f383740..a53596f 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "dependencies": { "@sentry/node": "^6.19.7", "@sentry/tracing": "^6.19.7", + "@unblockneteasemusic/rust-napi": "^0.3.0-pre.1", "NeteaseCloudMusicApi": "^4.5.12", "better-sqlite3": "7.5.1", "change-case": "^4.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 329c271..84c4611 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,7 @@ specifiers: '@types/react-dom': ^18.0.1 '@typescript-eslint/eslint-plugin': ^5.21.0 '@typescript-eslint/parser': ^5.21.0 + '@unblockneteasemusic/rust-napi': ^0.3.0-pre.1 '@vitejs/plugin-react': ^1.3.1 '@vitest/ui': ^0.10.0 NeteaseCloudMusicApi: ^4.5.12 @@ -88,6 +89,7 @@ specifiers: dependencies: '@sentry/node': 6.19.7 '@sentry/tracing': 6.19.7 + '@unblockneteasemusic/rust-napi': 0.3.0-pre.1 NeteaseCloudMusicApi: 4.5.12 better-sqlite3: 7.5.1 change-case: 4.1.2 @@ -1212,6 +1214,142 @@ packages: eslint-visitor-keys: 3.3.0 dev: true + /@unblockneteasemusic/rust-napi-android-arm-eabi/0.3.0-pre.1: + resolution: {integrity: sha512-932T6uUSHbWXTS2lt0wTI5F2lsIrGea2aU22VwSFaHfpTgxB8qDfd+jn+zMRlpnqTmuglyd0hk/1yUZd9tgu+w==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@unblockneteasemusic/rust-napi-android-arm64/0.3.0-pre.1: + resolution: {integrity: sha512-RQMCzO7+0Iw+R/MHy0hvv9Vg6BzqrUmWk9bMLR0mkkYKxR0wPEaB7WpAvUfLRKevSqiWP8rrNRuzqGVBu0PaCg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@unblockneteasemusic/rust-napi-darwin-arm64/0.3.0-pre.1: + resolution: {integrity: sha512-M3YvPhYNyBSytho3FmyX1cj5k21ZlW14mPuy/5oLRw4qehAmjsSYjCEFLG5I29IlZTLN0sbIz92dqHkYclSXSg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@unblockneteasemusic/rust-napi-darwin-x64/0.3.0-pre.1: + resolution: {integrity: sha512-kN4Bur22hFo2UAJ4vljuEX4ue7TlhhOnz9Q3KrwhxOtv2KlQi2iQ/8tCl+/whKpqgf/cs/klQLDJj73PsE1G+w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@unblockneteasemusic/rust-napi-freebsd-x64/0.3.0-pre.1: + resolution: {integrity: sha512-tRBudpZX+0X8sDSP+LmnU9nfsT7939rCu+bZhizjHHe2jt02iX/ZLHOkEcVBh0VHhHVvTehj0zH3iHFkfYnR2Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + + /@unblockneteasemusic/rust-napi-linux-arm-gnueabihf/0.3.0-pre.1: + resolution: {integrity: sha512-3vkXlBm6f2dWOWLKaosTcFAO5b/VV9WvyT3PQBJFvq0PtRGonr2Zr1gYJC4zUz2UraSKaFg4GMKgopU2Duxgow==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@unblockneteasemusic/rust-napi-linux-arm64-gnu/0.3.0-pre.1: + resolution: {integrity: sha512-Zq1kjjXhle0OA7NzadzBQvjbTZfbK/qMuHay97+ZGXZH4uxv0jmJ2aQWR7HlrrKmQKpknURvrxbXmi8dxeI+SA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@unblockneteasemusic/rust-napi-linux-arm64-musl/0.3.0-pre.1: + resolution: {integrity: sha512-Yp7+Ra8ksx2nCZs18duK7BPtsY2chzdrBIrWY14N7aP0IIglwBcazP+GGFNaqqDx0nW+/0463pUsi8OgbWX+mA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@unblockneteasemusic/rust-napi-linux-x64-gnu/0.3.0-pre.1: + resolution: {integrity: sha512-A71/PhBCotAQPimGIJnZEYJwBv2FilhYC1OC4wOy3Rt54C9Cw12FJp49c7J13mZLktZfCJOSu6/6RPY8+6Yfrw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@unblockneteasemusic/rust-napi-linux-x64-musl/0.3.0-pre.1: + resolution: {integrity: sha512-klXwwdVb4LdHmUrdclZSfn6nQwXddBwJJk392wRagjGUyNbUkC9b3JHfMEdrssMIPtIGtNHWt/43z+saovZl2g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@unblockneteasemusic/rust-napi-win32-arm64-msvc/0.3.0-pre.1: + resolution: {integrity: sha512-jaX6UvQlRuH1iyextG34l8b19MtVFTZZX8U34oW3d7rZxcas5ZitEHzd6XfjpHcTtkXSyhQxx+WjDiY2BQ+B3A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@unblockneteasemusic/rust-napi-win32-ia32-msvc/0.3.0-pre.1: + resolution: {integrity: sha512-QgO05vQxxkU0+bfprxQMVLXxguI8N1ApPQCyAYNnvrKTQ/F6OjV+bQghlWaKwcIeve8zoYN1zgSHDyNj+0xrYA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@unblockneteasemusic/rust-napi-win32-x64-msvc/0.3.0-pre.1: + resolution: {integrity: sha512-6CI0YlQxHiU6vetwoAjYgBOFlWoTkLVUSd0tpEN9/5R7iExRUHdFdRfpXqPJzpYnAhZlGqAIslCayoNcf7vnQw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@unblockneteasemusic/rust-napi/0.3.0-pre.1: + resolution: {integrity: sha512-n1zDJvy5OEEMPQdhTPARRRQLM4Tnvx9UGq0smVKWu6CjutK6rcSIVoxe4ADILzBOY3RCe5vuo9Qn4RUzKCeeWQ==} + engines: {node: '>= 10'} + optionalDependencies: + '@unblockneteasemusic/rust-napi-android-arm-eabi': 0.3.0-pre.1 + '@unblockneteasemusic/rust-napi-android-arm64': 0.3.0-pre.1 + '@unblockneteasemusic/rust-napi-darwin-arm64': 0.3.0-pre.1 + '@unblockneteasemusic/rust-napi-darwin-x64': 0.3.0-pre.1 + '@unblockneteasemusic/rust-napi-freebsd-x64': 0.3.0-pre.1 + '@unblockneteasemusic/rust-napi-linux-arm-gnueabihf': 0.3.0-pre.1 + '@unblockneteasemusic/rust-napi-linux-arm64-gnu': 0.3.0-pre.1 + '@unblockneteasemusic/rust-napi-linux-arm64-musl': 0.3.0-pre.1 + '@unblockneteasemusic/rust-napi-linux-x64-gnu': 0.3.0-pre.1 + '@unblockneteasemusic/rust-napi-linux-x64-musl': 0.3.0-pre.1 + '@unblockneteasemusic/rust-napi-win32-arm64-msvc': 0.3.0-pre.1 + '@unblockneteasemusic/rust-napi-win32-ia32-msvc': 0.3.0-pre.1 + '@unblockneteasemusic/rust-napi-win32-x64-msvc': 0.3.0-pre.1 + dev: false + /@vitejs/plugin-react/1.3.1: resolution: {integrity: sha512-qQS8Y2fZCjo5YmDUplEXl3yn+aueiwxB7BaoQ4nWYJYR+Ai8NXPVLlkLobVMs5+DeyFyg9Lrz6zCzdX1opcvyw==} engines: {node: '>=12.0.0'} diff --git a/postcss.config.js b/postcss.config.js index bd7d068..effaa5d 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -2,7 +2,7 @@ const { colord } = require('colord') const colors = require('tailwindcss/colors') -const replaceBrandColorWithCssVar = () => { +const replaceBrandColorWithCSSVar = () => { const blues = Object.entries(colors.blue).map(([key, value]) => { const c = colord(value).toRgb() return { @@ -11,7 +11,7 @@ const replaceBrandColorWithCssVar = () => { } }) return { - postcssPlugin: 'replaceBrandColorWithCssVar', + postcssPlugin: 'replaceBrandColorWithCSSVar', Declaration(decl) { let value = decl.value blues.forEach(blue => { @@ -33,12 +33,12 @@ const replaceBrandColorWithCssVar = () => { }, } } -replaceBrandColorWithCssVar.postcss = true +replaceBrandColorWithCSSVar.postcss = true module.exports = { plugins: [ require('tailwindcss'), require('autoprefixer'), - replaceBrandColorWithCssVar, + replaceBrandColorWithCSSVar, ], } diff --git a/scripts/build.main.mjs b/scripts/build.main.mjs index c8db748..a497776 100644 --- a/scripts/build.main.mjs +++ b/scripts/build.main.mjs @@ -37,6 +37,7 @@ const options = { 'electron', 'NeteaseCloudMusicApi', 'better-sqlite3', + '@unblockneteasemusic/rust-napi' ], } diff --git a/scripts/generate.accent.color.css.js b/scripts/generate.accent.color.css.js index 8eb6944..6ec6c17 100644 --- a/scripts/generate.accent.color.css.js +++ b/scripts/generate.accent.color.css.js @@ -1,30 +1,9 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -const colors = require('tailwindcss/colors') const { colord } = require('colord') const prettier = require('prettier') const fs = require('fs') const prettierConfig = require('../prettier.config.js') - - const pickedColors = { - blue: colors.blue, - red: colors.red, - orange: colors.orange, - amber: colors.amber, - yellow: colors.yellow, - lime: colors.lime, - green: colors.green, - emerald: colors.emerald, - teal: colors.teal, - cyan: colors.cyan, - sky: colors.sky, - indigo: colors.indigo, - violet: colors.violet, - purple: colors.purple, - fuchsia: colors.fuchsia, - pink: colors.pink, - rose: colors.rose, -} -module.exports = pickedColors +const pickedColors = require('./pickedColors.js') const colorsCss = {} Object.entries(pickedColors).forEach(([name, colors]) => { @@ -47,4 +26,3 @@ ${name === 'blue' ? ':root' : `[data-accent-color='${name}']`} {${color} const formatted = prettier.format(css, { ...prettierConfig, parser: 'css' }) fs.writeFileSync('./src/renderer/styles/accentColor.scss', formatted) - diff --git a/scripts/pickedColors.js b/scripts/pickedColors.js new file mode 100644 index 0000000..f4de127 --- /dev/null +++ b/scripts/pickedColors.js @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const colors = require('tailwindcss/colors') + +const pickedColors = { + blue: colors.blue, + red: colors.red, + orange: colors.orange, + amber: colors.amber, + yellow: colors.yellow, + lime: colors.lime, + green: colors.green, + emerald: colors.emerald, + teal: colors.teal, + cyan: colors.cyan, + sky: colors.sky, + indigo: colors.indigo, + violet: colors.violet, + purple: colors.purple, + fuchsia: colors.fuchsia, + pink: colors.pink, + rose: colors.rose, +} + +module.exports = pickedColors diff --git a/src/main/cache.ts b/src/main/cache.ts index 9f29386..8146661 100644 --- a/src/main/cache.ts +++ b/src/main/cache.ts @@ -6,6 +6,7 @@ import log from './log' import fs from 'fs' import * as musicMetadata from 'music-metadata' import { APIs, APIsParams, APIsResponse } from '../shared/CacheAPIs' +import { TablesStructures } from './db' class Cache { constructor() { @@ -206,59 +207,6 @@ class Cache { return cache } } - - // Get audio cache if API is song/detail - if (api === APIs.SongUrl) { - const cache = db.find(Tables.Audio, Number(req.query.id)) - if (!cache) return - - const audioFileName = `${cache.id}-${cache.br}.${cache.type}` - - const isAudioFileExists = fs.existsSync( - `${app.getPath('userData')}/audio_cache/${audioFileName}` - ) - if (!isAudioFileExists) return - - log.debug(`[cache] Audio cache hit for ${req.path}`) - - return { - data: [ - { - source: cache.source, - id: cache.id, - url: `http://127.0.0.1:42710/yesplaymusic/audio/${audioFileName}`, - br: cache.br, - size: 0, - md5: '', - code: 200, - expi: 0, - type: cache.type, - gain: 0, - fee: 8, - uf: null, - payed: 0, - flag: 4, - canExtend: false, - freeTrialInfo: null, - level: 'standard', - encodeType: cache.type, - freeTrialPrivilege: { - resConsumable: false, - userConsumable: false, - listenType: null, - }, - freeTimeTrialPrivilege: { - resConsumable: false, - userConsumable: false, - type: 0, - remainTime: 0, - }, - urlSource: 0, - }, - ], - code: 200, - } - } } getAudio(fileName: string, res: Response) { @@ -279,17 +227,17 @@ class Cache { .status(206) .setHeader('Accept-Ranges', 'bytes') .setHeader('Connection', 'keep-alive') - .setHeader('Content-Range', `bytes 0-${audio.byteLength - 1}/${audio.byteLength}`) + .setHeader( + 'Content-Range', + `bytes 0-${audio.byteLength - 1}/${audio.byteLength}` + ) .send(audio) } catch (error) { res.status(500).send({ error }) } } - async setAudio( - buffer: Buffer, - { id, source }: { id: number; source: string } - ) { + async setAudio(buffer: Buffer, { id, url }: { id: number; url: string }) { const path = `${app.getPath('userData')}/audio_cache` try { @@ -299,16 +247,26 @@ class Cache { } const meta = await musicMetadata.parseBuffer(buffer) - const br = meta.format.bitrate - const type = { - 'MPEG 1 Layer 3': 'mp3', - 'Ogg Vorbis': 'ogg', - AAC: 'm4a', - FLAC: 'flac', - unknown: 'unknown', - }[meta.format.codec ?? 'unknown'] + const br = + meta?.format?.codec === 'OPUS' ? 165000 : meta.format.bitrate ?? 0 + const type = + { + 'MPEG 1 Layer 3': 'mp3', + 'Ogg Vorbis': 'ogg', + AAC: 'm4a', + FLAC: 'flac', + OPUS: 'opus', + }[meta.format.codec ?? ''] ?? 'unknown' - await fs.writeFile(`${path}/${id}-${br}.${type}`, buffer, error => { + let source: TablesStructures[Tables.Audio]['source'] = 'unknown' + if (url.includes('googlevideo.com')) source = 'youtube' + if (url.includes('126.net')) source = 'netease' + if (url.includes('migu.cn')) source = 'migu' + if (url.includes('kuwo.cn')) source = 'kuwo' + if (url.includes('bilivideo.com')) source = 'bilibili' + // TODO: missing kugou qq joox + + fs.writeFile(`${path}/${id}-${br}.${type}`, buffer, error => { if (error) { return log.error(`[cache] cacheAudio failed: ${error}`) } @@ -317,9 +275,9 @@ class Cache { db.upsert(Tables.Audio, { id, br, - type, + type: type as TablesStructures[Tables.Audio]['type'], source, - updateAt: Date.now(), + updatedAt: Date.now(), }) log.info(`[cache] cacheAudio ${id}-${br}.${type}`) diff --git a/src/main/db.ts b/src/main/db.ts index b8e6e8e..84f38d9 100644 --- a/src/main/db.ts +++ b/src/main/db.ts @@ -38,9 +38,17 @@ export interface TablesStructures { [Tables.Audio]: { id: number br: number - type: 'mp3' | 'flac' | 'ogg' | 'wav' | 'm4a' | 'aac' | 'unknown' - source: 'netease' | 'migu' | 'kuwo' | 'kugou' | 'youtube' - url: string + type: 'mp3' | 'flac' | 'ogg' | 'wav' | 'm4a' | 'aac' | 'unknown' | 'opus' + source: + | 'unknown' + | 'netease' + | 'migu' + | 'kuwo' + | 'kugou' + | 'youtube' + | 'qq' + | 'bilibili' + | 'joox' updatedAt: number } [Tables.CoverColor]: { diff --git a/src/main/index.ts b/src/main/index.ts index d052fd6..120ce11 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -15,19 +15,21 @@ import { initIpcMain } from './ipcMain' import { createTray, YPMTray } from './tray' import { IpcChannels } from '@/shared/IpcChannels' import { createTaskbar, Thumbar } from './windowsTaskbar' +import { Store as State, initialState } from '@/shared/store' const isWindows = process.platform === 'win32' const isMac = process.platform === 'darwin' const isLinux = process.platform === 'linux' const isDev = process.env.NODE_ENV === 'development' -interface TypedElectronStore { +export interface TypedElectronStore { window: { width: number height: number x?: number y?: number } + settings: State['settings'] } class Main { @@ -40,6 +42,7 @@ class Main { width: 1440, height: 960, }, + settings: initialState.settings, }, }) @@ -65,7 +68,7 @@ class Main { this.handleWindowEvents() this.createTray() this.createThumbar() - initIpcMain(this.win, this.tray, this.thumbar) + initIpcMain(this.win, this.tray, this.thumbar, this.store) this.initDevTools() }) } @@ -129,6 +132,51 @@ class Main { if (url.startsWith('https:')) shell.openExternal(url) return { action: 'deny' } }) + + this.disableCORS() + } + + disableCORS() { + if (!this.win) return + function UpsertKeyValue(obj, keyToChange, value) { + const keyToChangeLower = keyToChange.toLowerCase() + for (const key of Object.keys(obj)) { + if (key.toLowerCase() === keyToChangeLower) { + // Reassign old key + obj[key] = value + // Done + return + } + } + // Insert at end instead + obj[keyToChange] = value + } + + this.win.webContents.session.webRequest.onBeforeSendHeaders( + (details, callback) => { + const { requestHeaders, url } = details + UpsertKeyValue(requestHeaders, 'Access-Control-Allow-Origin', ['*']) + + if (url.includes('googlevideo.com')) { + requestHeaders['Sec-Fetch-Mode'] = 'no-cors' + requestHeaders['Sec-Fetch-Dest'] = 'audio' + requestHeaders['Range'] = 'bytes=0-' + } + + callback({ requestHeaders }) + } + ) + + this.win.webContents.session.webRequest.onHeadersReceived( + (details, callback) => { + const { responseHeaders } = details + UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Origin', ['*']) + UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Headers', ['*']) + callback({ + responseHeaders, + }) + } + ) } handleWindowEvents() { diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts index 6078a35..7117f6f 100644 --- a/src/main/ipcMain.ts +++ b/src/main/ipcMain.ts @@ -4,6 +4,8 @@ import { IpcChannels, IpcChannelsParams } from '../shared/IpcChannels' import cache from './cache' import log from './log' import fs from 'fs' +import Store from 'electron-store' +import { TypedElectronStore } from './index' import { APIs } from '../shared/CacheAPIs' import { YPMTray } from './tray' import { Thumbar } from './windowsTaskbar' @@ -18,11 +20,14 @@ const on = ( export function initIpcMain( win: BrowserWindow | null, tray: YPMTray | null, - thumbar: Thumbar | null + thumbar: Thumbar | null, + store: Store ) { initWindowIpcMain(win) initTrayIpcMain(tray) initTaskbarIpcMain(thumbar) + initStoreIpcMain(store) + initOtherIpcMain() } /** @@ -51,9 +56,7 @@ function initWindowIpcMain(win: BrowserWindow | null) { function initTrayIpcMain(tray: YPMTray | null) { on(IpcChannels.SetTrayTooltip, (e, { text }) => tray?.setTooltip(text)) - on(IpcChannels.Like, (e, { isLiked }) => - tray?.setLikeState(isLiked) - ) + on(IpcChannels.Like, (e, { isLiked }) => tray?.setLikeState(isLiked)) on(IpcChannels.Play, () => tray?.setPlayState(true)) on(IpcChannels.Pause, () => tray?.setPlayState(false)) @@ -71,60 +74,82 @@ function initTaskbarIpcMain(thumbar: Thumbar | null) { } /** - * 清除API缓存 + * 处理需要electron-store的事件 + * @param {Store} store */ -on(IpcChannels.ClearAPICache, () => { - db.truncate(Tables.Track) - db.truncate(Tables.Album) - db.truncate(Tables.Artist) - db.truncate(Tables.Playlist) - db.truncate(Tables.ArtistAlbum) - db.truncate(Tables.AccountData) - db.truncate(Tables.Audio) - db.vacuum() -}) - -/** - * Get API cache - */ -on(IpcChannels.GetApiCacheSync, (event, args) => { - const { api, query } = args - const data = cache.get(api, query) - event.returnValue = data -}) - -/** - * 缓存封面颜色 - */ -on(IpcChannels.CacheCoverColor, (event, args) => { - const { id, color } = args - cache.set(APIs.CoverColor, { id, color }) -}) - -/** - * 导出tables到json文件,方便查看table大小(dev环境) - */ -if (process.env.NODE_ENV === 'development') { - on(IpcChannels.DevDbExportJson, () => { - const tables = [ - Tables.ArtistAlbum, - Tables.Playlist, - Tables.Album, - Tables.Track, - Tables.Artist, - Tables.Audio, - Tables.AccountData, - Tables.Lyric, - ] - tables.forEach(table => { - const data = db.findAll(table) - - fs.writeFile(`./tmp/${table}.json`, JSON.stringify(data), function (err) { - if (err) { - return console.log(err) - } - console.log('The file was saved!') - }) - }) +function initStoreIpcMain(store: Store) { + /** + * 同步设置到Main + */ + on(IpcChannels.SyncSettings, (event, settings) => { + store.set('settings', settings) }) } + +/** + * 处理其他事件 + */ +function initOtherIpcMain() { + /** + * 清除API缓存 + */ + on(IpcChannels.ClearAPICache, () => { + db.truncate(Tables.Track) + db.truncate(Tables.Album) + db.truncate(Tables.Artist) + db.truncate(Tables.Playlist) + db.truncate(Tables.ArtistAlbum) + db.truncate(Tables.AccountData) + db.truncate(Tables.Audio) + db.vacuum() + }) + + /** + * Get API cache + */ + on(IpcChannels.GetApiCacheSync, (event, args) => { + const { api, query } = args + const data = cache.get(api, query) + event.returnValue = data + }) + + /** + * 缓存封面颜色 + */ + on(IpcChannels.CacheCoverColor, (event, args) => { + const { id, color } = args + cache.set(APIs.CoverColor, { id, color }) + }) + + /** + * 导出tables到json文件,方便查看table大小(dev环境) + */ + if (process.env.NODE_ENV === 'development') { + on(IpcChannels.DevDbExportJson, () => { + const tables = [ + Tables.ArtistAlbum, + Tables.Playlist, + Tables.Album, + Tables.Track, + Tables.Artist, + Tables.Audio, + Tables.AccountData, + Tables.Lyric, + ] + tables.forEach(table => { + const data = db.findAll(table) + + fs.writeFile( + `./tmp/${table}.json`, + JSON.stringify(data), + function (err) { + if (err) { + return console.log(err) + } + console.log('The file was saved!') + } + ) + }) + }) + } +} diff --git a/src/main/migrations/init.sql b/src/main/migrations/init.sql index c9905f0..f1bc92e 100644 --- a/src/main/migrations/init.sql +++ b/src/main/migrations/init.sql @@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS "AccountData" ("id" text NOT NULL,"json" text NOT NUL CREATE TABLE IF NOT EXISTS "Album" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id)); CREATE TABLE IF NOT EXISTS "ArtistAlbum" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id)); CREATE TABLE IF NOT EXISTS "Artist" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id)); -CREATE TABLE IF NOT EXISTS "Audio" ("id" integer NOT NULL,"br" int NOT NULL,"type" text NOT NULL,"srouce" text NOT NULL,"updateAt" int NOT NULL, PRIMARY KEY (id)); +CREATE TABLE IF NOT EXISTS "Audio" ("id" integer NOT NULL,"br" int NOT NULL,"type" text NOT NULL,"srouce" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id)); CREATE TABLE IF NOT EXISTS "Lyric" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" integer NOT NULL, PRIMARY KEY (id)); CREATE TABLE IF NOT EXISTS "Playlist" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id)); CREATE TABLE IF NOT EXISTS "Track" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id)); diff --git a/src/main/server.ts b/src/main/server.ts index 4a97384..81f0500 100644 --- a/src/main/server.ts +++ b/src/main/server.ts @@ -5,6 +5,12 @@ import log from './log' import cache from './cache' import fileUpload from 'express-fileupload' import path from 'path' +import fs from 'fs' +import { db, Tables } from 'db' +import { app } from 'electron' +import type { FetchAudioSourceResponse } from '@/shared/api/Track' +import UNM from '@unblockneteasemusic/rust-napi' +import { APIs as CacheAPIs } from '../shared/CacheAPIs' const isDev = process.env.NODE_ENV === 'development' const isProd = process.env.NODE_ENV === 'production' @@ -21,6 +27,7 @@ class Server { log.info('[server] starting http server') this.app.use(cookieParser()) this.app.use(fileUpload()) + this.getAudioUrlHandler() this.neteaseHandler() this.cacheAudioHandler() this.serveStaticForProd() @@ -32,7 +39,9 @@ class Server { const neteaseApi = require('NeteaseCloudMusicApi') as (params: any) => any[] Object.entries(neteaseApi).forEach(([name, handler]) => { - if (['serveNcmApi', 'getModulesDefinitions'].includes(name)) return + if (['serveNcmApi', 'getModulesDefinitions', 'song_url'].includes(name)) { + return + } name = pathCase(name) @@ -71,6 +80,205 @@ class Server { } } + getAudioUrlHandler() { + const getFromCache = (id: number) => { + // get from cache + const cache = db.find(Tables.Audio, id) + if (!cache) return + + const audioFileName = `${cache.id}-${cache.br}.${cache.type}` + + const isAudioFileExists = fs.existsSync( + `${app.getPath('userData')}/audio_cache/${audioFileName}` + ) + if (!isAudioFileExists) return + + log.debug(`[server] Audio cache hit for song/url`) + + return { + data: [ + { + source: cache.source, + id: cache.id, + url: `http://127.0.0.1:42710/yesplaymusic/audio/${audioFileName}`, + br: cache.br, + size: 0, + md5: '', + code: 200, + expi: 0, + type: cache.type, + gain: 0, + fee: 8, + uf: null, + payed: 0, + flag: 4, + canExtend: false, + freeTrialInfo: null, + level: 'standard', + encodeType: cache.type, + freeTrialPrivilege: { + resConsumable: false, + userConsumable: false, + listenType: null, + }, + freeTimeTrialPrivilege: { + resConsumable: false, + userConsumable: false, + type: 0, + remainTime: 0, + }, + urlSource: 0, + }, + ], + code: 200, + } + } + + const getFromNetease = async ( + req: Request + ): Promise => { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const getSongUrl = (require('NeteaseCloudMusicApi') as any).song_url + + const result = await getSongUrl({ ...req.query, cookie: req.cookies }) + + return result.body + } catch (error: any) { + return + } + } + + const unmExecutor = new UNM.Executor() + const getFromUNM = async (id: number, req: Request) => { + log.debug('[server] Fetching audio url from UNM') + let track: Track = cache.get(CacheAPIs.Track, { ids: String(id) }) + ?.songs?.[0] + if (!track) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const getSongDetail = (require('NeteaseCloudMusicApi') as any) + .song_detail + + track = await getSongDetail({ ...req.query, cookie: req.cookies }) + } + if (!track) return + + const trackForUNM = { + id: String(track.id), + name: track.name, + duration: track.dt, + album: { + id: String(track.al.id), + name: track.al.name, + }, + artists: [ + ...track.ar.map((a: Artist) => ({ + id: String(a.id), + name: a.name, + })), + ], + } + + const sourceList = ['ytdl'] + const context = {} + const matchedAudio = await unmExecutor.search( + sourceList, + trackForUNM, + context + ) + const retrievedSong = await unmExecutor.retrieve(matchedAudio, context) + const source = + retrievedSong.source === 'ytdl' ? 'youtube' : retrievedSong.source + if (retrievedSong.url) { + return { + data: [ + { + source, + id, + url: retrievedSong.url, + br: 128000, + size: 0, + md5: '', + code: 200, + expi: 0, + type: 'unknown', + gain: 0, + fee: 8, + uf: null, + payed: 0, + flag: 4, + canExtend: false, + freeTrialInfo: null, + level: 'standard', + encodeType: 'unknown', + freeTrialPrivilege: { + resConsumable: false, + userConsumable: false, + listenType: null, + }, + freeTimeTrialPrivilege: { + resConsumable: false, + userConsumable: false, + type: 0, + remainTime: 0, + }, + urlSource: 0, + unm: { + source, + song: matchedAudio.song, + }, + }, + ], + code: 200, + } + } + } + + const handler = async (req: Request, res: Response) => { + const id = Number(req.query.id) || 0 + if (id === 0) { + return res.status(400).send({ + code: 400, + msg: 'id is required or id is invalid', + }) + } + + // try { + // const fromCache = await getFromCache(id) + // if (fromCache) { + // res.status(200).send(fromCache) + // return + // } + // } catch (error) { + // log.error(`[server] getFromCache failed: ${String(error)}`) + // } + + // const fromNetease = await getFromNetease(req) + // if (fromNetease?.code === 200 && !fromNetease?.data?.[0].freeTrialInfo) { + // res.status(200).send(fromNetease) + // return + // } + + try { + const fromUNM = await getFromUNM(id, req) + if (fromUNM) { + res.status(200).send(fromUNM) + return + } + } catch (error) { + log.error(`[server] getFromNetease failed: ${String(error)}`) + } + + // if (fromNetease?.data?.[0].freeTrialInfo) { + // fromNetease.data[0].url = '' + // } + + // res.status(fromNetease?.code ?? 500).send(fromNetease) + } + + this.app.get('/netease/song/url', handler) + } + cacheAudioHandler() { this.app.get( '/yesplaymusic/audio/:filename', @@ -103,8 +311,8 @@ class Server { try { await cache.setAudio(req.files.file.data, { - id: id, - source: 'netease', + id, + url: String(req.query.url) || '', }) res.status(200).send('Audio cached!') } catch (error) { diff --git a/src/main/windowsTaskbar.ts b/src/main/windowsTaskbar.ts index ffac443..9243b96 100644 --- a/src/main/windowsTaskbar.ts +++ b/src/main/windowsTaskbar.ts @@ -67,9 +67,7 @@ class ThumbarImpl implements Thumbar { private _updateThumbarButtons(clear: boolean) { this._win.setThumbarButtons( - clear - ? [] - : [this._previous, this._playOrPause, this._next] + clear ? [] : [this._previous, this._playOrPause, this._next] ) } diff --git a/src/renderer/api/yesplaymusic.ts b/src/renderer/api/yesplaymusic.ts index e2269fd..0eec6bc 100644 --- a/src/renderer/api/yesplaymusic.ts +++ b/src/renderer/api/yesplaymusic.ts @@ -12,7 +12,7 @@ const request: AxiosInstance = axios.create({ export async function cacheAudio(id: number, audio: string) { const file = await axios.get(audio, { responseType: 'arraybuffer' }) - if (file.status !== 200) return + if (file.status !== 200 && file.status !== 206) return const formData = new FormData() const blob = new Blob([file.data], { type: 'multipart/form-data' }) diff --git a/src/renderer/components/Lyric/Lyric.tsx b/src/renderer/components/Lyric/Lyric.tsx index cdd2ef4..2614b89 100644 --- a/src/renderer/components/Lyric/Lyric.tsx +++ b/src/renderer/components/Lyric/Lyric.tsx @@ -12,7 +12,6 @@ import { lyricParser } from '@/renderer/utils/lyric' const Lyric = ({ className }: { className?: string }) => { // const ease = [0.5, 0.2, 0.2, 0.8] - console.log('rendering') const playerSnapshot = useSnapshot(player) const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track]) diff --git a/src/renderer/components/TracksAlbum.tsx b/src/renderer/components/TracksAlbum.tsx index af0f912..6d0356d 100644 --- a/src/renderer/components/TracksAlbum.tsx +++ b/src/renderer/components/TracksAlbum.tsx @@ -9,13 +9,8 @@ import { player } from '@/renderer/store' import { formatDuration } from '@/renderer/utils/common' import { State as PlayerState } from '@/renderer/utils/player' -const enableRenderLog = true - const PlayOrPauseButtonInTrack = memo( ({ isHighlight, trackID }: { isHighlight: boolean; trackID: number }) => { - if (enableRenderLog) - console.debug(`Rendering TracksAlbum.tsx PlayOrPauseButtonInTrack`) - const playerSnapshot = useSnapshot(player) const isPlaying = useMemo( () => playerSnapshot.state === PlayerState.Playing, @@ -58,9 +53,6 @@ const Track = memo( isHighlight?: boolean onClick: (e: React.MouseEvent, trackID: number) => void }) => { - if (enableRenderLog) - console.debug(`Rendering TracksAlbum.tsx Track ${track.name}`) - const subtitle = useMemo( () => track.tns?.at(0) ?? track.alia?.at(0), [track.alia, track.tns] diff --git a/src/renderer/components/TracksList.tsx b/src/renderer/components/TracksList.tsx index bb5ed67..d4c9dc1 100644 --- a/src/renderer/components/TracksList.tsx +++ b/src/renderer/components/TracksList.tsx @@ -179,8 +179,6 @@ const TracksList = memo( isSkeleton?: boolean onTrackDoubleClick?: (trackID: number) => void }) => { - console.debug('Rendering TrackList.tsx TrackList') - // Fake data when isLoading is true const skeletonTracks: Track[] = new Array(12).fill({}) diff --git a/src/renderer/hooks/useIpcRenderer.ts b/src/renderer/hooks/useIpcRenderer.ts index eaf5f30..c295907 100644 --- a/src/renderer/hooks/useIpcRenderer.ts +++ b/src/renderer/hooks/useIpcRenderer.ts @@ -1,8 +1,7 @@ import { IpcChannelsParams, IpcChannelsReturns } from '@/shared/IpcChannels' import { useEffect } from 'react' - -const useIpcRenderer = ( +const useIpcRenderer = ( channcel: T, listener: (event: any, value: IpcChannelsReturns[T]) => void ) => { diff --git a/src/renderer/ipcRenderer.ts b/src/renderer/ipcRenderer.ts index ca5efe4..1787655 100644 --- a/src/renderer/ipcRenderer.ts +++ b/src/renderer/ipcRenderer.ts @@ -1,5 +1,9 @@ import { player } from '@/renderer/store' -import { IpcChannels, IpcChannelsReturns, IpcChannelsParams } from '@/shared/IpcChannels' +import { + IpcChannels, + IpcChannelsReturns, + IpcChannelsParams, +} from '@/shared/IpcChannels' const on = ( channel: T, diff --git a/src/renderer/pages/Album.tsx b/src/renderer/pages/Album.tsx index 8f40a85..2b70829 100644 --- a/src/renderer/pages/Album.tsx +++ b/src/renderer/pages/Album.tsx @@ -86,7 +86,9 @@ const Header = ({ return formatDuration(duration, 'zh-CN', 'hh[hr] mm[min]') }, [album?.songs]) - const [isCoverError, setCoverError] = useState(coverUrl.includes('3132508627578625')) + const [isCoverError, setCoverError] = useState( + coverUrl.includes('3132508627578625') + ) const { data: userAlbums } = useUserAlbums() const isThisAlbumLiked = useMemo(() => { @@ -136,7 +138,7 @@ const Header = ({ coverUrl && ( setCoverError(true)} /> ) diff --git a/src/renderer/pages/Playlist.tsx b/src/renderer/pages/Playlist.tsx index 7f1ee2a..3e912ba 100644 --- a/src/renderer/pages/Playlist.tsx +++ b/src/renderer/pages/Playlist.tsx @@ -18,8 +18,6 @@ import { State as PlayerState, } from '@/renderer/utils/player' -const enableRenderLog = true - const PlayButton = ({ playlist, handlePlay, @@ -76,7 +74,6 @@ const Header = memo( isLoading: boolean handlePlay: () => void }) => { - if (enableRenderLog) console.debug('Rendering Playlist.tsx Header') const coverUrl = resizeImage(playlist?.coverImgUrl || '', 'lg') const mutationLikeAPlaylist = useMutationLikeAPlaylist() @@ -225,8 +222,6 @@ const Tracks = memo( handlePlay: (trackID: number | null) => void isLoadingPlaylist: boolean }) => { - if (enableRenderLog) console.debug('Rendering Playlist.tsx Tracks') - const { data: tracksPages, hasNextPage, @@ -281,8 +276,6 @@ const Tracks = memo( Tracks.displayName = 'Tracks' const Playlist = () => { - if (enableRenderLog) console.debug('Rendering Playlist.tsx Playlist') - const params = useParams() const { data: playlist, isLoading } = usePlaylist({ id: Number(params.id) || 0, diff --git a/src/renderer/pages/Settings/Appearance.tsx b/src/renderer/pages/Settings/Appearance.tsx index 362467c..5beead6 100644 --- a/src/renderer/pages/Settings/Appearance.tsx +++ b/src/renderer/pages/Settings/Appearance.tsx @@ -35,10 +35,15 @@ const AccentColor = () => { {Object.entries(colors).map(([color, bg]) => (
changeColor(color)} > - {color === accentColor &&
} + {color === accentColor && ( +
+ )}
))} @@ -47,12 +52,12 @@ const AccentColor = () => { } const Theme = () => { - return
-
主题
-
- + return ( +
+
主题
+
-
+ ) } const Appearance = () => { diff --git a/src/renderer/pages/Settings/Settings.tsx b/src/renderer/pages/Settings/Settings.tsx index 31dc9e4..ef0539e 100644 --- a/src/renderer/pages/Settings/Settings.tsx +++ b/src/renderer/pages/Settings/Settings.tsx @@ -2,6 +2,7 @@ import Avatar from '@/renderer/components/Avatar' import SvgIcon from '@/renderer/components/SvgIcon' import useUser from '@/renderer/hooks/useUser' import Appearance from './Appearance' +import UnblockNeteaseMusic from './UnblockNeteaseMusic' const UserCard = () => { const { data: user } = useUser() @@ -42,17 +43,30 @@ const UserCard = () => { ) } -const Sidebar = () => { - const categories = ['外观', '播放', '歌词', '其他', '试验性功能'] - const active = '外观' +const Sidebar = ({ + activeCategory, + setActiveCategory, +}: { + activeCategory: string + setActiveCategory: (category: string) => void +}) => { + const categories = [ + '外观', + '播放', + '歌词', + '其他', + 'UnblockNeteaseMusic', + '试验性功能', + ] return (
{categories.map(category => (
setActiveCategory(category)} className={classNames( 'btn-hover-animation my-px flex cursor-default items-center justify-between rounded-lg px-3 py-2 font-medium transition-colors duration-200 after:scale-[0.97] after:bg-black/[.06] dark:text-white dark:after:bg-white/10', - active === category + activeCategory === category ? 'text-black after:scale-100 after:opacity-100' : 'text-gray-600' )} @@ -65,14 +79,20 @@ const Sidebar = () => { } const Settings = () => { + const [activeCategory, setActiveCategory] = useState('外观') + return (
- +
- + {activeCategory === '外观' && } + {activeCategory === 'UnblockNeteaseMusic' && }
diff --git a/src/renderer/pages/Settings/UnblockNeteaseMusic.tsx b/src/renderer/pages/Settings/UnblockNeteaseMusic.tsx new file mode 100644 index 0000000..3b9c9b7 --- /dev/null +++ b/src/renderer/pages/Settings/UnblockNeteaseMusic.tsx @@ -0,0 +1,44 @@ +const UnblockNeteaseMusic = () => { + return ( +
+
+ UnblockNeteaseMusic +
+
+ +
+ 音源: +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ ) +} + +export default UnblockNeteaseMusic diff --git a/src/renderer/store.ts b/src/renderer/store.ts index 9b174ca..5d74bbe 100644 --- a/src/renderer/store.ts +++ b/src/renderer/store.ts @@ -1,37 +1,27 @@ import { proxy, subscribe } from 'valtio' import { devtools } from 'valtio/utils' import { Player } from '@/renderer/utils/player' -import {merge} from 'lodash-es' - -interface Store { - uiStates: { - loginPhoneCountryCode: string - showLyricPanel: boolean - } - settings: { - showSidebar: boolean - accentColor: string - } -} - -const initialState: Store = { - uiStates: { - loginPhoneCountryCode: '+86', - showLyricPanel: false, - }, - settings: { - showSidebar: true, - accentColor: 'blue', - }, -} +import { merge } from 'lodash-es' +import { IpcChannels } from '@/shared/IpcChannels' +import { Store, initialState } from '@/shared/store' const stateInLocalStorage = localStorage.getItem('state') export const state = proxy( - merge(initialState, stateInLocalStorage ? JSON.parse(stateInLocalStorage) : {}) + merge(initialState, [ + stateInLocalStorage ? JSON.parse(stateInLocalStorage) : {}, + { + uiStates: { + showLyricPanel: false, + }, + }, + ]) ) subscribe(state, () => { localStorage.setItem('state', JSON.stringify(state)) }) +subscribe(state.settings, () => { + window.ipcRenderer?.send(IpcChannels.SyncSettings, { ...state.settings }) +}) // player const playerInLocalStorage = localStorage.getItem('player') diff --git a/src/renderer/utils/player.ts b/src/renderer/utils/player.ts index e51aead..2076f73 100644 --- a/src/renderer/utils/player.ts +++ b/src/renderer/utils/player.ts @@ -224,15 +224,19 @@ export class Player { } if (this.trackID !== id) return Howler.unload() + const url = audio.includes('?') + ? `${audio}&ypm-id=${id}` + : `${audio}?ypm-id=${id}` const howler = new Howl({ - src: [`${audio}?id=${id}`], - format: ['mp3', 'flac'], + src: [url], + format: ['mp3', 'flac', 'webm'], html5: true, autoplay, volume: 1, onend: () => this._howlerOnEndCallback(), }) _howler = howler + window.howler = howler if (autoplay) { this.play() this.state = State.Playing @@ -257,7 +261,7 @@ export class Player { private _cacheAudio(audio: string) { if (audio.includes('yesplaymusic')) return - const id = Number(audio.split('?id=')[1]) + const id = Number(new URL(audio).searchParams.get('ypm-id')) if (isNaN(id) || !id) return cacheAudio(id, audio) } diff --git a/src/shared/IpcChannels.ts b/src/shared/IpcChannels.ts index c2f09e6..ec12520 100644 --- a/src/shared/IpcChannels.ts +++ b/src/shared/IpcChannels.ts @@ -1,5 +1,6 @@ import { APIs } from './CacheAPIs' import { RepeatMode } from './playerDataTypes' +import { Store } from '@/shared/store' export const enum IpcChannels { ClearAPICache = 'clear-api-cache', @@ -19,6 +20,7 @@ export const enum IpcChannels { Previous = 'previous', Like = 'like', Repeat = 'repeat', + SyncSettings = 'sync-settings', } // ipcMain.on params @@ -51,6 +53,7 @@ export interface IpcChannelsParams { [IpcChannels.Repeat]: { mode: RepeatMode } + [IpcChannels.SyncSettings]: Store['settings'] } // ipcRenderer.on params diff --git a/src/shared/store.ts b/src/shared/store.ts new file mode 100644 index 0000000..1e1871f --- /dev/null +++ b/src/shared/store.ts @@ -0,0 +1,46 @@ +export interface Store { + uiStates: { + loginPhoneCountryCode: string + showLyricPanel: boolean + } + settings: { + showSidebar: boolean + accentColor: string + unm: { + enabled: boolean + sources: Array< + 'migu' | 'kuwo' | 'kugou' | 'ytdl' | 'qq' | 'bilibili' | 'joox' + > + searchMode: 'order-first' | 'fast-first' + proxy: null | { + protocol: 'http' | 'https' | 'socks5' + host: string + port: number + username?: string + password?: string + } + cookies: { + qq?: string + joox?: string + } + } + } +} + +export const initialState: Store = { + uiStates: { + loginPhoneCountryCode: '+86', + showLyricPanel: false, + }, + settings: { + showSidebar: true, + accentColor: 'blue', + unm: { + enabled: true, + sources: ['migu'], + searchMode: 'order-first', + proxy: null, + cookies: {}, + }, + }, +} diff --git a/tailwind.config.js b/tailwind.config.js index 53bf61b..95b6f42 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const colors = require('tailwindcss/colors') -const pickedColors = require('./scripts/generate.accent.color.css.js') +const pickedColors = require('./scripts/pickedColors.js') module.exports = { content: [