From 719a3a60d4abf38f4b4ad806b73217fc44b94d39 Mon Sep 17 00:00:00 2001 From: memorydream <34763046+memorydream@users.noreply.github.com> Date: Sat, 2 Apr 2022 02:13:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E7=A7=81=E4=BA=BAFM?= =?UTF-8?q?=20(#1453)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 实现私人FM * 根据建议修改 * fix: APP启动时FMCard无法加载数据 * fix: coverUrl使用useMemo * fix: 在私人FM模式下禁用单曲循环 * fix: 私人FM模式下禁用部分按钮 * fix: 限制FMCard的歌手长度 * fix: 移除ArtistsInline的clamp参数,并将其作为隐式Fallback --- src/renderer/api/personalFM.ts | 116 ++++++++++++++++++++++ src/renderer/components/ArtistsInline.tsx | 7 +- src/renderer/components/FMCard.tsx | 110 ++++++++++++++------ src/renderer/components/Player.tsx | 30 ++++-- src/renderer/hooks/usePersonalFM.ts | 14 +++ src/renderer/utils/player.ts | 114 ++++++++++++++++++++- 6 files changed, 351 insertions(+), 40 deletions(-) create mode 100644 src/renderer/api/personalFM.ts create mode 100644 src/renderer/hooks/usePersonalFM.ts diff --git a/src/renderer/api/personalFM.ts b/src/renderer/api/personalFM.ts new file mode 100644 index 0000000..8cf4445 --- /dev/null +++ b/src/renderer/api/personalFM.ts @@ -0,0 +1,116 @@ +import request from '@/utils/request' + +export enum PersonalFMApiNames { + FETCH_PERSONAL_FM = 'fetchPersonalFM', +} + +export interface PersonalMusic { + name: null | string + id: number + size: number + extension: 'mp3' | 'flac' | null + sr: number + dfsId: number + bitrate: number + playTime: number + volumeDelta: number +} + +export interface FetchPersonalFMResponse { + code: number + popAdjust: boolean + data: { + name: string + id: number + position: number + alias: string[] + status: number + fee: number + copyrightId: number + disc?: string + no: number + artists: Artist[] + album: Album + starred: boolean + popularity: number + score: number + starredNum: number + duration: number + playedNum: number + dayPlays: number + hearTime: number + ringtone: null + crbt: null + audition: null + copyFrom: string + commentThreadId: string + rtUrl: string | null + ftype: number + rtUrls: (string | null)[] + copyright: number + transName: null | string + sign: null + mark: number + originCoverType: number + originSongSimpleData: null + single: number + noCopyrightRcmd: null + mvid: number + bMusic?: PersonalMusic + lMusic?: PersonalMusic + mMusic?: PersonalMusic + hMusic?: PersonalMusic + reason: string + privilege: { + id: number + fee: number + payed: number + st: number + pl: number + dl: number + sp: number + cp: number + subp: number + cs: boolean + maxbr: number + fl: number + toast: boolean + flag: number + preShell: boolean + playMaxbr: number + downloadMaxbr: number + rscl: null + freeTrialPrivilege: { + [key: string]: unknown + } + chargeInfoList: { + [key: string]: unknown + }[] + } + alg: string + s_ctrp: string + }[] +} + +export function fetchPersonalFM(): Promise { + return request({ + url: '/personal/fm', + method: 'get', + }) +} + +export interface FMTrashResponse { + songs: null[] + code: number + count: number +} + +export function fmTrash(id: number): Promise { + return request({ + url: '/fm/trash', + method: 'post', + params: { + id, + }, + }) +} diff --git a/src/renderer/components/ArtistsInline.tsx b/src/renderer/components/ArtistsInline.tsx index a6b8ca1..0210d44 100644 --- a/src/renderer/components/ArtistsInline.tsx +++ b/src/renderer/components/ArtistsInline.tsx @@ -15,7 +15,12 @@ const ArtistInline = ({ } return ( -
+
{artists.map((artist, index) => ( { + const classes = + 'btn-pressed-animation btn-hover-animation mr-1 cursor-default rounded-lg p-1.5 transition duration-200 after:bg-white/10' + + const playerSnapshot = useSnapshot(player) + const state = useMemo(() => playerSnapshot.state, [playerSnapshot.state]) + + const playOrPause = () => { + if (playerSnapshot.mode === PlayerMode.FM) { + player.playOrPause() + } else { + player.playFM() + } + } + + return ( +
+ + + + + +
+ ) } const FMCard = () => { - const coverUrl = - 'https://p1.music.126.net/lEzPSOjusKaRXKXT3987lQ==/109951166035876388.jpg?param=512y512' const [background, setBackground] = useState('') + const playerSnapshot = useSnapshot(player) + const track = useMemo(() => playerSnapshot.fmTrack, [playerSnapshot.fmTrack]) + const coverUrl = useMemo( + () => resizeImage(playerSnapshot.fmTrack?.al?.picUrl ?? '', 'md'), + [playerSnapshot.fmTrack] + ) + useEffect(() => { - average(coverUrl, { amount: 1, format: 'hex', sample: 1 }).then(color => { - const to = colord(color as string) - .darken(0.15) - .rotate(-5) - .toHex() - setBackground(`linear-gradient(to bottom right, ${color}, ${to})`) - }) + if (coverUrl) { + average(coverUrl, { amount: 1, format: 'hex', sample: 1 }).then(color => { + const to = colord(color as string) + .darken(0.15) + .rotate(-5) + .toHex() + setBackground(`linear-gradient(to bottom right, ${color}, ${to})`) + }) + } else { + setBackground(`linear-gradient(to bottom right, #66ccff, #ee0000)`) + } }, [coverUrl]) return ( @@ -28,28 +83,20 @@ const FMCard = () => { className='relative flex h-[198px] overflow-hidden rounded-2xl p-4' style={{ background }} > - + {coverUrl && }
{/* Track info */}
-
How Can I Make It OK?
-
Wolf Alice
+
{track?.name}
+
- {/* Actions */} - -
- {Object.values(ACTION).map(action => ( - - ))} -
+ {/* FM logo */}
@@ -62,4 +109,11 @@ const FMCard = () => { ) } +/** + * 不能在player的构造函数中调用initFM + * 那样在APP启动时不会加载私人FM的数据 + * 只能在这里进行数据的初始化 + */ +player.initFM() + export default FMCard diff --git a/src/renderer/components/Player.tsx b/src/renderer/components/Player.tsx index 490965c..a7d51ff 100644 --- a/src/renderer/components/Player.tsx +++ b/src/renderer/components/Player.tsx @@ -4,7 +4,7 @@ import Slider from '@/components/Slider' import SvgIcon from '@/components/SvgIcon' import { player } from '@/store' import { resizeImage } from '@/utils/common' -import { State as PlayerState } from '@/utils/player' +import { State as PlayerState, Mode as PlayerMode } from '@/utils/player' const PlayingTrack = () => { const navigate = useNavigate() @@ -74,11 +74,22 @@ const MediaControls = () => { const playerSnapshot = useSnapshot(player) const state = useMemo(() => playerSnapshot.state, [playerSnapshot.state]) const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track]) + const mode = useMemo(() => playerSnapshot.mode, [playerSnapshot.mode]) return (
- track && player.prevTrack()} disabled={!track}> - - + {mode === PlayerMode.PLAYLIST && ( + track && player.prevTrack()} + disabled={!track} + > + + + )} + {mode === PlayerMode.FM && ( + player.fmTrash()}> + + + )} track && player.playOrPause()} disabled={!track} @@ -101,15 +112,20 @@ const MediaControls = () => { } const Others = () => { + const playerSnapshot = useSnapshot(player) + const mode = useMemo(() => playerSnapshot.mode, [playerSnapshot.mode]) + + const isFM = () => mode === PlayerMode.FM + return (
- toast('Work in progress')}> + toast('Work in progress')} disabled={isFM()}> - toast('Work in progress')}> + toast('Work in progress')} disabled={isFM()}> - toast('Work in progress')}> + toast('Work in progress')} disabled={isFM()}> toast('Work in progress')}> diff --git a/src/renderer/hooks/usePersonalFM.ts b/src/renderer/hooks/usePersonalFM.ts new file mode 100644 index 0000000..2f81493 --- /dev/null +++ b/src/renderer/hooks/usePersonalFM.ts @@ -0,0 +1,14 @@ +import { fetchPersonalFM, PersonalFMApiNames } from '@/api/personalFM' +import reactQueryClient from '@/utils/reactQueryClient' + +export function fetchPersonalFMWithReactQuery() { + return reactQueryClient.fetchQuery( + PersonalFMApiNames.FETCH_PERSONAL_FM, + () => { + return fetchPersonalFM() + }, + { + retry: 3, + } + ) +} diff --git a/src/renderer/utils/player.ts b/src/renderer/utils/player.ts index 8a9e1f4..510c958 100644 --- a/src/renderer/utils/player.ts +++ b/src/renderer/utils/player.ts @@ -3,6 +3,8 @@ import { fetchAudioSourceWithReactQuery, fetchTracksWithReactQuery, } from '@/hooks/useTracks' +import { fetchPersonalFMWithReactQuery } from '@/hooks/usePersonalFM' +import { fmTrash } from '@/api/personalFM' import { cacheAudio } from '@/api/yesplaymusic' import { clamp } from 'lodash-es' @@ -41,11 +43,14 @@ export class Player { private _progress: number = 0 private _progressInterval: ReturnType | undefined private _volume: number = 1 // 0 to 1 + private _fmTrack: Track | null = null + private _fmInited = false state: State = State.INITIALIZING mode: Mode = Mode.PLAYLIST trackList: TrackID[] = [] trackListSource: TrackListSource | null = null + fmTrackList: TrackID[] = [] shuffle: boolean = false repeatMode: RepeatMode = RepeatMode.OFF @@ -89,8 +94,11 @@ export class Player { * Get current playing track ID */ get trackID(): TrackID { - const { trackList, _trackIndex } = this - return trackList[_trackIndex] ?? 0 + if (this.mode === Mode.PLAYLIST) { + const { trackList, _trackIndex } = this + return trackList[_trackIndex] ?? 0 + } + return this.fmTrackList[0] ?? 0 } /** @@ -100,6 +108,10 @@ export class Player { return this._track ?? null } + get fmTrack(): Track | null { + return this._fmTrack ?? null + } + /** * Get/Set progress of current track */ @@ -154,6 +166,7 @@ export class Player { private async _playTrack() { const track = await this._fetchTrack(this.trackID) if (track) this._track = track + if (track && this.mode === Mode.FM) this._fmTrack = track this._playAudio() } @@ -188,7 +201,7 @@ export class Player { private _howlerOnEndCallback() { console.log('_howlerOnEndCallback') - if (this.repeatMode === RepeatMode.ONE) { + if (this.mode !== Mode.FM && this.repeatMode === RepeatMode.ONE) { _howler.seek(0) _howler.play() } else { @@ -201,6 +214,27 @@ export class Player { cacheAudio(id, audio) } + private async _nextFMTrack() { + if (this.fmTrackList.length <= 1) { + for (let i = 0; i < 5; i++) { + const response = await fetchPersonalFMWithReactQuery() + if (!response?.data?.length) continue + this.fmTrackList.shift() + this.fmTrackList.push(...response?.data?.map(r => r.id)) + + this._playTrack() + break + } + } else { + this.fmTrackList.shift() + this._playTrack() + if (this.fmTrackList.length <= 1) { + const response = await fetchPersonalFMWithReactQuery() + this.fmTrackList.push(...response?.data?.map(r => r.id)) + } + } + } + /** * Play current track * @param {boolean} fade fade in @@ -255,6 +289,10 @@ export class Player { * Play previous track */ prevTrack() { + if (this.mode === Mode.FM) { + toast('Personal FM not support previous track') + return + } if (this._prevTrackIndex === undefined) { toast('No previous track') return @@ -266,8 +304,13 @@ export class Player { /** * Play next track */ - nextTrack() { + nextTrack(forceFM: boolean = false) { console.log(this) + if (forceFM || this.mode === Mode.FM) { + this.mode = Mode.FM + this._nextFMTrack() + return + } if (this._nextTrackIndex === undefined) { toast('No next track') this.pause() @@ -316,6 +359,69 @@ export class Player { this._playTrack() } + /** + * Play personal fm + */ + async playFM() { + this.mode = Mode.FM + if ( + this.fmTrackList.length > 0 && + this._fmTrack?.id === this.fmTrackList[0] + ) { + this._track = this._fmTrack + this._playAudio() + } else { + this._playTrack() + } + } + + /** + * Init personal fm + * should only be called in components/FMCard + */ + async initFM() { + if (this._fmInited) return + const response = await fetchPersonalFMWithReactQuery() + this.fmTrackList.push(...response?.data?.map(r => r.id)) + + const trackId = this.fmTrackList[0] + const track = await this._fetchTrack(trackId) + if (track) this._fmTrack = track + this._fmInited = true + } + + /** + * Trash current PersonalFMTrack + */ + async fmTrash() { + let trashId = this.fmTrackList.shift() ?? 0 + if (trashId === 0) return + + if (this.mode === Mode.FM) { + await this._nextFMTrack() + } else { + for (let i = 0; i < 5 && this.fmTrackList.length <= 1; i++) { + const response = await fetchPersonalFMWithReactQuery() + this.fmTrackList.push(...response?.data?.map(r => r.id)) + } + + for (let i = 0; i < 5; i++) { + let track = await this._fetchTrack(this.fmTrackList.at(0) ?? 0) + if (track) { + this._fmTrack = track + break + } else { + this.fmTrackList.shift() + if (this.fmTrackList.length <= 1) { + const response = await fetchPersonalFMWithReactQuery() + this.fmTrackList.push(...response?.data?.map(r => r.id)) + } + } + } + } + fmTrash(trashId) + } + /** * Play track in trackList by id */