feat: 使用sqlite3替换realm

This commit is contained in:
qier222 2022-03-30 00:53:05 +08:00
parent c4219afd3d
commit 1b86cbbee1
No known key found for this signature in database
GPG key ID: 9C85007ED905F14D
20 changed files with 292 additions and 691 deletions

View file

@ -27,15 +27,16 @@
"@sentry/node": "^6.19.2", "@sentry/node": "^6.19.2",
"@sentry/tracing": "^6.19.2", "@sentry/tracing": "^6.19.2",
"NeteaseCloudMusicApi": "^4.5.8", "NeteaseCloudMusicApi": "^4.5.8",
"better-sqlite3": "^7.5.0",
"change-case": "^4.1.2", "change-case": "^4.1.2",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"electron-log": "^4.4.6", "electron-log": "^4.4.6",
"electron-store": "^8.0.1", "electron-store": "^8.0.1",
"express": "^4.17.3", "express": "^4.17.3"
"realm": "^10.13.0"
}, },
"devDependencies": { "devDependencies": {
"@sentry/react": "^6.19.2", "@sentry/react": "^6.19.2",
"@types/better-sqlite3": "^7.5.0",
"@types/cookie-parser": "^1.4.2", "@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/express-fileupload": "^1.2.2", "@types/express-fileupload": "^1.2.2",

588
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -25,6 +25,7 @@ const options = {
), ),
'electron', 'electron',
'NeteaseCloudMusicApi', 'NeteaseCloudMusicApi',
'better-sqlite3',
], ],
} }

View file

