mirror of
https://github.com/qier222/YesPlayMusic.git
synced 2024-11-22 10:13:02 +08:00
feat: 支持 UNM rust
This commit is contained in:
parent
4d59401549
commit
4d54060a4f
|
@ -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",
|
||||
|
|
138
pnpm-lock.yaml
138
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'}
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ const options = {
|
|||
'electron',
|
||||
'NeteaseCloudMusicApi',
|
||||
'better-sqlite3',
|
||||
'@unblockneteasemusic/rust-napi'
|
||||
],
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
24
scripts/pickedColors.js
Normal file
24
scripts/pickedColors.js
Normal 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
|
|
@ -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}`)
|
||||
|
|
|
@ -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]: {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 = <T extends keyof IpcChannelsParams>(
|
|||
export function initIpcMain(
|
||||
win: BrowserWindow | null,
|
||||
tray: YPMTray | null,
|
||||
thumbar: Thumbar | null
|
||||
thumbar: Thumbar | null,
|
||||
store: Store<TypedElectronStore>
|
||||
) {
|
||||
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<TypedElectronStore>} 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<TypedElectronStore>) {
|
||||
/**
|
||||
* 同步设置到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!')
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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<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() {
|
||||
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) {
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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' })
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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<HTMLElement>, 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]
|
||||
|
|
|
@ -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({})
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { IpcChannelsParams, IpcChannelsReturns } from '@/shared/IpcChannels'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
|
||||
const useIpcRenderer = <T extends keyof IpcChannelsParams> (
|
||||
const useIpcRenderer = <T extends keyof IpcChannelsParams>(
|
||||
channcel: T,
|
||||
listener: (event: any, value: IpcChannelsReturns[T]) => void
|
||||
) => {
|
||||
|
|
|
@ -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 = <T extends keyof IpcChannelsParams>(
|
||||
channel: T,
|
||||
|
|
|
@ -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 && (
|
||||
<img
|
||||
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)}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -35,10 +35,15 @@ const AccentColor = () => {
|
|||
{Object.entries(colors).map(([color, bg]) => (
|
||||
<div
|
||||
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)}
|
||||
>
|
||||
{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>
|
||||
|
@ -47,12 +52,12 @@ const AccentColor = () => {
|
|||
}
|
||||
|
||||
const Theme = () => {
|
||||
return <div className='mt-4'>
|
||||
<div className='mb-2 dark:text-white'>主题</div>
|
||||
<div>
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='mb-2 dark:text-white'>主题</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Appearance = () => {
|
||||
|
|
|
@ -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 (
|
||||
<div>
|
||||
{categories.map(category => (
|
||||
<div
|
||||
key={category}
|
||||
onClick={() => 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 (
|
||||
<div className='mt-6'>
|
||||
<UserCard />
|
||||
|
||||
<div className='mt-8 grid grid-cols-[12rem_auto] gap-10'>
|
||||
<Sidebar />
|
||||
<Sidebar
|
||||
activeCategory={activeCategory}
|
||||
setActiveCategory={setActiveCategory}
|
||||
/>
|
||||
<div className=''>
|
||||
<Appearance />
|
||||
{activeCategory === '外观' && <Appearance />}
|
||||
{activeCategory === 'UnblockNeteaseMusic' && <UnblockNeteaseMusic />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
44
src/renderer/pages/Settings/UnblockNeteaseMusic.tsx
Normal file
44
src/renderer/pages/Settings/UnblockNeteaseMusic.tsx
Normal 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
|
|
@ -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<Store>(
|
||||
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')
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
46
src/shared/store.ts
Normal file
46
src/shared/store.ts
Normal 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: {},
|
||||
},
|
||||
},
|
||||
}
|
|
@ -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: [
|
||||
|
|
Loading…
Reference in New Issue
Block a user