Compare commits
5 Commits
87b3934356
...
6e9d4a5d82
Author | SHA1 | Date |
---|---|---|
Jill | 6e9d4a5d82 | |
Jill | 2b5aaee718 | |
Jill | 611ad91088 | |
Jill | 93c530e5e7 | |
Jill | be169d876e |
|
@ -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
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
|
|
173
public/index.js
173
public/index.js
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue