summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJon Bergli Heier <snakebite@jvnv.net>2012-12-16 11:53:03 +0100
committerJon Bergli Heier <snakebite@jvnv.net>2012-12-16 11:53:03 +0100
commit3072dcabb7f8d35f21ebfaeae53a1218ecd9d43e (patch)
treecc117e7da3923bc7948c0ce19cac985a297c4468
parentb96cdbf27095f57c4800f1295ddc76482ba62443 (diff)
Added album view.
-rw-r--r--.gitignore1
-rwxr-xr-xapp.py51
-rw-r--r--config.py10
-rw-r--r--coverart.py73
-rw-r--r--db.py4
-rw-r--r--static/index.html5
-rw-r--r--static/init.js126
-rw-r--r--static/nocover.jpgbin0 -> 3339 bytes
-rw-r--r--static/style.css12
-rw-r--r--static/templates.js3
10 files changed, 266 insertions, 19 deletions
diff --git a/.gitignore b/.gitignore
index 77b2ff9..bda4cf0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,5 @@
*.pyc
/config
/cache
+/covercache
/*.db
diff --git a/app.py b/app.py
index 5a077e0..8aecdf8 100755
--- a/app.py
+++ b/app.py
@@ -80,16 +80,46 @@ class JSONApplication(object):
start_response('200 OK', [])
return json.dumps(results)
+ def albums(self, environ, start_response, path):
+ page = int(path[1] if len(path) > 1 else 0)
+ page_size = config.getint('album_pagesize', default = 75)
+ try:
+ session = db.Session()
+ albums = session.query(db.Album).offset(page*page_size).limit(page_size)
+ results = [{'id': a.id, 'name': a.name, 'artist': {'name': a.artist.name}} for a in albums]
+ finally:
+ session.close()
+
+ start_response('200 OK', [])
+ return json.dumps(results)
+
+ def album(self, environ, start_response, path):
+ album = int(path[1])
+ try:
+ session = db.Session()
+ album = session.query(db.Album).filter(db.Album.id == album).one()
+ results = [self.format_track(x) for x in album.tracks]
+ finally:
+ session.close()
+
+ start_response('200 OK', [])
+ return json.dumps(results)
+
handlers = {
'list': list,
'hint': hint,
'search': search,
+ 'albums': albums,
+ 'album': album,
}
def __call__(self, environ, start_response, path):
module = path[0]
if module in self.handlers:
return self.handlers[module](self, environ, start_response, path)
+ else:
+ start_response('404 Not Found', [])
+ return []
class Application(object):
rfc1123_format = '%a, %d %b %Y %H:%M:%S +0000'
@@ -163,11 +193,32 @@ class Application(object):
return self._serve_path(environ, start_response, filename)
+ def album_cover(self, environ, start_response, path):
+ album = int(path[1].split('.')[0])
+ session = db.Session()
+ cover = None
+ try:
+ album = db.Album.get_by_id(session, album)
+ import coverart
+ cover = coverart.get_coverart(album)
+ except db.NoResultFound:
+ start_response('404 Not Found', [])
+ return []
+ finally:
+ session.close()
+
+ if not cover:
+ start_response('302 Found', [('Location', '/static/nocover.jpg')])
+ return []
+
+ return self._serve_path(environ, start_response, cover)
+
handlers = {
'json': json,
'static': static,
'file': file,
'track': track,
+ 'album-cover': album_cover,
}
def __call__(self, environ, start_response):
diff --git a/config.py b/config.py
index 3f8995b..cadeb14 100644
--- a/config.py
+++ b/config.py
@@ -19,7 +19,13 @@ class Config(object):
else:
raise
- def getint(self, key, section = config_section):
- return self.config.getint(section, key)
+ def getint(self, key, section = config_section, default = None):
+ try:
+ return self.config.getint(section, key)
+ except NoOptionError:
+ if default != None:
+ return default
+ else:
+ raise
config = Config()
diff --git a/coverart.py b/coverart.py
new file mode 100644
index 0000000..1d6fc3f
--- /dev/null
+++ b/coverart.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python2
+
+import db, mutagen, os
+from config import config
+from PIL import Image
+
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+
+def get_coverart(album, ignorecache = False):
+ covercache_dir = config.get('covercache_dir')
+ covercache = os.path.join(covercache_dir, '%d.jpg' % album.id)
+
+ def makecover(fileobj):
+ im = Image.open(fileobj)
+ im.thumbnail((128, 128), Image.ANTIALIAS)
+ im.save(covercache)
+ return covercache
+
+ if not ignorecache and os.path.exists(covercache):
+ return covercache
+
+ if not len(album.tracks):
+ return
+
+ track = album.tracks[0]
+ try:
+ f = mutagen.File(track.get_path())
+ except:
+ f = None
+ if hasattr(f, 'pictures') and len(f.pictures):
+ p = f.pictures[0]
+ s = StringIO.StringIO(p.data)
+ return makecover(s)
+
+ dirname = os.path.dirname(track.get_path())
+ files = os.listdir(dirname)
+ files.sort()
+ cover = []
+ for f in files:
+ filename = os.path.join(dirname, f)
+ if os.path.isdir(filename):
+ print filename
+ if os.path.isdir(filename) and f.lower() in ('scans', 'jpg', 'jpeg', 'img', 'image', 'cover', 'covers'):
+ files.extend(os.path.join(f, x) for x in os.listdir(filename))
+ files.sort()
+ if not os.path.isfile(filename):
+ continue
+ extensions = ('.jpg', '.jpeg', '.png', '.bmp')
+ root, ext = os.path.splitext(f)
+ # Ignore non-image files
+ if not ext.lower() in extensions:
+ continue
+ if os.path.split(root)[-1].lower() in ('cover', 'cover-large', 'cover-front'):
+ cover.insert(0, f)
+ elif 'cover' in f.lower() and not len(cover):
+ cover.append(f)
+ elif not len(cover):
+ cover.append(f)
+
+ while len(cover):
+ filename = os.path.join(dirname, cover.pop(0))
+ return makecover(open(filename, 'rb'))
+
+def main():
+ session = db.Session()
+ for album in session.query(db.Album):
+ get_coverart(album, True)
+
+if __name__ == '__main__':
+ main()
diff --git a/db.py b/db.py
index ebb5701..57e30c0 100644
--- a/db.py
+++ b/db.py
@@ -118,6 +118,10 @@ class Album(Base):
session.commit()
return album
+ @staticmethod
+ def get_by_id(session, id):
+ return session.query(Album).filter(Album.id == id).one()
+
class Track(Base):
__tablename__ = 'tracks'
diff --git a/static/index.html b/static/index.html
index b333ec9..635a0a0 100644
--- a/static/index.html
+++ b/static/index.html
@@ -30,6 +30,7 @@
<li><a href="#playlist-tab" accesskey="p"><span class="accessor">P</span>laylist</a></li>
<li><a href="#search-tab" accesskey="s"><span class="accessor">S</span>earch</a></li>
<li><a href="#directory-tab" accesskey="d"><span class="accessor">D</span>irectories</a></li>
+ <li><a href="#albums-tab" accesskey="a"><span class="accessor">A</span>lbums</a></li>
</ul>
<div id="playlist-tab">
<table class="track-table">
@@ -81,6 +82,10 @@
</tbody>
</table>
</div>
+ <div id="albums-tab">
+ <div id="albums-list">
+ </div>
+ </div>
</div>
</div>
</body>
diff --git a/static/init.js b/static/init.js
index 941d798..e75f173 100644
--- a/static/init.js
+++ b/static/init.js
@@ -67,7 +67,7 @@ function load_directory(dir_id, dir_item) {
track_list.selectable({
filter: 'tr',
stop: function(event, ui) {
- $('#directory-add').prop('disabled', $(track_list, ' .ui-selected').length == 0);
+ $('#directory-add').prop('disabled', track_list.find('.ui-selected').length == 0);
return true;
}
});
@@ -75,26 +75,119 @@ function load_directory(dir_id, dir_item) {
$('#directory-add').prop('disabled', true);
}
-function search_results(data) {
- var results = $('#search-results');
- results.empty();
- $.each(data, function(i, track) {
- var item = $(templates.directory_item(track));
- item.data('track', track);
- results.append(item);
+function set_tracks(container, select, click) {
+ return (function(tracks) {
+ container.empty();
+ $.each(tracks, function(i, track) {
+ var el = $(templates.directory_item(track));
+ el.data('track', track);
+ if(click !== undefined) {
+ el.find('a').click(function(event) {
+ click(event, track);
+ });
+ }
+ container.append(el);
+ });
+ if(select !== undefined) {
+ container.selectable({
+ filter: 'tr',
+ stop: function(event, ui) {
+ select.prop('disabled', $(container, ' .ui-selected').length == 0);
+ return true;
+ }
+ });
+ select.prop('disabled', true);
+ }
+ });
+}
+
+function show_album(album) {
+ var tabs = $('#tabs');
+ var tabid = '#album-tab-' + album.id;
+ var tab = $(tabid);
+ if(tab.length > 0) {
+ tabs.tabs('select', tab.index()-1);
+ return;
+ }
+ var tabli = $(templates.album_tabli({tabid: tabid, album: album}));
+ tabs.find('.ui-tabs-nav').append(tabli);
+ var tab = $(templates.album_tab(album));
+ tabs.append(tab).tabs('refresh');
+ var tracks = $(tabid + '-table tbody');
+ var addbutton = $(tab).find('input');
+ addbutton.click(function() {
+ var tracks = $(tabid + '-table tbody tr.ui-selected');
+ tracks.each(function(i, item) {
+ var track = $(item).data('track');
+ playlist.add(track);
+ });
});
- results.selectable({
- filter: 'tr',
- stop: function(event, ui) {
- $('#search-add').prop('disabled', results.children().length == 0);
- return true;
+ $.get('/json/album/' + album.id, set_tracks(tracks, addbutton), 'json');
+ tabs.tabs('select', tab.index()-1);
+}
+
+function add_albums(data) {
+ var div = $('#albums-list');
+ $.each(data, function(i, album) {
+ var el = $(templates.albums_item(album));
+ el.find('a').click(function() {
+ show_album(album);
+ return false;
+ });
+ div.append(el);
+ });
+}
+
+function setup_album_scrolling() {
+ $('#albums-list').scroll(function(event) {
+ if(albums_end)
+ return;
+ var scrolltop = event.target.scrollTop;
+ var scrollheight = event.target.scrollHeight;
+ var height = $(event.target).height();
+ var remaining = (scrollheight - height) - scrolltop;
+ if(remaining < 150 && !albums_loading) {
+ load_albums();
}
});
- $('#search-add').prop('disabled', true);
+}
+
+var albums_page = 0;
+var albums_loading = false;
+var albums_end = false;
+function load_albums(initiate_scrolling) {
+ var page = albums_page;
+ albums_page++;
+ albums_loading = true;
+ $.get('/json/albums/' + page, function(data) {
+ if(data.length > 0)
+ add_albums(data);
+ else
+ albums_end = true;
+ if(initiate_scrolling == true)
+ setup_album_scrolling();
+ albums_loading = false;
+ }, 'json');
}
$(document).ready(function() {
- $('#tabs').tabs();
+ var albums_initially_loaded = false;
+ $('#tabs').tabs({
+ activate: function(event, ui) {
+ if(ui.newPanel.selector == '#albums-tab') {
+ if(!albums_initially_loaded) {
+ load_albums(true);
+ albums_initially_loaded = true;
+ }
+ }
+ }
+ });
+ // Shamlessly stolen from the tabs manipulation example: http://jqueryui.com/tabs/#manipulation
+ $('#tabs span.ui-icon-close').live('click', function() {
+ var panelid = $(this).closest('li').remove().attr('aria-controls');
+ $('#' + panelid).remove();
+ $('#tabs').tabs('refresh');
+ });
preload_images();
load_directory(0);
$('#progress').slider();
@@ -115,12 +208,11 @@ $(document).ready(function() {
$('#search_box').keypress(function(event) {
if(event.keyCode == 13) {
var val = $(this).val();
- $.get('/json/search?q=' + encodeURIComponent(val), search_results, 'json');
+ $.get('/json/search?q=' + encodeURIComponent(val), set_tracks($('#search-results'), $('#search-add')), 'json');
}
});
$('#search-add').click(function(event) {
var tracks = $('#search-results tr.ui-selected');
- console.log(tracks);
tracks.each(function(i, item) {
var track = $(item).data('track');
playlist.add(track);
diff --git a/static/nocover.jpg b/static/nocover.jpg
new file mode 100644
index 0000000..59f725c
--- /dev/null
+++ b/static/nocover.jpg
Binary files differ
diff --git a/static/style.css b/static/style.css
index 34579f7..e7c1d85 100644
--- a/static/style.css
+++ b/static/style.css
@@ -1,7 +1,12 @@
* { padding: 0; margin: 0; }
+html, body, #content { height: 100%; }
+#tabs { height: 90%; }
+
.ui-tabs-nav li a { font-size: small; }
+#tabs #albums-tab.ui-tabs-panel { padding: 0; }
#progress { margin: .5em 1em; width: 300px; }
#tabs { font-size: 100%; }
+
.accessor { text-decoration: underline; }
.track-table { width: 100%; }
@@ -22,3 +27,10 @@
#search_box { border: 1px solid #888; border-radius: 10px; padding: 0 .3em; }
#search-results { margin-top: 1em; }
+
+#albums-tab { height: 95%; }
+#albums-list { overflow: auto; max-height: 100%; }
+.album-tile { width: 128px; display: inline-block; vertical-align: top; padding: 1em; }
+.album-tile a { text-decoration: none; }
+.album-tile a:hover { text-decoration: underline; }
+.album-tile .album-name { font-size: x-small; }
diff --git a/static/templates.js b/static/templates.js
index d0e7057..67be2e0 100644
--- a/static/templates.js
+++ b/static/templates.js
@@ -13,4 +13,7 @@ var templates = new (function Templates() {
this.directory_item = Handlebars.compile('<tr id="{{type}}-{{id}}"><td><a href="#" class="{{type}}{{#unless cache}} nocache{{/unless}}">{{trackname}}</a></td><td>{{metadata.artist}}</td><td>{{metadata.album}}</td></tr>');
// The playlist automatically adds a tr tag.
this.playlist_item = Handlebars.compile('<td><a href="#" class="play">{{trackname}}</a></td><td><a href="#">{{metadata.artist}}</a></td><td><a href="#">{{metadata.album}}</a></td><td class="track-buttons"><a href="#" class="delete"><img src="/static/icons/delete.png" alt="Delete" title="Delete" /></a></td>');
+ this.albums_item = Handlebars.compile('<div class="album-tile" id="albums-album-{{id}}"><a href="#album-tab-{{id}}" title="{{name}} by {{artist.name}}"><img src="/album-cover/{{id}}.jpg" alt="{{name}} by {{artist.name}}" /><br /><span class="album-name">{{name}}</span></div>');
+ this.album_tab = Handlebars.compile('<div id="album-tab-{{id}}"><input type="button" value="Add selected" /><table id="album-tab-{{id}}-table" class="track-table"><tbody><tr><td><img src="/static/icons/loading.gif" alt="Loading..." /></td></tr></tbody></table></div>');
+ this.album_tabli = Handlebars.compile('<li><a href="{{tabid}}">Album: {{album.name}}</a> <span class="ui-icon ui-icon-close">Remove tab</span></li>');
})();