1.0.6 - Discord integration, logging, bugfixes
This commit is contained in:
parent
863c1aff40
commit
83860ff052
15
app/.eslintrc.js
Normal file
15
app/.eslintrc.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
module.exports = {
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"commonjs": true,
|
||||||
|
"node": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"extends": "eslint:recommended",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 12
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"allowEmptyCatch": true
|
||||||
|
}
|
||||||
|
};
|
1
app/client/public/lastfm.svg
Normal file
1
app/client/public/lastfm.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="96px" height="96px"><path d="M 10.699219 16.691406 L 9.894531 14.539063 C 9.894531 14.539063 8.585938 15.972656 6.617188 15.972656 C 4.878906 15.972656 3.644531 14.488281 3.644531 12.117188 C 3.644531 9.074219 5.207031 7.988281 6.746094 7.988281 C 8.960938 7.988281 9.667969 9.394531 10.273438 11.199219 L 11.078125 13.671875 C 11.886719 16.070313 13.398438 18 17.757813 18 C 20.882813 18 23 17.058594 23 14.589844 C 23 12.585938 21.839844 11.546875 19.671875 11.050781 L 18.058594 10.707031 C 16.949219 10.460938 16.621094 10.015625 16.621094 9.273438 C 16.621094 8.433594 17.304688 7.9375 18.410156 7.9375 C 19.621094 7.9375 20.277344 8.382813 20.378906 9.445313 L 22.898438 9.148438 C 22.695313 6.925781 21.132813 6.007813 18.5625 6.007813 C 16.296875 6.007813 14.078125 6.851563 14.078125 9.542969 C 14.078125 11.226563 14.90625 12.289063 17 12.78125 L 18.714844 13.179688 C 20 13.476563 20.429688 13.992188 20.429688 14.710938 C 20.429688 15.625 19.519531 15.996094 17.808594 15.996094 C 15.261719 15.996094 14.203125 14.6875 13.597656 12.882813 L 12.769531 10.410156 C 11.707031 7.195313 10.019531 6.011719 6.667969 6.011719 C 2.964844 6.007813 1 8.308594 1 12.214844 C 1 15.972656 2.964844 18 6.492188 18 C 9.339844 18 10.699219 16.691406 10.699219 16.691406 Z"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
@ -124,18 +124,24 @@
|
|||||||
<v-icon>mdi-arrow-right</v-icon>
|
<v-icon>mdi-arrow-right</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-text-field
|
<!-- Search -->
|
||||||
|
<v-autocomplete
|
||||||
hide-details
|
hide-details
|
||||||
flat
|
|
||||||
prepend-inner-icon="mdi-magnify"
|
prepend-inner-icon="mdi-magnify"
|
||||||
|
flat
|
||||||
single-line
|
single-line
|
||||||
solo
|
solo
|
||||||
|
clearable
|
||||||
|
hide-no-data
|
||||||
placeholder='Search or paste Deezer URL. Use "/" to quickly focus.'
|
placeholder='Search or paste Deezer URL. Use "/" to quickly focus.'
|
||||||
v-model="searchQuery"
|
|
||||||
ref='searchBar'
|
|
||||||
:loading='searchLoading'
|
:loading='searchLoading'
|
||||||
@keyup='search'>
|
@keyup='search'
|
||||||
</v-text-field>
|
ref='searchBar'
|
||||||
|
v-model='searchQuery'
|
||||||
|
:search-input.sync='searchInput'
|
||||||
|
:items='suggestions'
|
||||||
|
></v-autocomplete>
|
||||||
|
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
|
|
||||||
<!-- Main -->
|
<!-- Main -->
|
||||||
@ -269,7 +275,10 @@ export default {
|
|||||||
showPlayer: false,
|
showPlayer: false,
|
||||||
position: '0.00',
|
position: '0.00',
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
searchLoading: false
|
searchLoading: false,
|
||||||
|
searchInput: null,
|
||||||
|
suggestions: [],
|
||||||
|
preventDoubleEnter: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -288,19 +297,24 @@ export default {
|
|||||||
},
|
},
|
||||||
async search(event) {
|
async search(event) {
|
||||||
//KeyUp event, enter
|
//KeyUp event, enter
|
||||||
if (event.keyCode !== 13) return;
|
if (event && event.keyCode !== 13) return;
|
||||||
|
//Prevent double navigation
|
||||||
|
if (this.preventDoubleEnter) return;
|
||||||
|
this.preventDoubleEnter = true;
|
||||||
|
setInterval(() => {this.preventDoubleEnter = false}, 50);
|
||||||
|
|
||||||
//Check if url
|
//Check if url
|
||||||
if (this.searchQuery.startsWith('http')) {
|
let query = this.searchInput;
|
||||||
|
if (query.startsWith('http')) {
|
||||||
this.searchLoading = true;
|
this.searchLoading = true;
|
||||||
let url = new URL(this.searchQuery);
|
let url = new URL(query);
|
||||||
|
|
||||||
//Normal link
|
//Normal link
|
||||||
if (url.hostname == 'www.deezer.com' || url.hostname == 'deezer.com' || url.hostname == 'deezer.page.link') {
|
if (url.hostname == 'www.deezer.com' || url.hostname == 'deezer.com' || url.hostname == 'deezer.page.link') {
|
||||||
|
|
||||||
//Share link
|
//Share link
|
||||||
if (url.hostname == 'deezer.page.link') {
|
if (url.hostname == 'deezer.page.link') {
|
||||||
let res = await this.$axios.get('/fullurl?url=' + encodeURIComponent(this.searchQuery));
|
let res = await this.$axios.get('/fullurl?url=' + encodeURIComponent(query));
|
||||||
url = new URL(res.data.url);
|
url = new URL(res.data.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,7 +346,7 @@ export default {
|
|||||||
this.searchLoading = false;
|
this.searchLoading = false;
|
||||||
} else {
|
} else {
|
||||||
//Normal search
|
//Normal search
|
||||||
this.$router.push({path: '/search', query: {q: this.searchQuery}});
|
this.$router.push({path: '/search', query: {q: query}});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
seek(val) {
|
seek(val) {
|
||||||
@ -383,6 +397,33 @@ export default {
|
|||||||
//Update position
|
//Update position
|
||||||
'$root.position'() {
|
'$root.position'() {
|
||||||
this.position = (this.$root.position / this.$root.duration()) * 100;
|
this.position = (this.$root.position / this.$root.duration()) * 100;
|
||||||
|
},
|
||||||
|
//Autofill
|
||||||
|
searchInput(query) {
|
||||||
|
//Filters
|
||||||
|
if (query && query.startsWith('/')) {
|
||||||
|
query = query.substring(1);
|
||||||
|
this.searchInput = query;
|
||||||
|
}
|
||||||
|
if (!query || (query && query.startsWith('http'))) {
|
||||||
|
this.searchLoading = false;
|
||||||
|
this.suggestions = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.searchLoading = true;
|
||||||
|
//Prevent spam
|
||||||
|
setTimeout(() => {
|
||||||
|
if (query != this.searchInput) return;
|
||||||
|
this.$axios.get('/suggestions/' + encodeURIComponent(query)).then((res) => {
|
||||||
|
if (query != this.searchInput) return;
|
||||||
|
this.suggestions = res.data;
|
||||||
|
this.searchLoading = false;
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
},
|
||||||
|
searchQuery(q) {
|
||||||
|
this.searchInput = q;
|
||||||
|
this.search(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-list-item @click='click' v-if='!card' :class='{dense: tiny}'>
|
<v-list-item @click='click' v-if='!card'>
|
||||||
<v-list-item-avatar>
|
<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>
|
||||||
|
@ -108,6 +108,17 @@ export default {
|
|||||||
dShow() {
|
dShow() {
|
||||||
if (!this.dShow) this.$emit('close');
|
if (!this.dShow) this.$emit('close');
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
//Auto download
|
||||||
|
if (!this.$root.settings.downloadDialog) {
|
||||||
|
this.download();
|
||||||
|
setInterval(() => {
|
||||||
|
this.$emit('close');
|
||||||
|
this.dShow = false;
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
@ -74,10 +74,6 @@ export default {
|
|||||||
},
|
},
|
||||||
//Play track
|
//Play track
|
||||||
async play(index) {
|
async play(index) {
|
||||||
if (this.tracks.length < this.count) {
|
|
||||||
await this.loadAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$root.queue.source = {
|
this.$root.queue.source = {
|
||||||
text: 'Loved tracks',
|
text: 'Loved tracks',
|
||||||
source: 'playlist',
|
source: 'playlist',
|
||||||
@ -85,6 +81,15 @@ export default {
|
|||||||
};
|
};
|
||||||
this.$root.replaceQueue(this.tracks);
|
this.$root.replaceQueue(this.tracks);
|
||||||
this.$root.playIndex(index);
|
this.$root.playIndex(index);
|
||||||
|
|
||||||
|
//Load all tracks
|
||||||
|
if (this.tracks.length < this.count) {
|
||||||
|
this.loadAll().then(() => {
|
||||||
|
this.$root.replaceQueue(this.tracks);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
removedTrack(index) {
|
removedTrack(index) {
|
||||||
this.tracks.splice(index, 1);
|
this.tracks.splice(index, 1);
|
||||||
|
@ -85,7 +85,7 @@ export default {
|
|||||||
},
|
},
|
||||||
//Scroll to currently playing lyric
|
//Scroll to currently playing lyric
|
||||||
scrollLyric() {
|
scrollLyric() {
|
||||||
if (!this.lyrics) return;
|
if (!this.lyrics || !this.lyrics.lyrics || this.lyrics.lyrics.length == 0) return;
|
||||||
|
|
||||||
//Prevent janky scrolling
|
//Prevent janky scrolling
|
||||||
if (this.currentLyricIndex == this.currentLyric()) return;
|
if (this.currentLyricIndex == this.currentLyric()) return;
|
||||||
|
@ -109,14 +109,12 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
async play() {
|
async play() {
|
||||||
let playlist = this.playlist;
|
let playlist = this.playlist;
|
||||||
//Load playlist tracks
|
//Load if no tracks
|
||||||
if (playlist.tracks.length != playlist.trackCount) {
|
if (!playlist || playlist.tracks.length == 0)
|
||||||
let data = await this.$axios.get(`/playlist/${playlist.id}?full=iguess`);
|
playlist = (await this.$axios.get(`/playlist/${playlist.id}?full=iguess`)).data;
|
||||||
playlist = data.data;
|
|
||||||
}
|
|
||||||
//Error handling
|
|
||||||
if (!playlist) return;
|
if (!playlist) return;
|
||||||
|
|
||||||
|
//Play
|
||||||
this.$root.queue.source = {
|
this.$root.queue.source = {
|
||||||
text: playlist.title,
|
text: playlist.title,
|
||||||
source: 'playlist',
|
source: 'playlist',
|
||||||
@ -124,6 +122,12 @@ export default {
|
|||||||
};
|
};
|
||||||
this.$root.replaceQueue(playlist.tracks);
|
this.$root.replaceQueue(playlist.tracks);
|
||||||
this.$root.playIndex(0);
|
this.$root.playIndex(0);
|
||||||
|
|
||||||
|
//Load all tracks
|
||||||
|
if (playlist.tracks.length != playlist.trackCount) {
|
||||||
|
let data = await this.$axios.get(`/playlist/${playlist.id}?full=iguess`);
|
||||||
|
playlist = data.data;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
//On click navigate to details
|
//On click navigate to details
|
||||||
click() {
|
click() {
|
||||||
|
@ -180,12 +180,14 @@ export default {
|
|||||||
this.$axios.put(`/library/tracks?id=${this.track.id}`);
|
this.$axios.put(`/library/tracks?id=${this.track.id}`);
|
||||||
},
|
},
|
||||||
goAlbum() {
|
goAlbum() {
|
||||||
|
this.$emit('redirect')
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
path: '/album',
|
path: '/album',
|
||||||
query: {album: JSON.stringify(this.track.album)}
|
query: {album: JSON.stringify(this.track.album)}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
goArtist(a) {
|
goArtist(a) {
|
||||||
|
this.$emit('redirect');
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
path: '/artist',
|
path: '/artist',
|
||||||
query: {artist: JSON.stringify(a)}
|
query: {artist: JSON.stringify(a)}
|
||||||
|
@ -131,9 +131,11 @@ new Vue({
|
|||||||
this.play();
|
this.play();
|
||||||
},
|
},
|
||||||
seek(t) {
|
seek(t) {
|
||||||
if (!this.audio) return;
|
if (!this.audio || isNaN(t) || !t) return;
|
||||||
//ms -> s
|
//ms -> s
|
||||||
this.audio.currentTime = (t / 1000);
|
this.audio.currentTime = (t / 1000);
|
||||||
|
|
||||||
|
this.updateState();
|
||||||
},
|
},
|
||||||
|
|
||||||
//Current track duration
|
//Current track duration
|
||||||
@ -161,11 +163,11 @@ new Vue({
|
|||||||
this.savePlaybackInfo();
|
this.savePlaybackInfo();
|
||||||
},
|
},
|
||||||
//Skip n tracks, can be negative
|
//Skip n tracks, can be negative
|
||||||
skip(n) {
|
async skip(n) {
|
||||||
let newIndex = this.queue.index + n;
|
let newIndex = this.queue.index + n;
|
||||||
//Out of bounds
|
//Out of bounds
|
||||||
if (newIndex < 0 || newIndex >= this.queue.data.length) return;
|
if (newIndex < 0 || newIndex >= this.queue.data.length) return;
|
||||||
this.playIndex(newIndex);
|
await this.playIndex(newIndex);
|
||||||
},
|
},
|
||||||
//Skip wrapper with shuffle
|
//Skip wrapper with shuffle
|
||||||
skipNext() {
|
skipNext() {
|
||||||
@ -253,7 +255,7 @@ new Vue({
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Repeat list
|
//Repeat list
|
||||||
if (this.queue.index == this.queue.data.length - 1) {
|
if (this.repeat == 1 && this.queue.index == this.queue.data.length - 1) {
|
||||||
this.skip(-(this.queue.data.length - 1));
|
this.skip(-(this.queue.data.length - 1));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -403,6 +405,18 @@ new Vue({
|
|||||||
|
|
||||||
this.logListenId = this.track.id;
|
this.logListenId = this.track.id;
|
||||||
await this.$axios.post(`/log`, this.track);
|
await this.$axios.post(`/log`, this.track);
|
||||||
|
},
|
||||||
|
//Send state update to integrations
|
||||||
|
async updateState() {
|
||||||
|
//Wait for duration
|
||||||
|
if (this.state == 2 && (this.duration() == null || isNaN(this.duration())))
|
||||||
|
await new Promise((res) => setTimeout(res, 1000));
|
||||||
|
this.$socket.emit('stateChange', {
|
||||||
|
position: this.position,
|
||||||
|
duration: this.duration(),
|
||||||
|
state: this.state,
|
||||||
|
track: this.track
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
@ -471,6 +485,12 @@ new Vue({
|
|||||||
this.sockets.subscribe('download', (data) => {
|
this.sockets.subscribe('download', (data) => {
|
||||||
this.download = data;
|
this.download = data;
|
||||||
});
|
});
|
||||||
|
//Play at offset (for integrations)
|
||||||
|
this.sockets.subscribe('playOffset', async (data) => {
|
||||||
|
this.queue.data.splice(this.queue.index + 1, 0, data.track);
|
||||||
|
await this.skip(1);
|
||||||
|
this.seek(data.position);
|
||||||
|
});
|
||||||
|
|
||||||
r();
|
r();
|
||||||
},
|
},
|
||||||
@ -499,6 +519,12 @@ new Vue({
|
|||||||
if (e.keyCode === 106 || e.keyCode === 74) this.$root.seek((this.position - 10000));
|
if (e.keyCode === 106 || e.keyCode === 74) this.$root.seek((this.position - 10000));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
//Watch state for integrations
|
||||||
|
state() {
|
||||||
|
this.updateState();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
router,
|
router,
|
||||||
vuetify,
|
vuetify,
|
||||||
|
@ -144,6 +144,7 @@
|
|||||||
><TrackTile
|
><TrackTile
|
||||||
:track='track'
|
:track='track'
|
||||||
@click='$root.playIndex(index)'
|
@click='$root.playIndex(index)'
|
||||||
|
@redirect='close'
|
||||||
></TrackTile>
|
></TrackTile>
|
||||||
</v-lazy>
|
</v-lazy>
|
||||||
|
|
||||||
@ -161,14 +162,13 @@
|
|||||||
></AlbumTile>
|
></AlbumTile>
|
||||||
<!-- Artists -->
|
<!-- Artists -->
|
||||||
<h3>Artists:</h3>
|
<h3>Artists:</h3>
|
||||||
<v-list dense>
|
<v-list>
|
||||||
<ArtistTile
|
<ArtistTile
|
||||||
v-for='(artist, index) in $root.track.artists'
|
v-for='(artist, index) in $root.track.artists'
|
||||||
:artist='artist'
|
:artist='artist'
|
||||||
:key="index + 'a' + artist.id"
|
:key="index + 'a' + artist.id"
|
||||||
@clicked='$emit("close")'
|
@clicked='$emit("close")'
|
||||||
tiny
|
tiny
|
||||||
class='text-left'
|
|
||||||
></ArtistTile>
|
></ArtistTile>
|
||||||
</v-list>
|
</v-list>
|
||||||
<!-- Meta -->
|
<!-- Meta -->
|
||||||
@ -179,6 +179,7 @@
|
|||||||
<h3>Source: {{$root.playbackInfo.source}}</h3>
|
<h3>Source: {{$root.playbackInfo.source}}</h3>
|
||||||
<h3>Format: {{$root.playbackInfo.format}}</h3>
|
<h3>Format: {{$root.playbackInfo.format}}</h3>
|
||||||
<h3>Quality: {{$root.playbackInfo.quality}}</h3>
|
<h3>Quality: {{$root.playbackInfo.quality}}</h3>
|
||||||
|
<h3>ID: {{$root.track.id}}</h3>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-tab-item>
|
</v-tab-item>
|
||||||
<!-- Lyrics -->
|
<!-- Lyrics -->
|
||||||
|
@ -87,9 +87,8 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
async playIndex(index) {
|
async playIndex(index) {
|
||||||
//Load tracks
|
//Load tracks
|
||||||
if (this.playlist.tracks.length < this.playlist.trackCount) {
|
if (this.playlist.tracks.length == 0)
|
||||||
await this.loadAllTracks();
|
await this.loadAllTracks();
|
||||||
}
|
|
||||||
|
|
||||||
this.$root.queue.source = {
|
this.$root.queue.source = {
|
||||||
text: this.playlist.title,
|
text: this.playlist.title,
|
||||||
@ -98,6 +97,13 @@ export default {
|
|||||||
};
|
};
|
||||||
this.$root.replaceQueue(this.playlist.tracks);
|
this.$root.replaceQueue(this.playlist.tracks);
|
||||||
this.$root.playIndex(index);
|
this.$root.playIndex(index);
|
||||||
|
|
||||||
|
//Load rest of tracks on background
|
||||||
|
if (this.playlist.tracks.length < this.playlist.trackCount) {
|
||||||
|
this.loadAllTracks().then(() => {
|
||||||
|
this.$root.replaceQueue(this.playlist.tracks);
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
play() {
|
play() {
|
||||||
this.playIndex(0);
|
this.playIndex(0);
|
||||||
|
@ -29,6 +29,17 @@
|
|||||||
@click:append='selectDownloadPath'
|
@click:append='selectDownloadPath'
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
||||||
|
<!-- Download dialog -->
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-action>
|
||||||
|
<v-checkbox v-model='$root.settings.downloadDialog'></v-checkbox>
|
||||||
|
</v-list-item-action>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Show download dialog</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>Always show download confirm dialog before downloading.</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
<!-- Create artist folder -->
|
<!-- Create artist folder -->
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
<v-list-item-action>
|
<v-list-item-action>
|
||||||
@ -57,6 +68,10 @@
|
|||||||
hint='Variables: %title%, %artists%, %artist%, %feats%, %trackNumber%, %0trackNumber%, %album%'
|
hint='Variables: %title%, %artists%, %artist%, %feats%, %trackNumber%, %0trackNumber%, %album%'
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
||||||
|
<!-- Accounts -->
|
||||||
|
<v-subheader>Integrations</v-subheader>
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
<!-- Log listening -->
|
<!-- Log listening -->
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
<v-list-item-action>
|
<v-list-item-action>
|
||||||
@ -69,11 +84,46 @@
|
|||||||
</v-list-item>
|
</v-list-item>
|
||||||
<!-- LastFM -->
|
<!-- LastFM -->
|
||||||
<v-list-item @click='connectLastFM' v-if='!$root.settings.lastFM'>
|
<v-list-item @click='connectLastFM' v-if='!$root.settings.lastFM'>
|
||||||
|
<v-list-item-avatar>
|
||||||
|
<v-img src='lastfm.svg'></v-img>
|
||||||
|
</v-list-item-avatar>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-list-item-title>Login with LastFM</v-list-item-title>
|
<v-list-item-title>Login with LastFM</v-list-item-title>
|
||||||
<v-list-item-subtitle>Connect your LastFM account to allow scrobbling.</v-list-item-subtitle>
|
<v-list-item-subtitle>Connect your LastFM account to allow scrobbling.</v-list-item-subtitle>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
<v-list-item v-if='$root.settings.lastFM' @click='disconnectLastFM'>
|
||||||
|
<v-list-item-avatar>
|
||||||
|
<v-icon>mdi-logout</v-icon>
|
||||||
|
</v-list-item-avatar>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title class='red--text'>Disconnect LastFM</v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
<!-- Discord -->
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-action>
|
||||||
|
<v-checkbox v-model='$root.settings.enableDiscord' @click='snackbarText = "Requires restart to apply!"; snackbar = true'></v-checkbox>
|
||||||
|
</v-list-item-action>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Discord Rich Presence</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>Enable Discord Rich Presence, requires restart to toggle!</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
<!-- Discord Join Button -->
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-action>
|
||||||
|
<v-checkbox v-model='$root.settings.discordJoin' @click='snackbarText = "Requires restart to apply!"; snackbar = true'></v-checkbox>
|
||||||
|
</v-list-item-action>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Discord Join Button</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>Enable Discord join button for syncing tracks, requires restart to toggle!</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<!-- Misc -->
|
||||||
|
<v-subheader>Other</v-subheader>
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
<!-- Minimize to tray -->
|
<!-- Minimize to tray -->
|
||||||
<v-list-item v-if='$root.settings.electron'>
|
<v-list-item v-if='$root.settings.electron'>
|
||||||
@ -108,6 +158,22 @@
|
|||||||
Save
|
Save
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
|
<!-- Info snackbar -->
|
||||||
|
<v-snackbar v-model="snackbar">
|
||||||
|
{{ snackbarText }}
|
||||||
|
|
||||||
|
<template v-slot:action="{ attrs }">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
text
|
||||||
|
v-bind="attrs"
|
||||||
|
@click="snackbar = false"
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-snackbar>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -124,7 +190,9 @@ export default {
|
|||||||
],
|
],
|
||||||
streamingQuality: null,
|
streamingQuality: null,
|
||||||
downloadQuality: null,
|
downloadQuality: null,
|
||||||
devToolsCounter: 0
|
devToolsCounter: 0,
|
||||||
|
snackbarText: null,
|
||||||
|
snackbar: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -178,6 +246,12 @@ export default {
|
|||||||
async connectLastFM() {
|
async connectLastFM() {
|
||||||
let res = await this.$axios.get('/lastfm');
|
let res = await this.$axios.get('/lastfm');
|
||||||
window.location.replace(res.data.url);
|
window.location.replace(res.data.url);
|
||||||
|
},
|
||||||
|
//Disconnect LastFM
|
||||||
|
async disconnectLastFM() {
|
||||||
|
this.$root.settings.lastFM = null;
|
||||||
|
await this.$root.saveSettings();
|
||||||
|
window.location.reload();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
1127
app/package-lock.json
generated
1127
app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "freezer",
|
"name": "freezer",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "background.js",
|
"main": "background.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -12,13 +12,17 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
"browser-id3-writer": "^4.4.0",
|
"browser-id3-writer": "^4.4.0",
|
||||||
|
"discord-rpc": "^3.1.4",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"lastfmapi": "^0.1.1",
|
"lastfmapi": "^0.1.1",
|
||||||
"metaflac-js2": "^1.0.7",
|
"metaflac-js2": "^1.0.7",
|
||||||
"nedb": "^1.8.0",
|
"nedb": "^1.8.0",
|
||||||
"nodeezcryptor": "git+https://notabug.org/xefglm/nodeezcryptor",
|
"nodeezcryptor": "git+https://notabug.org/xefglm/nodeezcryptor",
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"socket.io": "^2.3.0"
|
"socket.io": "^2.3.0",
|
||||||
|
"winston": "^3.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {}
|
"devDependencies": {
|
||||||
|
"eslint": "^7.10.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ const axios = require('axios');
|
|||||||
const decryptor = require('nodeezcryptor');
|
const decryptor = require('nodeezcryptor');
|
||||||
const querystring = require('querystring');
|
const querystring = require('querystring');
|
||||||
const {Transform} = require('stream');
|
const {Transform} = require('stream');
|
||||||
|
const {Track} = require('./definitions');
|
||||||
|
const logger = require('./winston');
|
||||||
|
|
||||||
class DeezerAPI {
|
class DeezerAPI {
|
||||||
|
|
||||||
@ -162,6 +164,45 @@ class DeezerAPI {
|
|||||||
|
|
||||||
return `https://e-cdns-proxy-${md5origin.substring(0, 1)}.dzcdn.net/mobile/1/${step3}`;
|
return `https://e-cdns-proxy-${md5origin.substring(0, 1)}.dzcdn.net/mobile/1/${step3}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Quality fallback
|
||||||
|
async qualityFallback(info, quality = 3) {
|
||||||
|
if (quality == 1) return {
|
||||||
|
quality: '128kbps',
|
||||||
|
format: 'MP3',
|
||||||
|
source: 'stream',
|
||||||
|
url: `/stream/${info}?q=1`
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
let tdata = Track.getUrlInfo(info);
|
||||||
|
let res = await axios.head(DeezerAPI.getUrl(tdata.trackId, tdata.md5origin, tdata.mediaVersion, quality));
|
||||||
|
if (quality == 3) {
|
||||||
|
return {
|
||||||
|
quality: '320kbps',
|
||||||
|
format: 'MP3',
|
||||||
|
source: 'stream',
|
||||||
|
url: `/stream/${info}?q=3`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Bitrate will be calculated in client
|
||||||
|
return {
|
||||||
|
quality: res.headers['content-length'],
|
||||||
|
format: 'FLAC',
|
||||||
|
source: 'stream',
|
||||||
|
url: `/stream/${info}?q=9`
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warning('Qualiy fallback: ' + e);
|
||||||
|
//Fallback
|
||||||
|
//9 - FLAC
|
||||||
|
//3 - MP3 320
|
||||||
|
//1 - MP3 128
|
||||||
|
let q = quality;
|
||||||
|
if (quality == 9) q = 3;
|
||||||
|
if (quality == 3) q = 1;
|
||||||
|
return this.qualityFallback(info, q);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeezerDecryptionStream extends Transform {
|
class DeezerDecryptionStream extends Transform {
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
const {DeezerAPI} = require('./deezer');
|
|
||||||
|
|
||||||
//Datatypes, constructor parameters = gw_light API call.
|
//Datatypes, constructor parameters = gw_light API call.
|
||||||
class Track {
|
class Track {
|
||||||
constructor(json) {
|
constructor(json) {
|
||||||
@ -32,13 +30,12 @@ class Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Get Deezer CDN url by streamUrl
|
//Get Deezer CDN url by streamUrl
|
||||||
static getUrl(info, quality = 3) {
|
static getUrlInfo(info) {
|
||||||
let md5origin = info.substring(0, 32);
|
let md5origin = info.substring(0, 32);
|
||||||
if (info.charAt(32) == '1') md5origin += '.mp3';
|
if (info.charAt(32) == '1') md5origin += '.mp3';
|
||||||
let mediaVersion = parseInt(info.substring(33, 34)).toString();
|
let mediaVersion = parseInt(info.substring(33, 34)).toString();
|
||||||
let trackId = info.substring(35);
|
let trackId = info.substring(35);
|
||||||
let url = DeezerAPI.getUrl(trackId, md5origin, mediaVersion, quality);
|
return {trackId, md5origin, mediaVersion};
|
||||||
return url;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,6 +121,8 @@ class DeezerImage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
url(size = 256) {
|
url(size = 256) {
|
||||||
|
if (!this.hash)
|
||||||
|
return `https://e-cdns-images.dzcdn.net/images/${this.type}/${size}x${size}-000000-80-0-0.jpg`;
|
||||||
return `https://e-cdns-images.dzcdn.net/images/${this.type}/${this.hash}/${size}x${size}-000000-80-0-0.jpg`;
|
return `https://e-cdns-images.dzcdn.net/images/${this.type}/${this.hash}/${size}x${size}-000000-80-0-0.jpg`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,13 @@ const {Track} = require('./definitions');
|
|||||||
const decryptor = require('nodeezcryptor');
|
const decryptor = require('nodeezcryptor');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const logger = require('./winston');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const Datastore = require('nedb');
|
const Datastore = require('nedb');
|
||||||
const ID3Writer = require('browser-id3-writer');
|
const ID3Writer = require('browser-id3-writer');
|
||||||
const Metaflac = require('metaflac-js2');
|
const Metaflac = require('metaflac-js2');
|
||||||
const sanitize = require("sanitize-filename");
|
const sanitize = require("sanitize-filename");
|
||||||
|
const { DeezerAPI } = require('./deezer');
|
||||||
|
|
||||||
class Downloads {
|
class Downloads {
|
||||||
constructor(settings, qucb) {
|
constructor(settings, qucb) {
|
||||||
@ -246,9 +248,12 @@ class Download {
|
|||||||
this.downloaded = start;
|
this.downloaded = start;
|
||||||
|
|
||||||
//Get download info
|
//Get download info
|
||||||
if (!this.url) this.url = Track.getUrl(this.track.streamUrl, this.quality);
|
if (!this.url) {
|
||||||
|
let streamInfo = Track.getUrlInfo(this.track.streamUrl);
|
||||||
|
this.url = DeezerAPI.getUrl(streamInfo.trackId, streamInfo.md5origin, streamInfo.mediaVersion, this.quality);
|
||||||
|
}
|
||||||
this._request = https.get(this.url, {headers: {'Range': `bytes=${start}-`}}, (r) => {
|
this._request = https.get(this.url, {headers: {'Range': `bytes=${start}-`}}, (r) => {
|
||||||
|
let skip = false;
|
||||||
//Error
|
//Error
|
||||||
if (r.statusCode >= 400) {
|
if (r.statusCode >= 400) {
|
||||||
//Fallback on error
|
//Fallback on error
|
||||||
@ -261,12 +266,31 @@ class Download {
|
|||||||
};
|
};
|
||||||
//Error
|
//Error
|
||||||
this.state = -1;
|
this.state = -1;
|
||||||
console.log(`Undownloadable track ID: ${this.track.id}`);
|
logger.error(`Undownloadable track ID: ${this.track.id}`);
|
||||||
return this.onDone();
|
return this.onDone();
|
||||||
|
} else {
|
||||||
|
this.path += (this.quality == 9) ? '.flac' : '.mp3';
|
||||||
|
|
||||||
|
//Check if file exits
|
||||||
|
fs.access(this.path, (err) => {
|
||||||
|
if (err) {
|
||||||
|
//Pipe data to file
|
||||||
|
r.pipe(fs.createWriteStream(tmp, {flags: 'a'}));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
logger.warn('File already exists! Skipping...');
|
||||||
|
skip = true;
|
||||||
|
this._request.end();
|
||||||
|
this.state = 3;
|
||||||
|
return this.onDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//On download done
|
//On download done
|
||||||
r.on('end', () => {
|
r.on('end', () => {
|
||||||
|
if (skip) return;
|
||||||
if (this.downloaded != this.size) return;
|
if (this.downloaded != this.size) return;
|
||||||
this._finished(tmp);
|
this._finished(tmp);
|
||||||
});
|
});
|
||||||
@ -276,15 +300,13 @@ class Download {
|
|||||||
});
|
});
|
||||||
|
|
||||||
r.on('error', (e) => {
|
r.on('error', (e) => {
|
||||||
console.log(`Download error: ${e}`);
|
logger.error(`Download error: ${e}`);
|
||||||
//TODO: Download error handling
|
//TODO: Download error handling
|
||||||
})
|
})
|
||||||
|
|
||||||
//Save size
|
//Save size
|
||||||
this.size = parseInt(r.headers['content-length'], 10) + start;
|
this.size = parseInt(r.headers['content-length'], 10) + start;
|
||||||
|
|
||||||
//Pipe data to file
|
|
||||||
r.pipe(fs.createWriteStream(tmp, {flags: 'a'}));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -311,7 +333,7 @@ class Download {
|
|||||||
} catch (e) {};
|
} catch (e) {};
|
||||||
|
|
||||||
//Decrypt
|
//Decrypt
|
||||||
this.path += (this.quality == 9) ? '.flac' : '.mp3';
|
//this.path += (this.quality == 9) ? '.flac' : '.mp3';
|
||||||
decryptor.decryptFile(decryptor.getKey(this.track.id), tmp, `${tmp}.DEC`);
|
decryptor.decryptFile(decryptor.getKey(this.track.id), tmp, `${tmp}.DEC`);
|
||||||
fs.promises.copyFile(`${tmp}.DEC`, this.path);
|
fs.promises.copyFile(`${tmp}.DEC`, this.path);
|
||||||
//Delete encrypted
|
//Delete encrypted
|
||||||
|
142
app/src/integrations.js
Normal file
142
app/src/integrations.js
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
const LastfmAPI = require('lastfmapi');
|
||||||
|
const DiscordRPC = require('discord-rpc');
|
||||||
|
const {EventEmitter} = require('events');
|
||||||
|
const logger = require('./winston');
|
||||||
|
|
||||||
|
class Integrations extends EventEmitter {
|
||||||
|
|
||||||
|
//LastFM, Discord etc
|
||||||
|
|
||||||
|
constructor(settings) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.settings = settings;
|
||||||
|
this.discordReady = false;
|
||||||
|
this.discordRPC = null;
|
||||||
|
|
||||||
|
//LastFM
|
||||||
|
//plz don't steal creds, it's just lastfm
|
||||||
|
this.lastfm = new LastfmAPI({
|
||||||
|
api_key: 'b6ab5ae967bcd8b10b23f68f42493829',
|
||||||
|
secret: '861b0dff9a8a574bec747f9dab8b82bf'
|
||||||
|
});
|
||||||
|
this.authorizeLastFM();
|
||||||
|
|
||||||
|
//Discord
|
||||||
|
if (settings.enableDiscord)
|
||||||
|
this.connectDiscord();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSettings(settings) {
|
||||||
|
this.settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Autorize lastfm with saved credentials
|
||||||
|
authorizeLastFM() {
|
||||||
|
if (!this.settings.lastFM) return;
|
||||||
|
this.lastfm.setSessionCredentials(this.settings.lastFM.name, this.settings.lastFM.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Login to lastfm by token
|
||||||
|
async loginLastFM(token) {
|
||||||
|
let response = await new Promise((res) => {
|
||||||
|
this.lastfm.authenticate(token, (err, sess) => {
|
||||||
|
if (err) res();
|
||||||
|
res({
|
||||||
|
name: sess.username,
|
||||||
|
key: sess.key
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.settings.lastFM = response;
|
||||||
|
this.authorizeLastFM();
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
//LastFM Scrobble
|
||||||
|
async scrobbleLastFM(title, artist) {
|
||||||
|
if (this.settings.lastFM)
|
||||||
|
this.lastfm.track.scrobble({
|
||||||
|
artist: artist,
|
||||||
|
track: title,
|
||||||
|
timestamp: Math.floor((new Date()).getTime() / 1000)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Connect to discord client
|
||||||
|
connectDiscord() {
|
||||||
|
//Don't steal, k ty
|
||||||
|
const CLIENTID = '759835951450292324';
|
||||||
|
|
||||||
|
this.discordReady = false;
|
||||||
|
DiscordRPC.register(CLIENTID);
|
||||||
|
this.discordRPC = new DiscordRPC.Client({transport: 'ipc'});
|
||||||
|
this.discordRPC.on('connected', () => {
|
||||||
|
this.discordReady = true;
|
||||||
|
|
||||||
|
//Allow discord "join" button
|
||||||
|
if (this.settings.discordJoin) {
|
||||||
|
//Always accept join requests
|
||||||
|
this.discordRPC.subscribe('ACTIVITY_JOIN_REQUEST', (user) => {
|
||||||
|
this.discordRPC.sendJoinInvite(user.user).catch((e) => {
|
||||||
|
logger.warning('Unable to accept Discord invite: ' + e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
//Joined
|
||||||
|
this.discordRPC.subscribe('ACTIVITY_JOIN', async (data) => {
|
||||||
|
let params = JSON.parse(data.secret);
|
||||||
|
this.emit('discordJoin', params);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//Connect to discord
|
||||||
|
this.discordRPC.login({clientId: CLIENTID}).catch(() => {
|
||||||
|
logger.info('Error connecting to Discord!');
|
||||||
|
//Wait 5s to retry
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.discordReady)
|
||||||
|
this.connectDiscord();
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Called when playback state changed
|
||||||
|
async updateState(data) {
|
||||||
|
if (this.discordReady) {
|
||||||
|
let richPresence = {
|
||||||
|
state: data.track.artistString,
|
||||||
|
details: data.track.title,
|
||||||
|
largeImageKey: 'icon',
|
||||||
|
instance: true,
|
||||||
|
}
|
||||||
|
//Show timestamp only if playing
|
||||||
|
if (data.state == 2) {
|
||||||
|
Object.assign(richPresence, {
|
||||||
|
startTimestamp: Date.now() - data.position,
|
||||||
|
endTimestamp: (Date.now() - data.position) + data.duration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//Enabled discord join
|
||||||
|
if (this.settings.discordJoin) {
|
||||||
|
Object.assign(richPresence, {
|
||||||
|
partySize: 1,
|
||||||
|
partyMax: 10,
|
||||||
|
matchSecret: 'match_secret_' + data.track.id,
|
||||||
|
joinSecret: JSON.stringify({
|
||||||
|
pos: Math.floor(data.position),
|
||||||
|
ts: Date.now(),
|
||||||
|
id: data.track.id
|
||||||
|
}),
|
||||||
|
partyId: 'party_id_' + data.track.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//Set
|
||||||
|
this.discordRPC.setActivity(richPresence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {Integrations};
|
@ -3,15 +3,17 @@ const path = require('path');
|
|||||||
const https = require('https');
|
const https = require('https');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const axios = require('axios').default;
|
const axios = require('axios').default;
|
||||||
const LastfmAPI = require('lastfmapi');
|
const logger = require('./winston');
|
||||||
const {DeezerAPI, DeezerDecryptionStream} = require('./deezer');
|
const {DeezerAPI, DeezerDecryptionStream} = require('./deezer');
|
||||||
const {Settings} = require('./settings');
|
const {Settings} = require('./settings');
|
||||||
const {Track, Album, Artist, Playlist, DeezerProfile, SearchResults, DeezerLibrary, DeezerPage, Lyrics} = require('./definitions');
|
const {Track, Album, Artist, Playlist, DeezerProfile, SearchResults, DeezerLibrary, DeezerPage, Lyrics} = require('./definitions');
|
||||||
const {Downloads} = require('./downloads');
|
const {Downloads} = require('./downloads');
|
||||||
|
const {Integrations} = require('./integrations');
|
||||||
|
|
||||||
let settings;
|
let settings;
|
||||||
let deezer;
|
let deezer;
|
||||||
let downloads;
|
let downloads;
|
||||||
|
let integrations;
|
||||||
|
|
||||||
let sockets = [];
|
let sockets = [];
|
||||||
|
|
||||||
@ -22,12 +24,6 @@ app.use(express.static(path.join(__dirname, '../client', 'dist')));
|
|||||||
//Server
|
//Server
|
||||||
const server = require('http').createServer(app);
|
const server = require('http').createServer(app);
|
||||||
const io = require('socket.io').listen(server);
|
const io = require('socket.io').listen(server);
|
||||||
//LastFM
|
|
||||||
//plz don't steal creds, it's just lastfm
|
|
||||||
let lastfm = new LastfmAPI({
|
|
||||||
api_key: 'b6ab5ae967bcd8b10b23f68f42493829',
|
|
||||||
secret: '861b0dff9a8a574bec747f9dab8b82bf'
|
|
||||||
});
|
|
||||||
|
|
||||||
//Get playback info
|
//Get playback info
|
||||||
app.get('/playback', async (req, res) => {
|
app.get('/playback', async (req, res) => {
|
||||||
@ -58,6 +54,7 @@ app.post('/settings', async (req, res) => {
|
|||||||
if (req.body) {
|
if (req.body) {
|
||||||
Object.assign(settings, req.body);
|
Object.assign(settings, req.body);
|
||||||
downloads.settings = settings;
|
downloads.settings = settings;
|
||||||
|
integrations.updateSettings(settings);
|
||||||
await settings.save();
|
await settings.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,14 +238,15 @@ app.put('/library/:type', async (req, res) => {
|
|||||||
app.get('/streaminfo/:info', async (req, res) => {
|
app.get('/streaminfo/:info', async (req, res) => {
|
||||||
let info = req.params.info;
|
let info = req.params.info;
|
||||||
let quality = req.query.q ? req.query.q : 3;
|
let quality = req.query.q ? req.query.q : 3;
|
||||||
return res.json(await qualityFallback(info, quality));
|
return res.json(await deezer.qualityFallback(info, quality));
|
||||||
});
|
});
|
||||||
|
|
||||||
// S T R E A M I N G
|
// S T R E A M I N G
|
||||||
app.get('/stream/:info', (req, res) => {
|
app.get('/stream/:info', (req, res) => {
|
||||||
//Parse stream info
|
//Parse stream info
|
||||||
let quality = req.query.q ? req.query.q : 3;
|
let quality = req.query.q ? req.query.q : 3;
|
||||||
let url = Track.getUrl(req.params.info, quality);
|
let streamInfo = Track.getUrlInfo(req.params.info);
|
||||||
|
let url = DeezerAPI.getUrl(streamInfo.trackId, streamInfo.md5origin, streamInfo.mediaVersion, quality);
|
||||||
let trackId = req.params.info.substring(35);
|
let trackId = req.params.info.substring(35);
|
||||||
|
|
||||||
//MIME type of audio
|
//MIME type of audio
|
||||||
@ -307,7 +305,7 @@ app.get('/stream/:info', (req, res) => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
//Internet/Request error
|
//Internet/Request error
|
||||||
_request.on('error', (e) => {
|
_request.on('error', () => {
|
||||||
//console.log('Streaming error: ' + e);
|
//console.log('Streaming error: ' + e);
|
||||||
//HTML audio will restart automatically
|
//HTML audio will restart automatically
|
||||||
res.destroy();
|
res.destroy();
|
||||||
@ -367,6 +365,20 @@ app.get('/lyrics/:id', async (req, res) => {
|
|||||||
res.send(new Lyrics(data.results));
|
res.send(new Lyrics(data.results));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Search Suggestions
|
||||||
|
app.get('/suggestions/:query', async (req, res) => {
|
||||||
|
let query = req.params.query;
|
||||||
|
try {
|
||||||
|
let data = await deezer.callApi('search_getSuggestedQueries', {
|
||||||
|
QUERY: query
|
||||||
|
});
|
||||||
|
let out = data.results.SUGGESTION.map((s) => s.QUERY);
|
||||||
|
res.json(out);
|
||||||
|
} catch (e) {
|
||||||
|
res.json([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
//Post list of tracks to download
|
//Post list of tracks to download
|
||||||
app.post('/downloads', async (req, res) => {
|
app.post('/downloads', async (req, res) => {
|
||||||
let tracks = req.body;
|
let tracks = req.body;
|
||||||
@ -410,12 +422,7 @@ app.delete('/downloads/:index', async (req, res) => {
|
|||||||
//Log listen to deezer & lastfm
|
//Log listen to deezer & lastfm
|
||||||
app.post('/log', async (req, res) => {
|
app.post('/log', async (req, res) => {
|
||||||
//LastFM
|
//LastFM
|
||||||
if (settings.lastFM)
|
integrations.scrobbleLastFM(req.body.title, req.body.artists[0].name);
|
||||||
lastfm.track.scrobble({
|
|
||||||
artist: req.body.artists[0].name,
|
|
||||||
track: req.body.title,
|
|
||||||
timestamp: Math.floor((new Date()).getTime() / 1000)
|
|
||||||
});
|
|
||||||
|
|
||||||
//Deezer
|
//Deezer
|
||||||
if (settings.logListen)
|
if (settings.logListen)
|
||||||
@ -436,26 +443,19 @@ app.get('/lastfm', async (req, res) => {
|
|||||||
//Got token
|
//Got token
|
||||||
if (req.query.token) {
|
if (req.query.token) {
|
||||||
let token = req.query.token;
|
let token = req.query.token;
|
||||||
await new Promise((res, rej) => {
|
//Authorize
|
||||||
lastfm.authenticate(token, (err, sess) => {
|
let authinfo = await integrations.loginLastFM(token);
|
||||||
if (err) res();
|
if (authinfo) {
|
||||||
//Save to settings
|
settings.lastFM = authinfo;
|
||||||
settings.lastFM = {
|
|
||||||
name: sess.username,
|
|
||||||
key: sess.key
|
|
||||||
};
|
|
||||||
settings.save();
|
settings.save();
|
||||||
res();
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
authorizeLastFM();
|
|
||||||
//Redirect to homepage
|
//Redirect to homepage
|
||||||
return res.redirect('/');
|
return res.redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
//Get auth url
|
//Get auth url
|
||||||
res.json({
|
res.json({
|
||||||
url: lastfm.getAuthenticationUrl({cb: `http://${req.socket.remoteAddress}:${settings.port}/lastfm`})
|
url: integrations.lastfm.getAuthenticationUrl({cb: `http://${req.socket.remoteAddress}:${settings.port}/lastfm`})
|
||||||
}).end();
|
}).end();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -478,50 +478,11 @@ io.on('connection', (socket) => {
|
|||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
sockets.splice(sockets.indexOf(socket), 1);
|
sockets.splice(sockets.indexOf(socket), 1);
|
||||||
});
|
});
|
||||||
|
//Send to integrations
|
||||||
|
socket.on('stateChange', (data) => {
|
||||||
|
integrations.updateState(data);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
//Quality fallback
|
|
||||||
async function qualityFallback(info, quality = 3) {
|
|
||||||
if (quality == 1) return {
|
|
||||||
quality: '128kbps',
|
|
||||||
format: 'MP3',
|
|
||||||
source: 'stream',
|
|
||||||
url: `/stream/${info}?q=1`
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
let res = await axios.head(Track.getUrl(info, quality));
|
|
||||||
if (quality == 3) {
|
|
||||||
return {
|
|
||||||
quality: '320kbps',
|
|
||||||
format: 'MP3',
|
|
||||||
source: 'stream',
|
|
||||||
url: `/stream/${info}?q=3`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//Bitrate will be calculated in client
|
|
||||||
return {
|
|
||||||
quality: res.headers['content-length'],
|
|
||||||
format: 'FLAC',
|
|
||||||
source: 'stream',
|
|
||||||
url: `/stream/${info}?q=9`
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
//Fallback
|
|
||||||
//9 - FLAC
|
|
||||||
//3 - MP3 320
|
|
||||||
//1 - MP3 128
|
|
||||||
let q = quality;
|
|
||||||
if (quality == 9) q = 3;
|
|
||||||
if (quality == 3) q = 1;
|
|
||||||
return qualityFallback(info, q);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Autorize lastfm with saved credentials
|
|
||||||
function authorizeLastFM() {
|
|
||||||
if (!settings.lastFM) return;
|
|
||||||
lastfm.setSessionCredentials(settings.lastFM.name, settings.lastFM.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
//ecb = Error callback
|
//ecb = Error callback
|
||||||
async function createServer(electron = false, ecb) {
|
async function createServer(electron = false, ecb) {
|
||||||
@ -559,8 +520,21 @@ async function createServer(electron = false, ecb) {
|
|||||||
});
|
});
|
||||||
}, 350);
|
}, 350);
|
||||||
|
|
||||||
//LastFM
|
//Integrations (lastfm, discord)
|
||||||
authorizeLastFM();
|
integrations = new Integrations(settings);
|
||||||
|
//Discord Join = Sync tracks
|
||||||
|
integrations.on('discordJoin', async (data) => {
|
||||||
|
let trackData = await deezer.callApi('deezer.pageTrack', {sng_id: data.id});
|
||||||
|
let track = new Track(trackData.results.DATA);
|
||||||
|
let out = {
|
||||||
|
track: track,
|
||||||
|
position: (Date.now() - data.ts) + data.pos
|
||||||
|
}
|
||||||
|
//Emit to sockets
|
||||||
|
sockets.forEach((s) => {
|
||||||
|
s.emit('playOffset', out);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
//Start server
|
//Start server
|
||||||
server.on('error', (e) => {
|
server.on('error', (e) => {
|
||||||
|
@ -22,9 +22,12 @@ class Settings {
|
|||||||
this.createAlbumFolder = true;
|
this.createAlbumFolder = true;
|
||||||
this.createArtistFolder = true;
|
this.createArtistFolder = true;
|
||||||
this.downloadFilename = '%0trackNumber%. %artists% - %title%';
|
this.downloadFilename = '%0trackNumber%. %artists% - %title%';
|
||||||
|
this.downloadDialog = true;
|
||||||
|
|
||||||
this.logListen = false;
|
this.logListen = false;
|
||||||
this.lastFM = null;
|
this.lastFM = null;
|
||||||
|
this.enableDiscord = false;
|
||||||
|
this.discordJoin = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Based on electorn app.getPath
|
//Based on electorn app.getPath
|
||||||
|
23
app/src/winston.js
Normal file
23
app/src/winston.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const winston = require('winston');
|
||||||
|
const path = require('path');
|
||||||
|
const {Settings} = require('./settings');
|
||||||
|
const { Transform } = require('stream');
|
||||||
|
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
level: 'info',
|
||||||
|
format: winston.format.simple(),
|
||||||
|
transports: [
|
||||||
|
new winston.transports.Console(),
|
||||||
|
new winston.transports.File({filename: path.join(Settings.getDir(), "freezer-server.log")}),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
//Node errors
|
||||||
|
process.on('uncaughtException', (err) => {
|
||||||
|
logger.error('Unhandled Exception: ' + err + "\nStack: " + err.stack);
|
||||||
|
});
|
||||||
|
process.on('unhandledRejection', (err) => {
|
||||||
|
logger.error('Unhandled Rejection: ' + err + "\nStack: " + err.stack);
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = logger;
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "freezer",
|
"name": "freezer",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"pack": "electron-builder --dir",
|
"pack": "electron-builder --dir",
|
||||||
|
Loading…
Reference in New Issue
Block a user