mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-16 13:17:46 +00:00
feat: updates
This commit is contained in:
parent
222fb02355
commit
47e41dea9b
24 changed files with 380 additions and 130 deletions
|
|
@ -1,56 +1,86 @@
|
||||||
import log from './log'
|
import log from './log'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { AppleMusicAlbum, AppleMusicArtist } from '@/shared/AppleMusic'
|
||||||
|
|
||||||
const token =
|
const headers = {
|
||||||
'Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IldlYlBsYXlLaWQifQ.eyJpc3MiOiJBTVBXZWJQbGF5IiwiaWF0IjoxNjQ2NjU1MDgwLCJleHAiOjE2NjIyMDcwODB9.pyOrt2FmP0cHkzYtO8KiEzQL2t1qpRszzxIYbLH7faXSddo6PQei771Ja3aGwGVU4hD99lZAw7bwat60iBcGiQ'
|
Authority: 'amp-api.music.apple.com',
|
||||||
|
Accept: '*/*',
|
||||||
|
Authorization:
|
||||||
|
'Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IldlYlBsYXlLaWQifQ.eyJpc3MiOiJBTVBXZWJQbGF5IiwiaWF0IjoxNjQ2NjU1MDgwLCJleHAiOjE2NjIyMDcwODB9.pyOrt2FmP0cHkzYtO8KiEzQL2t1qpRszzxIYbLH7faXSddo6PQei771Ja3aGwGVU4hD99lZAw7bwat60iBcGiQ',
|
||||||
|
Referer: 'https://music.apple.com/',
|
||||||
|
'Sec-Fetch-Dest': 'empty',
|
||||||
|
'Sec-Fetch-Mode': 'cors',
|
||||||
|
'Sec-Fetch-Site': 'cross-site',
|
||||||
|
'User-Agent':
|
||||||
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Cider/1.5.1 Chrome/100.0.4896.160 Electron/18.3.3 Safari/537.36',
|
||||||
|
'Accept-Encoding': 'gzip',
|
||||||
|
}
|
||||||
|
|
||||||
export const getCoverVideo = async ({
|
export const getAlbum = async ({
|
||||||
name,
|
name,
|
||||||
artist,
|
artist,
|
||||||
}: {
|
}: {
|
||||||
name: string
|
name: string
|
||||||
artist: string
|
artist: string
|
||||||
}): Promise<string | undefined> => {
|
}): Promise<AppleMusicAlbum | undefined> => {
|
||||||
const keyword = `${artist} ${name}`
|
const keyword = `${artist} ${name}`
|
||||||
log.debug(`[appleMusic] getCoverVideo: ${keyword}`)
|
log.debug(`[appleMusic] getAlbum: ${keyword}`)
|
||||||
const searchResult = await axios({
|
const searchResult = await axios({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
headers,
|
||||||
url: 'https://amp-api.music.apple.com/v1/catalog/us/search',
|
url: 'https://amp-api.music.apple.com/v1/catalog/us/search',
|
||||||
params: {
|
params: {
|
||||||
term: keyword,
|
term: keyword,
|
||||||
types: 'albums',
|
types: 'albums',
|
||||||
'fields[albums]': 'artistName,name,editorialVideo',
|
'fields[albums]': 'artistName,name,editorialVideo,editorialNotes',
|
||||||
platform: 'web',
|
platform: 'web',
|
||||||
limit: '1',
|
limit: '5',
|
||||||
},
|
l: 'en-us', // TODO: get from settings
|
||||||
headers: {
|
|
||||||
Authority: 'amp-api.music.apple.com',
|
|
||||||
Accept: '*/*',
|
|
||||||
Authorization: token,
|
|
||||||
Referer: 'http://localhost:9000/',
|
|
||||||
'Sec-Fetch-Dest': 'empty',
|
|
||||||
'Sec-Fetch-Mode': 'cors',
|
|
||||||
'Sec-Fetch-Site': 'cross-site',
|
|
||||||
'User-Agent':
|
|
||||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Cider/1.5.1 Chrome/100.0.4896.160 Electron/18.3.3 Safari/537.36',
|
|
||||||
'Accept-Encoding': 'gzip',
|
|
||||||
},
|
},
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
log.debug('[appleMusic] Search album error', e)
|
log.debug('[appleMusic] Search album error', e)
|
||||||
})
|
})
|
||||||
|
|
||||||
const album = searchResult?.data?.results?.albums?.data?.[0]
|
const albums = searchResult?.data?.results?.albums?.data as AppleMusicAlbum[]
|
||||||
|
const album =
|
||||||
|
albums.find(
|
||||||
|
a =>
|
||||||
|
a.attributes.name.toLowerCase() === name.toLowerCase() &&
|
||||||
|
a.attributes.artistName.toLowerCase() === artist.toLowerCase()
|
||||||
|
) || albums[0]
|
||||||
if (!album) {
|
if (!album) {
|
||||||
log.debug('[appleMusic] No album found on apple music')
|
log.debug('[appleMusic] No album found on apple music')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.debug(
|
|
||||||
`[appleMusic] Got ${album?.id}: ${album?.attributes?.name} by ${album?.attributes?.artistName}`
|
|
||||||
)
|
|
||||||
|
|
||||||
const url = album?.attributes?.editorialVideo?.motionSquareVideo1x1?.video
|
return album
|
||||||
if (!url) {
|
}
|
||||||
log.debug('[appleMusic] Album does not have video cover')
|
|
||||||
}
|
export const getArtist = async (
|
||||||
return url
|
name: string
|
||||||
|
): Promise<AppleMusicArtist | undefined> => {
|
||||||
|
const searchResult = await axios({
|
||||||
|
method: 'GET',
|
||||||
|
url: 'https://amp-api.music.apple.com/v1/catalog/us/search',
|
||||||
|
headers,
|
||||||
|
params: {
|
||||||
|
term: name,
|
||||||
|
types: 'artists',
|
||||||
|
'fields[artists]': 'url,name,artwork,editorialVideo,artistBio',
|
||||||
|
'omit[resource:artists]': 'relationships',
|
||||||
|
platform: 'web',
|
||||||
|
limit: '1',
|
||||||
|
l: 'en-us', // TODO: get from settings
|
||||||
|
},
|
||||||
|
}).catch(e => {
|
||||||
|
log.debug('[appleMusic] Search artist error', e)
|
||||||
|
})
|
||||||
|
|
||||||
|
const artist = searchResult?.data?.results?.artists?.data?.[0]
|
||||||
|
if (
|
||||||
|
artist &&
|
||||||
|
artist?.attributes?.name?.toLowerCase() === name.toLowerCase()
|
||||||
|
) {
|
||||||
|
return artist
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,13 +112,23 @@ class Cache {
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case APIs.VideoCover: {
|
case APIs.AppleMusicAlbum: {
|
||||||
if (!data.id) return
|
if (!data.id) return
|
||||||
db.upsert(Tables.VideoCover, {
|
db.upsert(Tables.AppleMusicAlbum, {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
url: data.url || 'no',
|
json: data.album ? JSON.stringify(data.album) : 'no',
|
||||||
queriedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
})
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case APIs.AppleMusicArtist: {
|
||||||
|
if (!data) return
|
||||||
|
db.upsert(Tables.AppleMusicArtist, {
|
||||||
|
id: data.id,
|
||||||
|
json: data.artist ? JSON.stringify(data.artist) : 'no',
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
})
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -130,6 +140,7 @@ class Cache {
|
||||||
case APIs.Personalized:
|
case APIs.Personalized:
|
||||||
case APIs.RecommendResource:
|
case APIs.RecommendResource:
|
||||||
case APIs.UserArtists:
|
case APIs.UserArtists:
|
||||||
|
case APIs.ListenedRecords:
|
||||||
case APIs.Likelist: {
|
case APIs.Likelist: {
|
||||||
const data = db.find(Tables.AccountData, api)
|
const data = db.find(Tables.AccountData, api)
|
||||||
if (data?.json) return JSON.parse(data.json)
|
if (data?.json) return JSON.parse(data.json)
|
||||||
|
|
@ -179,8 +190,14 @@ class Cache {
|
||||||
case APIs.Artist: {
|
case APIs.Artist: {
|
||||||
if (isNaN(Number(params?.id))) return
|
if (isNaN(Number(params?.id))) return
|
||||||
const data = db.find(Tables.Artist, params.id)
|
const data = db.find(Tables.Artist, params.id)
|
||||||
if (data?.json) return JSON.parse(data.json)
|
const fromAppleData = db.find(Tables.AppleMusicArtist, params.id)
|
||||||
break
|
const fromApple = fromAppleData?.json && JSON.parse(fromAppleData.json)
|
||||||
|
const fromNetease = data?.json && JSON.parse(data.json)
|
||||||
|
if (fromNetease && fromApple && fromApple !== 'no') {
|
||||||
|
fromNetease.artist.img1v1Url = fromApple.attributes.artwork.url
|
||||||
|
fromNetease.artist.briefDesc = fromApple.attributes.artistBio
|
||||||
|
}
|
||||||
|
return fromNetease ? fromNetease : undefined
|
||||||
}
|
}
|
||||||
case APIs.ArtistAlbum: {
|
case APIs.ArtistAlbum: {
|
||||||
if (isNaN(Number(params?.id))) return
|
if (isNaN(Number(params?.id))) return
|
||||||
|
|
@ -208,9 +225,29 @@ class Cache {
|
||||||
if (isNaN(Number(params?.id))) return
|
if (isNaN(Number(params?.id))) return
|
||||||
return db.find(Tables.CoverColor, params.id)?.color
|
return db.find(Tables.CoverColor, params.id)?.color
|
||||||
}
|
}
|
||||||
case APIs.VideoCover: {
|
case APIs.Artists: {
|
||||||
|
if (!params.ids?.length) return
|
||||||
|
const artists = db.findMany(Tables.Artist, params.ids)
|
||||||
|
if (artists.length !== params.ids.length) return
|
||||||
|
const result = artists.map(a => JSON.parse(a.json))
|
||||||
|
result.sort((a, b) => {
|
||||||
|
const indexA: number = params.ids.indexOf(a.artist.id)
|
||||||
|
const indexB: number = params.ids.indexOf(b.artist.id)
|
||||||
|
return indexA - indexB
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
case APIs.AppleMusicAlbum: {
|
||||||
if (isNaN(Number(params?.id))) return
|
if (isNaN(Number(params?.id))) return
|
||||||
return db.find(Tables.VideoCover, params.id)?.url
|
const data = db.find(Tables.AppleMusicAlbum, params.id)
|
||||||
|
if (data?.json && data.json !== 'no') return JSON.parse(data.json)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case APIs.AppleMusicArtist: {
|
||||||
|
if (isNaN(Number(params?.id))) return
|
||||||
|
const data = db.find(Tables.AppleMusicArtist, params.id)
|
||||||
|
if (data?.json && data.json !== 'no') return JSON.parse(data.json)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ export const enum Tables {
|
||||||
AccountData = 'AccountData',
|
AccountData = 'AccountData',
|
||||||
CoverColor = 'CoverColor',
|
CoverColor = 'CoverColor',
|
||||||
AppData = 'AppData',
|
AppData = 'AppData',
|
||||||
VideoCover = 'VideoCover',
|
AppleMusicAlbum = 'AppleMusicAlbum',
|
||||||
|
AppleMusicArtist = 'AppleMusicArtist',
|
||||||
}
|
}
|
||||||
interface CommonTableStructure {
|
interface CommonTableStructure {
|
||||||
id: number
|
id: number
|
||||||
|
|
@ -62,11 +63,8 @@ export interface TablesStructures {
|
||||||
id: 'appVersion' | 'skippedVersion'
|
id: 'appVersion' | 'skippedVersion'
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
[Tables.VideoCover]: {
|
[Tables.AppleMusicAlbum]: CommonTableStructure
|
||||||
id: number
|
[Tables.AppleMusicArtist]: CommonTableStructure
|
||||||
url: string
|
|
||||||
queriedAt: number
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TableNames = keyof TablesStructures
|
type TableNames = keyof TablesStructures
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { Thumbar } from './windowsTaskbar'
|
||||||
import fastFolderSize from 'fast-folder-size'
|
import fastFolderSize from 'fast-folder-size'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import prettyBytes from 'pretty-bytes'
|
import prettyBytes from 'pretty-bytes'
|
||||||
import { getCoverVideo } from './appleMusic'
|
import { getArtist, getAlbum } from './appleMusic'
|
||||||
|
|
||||||
const on = <T extends keyof IpcChannelsParams>(
|
const on = <T extends keyof IpcChannelsParams>(
|
||||||
channel: T,
|
channel: T,
|
||||||
|
|
@ -155,19 +155,39 @@ function initOtherIpcMain() {
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取动态封面
|
* 从Apple Music获取专辑信息
|
||||||
*/
|
*/
|
||||||
handle(IpcChannels.GetVideoCover, async (event, { id, name, artist }) => {
|
handle(
|
||||||
const fromCache = cache.get(APIs.VideoCover, { id })
|
IpcChannels.GetAlbumFromAppleMusic,
|
||||||
if (fromCache) {
|
async (event, { id, name, artist }) => {
|
||||||
return fromCache === 'no' ? undefined : fromCache
|
const fromCache = cache.get(APIs.AppleMusicAlbum, { id })
|
||||||
}
|
if (fromCache) {
|
||||||
|
return fromCache === 'no' ? undefined : fromCache
|
||||||
|
}
|
||||||
|
|
||||||
const fromApple = await getCoverVideo({ name, artist })
|
const fromApple = await getAlbum({ name, artist })
|
||||||
cache.set(APIs.VideoCover, { id, url: fromApple || 'no' })
|
cache.set(APIs.AppleMusicAlbum, { id, album: fromApple })
|
||||||
|
return fromApple
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从Apple Music获取歌手信息
|
||||||
|
**/
|
||||||
|
handle(IpcChannels.GetArtistFromAppleMusic, async (event, { id, name }) => {
|
||||||
|
const fromApple = await getArtist(name)
|
||||||
|
cache.set(APIs.AppleMusicArtist, { id, artist: fromApple })
|
||||||
return fromApple
|
return fromApple
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从缓存读取Apple Music歌手信息
|
||||||
|
*/
|
||||||
|
on(IpcChannels.GetArtistFromAppleMusic, (event, { id }) => {
|
||||||
|
const artist = cache.get(APIs.AppleMusicArtist, id)
|
||||||
|
event.returnValue = artist === 'no' ? undefined : artist
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出tables到json文件,方便查看table大小(dev环境)
|
* 导出tables到json文件,方便查看table大小(dev环境)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import type { FetchAudioSourceResponse } from '@/shared/api/Track'
|
||||||
import UNM from '@unblockneteasemusic/rust-napi'
|
import UNM from '@unblockneteasemusic/rust-napi'
|
||||||
import { APIs as CacheAPIs } from '@/shared/CacheAPIs'
|
import { APIs as CacheAPIs } from '@/shared/CacheAPIs'
|
||||||
import { isProd } from './utils'
|
import { isProd } from './utils'
|
||||||
|
import { APIs } from '@/shared/CacheAPIs'
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
port = Number(
|
port = Number(
|
||||||
|
|
@ -20,6 +21,8 @@ class Server {
|
||||||
: process.env.ELECTRON_DEV_NETEASE_API_PORT ?? 3000
|
: process.env.ELECTRON_DEV_NETEASE_API_PORT ?? 3000
|
||||||
)
|
)
|
||||||
app = express()
|
app = express()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
netease = require('NeteaseCloudMusicApi') as any
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
log.info('[server] starting http server')
|
log.info('[server] starting http server')
|
||||||
|
|
@ -28,16 +31,16 @@ class Server {
|
||||||
this.getAudioUrlHandler()
|
this.getAudioUrlHandler()
|
||||||
this.neteaseHandler()
|
this.neteaseHandler()
|
||||||
this.cacheAudioHandler()
|
this.cacheAudioHandler()
|
||||||
this.serveStaticForProd()
|
this.serveStaticForProduction()
|
||||||
this.listen()
|
this.listen()
|
||||||
}
|
}
|
||||||
|
|
||||||
neteaseHandler() {
|
neteaseHandler() {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
Object.entries(this.netease).forEach(([name, handler]: [string, any]) => {
|
||||||
const neteaseApi = require('NeteaseCloudMusicApi') as (params: any) => any[]
|
// 例外处理
|
||||||
|
if (
|
||||||
Object.entries(neteaseApi).forEach(([name, handler]) => {
|
['serveNcmApi', 'getModulesDefinitions', APIs.SongUrl].includes(name)
|
||||||
if (['serveNcmApi', 'getModulesDefinitions', 'song_url'].includes(name)) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,7 +75,7 @@ class Server {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
serveStaticForProd() {
|
serveStaticForProduction() {
|
||||||
if (isProd) {
|
if (isProd) {
|
||||||
this.app.use('/', express.static(path.join(__dirname, '../web/')))
|
this.app.use('/', express.static(path.join(__dirname, '../web/')))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,5 @@ CREATE TABLE IF NOT EXISTS "Track" ("id" integer NOT NULL,"json" text NOT NULL,"
|
||||||
CREATE TABLE IF NOT EXISTS "AppData" ("id" text NOT NULL,"value" text, PRIMARY KEY (id));
|
CREATE TABLE IF NOT EXISTS "AppData" ("id" text NOT NULL,"value" text, PRIMARY KEY (id));
|
||||||
CREATE TABLE IF NOT EXISTS "CoverColor" ("id" integer NOT NULL,"color" text NOT NULL, "queriedAt" int NOT NULL, PRIMARY KEY (id));
|
CREATE TABLE IF NOT EXISTS "CoverColor" ("id" integer NOT NULL,"color" text NOT NULL, "queriedAt" int NOT NULL, PRIMARY KEY (id));
|
||||||
CREATE TABLE IF NOT EXISTS "Audio" ("id" integer NOT NULL,"br" int NOT NULL,"type" text NOT NULL,"source" text NOT NULL,"updatedAt" int NOT NULL, "queriedAt" int NOT NULL, PRIMARY KEY (id));
|
CREATE TABLE IF NOT EXISTS "Audio" ("id" integer NOT NULL,"br" int NOT NULL,"type" text NOT NULL,"source" text NOT NULL,"updatedAt" int NOT NULL, "queriedAt" int NOT NULL, PRIMARY KEY (id));
|
||||||
CREATE TABLE IF NOT EXISTS "VideoCover" ("id" integer NOT NULL,"url" text NOT NULL,"updatedAt" int NOT NULL, "queriedAt" int NOT NULL, PRIMARY KEY (id));
|
CREATE TABLE IF NOT EXISTS "AppleMusicArtist" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));
|
||||||
|
CREATE TABLE IF NOT EXISTS "AppleMusicAlbum" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));
|
||||||
|
|
|
||||||
30
packages/shared/AppleMusic.ts
Normal file
30
packages/shared/AppleMusic.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
export interface AppleMusicAlbum {
|
||||||
|
attributes: {
|
||||||
|
name: string
|
||||||
|
artistName: string
|
||||||
|
editorialVideo: {
|
||||||
|
motionSquareVideo1x1: {
|
||||||
|
video: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editorialNotes: {
|
||||||
|
short: string
|
||||||
|
standard: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppleMusicArtist {
|
||||||
|
attributes: {
|
||||||
|
name: string
|
||||||
|
artistBio: string
|
||||||
|
editorialVideo: {
|
||||||
|
motionArtistSquare1x1: {
|
||||||
|
video: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
artwork: {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ import {
|
||||||
FetchPlaylistResponse,
|
FetchPlaylistResponse,
|
||||||
FetchRecommendedPlaylistsResponse,
|
FetchRecommendedPlaylistsResponse,
|
||||||
} from './api/Playlists'
|
} from './api/Playlists'
|
||||||
|
import { AppleMusicAlbum, AppleMusicArtist } from 'AppleMusic'
|
||||||
|
|
||||||
export const enum APIs {
|
export const enum APIs {
|
||||||
Album = 'album',
|
Album = 'album',
|
||||||
|
|
@ -41,8 +42,10 @@ export const enum APIs {
|
||||||
ListenedRecords = 'user/record',
|
ListenedRecords = 'user/record',
|
||||||
|
|
||||||
// not netease api
|
// not netease api
|
||||||
|
Artists = 'artistsNotNetease',
|
||||||
CoverColor = 'cover_color',
|
CoverColor = 'cover_color',
|
||||||
VideoCover = 'video_cover',
|
AppleMusicAlbum = 'apple_music_album',
|
||||||
|
AppleMusicArtist = 'apple_music_artist',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface APIsParams {
|
export interface APIsParams {
|
||||||
|
|
@ -61,9 +64,12 @@ export interface APIsParams {
|
||||||
[APIs.UserArtists]: void
|
[APIs.UserArtists]: void
|
||||||
[APIs.UserPlaylist]: void
|
[APIs.UserPlaylist]: void
|
||||||
[APIs.SimilarArtist]: { id: number }
|
[APIs.SimilarArtist]: { id: number }
|
||||||
[APIs.CoverColor]: { id: number }
|
|
||||||
[APIs.VideoCover]: { id: number }
|
|
||||||
[APIs.ListenedRecords]: { id: number; type: number }
|
[APIs.ListenedRecords]: { id: number; type: number }
|
||||||
|
|
||||||
|
[APIs.Artists]: { ids: number[] }
|
||||||
|
[APIs.CoverColor]: { id: number }
|
||||||
|
[APIs.AppleMusicAlbum]: { id: number }
|
||||||
|
[APIs.AppleMusicArtist]: { id: number }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface APIsResponse {
|
export interface APIsResponse {
|
||||||
|
|
@ -82,7 +88,10 @@ export interface APIsResponse {
|
||||||
[APIs.UserArtists]: FetchUserArtistsResponse
|
[APIs.UserArtists]: FetchUserArtistsResponse
|
||||||
[APIs.UserPlaylist]: FetchUserPlaylistsResponse
|
[APIs.UserPlaylist]: FetchUserPlaylistsResponse
|
||||||
[APIs.SimilarArtist]: FetchSimilarArtistsResponse
|
[APIs.SimilarArtist]: FetchSimilarArtistsResponse
|
||||||
[APIs.CoverColor]: string | undefined
|
|
||||||
[APIs.VideoCover]: string | undefined
|
|
||||||
[APIs.ListenedRecords]: FetchListenedRecordsResponse
|
[APIs.ListenedRecords]: FetchListenedRecordsResponse
|
||||||
|
|
||||||
|
[APIs.Artists]: FetchArtistResponse[]
|
||||||
|
[APIs.CoverColor]: string | undefined
|
||||||
|
[APIs.AppleMusicAlbum]: AppleMusicAlbum | 'no'
|
||||||
|
[APIs.AppleMusicArtist]: AppleMusicArtist | 'no'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { AppleMusicAlbum, AppleMusicArtist } from './AppleMusic'
|
||||||
import { APIs } from './CacheAPIs'
|
import { APIs } from './CacheAPIs'
|
||||||
import { RepeatMode } from './playerDataTypes'
|
import { RepeatMode } from './playerDataTypes'
|
||||||
|
|
||||||
|
|
@ -22,8 +23,8 @@ export const enum IpcChannels {
|
||||||
SyncSettings = 'SyncSettings',
|
SyncSettings = 'SyncSettings',
|
||||||
GetAudioCacheSize = 'GetAudioCacheSize',
|
GetAudioCacheSize = 'GetAudioCacheSize',
|
||||||
ResetWindowSize = 'ResetWindowSize',
|
ResetWindowSize = 'ResetWindowSize',
|
||||||
GetVideoCover = 'GetVideoCover',
|
GetAlbumFromAppleMusic = 'GetAlbumFromAppleMusic',
|
||||||
SetVideoCover = 'SetVideoCover',
|
GetArtistFromAppleMusic = 'GetArtistFromAppleMusic',
|
||||||
}
|
}
|
||||||
|
|
||||||
// ipcMain.on params
|
// ipcMain.on params
|
||||||
|
|
@ -59,8 +60,12 @@ export interface IpcChannelsParams {
|
||||||
[IpcChannels.SyncSettings]: any
|
[IpcChannels.SyncSettings]: any
|
||||||
[IpcChannels.GetAudioCacheSize]: void
|
[IpcChannels.GetAudioCacheSize]: void
|
||||||
[IpcChannels.ResetWindowSize]: void
|
[IpcChannels.ResetWindowSize]: void
|
||||||
[IpcChannels.GetVideoCover]: { id: number; name: string; artist: string }
|
[IpcChannels.GetAlbumFromAppleMusic]: {
|
||||||
[IpcChannels.SetVideoCover]: { id: number; url: string }
|
id: number
|
||||||
|
name: string
|
||||||
|
artist: string
|
||||||
|
}
|
||||||
|
[IpcChannels.GetArtistFromAppleMusic]: { id: number; name: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ipcRenderer.on params
|
// ipcRenderer.on params
|
||||||
|
|
@ -82,6 +87,6 @@ export interface IpcChannelsReturns {
|
||||||
[IpcChannels.Like]: void
|
[IpcChannels.Like]: void
|
||||||
[IpcChannels.Repeat]: RepeatMode
|
[IpcChannels.Repeat]: RepeatMode
|
||||||
[IpcChannels.GetAudioCacheSize]: void
|
[IpcChannels.GetAudioCacheSize]: void
|
||||||
[IpcChannels.GetVideoCover]: string | undefined
|
[IpcChannels.GetAlbumFromAppleMusic]: AppleMusicAlbum | undefined
|
||||||
[IpcChannels.SetVideoCover]: void
|
[IpcChannels.GetArtistFromAppleMusic]: AppleMusicArtist | undefined
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,13 @@ export default function useArtists(ids: number[]) {
|
||||||
{
|
{
|
||||||
enabled: !!ids && ids.length > 0,
|
enabled: !!ids && ids.length > 0,
|
||||||
staleTime: 5 * 60 * 1000, // 5 mins
|
staleTime: 5 * 60 * 1000, // 5 mins
|
||||||
// placeholderData: (): FetchArtistResponse[] =>
|
initialData: (): FetchArtistResponse[] =>
|
||||||
// window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, {
|
window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, {
|
||||||
// api: APIs.Artist,
|
api: APIs.Artists,
|
||||||
// query: {
|
query: {
|
||||||
// ids,
|
ids,
|
||||||
// },
|
},
|
||||||
// }),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export default function useUserListenedRecords(params: {
|
||||||
enabled: !!uid,
|
enabled: !!uid,
|
||||||
placeholderData: (): FetchListenedRecordsResponse =>
|
placeholderData: (): FetchListenedRecordsResponse =>
|
||||||
window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, {
|
window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, {
|
||||||
api: APIs.UserArtists,
|
api: APIs.ListenedRecords,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,30 @@ const Artist = ({ artist }: { artist: Artist }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Placeholder = ({ row }: { row: number }) => {
|
||||||
|
return (
|
||||||
|
<div className='no-scrollbar flex snap-x overflow-x-scroll lg:grid lg:w-auto lg:grid-cols-5 lg:gap-10'>
|
||||||
|
{[...new Array(row * 5).keys()].map(i => (
|
||||||
|
<div
|
||||||
|
className='flex snap-start flex-col items-center px-2.5 lg:px-0'
|
||||||
|
key={i}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='aspect-square w-full rounded-full bg-white dark:bg-neutral-800'
|
||||||
|
style={{
|
||||||
|
minHeight: '96px',
|
||||||
|
minWidth: '96px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className='line-clamp-1 mt-2.5 w-1/2 rounded-full text-12 font-medium text-transparent dark:bg-neutral-800 lg:text-14 lg:font-bold'>
|
||||||
|
NAME
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const ArtistRow = ({
|
const ArtistRow = ({
|
||||||
artists,
|
artists,
|
||||||
title,
|
title,
|
||||||
|
|
@ -70,24 +94,7 @@ const ArtistRow = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Placeholder */}
|
{/* Placeholder */}
|
||||||
{placeholderRow && !artists && (
|
{placeholderRow && !artists && <Placeholder row={placeholderRow} />}
|
||||||
<div className='no-scrollbar flex snap-x overflow-x-scroll lg:grid lg:w-auto lg:grid-cols-5 lg:gap-10'>
|
|
||||||
{[...new Array(placeholderRow * 5).keys()].map(i => (
|
|
||||||
<div className='snap-start px-2.5 lg:px-0' key={i}>
|
|
||||||
<div
|
|
||||||
className='aspect-square w-full rounded-full bg-white dark:bg-neutral-800'
|
|
||||||
style={{
|
|
||||||
minHeight: '96px',
|
|
||||||
minWidth: '96px',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className='mt-2.5 text-12 font-medium text-transparent lg:text-14 lg:font-bold'>
|
|
||||||
PLACE
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ const TrackList = ({ className }: { className?: string }) => {
|
||||||
overscan={10}
|
overscan={10}
|
||||||
components={{
|
components={{
|
||||||
Header: () => <div className='h-8'></div>,
|
Header: () => <div className='h-8'></div>,
|
||||||
Footer: () => <div className='h-1'></div>,
|
Footer: () => <div className='h-8'></div>,
|
||||||
}}
|
}}
|
||||||
itemContent={(index, track) => (
|
itemContent={(index, track) => (
|
||||||
<Track
|
<Track
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,10 @@ const NavigationButtons = () => {
|
||||||
await controlsBack.start({ x: -5 })
|
await controlsBack.start({ x: -5 })
|
||||||
await controlsBack.start({ x: 0 })
|
await controlsBack.start({ x: 0 })
|
||||||
}}
|
}}
|
||||||
className='app-region-no-drag rounded-full bg-day-600 p-2.5 dark:bg-night-600'
|
className='app-region-no-drag rounded-full bg-white/10 p-2.5 text-white/40 backdrop-blur-3xl'
|
||||||
>
|
>
|
||||||
<motion.div animate={controlsBack} transition={transition}>
|
<motion.div animate={controlsBack} transition={transition}>
|
||||||
<Icon name='back' className='h-7 w-7 text-neutral-500' />
|
<Icon name='back' className='h-7 w-7 ' />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|
@ -32,10 +32,10 @@ const NavigationButtons = () => {
|
||||||
await controlsForward.start({ x: 5 })
|
await controlsForward.start({ x: 5 })
|
||||||
await controlsForward.start({ x: 0 })
|
await controlsForward.start({ x: 0 })
|
||||||
}}
|
}}
|
||||||
className='app-region-no-drag ml-2.5 rounded-full bg-day-600 p-2.5 dark:bg-night-600'
|
className='app-region-no-drag ml-2.5 rounded-full bg-white/10 p-2.5 text-white/40 backdrop-blur-3xl'
|
||||||
>
|
>
|
||||||
<motion.div animate={controlsForward} transition={transition}>
|
<motion.div animate={controlsForward} transition={transition}>
|
||||||
<Icon name='forward' className='h-7 w-7 text-neutral-500' />
|
<Icon name='forward' className='h-7 w-7' />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const SearchBox = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
'app-region-no-drag flex items-center rounded-full bg-day-600 p-2.5 text-neutral-500 dark:bg-night-600',
|
'app-region-no-drag flex items-center rounded-full bg-white/10 p-2.5 text-white/40 backdrop-blur-3xl',
|
||||||
css`
|
css`
|
||||||
${bp.lg} {
|
${bp.lg} {
|
||||||
min-width: 284px;
|
min-width: 284px;
|
||||||
|
|
@ -23,7 +23,7 @@ const SearchBox = () => {
|
||||||
<input
|
<input
|
||||||
placeholder='Search'
|
placeholder='Search'
|
||||||
className={cx(
|
className={cx(
|
||||||
'flex-shrink bg-transparent font-medium placeholder:text-neutral-500 dark:text-neutral-200',
|
'flex-shrink bg-transparent font-medium placeholder:text-white/40 dark:text-white/80',
|
||||||
css`
|
css`
|
||||||
@media (max-width: 420px) {
|
@media (max-width: 420px) {
|
||||||
width: 142px;
|
width: 142px;
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,21 @@ import uiStates from '@/web/states/uiStates'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
import { ease } from '@/web/utils/const'
|
import { ease } from '@/web/utils/const'
|
||||||
|
import { useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
const Background = () => {
|
const Background = () => {
|
||||||
const { hideTopbarBackground } = useSnapshot(uiStates)
|
const { hideTopbarBackground } = useSnapshot(uiStates)
|
||||||
|
const location = useLocation()
|
||||||
|
const isPageHaveBlurBG =
|
||||||
|
location.pathname.startsWith('/album/') ||
|
||||||
|
location.pathname.startsWith('/artist/') ||
|
||||||
|
location.pathname.startsWith('/playlist/')
|
||||||
|
const show = !hideTopbarBackground || !isPageHaveBlurBG
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{!hideTopbarBackground && (
|
{show && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import { ease } from '@/web/utils/const'
|
||||||
import { injectGlobal } from '@emotion/css'
|
import { injectGlobal } from '@emotion/css'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import BlurBackground from '@/web/components/New/BlurBackground'
|
import BlurBackground from '@/web/components/New/BlurBackground'
|
||||||
|
import useAppleMusicAlbum from '@/web/hooks/useAppleMusicAlbum'
|
||||||
|
import { AppleMusicAlbum } from '@/shared/AppleMusic'
|
||||||
|
|
||||||
injectGlobal`
|
injectGlobal`
|
||||||
.plyr__video-wrapper,
|
.plyr__video-wrapper,
|
||||||
|
|
@ -47,12 +49,20 @@ const VideoCover = ({ source }: { source?: string }) => {
|
||||||
|
|
||||||
const Cover = memo(
|
const Cover = memo(
|
||||||
({ album, playlist }: { album?: Album; playlist?: Playlist }) => {
|
({ album, playlist }: { album?: Album; playlist?: Playlist }) => {
|
||||||
const isMobile = useIsMobile()
|
const { data: albumFromApple } = useAppleMusicAlbum({
|
||||||
const { data: videoCover } = useVideoCover({
|
|
||||||
id: album?.id,
|
id: album?.id,
|
||||||
name: album?.name,
|
name: album?.name,
|
||||||
artist: album?.artist.name,
|
artist: album?.artist.name,
|
||||||
})
|
})
|
||||||
|
const { data: videoCoverFromRemote } = useVideoCover({
|
||||||
|
id: album?.id,
|
||||||
|
name: album?.name,
|
||||||
|
artist: album?.artist.name,
|
||||||
|
enabled: !window.env?.isElectron,
|
||||||
|
})
|
||||||
|
const videoCover =
|
||||||
|
albumFromApple?.attributes?.editorialVideo?.motionSquareVideo1x1?.video ||
|
||||||
|
videoCoverFromRemote
|
||||||
const cover = album?.picUrl || playlist?.coverImgUrl || ''
|
const cover = album?.picUrl || playlist?.coverImgUrl || ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -111,6 +121,12 @@ const TrackListHeader = ({
|
||||||
const duration = album?.songs?.reduce((acc, cur) => acc + cur.dt, 0) || 0
|
const duration = album?.songs?.reduce((acc, cur) => acc + cur.dt, 0) || 0
|
||||||
return formatDuration(duration, 'en', 'hh[hr] mm[min]')
|
return formatDuration(duration, 'en', 'hh[hr] mm[min]')
|
||||||
}, [album?.songs])
|
}, [album?.songs])
|
||||||
|
const { data: albumFromApple, isLoading: isLoadingAlbumFromApple } =
|
||||||
|
useAppleMusicAlbum({
|
||||||
|
id: album?.id,
|
||||||
|
name: album?.name,
|
||||||
|
artist: album?.artist.name,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -172,9 +188,17 @@ const TrackListHeader = ({
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<div className='line-clamp-3 mt-6 whitespace-pre-wrap text-14 font-bold dark:text-night-400 '>
|
<div
|
||||||
{album?.description || playlist?.description}
|
className='line-clamp-3 mt-6 whitespace-pre-wrap text-14 font-bold dark:text-night-400 '
|
||||||
</div>
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: !isLoadingAlbumFromApple
|
||||||
|
? albumFromApple?.attributes?.editorialNotes?.standard ||
|
||||||
|
album?.description ||
|
||||||
|
playlist?.description ||
|
||||||
|
''
|
||||||
|
: '',
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
26
packages/web/hooks/useAppleMusicAlbum.ts
Normal file
26
packages/web/hooks/useAppleMusicAlbum.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { IpcChannels } from '@/shared/IpcChannels'
|
||||||
|
import { useQuery } from 'react-query'
|
||||||
|
|
||||||
|
export default function useAppleMusicAlbum(props: {
|
||||||
|
id?: number
|
||||||
|
name?: string
|
||||||
|
artist?: string
|
||||||
|
}) {
|
||||||
|
const { id, name, artist } = props
|
||||||
|
return useQuery(
|
||||||
|
['useAppleMusicAlbum', props],
|
||||||
|
async () => {
|
||||||
|
if (!id || !name || !artist) return
|
||||||
|
return window.ipcRenderer?.invoke(IpcChannels.GetAlbumFromAppleMusic, {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
artist,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!id && !!name && !!artist,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchInterval: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
33
packages/web/hooks/useAppleMusicArtist.ts
Normal file
33
packages/web/hooks/useAppleMusicArtist.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { AppleMusicArtist } from '@/shared/AppleMusic'
|
||||||
|
import { APIs } from '@/shared/CacheAPIs'
|
||||||
|
import { IpcChannels } from '@/shared/IpcChannels'
|
||||||
|
import { useQuery } from 'react-query'
|
||||||
|
|
||||||
|
export default function useAppleMusicArtist(props: {
|
||||||
|
id?: number
|
||||||
|
name?: string
|
||||||
|
}) {
|
||||||
|
const { id, name } = props
|
||||||
|
return useQuery(
|
||||||
|
['useAppleMusicArtist', props],
|
||||||
|
async () => {
|
||||||
|
if (!id || !name) return
|
||||||
|
return window.ipcRenderer?.invoke(IpcChannels.GetArtistFromAppleMusic, {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!id && !!name,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchInterval: false,
|
||||||
|
initialData: (): AppleMusicArtist =>
|
||||||
|
window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, {
|
||||||
|
api: APIs.AppleMusicArtist,
|
||||||
|
query: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { IpcChannels } from '@/shared/IpcChannels'
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { useQuery } from 'react-query'
|
import { useQuery } from 'react-query'
|
||||||
|
|
||||||
|
|
@ -6,25 +5,14 @@ export default function useVideoCover(props: {
|
||||||
id?: number
|
id?: number
|
||||||
name?: string
|
name?: string
|
||||||
artist?: string
|
artist?: string
|
||||||
|
enabled?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { id, name, artist } = props
|
const { id, name, artist, enabled = true } = props
|
||||||
return useQuery(
|
return useQuery(
|
||||||
['useVideoCover', props],
|
['useVideoCover', props],
|
||||||
async () => {
|
async () => {
|
||||||
if (!id || !name || !artist) return
|
if (!id || !name || !artist) return
|
||||||
|
|
||||||
const fromMainProcess = await window.ipcRenderer?.invoke(
|
|
||||||
IpcChannels.GetVideoCover,
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
artist,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (fromMainProcess) {
|
|
||||||
return fromMainProcess
|
|
||||||
}
|
|
||||||
|
|
||||||
const fromRemote = await axios.get('/yesplaymusic/video-cover', {
|
const fromRemote = await axios.get('/yesplaymusic/video-cover', {
|
||||||
params: props,
|
params: props,
|
||||||
})
|
})
|
||||||
|
|
@ -33,7 +21,7 @@ export default function useVideoCover(props: {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!id && !!name && !!artist,
|
enabled: !!id && !!name && !!artist && enabled,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchInterval: false,
|
refetchInterval: false,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,20 @@
|
||||||
import useIsMobile from '@/web/hooks/useIsMobile'
|
import useIsMobile from '@/web/hooks/useIsMobile'
|
||||||
|
import useAppleMusicArtist from '@/web/hooks/useAppleMusicArtist'
|
||||||
|
|
||||||
const ArtistInfo = ({ artist }: { artist?: Artist }) => {
|
const ArtistInfo = ({ artist }: { artist?: Artist }) => {
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
|
const { data: artistFromApple, isLoading: isLoadingArtistFromApple } =
|
||||||
|
useAppleMusicArtist({
|
||||||
|
id: artist?.id,
|
||||||
|
name: artist?.name,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='text-28 font-semibold text-night-50 lg:text-32'>
|
<div className='text-28 font-semibold text-night-50 lg:text-32'>
|
||||||
{artist?.name}
|
{artist?.name}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-white-400 mt-2.5 text-24 font-medium lg:mt-6'>
|
<div className='mt-2.5 text-24 font-medium text-night-400 lg:mt-6'>
|
||||||
Artist
|
Artist
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-1 text-12 font-medium text-night-400'>
|
<div className='mt-1 text-12 font-medium text-night-400'>
|
||||||
|
|
@ -16,9 +23,9 @@ const ArtistInfo = ({ artist }: { artist?: Artist }) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
{!isMobile && (
|
{!isMobile && !isLoadingArtistFromApple && (
|
||||||
<div className='line-clamp-5 mt-6 text-14 font-bold text-night-400'>
|
<div className='line-clamp-5 mt-6 text-14 font-bold text-night-400'>
|
||||||
{artist?.briefDesc}
|
{artistFromApple?.attributes?.artistBio || artist?.briefDesc}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,15 @@ import BlurBackground from '@/web/components/New/BlurBackground'
|
||||||
import ArtistInfo from './ArtistInfo'
|
import ArtistInfo from './ArtistInfo'
|
||||||
import Actions from './Actions'
|
import Actions from './Actions'
|
||||||
import LatestRelease from './LatestRelease'
|
import LatestRelease from './LatestRelease'
|
||||||
|
import useAppleMusicArtist from '@/web/hooks/useAppleMusicArtist'
|
||||||
|
|
||||||
const Header = ({ artist }: { artist?: Artist }) => {
|
const Header = ({ artist }: { artist?: Artist }) => {
|
||||||
|
const { data: artistFromApple, isLoading: isLoadingArtistFromApple } =
|
||||||
|
useAppleMusicArtist({
|
||||||
|
id: artist?.id,
|
||||||
|
name: artist?.name,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
|
|
@ -27,10 +34,23 @@ const Header = ({ artist }: { artist?: Artist }) => {
|
||||||
grid-area: cover;
|
grid-area: cover;
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
src={resizeImage(artist?.img1v1Url || '', 'lg')}
|
src={resizeImage(
|
||||||
|
isLoadingArtistFromApple
|
||||||
|
? ''
|
||||||
|
: artistFromApple?.attributes?.artwork?.url ||
|
||||||
|
artist?.img1v1Url ||
|
||||||
|
'',
|
||||||
|
'lg'
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BlurBackground cover={artist?.img1v1Url} />
|
<BlurBackground
|
||||||
|
cover={
|
||||||
|
isLoadingArtistFromApple
|
||||||
|
? ''
|
||||||
|
: artistFromApple?.attributes?.artwork?.url || artist?.img1v1Url
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ class ScrollPositions {
|
||||||
}
|
}
|
||||||
|
|
||||||
set(pathname: string, top: number) {
|
set(pathname: string, top: number) {
|
||||||
console.log('set', pathname, top)
|
|
||||||
const nestedPath = `/${pathname.split('/')[1]}`
|
const nestedPath = `/${pathname.split('/')[1]}`
|
||||||
const restPath = pathname.split('/').slice(2).join('/')
|
const restPath = pathname.split('/').slice(2).join('/')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,12 @@ export function resizeImage(
|
||||||
md: '512',
|
md: '512',
|
||||||
lg: '1024',
|
lg: '1024',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (url.includes('mzstatic.com')) {
|
||||||
|
// from Apple Music
|
||||||
|
return url.replace('{w}', sizeMap[size]).replace('{h}', sizeMap[size])
|
||||||
|
}
|
||||||
|
|
||||||
return `${url}?param=${sizeMap[size]}y${sizeMap[size]}`.replace(
|
return `${url}?param=${sizeMap[size]}y${sizeMap[size]}`.replace(
|
||||||
/http(s?):\/\/p\d.music.126.net/,
|
/http(s?):\/\/p\d.music.126.net/,
|
||||||
'https://p1.music.126.net'
|
'https://p1.music.126.net'
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue