Compare commits

...

5 Commits

Author SHA1 Message Date
Jill 6e9d4a5d82 switch to flexboxes for album display 2021-10-20 19:38:17 +03:00
Jill 2b5aaee718 some design changes 2021-10-20 19:12:43 +03:00
Jill 611ad91088 better frontend error handling 2021-10-20 18:40:59 +03:00
Jill 93c530e5e7 updated download screen 2021-10-20 17:41:10 +03:00
Jill be169d876e unhardcode websocket server location 2021-10-20 16:25:00 +03:00
6 changed files with 248 additions and 75 deletions

View File

@ -20,9 +20,7 @@ it's intended use is for small groups of people to self-host, and as such there'
3. `npm install`
4. replace all mentions of `deemix.oat.zone` in `public/index.html` with your own domain (and `wss://` with `ws://` if needed)
5. (optionally) put the service on pm2 like such: `pm2 start src/index.js --name deemix-web-frontend` (or just run it with `node src/index.js`)
4. (optionally) put the service on pm2 like such: `pm2 start src/index.js --name deemix-web-frontend` (or just run it with `node src/index.js`)
### nginx addenum

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
viewBox="0 0 512 512"
enable-background="new 0 0 512 512"
id="svg10"
sodipodi:docname="download.svg"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs14" />
<sodipodi:namedview
id="namedview12"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="0.56761894"
inkscape:cx="367.3239"
inkscape:cy="27.307052"
inkscape:window-width="1920"
inkscape:window-height="1025"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg10" />
<g
id="g8"
style="fill:#000000">
<g
id="g6"
style="fill:#000000">
<path
d="M480.6,341.4c-11.3,0-20.4,9.1-20.4,20.4v98.4H51.8v-98.4c0-11.3-9.1-20.4-20.4-20.4c-11.3,0-20.4,9.1-20.4,20.4v118.8 c0,11.3,9.1,20.4,20.4,20.4h449.2c11.3,0,20.4-9.1,20.4-20.4V361.8C501,350.5,491.9,341.4,480.6,341.4z"
id="path2"
style="fill:#000000" />
<path
d="m241,365.6c11.5,11.6 25.6,5.2 29.9,0l117.3-126.2c7.7-8.3 7.2-21.2-1.1-28.9-8.3-7.7-21.2-7.2-28.8,1.1l-81.9,88.1v-265.2c0-11.3-9.1-20.4-20.4-20.4-11.3,0-20.4,9.1-20.4,20.4v265.3l-81.9-88.1c-7.7-8.3-20.6-8.7-28.9-1.1-8.3,7.7-8.7,20.6-1.1,28.9l117.3,126.1z"
id="path4"
style="fill:#000000" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -46,13 +46,14 @@
color:rgb(131, 131, 243);
}
.link:hover {
color: rgb(151, 151, 263);
color: rgb(151, 151, 255);
filter: drop-shadow( 0px 0px 2px #8383F3);
}
.album-download {
filter: invert(100%) hue-rotate(180deg);
filter: invert(100%);
}
.album-download:hover {
filter: invert(100%) sepia(100%) saturate(800%) brightness(70%) hue-rotate(180deg);
filter: invert(50%) sepia(58%) saturate(893%) hue-rotate(206deg) brightness(99%) contrast(92%) drop-shadow( 0px 0px 5px #8383F3);
}
.lds-ring div {
border: 8px solid #fff;
@ -82,6 +83,12 @@
.slider {
background-color: rgb(131, 131, 243);
}
.slider:hover {
filter: drop-shadow( 0px 0px 5px #8383F3);
}
#progress-state {
background-color: #0a0a0f;
}
}
@media (prefers-color-scheme: light) {
body {
@ -129,12 +136,13 @@
}
.link:hover {
color: #f484b6;
filter: drop-shadow( 0px 0px 2px #f484b6);
}
.album-download {
filter: invert(0%) hue-rotate(276deg);
filter: none;
}
.album-download:hover {
filter: invert(100%) sepia(100%) saturate(2000%) brightness(70%) hue-rotate(276deg);
filter: invert(65%) sepia(45%) saturate(772%) hue-rotate(295deg) brightness(103%) contrast(91%) drop-shadow( 0px 0px 5px #f484b6);
}
.lds-ring div {
border: 8px solid #1e1e2d;
@ -164,9 +172,15 @@
.slider {
background-color: #ea74ac;
}
.slider:hover {
filter: drop-shadow( 0px 0px 5px #ea74ac);
}
#git {
filter: invert(100%);
}
#progress-state {
background-color: #fafafa;
}
}
body {
@ -198,6 +212,8 @@ input {
border-radius: 10px 10px 0px 0px;
transition: 0.1s border-left ease-out, 0.1s background-color ease-in-out;
height: 96px;
display: flex;
justify-content: space-between;
}
.small {
font-size: medium;
@ -220,29 +236,34 @@ input {
height: 100%;
border-radius: 10px;
transition: 0.1s border ease-out, 0.1s box-shadow ease-out;
margin: none;
padding: none;
width: 96px;
height: 96px;
}
.album-image-wrapper {
transition: 0.1s border ease-out;
float: right;
max-height: 100%;
width: 96px;
padding: none;
margin: none;
}
.album-metadata {
display: flex;
flex-direction: column;
width: 100%;
}
.metadata {
height: 100%;
}
.link {
cursor: pointer;
transition: 0.1s color ease-out;
transition: 0.1s color ease-out, 0.1s filter ease-out;
}
.album-download {
position: relative;
top: 20px;
width: 32px;
height: 32px;
cursor: pointer;
transition: 0.1s filter ease-out;
}
.track .album-download {
position: relative;
top: 20px;
}
.lds-ring {
display: inline-block;
position: relative;
@ -361,8 +382,8 @@ input {
left: 0;
right: 0;
bottom: 0;
-webkit-transition: .4s;
transition: .4s;
-webkit-transition: .2s;
transition: .2s;
}
.slider:before {
@ -373,8 +394,8 @@ input {
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
-webkit-transition: .2s;
transition: .2s;
}
input:checked + .slider:before {
@ -397,4 +418,33 @@ input:checked + .slider:before {
}
#header-left > * {
margin-right: 16px;
}
#progress-state {
font-family: monospace;
font-size: 12px;
max-height: 50px;
border-radius: 10px;
overflow: auto;
width: 60%;
margin-top: 5px;
padding: 2px;
}
.album-downloading {
border-radius: 10px;
}
.error {
background-color: rgb(255, 155, 155, 0.3);
padding: 20px;
border-radius: 15px;
border: 3px solid rgb(255, 155, 155, 0.8);
text-align: center;
margin: 15px;
width: 400px;
display: none; /* this is changed by the js */
}
.error .big {
font-size: x-large;
}

View File

@ -22,6 +22,7 @@
</div>
</div>
<span id="main">
<div class="error" id="error"></div>
<input type="search" id="album-search" name="q">
<div id="progress"><div id="progress-album"></div><div id="progress-bar-wrapper"></div></div>
<div id="albums"></div>

View File

@ -28,14 +28,84 @@ function setTheme(theme) {
if (e.constructor != CSSMediaRule) return;
if (e.originalConditionText) e.conditionText = e.originalConditionText;
else e.originalConditionText = e.conditionText
if (theme == "system") return
if (theme === 'system') return
let match = e.conditionText.match(/prefers-color-scheme:\s*(light|dark)/i)
if (!match) return;
e.conditionText = e.conditionText.replace(match[0], (match[1].toLowerCase() == theme ? 'min' : 'max') + '-width: 0')
});
}
function getWebsocketLocation() {
return window.window.location.toString().replace('https://', 'wss://').replace('http://', 'ws://');
}
function addlog(log, text) {
log += `<br>${text}`;
log = log.split('<br>').slice(-3).join('<br>');
if (log.startsWith('<br>')) log = log.replace('<br>', '');
return log;
}
function startDownload(id, isAlbum) {
let log = '';
let coverArt;
let title;
let artist;
let type = isAlbum ? 'album' : 'track'
document.getElementById('albums').innerHTML = '';
document.getElementById('progress-album').innerHTML = '<div class="lds-ring"><div></div><div></div><div></div><div></div></div>';
const ws = new WebSocket(`${getWebsocketLocation()}api/${type}?id=${id}`);
ws.onmessage = (m) => {
const d = JSON.parse(m.data);
console.log(d);
if (d.key === 'downloadInfo') {
log = addlog(log, `[${d.data.data.title}] ${d.data.state}`);
} else if (d.key === 'updateQueue') {
if (d.data.progress) {
document.getElementById('progress-bar-wrapper').innerHTML = `<br><div id="progress-bar"><div id="progress-bar-inner" style="height:100%;width:${d.data.progress}%"></div></div>`
}
} else if (d.key === 'coverArt') {
log = addlog(log, 'Fetched cover art');
coverArt = d.data;
} else if (d.key === 'metadata') {
log = addlog(log, 'Fetched metadata');
title = d.data.title;
artist = d.data.artist;
} else if (d.key === 'download') {
download(d.data);
} else if (d.key === 'finishDownload') {
log = addlog(log, 'Download finished');
}
document.getElementById('progress-album').innerHTML = `<div class="album album-downloading" id="album-${id}"><span class="album-image-wrapper"><img class="album-image" width="128" height="128" src="${coverArt}"></span><span class="big">${title || ''}</span><br><span class="small">by ${artist || ''}</span><br><div id="progress-state">${log || ''}</div></div>`;
}
ws.onerror = (e) => {
console.log('error: ' + e);
error(e.toString());
}
ws.onclose = (e) => {
change();
if (e.code !== 1000) error(`websocket closed unexpectedly with code ${e.code}\n${e.reason}`);
}
}
function error(e) {
document.getElementById('error').innerHTML = `<div class="big">error!</div>${e.split('\n').join('<br>')}`;
document.getElementById('error').style.display = 'block';
console.error(e);
}
function clearError() {
document.getElementById('error').innerHTML = '';
document.getElementById('error').style.display = 'none';
}
let change; // fuck off js
window.onload = () => {
clearError();
// dirty theme hacks :tm:
const color = window.getComputedStyle(document.querySelector('body')).getPropertyValue('color');
@ -68,15 +138,39 @@ window.onload = () => {
const search = document.getElementById('album-search');
search.setAttribute('placeholder', placeholders[Math.floor(Math.random() * placeholders.length)]);
async function change() {
change = async () => {
clearError();
const value = document.getElementById('album-search').value;
if (value === '') return document.getElementById('albums').innerHTML = '';
document.getElementById('progress-album').innerHTML = '';
document.getElementById('progress-bar-wrapper').innerHTML = '';
document.getElementById('albums').innerHTML = '<div class="lds-ring"><div></div><div></div><div></div><div></div></div>';
const d = await axios.get('/api/search', {params: {search: value}});
let d;
try {
d = await axios.get('/api/search', {params: {search: value}});
} catch(err) {
error(err.toString());
document.getElementById('albums').innerHTML = '';
return;
}
document.getElementById('albums').innerHTML = d.data.map(d =>
`<div class="album" id="album-${d.id}"><span class="album-image-wrapper"><img class="album-image" width="128" height="128" src="https://e-cdns-images.dzcdn.net/images/cover/${d.cover}/128x128-000000-80-0-0.jpg"></span><span class="big">${d.title}</span><br><span class="small">by ${d.artist.name}</span><br><img class="album-download" width="48" height="48" src="https://img.icons8.com/material-sharp/48/000000/download--v1.png"></div><div class="album-bottom" id="album-bottom-${d.id}"></div>`
`
<div class="album" id="album-${d.id}">
<div class="album-metadata">
<span class="metadata">
<span class="big">${d.title}</span>
<br>
<span class="small">by ${d.artist.name}</span>
</span>
<img class="album-download" width="48" height="48" src="assets/download.svg">
</div>
<div class="album-image-wrapper">
<img class="album-image" width="128" height="128" src="https://e-cdns-images.dzcdn.net/images/cover/${d.cover}/128x128-000000-80-0-0.jpg">
</div>
</div>
<div class="album-bottom" id="album-bottom-${d.id}"></div>
`
).join('<br>');
if (d.data.length === 0) return document.getElementById('albums').innerHTML = '<span class="small">Not found!</span>';
@ -84,62 +178,41 @@ window.onload = () => {
for (c of document.getElementById('albums').children) {
if (c.children[5]) {
let id = c.id.split('-')[1];
c.children[5].onclick = (a) => {
let coverArt
document.getElementById('albums').innerHTML = '';
document.getElementById('progress-album').innerHTML = '<div class="lds-ring"><div></div><div></div><div></div><div></div></div>';
const ws = new WebSocket('wss://deemix.oat.zone/api/album?id=' + id);
ws.onmessage = (m) => {
const d = JSON.parse(m.data);
if (d.key === 'downloadInfo') {
document.getElementById('progress-album').innerHTML = `<div class="album" id="album-${d.data.data.id}"><span class="album-image-wrapper"><img class="album-image" width="128" height="128" src="${coverArt}"></span><span class="big">${d.data.data.title}</span><br><span class="small">by ${d.data.data.artist}</span><br><span class="small" id="progress-state">${d.data.state}</span></div>`;
} else if (d.key === 'updateQueue') {
if (d.data.progress) {
document.getElementById('progress-bar-wrapper').innerHTML = `<br><div id="progress-bar"><div id="progress-bar-inner" style="height:100%;width:${d.data.progress}%"></div></div>`
}
} else if (d.key === 'coverArt') {
coverArt = d.data;
} else if (d.key === 'download') {
download(d.data);
}
}
c.children[5].onclick = () => {
clearError();
startDownload(id, true);
}
}
let id = c.id.split('-')[1];
if (document.getElementById('album-bottom-' + id)) {
document.getElementById('album-bottom-' + id).innerHTML = '<div class="lds-ring"><div></div><div></div><div></div><div></div></div>';
const album = await axios.get('/api/album', {params: {id: id}});
let album;
try {
album = await axios.get('/api/album', {params: {id: id}});
} catch(err) {
error(err.toString());
document.getElementById('album-bottom-' + id).innerHTML = '';
return;
}
document.getElementById('album-bottom-' + id).innerHTML = album.data.tracks.map(d =>
`<div class="track" id="track-${d.id}"><span>${d.artist} - ${d.title}</span><span><span class="track-download-wrapper"><img class="album-download" width="32" height="32" src="https://img.icons8.com/material-sharp/48/000000/download--v1.png"></span> ${formatTime(d.duration)}</span></div>`
`
<div class="track" id="track-${d.id}">
<span>${d.artist} - ${d.title}</span>
<span>
<span class="track-download-wrapper">
<img class="album-download" width="32" height="32" src="assets/download.svg">
</span>
${formatTime(d.duration)}
</span>
</div>
`
).join('');
for (track of document.getElementById('album-bottom-' + id).children) {
let trackId = track.id.split('-')[1];
track.children[1].children[0].onclick = () => {
console.log(trackId);
let coverArt
document.getElementById('albums').innerHTML = '';
document.getElementById('progress-album').innerHTML = '<div class="lds-ring"><div></div><div></div><div></div><div></div></div>';
const ws = new WebSocket('wss://deemix.oat.zone/api/track?id=' + trackId);
ws.onmessage = (m) => {
const d = JSON.parse(m.data);
console.log(d);
if (d.key === 'downloadInfo') {
document.getElementById('progress-album').innerHTML = `<div class="album" id="album-${d.data.data.id}"><span class="album-image-wrapper"><img class="album-image" width="128" height="128" src="${coverArt}"></span><span class="big">${d.data.data.title}</span><br><span class="small">by ${d.data.data.artist}</span><br><span class="small" id="progress-state">${d.data.state}</span></div>`;
} else if (d.key === 'updateQueue') {
if (d.data.progress) {
document.getElementById('progress-bar-wrapper').innerHTML = `<br><div id="progress-bar"><div id="progress-bar-inner" style="height:100%;width:${d.data.progress}%"></div></div>`
}
} else if (d.key === 'coverArt') {
coverArt = d.data;
} else if (d.key === 'download') {
download(d.data);
} else if (d.key === 'finishDownload') {
change();
}
}
clearError();
startDownload(trackId, false);
}
}
}

View File

@ -97,7 +97,7 @@ app.ws('/api/album', async (ws, req) => {
}, 1000 * 60 * 60 /* 1 hour */);
}
ws.send(JSON.stringify({key, data}));
if (data.state !== 'tagging' && data.state !== 'getAlbumArt' && data.state !== 'getTags') ws.send(JSON.stringify({key, data}));
//console.log(`[${key}] ${inspect(data)}`);
}
};
@ -110,6 +110,7 @@ app.ws('/api/album', async (ws, req) => {
}
listener.send('coverArt', album.cover_medium);
listener.send('metadata', {id: album.id, title: album.title, artist: album.artist.name});
let dlObj = await deemix.generateDownloadObject(deezerInstance, 'https://www.deezer.com/album/' + req.query.id, deezer.TrackFormats.FLAC);
deemixDownloader = new deemix.downloader.Downloader(deezerInstance, dlObj, deemixSettings, listener);
@ -135,8 +136,8 @@ app.ws('/api/track', async (ws, req) => {
}, 1000 * 60 * 60 /* 1 hour */);
}
ws.send(JSON.stringify({key, data}));
console.log(`[${key}] ${inspect(data)}`);
if (data.state !== 'tagging' && data.state !== 'getAlbumArt' && data.state !== 'getTags') ws.send(JSON.stringify({key, data}));
//console.log(`[${key}] ${inspect(data)}`);
}
};
@ -148,6 +149,7 @@ app.ws('/api/track', async (ws, req) => {
}
listener.send('coverArt', track.album.cover_medium);
listener.send('metadata', {id: track.id, title: track.title, artist: track.artist.name});
let dlObj = await deemix.generateDownloadObject(deezerInstance, 'https://www.deezer.com/track/' + req.query.id, deezer.TrackFormats.FLAC);
deemixDownloader = new deemix.downloader.Downloader(deezerInstance, dlObj, deemixSettings, listener);