diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rwxr-xr-x | app.py | 51 | ||||
-rw-r--r-- | config.py | 10 | ||||
-rw-r--r-- | coverart.py | 73 | ||||
-rw-r--r-- | db.py | 4 | ||||
-rw-r--r-- | static/index.html | 5 | ||||
-rw-r--r-- | static/init.js | 126 | ||||
-rw-r--r-- | static/nocover.jpg | bin | 0 -> 3339 bytes | |||
-rw-r--r-- | static/style.css | 12 | ||||
-rw-r--r-- | static/templates.js | 3 |
10 files changed, 266 insertions, 19 deletions
@@ -2,4 +2,5 @@ *.pyc /config /cache +/covercache /*.db @@ -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): @@ -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() @@ -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 Binary files differnew file mode 100644 index 0000000..59f725c --- /dev/null +++ b/static/nocover.jpg 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>'); })(); |