|
|
|
@ -14,6 +14,15 @@ import { decode as base642Buffer } from '@/utils/base64';
|
|
|
|
|
|
|
|
|
|
const PLAY_PAUSE_FADE_DURATION = 200;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @readonly
|
|
|
|
|
* @enum {string}
|
|
|
|
|
*/
|
|
|
|
|
const UNPLAYABLE_CONDITION = {
|
|
|
|
|
PLAY_NEXT_TRACK: 'playNextTrack',
|
|
|
|
|
PLAY_PREV_TRACK: 'playPrevTrack',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const electron =
|
|
|
|
|
process.env.IS_ELECTRON === true ? window.require('electron') : null;
|
|
|
|
|
const ipcRenderer =
|
|
|
|
@ -164,6 +173,9 @@ export default class {
|
|
|
|
|
get currentTrack() {
|
|
|
|
|
return this._currentTrack;
|
|
|
|
|
}
|
|
|
|
|
get currentTrackID() {
|
|
|
|
|
return this._currentTrack?.id ?? 0;
|
|
|
|
|
}
|
|
|
|
|
get playlistSource() {
|
|
|
|
|
return this._playlistSource;
|
|
|
|
|
}
|
|
|
|
@ -199,7 +211,7 @@ export default class {
|
|
|
|
|
|
|
|
|
|
if (this._enabled) {
|
|
|
|
|
// 恢复当前播放歌曲
|
|
|
|
|
this._replaceCurrentTrack(this._currentTrack.id, false).then(() => {
|
|
|
|
|
this._replaceCurrentTrack(this.currentTrackID, false).then(() => {
|
|
|
|
|
this._howler?.seek(localStorage.getItem('playerCurrentTrackTime') ?? 0);
|
|
|
|
|
}); // update audio source and init howler
|
|
|
|
|
this._initMediaSession();
|
|
|
|
@ -278,7 +290,7 @@ export default class {
|
|
|
|
|
// 返回 [trackID, index]
|
|
|
|
|
return [this.list[next], next];
|
|
|
|
|
}
|
|
|
|
|
async _shuffleTheList(firstTrackID = this._currentTrack.id) {
|
|
|
|
|
async _shuffleTheList(firstTrackID = this.currentTrackID) {
|
|
|
|
|
let list = this._list.filter(tid => tid !== firstTrackID);
|
|
|
|
|
if (firstTrackID === 'first') list = this._list;
|
|
|
|
|
this._shuffledList = shuffle(list);
|
|
|
|
@ -321,6 +333,25 @@ export default class {
|
|
|
|
|
this._nextTrackCallback();
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
this._howler.on('loaderror', (_, errCode) => {
|
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code
|
|
|
|
|
// code 3: MEDIA_ERR_DECODE
|
|
|
|
|
if (errCode === 3) {
|
|
|
|
|
this._playNextTrack(this._isPersonalFM);
|
|
|
|
|
} else {
|
|
|
|
|
const t = this.progress;
|
|
|
|
|
this._replaceCurrentTrackAudio(this.currentTrack, false, false).then(
|
|
|
|
|
replaced => {
|
|
|
|
|
// 如果 replaced 为 false,代表当前的 track 已经不是这里想要替换的track
|
|
|
|
|
// 此时则不修改当前的歌曲进度
|
|
|
|
|
if (replaced) {
|
|
|
|
|
this._howler?.seek(t);
|
|
|
|
|
this.play();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if (autoplay) {
|
|
|
|
|
this.play();
|
|
|
|
|
if (this._currentTrack.name) {
|
|
|
|
@ -452,34 +483,62 @@ export default class {
|
|
|
|
|
_replaceCurrentTrack(
|
|
|
|
|
id,
|
|
|
|
|
autoplay = true,
|
|
|
|
|
ifUnplayableThen = 'playNextTrack'
|
|
|
|
|
ifUnplayableThen = UNPLAYABLE_CONDITION.PLAY_NEXT_TRACK
|
|
|
|
|
) {
|
|
|
|
|
if (autoplay && this._currentTrack.name) {
|
|
|
|
|
this._scrobble(this.currentTrack, this._howler?.seek());
|
|
|
|
|
}
|
|
|
|
|
return getTrackDetail(id).then(data => {
|
|
|
|
|
let track = data.songs[0];
|
|
|
|
|
const track = data.songs[0];
|
|
|
|
|
this._currentTrack = track;
|
|
|
|
|
this._updateMediaSessionMetaData(track);
|
|
|
|
|
return this._replaceCurrentTrackAudio(
|
|
|
|
|
track,
|
|
|
|
|
autoplay,
|
|
|
|
|
true,
|
|
|
|
|
ifUnplayableThen
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* @returns 是否成功加载音频,并使用加载完成的音频替换了howler实例
|
|
|
|
|
*/
|
|
|
|
|
_replaceCurrentTrackAudio(
|
|
|
|
|
track,
|
|
|
|
|
autoplay,
|
|
|
|
|
isCacheNextTrack,
|
|
|
|
|
ifUnplayableThen = UNPLAYABLE_CONDITION.PLAY_NEXT_TRACK
|
|
|
|
|
) {
|
|
|
|
|
return this._getAudioSource(track).then(source => {
|
|
|
|
|
if (source) {
|
|
|
|
|
let replaced = false;
|
|
|
|
|
if (track.id === this.currentTrackID) {
|
|
|
|
|
this._playAudioSource(source, autoplay);
|
|
|
|
|
replaced = true;
|
|
|
|
|
}
|
|
|
|
|
if (isCacheNextTrack) {
|
|
|
|
|
this._cacheNextTrack();
|
|
|
|
|
return source;
|
|
|
|
|
} else {
|
|
|
|
|
store.dispatch('showToast', `无法播放 ${track.name}`);
|
|
|
|
|
if (ifUnplayableThen === 'playNextTrack') {
|
|
|
|
|
if (this.isPersonalFM) {
|
|
|
|
|
this.playNextFMTrack();
|
|
|
|
|
} else {
|
|
|
|
|
this.playNextTrack();
|
|
|
|
|
}
|
|
|
|
|
return replaced;
|
|
|
|
|
} else {
|
|
|
|
|
store.dispatch('showToast', `无法播放 ${track.name}`);
|
|
|
|
|
switch (ifUnplayableThen) {
|
|
|
|
|
case UNPLAYABLE_CONDITION.PLAY_NEXT_TRACK:
|
|
|
|
|
this._playNextTrack(this.isPersonalFM);
|
|
|
|
|
break;
|
|
|
|
|
case UNPLAYABLE_CONDITION.PLAY_PREV_TRACK:
|
|
|
|
|
this.playPrevTrack();
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
store.dispatch(
|
|
|
|
|
'showToast',
|
|
|
|
|
`undefined Unplayable condition: ${ifUnplayableThen}`
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
_cacheNextTrack() {
|
|
|
|
|
let nextTrackID = this._isPersonalFM
|
|
|
|
@ -511,11 +570,7 @@ export default class {
|
|
|
|
|
this.playPrevTrack();
|
|
|
|
|
});
|
|
|
|
|
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
|
|
|
|
if (this.isPersonalFM) {
|
|
|
|
|
this.playNextFMTrack();
|
|
|
|
|
} else {
|
|
|
|
|
this.playNextTrack();
|
|
|
|
|
}
|
|
|
|
|
this._playNextTrack(this.isPersonalFM);
|
|
|
|
|
});
|
|
|
|
|
navigator.mediaSession.setActionHandler('stop', () => {
|
|
|
|
|
this.pause();
|
|
|
|
@ -579,11 +634,9 @@ export default class {
|
|
|
|
|
_nextTrackCallback() {
|
|
|
|
|
this._scrobble(this._currentTrack, 0, true);
|
|
|
|
|
if (!this.isPersonalFM && this.repeatMode === 'one') {
|
|
|
|
|
this._replaceCurrentTrack(this._currentTrack.id);
|
|
|
|
|
} else if (this.isPersonalFM) {
|
|
|
|
|
this.playNextFMTrack();
|
|
|
|
|
this._replaceCurrentTrack(this.currentTrackID);
|
|
|
|
|
} else {
|
|
|
|
|
this.playNextTrack();
|
|
|
|
|
this._playNextTrack(this.isPersonalFM);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_loadPersonalFMNextTrack() {
|
|
|
|
@ -628,11 +681,14 @@ export default class {
|
|
|
|
|
}
|
|
|
|
|
ipcRenderer?.send('pauseDiscordPresence', track);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentTrackID() {
|
|
|
|
|
const { list, current } = this._getListAndCurrent();
|
|
|
|
|
return list[current];
|
|
|
|
|
_playNextTrack(isPersonal) {
|
|
|
|
|
if (isPersonal) {
|
|
|
|
|
this.playNextFMTrack();
|
|
|
|
|
} else {
|
|
|
|
|
this.playNextTrack();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
appendTrack(trackID) {
|
|
|
|
|
this.list.append(trackID);
|
|
|
|
|
}
|
|
|
|
@ -697,7 +753,11 @@ export default class {
|
|
|
|
|
const [trackID, index] = this._getPrevTrack();
|
|
|
|
|
if (trackID === undefined) return false;
|
|
|
|
|
this.current = index;
|
|
|
|
|
this._replaceCurrentTrack(trackID, true, 'playPrevTrack');
|
|
|
|
|
this._replaceCurrentTrack(
|
|
|
|
|
trackID,
|
|
|
|
|
true,
|
|
|
|
|
UNPLAYABLE_CONDITION.PLAY_PREV_TRACK
|
|
|
|
|
);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
saveSelfToLocalStorage() {
|
|
|
|
@ -838,20 +898,14 @@ export default class {
|
|
|
|
|
addTrackToPlayNext(trackID, playNow = false) {
|
|
|
|
|
this._playNextList.push(trackID);
|
|
|
|
|
if (playNow) {
|
|
|
|
|
if (this.isPersonalFM) {
|
|
|
|
|
this.playNextFMTrack();
|
|
|
|
|
} else {
|
|
|
|
|
this.playNextTrack();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
playPersonalFM() {
|
|
|
|
|
this._isPersonalFM = true;
|
|
|
|
|
if (!this._enabled) this._enabled = true;
|
|
|
|
|
if (this._currentTrack.id !== this._personalFMTrack.id) {
|
|
|
|
|
this._replaceCurrentTrack(this._personalFMTrack.id).then(() =>
|
|
|
|
|
this.playOrPause()
|
|
|
|
|
);
|
|
|
|
|
if (this.currentTrackID !== this._personalFMTrack.id) {
|
|
|
|
|
this._replaceCurrentTrack(this._personalFMTrack.id, true);
|
|
|
|
|
} else {
|
|
|
|
|
this.playOrPause();
|
|
|
|
|
}
|
|
|
|
|