@ -1,5 +1,5 @@
import { db, ModelNames, realm } from './database' import { db, Tables } from './db'
import type { FetchTracksResponse } from '../renderer/src/api/track' import type { FetchTracksResponse } from '../renderer/api/track'
import { app, ipcMain } from 'electron' import { app, ipcMain } from 'electron'
import { Request, Response } from 'express' import { Request, Response } from 'express'
import logger from './logger' import logger from './logger'
@ -8,86 +8,82 @@ import * as musicMetadata from 'music-metadata'
export async function setCache(api: string, data: any, query: any) { export async function setCache(api: string, data: any, query: any) {
switch (api) { switch (api) {
case 'user/playlist':
case 'user/account': case 'user/account':
case 'personalized': case 'personalized':
case 'recommend/resource':
case 'likelist': { case 'likelist': {
if (!data) return if (!data) return
db.set(ModelNames.ACCOUNT_DATA, api, data) console.log(api)
break db.upsert(Tables.ACCOUNT_DATA, {
} id: api,
case 'user/playlist': { json: JSON.stringify(data),
if (!data.playlist) return updateAt: Date.now(),
db.set(ModelNames.USER_PLAYLISTS, Number(query.uid), data) })
break break
} }
case 'song/detail': { case 'song/detail': {
console.log('dsdadasdas')
if (!data.songs) return if (!data.songs) return
const tracks = (data as FetchTracksResponse).songs const tracks = (data as FetchTracksResponse).songs.map(t => ({
db.batchSet( id: t.id,
ModelNames.TRACK, json: JSON.stringify(t),
tracks.map(t => ({ updatedAt: Date.now(),
id: t.id, }))
json: JSON.stringify(t), db.upsertMany(Tables.TRACK, tracks)
updateAt: Date.now(),
}))
)
break break
} }
case 'album': { case 'album': {
if (!data.album) return if (!data.album) return
data.album.songs = (data as FetchTracksResponse).songs data.album.songs = (data as FetchTracksResponse).songs
db.set(ModelNames.ALBUM, Number(data.album.id), data) db.upsert(Tables.ALBUM, {
id: data.album.id,
json: JSON.stringify(data),
updatedAt: Date.now(),
})
break break
} }
case 'playlist/detail': { case 'playlist/detail': {
if (!data.playlist) return if (!data.playlist) return
db.set(ModelNames.PLAYLIST, Number(data.playlist.id), data) db.upsert(Tables.PLAYLIST, {
id: data.playlist.id,
json: JSON.stringify(data),
updatedAt: Date.now(),
})
break break
} }
case 'artists': { case 'artists': {
if (!data.artist) return if (!data.artist) return
db.set(ModelNames.ARTIST, Number(data.artist.id), data) db.upsert(Tables.ARTIST, {
id: data.artist.id,
json: JSON.stringify(data),
updatedAt: Date.now(),
})
break break
} }
case 'artist/album': { case 'artist/album': {
if (!data.hotAlbums) return if (!data.hotAlbums) return
db.set(ModelNames.ARTIST_ALBUMS, Number(data.artist.id), data) db.upsert(Tables.ARTIST_ALBUMS, {
id: data.artist.id,
json: JSON.stringify(data),
updatedAt: Date.now(),
})
break break
} }
} }
} }
/** export function getCache(api: string, query: any): any {
* Check if the cache is expired
* @param updateAt from database, milliseconds
* @param staleTime minutes
*/
const isCacheExpired = (updateAt: number, staleTime: number) => {
return Date.now() - updateAt > staleTime * 1000 * 60
}
export function getCache(
api: string,
query: any,
checkIsExpired: boolean = false
): any {
switch (api) { switch (api) {
case 'user/account': case 'user/account':
case 'user/playlist':
case 'personalized': case 'personalized':
case 'recommend/resource':
case 'likelist': { case 'likelist': {
const data = db.get(ModelNames.ACCOUNT_DATA, api) as any const data = db.find(Tables.ACCOUNT_DATA, api)
if (data?.json) return JSON.parse(data.json) if (data?.json) return JSON.parse(data.json)
break break
} }
case 'user/playlist': {
if (isNaN(Number(query.uid))) return
const userPlaylists = db.get(
ModelNames.USER_PLAYLISTS,
Number(query?.uid)
) as any
if (userPlaylists?.json) return JSON.parse(userPlaylists.json)
break
}
case 'song/detail': { case 'song/detail': {
const ids: string[] = query?.ids.split(',') const ids: string[] = query?.ids.split(',')
if (ids.length === 0) return if (ids.length === 0) return
@ -98,10 +94,8 @@ export function getCache(
}) })
if (!isIDsValid) return if (!isIDsValid) return
const idsQuery = ids.map(id => `id = ${id}`).join(' OR ') const tracksRaw = db.findMany(Tables.TRACK, ids)
const tracksRaw = realm
.objects(ModelNames.TRACK)
.filtered(`(${idsQuery})`)
if (tracksRaw.length !== ids.length) { if (tracksRaw.length !== ids.length) {
return return
} }
@ -118,33 +112,27 @@ export function getCache(
} }
case 'album': { case 'album': {
if (isNaN(Number(query?.id))) return if (isNaN(Number(query?.id))) return
const album = db.get(ModelNames.ALBUM, Number(query?.id)) as any const data = db.find(Tables.ALBUM, query.id)
if (checkIsExpired && isCacheExpired(album?.updateAt, 24 * 60)) return console.log(data)
if (album?.json) return JSON.parse(album.json) if (data?.json) return JSON.parse(data.json)
break break
} }
case 'playlist/detail': { case 'playlist/detail': {
if (isNaN(Number(query?.id))) return if (isNaN(Number(query?.id))) return
const playlist = db.get(ModelNames.PLAYLIST, Number(query?.id)) as any const data = db.find(Tables.PLAYLIST, query.id)
if (checkIsExpired && isCacheExpired(playlist?.updateAt, 10)) return if (data?.json) return JSON.parse(data.json)
if (playlist?.json) return JSON.parse(playlist.json)
break break
} }
case 'artists': { case 'artists': {
if (isNaN(Number(query?.id))) return if (isNaN(Number(query?.id))) return
const artist = db.get(ModelNames.ARTIST, Number(query?.id)) as any const data = db.find(Tables.ARTIST, query.id)
if (checkIsExpired && isCacheExpired(artist?.updateAt, 30)) return if (data?.json) return JSON.parse(data.json)
if (artist?.json) return JSON.parse(artist.json)
break break
} }
case 'artist/album': { case 'artist/album': {
if (isNaN(Number(query?.id))) return if (isNaN(Number(query?.id))) return
const artistAlbums = db.get( const data = db.find(Tables.ARTIST_ALBUMS, query.id)
ModelNames.ARTIST_ALBUMS, if (data?.json) return JSON.parse(data.json)
Number(query?.id)
) as any
if (checkIsExpired && isCacheExpired(artistAlbums?.updateAt, 30)) return
if (artistAlbums?.json) return JSON.parse(artistAlbums.json)
break break
} }
} }
@ -162,7 +150,7 @@ export async function getCacheForExpress(api: string, req: Request) {
// Get audio cache if API is song/detail // Get audio cache if API is song/detail
if (api === 'song/url') { if (api === 'song/url') {
const cache = db.get(ModelNames.AUDIO, Number(req.query.id)) as any const cache = db.find(Tables.AUDIO, Number(req.query.id))
if (!cache) return if (!cache) return
const audioFileName = `${cache.id}-${cache.br}.${cache.type}` const audioFileName = `${cache.id}-${cache.br}.${cache.type}`
@ -224,7 +212,7 @@ export function getAudioCache(fileName: string, res: Response) {
const path = `${app.getPath('userData')}/audio_cache/${fileName}` const path = `${app.getPath('userData')}/audio_cache/${fileName}`
const audio = fs.readFileSync(path) const audio = fs.readFileSync(path)
if (audio.byteLength === 0) { if (audio.byteLength === 0) {
db.delete(ModelNames.AUDIO, Number(id)) db.delete(Tables.AUDIO, id)
fs.unlinkSync(path) fs.unlinkSync(path)
return res.status(404).send({ error: 'Audio not found' }) return res.status(404).send({ error: 'Audio not found' })
} }
@ -263,18 +251,12 @@ export async function cacheAudio(
} }
logger.info(`Audio file ${id}-${br}.${type} cached!`) logger.info(`Audio file ${id}-${br}.${type} cached!`)
realm.write(() => { db.upsert(Tables.AUDIO, {
realm.create( id,
ModelNames.AUDIO, br,
{ type,
id: Number(id), source,
type, updateAt: Date.now(),
br,
source,
updateAt: Date.now(),
},
'modified'
)
}) })
logger.info(`[cache] cacheAudio ${id}-${br}.${type}`) logger.info(`[cache] cacheAudio ${id}-${br}.${type}`)
@ -283,6 +265,6 @@ export async function cacheAudio(
ipcMain.on('getApiCacheSync', (event, args) => { ipcMain.on('getApiCacheSync', (event, args) => {
const { api, query } = args const { api, query } = args
const data = getCache(api, query, false) const data = getCache(api, query)
event.returnValue = data event.returnValue = data
}) })

View file

@ -122,10 +122,4 @@ ipcMain.on('test', () => {
console.log('The file was saved!') console.log('The file was saved!')
}) })
}) })
// realm.write(() => {
// realm.deleteAll()
// })
realm.compact()
}) })

