feat: 支持 UNM rust

This commit is contained in:
qier222 2022-05-01 19:53:25 +08:00
parent 4d59401549
commit 4d54060a4f
No known key found for this signature in database
GPG key ID: 9C85007ED905F14D
29 changed files with 717 additions and 231 deletions

View file

@ -41,6 +41,7 @@
"dependencies": { "dependencies": {
"@sentry/node": "^6.19.7", "@sentry/node": "^6.19.7",
"@sentry/tracing": "^6.19.7", "@sentry/tracing": "^6.19.7",
"@unblockneteasemusic/rust-napi": "^0.3.0-pre.1",
"NeteaseCloudMusicApi": "^4.5.12", "NeteaseCloudMusicApi": "^4.5.12",
"better-sqlite3": "7.5.1", "better-sqlite3": "7.5.1",
"change-case": "^4.1.2", "change-case": "^4.1.2",

138
pnpm-lock.yaml generated
View file

@ -22,6 +22,7 @@ specifiers:
'@types/react-dom': ^18.0.1 '@types/react-dom': ^18.0.1
'@typescript-eslint/eslint-plugin': ^5.21.0 '@typescript-eslint/eslint-plugin': ^5.21.0
'@typescript-eslint/parser': ^5.21.0 '@typescript-eslint/parser': ^5.21.0
'@unblockneteasemusic/rust-napi': ^0.3.0-pre.1
'@vitejs/plugin-react': ^1.3.1 '@vitejs/plugin-react': ^1.3.1
'@vitest/ui': ^0.10.0 '@vitest/ui': ^0.10.0
NeteaseCloudMusicApi: ^4.5.12 NeteaseCloudMusicApi: ^4.5.12
@ -88,6 +89,7 @@ specifiers:
dependencies: dependencies:
'@sentry/node': 6.19.7 '@sentry/node': 6.19.7
'@sentry/tracing': 6.19.7 '@sentry/tracing': 6.19.7
'@unblockneteasemusic/rust-napi': 0.3.0-pre.1
NeteaseCloudMusicApi: 4.5.12 NeteaseCloudMusicApi: 4.5.12
better-sqlite3: 7.5.1 better-sqlite3: 7.5.1
change-case: 4.1.2 change-case: 4.1.2
@ -1212,6 +1214,142 @@ packages:
eslint-visitor-keys: 3.3.0 eslint-visitor-keys: 3.3.0
dev: true 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: /@vitejs/plugin-react/1.3.1:
resolution: {integrity: sha512-qQS8Y2fZCjo5YmDUplEXl3yn+aueiwxB7BaoQ4nWYJYR+Ai8NXPVLlkLobVMs5+DeyFyg9Lrz6zCzdX1opcvyw==} resolution: {integrity: sha512-qQS8Y2fZCjo5YmDUplEXl3yn+aueiwxB7BaoQ4nWYJYR+Ai8NXPVLlkLobVMs5+DeyFyg9Lrz6zCzdX1opcvyw==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}

View file

@ -2,7 +2,7 @@
const { colord } = require('colord') const { colord } = require('colord')
const colors = require('tailwindcss/colors') const colors = require('tailwindcss/colors')
const replaceBrandColorWithCssVar = () => { const replaceBrandColorWithCSSVar = () => {
const blues = Object.entries(colors.blue).map(([key, value]) => { const blues = Object.entries(colors.blue).map(([key, value]) => {
const c = colord(value).toRgb() const c = colord(value).toRgb()
return { return {
@ -11,7 +11,7 @@ const replaceBrandColorWithCssVar = () => {
} }
}) })
return { return {
postcssPlugin: 'replaceBrandColorWithCssVar', postcssPlugin: 'replaceBrandColorWithCSSVar',
Declaration(decl) { Declaration(decl) {
let value = decl.value let value = decl.value
blues.forEach(blue => { blues.forEach(blue => {
@ -33,12 +33,12 @@ const replaceBrandColorWithCssVar = () => {
}, },
} }
} }
replaceBrandColorWithCssVar.postcss = true replaceBrandColorWithCSSVar.postcss = true
module.exports = { module.exports = {
plugins: [ plugins: [
require('tailwindcss'), require('tailwindcss'),
require('autoprefixer'), require('autoprefixer'),
replaceBrandColorWithCssVar, replaceBrandColorWithCSSVar,
], ],
} }

View file

@ -37,6 +37,7 @@ const options = {
'electron', 'electron',
'NeteaseCloudMusicApi', 'NeteaseCloudMusicApi',
'better-sqlite3', 'better-sqlite3',
'@unblockneteasemusic/rust-napi'
], ],
} }

View file

@ -1,30 +1,9 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
const colors = require('tailwindcss/colors')
const { colord } = require('colord') const { colord } = require('colord')
const prettier = require('prettier') const prettier = require('prettier')
const fs = require('fs') const fs = require('fs')
const prettierConfig = require('../prettier.config.js') const prettierConfig = require('../prettier.config.js')
const pickedColors = require('./pickedColors.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 colorsCss = {} const colorsCss = {}
Object.entries(pickedColors).forEach(([name, colors]) => { 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' }) const formatted = prettier.format(css, { ...prettierConfig, parser: 'css' })
fs.writeFileSync('./src/renderer/styles/accentColor.scss', formatted) fs.writeFileSync('./src/renderer/styles/accentColor.scss', formatted)

24
scripts/pickedColors.js Normal file
View file

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

View file

@ -6,6 +6,7 @@ import log from './log'
import fs from 'fs' import fs from 'fs'
import * as musicMetadata from 'music-metadata' import * as musicMetadata from 'music-metadata'
import { APIs, APIsParams, APIsResponse } from '../shared/CacheAPIs' import { APIs, APIsParams, APIsResponse } from '../shared/CacheAPIs'
import { TablesStructures } from './db'
class Cache { class Cache {
constructor() { constructor() {
@ -206,59 +207,6 @@ class Cache {
return 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) { getAudio(fileName: string, res: Response) {
@ -279,17 +227,17 @@ class Cache {
.status(206) .status(206)
.setHeader('Accept-Ranges', 'bytes') .setHeader('Accept-Ranges', 'bytes')
.setHeader('Connection', 'keep-alive') .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) .send(audio)
} catch (error) { } catch (error) {
res.status(500).send({ error }) res.status(500).send({ error })
} }
} }
async setAudio( async setAudio(buffer: Buffer, { id, url }: { id: number; url: string }) {
buffer: Buffer,
{ id, source }: { id: number; source: string }
) {
const path = `${app.getPath('userData')}/audio_cache` const path = `${app.getPath('userData')}/audio_cache`
try { try {
@ -299,16 +247,26 @@ class Cache {
} }
const meta = await musicMetadata.parseBuffer(buffer) const meta = await musicMetadata.parseBuffer(buffer)
const br = meta.format.bitrate const br =
const type = { meta?.format?.codec === 'OPUS' ? 165000 : meta.format.bitrate ?? 0
const type =
{
'MPEG 1 Layer 3': 'mp3', 'MPEG 1 Layer 3': 'mp3',
'Ogg Vorbis': 'ogg', 'Ogg Vorbis': 'ogg',
AAC: 'm4a', AAC: 'm4a',
FLAC: 'flac', FLAC: 'flac',
unknown: 'unknown', OPUS: 'opus',
}[meta.format.codec ?? 'unknown'] }[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) { if (error) {
return log.error(`[cache] cacheAudio failed: ${error}`) return log.error(`[cache] cacheAudio failed: ${error}`)
} }
@ -317,9 +275,9 @@ class Cache {
db.upsert(Tables.Audio, { db.upsert(Tables.Audio, {
id, id,
br, br,
type, type: type as TablesStructures[Tables.Audio]['type'],
source, source,
updateAt: Date.now(), updatedAt: Date.now(),
}) })
log.info(`[cache] cacheAudio ${id}-${br}.${type}`) log.info(`[cache] cacheAudio ${id}-${br}.${type}`)

View file

@ -38,9 +38,17 @@ export interface TablesStructures {
[Tables.Audio]: { [Tables.Audio]: {
id: number id: number
br: number br: number
type: 'mp3' | 'flac' | 'ogg' | 'wav' | 'm4a' | 'aac' | 'unknown' type: 'mp3' | 'flac' | 'ogg' | 'wav' | 'm4a' | 'aac' | 'unknown' | 'opus'
source: 'netease' | 'migu' | 'kuwo' | 'kugou' | 'youtube' source:
url: string | 'unknown'
| 'netease'
| 'migu'
| 'kuwo'
| 'kugou'
| 'youtube'
| 'qq'
| 'bilibili'
| 'joox'
updatedAt: number updatedAt: number
} }
[Tables.CoverColor]: { [Tables.CoverColor]: {

View file

@ -15,19 +15,21 @@ import { initIpcMain } from './ipcMain'
import { createTray, YPMTray } from './tray' import { createTray, YPMTray } from './tray'
import { IpcChannels } from '@/shared/IpcChannels' import { IpcChannels } from '@/shared/IpcChannels'
import { createTaskbar, Thumbar } from './windowsTaskbar' import { createTaskbar, Thumbar } from './windowsTaskbar'
import { Store as State, initialState } from '@/shared/store'
const isWindows = process.platform === 'win32' const isWindows = process.platform === 'win32'
const isMac = process.platform === 'darwin' const isMac = process.platform === 'darwin'
const isLinux = process.platform === 'linux' const isLinux = process.platform === 'linux'
const isDev = process.env.NODE_ENV === 'development' const isDev = process.env.NODE_ENV === 'development'
interface TypedElectronStore { export interface TypedElectronStore {
window: { window: {
width: number width: number
height: number height: number
x?: number x?: number
y?: number y?: number
} }
settings: State['settings']
} }
class Main { class Main {
@ -40,6 +42,7 @@ class Main {
width: 1440, width: 1440,
height: 960, height: 960,
}, },
settings: initialState.settings,
}, },
}) })
@ -65,7 +68,7 @@ class Main {
this.handleWindowEvents() this.handleWindowEvents()
this.createTray() this.createTray()
this.createThumbar() this.createThumbar()
initIpcMain(this.win, this.tray, this.thumbar) initIpcMain(this.win, this.tray, this.thumbar, this.store)
this.initDevTools() this.initDevTools()
}) })
} }
@ -129,6 +132,51 @@ class Main {
if (url.startsWith('https:')) shell.openExternal(url) if (url.startsWith('https:')) shell.openExternal(url)
return { action: 'deny' } 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() { handleWindowEvents() {

View file

@ -4,6 +4,8 @@ import { IpcChannels, IpcChannelsParams } from '../shared/IpcChannels'
import cache from './cache' import cache from './cache'
import log from './log' import log from './log'
import fs from 'fs' import fs from 'fs'
import Store from 'electron-store'
import { TypedElectronStore } from './index'
import { APIs } from '../shared/CacheAPIs' import { APIs } from '../shared/CacheAPIs'
import { YPMTray } from './tray' import { YPMTray } from './tray'
import { Thumbar } from './windowsTaskbar' import { Thumbar } from './windowsTaskbar'
@ -18,11 +20,14 @@ const on = <T extends keyof IpcChannelsParams>(
export function initIpcMain( export function initIpcMain(
win: BrowserWindow | null, win: BrowserWindow | null,
tray: YPMTray | null, tray: YPMTray | null,
thumbar: Thumbar | null thumbar: Thumbar | null,
store: Store<TypedElectronStore>
) { ) {
initWindowIpcMain(win) initWindowIpcMain(win)
initTrayIpcMain(tray) initTrayIpcMain(tray)
initTaskbarIpcMain(thumbar) initTaskbarIpcMain(thumbar)
initStoreIpcMain(store)
initOtherIpcMain()
} }
/** /**
@ -51,9 +56,7 @@ function initWindowIpcMain(win: BrowserWindow | null) {
function initTrayIpcMain(tray: YPMTray | null) { function initTrayIpcMain(tray: YPMTray | null) {
on(IpcChannels.SetTrayTooltip, (e, { text }) => tray?.setTooltip(text)) on(IpcChannels.SetTrayTooltip, (e, { text }) => tray?.setTooltip(text))
on(IpcChannels.Like, (e, { isLiked }) => on(IpcChannels.Like, (e, { isLiked }) => tray?.setLikeState(isLiked))
tray?.setLikeState(isLiked)
)
on(IpcChannels.Play, () => tray?.setPlayState(true)) on(IpcChannels.Play, () => tray?.setPlayState(true))
on(IpcChannels.Pause, () => tray?.setPlayState(false)) on(IpcChannels.Pause, () => tray?.setPlayState(false))
@ -71,9 +74,26 @@ function initTaskbarIpcMain(thumbar: Thumbar | null) {
} }
/** /**
* electron-store的事件
* @param {Store<TypedElectronStore>} store
*/
function initStoreIpcMain(store: Store<TypedElectronStore>) {
/**
* Main
*/
on(IpcChannels.SyncSettings, (event, settings) => {
store.set('settings', settings)
})
}
/**
*
*/
function initOtherIpcMain() {
/**
* API缓存 * API缓存
*/ */
on(IpcChannels.ClearAPICache, () => { on(IpcChannels.ClearAPICache, () => {
db.truncate(Tables.Track) db.truncate(Tables.Track)
db.truncate(Tables.Album) db.truncate(Tables.Album)
db.truncate(Tables.Artist) db.truncate(Tables.Artist)
@ -82,29 +102,29 @@ on(IpcChannels.ClearAPICache, () => {
db.truncate(Tables.AccountData) db.truncate(Tables.AccountData)
db.truncate(Tables.Audio) db.truncate(Tables.Audio)
db.vacuum() db.vacuum()
}) })
/** /**
* Get API cache * Get API cache
*/ */
on(IpcChannels.GetApiCacheSync, (event, args) => { on(IpcChannels.GetApiCacheSync, (event, args) => {
const { api, query } = args const { api, query } = args
const data = cache.get(api, query) const data = cache.get(api, query)
event.returnValue = data event.returnValue = data
}) })
/** /**
* *
*/ */
on(IpcChannels.CacheCoverColor, (event, args) => { on(IpcChannels.CacheCoverColor, (event, args) => {
const { id, color } = args const { id, color } = args
cache.set(APIs.CoverColor, { id, color }) cache.set(APIs.CoverColor, { id, color })
}) })
/** /**
* tables到json文件便table大小dev环境 * tables到json文件便table大小dev环境
*/ */
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
on(IpcChannels.DevDbExportJson, () => { on(IpcChannels.DevDbExportJson, () => {
const tables = [ const tables = [
Tables.ArtistAlbum, Tables.ArtistAlbum,
@ -119,12 +139,17 @@ if (process.env.NODE_ENV === 'development') {
tables.forEach(table => { tables.forEach(table => {
const data = db.findAll(table) const data = db.findAll(table)
fs.writeFile(`./tmp/${table}.json`, JSON.stringify(data), function (err) { fs.writeFile(
`./tmp/${table}.json`,
JSON.stringify(data),
function (err) {
if (err) { if (err) {
return console.log(err) return console.log(err)
} }
console.log('The file was saved!') console.log('The file was saved!')
}
)
}) })
}) })
}) }
} }

View file

@ -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 "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 "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 "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 "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 "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)); CREATE TABLE IF NOT EXISTS "Track" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));

View file

@ -5,6 +5,12 @@ import log from './log'
import cache from './cache' import cache from './cache'
import fileUpload from 'express-fileupload' import fileUpload from 'express-fileupload'
import path from 'path' 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 isDev = process.env.NODE_ENV === 'development'
const isProd = process.env.NODE_ENV === 'production' const isProd = process.env.NODE_ENV === 'production'
@ -21,6 +27,7 @@ class Server {
log.info('[server] starting http server') log.info('[server] starting http server')
this.app.use(cookieParser()) this.app.use(cookieParser())
this.app.use(fileUpload()) this.app.use(fileUpload())
this.getAudioUrlHandler()
this.neteaseHandler() this.neteaseHandler()
this.cacheAudioHandler() this.cacheAudioHandler()
this.serveStaticForProd() this.serveStaticForProd()
@ -32,7 +39,9 @@ class Server {
const neteaseApi = require('NeteaseCloudMusicApi') as (params: any) => any[] const neteaseApi = require('NeteaseCloudMusicApi') as (params: any) => any[]
Object.entries(neteaseApi).forEach(([name, handler]) => { Object.entries(neteaseApi).forEach(([name, handler]) => {
if (['serveNcmApi', 'getModulesDefinitions'].includes(name)) return if (['serveNcmApi', 'getModulesDefinitions', 'song_url'].includes(name)) {
return
}
name = pathCase(name) 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<FetchAudioSourceResponse | undefined> => {
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() { cacheAudioHandler() {
this.app.get( this.app.get(
'/yesplaymusic/audio/:filename', '/yesplaymusic/audio/:filename',
@ -103,8 +311,8 @@ class Server {
try { try {
await cache.setAudio(req.files.file.data, { await cache.setAudio(req.files.file.data, {
id: id, id,
source: 'netease', url: String(req.query.url) || '',
}) })
res.status(200).send('Audio cached!') res.status(200).send('Audio cached!')
} catch (error) { } catch (error) {

View file

@ -67,9 +67,7 @@ class ThumbarImpl implements Thumbar {
private _updateThumbarButtons(clear: boolean) { private _updateThumbarButtons(clear: boolean) {
this._win.setThumbarButtons( this._win.setThumbarButtons(
clear clear ? [] : [this._previous, this._playOrPause, this._next]
? []
: [this._previous, this._playOrPause, this._next]
) )
} }

View file

@ -12,7 +12,7 @@ const request: AxiosInstance = axios.create({
export async function cacheAudio(id: number, audio: string) { export async function cacheAudio(id: number, audio: string) {
const file = await axios.get(audio, { responseType: 'arraybuffer' }) 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 formData = new FormData()
const blob = new Blob([file.data], { type: 'multipart/form-data' }) const blob = new Blob([file.data], { type: 'multipart/form-data' })

View file

@ -12,7 +12,6 @@ import { lyricParser } from '@/renderer/utils/lyric'
const Lyric = ({ className }: { className?: string }) => { const Lyric = ({ className }: { className?: string }) => {
// const ease = [0.5, 0.2, 0.2, 0.8] // const ease = [0.5, 0.2, 0.2, 0.8]
console.log('rendering')
const playerSnapshot = useSnapshot(player) const playerSnapshot = useSnapshot(player)
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track]) const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])

View file

@ -9,13 +9,8 @@ import { player } from '@/renderer/store'
import { formatDuration } from '@/renderer/utils/common' import { formatDuration } from '@/renderer/utils/common'
import { State as PlayerState } from '@/renderer/utils/player' import { State as PlayerState } from '@/renderer/utils/player'
const enableRenderLog = true
const PlayOrPauseButtonInTrack = memo( const PlayOrPauseButtonInTrack = memo(
({ isHighlight, trackID }: { isHighlight: boolean; trackID: number }) => { ({ isHighlight, trackID }: { isHighlight: boolean; trackID: number }) => {
if (enableRenderLog)
console.debug(`Rendering TracksAlbum.tsx PlayOrPauseButtonInTrack`)
const playerSnapshot = useSnapshot(player) const playerSnapshot = useSnapshot(player)
const isPlaying = useMemo( const isPlaying = useMemo(
() => playerSnapshot.state === PlayerState.Playing, () => playerSnapshot.state === PlayerState.Playing,
@ -58,9 +53,6 @@ const Track = memo(
isHighlight?: boolean isHighlight?: boolean
onClick: (e: React.MouseEvent<HTMLElement>, trackID: number) => void onClick: (e: React.MouseEvent<HTMLElement>, trackID: number) => void
}) => { }) => {
if (enableRenderLog)
console.debug(`Rendering TracksAlbum.tsx Track ${track.name}`)
const subtitle = useMemo( const subtitle = useMemo(
() => track.tns?.at(0) ?? track.alia?.at(0), () => track.tns?.at(0) ?? track.alia?.at(0),
[track.alia, track.tns] [track.alia, track.tns]

View file

@ -179,8 +179,6 @@ const TracksList = memo(
isSkeleton?: boolean isSkeleton?: boolean
onTrackDoubleClick?: (trackID: number) => void onTrackDoubleClick?: (trackID: number) => void
}) => { }) => {
console.debug('Rendering TrackList.tsx TrackList')
// Fake data when isLoading is true // Fake data when isLoading is true
const skeletonTracks: Track[] = new Array(12).fill({}) const skeletonTracks: Track[] = new Array(12).fill({})

View file

@ -1,8 +1,7 @@
import { IpcChannelsParams, IpcChannelsReturns } from '@/shared/IpcChannels' import { IpcChannelsParams, IpcChannelsReturns } from '@/shared/IpcChannels'
import { useEffect } from 'react' import { useEffect } from 'react'
const useIpcRenderer = <T extends keyof IpcChannelsParams>(
const useIpcRenderer = <T extends keyof IpcChannelsParams> (
channcel: T, channcel: T,
listener: (event: any, value: IpcChannelsReturns[T]) => void listener: (event: any, value: IpcChannelsReturns[T]) => void
) => { ) => {

View file

@ -1,5 +1,9 @@
import { player } from '@/renderer/store' import { player } from '@/renderer/store'
import { IpcChannels, IpcChannelsReturns, IpcChannelsParams } from '@/shared/IpcChannels' import {
IpcChannels,
IpcChannelsReturns,
IpcChannelsParams,
} from '@/shared/IpcChannels'
const on = <T extends keyof IpcChannelsParams>( const on = <T extends keyof IpcChannelsParams>(
channel: T, channel: T,

View file

@ -86,7 +86,9 @@ const Header = ({
return formatDuration(duration, 'zh-CN', 'hh[hr] mm[min]') return formatDuration(duration, 'zh-CN', 'hh[hr] mm[min]')
}, [album?.songs]) }, [album?.songs])
const [isCoverError, setCoverError] = useState(coverUrl.includes('3132508627578625')) const [isCoverError, setCoverError] = useState(
coverUrl.includes('3132508627578625')
)
const { data: userAlbums } = useUserAlbums() const { data: userAlbums } = useUserAlbums()
const isThisAlbumLiked = useMemo(() => { const isThisAlbumLiked = useMemo(() => {
@ -136,7 +138,7 @@ const Header = ({
coverUrl && ( coverUrl && (
<img <img
src={coverUrl} src={coverUrl}
className='rounded-2xl border w-full border-b-0 border-black border-opacity-5 dark:border-white dark:border-opacity-5' className='w-full rounded-2xl border border-b-0 border-black border-opacity-5 dark:border-white dark:border-opacity-5'
onError={() => setCoverError(true)} onError={() => setCoverError(true)}
/> />
) )

View file

@ -18,8 +18,6 @@ import {
State as PlayerState, State as PlayerState,
} from '@/renderer/utils/player' } from '@/renderer/utils/player'
const enableRenderLog = true
const PlayButton = ({ const PlayButton = ({
playlist, playlist,
handlePlay, handlePlay,
@ -76,7 +74,6 @@ const Header = memo(
isLoading: boolean isLoading: boolean
handlePlay: () => void handlePlay: () => void
}) => { }) => {
if (enableRenderLog) console.debug('Rendering Playlist.tsx Header')
const coverUrl = resizeImage(playlist?.coverImgUrl || '', 'lg') const coverUrl = resizeImage(playlist?.coverImgUrl || '', 'lg')
const mutationLikeAPlaylist = useMutationLikeAPlaylist() const mutationLikeAPlaylist = useMutationLikeAPlaylist()
@ -225,8 +222,6 @@ const Tracks = memo(
handlePlay: (trackID: number | null) => void handlePlay: (trackID: number | null) => void
isLoadingPlaylist: boolean isLoadingPlaylist: boolean
}) => { }) => {
if (enableRenderLog) console.debug('Rendering Playlist.tsx Tracks')
const { const {
data: tracksPages, data: tracksPages,
hasNextPage, hasNextPage,
@ -281,8 +276,6 @@ const Tracks = memo(
Tracks.displayName = 'Tracks' Tracks.displayName = 'Tracks'
const Playlist = () => { const Playlist = () => {
if (enableRenderLog) console.debug('Rendering Playlist.tsx Playlist')
const params = useParams() const params = useParams()
const { data: playlist, isLoading } = usePlaylist({ const { data: playlist, isLoading } = usePlaylist({
id: Number(params.id) || 0, id: Number(params.id) || 0,

View file

@ -35,10 +35,15 @@ const AccentColor = () => {
{Object.entries(colors).map(([color, bg]) => ( {Object.entries(colors).map(([color, bg]) => (
<div <div
key={color} key={color}
className={classNames(bg, 'mr-2.5 h-5 w-5 rounded-full flex items-center justify-center')} className={classNames(
bg,
'mr-2.5 flex h-5 w-5 items-center justify-center rounded-full'
)}
onClick={() => changeColor(color)} onClick={() => changeColor(color)}
> >
{color === accentColor && <div className='bg-white h-1.5 w-1.5 rounded-full'></div>} {color === accentColor && (
<div className='h-1.5 w-1.5 rounded-full bg-white'></div>
)}
</div> </div>
))} ))}
</div> </div>
@ -47,12 +52,12 @@ const AccentColor = () => {
} }
const Theme = () => { const Theme = () => {
return <div className='mt-4'> return (
<div className='mt-4'>
<div className='mb-2 dark:text-white'></div> <div className='mb-2 dark:text-white'></div>
<div> <div></div>
</div>
</div> </div>
)
} }
const Appearance = () => { const Appearance = () => {

View file

@ -2,6 +2,7 @@ import Avatar from '@/renderer/components/Avatar'
import SvgIcon from '@/renderer/components/SvgIcon' import SvgIcon from '@/renderer/components/SvgIcon'
import useUser from '@/renderer/hooks/useUser' import useUser from '@/renderer/hooks/useUser'
import Appearance from './Appearance' import Appearance from './Appearance'
import UnblockNeteaseMusic from './UnblockNeteaseMusic'
const UserCard = () => { const UserCard = () => {
const { data: user } = useUser() const { data: user } = useUser()
@ -42,17 +43,30 @@ const UserCard = () => {
) )
} }
const Sidebar = () => { const Sidebar = ({
const categories = ['外观', '播放', '歌词', '其他', '试验性功能'] activeCategory,
const active = '外观' setActiveCategory,
}: {
activeCategory: string
setActiveCategory: (category: string) => void
}) => {
const categories = [
'外观',
'播放',
'歌词',
'其他',
'UnblockNeteaseMusic',
'试验性功能',
]
return ( return (
<div> <div>
{categories.map(category => ( {categories.map(category => (
<div <div
key={category} key={category}
onClick={() => setActiveCategory(category)}
className={classNames( 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', '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-black after:scale-100 after:opacity-100'
: 'text-gray-600' : 'text-gray-600'
)} )}
@ -65,14 +79,20 @@ const Sidebar = () => {
} }
const Settings = () => { const Settings = () => {
const [activeCategory, setActiveCategory] = useState('外观')
return ( return (
<div className='mt-6'> <div className='mt-6'>
<UserCard /> <UserCard />
<div className='mt-8 grid grid-cols-[12rem_auto] gap-10'> <div className='mt-8 grid grid-cols-[12rem_auto] gap-10'>
<Sidebar /> <Sidebar
activeCategory={activeCategory}
setActiveCategory={setActiveCategory}
/>
<div className=''> <div className=''>
<Appearance /> {activeCategory === '外观' && <Appearance />}
{activeCategory === 'UnblockNeteaseMusic' && <UnblockNeteaseMusic />}
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,44 @@
const UnblockNeteaseMusic = () => {
return (
<div>
<div className='text-xl font-medium text-gray-800 dark:text-white/70'>
UnblockNeteaseMusic
</div>
<div className='mt-3 h-px w-full bg-black/5 dark:bg-white/10'></div>
<div>
:
<div>
<input type='checkbox' id='migu' value='migu' />
<label htmlFor='migu'>migu</label>
</div>
<div>
<input type='checkbox' id='youtube' value='youtube' />
<label htmlFor='youtube'>youtube</label>
</div>
<div>
<input type='checkbox' id='kugou' value='kugou' />
<label htmlFor='kugou'>kugou</label>
</div>
<div>
<input type='checkbox' id='kuwo' value='kuwo' />
<label htmlFor='kuwo'>kuwo</label>
</div>
<div>
<input type='checkbox' id='qq' value='qq' />
<label htmlFor='qq'>qq</label>
</div>
<div>
<input type='checkbox' id='bilibili' value='bilibili' />
<label htmlFor='bilibili'>bilibili</label>
</div>
<div>
<input type='checkbox' id='joox' value='joox' />
<label htmlFor='joox'>joox</label>
</div>
</div>
</div>
)
}
export default UnblockNeteaseMusic

View file

@ -1,37 +1,27 @@
import { proxy, subscribe } from 'valtio' import { proxy, subscribe } from 'valtio'
import { devtools } from 'valtio/utils' import { devtools } from 'valtio/utils'
import { Player } from '@/renderer/utils/player' import { Player } from '@/renderer/utils/player'
import {merge} from 'lodash-es' import { merge } from 'lodash-es'
import { IpcChannels } from '@/shared/IpcChannels'
interface Store { import { Store, initialState } from '@/shared/store'
uiStates: {
loginPhoneCountryCode: string
showLyricPanel: boolean
}
settings: {
showSidebar: boolean
accentColor: string
}
}
const initialState: Store = {
uiStates: {
loginPhoneCountryCode: '+86',
showLyricPanel: false,
},
settings: {
showSidebar: true,
accentColor: 'blue',
},
}
const stateInLocalStorage = localStorage.getItem('state') const stateInLocalStorage = localStorage.getItem('state')
export const state = proxy<Store>( export const state = proxy<Store>(
merge(initialState, stateInLocalStorage ? JSON.parse(stateInLocalStorage) : {}) merge(initialState, [
stateInLocalStorage ? JSON.parse(stateInLocalStorage) : {},
{
uiStates: {
showLyricPanel: false,
},
},
])
) )
subscribe(state, () => { subscribe(state, () => {
localStorage.setItem('state', JSON.stringify(state)) localStorage.setItem('state', JSON.stringify(state))
}) })
subscribe(state.settings, () => {
window.ipcRenderer?.send(IpcChannels.SyncSettings, { ...state.settings })
})
// player // player
const playerInLocalStorage = localStorage.getItem('player') const playerInLocalStorage = localStorage.getItem('player')

View file

@ -224,15 +224,19 @@ export class Player {
} }
if (this.trackID !== id) return if (this.trackID !== id) return
Howler.unload() Howler.unload()
const url = audio.includes('?')
? `${audio}&ypm-id=${id}`
: `${audio}?ypm-id=${id}`
const howler = new Howl({ const howler = new Howl({
src: [`${audio}?id=${id}`], src: [url],
format: ['mp3', 'flac'], format: ['mp3', 'flac', 'webm'],
html5: true, html5: true,
autoplay, autoplay,
volume: 1, volume: 1,
onend: () => this._howlerOnEndCallback(), onend: () => this._howlerOnEndCallback(),
}) })
_howler = howler _howler = howler
window.howler = howler
if (autoplay) { if (autoplay) {
this.play() this.play()
this.state = State.Playing this.state = State.Playing
@ -257,7 +261,7 @@ export class Player {
private _cacheAudio(audio: string) { private _cacheAudio(audio: string) {
if (audio.includes('yesplaymusic')) return 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 if (isNaN(id) || !id) return
cacheAudio(id, audio) cacheAudio(id, audio)
} }

View file

@ -1,5 +1,6 @@
import { APIs } from './CacheAPIs' import { APIs } from './CacheAPIs'
import { RepeatMode } from './playerDataTypes' import { RepeatMode } from './playerDataTypes'
import { Store } from '@/shared/store'
export const enum IpcChannels { export const enum IpcChannels {
ClearAPICache = 'clear-api-cache', ClearAPICache = 'clear-api-cache',
@ -19,6 +20,7 @@ export const enum IpcChannels {
Previous = 'previous', Previous = 'previous',
Like = 'like', Like = 'like',
Repeat = 'repeat', Repeat = 'repeat',
SyncSettings = 'sync-settings',
} }
// ipcMain.on params // ipcMain.on params
@ -51,6 +53,7 @@ export interface IpcChannelsParams {
[IpcChannels.Repeat]: { [IpcChannels.Repeat]: {
mode: RepeatMode mode: RepeatMode
} }
[IpcChannels.SyncSettings]: Store['settings']
} }
// ipcRenderer.on params // ipcRenderer.on params

46
src/shared/store.ts Normal file
View file

@ -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: {},
},
},
}

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
const colors = require('tailwindcss/colors') const colors = require('tailwindcss/colors')
const pickedColors = require('./scripts/generate.accent.color.css.js') const pickedColors = require('./scripts/pickedColors.js')
module.exports = { module.exports = {
content: [ content: [