Shuffle/Repeat fixes, Prebuilt frontend, Linux icon, Playback errors, Unsynced lyrics, Bug fixes
This commit is contained in:
parent
27b55a4876
commit
863c1aff40
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
|
||||||
|
@ -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
|
||||||
|
2
app/client/.gitignore
vendored
2
app/client/.gitignore
vendored
@ -1,7 +1,5 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
/dist
|
|
||||||
|
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
|
@ -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;
|
||||||
this.$router.push({path: '/search', query: {q: this.searchQuery}});
|
|
||||||
|
//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) {
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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() {
|
||||||
|
@ -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>
|
||||||
|
@ -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": {
|
||||||
|
@ -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 = [];
|
||||||
|
@ -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', {
|
|
||||||
playlist_id: req.params.id.toString(),
|
|
||||||
lang: 'us',
|
|
||||||
nb: 50,
|
|
||||||
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', {
|
let data = await deezer.callApi('deezer.pagePlaylist', {
|
||||||
playlist_id: req.params.id.toString(),
|
playlist_id: req.params.id.toString(),
|
||||||
lang: 'us',
|
lang: 'us',
|
||||||
nb: chunk,
|
nb: nb,
|
||||||
start: 0,
|
start: req.query.start ? parseInt(req.query.start, 10) : 0,
|
||||||
tags: true
|
tags: true
|
||||||
});
|
});
|
||||||
let playlist = new Playlist(data.results.DATA, data.results.SONGS);
|
return res.send(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
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
BIN
build/iconset/256x256.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
build/iconset/512x512.png
Normal file
BIN
build/iconset/512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
13
package.json
13
package.json
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user