refactor: player

This commit is contained in:
qier222 2021-01-05 22:17:47 +08:00
parent 0482e6a1ed
commit f6c36fbcac
22 changed files with 659 additions and 500 deletions

View file

@ -9,7 +9,7 @@
</main>
<transition name="slide-up">
<Player
v-if="this.$store.state.player.enable"
v-if="this.$store.state.player.enabled"
ref="player"
v-show="
['mv', 'loginUsername', 'login', 'loginAccount'].includes(

View file

@ -29,8 +29,6 @@
</template>
<script>
import { playAlbumByID, playPlaylistByID, playArtistByID } from "@/utils/play";
export default {
props: {
id: { type: Number, required: true },
@ -74,12 +72,13 @@ export default {
},
methods: {
play() {
const player = this.$store.state.player;
const playActions = {
album: playAlbumByID,
playlist: playPlaylistByID,
artist: playArtistByID,
album: player.playAlbumByID,
playlist: player.playPlaylistByID,
artist: player.playArtistByID,
};
playActions[this.type](this.id);
playActions[this.type].bind(player)(this.id);
},
goTo() {
this.$router.push({ name: this.type, params: { id: this.id } });

View file

@ -36,7 +36,6 @@
</span>
</div>
</div>
<!-- 账号登录才会显示 like 图标 -->
<div class="like-button">
<button-icon
@click.native="likeCurrentSong"
@ -60,9 +59,9 @@
<button-icon
class="play"
@click.native="play"
:title="$t(playing ? 'player.pause' : 'player.play')"
:title="$t(player.playing ? 'player.pause' : 'player.play')"
>
<svg-icon :iconClass="playing ? 'pause' : 'play'"
<svg-icon :iconClass="player.playing ? 'pause' : 'play'"
/></button-icon>
<button-icon @click.native="next" :title="$t('player.next')"
><svg-icon icon-class="next"
@ -77,15 +76,18 @@
/></button-icon>
<button-icon
:title="
player.repeat === 'one'
player.repeatMode === 'one'
? $t('player.repeatTrack')
: $t('player.repeat')
"
@click.native="repeat"
:class="{ active: player.repeat !== 'off' }"
:class="{ active: player.repeatMode !== 'off' }"
>
<svg-icon icon-class="repeat" v-show="player.repeat !== 'one'" />
<svg-icon icon-class="repeat-1" v-show="player.repeat === 'one'" />
<svg-icon icon-class="repeat" v-show="player.repeatMode !== 'one'" />
<svg-icon
icon-class="repeat-1"
v-show="player.repeatMode === 'one'"
/>
</button-icon>
<button-icon
@click.native="shuffle"
@ -94,7 +96,7 @@
><svg-icon icon-class="shuffle"
/></button-icon>
<div class="volume-control">
<button-icon :title="$t('player.mute')" @click.native="mute">
<button-icon :title="$t('player.mute')" @click.native="player.mute">
<svg-icon icon-class="volume" v-show="volume > 0.5" />
<svg-icon icon-class="volume-mute" v-show="volume === 0" />
<svg-icon
@ -121,13 +123,11 @@
</template>
<script>
import { updateMediaSessionMetaData } from "@/utils/mediaSession";
import { mapState, mapMutations, mapActions } from "vuex";
import { isAccountLoggedIn } from "@/utils/auth";
import { userLikedSongsIDs } from "@/api/user";
import { likeATrack } from "@/api/track";
import "@/assets/css/slider.css";
import { Howler } from "howler";
import ButtonIcon from "@/components/ButtonIcon.vue";
import VueSlider from "vue-slider-component";
@ -147,10 +147,7 @@ export default {
},
mounted() {
setInterval(() => {
// fix _id
if (this.howler && this.howler._sounds?.[0]?._id) {
this.progress = ~~this.howler.seek();
}
this.progress = ~~this.player.seek();
}, 1000);
if (isAccountLoggedIn()) {
userLikedSongsIDs(this.data.user.userId).then((data) => {
@ -159,7 +156,7 @@ export default {
}
},
computed: {
...mapState(["player", "howler", "settings", "liked", "data"]),
...mapState(["player", "settings", "liked", "data"]),
currentTrack() {
return this.player.currentTrack;
},
@ -168,92 +165,46 @@ export default {
return this.player.volume;
},
set(value) {
this.updatePlayerState({ key: "volume", value });
Howler.volume(value);
this.player.volume = value;
},
},
playing() {
if (this.howler) {
if (this.howler.state() === "loading") {
this.updatePlayerState({ key: "playing", value: true });
return true;
}
const status = this.howler.playing();
this.updatePlayerState({ key: "playing", value: status });
return status;
} else {
return false;
}
return this.player.playing;
},
progressMax() {
let max = ~~(this.currentTrack.dt / 1000);
let max = ~~(this.player.currentTrack.dt / 1000);
return max > 1 ? max - 1 : max;
},
},
methods: {
...mapMutations([
"turnOnShuffleMode",
"turnOffShuffleMode",
"updatePlayerState",
"updateRepeatStatus",
"updateLikedSongs",
]),
...mapActions([
"nextTrack",
"previousTrack",
"playTrackOnListByID",
"addNextTrackEvent",
"showToast",
]),
...mapMutations(["updateLikedSongs"]),
...mapActions(["showToast"]),
play() {
if (this.playing) {
this.howler.pause();
} else {
if (this.howler.state() === "unloaded") {
this.playTrackOnListByID(this.currentTrack.id);
}
this.howler.play();
if (this.howler._onend.length === 0) {
this.addNextTrackEvent();
updateMediaSessionMetaData(this.currentTrack);
}
}
this.player.playing ? this.player.pause() : this.player.play();
},
next() {
this.progress = 0;
this.nextTrack(true);
if (this.player.playNextTrack()) this.progress = 0;
},
previous() {
this.progress = 0;
this.previousTrack();
if (this.player.playPrevTrack()) this.progress = 0;
},
shuffle() {
if (this.player.shuffle === true) {
this.turnOffShuffleMode();
} else {
this.turnOnShuffleMode();
}
this.player.shuffle = !this.player.shuffle;
console.log(this.player);
},
repeat() {
if (this.player.repeat === "on") {
this.updateRepeatStatus("one");
} else if (this.player.repeat === "one") {
this.updateRepeatStatus("off");
console.log(this.player.repeatMode);
if (this.player.repeatMode === "on") {
this.player.repeatMode = "one";
} else if (this.player.repeatMode === "one") {
this.player.repeatMode = "off";
} else {
this.updateRepeatStatus("on");
}
},
mute() {
if (this.volume === 0) {
this.volume = this.oldVolume;
} else {
this.oldVolume = this.volume;
this.volume = 0;
this.player.repeatMode = "on";
}
},
setSeek() {
this.progress = this.$refs.progress.getValue();
this.howler.seek(this.$refs.progress.getValue());
this.player.seek(this.$refs.progress.getValue());
},
goToNextTracksPage() {
this.$route.name === "next"
@ -285,15 +236,20 @@ export default {
});
},
goToList() {
if (this.player.listInfo.id === this.data.likedSongPlaylistID)
if (this.player.playlistSource.id === this.data.likedSongPlaylistID)
this.$router.push({ path: "/library/liked-songs" });
else
this.$router.push({
path: "/" + this.player.listInfo.type + "/" + this.player.listInfo.id,
path:
"/" +
this.player.playlistSource.type +
"/" +
this.player.playlistSource.id,
});
},
goToAlbum() {
this.$router.push({ path: "/album/" + this.currentTrack.al.id });
if (this.player.currentTrack.al.id === 0) return;
this.$router.push({ path: "/album/" + this.player.currentTrack.al.id });
},
goToArtist(id) {
this.$router.push({ path: "/artist/" + id });

View file

@ -41,12 +41,6 @@
<script>
import { mapActions, mapMutations, mapState } from "vuex";
import { likeATrack } from "@/api/track";
import {
playPlaylistByID,
playAlbumByID,
playAList,
appendTrackToPlayerList,
} from "@/utils/play";
import { addOrRemoveTrackFromPlaylist } from "@/api/playlist";
import { isAccountLoggedIn } from "@/utils/auth";
@ -140,24 +134,32 @@ export default {
} else if (this.dbclickTrackFunc === "playTrackOnListByID") {
this.playTrackOnListByID(trackID);
} else if (this.dbclickTrackFunc === "playPlaylistByID") {
playPlaylistByID(this.id, trackID);
this.$store.state.player.playPlaylistByID(this.id, trackID);
}
},
playThisListDefault(trackID) {
if (this.type === "playlist") {
playPlaylistByID(this.id, trackID);
this.$store.state.player.playPlaylistByID(this.id, trackID);
} else if (this.type === "album") {
playAlbumByID(this.id, trackID);
this.$store.state.player.playAlbumByID(this.id, trackID);
} else if (this.type === "tracklist") {
let trackIDs = this.tracks.map((t) => t.id);
playAList(trackIDs, this.tracks[0].ar[0].id, "artist", trackID);
this.$store.state.player.replacePlaylist(
trackIDs,
this.id,
"artist",
trackID
);
}
},
play() {
appendTrackToPlayerList(this.clickTrack.id, true);
this.$store.state.player.addTrackToPlayNext(
this.rightClickedTrack.id,
true
);
},
playNext() {
appendTrackToPlayerList(this.clickTrack.id);
this.$store.state.player.addTrackToPlayNext(this.rightClickedTrack.id);
},
like() {
this.likeASong(this.rightClickedTrack.id);

View file

@ -62,9 +62,9 @@ export function createTouchBar(window) {
},
});
ipcMain.on("vuex-state", (e, { player, liked }) => {
playButton.label = player.playing === true ? "􀊆" : "􀊄";
likeButton.label = liked.songs.includes(player.currentTrack.id) ? "􀊵" : "􀊴";
ipcMain.on("player", (e, { playing, likedCurrentTrack }) => {
playButton.label = playing === true ? "􀊆" : "􀊄";
likeButton.label = likedCurrentTrack ? "􀊵" : "􀊴";
});
const touchBar = new TouchBar({

View file

@ -6,7 +6,6 @@ import store from "./store";
import i18n from "@/locale";
import "@/assets/icons";
import "@/utils/filters";
import { initMediaSession } from "@/utils/mediaSession";
import "./registerServiceWorker";
import { dailyTask } from "@/utils/common";
@ -21,8 +20,6 @@ Vue.use(VueAnalytics, {
Vue.config.productionTip = false;
initMediaSession();
if (process.env.VUE_APP_ENABLE_SENTRY === "true") {
Sentry.init({
dsn:

View file

@ -1,185 +1,4 @@
import { updateMediaSessionMetaData } from "@/utils/mediaSession";
import { getTrackDetail, scrobble, getMP3 as getMP3Api } from "@/api/track";
import { isAccountLoggedIn } from "@/utils/auth";
import { updateHttps } from "@/utils/common";
import localforage from "localforage";
import store from "@/store";
import { cacheTrack } from "@/utils/db";
const electron =
process.env.IS_ELECTRON === true ? window.require("electron") : null;
const ipcRenderer =
process.env.IS_ELECTRON === true ? electron.ipcRenderer : null;
export default {
switchTrack(
{ state, dispatch, commit },
{ id, sort = 0, autoplay = true, ifUnplayableThen = "nextTrack" }
) {
getTrackDetail(id).then((data) => {
let track = data.songs[0];
track.sort = sort;
// 获取当前的播放时间。初始化为 loading 状态时返回 howler 的实例而不是浮点数时间,比如 1.332
let time = state.howler ? state.howler.seek() : 0;
let currentTime = 0;
if (time === 0) {
// state.howler._duration 可以获得当前实例的播放时长
currentTime = 180;
}
if (time.toString() === "[object Object]") {
currentTime = 0;
}
if (time > 0) {
currentTime = time;
}
scrobble({
id: state.player.currentTrack.id,
sourceid: state.player.listInfo.id,
time: currentTime,
});
commit("updateCurrentTrack", track);
updateMediaSessionMetaData(track);
document.title = `${track.name} · ${track.ar[0].name} - YesPlayMusic`;
let unblockSongUrl = null;
if (track.playable === false) {
let res = undefined;
if (process.env.IS_ELECTRON === true) {
res = ipcRenderer.sendSync("unblock-music", track);
}
if (res?.url) {
unblockSongUrl = res.url;
} else {
dispatch(ifUnplayableThen);
return;
}
}
function commitMP3(mp3) {
commit("replaceMP3", { mp3, autoplay });
state.howler.once("end", () => {
dispatch("nextTrack");
});
}
function getMP3(id) {
return getMP3Api(id).then((data) => {
// 未知情况下会没有返回数据导致报错,增加防范逻辑
if (data.data[0]) {
const url = updateHttps(data.data[0].url);
commitMP3(url);
return url;
}
});
}
if (isAccountLoggedIn()) {
if (store.state.settings.automaticallyCacheSongs === true) {
let tracks = localforage.createInstance({
name: "tracks",
});
tracks
.getItem(`${track.id}`)
.then((t) => {
if (t !== null) {
const blob = new Blob([t.mp3]);
commitMP3(URL.createObjectURL(blob));
} else {
if (unblockSongUrl) {
commitMP3(unblockSongUrl);
cacheTrack(`${track.id}`, unblockSongUrl);
} else {
getMP3(track.id).then((url) => {
cacheTrack(`${track.id}`, url);
});
}
}
})
.catch((err) => {
console.log(err.messaeg);
if (unblockSongUrl) {
commitMP3(unblockSongUrl);
} else {
getMP3(track.id);
}
});
} else {
if (unblockSongUrl) {
commitMP3(unblockSongUrl);
} else {
getMP3(track.id);
}
}
} else {
commitMP3(
unblockSongUrl ||
`https://music.163.com/song/media/outer/url?id=${track.id}`
);
}
});
},
playFirstTrackOnList({ state, dispatch }) {
dispatch(
"switchTrack",
state.player.list.find((t) => t.sort === 0)
);
},
playTrackOnListByID({ state, commit, dispatch }, trackID) {
let track = state.player.list.find((t) => t.id === trackID);
dispatch("switchTrack", track);
if (state.player.shuffle) {
// 当随机模式开启时双击列表的一首歌进行播放此时要把这首歌的sort调到第一(0),这样用户就能随机播放完整的歌单
let otherTrack = state.player.list.find((t) => t.sort === 0);
commit("switchSortBetweenTwoTracks", {
trackID1: track.id,
trackID2: otherTrack.id,
});
}
},
nextTrack({ state, dispatch }, realNext = false) {
let nextTrack = state.player.list.find(
(track) => track.sort === state.player.currentTrack.sort + 1
);
if (state.player.repeat === "one" && realNext === false) {
nextTrack = state.player.currentTrack;
}
if (nextTrack === undefined) {
if (state.player.repeat !== "off") {
nextTrack = state.player.list.find((t) => t.sort === 0);
} else {
document.title = "YesPlayMusic";
return;
}
}
dispatch("switchTrack", nextTrack);
},
previousTrack({ state, dispatch }) {
let previousTrack = state.player.list.find(
(track) => track.sort === state.player.currentTrack.sort - 1
);
if (previousTrack == undefined) {
if (state.player.repeat !== "off") {
previousTrack = state.player.list.reduce((x, y) => (x > y ? x : y));
} else {
previousTrack = state.player.list.find((t) => t.sort === 0);
}
}
dispatch("switchTrack", {
id: previousTrack.id,
sort: previousTrack.sort,
ifUnplayableThen: "previousTrack",
});
},
addNextTrackEvent({ state, dispatch }) {
state.howler.once("end", () => {
dispatch("nextTrack");
});
},
showToast({ state, commit }, text) {
if (state.toast.timer !== null) {
clearTimeout(state.toast.timer);

View file

@ -3,23 +3,13 @@ import Vuex from "vuex";
import state from "./state";
import mutations from "./mutations";
import actions from "./actions";
import initLocalStorage from "./initLocalStorage";
import { Howler } from "howler";
import { changeAppearance } from "@/utils/common";
import updateApp from "@/utils/updateApp";
import pkg from "../../package.json";
import Player from "@/utils/Player";
// vuex 自定义插件
import { getBroadcastPlugin } from "./plugins/broadcast";
import saveToLocalStorage from "./plugins/localStorage";
if (localStorage.getItem("appVersion") === null) {
localStorage.setItem("player", JSON.stringify(initLocalStorage.player));
localStorage.setItem("settings", JSON.stringify(initLocalStorage.settings));
localStorage.setItem("data", JSON.stringify(initLocalStorage.data));
localStorage.setItem("appVersion", pkg.version);
window.location.reload();
}
updateApp();
Vue.use(Vuex);
@ -39,19 +29,6 @@ const options = {
const store = new Vuex.Store(options);
Howler.volume(store.state.player.volume);
// 防止软件第一次打开资源加载2次
Howler.autoUnlock = false;
const currentTrack = store.state?.player?.currentTrack;
if (currentTrack?.id) {
store.dispatch("switchTrack", {
id: currentTrack.id,
sort: currentTrack.sort,
autoplay: false,
});
}
if ([undefined, null].includes(store.state.settings.lang)) {
let lang = "en";
if (navigator.language.slice(0, 2) === "zh") lang = "zh-CN";
@ -69,4 +46,17 @@ window
}
});
let player = new Player();
player = new Proxy(player, {
set(target, prop, val) {
// console.log({ prop, val });
target[prop] = val;
if (prop === "_howler") return true;
target.saveSelfToLocalStorage();
target.sendSelfToIpcMain();
return true;
},
});
store.state.player = player;
export default store;

View file

@ -1,21 +1,7 @@
import { playlistCategories } from "@/utils/staticData";
let localStorage = {
player: {
enable: false,
show: true,
playing: false,
shuffle: false,
volume: 1,
repeat: "off", // on | off | one
currentTrack: {},
notShuffledList: [],
list: [],
listInfo: {
type: "",
id: "",
},
},
player: {},
settings: {
playlistCategories,
lang: null,

View file

@ -1,100 +1,7 @@
import { Howl, Howler } from "howler";
import { shuffleAList } from "@/utils/common";
export default {
updatePlayerState(state, { key, value }) {
state.player[key] = value;
},
updateCurrentTrack(state, track) {
state.player.currentTrack = track;
},
replaceMP3(state, { mp3, autoplay }) {
if (state.howler) {
Howler.unload();
}
state.howler = new Howl({
src: [mp3],
autoplay,
html5: true,
format: ["mp3", "flac"],
});
},
updatePlayerList(state, list) {
state.player.list = list;
if (state.player.enable !== true) state.player.enable = true;
},
updateListInfo(state, info) {
state.player.listInfo = info;
},
updateRepeatStatus(state, status) {
state.player.repeat = status;
},
appendTrackToPlayerList(state, { track, playNext = false }) {
let existTrack = state.player.list.find((t) => t.id === track.id);
if (
(existTrack === null || existTrack === undefined) &&
playNext === false
) {
state.player.list.push(track);
return;
}
// 把track加入到正在播放歌曲的下一首位置
state.player.list = state.player.list.map((t) => {
if (t.sort > state.player.currentTrack.sort) {
t.sort = t.sort + 1;
}
return t;
});
track.sort = state.player.currentTrack.sort + 1;
state.player.list.push(track);
},
turnOnShuffleMode(state) {
state.player.notShuffledList = JSON.parse(
JSON.stringify(state.player.list)
);
state.player.shuffle = true;
let newSorts = shuffleAList(
state.player.list.filter((t) => t.sort > state.player.currentTrack.sort)
);
state.player.list = state.player.list.map((track) => {
if (newSorts[track.id] !== undefined) track.sort = newSorts[track.id];
return track;
});
},
turnOffShuffleMode(state) {
state.player.shuffle = false;
state.player.list = JSON.parse(
JSON.stringify(state.player.notShuffledList)
);
state.player.currentTrack.sort = state.player.list.find(
(t) => t.id === state.player.currentTrack.id
).sort;
},
shuffleTheListBeforePlay(state) {
state.player.notShuffledList = JSON.parse(
JSON.stringify(state.player.list)
);
let newSorts = shuffleAList(state.player.list);
state.player.list = state.player.list.map((track) => {
track.sort = newSorts[track.id];
return track;
});
},
updateLikedSongs(state, trackIDs) {
state.liked.songs = trackIDs;
},
switchSortBetweenTwoTracks(state, { trackID1, trackID2 }) {
let t1 = state.player.list.find((t) => t.id === trackID1);
let t2 = state.player.list.find((t) => t.id === trackID2);
let sorts = [t1.sort, t2.sort];
state.player.list = state.player.list.map((t) => {
if (t.id === t1.id) t.sort = sorts[1];
if (t.id === t2.id) t.sort = sorts[0];
return t;
});
state.player.sendSelfToIpcMain();
},
changeLang(state, lang) {
state.settings.lang = lang;

View file

@ -1,7 +1,6 @@
export default (store) => {
store.subscribe((mutation, state) => {
// console.log(mutation);
localStorage.setItem("player", JSON.stringify(state.player));
localStorage.setItem("settings", JSON.stringify(state.settings));
localStorage.setItem("data", JSON.stringify(state.data));
});

View file

@ -1,3 +1,12 @@
import initLocalStorage from "./initLocalStorage";
import pkg from "../../package.json";
if (localStorage.getItem("appVersion") === null) {
localStorage.setItem("settings", JSON.stringify(initLocalStorage.settings));
localStorage.setItem("data", JSON.stringify(initLocalStorage.data));
localStorage.setItem("appVersion", pkg.version);
}
export default {
howler: null,
liked: {

377
src/utils/Player.js Normal file
View file

@ -0,0 +1,377 @@
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),
});
}
}

View file

@ -1,53 +0,0 @@
import store from "@/store";
import { getAlbum } from "@/api/album";
import { getPlaylistDetail } from "@/api/playlist";
import { getArtist } from "@/api/artist";
export function playAList(list, id, type, trackID = "first") {
let filteredList = list.map((id, index) => {
return { sort: index, id };
});
store.commit("updatePlayerList", filteredList);
if (store.state.player.shuffle) store.commit("shuffleTheListBeforePlay");
if (trackID === "first") store.dispatch("playFirstTrackOnList");
else store.dispatch("playTrackOnListByID", trackID);
store.commit("updateListInfo", { type, id });
}
export function playAlbumByID(id, trackID = "first") {
getAlbum(id).then((data) => {
let trackIDs = data.songs.map((t) => t.id);
playAList(trackIDs, id, "album", trackID);
});
}
export function playPlaylistByID(id, trackID = "first", noCache = false) {
getPlaylistDetail(id, noCache).then((data) => {
let trackIDs = data.playlist.trackIds.map((t) => t.id);
playAList(trackIDs, id, "playlist", trackID);
});
}
export function playArtistByID(id, trackID = "first") {
getArtist(id).then((data) => {
let trackIDs = data.hotSongs.map((t) => t.id);
playAList(trackIDs, id, "artist", trackID);
});
}
export function appendTrackToPlayerList(trackID, playNext = false) {
let filteredTrack = {
sort: 0,
id: trackID,
};
store.commit("appendTrackToPlayerList", {
track: filteredTrack,
playNext,
});
if (playNext) {
store.dispatch("nextTrack", true);
}
}

View file

@ -24,8 +24,31 @@ const updateData = () => {
localStorage.setItem("data", JSON.stringify(data));
};
const updatePlayer = () => {
let parsedData = JSON.parse(localStorage.getItem("player"));
let appVersion = localStorage.getItem("appVersion");
if (appVersion === `"0.2.5"`) parsedData = {}; // 0.2.6版本重构了player
const data = {
_repeatMode: "off",
_shuffle: false,
_list: [],
_current: 0,
_playlistSource: {},
_volume: 1,
_volumeBeforeMuted: 1,
_currentTrack: {},
_playNextList: [],
_enabled: false,
_shuffledList: [],
_shuffledCurrent: 0,
...parsedData,
};
localStorage.setItem("player", JSON.stringify(data));
};
export default function () {
updateSetting();
updateData();
updatePlayer();
localStorage.setItem("appVersion", JSON.stringify(pkg.version));
}

View file

@ -124,7 +124,6 @@
import { mapMutations, mapActions, mapState } from "vuex";
import { getArtistAlbum } from "@/api/artist";
import { getTrackDetail } from "@/api/track";
import { playAlbumByID } from "@/utils/play";
import { getAlbum, albumDynamicDetail, likeAAlbum } from "@/api/album";
import { splitSoundtrackAlbumTitle, splitAlbumTitle } from "@/utils/common";
import NProgress from "nprogress";
@ -202,7 +201,7 @@ export default {
if (this.tracks.find((t) => t.playable !== false) === undefined) {
return;
}
playAlbumByID(id, trackID);
this.$store.state.player.playAlbumByID(id, trackID);
},
likeAlbum(toast = false) {
if (!isAccountLoggedIn()) {

View file

@ -112,7 +112,6 @@ import {
artistMv,
followAArtist,
} from "@/api/artist";
import { playAList } from "@/utils/play";
import { isAccountLoggedIn } from "@/utils/auth";
import NProgress from "nprogress";
@ -186,7 +185,12 @@ export default {
},
playPopularSongs(trackID = "first") {
let trackIDs = this.popularTracks.map((t) => t.id);
playAList(trackIDs, this.artist.id, "artist", trackID);
this.$store.state.player.replacePlaylist(
trackIDs,
this.artist.id,
"artist",
trackID
);
},
followArtist() {
if (!isAccountLoggedIn()) {

View file

@ -125,7 +125,6 @@ import {
import { randomNum, dailyTask } from "@/utils/common";
import { getPlaylistDetail } from "@/api/playlist";
import { isAccountLoggedIn } from "@/utils/auth";
import { playPlaylistByID } from "@/utils/play";
import NProgress from "nprogress";
import TrackList from "@/components/TrackList.vue";
@ -193,7 +192,11 @@ export default {
...mapActions(["showToast"]),
...mapMutations(["updateModal"]),
playLikedSongs() {
playPlaylistByID(this.playlists[0].id, "first", true);
this.$store.state.player.playPlaylistByID(
this.playlists[0].id,
"first",
true
);
},
updateCurrentTab(tab) {
if (!isAccountLoggedIn() && tab !== "playlists") {

View file

@ -182,7 +182,6 @@ import {
subscribePlaylist,
deletePlaylist,
} from "@/api/playlist";
import { playAList } from "@/utils/play";
import { getTrackDetail } from "@/api/track";
import { isAccountLoggedIn } from "@/utils/auth";
@ -335,7 +334,12 @@ export default {
...mapActions(["playFirstTrackOnList", "playTrackOnListByID", "showToast"]),
playPlaylistByID(trackID = "first") {
let trackIDs = this.playlist.trackIds.map((t) => t.id);
playAList(trackIDs, this.playlist.id, "playlist", trackID);
this.$store.state.player.replacePlaylist(
trackIDs,
this.playlist.id,
"playlist",
trackID
);
},
likePlaylist(toast = false) {
if (!isAccountLoggedIn()) {

View file

@ -105,7 +105,6 @@
<script>
import { mapState } from "vuex";
import NProgress from "nprogress";
import { appendTrackToPlayerList } from "@/utils/play";
import { search } from "@/api/others";
import Cover from "@/components/Cover.vue";
@ -148,7 +147,7 @@ export default {
},
playTrackInSearchResult(id) {
let track = this.tracks.find((t) => t.id === id);
appendTrackToPlayerList(track, true);
this.$store.state.player.appendTrackToPlayerList(track, true);
},
getData(keywords) {
search({ keywords: keywords, type: 1018 }).then((data) => {