88
src/main/db.ts Normal file
View file

@ -0,0 +1,88 @@
import path from 'path'
import { app, ipcMain } from 'electron'
import fs from 'fs'
import SQLite3 from 'better-sqlite3'
export enum Tables {
TRACK = 'track',
ALBUM = 'album',
ARTIST = 'artist',
PLAYLIST = 'playlist',
ARTIST_ALBUMS = 'artist_album',
USER_PLAYLISTS = 'user_playlist',
// Special tables
ACCOUNT_DATA = 'account_data',
AUDIO = 'audio',
}
const sqlite = new SQLite3(
path.resolve(app.getPath('userData'), './api_cache/db.sqlite')
)
export const db = {
find: (table: Tables, key: number | string) => {
return sqlite
.prepare(`SELECT * FROM ${table} WHERE id = ? LIMIT 1`)
.get(key)
},
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()
},
upsert: (table: Tables, data: any) => {
const valuesQuery = Object.keys(data)
.map(key => `:${key}`)
.join(', ')
return sqlite
.prepare(`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`)
.run(data)
},
upsertMany: (table: Tables, data: any[]) => {
const valuesQuery = Object.keys(data[0])
.map(key => `:${key}`)
.join(', ')
const upsert = sqlite.prepare(
`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`
)
const upsertMany = 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[]) => {
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()
},
}
ipcMain.on('db-export-json', () => {
const tables = [
Tables.ARTIST_ALBUMS,
Tables.PLAYLIST,
Tables.ALBUM,
Tables.TRACK,
Tables.ARTIST,
Tables.AUDIO,
Tables.ACCOUNT_DATA,
]
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!')
})
})
})

View file

@ -12,6 +12,7 @@ import path, { join } from 'path'
import logger from './logger' import logger from './logger'
import './server' import './server'
// import './database' // import './database'
import './db'
const isWindows = process.platform === 'win32' const isWindows = process.platform === 'win32'
const isMac = process.platform === 'darwin' const isMac = process.platform === 'darwin'

View file

@ -3,7 +3,7 @@ import path from 'path'
import { app } from 'electron' import { app } from 'electron'
import fs from 'fs' import fs from 'fs'
const isDev = !app.isPackaged const isDev = process.env.NODE_ENV === 'development'
if (isDev) { if (isDev) {
const devUserDataPath = path.resolve(process.cwd(), './tmp/userData') const devUserDataPath = path.resolve(process.cwd(), './tmp/userData')

View file

@ -8,7 +8,7 @@ logger.info(`[sentry] init sentry`)
Sentry.init({ Sentry.init({
dsn: 'https://2aaaa67f1c3d4d6baefafa5d58fcf340@o436528.ingest.sentry.io/6274637', dsn: 'https://2aaaa67f1c3d4d6baefafa5d58fcf340@o436528.ingest.sentry.io/6274637',
release: `yesplaymusic@${pkg.version}`, release: `yesplaymusic@${pkg.version}`,
// environment: import.meta.env.MODE, environment: process.env.NODE_ENV,
// Set tracesSampleRate to 1.0 to capture 100% // Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring. // of transactions for performance monitoring.

View file

@ -2,12 +2,12 @@ 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 {
// setCache, setCache,
// getCacheForExpress, getCacheForExpress,
// cacheAudio, cacheAudio,
// getAudioCache, getAudioCache,
// } from './cache' } from './cache'
import fileUpload from 'express-fileupload' import fileUpload from 'express-fileupload'
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
@ -26,8 +26,8 @@ Object.entries(neteaseApi).forEach(([name, handler]) => {
logger.info(`[server] Handling request: ${req.path}`) logger.info(`[server] Handling request: ${req.path}`)
// Get from cache // Get from cache
// const cache = await getCacheForExpress(name, req) const cache = await getCacheForExpress(name, req)
// if (cache) return res.json(cache) if (cache) return res.json(cache)
// Request netease api // Request netease api
try { try {
@ -36,7 +36,7 @@ Object.entries(neteaseApi).forEach(([name, handler]) => {
cookie: `MUSIC_U=${req.cookies['MUSIC_U']}`, cookie: `MUSIC_U=${req.cookies['MUSIC_U']}`,
}) })
// setCache(name, result.body, req.query) setCache(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)
@ -51,7 +51,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) getAudioCache(req.params.filename, res)
} }
) )
app.post('/yesplaymusic/audio/:id', async (req: Request, res: Response) => { app.post('/yesplaymusic/audio/:id', async (req: Request, res: Response) => {
@ -72,10 +72,10 @@ app.post('/yesplaymusic/audio/:id', async (req: Request, res: Response) => {
} }
try { try {
// await cacheAudio(req.files.file.data, { await cacheAudio(req.files.file.data, {
// id: id, id: id,
// source: 'netease', source: 'netease',
// }) })
res.status(200).send('Audio cached!') res.status(200).send('Audio cached!')
} catch (error) { } catch (error) {
res.status(500).send({ error }) res.status(500).send({ error })

View file

@ -18,13 +18,13 @@ export default function useAlbum(params: FetchAlbumParams, noCache?: boolean) {
{ {
enabled: !!params.id, enabled: !!params.id,
staleTime: 24 * 60 * 60 * 1000, // 24 hours staleTime: 24 * 60 * 60 * 1000, // 24 hours
// placeholderData: (): FetchAlbumResponse => placeholderData: (): FetchAlbumResponse =>
// window.ipcRenderer.sendSync('getApiCacheSync', { window.ipcRenderer?.sendSync('getApiCacheSync', {
// api: 'album', api: 'album',
// query: { query: {
// id: params.id, id: params.id,
// }, },
// }), }),
} }
) )
} }

View file

@ -12,13 +12,13 @@ export default function useArtist(
{ {
enabled: !!params.id && params.id > 0 && !isNaN(Number(params.id)), enabled: !!params.id && params.id > 0 && !isNaN(Number(params.id)),
staleTime: 5 * 60 * 1000, // 5 mins staleTime: 5 * 60 * 1000, // 5 mins
// placeholderData: (): FetchArtistResponse => placeholderData: (): FetchArtistResponse =>
// window.ipcRenderer.sendSync('getApiCacheSync', { window.ipcRenderer?.sendSync('getApiCacheSync', {
// api: 'artists', api: 'artists',
// query: { query: {
// id: params.id, id: params.id,
// }, },
// }), }),
} }
) )
} }

View file

@ -15,13 +15,13 @@ export default function useUserAlbums(params: FetchArtistAlbumsParams) {
{ {
enabled: !!params.id && params.id !== 0, enabled: !!params.id && params.id !== 0,
staleTime: 3600000, staleTime: 3600000,
// placeholderData: (): FetchArtistAlbumsResponse => placeholderData: (): FetchArtistAlbumsResponse =>
// window.ipcRenderer.sendSync('getApiCacheSync', { window.ipcRenderer?.sendSync('getApiCacheSync', {
// api: 'artist/album', api: 'artist/album',
// query: { query: {
// id: params.id, id: params.id,
// }, },
// }), }),
} }
) )
} }

View file

@ -17,13 +17,13 @@ export default function usePlaylist(
{ {
enabled: !!(params.id && params.id > 0 && !isNaN(Number(params.id))), enabled: !!(params.id && params.id > 0 && !isNaN(Number(params.id))),
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
// placeholderData: (): FetchPlaylistResponse | undefined => placeholderData: (): FetchPlaylistResponse | undefined =>
// window.ipcRenderer.sendSync('getApiCacheSync', { window.ipcRenderer?.sendSync('getApiCacheSync', {
// api: 'playlist/detail', api: 'playlist/detail',
// query: { query: {
// id: params.id, id: params.id,
// }, },
// }), }),
} }
) )
} }

