summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJon Bergli Heier <snakebite@jvnv.net>2011-08-16 14:49:40 +0200
committerJon Bergli Heier <snakebite@jvnv.net>2011-08-16 14:49:40 +0200
commita3e86f3be768c8fa1fc2af12d5e5d66d9d9b82e8 (patch)
treeb7ff0104001b62709e22cdaefa0adc23ea07b919
parent74ad26edc2cf0d8aa8d5d485d708de1a34aa75c0 (diff)
Implemented basic cuesheet support, some playlist fixes.
-rwxr-xr-xapp.py12
-rw-r--r--cuesheet.py103
-rw-r--r--directory.py39
-rw-r--r--events.py17
-rw-r--r--recode.py23
-rw-r--r--static/icons/delete.pngbin0 -> 715 bytes
-rw-r--r--static/index.html4
-rw-r--r--static/player.js30
-rw-r--r--static/playlist.js52
-rw-r--r--static/style.css4
10 files changed, 239 insertions, 45 deletions
diff --git a/app.py b/app.py
index c2ef0a3..3b887d8 100755
--- a/app.py
+++ b/app.py
@@ -35,8 +35,10 @@ class Application(object):
return open(filename, 'rb')
def cache(self, environ, start_response, path):
+ args = cgi.FieldStorage(environ = environ)
path = os.path.join(*path[1:])
- cache_path = File(path).get_cache_path()
+ track = args.getvalue('track') if 'track' in args else None
+ cache_path = File(path, track = track).get_cache_path()
if not os.path.exists(cache_path) or '..' in path:
start_response('404 Not Found', [])
@@ -61,8 +63,9 @@ class Application(object):
def json_recode(self, environ, start_response, path):
args = cgi.FieldStorage(environ = environ)
path = args.getvalue('path') if 'path' in args else None
+ track = args.getvalue('track') if 'track' in args else None
- f = File(path)
+ f = File(path, track = track)
# see json_play()
if not os.path.splitext(path)[1] in ('.mp3', '.ogg'):
decoder = 'ffmpeg'
@@ -76,8 +79,9 @@ class Application(object):
args = cgi.FieldStorage(environ = environ)
path = args.getvalue('path')
+ track = args.getvalue('track') if 'track' in args else None
- f = File(path)
+ f = File(path, track = track)
# TODO: replace this with some sane logic
if not os.path.splitext(path)[1] in ('.mp3', '.ogg'):
cache_path = f.get_cache_path()
@@ -86,7 +90,7 @@ class Application(object):
if not os.path.exists(cache_path):
f.start_recode(decoder, encoder, environ['sessionid'])
else:
- events.event_pub.play(environ['sessionid'], '/cache/{0}'.format(path))
+ events.event_pub.play(environ['sessionid'], '/cache/{0}{1}'.format(path, ('?track=' + track if track else '')))
else:
events.event_pub.play(environ['sessionid'], '/files/{0}'.format(path))
diff --git a/cuesheet.py b/cuesheet.py
new file mode 100644
index 0000000..827dbd9
--- /dev/null
+++ b/cuesheet.py
@@ -0,0 +1,103 @@
+import re
+
+cdtext_re = {
+ 'REM': r'^(REM) (.+)$',
+ 'PERFORMER': r'^(PERFORMER) "?(.+?)"?$',
+ 'TITLE': r'^(TITLE) "?(.+?)"?$',
+ 'FILE': r'^(FILE) "?(.+?)"? (BINARY|MOTOROLA|AIFF|WAVE|MP3)$',
+ 'TRACK': r'^(TRACK) (\d+) (AUDIO|CDG|MODE1/2048|MODE1/2352|MODE2/2336|MODE2/2352|CDI/2336|CDI2352)$',
+ 'INDEX': r'^(INDEX) (\d+) (\d+):(\d+):(\d+)$',
+ 'FLAGS': r'^((?:DCP|4CH|PRE|SCMS) ?){1,4}$',
+}
+
+for k, v in cdtext_re.iteritems():
+ cdtext_re[k] = re.compile(v)
+
+class CDText(object):
+ def __init__(self, str):
+ name = str.split()[0]
+ self.re = cdtext_re[name]
+ l = self.parse(str)
+ self.type, self.value = l[0], l[1:]
+ if type(self.value) == tuple and len(self.value) == 1:
+ self.value = self.value[0]
+
+ def __repr__(self):
+ return '<CDText "%s" "%s">' % (self.type, self.value)
+
+ def __str__(self):
+ return repr(self)
+
+ def parse(self, str):
+ r = self.re.match(str)
+ if not r:
+ return None, None
+ return r.groups()
+
+class FieldDescriptor(object):
+ def __init__(self, field):
+ self.field = field
+
+ def __get__(self, instance, owner):
+ def find(name):
+ for l in instance.cdtext:
+ if l.type == name:
+ return l
+ cdtext = find(self.field)
+ return cdtext.value if cdtext else None
+
+class Track(object):
+ def __init__(self):
+ self.cdtext = []
+ self.abs_tot, self.abs_end, self.nextstart = 0, 0, None
+
+ def add(self, cdtext):
+ self.cdtext.append(cdtext)
+
+ def set_abs_tot(self, tot):
+ self.abs_tot = tot
+
+ def set_abs_end(self, end):
+ self.abs_end
+
+ def get_start_time(self):
+ index = self.index
+ return int(index[1])*60 + int(index[2])
+
+ def get_length(self):
+ return self.nextstart - self.get_start_time()
+
+for f in cdtext_re.keys():
+ setattr(Track, f.lower(), FieldDescriptor(f))
+
+class Cuesheet(object):
+ def __init__(self, filename = None, fileobj = None):
+ if not fileobj and filename:
+ fileobj = open(filename, 'rb')
+ if fileobj:
+ self.parse(fileobj)
+
+ def parse(self, f):
+ info = []
+ tracks = []
+ track = Track()
+ info.append(track)
+ if not f.read(3) == '\xef\xbb\xbf':
+ f.seek(0)
+ for line in f:
+ cdtext = CDText(line.strip())
+ if cdtext.type == 'TRACK':
+ track = Track()
+ tracks.append(track)
+ track.add(cdtext)
+ self.info = info
+ self.tracks = tracks
+
+ def get_next(self, track):
+ found = False
+ for i in self.tracks:
+ if found:
+ return i
+ elif i == track:
+ found = True
+ return None
diff --git a/directory.py b/directory.py
index 6860155..79024f4 100644
--- a/directory.py
+++ b/directory.py
@@ -1,12 +1,13 @@
-import os, mimetypes, recode, events
+import os, mimetypes, recode, events, cuesheet
from config import config
class DirectoryEntry(object):
'''Base class for directory entries.'''
- def __init__(self, path, isabs = False):
+ def __init__(self, path, isabs = False, track = None):
self.path = path
+ self.track = track
if isabs:
self.abs_path = path
@@ -27,7 +28,7 @@ class DirectoryEntry(object):
return '<a href="/files/{path}">{name}</a><br />'.format(path = self.path, name = os.path.basename(self.path))
def json(self):
- return {'type': self.entry_type, 'name': self.path}
+ return {'type': self.entry_type, 'name': self.path, 'track': self.track}
class Directory(DirectoryEntry):
'''A directory entry inside a directory.'''
@@ -44,7 +45,12 @@ class Directory(DirectoryEntry):
if os.path.isdir(abs_path):
directories.append(Directory(rel_path))
elif os.path.isfile(abs_path):
- files.append(File(rel_path))
+ if os.path.splitext(f)[1] == '.cue':
+ cue = cuesheet.Cuesheet(abs_path)
+ for t in cue.tracks:
+ files.append(File(rel_path, track = t.track[0]))
+ else:
+ files.append(File(rel_path))
return sorted(directories) + sorted(files)
class File(DirectoryEntry):
@@ -86,13 +92,30 @@ class File(DirectoryEntry):
def get_cache_path(self):
cache_path = os.path.join(config.get('cache_dir'), self.path)
- cache_path = os.path.splitext(cache_path)[0] + '.ogg'
+ cache_path = os.path.splitext(cache_path)[0]
+ if self.track:
+ cache_path += '.' + self.track
+ cache_path += '.ogg'
return cache_path
def get_cache_file(self):
return File(self.get_cache_path(), True)
def recode(self, decoder, encoder, sessionid = None):
+ if self.track:
+ cue = cuesheet.Cuesheet(self.abs_path)
+ t = cue.tracks[int(self.track)-1]
+ start_time = t.get_start_time()
+ next = cue.get_next(t)
+ if next:
+ end_time = next.get_start_time()
+ else:
+ end_time = None
+ path = os.path.join(os.path.dirname(self.abs_path), cue.info[0].file[0])
+ else:
+ path = self.abs_path
+ start_time, end_time = None, None
+
decoder = recode.decoders[decoder]()
encoder = recode.encoders[encoder]()
recoder = recode.Recoder(decoder, encoder)
@@ -104,9 +127,9 @@ class File(DirectoryEntry):
os.makedirs(cache_path_dir)
# check if file is cached
if not os.path.exists(cache_path):
- events.event_pub.recoding(self.path)
- recoder.recode(self.abs_path, cache_path)
- events.event_pub.cached(self.path)
+ events.event_pub.recoding(self.path, self.track)
+ recoder.recode(path, cache_path, start_time = start_time, end_time = end_time)
+ events.event_pub.cached(self.path, self.track)
if sessionid:
events.event_pub.play(sessionid, '/cache/{0}'.format(self.path))
diff --git a/events.py b/events.py
index 451e6f0..0d0360d 100644
--- a/events.py
+++ b/events.py
@@ -25,8 +25,15 @@ def EventSubscriber(app, environ, start_response, path):
if address == session:
address, message = message.split(None, 1)
- if address in ('cached', 'recoding', 'play'):
+ data = None
+ if address in ('cached', 'recoding'):
+ track, path = message.split(None, 1)
+ data = json.dumps({'type': address, 'path': path, 'track': track})
+ yield 'data: {0}\n\n'.format(data)
+ elif address in ('play',):
data = json.dumps({'type': address, 'path': message})
+
+ if data:
yield 'data: {0}\n\n'.format(data)
socket.close()
@@ -37,11 +44,11 @@ class EventPublisher(object):
self.socket = self.context.socket(zmq.PUB)
self.socket.bind(config.get('event_publisher'))
- def recoding(self, path):
- self.socket.send('recoding {0}'.format(path))
+ def recoding(self, path, track):
+ self.socket.send('recoding {0} {1}'.format(track or '_', path))
- def cached(self, path):
- self.socket.send('cached {0}'.format(path))
+ def cached(self, path, track):
+ self.socket.send('cached {0} {1}'.format(track or '_', path))
def play(self, session, path):
self.socket.send('session-{0} play {1}'.format(session, path))
diff --git a/recode.py b/recode.py
index 8b3ebb0..e67328e 100644
--- a/recode.py
+++ b/recode.py
@@ -33,8 +33,14 @@ class Codec(object):
class FFmpeg(Decoder):
decoder_name = 'ffmpeg'
- def decode(self, source, dest, *args):
- cmd = (x.format(infile = source, outfile = dest) for x in 'ffmpeg -loglevel quiet -i {infile} -y {outfile}'.split())
+ def decode(self, source, dest, *args, **kwargs):
+ cmd = 'ffmpeg -loglevel quiet'.split()
+ if 'start_time' in kwargs:
+ cmd += ['-ss', str(kwargs['start_time'])]
+ if 'end_time' in kwargs and kwargs['end_time']:
+ cmd += ['-t', str(kwargs['end_time'] - kwargs['start_time'])]
+ cmd += ['-i', source, '-y', dest]
+ #cmd = (x.format(infile = source, outfile = dest) for x in 'ffmpeg -loglevel quiet -i {infile} -y {outfile}'.split())
p = subprocess.Popen(cmd, stderr = subprocess.PIPE, close_fds = True)
p.stderr.close()
p.wait()
@@ -42,7 +48,7 @@ class FFmpeg(Decoder):
class Ogg(Encoder):
encoder_name = 'ogg'
- def encode(self, source, dest, *args):
+ def encode(self, source, dest, *args, **kwargs):
options = config.get('options', 'encoder/ogg', '')
cmd = ['oggenc', '-Q'] + options.split() + [source, '-o', dest]
subprocess.call(cmd)
@@ -52,18 +58,13 @@ class Recoder(object):
self.decoder = decoder
self.encoder = encoder
- def recode(self, source, dest):
+ def recode(self, source, dest, **kwargs):
if self.decoder.__class__ == self.encoder.__class__ and hasattr(self.decoder, 'recode'):
- print 'recoding'
- print self.decoder
self.decoder.recode(source, dest)
else:
with tempfile.NamedTemporaryFile(mode = 'wb', prefix = 'foo', suffix = '.wav', delete = True) as temp:
- print 'temp file:', temp.name
- print 'decoding'
- self.decoder.decode(source, temp.name)
- print 'encoding'
- self.encoder.encode(temp.name, dest)
+ self.decoder.decode(source, temp.name, **kwargs)
+ self.encoder.encode(temp.name, dest, **kwargs)
class RecodeThread(threading.Thread):
lock = threading.Lock()
diff --git a/static/icons/delete.png b/static/icons/delete.png
new file mode 100644
index 0000000..08f2493
--- /dev/null
+++ b/static/icons/delete.png
Binary files differ
diff --git a/static/index.html b/static/index.html
index 6f24aba..c1d75db 100644
--- a/static/index.html
+++ b/static/index.html
@@ -16,12 +16,12 @@
</div>
<div id="current-dir"></div>
<div id="hpanel">
- <span>
+ <span "browser-span">
<span class="list-header">Browse directory</span>
<a href="#" onclick="add_directory(); return false" id="add-dir">Add all</a>
<ul id="song-links"></ul>
</span>
- <span>
+ <span id="playlist-span">
<span class="list-header">Song queue</span>
<ul id="playlist"></ul>
</span>
diff --git a/static/player.js b/static/player.js
index 90acf0b..b358eb5 100644
--- a/static/player.js
+++ b/static/player.js
@@ -12,22 +12,28 @@ for(var i = 0; i < cache_images.length; i++) {
img.src = '/static/icons/' + cache_images[i];
}
-function MusicListing(type, path, name, cached) {
+function MusicListing(type, path, name, track, cached) {
this.type = type;
this.path = path;
this.name = name ? name : path.split('/').pop();
+ this.track = track;
this.a = document.createElement('a');
this.a.ml = this;
this.play = function() {
+ var path = '/play?path=' + encodeURIComponent(this.path);
+ if(this.track)
+ path += '&track=' + this.track;
log('playing ' + path);
xmlhttp = new XMLHttpRequest();
- xmlhttp.open('GET', '/play?path=' + encodeURIComponent(path));
+ xmlhttp.open('GET', path);
xmlhttp.send(null);
}
this.recode = function() {
var path = '/recode?path=' + encodeURIComponent(this.path);
+ if(this.track)
+ path += '&track=' + this.track;
var a = this.a;
var ml = this;
var xmlhttp = new XMLHttpRequest();
@@ -43,7 +49,10 @@ function MusicListing(type, path, name, cached) {
className += ' file-cached'
a.setAttribute('class', className);
a.setAttribute('href', '#');
- a.appendChild(document.createTextNode(this.name));
+ var name = this.name;
+ if(this.track)
+ name += ' (track ' + this.track + ')';
+ a.appendChild(document.createTextNode(name));
var ml = this;
a.onclick = function() {
@@ -104,9 +113,10 @@ function list(root) {
for(var i = 0; i < json.length; i++) {
var type = json[i]["type"];
var path = json[i]["name"];
+ var track = json[i]["track"];
var name = path.substring(path.lastIndexOf('/')+1);
var cached = type == "file" ? json[i]["cached"] : false;
- var l = new MusicListing(type, path, name, cached);
+ var l = new MusicListing(type, path, name, track, cached);
output_link(l);
}
}
@@ -131,11 +141,11 @@ function add_directory() {
var source = null;
-function get_a(path) {
+function get_a(path, track) {
var as = document.getElementById('song-links').getElementsByTagName('a');
for(var i = 0; i < as.length; i++) {
var a = as[i];
- if(a.ml.path == path)
+ if(a.ml.path == path && (!track || track == a.ml.track))
return a;
}
}
@@ -146,12 +156,16 @@ function event_handler(event) {
case 'cached':
case 'recoding':
log('[' + data['type'] + '] ' + data['path']);
+ var track = null;
+ if('track' in data)
+ track = data['track'];
+ log('track: ' + track);
// update directory browser
- var a = get_a(data['path']);
+ var a = get_a(data['path'], track);
if(a)
a.setAttribute('class', 'file file-' + data['type']);
// update song queue
- var li = playlist.get(data['path']);
+ var li = playlist.get(data['path'], track);
if(li) {
a = li.getElementsByTagName('a')[0];
if(data['type'] == 'cached') {
diff --git a/static/playlist.js b/static/playlist.js
index 38db463..d2ac424 100644
--- a/static/playlist.js
+++ b/static/playlist.js
@@ -3,12 +3,15 @@ function Playlist(pl, audio) {
this.audio = audio;
this.current = null;
- this.get = function(path) {
+ this.get = function(path, track) {
var li = this.pl.getElementsByTagName('li')[0];
while(li) {
- var a = li.getElementsByTagName('a')[0];
- if(a.ml.path == path)
- return li;
+ var as = li.getElementsByTagName('a');
+ if(as.length > 0) {
+ var a = as[0];
+ if(a.ml.path == path && (!track || a.ml.track == track))
+ return li;
+ }
li = li.nextElementSibling;
}
return null;
@@ -17,7 +20,10 @@ function Playlist(pl, audio) {
this.add = function(ml) {
var a = document.createElement('a');
a.setAttribute('href', '#');
- a.appendChild(document.createTextNode(ml.path));
+ var name = ml.name;
+ if(ml.track)
+ name += ' (track ' + ml.track + ')';
+ a.appendChild(document.createTextNode(name));
a.ml = ml;
var li = document.createElement('li');
var pl = this;
@@ -30,9 +36,13 @@ function Playlist(pl, audio) {
var nextsong = li.nextElementSibling;
log('nextsong: ' + nextsong);
+ // avoid breaking when a track has been removed
+ while(nextsong && nextsong.childElementCount == 0)
+ nextsong = nextsong.nextElementSibling;
if(nextsong) {
var nexta = nextsong.getElementsByTagName('a')[0];
log('nexta: ' + nexta);
+ log(nexta.onclick);
log('next ml: ' + nexta.ml);
nexta.ml.recode();
}
@@ -41,7 +51,34 @@ function Playlist(pl, audio) {
}
log(a.click);
- li.appendChild(a);
+ var span = document.createElement('div');
+
+ // anchor to remove track from playlist
+ var a_del = document.createElement('a');
+ a_del.setAttribute('class', 'delete');
+ a_del.setAttribute('href', '#');
+ a_del.setAttribute('title', 'Remove');
+ var del_img = new Image();
+ del_img.src = '/static/icons/delete.png';
+ a_del.appendChild(del_img);
+ a_del.onclick = function() {
+ li.removeChild(span);
+ return false;
+ }
+ a_del.hidden = true;
+
+ span.onmouseover = function() {
+ a_del.hidden = false;
+ }
+ span.onmouseout = function() {
+ a_del.hidden = true;
+ }
+
+ span.appendChild(a);
+ span.appendChild(document.createTextNode(' '));
+ span.appendChild(a_del);
+
+ li.appendChild(span);
this.pl.appendChild(li);
if(this.pl.firstChild == li || (this.current && this.current.nextElementSibling == li))
@@ -58,6 +95,9 @@ function Playlist(pl, audio) {
this.current = this.current.nextElementSibling;
old.parentElement.removeChild(old);
}
+ // avoid breaking when a track has been removed
+ while(this.current && this.current.childElementCount == 0)
+ this.current = this.current.nextElementSibling;
if(this.current) {
var a = this.current.getElementsByTagName('a')[0];
a.onclick();
diff --git a/static/style.css b/static/style.css
index 35319c0..8f3b636 100644
--- a/static/style.css
+++ b/static/style.css
@@ -4,7 +4,8 @@ ul#song-links a:hover, ul#playlist a:hover { text-decoration: underline; }
a.dir { background-image: url('/static/icons/folder.png'); }
a.file { background-image: url('/static/icons/music.png'); }
a.file-cached { background-image: url('/static/icons/music-cached.png'); }
-a.dir, a.file, ul#playlist li a { background-repeat: no-repeat; padding-left: 20px; }
+a.dir, a.file, ul#playlist li a:first-child { background-repeat: no-repeat; padding-left: 20px; }
+ul#playlist img { vertical-align: middle; }
a.file-recoding { background-image: url('/static/icons/loading.gif'); }
a.file-queued { background-image: url('/static/icons/music-queued.png'); }
div#hpanel { margin-top: 1em; }
@@ -14,3 +15,4 @@ ul#playlist li a.playing { background-image: url('/static/icons/sound.png'); }
span.list-header { font-size: large; }
ul#song-links { width: 30em; overflow: auto; white-space: nowrap; resize: both; }
a#add-dir { font-size: small; }
+span#playlist-span { position: absolute; left: 30em; right: 1em; }