YesPlayMusic/src/utils/Player.js
2021-12-30 18:16:49 +08:00

653 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}