mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-17 05:38:04 +00:00
653 lines
19 KiB
JavaScript
653 lines
19 KiB
JavaScript
import { getTrackDetail, scrobble, getMP3 } from '@/api/track';
|
||
import shuffle from 'lodash/shuffle';
|
||
import { Howler, Howl } from 'howler';
|
||
import { cacheTrackSource, getTrackSource } from '@/utils/db';
|
||
import { getAlbum } from '@/api/album';
|
||
import { getPlaylistDetail } from '@/api/playlist';
|
||
import { getArtist } from '@/api/artist';
|
||
import { personalFM, fmTrash } from '@/api/others';
|
||
import store from '@/store';
|
||
import { isAccountLoggedIn } from '@/utils/auth';
|
||
import { trackUpdateNowPlaying, trackScrobble } from '@/api/lastfm';
|
||
|
||
const electron =
|
||
process.env.IS_ELECTRON === true ? window.require('electron') : null;
|
||
const ipcRenderer =
|
||
process.env.IS_ELECTRON === true ? electron.ipcRenderer : null;
|
||
|
||
export default class {
|
||
constructor() {
|
||
// 播放器状态
|
||
this._playing = false; // 是否正在播放中
|
||
this._progress = 0; // 当前播放歌曲的进度
|
||
this._enabled = false; // 是否启用Player
|
||
this._repeatMode = 'off'; // off | on | one
|
||
this._shuffle = false; // true | false
|
||
this._volume = 1; // 0 to 1
|
||
this._volumeBeforeMuted = 1; // 用于保存静音前的音量
|
||
|
||
// 播放信息
|
||
this._list = []; // 播放列表
|
||
this._current = 0; // 当前播放歌曲在播放列表里的index
|
||
this._shuffledList = []; // 被随机打乱的播放列表,随机播放模式下会使用此播放列表
|
||
this._shuffledCurrent = 0; // 当前播放歌曲在随机列表里面的index
|
||
this._playlistSource = { type: 'album', id: 123 }; // 当前播放列表的信息
|
||
this._currentTrack = { id: 86827685 }; // 当前播放歌曲的详细信息
|
||
this._playNextList = []; // 当这个list不为空时,会优先播放这个list的歌
|
||
this._isPersonalFM = false; // 是否是私人FM模式
|
||
this._personalFMTrack = { id: 0 }; // 私人FM当前歌曲
|
||
this._personalFMNextTrack = { id: 0 }; // 私人FM下一首歌曲信息(为了快速加载下一首)
|
||
|
||
/**
|
||
* The blob records for cleanup.
|
||
*
|
||
* @private
|
||
* @type {string[]}
|
||
*/
|
||
this.createdBlobRecords = [];
|
||
|
||
// howler (https://github.com/goldfire/howler.js)
|
||
this._howler = null;
|
||
Object.defineProperty(this, '_howler', {
|
||
enumerable: false,
|
||
});
|
||
|
||
// init
|
||
this._init();
|
||
|
||
window.yesplaymusic = {};
|
||
window.yesplaymusic.player = this;
|
||
}
|
||
|
||
get repeatMode() {
|
||
return this._repeatMode;
|
||
}
|
||
set repeatMode(mode) {
|
||
if (this._isPersonalFM) return;
|
||
if (!['off', 'on', 'one'].includes(mode)) {
|
||
console.warn("repeatMode: invalid args, must be 'on' | 'off' | 'one'");
|
||
return;
|
||
}
|
||
this._repeatMode = mode;
|
||
}
|
||
get shuffle() {
|
||
return this._shuffle;
|
||
}
|
||
set shuffle(shuffle) {
|
||
if (this._isPersonalFM) return;
|
||
if (shuffle !== true && shuffle !== false) {
|
||
console.warn('shuffle: invalid args, must be Boolean');
|
||
return;
|
||
}
|
||
this._shuffle = shuffle;
|
||
if (shuffle) {
|
||
this._shuffleTheList();
|
||
}
|
||
}
|
||
get volume() {
|
||
return this._volume;
|
||
}
|
||
set volume(volume) {
|
||
this._volume = volume;
|
||
Howler.volume(volume);
|
||
}
|
||
get list() {
|
||
return this.shuffle ? this._shuffledList : this._list;
|
||
}
|
||
set list(list) {
|
||
this._list = list;
|
||
}
|
||
get current() {
|
||
return this.shuffle ? this._shuffledCurrent : this._current;
|
||
}
|
||
set current(current) {
|
||
if (this.shuffle) {
|
||
this._shuffledCurrent = current;
|
||
} else {
|
||
this._current = current;
|
||
}
|
||
}
|
||
get enabled() {
|
||
return this._enabled;
|
||
}
|
||
get playing() {
|
||
return this._playing;
|
||
}
|
||
get currentTrack() {
|
||
return this._currentTrack;
|
||
}
|
||
get playlistSource() {
|
||
return this._playlistSource;
|
||
}
|
||
get playNextList() {
|
||
return this._playNextList;
|
||
}
|
||
get isPersonalFM() {
|
||
return this._isPersonalFM;
|
||
}
|
||
get personalFMTrack() {
|
||
return this._personalFMTrack;
|
||
}
|
||
get currentTrackDuration() {
|
||
const trackDuration = this._currentTrack.dt || 1000;
|
||
let duration = ~~(trackDuration / 1000);
|
||
return duration > 1 ? duration - 1 : duration;
|
||
}
|
||
get progress() {
|
||
return this._progress;
|
||
}
|
||
set progress(value) {
|
||
if (this._howler) {
|
||
this._howler.seek(value);
|
||
}
|
||
}
|
||
get isCurrentTrackLiked() {
|
||
return store.state.liked.songs.includes(this.currentTrack.id);
|
||
}
|
||
|
||
_init() {
|
||
this._loadSelfFromLocalStorage();
|
||
Howler.autoUnlock = false;
|
||
Howler.usingWebAudio = true;
|
||
Howler.volume(this.volume);
|
||
|
||
if (this._enabled) {
|
||
// 恢复当前播放歌曲
|
||
this._replaceCurrentTrack(this._currentTrack.id, false).then(() => {
|
||
this._howler?.seek(localStorage.getItem('playerCurrentTrackTime') ?? 0);
|
||
}); // update audio source and init howler
|
||
this._initMediaSession();
|
||
}
|
||
|
||
this._setIntervals();
|
||
|
||
// 初始化私人FM
|
||
if (this._personalFMTrack.id === 0 || this._personalFMNextTrack.id === 0) {
|
||
personalFM().then(result => {
|
||
this._personalFMTrack = result.data[0];
|
||
this._personalFMNextTrack = result.data[1];
|
||
return this._personalFMTrack;
|
||
});
|
||
}
|
||
}
|
||
_setIntervals() {
|
||
// 同步播放进度
|
||
// TODO: 如果 _progress 在别的地方被改变了,这个定时器会覆盖之前改变的值,是bug
|
||
setInterval(() => {
|
||
if (this._howler === null) return;
|
||
this._progress = this._howler.seek();
|
||
localStorage.setItem('playerCurrentTrackTime', this._progress);
|
||
}, 1000);
|
||
}
|
||
_getNextTrack() {
|
||
if (this._playNextList.length > 0) {
|
||
let trackID = this._playNextList.shift();
|
||
return [trackID, this.current];
|
||
}
|
||
|
||
// 当歌曲是列表最后一首 && 循环模式开启
|
||
if (this.list.length === this.current + 1 && this.repeatMode === 'on') {
|
||
return [this.list[0], 0];
|
||
}
|
||
|
||
// 返回 [trackID, index]
|
||
return [this.list[this.current + 1], this.current + 1];
|
||
}
|
||
_getPrevTrack() {
|
||
// 当歌曲是列表第一首 && 循环模式开启
|
||
if (this.current === 0 && this.repeatMode === 'on') {
|
||
return [this.list[this.list.length - 1], this.list.length - 1];
|
||
}
|
||
|
||
// 返回 [trackID, index]
|
||
return [this.list[this.current - 1], this.current - 1];
|
||
}
|
||
async _shuffleTheList(firstTrackID = this._currentTrack.id) {
|
||
let list = this._list.filter(tid => tid !== firstTrackID);
|
||
if (firstTrackID === 'first') list = this._list;
|
||
this._shuffledList = shuffle(list);
|
||
if (firstTrackID !== 'first') this._shuffledList.unshift(firstTrackID);
|
||
}
|
||
async _scrobble(track, time, completed = false) {
|
||
console.debug(
|
||
`[debug][Player.js] scrobble track 👉 ${track.name} by ${track.ar[0].name} 👉 time:${time} completed: ${completed}`
|
||
);
|
||
const trackDuration = ~~(track.dt / 1000);
|
||
time = completed ? trackDuration : ~~time;
|
||
scrobble({
|
||
id: track.id,
|
||
sourceid: this.playlistSource.id,
|
||
time,
|
||
});
|
||
if (
|
||
store.state.lastfm.key !== undefined &&
|
||
(time >= trackDuration / 2 || time >= 240)
|
||
) {
|
||
const timestamp = ~~(new Date().getTime() / 1000) - time;
|
||
trackScrobble({
|
||
artist: track.ar[0].name,
|
||
track: track.name,
|
||
timestamp,
|
||
album: track.al.name,
|
||
trackNumber: track.no,
|
||
duration: trackDuration,
|
||
});
|
||
}
|
||
}
|
||
_playAudioSource(source, autoplay = true) {
|
||
Howler.unload();
|
||
this._howler = new Howl({
|
||
src: [source],
|
||
html5: true,
|
||
format: ['mp3', 'flac'],
|
||
});
|
||
if (autoplay) {
|
||
this.play();
|
||
if (this._currentTrack.name) {
|
||
document.title = `${this._currentTrack.name} · ${this._currentTrack.ar[0].name} - YesPlayMusic`;
|
||
}
|
||
}
|
||
this.setOutputDevice();
|
||
this._howler.once('end', () => {
|
||
this._nextTrackCallback();
|
||
});
|
||
}
|
||
_getAudioSourceFromCache(id) {
|
||
return getTrackSource(id).then(t => {
|
||
if (!t) return null;
|
||
|
||
// Create a new object URL.
|
||
const source = URL.createObjectURL(new Blob([t.source]));
|
||
|
||
// Clean up the previous object URLs since we've created a new one.
|
||
// Revoke object URLs can release the memory taken by a Blob,
|
||
// which occupied a large proportion of memory.
|
||
for (const url in this.createdBlobRecords) {
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
// Then, we replace the createBlobRecords with new one with
|
||
// our newly created object URL.
|
||
this.createdBlobRecords = [source];
|
||
|
||
return source;
|
||
});
|
||
}
|
||
_getAudioSourceFromNetease(track) {
|
||
if (isAccountLoggedIn()) {
|
||
return getMP3(track.id).then(result => {
|
||
if (!result.data[0]) return null;
|
||
if (!result.data[0].url) return null;
|
||
if (result.data[0].freeTrialInfo !== null) return null; // 跳过只能试听的歌曲
|
||
const source = result.data[0].url.replace(/^http:/, 'https:');
|
||
if (store.state.settings.automaticallyCacheSongs) {
|
||
cacheTrackSource(track, source, result.data[0].br);
|
||
}
|
||
return source;
|
||
});
|
||
} else {
|
||
return new Promise(resolve => {
|
||
resolve(`https://music.163.com/song/media/outer/url?id=${track.id}`);
|
||
});
|
||
}
|
||
}
|
||
async _getAudioSourceFromUnblockMusic(track) {
|
||
console.debug(`[debug][Player.js] _getAudioSourceFromUnblockMusic`);
|
||
if (
|
||
process.env.IS_ELECTRON !== true ||
|
||
store.state.settings.enableUnblockNeteaseMusic === false
|
||
) {
|
||
return null;
|
||
}
|
||
const source = await ipcRenderer.invoke('unblock-music', track);
|
||
if (store.state.settings.automaticallyCacheSongs && source?.url) {
|
||
// TODO: 将unblockMusic字样换成真正的来源(比如酷我咪咕等)
|
||
cacheTrackSource(track, source.url, 128000, 'unblockMusic');
|
||
}
|
||
return source?.url;
|
||
}
|
||
_getAudioSource(track) {
|
||
return this._getAudioSourceFromCache(String(track.id))
|
||
.then(source => {
|
||
return source ?? this._getAudioSourceFromNetease(track);
|
||
})
|
||
.then(source => {
|
||
return source ?? this._getAudioSourceFromUnblockMusic(track);
|
||
});
|
||
}
|
||
_replaceCurrentTrack(
|
||
id,
|
||
autoplay = true,
|
||
ifUnplayableThen = 'playNextTrack'
|
||
) {
|
||
if (autoplay && this._currentTrack.name) {
|
||
this._scrobble(this.currentTrack, this._howler?.seek());
|
||
}
|
||
return getTrackDetail(id).then(data => {
|
||
let track = data.songs[0];
|
||
this._currentTrack = track;
|
||
this._updateMediaSessionMetaData(track);
|
||
return this._getAudioSource(track).then(source => {
|
||
if (source) {
|
||
this._playAudioSource(source, autoplay);
|
||
this._cacheNextTrack();
|
||
return source;
|
||
} else {
|
||
store.dispatch('showToast', `无法播放 ${track.name}`);
|
||
ifUnplayableThen === 'playNextTrack'
|
||
? this.playNextTrack()
|
||
: this.playPrevTrack();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
_cacheNextTrack() {
|
||
let nextTrackID = this._isPersonalFM
|
||
? this._personalFMNextTrack.id
|
||
: this._getNextTrack()[0];
|
||
if (!nextTrackID) return;
|
||
if (this._personalFMTrack.id == nextTrackID) return;
|
||
getTrackDetail(nextTrackID).then(data => {
|
||
let track = data.songs[0];
|
||
this._getAudioSource(track);
|
||
});
|
||
}
|
||
_loadSelfFromLocalStorage() {
|
||
const player = JSON.parse(localStorage.getItem('player'));
|
||
if (!player) return;
|
||
for (const [key, value] of Object.entries(player)) {
|
||
this[key] = value;
|
||
}
|
||
}
|
||
_initMediaSession() {
|
||
if ('mediaSession' in navigator) {
|
||
navigator.mediaSession.setActionHandler('play', () => {
|
||
this.play();
|
||
});
|
||
navigator.mediaSession.setActionHandler('pause', () => {
|
||
this.pause();
|
||
});
|
||
navigator.mediaSession.setActionHandler('previoustrack', () => {
|
||
this.playPrevTrack();
|
||
});
|
||
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
||
this.playNextTrack();
|
||
});
|
||
navigator.mediaSession.setActionHandler('stop', () => {
|
||
this.pause();
|
||
});
|
||
navigator.mediaSession.setActionHandler('seekto', event => {
|
||
this.seek(event.seekTime);
|
||
this._updateMediaSessionPositionState();
|
||
});
|
||
navigator.mediaSession.setActionHandler('seekbackward', event => {
|
||
this.seek(this.seek() - (event.seekOffset || 10));
|
||
this._updateMediaSessionPositionState();
|
||
});
|
||
navigator.mediaSession.setActionHandler('seekforward', event => {
|
||
this.seek(this.seek() + (event.seekOffset || 10));
|
||
this._updateMediaSessionPositionState();
|
||
});
|
||
}
|
||
}
|
||
_updateMediaSessionMetaData(track) {
|
||
if ('mediaSession' in navigator === false) {
|
||
return;
|
||
}
|
||
let artists = track.ar.map(a => a.name);
|
||
navigator.mediaSession.metadata = new window.MediaMetadata({
|
||
title: track.name,
|
||
artist: artists.join(','),
|
||
album: track.al.name,
|
||
artwork: [
|
||
{
|
||
src: track.al.picUrl + '?param=512y512',
|
||
type: 'image/jpg',
|
||
sizes: '512x512',
|
||
},
|
||
],
|
||
});
|
||
}
|
||
_updateMediaSessionPositionState() {
|
||
if ('mediaSession' in navigator === false) {
|
||
return;
|
||
}
|
||
if ('setPositionState' in navigator.mediaSession) {
|
||
navigator.mediaSession.setPositionState({
|
||
duration: ~~(this.currentTrack.dt / 1000),
|
||
playbackRate: 1.0,
|
||
position: this.seek(),
|
||
});
|
||
}
|
||
}
|
||
_nextTrackCallback() {
|
||
this._scrobble(this._currentTrack, 0, true);
|
||
if (!this.isPersonalFM && this.repeatMode === 'one') {
|
||
this._replaceCurrentTrack(this._currentTrack.id);
|
||
} else {
|
||
this.playNextTrack();
|
||
}
|
||
}
|
||
_loadPersonalFMNextTrack() {
|
||
return personalFM().then(result => {
|
||
this._personalFMNextTrack = result.data[0];
|
||
this._cacheNextTrack(); // cache next track
|
||
return this._personalFMNextTrack;
|
||
});
|
||
}
|
||
_playDiscordPresence(track, seekTime = 0) {
|
||
if (
|
||
process.env.IS_ELECTRON !== true ||
|
||
store.state.settings.enableDiscordRichPresence === false
|
||
) {
|
||
return null;
|
||
}
|
||
let copyTrack = { ...track };
|
||
copyTrack.dt -= seekTime * 1000;
|
||
ipcRenderer.send('playDiscordPresence', copyTrack);
|
||
}
|
||
_pauseDiscordPresence(track) {
|
||
if (
|
||
process.env.IS_ELECTRON !== true ||
|
||
store.state.settings.enableDiscordRichPresence === false
|
||
) {
|
||
return null;
|
||
}
|
||
ipcRenderer.send('pauseDiscordPresence', track);
|
||
}
|
||
|
||
currentTrackID() {
|
||
const { list, current } = this._getListAndCurrent();
|
||
return list[current];
|
||
}
|
||
appendTrack(trackID) {
|
||
this.list.append(trackID);
|
||
}
|
||
playNextTrack(isFM = false) {
|
||
if (this._isPersonalFM || isFM === true) {
|
||
this._isPersonalFM = true;
|
||
this._personalFMTrack = this._personalFMNextTrack;
|
||
this._replaceCurrentTrack(this._personalFMTrack.id);
|
||
this._loadPersonalFMNextTrack();
|
||
return true;
|
||
}
|
||
// TODO: 切换歌曲时增加加载中的状态
|
||
const [trackID, index] = this._getNextTrack();
|
||
if (trackID === undefined) {
|
||
this._howler?.stop();
|
||
this._playing = false;
|
||
return false;
|
||
}
|
||
this.current = index;
|
||
this._replaceCurrentTrack(trackID);
|
||
return true;
|
||
}
|
||
playPrevTrack() {
|
||
const [trackID, index] = this._getPrevTrack();
|
||
if (trackID === undefined) return false;
|
||
this.current = index;
|
||
this._replaceCurrentTrack(trackID, true, 'playPrevTrack');
|
||
return true;
|
||
}
|
||
saveSelfToLocalStorage() {
|
||
let player = {};
|
||
for (let [key, value] of Object.entries(this)) {
|
||
if (key === '_playing') continue;
|
||
player[key] = value;
|
||
}
|
||
|
||
localStorage.setItem('player', JSON.stringify(player));
|
||
}
|
||
|
||
pause() {
|
||
this._howler?.pause();
|
||
this._playing = false;
|
||
document.title = 'YesPlayMusic';
|
||
this._pauseDiscordPresence(this._currentTrack);
|
||
}
|
||
play() {
|
||
if (this._howler?.playing()) return;
|
||
this._howler?.play();
|
||
this._playing = true;
|
||
if (this._currentTrack.name) {
|
||
document.title = `${this._currentTrack.name} · ${this._currentTrack.ar[0].name} - YesPlayMusic`;
|
||
}
|
||
this._playDiscordPresence(this._currentTrack, this.seek());
|
||
if (store.state.lastfm.key !== undefined) {
|
||
trackUpdateNowPlaying({
|
||
artist: this.currentTrack.ar[0].name,
|
||
track: this.currentTrack.name,
|
||
album: this.currentTrack.al.name,
|
||
trackNumber: this.currentTrack.no,
|
||
duration: ~~(this.currentTrack.dt / 1000),
|
||
});
|
||
}
|
||
}
|
||
playOrPause() {
|
||
if (this._howler?.playing()) {
|
||
this.pause();
|
||
} else {
|
||
this.play();
|
||
}
|
||
}
|
||
seek(time = null) {
|
||
if (time !== null) {
|
||
this._howler?.seek(time);
|
||
if (this._playing)
|
||
this._playDiscordPresence(this._currentTrack, this.seek());
|
||
}
|
||
return this._howler === null ? 0 : this._howler.seek();
|
||
}
|
||
mute() {
|
||
if (this.volume === 0) {
|
||
this.volume = this._volumeBeforeMuted;
|
||
} else {
|
||
this._volumeBeforeMuted = this.volume;
|
||
this.volume = 0;
|
||
}
|
||
}
|
||
setOutputDevice() {
|
||
if (this._howler?._sounds.length <= 0 || !this._howler?._sounds[0]._node) {
|
||
return;
|
||
}
|
||
this._howler?._sounds[0]._node.setSinkId(store.state.settings.outputDevice);
|
||
}
|
||
|
||
replacePlaylist(
|
||
trackIDs,
|
||
playlistSourceID,
|
||
playlistSourceType,
|
||
autoPlayTrackID = 'first'
|
||
) {
|
||
this._isPersonalFM = false;
|
||
if (!this._enabled) this._enabled = true;
|
||
this.list = trackIDs;
|
||
this.current = 0;
|
||
this._playlistSource = {
|
||
type: playlistSourceType,
|
||
id: playlistSourceID,
|
||
};
|
||
if (this.shuffle) this._shuffleTheList(autoPlayTrackID);
|
||
if (autoPlayTrackID === 'first') {
|
||
this._replaceCurrentTrack(this.list[0]);
|
||
} else {
|
||
this.current = trackIDs.indexOf(autoPlayTrackID);
|
||
this._replaceCurrentTrack(autoPlayTrackID);
|
||
}
|
||
}
|
||
playAlbumByID(id, trackID = 'first') {
|
||
getAlbum(id).then(data => {
|
||
let trackIDs = data.songs.map(t => t.id);
|
||
this.replacePlaylist(trackIDs, id, 'album', trackID);
|
||
});
|
||
}
|
||
playPlaylistByID(id, trackID = 'first', noCache = false) {
|
||
console.debug(
|
||
`[debug][Player.js] playPlaylistByID 👉 id:${id} trackID:${trackID} noCache:${noCache}`
|
||
);
|
||
getPlaylistDetail(id, noCache).then(data => {
|
||
let trackIDs = data.playlist.trackIds.map(t => t.id);
|
||
this.replacePlaylist(trackIDs, id, 'playlist', trackID);
|
||
});
|
||
}
|
||
playArtistByID(id, trackID = 'first') {
|
||
getArtist(id).then(data => {
|
||
let trackIDs = data.hotSongs.map(t => t.id);
|
||
this.replacePlaylist(trackIDs, id, 'artist', trackID);
|
||
});
|
||
}
|
||
playTrackOnListByID(id, listName = 'default') {
|
||
if (listName === 'default') {
|
||
this._current = this._list.findIndex(t => t === id);
|
||
}
|
||
this._replaceCurrentTrack(id);
|
||
}
|
||
addTrackToPlayNext(trackID, playNow = false) {
|
||
this._playNextList.push(trackID);
|
||
if (playNow) 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()
|
||
);
|
||
} else {
|
||
this.playOrPause();
|
||
}
|
||
}
|
||
moveToFMTrash() {
|
||
this._isPersonalFM = true;
|
||
this.playNextTrack();
|
||
fmTrash(this._personalFMTrack.id);
|
||
}
|
||
|
||
sendSelfToIpcMain() {
|
||
if (process.env.IS_ELECTRON !== true) return false;
|
||
ipcRenderer.send('player', {
|
||
playing: this.playing,
|
||
likedCurrentTrack: store.state.liked.songs.includes(this.currentTrack.id),
|
||
});
|
||
}
|
||
|
||
switchRepeatMode() {
|
||
if (this._repeatMode === 'on') {
|
||
this.repeatMode = 'one';
|
||
} else if (this._repeatMode === 'one') {
|
||
this.repeatMode = 'off';
|
||
} else {
|
||
this.repeatMode = 'on';
|
||
}
|
||
}
|
||
switchShuffle() {
|
||
this.shuffle = !this.shuffle;
|
||
}
|
||
|
||
clearPlayNextList() {
|
||
this._playNextList = [];
|
||
}
|
||
removeTrackFromQueue(index) {
|
||
this._playNextList.splice(index, 1);
|
||
}
|
||
}
|