diff --git a/public/index.css b/public/index.css
index 3c4ba9f..f3960b8 100644
--- a/public/index.css
+++ b/public/index.css
@@ -362,6 +362,36 @@ canvas#visualizer {
.sidebar a {
color: #18b9c1;
}
+.modal {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: hsla(0, 0%, 1%, 0.5);
+}
+.modal-box {
+ max-width: 500px;
+ margin: auto;
+ margin-top: 10%;
+ background-color: #152b3a;
+}
+.modal-header {
+ padding: 10px;
+ font-size: 2em;
+ background-color: #1087bd;
+ border-bottom: 4px solid #1b6da5;
+}
+.modal-content {
+ padding: 10px;
+}
+.modal-content label {
+ display: block;
+ margin-top: 10px;
+}
+.modal-content input[type="text"] {
+ width: 100%;
+}
@media only screen and (max-width: 600px) {
tr td:nth-child(1), th:nth-child(1) {
display: none;
diff --git a/public/index.html b/public/index.html
index f1cffb4..f8573de 100644
--- a/public/index.html
+++ b/public/index.html
@@ -71,6 +71,7 @@
- Play Track
- Queue Track
+ - Edit Metadata
- Download
- Add to Playlist
@@ -136,6 +137,29 @@
+
diff --git a/public/index.js b/public/index.js
index 404d83b..fbaebbb 100644
--- a/public/index.js
+++ b/public/index.js
@@ -7,6 +7,9 @@
var optdrop = document.getElementById('options-drop')
var optmenu = document.getElementById('options')
var loggedin = document.getElementById('logged-in')
+ var trackedit = document.getElementById('track-edit-modal')
+ var trackform = document.getElementById('track-set')
+ var trackclose = document.getElementById('track-edit-close')
var menu = document.getElementById('menu')
@@ -19,6 +22,7 @@
Duration | \
'
+ var editing = null
var nowPlaying = 0
var externalStream = false
@@ -45,6 +49,7 @@
var ctxPlaylist = document.getElementById('ctx-playlists')
// Options
+ var optcont = document.querySelector('.sidebar.bar')
var options = {
autoplay: true,
trackids: true,
@@ -151,7 +156,8 @@
return artist + ' - ' + title
}
- function handleHash (hash) {
+ function handleHash () {
+ let hash = window.location.hash
if (hash.indexOf('#') === 0) hash = hash.substr(1)
if (hash.length === 0) return
@@ -269,6 +275,25 @@
})
}
+ function closeTrackModal () {
+ editing = null
+ trackedit.style.display = 'none'
+ }
+
+ function editTrack (tid) {
+ httpGet('/api/track/' + tid).then(function (metadata) {
+ editing = tid
+ for (let i in metadata) {
+ let el = trackform.querySelector('#ts-' + i)
+ if (!el) continue
+ el.value = metadata[i]
+ }
+ trackedit.style.display = 'block'
+ }, function (e) {
+ console.log(e)
+ })
+ }
+
function ctxHandle (el) {
if (ctxState === 0) return
let dt = el.getAttribute("data-action")
@@ -285,6 +310,9 @@
case 'download':
window.open('/api/serve/by-id/' + ctxState + '?dl=1', '_blank')
break
+ case 'edit':
+ editTrack(ctxState)
+ break
case 'playlist-remove':
removeFromPlaylist(ctxState)
break
@@ -311,6 +339,9 @@
let plAdd = menu.querySelector('.playlist-add')
plAdd.style.display = isNaN(parseInt(xid)) ? 'none' : 'block'
+ let tEdit = menu.querySelector('.ctx-item[data-action="edit"]')
+ tEdit.style.display = isNaN(parseInt(xid)) ? 'none' : 'block'
+
let plDel = menu.querySelector('.ctx-item[data-action="playlist-remove"]')
plDel.style.display = (playlist != null && playlist >= 0) ? 'block' : 'none'
@@ -368,7 +399,7 @@
let tag = trackDataRow(track)
tag.addEventListener('click', function (e) {
- play(track.trackId || track.id)
+ play.call(this, track.trackId || track.id)
}, false)
tag.addEventListener('contextmenu', function (e) {
@@ -737,6 +768,33 @@
showTracks(pagePrev !== 0 ? pagePrev : 1)
})
+ var metas = ['title', 'artist', 'album', 'year', 'genre', 'track']
+ trackform.addEventListener('submit', function (e) {
+ e.preventDefault()
+ if (editing == null) return closeTrackModal()
+
+ let meta = {}
+
+ for (let k in metas) {
+ let p = metas[k]
+ let a = trackedit.querySelector('#ts-' + p)
+ if (!a) continue
+ meta[p] = a.value
+ }
+
+ httpPost('/api/track/' + editing, meta).then(function () {
+ closeTrackModal()
+ alert('Successfully edited track metadata!')
+ if (!playlist || playlist < 0) showTracks(pageNum)
+ else showPlaylist(playlist)
+ }).catch(function (e) {
+ alert(e.message)
+ console.error(e)
+ })
+ }, false)
+
+ trackclose.addEventListener('click', closeTrackModal, false)
+
document.getElementById('player-next').addEventListener('click', playNext, false)
document.getElementById('player-prev').addEventListener('click', playPrevious, false)
@@ -760,7 +818,7 @@
window.addEventListener('hashchange', function (e) {
e.preventDefault()
- handleHash(window.location.hash)
+ handleHash()
}, false)
window.addEventListener('resize', function (e) {
@@ -781,7 +839,6 @@
}
})
- var optcont = document.querySelector('.sidebar.bar')
document.addEventListener('click', function (event) {
// event.target.closest(optcont) === null
if (!optcont.contains(event.target) && optdrop.className.indexOf('active') !== -1) {
@@ -810,7 +867,7 @@
checkUser()
loadOptions()
showTracks(1)
- handleHash(window.location.hash)
+ handleHash()
populatePlaylists()
handleSelect()
handleOptions()
diff --git a/src/common/async.js b/src/common/async.js
index bf22f13..c8dd5a2 100644
--- a/src/common/async.js
+++ b/src/common/async.js
@@ -2,8 +2,11 @@ import {exec} from 'child_process'
import path from 'path'
import fs from 'fs-extra'
import url from 'url'
+import os from 'os'
import qs from 'querystring'
+const supportedMetadata = ['album', 'genre', 'title', 'artist', 'year', 'track']
+
function filewalker (dir, done) {
let results = []
@@ -50,6 +53,17 @@ async function insertDB (db, track) {
return track
}
+async function updateDB (db, id, meta) {
+ let sanit = []
+ for (let key in meta) {
+ if (supportedMetadata.indexOf(key) === -1) continue
+ let val = meta[key]
+ sanit.push(key + '="' + val + '"')
+ }
+ await db.run('UPDATE Track SET ' + sanit.join(',') + ' WHERE id = ?', id)
+ return meta
+}
+
function getFiles (dir) {
return new Promise((resolve, reject) => {
filewalker(dir, (err, files) => {
@@ -85,4 +99,77 @@ function copyAsync (fsrc, fdst) {
})
}
-export default {getFiles, promiseExec, askAsync, insertDB, copyAsync}
+async function getInfos (file) {
+ let formatData = await promiseExec(`ffprobe -i "${file}" -show_entries format -v quiet -of json`)
+
+ let parsed = JSON.parse(formatData.stdout)
+ if (!parsed || !parsed.format || !parsed.format.duration) throw new Error('Failed to parse metadata!')
+ parsed = parsed.format
+
+ let data = {
+ file,
+ duration: parseFloat(parsed.duration)
+ }
+
+ if (Math.floor(data.duration) === 0) throw new Error('Invalid file type!')
+
+ if (parsed.tags) {
+ for (let k in parsed.tags) {
+ let tagtype = k.toLowerCase()
+ let value = parsed.tags[k]
+
+ if (tagtype === 'date') tagtype = 'year'
+ if (tagtype === 'track' || tagtype === 'year') {
+ if (value.indexOf('/') !== -1) {
+ value = value.split('/')[0]
+ }
+
+ value = parseInt(value)
+ }
+
+ data[tagtype] = value
+ }
+ }
+
+ if (!data.title) {
+ let parsed = path.parse(file)
+ data.title = parsed.name
+ }
+
+ return data
+}
+
+async function setMetadata (file, meta, source, cleanup = false) {
+ if (!meta || !meta['title'] || meta.title === '') throw new Error('Invalid metadata provided')
+ let sanit = []
+ let dbq = {}
+ for (let key in meta) {
+ if (supportedMetadata.indexOf(key) === -1) continue
+ let val = meta[key]
+ if ((key === 'artist' || key === 'title') && (!val || val === '')) continue
+ dbq[key] = val
+ if (key === 'year') key = 'date'
+ sanit.push('-metadata ' + key + '="' + val + '"')
+ }
+
+ if (!sanit.length) throw new Error('Invalid metadata provided')
+
+ if (!source) {
+ let p = path.parse(file)
+ source = path.join(os.tmpdir(), '.retmp' + p.ext)
+ cleanup = true
+ await copyAsync(file, source)
+ await fs.unlink(file)
+ }
+
+ await promiseExec(`ffmpeg -i "${source}" ${sanit.join(' ')} -codec copy "${file}"`)
+
+ if (cleanup) {
+ await fs.unlink(source)
+ }
+
+ return { file, dbq }
+}
+
+export default { getFiles, promiseExec, askAsync, insertDB, updateDB,
+ copyAsync, getInfos, setMetadata, supportedMetadata }
diff --git a/src/common/download.js b/src/common/download.js
index 3d25e70..74b7a4f 100644
--- a/src/common/download.js
+++ b/src/common/download.js
@@ -93,44 +93,4 @@ function fetchVideo (data) {
})
}
-async function getInfos (file) {
- let formatData = await asn.promiseExec(`ffprobe -i "${file}" -show_entries format -v quiet -of json`)
-
- let parsed = JSON.parse(formatData.stdout)
- if (!parsed || !parsed.format || !parsed.format.duration) throw new Error('Failed to parse metadata!')
- parsed = parsed.format
-
- let data = {
- file,
- duration: parseFloat(parsed.duration)
- }
-
- if (Math.floor(data.duration) === 0) throw new Error('Invalid file type!')
-
- if (parsed.tags) {
- for (let k in parsed.tags) {
- let tagtype = k.toLowerCase()
- let value = parsed.tags[k]
-
- if (tagtype === 'date') tagtype = 'year'
- if (tagtype === 'track' || tagtype === 'year') {
- if (value.indexOf('/') !== -1) {
- value = value.split('/')[0]
- }
-
- value = parseInt(value)
- }
-
- data[tagtype] = value
- }
- }
-
- if (!data.title) {
- let parsed = path.parse(file)
- data.title = parsed.name
- }
-
- return data
-}
-
-export default {parseTitle, getVideoInfo, fetchVideo, getInfos}
+export default { parseTitle, getVideoInfo, fetchVideo }
diff --git a/src/dbpopulate.js b/src/dbpopulate.js
index 4505ee9..b29f548 100644
--- a/src/dbpopulate.js
+++ b/src/dbpopulate.js
@@ -51,7 +51,7 @@ async function handlePassed (db, fpath) {
let trackinf
try {
- trackinf = await dl.getInfos(filePath)
+ trackinf = await asn.getInfos(filePath)
} catch (e) {
trackinf = await interactive(filePath, db)
}
@@ -93,7 +93,7 @@ async function run () {
// if (cleanTrackData.length > 10) break // debug purposes
process.stdout.write(`\rProcessing file ${parseInt(i) + 1} of ${files.length}.. (${skips} skipped)`)
try {
- let fd = await dl.getInfos(file)
+ let fd = await asn.getInfos(file)
cleanTrackData.push(fd)
} catch (e) {
skips++
diff --git a/src/download.js b/src/download.js
index 2f6682b..acc8cc7 100644
--- a/src/download.js
+++ b/src/download.js
@@ -49,15 +49,13 @@ async function download (furl) {
}
let fn = path.join(musicdir, filename)
-
- await asn.promiseExec(`ffmpeg -i "${file.source}" -metadata artist="${clean.artist}" -metadata title="${clean.title}" -codec copy "${fn}"`)
- await fs.unlink(file.source)
+ await asn.setMetadata(fn, clean, file.source, true)
let addAnsw = await asn.askAsync(rl, `Would you like to add it to the database now? [N/y] ? `)
if (addAnsw && addAnsw.trim().toLowerCase().indexOf('y') === 0) {
// Add to database
try {
- let verify = await dl.getInfos(fn)
+ let verify = await asn.getInfos(fn)
await asn.insertDB(await dbPromise, verify)
} catch (e) {
console.warn('=!= Add to database failed!')
diff --git a/src/server.js b/src/server.js
index 65a7980..8601fb0 100644
--- a/src/server.js
+++ b/src/server.js
@@ -10,6 +10,8 @@ import ffmpeg from 'fluent-ffmpeg'
import { user, userMiddleware } from './user'
import { dbPromise } from './database'
+import asn from './common/async'
+
import playlist from './playlist'
import lastfm from './lastfm'
@@ -185,6 +187,20 @@ router.get('/track/:id', userMiddleware, async (req, res, next) => {
res.jsonp(track)
})
+router.post('/track/:id', userMiddleware, async (req, res, next) => {
+ let id = req.params.id
+ let meta = req.body
+
+ let db = await dbPromise
+ let track = await db.get('SELECT file FROM Track WHERE id = ?', id)
+ if (!track) throw new Error('404 track not found')
+
+ let m = await asn.setMetadata(track.file, meta)
+ await asn.updateDB(db, id, m.dbq)
+
+ res.jsonp(m)
+})
+
// --------- //
// PLAYLISTS //
// --------- //