mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-16 13:17:46 +00:00
feat: 缓存歌单封面颜色
This commit is contained in:
parent
766e866497
commit
1591586735
10 changed files with 506 additions and 396 deletions
15
src/main/CacheAPIsName.ts
Normal file
15
src/main/CacheAPIsName.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
export enum APIs {
|
||||||
|
UserPlaylist = 'user/playlist',
|
||||||
|
UserAccount = 'user/account',
|
||||||
|
Personalized = 'personalized',
|
||||||
|
RecommendResource = 'recommend/resource',
|
||||||
|
Likelist = 'likelist',
|
||||||
|
SongDetail = 'song/detail',
|
||||||
|
SongUrl = 'song/url',
|
||||||
|
Album = 'album',
|
||||||
|
PlaylistDetail = 'playlist/detail',
|
||||||
|
Artists = 'artists',
|
||||||
|
ArtistAlbum = 'artist/album',
|
||||||
|
Lyric = 'lyric',
|
||||||
|
CoverColor = 'cover_color',
|
||||||
|
}
|
||||||
|
|
@ -6,4 +6,5 @@ export enum IpcChannels {
|
||||||
IsMaximized = 'is-maximized',
|
IsMaximized = 'is-maximized',
|
||||||
GetApiCacheSync = 'get-api-cache-sync',
|
GetApiCacheSync = 'get-api-cache-sync',
|
||||||
DevDbExportJson = 'dev-db-export-json',
|
DevDbExportJson = 'dev-db-export-json',
|
||||||
|
CacheCoverColor = 'cache-cover-color',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,300 +5,322 @@ import { Request, Response } from 'express'
|
||||||
import logger from './logger'
|
import logger from './logger'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import * as musicMetadata from 'music-metadata'
|
import * as musicMetadata from 'music-metadata'
|
||||||
|
import { APIs } from './CacheAPIsName'
|
||||||
|
|
||||||
export async function setCache(api: string, data: any, query: any) {
|
class Cache {
|
||||||
switch (api) {
|
constructor() {
|
||||||
case 'user/playlist':
|
//
|
||||||
case 'user/account':
|
}
|
||||||
case 'personalized':
|
|
||||||
case 'recommend/resource':
|
set(api: string, data: any, query: any = {}) {
|
||||||
case 'likelist': {
|
switch (api) {
|
||||||
if (!data) return
|
case APIs.UserPlaylist:
|
||||||
db.upsert(Tables.ACCOUNT_DATA, {
|
case APIs.UserAccount:
|
||||||
id: api,
|
case APIs.Personalized:
|
||||||
json: JSON.stringify(data),
|
case APIs.RecommendResource:
|
||||||
updateAt: Date.now(),
|
case APIs.Likelist: {
|
||||||
})
|
if (!data) return
|
||||||
break
|
db.upsert(Tables.AccountData, {
|
||||||
}
|
id: api,
|
||||||
case 'song/detail': {
|
json: JSON.stringify(data),
|
||||||
if (!data.songs) return
|
updateAt: Date.now(),
|
||||||
const tracks = (data as FetchTracksResponse).songs.map(t => ({
|
})
|
||||||
id: t.id,
|
break
|
||||||
json: JSON.stringify(t),
|
}
|
||||||
updatedAt: Date.now(),
|
case APIs.SongDetail: {
|
||||||
}))
|
if (!data.songs) return
|
||||||
db.upsertMany(Tables.TRACK, tracks)
|
const tracks = (data as FetchTracksResponse).songs.map(t => ({
|
||||||
break
|
id: t.id,
|
||||||
}
|
json: JSON.stringify(t),
|
||||||
case 'album': {
|
|
||||||
if (!data.album) return
|
|
||||||
data.album.songs = data.songs
|
|
||||||
db.upsert(Tables.ALBUM, {
|
|
||||||
id: data.album.id,
|
|
||||||
json: JSON.stringify(data.album),
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'playlist/detail': {
|
|
||||||
if (!data.playlist) return
|
|
||||||
db.upsert(Tables.PLAYLIST, {
|
|
||||||
id: data.playlist.id,
|
|
||||||
json: JSON.stringify(data),
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'artists': {
|
|
||||||
if (!data.artist) return
|
|
||||||
db.upsert(Tables.ARTIST, {
|
|
||||||
id: data.artist.id,
|
|
||||||
json: JSON.stringify(data),
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'artist/album': {
|
|
||||||
if (!data.hotAlbums) return
|
|
||||||
db.createMany(
|
|
||||||
Tables.ALBUM,
|
|
||||||
data.hotAlbums.map((a: Album) => ({
|
|
||||||
id: a.id,
|
|
||||||
json: JSON.stringify(a),
|
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
}))
|
}))
|
||||||
)
|
db.upsertMany(Tables.Track, tracks)
|
||||||
const modifiedData = {
|
break
|
||||||
...data,
|
}
|
||||||
hotAlbums: data.hotAlbums.map((a: Album) => a.id),
|
case APIs.Album: {
|
||||||
|
if (!data.album) return
|
||||||
|
data.album.songs = data.songs
|
||||||
|
db.upsert(Tables.Album, {
|
||||||
|
id: data.album.id,
|
||||||
|
json: JSON.stringify(data.album),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case APIs.PlaylistDetail: {
|
||||||
|
if (!data.playlist) return
|
||||||
|
db.upsert(Tables.Playlist, {
|
||||||
|
id: data.playlist.id,
|
||||||
|
json: JSON.stringify(data),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case APIs.Artists: {
|
||||||
|
if (!data.artist) return
|
||||||
|
db.upsert(Tables.Artist, {
|
||||||
|
id: data.artist.id,
|
||||||
|
json: JSON.stringify(data),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case APIs.ArtistAlbum: {
|
||||||
|
if (!data.hotAlbums) return
|
||||||
|
db.createMany(
|
||||||
|
Tables.Album,
|
||||||
|
data.hotAlbums.map((a: Album) => ({
|
||||||
|
id: a.id,
|
||||||
|
json: JSON.stringify(a),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const modifiedData = {
|
||||||
|
...data,
|
||||||
|
hotAlbums: data.hotAlbums.map((a: Album) => a.id),
|
||||||
|
}
|
||||||
|
db.upsert(Tables.ArtistAlbum, {
|
||||||
|
id: data.artist.id,
|
||||||
|
json: JSON.stringify(modifiedData),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case APIs.Lyric: {
|
||||||
|
if (!data.lrc) return
|
||||||
|
db.upsert(Tables.Lyric, {
|
||||||
|
id: query.id,
|
||||||
|
json: JSON.stringify(data),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case APIs.CoverColor: {
|
||||||
|
if (!data.id || !data.color) return
|
||||||
|
if (/^#([a-fA-F0-9]){3}$|[a-fA-F0-9]{6}$/.test(data.color) === false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db.upsert(Tables.CoverColor, {
|
||||||
|
id: data.id,
|
||||||
|
color: data.color,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
db.upsert(Tables.ARTIST_ALBUMS, {
|
|
||||||
id: data.artist.id,
|
|
||||||
json: JSON.stringify(modifiedData),
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'lyric': {
|
|
||||||
if (!data.lrc) return
|
|
||||||
db.upsert(Tables.LYRIC, {
|
|
||||||
id: query.id,
|
|
||||||
json: JSON.stringify(data),
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export function getCache(api: string, query: any): any {
|
get(api: string, query: any): any {
|
||||||
switch (api) {
|
switch (api) {
|
||||||
case 'user/account':
|
case APIs.UserPlaylist:
|
||||||
case 'user/playlist':
|
case APIs.UserAccount:
|
||||||
case 'personalized':
|
case APIs.Personalized:
|
||||||
case 'recommend/resource':
|
case APIs.RecommendResource:
|
||||||
case 'likelist': {
|
case APIs.Likelist: {
|
||||||
const data = db.find(Tables.ACCOUNT_DATA, api)
|
const data = db.find(Tables.AccountData, api)
|
||||||
if (data?.json) return JSON.parse(data.json)
|
if (data?.json) return JSON.parse(data.json)
|
||||||
break
|
break
|
||||||
}
|
|
||||||
case 'song/detail': {
|
|
||||||
const ids: string[] = query?.ids.split(',')
|
|
||||||
if (ids.length === 0) return
|
|
||||||
|
|
||||||
let isIDsValid = true
|
|
||||||
ids.forEach(id => {
|
|
||||||
if (id === '' || isNaN(Number(id))) isIDsValid = false
|
|
||||||
})
|
|
||||||
if (!isIDsValid) return
|
|
||||||
|
|
||||||
const tracksRaw = db.findMany(Tables.TRACK, ids)
|
|
||||||
|
|
||||||
if (tracksRaw.length !== ids.length) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const tracks = ids.map(id => {
|
case APIs.SongDetail: {
|
||||||
const track = tracksRaw.find(t => t.id === Number(id)) as any
|
const ids: string[] = query?.ids.split(',')
|
||||||
return JSON.parse(track.json)
|
if (ids.length === 0) return
|
||||||
})
|
|
||||||
|
let isIDsValid = true
|
||||||
|
ids.forEach(id => {
|
||||||
|
if (id === '' || isNaN(Number(id))) isIDsValid = false
|
||||||
|
})
|
||||||
|
if (!isIDsValid) return
|
||||||
|
|
||||||
|
const tracksRaw = db.findMany(Tables.Track, ids)
|
||||||
|
|
||||||
|
if (tracksRaw.length !== ids.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const tracks = ids.map(id => {
|
||||||
|
const track = tracksRaw.find(t => t.id === Number(id)) as any
|
||||||
|
return JSON.parse(track.json)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
songs: tracks,
|
||||||
|
privileges: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case APIs.Album: {
|
||||||
|
if (isNaN(Number(query?.id))) return
|
||||||
|
const data = db.find(Tables.Album, query.id)
|
||||||
|
if (data?.json)
|
||||||
|
return {
|
||||||
|
resourceState: true,
|
||||||
|
songs: [],
|
||||||
|
code: 200,
|
||||||
|
album: JSON.parse(data.json),
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case APIs.PlaylistDetail: {
|
||||||
|
if (isNaN(Number(query?.id))) return
|
||||||
|
const data = db.find(Tables.Playlist, query.id)
|
||||||
|
if (data?.json) return JSON.parse(data.json)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case APIs.Artists: {
|
||||||
|
if (isNaN(Number(query?.id))) return
|
||||||
|
const data = db.find(Tables.Artist, query.id)
|
||||||
|
if (data?.json) return JSON.parse(data.json)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case APIs.ArtistAlbum: {
|
||||||
|
if (isNaN(Number(query?.id))) return
|
||||||
|
|
||||||
|
const artistAlbumsRaw = db.find(Tables.ArtistAlbum, query.id)
|
||||||
|
if (!artistAlbumsRaw?.json) return
|
||||||
|
const artistAlbums = JSON.parse(artistAlbumsRaw.json)
|
||||||
|
|
||||||
|
const albumsRaw = db.findMany(Tables.Album, artistAlbums.hotAlbums)
|
||||||
|
if (albumsRaw.length !== artistAlbums.hotAlbums.length) return
|
||||||
|
const albums = albumsRaw.map(a => JSON.parse(a.json))
|
||||||
|
|
||||||
|
artistAlbums.hotAlbums = artistAlbums.hotAlbums.map((id: number) =>
|
||||||
|
albums.find(a => a.id === id)
|
||||||
|
)
|
||||||
|
return artistAlbums
|
||||||
|
}
|
||||||
|
case APIs.Lyric: {
|
||||||
|
if (isNaN(Number(query?.id))) return
|
||||||
|
const data = db.find(Tables.Lyric, query.id)
|
||||||
|
if (data?.json) return JSON.parse(data.json)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case APIs.CoverColor: {
|
||||||
|
if (isNaN(Number(query?.id))) return
|
||||||
|
return db.find(Tables.CoverColor, query.id)?.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getForExpress(api: string, req: Request) {
|
||||||
|
// Get track detail cache
|
||||||
|
if (api === APIs.SongDetail) {
|
||||||
|
const cache = this.get(api, req.query)
|
||||||
|
if (cache) {
|
||||||
|
logger.debug(`[cache] Cache hit for ${req.path}`)
|
||||||
|
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
|
||||||
|
|
||||||
|
logger.debug(`[cache] Audio cache hit for ${req.path}`)
|
||||||
|
|
||||||
return {
|
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,
|
code: 200,
|
||||||
songs: tracks,
|
|
||||||
privileges: {},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'album': {
|
|
||||||
if (isNaN(Number(query?.id))) return
|
|
||||||
const data = db.find(Tables.ALBUM, query.id)
|
|
||||||
if (data?.json)
|
|
||||||
return {
|
|
||||||
resourceState: true,
|
|
||||||
songs: [],
|
|
||||||
code: 200,
|
|
||||||
album: JSON.parse(data.json),
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'playlist/detail': {
|
|
||||||
if (isNaN(Number(query?.id))) return
|
|
||||||
const data = db.find(Tables.PLAYLIST, query.id)
|
|
||||||
if (data?.json) return JSON.parse(data.json)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'artists': {
|
|
||||||
if (isNaN(Number(query?.id))) return
|
|
||||||
const data = db.find(Tables.ARTIST, query.id)
|
|
||||||
if (data?.json) return JSON.parse(data.json)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'artist/album': {
|
|
||||||
if (isNaN(Number(query?.id))) return
|
|
||||||
|
|
||||||
const artistAlbumsRaw = db.find(Tables.ARTIST_ALBUMS, query.id)
|
|
||||||
if (!artistAlbumsRaw?.json) return
|
|
||||||
const artistAlbums = JSON.parse(artistAlbumsRaw.json)
|
|
||||||
|
|
||||||
const albumsRaw = db.findMany(Tables.ALBUM, artistAlbums.hotAlbums)
|
|
||||||
if (albumsRaw.length !== artistAlbums.hotAlbums.length) return
|
|
||||||
const albums = albumsRaw.map(a => JSON.parse(a.json))
|
|
||||||
|
|
||||||
artistAlbums.hotAlbums = artistAlbums.hotAlbums.map((id: number) =>
|
|
||||||
albums.find(a => a.id === id)
|
|
||||||
)
|
|
||||||
return artistAlbums
|
|
||||||
}
|
|
||||||
case 'lyric': {
|
|
||||||
if (isNaN(Number(query?.id))) return
|
|
||||||
const data = db.find(Tables.LYRIC, query.id)
|
|
||||||
if (data?.json) return JSON.parse(data.json)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCacheForExpress(api: string, req: Request) {
|
getAudio(fileName: string, res: Response) {
|
||||||
// Get track detail cache
|
if (!fileName) {
|
||||||
if (api === 'song/detail') {
|
return res.status(400).send({ error: 'No filename provided' })
|
||||||
const cache = getCache(api, req.query)
|
}
|
||||||
if (cache) {
|
const id = Number(fileName.split('-')[0])
|
||||||
logger.debug(`[cache] Cache hit for ${req.path}`)
|
|
||||||
return cache
|
try {
|
||||||
|
const path = `${app.getPath('userData')}/audio_cache/${fileName}`
|
||||||
|
const audio = fs.readFileSync(path)
|
||||||
|
if (audio.byteLength === 0) {
|
||||||
|
db.delete(Tables.Audio, id)
|
||||||
|
fs.unlinkSync(path)
|
||||||
|
return res.status(404).send({ error: 'Audio not found' })
|
||||||
|
}
|
||||||
|
res.send(audio)
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ error })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get audio cache if API is song/detail
|
async setAudio(
|
||||||
if (api === 'song/url') {
|
buffer: Buffer,
|
||||||
const cache = db.find(Tables.AUDIO, Number(req.query.id))
|
{ id, source }: { id: number; source: string }
|
||||||
if (!cache) return
|
) {
|
||||||
|
const path = `${app.getPath('userData')}/audio_cache`
|
||||||
|
|
||||||
const audioFileName = `${cache.id}-${cache.br}.${cache.type}`
|
try {
|
||||||
|
fs.statSync(path)
|
||||||
const isAudioFileExists = fs.existsSync(
|
} catch (e) {
|
||||||
`${app.getPath('userData')}/audio_cache/${audioFileName}`
|
fs.mkdirSync(path)
|
||||||
)
|
|
||||||
if (!isAudioFileExists) return
|
|
||||||
|
|
||||||
logger.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,
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAudioCache(fileName: string, res: Response) {
|
const meta = await musicMetadata.parseBuffer(buffer)
|
||||||
if (!fileName) {
|
const br = meta.format.bitrate
|
||||||
return res.status(400).send({ error: 'No filename provided' })
|
const type = {
|
||||||
}
|
'MPEG 1 Layer 3': 'mp3',
|
||||||
const id = Number(fileName.split('-')[0])
|
'Ogg Vorbis': 'ogg',
|
||||||
|
AAC: 'm4a',
|
||||||
|
FLAC: 'flac',
|
||||||
|
unknown: 'unknown',
|
||||||
|
}[meta.format.codec ?? 'unknown']
|
||||||
|
|
||||||
try {
|
await fs.writeFile(`${path}/${id}-${br}.${type}`, buffer, error => {
|
||||||
const path = `${app.getPath('userData')}/audio_cache/${fileName}`
|
if (error) {
|
||||||
const audio = fs.readFileSync(path)
|
return logger.error(`[cache] cacheAudio failed: ${error}`)
|
||||||
if (audio.byteLength === 0) {
|
}
|
||||||
db.delete(Tables.AUDIO, id)
|
logger.info(`Audio file ${id}-${br}.${type} cached!`)
|
||||||
fs.unlinkSync(path)
|
|
||||||
return res.status(404).send({ error: 'Audio not found' })
|
|
||||||
}
|
|
||||||
res.send(audio)
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).send({ error })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache audio info local folder
|
db.upsert(Tables.Audio, {
|
||||||
export async function cacheAudio(
|
id,
|
||||||
buffer: Buffer,
|
br,
|
||||||
{ id, source }: { id: number; source: string }
|
type,
|
||||||
) {
|
source,
|
||||||
const path = `${app.getPath('userData')}/audio_cache`
|
updateAt: Date.now(),
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
logger.info(`[cache] cacheAudio ${id}-${br}.${type}`)
|
||||||
fs.statSync(path)
|
|
||||||
} catch (e) {
|
|
||||||
fs.mkdirSync(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
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']
|
|
||||||
|
|
||||||
await fs.writeFile(`${path}/${id}-${br}.${type}`, buffer, error => {
|
|
||||||
if (error) {
|
|
||||||
return logger.error(`[cache] cacheAudio failed: ${error}`)
|
|
||||||
}
|
|
||||||
logger.info(`Audio file ${id}-${br}.${type} cached!`)
|
|
||||||
|
|
||||||
db.upsert(Tables.AUDIO, {
|
|
||||||
id,
|
|
||||||
br,
|
|
||||||
type,
|
|
||||||
source,
|
|
||||||
updateAt: Date.now(),
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
logger.info(`[cache] cacheAudio ${id}-${br}.${type}`)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default new Cache()
|
||||||
|
|
|
||||||
157
src/main/db.ts
157
src/main/db.ts
|
|
@ -7,110 +7,127 @@ import { createFileIfNotExist } from './utils'
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development'
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
logger.info('[db] Initializing database...')
|
|
||||||
|
|
||||||
export enum Tables {
|
export enum Tables {
|
||||||
TRACK = 'track',
|
Track = 'track',
|
||||||
ALBUM = 'album',
|
Album = 'album',
|
||||||
ARTIST = 'artist',
|
Artist = 'artist',
|
||||||
PLAYLIST = 'playlist',
|
Playlist = 'playlist',
|
||||||
ARTIST_ALBUMS = 'artist_album',
|
ArtistAlbum = 'artist_album',
|
||||||
LYRIC = 'lyric',
|
Lyric = 'lyric',
|
||||||
|
|
||||||
// Special tables
|
// Special tables
|
||||||
ACCOUNT_DATA = 'account_data',
|
AccountData = 'account_data',
|
||||||
AUDIO = 'audio',
|
Audio = 'audio',
|
||||||
|
CoverColor = 'cover_color',
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbFilePath = path.resolve(
|
class DB {
|
||||||
app.getPath('userData'),
|
sqlite: SQLite3.Database
|
||||||
'./api_cache/db.sqlite'
|
dbFilePath: string = path.resolve(
|
||||||
)
|
app.getPath('userData'),
|
||||||
createFileIfNotExist(dbFilePath)
|
'./api_cache/db.sqlite'
|
||||||
|
|
||||||
const sqlite = new SQLite3(dbFilePath, {
|
|
||||||
nativeBinding: path.join(__dirname, `./better_sqlite3_${process.arch}.node`),
|
|
||||||
})
|
|
||||||
sqlite.pragma('auto_vacuum = FULL')
|
|
||||||
|
|
||||||
// Init tables if not exist
|
|
||||||
const trackTable = sqlite
|
|
||||||
.prepare("SELECT * FROM sqlite_master WHERE name='track' and type='table'")
|
|
||||||
.get()
|
|
||||||
if (!trackTable) {
|
|
||||||
const migration = fs.readFileSync(
|
|
||||||
isDev
|
|
||||||
? path.join(process.cwd(), './src/main/migrations/init.sql')
|
|
||||||
: path.join(__dirname, './migrations/init.sql'),
|
|
||||||
'utf8'
|
|
||||||
)
|
)
|
||||||
sqlite.exec(migration)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const db = {
|
constructor() {
|
||||||
find: (table: Tables, key: number | string) => {
|
logger.info('[db] Initializing database...')
|
||||||
return sqlite
|
|
||||||
|
createFileIfNotExist(this.dbFilePath)
|
||||||
|
|
||||||
|
this.sqlite = new SQLite3(this.dbFilePath, {
|
||||||
|
nativeBinding: path.join(
|
||||||
|
__dirname,
|
||||||
|
`./better_sqlite3_${process.arch}.node`
|
||||||
|
),
|
||||||
|
})
|
||||||
|
this.sqlite.pragma('auto_vacuum = FULL')
|
||||||
|
this.initTables()
|
||||||
|
|
||||||
|
logger.info('[db] Database initialized')
|
||||||
|
}
|
||||||
|
|
||||||
|
initTables() {
|
||||||
|
const migration = fs.readFileSync(
|
||||||
|
isDev
|
||||||
|
? path.join(process.cwd(), './src/main/migrations/init.sql')
|
||||||
|
: path.join(__dirname, './migrations/init.sql'),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
this.sqlite.exec(migration)
|
||||||
|
}
|
||||||
|
|
||||||
|
find(table: Tables, key: number | string) {
|
||||||
|
return this.sqlite
|
||||||
.prepare(`SELECT * FROM ${table} WHERE id = ? LIMIT 1`)
|
.prepare(`SELECT * FROM ${table} WHERE id = ? LIMIT 1`)
|
||||||
.get(key)
|
.get(key)
|
||||||
},
|
}
|
||||||
findMany: (table: Tables, keys: number[] | string[]) => {
|
|
||||||
|
findMany(table: Tables, keys: number[] | string[]) {
|
||||||
const idsQuery = keys.map(key => `id = ${key}`).join(' OR ')
|
const idsQuery = keys.map(key => `id = ${key}`).join(' OR ')
|
||||||
return sqlite.prepare(`SELECT * FROM ${table} WHERE ${idsQuery}`).all()
|
return this.sqlite.prepare(`SELECT * FROM ${table} WHERE ${idsQuery}`).all()
|
||||||
},
|
}
|
||||||
findAll: (table: Tables) => {
|
|
||||||
return sqlite.prepare(`SELECT * FROM ${table}`).all()
|
findAll(table: Tables) {
|
||||||
},
|
return this.sqlite.prepare(`SELECT * FROM ${table}`).all()
|
||||||
create: (table: Tables, data: any, skipWhenExist: boolean = true) => {
|
}
|
||||||
|
|
||||||
|
create(table: Tables, data: any, skipWhenExist: boolean = true) {
|
||||||
if (skipWhenExist && db.find(table, data.id)) return
|
if (skipWhenExist && db.find(table, data.id)) return
|
||||||
return sqlite.prepare(`INSERT INTO ${table} VALUES (?)`).run(data)
|
return this.sqlite.prepare(`INSERT INTO ${table} VALUES (?)`).run(data)
|
||||||
},
|
}
|
||||||
createMany: (table: Tables, data: any[], skipWhenExist: boolean = true) => {
|
|
||||||
|
createMany(table: Tables, data: any[], skipWhenExist: boolean = true) {
|
||||||
const valuesQuery = Object.keys(data[0])
|
const valuesQuery = Object.keys(data[0])
|
||||||
.map(key => `:${key}`)
|
.map(key => `:${key}`)
|
||||||
.join(', ')
|
.join(', ')
|
||||||
const insert = sqlite.prepare(
|
const insert = this.sqlite.prepare(
|
||||||
`INSERT ${
|
`INSERT ${
|
||||||
skipWhenExist ? 'OR IGNORE' : ''
|
skipWhenExist ? 'OR IGNORE' : ''
|
||||||
} INTO ${table} VALUES (${valuesQuery})`
|
} INTO ${table} VALUES (${valuesQuery})`
|
||||||
)
|
)
|
||||||
const insertMany = sqlite.transaction((rows: any[]) => {
|
const insertMany = this.sqlite.transaction((rows: any[]) => {
|
||||||
rows.forEach((row: any) => insert.run(row))
|
rows.forEach((row: any) => insert.run(row))
|
||||||
})
|
})
|
||||||
insertMany(data)
|
insertMany(data)
|
||||||
},
|
}
|
||||||
upsert: (table: Tables, data: any) => {
|
|
||||||
|
upsert(table: Tables, data: any) {
|
||||||
const valuesQuery = Object.keys(data)
|
const valuesQuery = Object.keys(data)
|
||||||
.map(key => `:${key}`)
|
.map(key => `:${key}`)
|
||||||
.join(', ')
|
.join(', ')
|
||||||
return sqlite
|
return this.sqlite
|
||||||
.prepare(`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`)
|
.prepare(`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`)
|
||||||
.run(data)
|
.run(data)
|
||||||
},
|
}
|
||||||
upsertMany: (table: Tables, data: any[]) => {
|
|
||||||
|
upsertMany(table: Tables, data: any[]) {
|
||||||
const valuesQuery = Object.keys(data[0])
|
const valuesQuery = Object.keys(data[0])
|
||||||
.map(key => `:${key}`)
|
.map(key => `:${key}`)
|
||||||
.join(', ')
|
.join(', ')
|
||||||
const upsert = sqlite.prepare(
|
const upsert = this.sqlite.prepare(
|
||||||
`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`
|
`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`
|
||||||
)
|
)
|
||||||
const upsertMany = sqlite.transaction((rows: any[]) => {
|
const upsertMany = this.sqlite.transaction((rows: any[]) => {
|
||||||
rows.forEach((row: any) => upsert.run(row))
|
rows.forEach((row: any) => upsert.run(row))
|
||||||
})
|
})
|
||||||
upsertMany(data)
|
upsertMany(data)
|
||||||
},
|
}
|
||||||
delete: (table: Tables, key: number | string) => {
|
|
||||||
return sqlite.prepare(`DELETE FROM ${table} WHERE id = ?`).run(key)
|
delete(table: Tables, key: number | string) {
|
||||||
},
|
return this.sqlite.prepare(`DELETE FROM ${table} WHERE id = ?`).run(key)
|
||||||
deleteMany: (table: Tables, keys: number[] | string[]) => {
|
}
|
||||||
|
|
||||||
|
deleteMany(table: Tables, keys: number[] | string[]) {
|
||||||
const idsQuery = keys.map(key => `id = ${key}`).join(' OR ')
|
const idsQuery = keys.map(key => `id = ${key}`).join(' OR ')
|
||||||
return sqlite.prepare(`DELETE FROM ${table} WHERE ${idsQuery}`).run()
|
return this.sqlite.prepare(`DELETE FROM ${table} WHERE ${idsQuery}`).run()
|
||||||
},
|
}
|
||||||
truncate: (table: Tables) => {
|
|
||||||
return sqlite.prepare(`DELETE FROM ${table}`).run()
|
truncate(table: Tables) {
|
||||||
},
|
return this.sqlite.prepare(`DELETE FROM ${table}`).run()
|
||||||
vacuum: () => {
|
}
|
||||||
return sqlite.prepare('VACUUM').run()
|
|
||||||
},
|
vacuum() {
|
||||||
|
return this.sqlite.prepare('VACUUM').run()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('[db] Database initialized')
|
export const db = new DB()
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { BrowserWindow, ipcMain, app } from 'electron'
|
import { BrowserWindow, ipcMain, app } from 'electron'
|
||||||
import { db, Tables } from './db'
|
import { db, Tables } from './db'
|
||||||
import { IpcChannels } from './IpcChannelsName'
|
import { IpcChannels } from './IpcChannelsName'
|
||||||
import { getCache } from './cache'
|
import cache from './cache'
|
||||||
import logger from './logger'
|
import logger from './logger'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import { APIs } from './CacheAPIsName'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理需要win对象的事件
|
* 处理需要win对象的事件
|
||||||
|
|
@ -28,13 +29,13 @@ export function initIpcMain(win: BrowserWindow | null) {
|
||||||
* 清除API缓存
|
* 清除API缓存
|
||||||
*/
|
*/
|
||||||
ipcMain.on(IpcChannels.ClearAPICache, () => {
|
ipcMain.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)
|
||||||
db.truncate(Tables.PLAYLIST)
|
db.truncate(Tables.Playlist)
|
||||||
db.truncate(Tables.ARTIST_ALBUMS)
|
db.truncate(Tables.ArtistAlbum)
|
||||||
db.truncate(Tables.ACCOUNT_DATA)
|
db.truncate(Tables.AccountData)
|
||||||
db.truncate(Tables.AUDIO)
|
db.truncate(Tables.Audio)
|
||||||
db.vacuum()
|
db.vacuum()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -43,24 +44,32 @@ ipcMain.on(IpcChannels.ClearAPICache, () => {
|
||||||
*/
|
*/
|
||||||
ipcMain.on(IpcChannels.GetApiCacheSync, (event, args) => {
|
ipcMain.on(IpcChannels.GetApiCacheSync, (event, args) => {
|
||||||
const { api, query } = args
|
const { api, query } = args
|
||||||
const data = getCache(api, query)
|
const data = cache.get(api, query)
|
||||||
event.returnValue = data
|
event.returnValue = data
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存封面颜色
|
||||||
|
*/
|
||||||
|
ipcMain.on(IpcChannels.CacheCoverColor, (event, args) => {
|
||||||
|
const { id, color } = args.query
|
||||||
|
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') {
|
||||||
ipcMain.on(IpcChannels.DevDbExportJson, () => {
|
ipcMain.on(IpcChannels.DevDbExportJson, () => {
|
||||||
const tables = [
|
const tables = [
|
||||||
Tables.ARTIST_ALBUMS,
|
Tables.ArtistAlbum,
|
||||||
Tables.PLAYLIST,
|
Tables.Playlist,
|
||||||
Tables.ALBUM,
|
Tables.Album,
|
||||||
Tables.TRACK,
|
Tables.Track,
|
||||||
Tables.ARTIST,
|
Tables.Artist,
|
||||||
Tables.AUDIO,
|
Tables.Audio,
|
||||||
Tables.ACCOUNT_DATA,
|
Tables.AccountData,
|
||||||
Tables.LYRIC,
|
Tables.Lyric,
|
||||||
]
|
]
|
||||||
tables.forEach(table => {
|
tables.forEach(table => {
|
||||||
const data = db.findAll(table)
|
const data = db.findAll(table)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
CREATE TABLE "account_data" ("id" text NOT NULL,"json" text NOT NULL,"updateAt" int NOT NULL, PRIMARY KEY (id));
|
CREATE TABLE IF NOT EXISTS "account_data" ("id" text NOT NULL,"json" text NOT NULL,"updateAt" int NOT NULL, PRIMARY KEY (id));
|
||||||
CREATE TABLE "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 "artist_album" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));
|
CREATE TABLE IF NOT EXISTS "artist_album" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));
|
||||||
CREATE TABLE "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 "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,"updateAt" int NOT NULL, PRIMARY KEY (id));
|
||||||
CREATE TABLE "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 "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 "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));
|
||||||
|
CREATE TABLE IF NOT EXISTS "cover_color" ("id" integer NOT NULL,"color" text NOT NULL, PRIMARY KEY (id));
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,7 @@ import { pathCase } from 'change-case'
|
||||||
import cookieParser from 'cookie-parser'
|
import cookieParser from 'cookie-parser'
|
||||||
import express, { Request, Response } from 'express'
|
import express, { Request, Response } from 'express'
|
||||||
import logger from './logger'
|
import logger from './logger'
|
||||||
import {
|
import cache from './cache'
|
||||||
setCache,
|
|
||||||
getCacheForExpress,
|
|
||||||
cacheAudio,
|
|
||||||
getAudioCache,
|
|
||||||
} from './cache'
|
|
||||||
import fileUpload from 'express-fileupload'
|
import fileUpload from 'express-fileupload'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
|
|
@ -32,8 +27,8 @@ Object.entries(neteaseApi).forEach(([name, handler]) => {
|
||||||
logger.debug(`[server] Handling request: ${req.path}`)
|
logger.debug(`[server] Handling request: ${req.path}`)
|
||||||
|
|
||||||
// Get from cache
|
// Get from cache
|
||||||
const cache = await getCacheForExpress(name, req)
|
const cacheData = await cache.getForExpress(name, req)
|
||||||
if (cache) return res.json(cache)
|
if (cacheData) return res.json(cacheData)
|
||||||
|
|
||||||
// Request netease api
|
// Request netease api
|
||||||
try {
|
try {
|
||||||
|
|
@ -42,7 +37,7 @@ Object.entries(neteaseApi).forEach(([name, handler]) => {
|
||||||
cookie: req.cookies,
|
cookie: req.cookies,
|
||||||
})
|
})
|
||||||
|
|
||||||
setCache(name, result.body, req.query)
|
cache.set(name, result.body, req.query)
|
||||||
return res.send(result.body)
|
return res.send(result.body)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.status(500).send(error)
|
return res.status(500).send(error)
|
||||||
|
|
@ -57,7 +52,7 @@ Object.entries(neteaseApi).forEach(([name, handler]) => {
|
||||||
app.get(
|
app.get(
|
||||||
'/yesplaymusic/audio/:filename',
|
'/yesplaymusic/audio/:filename',
|
||||||
async (req: Request, res: Response) => {
|
async (req: Request, res: Response) => {
|
||||||
getAudioCache(req.params.filename, res)
|
cache.getAudio(req.params.filename, res)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
app.post('/yesplaymusic/audio/:id', async (req: Request, res: Response) => {
|
app.post('/yesplaymusic/audio/:id', async (req: Request, res: Response) => {
|
||||||
|
|
@ -78,7 +73,7 @@ app.post('/yesplaymusic/audio/:id', async (req: Request, res: Response) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await cacheAudio(req.files.file.data, {
|
await cache.setAudio(req.files.file.data, {
|
||||||
id: id,
|
id: id,
|
||||||
source: 'netease',
|
source: 'netease',
|
||||||
})
|
})
|
||||||
|
|
|
||||||
11
src/renderer/auto-imports.d.ts
vendored
11
src/renderer/auto-imports.d.ts
vendored
|
|
@ -2,14 +2,23 @@
|
||||||
// We suggest you to commit this file into source control
|
// We suggest you to commit this file into source control
|
||||||
declare global {
|
declare global {
|
||||||
const classNames: typeof import('classnames')['default']
|
const classNames: typeof import('classnames')['default']
|
||||||
|
const createRef: typeof import('react')['createRef']
|
||||||
|
const forwardRef: typeof import('react')['forwardRef']
|
||||||
|
const lazy: typeof import('react')['lazy']
|
||||||
|
const memo: typeof import('react')['memo']
|
||||||
|
const startTransition: typeof import('react')['startTransition']
|
||||||
const toast: typeof import('react-hot-toast')['toast']
|
const toast: typeof import('react-hot-toast')['toast']
|
||||||
const useCallback: typeof import('react')['useCallback']
|
const useCallback: typeof import('react')['useCallback']
|
||||||
const useContext: typeof import('react')['useContext']
|
const useContext: typeof import('react')['useContext']
|
||||||
const useDebugValue: typeof import('react')['useDebugValue']
|
const useDebugValue: typeof import('react')['useDebugValue']
|
||||||
|
const useDeferredValue: typeof import('react')['useDeferredValue']
|
||||||
const useEffect: typeof import('react')['useEffect']
|
const useEffect: typeof import('react')['useEffect']
|
||||||
const useEffectOnce: typeof import('react-use')['useEffectOnce']
|
const useEffectOnce: typeof import('react-use')['useEffectOnce']
|
||||||
|
const useId: typeof import('react')['useId']
|
||||||
const useImperativeHandle: typeof import('react')['useImperativeHandle']
|
const useImperativeHandle: typeof import('react')['useImperativeHandle']
|
||||||
const useInfiniteQuery: typeof import('react-query')['useInfiniteQuery']
|
const useInfiniteQuery: typeof import('react-query')['useInfiniteQuery']
|
||||||
|
const useInsertionEffect: typeof import('react')['useInsertionEffect']
|
||||||
|
const useLayoutEffect: typeof import('react')['useLayoutEffect']
|
||||||
const useMemo: typeof import('react')['useMemo']
|
const useMemo: typeof import('react')['useMemo']
|
||||||
const useMutation: typeof import('react-query')['useMutation']
|
const useMutation: typeof import('react-query')['useMutation']
|
||||||
const useNavigate: typeof import('react-router-dom')['useNavigate']
|
const useNavigate: typeof import('react-router-dom')['useNavigate']
|
||||||
|
|
@ -19,5 +28,7 @@ declare global {
|
||||||
const useRef: typeof import('react')['useRef']
|
const useRef: typeof import('react')['useRef']
|
||||||
const useSnapshot: typeof import('valtio')['useSnapshot']
|
const useSnapshot: typeof import('valtio')['useSnapshot']
|
||||||
const useState: typeof import('react')['useState']
|
const useState: typeof import('react')['useState']
|
||||||
|
const useSyncExternalStore: typeof import('react')['useSyncExternalStore']
|
||||||
|
const useTransition: typeof import('react')['useTransition']
|
||||||
}
|
}
|
||||||
export {}
|
export {}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { average } from 'color.js'
|
import { average } from 'color.js'
|
||||||
import { colord } from 'colord'
|
import { colord } from 'colord'
|
||||||
import { player } from '@/renderer/store'
|
import { player } from '@/renderer/store'
|
||||||
import { resizeImage } from '@/renderer/utils/common'
|
import { resizeImage, getCoverColor } from '@/renderer/utils/common'
|
||||||
import SvgIcon from './SvgIcon'
|
import SvgIcon from './SvgIcon'
|
||||||
import ArtistInline from './ArtistsInline'
|
import ArtistInline from './ArtistsInline'
|
||||||
import {
|
import {
|
||||||
|
|
@ -59,7 +59,7 @@ const MediaControls = () => {
|
||||||
|
|
||||||
const FMCard = () => {
|
const FMCard = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [background, setBackground] = useState('')
|
const [bgColor, setBgColor] = useState({ from: '#222', to: '#222' })
|
||||||
|
|
||||||
const playerSnapshot = useSnapshot(player)
|
const playerSnapshot = useSnapshot(player)
|
||||||
const track = useMemo(() => playerSnapshot.fmTrack, [playerSnapshot.fmTrack])
|
const track = useMemo(() => playerSnapshot.fmTrack, [playerSnapshot.fmTrack])
|
||||||
|
|
@ -69,24 +69,19 @@ const FMCard = () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cover = resizeImage(track?.al?.picUrl ?? '', 'xs')
|
getCoverColor(track?.al?.picUrl || '').then(color => {
|
||||||
if (cover) {
|
if (!color) return
|
||||||
average(cover, { amount: 1, format: 'hex', sample: 1 }).then(color => {
|
const to = colord(color).darken(0.15).rotate(-5).toHex()
|
||||||
let c = colord(color as string)
|
setBgColor({ from: color, to })
|
||||||
const hsl = c.toHsl()
|
})
|
||||||
if (hsl.s > 50) c = colord({ ...hsl, s: 50 })
|
|
||||||
if (hsl.l > 50) c = colord({ ...c.toHsl(), l: 50 })
|
|
||||||
if (hsl.l < 30) c = colord({ ...c.toHsl(), l: 30 })
|
|
||||||
const to = c.darken(0.15).rotate(-5).toHex()
|
|
||||||
setBackground(`linear-gradient(to bottom, ${c.toHex()}, ${to})`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [track?.al?.picUrl])
|
}, [track?.al?.picUrl])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='relative flex h-[198px] overflow-hidden rounded-2xl bg-gray-100 p-4 dark:bg-gray-800'
|
className='relative flex h-[198px] overflow-hidden rounded-2xl bg-gray-100 p-4 dark:bg-gray-800'
|
||||||
style={{ background }}
|
style={{
|
||||||
|
background: `linear-gradient(to bottom, ${bgColor.from}, ${bgColor.to})`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{coverUrl ? (
|
{coverUrl ? (
|
||||||
<img
|
<img
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
|
import { IpcChannels } from '@/main/IpcChannelsName'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import duration from 'dayjs/plugin/duration'
|
import duration from 'dayjs/plugin/duration'
|
||||||
|
import { APIs } from '@/main/CacheAPIsName'
|
||||||
|
import { average } from 'color.js'
|
||||||
|
import { colord } from 'colord'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 调整网易云封面图片大小
|
* @description 调整网易云封面图片大小
|
||||||
|
|
@ -70,10 +74,10 @@ export function formatDuration(
|
||||||
|
|
||||||
dayjs.extend(duration)
|
dayjs.extend(duration)
|
||||||
|
|
||||||
let time = dayjs.duration(milliseconds)
|
const time = dayjs.duration(milliseconds)
|
||||||
let hours = time.hours().toString()
|
const hours = time.hours().toString()
|
||||||
let mins = time.minutes().toString()
|
const mins = time.minutes().toString()
|
||||||
let seconds = time.seconds().toString().padStart(2, '0')
|
const seconds = time.seconds().toString().padStart(2, '0')
|
||||||
|
|
||||||
if (format === 'hh:mm:ss') {
|
if (format === 'hh:mm:ss') {
|
||||||
return hours !== '0'
|
return hours !== '0'
|
||||||
|
|
@ -111,3 +115,43 @@ export function scrollToTop(smooth = false) {
|
||||||
if (!main) return
|
if (!main) return
|
||||||
main.scrollTo({ top: 0, behavior: smooth ? 'smooth' : 'auto' })
|
main.scrollTo({ top: 0, behavior: smooth ? 'smooth' : 'auto' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCoverColor(coverUrl: string) {
|
||||||
|
const id = new URL(coverUrl).pathname.split('/').pop()?.split('.')[0]
|
||||||
|
const colorFromCache = window.ipcRenderer?.sendSync(
|
||||||
|
IpcChannels.GetApiCacheSync,
|
||||||
|
{
|
||||||
|
api: APIs.CoverColor,
|
||||||
|
query: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
) as string | undefined
|
||||||
|
return colorFromCache || calcCoverColor(coverUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cacheCoverColor(coverUrl: string, color: string) {
|
||||||
|
const id = new URL(coverUrl).pathname.split('/').pop()?.split('.')[0]
|
||||||
|
|
||||||
|
window.ipcRenderer?.send(IpcChannels.CacheCoverColor, {
|
||||||
|
api: APIs.CoverColor,
|
||||||
|
query: {
|
||||||
|
id,
|
||||||
|
color,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function calcCoverColor(coverUrl: string) {
|
||||||
|
if (!coverUrl) return
|
||||||
|
const cover = resizeImage(coverUrl, 'xs')
|
||||||
|
return average(cover, { amount: 1, format: 'hex', sample: 1 }).then(color => {
|
||||||
|
let c = colord(color as string)
|
||||||
|
const hsl = c.toHsl()
|
||||||
|
if (hsl.s > 50) c = colord({ ...hsl, s: 50 })
|
||||||
|
if (hsl.l > 50) c = colord({ ...c.toHsl(), l: 50 })
|
||||||
|
if (hsl.l < 30) c = colord({ ...c.toHsl(), l: 30 })
|
||||||
|
cacheCoverColor(coverUrl, c.toHex())
|
||||||
|
return c.toHex()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue