diff --git a/src/main/CacheAPIsName.ts b/src/main/CacheAPIsName.ts new file mode 100644 index 0000000..ed39ae2 --- /dev/null +++ b/src/main/CacheAPIsName.ts @@ -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', +} diff --git a/src/main/IpcChannelsName.ts b/src/main/IpcChannelsName.ts index d34047a..4dacad1 100644 --- a/src/main/IpcChannelsName.ts +++ b/src/main/IpcChannelsName.ts @@ -6,4 +6,5 @@ export enum IpcChannels { IsMaximized = 'is-maximized', GetApiCacheSync = 'get-api-cache-sync', DevDbExportJson = 'dev-db-export-json', + CacheCoverColor = 'cache-cover-color', } diff --git a/src/main/cache.ts b/src/main/cache.ts index 961c551..5108365 100644 --- a/src/main/cache.ts +++ b/src/main/cache.ts @@ -5,300 +5,322 @@ import { Request, Response } from 'express' import logger from './logger' import fs from 'fs' import * as musicMetadata from 'music-metadata' +import { APIs } from './CacheAPIsName' -export async function setCache(api: string, data: any, query: any) { - switch (api) { - case 'user/playlist': - case 'user/account': - case 'personalized': - case 'recommend/resource': - case 'likelist': { - if (!data) return - db.upsert(Tables.ACCOUNT_DATA, { - id: api, - json: JSON.stringify(data), - updateAt: Date.now(), - }) - break - } - case 'song/detail': { - if (!data.songs) return - const tracks = (data as FetchTracksResponse).songs.map(t => ({ - id: t.id, - json: JSON.stringify(t), - updatedAt: Date.now(), - })) - db.upsertMany(Tables.TRACK, tracks) - break - } - 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), +class Cache { + constructor() { + // + } + + set(api: string, data: any, query: any = {}) { + switch (api) { + case APIs.UserPlaylist: + case APIs.UserAccount: + case APIs.Personalized: + case APIs.RecommendResource: + case APIs.Likelist: { + if (!data) return + db.upsert(Tables.AccountData, { + id: api, + json: JSON.stringify(data), + updateAt: Date.now(), + }) + break + } + case APIs.SongDetail: { + if (!data.songs) return + const tracks = (data as FetchTracksResponse).songs.map(t => ({ + id: t.id, + json: JSON.stringify(t), updatedAt: Date.now(), })) - ) - const modifiedData = { - ...data, - hotAlbums: data.hotAlbums.map((a: Album) => a.id), + db.upsertMany(Tables.Track, tracks) + break + } + 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 { - switch (api) { - case 'user/account': - case 'user/playlist': - case 'personalized': - case 'recommend/resource': - case 'likelist': { - const data = db.find(Tables.ACCOUNT_DATA, api) - if (data?.json) return JSON.parse(data.json) - 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 + get(api: string, query: any): any { + switch (api) { + case APIs.UserPlaylist: + case APIs.UserAccount: + case APIs.Personalized: + case APIs.RecommendResource: + case APIs.Likelist: { + const data = db.find(Tables.AccountData, api) + if (data?.json) return JSON.parse(data.json) + break } - const tracks = ids.map(id => { - const track = tracksRaw.find(t => t.id === Number(id)) as any - return JSON.parse(track.json) - }) + case APIs.SongDetail: { + 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 => { + 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 { + 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, - 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) { - // Get track detail cache - if (api === 'song/detail') { - const cache = getCache(api, req.query) - if (cache) { - logger.debug(`[cache] Cache hit for ${req.path}`) - return cache + getAudio(fileName: string, res: Response) { + if (!fileName) { + return res.status(400).send({ error: 'No filename provided' }) + } + const id = Number(fileName.split('-')[0]) + + 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 - if (api === 'song/url') { - const cache = db.find(Tables.AUDIO, Number(req.query.id)) - if (!cache) return + async setAudio( + buffer: Buffer, + { id, source }: { id: number; source: string } + ) { + const path = `${app.getPath('userData')}/audio_cache` - 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 { - 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, + try { + fs.statSync(path) + } catch (e) { + fs.mkdirSync(path) } - } -} -export function getAudioCache(fileName: string, res: Response) { - if (!fileName) { - return res.status(400).send({ error: 'No filename provided' }) - } - const id = Number(fileName.split('-')[0]) + 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'] - 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 }) - } -} + 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!`) -// Cache audio info local folder -export async function cacheAudio( - buffer: Buffer, - { id, source }: { id: number; source: string } -) { - const path = `${app.getPath('userData')}/audio_cache` + db.upsert(Tables.Audio, { + id, + br, + type, + source, + updateAt: Date.now(), + }) - try { - 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}`) }) - - logger.info(`[cache] cacheAudio ${id}-${br}.${type}`) - }) + } } + +export default new Cache() diff --git a/src/main/db.ts b/src/main/db.ts index 56bf041..864d2a1 100644 --- a/src/main/db.ts +++ b/src/main/db.ts @@ -7,110 +7,127 @@ import { createFileIfNotExist } from './utils' const isDev = process.env.NODE_ENV === 'development' -logger.info('[db] Initializing database...') - export enum Tables { - TRACK = 'track', - ALBUM = 'album', - ARTIST = 'artist', - PLAYLIST = 'playlist', - ARTIST_ALBUMS = 'artist_album', - LYRIC = 'lyric', + Track = 'track', + Album = 'album', + Artist = 'artist', + Playlist = 'playlist', + ArtistAlbum = 'artist_album', + Lyric = 'lyric', // Special tables - ACCOUNT_DATA = 'account_data', - AUDIO = 'audio', + AccountData = 'account_data', + Audio = 'audio', + CoverColor = 'cover_color', } -const dbFilePath = path.resolve( - app.getPath('userData'), - './api_cache/db.sqlite' -) -createFileIfNotExist(dbFilePath) - -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' +class DB { + sqlite: SQLite3.Database + dbFilePath: string = path.resolve( + app.getPath('userData'), + './api_cache/db.sqlite' ) - sqlite.exec(migration) -} -export const db = { - find: (table: Tables, key: number | string) => { - return sqlite + constructor() { + logger.info('[db] Initializing database...') + + 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`) .get(key) - }, - findMany: (table: Tables, keys: number[] | string[]) => { + } + + findMany(table: Tables, keys: number[] | string[]) { const idsQuery = keys.map(key => `id = ${key}`).join(' OR ') - return sqlite.prepare(`SELECT * FROM ${table} WHERE ${idsQuery}`).all() - }, - findAll: (table: Tables) => { - return sqlite.prepare(`SELECT * FROM ${table}`).all() - }, - create: (table: Tables, data: any, skipWhenExist: boolean = true) => { + return this.sqlite.prepare(`SELECT * FROM ${table} WHERE ${idsQuery}`).all() + } + + findAll(table: Tables) { + return this.sqlite.prepare(`SELECT * FROM ${table}`).all() + } + + create(table: Tables, data: any, skipWhenExist: boolean = true) { if (skipWhenExist && db.find(table, data.id)) return - return sqlite.prepare(`INSERT INTO ${table} VALUES (?)`).run(data) - }, - createMany: (table: Tables, data: any[], skipWhenExist: boolean = true) => { + return this.sqlite.prepare(`INSERT INTO ${table} VALUES (?)`).run(data) + } + + createMany(table: Tables, data: any[], skipWhenExist: boolean = true) { const valuesQuery = Object.keys(data[0]) .map(key => `:${key}`) .join(', ') - const insert = sqlite.prepare( + const insert = this.sqlite.prepare( `INSERT ${ skipWhenExist ? 'OR IGNORE' : '' } INTO ${table} VALUES (${valuesQuery})` ) - const insertMany = sqlite.transaction((rows: any[]) => { + const insertMany = this.sqlite.transaction((rows: any[]) => { rows.forEach((row: any) => insert.run(row)) }) insertMany(data) - }, - upsert: (table: Tables, data: any) => { + } + + upsert(table: Tables, data: any) { const valuesQuery = Object.keys(data) .map(key => `:${key}`) .join(', ') - return sqlite + return this.sqlite .prepare(`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`) .run(data) - }, - upsertMany: (table: Tables, data: any[]) => { + } + + upsertMany(table: Tables, data: any[]) { const valuesQuery = Object.keys(data[0]) .map(key => `:${key}`) .join(', ') - const upsert = sqlite.prepare( + const upsert = this.sqlite.prepare( `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)) }) upsertMany(data) - }, - delete: (table: Tables, key: number | string) => { - return sqlite.prepare(`DELETE FROM ${table} WHERE id = ?`).run(key) - }, - deleteMany: (table: Tables, keys: number[] | string[]) => { + } + + delete(table: Tables, key: number | string) { + return this.sqlite.prepare(`DELETE FROM ${table} WHERE id = ?`).run(key) + } + + deleteMany(table: Tables, keys: number[] | string[]) { const idsQuery = keys.map(key => `id = ${key}`).join(' OR ') - return sqlite.prepare(`DELETE FROM ${table} WHERE ${idsQuery}`).run() - }, - truncate: (table: Tables) => { - return sqlite.prepare(`DELETE FROM ${table}`).run() - }, - vacuum: () => { - return sqlite.prepare('VACUUM').run() - }, + return this.sqlite.prepare(`DELETE FROM ${table} WHERE ${idsQuery}`).run() + } + + truncate(table: Tables) { + return this.sqlite.prepare(`DELETE FROM ${table}`).run() + } + + vacuum() { + return this.sqlite.prepare('VACUUM').run() + } } -logger.info('[db] Database initialized') +export const db = new DB() diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts index c16a31a..8265c89 100644 --- a/src/main/ipcMain.ts +++ b/src/main/ipcMain.ts @@ -1,9 +1,10 @@ import { BrowserWindow, ipcMain, app } from 'electron' import { db, Tables } from './db' import { IpcChannels } from './IpcChannelsName' -import { getCache } from './cache' +import cache from './cache' import logger from './logger' import fs from 'fs' +import { APIs } from './CacheAPIsName' /** * 处理需要win对象的事件 @@ -28,13 +29,13 @@ export function initIpcMain(win: BrowserWindow | null) { * 清除API缓存 */ ipcMain.on(IpcChannels.ClearAPICache, () => { - db.truncate(Tables.TRACK) - db.truncate(Tables.ALBUM) - db.truncate(Tables.ARTIST) - db.truncate(Tables.PLAYLIST) - db.truncate(Tables.ARTIST_ALBUMS) - db.truncate(Tables.ACCOUNT_DATA) - db.truncate(Tables.AUDIO) + 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() }) @@ -43,24 +44,32 @@ ipcMain.on(IpcChannels.ClearAPICache, () => { */ ipcMain.on(IpcChannels.GetApiCacheSync, (event, args) => { const { api, query } = args - const data = getCache(api, query) + const data = cache.get(api, query) event.returnValue = data }) +/** + * 缓存封面颜色 + */ +ipcMain.on(IpcChannels.CacheCoverColor, (event, args) => { + const { id, color } = args.query + cache.set(APIs.CoverColor, { id, color }) +}) + /** * 导出tables到json文件,方便查看table大小(dev环境) */ if (process.env.NODE_ENV === 'development') { ipcMain.on(IpcChannels.DevDbExportJson, () => { const tables = [ - Tables.ARTIST_ALBUMS, - Tables.PLAYLIST, - Tables.ALBUM, - Tables.TRACK, - Tables.ARTIST, - Tables.AUDIO, - Tables.ACCOUNT_DATA, - Tables.LYRIC, + Tables.ArtistAlbum, + Tables.Playlist, + Tables.Album, + Tables.Track, + Tables.Artist, + Tables.Audio, + Tables.AccountData, + Tables.Lyric, ] tables.forEach(table => { const data = db.findAll(table) diff --git a/src/main/migrations/init.sql b/src/main/migrations/init.sql index 18ea69c..516fea9 100644 --- a/src/main/migrations/init.sql +++ b/src/main/migrations/init.sql @@ -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 "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 "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 "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 "track" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" 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 IF NOT EXISTS "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 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 "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)); +CREATE TABLE IF NOT EXISTS "cover_color" ("id" integer NOT NULL,"color" text NOT NULL, PRIMARY KEY (id)); diff --git a/src/main/server.ts b/src/main/server.ts index a206c1e..111fb69 100644 --- a/src/main/server.ts +++ b/src/main/server.ts @@ -2,12 +2,7 @@ import { pathCase } from 'change-case' import cookieParser from 'cookie-parser' import express, { Request, Response } from 'express' import logger from './logger' -import { - setCache, - getCacheForExpress, - cacheAudio, - getAudioCache, -} from './cache' +import cache from './cache' import fileUpload from 'express-fileupload' import path from 'path' @@ -32,8 +27,8 @@ Object.entries(neteaseApi).forEach(([name, handler]) => { logger.debug(`[server] Handling request: ${req.path}`) // Get from cache - const cache = await getCacheForExpress(name, req) - if (cache) return res.json(cache) + const cacheData = await cache.getForExpress(name, req) + if (cacheData) return res.json(cacheData) // Request netease api try { @@ -42,7 +37,7 @@ Object.entries(neteaseApi).forEach(([name, handler]) => { cookie: req.cookies, }) - setCache(name, result.body, req.query) + cache.set(name, result.body, req.query) return res.send(result.body) } catch (error) { return res.status(500).send(error) @@ -57,7 +52,7 @@ Object.entries(neteaseApi).forEach(([name, handler]) => { app.get( '/yesplaymusic/audio/:filename', 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) => { @@ -78,7 +73,7 @@ app.post('/yesplaymusic/audio/:id', async (req: Request, res: Response) => { } try { - await cacheAudio(req.files.file.data, { + await cache.setAudio(req.files.file.data, { id: id, source: 'netease', }) diff --git a/src/renderer/auto-imports.d.ts b/src/renderer/auto-imports.d.ts index d158698..5d16fdd 100644 --- a/src/renderer/auto-imports.d.ts +++ b/src/renderer/auto-imports.d.ts @@ -2,14 +2,23 @@ // We suggest you to commit this file into source control declare global { 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 useCallback: typeof import('react')['useCallback'] const useContext: typeof import('react')['useContext'] const useDebugValue: typeof import('react')['useDebugValue'] + const useDeferredValue: typeof import('react')['useDeferredValue'] const useEffect: typeof import('react')['useEffect'] const useEffectOnce: typeof import('react-use')['useEffectOnce'] + const useId: typeof import('react')['useId'] const useImperativeHandle: typeof import('react')['useImperativeHandle'] 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 useMutation: typeof import('react-query')['useMutation'] const useNavigate: typeof import('react-router-dom')['useNavigate'] @@ -19,5 +28,7 @@ declare global { const useRef: typeof import('react')['useRef'] const useSnapshot: typeof import('valtio')['useSnapshot'] const useState: typeof import('react')['useState'] + const useSyncExternalStore: typeof import('react')['useSyncExternalStore'] + const useTransition: typeof import('react')['useTransition'] } export {} diff --git a/src/renderer/components/FMCard.tsx b/src/renderer/components/FMCard.tsx index d146693..bbb1f86 100644 --- a/src/renderer/components/FMCard.tsx +++ b/src/renderer/components/FMCard.tsx @@ -1,7 +1,7 @@ import { average } from 'color.js' import { colord } from 'colord' import { player } from '@/renderer/store' -import { resizeImage } from '@/renderer/utils/common' +import { resizeImage, getCoverColor } from '@/renderer/utils/common' import SvgIcon from './SvgIcon' import ArtistInline from './ArtistsInline' import { @@ -59,7 +59,7 @@ const MediaControls = () => { const FMCard = () => { const navigate = useNavigate() - const [background, setBackground] = useState('') + const [bgColor, setBgColor] = useState({ from: '#222', to: '#222' }) const playerSnapshot = useSnapshot(player) const track = useMemo(() => playerSnapshot.fmTrack, [playerSnapshot.fmTrack]) @@ -69,24 +69,19 @@ const FMCard = () => { ) useEffect(() => { - const cover = resizeImage(track?.al?.picUrl ?? '', 'xs') - if (cover) { - 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 }) - const to = c.darken(0.15).rotate(-5).toHex() - setBackground(`linear-gradient(to bottom, ${c.toHex()}, ${to})`) - }) - } + getCoverColor(track?.al?.picUrl || '').then(color => { + if (!color) return + const to = colord(color).darken(0.15).rotate(-5).toHex() + setBgColor({ from: color, to }) + }) }, [track?.al?.picUrl]) return (
{coverUrl ? ( { + 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() + }) +}