import { getTrackDetail, scrobble, getMP3 } from "@/api/track"; import { shuffle } from "lodash"; import { Howler, Howl } from "howler"; import localforage from "localforage"; import { cacheTrack } from "@/utils/db"; import { getAlbum } from "@/api/album"; import { getPlaylistDetail } from "@/api/playlist"; import { getArtist } from "@/api/artist"; import store from "@/store"; 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._enabled = false; 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; // current track index this._shuffledList = []; this._shuffledCurrent = 0; this._playlistSource = { type: "album", id: 123 }; this._currentTrack = { id: 86827685 }; this._playNextList = []; // 当这个list不为空时,会优先播放这个list的歌 this._playing = false; this._howler = null; Object.defineProperty(this, "_howler", { enumerable: false, }); this._init(); } get repeatMode() { return this._repeatMode; } set repeatMode(mode) { 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 (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; } _init() { Howler.autoUnlock = false; this._loadSelfFromLocalStorage(); this._replaceCurrentTrack(this._currentTrack.id, false); // update audio source and init howler this._initMediaSession(); } _getNextTrack() { // 返回 [trackID, index] 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]; } 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]; } 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(complete = false) { let time = this._howler.seek(); if (complete) { time = ~~(this._currentTrack.dt / 100); } scrobble({ id: this._currentTrack.id, sourceid: this.playlistSource.id, time, }); } _playAudioSource(source, autoplay = true) { Howler.unload(); this._howler = new Howl({ src: [source], html5: true, format: ["mp3", "flac"], }); if (autoplay) this.play(); this._howler.once("end", () => { this._nextTrackCallback(); }); } _getAudioSourceFromCache(id) { let tracks = localforage.createInstance({ name: "tracks" }); return tracks.getItem(id).then((t) => { if (t === null) return null; const source = URL.createObjectURL(new Blob([t.mp3])); return source; }); } _getAudioSourceFromNetease(track) { return getMP3(track.id).then((data) => { if (!data.data[0]) return null; if (data.data[0].freeTrialInfo !== null) return null; // 跳过只能试听的歌曲 const source = data.data[0].url.replace(/^http:/, "https:"); if (store.state.settings.automaticallyCacheSongs) { cacheTrack(track.id, source); } return source; }); } _getAudioSourceFromUnblockMusic(track) { if (process.env.IS_ELECTRON !== true) return null; const source = ipcRenderer.sendSync("unblock-music", track); return source?.url; } _getAudioSource(track) { return this._getAudioSourceFromCache(String(track.id)) .then((source) => { if (!source) return null; return source; }) .then((source) => { if (source) return source; return this._getAudioSourceFromNetease(track); }) .then((source) => { if (source) return source; return this._getAudioSourceFromUnblockMusic(track); }); } _replaceCurrentTrack( id, autoplay = true, ifUnplayableThen = "playNextTrack" ) { return getTrackDetail(id).then((data) => { let track = data.songs[0]; this._currentTrack = track; this._updateMediaSessionMetaData(track); document.title = `${track.name} · ${track.ar[0].name} - YesPlayMusic`; this._getAudioSource(track).then((source) => { if (source) { this._playAudioSource(source, autoplay); return source; } else { ifUnplayableThen === "playNextTrack" ? this.playNextTrack() : this.playPrevTrack(); } }); }); } _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(); }); } } _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", }, ], }); } _nextTrackCallback() { this._scrobble(true); if (this.repeatMode === "one") { this._howler.play(); } else { this.playNextTrack(); } } currentTrackID() { const { list, current } = this._getListAndCurrent(); return list[current]; } appendTrack(trackID) { this.list.append(trackID); } playNextTrack() { // TODO: 切换歌曲时增加加载中的状态 const [trackID, index] = this._getNextTrack(); if (trackID === undefined) { this._howler.stop(); 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; } play() { this._howler.play(); this._playing = true; } seek(time = null) { if (time !== null) this._howler.seek(time); return this._howler.seek(); } mute() { if (this.volume === 0) { this.volume = this._volumeBeforeMuted; } else { this._volumeBeforeMuted = this.volume; this.volume = 0; } } replacePlaylist( trackIDs, playlistSourceID, playlistSourceType, autoPlayTrackID = "first" ) { 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) { 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); }); } addTrackToPlayNext(trackID, playNow = false) { this._playNextList.push(trackID); if (playNow) this.playNextTrack(); } sendSelfToIpcMain() { if (process.env.IS_ELECTRON !== true) return false; ipcRenderer.send("player", { playing: this.playing, likedCurrentTrack: store.state.liked.songs.includes(this.currentTrack.id), }); } }