-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
438 lines (363 loc) · 16.2 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
//! komentar
//? Komentar
// TODO komentar
import { check_env } from './lib/check_env.js';
import { create_dirs } from './lib/create_dirs.js';
check_env(); // Preverjamo, ali imamo nastavljene vse env spremenljivke
// Importamo module
import 'dotenv/config' // ENV
import express from 'express'; // Spletni strežnik
import { Issuer, Strategy } from 'openid-client'; // OpenID knjižnica
import passport from 'passport'; // AUTH
import expressSession from 'express-session'; // Session
import multer from 'multer'; // Middelware za datoteke
import path from 'path'; // Procesiranje poti za datoteke
import fs from 'fs'; // Delo z datotekami
import { readdir } from 'fs/promises';
import WebRadio from 'express-web-radio'; // Streaming zvoka (radio del)
import axios from 'axios'; // HTTP requests
const uploadDir = 'uploads';
const playingDir = 'playing';
// Kreiramo aplikacijo
const app = express();
// Nastavimo EJS za naš view engine
app.set('view engine', 'ejs');
create_dirs(); // Kličemo datoteko z kodo, ki po potrebi ustvari nove mape
// Funkcija, ki pomaga pri nalaganju in preimenovanju datoteke
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, uploadDir);
},
filename: async function (req, file, cb) {
const fileExtension = path.extname(file.originalname).toLowerCase();
if (fileExtension !== '.mp3') {
return cb(('Samo .mp3 datoteke so dovoljene!'), null);
}
const artist = req.body.artist;
const songName = req.body.songName;
if (artist == undefined || artist == "" || songName == undefined || songName == ""){
return cb(('Manjka ime avtorja in/ali ime pesmi.'), null);
}
const fileName = `${artist} - ${songName}${fileExtension}`;
// Preveri, ali datoteka s tem imenom že obstaja
try {
// Datoteka že obstaja
await fs.promises.access(path.join(uploadDir, fileName));
return cb(('Datoteka s tem imenom že obstaja! Izberite drugo ime.'), null);
} catch (err) {
// Datoteka ne obstaja, nadaljuj s trenutnim imenom
cb(null, fileName);
}
}
});
// Dodamo še dodatno preverjanje imen avtorja in pesmi z regexom
const upload = multer({
storage: storage,
fileFilter: function (req, file, cb) {
const validFileName = /^[^\/\'\-]*$/; // Regex za preverjanje, da v imenu ni znaka "-"
// Preverimo, ali ime avtorja in pesmi vsebujeta znak "-"
if (!validFileName.test(req.body.artist)) {
return cb('Ime izvajalca ne sme vsebovati znaka "-" ali znaka "/" ali znaka "\'". Prosimo, izberite drugo ime.', false);
}
if (!validFileName.test(req.body.songName)) {
return cb('Ime skladbe ne sme vsebovati znaka "-" ali znaka "/" ali znaka "\'". Prosimo, izberite drugo ime.', false);
}
cb(null, true);
}
});
// Naredimo povezavo na naš OpenID strežnik, ki jo bodo uporabljali uporabniki za prijavo
const keycloakIssuer = await Issuer.discover(process.env.keycloakIssuer)
const client = new keycloakIssuer.Client({
client_id: process.env.client_id,
client_secret: process.env.client_secret,
redirect_uris: [process.env.redirect_uris],
post_logout_redirect_uris: [process.env.post_logout_redirect_uris],
response_types: ['code'],
});
// Session
var memoryStore = new expressSession.MemoryStore();
app.use(
expressSession({
secret: process.env.cookie_secret,
resave: false,
saveUninitialized: true,
store: memoryStore
})
);
app.use(passport.initialize());
app.use(passport.authenticate('session'));
// Strategija za avtentikacijo
passport.use('oidc', new Strategy({client}, (tokenSet, done)=>{
return done(null, tokenSet.claims());
}))
// Serializacija uporabnika (prijava)
passport.serializeUser(function(user, done) {
done(null, user);
});
// Deserializacija uporabnika (odjava)
passport.deserializeUser(function(user, done) {
done(null, user);
});
// Preusmeri na openID prijavno stran
app.get('/login', passport.authenticate('oidc'));
// Vračilo klica z openID prijavne spletne strani
app.get('/auth/callback', (req, res, next) => {
passport.authenticate('oidc', {
successRedirect: '/',
failureRedirect: '/'
})(req, res, next);
});
// Funkcija bo preverjala ali je uporabnik prijavljen (middleware)
var checkAuthenticated = (req, res, next) => {
if (req.isAuthenticated()) {
return next();
}
res.redirect("/login");
}
// Domača spletna stran
app.get('/',function(req, res){
res.render('index',{ user: req.user });
});
// Funkcija za odjavo
app.get('/logout', (req, res) => {
res.redirect(client.endSessionUrl());
});
// Povratna funkcija za odjavo
app.get('/logout/callback', (req, res) => {
// Poskrbi, da še aplikacija odjavi uporabnika
req.logout((err) => {
if (err) {
console.error(cas, "Napaka pri odjavi uporabnika: ",err);
return next(err);
}
// Preusmerimo uporabnika na domačo stran
res.redirect('/');
});
});
// Pot, kjer uporabnik pride do nastavitev svojega računa
app.get('/settings', (req, res) => {
// Pridobimo z .env
const settings = process.env.keycloakIssuer + '/account/';
res.redirect(settings);
});
// Pot za statične datoteke (ikone, slike, CSS, JS, ...)
app.use('/',express.static('public'));
// Pot za predogled glasbe (knjižnica)
app.use('/music', checkAuthenticated ,express.static('uploads'));
// Pot za nalaganje glasbe (GET)
app.get('/upload', checkAuthenticated, (req, res) => {
res.render('upload', { user: req.user, message:'', errorMessage: '' });
});
// Pot za nalaganje glasbe (POST)
app.post('/upload', checkAuthenticated, upload.single('musicFile'), async(req, res) => {
try{
// Preverimo, če je dejansko datoteka podana
if (!req.file) {
console.error(cas(), "Uporabnik ni podal datoteke pri nalaganju! ","(", req.user.name, ")");
return res.status(400).json({ error: 'Datoteka ni bila podana!' });
}
// Preveri, ali je uporabnik vpisal ime skladbe
const songName = req.body.songName;
if (!songName) {
console.error(cas(), "Uporabnik ni vpisal ime skladbe pri nalaganju nove pesmi! ","(", req.user.name, ")");
return res.status(400).json({ error: 'Vpišite ime skladbe!' });
}
// Preveri, ali je uporabnik vpisal izvajatelja
const artist = req.body.artist;
if (!artist) {
console.error(cas(), "Uporabnik ni vpisal izvajatelja pri nalaganju nove pesmi! ","(", req.user.name, ")");
return res.status(400).json({ error: 'Vpišite izvajatelja!' });
}
// Preverimo datoteko
if (req.fileValidationError) {
console.error(cas(), "Napaka pri validaciji novo naložene glasbe! ","(", req.user.name, ")");
return res.status(400).json({ error: 'Napaka pri preverjanju datoteke!' });
}
console.log(cas(), "Pesem je bila uspešno naložna: ", songName, "(", req.user.name, ")");
res.render('upload', { user: req.user, message: 'Pesem je bila uspešno naložena', errorMessage: '' });
} catch (err){
console.error(cas(), "Napaka strežnika /upload (post): ",songName, "(", req.user.name, ")");
res.status(500).render('upload', { user: req.user, errorMessage: 'Napaka! Prišlo je do napake na strežniški strani!.', message: '' });
}
});
// Pot za pregledovanje in upravljanje glasbe (GET)
app.get('/library', checkAuthenticated, async (req, res) => {
try {
const [libraryFiles, playingFiles] = await Promise.all([readdir(uploadDir), readdir(playingDir)]); // Asinhrono branje, da lahko hkrati beremo z obeh map
const librarySongs = libraryFiles
.filter(file => file.endsWith('.mp3'))
.map(file => ({
name: file,
isInPlaying: playingFiles.includes(file),
}));
res.render('library', { songs: librarySongs, user: req.user, errorMessage: null});
} catch (err) {
console.error(cas(), "Napaka strežnika /library (get): ",songName, "(", req.user.name, ")");
res.status(500).render('library', { user: req.user, errorMessage: 'Napaka! Prišlo je do napake na strežniški strani!.' });
}
});
// Dodajmo končno točko za izbris pesmi (DELETE)
app.delete('/delete-song/:songName', checkAuthenticated, async (req, res) => {
try {
// Dekodirami ime pesmi, ki bi jo radi izbrisali
const songName = decodeURIComponent(req.params.songName);
// Sestavimo pot do datoteke, ki bi jo radi izbrisali
const filePath = path.join(uploadDir, songName);
// Preveri, ali datoteka obstaja
await fs.promises.access(filePath);
// Pridobi ime trenutne pesmi
const request_url = `${process.env.protocol}://${process.env.domain}:${process.env.port}/current-song`;
const currentSongResponse = await axios.get(request_url);
const { song: currentSong } = currentSongResponse.data;
// Preveri, ali se trenutno predvaja ta pesem
if (currentSong === parseSongDetails(songName).song || currentSong === 'Nobena pesem se ne predvaja trenutno.') {
res.status(500).json({ error: 'Ne moreš izbrisati pesmi, ki se trenutno predvaja.' });
} else{
// Izbriši datoteko
await fs.promises.unlink(filePath);
}
// Pridobim imena pesmi za izpis
const libraryFiles = await readdir(uploadDir);
const playingFiles = await readdir(playingDir);
const librarySongs = libraryFiles
.filter(file => file.endsWith('.mp3'))
.map(file => ({
name: file,
isInPlaying: playingFiles.includes(file),
}));
// Posreduj sporočilo o uspehu in renderaj stran
console.error(cas(), "Uspešno izbrisana pesem: ",songName, "(", req.user.name, ")");
res.status(200).render('library', { songs: librarySongs, user: req.user, successMessage: 'Pesem uspešno izbrisana.' });
} catch (err) {
// Preverimo, ali datoteka obstaja
if (err.code === 'ENOENT') {
// Izpis v primeru, da pesem ne obstaja
console.error(cas(), "Datoteka ne obstaja: ",songName, "(", req.user.name, ")");
res.status(400).render('library', { user: req.user, errorMessage: 'Datoteka ne obstaja.' });
} else {
// Izpis v primeru, da pesem obstaja
console.error(cas(), "Napaka pri brisanju pesmi: ",songName, " | ", err, "(", req.user.name, ")");
res.status(500).render('library', { user: req.user, errorMessage: 'Napaka pri brisanju pesmi.' });
}
}
});
// Dodajanje glasb na seznam predvajanja
app.post('/add-to-playlist/:songName', checkAuthenticated, (req, res) => {
// Dekodiramo ime pesmi, ki bi jo radi dodali na seznam predvajanja
const songName = decodeURIComponent(req.params.songName);
// Preveri, ali datoteka obstaja v mapi uploads
const sourceFilePath = path.join(process.cwd(), 'uploads', songName);
// Preverimo, ali pesem ki jo želimo dodati na seznam predvajanja obstaja
if (!fs.existsSync(sourceFilePath)) {
return res.status(400).json({ error: 'Pesem ne obstaja.' });
}
// Določimo ciljni direktorij za kopiranje datoteke
const destinationDirectory = path.join(process.cwd(), 'playing');
const destinationFilePath = path.join(destinationDirectory, songName);
// Preveri, ali datoteka že obstaja v mapi playing
if (fs.existsSync(destinationFilePath)) {
return res.status(400).json({ error: 'Pesem že obstaja v predvajalniku.' });
}
// Kopiraj datoteko v mapo playing
fs.copyFileSync(sourceFilePath, destinationFilePath);
console.error(cas(), "Pesem je bila dodana na seznam predvajanja: ",songName, "(", req.user.name, ")");
res.json({ message: 'Pesem je bila uspešno dodana v predvajalnik.' });
});
// Pot za odstranjevanje pesmi z seznama predvajanja
app.post('/remove-from-playlist/:songName', checkAuthenticated, async (req, res) => {
let songName;
try {
// Dekodiramo ime pesmi, ki bi jo radi dodali na seznam predvajanja
songName = decodeURIComponent(req.params.songName);
// Sestavimo pot do pesmi
const filePath = path.join(playingDir, songName);
// Preveri, ali datoteka obstaja
await fs.promises.access(filePath);
// Pridobi ime trenutne pesmi
const request_url = `${process.env.protocol}://${process.env.domain}:${process.env.port}/current-song`;
const currentSongResponse = await axios.get(request_url);
const { song: currentSong } = currentSongResponse.data;
// Preveri, ali se trenutno predvaja ta pesem
if (currentSong === parseSongDetails(songName).song || currentSong === 'Nobena pesem se ne predvaja trenutno.') {
return res.status(500).json({ error: 'Ne moreš izbrisati pesmi, ki se trenutno predvaja.' });
} else {
// Izbriši datoteko
await fs.promises.unlink(filePath);
}
console.error(cas(), "Pesem je bila uspešno izbrisana s seznama predvajanja: ", songName, "(", req.user.name, ")");
res.json({ message: 'Pesem uspešno izbrisana.' });
} catch (err) {
console.error(err);
if (err.code === 'ENOENT') {
res.status(400).json({ error: 'Datoteka ne obstaja.' });
} else {
console.error(cas(), "Napaka pri brisanju pesmi z seznama predvajanja: ", songName, " | " ,err, "(", req.user.name, ")");
res.status(500).json({ error: 'Napaka pri brisanju pesmi.' });
}
}
});
// Funkcija namenjena parsanju imena ustvarjalca in pesmi z imena pesmi
function parseSongDetails(fileName) {
// Format skladb bi moral biti "Izvajalec - Skladba.mp3"
const [artist, composerWithExtension] = fileName.split(" - ");
const song = composerWithExtension.replace(".mp3", "");
return { artist, song };
}
// Definiranje funkcije z nastavitvami za pretakanje glasbe
const radio = new WebRadio({
audioDirectory: "./playing", // Mapa katera služi kot seznam predvajanja
loop: true, // Neskončno ponavljanje glasbe
shuffle: true, // Naključen izbor glasbe
logFn: (msg) => {
// Izpis glasbe, ki se trenutno predvaja ter parsanje imena izvajalca ter pesmi
console.log(cas(),`[Radio]: ${msg}`);
// Parsanje podatkov o pesmi, ki se trenutno predvaja (hvala knjižnjici, ker ne podpira tega :( ))
const match = msg.match(/Now Playing: (.+) \| Bitrate: ([\d\.]+)kbps/);
//console.log (match);
//console.log(msg);
if (match && match.length === 3) {
const songDetails = parseSongDetails(match[1]);
radio.currentSong = {
artist: songDetails.artist,
song: songDetails.song,
bitrate: parseInt(match[2]),
};
}
},
});
// Zaženemo radio
radio.start();
// Link, kjer bomo pretakali glabno
app.get("/stream", radio.connect());
// Spletna stran, kjer bo predvajalnik za radio
app.get('/radio', (req,res) => {
res.render('radio');
});
// Dodaj novo pot za pridobivanje trenutne pesmi
app.get('/current-song', (req,res) => {
// Pridobimpo podatke z našega parserja
const { artist, song } = radio.currentSong || {};
//console.log(cas(), "Trenutno predvaja pesem: ", artist, " - ", song);
// Preverimo, ali je uspešno prepoznal izvajalca in pesem
if (artist !== undefined && song !== undefined) {
res.json({ artist, song });
} else {
res.status(500).json({ error: "Nobena pesem se ne predvaja trenutno." });
}
});
// Pot za strani, ki ne obstajajo (HTTP 404)
app.get('*', (req, res) => {
console.error(cas(), '[Dostop] Nekdo je neuspšno poizkušal dostopati do: ', req.originalUrl);
res.status(404).send('Ne obstaja');
});
// Povemo aplikacije kje naj deluje
app.listen(process.env.port, function () {
console.log(cas(), `Aplikacija deluje na ${process.env.protocol}://${process.env.domain}:${process.env.port}`);
});
// Funkcija za generiranje datuma in časa (izpis v terminal)
function cas() {
const now = new Date();
const date = now.toISOString().slice(0, 10);
const time = now.toTimeString().slice(0, 8);
return `${date} ${time}`;
}