Shuffle/Repeat fixes, Prebuilt frontend, Linux icon, Playback errors, Unsynced lyrics, Bug fixes

This commit is contained in:
exttex 2020-09-20 19:17:28 +02:00
parent 27b55a4876
commit 863c1aff40
15 changed files with 133 additions and 57 deletions

2
.gitignore vendored
View File

@ -2,7 +2,7 @@ dist/
node_modules/ node_modules/
app/node_modules/ app/node_modules/
app/dist/ app/dist/
app/client/dist/ #app/client/dist/
app/client/node_modules/ app/client/node_modules/
electron_dist/ electron_dist/
freezer-*.tgz freezer-*.tgz

View File

@ -26,6 +26,11 @@ Or manually:
npm i npm i
cd app cd app
npm i npm i
```
**1.0.5** - Prebuilt frontend is now included in the repo. The following steps are optional (but recommended):
```
cd client cd client
npm i npm i
npm run build npm run build

View File

@ -1,7 +1,5 @@
.DS_Store .DS_Store
node_modules node_modules
/dist
# local env files # local env files
.env.local .env.local

View File

@ -2,7 +2,7 @@
<v-app v-esc='closePlayer'> <v-app v-esc='closePlayer'>
<!-- Fullscreen player overlay --> <!-- Fullscreen player overlay -->
<v-overlay :value='showPlayer' opacity='0.97' z-index="100"> <v-overlay :value='showPlayer' opacity='1.00' z-index="100">
<FullscreenPlayer @close='closePlayer' @volumeChange='volume = $root.volume'></FullscreenPlayer> <FullscreenPlayer @close='closePlayer' @volumeChange='volume = $root.volume'></FullscreenPlayer>
</v-overlay> </v-overlay>
@ -130,8 +130,10 @@
prepend-inner-icon="mdi-magnify" prepend-inner-icon="mdi-magnify"
single-line single-line
solo solo
placeholder='Search or paste Deezer URL. Use "/" to quickly focus.'
v-model="searchQuery" v-model="searchQuery"
ref='searchBar' ref='searchBar'
:loading='searchLoading'
@keyup='search'> @keyup='search'>
</v-text-field> </v-text-field>
</v-app-bar> </v-app-bar>
@ -191,7 +193,7 @@
<v-icon v-if='!$root.isPlaying()'>mdi-play</v-icon> <v-icon v-if='!$root.isPlaying()'>mdi-play</v-icon>
<v-icon v-if='$root.isPlaying()'>mdi-pause</v-icon> <v-icon v-if='$root.isPlaying()'>mdi-pause</v-icon>
</v-btn> </v-btn>
<v-btn icon large @click.stop='$root.skip(1)'> <v-btn icon large @click.stop='$root.skipNext'>
<v-icon>mdi-skip-next</v-icon> <v-icon>mdi-skip-next</v-icon>
</v-btn> </v-btn>
</v-col> </v-col>
@ -267,6 +269,7 @@ export default {
showPlayer: false, showPlayer: false,
position: '0.00', position: '0.00',
searchQuery: '', searchQuery: '',
searchLoading: false
} }
}, },
methods: { methods: {
@ -283,10 +286,54 @@ export default {
next() { next() {
this.$router.go(1); this.$router.go(1);
}, },
search(event) { async search(event) {
//KeyUp event, enter //KeyUp event, enter
if (event.keyCode !== 13) return; if (event.keyCode !== 13) return;
//Check if url
if (this.searchQuery.startsWith('http')) {
this.searchLoading = true;
let url = new URL(this.searchQuery);
//Normal link
if (url.hostname == 'www.deezer.com' || url.hostname == 'deezer.com' || url.hostname == 'deezer.page.link') {
//Share link
if (url.hostname == 'deezer.page.link') {
let res = await this.$axios.get('/fullurl?url=' + encodeURIComponent(this.searchQuery));
url = new URL(res.data.url);
}
let supported = ['track', 'artist', 'album', 'playlist'];
let path = url.pathname.substring(1).split('/');
if (path.length == 3) path = path.slice(1);
let type = path[0];
if (supported.includes(type)) {
//Dirty lol
let res = await this.$axios('/' + path.join('/'));
if (res.data) {
//Add to queue
if (type == 'track') {
this.$root.queue.data.splice(this.$root.queue.index + 1, 0, res.data);
this.$root.skip(1);
}
//Show details page
if (type == 'artist' || type == 'album' || type == 'playlist') {
let query = {};
query[type] = JSON.stringify(res.data);
this.$router.push({path: `/${type}`, query: query});
}
}
}
}
this.searchLoading = false;
} else {
//Normal search
this.$router.push({path: '/search', query: {q: this.searchQuery}}); this.$router.push({path: '/search', query: {q: this.searchQuery}});
}
}, },
seek(val) { seek(val) {
this.$root.seek(Math.round((val / 100) * this.$root.duration())); this.$root.seek(Math.round((val / 100) * this.$root.duration()));
@ -314,7 +361,7 @@ export default {
if (event.keyCode != 47) return; if (event.keyCode != 47) return;
this.$refs.searchBar.focus(); this.$refs.searchBar.focus();
setTimeout(() => { setTimeout(() => {
this.searchQuery = this.searchQuery.replace(new RegExp('/', 'g'), ''); if (this.searchQuery.startsWith('/')) this.searchQuery = this.searchQuery.substring(1);
}, 40); }, 40);
}); });

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<v-list-item @click='click' v-if='!card' :class='{dense: tiny}'> <v-list-item @click='click' v-if='!card' :class='{dense: tiny}'>
<v-list-item-avatar v-if='!tiny'> <v-list-item-avatar>
<v-img :src='artist.picture.thumb'></v-img> <v-img :src='artist.picture.thumb'></v-img>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content> <v-list-item-content>

View File

@ -4,7 +4,7 @@
<v-progress-circular indeterminate v-if='loading'></v-progress-circular> <v-progress-circular indeterminate v-if='loading'></v-progress-circular>
</div> </div>
<div v-if='!loading && lyrics' class='text-center'> <div v-if='!loading && lyrics && lyrics.lyrics.length > 0' class='text-center'>
<div v-for='(lyric, index) in lyrics.lyrics' :key='lyric.offset' class='my-8 mx-4'> <div v-for='(lyric, index) in lyrics.lyrics' :key='lyric.offset' class='my-8 mx-4'>
<span <span
class='my-8' class='my-8'
@ -16,8 +16,18 @@
</div> </div>
</div> </div>
<!-- Unsynchronized -->
<div v-if='!loading && lyrics && lyrics.text.length > 0 && lyrics.lyrics.length == 0' class='text-center'>
<span v-for='(lyric, index) in lyrics.text' :key='"US" + index' class='my-8 mx-4'>
<span class='my-8 text-h6 font-weight-regular'>
{{lyric}}
</span>
<br>
</span>
</div>
<!-- Error --> <!-- Error -->
<div v-if='!loading && !lyrics' class='pa-4 text-center'> <div v-if='!loading && !lyrics && lyrics.text.length == 0 && lyrics.lyrics.length == 0' class='pa-4 text-center'>
<span class='red--text text-h5'> <span class='red--text text-h5'>
Error loading lyrics or lyrics not found! Error loading lyrics or lyrics not found!
</span> </span>
@ -49,7 +59,7 @@ export default {
try { try {
let res = await this.$axios.get(`/lyrics/${this.songId}`); let res = await this.$axios.get(`/lyrics/${this.songId}`);
if (res.data && res.data.lyrics.length > 0) this.lyrics = res.data; if (res.data && res.data.lyrics) this.lyrics = res.data;
} catch (e) {true;} } catch (e) {true;}
this.loading = false; this.loading = false;

View File

@ -167,6 +167,17 @@ new Vue({
if (newIndex < 0 || newIndex >= this.queue.data.length) return; if (newIndex < 0 || newIndex >= this.queue.data.length) return;
this.playIndex(newIndex); this.playIndex(newIndex);
}, },
//Skip wrapper with shuffle
skipNext() {
if (this.shuffle) {
let index = Math.round(Math.random()*this.queue.data.length) - this.queue.index;
this.skip(index);
this.savePlaybackInfo();
return;
}
this.skip(1);
this.savePlaybackInfo();
},
toggleMute() { toggleMute() {
if (this.audio) this.audio.muted = !this.audio.muted; if (this.audio) this.audio.muted = !this.audio.muted;
this.muted = !this.muted; this.muted = !this.muted;
@ -200,6 +211,9 @@ new Vue({
this.configureAudio(); this.configureAudio();
this.state = 1; this.state = 1;
if (autoplay) this.play(); if (autoplay) this.play();
//Loads more tracks if end of list
this.loadSTL();
}, },
//Configure html audio element //Configure html audio element
configureAudio() { configureAudio() {
@ -223,6 +237,13 @@ new Vue({
this.audio.addEventListener('ended', async () => { this.audio.addEventListener('ended', async () => {
//Repeat track
if (this.repeat == 2) {
this.seek(0);
this.audio.play();
return;
}
//Shuffle //Shuffle
if (this.shuffle) { if (this.shuffle) {
let index = Math.round(Math.random()*this.queue.data.length) - this.queue.index; let index = Math.round(Math.random()*this.queue.data.length) - this.queue.index;
@ -231,13 +252,6 @@ new Vue({
return; return;
} }
//Repeat track
if (this.repeat == 2) {
this.seek(0);
this.audio.play();
return;
}
//Repeat list //Repeat list
if (this.queue.index == this.queue.data.length - 1) { if (this.queue.index == this.queue.data.length - 1) {
this.skip(-(this.queue.data.length - 1)); this.skip(-(this.queue.data.length - 1));
@ -284,7 +298,7 @@ new Vue({
//Controls //Controls
navigator.mediaSession.setActionHandler('play', this.play); navigator.mediaSession.setActionHandler('play', this.play);
navigator.mediaSession.setActionHandler('pause', this.pause); navigator.mediaSession.setActionHandler('pause', this.pause);
navigator.mediaSession.setActionHandler('nexttrack', () => this.skip(1)); navigator.mediaSession.setActionHandler('nexttrack', this.skipNext);
navigator.mediaSession.setActionHandler('previoustrack', () => this.skip(-1)); navigator.mediaSession.setActionHandler('previoustrack', () => this.skip(-1));
}, },
//Get Deezer CDN image url //Get Deezer CDN image url
@ -328,6 +342,16 @@ new Vue({
//Might get canceled //Might get canceled
if (this.gapless.promise) resolve(); if (this.gapless.promise) resolve();
}, },
//Load more SmartTrackList tracks
async loadSTL() {
if (this.queue.data.length - 1 == this.queue.index && this.queue.source.source == 'smarttracklist') {
let data = await this.$axios.get('/smarttracklist/' + this.queue.source.data);
if (data.data) {
this.queue.data = this.queue.data.concat(data.data);
}
this.savePlaybackInfo();
}
},
//Update & save settings //Update & save settings
async saveSettings() { async saveSettings() {

View File

@ -63,7 +63,7 @@
</v-col> </v-col>
<v-col> <v-col>
<v-btn icon x-large @click='$root.skip(1)'> <v-btn icon x-large @click='$root.skipNext'>
<v-icon size='42px'>mdi-skip-next</v-icon> <v-icon size='42px'>mdi-skip-next</v-icon>
</v-btn> </v-btn>
</v-col> </v-col>

View File

@ -1,7 +1,7 @@
{ {
"name": "freezer", "name": "freezer",
"private": true, "private": true,
"version": "1.0.4", "version": "1.0.5",
"description": "", "description": "",
"main": "background.js", "main": "background.js",
"scripts": { "scripts": {

View File

@ -247,7 +247,10 @@ class Lyrics {
constructor(json) { constructor(json) {
this.id = json.LYRICS_ID; this.id = json.LYRICS_ID;
this.writer = json.LYRICS_WRITERS; this.writer = json.LYRICS_WRITERS;
this.text = json.LYRICS_TEXT; this.text = [];
if (json.LYRICS_TEXT) {
this.text = json.LYRICS_TEXT.replace(new RegExp('\\r', 'g'), '').split('\n');
}
//Parse invidual lines //Parse invidual lines
this.lyrics = []; this.lyrics = [];

View File

@ -102,40 +102,15 @@ app.get('/artist/:id', async (req, res) => {
//start & full query parameters //start & full query parameters
app.get('/playlist/:id', async (req, res) => { app.get('/playlist/:id', async (req, res) => {
//Set anything to `full` query parameter to get entire playlist //Set anything to `full` query parameter to get entire playlist
if (!req.query.full) { let nb = req.query.full ? 100000 : 50;
let data = await deezer.callApi('deezer.pagePlaylist', { let data = await deezer.callApi('deezer.pagePlaylist', {
playlist_id: req.params.id.toString(), playlist_id: req.params.id.toString(),
lang: 'us', lang: 'us',
nb: 50, nb: nb,
start: req.query.start ? parseInt(req.query.start, 10) : 0, start: req.query.start ? parseInt(req.query.start, 10) : 0,
tags: true tags: true
}); });
return res.send(new Playlist(data.results.DATA, data.results.SONGS)); return res.send(new Playlist(data.results.DATA, data.results.SONGS));
}
//Entire playlist
let chunk = 200;
let data = await deezer.callApi('deezer.pagePlaylist', {
playlist_id: req.params.id.toString(),
lang: 'us',
nb: chunk,
start: 0,
tags: true
});
let playlist = new Playlist(data.results.DATA, data.results.SONGS);
let missingChunks = Math.ceil((playlist.trackCount - playlist.tracks.length)/chunk);
//Extend playlist
for(let i=0; i<missingChunks; i++) {
let d = await deezer.callApi('deezer.pagePlaylist', {
playlist_id: id.toString(),
lang: 'us',
nb: chunk,
start: (i+1)*chunk,
tags: true
});
playlist.extend(d.results.SONGS);
}
res.send(playlist);
}); });
//DELETE playlist //DELETE playlist
@ -484,6 +459,13 @@ app.get('/lastfm', async (req, res) => {
}).end(); }).end();
}); });
//Get URL from deezer.page.link
app.get('/fullurl', async (req, res) => {
let url = req.query.url;
let r = await axios.get(url, {validateStatus: null});
res.json({url: r.request.res.responseUrl});
});
//Redirect to index on unknown path //Redirect to index on unknown path
app.all('*', (req, res) => { app.all('*', (req, res) => {
res.redirect('/'); res.redirect('/');

BIN
build/iconset/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
build/iconset/256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
build/iconset/512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,7 +1,7 @@
{ {
"name": "freezer", "name": "freezer",
"private": true, "private": true,
"version": "1.0.4", "version": "1.0.5",
"description": "", "description": "",
"scripts": { "scripts": {
"pack": "electron-builder --dir", "pack": "electron-builder --dir",
@ -9,7 +9,6 @@
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"build": "cd app && npm i && cd client && npm i && npm run build && cd .. && cd .. && npm run dist" "build": "cd app && npm i && cd client && npm i && npm run build && cd .. && cd .. && npm run dist"
}, },
"author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"electron": "^9.2.1", "electron": "^9.2.1",
@ -42,7 +41,15 @@
"AppImage" "AppImage"
], ],
"category": "audio", "category": "audio",
"icon": "build/icon.png" "icon": "build/iconset"
},
"appImage": {
"desktop": {
"X-AppImage-Name": "Freezer",
"Name": "Freezer",
"Type": "Application",
"Categories": "AudioVideo"
}
} }
} }
} }