diff --git a/bin/initdb-mysql.pl b/bin/initdb-mysql.pl index eb66f22..d87b04b 100755 --- a/bin/initdb-mysql.pl +++ b/bin/initdb-mysql.pl @@ -16,6 +16,7 @@ $db->do("DROP TABLE IF EXISTS players"); $db->do("DROP TABLE IF EXISTS playlists"); $db->do("DROP TABLE IF EXISTS playlist_contents"); +$db->do("DROP TABLE IF EXISTS art_cache"); $db->do("CREATE TABLE songs (song_id INT UNSIGNED AUTO_INCREMENT, path VARCHAR(1024) NOT NULL, artist VARCHAR(256), albumartist VARCHAR(256), album VARCHAR(256), title @@ -37,3 +38,6 @@ $db->do("CREATE TABLE playlist_contents (playlist_id INT UNSIGNED, song_id INT UNSIGNED, priority INT, UNIQUE(playlist_id,song_id))"); + +$db->do("CREATE TABLE art_cache (artist VARCHAR(256), album VARCHAR(256), title VARCHAR(256), + image BLOB, url VARCHAR(512))"); diff --git a/bin/initdb-sqlite.pl b/bin/initdb-sqlite.pl index fca2b1d..e3fbf9f 100755 --- a/bin/initdb-sqlite.pl +++ b/bin/initdb-sqlite.pl @@ -16,6 +16,7 @@ $db->do("DROP TABLE IF EXISTS players"); $db->do("DROP TABLE IF EXISTS playlists"); $db->do("DROP TABLE IF EXISTS playlist_contents"); +$db->do("DROP TABLE IF EXISTS art_cache"); $db->do("CREATE TABLE songs (song_id INTEGER PRIMARY KEY AUTOINCREMENT, path VARCHAR(1024) NOT NULL, artist VARCHAR(256), albumartist VARCHAR(256), album VARCHAR(256), title @@ -35,5 +36,8 @@ $db->do("CREATE TABLE playlists (who VARCHAR(256) NOT NULL, playlist_id INTEGER PRIMARY KEY AUTOINCREMENT, title VARCHAR(256) NOT NULL)"); -$db->do("CREATE TABLE playlist_contents (playlist_id INT, song_id INT - , priority INT, UNIQUE(playlist_id,song_id))"); +$db->do("CREATE TABLE playlist_contents (playlist_id INT, song_id INT, + priority INT, UNIQUE(playlist_id,song_id))"); + +$db->do("CREATE TABLE art_cache (artist VARCHAR(256), album VARCHAR(256), title VARCHAR(256), + image BLOB, url VARCHAR(512))"); diff --git a/index2.html b/index2.html index 94be7a8..33f028e 100644 --- a/index2.html +++ b/index2.html @@ -12,8 +12,9 @@ - + +
@@ -271,6 +272,14 @@ Close
-
no text... why?
+
+
+

Title

+ Content +
+
+ Okay +
+
diff --git a/json.pl b/json.pl index a438f06..88dcdb7 100755 --- a/json.pl +++ b/json.pl @@ -34,19 +34,28 @@ my $mode = lc($q->param('mode') || ''); $mode = 'status' if $mode =~ /^_/ or $mode =~ /[^\w_]/ or $mode eq 'new'; $mode = 'status' unless $web->can($mode); + my($headers, $data) = $web->$mode; - $q->no_cache(1); - binmode STDOUT, ':utf8'; - print $q->header( - @$headers, - -type => 'application/json', - ); - print scalar JSON::DWIW->new({ - pretty => 1, - escape_multi_byte => 1, - bad_char_policy => 'convert', - })->to_json($data); + my %headers = @$headers; + + # If they don't specify a type, assume it is a data structure that we + # should encode to JSON and change the header accordingly. + unless ($headers{'-type'}) { + $headers{'-type'} = 'application/json'; + $data = scalar JSON::DWIW->new({ + pretty => 1, + escape_multi_byte => 1, + bad_char_policy => 'convert', + })->to_json($data); + $q->no_cache(1); + binmode STDOUT, ':utf8'; + } + + print $q->header(%headers); + if ($data) { + print $data; + } # finish FastCGI if needed and auto-reload ourselves if we were modified $req->Finish if $running_under_fastcgi; diff --git a/lib/Acoustics/Web.pm b/lib/Acoustics/Web.pm index 23e8741..6387f29 100644 --- a/lib/Acoustics/Web.pm +++ b/lib/Acoustics/Web.pm @@ -8,6 +8,7 @@ use Time::HiRes 'sleep'; use Moose; use Module::Load 'load'; use List::Util 'shuffle'; +use LWP::Simple; has 'acoustics' => (is => 'ro', isa => 'Acoustics'); has 'cgi' => (is => 'ro', isa => 'Object'); @@ -917,5 +918,94 @@ sub stats return [], $results; } +=head2 art + +Return album art image for the requested song. + +=cut + +sub art +{ + my $self = shift; + my $artist = $self->cgi->param("artist"); + my $album = $self->cgi->param("album"); + my $title = $self->cgi->param("title"); + my $set = $self->cgi->param("set"); + my $size = 128; + if ($self->cgi->param("size")) { + $size = int($self->cgi->param("size")); + } + + # It's okay if an argument is blank, for the most part. + if (!$artist) { $artist = ""; } + if (!$album) { $album = ""; } + if (!$title) { $title = ""; } + my $ret = {}; + + # Fix issues with albumless tracks + if (length $album < 1) { + $album = $title . "__" . $artist; + } + + # This is a request to set to a specific URL. + if ($set && $set == "yes") { + my $url = $self->cgi->param("image"); + my $image = get $url; + INFO("adding art cache for " . $title . ": " . $url); + $self->acoustics->query('delete_art_cache', {artist => $artist, album => $album, title => $title}); + $self->acoustics->query('insert_art_cache', {artist => $artist, album => $album, title => $title, image => $image, url => $url }); + return [], {}; + } + + # Check the database first. + my @queries = ( + {artist => $artist, album => $album, title => $title}, + {artist => $artist, album => $album}, + {album => $album}, + ); + + my @results; + + # Try some queries until we get results. + while (!@results && @queries) { + @results = $self->acoustics->query('select_art_cache', shift @queries); + } + + # We have a result from the database, dump that and bale. + if (@results) { + unless (length $results[0]->{image} > 10) { + # We found something, but we only have a URL stored for some reason. + # This could be because a plugin added it but didn't store the result. + $results[0]->{image} = get $results[0]->{url}; + INFO("updating art cache for " . $results[0]->{title}); + $self->acoustics->query('delete_art_cache', {artist => $artist, album => $album, title => $title}); + $self->acoustics->query('insert_art_cache', {artist => $artist, album => $album, title => $title, image => $results[0]->{image}, url => $results[0]->{url} }); + } + return [-type => "image/png"], $results[0]->{image}; + } + + # XXX: Extension hooks should be added here to + # pull from LastFM / Amazon / whatever + + # Fall back to local icon. + my $last_try; + if ($size < 100) { + $last_try = "www-data/icons/cd_big.png"; + } else { + $last_try = "www-data/icons/big_a.png"; + } + open IMAGE, $last_try; + local $/; + $ret->{image} = ; + close IMAGE; + + + # TODO: We should probably use Perl's GD bindings to resize + # the result image to the requested size. I'm not + # entirely sure whether I want to, though - klange + + return [-type => "image/png"], $ret->{image}; +} + 1; diff --git a/www-data/acoustics2.css b/www-data/acoustics2.css index 3b0ba5e..99b83da 100644 --- a/www-data/acoustics2.css +++ b/www-data/acoustics2.css @@ -919,13 +919,45 @@ a img { border-top-right-radius: 15px; } -#messageBox { - font-size: 12px; -} - .mini-album-art { height: 16px; width: 16px; vertical-align: middle; padding-right: 2px; } + +#message-box { + position: absolute; + top: 50%; + left: 50%; + width: 600px; + height: 120px; + margin-top: -60px; + margin-left: -300px; + z-index: 3; + padding: 20px; + padding-top: 5px; + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; + -moz-box-shadow: 5px 5px 5px rgba(0,0,0,0.5); + -webkit-box-shadow: 5px 5px 5px rgba(0,0,0,0.5); + box-shadow: 5px 5px 5px rgba(0,0,0,0.5); + border: 1px solid #B62222; + background-color: #EC3232; + background-color: rgba(236,50,50,0.9); + color: #FFF; + text-align: center; + + display: none; +} + #message-box div h1 { + font-size: 16px; + font-weight: bold; + } + #message-box-close { + position: absolute; + right: 10px; + bottom: 10px; + } + diff --git a/www-data/acoustics2.js b/www-data/acoustics2.js index 7562f7f..f8fe5b7 100644 --- a/www-data/acoustics2.js +++ b/www-data/acoustics2.js @@ -3,6 +3,7 @@ var volume; var stateTimer; var templates = {}; var jsonSource = 'json.pl'; +var artSource = 'json.pl?mode=art'; var playingTimer; var elapsedTime = 0; var totalTime = 0; @@ -60,8 +61,47 @@ $(document).ready(function() { $("#search-results-toggle-right-panel").click(function() { toggleQueueExplicit(); }); insertAdvancedSearch(1); insertAdvancedSearch(2); + + /* XXX REMOVE THIS IN FINAL RELEASE XXX */ + /* If they haven't seen it, present users with the + * "Welcome to Acoustics Beta" dialog, which appears + * in a warning box. */ + + function setCookie(name, value, expires) { + var exdate = new Date(); + exdate.setDate(exdate.getDate() + expires); + var value = escape(value) + ((expires == null) ? "" : "; expires=" + exdate.toUTCString()); + document.cookie = name + "=" + value; + } + function getCookie(name) { + var i, x, y, cookies = document.cookie.split(";"); + for (i = 0; i < cookies.length; i++) { + x = cookies[i].substr(0, cookies[i].indexOf("=")); + y = cookies[i].substr(cookies[i].indexOf("=")+1); + x = x.replace(/^\s+|\s+$/g,""); + if (x == name) { + return unescape(y); + } + } + return null; + } + + if (getCookie("_seen_beta") != "yes") { + showMessage("Wecome to Acoustics Beta!", + "You are using the Beta release of Acoustics 2.0, "+ + "a massive new release featuring a brand new interface. "+ + "This release is not finished, and as such may have bugs "+ + "or general usability issues or missing features.

"+ + "Thank you for taking the time to test the Beta."); + setCookie("_seen_beta","yes",10000); + } + + + /* XXX REMOVE THE ABOVE IN FINAL RELEASE XXX */ + }); + function insertAdvancedSearch(id) { var entry = templates.advancedSearchEntry.clone(); if (id == 1) { @@ -283,7 +323,7 @@ function fillResultTable(json) { $(".search-results-entry-vote", entry).attr('href', 'javascript:voteSong(' + song.song_id + ')'); - $(".search-results-entry-title a", entry).html(song.title); + $(".search-results-entry-title a", entry).html("" + song.title); $(".search-results-entry-title a", entry).attr('href', '#SongDetails/' + song.song_id); @@ -295,6 +335,8 @@ function fillResultTable(json) { $(".search-results-entry-artist a", entry).attr('href', '#SelectRequest/artist/' + uriencode(song.artist)); $("#search-results-table tbody").append(entry); + //getLastfmArtImage(song.artist,song.album,song.title,$("#song-art-" + song.song_id)); + total_length += parseInt(song.length); } $("#search-results-table").trigger("update"); @@ -448,12 +490,25 @@ function handlePlayerStateRequest(json) { } $("#nothing-playing-info", nowPlayingPanel).remove(); $("#now-playing-info").replaceWith(nowPlayingPanel); - getLastfmArt(); + $("#now-playing-album-art").empty(); + $("#now-playing-album-art").append("" + + ""); + $("#now-playing-album-art-img").reflect({height: 16}); $("#now-playing-progress").progressbar({value: Math.floor(100 * (elapsedTime/totalTime))}); /* Full screen view */ $("#fullscreen-title").html(nowPlaying.title); $("#fullscreen-artist").html(nowPlaying.artist); $("#fullscreen-album").html(nowPlaying.album); + $("#fullscreen-album-art").empty(); + $("#fullscreen-album-art").append("" + + ""); + if (!$.browser.webkit) { + $("#fullscreen-album-art-img").reflect({height: 100}); + } + /* And here's the fun part */ + jQuery.favicon(getAlbumArtUrl(nowPlaying.artist,nowPlaying.album,nowPlaying.title,20)); /* Title Bar */ document.title = nowPlaying.title + " - " + nowPlaying.artist + " [Acoustics]"; } else { @@ -463,6 +518,7 @@ function handlePlayerStateRequest(json) { $("#now-playing-panel").replaceWith(nowPlayingPanel); $("#nothing-playing-info").show(); clearFullscreen(); + jQuery.favicon("www-data/images/ui2/favicon.ico"); document.title = "Acoustics"; totalTime = -1; } @@ -592,9 +648,12 @@ function songDetails(id) { $("#song-details-file a").attr('title', json.path); $("#song-details-file a").attr('href', '#SelectRequest/path/' + uriencode(json.path)); + $("#song-details-album-art").empty(); + $("#song-details-album-art").append("" + + ""); $("#search-results-song-details").slideDown(300, function() { - //$("#song-details-album-art-img").reflect({height: 32}); - getLastfmArtFloat(json.artist,json.album); + $("#song-details-album-art img").reflect({height: 40}); }); if (json.who.length > 0) { $("#song-details-voters").html(htmlForVoters(json.who)); @@ -621,24 +680,27 @@ function hideSongDetails() { $("#search-results-song-details").slideUp(300); } -$("#messageBox").ready(function() { - $("#messageBox").dialog({ - autoOpen: false, - modal: true, - buttons: {"ok": function() { - $(this).dialog("close"); - // set the text back to default - // (so we know if someone forgot to set it in another call) - $(this).html("no text... why?"); - }} +$("#message-box").ready(function() { + $("#message-box").ajaxError(function (e, xhr, opts, err) { + showMessage("Communication Error", xhr.responseText); }); +}); - $("#messageBox").ajaxError(function (e, xhr, opts, err) { - $(this).dialog('option', 'title', 'Communication Error'); - $(this).html(xhr.responseText); - $(this).dialog('open'); +function showMessage(title, message) { + $("#message-box-title").empty(); + $("#message-box-message").empty(); + $("#message-box-title").html(title); + $("#message-box-message").html(message); + $("#message-box").show(100, function() { + var h = $("#message-box-inner").height() + 10; + $("#message-box").height(h); + $("#message-box").css("margin-top", (-h / 2) + "px"); }); -}); +} + +function closeMessageBox() { + $("#message-box").hide(300); +} function advancedSearchFormSubmit() { var conditions = ["OR"]; @@ -679,22 +741,30 @@ function formSearch() { } function uriencode(str) { + return encodeURIComponent(formencode(str)); +} + +function formencode(str) { str = new String(str); str = str.replace(/\&/g, '%26'); str = str.replace(/\+/g, '%2b'); str = str.replace(/\#/g, '%23'); str = str.replace(/\//g, '%2f'); - return encodeURIComponent(str); + return str; } -function formencode(str) { +function jsencode(str) { str = new String(str); - str = str.replace(/\&/g, '%26'); - str = str.replace(/\+/g, '%2b'); - str = str.replace(/\#/g, '%23'); - str = str.replace(/\//g, '%2f'); + str = str.replace(/\'/g, '''); + str = str.replace(/\"/g, '\\\"'); + return str; +} +function moreencode(str) { + str = uriencode(str); + str = str.replace(/\'/g, '''); + str = str.replace(/\"/g, '"'); return str; } @@ -748,83 +818,13 @@ function setMenuItem(item) { $("#header-bar-menu-" + item).addClass("header-bar-menu-selected", 100); } -function getLastfmUrl(artist, album) { - var api_key = "46d779178cb5e43eefe754d0c1c1fecf"; - var url = "http://ws.audioscrobbler.com/2.0/?method=album.getInfo&api_key=" + api_key + "&format=json"; - return url + "&artist=" + uriencode(artist) + "&album=" + uriencode(album); +function fixArt(artist, album, title) { + newArt = prompt("Correct album art for " + title + " by " + artist + ":", "http://example.com/some_image.jpg"); + $.get(getAlbumArtUrl(artist,album,title,0) + "&set=yes&image=" + newArt); } -function getLastfmPreferred(data, size) { - var path = undefined; - if (data.album) { - if (size > 200) { - for (var i = 0; i < data.album.image.length; i++) { - if (data.album.image[i].size == "extralarge") { - path = data.album.image[i]["#text"]; - } - } - } - if (size > 50) { - if (!path) { - for (var i = 0; i < data.album.image.length; i++) { - if (data.album.image[i].size == "large") { - path = data.album.image[i]["#text"]; - } - } - } - } - if (size > 30) { - if (!path) { - for (var i = 0; i < data.album.image.length; i++) { - if (data.album.image[i].size == "medium") { - path = data.album.image[i]["#text"]; - } - } - } - } - if (!path) { - path = data.album.image[0]["#text"]; - } - } - if (!path) { - if (size < 128) { - path = "www-data/icons/cd_big.png"; - } else { - path = "www-data/icons/big_a.png"; - } - } - return path; -} - -function getLastfmArt() { - if (!nowPlaying) { return; } - $.getJSON( - getLastfmUrl(nowPlaying.artist,nowPlaying.album) + "&callback=?", - function (data) { - path = getLastfmPreferred(data, 64); - $("#now-playing-album-art").empty(); - $("#now-playing-album-art").append(""); - $("#now-playing-album-art-img").reflect({height: 16}); - path = getLastfmPreferred(data, 300); - $("#fullscreen-album-art").empty(); - $("#fullscreen-album-art").append(""); - if (!$.browser.webkit) { - $("#fullscreen-album-art-img").reflect({height: 100}); - } - } - ); -} - -function getLastfmArtFloat(artist, album) { - $.getJSON( - getLastfmUrl(artist,album) + "&callback=?", - function (data) { - path = getLastfmPreferred(data, 128); - $("#song-details-album-art").empty(); - $("#song-details-album-art").append(""); - $("#song-details-album-art-img").reflect({height: 32}); - } - ); +function getAlbumArtUrl(artist, album, title, size) { + return artSource + "&artist=" + moreencode(artist) + "&album=" + moreencode(album) + "&title=" + moreencode(title) + "&size=" + size } function unfullscreen() { @@ -833,7 +833,6 @@ function unfullscreen() { function fullscreen() { $("#fullscreen-view").fadeIn(300, function() { - getLastfmArt(); }); } diff --git a/www-data/jquery.favicon.js b/www-data/jquery.favicon.js new file mode 100644 index 0000000..594b8e6 --- /dev/null +++ b/www-data/jquery.favicon.js @@ -0,0 +1,156 @@ +/** + * jQuery Favicon plugin + * http://hellowebapps.com/products/jquery-favicon/ + * + * Copyright (c) 2010 Volodymyr Iatsyshyn (viatsyshyn@hellowebapps.com) + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * + */ + +(function($){ + + var canvas; + + function apply (url) { + $('link[rel$=icon]').replaceWith(''); + $('head').append( + $('') + .attr('href', url)); + } + + /** + * jQuery.favicon + * + * @param {String} iconURL + * @param {String} alternateURL + * @param {Function} onDraw + * + * function (iconURL) + * function (iconURL, onDraw) + * function (iconURL, alternateURL, onDraw) + */ + $.favicon = function(iconURL, alternateURL, onDraw) { + + if (arguments.length == 2) { + // alternateURL is optional + onDraw = alternateURL; + } + + if (onDraw) { + canvas = canvas || $('')[0]; + if (canvas.getContext) { + var img = $('')[0]; + img.onload = function () { + $.favicon.unanimate(); + + canvas.height = canvas.width = this.width; + var ctx = canvas.getContext('2d'); + ctx.drawImage(this, 0, 0); + onDraw(ctx); + apply(canvas.toDataURL('image/png')); + }; + img.src = iconURL; + } else { + apply(alternateURL || iconURL); + } + } else { + $.favicon.unanimate(); + + apply(iconURL); + } + + return this; + }; + + var animation = { + timer: null, + frames: [], + size: 16, + count: 1 + }; + + $.extend($.favicon, { + /** + * jQuery.favicon.animate - starts frames based animation + * + * @param {String} animationURL Should be image that contains frames joined horizontally + * @param {String} alternateURL Normal one frame image that will be used if Canvas is not supported + * @param {Object} options optional + * + * function (animationURL, alternateURL) + * function (animationURL, alternateURL, { + * interval: 1000, // change frame in X ms, default is 1000ms + * onDraw: function (context, frame) {}, // is called each frame + * onStop: function () {}, // is called on animation stop + * frames: [1,3,5] // display frames in this exact order, defaults is all frames + * }) + */ + animate: function (animationURL, alternateURL, options) { + options = options || {}; + + canvas = canvas || $('')[0]; + if (canvas.getContext) { + var img = $('')[0]; + img.onload = function () { + + $.favicon.unanimate(); + + animation.onStop = options.onStop; + + animation.image = this; + canvas.height = canvas.width = animation.size = this.height; + animation.count = this.width / this.height; + + var frames = []; + for (var i = 0; i < animation.count; ++i) frames.push(i); + animation.frames = options.frames || frames; + + var ctx = canvas.getContext('2d'); + + options.onStart && options.onStart(); + animation.timer = setInterval(function () { + // get current frame + var frame = animation.frames.shift(); + animation.frames.push(frame); + + // check if frame exists + if (frame >= animation.count) { + clearInterval(animation.timer); + animation.timer = null; + + throw new Error('jQuery.favicon.animate: frame #' + frame + ' do not exists in "' + animationURL + '"'); + } + + // draw frame + var s = animation.size; + ctx.drawImage(animation.image, s * frame, 0, s, s, 0, 0, s, s); + + // User Draw event + options.onDraw && options.onDraw(ctx, frame); + + // set favicon + apply(canvas.toDataURL('image/png')); + }, options.interval || 1000); + }; + img.src = animationURL; + } else { + apply(alternateURL || animationURL); + } + }, + + /** + * jQuery.favicon.unanimate - stops current animation + */ + unanimate: function () { + if (animation.timer) { + clearInterval(animation.timer); + animation.timer = null; + + animation.onStop && animation.onStop(); + } + } + }) +})(jQuery); \ No newline at end of file