View file

@ -16,13 +16,13 @@ export default function useTracks(params: FetchTracksParams) {
enabled: params.ids.length !== 0, enabled: params.ids.length !== 0,
refetchInterval: false, refetchInterval: false,
staleTime: Infinity, staleTime: Infinity,
// initialData: (): FetchTracksResponse | undefined => initialData: (): FetchTracksResponse | undefined =>
// window.ipcRenderer.sendSync('getApiCacheSync', { window.ipcRenderer?.sendSync('getApiCacheSync', {
// api: 'song/detail', api: 'song/detail',
// query: { query: {
// ids: params.ids.join(','), ids: params.ids.join(','),
// }, },
// }), }),
} }
) )
} }

View file

@ -4,9 +4,9 @@ import { UserApiNames } from '@/api/user'
export default function useUser() { export default function useUser() {
return useQuery(UserApiNames.FETCH_USER_ACCOUNT, fetchUserAccount, { return useQuery(UserApiNames.FETCH_USER_ACCOUNT, fetchUserAccount, {
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
// placeholderData: (): fetchUserAccountResponse | undefined => placeholderData: (): fetchUserAccountResponse | undefined =>
// window.ipcRenderer.sendSync('getApiCacheSync', { window.ipcRenderer?.sendSync('getApiCacheSync', {
// api: 'user/account', api: 'user/account',
// }), }),
}) })
} }

