commit 28732c0ea3e167a6dfafa16f233839c1a5487cd8 Author: Evert Prants Date: Fri Oct 5 13:36:57 2018 +0300 Initial Commit diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..1e78999 --- /dev/null +++ b/.babelrc @@ -0,0 +1,13 @@ +{ + "presets": [ + "env", + "react", + "stage-0" + ], + "plugins": [ + "transform-class-properties", + "transform-decorators", + "transform-react-constant-elements", + "transform-react-inline-elements" + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e96a533 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.db +*.bak +values.json +/node_modules/ +/package-lock.json diff --git a/dbpopulate.js b/dbpopulate.js new file mode 100755 index 0000000..519b156 --- /dev/null +++ b/dbpopulate.js @@ -0,0 +1,258 @@ +#!/usr/bin/env babel-node +import path from 'path' +import sqlite from 'sqlite' +import Promise from 'bluebird' +import fs from 'fs-extra' +import {exec} from 'child_process' + +const values = require(path.join(__dirname, 'values.json')) +const musicdir = path.resolve(values.directory) + +const dbPromise = Promise.resolve() + .then(() => sqlite.open(path.join(__dirname, values.database), { Promise, cache: true })) + .then(db => db.migrate()) + +const readline = require('readline') + +// ffprobe -i -show_entries format=duration -v quiet -of csv="p=0" + +function filewalker (dir, done) { + let results = [] + + fs.readdir(dir, function (err, list) { + if (err) return done(err) + + var pending = list.length + + if (!pending) return done(null, results) + + list.forEach(function (file) { + file = path.resolve(dir, file) + + fs.stat(file, function (err, stat) { + if (err) return done(err) + + // If directory, execute a recursive call + if (stat && stat.isDirectory()) { + // Add directory to array [comment if you need to remove the directories from the array] + results.push(file) + + filewalker(file, function (err, res) { + if (err) return done(err) + results = results.concat(res) + if (!--pending) done(null, results) + }) + } else { + results.push(file) + + if (!--pending) done(null, results) + } + }) + }) + }) +} + +function getFiles (dir) { + return new Promise((resolve, reject) => { + filewalker(dir, (err, files) => { + if (err) return reject(err) + resolve(files) + }) + }) +} + +function promiseExec (cmd, opts) { + return new Promise((resolve, reject) => { + exec(cmd, opts, function (err, stdout, stderr) { + if (err) return reject(err) + resolve({stdout, stderr}) + }) + }) +} + +async function getInfos (file) { + let id3 = await promiseExec(`id3 "${file}"`) + let prds = id3.stdout.split('\n') + let data = {} + + // Get id3 tags + for (let i in prds) { + let line = prds[i] + let parts = line.split(': ') + if (parts.length) { + let tagtype = parts[0].toLowerCase() + let tagdata = line.substring(parts[0].length + 2) + + if (tagtype === '') continue + if (tagtype === 'metadata' && tagdata === 'none found') throw new Error(`No metadata for file "${file}"!`) + if (tagtype === 'track' || tagtype === 'year') { + if (tagdata.indexOf('/') !== -1) { + tagdata = tagdata.split('/')[0] + } + + tagdata = parseInt(tagdata) + } + + data[tagtype] = tagdata + } + } + + if (!data.title) { + let parsed = path.parse(file) + data.title = parsed.name + } + + // Get track length + let len = await promiseExec(`ffprobe -i "${file}" -show_entries format=duration -v quiet -of csv="p=0"`) + len = parseFloat(len.stdout) + data.duration = len + + return data +} + +function parseTitle(data) { + let tt = data.title + + // Remove []'s from the beginning + tt = tt.replace(/^\[\w+\]\s?/i, '') + + // Remove "Official Video/Audio" tag + tt = tt.replace(/\s?(?:[\(\)\[\]])?Official\s?[\w]+(?:[\(\)\[\]])?/i, '') + + // Remove "Audio" tag + tt = tt.replace(/\s?(?:[\(\)\[\]])Audio?(?:[\(\)\[\]])/i, '') + + // Remove "lyrics" tag + tt = tt.replace(/\s?(?:[\(\)\[\]])?lyrics?\s?(?:[\w]+)?(?:[\(\)\[\]])?\s?/i, '') + + // Artist / Title split + let at = tt.split(' - ', 2) + let artist + let title + + if (at.length > 1) { + artist = at[0] + title = tt.substring(artist.length + 3) + } else { + artist = data.artist + title = tt + } + + data.title = title + data.artist = artist + + return data +} + +function askAsync (rl, q) { + return new Promise((resolve, reject) => { + rl.question(q, resolve) + }) +} + +async function interactive (fpath, db) { + let rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }) + + console.log('No metadata found for specified file! Interactive mode enabled.\n') + + let pt = path.parse(fpath) + let track = { + file: fpath, + title: pt.name + } + let clean = parseTitle(track) + + console.log('Determined Title: ' + clean.title) + console.log('Determined Artist: ' + clean.artist) + + let newTitle = await askAsync(rl, `Title [${clean.title}] ? `) + let newArtist = await askAsync(rl, `Artist [${clean.artist}] ? `) + + if (newTitle.trim() !== '') + track.title = newTitle + + if (newArtist.trim() !== '') + track.artist = newArtist + + let ensure = await db.get('SELECT * FROM Track WHERE title=? AND artist=?', track.title, track.artist) + if (ensure) { + console.error('A track of this description already exists in the database.') + return rl.close() + } + + let len = await promiseExec(`ffprobe -i "${track.file}" -show_entries format=duration -v quiet -of csv="p=0"`) + track.duration = parseFloat(len) + + rl.close() + + return track +} + +async function run () { + let db = await dbPromise + + if (process.argv[2] != null) { + let filePath = path.resolve(process.argv[2]) + let trackinf + + try { + trackinf = await getInfos(filePath) + } catch (e) { + trackinf = await interactive(filePath, db) + } + + if (!trackinf) { + console.error('Nothing to do.') + return + } + + await db.run('INSERT INTO Track VALUES (NULL,?,?,?,?,?,?,?,?)', + [trackinf.title, trackinf.artist, trackinf.file, trackinf.album || null, trackinf.genre || null, trackinf.track || null, + trackinf.year || null, Math.floor(trackinf.duration)]) + + console.log('Done.') + return + } + + let files = await getFiles(musicdir) + let cleanTrackData = [] + let skips = 0 + + for (let i in files) { + let file = files[i] + // if (cleanTrackData.length > 10) break // debug purposes + process.stdout.write(`\rProcessing file ${parseInt(i) + 1} of ${files.length}.. (${skips} skipped)`) + try { + let fd = await getInfos(file) + cleanTrackData.push(fd) + } catch (e) { + skips++ + continue + } + } + process.stdout.write(`\r${cleanTrackData.length} files indexed, ${skips} files were skipped. \n`) + let entries = 0 + + for (let i in cleanTrackData) { + let track = cleanTrackData[i] + process.stdout.write(`\rPopulating database.. (Track ${parseInt(i) + 1} of ${cleanTrackData.length})`) + try { + let ensure = await db.get('SELECT * FROM Track WHERE title=? AND artist=?', track.title, track.artist) + if (ensure) continue + await db.run('INSERT INTO Track VALUES (NULL,?,?,?,?,?,?,?,?)', + [track.title, track.artist, track.file, track.album || null, track.genre || null, track.track || null, + track.year || null, Math.floor(track.duration)]) + entries++ + } catch (e) { + console.warn(e.message) + continue + } + } + + console.log(`\n${entries} tracks were successfully added to the cache!`) +} + +run() diff --git a/download.js b/download.js new file mode 100755 index 0000000..addd6f2 --- /dev/null +++ b/download.js @@ -0,0 +1,134 @@ +#!/usr/bin/env node +'use strict' + +const spawn = require('child_process').spawn +const fs = require('fs') + +const readline = require('readline') + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}) + +function download (arg, handleCb) { + let yt = spawn('youtube-dl', ['--no-playlist', '--playlist-end', 1, '-j', '-f', 'bestaudio/best', arg]) + + let output = '' + + yt.stdout.on('data', function (chunk) { + output += chunk.toString('utf8') + }) + yt.on('close', function () { + let data = JSON.parse(output) + delete data.formats + fetchVideo(data, handleCb) + }) +} + +function fetchVideo (data, cb) { + console.log('audio codec:', data.acodec) + if (data.acodec !== 'mp3' || data.vcodec !== 'none') { + let tempName = __dirname + '/tmp.yt.' + data.id + '.mp3' + let ffmpeg = spawn('ffmpeg', ['-hide_banner', '-i', data.url, '-codec:a', 'libmp3lame', '-q:a', 2, '-joint_stereo', 1, '-y', tempName]) + + ffmpeg.stdout.pipe(process.stderr) + ffmpeg.stderr.pipe(process.stderr) + data.filename = tempName + console.log('Downloading ' + data.title + '...') + + ffmpeg.on('close', function () { + outputVideo(data, cb) + }) + } else { + console.log('Invalid format returned.') + cb(null) + } +} + +function outputVideo (video, cb) { + cb({ + title: video.title, + artist: video.uploader, + url: video.webpage_url, + art: video.thumbnail, + source: video.filename + }) +} + +function parseTitle(data) { + let tt = data.title + + // Remove []'s from the beginning + tt = tt.replace(/^\[\w+\]\s?/i, '') + + // Remove "Official Video/Audio" tag + tt = tt.replace(/\s?(?:[\(\)\[\]])?Official\s?[\w]+(?:[\(\)\[\]])?/i, '') + + // Remove "Audio" tag + tt = tt.replace(/\s?(?:[\(\)\[\]])Audio?(?:[\(\)\[\]])/i, '') + + // Remove "lyrics" tag + tt = tt.replace(/\s?(?:[\(\)\[\]])?lyrics?\s?(?:[\w]+)?(?:[\(\)\[\]])?\s?/i, '') + + // Artist / Title split + let at = tt.split(' - ', 2) + let artist + let title + + if (at.length > 1) { + artist = at[0] + title = tt.substring(artist.length + 3) + } else { + artist = data.artist + title = tt + } + + data.title = title + data.artist = artist + + return data +} + +download(process.argv[2], function (s) { + let clean = parseTitle(s) + let filename = clean.artist + ' - ' + clean.title + '.mp3' + + console.log('Cleaning up..') + console.log('Original Title: ' + s.title + '\n') + console.log('Determined Title: ' + clean.title) + console.log('Determined Artist: ' + clean.artist) + console.log('Determined File Name: ' + filename) + + rl.question(`Title [${clean.title}] ? `, (answer) => { + if (answer && answer.trim() !== '') { + clean.title = answer + } + + rl.question(`Artist [${clean.artist}] ? `, (answer) => { + if (answer && answer.trim() !== '') { + clean.artist = answer + } + + rl.question(`File [${filename}] ? `, (answer) => { + if (answer && answer.trim() !== '') { + filename = answer + } + + let fn = __dirname + '/' + filename + fs.rename(s.source, fn, function (err) { + if (err) { + console.error(err) + return rl.close() + } + + let id3 = spawn('id3', ['-a', clean.artist, '-t', clean.title, fn]) + id3.on('close', () => { + console.log('Saved as ' + fn) + rl.close() + }) + }) + }) + }) + }) +}) diff --git a/migrations/001-initial.sql b/migrations/001-initial.sql new file mode 100644 index 0000000..48e3410 --- /dev/null +++ b/migrations/001-initial.sql @@ -0,0 +1,33 @@ +-- Up + +CREATE TABLE Playlist ( + id INTEGER PRIMARY KEY, + title TEXT +); + +CREATE TABLE Track ( + id INTEGER PRIMARY KEY, + title TEXT, + artist TEXT, + file TEXT, + album TEXT, + genre TEXT, + track INTEGER, + year INTEGER, + duration INTEGER +); + +CREATE TABLE PlaylistEntry ( + id INTEGER PRIMARY KEY, + playlistId INTEGER, + trackId INTEGER, + CONSTRAINT PE_fk_playlistId FOREIGN KEY (playlistId) + REFERENCES Playlist (id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT PE_fk_trackId FOREIGN KEY (trackId) + REFERENCES Track (id) ON UPDATE CASCADE ON DELETE CASCADE +); + +-- Down +DROP TABLE Playlist; +DROP TABLE Track; +DROP TABLE PlaylistEntry; diff --git a/package.json b/package.json new file mode 100644 index 0000000..855e0ee --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "btrtracks", + "version": "1.0.0", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "private": true, + "dependencies": { + "babel-core": "^6.26.3", + "babel-env": "^2.4.1", + "bluebird": "^3.5.2", + "express": "^4.16.3", + "express-async-errors": "^3.0.0", + "fs-extra": "^7.0.0", + "sqlite": "^3.0.0" + } +} diff --git a/public/index.css b/public/index.css new file mode 100644 index 0000000..c3e65c4 --- /dev/null +++ b/public/index.css @@ -0,0 +1,103 @@ +* { + margin: 0; + padding: 0; +} +body { + background-color: #0d171e; + font-family: sans-serif; + color: #fff; +} +.container { + display: flex; + flex-direction: column; + flex-grow: 1; + min-height: 0; + height: 100vh; +} +#search { + color: #fff; + background-color: #152b3a; + border: 0; + padding: 10px; + font-size: 2.5em; + border-left: 10px solid #112330; + display: block; +} +.table-container { + flex-grow: 1; + overflow: auto; + overflow-x: hidden; +} +.player { + display: flex; + min-height: max-content; + flex-direction: column; + bottom: 0; + width: 100%; + background-color: #152b3a; +} +.player audio { + width: 100%; +} +.player #playing { + padding: 10px; +} +table { + table-layout: fixed; + border-spacing: 0; + border-collapse: collapse; +} +th.small { + width: 5%; +} +td { + cursor: pointer; +} +td,th { + padding: 5px; +} +tr:nth-child(even) { + background-color: #0f1b23; +} +.pages { + display: flex; + flex-direction: row; + min-height: max-content; +} +.pages .paging { + flex: 1; + text-align: center; + font-size: 3em; +} +.paging.btn { + background-color: #112635; + cursor: pointer; +} +.paging.bg { + background-color: #0e202c; +} +@media only screen and (max-width: 600px) { + tr td:nth-child(1), th:nth-child(1) { + display: none; + } + tr td:nth-child(3), th:nth-child(3) { + display: none; + } + tr td:nth-child(4), th:nth-child(4) { + display: none; + } + tr td:nth-child(5), th:nth-child(5) { + display: none; + } + tr td:nth-child(6), th:nth-child(6) { + display: none; + } + .pages .paging { + flex: 1; + text-align: center; + font-size: 1.3em; + } + .allowance { + margin-bottom: 100px !important; + } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..9cad845 --- /dev/null +++ b/public/index.html @@ -0,0 +1,43 @@ + + + + + + + Music Server + + +
+ +
+ + + + + + + + + + + +
#TrackArtistAlbumYearDuration
+
+
+
<<
+
<
+
0 / 0
+
>
+
>>
+
+
+
Nothing playing
+ +
+ +
+
+
+ + + diff --git a/public/index.js b/public/index.js new file mode 100644 index 0000000..accd7f9 --- /dev/null +++ b/public/index.js @@ -0,0 +1,145 @@ +var table = document.querySelector('#ttable') +var input = document.querySelector('#search') +var audio = document.querySelector('#player') +var playing = document.querySelector('#playing') + +var tableHead = ' \ + # \ + Track \ + Artist \ + Album \ + Year \ + Duration \ +' +var nowPlaying = 0 + +var pageNum = 0 +var pages = 1 +var paginator = document.querySelector('.pages') +var pageNumText = document.querySelector('.pages #pagenum') + +window.mobilecheck = function() { + var check = false; + (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera); + return check; +}; + +// Seconds into HH:MM:SS +function toHHMMSS (numbr) { + let secNum = parseInt(numbr, 10) // don't forget the second param + let hours = Math.floor(secNum / 3600) + let minutes = Math.floor((secNum - (hours * 3600)) / 60) + let seconds = secNum - (hours * 3600) - (minutes * 60) + + if (hours < 10) hours = '0' + hours + if (minutes < 10) minutes = '0' + minutes + if (seconds < 10) seconds = '0' + seconds + + let time = '' + if (parseInt(hours) > 0) { + time = hours + ':' + minutes + ':' + seconds + } else { + time = minutes + ':' + seconds + } + return time +} + +function httpGet (url, callback) { + return new Promise(function (resolve, reject) { + var xmlHttp = new XMLHttpRequest() + xmlHttp.onreadystatechange = function () { + if (xmlHttp.readyState === 4 && xmlHttp.status === 200) { + resolve(JSON.parse(xmlHttp.responseText)) + } else if (xmlHttp.readyState === 4) { + reject(new Error(xmlHttp.status)) + } + } + xmlHttp.open('GET', url, true) + xmlHttp.send(null) + }) +} + +function shortTitle (title, artist) { + title = title.replace(/&/g, '&') + if (!artist) return title + return artist + ' - ' + title +} + +function constructList (tracks) { + table.innerHTML = tableHead + for (let i in tracks) { + let track = tracks[i] + let title = window.mobilecheck() ? shortTitle(track.title, track.artist) : track.title + let htmlAdder = '' + htmlAdder += '' + track.id + '' + htmlAdder += '' + title + '' + htmlAdder += '' + (track.artist || '') + '' + htmlAdder += '' + (track.album || '') + '' + htmlAdder += '' + (track.year || '') + '' + htmlAdder += '' + toHHMMSS(track.duration) + '' + htmlAdder += '' + table.innerHTML += htmlAdder + } +} + +function updatePaging () { + if (input.value.trim() === '') { + paginator.style.display = 'flex' + } else { + paginator.style.display = 'none' + return + } + + pageNumText.innerHTML = pageNum + ' / ' + pages +} + +function showTracks (page) { + if (page > pages) return + if (page < 1) return + + httpGet('/api/tracks?page=' + page).then(function (data) { + pageNum = page + pages = data.pageCount + constructList(data.tracks) + updatePaging() + }) +} + +function search (query) { + updatePaging() + + if (query.trim() === '') { + return showTracks(pageNum) + } + + httpGet('/api/tracks/search?string=' + query).then(function (data) { + constructList(data) + }) +} + +function play (id) { + httpGet('/api/track/' + id).then(function (data) { + let title = shortTitle(data.title, data.artist) + playing.innerHTML = title + document.title = title + + audio.src = '/api/serve/by-id/' + id + audio.play() + + nowPlaying = id + }, function (e) { + console.log(e) + }) +} + +input.addEventListener('keyup', function (e) { + e.which === 13 && search(input.value) +}, false) + +audio.addEventListener('ended', function (e) { + play(nowPlaying + 1) +}) + +showTracks(1) + +window.play = play diff --git a/server.js b/server.js new file mode 100755 index 0000000..3e3e1d5 --- /dev/null +++ b/server.js @@ -0,0 +1,108 @@ +#!/usr/bin/env babel-node +import path from 'path' +import sqlite from 'sqlite' +import Promise from 'bluebird' +import express from 'express' + +require('express-async-errors') + +const values = require(path.join(__dirname, 'values.json')) +const tracksPerPage = 100 + +const dbPromise = Promise.resolve() + .then(() => sqlite.open(path.join(__dirname, values.database), { Promise, cache: true })) + .then(db => db.migrate()) + +const app = express() +const port = process.env.PORT || 3000 + +const router = express.Router() + +router.get('/tracks', async (req, res) => { + let page = parseInt(req.query.page) || 1 + if (isNaN(page)) { + page = 1 + } + + let db = await dbPromise + let count = (await db.get('SELECT COUNT(*) FROM Track'))['COUNT(*)'] + + let pageCount = Math.ceil(count / tracksPerPage) + + if (page > pageCount) page = pageCount + + let offset = (page - 1) * tracksPerPage + let tracks = await db.all('SELECT * FROM Track LIMIT ? OFFSET ?', tracksPerPage, offset) + + for (let i in tracks) { + delete tracks[i].file + } + + res.jsonp({ + page, count, pageCount, tracks + }) +}) + +router.get('/tracks/search', async (req, res) => { + let query = req.query.string + + let db = await dbPromise + let tracks = await db.all('SELECT * FROM Track WHERE title || ifnull(artist,\'\') || ifnull(album,\'\') LIKE ? LIMIT 100', `%${query}%`) + + for (let i in tracks) { + delete tracks[i].file + } + + res.jsonp(tracks) +}) + +router.get('/track/:id', async (req, res, next) => { + let id = req.params.id + let db = await dbPromise + let track = await db.get('SELECT * FROM Track WHERE id = ?', id) + if (!track) return next(new Error('404 file not found')) + + delete track.file + + res.jsonp(track) +}) + +router.get('/playlists', async (req, res, next) => { + let db = await dbPromise + let playlists = await db.all('SELECT * FROM Playlist') + res.jsonp(playlists) +}) + +router.get('/playlist/:id', async (req, res, next) => { + let id = req.params.id + let db = await dbPromise + let playlist = await db.get('SELECT title FROM Playlist WHERE id = ?', id) + + if (!playlist) return next(new Error('404 file not found')) + + let tracks = await db.all('SELECT trackId FROM PlaylistEntry WHERE playlistId = ?', id) + playlist.tracks = tracks + res.jsonp(playlist) +}) + +router.get('/serve/by-id/:id', async (req, res, next) => { + let id = req.params.id + let db = await dbPromise + let track = await db.get('SELECT file FROM Track WHERE id = ?', id) + if (!track) return next(new Error('404 file not found')) + + res.set('Cache-Control', 'public, max-age=31557600') + res.sendFile(path.resolve(track.file)) +}) + +router.use((err, req, res, next) => { + console.error(err) + res.status(404).jsonp({error: 404}) +}) + +app.use('/api', router) +app.use('/', express.static(path.join(__dirname, 'public'))) + +app.listen(port, '127.0.0.1', function () { + console.log(`app running on port ${port}`) +})