Shuffle/Repeat fixes, Prebuilt frontend, Linux icon, Playback errors, Unsynced lyrics, Bug fixes
This commit is contained in:
parent
27b55a4876
commit
863c1aff40
|
@ -2,7 +2,7 @@ dist/
|
|||
node_modules/
|
||||
app/node_modules/
|
||||
app/dist/
|
||||
app/client/dist/
|
||||
#app/client/dist/
|
||||
app/client/node_modules/
|
||||
electron_dist/
|
||||
freezer-*.tgz
|
||||
|
|
|
@ -26,6 +26,11 @@ Or manually:
|
|||
npm i
|
||||
cd app
|
||||
npm i
|
||||
```
|
||||
|
||||
**1.0.5** - Prebuilt frontend is now included in the repo. The following steps are optional (but recommended):
|
||||
|
||||
```
|
||||
cd client
|
||||
npm i
|
||||
npm run build
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<v-app v-esc='closePlayer'>
|
||||
|
||||
<!-- 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>
|
||||
</v-overlay>
|
||||
|
||||
|
@ -130,8 +130,10 @@
|
|||
prepend-inner-icon="mdi-magnify"
|
||||
single-line
|
||||
solo
|
||||
placeholder='Search or paste Deezer URL. Use "/" to quickly focus.'
|
||||
v-model="searchQuery"
|
||||
ref='searchBar'
|
||||
:loading='searchLoading'
|
||||
@keyup='search'>
|
||||
</v-text-field>
|
||||
</v-app-bar>
|
||||
|
@ -191,7 +193,7 @@
|
|||
<v-icon v-if='!$root.isPlaying()'>mdi-play</v-icon>
|
||||
<v-icon v-if='$root.isPlaying()'>mdi-pause</v-icon>
|
||||
</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-btn>
|
||||
</v-col>
|
||||
|
@ -267,6 +269,7 @@ export default {
|
|||
showPlayer: false,
|
||||
position: '0.00',
|
||||
searchQuery: '',
|
||||
searchLoading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -283,10 +286,54 @@ export default {
|
|||
next() {
|
||||
this.$router.go(1);
|
||||
},
|
||||
search(event) {
|
||||
async search(event) {
|
||||
//KeyUp event, enter
|
||||
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}});
|
||||
}
|
||||
},
|
||||
seek(val) {
|
||||
this.$root.seek(Math.round((val / 100) * this.$root.duration()));
|
||||
|
@ -314,7 +361,7 @@ export default {
|
|||
if (event.keyCode != 47) return;
|
||||
this.$refs.searchBar.focus();
|
||||
setTimeout(() => {
|
||||
this.searchQuery = this.searchQuery.replace(new RegExp('/', 'g'), '');
|
||||
if (this.searchQuery.startsWith('/')) this.searchQuery = this.searchQuery.substring(1);
|
||||
}, 40);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<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-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<v-progress-circular indeterminate v-if='loading'></v-progress-circular>
|
||||
</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'>
|
||||
<span
|
||||
class='my-8'
|
||||
|
@ -16,8 +16,18 @@
|
|||
</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 -->
|
||||
<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'>
|
||||
Error loading lyrics or lyrics not found!
|
||||
</span>
|
||||
|
@ -49,7 +59,7 @@ export default {
|
|||
try {
|
||||
|
||||
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;}
|
||||
this.loading = false;
|
||||
|
|
|
@ -167,6 +167,17 @@ new Vue({
|
|||
if (newIndex < 0 || newIndex >= this.queue.data.length) return;
|
||||
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() {
|
||||
if (this.audio) this.audio.muted = !this.audio.muted;
|
||||
this.muted = !this.muted;
|
||||
|
@ -200,6 +211,9 @@ new Vue({
|
|||
this.configureAudio();
|
||||
this.state = 1;
|
||||
if (autoplay) this.play();
|
||||
|
||||
//Loads more tracks if end of list
|
||||
this.loadSTL();
|
||||
},
|
||||
//Configure html audio element
|
||||
configureAudio() {
|
||||
|
@ -223,6 +237,13 @@ new Vue({
|
|||
|
||||
this.audio.addEventListener('ended', async () => {
|
||||
|
||||
//Repeat track
|
||||
if (this.repeat == 2) {
|
||||
this.seek(0);
|
||||
this.audio.play();
|
||||
return;
|
||||
}
|
||||
|
||||
//Shuffle
|
||||
if (this.shuffle) {
|
||||
let index = Math.round(Math.random()*this.queue.data.length) - this.queue.index;
|
||||
|
@ -231,13 +252,6 @@ new Vue({
|
|||
return;
|
||||
}
|
||||
|
||||
//Repeat track
|
||||
if (this.repeat == 2) {
|
||||
this.seek(0);
|
||||
this.audio.play();
|
||||
return;
|
||||
}
|
||||
|
||||
//Repeat list
|
||||
if (this.queue.index == this.queue.data.length - 1) {
|
||||
this.skip(-(this.queue.data.length - 1));
|
||||
|
@ -284,7 +298,7 @@ new Vue({
|
|||
//Controls
|
||||
navigator.mediaSession.setActionHandler('play', this.play);
|
||||
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));
|
||||
},
|
||||
//Get Deezer CDN image url
|
||||
|
@ -328,6 +342,16 @@ new Vue({
|
|||
//Might get canceled
|
||||
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
|
||||
async saveSettings() {
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
</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-btn>
|
||||
</v-col>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "freezer",
|
||||
"private": true,
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.5",
|
||||
"description": "",
|
||||
"main": "background.js",
|
||||
"scripts": {
|
||||
|
|
|
@ -247,7 +247,10 @@ class Lyrics {
|
|||
constructor(json) {
|
||||
this.id = json.LYRICS_ID;
|
||||
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
|
||||
this.lyrics = [];
|
||||
|
|
|
@ -102,40 +102,15 @@ app.get('/artist/:id', async (req, res) => {
|
|||
//start & full query parameters
|
||||
app.get('/playlist/:id', async (req, res) => {
|
||||
//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', {
|
||||
playlist_id: req.params.id.toString(),
|
||||
lang: 'us',
|
||||
nb: 50,
|
||||
nb: nb,
|
||||
start: req.query.start ? parseInt(req.query.start, 10) : 0,
|
||||
tags: true
|
||||
});
|
||||
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
|
||||
|
@ -484,6 +459,13 @@ app.get('/lastfm', async (req, res) => {
|
|||
}).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
|
||||
app.all('*', (req, res) => {
|
||||
res.redirect('/');
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 9.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
13
package.json
13
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "freezer",
|
||||
"private": true,
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.5",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"pack": "electron-builder --dir",
|
||||
|
@ -9,7 +9,6 @@
|
|||
"postinstall": "electron-builder install-app-deps",
|
||||
"build": "cd app && npm i && cd client && npm i && npm run build && cd .. && cd .. && npm run dist"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"electron": "^9.2.1",
|
||||
|
@ -42,7 +41,15 @@
|
|||
"AppImage"
|
||||
],
|
||||
"category": "audio",
|
||||
"icon": "build/icon.png"
|
||||
"icon": "build/iconset"
|
||||
},
|
||||
"appImage": {
|
||||
"desktop": {
|
||||
"X-AppImage-Name": "Freezer",
|
||||
"Name": "Freezer",
|
||||
"Type": "Application",
|
||||
"Categories": "AudioVideo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue