Commits

Adam Gomaa committed d46a480

Move static stuff into static/

Comments (0)

Files changed (7)

webmusic/static/music-body.html

+<div class="left height-100">
+  <div id="artists">
+  </div>
+</div>
+<div class="right height-100">
+  <div id="controls">
+    <div class="controls-row">
+      <div id="play-pause">P</div>
+      <div id="time">
+        <div id="elapsed"></div>
+        <div id="remaining"></div>
+      </div>
+      <div id="slider"></div>
+      <div id="duration"></div>
+    </div>
+    <div id="track-details">
+      <div id="current-song">Nothing playing</div>
+      <div>
+        <span class="trigger-resize">trigger resize</span> |
+        <span class="reindex">reindex</span> |
+        reload <span class="reload-artists">artists</span> |
+        <span class="reload-css">css</span> |
+        <span class="reload-js">js</span> |
+        <span class="reload-html">html</span> |
+        <span id="loop-behavior">no loop</span>
+      </div>
+    </div>
+  </div>
+  <div id="listings">
+    <div id="albums"></div>
+    <div id="songs"></div>
+  </div>
+  <div id="console"></div>
+</div>
+<div id="templates" style="display:none;">
+<div id="artist-template">
+  <span class="artist-name"><%= name %></span>
+  <span class="artist-counts">(<%= num_albums %>/<%= num_songs %>)</span>
+</div>
+<div id="album-template">
+  <span class="album-name"><%= text %></span>
+</div>
+<div id="song-template">
+  <span class="song-name"><%= text %></span>
+  <span class="duration"><%= duration %></span>
+</div>
+</div>

webmusic/static/music.css

+body,div
+{padding:0;margin:0;}
+body
+{
+    padding:0;margin:0;
+    font-family: Liberation Sans;
+    color: #fff;
+    background-color: #000;
+    position: relative;
+    overflow:hidden;
+}
+
+.controls-row{ height:50px }
+.controls-row>div{float:left;}
+
+.height-100{height: 100%;}
+
+body>.left,
+body>.right
+{position: absolute;}
+body>.left{left:0;}
+body>.right{right:0;}
+/*.left{width: 67%;}
+.right{width: 33%;}*/
+.left{width: 75%;}
+.right{width: 25%;}
+
+#listings>div
+{
+    float: left;
+    width: 50%;
+}
+
+#albums > div,
+#songs > div
+{
+    border-bottom: solid #333 1px;
+    padding: 3px 0px 3px 3px;
+    font-family: Liberation Sans;
+    color: #fff;
+}
+
+#artists
+{
+    overflow:auto;
+    font-family: Liberation Sans;
+    font-size: 19px;
+    background-color: #000;
+}
+#artists .column{position: absolute; top: 0;}
+#artists .column:first-child{margin-left: 0;}
+.artist
+{
+    cursor: pointer;
+    white-space: nowrap;
+    overflow: hidden;
+}
+.artist.odd
+{
+    color: #ddf;
+/*    background-color: #004;*/
+}
+.artist.even
+{
+    color: #dfd;
+/*    background-color: #003800;*/
+}
+.artist.selected
+{
+    color: #fff;
+    background-color: #060;
+}
+.artist.search-not-matched
+{
+    color: #666!important;
+    background-color: #000!important;
+}
+.artist:hover
+{
+    color: #fff;
+    background-color: #333;
+    overflow:visible;
+    z-index:1;
+    width: auto!important;
+    position:relative;
+}
+
+#albums > div.selected,
+#songs > div.selected
+{
+    background-color: #080;
+}
+
+/* Hide controls until they get initialized */
+#controls{display:none;}
+
+input.search
+{
+    background-color: #300;
+    padding: 6px;
+    border: solid #888 1px;
+    color: #fff;
+    font-size: 20px;
+    display: block;
+}
+#current-song{}
+#duration{line-height: 50px;}
+#time{line-height: 20px; font-size:15px;}
+#track-details
+{
+    overflow: hidden;
+}
+#track-details-inner,#console
+{
+    color: #eee;
+    font-size: 12px;
+    font-family: monospace;
+}
+#console
+{
+    background-color:#222;
+    white-space:normal;
+    overflow-x: hidden;
+    overflow-y: hidden;
+    position: absolute;
+    width: 100%;
+    bottom:0;
+}
+
+#play-pause
+{
+    margin-top: 10px;
+    display: inline;
+    width: 80px;
+}
+#play-pause .ui-button-text
+{
+    display: inline;
+    padding: 5px 13px;
+    line-height: 1;
+}
+
+#slider
+{
+    margin-top: 15px;
+    margin-left: 5px;
+    margin-right: 5px;
+}
+
+
+#slider .ui-slider-handle
+{
+    width: 10px;
+}
+#slider .ui-slider-handle.paused
+{
+    background-image: none;
+    background-color: #000;
+}
+
+#slider .ui-slider-handle
+{
+    margin-left: -5px;
+}
+
+
+.trigger-resize,
+.reindex,
+.reload-artists,
+.reload-css,
+.reload-html,
+.reload-js,
+#loop-behavior
+{
+    cursor: pointer;
+    text-decoration: underline;
+}
+
+#songs .duration
+{
+    float: right;
+    color: #aaa;
+}

webmusic/static/music.js

+$(function(){
+    var get_template = function(selector){
+        return _.template($(selector).html().replace(/&lt;/g, "<").replace(/&gt;/g, ">"));
+    }
+    var format_time = function(duration){
+        var mins = Math.floor(duration / 60);
+        var secs = Math.floor(duration % 60);
+        if(secs<10){secs = "0" + secs}
+        return mins + ":" + secs;
+    }
+
+    var Artist = Backbone.Model.extend({});
+    var Album = Backbone.Model.extend({});
+    var Song = Backbone.Model.extend({
+        duration_display: function(){
+            return format_time(this.get("duration"));
+        }
+    });
+
+    var ArtistCollection = Backbone.Collection.extend({model: Artist});
+    var AlbumCollection = Backbone.Collection.extend({model: Album});
+    var SongCollection = Backbone.Collection.extend({model: Song});
+
+    // Main application view
+    var MusicApp = Backbone.View.extend({
+        events: {
+            "keydown": "handleGlobalKey",
+        },
+        /* This doesn't seem quite kosher. But this way I can catch
+         * global keys in the normal declarative way. */
+        el: $(window.document),
+        initialize: function(){
+            this._inittime = new Date();
+            this._apptime = 0;
+            _.bindAll(this, "initBody", "log", "initViews",
+                      "onResize", "appTime", "loadSong",
+                      "startPlayback", "seekRatio", "seekTime",
+                      "timeUpdate", "audioEnded", "handleGlobalKey", "post");
+            this.url = _music_url
+            this.ajax_url = this.url + "_ajax";
+            this.media_url = this.url + "getmedia/";
+            this.loop = null;
+            this.loop_states = [null, 'song', 'album'];
+            this.slider_max = 500;
+            this.console_lines = 10;
+            this.initBody().then(this.initViews);
+            /* jquery object of nodes that have a namespaced .music event
+             * handler */
+            this.$bound = $();
+        },
+        initBody: function(){
+            return $.get(this.url + "static/music-body.html").then(function(data){
+                $("body").html(data);
+            });
+        },
+        log: function(msg){
+            this.console.clog("[" + this.appTime() + "] " + msg);
+        },
+        post: function(action, data){
+            if(!data){data={} };
+            data.action = action;
+            return $.post(this.ajax_url, data, "json");
+        },
+        appTime: function(){
+            /* seconds this has run */
+            var s = (new Date() - this._inittime) / 1000;
+            var d = s - this._apptime;
+            this._apptime = s;
+            return "" + format_time(s) + "+" + format_time(d);
+        },
+        mkaudio: function(){
+            $("body").find("audio").remove()
+            $("body").append("<audio></audio>");
+            this.$audio = $("audio")
+            this.$audio.attr("preload", "auto")
+            this.audio = this.$audio[0];
+
+            this.$audio.bind("canplaythrough", this.startPlayback);
+            this.$audio.bind("timeupdate", _.throttle(this.timeUpdate, 500));
+            this.audio.addEventListener("ended", this.audioEnded)
+            //this.$audio.bind("ended", this.audioEnded);
+        },
+        initViews: function(){
+            this.mkaudio();
+            this.console = new ConsoleView({app: this, el: $("#console")});
+            this.controls = new ControlsView({app: this, el: $("#controls")});
+            this.artists = new ArtistsView({app: this, el: $("#artists")});
+            this.albums = new AlbumsView({app: this, el: $("#albums")});
+            this.songs = new SongsView({app: this, el: $("#songs")});
+            $(window).bind("resize", _.debounce(this.onResize, 500));
+            //$(window).bind("keydown", this.handleGlobalKey);
+            this.onResize();
+        },
+        onResize: function(){
+            this.log("resizing");
+            $("body").css("height", $(window).height());
+            this.controls.resizeControls();
+            this.artists.render();
+        },
+        loadSong: function(song){
+            if(!this.audio.paused){
+                this.controls.playPauseClick();
+            }
+            var song_text = song["title"];
+            var song_path = song['path'];
+            $("title").text(song_text + " - webmusic");
+            var src = this.media_url + song.get("_id").$oid;
+            this.log("src:" + src)
+            this.mkaudio();
+            this.$audio.attr("src", src);
+            this.audio.load();
+        },
+        startPlayback: function(){
+            this.audio.play();
+            this.controls.$duration.text(format_time(this.audio.duration));
+        },
+        seekRatio: function(ratio){
+            this.seekTime(this.audio.duration * ratio);
+        },
+        seekTime: function(time){
+            this.audio.currentTime = time;
+            this.log("seeking to: " + format_time(time));
+        },
+        timeUpdate: function(){
+            var completion = this.audio.currentTime / this.audio.duration;
+            this.controls.$slider.slider("value", completion * 500);
+            this.controls.$remaining.text(format_time(this.audio.duration - this.audio.currentTime));
+            this.controls.$elapsed.text(format_time(this.audio.currentTime));
+        },
+        audioEnded: function(){
+            this.log("audio ended");
+            if(!this.loop){
+                $(this.songs.el).find(">.selected").next().trigger("click");
+            }else if(this.loop=="song"){
+                $(this.songs.el).find(">.selected").trigger("click");
+            }else if(this.loop=="album"){
+                if($(this.songs.el).find(">.selected").next().length){
+                    $(this.songs.el).find(">.selected").next().trigger("click");
+                }else{
+                    $(this.songs.el).children().first().trigger("click")
+                }
+            }
+        },
+        globalKeys: {
+            // " " space
+            32: function(evt){
+                app.controls.playPauseClick();
+            }
+        },
+        // I wasted more time naming this method than I care to admit.
+        handleGlobalKey: function(evt){
+            if(evt.which in this.globalKeys){
+                this.globalKeys[evt.which](evt);
+            }
+        }
+    });
+
+
+    var AppView = Backbone.View.extend({
+        initialize: function(options){
+            this.app = this.options.app;
+            this.log = this.app.log;
+            this.post = this.app.post;
+        },
+    });
+
+
+    var ConsoleView = AppView.extend({
+        initalize: function(options){
+            this.constructor.__super__.initialize.apply(this, [options])
+            _.bindAll(this, "clog")
+        },
+        clog: function(msg){
+            if($(this.el).children().length>9){
+                $(this.el).children(":first").remove();
+            }
+            $(this.el).append("<div>" + msg + "</div>")
+            return this;
+        }
+    });
+    var ControlsView = AppView.extend({
+        events: {
+            "click #play-pause": "playPauseClick",
+            "slidestop #slider": "slideSeek",
+            "click .trigger-resize": "clickResize",
+            "click .reindex": "clickReindex",
+            "click .reload-artists": "reloadArtists",
+            "click .reload-css": "reloadCss",
+            "click .reload-js": "reloadJs",
+            "click .reload-html": "reloadHtml",
+            "click #loop-behavior": "cycleLoopStates",
+        },
+        initialize: function(options){
+            this.constructor.__super__.initialize.apply(this, [options]);
+            _.bindAll(this, "slideSeek", "resizeControls",
+                      "playPauseClick", "clickResize",
+                      "clickReindex",
+                      "reloadArtists", "reloadCss", "reloadJs", "reloadHtml",
+                      "cycleLoopStates");
+            this.app.log("ControlsView initializing");
+            this.$playbtn = this.$("#play-pause");
+            this.$playbtn.button();
+
+            this.$time = this.$("#time");
+            this.$elapsed = this.$("#elapsed");
+            this.$duration = this.$("#duration");
+            this.$remaining = this.$("#remaining");
+
+            this.$elapsed.text("00:00")
+            this.$duration.text("00:00")
+            this.$remaining.text("00:00")
+
+            this.$slider = this.$("#slider");
+            this.$slider.slider({max: 500})
+
+            $(this.el).show();
+            this.app.log("ControlsView initialization done")
+        },
+        slideSeek: function(event, ui){
+            var completion = ui.value / 500;
+            this.app.seekRatio(completion);
+        },
+        resizeControls: function(){
+            /* Resize the controls.
+
+               Get the width of the controls container, subtract the width of
+               all the controls except for the slider, then set the slider
+               width to that.
+
+            */
+            var $row = this.$(".controls-row")
+            var d = {};
+            d.row = $row.width();
+            d.play = $row.find("#play-pause").outerWidth(true);
+            d.time = this.$time.outerWidth(true);
+            d.duration = this.$duration.outerWidth(true);
+            /* total margin/border on slider, both sides */
+            d.slider_extra = this.$slider.outerWidth(true) - this.$slider.width();
+            d.slider_prev = this.$slider.width();
+
+            d.slider_new = d.row - (d.play + d.time + d.duration + d.slider_extra);
+
+            this.$slider.width(d.slider_new);
+        },
+        playPauseClick: function(){
+            if(this.app.audio.paused){
+                this.app.audio.play();
+                this.$playbtn.text("pause")
+                this.$(".ui-slider-handle").removeClass("paused")
+            }else{
+                this.app.audio.pause();
+                this.$playbtn.text("play")
+                this.$(".ui-slider-handle").addClass("paused")
+            }
+        },
+        clickReindex: function(){
+            var postdata = {action: "reindex"};
+            this.post("reindex");
+        },
+        clickResize: function(){
+            $(window).trigger("resize");
+        },
+        reloadArtists: function(){/* TODO */},
+        reloadCss: function(){/* TODO */},
+        reloadJs: function(){/* TODO */},
+        reloadHtml: function(){/* TODO */},
+        cycleLoopStates: function(){
+            var thisindex = this.app.loop_states.indexOf(this.app.loop);
+            var nextindex = (thisindex+1) % this.app.loop_states.length;
+            this.app.loop = this.app.loop_states[nextindex];
+            if(this.app.loop){
+                this.$("#loop-behavior").text("loop " + this.app.loop)
+            }else{
+                this.$("#loop-behavior").text("no loop");
+            }
+        },
+    });
+
+
+
+
+    var ArtistsView = AppView.extend({
+        events: {
+            "click": "clickArtist"
+        },
+        initialize: function(options){
+            _.bindAll(this, "loadArtists", "render", 'artistNodes', 'clickArtist');
+            this.constructor.__super__.initialize.apply(this, [options]);
+            this.log("ArtistsView Initializing");
+            this.artists = new ArtistCollection();
+            this.artists.bind("reset", this.render);
+            this.loadArtists().then(_.bind(function(){
+                this.app.log("ArtistsView Initialization Done");
+            }, this));
+        },
+        loadArtists: function(){
+            var view = this;
+            return this.post("get-artists").then(function(data){
+                view.artists.reset(data);
+            });
+        },
+        render: function(){
+            $(this.el).empty();
+            var artists_height = $(this.el).height();
+            var window_height = $(window).height();
+            var num_columns_wanted = Math.ceil(artists_height / window_height);
+            var artist_nodes = this.artistNodes();
+            var column = $(this.el).append('<div class="column"></div>').find(".column:last");
+            var newColumn = _.bind(function(){
+                return $(this.el).append('<div class="column"></div>').find(".column:last");
+            }, this);
+            artist_nodes.each(function(){
+                column.append(this);
+                if(column.children().last().position().top+column.children().last().height()>window_height){
+                    var node = column.children().last().detach();
+                    column = newColumn();
+                    column.append(node);
+                }
+            });
+            var len_columns = this.$(".column").length;
+            var col_width_perc = 100;
+            var margin_perc = 1
+            /* Subject margin per column */
+            col_width_perc -= len_columns * margin_perc;
+            var col_width_perc = col_width_perc / len_columns;
+            this.$(".column").each(function(ii){
+                $(this).width(col_width_perc + "%");
+                $(this).css("left", ((col_width_perc+margin_perc) * ii) + "%");
+            });
+        },
+        artistNodes: function(){
+            return $(this.artists.map(function(a, i){
+                var view = new SingleArtistView({app: app, artist: a});
+                var el = view.render().el;
+                if(i%2){$(el).addClass("even")}
+                else{$(el).addClass("odd")}
+                return el;
+            }));
+        },
+        clickArtist: function(event){
+            $(event.target).closest(".artist").data("view").handleClick();;
+        }
+    });
+    var SingleArtistView = AppView.extend({
+        className: "artist",
+        initialize: function(options){
+            _.bindAll(this, "render", "handleClick")
+            this.constructor.__super__.initialize.apply(this, [options])
+            this.artist = options.artist;
+            $(this.el).data("view", this);
+        },
+        render: function(){
+            var template_context = {
+                name: this.artist.get("name"),
+                num_albums: this.artist.get("num_albums"),
+                num_songs: this.artist.get("num_songs"),
+            };
+            $(this.el).html(get_template("#artist-template")(template_context))
+            return this;
+        },
+        handleClick: function(){
+            this.app.$(".artist.selected").removeClass("selected");
+            $(this.el).addClass("selected");
+            this.post("get-albums", {artist: this.artist.get("_id").$oid})
+                .then(function(albums){app.albums.albums.reset(albums)});
+        }
+
+    });
+    var AlbumsView = AppView.extend({
+        events: {
+        },
+        initialize: function(options){
+            _.bindAll(this, "resetAlbums", "addAlbum");
+            this.constructor.__super__.initialize.apply(this, [options])
+            this.albums = new AlbumCollection();
+            this.albums.bind("reset", this.resetAlbums);
+        },
+        resetAlbums: function(){
+            $(this.el).empty();
+            this.albums.each(this.addAlbum);
+        },
+        addAlbum: function(album){
+            var view = new SingleAlbumView({app: this.app, album: album});
+            $(this.el).append(view.render().el);
+        }
+    });
+    var SingleAlbumView = AppView.extend({
+        className: "album",
+        events: {"click": "handleClick"},
+        initialize: function(options){
+            this.constructor.__super__.initialize.apply(this, [options])
+            this.album = options.album;
+            _.bindAll(this, "render", "handleClick");
+        },
+        render: function(){
+            var album_text;
+            var album = this.album;
+            if(album.get("year")){
+                album_text = album.get("year") + " - " + album.get("name");
+            }else{
+                album_text = album.get("name");
+            }
+            var template_context = {text: album_text};
+            $(this.el).html(get_template("#album-template")(template_context))
+            return this;
+        },
+        handleClick: function(){
+            $(this.el).addClass("selected").siblings().removeClass("selected");
+            var album_id = this.album.get("_id")['$oid'];
+            var postdata = {album: album_id};
+            this.post("get-songs", postdata).then(function(songs){
+                app.songs.songs.reset(songs);
+            });
+        }
+    })
+
+    var SongsView = AppView.extend({
+        initialize: function(options){
+            _.bindAll(this, "resetSongs", "addSong");
+            this.constructor.__super__.initialize.apply(this, [options])
+            this.songs = new SongCollection();
+            this.songs.bind("reset", this.resetSongs);
+        },
+        resetSongs: function(){
+            $(this.el).empty();
+            this.songs.each(this.addSong);
+        },
+        addSong: function(song){
+            var view = new SingleSongView({app: this.app, song: song});
+            $(this.el).append(view.render().el);
+        }
+    });
+
+    var SingleSongView = AppView.extend({
+        className: "song",
+        events: {"click": "handleClick"},
+        initialize: function(options){
+            this.constructor.__super__.initialize.apply(this, [options]);
+            this.song = options.song;
+            _.bindAll(this, "handleClick", "render");
+        },
+        handleClick: function(event){
+            $(this.el).addClass("selected").siblings().removeClass("selected");
+            this.app.loadSong(this.song);
+        },
+        render: function(){
+            var template_context = {
+                text: this.song.get("title"),
+                duration: this.song.duration_display(),
+            }
+            $(this.el).html(get_template("#song-template")(template_context));
+            return this;
+        }
+    });
+
+    window.app = new MusicApp();
+});

webmusic/templates/music-body.html

-<div class="left height-100">
-  <div id="artists">
-  </div>
-</div>
-<div class="right height-100">
-  <div id="controls">
-    <div class="controls-row">
-      <div id="play-pause">P</div>
-      <div id="time">
-        <div id="elapsed"></div>
-        <div id="remaining"></div>
-      </div>
-      <div id="slider"></div>
-      <div id="duration"></div>
-    </div>
-    <div id="track-details">
-      <div id="current-song">Nothing playing</div>
-      <div>
-        <span class="trigger-resize">trigger resize</span> |
-        <span class="reindex">reindex</span> |
-        reload <span class="reload-artists">artists</span> |
-        <span class="reload-css">css</span> |
-        <span class="reload-js">js</span> |
-        <span class="reload-html">html</span> |
-        <span id="loop-behavior">no loop</span>
-      </div>
-    </div>
-  </div>
-  <div id="listings">
-    <div id="albums"></div>
-    <div id="songs"></div>
-  </div>
-  <div id="console"></div>
-</div>
-<div id="templates" style="display:none;">
-<div id="artist-template">
-  <span class="artist-name"><%= name %></span>
-  <span class="artist-counts">(<%= num_albums %>/<%= num_songs %>)</span>
-</div>
-<div id="album-template">
-  <span class="album-name"><%= text %></span>
-</div>
-<div id="song-template">
-  <span class="song-name"><%= text %></span>
-  <span class="duration"><%= duration %></span>
-</div>
-</div>

webmusic/templates/music.css

-body,div
-{padding:0;margin:0;}
-body
-{
-    padding:0;margin:0;
-    font-family: Liberation Sans;
-    color: #fff;
-    background-color: #000;
-    position: relative;
-    overflow:hidden;
-}
-
-.controls-row{ height:50px }
-.controls-row>div{float:left;}
-
-.height-100{height: 100%;}
-
-body>.left,
-body>.right
-{position: absolute;}
-body>.left{left:0;}
-body>.right{right:0;}
-/*.left{width: 67%;}
-.right{width: 33%;}*/
-.left{width: 75%;}
-.right{width: 25%;}
-
-#listings>div
-{
-    float: left;
-    width: 50%;
-}
-
-#albums > div,
-#songs > div
-{
-    border-bottom: solid #333 1px;
-    padding: 3px 0px 3px 3px;
-    font-family: Liberation Sans;
-    color: #fff;
-}
-
-#artists
-{
-    overflow:auto;
-    font-family: Liberation Sans;
-    font-size: 19px;
-    background-color: #000;
-}
-#artists .column{position: absolute; top: 0;}
-#artists .column:first-child{margin-left: 0;}
-.artist
-{
-    cursor: pointer;
-    white-space: nowrap;
-    overflow: hidden;
-}
-.artist.odd
-{
-    color: #ddf;
-/*    background-color: #004;*/
-}
-.artist.even
-{
-    color: #dfd;
-/*    background-color: #003800;*/
-}
-.artist.selected
-{
-    color: #fff;
-    background-color: #060;
-}
-.artist.search-not-matched
-{
-    color: #666!important;
-    background-color: #000!important;
-}
-.artist:hover
-{
-    color: #fff;
-    background-color: #333;
-    overflow:visible;
-    z-index:1;
-    width: auto!important;
-    position:relative;
-}
-
-#albums > div.selected,
-#songs > div.selected
-{
-    background-color: #080;
-}
-
-/* Hide controls until they get initialized */
-#controls{display:none;}
-
-input.search
-{
-    background-color: #300;
-    padding: 6px;
-    border: solid #888 1px;
-    color: #fff;
-    font-size: 20px;
-    display: block;
-}
-#current-song{}
-#duration{line-height: 50px;}
-#time{line-height: 20px; font-size:15px;}
-#track-details
-{
-    overflow: hidden;
-}
-#track-details-inner,#console
-{
-    color: #eee;
-    font-size: 12px;
-    font-family: monospace;
-}
-#console
-{
-    background-color:#222;
-    white-space:normal;
-    overflow-x: hidden;
-    overflow-y: hidden;
-    position: absolute;
-    width: 100%;
-    bottom:0;
-}
-
-#play-pause
-{
-    margin-top: 10px;
-    display: inline;
-    width: 80px;
-}
-#play-pause .ui-button-text
-{
-    display: inline;
-    padding: 5px 13px;
-    line-height: 1;
-}
-
-#slider
-{
-    margin-top: 15px;
-    margin-left: 5px;
-    margin-right: 5px;
-}
-
-
-#slider .ui-slider-handle
-{
-    width: 10px;
-}
-#slider .ui-slider-handle.paused
-{
-    background-image: none;
-    background-color: #000;
-}
-
-#slider .ui-slider-handle
-{
-    margin-left: -5px;
-}
-
-
-.trigger-resize,
-.reindex,
-.reload-artists,
-.reload-css,
-.reload-html,
-.reload-js,
-#loop-behavior
-{
-    cursor: pointer;
-    text-decoration: underline;
-}
-
-#songs .duration
-{
-    float: right;
-    color: #aaa;
-}

webmusic/templates/music.html

 <!DOCTYPE html>
 <html>
 <head>
+{% macro static(filename) %}{{ url_for("static", filename=filename) }}{% endmacro %}
   <title>webmusic</title>
-  <link rel="stylesheet" href="{{ music_url }}getmedia/__music.css">
+  <link rel="stylesheet" href="{{ static("music.css") }}">
   <link rel="stylesheet" href="//ajax.googleapis.com/ajax/libs/jqueryui/1.8.17/themes/vader/jquery-ui.css">
 {% if false %}{# for debugging JS stuff #}
   <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.js"></script>
 {# music_js is static, so define the urls here. #}
 _music_url = "{{ music_url }}";
 </script>
-<script type="text/javascript" src="{{ music_url }}getmedia/__music.js"></script>
+<script type="text/javascript" src="{{ static("music.js") }}"></script>
 </head>
 <body></body>
 </html>

webmusic/templates/music.js

-$(function(){
-    var get_template = function(selector){
-        return _.template($(selector).html().replace(/&lt;/g, "<").replace(/&gt;/g, ">"));
-    }
-    var format_time = function(duration){
-        var mins = Math.floor(duration / 60);
-        var secs = Math.floor(duration % 60);
-        if(secs<10){secs = "0" + secs}
-        return mins + ":" + secs;
-    }
-
-    var Artist = Backbone.Model.extend({});
-    var Album = Backbone.Model.extend({});
-    var Song = Backbone.Model.extend({
-        duration_display: function(){
-            return format_time(this.get("duration"));
-        }
-    });
-
-    var ArtistCollection = Backbone.Collection.extend({model: Artist});
-    var AlbumCollection = Backbone.Collection.extend({model: Album});
-    var SongCollection = Backbone.Collection.extend({model: Song});
-
-    // Main application view
-    var MusicApp = Backbone.View.extend({
-        events: {
-            "keydown": "handleGlobalKey",
-        },
-        /* This doesn't seem quite kosher. But this way I can catch
-         * global keys in the normal declarative way. */
-        el: $(window.document),
-        initialize: function(){
-            this._inittime = new Date();
-            this._apptime = 0;
-            _.bindAll(this, "initBody", "log", "initViews",
-                      "onResize", "appTime", "loadSong",
-                      "startPlayback", "seekRatio", "seekTime",
-                      "timeUpdate", "audioEnded", "handleGlobalKey", "post");
-            this.url = _music_url
-            this.ajax_url = this.url + "_ajax";
-            this.media_url = this.url + "getmedia/";
-            this.loop = null;
-            this.loop_states = [null, 'song', 'album'];
-            this.slider_max = 500;
-            this.console_lines = 10;
-            this.initBody().then(this.initViews);
-            /* jquery object of nodes that have a namespaced .music event
-             * handler */
-            this.$bound = $();
-        },
-        initBody: function(){
-            return $.get(this.media_url + "__body.html").then(function(data){
-                $("body").html(data);
-            });
-        },
-        log: function(msg){
-            this.console.clog("[" + this.appTime() + "] " + msg);
-        },
-        post: function(action, data){
-            if(!data){data={} };
-            data.action = action;
-            return $.post(this.ajax_url, data, "json");
-        },
-        appTime: function(){
-            /* seconds this has run */
-            var s = (new Date() - this._inittime) / 1000;
-            var d = s - this._apptime;
-            this._apptime = s;
-            return "" + format_time(s) + "+" + format_time(d);
-        },
-        mkaudio: function(){
-            $("body").find("audio").remove()
-            $("body").append("<audio></audio>");
-            this.$audio = $("audio")
-            this.$audio.attr("preload", "auto")
-            this.audio = this.$audio[0];
-
-            this.$audio.bind("canplaythrough", this.startPlayback);
-            this.$audio.bind("timeupdate", _.throttle(this.timeUpdate, 500));
-            this.audio.addEventListener("ended", this.audioEnded)
-            //this.$audio.bind("ended", this.audioEnded);
-        },
-        initViews: function(){
-            this.mkaudio();
-            this.console = new ConsoleView({app: this, el: $("#console")});
-            this.controls = new ControlsView({app: this, el: $("#controls")});
-            this.artists = new ArtistsView({app: this, el: $("#artists")});
-            this.albums = new AlbumsView({app: this, el: $("#albums")});
-            this.songs = new SongsView({app: this, el: $("#songs")});
-            $(window).bind("resize", _.debounce(this.onResize, 500));
-            //$(window).bind("keydown", this.handleGlobalKey);
-            this.onResize();
-        },
-        onResize: function(){
-            this.log("resizing");
-            $("body").css("height", $(window).height());
-            this.controls.resizeControls();
-            this.artists.render();
-        },
-        loadSong: function(song){
-            if(!this.audio.paused){
-                this.controls.playPauseClick();
-            }
-            var song_text = song["title"];
-            var song_path = song['path'];
-            $("title").text(song_text + " - webmusic");
-            var src = this.media_url + song.get("_id").$oid;
-            this.log("src:" + src)
-            this.mkaudio();
-            this.$audio.attr("src", src);
-            this.audio.load();
-        },
-        startPlayback: function(){
-            this.audio.play();
-            this.controls.$duration.text(format_time(this.audio.duration));
-        },
-        seekRatio: function(ratio){
-            this.seekTime(this.audio.duration * ratio);
-        },
-        seekTime: function(time){
-            this.audio.currentTime = time;
-            this.log("seeking to: " + format_time(time));
-        },
-        timeUpdate: function(){
-            var completion = this.audio.currentTime / this.audio.duration;
-            this.controls.$slider.slider("value", completion * 500);
-            this.controls.$remaining.text(format_time(this.audio.duration - this.audio.currentTime));
-            this.controls.$elapsed.text(format_time(this.audio.currentTime));
-        },
-        audioEnded: function(){
-            this.log("audio ended");
-            if(!this.loop){
-                $(this.songs.el).find(">.selected").next().trigger("click");
-            }else if(this.loop=="song"){
-                $(this.songs.el).find(">.selected").trigger("click");
-            }else if(this.loop=="album"){
-                if($(this.songs.el).find(">.selected").next().length){
-                    $(this.songs.el).find(">.selected").next().trigger("click");
-                }else{
-                    $(this.songs.el).children().first().trigger("click")
-                }
-            }
-        },
-        globalKeys: {
-            // " " space
-            32: function(evt){
-                app.controls.playPauseClick();
-            }
-        },
-        // I wasted more time naming this method than I care to admit.
-        handleGlobalKey: function(evt){
-            if(evt.which in this.globalKeys){
-                this.globalKeys[evt.which](evt);
-            }
-        }
-    });
-
-
-    var AppView = Backbone.View.extend({
-        initialize: function(options){
-            this.app = this.options.app;
-            this.log = this.app.log;
-            this.post = this.app.post;
-        },
-    });
-
-
-    var ConsoleView = AppView.extend({
-        initalize: function(options){
-            this.constructor.__super__.initialize.apply(this, [options])
-            _.bindAll(this, "clog")
-        },
-        clog: function(msg){
-            if($(this.el).children().length>9){
-                $(this.el).children(":first").remove();
-            }
-            $(this.el).append("<div>" + msg + "</div>")
-            return this;
-        }
-    });
-    var ControlsView = AppView.extend({
-        events: {
-            "click #play-pause": "playPauseClick",
-            "slidestop #slider": "slideSeek",
-            "click .trigger-resize": "clickResize",
-            "click .reindex": "clickReindex",
-            "click .reload-artists": "reloadArtists",
-            "click .reload-css": "reloadCss",
-            "click .reload-js": "reloadJs",
-            "click .reload-html": "reloadHtml",
-            "click #loop-behavior": "cycleLoopStates",
-        },
-        initialize: function(options){
-            this.constructor.__super__.initialize.apply(this, [options]);
-            _.bindAll(this, "slideSeek", "resizeControls",
-                      "playPauseClick", "clickResize",
-                      "clickReindex",
-                      "reloadArtists", "reloadCss", "reloadJs", "reloadHtml",
-                      "cycleLoopStates");
-            this.app.log("ControlsView initializing");
-            this.$playbtn = this.$("#play-pause");
-            this.$playbtn.button();
-
-            this.$time = this.$("#time");
-            this.$elapsed = this.$("#elapsed");
-            this.$duration = this.$("#duration");
-            this.$remaining = this.$("#remaining");
-
-            this.$elapsed.text("00:00")
-            this.$duration.text("00:00")
-            this.$remaining.text("00:00")
-
-            this.$slider = this.$("#slider");
-            this.$slider.slider({max: 500})
-
-            $(this.el).show();
-            this.app.log("ControlsView initialization done")
-        },
-        slideSeek: function(event, ui){
-            var completion = ui.value / 500;
-            this.app.seekRatio(completion);
-        },
-        resizeControls: function(){
-            /* Resize the controls.
-
-               Get the width of the controls container, subtract the width of
-               all the controls except for the slider, then set the slider
-               width to that.
-
-            */
-            var $row = this.$(".controls-row")
-            var d = {};
-            d.row = $row.width();
-            d.play = $row.find("#play-pause").outerWidth(true);
-            d.time = this.$time.outerWidth(true);
-            d.duration = this.$duration.outerWidth(true);
-            /* total margin/border on slider, both sides */
-            d.slider_extra = this.$slider.outerWidth(true) - this.$slider.width();
-            d.slider_prev = this.$slider.width();
-
-            d.slider_new = d.row - (d.play + d.time + d.duration + d.slider_extra);
-
-            this.$slider.width(d.slider_new);
-        },
-        playPauseClick: function(){
-            if(this.app.audio.paused){
-                this.app.audio.play();
-                this.$playbtn.text("pause")
-                this.$(".ui-slider-handle").removeClass("paused")
-            }else{
-                this.app.audio.pause();
-                this.$playbtn.text("play")
-                this.$(".ui-slider-handle").addClass("paused")
-            }
-        },
-        clickReindex: function(){
-            var postdata = {action: "reindex"};
-            this.post("reindex");
-        },
-        clickResize: function(){
-            $(window).trigger("resize");
-        },
-        reloadArtists: function(){/* TODO */},
-        reloadCss: function(){/* TODO */},
-        reloadJs: function(){/* TODO */},
-        reloadHtml: function(){/* TODO */},
-        cycleLoopStates: function(){
-            var thisindex = this.app.loop_states.indexOf(this.app.loop);
-            var nextindex = (thisindex+1) % this.app.loop_states.length;
-            this.app.loop = this.app.loop_states[nextindex];
-            if(this.app.loop){
-                this.$("#loop-behavior").text("loop " + this.app.loop)
-            }else{
-                this.$("#loop-behavior").text("no loop");
-            }
-        },
-    });
-
-
-
-
-    var ArtistsView = AppView.extend({
-        events: {
-            "click": "clickArtist"
-        },
-        initialize: function(options){
-            _.bindAll(this, "loadArtists", "render", 'artistNodes', 'clickArtist');
-            this.constructor.__super__.initialize.apply(this, [options]);
-            this.log("ArtistsView Initializing");
-            this.artists = new ArtistCollection();
-            this.artists.bind("reset", this.render);
-            this.loadArtists().then(_.bind(function(){
-                this.app.log("ArtistsView Initialization Done");
-            }, this));
-        },
-        loadArtists: function(){
-            var view = this;
-            return this.post("get-artists").then(function(data){
-                view.artists.reset(data);
-            });
-        },
-        render: function(){
-            $(this.el).empty();
-            var artists_height = $(this.el).height();
-            var window_height = $(window).height();
-            var num_columns_wanted = Math.ceil(artists_height / window_height);
-            var artist_nodes = this.artistNodes();
-            var column = $(this.el).append('<div class="column"></div>').find(".column:last");
-            var newColumn = _.bind(function(){
-                return $(this.el).append('<div class="column"></div>').find(".column:last");
-            }, this);
-            artist_nodes.each(function(){
-                column.append(this);
-                if(column.children().last().position().top+column.children().last().height()>window_height){
-                    var node = column.children().last().detach();
-                    column = newColumn();
-                    column.append(node);
-                }
-            });
-            var len_columns = this.$(".column").length;
-            var col_width_perc = 100;
-            var margin_perc = 1
-            /* Subject margin per column */
-            col_width_perc -= len_columns * margin_perc;
-            var col_width_perc = col_width_perc / len_columns;
-            this.$(".column").each(function(ii){
-                $(this).width(col_width_perc + "%");
-                $(this).css("left", ((col_width_perc+margin_perc) * ii) + "%");
-            });
-        },
-        artistNodes: function(){
-            return $(this.artists.map(function(a, i){
-                var view = new SingleArtistView({app: app, artist: a});
-                var el = view.render().el;
-                if(i%2){$(el).addClass("even")}
-                else{$(el).addClass("odd")}
-                return el;
-            }));
-        },
-        clickArtist: function(event){
-            $(event.target).closest(".artist").data("view").handleClick();;
-        }
-    });
-    var SingleArtistView = AppView.extend({
-        className: "artist",
-        initialize: function(options){
-            _.bindAll(this, "render", "handleClick")
-            this.constructor.__super__.initialize.apply(this, [options])
-            this.artist = options.artist;
-            $(this.el).data("view", this);
-        },
-        render: function(){
-            var template_context = {
-                name: this.artist.get("name"),
-                num_albums: this.artist.get("num_albums"),
-                num_songs: this.artist.get("num_songs"),
-            };
-            $(this.el).html(get_template("#artist-template")(template_context))
-            return this;
-        },
-        handleClick: function(){
-            this.app.$(".artist.selected").removeClass("selected");
-            $(this.el).addClass("selected");
-            this.post("get-albums", {artist: this.artist.get("_id").$oid})
-                .then(function(albums){app.albums.albums.reset(albums)});
-        }
-
-    });
-    var AlbumsView = AppView.extend({
-        events: {
-        },
-        initialize: function(options){
-            _.bindAll(this, "resetAlbums", "addAlbum");
-            this.constructor.__super__.initialize.apply(this, [options])
-            this.albums = new AlbumCollection();
-            this.albums.bind("reset", this.resetAlbums);
-        },
-        resetAlbums: function(){
-            $(this.el).empty();
-            this.albums.each(this.addAlbum);
-        },
-        addAlbum: function(album){
-            var view = new SingleAlbumView({app: this.app, album: album});
-            $(this.el).append(view.render().el);
-        }
-    });
-    var SingleAlbumView = AppView.extend({
-        className: "album",
-        events: {"click": "handleClick"},
-        initialize: function(options){
-            this.constructor.__super__.initialize.apply(this, [options])
-            this.album = options.album;
-            _.bindAll(this, "render", "handleClick");
-        },
-        render: function(){
-            var album_text;
-            var album = this.album;
-            if(album.get("year")){
-                album_text = album.get("year") + " - " + album.get("name");
-            }else{
-                album_text = album.get("name");
-            }
-            var template_context = {text: album_text};
-            $(this.el).html(get_template("#album-template")(template_context))
-            return this;
-        },
-        handleClick: function(){
-            $(this.el).addClass("selected").siblings().removeClass("selected");
-            var album_id = this.album.get("_id")['$oid'];
-            var postdata = {album: album_id};
-            this.post("get-songs", postdata).then(function(songs){
-                app.songs.songs.reset(songs);
-            });
-        }
-    })
-
-    var SongsView = AppView.extend({
-        initialize: function(options){
-            _.bindAll(this, "resetSongs", "addSong");
-            this.constructor.__super__.initialize.apply(this, [options])
-            this.songs = new SongCollection();
-            this.songs.bind("reset", this.resetSongs);
-        },
-        resetSongs: function(){
-            $(this.el).empty();
-            this.songs.each(this.addSong);
-        },
-        addSong: function(song){
-            var view = new SingleSongView({app: this.app, song: song});
-            $(this.el).append(view.render().el);
-        }
-    });
-
-    var SingleSongView = AppView.extend({
-        className: "song",
-        events: {"click": "handleClick"},
-        initialize: function(options){
-            this.constructor.__super__.initialize.apply(this, [options]);
-            this.song = options.song;
-            _.bindAll(this, "handleClick", "render");
-        },
-        handleClick: function(event){
-            $(this.el).addClass("selected").siblings().removeClass("selected");
-            this.app.loadSong(this.song);
-        },
-        render: function(){
-            var template_context = {
-                text: this.song.get("title"),
-                duration: this.song.duration_display(),
-            }
-            $(this.el).html(get_template("#song-template")(template_context));
-            return this;
-        }
-    });
-
-    window.app = new MusicApp();
-});