View file

@ -13,13 +13,13 @@ export default function useUserLikedSongsIDs(
{ {
enabled: !!(params.uid && params.uid !== 0), enabled: !!(params.uid && params.uid !== 0),
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
// placeholderData: (): FetchUserLikedSongsIDsResponse | undefined => placeholderData: (): FetchUserLikedSongsIDsResponse | undefined =>
// window.ipcRenderer.sendSync('getApiCacheSync', { window.ipcRenderer?.sendSync('getApiCacheSync', {
// api: 'likelist', api: 'likelist',
// query: { query: {
// uid: params.uid, uid: params.uid,
// }, },
// }), }),
} }
) )
} }

View file

@ -17,13 +17,13 @@ export default function useUserPlaylists(params: FetchUserPlaylistsParams) {
params.uid !== 0 && params.uid !== 0 &&
params.offset !== undefined params.offset !== undefined
), ),
// placeholderData: (): FetchUserPlaylistsResponse => placeholderData: (): FetchUserPlaylistsResponse =>
// window.ipcRenderer.sendSync('getApiCacheSync', { window.ipcRenderer?.sendSync('getApiCacheSync', {
// api: 'user/playlist', api: 'user/playlist',
// query: { query: {
// uid: params.uid, uid: params.uid,
// }, },
// }), }),
} }
) )
} }

View file

@ -12,7 +12,7 @@ Sentry.init({
dsn: 'https://7cc7879b42ba4bed9f66fb6752558475@o436528.ingest.sentry.io/6274630', dsn: 'https://7cc7879b42ba4bed9f66fb6752558475@o436528.ingest.sentry.io/6274630',
integrations: [new BrowserTracing()], integrations: [new BrowserTracing()],
release: `yesplaymusic@${pkg.version}`, release: `yesplaymusic@${pkg.version}`,
// environment: import.meta.env.MODE, environment: import.meta.env.MODE,
// Set tracesSampleRate to 1.0 to capture 100% // Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring. // of transactions for performance monitoring.

View file

@ -17,8 +17,10 @@ export default function Home() {
return fetchRecommendedPlaylists({}) return fetchRecommendedPlaylists({})
}, },
{ {
// placeholderData: () => placeholderData: () =>
// window.ipcRenderer.sendSync('getApiCacheSync', { api: 'personalized' }), window.ipcRenderer?.sendSync('getApiCacheSync', {
api: 'personalized',
}),
} }
) )
@ -27,7 +29,13 @@ export default function Home() {
isLoading: isLoadingDailyRecommendPlaylists, isLoading: isLoadingDailyRecommendPlaylists,
} = useQuery( } = useQuery(
PlaylistApiNames.FETCH_DAILY_RECOMMEND_PLAYLISTS, PlaylistApiNames.FETCH_DAILY_RECOMMEND_PLAYLISTS,
fetchDailyRecommendPlaylists fetchDailyRecommendPlaylists,
{
placeholderData: () =>
window.ipcRenderer?.sendSync('getApiCacheSync', {
api: 'recommend/resource',
}),
}
) )
const playlists = [ const playlists = [