diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 00000000..91600595 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +service_name: travis-ci diff --git a/.gitignore b/.gitignore index f356293e..2648647e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ logs results npm-debug.log +config.js + +config.js diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..5d5d073e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: +- '0.10' +services: + - mongodb + - redis-server +after_script: istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage diff --git a/README.md b/README.md index de61b680..89cd81fd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ -# soundtrack.io +soundtrack.io +============= +[![Build Status](https://img.shields.io/travis/martindale/soundtrack.io.svg?branch=soundtrack.io&style=flat-square)](https://travis-ci.org/martindale/soundtrack.io) +[![Coverage Status](https://img.shields.io/coveralls/martindale/soundtrack.io/soundtrack.io.svg?style=flat-square)](https://coveralls.io/r/martindale/soundtrack.io) -soundtrack.io is a collaborative online jukebox. +soundtrack.io is a collaborative online jukebox. It is an experimental Internet radio platform. Vote on what plays next, like Reddit for music. Aggregates streams from sources like YouTube and SoundCloud, so when a song is queued, it has multiple locations to play from if any one source fails for any particular reason. ## Getting Started @@ -13,7 +16,7 @@ Homebrew is recommended for OS X users. Once you have them installed, go ahead and clone the repository. - git clone git@github.com:fractaloop/soundtrack.io.git + git clone git@github.com:martindale/soundtrack.io.git cd soundtrack.io You will need to fetch the dependencies and then you can start up the server. @@ -21,6 +24,12 @@ You will need to fetch the dependencies and then you can start up the server. npm install node soundtrack.js +## API + +Deleting tracks: +`$.ajax('/playlist/520e6bda3cb680003700049c', { type: 'DELETE', data: { index: 1 } });` + ## Contributing +Want to help? Claim something in [the "ready" column on our Waffle.io](https://waffle.io/martindale/soundtrack.io) by assigning it to yourself. [Fork. Commit. Pull request.](https://help.github.com/articles/fork-a-repo) diff --git a/addJob.js b/addJob.js new file mode 100644 index 00000000..819bfde2 --- /dev/null +++ b/addJob.js @@ -0,0 +1,11 @@ +var config = require('./config'); + +var Monq = require('monq'); +var monq = Monq('mongodb://localhost:27017/' + config.database.name ); +var jobs = monq.queue( config.database.name ); + +/**/jobs.enqueue('artist:update', { id: '52166226a1d626d547000338' } , function(err, job) { +/*/jobs.enqueue('test', { id: '5390073d1559212f560012b4' } , function(err, job) {/**/ + console.log('enqueued: ' , job ); + process.exit(); +}); \ No newline at end of file diff --git a/config.js b/config.js index da9b35d7..0b388947 100644 --- a/config.js +++ b/config.js @@ -1,6 +1,7 @@ module.exports = { app: { - safe: process.env.SOUNDTRACK_APP_SAFE || false + name: 'soundtrack' + , safe: process.env.SOUNDTRACK_APP_SAFE || false , host: process.env.SOUNDTRACK_APP_HOST || 'soundtrack.io' , port: process.env.SOUNDTRACK_APP_PORT || 13000 }, @@ -8,6 +9,10 @@ module.exports = { name: process.env.SOUNDTRACK_DB_NAME || 'soundtrack' , host: 'localhost' }, + redis: { + host: process.env.SOUNDTRACK_REDIS_HOST || 'localhost' + , port: process.env.SOUNDTRACK_REDIS_PORT || 6379 + }, sessions: { key: 'put yourself a fancy little key here' }, @@ -22,5 +27,9 @@ module.exports = { soundcloud: { id: process.env.SOUNDTRACK_SOUNDCLOUD_ID || 'id here' , secret: process.env.SOUNDTRACK_SOUNDCLOUD_SECRET || 'secret here' + }, + spotify: { + id: process.env.SOUNDTRACK_SPOTIFY_ID || 'id here', + secret: process.env.SOUNDTRACK_SPOTIFY_SECRET || 'secret here' } -} \ No newline at end of file +} diff --git a/controllers/artists.js b/controllers/artists.js index 820a753f..dd498dde 100644 --- a/controllers/artists.js +++ b/controllers/artists.js @@ -1,38 +1,192 @@ var rest = require('restler') - , _ = require('underscore'); + , _ = require('underscore') + , async = require('async'); module.exports = { list: function(req, res, next) { - Artist.find({}).sort('name').exec(function(err, artists) { - res.render('artists', { - artists: artists + var limit = (req.param('limit')) ? req.param('limit') : 100; + var query = (req.param('q')) ? { name: new RegExp('(.*)'+req.param('q')+'(.*)', 'i') } : undefined; + + async.parallel([ + function(done) { + Artist.count().exec( done ); + }, + function(done) { + Artist.find( query ).sort('name').limit( limit ).exec( function(err, artists) { + async.map( artists , function(x, countComplete) { + Track.count({ $or: [ + { _artist: x._id } + , { _credits: x._id } + ] }).exec( function(err, trackCount) { + x = x.toObject(); + x.tracks = trackCount; + countComplete( err, x ); + }); + }, done ); + }); + } + ], function(err, results) { + res.format({ + json: function() { + res.send( results[1].map(function(x) { + if (typeof(x.toObject) == 'function') { + x = x.toObject(); + } + //x.value = x._id; + x.value = x.name; + return x; + }) ); + }, + html: function() { + res.render('artists', { + count: results[0] + , limit: limit + , artists: results[1] + }); + } }); }); }, - view: function(req, res, next) { + delete: function(req, res, next) { Artist.findOne({ slug: req.param('artistSlug') }).exec(function(err, artist) { if (!artist) { return next(); } + + Track.find({ $or: [ + { _artist: artist._id } + , { _credits: artist._id } + ] }).exec(function(err, tracks) { + + res.send(tracks.length); + + }); + + }); + }, + edit: function(req, res, next) { + Artist.findOne({ $or: [ + { _id: req.param('artistID') } + , { slug: req.param('artistSlug') } + , { slugs: req.param('artistSlug') } + ] }).sort('_id').exec(function(err, artist) { + if (!artist) { return next(); } + + artist.name = req.param('name') || artist.name; + artist.bio = req.param('bio') || artist.bio; + + // reset updated field, for re-crawl + artist.tracking.tracks.updated = undefined; + + Artist.find({ $or: [ + { _id: req.param('artistID') } + , { slug: req.param('artistSlug') } + , { slugs: req.param('artistSlug') } + ] }).exec(function(err, artists) { + + var allArtistIDs = artists.map(function(x) { return x._id; }); + + artists.forEach(function(a) { + artist.slugs = _.union( artist.slugs , a.slugs ); + }); + artist.slugs = _.uniq( artist.slugs ); + + async.parallel([ + function(done) { + Track.update({ _artist: { $in: allArtistIDs } }, { + _artist: artist._id.toString() + }, { multi: true }, done ); + }, + function(done) { + Track.update({ '_credits.$': { $in: allArtistIDs } }, { + $addToSet: { _credits: artist._id } + }, { multi: true }, done ); + } + ], function(err, results) { + if (err) { console.log(err); } + + artist.save(function(err) { + if (err) { console.log(err); } + + res.format({ + json: function() { + res.send({ + status: 'success' + , message: 'artist edited successfully' + }); + }, + html: function() { + res.redirect('/' + artist.slug ); + } + }); + }); + }); + }); + }); + }, + view: function(req, res, next) { + Person.count({ slug: req.param('artistSlug') }).exec(function(err, num) { + if (num) return next(); + + var limit = (req.param('limit')) ? req.param('limit') : 100; - Track.find({ _artist: artist._id }).exec(function(err, tracks) { + Artist.findOne({ $or: [ + { slug: req.param('artistSlug') } + , { slugs: req.param('artistSlug') } + ] }).exec(function(err, artist) { + if (!artist) { return next(); } + + // handle artist renames + if (req.param('artistSlug') !== artist.slug) { + return res.redirect('/' + artist.slug); + } - Play.aggregate([ - { $match: { _track: { $in: tracks.map(function(x) { return x._id; }) } } }, - { $group: { _id: '$_track', count: { $sum: 1 } } }, - { $sort: { 'count': -1 } } - ], function(err, trackScores) { + Track.find({ $or: [ + { _artist: artist._id } + , { _credits: artist._id } + ] }).populate('_artist').exec(function(err, tracks) { - res.render('artist', { - artist: artist - , tracks: tracks.map(function(track) { - var plays = _.find( trackScores , function(x) { return x._id.toString() == track._id.toString() } ); - track.plays = (plays) ? plays.count : 0; - return track; - }) + Play.aggregate([ + { $match: { _track: { $in: tracks.map(function(x) { return x._id; }) } } }, + { $group: { _id: '$_track', count: { $sum: 1 } } }, + { $sort: { 'count': -1 } } + ], function(err, trackScores) { + + tracks = tracks.map(function(track) { + var plays = _.find( trackScores , function(x) { return x._id.toString() == track._id.toString() } ); + track.plays = (plays) ? plays.count : 0; + return track; + }).sort(function(a, b) { + return b.plays - a.plays; + }); + + var trackCount = tracks.length; + + tracks = tracks.slice( 0 , limit - 1 ); + + res.format({ + json: function() { + res.send( artist ); + }, + html: function() { + res.render('artist', { + artist: artist + , tracks: tracks + , trackCount: trackCount + }); + } + }); }); - }); + if (req.app.config.jobs && req.app.config.jobs.enabled) { + req.app.agency.publish('artist:update', { + id: artist._id + , timeout: 3 * 60 * 1000 + }, function(err, job) { + console.log('update artist completed'); + }); + } + }); }); }); } -} \ No newline at end of file +} diff --git a/controllers/chat.js b/controllers/chat.js index 3eaeb0f3..73eeed3f 100644 --- a/controllers/chat.js +++ b/controllers/chat.js @@ -2,9 +2,17 @@ var async = require('async'); module.exports = { view: function(req, res, next) { - Chat.find({}).sort('timestamp').populate('_author').limit(20).exec(function(err, chats) { - res.render('chats', { - chats: chats + var limit = (req.param('limit')) ? req.param('limit') : 100; + + Chat.find({ + _room: req.roomObj._id + }).sort('-_id').populate('_author _track _play').limit( limit ).exec(function(err, chats) { + Artist.populate( chats , { + path: '_track._artist' + }, function(err, chats) { + res.render('chats', { + chats: chats + }); }); }); }, @@ -37,4 +45,4 @@ module.exports = { }); }); } -} \ No newline at end of file +} diff --git a/controllers/pages.js b/controllers/pages.js index defca662..93634cfc 100644 --- a/controllers/pages.js +++ b/controllers/pages.js @@ -1,35 +1,134 @@ +var async = require('async'); +var _ = require('underscore'); + module.exports = { index: function(req, res, next) { - Chat.find({}).limit(10).sort('-created').populate('_author').exec(function(err, messages) { - Playlist.find({ _creator: ((req.user) ? req.user._id : undefined) }).sort('name').exec(function(err, playlists) { - - if (err) { console.log(err); } - console.log(playlists); - - res.render('index', { - messages: messages.reverse() - , backup: [] - , playlists: playlists - , room: req.app.room + if (!req.roomObj) { + var sortedRooms = []; + + Object.keys( req.app.locals.rooms ).forEach(function( roomName ) { + var room = req.app.locals.rooms[ roomName ]; + room.listenerCount = Object.keys( room.listeners ).length; + sortedRooms.push( room ); + }); + + sortedRooms = sortedRooms.sort(function(a, b) { + return b.listenerCount - a.listenerCount; + }); + + return async.map( sortedRooms , function( room , done ) { + Person.populate( room , { + path: '_owner' + }, done ); + } , function(err, finalRooms) { + return res.render('rooms', { + rooms: finalRooms }); }); + } + + async.parallel([ + collectChats, + collectPlaylists + ], function(err, results) { + var messages = results[0].reverse(); + var playlists = results[1]; + res.render('index', { + messages: messages + , backup: [] + , playlists: playlists || [] + , room: req.app.rooms[ req.room ] + , page: { + title: req.roomObj.name + } + }); + }); + + function collectChats( done ) { + Chat.find({ + _room: req.roomObj._id + }).limit(10).sort('-created').populate('_author _track _play').exec(function(err, messages) { + Artist.populate( messages, { + path: '_track._artist' + }, done ); + }); + } + function collectPlaylists( done ) { + Playlist.find({ _creator: ((req.user) ? req.user._id : undefined) }).sort('name').exec( done ); + } }, about: function(req, res, next) { res.render('about', { }); }, history: function(req, res) { - Play.find({}).populate('_track _curator').sort('-timestamp').limit(100).exec(function(err, plays) { + Play.find({ + _room: req.roomObj._id + }).populate('_track _curator').sort('-timestamp').limit(100).exec(function(err, plays) { Artist.populate(plays, { path: '_track._artist' }, function(err, plays) { - console.log(plays); - res.render('history', { plays: plays }); }); }); + }, + stats: function(req, res, next) { + var LIMIT = 50; + + var functions = [ + function collectTopTracks( done ) { + Play.aggregate([ + { $match: { + _curator: { $exists: true }, + _room: req.roomObj._id + } }, + { $group: { _id: '$_track', count: { $sum: 1 } } }, + { $sort: { 'count': -1 } }, + { $limit: LIMIT } + ], function(err, collected) { + Track.find({ _id: { $in: collected.map(function(x) { return x._id; }) } }).populate('_artist').exec(function(err, input) { + var output = []; + for (var i = 0; i < collected.length; i++) { + output.push( _.extend( collected[i] , input[i] ) ); + } + done( err , output ); + }); + } ); + }, + function collectTopDJs( done ) { + Play.aggregate([ + { $match: { + _curator: { $exists: true }, + _room: req.roomObj._id + } }, + { $group: { _id: '$_curator', count: { $sum: 1 } } }, + { $sort: { 'count': -1 } }, + { $limit: LIMIT } + ], function(err, collected) { + Person.find({ _id: { $in: collected.map(function(x) { return x._id; }) } }).exec(function(err, input) { + var output = []; + for (var i = 0; i < collected.length; i++) { + output.push( _.extend( collected[i] , input[i] ) ); + } + done( err , output ); + }); + } ); + } + ]; + + async.parallel( functions , function(err, results) { + var stats = { + topTracks: results[0], + topDJs: results[1] + } + + res.format({ + json: function() { res.send(stats); }, + html: function() { res.render('stats', stats ); } + }); + }); } -} \ No newline at end of file +} diff --git a/controllers/people.js b/controllers/people.js index b3ecef8d..43781717 100644 --- a/controllers/people.js +++ b/controllers/people.js @@ -1,22 +1,199 @@ +var async = require('async'); +var _ = require('underscore'); + module.exports = { profile: function(req, res, next) { Person.findOne({ slug: req.param('usernameSlug') }).exec(function(err, person) { - if (!person) { return next(); } + if (!person) return next(); + + var LIMIT = 50; + + async.parallel([ + collectUserPlaylists, + collectUserPlays, + collectPlayStats, + collectArtistData + ], function(err, results) { - Playlist.find({ _creator: person._id, public: true }).exec(function(err, playlists) { - res.render('person', { + var playlists = results[0]; + // TODO: use reduce(); + playlists = playlists.map(function(playlist) { + playlist.length = 0; + playlist._tracks.forEach(function(track) { + playlist.length += track.duration; + }); + return playlist; + }); + + return res.render('person', { person: person , playlists: playlists + , plays: results[1] + , favoriteTracks: { + allTime: results[2][0].filter(function(x) { + return x._artist; + }), + past30days: results[2][1].filter(function(x) { + return x._artist; + }) + } + , artist: (results[3]) ? results[3].artist : null + , tracks: (results[3]) ? results[3].tracks : null + , trackCount: (results[3]) ? results[3].trackCount : null }); + + if (req.app.config.jobs && req.app.config.jobs.enabled) { + req.app.agency.publish('artist:update', { + id: (results[3]) ? results[3].artist._id : null + , timeout: 3 * 60 * 1000 + }, function(err, job) { + console.log('update artist completed'); + }); + } + }); + function collectUserPlays(done) { + Play.find({ _curator: person._id }).sort('-timestamp').limit(20).populate('_track _curator').exec(function(err, plays) { + Artist.populate( plays , { + path: '_track._artist _track._credits' + }, done ); + }); + } + + function collectUserPlaylists(done) { + var q = { _creator: person._id }; + + if (!req.user || req.user._id.toString() !== person._id.toString()) { + q.public = true; + } + + Playlist.find( q ).sort('-_id').populate('_tracks').exec(function(err, playlists) { + done( err , playlists ); + }); + } + + function collectPlayStats(done) { + async.parallel([ + function(complete) { + Play.aggregate([ + { $match: { + _curator: person._id + } }, + { $group: { _id: '$_track', count: { $sum: 1 } } }, + { $sort: { 'count': -1 } }, + { $limit: LIMIT } + ], function(err, collected) { + Track.find({ _id: { $in: collected.map(function(x) { return x._id; }) } }).populate('_artist').exec(function(err, input) { + var output = []; + for (var i = 0; i < collected.length; i++) { + output.push( _.extend( collected[i] , input[i] ) ); + } + complete( err , output ); + }); + } ); + }, + function(complete) { + Play.aggregate([ + { $match: { + _curator: person._id, + timestamp: { $gte: new Date((new Date()) - 30 * 24 * 3600 * 1000) } + } }, + { $group: { _id: '$_track', count: { $sum: 1 } } }, + { $sort: { 'count': -1 } }, + { $limit: LIMIT } + ], function(err, collected) { + Track.find({ _id: { $in: collected.map(function(x) { return x._id; }) } }).populate('_artist').exec(function(err, input) { + var output = []; + for (var i = 0; i < collected.length; i++) { + output.push( _.extend( collected[i] , input[i] ) ); + } + complete( err , output ); + }); + } ); + } + ], done ); + } + + function collectArtistData( artistComplete ) { + Artist.findOne({ $or: [ + { slug: req.param('usernameSlug') } + , { slugs: req.param('usernameSlug') } + ] }).exec(function(err, artist) { + if (!artist) return artistComplete(); + + // handle artist renames + if (req.param('usernameSlug') !== artist.slug) { + res.redirect('/' + artist.slug); + return artistComplete('redirected'); + } + + Track.find({ $or: [ + { _artist: artist._id } + , { _credits: artist._id } + ] }).populate('_artist').exec(function(err, tracks) { + + Play.aggregate([ + { $match: { _track: { $in: tracks.map(function(x) { return x._id; }) } } }, + { $group: { _id: '$_track', count: { $sum: 1 } } }, + { $sort: { 'count': -1 } } + ], function(err, trackScores) { + + tracks = tracks.map(function(track) { + var plays = _.find( trackScores , function(x) { return x._id.toString() == track._id.toString() } ); + track.plays = (plays) ? plays.count : 0; + return track; + }).sort(function(a, b) { + return b.plays - a.plays; + }); + + var trackCount = tracks.length; + + tracks = tracks.slice( 0 , LIMIT - 1 ); + + return artistComplete( null , { + artist: artist + , tracks: tracks + , trackCount: trackCount + }); + }); + + /* req.soundtrack._jobs.enqueue('artist:update', { + id: artist._id + , timeout: 3 * 60 * 1000 + }, function(err, job) { + console.log('update artist queued'); + }); */ + + }); + }); + } + + }); + }, + mentions: function(req, res, next) { + Person.findOne({ slug: req.param('usernameSlug') }).exec(function(err, person) { + if (!person) { return next(); } + + Chat.find({ message: new RegExp( person.username , 'i') }).sort('-created').limit(100).populate('_author _track _play').exec(function(err, chats) { + res.render('chats', { + chats: chats + }); + }); }); }, edit: function(req, res, next) { Person.findOne({ slug: req.param('usernameSlug') }).exec(function(err, person) { if (!person) { return next(); } - person.bio = (req.param('bio')) ? req.param('bio') : person.bio; + person.bio = (req.param('bio')) ? req.param('bio') : person.bio; + person.email = (req.param('email')) ? req.param('email') : person.email; + + if (typeof(person.email) == 'string') { + var hash = require('crypto').createHash('md5').update( person.email.toLowerCase() ).digest('hex'); + person.avatar.url = 'https://www.gravatar.com/avatar/' + hash + '?d=https://soundtrack.io/img/user-avatar.png'; + } + person.save(function(err) { req.flash('info', 'Profile saved successfully!'); res.redirect('/' + person.slug ); @@ -31,6 +208,42 @@ module.exports = { }); }); }, + listPlays: function(req, res, next) { + var limit = (req.param('limit')) ? parseInt(req.param('limit')) : 1000000; + + Person.findOne({ slug: req.param('usernameSlug') }).exec(function(err, person) { + if (!person) { return next(); } + + async.parallel([ + function(done) { + Playlist.find({ _creator: person._id, public: true }).exec( done ); + }, + function(done) { + Play.find({ + _curator: person._id, + _room: (req.roomObj) ? req.roomObj._id : undefined + }).sort('-timestamp').populate('_track _curator').limit( limit ).exec(function(err, plays) { + Artist.populate( plays , { + path: '_track._artist _track._credits' + }, done ); + }); + } + ], function(err, results) { + res.format({ + json: function() { + res.send( results[1] ); + }, + html: function() { + res.render('person-plays', { + person: person + , playlists: results[0] + , plays: results[1] + }); + } + }); + }); + }); + }, setUsernameForm: function(req, res, next) { if (!req.user || (req.user && req.user.username)) { return res.redirect('/'); @@ -55,4 +268,4 @@ module.exports = { }); } -} \ No newline at end of file +} diff --git a/controllers/playlists.js b/controllers/playlists.js index 4622ea07..568b56c9 100644 --- a/controllers/playlists.js +++ b/controllers/playlists.js @@ -1,23 +1,87 @@ -var _ = require('underscore') +var _ = require('underscore'); +var async = require('async'); +var rest = require('restler'); module.exports = { + list: function(req, res, next) { + Playlist.find({ public: true }).sort('-updated').populate('_tracks _creator _owner').exec(function(err, playlists) { + // TODO: use reduce(); + playlists = playlists.map(function(playlist) { + playlist.length = 0; + playlist._tracks.forEach(function(track) { + playlist.length += track.duration; + }); + return playlist; + }); + + res.format({ + json: function() { + res.send( playlists ); + }, + html: function() { + res.render('sets', { + sets: playlists + }); + } + }); + }); + }, + listPerson: function(req, res, next) { + Person.findOne({ slug: req.param('usernameSlug') }).exec(function(err, person) { + if (!person) return next(); + + var q = { _creator: person._id }; + + if (!req.user || req.user._id.toString() !== person._id.toString()) { + q.public = true; + } + + Playlist.find( q ).sort('-_id').populate('_tracks').exec(function(err, playlists) { + // TODO: use reduce(); + playlists = playlists.map(function(playlist) { + playlist.length = 0; + playlist._tracks.forEach(function(track) { + playlist.length += track.duration; + }); + return playlist; + }); + + res.render('playlists', { + person: person + , playlists: playlists + }); + }); + }); + }, view: function(req, res, next) { Person.findOne({ slug: req.param('usernameSlug') }).exec(function(err, person) { if (!person) { return next(); } - // TODO: use $or to allow user to view non-public var slug = req.param('playlistSlug').split('.')[0]; - Playlist.findOne({ $or: [ - { _creator: person._id, public: true } - , { _creator: (req.user) ? req.user._id : person._id } - ], slug: slug }).populate('_tracks _creator').exec(function(err, playlist) { + + var query = { + slug: slug + }; + + if (person) { + query._creator = person._id; + } else if (req.user) { + query._creator = req.user._id; + } + + if (req.user && req.user._id.toString() !== person._id.toString()) { + query.public = true; + } + + console.log('query', query ); + + Playlist.findOne( query ).populate('_tracks _creator _parent').exec(function(err, playlist) { if (!playlist) { return next(); } Artist.populate(playlist, { path: '_tracks._artist' }, function(err, playlist) { - res.format({ json: function() { //var playlist = playlist.toObject(); @@ -29,8 +93,12 @@ module.exports = { res.send( playlist ); }, html: function() { - res.render('playlist', { - playlist: playlist + Person.populate( playlist , { + path: '_parent._owner' + }, function(err, playlist) { + res.render('playlist', { + playlist: playlist + }); }); } }); @@ -40,29 +108,186 @@ module.exports = { }); }, - create: function(req, res, next) { - var playlist = new Playlist({ - name: req.param('name') - , _creator: req.user._id - , public: (req.param('public') == 'true') ? true : false + delete: function(req, res, next) { + Playlist.remove({ + _id: req.param('playlistID'), + _creator: req.user._id + }).exec(function(err, numberRemoved) { + if (err || !numberRemoved) return next(); + return res.send('ok'); }); + }, + removeTrackFromPlaylist: function(req, res, next) { + if (!~(req.param('index'))) return next(); - Track.findOne({ _id: req.param('trackID') }).exec(function(err, track) { - if (!track) { return next(); } - - playlist._tracks.push( track._id ); + Playlist.findOne({ + _id: req.param('playlistID'), + _creator: req.user._id + }).exec(function(err, playlist) { + if (err || !playlist) return next(); + playlist._tracks.splice( req.param('index') , 1 ); playlist.save(function(err) { - res.send({ - status: 'success' - , results: { - _id: playlist._id - , name: playlist.name - , tracks: [ track ] + return res.send('ok'); + }); + + }); + }, + createForm: function(req, res, next) { + if (!req.user) return next(); + res.render('playlists-create'); + }, + create: function(req, res, next) { + Playlist.findOne({ _id: req.param('parentID') , public: true }).exec(function(err, parent) { + + var playlist = new Playlist({ + name: req.param('name') || ((parent) ? parent.name : null) + , description: req.param('description') || ((parent) ? parent.description : null) + , public: (req.param('status') === 'public') ? true : false + , _creator: req.user._id + , _owner: req.user._id + , _parent: (req.param('parentID') && parent) ? parent._id : null + , _tracks: (parent) ? parent._tracks : [] + }); + + Track.findOne({ _id: req.param('trackID') }).exec(function(err, track) { + if (track) playlist._tracks.push( track._id ); + + playlist.save(function(err) { + if (err) { + return res.format({ + json: function() { res.send({ status: 'error', message: err }); }, + html: function() { res.status(500).render('500'); } + }); + } + + res.status(303); + + res.format({ + json: function() { + res.send({ + status: 'success' + , results: { + _id: playlist._id + , name: playlist.name + , tracks: [ track ] + } + }); + }, + html: function() { + req.flash('info', 'Set created successfully!'); + res.redirect('/' + req.user.slug + '/' + playlist.slug ); } + }); }); }); }); + }, + import: function(req, res, next) { + var playlist = new Playlist(); + + switch (req.param('source')) { + default: + return res.status(400).end(); + break; + case 'soundcloud': + self.trackFromSource( 'soundcloud' , item.id , cb ); + break; + } + + }, + syncSetup: function(req, res, next) { + + if (!req.user) return res.redirect('/login'); + if (!req.user.profiles || !req.user.profiles.spotify) return res.redirect('/auth/spotify'); + if (!req.user.profiles.spotify.token) return res.redirect('/auth/spotify'); + //if (req.user.profiles.spotify.expires < Date.now()) return res.redirect('/auth/spotify'); + + // stub for spotify API auth + var spotify = { + get: function( path ) { + return rest.get('https://api.spotify.com/v1/' + path , { + headers: { + 'Authorization': 'Bearer ' + req.user.profiles.spotify.token + } + }); + } + } + + var playlist = req.param('playlist'); + + if (playlist) { + try { + playlist = JSON.parse( playlist ); + } catch (e) { + return res.render('500'); + } + + var url = 'users/' + playlist.user + '/playlists/' + playlist.id + '?limit=250'; + spotify.get( url ).on('complete', function(spotifyPlaylist , response ) { + if (!spotifyPlaylist || response.statusCode !== 200) { + req.flash('error', 'Could not retrieve list from Spotify. ' + response.statusCode ); + return res.redirect('back'); + } + + console.log('spotifyPlaylist', spotifyPlaylist); + console.log('will be public: ', spotifyPlaylist.public); + + var tracks = spotifyPlaylist.tracks.items.map(function(x) { + return { + title: x.track.name, + artist: x.track.artists[0].name, + credits: x.track.artists.map(function(y) { + return y.name + }), + duration: x.track.duration_ms / 1000 + } + }); + + var pushers = []; + tracks.forEach(function(track) { + pushers.push(function(done) { + req.soundtrack.trackFromSource('object', track , done ); + }); + }); + + async.series( pushers , function(err, tracks) { + + tracks.forEach(function(track) { + /* req.app.agency.publish('track:crawl', { + id: track._id + }, function(err) { + console.log('track crawled, doing stuff in initiator'); + }); */ + }); + + var playlist = new Playlist({ + name: spotifyPlaylist.name, + description: spotifyPlaylist.description, + public: spotifyPlaylist.public, + _creator: req.user._id, + _owner: req.user._id, + _tracks: tracks.map(function(x) { return x._id }), + remotes: { + spotify: { + id: spotifyPlaylist.id + } + } + }); + playlist.save(function(err) { + res.redirect('/' + req.user.slug + '/' + playlist.slug ); + }); + }); + }); + + } else { + spotify.get('users/' + req.user.profiles.spotify.id + '/playlists').on('complete', function(results, response) { + if (response.statusCode == 401) return res.redirect('/auth/spotify'); + res.render('sets-import', { + playlists: results.items + }); + }); + } }, edit: function(req, res, next) { @@ -70,14 +295,13 @@ module.exports = { if (!playlist) { return next(); } playlist.description = (req.param('description')) ? req.param('description') : playlist.description; - switch (req.param('public')) { - case 'true': - playlist.public = true; - break; - case 'false': - playlist.public = false; - break; + + if (req.param('status')) { + playlist.public = (req.param('status') === 'public') ? true : false; } + + playlist.updated = new Date(); + playlist.save(function(err) { res.send({ status: 'success' @@ -95,6 +319,7 @@ module.exports = { if (!track) { return next(); } playlist._tracks.push( track._id ); + playlist.updated = new Date(); playlist.save(function(err) { res.send({ status: 'success' @@ -110,4 +335,4 @@ module.exports = { }); } -}; \ No newline at end of file +}; diff --git a/controllers/rooms.js b/controllers/rooms.js new file mode 100644 index 00000000..79c756e7 --- /dev/null +++ b/controllers/rooms.js @@ -0,0 +1,72 @@ +var slugify = require('speakingurl'); + +module.exports = { + list: function(req, res, next) { + Room.find().exec(function(err, rooms) { + res.format({ + json: function() { res.send( rooms ); } + }) + }); + }, + create: function(req, res, next) { + var name = req.param('name'); + var slug = req.param('slug'); + var description = req.param('description'); + + if (!name || !slug) { + res.flash('error', 'You must provide a name and a slug!'); + return res.redirect('back'); + } + + slug = slugify( slug ); + + Room.count({ slug: slug }).exec(function(err, count) { + if (count) { + res.flash('error', 'That room already exists.'); + return res.redirect('back'); + } + + var room = new Room({ + name: name, + slug: slug, + description: description, + _creator: req.user._id, + _owner: req.user._id + }); + room.save(function(err) { + if (err) return res.error( err ); + + var playlist = [] + var app = req.app; + + app.rooms[ room.slug ] = room; + app.rooms[ room.slug ].playlist = playlist; + app.rooms[ room.slug ].listeners = {}; + + app.rooms[ room.slug ].bind( req.soundtrack ); + + app.rooms[ room.slug ].startMusic( errorHandler ); + + function done() { + app.locals.rooms = app.rooms; + + var config = req.app.config; + return res.redirect( ((config.app.safe) ? 'https://' : 'http://') + slug + '.' + config.app.host ); + } + + function errorHandler(err) { + if (err) { + return app.rooms[ room.slug ].retryTimer = setTimeout(function() { + app.rooms[ room.slug ].startMusic( errorHandler ); + }, 5000 ); + } + + return done(); + } + + }); + + }); + + } +} diff --git a/controllers/tracks.js b/controllers/tracks.js index 55f64d8b..5e61531f 100644 --- a/controllers/tracks.js +++ b/controllers/tracks.js @@ -1,32 +1,250 @@ var async = require('async') - , _ = require('underscore'); + , _ = require('underscore') + , util = require('../util'); module.exports = { list: function(req, res, next) { - Play.aggregate([ - { $group: { _id: '$_track', count: { $sum: 1 } } }, - { $sort: { 'count': -1 } }, - { $limit: 100 } - ], function(err, tracks) { - - Track.find({ _id: { $in: tracks.map(function(x) { return x._id; }) }}).populate('_artist').exec(function(err, tracks) { - res.render('tracks', { - tracks: tracks + var LIMIT = 10; + + var roomID = undefined; + if (req.roomObj) roomID = req.roomObj._id; + + var functions = []; + if (!req.param('q')) { + functions.push( function collectAllTime( done ) { + Play.aggregate([ + { $match: { _curator: { $exists: true }, _room: roomID } }, + { $group: { _id: '$_track', count: { $sum: 1 } } }, + { $sort: { 'count': -1 } }, + { $limit: LIMIT } + ], function(err, collected) { + Track.find({ _id: { $in: collected.map(function(x) { return x._id; }) } }).populate('_artist').exec(function(err, input) { + var output = []; + for (var i = 0; i < collected.length; i++) { + output.push( _.extend( collected[i] , input[i] ) ); + } + done( err , output ); + }); + } ); + } ); + + functions.push( function collectThirtyDays( done ) { + Play.aggregate([ + { $match: { + _curator: { $exists: true }, + _room: roomID, + timestamp: { $gte: new Date((new Date()) - 30 * 24 * 3600 * 1000) } + } }, + { $group: { _id: '$_track', count: { $sum: 1 } } }, + { $sort: { 'count': -1 } }, + { $limit: LIMIT } + ], function(err, collected) { + Track.find({ _id: { $in: collected.map(function(x) { return x._id; }) } }).populate('_artist').exec(function(err, input) { + var output = []; + for (var i = 0; i < collected.length; i++) { + output.push( _.extend( collected[i] , input[i] ) ); + } + done( err , output ); + }); + } ); + } ); + + functions.push( function collectSevenDays( done ) { + Play.aggregate([ + { $match: { + _curator: { $exists: true }, + _room: roomID, + timestamp: { $gte: new Date((new Date()) - 7 * 24 * 3600 * 1000) } + } }, + { $group: { _id: '$_track', count: { $sum: 1 } } }, + { $sort: { 'count': -1 } }, + { $limit: LIMIT } + ], function(err, collected) { + Track.find({ _id: { $in: collected.map(function(x) { return x._id; }) } }).populate('_artist').exec(function(err, input) { + var output = []; + for (var i = 0; i < collected.length; i++) { + output.push( _.extend( collected[i] , input[i] ) ); + } + done( err , output ); + }); + } ); + } ); + } + + async.parallel( functions , function(err, statResults) { + + var limit = (req.param('limit')) ? parseInt(req.param('limit')) : 100; + var query = (req.param('q')) ? { name: new RegExp('(.*)'+req.param('q')+'(.*)', 'i') } : {}; + + if (req.param('nsfw') === '✓') { + query.flags = { + nsfw: true + } + } + if (req.param('live') === '✓') { + query.flags = { + live: true + } + } + + Track.find( query ).populate('_artist').limit( limit ).exec(function(err, tracks) { + if (err) console.log(err); + + Track.count( query ).exec(function(err, count) { + if (err) console.log(err); + + res.format({ + json: function() { + res.send( tracks ); + }, + html: function() { + res.render('tracks', { + tracks: tracks + , count: count + , limit: limit + , query: req.param('q') + , topTracksAll: statResults[0] + , topTracks30: statResults[1] + , topTracks7: statResults[2] + }); + } + }); + }); + }); + }); + }, + pool: function(req, res, next) { + req.roomObj.generatePool(function(err, plays, query) { + if (!plays || !plays.length) var plays = []; + Track.find({ _id: { $in: plays.map(function(x) { return x._track; }) } }).populate('_artist _credits').exec(function(err, tracks) { + res.format({ + json: function() { + res.send( tracks ); + }, + html: function() { + res.render('pool', { + tracks: tracks, + query: query + }); + } }); }); - }); }, edit: function(req, res, next) { - Track.findOne({ _id: req.param('trackID') }).exec(function(err, track) { - if (err || !track) { return next(); } - track.title = (req.param('title')) ? req.param('title') : track.title; - track.save(function(err) { + if (!req.param('artistName') || !req.param('title')) { + + if (['true', 'false'].indexOf( req.param('nsfw') ) >= 0 ) { + Track.findOne({ _id: req.param('trackID') }).populate('_artist').exec(function(err, track) { + if (!track) { return next(); } + + track.flags.nsfw = req.param('nsfw'); + track.save(function(err) { + res.send({ + status: 'success' + , message: 'Track edited successfully.' + }); + + req.soundtrack.broadcast({ + type: 'edit' + , track: track + }); + + }); + }); + } else if (['true', 'false'].indexOf( req.param('live') ) >= 0 ) { + Track.findOne({ _id: req.param('trackID') }).populate('_artist').exec(function(err, track) { + if (!track) { return next(); } + + track.flags.live = req.param('live'); + track.save(function(err) { + res.send({ + status: 'success' + , message: 'Track edited successfully.' + }); + + req.soundtrack.broadcast({ + type: 'edit' + , track: track + }); + + }); + }); + } else { res.send({ - status: 'success' - , message: 'Track edited successfully.' + status: 'error' + , message: 'Required parameters not specified.' }); + } + } else { + // parse the submitted values (which may or may not yet exist in the database) + var stringToParse = req.param('artistName') + ' - ' + req.param('title'); + util.parseTitleString( stringToParse , function(parts) { + console.log('edited track, parsed parts into: ') + console.log(parts); + + // find the edited track... + Track.findOne({ _id: req.param('trackID') }).exec(function(err, track) { + if (err || !track) return next(); + + // get the artist parsed from the new artist name... + Artist.findOne({ name: parts.artist }).exec(function(err, artist) { + if (err) console.log(err); + + if (!artist ) var artist = new Artist({ name: req.param('artistName') }); + + // go ahead and issue a save for it (so it exists when we save the track) + artist.save(function(err) { + if (err) { console.log(err); } + + // find a list of artists in the parsed credits... + Artist.find({ name: { $in: parts.credits } }, { _id: 1 }).exec(function(err, credits) { + + // all we want are the IDs... + var creditIDs = credits.map(function(x) { return x._id; }); + + // update the corresponding track to set values... + // new? title + // new? artist + // new? credits + Track.update( + { _id: track._id }, + { + $addToSet: { _credits: { $each: creditIDs } } + , $set: { + _artist: (artist && artist._id.toString() != track._artist.toString()) ? artist._id : track._artist + , title: parts.title || track.title + } + } + ).exec(function(err, numAffected) { + + res.send({ + status: 'success' + , message: 'Track edited successfully.' + }); + + // prepare for over-the-wire broadcast... + track = track.toObject(); + track._artist = artist; + track.title = parts.title || track.title; + + req.soundtrack.broadcast({ + type: 'edit' + , track: track + }); + }); + }); + }); + }); + }); + }); + } + }, + merge: function(req, res, next) { + Track.findOne({ _id: req.param('from') }).exec(function(err, from) { + Track.findOne({ _id: req.param('to') }).exec(function(err, to) { + }); }); }, @@ -35,50 +253,149 @@ module.exports = { Track.findOne({ $or: [ { _id: req.param('trackID') } , { slug: req.param('trackSlug') } - ] }).populate('_artist').exec(function(err, track) { - res.format({ - json: function() { - res.send( track ); - }, - html: function() { - Play.find({ _track: track._id }).sort('-timestamp').populate('_curator').exec(function(err, history) { - if (err) { console.log(err); } - - var queries = []; - for (var d = 29; d >= 0; d--) { - - var start = new Date(); - start.setHours('0'); - start.setMinutes('0'); - start.setSeconds('0'); - start.setMilliseconds('0'); - - var end = new Date( start.getTime() ); - - start = new Date( start - (d * 1000 * 60 * 60 * 24) ); - end = new Date( start.getTime() + 1000 * 60 * 60 * 24 ); - - queries.push({ timestamp: { - $gte: start - , $lt: end - } }); - } - - // TODO: use some sort of map so this can be switched parallel - async.series( - queries.map(function(q) { - return function(done) { Play.count( _.extend({ _track: track._id }, q) ).exec(done); }; - }), function(err, playsPerDay) { - res.render('track', { - track: track - , history: history - , playsPerDay: playsPerDay - }); + ] }).populate('_artist _credits').exec(function(err, track) { + if (!track) { return next(); } + + var functions = [ + function(done) { + Chat.find({ _track: track._id }).sort('-_id').limit(20).populate('_author').exec( done ); + } + ]; + + if (!track._artist || (track._artist && track._artist.slug === '')) { + + functions.push(function(done) { + + var stringToParse = ''; + + for (var source in track.sources) { + for (var i = 0; i < track.sources[source].length; i++) { + if (track.sources[ source ][i].data && track.sources[ source ][i].data.title) { + stringToParse = track.sources[ source ][i].data.title; } - ); + } + } + + console.log('parsing: ' + stringToParse); + util.parseTitleString( stringToParse , function(parts) { + + Artist.findOne({ name: parts.artist }).exec(function(err, artist) { + if (!artist) { var artist = new Artist({ name: parts.artist }); } + + Artist.find({ name: { $in: parts.credits } }, { _id: 1 }).exec(function(err, artists) { + var creditIDs = artists.map(function(x) { return x._id.toString(); }); + + console.log(creditIDs); + + Track.update( + { _id: track._id }, + { + $addToSet: { _credits: { $each: creditIDs } } + , $set: { + _artist: (artist && artist._id.toString() != track._artist.toString()) ? artist._id : track._artist + , title: parts.title || track.title + } + } + ).exec( done ); + }); + }); }); - } + }); + } + + console.log('functions length is ' + functions.length ); + + async.series( functions , function(err, results) { + var chats = results[0]; + + Track.findOne({ $or: [ + { _id: req.param('trackID') } + , { slug: req.param('trackSlug') } + ] }).populate('_artist _credits').exec(function(err, track) { + + /* req.app.agency.publish('track:crawl', { + id: track._id + }, function(err) { + console.log('track crawling completed'); + }); */ + + res.format({ + json: function() { + res.send( track ); + }, + html: function() { + Play.find({ _track: track._id }).sort('-timestamp').populate('_curator _room').exec(function(err, history) { + if (err) { console.log(err); } + + var queries = []; + for (var d = 29; d >= 0; d--) { + + var start = new Date(); + start.setHours('0'); + start.setMinutes('0'); + start.setSeconds('0'); + start.setMilliseconds('0'); + + var end = new Date( start.getTime() ); + + start = new Date( start - (d * 1000 * 60 * 60 * 24) ); + end = new Date( start.getTime() + 1000 * 60 * 60 * 24 ); + + queries.push({ timestamp: { + $gte: start + , $lt: end + } }); + } + + // TODO: use some sort of map so this can be switched parallel + async.series( + queries.map(function(q) { + return function(done) { Play.count( _.extend({ _track: track._id }, q) ).exec(done); }; + }), function(err, playsPerDay) { + + Track.find({ + _artist: track._artist._id + , slug: track.slug + }).exec(function(err, dupes) { + + + Playlist.find({ + _creator: (req.user) ? req.user._id : undefined + }).exec(function(err, playlists) { + + Play.aggregate([ + { $match: { _track: track._id , _curator: { $exists: true } } }, + { $group: { _id: '$_room', count: { $sum: 1 } } }, + { $sort: { 'count': -1 } }, + //{ $limit: LIMIT } + ], function(err, topRooms) { + + Room.populate( topRooms , { + path: '_id' + }, function(err, populatedTopRooms) { + res.render('track', { + track: track + , history: history + , playsPerDay: playsPerDay + , chats: chats + , dupes: dupes + , playlists: playlists + , topRooms: topRooms + }); + }); + + + }); + + }); + }); + } + ); + }); + } + }); + }); }); }); } -} \ No newline at end of file +} diff --git a/db.js b/db.js index 1c6dd5c8..1bba2d1b 100644 --- a/db.js +++ b/db.js @@ -1,11 +1,12 @@ var config = require('./config') , mongoose = require('mongoose') - , redis = require("redis") + , redis = require('redis') , client = redis.createClient(); -mongoose.connect(config.database.host, config.database.name); +var source = mongoose.connect(config.database.host, config.database.name); module.exports = { mongoose: mongoose , client: client -}; \ No newline at end of file + , source: source +}; diff --git a/lib/Queue.js b/lib/Queue.js new file mode 100644 index 00000000..82d468cf --- /dev/null +++ b/lib/Queue.js @@ -0,0 +1,163 @@ +require('debug-trace')({ always: true }); + +var util = require('util'); +var EventEmitter = require('events').EventEmitter; + +var async = require('async'); +var redis = require('redis'); + +var Queue = function(config) { + var self = this; + + self._database = config.app.name || 'maki'; + self._redis = redis.createClient( config.redis.port , config.redis.host ); + + self._kue = require('kue'); + self._kue.redis.createClient = function() { + console.log('createClient()'); + return self._redis; + } + + self._jobs = self._kue.createQueue({ + prefix: config.app.name + ':q' + }); + + self.jobs = []; + +} +var Job = function( job ) { + // pull things from the internal job + this.id = job._job.id; + this.name = job.name; + this.type = job._job.data.type; + this.date = (new Date()).getTime(); + this.data = job._job.data.data; + this._job = job._job; +}; +var Worker = function( queue , type ) { + this.queue = queue; + this.type = type; +}; + +util.inherits( Queue , EventEmitter ); +util.inherits( Job , EventEmitter ); +util.inherits( Worker , EventEmitter ); + +Queue.prototype.get = function( jobID , cb ) { + var self = this; + + self._redis.get('jobs:' + jobID, function(err, reply) { + if (err || !reply) throw new Error('something went wrong:' + err); + + try { + var job = JSON.parse(reply); + } catch(e) { + return cb(e); + } + + if (!job) { + return self.cleanup( jobID , function() { + cb('no such entry in redis') + }); + } + + // kue-specific + self._kue.Job.get( jobID , function(err , thisKueJob ) { + if (err) return cb( err ); + job._job = thisKueJob; + var fullJob = new Job( job ); + return cb( null , fullJob ); + }); + }); +} + +Queue.prototype.register = function( kueJob ) { + +}; + +Queue.prototype.cleanup = function( jobID , cleanupComplete ) { + var self = this; + async.series([ + function(d) { self._redis.zrem('queue', jobID , d ); }, + function(d) { self._redis.del('job:' + jobID, jobID , d ); } + ], function(err) { + cleanupComplete( err ); + }); +} + +Queue.prototype.push = function( name , data , callback ) { + var self = this; + + var kueJob = self._jobs.create( self._database , data ); + + // now actually save and emit + kueJob.save(function(err) { + console.log('job saved to kue'); + + // our internal version of the job + var queueJob = new Job({ + title: data.name, + _job: kueJob + }); + + self.jobs.push( queueJob ); + + // save the data for the job + function addJob( j , d ) { + var metadata = j; + delete metadata._job; + self._redis.set('jobs:' + j.id , JSON.stringify( metadata ), d ); + } + + // take the job ID and insert it into the queue + function addJobToQueue( jobID , d ) { + self._redis.zadd( 'queue' , jobID , jobID , d ); + } + + async.waterfall([ + function(done) { addJob( queueJob , done ); }, + function(arg1 , done) { addJobToQueue( queueJob.id , done ); } + ], function(err, results) { + console.log('supppppppp') + console.log(err, results); + + self.emit('job', queueJob ); + + callback( err , queueJob ); + + }); + }); +} + +Queue.prototype.process = function( type , evaluator ) { + var self = this; + // get the list of previous jobs + self._redis.zrange('queue', 0, -1, function(e, jobs ) { + // populate them with their corresponding internals: + // - get the metadata (specific to Queue) + // - get the specific engine's version of the job (Kue) + async.map( jobs , function( j , c ) { + self.get( j , c ); + }, function(err, realJobs) { + if (err) console.log(err); + + self.jobs = realJobs; + console.log('queue strapped.', self.jobs.length + ' jobs available'); + + self._jobs.process( self._database , function( kueJob , kueComplete ) { + + console.log('inside kue job... id ' + kueJob.id ); + + self.get( kueJob.id , function(err, job) { + evaluator( job , function(err, result) { + self.cleanup( kueJob.id , function() { + kueComplete( err , result ); + }); + }); + }); + }); + }); + }); + +} +module.exports = Queue; \ No newline at end of file diff --git a/lib/last.fm.js b/lib/last.fm.js new file mode 100644 index 00000000..d7a25731 --- /dev/null +++ b/lib/last.fm.js @@ -0,0 +1,85 @@ +var _ = require('underscore'); + +module.exports = { + authSetup: function(req, res) { + //var authUrl = lastfm.getAuthenticationUrl({ cb: ((config.app.safe) ? 'http://' : 'http://') + config.app.host + '/auth/lastfm/callback' }); + var authUrl = lastfm.getAuthenticationUrl({ cb: (( app.config.app.safe) ? 'http://' : 'http://') + 'soundtrack.io/auth/lastfm/callback' }); + res.redirect(authUrl); + }, + authCallback: function(req, res) { + lastfm.authenticate( req.param('token') , function(err, session) { + console.log(session); + + if (err) { + console.log(err); + req.flash('error', 'Something went wrong with authentication.'); + return res.redirect('/'); + } + + Person.findOne({ $or: [ + { _id: (req.user) ? req.user._id : undefined } + , { 'profiles.lastfm.username': session.username } + ]}).exec(function(err, person) { + + if (!person) { + var person = new Person({ username: 'reset this later ' }); + } + + person.profiles.lastfm = { + username: session.username + , key: session.key + , updated: new Date() + }; + + person.save(function(err) { + if (err) { console.log(err); } + req.session.passport.user = person._id; + res.redirect('/'); + }); + + }); + + }); + }, + scrobbleActive: function(requestedTrack, cb) { + console.log('scrobbling to active listeners...'); + + Track.findOne({ _id: requestedTrack._id }).populate('_artist').exec(function(err, track) { + if (!track || track._artist.name && track._artist.name.toLowerCase() == 'gobbly') { return false; } + + Person.find({ _id: { $in: _.toArray(app.room.listeners).map(function(x) { return x._id; }) } }).exec(function(err, people) { + _.filter( people , function(x) { + console.log('evaluating listener:'); + console.log(x); + return (x.profiles && x.profiles.lastfm && x.profiles.lastfm.username && x.preferences.scrobble); + } ).forEach(function(user) { + console.log('listener available:'); + console.log(user); + + var lastfm = new app.LastFM({ + api_key: app.config.lastfm.key + , secret: app.config.lastfm.secret + }); + + var creds = { + username: user.profiles.lastfm.username + , key: user.profiles.lastfm.key + }; + + lastfm.setSessionCredentials( creds.username , creds.key ); + lastfm.track.scrobble({ + artist: track._artist.name + , track: track.title + , timestamp: Math.floor((new Date()).getTime() / 1000) - 300 + }, function(err, scrobbles) { + if (err) { return console.log('le fail...', err); } + + console.log(scrobbles); + cb(); + }); + }); + }); + }); + + } +} \ No newline at end of file diff --git a/lib/soundtrack.js b/lib/soundtrack.js new file mode 100644 index 00000000..b420d40a --- /dev/null +++ b/lib/soundtrack.js @@ -0,0 +1,671 @@ +var _ = require('underscore'); +var util = require('../util'); +var rest = require('restler'); +var async = require('async'); +var slug = require('speakingurl'); + +var Soundtrack = function(app) { + var self = this; + + this.app = app; + this.app.rooms = {}; + this.backupTracks = []; + this.timers = { + scrobble: {} + }; + + this.DEBUG = false; + +}; + +Soundtrack.prototype.start = function() { + var self = this; + // periodically check for idle sockets + setInterval(function() { + self.markAndSweep(); + }, self.app.config.connection.checkInterval ); +}; + +Soundtrack.prototype.sortPlaylist = function() { + var self = this; + var app = this.app; + app.room.playlist = _.union( [ app.room.playlist[0] ] , app.room.playlist.slice(1).sort(function(a, b) { + if (b.score === a.score) { + return a.timestamp - b.timestamp; + } else { + return b.score - a.score; + } + }) ); +} +Soundtrack.prototype.broadcast = function(msg) { + var self = this; + var app = this.app; + switch (msg.type) { + // special handling for edits + // this allows us to simply update the in-memory version of a track, + // rather than querying mongoDB several more times for data. + case 'edit': + for (var roomName in app.rooms) { + var room = app.rooms[ roomName ]; + for (var i = 0; i < room.playlist.length; i++) { + if (!room.playlist[ i ]._id || !msg.track._id) return console.error('no id somewhere...'); + if ( room.playlist[ i ]._id.toString() == msg.track._id.toString() ) { + room.playlist[ i ].title = msg.track.title; + room.playlist[ i ].slug = msg.track.slug; + room.playlist[ i ].flags = msg.track.flags; + room.playlist[ i ]._artist.name = msg.track._artist.name; + room.playlist[ i ]._artist.slug = msg.track._artist.slug; + } + } + } + break; + } + + //app.redis.set(app.config.database.name + ':playlist', JSON.stringify( app.room.playlist ) ); + + var json = JSON.stringify(msg); + for (var id in app.clients) { + app.clients[id].write(json); + } +}; +Soundtrack.prototype.whisper = function(id, msg) { + var self = this; + var app = this.app; + var json = JSON.stringify(msg); + app.clients[id].write(json); +}; +Soundtrack.prototype.markAndSweep = function(){ + var self = this; + var app = this.app; + + self.broadcast({type: 'ping'}); // we should probably not do this globally... instead, start interval after client connect? + + var time = (new Date()).getTime(); + self.forEachClient(function(client, id){ + if (client.pongTime < time - app.config.connection.clientTimeout) { + client.close('', 'Timeout'); + + for (var roomName in app.rooms) { + var room = app.rooms[ roomName ]; + + if (client.user) { + if (!room.listeners[ client.user._id ]) { + room.listeners[ client.user._id ] = { ids: [] }; + } + + room.listeners[ client.user._id ].ids = _.reject( room.listeners[ client.user._id ].ids , function(x) { + return x == client.id; + }); + } + + for (var userID in room.listeners) { + if (room.listeners[ userID ].ids.length === 0) { + delete room.listeners[ userID ]; + self.broadcast({ + type: 'part' + , data: { + _id: (app.clients[id] && app.clients[id].user) ? app.clients[id].user._id : undefined + } + }); + } + } + } + + delete app.clients[id]; + } + }); +}; +Soundtrack.prototype.forEachClient = function(fn) { + var self = this; + var app = this.app; + for (var id in app.clients) { + fn(app.clients[id], id) + } +}; + +Soundtrack.prototype.trackFromSource = function(source, id, sourceCallback) { + var self = this; + var app = self.app; + + if (self.DEBUG) console.log('trackFromSource() : ' + source + ' ' + id ); + + switch (source) { + default: + sourceCallback('Unknown source: ' + source); + break; + case 'soundtrack': + Track.findOne({ _id: id }).populate('_artist').exec( sourceCallback ); + break; + case 'bandcamp': + var obj = id; + Artist.findOne({ slug: slug(obj.artist) }).exec(function(err, artist) { + if (err || !artist) var artist = new Artist({ name: obj.artist }); + + Track.findOne({ + slug: slug(obj.title), + _artist: artist._id + }).exec(function(err, track) { + if (err || !track) var track = new Track({ title: obj.title, sources: obj.sources }); + + track._artist = artist._id; + track.duration = obj.duration; + + if (obj.thumbnail) { + track.images.thumbnail = obj.thumbnail; + } + track.sources.bandcamp = [ { + id: obj.id, + + data: { + track: obj.id, + url: obj.url, + artwork_url: obj.artwork_url, + size: 'large', + bgcol: 'ffffff', + linkcol: '0687f5', + tracklist: false, + transparent: true, + baseUrl: 'http://bandcamp.com/EmbeddedPlayer/' + } + } ] + + artist.save(function(err) { + if (err) console.log(err); + track.save(function(err) { + if (err) console.log(err); + track._artist = artist; + sourceCallback( err , track ); + }); + }); + + }); + + }); + break; + case 'object': + var obj = id; + Artist.findOne({ slug: slug(obj.artist) }).exec(function(err, artist) { + if (err || !artist) var artist = new Artist({ name: obj.artist }); + + Track.findOne({ + slug: slug(obj.title), + _artist: artist._id + }).exec(function(err, track) { + if (err || !track) var track = new Track({ title: obj.title }); + + track._artist = artist._id; + track.duration = obj.duration; + + artist.save(function(err) { + if (err) console.log(err); + track.save(function(err) { + if (err) console.log(err); + + track._artist = artist; + sourceCallback( err , track ); + + if (app.config.jobs && app.config.jobs.enabled) { + app.agency.publish('track:crawl', { + id: track._id + }, function(err) { + console.log('track crawling completed'); + }); + } + + }); + }); + + }); + + }); + break; + case 'lastfm': + // TODO: make this work at top level of this function + var data = id; + + if (!data.url) { return sourceCallback('no url (id used for lastfm)'); } + if (!data.name) { return sourceCallback('no title'); } + + Track.findOne({ 'sources.lastfm.id': data.url }).exec(function(err, track) { + if (err) { return sourceCallback(err); } + + Artist.findOne({ $or: [ + { slug: slug( data.artist.name ) } + , { name: data.artist.name } + ] }).exec(function(err, artist) { + if (err) { return sourceCallback(err); } + + if (!artist) { + var artist = new Artist({ name: data.artist.name }); + } + + artist.tracking.tracks.updated = new Date(); + + artist.save(function(err) { + if (err) { return sourceCallback(err); } + + if (!track) { + var track = new Track({ + title: data.name + , _artist: artist._id + , duration: data.duration + , sources: { + lastfm: [ { + id: data.url + , duration: data.duration + , data: data + } ] + } + }); + } + + self.gatherSources( track , function(err, gatheredTrack) { + // if there was an error, do NOT save! + // this prevents tracks with no playable sources from saving to + // the database. + if (err) { + console.log( err ); + return sourceCallback(err); + } + + track.save(function(err) { + return sourceCallback( err , track ); + }); + }); + + }); + }); + }); + break; + case 'soundcloud': + rest.get('https://api.soundcloud.com/tracks/'+parseInt(id)+'.json?client_id='+app.config.soundcloud.id).on('complete', function(data, response) { + console.log(response); + if (!data.title) return sourceCallback('No Soundcloud track found in ' , data ); + + var TRACK_SEPARATOR = ' - '; + var stringToParse = (data.title.split( TRACK_SEPARATOR ).length > 1) ? data.title : data.user.username + ' - ' + data.title; + + util.parseTitleString( stringToParse , function(parts) { + + //console.log('parts: ' + JSON.stringify(parts) ); + + // does the track already exist? + Track.findOne({ $or: [ + { 'sources.soundcloud.id': data.id } + ] }).exec(function(err, track) { + if (!track) { var track = new Track({}); } // no? create a new one. + + // does the artist already exist? + Artist.findOne({ $or: [ + { _id: track._artist } + , { slug: slug( parts.artist ) } + ] }).exec(function(err, artist) { + if (err) { console.log(err); } + if (!artist) { var artist = new Artist({}); } // no? create a new one. + + artist.name = artist.name || parts.artist; + + artist.save(function(err) { + if (err) { console.log(err); } + + track.title = track.title || parts.title; + track._artist = track._artist || artist._id; + track.duration = track.duration || data.duration / 1000; + + var sourceIDs = track.sources[ source ].map(function(x) { return x.id; }); + var index = sourceIDs.indexOf( data.id ); + if (index == -1) { + track.sources[ source ].push({ + id: data.id + , data: data + }); + } else { + track.sources[ source ][ index ].data = data; + } + + track.save(function(err) { + Artist.populate(track, { + path: '_artist' + }, function(err, track) { + sourceCallback(err, track); + }); + }); + + }); + + }); + + }); + }); + }); + break; + case 'youtube': + self.getYoutubeVideo( id , function(track) { + if (track) { + return sourceCallback(null, track); + } else { + return sourceCallback('No track returned.'); + } + }); + break; + } +}; + +Soundtrack.prototype.getYoutubeVideo = function(videoID, internalCallback) { + var self = this; + var app = self.app; + + console.log('getYoutubeVideo() : ' + videoID ); + rest.get('http://gdata.youtube.com/feeds/api/videos/'+videoID+'?v=2&alt=jsonc').on('complete', function(data, response) { + if (!data || !data.data) { return internalCallback('error retrieving video from youtube: ' + JSON.stringify(data) ); } + + var video = data.data; + Track.findOne({ + 'sources.youtube.id': video.id + }).exec(function(err, track) { + if (err) console.error( err ); + if (track) { + return Artist.populate( track , { + path: '_artist' + }, function(err, track) { + internalCallback( track ); + }); + } + + var track = new Track({ title: video.title }); + + util.parseTitleString( video.title , function(parts) { + + if (self.DEBUG) console.log( video.title + ' was parsed into:'); + if (self.DEBUG) console.log(parts); + + async.mapSeries( parts.credits , function( artistName , artistCollector ) { + Artist.findOne({ $or: [ + { slug: slug( artistName ) } + , { name: artistName } + ] }).exec( function(err, artist) { + if (!artist) var artist = new Artist({ name: artistName }); + + artist.save(function(err) { + if (err) { console.log(err); } + artistCollector(err, artist); + }); + }); + }, function(err, results) { + + Artist.findOne({ $or: [ + { _id: track._artist } + , { slug: slug( parts.artist ) } + , { name: parts.artist } + ] }).exec(function(err, artist) { + if (!artist) var artist = new Artist({ name: parts.artist }); + artist.save(function(err) { + if (err) console.log(err); + + // only use parsed version if original title is unchanged + track.title = (track.title == video.title) ? parts.title : track.title; + track._artist = artist._id; + track._credits = results.map(function(x) { return x._id; }); + + track.duration = (track.duration) ? track.duration : video.duration; + track.images.thumbnail.url = (track.images.thumbnail.url) ? track.images.thumbnail.url : video.thumbnail.sqDefault; + + var youtubeVideoIDs = track.sources.youtube.map(function(x) { return x.id; }); + var index = youtubeVideoIDs.indexOf( video.id ); + if (index == -1) { + track.sources.youtube.push({ + id: video.id + , data: video + }); + } else { + track.sources.youtube[ index ].data = video; + } + + track.save(function(err) { + if (err) console.log(err); + + // begin cleanup + //track = track.toObject(); + track._artist = { + _id: artist._id + , name: artist.name + , slug: artist.slug + }; + + for (var source in track.sources.toObject()) { + for (var i = 0; i < track.sources[ source ].length; i++) { + delete track.sources[ source ].data; + } + } + // end cleanup + + internalCallback( track ); + }); + }); + }); + }); + }); + }); + }); +}; + +Soundtrack.prototype.gatherSources = function(track , callback) { + var self = this; + + var now = new Date(); + + Artist.populate( track , { + path: '_artist' + }, function(err, track) { + if (err) return callback(err); + if (!track._artist) return callback('no artist provided'); + + var fullTitle = track._artist.name + ' - ' + track.title; + var query = encodeURIComponent( fullTitle ); + var lenMax = track.duration + (track.duration * 0.05); + var lenMin = track.duration - (track.duration * 0.05); + + var maxTracks = 5; + + async.parallel([ + function(done) { + rest.get('https://gdata.youtube.com/feeds/api/videos?max-results='+maxTracks+'&v=2&alt=jsonc&q=' + query ).on('complete', function(data) { + var functions = []; + + if (data.data && data.data.items) { + + console.log('youtube videos gathered, %d videos', data.data.items.length); + + for (var i = 0; i < data.data.items.length; i++) { + if (self.DEBUG) console.log('trying youtube %d', i ); + + var item = data.data.items[ i ]; + if (item.title != fullTitle) { continue; } + + if (item.duration > lenMin && item.duration < lenMax) { + functions.push( function(cb) { + self.trackFromSource( 'youtube' , item.id , cb ); + } ); + } + } + + console.log('%d items, %e functions', data.data.items.length , functions.length ); + + } + + async.parallel( functions , function( err , youtubeTracks ) { + youtubeTracks.forEach(function(singleTrack) { + if (!singleTrack.sources) { + console.log('wattttttttttttttt', singleTrack); + return; + } + singleTrack.sources.youtube.forEach(function(youtubeSource) { + track.sources.youtube.push( youtubeSource ); + }); + }); + + console.log('youtube complete!'); + done( err , youtubeTracks ); + }); + + }); + }, + function(done) { + rest.get('https://api.soundcloud.com/tracks.json?limit='+maxTracks+'&client_id=7fbc3f4099d3390415d4c95f16f639ae&q='+query).on('complete', function(data) { + var functions = []; + + if (data instanceof Array) { + + console.log('soundcloud gathered, %d tracks', data.length); + for (var i = 0; i < data.length; i++) { + if (self.DEBUG) console.log('trying soundcloud %d', i ); + + var item = data[ i ]; + item.duration = item.duration / 1000; + if (item.duration > lenMin && item.duration < lenMax) { + if (item.title != fullTitle) { continue; } + + functions.push( function(cb) { + self.trackFromSource( 'soundcloud' , item.id , cb ); + } ); + } + } + + console.log('%d items, %e functions', data.length , functions.length ); + } + + async.parallel( functions , function( err , soundcloudTracks ) { + + soundcloudTracks.forEach(function(singleTrack) { + if (!singleTrack) return; + singleTrack.sources.soundcloud.forEach(function(soundcloudSource) { + track.sources.soundcloud.push( soundcloudSource ); + }); + }); + + console.log('soundcloud complete!'); + done( err , soundcloudTracks ); + }); + + }); + } + ], function(err, results) { + console.log('all sources complete!'); + + track.updated = new Date(); + + var playableSources = 0; + for (var source in track.sources) { + if (!track.sources[ source ]) continue; + for (var i = 0; i < track.sources[ source ].length; i++) { + if (['soundcloud', 'youtube'].indexOf( source ) >= 0) playableSources += 1; + } + } + + if (!playableSources) { + if (self.DEBUG) console.log('HEYYYYYYYYY NONE'); + return callback('No playable sources.'); + } + + if (self.DEBUG) console.log('saving...'); + + track.save(function(err) { + console.log('saved!', err); + callback(err, results); + }); + + }); + }); +} + + +Soundtrack.prototype.lastfmAuthSetup = function(req, res) { + var self = this; + var app = this.app; + + //var authUrl = lastfm.getAuthenticationUrl({ cb: ((config.app.safe) ? 'http://' : 'http://') + config.app.host + '/auth/lastfm/callback' }); + var authUrl = app.lastfm.getAuthenticationUrl({ cb: 'https://soundtrack.io/auth/lastfm/callback' }); + res.redirect(authUrl); +}; +Soundtrack.prototype.lastfmAuthCallback = function(req, res) { + var self = this; + var app = this.app; + + lastfm.authenticate( req.param('token') , function(err, session) { + console.log(session); + + if (err) { + console.log(err); + req.flash('error', 'Something went wrong with authentication.'); + return res.redirect('/'); + } + + Person.findOne({ $or: [ + { _id: (req.user) ? req.user._id : undefined } + , { 'profiles.lastfm.username': session.username } + ]}).exec(function(err, person) { + + if (!person) { + var person = new Person({ username: 'reset this later ' }); + } + + person.profiles.lastfm = { + username: session.username + , key: session.key + , updated: new Date() + }; + + person.save(function(err) { + if (err) { console.log(err); } + req.session.passport.user = person._id; + res.redirect('/'); + }); + + }); + + }); +}; +Soundtrack.prototype.scrobbleActive = function(requestedTrack, cb) { + var self = this; + var app = this.app; + + console.log('scrobbling to active listeners...'); + + Track.findOne({ _id: requestedTrack._id }).populate('_artist').exec(function(err, track) { + if (!track || track._artist.name && track._artist.name.toLowerCase() == 'gobbly') { return false; } + + Person.find({ _id: { $in: _.toArray(app.room.listeners).map(function(x) { return x._id; }) } }).exec(function(err, people) { + _.filter( people , function(x) { + console.log('evaluating listener:'); + console.log(x); + return (x.profiles && x.profiles.lastfm && x.profiles.lastfm.username && x.preferences.scrobble); + } ).forEach(function(user) { + console.log('listener available:' + user._id + ' ' + user.username ); + + var lastfm = new app.LastFM({ + api_key: app.config.lastfm.key + , secret: app.config.lastfm.secret + }); + + var creds = { + username: user.profiles.lastfm.username + , key: user.profiles.lastfm.key + }; + + lastfm.setSessionCredentials( creds.username , creds.key ); + lastfm.track.scrobble({ + artist: track._artist.name + , track: track.title + , timestamp: Math.floor((new Date()).getTime() / 1000) - 300 + }, function(err, scrobbles) { + if (err) { return console.log('le fail...', err); } + + console.log(scrobbles); + cb(); + }); + }); + }); + }); +} + +module.exports = Soundtrack; diff --git a/models/Artist.js b/models/Artist.js index 4eb0a9df..9966dae9 100644 --- a/models/Artist.js +++ b/models/Artist.js @@ -8,28 +8,46 @@ var mongoose = require('mongoose') // this defines the fields associated with the model, // and moreover, their type. var ArtistSchema = new Schema({ - name: { type: String, required: true, unique: true } + name: { type: String, required: true, unique: true } // canonical name + , names: [ { type: String } ] // known names , image: { url: { type: String, default: 'http://coursefork.org/img/user-avatar.png' } } , bio: String + , tracking: { + tracks: { + updated: { type: Date , default: 0 } + } + } }); -ArtistSchema.post('init', function() { +ArtistSchema.pre('save', function(next) { var self = this; if (!self.bio || !self.image.url) { - rest.get('http://ws.audioscrobbler.com/2.0/?method=artist.getinfo&artist=Cher&format=json&api_key=89a54d8c58f533944fee0196aa227341').on('complete', function(data) { + rest.get('http://ws.audioscrobbler.com/2.0/?method=artist.getinfo&artist='+encodeURIComponent(self.name)+'&format=json&api_key=89a54d8c58f533944fee0196aa227341').on('complete', function(data) { if (data.artist) { - self.bio = strip_tags(data.artist.bio.summary).replace(/Read more about (.*) on Last.fm./, ''); - self.image.url = data.artist.image[3]['#text']; + if (data.artist.bio) { + self.bio = strip_tags(data.artist.bio.summary).replace(/Read more about (.*) on Last.fm./, ''); + } + if (data.artist.image[3]) { + self.image.url = data.artist.image[3]['#text']; + } } + + next(); }); + } else { + next(); } }); -ArtistSchema.plugin( slug('name') ); +ArtistSchema.plugin( slug('name', { + track: true + , lang: false +}) ); ArtistSchema.index({ slug: 1 }); +ArtistSchema.index({ slugs: 1 }); var Artist = mongoose.model('Artist', ArtistSchema); @@ -77,4 +95,4 @@ function strip_tags (input, allowed) { return input.replace(commentsAndPhpTags, '').replace(tags, function ($0, $1) { return allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : ''; }); -} \ No newline at end of file +} diff --git a/models/Chat.js b/models/Chat.js index b842714f..37da2b30 100644 --- a/models/Chat.js +++ b/models/Chat.js @@ -6,15 +6,20 @@ var ChatSchema = new Schema({ _author: { type: ObjectId, required: true, ref: 'Person' } , created: { type: Date, default: Date.now } , message: { type: String } + , _track: { type: ObjectId, ref: 'Track', index: true } + , _play: { type: ObjectId, ref: 'Play' , index: true } + , _room: { type: ObjectId, ref: 'Room' , index: true } }); ChatSchema.virtual('isoDate').get(function() { return this.created.toISOString(); }); +ChatSchema.index({ _room: 1 , created: -1 }); + var Chat = mongoose.model('Chat', ChatSchema); // export the model to anything requiring it. module.exports = { Chat: Chat -}; \ No newline at end of file +}; diff --git a/models/Person.js b/models/Person.js index e83c0592..08a072d2 100644 --- a/models/Person.js +++ b/models/Person.js @@ -8,7 +8,8 @@ var mongoose = require('mongoose') // and moreover, their type. var PersonSchema = new Schema({ email: { type: String, unique: true, sparse: true } - , roles: [ { type: String, enum: ['editor'] } ] + , roles: [ { type: String, enum: ['editor', 'moderator'] } ] + , created: { type: Date , default: Date.now , required: true } , avatar: { url: { type: String, default: '/img/user-avatar.png' } } @@ -19,11 +20,19 @@ var PersonSchema = new Schema({ , username: String , key: String , updated: Date + }, + spotify: { + id: String, + username: String, + token: String, + updated: Date, + expires: Number } } , preferences: { scrobble: { type: Boolean, default: true } } + , _playlists: [ { type: ObjectId , ref: 'Playlist' } ] }); PersonSchema.plugin(passportLocalMongoose); diff --git a/models/Play.js b/models/Play.js index 9808c698..aaf05d96 100644 --- a/models/Play.js +++ b/models/Play.js @@ -5,15 +5,23 @@ var mongoose = require('mongoose') // this defines the fields associated with the model, // and moreover, their type. var PlaySchema = new Schema({ - _track: { type: ObjectId, ref: 'Track' } - , _curator: { type: ObjectId, ref: 'Person' } - , timestamp: { type: Date, default: Date.now } + _track: { type: ObjectId, ref: 'Track' , index: true } + , _artist: { type: ObjectId, ref: 'Artist' } + , _artists: [ { type: ObjectId, ref: 'Artist' } ] + , _curator: { type: ObjectId, ref: 'Person', index: true } + , _room: { type: ObjectId, ref: 'Room', required: true , index: true } + , timestamp: { type: Date, default: Date.now, index: true } + , length: { type: Number } + , played: { type: Number } }); PlaySchema.virtual('isoDate').get(function() { return this.timestamp.toISOString(); }); +PlaySchema.index({ timestamp: 1 , _room: 1 }); +PlaySchema.index({ timestamp: 1 , _room: 1 }); + var Play = mongoose.model('Play', PlaySchema); // export the model to anything requiring it. diff --git a/models/Playlist.js b/models/Playlist.js index a4087ff9..84c1caf7 100644 --- a/models/Playlist.js +++ b/models/Playlist.js @@ -12,14 +12,27 @@ var PlaylistSchema = new Schema({ , created: { type: Date, default: Date.now } , updated: { type: Date } , _creator: { type: ObjectId, ref: 'Person' } + , _owner: { type: ObjectId, ref: 'Person', index: true } + , _parent: { type: ObjectId, ref: 'Playlist' } , _tracks: [ { type: ObjectId, ref: 'Track' } ] , _subscribers: [ { type: ObjectId, ref: 'Person' } ] + , remotes: { + spotify: { + id: String, + updated: { type: Date , default: Date.now } + } + } }); PlaylistSchema.virtual('isoDate').get(function() { return this.timestamp.toISOString(); }); +PlaylistSchema.post('init', function() { + if (!this._owner) this._owner = this._creator; + if (!this.updated) this.updated = this.created; +}); + PlaylistSchema.plugin( slug('name') ); PlaylistSchema.index({ slug: 1 }); diff --git a/models/Room.js b/models/Room.js new file mode 100644 index 00000000..ad96d9fe --- /dev/null +++ b/models/Room.js @@ -0,0 +1,416 @@ +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; +var ObjectId = mongoose.SchemaTypes.ObjectId; +var slug = require('mongoose-slug'); + +var _ = require('underscore'); +var async = require('async'); +var util = require('../util'); + +// this defines the fields associated with the model, +// and moreover, their type. +var RoomSchema = new Schema({ + name: { type: String , required: true } + , description: { type: String } + , _creator: { type: ObjectId, ref: 'Person' } + , _owner: { type: ObjectId, ref: 'Person' } + , created: { type: Date, default: Date.now } + , _moderators: [ { type: ObjectId , ref: 'Person' } ] +}); + +RoomSchema.plugin( slug('name'), { + required: true +} ); +RoomSchema.index({ slug: 1 }); + +RoomSchema.methods.bind = function( soundtrack ) { + this.soundtrack = soundtrack; +}; +RoomSchema.methods.broadcast = function( msg , GLOBAL ) { + if (GLOBAL) return this.soundtrack.broadcast( msg ); + + var room = this; + var app = room.soundtrack.app; + + var myClients = _.flatten( _.toArray( room.listeners ).map(function(l) { + return l.ids; + }) ); + + var json = JSON.stringify(msg); + for (var id in app.clients) { + if (app.clients[id].room === room._id.toString()) { + app.clients[id].write(json); + } + } +}; +RoomSchema.methods.queueTrack = function( track , curator , callback ) { + var room = this; + + Track.findOne({ _id: track._id }).populate('_artist _credits').exec(function(err, realTrack) { + if (err || !realTrack) return callback('Could not acquire that track.'); + + var playlistItem = realTrack.toObject(); + + playlistItem._artist = { + _id: playlistItem._artist._id + , name: playlistItem._artist.name + , slug: playlistItem._artist.slug + }; + + var playableSources = 0; + for (var source in playlistItem.sources) { + for (var i = 0; i < playlistItem.sources[ source ].length; i++) { + if (['soundcloud', 'youtube', 'bandcamp'].indexOf( source ) >= 0) playableSources += 1; + delete playlistItem.sources[ source ][ i ].data; + } + } + + if (!playableSources) { + return callback({ + status: 'error' + , message: 'No playable sources.' + }); + } + + room.playlist.push( _.extend( playlistItem , { + score: 0 + , votes: {} // TODO: auto-upvote? + , timestamp: new Date() + , curator: { + _id: curator._id + , username: curator.username + , slug: curator.slug + } + } ) ); + + room.sortPlaylist(); + + room.savePlaylist(function() { + room.broadcast({ + type: 'playlist:add', + data: track + }); + return callback(); + }); + }); +}; +RoomSchema.methods.sortPlaylist = function() { + var room = this; + room.playlist = _.union( [ room.playlist[0] ] , room.playlist.slice(1).sort(function(a, b) { + if (b.score === a.score) { + return a.timestamp - b.timestamp; + } else { + return b.score - a.score; + } + }) ); +}; +RoomSchema.methods.savePlaylist = function( saved ) { + if (!saved) var saved = new Function(); + var self = this; + var app = self.soundtrack.app; + + //console.log('saving playlist'); + //console.log('as exists', self.playlist ); + //console.log('as stringified', JSON.stringify( self.playlist )); + + app.redis.set( app.config.database.name + ':rooms:' + self.slug + ':playlist', JSON.stringify( self.playlist ) ); + + app.rooms[ self.slug ] = self; + + saved(); +}; + +RoomSchema.methods.generatePool = function( gain , failpoint , cb ) { + var room = this; + var MAXIMUM_PLAY_AGE = 180; + + if (typeof(gain) === 'function') { + var cb = gain; + var gain = 0; + var failpoint = MAXIMUM_PLAY_AGE; + } + + if (typeof(failpoint) === 'function') { + var cb = failpoint; + var failpoint = MAXIMUM_PLAY_AGE; + } + + if (!gain) var gain = 0; + if (!failpoint) var failpoint = MAXIMUM_PLAY_AGE; + + var query = {}; + + // must be queued by a real person + query._curator = { $exists: true }; + // must have been played in this room + query._room = room._id; + // must have been queued within the past 7 days + query = _.extend( query , { + $or: util.timeSeries('timestamp', 3600*3*1000, 24*60*1000*60, 7 + gain ), + //timestamp: { $lt: (new Date()) - 3600 * 3 * 1000 } + }); + + // but not if it's been played recently! + // TODO: one level of callbacks to collect this! + Play.count({ _room: room._id }).exec(function(err, totalPlays) { + if (!totalPlays) { + // no tracks have ever been played. full query. + query = {}; + } else if (gain > failpoint) { + // just query the whole damned room. + query = { _room: room._id }; + } + + Play.find( query ).limit( 4096 ).sort('timestamp').exec(function(err, plays) { + Play.find({ + _room: room._id, + timestamp: { $gte: (new Date()) - 3600 * 3 * 1000 } + }).exec(function(err, exclusions) { + query.exclusionIDs = exclusions.map(function(x) { return x._track.toString(); }); + + plays = plays.filter(function(x) { + //console.log('filtering ', x ); + //console.log('exclusions checker,', x._track.toString() , 'in' , query.exclusionIDs , '?'); + //console.log(!~query.exclusionIDs.indexOf( x._track.toString() )); + return !~query.exclusionIDs.indexOf( x._track.toString() ); + }); + + if (err) console.error( err ); + if ((!plays || plays.length < 10) && (gain <= failpoint)) return room.generatePool( gain + 7 , failpoint , cb ); + if ((!plays) && (gain > failpoint)) return cb('init'); + + return cb( err , plays , query ); + + }); + + }); + }); + +}; +RoomSchema.methods.selectTrack = function( cb ) { + var room = this; + + room.generatePool(function(err, plays) { + if (err || !plays || plays.length === 0) { + console.log('room ' + room.slug + ' has no pool (POOL\'S CLOSED!)'); + return room.soundtrack.trackFromSource('youtube', 'wZThMWK9GxA', function(err, track) { + Artist.populate( track , '_artist' , cb ); + }); + } + + var randomSelection = plays[ _.random(0, plays.length - 1 ) ]; + Track.findOne({ _id: randomSelection._track }).populate('_artist').exec( cb ); + }); + +}; +RoomSchema.methods.ensureQueue = function(callback) { + var room = this; + if (room.playlist.length > 0) return callback(); + + room.selectTrack(function(err, track) { + if (err || !track) return callback( err ); + track.startTime = Date.now(); + // TODO: add score: 0 and votes: {}? + room.playlist.push( track ); + return callback(); + }); + +}; +RoomSchema.methods.nextSong = function( done ) { + if (!done) var done = new Function(); + var room = this; + var app = room.soundtrack.app; + + //console.log('old playlist length', room.playlist.length); + var lastTrack = room.playlist.shift(); + //console.log('lastTrack was', lastTrack); + //console.log('new playlist length', room.playlist.length); + + room.ensureQueue(function() { + room.savePlaylist(function() { + //console.log('saved, ', err ); + room.startMusic(function() { + console.log('nextSong() started music'); + done(); + }); + }); + }); +}; + +RoomSchema.methods.startMusic = function( cb ) { + var room = this; + if (!room.playlist[0]) { + console.log('no playlist'); + return Track.count(function(err, count) { + if (!count) return cb('no tracks. new install? TODO: base set.'); + var rand = Math.floor(Math.random() * count); + Track.findOne().skip( rand ).exec(function(err, track) { + room.playlist.push( track ); + room.savePlaylist(function(err) { + return cb('zero-length playlist. inserting random'); + }); + }); + }); + } + + room.track = room.playlist[0]; + if (!room.track.startTime) room.track.startTime = Date.now(); + + var now = Date.now(); + var seekTo = (now - room.playlist[0].startTime) / 1000; + + Track.findOne({ _id: room.track._id }).populate('_artist _artists').lean().exec(function(err, track) { + if (err || !track) return cb('no such track (severe error)'); + + // temporary collect exact matches... + // testing for future merging of track data for advances + var query = { _artist: track._artist._id , title: track.title, _id: { $ne: track._id } }; + Track.find( query ).lean().exec(function(err, tracks) { + var sources = track.sources; + tracks.forEach(function(t) { + for (var source in t.sources) { + sources[ source ] = _.union( sources[ source ] , t.sources[ source ] ); + } + }); + + room.broadcast({ + type: 'track', + data: _.extend( room.track , track ), + sources: sources, + seekTo: seekTo + }); + + clearTimeout( room.trackTimer ); + clearTimeout( room.permaTimer ); + + room.trackTimer = setTimeout(function() { + room.nextSong(); + }, (room.track.duration - seekTo) * 1000 ); + + if (room.soundtrack.app.lastfm) { + room.setListeningActive( room.track , new Function() ); + } + + if (room.track.duration > 30) { + var FOUR_MINUTES = 4 * 60; + var scrobbleTime = (room.track.duration > FOUR_MINUTES) ? FOUR_MINUTES : room.track.duration / 2; + + room.permaTimer = setTimeout(function() { + + async.parallel([ + insertIntoPlayHistory, + scrobbleIfEnabled + ], function(err, results) { + if (err) console.log(err); + console.log('play history updated and lastfm scrobbled!'); + }); + + function insertIntoPlayHistory( done ) { + var play = new Play({ + _track: room.track._id, + _curator: (room.track.curator) ? room.track.curator._id : undefined, + _room: room._id, + timestamp: now + }); + play.save( done ); + } + + function scrobbleIfEnabled( done ) { + if (!room.soundtrack.app.lastfm) return done(); + room.scrobbleActive( room.track , done ); + } + + }, scrobbleTime * 1000 ); + } + + return cb(); + + }); + }); +}; + +RoomSchema.methods.scrobbleActive = function(requestedTrack, cb) { + var room = this; + var app = room.soundtrack.app; + + console.log('scrobbling to active listeners...'); + + Track.findOne({ _id: requestedTrack._id }).populate('_artist').exec(function(err, track) { + if (!track || track._artist.name && track._artist.name.toLowerCase() == 'gobbly') { return false; } + + Person.find({ _id: { $in: _.toArray( room.listeners ).map(function(x) { return x._id; }) } }).exec(function(err, people) { + _.filter( people , function(x) { + return (x.profiles && x.profiles.lastfm && x.profiles.lastfm.username && x.preferences.scrobble); + } ).forEach(function(user) { + console.log('listener available:' + user._id + ' ' + user.username ); + + var lastfm = new app.LastFM({ + api_key: app.config.lastfm.key + , secret: app.config.lastfm.secret + }); + + var creds = { + username: user.profiles.lastfm.username + , key: user.profiles.lastfm.key + }; + + lastfm.setSessionCredentials( creds.username , creds.key ); + lastfm.track.scrobble({ + artist: track._artist.name + , track: track.title + , duration: Math.floor(track.duration) + , timestamp: Math.floor((new Date()).getTime() / 1000) + }, function(err, scrobbles) { + if (err) { return console.log('le fail...', err); } + cb(); + }); + }); + }); + }); +} + +RoomSchema.methods.setListeningActive = function(requestedTrack, cb) { + var room = this; + var app = room.soundtrack.app; + + console.log('setting "listening to" for active listeners...'); + + Track.findOne({ _id: requestedTrack._id }).populate('_artist').exec(function(err, track) { + if (!track || track._artist.name && track._artist.name.toLowerCase() == 'gobbly') { return false; } + + Person.find({ _id: { $in: _.toArray( room.listeners ).map(function(x) { return x._id; }) } }).exec(function(err, people) { + _.filter( people , function(x) { + return (x.profiles && x.profiles.lastfm && x.profiles.lastfm.username && x.preferences.scrobble); + } ).forEach(function(user) { + console.log('listener available:' + user._id + ' ' + user.username ); + + var lastfm = new app.LastFM({ + api_key: app.config.lastfm.key + , secret: app.config.lastfm.secret + }); + + var creds = { + username: user.profiles.lastfm.username + , key: user.profiles.lastfm.key + }; + + lastfm.setSessionCredentials( creds.username , creds.key ); + lastfm.track.updateNowPlaying({ + artist: track._artist.name + , track: track.title + , duration: Math.floor(track.duration) + }, function(err, scrobbles) { + if (err) return console.log('le fail...', err); + cb(); + }); + }); + }); + }); +} + +var Room = mongoose.model('Room', RoomSchema); + +// export the model to anything requiring it. +module.exports = { + Room: Room +}; diff --git a/models/Source.js b/models/Source.js new file mode 100644 index 00000000..e7a4b47c --- /dev/null +++ b/models/Source.js @@ -0,0 +1,46 @@ +var mongoose = require('mongoose') + , Schema = mongoose.Schema + , ObjectId = mongoose.SchemaTypes.ObjectId; + +var config = require('../config'); + +// this defines the fields associated with the model, +// and moreover, their type. +var SourceSchema = new Schema({ + id: { type: String, required: true, unique: true, id: 1 } + , type: { type: String, enum: ['audio/mp3', 'video/youtube', 'video/mp4'] } + , start: { type: Number, default: 0 } + , end: { type: Number } + , flags: { + live: { type: Boolean, default: false } // ~bad audio + , nsfw: { type: Boolean, default: false } // ~bad video + , down: { type: Boolean, default: false } // offline + , restricted: { type: Boolean, default: false } // non-free (libre) + } + , stats: { + created: { type: Date, default: Date.now } + , updated: { type: Date } + } +}); + +SourceSchema.virtual('uri').get(function() { + var parts = id.split(':'); + switch (source) { + case 'youtube': + return 'https://www.youtube.com/watch?v=' + parts[1]; + break; + case 'soundcloud': + return 'https://api.soundcloud.com/tracks/' + parts[1] + '?clientID=' + config.soundcloud.id; + break; + default: + return undefined; + break; + } +}); + +var Source = mongoose.model('Source', SourceSchema); + +// export the model to anything requiring it. +module.exports = { + Source: Source +}; diff --git a/models/Track.js b/models/Track.js index 7b93c334..e6243017 100644 --- a/models/Track.js +++ b/models/Track.js @@ -2,29 +2,52 @@ var mongoose = require('mongoose') , Schema = mongoose.Schema , ObjectId = mongoose.SchemaTypes.ObjectId , slug = require('mongoose-slug') - , slugify = require('mongoose-slug'); + , slugify = require('mongoose-slug') + , rest = require('restler') + , async = require('async') + , _ = require('underscore'); // this defines the fields associated with the model, // and moreover, their type. var TrackSchema = new Schema({ title: { type: String, required: true } - , _artist: { type: ObjectId, ref: 'Artist' } - , _credits: [ { type: ObjectId, ref: 'Artist' } ] - , duration: { type: Number } + , titles: [ { type: String } ] + , _artist: { type: ObjectId, ref: 'Artist', index: true } + , _credits: [ { type: ObjectId, ref: 'Artist', index: true } ] + , duration: { type: Number } // time in seconds + , flags: { + nsfw: { type: Boolean, default: false } + , live: { type: Boolean, default: false } + } + , description: { type: String } , images: { thumbnail: { url: { type: String } } } + , updated: { type: Date, default: 0 } + , _sources: [ { type: ObjectId, ref: 'Source' } ] , sources: { + lastfm: [ new Schema({ + id: { type: String , required: true , index: true } + , duration: { type: Number } + , data: {} + }) ], youtube: [ new Schema({ - id: { type: String, required: true } + id: { type: String, required: true , index: true } , start: { type: Number, default: 0 } , duration: { type: Number } + , data: {} })], soundcloud: [ new Schema({ - id: { type: String, required: true } + id: { type: String, required: true , index: true } + , data: {} + })], + bandcamp: [ new Schema({ + id: { type: String, required: true } + , data: {} })], vimeo: [ new Schema({ - id: { type: String, required: true } + id: { type: String, required: true } + , data: {} })] } , _remixes: [ new Schema({ @@ -33,6 +56,57 @@ var TrackSchema = new Schema({ }) ] }); +TrackSchema.pre('save', function(next) { + var self = this; + + /** for (var source in self.sources) { + self._sources.push({ }); + } **/ + + // de-dupe sources + var tempTrack = self.toObject(); + for (var source in tempTrack.sources) { + var sourceMap = {}; + + var sourcesForProvider = tempTrack.sources[ source ] || []; + sourcesForProvider.forEach(function( s ) { + sourceMap[ s.id ] = s; + }); + + self.sources[ source ] = Object.keys( sourceMap ).map(function( k ) { + return sourceMap[ k ]; + }); + } + + ['youtube', 'soundcloud'].forEach(function(source) { + + switch (source) { + case 'youtube': var type = 'video/youtube'; break; + case 'soundcloud': var type = 'audio/mp3'; break; + } + + self.sources[ source ].forEach(function(s) { + Source.findOne({ uri: s.id }).exec(function(err, storedSource) { + if (err) { console.log(err); } + if (!storedSource && type) { + var storedSource = new Source({ + id: [ source , s.id ].join(':') + , type: type + }); + storedSource.save(function(err) { + if (err) { console.log(err); } + }); + } + }); + }); + }); + + // de-dupe credits + self._credits = _.uniq( self._credits ); + + next(); +}); + TrackSchema.post('init', function() { var self = this; @@ -70,7 +144,9 @@ TrackSchema.statics.random = function(callback) { }.bind(this)); }; -TrackSchema.plugin( slug('title') ); +TrackSchema.plugin( slug('title', { + track: true +}) ); TrackSchema.index({ slug: 1 }); var Track = mongoose.model('Track', TrackSchema); diff --git a/monitor.js b/monitor.js new file mode 100644 index 00000000..d73c097e --- /dev/null +++ b/monitor.js @@ -0,0 +1,7 @@ +var config = require('./config'); +var db = require('./db'); + +var Queue = require('./lib/Queue'); +var jobs = new Queue( config ); + +jobs._kue.app.listen(3000); \ No newline at end of file diff --git a/package.json b/package.json index ddd42b65..66177569 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,45 @@ { "name": "soundtrack.io", "version": "0.0.0", - "description": "ERROR: No README.md file found!", + "description": "soundtrack.io is a collaborative online jukebox.", "main": "soundtrack.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "mocha", + "coverage": "NODE_ENV=test istanbul cover _mocha -- --recursive", + "start": "node ./soundtrack.js" }, - "repository": "", - "author": "", - "license": "BSD", + "repository": "git@github.com:martindale/soundtrack.io.git", + "author": "Eric Martindale", + "license": "MIT", "dependencies": { - "passport": "~0.1.17", - "passport-local-mongoose": "~0.2.4", - "express": "~3.3.3", - "jade": "~0.32.0", - "mongoose": "~3.6.14", "async": "~0.2.9", - "underscore": "~1.4.4", - "mongoose-redis-cache": "0.0.3", - "connect-redis": "~1.4.5", - "mongoose-slug": "~1.1.1", - "sockjs": "~0.3.7", - "restler": "git+http://github.com/danwrong/restler.git", + "body-parser": "^1.11.0", + "connect-redis": "^2.2.0", + "express": "^4.11.2", + "express-session": "^1.10.2", "flashify": "~0.1.2", - "pkgcloud": "~0.8.2", - "passport-local": "~0.1.6", + "heapdump": "^0.3.5", + "jade": "~0.32.0", + "lastfmapi": "0.0.4", "marked": "~0.2.9", "moment": "~2.0.0", - "redis": "~0.8.4", - "slug-component": "~1.1.0", - "validator": "~1.3.0", - "connect-cachify": "0.0.15", - "lastfmapi": "0.0.4" + "mongoose": "~3.6.14", + "mongoose-agency": "0.0.0", + "mongoose-slug": "~1.3.0", + "passport": "~0.1.17", + "passport-local": "~0.1.6", + "passport-local-mongoose": "~0.2.4", + "passport-spotify": "^0.1.0", + "redis": "^0.12.1", + "restler": "git+http://github.com/danwrong/restler.git", + "sockjs": "^0.3.12", + "speakingurl": "~0.9.0", + "underscore": "~1.4.4", + "validator": "~1.3.0" + }, + "devDependencies": { + "coveralls": "^2.11.2", + "istanbul": "^0.3.5", + "mocha": "^2.1.0" } } diff --git a/public/css/bootstrap-slider.min.css b/public/css/bootstrap-slider.min.css new file mode 100644 index 00000000..02b4cc26 --- /dev/null +++ b/public/css/bootstrap-slider.min.css @@ -0,0 +1,6 @@ +/*! + * Slider for Bootstrap + * + * Licensed under the Apache License v2.0 + * + */.slider{display:inline-block;vertical-align:middle;position:relative}.slider.slider-horizontal{width:210px;height:20px}.slider.slider-horizontal .slider-track{height:10px;width:100%;margin-top:-5px;top:50%;left:0}.slider.slider-horizontal .slider-selection{height:100%;top:0;bottom:0}.slider.slider-horizontal .slider-handle{margin-left:-10px;margin-top:-5px}.slider.slider-horizontal .slider-handle.triangle{border-width:0 10px 10px 10px;width:0;height:0;border-bottom-color:#0480be;margin-top:0}.slider.slider-vertical{height:210px;width:20px}.slider.slider-vertical .slider-track{width:10px;height:100%;margin-left:-5px;left:50%;top:0}.slider.slider-vertical .slider-selection{width:100%;left:0;top:0;bottom:0}.slider.slider-vertical .slider-handle{margin-left:-5px;margin-top:-10px}.slider.slider-vertical .slider-handle.triangle{border-width:10px 0 10px 10px;width:1px;height:1px;border-left-color:#0480be;margin-left:0}.slider.slider-disabled .slider-handle{background-image:-webkit-linear-gradient(top,#dfdfdf 0,#bebebe 100%);background-image:linear-gradient(to bottom,#dfdfdf 0,#bebebe 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdfdfdf',endColorstr='#ffbebebe',GradientType=0)}.slider.slider-disabled .slider-track{background-image:-webkit-linear-gradient(top,#e5e5e5 0,#e9e9e9 100%);background-image:linear-gradient(to bottom,#e5e5e5 0,#e9e9e9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe5e5e5',endColorstr='#ffe9e9e9',GradientType=0);cursor:not-allowed}.slider input{display:none}.slider .tooltip-inner{white-space:nowrap}.slider-track{position:absolute;cursor:pointer;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#f9f9f9 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#f9f9f9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);border-radius:4px}.slider-selection{position:absolute;background-image:-webkit-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#f9f9f9 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:4px}.slider-handle{position:absolute;width:20px;height:20px;background-color:#3a94a5;background-image:-webkit-linear-gradient(top,#149bdf 0,#0480be 100%);background-image:linear-gradient(to bottom,#149bdf 0,#0480be 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);filter:none;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);opacity:.8;border:0 solid transparent}.slider-handle.round{border-radius:50%}.slider-handle.triangle{background:transparent none} \ No newline at end of file diff --git a/public/css/font-awesome.min.css b/public/css/font-awesome.min.css new file mode 100644 index 00000000..449d6ac5 --- /dev/null +++ b/public/css/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.0.3');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.0.3') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.0.3') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.2857142857142858em;text-align:center}.fa-ul{padding-left:0;margin-left:2.142857142857143em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.142857142857143em;width:2.142857142857143em;top:.14285714285714285em;text-align:center}.fa-li.fa-lg{left:-1.8571428571428572em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0,mirror=1);-webkit-transform:scale(-1,1);-moz-transform:scale(-1,1);-ms-transform:scale(-1,1);-o-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2,mirror=1);-webkit-transform:scale(1,-1);-moz-transform:scale(1,-1);-ms-transform:scale(1,-1);-o-transform:scale(1,-1);transform:scale(1,-1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-asc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-desc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-reply-all:before{content:"\f122"}.fa-mail-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"} \ No newline at end of file diff --git a/public/css/main.css b/public/css/main.css index 2b149109..107c4a51 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -107,6 +107,9 @@ footer { border-bottom-left-radius: 3px; margin: -5px; } +.navbar .brand { + padding: 10px 0px 10px; +} .navbar a.brand { padding-left: 12px; margin-left: 0; @@ -114,7 +117,7 @@ footer { } .navbar a.brand img { max-height: 1em; - padding-right: 0.5em; + /* padding-right: 0.5em; */ margin-bottom: -0.2em; vertical-align: top; } @@ -226,13 +229,19 @@ footer { #userlist { height: 508px; } -#user-playlists-dropdown { +#user-playlists-dropdown, .user-playlists-dropdown { max-height: 300px; overflow-y: scroll; } -#user-playlists-dropdown a { +#user-playlists-dropdown a, .user-playlists-dropdown a { cursor: pointer; } +.dropdown-menu>li>a { + color: lightgrey; +} +.dropdown-menu.user-playlists-dropdown>li>a:hover, .dropdown-menu.user-playlists-dropdown>li>a:focus, .dropdown-submenu.user-playlists-dropdown:hover>a, .dropdown-submenu.user-playlists-dropdown:focus>a,.dropdown-submenu.user-playlists-dropdown>a { + color: white; +} #announcement{ color: #08C; @@ -289,8 +298,7 @@ footer { .playlist-controls { float:left; - padding-right: 5px; - padding-left: 5px; + margin-right: 5px; } .active .playlist-controls { visibility: hidden; @@ -314,6 +322,7 @@ footer { text-transform: uppercase; font-weight: bold; font-size: 0.8em; + padding-left: 0.2em; } #playlist-list li.active { display: none; @@ -336,55 +345,77 @@ footer { box-shadow: inset 0 20px 20px -20px #000000; /* background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 1)), to(rgba(0, 0, 0, 0))); */ } + /* ######################################### */ /* #### Responsive Tweaks by jDizzy on 140311 #### */ /* ######################################### */ - @media (max-width:1199px) and (min-width:980px){ - #chat-form .input-block-level { - width: 536px; - } +/* begin le Dizzcode */ +@media (max-width:1199px) and (min-width:980px){ + #chat-form .input-block-level { + width: 536px; + } + } + +@media (max-width: 979px) { + body {padding:0;} + .container { + width: 100%; + margin-left:0; + margin-right:0; + } + + footer {margin:0;} + } @media (max-width:979px) and (min-width:768px){ - #chat-form .input-block-level { - width: 85%; - } + #chat-form .input-block-level { + width: 85%; + } } @media (max-width: 979px) and (min-width: 945px) { - .row-fluid [class*="span"].padded-left { - padding-left:0; - width:290px; - } + .row-fluid [class*="span"].padded-left { + padding-left:0; + width:290px; + } } @media (max-width: 944px) { - .row-fluid [class*="span"].padded-left { - width:100%; - padding-left:12px; - padding-right:12px; + .row-fluid [class*="span"].padded-left { + width:100%; + padding-left:12px; + padding-right:12px; } - #playlist { - height:auto; - max-height:200px; - } - - .row-fluid [class*="span"].unpadded { - width:100%; - padding:0 12px; - margin:0; - } - - #search-form .input-block-level { - width: 90%; - } + #playlist { + height:auto; + max-height:200px; + } + + .row-fluid [class*="span"].unpadded { + width:100%; + padding:0 12px; + margin:0; + } + + #search-form .input-block-level { + width: 90%; + } } @media (max-width:767px){ - .navbar-fixed-top, .navbar-fixed-bottom, .navbar-static-top { - margin-right: 0; - margin-left: 0; - } + .navbar-fixed-top, .navbar-fixed-bottom, .navbar-static-top { + margin-right: 0; + margin-left: 0; + } } +/* end le Dizzcode */ + +/* videoJS stuff */ +.vjs-fade-in,.vjs-fade-out { + visibility: visible !important; + opacity: 1 !important; + transition-duration: 0s!important; +} diff --git a/public/css/slider.css b/public/css/slider.css deleted file mode 100644 index a7f8370b..00000000 --- a/public/css/slider.css +++ /dev/null @@ -1,138 +0,0 @@ -/*! - * Slider for Bootstrap - * - * Copyright 2012 Stefan Petre - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - */ -.slider { - display: inline-block; - vertical-align: middle; - position: relative; -} -.slider.slider-horizontal { - width: 100%; - height: 20px; -} -.slider.slider-horizontal .slider-track { - height: 10px; - width: 100%; - margin-top: -5px; - top: 50%; - left: 0; -} -.slider.slider-horizontal .slider-selection { - height: 100%; - top: 0; - bottom: 0; -} -.slider.slider-horizontal .slider-handle { - margin-left: -10px; - margin-top: -5px; -} -.slider.slider-horizontal .slider-handle.triangle { - border-width: 0 10px 10px 10px; - width: 0; - height: 0; - border-bottom-color: #0480be; - margin-top: 0; -} -.slider.slider-vertical { - height: 210px; - width: 20px; -} -.slider.slider-vertical .slider-track { - width: 10px; - height: 100%; - margin-left: -5px; - left: 50%; - top: 0; -} -.slider.slider-vertical .slider-selection { - width: 100%; - left: 0; - top: 0; - bottom: 0; -} -.slider.slider-vertical .slider-handle { - margin-left: -5px; - margin-top: -10px; -} -.slider.slider-vertical .slider-handle.triangle { - border-width: 10px 0 10px 10px; - width: 1px; - height: 1px; - border-left-color: #0480be; - margin-left: 0; -} -.slider input { - display: none; -} -.slider .tooltip-inner { - white-space: nowrap; -} -.slider-track { - position: absolute; - cursor: pointer; - background-color: #f7f7f7; - background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); - background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0); - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} -.slider-selection { - position: absolute; - background-color: #f7f7f7; - background-image: -moz-linear-gradient(top, #f9f9f9, #f5f5f5); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f9f9f9), to(#f5f5f5)); - background-image: -webkit-linear-gradient(top, #f9f9f9, #f5f5f5); - background-image: -o-linear-gradient(top, #f9f9f9, #f5f5f5); - background-image: linear-gradient(to bottom, #f9f9f9, #f5f5f5); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0); - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} -.slider-handle { - position: absolute; - width: 20px; - height: 20px; - background-color: #0e90d2; - background-image: -moz-linear-gradient(top, #149bdf, #0480be); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); - background-image: -webkit-linear-gradient(top, #149bdf, #0480be); - background-image: -o-linear-gradient(top, #149bdf, #0480be); - background-image: linear-gradient(to bottom, #149bdf, #0480be); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0); - -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); - -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); - box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); - opacity: 0.8; - border: 0px solid transparent; -} -.slider-handle.round { - -webkit-border-radius: 20px; - -moz-border-radius: 20px; - border-radius: 20px; -} -.slider-handle.triangle { - background: transparent none; -} \ No newline at end of file diff --git a/public/css/typeahead.js-bootstrap.css b/public/css/typeahead.js-bootstrap.css new file mode 100644 index 00000000..e44b6739 --- /dev/null +++ b/public/css/typeahead.js-bootstrap.css @@ -0,0 +1,49 @@ +.twitter-typeahead .tt-query, +.twitter-typeahead .tt-hint { + margin-bottom: 0; +} + +.tt-dropdown-menu { + min-width: 160px; + margin-top: 2px; + padding: 5px 0; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0,0,0,.2); + *border-right-width: 2px; + *border-bottom-width: 2px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2); + -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2); + box-shadow: 0 5px 10px rgba(0,0,0,.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.tt-suggestion { + display: block; + padding: 3px 20px; +} + +.tt-suggestion.tt-is-under-cursor { + color: #fff; + background-color: #0081c2; + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0) +} + +.tt-suggestion.tt-is-under-cursor a { + color: #fff; +} + +.tt-suggestion p { + margin: 0; +} diff --git a/public/fonts/FontAwesome.otf b/public/fonts/FontAwesome.otf new file mode 100644 index 00000000..8b0f54e4 Binary files /dev/null and b/public/fonts/FontAwesome.otf differ diff --git a/public/fonts/fontawesome-webfont.eot b/public/fonts/fontawesome-webfont.eot new file mode 100755 index 00000000..7c79c6a6 Binary files /dev/null and b/public/fonts/fontawesome-webfont.eot differ diff --git a/public/fonts/fontawesome-webfont.svg b/public/fonts/fontawesome-webfont.svg new file mode 100755 index 00000000..45fdf338 --- /dev/null +++ b/public/fonts/fontawesome-webfont.svg @@ -0,0 +1,414 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/fonts/fontawesome-webfont.ttf b/public/fonts/fontawesome-webfont.ttf new file mode 100755 index 00000000..e89738de Binary files /dev/null and b/public/fonts/fontawesome-webfont.ttf differ diff --git a/public/fonts/fontawesome-webfont.woff b/public/fonts/fontawesome-webfont.woff new file mode 100755 index 00000000..8c1748aa Binary files /dev/null and b/public/fonts/fontawesome-webfont.woff differ diff --git a/public/img/last.fm.ico b/public/img/last.fm.ico new file mode 100644 index 00000000..80cf74ce Binary files /dev/null and b/public/img/last.fm.ico differ diff --git a/public/img/spotify.png b/public/img/spotify.png new file mode 100644 index 00000000..2c9b6519 Binary files /dev/null and b/public/img/spotify.png differ diff --git a/public/js/angular.min.js b/public/js/angular.min.js index 2b220688..72a0970f 100644 --- a/public/js/angular.min.js +++ b/public/js/angular.min.js @@ -1,163 +1,219 @@ /* - AngularJS v1.0.7 - (c) 2010-2012 Google, Inc. http://angularjs.org + AngularJS v1.3.0-beta.7 + (c) 2010-2014 Google, Inc. http://angularjs.org License: MIT */ -(function(P,T,q){'use strict';function m(b,a,c){var d;if(b)if(H(b))for(d in b)d!="prototype"&&d!="length"&&d!="name"&&b.hasOwnProperty(d)&&a.call(c,b[d],d);else if(b.forEach&&b.forEach!==m)b.forEach(a,c);else if(!b||typeof b.length!=="number"?0:typeof b.hasOwnProperty!="function"&&typeof b.constructor!="function"||b instanceof K||ca&&b instanceof ca||wa.call(b)!=="[object Object]"||typeof b.callee==="function")for(d=0;d=0&&b.splice(c,1);return a}function U(b,a){if(oa(b)||b&&b.$evalAsync&&b.$watch)throw Error("Can't copy Window or Scope");if(a){if(b===a)throw Error("Can't copy equivalent objects or arrays");if(E(b))for(var c=a.length=0;c2?ha.call(arguments,2):[];return H(a)&&!(a instanceof RegExp)?c.length?function(){return arguments.length?a.apply(b,c.concat(ha.call(arguments,0))):a.apply(b,c)}:function(){return arguments.length?a.apply(b,arguments):a.call(b)}:a}function ic(b,a){var c=a;/^\$+/.test(b)?c=q:oa(a)?c="$WINDOW":a&&T===a?c="$DOCUMENT":a&&a.$evalAsync&&a.$watch&&(c="$SCOPE");return c}function da(b,a){return JSON.stringify(b, -ic,a?" ":null)}function pb(b){return B(b)?JSON.parse(b):b}function Ua(b){b&&b.length!==0?(b=z(""+b),b=!(b=="f"||b=="0"||b=="false"||b=="no"||b=="n"||b=="[]")):b=!1;return b}function pa(b){b=u(b).clone();try{b.html("")}catch(a){}var c=u("
").append(b).html();try{return b[0].nodeType===3?z(c):c.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+z(b)})}catch(d){return z(c)}}function Va(b){var a={},c,d;m((b||"").split("&"),function(b){b&&(c=b.split("="),d=decodeURIComponent(c[0]), -a[d]=y(c[1])?decodeURIComponent(c[1]):!0)});return a}function qb(b){var a=[];m(b,function(b,d){a.push(Wa(d,!0)+(b===!0?"":"="+Wa(b,!0)))});return a.length?a.join("&"):""}function Xa(b){return Wa(b,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function Wa(b,a){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,a?"%20":"+")}function jc(b,a){function c(a){a&&d.push(a)}var d=[b],e,g,h=["ng:app","ng-app","x-ng-app", -"data-ng-app"],f=/\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;m(h,function(a){h[a]=!0;c(T.getElementById(a));a=a.replace(":","\\:");b.querySelectorAll&&(m(b.querySelectorAll("."+a),c),m(b.querySelectorAll("."+a+"\\:"),c),m(b.querySelectorAll("["+a+"]"),c))});m(d,function(a){if(!e){var b=f.exec(" "+a.className+" ");b?(e=a,g=(b[2]||"").replace(/\s+/g,",")):m(a.attributes,function(b){if(!e&&h[b.name])e=a,g=b.value})}});e&&a(e,g?[g]:[])}function rb(b,a){var c=function(){b=u(b);a=a||[];a.unshift(["$provide",function(a){a.value("$rootElement", -b)}]);a.unshift("ng");var c=sb(a);c.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return c},d=/^NG_DEFER_BOOTSTRAP!/;if(P&&!d.test(P.name))return c();P.name=P.name.replace(d,"");Ya.resumeBootstrap=function(b){m(b,function(b){a.push(b)});c()}}function Za(b,a){a=a||"_";return b.replace(kc,function(b,d){return(d?a:"")+b.toLowerCase()})}function $a(b,a,c){if(!b)throw Error("Argument '"+(a||"?")+"' is "+(c||"required")); -return b}function qa(b,a,c){c&&E(b)&&(b=b[b.length-1]);$a(H(b),a,"not a function, got "+(b&&typeof b=="object"?b.constructor.name||"Object":typeof b));return b}function lc(b){function a(a,b,e){return a[b]||(a[b]=e())}return a(a(b,"angular",Object),"module",function(){var b={};return function(d,e,g){e&&b.hasOwnProperty(d)&&(b[d]=null);return a(b,d,function(){function a(c,d,e){return function(){b[e||"push"]([c,d,arguments]);return k}}if(!e)throw Error("No module: "+d);var b=[],c=[],j=a("$injector", -"invoke"),k={_invokeQueue:b,_runBlocks:c,requires:e,name:d,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:j,run:function(a){c.push(a);return this}};g&&j(g);return k})}})}function tb(b){return b.replace(mc,function(a,b,d,e){return e?d.toUpperCase(): -d}).replace(nc,"Moz$1")}function ab(b,a){function c(){var e;for(var b=[this],c=a,h,f,i,j,k,l;b.length;){h=b.shift();f=0;for(i=h.length;f 
"+b;a.removeChild(a.firstChild);bb(this,a.childNodes);this.remove()}else bb(this,b)}function cb(b){return b.cloneNode(!0)}function ra(b){ub(b);for(var a=0,b=b.childNodes||[];a-1}function xb(b,a){a&&m(a.split(" "),function(a){b.className=Q((" "+b.className+" ").replace(/[\n\t]/g," ").replace(" "+Q(a)+" "," "))})} -function yb(b,a){a&&m(a.split(" "),function(a){if(!Ca(b,a))b.className=Q(b.className+" "+Q(a))})}function bb(b,a){if(a)for(var a=!a.nodeName&&y(a.length)&&!oa(a)?a:[a],c=0;c4096&&c.warn("Cookie '"+a+"' possibly not set or overflowed because it was too large ("+d+" > 4096 bytes)!")}else{if(i.cookie!==$){$=i.cookie;d=$.split("; ");r={};for(f=0;f0&&(a=unescape(e.substring(0,j)),r[a]===q&&(r[a]=unescape(e.substring(j+1))))}return r}};f.defer=function(a,b){var c; -p++;c=l(function(){delete o[c];e(a)},b||0);o[c]=!0;return c};f.defer.cancel=function(a){return o[a]?(delete o[a],n(a),e(C),!0):!1}}function wc(){this.$get=["$window","$log","$sniffer","$document",function(b,a,c,d){return new vc(b,d,a,c)}]}function xc(){this.$get=function(){function b(b,d){function e(a){if(a!=l){if(n){if(n==a)n=a.n}else n=a;g(a.n,a.p);g(a,l);l=a;l.n=null}}function g(a,b){if(a!=b){if(a)a.p=b;if(b)b.n=a}}if(b in a)throw Error("cacheId "+b+" taken");var h=0,f=v({},d,{id:b}),i={},j=d&& -d.capacity||Number.MAX_VALUE,k={},l=null,n=null;return a[b]={put:function(a,b){var c=k[a]||(k[a]={key:a});e(c);w(b)||(a in i||h++,i[a]=b,h>j&&this.remove(n.key))},get:function(a){var b=k[a];if(b)return e(b),i[a]},remove:function(a){var b=k[a];if(b){if(b==l)l=b.p;if(b==n)n=b.n;g(b.n,b.p);delete k[a];delete i[a];h--}},removeAll:function(){i={};h=0;k={};l=n=null},destroy:function(){k=f=i=null;delete a[b]},info:function(){return v({},f,{size:h})}}}var a={};b.info=function(){var b={};m(a,function(a,e){b[e]= -a.info()});return b};b.get=function(b){return a[b]};return b}}function yc(){this.$get=["$cacheFactory",function(b){return b("templates")}]}function Db(b){var a={},c="Directive",d=/^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/,e=/(([\d\w\-_]+)(?:\:([^;]+))?;?)/,g="Template must have exactly one root element. was: ",h=/^\s*(https?|ftp|mailto|file):/;this.directive=function i(d,e){B(d)?($a(e,"directive"),a.hasOwnProperty(d)||(a[d]=[],b.factory(d+c,["$injector","$exceptionHandler",function(b,c){var e=[];m(a[d], -function(a){try{var g=b.invoke(a);if(H(g))g={compile:I(g)};else if(!g.compile&&g.link)g.compile=I(g.link);g.priority=g.priority||0;g.name=g.name||d;g.require=g.require||g.controller&&g.name;g.restrict=g.restrict||"A";e.push(g)}catch(h){c(h)}});return e}])),a[d].push(e)):m(d,nb(i));return this};this.urlSanitizationWhitelist=function(a){return y(a)?(h=a,this):h};this.$get=["$injector","$interpolate","$exceptionHandler","$http","$templateCache","$parse","$controller","$rootScope","$document",function(b, -j,k,l,n,o,p,s,t){function x(a,b,c){a instanceof u||(a=u(a));m(a,function(b,c){b.nodeType==3&&b.nodeValue.match(/\S+/)&&(a[c]=u(b).wrap("").parent()[0])});var d=A(a,b,a,c);return function(b,c){$a(b,"scope");for(var e=c?ua.clone.call(a):a,j=0,g=e.length;jr.priority)break;if(Y=r.scope)ta("isolated scope",J,r,D),L(Y)&&(M(D,"ng-isolate-scope"),J=r),M(D,"ng-scope"),s=s||r;F=r.name;if(Y=r.controller)y=y||{},ta("'"+F+"' controller",y[F],r,D),y[F]=r;if(Y=r.transclude)ta("transclusion",ja,r,D),ja=r,l=r.priority,Y=="element"?(W=u(b),D=c.$$element=u(T.createComment(" "+ -F+": "+c[F]+" ")),b=D[0],C(e,u(W[0]),b),V=x(W,d,l)):(W=u(cb(b)).contents(),D.html(""),V=x(W,d));if(Y=r.template)if(ta("template",A,r,D),A=r,Y=Fb(Y),r.replace){W=u("
"+Q(Y)+"
").contents();b=W[0];if(W.length!=1||b.nodeType!==1)throw Error(g+Y);C(e,D,b);F={$attr:{}};a=a.concat(N(b,a.splice(v+1,a.length-(v+1)),F));$(c,F);z=a.length}else D.html(Y);if(r.templateUrl)ta("template",A,r,D),A=r,i=R(a.splice(v,a.length-v),i,D,c,e,r.replace,V),z=a.length;else if(r.compile)try{w=r.compile(D,c,V),H(w)? -j(null,w):w&&j(w.pre,w.post)}catch(G){k(G,pa(D))}if(r.terminal)i.terminal=!0,l=Math.max(l,r.priority)}i.scope=s&&s.scope;i.transclude=ja&&V;return i}function r(d,e,g,j){var h=!1;if(a.hasOwnProperty(e))for(var o,e=b.get(e+c),l=0,p=e.length;lo.priority)&&o.restrict.indexOf(g)!=-1)d.push(o),h=!0}catch(n){k(n)}return h}function $(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;m(a,function(d,e){e.charAt(0)!="$"&&(b[e]&&(d+=(e==="style"?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});m(b, -function(b,g){g=="class"?(M(e,b),a["class"]=(a["class"]?a["class"]+" ":"")+b):g=="style"?e.attr("style",e.attr("style")+";"+b):g.charAt(0)!="$"&&!a.hasOwnProperty(g)&&(a[g]=b,d[g]=c[g])})}function R(a,b,c,d,e,j,h){var i=[],k,o,p=c[0],t=a.shift(),s=v({},t,{controller:null,templateUrl:null,transclude:null,scope:null});c.html("");l.get(t.templateUrl,{cache:n}).success(function(l){var n,t,l=Fb(l);if(j){t=u("
"+Q(l)+"
").contents();n=t[0];if(t.length!=1||n.nodeType!==1)throw Error(g+l);l={$attr:{}}; -C(e,c,n);N(n,a,l);$(d,l)}else n=p,c.html(l);a.unshift(s);k=J(a,n,d,h);for(o=A(c[0].childNodes,h);i.length;){var r=i.pop(),l=i.pop();t=i.pop();var ia=i.pop(),D=n;t!==p&&(D=cb(n),C(l,u(t),D));k(function(){b(o,ia,D,e,r)},ia,D,e,r)}i=null}).error(function(a,b,c,d){throw Error("Failed to load template: "+d.url);});return function(a,c,d,e,g){i?(i.push(c),i.push(d),i.push(e),i.push(g)):k(function(){b(o,c,d,e,g)},c,d,e,g)}}function F(a,b){return b.priority-a.priority}function ta(a,b,c,d){if(b)throw Error("Multiple directives ["+ -b.name+", "+c.name+"] asking for "+a+" on: "+pa(d));}function y(a,b){var c=j(b,!0);c&&a.push({priority:0,compile:I(function(a,b){var d=b.parent(),e=d.data("$binding")||[];e.push(c);M(d.data("$binding",e),"ng-binding");a.$watch(c,function(a){b[0].nodeValue=a})})})}function V(a,b,c,d){var e=j(c,!0);e&&b.push({priority:100,compile:I(function(a,b,c){b=c.$$observers||(c.$$observers={});d==="class"&&(e=j(c[d],!0));c[d]=q;(b[d]||(b[d]=[])).$$inter=!0;(c.$$observers&&c.$$observers[d].$$scope||a).$watch(e, -function(a){c.$set(d,a)})})})}function C(a,b,c){var d=b[0],e=d.parentNode,g,j;if(a){g=0;for(j=a.length;g -0){var e=R[0],f=e.text;if(f==a||f==b||f==c||f==d||!a&&!b&&!c&&!d)return e}return!1}function f(b,c,d,f){return(b=h(b,c,d,f))?(a&&!b.json&&e("is not valid json",b),R.shift(),b):!1}function i(a){f(a)||e("is unexpected, expecting ["+a+"]",h())}function j(a,b){return function(c,d){return a(c,d,b)}}function k(a,b,c){return function(d,e){return b(d,e,a,c)}}function l(){for(var a=[];;)if(R.length>0&&!h("}",")",";","]")&&a.push(w()),!f(";"))return a.length==1?a[0]:function(b,c){for(var d,e=0;e","<=",">="))a=k(a,b.fn,t());return a}function x(){for(var a=m(),b;b=f("*","/","%");)a=k(a,b.fn,m());return a}function m(){var a;return f("+")?A():(a=f("-"))?k(r,a.fn,m()):(a=f("!"))?j(a.fn,m()):A()}function A(){var a;if(f("("))a=w(),i(")");else if(f("["))a=N();else if(f("{"))a=J();else{var b=f();(a=b.fn)||e("not a primary expression",b)}for(var c;b=f("(","[",".");)b.text==="("?(a=y(a,c),c=null):b.text==="["?(c=a,a=V(a)):b.text==="."?(c=a,a=u(a)):e("IMPOSSIBLE");return a}function N(){var a= -[];if(g().text!="]"){do a.push(F());while(f(","))}i("]");return function(b,c){for(var d=[],e=0;e1;d++){var e=a.shift(),g=b[e];g||(g={},b[e]=g);b=g}return b[a.shift()]= -c}function gb(b,a,c){if(!a)return b;for(var a=a.split("."),d,e=b,g=a.length,h=0;h7),hasEvent:function(c){if(c=="input"&&Z==9)return!1;if(w(a[c])){var e=b.document.createElement("div");a[c]="on"+c in e}return a[c]},csp:!1}}]}function Vc(){this.$get=I(P)}function Ob(b){var a={},c,d,e;if(!b)return a;m(b.split("\n"),function(b){e=b.indexOf(":");c=z(Q(b.substr(0, -e)));d=Q(b.substr(e+1));c&&(a[c]?a[c]+=", "+d:a[c]=d)});return a}function Pb(b){var a=L(b)?b:q;return function(c){a||(a=Ob(b));return c?a[z(c)]||null:a}}function Qb(b,a,c){if(H(c))return c(b,a);m(c,function(c){b=c(b,a)});return b}function Wc(){var b=/^\s*(\[|\{[^\{])/,a=/[\}\]]\s*$/,c=/^\)\]\}',?\n/,d=this.defaults={transformResponse:[function(d){B(d)&&(d=d.replace(c,""),b.test(d)&&a.test(d)&&(d=pb(d,!0)));return d}],transformRequest:[function(a){return L(a)&&wa.apply(a)!=="[object File]"?da(a):a}], -headers:{common:{Accept:"application/json, text/plain, */*","X-Requested-With":"XMLHttpRequest"},post:{"Content-Type":"application/json;charset=utf-8"},put:{"Content-Type":"application/json;charset=utf-8"}}},e=this.responseInterceptors=[];this.$get=["$httpBackend","$browser","$cacheFactory","$rootScope","$q","$injector",function(a,b,c,i,j,k){function l(a){function c(a){var b=v({},a,{data:Qb(a.data,a.headers,f)});return 200<=a.status&&a.status<300?b:j.reject(b)}a.method=la(a.method);var e=a.transformRequest|| -d.transformRequest,f=a.transformResponse||d.transformResponse,g=d.headers,g=v({"X-XSRF-TOKEN":b.cookies()["XSRF-TOKEN"]},g.common,g[z(a.method)],a.headers),e=Qb(a.data,Pb(g),e),i;w(a.data)&&delete g["Content-Type"];i=n(a,e,g);i=i.then(c,c);m(s,function(a){i=a(i)});i.success=function(b){i.then(function(c){b(c.data,c.status,c.headers,a)});return i};i.error=function(b){i.then(null,function(c){b(c.data,c.status,c.headers,a)});return i};return i}function n(b,c,d){function e(a,b,c){m&&(200<=a&&a<300?m.put(q, -[a,b,Ob(c)]):m.remove(q));f(b,a,c);i.$apply()}function f(a,c,d){c=Math.max(c,0);(200<=c&&c<300?k.resolve:k.reject)({data:a,status:c,headers:Pb(d),config:b})}function h(){var a=za(l.pendingRequests,b);a!==-1&&l.pendingRequests.splice(a,1)}var k=j.defer(),n=k.promise,m,s,q=o(b.url,b.params);l.pendingRequests.push(b);n.then(h,h);b.cache&&b.method=="GET"&&(m=L(b.cache)?b.cache:p);if(m)if(s=m.get(q))if(s.then)return s.then(h,h),s;else E(s)?f(s[1],s[0],U(s[2])):f(s,200,{});else m.put(q,n);s||a(b.method, -q,c,e,d,b.timeout,b.withCredentials);return n}function o(a,b){if(!b)return a;var c=[];fc(b,function(a,b){a==null||a==q||(L(a)&&(a=da(a)),c.push(encodeURIComponent(b)+"="+encodeURIComponent(a)))});return a+(a.indexOf("?")==-1?"?":"&")+c.join("&")}var p=c("$http"),s=[];m(e,function(a){s.push(B(a)?k.get(a):k.invoke(a))});l.pendingRequests=[];(function(a){m(arguments,function(a){l[a]=function(b,c){return l(v(c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){m(arguments,function(a){l[a]= -function(b,c,d){return l(v(d||{},{method:a,url:b,data:c}))}})})("post","put");l.defaults=d;return l}]}function Xc(){this.$get=["$browser","$window","$document",function(b,a,c){return Yc(b,Zc,b.defer,a.angular.callbacks,c[0],a.location.protocol.replace(":",""))}]}function Yc(b,a,c,d,e,g){function h(a,b){var c=e.createElement("script"),d=function(){e.body.removeChild(c);b&&b()};c.type="text/javascript";c.src=a;Z?c.onreadystatechange=function(){/loaded|complete/.test(c.readyState)&&d()}:c.onload=c.onerror= -d;e.body.appendChild(c)}return function(e,i,j,k,l,n,o){function p(a,c,d,e){c=(i.match(Hb)||["",g])[1]=="file"?d?200:404:c;a(c==1223?204:c,d,e);b.$$completeOutstandingRequest(C)}b.$$incOutstandingRequestCount();i=i||b.url();if(z(e)=="jsonp"){var s="_"+(d.counter++).toString(36);d[s]=function(a){d[s].data=a};h(i.replace("JSON_CALLBACK","angular.callbacks."+s),function(){d[s].data?p(k,200,d[s].data):p(k,-2);delete d[s]})}else{var t=new a;t.open(e,i,!0);m(l,function(a,b){a&&t.setRequestHeader(b,a)}); -var q;t.onreadystatechange=function(){if(t.readyState==4){var a=t.getAllResponseHeaders(),b=["Cache-Control","Content-Language","Content-Type","Expires","Last-Modified","Pragma"];a||(a="",m(b,function(b){var c=t.getResponseHeader(b);c&&(a+=b+": "+c+"\n")}));p(k,q||t.status,t.responseText,a)}};if(o)t.withCredentials=!0;t.send(j||"");n>0&&c(function(){q=-1;t.abort()},n)}}}function $c(){this.$get=function(){return{id:"en-us",NUMBER_FORMATS:{DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{minInt:1,minFrac:0, -maxFrac:3,posPre:"",posSuf:"",negPre:"-",negSuf:"",gSize:3,lgSize:3},{minInt:1,minFrac:2,maxFrac:2,posPre:"\u00a4",posSuf:"",negPre:"(\u00a4",negSuf:")",gSize:3,lgSize:3}],CURRENCY_SYM:"$"},DATETIME_FORMATS:{MONTH:"January,February,March,April,May,June,July,August,September,October,November,December".split(","),SHORTMONTH:"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec".split(","),DAY:"Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday".split(","),SHORTDAY:"Sun,Mon,Tue,Wed,Thu,Fri,Sat".split(","), -AMPMS:["AM","PM"],medium:"MMM d, y h:mm:ss a","short":"M/d/yy h:mm a",fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",mediumDate:"MMM d, y",shortDate:"M/d/yy",mediumTime:"h:mm:ss a",shortTime:"h:mm a"},pluralCat:function(b){return b===1?"one":"other"}}}}function ad(){this.$get=["$rootScope","$browser","$q","$exceptionHandler",function(b,a,c,d){function e(e,f,i){var j=c.defer(),k=j.promise,l=y(i)&&!i,f=a.defer(function(){try{j.resolve(e())}catch(a){j.reject(a),d(a)}l||b.$apply()},f),i=function(){delete g[k.$$timeoutId]}; -k.$$timeoutId=f;g[f]=j;k.then(i,i);return k}var g={};e.cancel=function(b){return b&&b.$$timeoutId in g?(g[b.$$timeoutId].reject("canceled"),a.defer.cancel(b.$$timeoutId)):!1};return e}]}function Rb(b){function a(a,e){return b.factory(a+c,e)}var c="Filter";this.register=a;this.$get=["$injector",function(a){return function(b){return a.get(b+c)}}];a("currency",Sb);a("date",Tb);a("filter",bd);a("json",cd);a("limitTo",dd);a("lowercase",ed);a("number",Ub);a("orderBy",Vb);a("uppercase",fd)}function bd(){return function(b, -a){if(!E(b))return b;var c=[];c.check=function(a){for(var b=0;b-1;case "object":for(var c in a)if(c.charAt(0)!=="$"&&d(a[c],b))return!0;return!1;case "array":for(c=0;ce+1?h="0":(f=h,j=!0)}if(!j){h=(h.split(Xb)[1]||"").length;w(e)&&(e=Math.min(Math.max(a.minFrac,h),a.maxFrac));var h=Math.pow(10,e),b=Math.round(b*h)/h,b=(""+b).split(Xb),h=b[0],b=b[1]||"",j=0,k=a.lgSize, -l=a.gSize;if(h.length>=k+l)for(var j=h.length-k,n=0;n0||e> --c)e+=c;e===0&&c==-12&&(e=12);return jb(e,a,d)}}function Ja(b,a){return function(c,d){var e=c["get"+b](),g=la(a?"SHORT"+b:b);return d[g][e]}}function Tb(b){function a(a){var b;if(b=a.match(c)){var a=new Date(0),g=0,h=0;b[9]&&(g=G(b[9]+b[10]),h=G(b[9]+b[11]));a.setUTCFullYear(G(b[1]),G(b[2])-1,G(b[3]));a.setUTCHours(G(b[4]||0)-g,G(b[5]||0)-h,G(b[6]||0),G(b[7]||0))}return a}var c=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c, -e){var g="",h=[],f,i,e=e||"mediumDate",e=b.DATETIME_FORMATS[e]||e;B(c)&&(c=gd.test(c)?G(c):a(c));Qa(c)&&(c=new Date(c));if(!na(c))return c;for(;e;)(i=hd.exec(e))?(h=h.concat(ha.call(i,1)),e=h.pop()):(h.push(e),e=null);m(h,function(a){f=id[a];g+=f?f(c,b.DATETIME_FORMATS):a.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function cd(){return function(b){return da(b,!0)}}function dd(){return function(b,a){if(!(b instanceof Array))return b;var a=G(a),c=[],d,e;if(!b||!(b instanceof Array))return c; -a>b.length?a=b.length:a<-b.length&&(a=-b.length);a>0?(d=0,e=a):(d=b.length+a,e=b.length);for(;dn?(d.$setValidity("maxlength",!1),q):(d.$setValidity("maxlength",!0),a)};d.$parsers.push(c);d.$formatters.push(c)}}function kb(b,a){b="ngClass"+b;return S(function(c,d,e){function g(b){if(a===!0||c.$index%2===a)i&&!fa(b,i)&&h(i),f(b);i=U(b)}function h(a){L(a)&& -!E(a)&&(a=Ra(a,function(a,b){if(a)return b}));d.removeClass(E(a)?a.join(" "):a)}function f(a){L(a)&&!E(a)&&(a=Ra(a,function(a,b){if(a)return b}));a&&d.addClass(E(a)?a.join(" "):a)}var i=q;c.$watch(e[b],g,!0);e.$observe("class",function(){var a=c.$eval(e[b]);g(a,a)});b!=="ngClass"&&c.$watch("$index",function(d,g){var i=d&1;i!==g&1&&(i===a?f(c.$eval(e[b])):h(c.$eval(e[b])))})})}var z=function(b){return B(b)?b.toLowerCase():b},la=function(b){return B(b)?b.toUpperCase():b},Z=G((/msie (\d+)/.exec(z(navigator.userAgent))|| -[])[1]),u,ca,ha=[].slice,Pa=[].push,wa=Object.prototype.toString,Ya=P.angular||(P.angular={}),sa,fb,aa=["0","0","0"];C.$inject=[];ma.$inject=[];fb=Z<9?function(b){b=b.nodeName?b:b[0];return b.scopeName&&b.scopeName!="HTML"?la(b.scopeName+":"+b.nodeName):b.nodeName}:function(b){return b.nodeName?b.nodeName:b[0].nodeName};var kc=/[A-Z]/g,jd={full:"1.0.7",major:1,minor:0,dot:7,codeName:"monochromatic-rainbow"},Ba=K.cache={},Aa=K.expando="ng-"+(new Date).getTime(),oc=1,$b=P.document.addEventListener? -function(b,a,c){b.addEventListener(a,c,!1)}:function(b,a,c){b.attachEvent("on"+a,c)},db=P.document.removeEventListener?function(b,a,c){b.removeEventListener(a,c,!1)}:function(b,a,c){b.detachEvent("on"+a,c)},mc=/([\:\-\_]+(.))/g,nc=/^moz([A-Z])/,ua=K.prototype={ready:function(b){function a(){c||(c=!0,b())}var c=!1;this.bind("DOMContentLoaded",a);K(P).bind("load",a)},toString:function(){var b=[];m(this,function(a){b.push(""+a)});return"["+b.join(", ")+"]"},eq:function(b){return b>=0?u(this[b]):u(this[this.length+ -b])},length:0,push:Pa,sort:[].sort,splice:[].splice},Ea={};m("multiple,selected,checked,disabled,readOnly,required".split(","),function(b){Ea[z(b)]=b});var Bb={};m("input,select,option,textarea,button,form".split(","),function(b){Bb[la(b)]=!0});m({data:wb,inheritedData:Da,scope:function(b){return Da(b,"$scope")},controller:zb,injector:function(b){return Da(b,"$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:Ca,css:function(b,a,c){a=tb(a);if(y(c))b.style[a]=c;else{var d;Z<=8&&(d= -b.currentStyle&&b.currentStyle[a],d===""&&(d="auto"));d=d||b.style[a];Z<=8&&(d=d===""?q:d);return d}},attr:function(b,a,c){var d=z(a);if(Ea[d])if(y(c))c?(b[a]=!0,b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d));else return b[a]||(b.attributes.getNamedItem(a)||C).specified?d:q;else if(y(c))b.setAttribute(a,c);else if(b.getAttribute)return b=b.getAttribute(a,2),b===null?q:b},prop:function(b,a,c){if(y(c))b[a]=c;else return b[a]},text:v(Z<9?function(b,a){if(b.nodeType==1){if(w(a))return b.innerText; -b.innerText=a}else{if(w(a))return b.nodeValue;b.nodeValue=a}}:function(b,a){if(w(a))return b.textContent;b.textContent=a},{$dv:""}),val:function(b,a){if(w(a))return b.value;b.value=a},html:function(b,a){if(w(a))return b.innerHTML;for(var c=0,d=b.childNodes;c":function(a,c,d,e){return d(a,c)>e(a,c)},"<=":function(a,c,d,e){return d(a,c)<=e(a,c)},">=":function(a,c,d,e){return d(a,c)>=e(a,c)},"&&":function(a,c,d,e){return d(a,c)&&e(a,c)},"||":function(a,c,d,e){return d(a,c)||e(a,c)},"&":function(a,c,d,e){return d(a,c)&e(a,c)},"|":function(a,c,d,e){return e(a,c)(a,c,d(a,c))},"!":function(a,c,d){return!d(a,c)}},Mc={n:"\n",f:"\u000c",r:"\r",t:"\t",v:"\u000b","'":"'",'"':'"'},ib={},Zc=P.XMLHttpRequest||function(){try{return new ActiveXObject("Msxml2.XMLHTTP.6.0")}catch(a){}try{return new ActiveXObject("Msxml2.XMLHTTP.3.0")}catch(c){}try{return new ActiveXObject("Msxml2.XMLHTTP")}catch(d){}throw Error("This browser does not support XMLHttpRequest."); -};Rb.$inject=["$provide"];Sb.$inject=["$locale"];Ub.$inject=["$locale"];var Xb=".",id={yyyy:O("FullYear",4),yy:O("FullYear",2,0,!0),y:O("FullYear",1),MMMM:Ja("Month"),MMM:Ja("Month",!0),MM:O("Month",2,1),M:O("Month",1,1),dd:O("Date",2),d:O("Date",1),HH:O("Hours",2),H:O("Hours",1),hh:O("Hours",2,-12),h:O("Hours",1,-12),mm:O("Minutes",2),m:O("Minutes",1),ss:O("Seconds",2),s:O("Seconds",1),EEEE:Ja("Day"),EEE:Ja("Day",!0),a:function(a,c){return a.getHours()<12?c.AMPMS[0]:c.AMPMS[1]},Z:function(a){var a= --1*a.getTimezoneOffset(),c=a>=0?"+":"";c+=jb(Math[a>0?"floor":"ceil"](a/60),2)+jb(Math.abs(a%60),2);return c}},hd=/((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/,gd=/^\d+$/;Tb.$inject=["$locale"];var ed=I(z),fd=I(la);Vb.$inject=["$parse"];var kd=I({restrict:"E",compile:function(a,c){Z<=8&&(!c.href&&!c.name&&c.$set("href",""),a.append(T.createComment("IE fix")));return function(a,c){c.bind("click",function(a){c.attr("href")||a.preventDefault()})}}}),lb={};m(Ea,function(a, -c){var d=ea("ng-"+c);lb[d]=function(){return{priority:100,compile:function(){return function(a,g,h){a.$watch(h[d],function(a){h.$set(c,!!a)})}}}}});m(["src","href"],function(a){var c=ea("ng-"+a);lb[c]=function(){return{priority:99,link:function(d,e,g){g.$observe(c,function(c){c&&(g.$set(a,c),Z&&e.prop(a,g[a]))})}}}});var Ma={$addControl:C,$removeControl:C,$setValidity:C,$setDirty:C};Yb.$inject=["$element","$attrs","$scope"];var Pa=function(a){return["$timeout",function(c){var d={name:"form",restrict:"E", -controller:Yb,compile:function(){return{pre:function(a,d,h,f){if(!h.action){var i=function(a){a.preventDefault?a.preventDefault():a.returnValue=!1};$b(d[0],"submit",i);d.bind("$destroy",function(){c(function(){db(d[0],"submit",i)},0,!1)})}var j=d.parent().controller("form"),k=h.name||h.ngForm;k&&(a[k]=f);j&&d.bind("$destroy",function(){j.$removeControl(f);k&&(a[k]=q);v(f,Ma)})}}}};return a?v(U(d),{restrict:"EAC"}):d}]},ld=Pa(),md=Pa(!0),nd=/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/, -od=/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/,pd=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/,bc={text:Oa,number:function(a,c,d,e,g,h){Oa(a,c,d,e,g,h);e.$parsers.push(function(a){var c=X(a);return c||pd.test(a)?(e.$setValidity("number",!0),a===""?null:c?a:parseFloat(a)):(e.$setValidity("number",!1),q)});e.$formatters.push(function(a){return X(a)?"":""+a});if(d.min){var f=parseFloat(d.min),a=function(a){return!X(a)&&ai?(e.$setValidity("max",!1),q):(e.$setValidity("max",!0),a)};e.$parsers.push(d);e.$formatters.push(d)}e.$formatters.push(function(a){return X(a)||Qa(a)?(e.$setValidity("number",!0),a):(e.$setValidity("number",!1),q)})},url:function(a,c,d,e,g,h){Oa(a,c,d,e,g,h);a=function(a){return X(a)||nd.test(a)?(e.$setValidity("url",!0),a):(e.$setValidity("url",!1),q)};e.$formatters.push(a);e.$parsers.push(a)},email:function(a, -c,d,e,g,h){Oa(a,c,d,e,g,h);a=function(a){return X(a)||od.test(a)?(e.$setValidity("email",!0),a):(e.$setValidity("email",!1),q)};e.$formatters.push(a);e.$parsers.push(a)},radio:function(a,c,d,e){w(d.name)&&c.attr("name",xa());c.bind("click",function(){c[0].checked&&a.$apply(function(){e.$setViewValue(d.value)})});e.$render=function(){c[0].checked=d.value==e.$viewValue};d.$observe("value",e.$render)},checkbox:function(a,c,d,e){var g=d.ngTrueValue,h=d.ngFalseValue;B(g)||(g=!0);B(h)||(h=!1);c.bind("click", -function(){a.$apply(function(){e.$setViewValue(c[0].checked)})});e.$render=function(){c[0].checked=e.$viewValue};e.$formatters.push(function(a){return a===g});e.$parsers.push(function(a){return a?g:h})},hidden:C,button:C,submit:C,reset:C},cc=["$browser","$sniffer",function(a,c){return{restrict:"E",require:"?ngModel",link:function(d,e,g,h){h&&(bc[z(g.type)]||bc.text)(d,e,g,h,c,a)}}}],La="ng-valid",Ka="ng-invalid",Na="ng-pristine",Zb="ng-dirty",qd=["$scope","$exceptionHandler","$attrs","$element","$parse", -function(a,c,d,e,g){function h(a,c){c=c?"-"+Za(c,"-"):"";e.removeClass((a?Ka:La)+c).addClass((a?La:Ka)+c)}this.$modelValue=this.$viewValue=Number.NaN;this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$name=d.name;var f=g(d.ngModel),i=f.assign;if(!i)throw Error(Eb+d.ngModel+" ("+pa(e)+")");this.$render=C;var j=e.inheritedData("$formController")||Ma,k=0,l=this.$error={};e.addClass(Na);h(!0);this.$setValidity=function(a, -c){if(l[a]!==!c){if(c){if(l[a]&&k--,!k)h(!0),this.$valid=!0,this.$invalid=!1}else h(!1),this.$invalid=!0,this.$valid=!1,k++;l[a]=!c;h(c,a);j.$setValidity(a,c,this)}};this.$setViewValue=function(d){this.$viewValue=d;if(this.$pristine)this.$dirty=!0,this.$pristine=!1,e.removeClass(Na).addClass(Zb),j.$setDirty();m(this.$parsers,function(a){d=a(d)});if(this.$modelValue!==d)this.$modelValue=d,i(a,d),m(this.$viewChangeListeners,function(a){try{a()}catch(d){c(d)}})};var n=this;a.$watch(function(){var c= -f(a);if(n.$modelValue!==c){var d=n.$formatters,e=d.length;for(n.$modelValue=c;e--;)c=d[e](c);if(n.$viewValue!==c)n.$viewValue=c,n.$render()}})}],rd=function(){return{require:["ngModel","^?form"],controller:qd,link:function(a,c,d,e){var g=e[0],h=e[1]||Ma;h.$addControl(g);c.bind("$destroy",function(){h.$removeControl(g)})}}},sd=I({require:"ngModel",link:function(a,c,d,e){e.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),dc=function(){return{require:"?ngModel",link:function(a,c,d,e){if(e){d.required= -!0;var g=function(a){if(d.required&&(X(a)||a===!1))e.$setValidity("required",!1);else return e.$setValidity("required",!0),a};e.$formatters.push(g);e.$parsers.unshift(g);d.$observe("required",function(){g(e.$viewValue)})}}}},td=function(){return{require:"ngModel",link:function(a,c,d,e){var g=(a=/\/(.*)\//.exec(d.ngList))&&RegExp(a[1])||d.ngList||",";e.$parsers.push(function(a){var c=[];a&&m(a.split(g),function(a){a&&c.push(Q(a))});return c});e.$formatters.push(function(a){return E(a)?a.join(", "): -q})}}},ud=/^(true|false|\d+)$/,vd=function(){return{priority:100,compile:function(a,c){return ud.test(c.ngValue)?function(a,c,g){g.$set("value",a.$eval(g.ngValue))}:function(a,c,g){a.$watch(g.ngValue,function(a){g.$set("value",a,!1)})}}}},wd=S(function(a,c,d){c.addClass("ng-binding").data("$binding",d.ngBind);a.$watch(d.ngBind,function(a){c.text(a==q?"":a)})}),xd=["$interpolate",function(a){return function(c,d,e){c=a(d.attr(e.$attr.ngBindTemplate));d.addClass("ng-binding").data("$binding",c);e.$observe("ngBindTemplate", -function(a){d.text(a)})}}],yd=[function(){return function(a,c,d){c.addClass("ng-binding").data("$binding",d.ngBindHtmlUnsafe);a.$watch(d.ngBindHtmlUnsafe,function(a){c.html(a||"")})}}],zd=kb("",!0),Ad=kb("Odd",0),Bd=kb("Even",1),Cd=S({compile:function(a,c){c.$set("ngCloak",q);a.removeClass("ng-cloak")}}),Dd=[function(){return{scope:!0,controller:"@"}}],Ed=["$sniffer",function(a){return{priority:1E3,compile:function(){a.csp=!0}}}],ec={};m("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave".split(" "), -function(a){var c=ea("ng-"+a);ec[c]=["$parse",function(d){return function(e,g,h){var f=d(h[c]);g.bind(z(a),function(a){e.$apply(function(){f(e,{$event:a})})})}}]});var Fd=S(function(a,c,d){c.bind("submit",function(){a.$apply(d.ngSubmit)})}),Gd=["$http","$templateCache","$anchorScroll","$compile",function(a,c,d,e){return{restrict:"ECA",terminal:!0,compile:function(g,h){var f=h.ngInclude||h.src,i=h.onload||"",j=h.autoscroll;return function(g,h){var n=0,o,p=function(){o&&(o.$destroy(),o=null);h.html("")}; -g.$watch(f,function(f){var m=++n;f?a.get(f,{cache:c}).success(function(a){m===n&&(o&&o.$destroy(),o=g.$new(),h.html(a),e(h.contents())(o),y(j)&&(!j||g.$eval(j))&&d(),o.$emit("$includeContentLoaded"),g.$eval(i))}).error(function(){m===n&&p()}):p()})}}}}],Hd=S({compile:function(){return{pre:function(a,c,d){a.$eval(d.ngInit)}}}}),Id=S({terminal:!0,priority:1E3}),Jd=["$locale","$interpolate",function(a,c){var d=/{}/g;return{restrict:"EA",link:function(e,g,h){var f=h.count,i=g.attr(h.$attr.when),j=h.offset|| -0,k=e.$eval(i),l={},n=c.startSymbol(),o=c.endSymbol();m(k,function(a,e){l[e]=c(a.replace(d,n+f+"-"+j+o))});e.$watch(function(){var c=parseFloat(e.$eval(f));return isNaN(c)?"":(c in k||(c=a.pluralCat(c-j)),l[c](e,g,!0))},function(a){g.text(a)})}}}],Kd=S({transclude:"element",priority:1E3,terminal:!0,compile:function(a,c,d){return function(a,c,h){var f=h.ngRepeat,h=f.match(/^\s*(.+)\s+in\s+(.*)\s*$/),i,j,k;if(!h)throw Error("Expected ngRepeat in form of '_item_ in _collection_' but got '"+f+"'.");f= -h[1];i=h[2];h=f.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);if(!h)throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '"+f+"'.");j=h[3]||h[1];k=h[2];var l=new eb;a.$watch(function(a){var e,f,h=a.$eval(i),m=c,q=new eb,y,A,u,w,r,v;if(E(h))r=h||[];else{r=[];for(u in h)h.hasOwnProperty(u)&&u.charAt(0)!="$"&&r.push(u);r.sort()}y=r.length-1;e=0;for(f=r.length;ez;)u.pop().element.remove()}for(;r.length> -x;)r.pop()[0].element.remove()}var i;if(!(i=s.match(d)))throw Error("Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '"+s+"'.");var j=c(i[2]||i[1]),k=i[4]||i[6],l=i[5],m=c(i[3]||""),n=c(i[2]?i[1]:k),o=c(i[7]),r=[[{element:f,label:""}]];t&&(a(t)(e),t.removeClass("ng-scope"),t.remove());f.html("");f.bind("change",function(){e.$apply(function(){var a,c=o(e)||[],d={},h,i,j,m,s,t;if(p){i=[];m=0;for(t=r.length;m@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak{display:none;}ng\\:form{display:block;}'); +(function(N,S,s){'use strict';function I(b){return function(){var a=arguments[0],c,a="["+(b?b+":":"")+a+"] http://errors.angularjs.org/1.3.0-beta.7/"+(b?b+"/":"")+a;for(c=1;c").append(b).html();try{return 3===b[0].nodeType?u(c):c.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/, +function(a,b){return"<"+u(b)})}catch(d){return u(c)}}function fc(b){try{return decodeURIComponent(b)}catch(a){}}function gc(b){var a={},c,d;q((b||"").split("&"),function(b){b&&(c=b.split("="),d=fc(c[0]),F(d)&&(b=F(c[1])?fc(c[1]):!0,a[d]?M(a[d])?a[d].push(b):a[d]=[a[d],b]:a[d]=b))});return a}function Db(b){var a=[];q(b,function(b,d){M(b)?q(b,function(b){a.push(za(d,!0)+(!0===b?"":"="+za(b,!0)))}):a.push(za(d,!0)+(!0===b?"":"="+za(b,!0)))});return a.length?a.join("&"):""}function hb(b){return za(b, +!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function za(b,a){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,a?"%20":"+")}function hd(b,a){var c,d,e=hc.length;b=y(b);for(d=0;d")+ +d[2];for(d=d[0];d--;)c=c.lastChild;f=f.concat(ra.call(c.childNodes,void 0));c=e.firstChild;c.textContent=""}else f.push(a.createTextNode(b));e.textContent="";e.innerHTML="";q(f,function(a){e.appendChild(a)});return e}function U(b){if(b instanceof U)return b;x(b)&&(b=Y(b));if(!(this instanceof U)){if(x(b)&&"<"!=b.charAt(0))throw Kb("nosel");return new U(b)}if(x(b)){var a;a=S;var c;b=(c=Ae.exec(b))?[a.createElement(c[1])]:(c=xe(b,a))?c.childNodes:[]}pc(this,b)}function Lb(b){return b.cloneNode(!0)} +function Ha(b){qc(b);var a=0;for(b=b.childNodes||[];a=P?(c.preventDefault=null,c.stopPropagation=null,c.isDefaultPrevented=null):(delete c.preventDefault,delete c.stopPropagation,delete c.isDefaultPrevented)};c.elem=b;return c}function Ia(b){var a=typeof b,c;"object"==a&&null!==b?"function"==typeof(c=b.$$hashKey)?c= +b.$$hashKey():c===s&&(c=b.$$hashKey=db()):c=b;return a+":"+c}function Wa(b){q(b,this.put,this)}function De(b){return(b=b.toString().replace(xc,"").match(yc))?"function("+(b[1]||"").replace(/[\s\r\n]+/," ")+")":"fn"}function Nb(b,a,c){var d;if("function"==typeof b){if(!(d=b.$inject)){d=[];if(b.length){if(a)throw x(c)&&c||(c=b.name||De(b)),Ja("strictdi",c);a=b.toString().replace(xc,"");a=a.match(yc);q(a[1].split(Ee),function(a){a.replace(Fe,function(a,b,c){d.push(c)})})}b.$inject=d}}else M(b)?(a=b.length- +1,Ra(b[a],"fn"),d=b.slice(0,a)):Ra(b,"fn",!0);return d}function Eb(b,a){function c(a){return function(b,c){if(R(b))q(b,$b(a));else return a(b,c)}}function d(a,b){Aa(a,"service");if(O(b)||M(b))b=n.instantiate(b);if(!b.$get)throw Ja("pget",a);return p[a+m]=b}function e(a,b){return d(a,{$get:b})}function f(a){var b=[],c,d,e,h;q(a,function(a){if(!l.get(a)){l.put(a,!0);try{if(x(a))for(c=Sa(a),b=b.concat(f(c.requires)).concat(c._runBlocks),d=c._invokeQueue,e=0,h=d.length;e 4096 bytes)!"));else{if(m.cookie!==Z)for(Z=m.cookie,d=Z.split("; "),L={},f=0;fk&&this.remove(n.key),b},get:function(a){if(k").parent()[0])});var f=K(a,b,a,c,d,e);ma(a,"ng-scope");return function(b,c,d){Fb(b,"scope");var e=c?Ka.clone.call(a):a;q(d,function(a,b){e.data("$"+b+"Controller",a)});d=0;for(var g=e.length;darguments.length&& +(b=a,a=s);w&&(c=Z);return n(a,b,c)}var z,v,Xa,C,K,G,Z={},pb;z=c===f?d:cc(d,new Ob(y(f),d.$attr));v=z.$$element;if(L){var ba=/^\s*([@=&])(\??)\s*(\w*)\s*$/;g=y(f);G=e.$new(!0);ha&&ha===L.$$originalDirective?g.data("$isolateScope",G):g.data("$isolateScopeNoTemplate",G);ma(g,"ng-isolate-scope");q(L.scope,function(a,c){var d=a.match(ba)||[],f=d[3]||c,g="?"==d[2],d=d[1],m,l,p,n;G.$$isolateBindings[c]=d+f;switch(d){case "@":z.$observe(f,function(a){G[c]=a});z.$$observers[f].$$scope=e;z[f]&&(G[c]=b(z[f])(e)); +break;case "=":if(g&&!z[f])break;l=r(z[f]);n=l.literal?ya:function(a,b){return a===b};p=l.assign||function(){m=G[c]=l(e);throw ia("nonassign",z[f],L.name);};m=G[c]=l(e);G.$watch(function(){var a=l(e);n(a,G[c])||(n(a,m)?p(e,a=G[c]):G[c]=a);return m=a},null,l.literal);break;case "&":l=r(z[f]);G[c]=function(a){return l(e,a)};break;default:throw ia("iscp",L.name,c,a);}})}pb=n&&E;W&&q(W,function(a){var b={$scope:a===L||a.$$isolateScope?G:e,$element:v,$attrs:z,$transclude:pb},c;K=a.controller;"@"==K&&(K= +z[a.name]);c=t(K,b);Z[a.name]=c;w||v.data("$"+a.name+"Controller",c);a.controllerAs&&(b.$scope[a.controllerAs]=c)});g=0;for(Xa=m.length;gH.priority)break;if(T=H.scope)K=K||H,H.templateUrl||(A("new/isolated scope",L,H,u),R(T)&&(L=H));ca=H.name;!H.templateUrl&&H.controller&&(T=H.controller,W=W||{},A("'"+ca+"' controller",W[ca],H,u),W[ca]=H);if(T=H.transclude)qb=!0,H.$$tlb||(A("transclusion",ba,H, +u),ba=H),"element"==T?(w=!0,z=H.priority,T=G(c,Q,V),u=d.$$element=y(S.createComment(" "+ca+": "+d[ca]+" ")),c=u[0],rb(f,y(ra.call(T,0)),c),I=v(T,e,z,g&&g.name,{nonTlbTranscludeDirective:ba})):(T=y(Lb(c)).contents(),u.empty(),I=v(T,e));if(H.template)if(A("template",ha,H,u),ha=H,T=O(H.template)?H.template(u,d):H.template,T=U(T),H.replace){g=H;T=Jb.test(T)?y(Y(T)):[];c=T[0];if(1!=T.length||1!==c.nodeType)throw ia("tplrt",ca,"");rb(f,u,c);P={$attr:{}};T=Z(c,[],P);var X=a.splice(ja+1,a.length-(ja+1)); +L&&ob(T);a=a.concat(T).concat(X);F(d,P);P=a.length}else u.html(T);if(H.templateUrl)A("template",ha,H,u),ha=H,H.replace&&(g=H),J=D(a.splice(ja,a.length-ja),u,d,f,I,m,p,{controllerDirectives:W,newIsolateScopeDirective:L,templateDirective:ha,nonTlbTranscludeDirective:ba}),P=a.length;else if(H.compile)try{N=H.compile(u,d,I),O(N)?E(null,N,Q,V):N&&E(N.pre,N.post,Q,V)}catch(He){l(He,ga(u))}H.terminal&&(J.terminal=!0,z=Math.max(z,H.priority))}J.scope=K&&!0===K.scope;J.transclude=qb&&I;n.hasElementTranscludeDirective= +w;return J}function ob(a){for(var b=0,c=a.length;bn.priority)&&-1!=n.restrict.indexOf(f)&&(p&&(n=bc(n,{$$start:p,$$end:r})),b.push(n),k=n)}catch(z){l(z)}}return k}function F(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;q(a,function(d,e){"$"!=e.charAt(0)&&(b[e]&&(d+=("style"===e?";":" ")+b[e]),a.$set(e,d,!0,c[e]))}); +q(b,function(b,f){"class"==f?(ma(e,b),a["class"]=(a["class"]?a["class"]+" ":"")+b):"style"==f?(e.attr("style",e.attr("style")+";"+b),a.style=(a.style?a.style+";":"")+b):"$"==f.charAt(0)||a.hasOwnProperty(f)||(a[f]=b,d[f]=c[f])})}function D(a,b,c,d,e,f,g,k){var m=[],l,r,E=b[0],t=a.shift(),z=w({},t,{templateUrl:null,transclude:null,replace:null,$$originalDirective:t}),J=O(t.templateUrl)?t.templateUrl(b,c):t.templateUrl;b.empty();p.get(B.getTrustedResourceUrl(J),{cache:n}).success(function(p){var n, +B;p=U(p);if(t.replace){p=Jb.test(p)?y(Y(p)):[];n=p[0];if(1!=p.length||1!==n.nodeType)throw ia("tplrt",t.name,J);p={$attr:{}};rb(d,b,n);var v=Z(n,[],p);R(t.scope)&&ob(v);a=v.concat(a);F(c,p)}else n=E,b.html(p);a.unshift(z);l=ha(a,n,c,e,b,t,f,g,k);q(d,function(a,c){a==n&&(d[c]=b[0])});for(r=K(b[0].childNodes,e);m.length;){p=m.shift();B=m.shift();var C=m.shift(),G=m.shift(),v=b[0];if(B!==E){var W=B.className;k.hasElementTranscludeDirective&&t.replace||(v=Lb(n));rb(C,y(B),v);ma(y(v),W)}B=l.transclude? +L(p,l.transclude):G;l(r,p,v,d,B)}m=null}).error(function(a,b,c,d){throw ia("tpload",d.url);});return function(a,b,c,d,e){m?(m.push(b),m.push(c),m.push(d),m.push(e)):l(r,b,c,d,e)}}function qb(a,b){var c=b.priority-a.priority;return 0!==c?c:a.name!==b.name?a.namea.status?b:p.reject(b)}var d={method:"get",transformRequest:e.transformRequest,transformResponse:e.transformResponse},f=function(a){function b(a){var c;q(a,function(b,d){O(b)&&(c=b(),null!=c?a[d]=c:delete a[d])})}var c=e.headers,d=w({},a.headers),f,g,c=w({},c.common,c[u(a.method)]);b(c);b(d);a:for(f in c){a=u(f);for(g in d)if(u(g)===a)continue a;d[f]=c[f]}return d}(a);w(d,a);d.headers=f;d.method=Fa(d.method);(a=Pb(d.url)? +b.cookies()[d.xsrfCookieName||e.xsrfCookieName]:s)&&(f[d.xsrfHeaderName||e.xsrfHeaderName]=a);var g=[function(a){f=a.headers;var b=Dc(a.data,Cc(f),a.transformRequest);A(a.data)&&q(f,function(a,b){"content-type"===u(b)&&delete f[b]});A(a.withCredentials)&&!A(e.withCredentials)&&(a.withCredentials=e.withCredentials);return t(a,b,f).then(c,c)},s],h=p.when(d);for(q(B,function(a){(a.request||a.requestError)&&g.unshift(a.request,a.requestError);(a.response||a.responseError)&&g.push(a.response,a.responseError)});g.length;){a= +g.shift();var k=g.shift(),h=h.then(a,k)}h.success=function(a){h.then(function(b){a(b.data,b.status,b.headers,d)});return h};h.error=function(a){h.then(null,function(b){a(b.data,b.status,b.headers,d)});return h};return h}function t(b,c,f){function g(a,b,c,e){B&&(200<=a&&300>a?B.put(s,[a,b,Bc(c),e]):B.remove(s));m(b,a,c,e);d.$$phase||d.$apply()}function m(a,c,d,e){c=Math.max(c,0);(200<=c&&300>c?n.resolve:n.reject)({data:a,status:c,headers:Cc(d),config:b,statusText:e})}function k(){var a=fb(r.pendingRequests, +b);-1!==a&&r.pendingRequests.splice(a,1)}var n=p.defer(),t=n.promise,B,q,s=J(b.url,b.params);r.pendingRequests.push(b);t.then(k,k);(b.cache||e.cache)&&(!1!==b.cache&&"GET"==b.method)&&(B=R(b.cache)?b.cache:R(e.cache)?e.cache:E);if(B)if(q=B.get(s),F(q)){if(q.then)return q.then(k,k),q;M(q)?m(q[1],q[0],aa(q[2]),q[3]):m(q,200,{},"OK")}else B.put(s,t);A(q)&&a(b.method,s,c,g,f,b.timeout,b.withCredentials,b.responseType);return t}function J(a,b){if(!b)return a;var c=[];dd(b,function(a,b){null===a||A(a)|| +(M(a)||(a=[a]),q(a,function(a){R(a)&&(a=sa(a));c.push(za(b)+"="+za(a))}))});0=P&&(!b.match(/^(get|post|head|put|delete|options)$/i)||!N.XMLHttpRequest))return new N.ActiveXObject("Microsoft.XMLHTTP");if(N.XMLHttpRequest)return new N.XMLHttpRequest;throw I("$httpBackend")("noxhr");}function he(){this.$get=["$browser","$window","$document",function(b,a,c){return Ke(b,Je,b.defer,a.angular.callbacks, +c[0])}]}function Ke(b,a,c,d,e){function f(a,b,c){var f=e.createElement("script"),g=null;f.type="text/javascript";f.src=a;f.async=!0;g=function(a){Ua(f,"load",g);Ua(f,"error",g);e.body.removeChild(f);f=null;var h=-1,t="unknown";a&&("load"!==a.type||d[b].called||(a={type:"error"}),t=a.type,h="error"===a.type?404:200);c&&c(h,t)};sb(f,"load",g);sb(f,"error",g);e.body.appendChild(f);return g}var g=-1;return function(e,m,k,l,p,n,r,t){function J(){B=g;W&&W();v&&v.abort()}function E(a,d,e,f,g){K&&c.cancel(K); +W=v=null;0===d&&(d=e?200:"file"==ta(m).protocol?404:0);a(1223===d?204:d,e,f,g||"");b.$$completeOutstandingRequest(D)}var B;b.$$incOutstandingRequestCount();m=m||b.url();if("jsonp"==u(e)){var z="_"+(d.counter++).toString(36);d[z]=function(a){d[z].data=a;d[z].called=!0};var W=f(m.replace("JSON_CALLBACK","angular.callbacks."+z),z,function(a,b){E(l,a,d[z].data,"",b);d[z]=D})}else{var v=a(e);v.open(e,m,!0);q(p,function(a,b){F(a)&&v.setRequestHeader(b,a)});v.onreadystatechange=function(){if(v&&4==v.readyState){var a= +null,b=null;B!==g&&(a=v.getAllResponseHeaders(),b="response"in v?v.response:v.responseText);E(l,B||v.status,b,a,v.statusText||"")}};r&&(v.withCredentials=!0);if(t)try{v.responseType=t}catch(s){if("json"!==t)throw s;}v.send(k||null)}if(0=h&&(p.resolve(r),l(n.$$intervalId),delete e[n.$$intervalId]);t||b.$apply()},g);e[n.$$intervalId]=p;return n}var e={};d.cancel=function(a){return a&&a.$$intervalId in e?(e[a.$$intervalId].reject("canceled"),clearInterval(a.$$intervalId),delete e[a.$$intervalId], +!0):!1};return d}]}function nd(){this.$get=function(){return{id:"en-us",NUMBER_FORMATS:{DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{minInt:1,minFrac:0,maxFrac:3,posPre:"",posSuf:"",negPre:"-",negSuf:"",gSize:3,lgSize:3},{minInt:1,minFrac:2,maxFrac:2,posPre:"\u00a4",posSuf:"",negPre:"(\u00a4",negSuf:")",gSize:3,lgSize:3}],CURRENCY_SYM:"$"},DATETIME_FORMATS:{MONTH:"January February March April May June July August September October November December".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "), +DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),AMPMS:["AM","PM"],medium:"MMM d, y h:mm:ss a","short":"M/d/yy h:mm a",fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",mediumDate:"MMM d, y",shortDate:"M/d/yy",mediumTime:"h:mm:ss a",shortTime:"h:mm a"},pluralCat:function(b){return 1===b?"one":"other"}}}}function Qb(b){b=b.split("/");for(var a=b.length;a--;)b[a]=hb(b[a]);return b.join("/")}function Fc(b,a,c){b=ta(b,c);a.$$protocol= +b.protocol;a.$$host=b.hostname;a.$$port=Q(b.port)||Le[b.protocol]||null}function Gc(b,a,c){var d="/"!==b.charAt(0);d&&(b="/"+b);b=ta(b,c);a.$$path=decodeURIComponent(d&&"/"===b.pathname.charAt(0)?b.pathname.substring(1):b.pathname);a.$$search=gc(b.search);a.$$hash=decodeURIComponent(b.hash);a.$$path&&"/"!=a.$$path.charAt(0)&&(a.$$path="/"+a.$$path)}function oa(b,a){if(0===a.indexOf(b))return a.substr(b.length)}function Ya(b){var a=b.indexOf("#");return-1==a?b:b.substr(0,a)}function Rb(b){return b.substr(0, +Ya(b).lastIndexOf("/")+1)}function Hc(b,a){this.$$html5=!0;a=a||"";var c=Rb(b);Fc(b,this,b);this.$$parse=function(a){var e=oa(c,a);if(!x(e))throw Sb("ipthprfx",a,c);Gc(e,this,b);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var a=Db(this.$$search),b=this.$$hash?"#"+hb(this.$$hash):"";this.$$url=Qb(this.$$path)+(a?"?"+a:"")+b;this.$$absUrl=c+this.$$url.substr(1)};this.$$rewrite=function(d){var e;if((e=oa(b,d))!==s)return d=e,(e=oa(a,e))!==s?c+(oa("/",e)||e):b+d;if((e=oa(c, +d))!==s)return c+e;if(c==d+"/")return c}}function Tb(b,a){var c=Rb(b);Fc(b,this,b);this.$$parse=function(d){var e=oa(b,d)||oa(c,d),e="#"==e.charAt(0)?oa(a,e):this.$$html5?e:"";if(!x(e))throw Sb("ihshprfx",d,a);Gc(e,this,b);d=this.$$path;var f=/^\/[A-Z]:(\/.*)/;0===e.indexOf(b)&&(e=e.replace(b,""));f.exec(e)||(d=(e=f.exec(d))?e[1]:d);this.$$path=d;this.$$compose()};this.$$compose=function(){var c=Db(this.$$search),e=this.$$hash?"#"+hb(this.$$hash):"";this.$$url=Qb(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl= +b+(this.$$url?a+this.$$url:"")};this.$$rewrite=function(a){if(Ya(b)==Ya(a))return a}}function Ub(b,a){this.$$html5=!0;Tb.apply(this,arguments);var c=Rb(b);this.$$rewrite=function(d){var e;if(b==Ya(d))return d;if(e=oa(c,d))return b+a+e;if(c===d+"/")return c};this.$$compose=function(){var c=Db(this.$$search),e=this.$$hash?"#"+hb(this.$$hash):"";this.$$url=Qb(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl=b+a+this.$$url}}function tb(b){return function(){return this[b]}}function Ic(b,a){return function(c){if(A(c))return this[b]; +this[b]=a(c);this.$$compose();return this}}function ie(){var b="",a=!1;this.hashPrefix=function(a){return F(a)?(b=a,this):b};this.html5Mode=function(b){return F(b)?(a=b,this):a};this.$get=["$rootScope","$browser","$sniffer","$rootElement",function(c,d,e,f){function g(a){c.$broadcast("$locationChangeSuccess",h.absUrl(),a)}var h,m,k=d.baseHref(),l=d.url(),p;a?(p=l.substring(0,l.indexOf("/",l.indexOf("//")+2))+(k||"/"),m=e.history?Hc:Ub):(p=Ya(l),m=Tb);h=new m(p,"#"+b);h.$$parse(h.$$rewrite(l));f.on("click", +function(a){if(!a.ctrlKey&&!a.metaKey&&2!=a.which){for(var e=y(a.target);"a"!==u(e[0].nodeName);)if(e[0]===f[0]||!(e=e.parent())[0])return;var g=e.prop("href");R(g)&&"[object SVGAnimatedString]"===g.toString()&&(g=ta(g.animVal).href);if(m===Ub){var k=e.attr("href")||e.attr("xlink:href");if(0>k.indexOf("://"))if(g="#"+b,"/"==k[0])g=p+g+k;else if("#"==k[0])g=p+g+(h.path()||"/")+k;else{for(var l=h.path().split("/"),k=k.split("/"),n=0;ne?Jc(d[0],d[1],d[2],d[3],d[4],c,a):function(b,f){var g=0,h;do h=Jc(d[g++],d[g++],d[g++],d[g++],d[g++],c,a)(b,f),f=s,b=h;while(ga)for(b in k++,e)e.hasOwnProperty(b)&&!d.hasOwnProperty(b)&&(q--, +delete e[b])}else e!==d&&(e=d,k++);return k},function(){p?(p=!1,b(d,d,c)):b(d,g,c);if(h)if(R(d))if(cb(d)){g=Array(d.length);for(var a=0;as&&(y=4-s,L[y]||(L[y]=[]),G=O(d.exp)?"fn: "+(d.exp.name||d.exp.toString()):d.exp,G+="; newVal: "+sa(f)+"; oldVal: "+sa(g),L[y].push(G));else if(d===c){v=!1;break a}}catch(u){n.$$phase=null,e(u)}if(!(h=K.$$childHead||K!==this&&K.$$nextSibling))for(;K!==this&&!(h=K.$$nextSibling);)K=K.$parent}while(K=h);if((v||k.length)&&!s--)throw n.$$phase=null,a("infdig", +b,sa(L));}while(v||k.length);for(n.$$phase=null;l.length;)try{l.shift()()}catch(x){e(x)}},$destroy:function(){if(!this.$$destroyed){var a=this.$parent;this.$broadcast("$destroy");this.$$destroyed=!0;this!==n&&(q(this.$$listenerCount,gb(null,l,this)),a.$$childHead==this&&(a.$$childHead=this.$$nextSibling),a.$$childTail==this&&(a.$$childTail=this.$$prevSibling),this.$$prevSibling&&(this.$$prevSibling.$$nextSibling=this.$$nextSibling),this.$$nextSibling&&(this.$$nextSibling.$$prevSibling=this.$$prevSibling), +this.$parent=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=this.$root=null,this.$$listeners={},this.$$watchers=this.$$asyncQueue=this.$$postDigestQueue=[],this.$destroy=this.$digest=this.$apply=D,this.$on=this.$watch=this.$watchGroup=function(){return D})}},$eval:function(a,b){return f(a)(this,b)},$evalAsync:function(a){n.$$phase||n.$$asyncQueue.length||g.defer(function(){n.$$asyncQueue.length&&n.$digest()});this.$$asyncQueue.push({scope:this,expression:a})},$$postDigest:function(a){this.$$postDigestQueue.push(a)}, +$apply:function(a){try{return m("$apply"),this.$eval(a)}catch(b){e(b)}finally{n.$$phase=null;try{n.$digest()}catch(c){throw e(c),c;}}},$on:function(a,b){var c=this.$$listeners[a];c||(this.$$listeners[a]=c=[]);c.push(b);var d=this;do d.$$listenerCount[a]||(d.$$listenerCount[a]=0),d.$$listenerCount[a]++;while(d=d.$parent);var e=this;return function(){c[fb(c,b)]=null;l(e,1,a)}},$emit:function(a,b){var c=[],d,f=this,g=!1,h={name:a,targetScope:f,stopPropagation:function(){g=!0},preventDefault:function(){h.defaultPrevented= +!0},defaultPrevented:!1},k=[h].concat(ra.call(arguments,1)),m,l;do{d=f.$$listeners[a]||c;h.currentScope=f;m=0;for(l=d.length;mc.msieDocumentMode)throw va("iequirks");var e=aa(fa);e.isEnabled=function(){return b};e.trustAs=d.trustAs;e.getTrusted=d.getTrusted;e.valueOf=d.valueOf;b||(e.trustAs=e.getTrusted=function(a,b){return b},e.valueOf=Da);e.parseAs=function(b,c){var d=a(c);return d.literal&&d.constant?d:function(a,c){return e.getTrusted(b,d(a,c))}};var f=e.parseAs,g=e.getTrusted,h=e.trustAs;q(fa,function(a,b){var c=u(b);e[Ta("parse_as_"+c)]=function(b){return f(a,b)};e[Ta("get_trusted_"+c)]=function(b){return g(a, +b)};e[Ta("trust_as_"+c)]=function(b){return h(a,b)}});return e}]}function pe(){this.$get=["$window","$document",function(b,a){var c={},d=Q((/android (\d+)/.exec(u((b.navigator||{}).userAgent))||[])[1]),e=/Boxee/i.test((b.navigator||{}).userAgent),f=a[0]||{},g=f.documentMode,h,m=/^(Moz|webkit|O|ms)(?=[A-Z])/,k=f.body&&f.body.style,l=!1,p=!1;if(k){for(var n in k)if(l=m.exec(n)){h=l[0];h=h.substr(0,1).toUpperCase()+h.substr(1);break}h||(h="WebkitOpacity"in k&&"webkit");l=!!("transition"in k||h+"Transition"in +k);p=!!("animation"in k||h+"Animation"in k);!d||l&&p||(l=x(f.body.style.webkitTransition),p=x(f.body.style.webkitAnimation))}return{history:!(!b.history||!b.history.pushState||4>d||e),hashchange:"onhashchange"in b&&(!g||7b;b=Math.abs(b);var g=b+"",h="",m=[],k=!1;if(-1!==g.indexOf("e")){var l=g.match(/([\d\.]+)e(-?)(\d+)/);l&&"-"==l[2]&&l[3]>e+1?g="0":(h=g,k=!0)}if(k)0b)&&(h=b.toFixed(e));else{g=(g.split(Uc)[1]||"").length;A(e)&&(e=Math.min(Math.max(a.minFrac,g),a.maxFrac));g=Math.pow(10,e);b=Math.round(b*g)/g;b=(""+b).split(Uc);g=b[0];b=b[1]||"";var l=0,p=a.lgSize,n=a.gSize;if(g.length>=p+n)for(l=g.length- +p,k=0;kb&&(d="-",b=-b);for(b=""+b;b.length-c)e+=c;0===e&&-12==c&&(e=12);return vb(e,a,d)}} +function wb(b,a){return function(c,d){var e=c["get"+b](),f=Fa(a?"SHORT"+b:b);return d[f][e]}}function Vc(b){var a=(new Date(b,0,1)).getDay();return new Date(b,0,(4>=a?5:12)-a)}function Wc(b){return function(a){var c=Vc(a.getFullYear());a=+new Date(a.getFullYear(),a.getMonth(),a.getDate()+(4-a.getDay()))-+c;a=1+Math.round(a/6048E5);return vb(a,b)}}function Qc(b){function a(a){var b;if(b=a.match(c)){a=new Date(0);var f=0,g=0,h=b[8]?a.setUTCFullYear:a.setFullYear,m=b[8]?a.setUTCHours:a.setHours;b[9]&& +(f=Q(b[9]+b[10]),g=Q(b[9]+b[11]));h.call(a,Q(b[1]),Q(b[2])-1,Q(b[3]));f=Q(b[4]||0)-f;g=Q(b[5]||0)-g;h=Q(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));m.call(a,f,g,h,b)}return a}var c=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,e){var f="",g=[],h,m;e=e||"mediumDate";e=b.DATETIME_FORMATS[e]||e;x(c)&&(c=Ve.test(c)?Q(c):a(c));Cb(c)&&(c=new Date(c));if(!qa(c))return c;for(;e;)(m=We.exec(e))?(g=g.concat(ra.call(m,1)),e= +g.pop()):(g.push(e),e=null);q(g,function(a){h=Xe[a];f+=h?h(c,b.DATETIME_FORMATS):a.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return f}}function Re(){return function(b){return sa(b,!0)}}function Se(){return function(b,a){if(!M(b)&&!x(b))return b;a=Infinity===Math.abs(Number(a))?Number(a):Q(a);if(x(b))return a?0<=a?b.slice(0,a):b.slice(a,b.length):"";var c=[],d,e;a>b.length?a=b.length:a<-b.length&&(a=-b.length);0b||37<=b&&40>=b)||n(a)});if(e.hasEvent("paste"))a.on("paste cut",n)}a.on("change",l)}d.$render=function(){a.val(d.$isEmpty(d.$viewValue)?"":d.$viewValue)};var r=c.ngPattern;r&&((e=r.match(/^\/(.*)\/([gim]*)$/))?(r=RegExp(e[1],e[2]),e=function(a){return pa(d,"pattern",d.$isEmpty(a)||r.test(a),a)}):e=function(c){var e=b.$eval(r);if(!e||!e.test)throw I("ngPattern")("noregexp", +r,e,ga(a));return pa(d,"pattern",d.$isEmpty(c)||e.test(c),c)},d.$formatters.push(e),d.$parsers.push(e));if(c.ngMinlength){var q=Q(c.ngMinlength);e=function(a){return pa(d,"minlength",d.$isEmpty(a)||a.length>=q,a)};d.$parsers.push(e);d.$formatters.push(e)}if(c.ngMaxlength){var J=Q(c.ngMaxlength);e=function(a){return pa(d,"maxlength",d.$isEmpty(a)||a.length<=J,a)};d.$parsers.push(e);d.$formatters.push(e)}}function Bb(b,a){return function(c){var d;return qa(c)?c:x(c)&&(b.lastIndex=0,c=b.exec(c))?(c.shift(), +d={yyyy:0,MM:1,dd:1,HH:0,mm:0},q(c,function(b,c){c=c(g.min);h.$setValidity("min",b);return b?a: +s},h.$parsers.push(e),h.$formatters.push(e));g.max&&(e=function(a){var b=h.$isEmpty(a)||c(a)<=c(g.max);h.$setValidity("max",b);return b?a:s},h.$parsers.push(e),h.$formatters.push(e))}}function Xb(b,a){b="ngClass"+b;return["$animate",function(c){function d(a,b){var c=[],d=0;a:for(;dP?function(b){b=b.nodeName?b:b[0];return b.scopeName&&"HTML"!=b.scopeName?Fa(b.scopeName+":"+b.nodeName):b.nodeName}:function(b){return b.nodeName?b.nodeName:b[0].nodeName};var hc=["ng-","data-ng-","ng:","x-ng-"],jd=/[A-Z]/g,md={full:"1.3.0-beta.7",major:1,minor:3,dot:0,codeName:"proper-attribution"},Va= +U.cache={},jb=U.expando="ng-"+(new Date).getTime(),Be=1,sb=N.document.addEventListener?function(b,a,c){b.addEventListener(a,c,!1)}:function(b,a,c){b.attachEvent("on"+a,c)},Ua=N.document.removeEventListener?function(b,a,c){b.removeEventListener(a,c,!1)}:function(b,a,c){b.detachEvent("on"+a,c)};U._data=function(b){return this.cache[b[this.expando]]||{}};var ve=/([\:\-\_]+(.))/g,we=/^moz([A-Z])/,Kb=I("jqLite"),Ae=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,Jb=/<|&#?\w+;/,ye=/<([\w:]+)/,ze=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, +da={option:[1,'"],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};da.optgroup=da.option;da.tbody=da.tfoot=da.colgroup=da.caption=da.thead;da.th=da.td;var Ka=U.prototype={ready:function(b){function a(){c||(c=!0,b())}var c=!1;"complete"===S.readyState?setTimeout(a):(this.on("DOMContentLoaded",a),U(N).on("load",a))}, +toString:function(){var b=[];q(this,function(a){b.push(""+a)});return"["+b.join(", ")+"]"},eq:function(b){return 0<=b?y(this[b]):y(this[this.length+b])},length:0,push:Ze,sort:[].sort,splice:[].splice},nb={};q("multiple selected checked disabled readOnly required open".split(" "),function(b){nb[u(b)]=b});var wc={};q("input select option textarea button form details".split(" "),function(b){wc[Fa(b)]=!0});q({data:sc,inheritedData:mb,scope:function(b){return y(b).data("$scope")||mb(b.parentNode||b,["$isolateScope", +"$scope"])},isolateScope:function(b){return y(b).data("$isolateScope")||y(b).data("$isolateScopeNoTemplate")},controller:tc,injector:function(b){return mb(b,"$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:Mb,css:function(b,a,c){a=Ta(a);if(F(c))b.style[a]=c;else{var d;8>=P&&(d=b.currentStyle&&b.currentStyle[a],""===d&&(d="auto"));d=d||b.style[a];8>=P&&(d=""===d?s:d);return d}},attr:function(b,a,c){var d=u(a);if(nb[d])if(F(c))c?(b[a]=!0,b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d)); +else return b[a]||(b.attributes.getNamedItem(a)||D).specified?d:s;else if(F(c))b.setAttribute(a,c);else if(b.getAttribute)return b=b.getAttribute(a,2),null===b?s:b},prop:function(b,a,c){if(F(c))b[a]=c;else return b[a]},text:function(){function b(b,d){var e=a[b.nodeType];if(A(d))return e?b[e]:"";b[e]=d}var a=[];9>P?(a[1]="innerText",a[3]="nodeValue"):a[1]=a[3]="textContent";b.$dv="";return b}(),val:function(b,a){if(A(a)){if("SELECT"===La(b)&&b.multiple){var c=[];q(b.options,function(a){a.selected&& +c.push(a.value||a.text)});return 0===c.length?null:c}return b.value}b.value=a},html:function(b,a){if(A(a))return b.innerHTML;for(var c=0,d=b.childNodes;c":function(a,c,d,e){return d(a,c)>e(a,c)},"<=":function(a,c,d,e){return d(a,c)<=e(a,c)},">=":function(a,c,d,e){return d(a,c)>=e(a,c)},"&&":function(a,c,d,e){return d(a,c)&&e(a,c)},"||":function(a,c,d,e){return d(a,c)||e(a,c)},"&":function(a,c,d,e){return d(a,c)&e(a,c)},"|":function(a,c,d,e){return e(a,c)(a,c,d(a,c))},"!":function(a,c,d){return!d(a,c)}},bf={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'}, +Wb=function(a){this.options=a};Wb.prototype={constructor:Wb,lex:function(a){this.text=a;this.index=0;this.ch=s;this.lastCh=":";this.tokens=[];var c;for(a=[];this.index=a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"=== +a},isIdent:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isExpOperator:function(a){return"-"===a||"+"===a||this.isNumber(a)},throwError:function(a,c,d){d=d||this.index;c=F(c)?"s "+c+"-"+this.index+" ["+this.text.substring(c,d)+"]":" "+d;throw Ba("lexerr",a,c,this.text);},readNumber:function(){for(var a="",c=this.index;this.index","<=",">="))a=this.binaryFn(a,c.fn,this.relational());return a},additive:function(){for(var a=this.multiplicative(),c;c=this.expect("+","-");)a=this.binaryFn(a,c.fn,this.multiplicative());return a},multiplicative:function(){for(var a=this.unary(),c;c=this.expect("*","/","%");)a=this.binaryFn(a,c.fn,this.unary());return a},unary:function(){var a;return this.expect("+")?this.primary():(a=this.expect("-"))?this.binaryFn($a.ZERO,a.fn, +this.unary()):(a=this.expect("!"))?this.unaryFn(a.fn,this.unary()):this.primary()},fieldAccess:function(a){var c=this,d=this.expect().text,e=Kc(d,this.options,this.text);return w(function(c,d,h){return e(h||a(c,d))},{assign:function(e,g,h){return ub(a(e,h),d,g,c.text,c.options)}})},objectIndex:function(a){var c=this,d=this.expression();this.consume("]");return w(function(e,f){var g=a(e,f),h=d(e,f),m;if(!g)return s;(g=Za(g[h],c.text))&&(g.then&&c.options.unwrapPromises)&&(m=g,"$$v"in g||(m.$$v=s,m.then(function(a){m.$$v= +a})),g=g.$$v);return g},{assign:function(e,f,g){var h=d(e,g);return Za(a(e,g),c.text)[h]=f}})},functionCall:function(a,c){var d=[];if(")"!==this.peekToken().text){do d.push(this.expression());while(this.expect(","))}this.consume(")");var e=this;return function(f,g){for(var h=[],m=c?c(f,g):f,k=0;ka.getHours()?c.AMPMS[0]:c.AMPMS[1]},Z:function(a){a=-1*a.getTimezoneOffset();return a=(0<=a?"+":"")+(vb(Math[0=P&&(c.href||c.name||c.$set("href",""),a.append(S.createComment("IE fix")));if(!c.href&&!c.xlinkHref&&!c.name)return function(a,c){var f="[object SVGAnimatedString]"===xa.call(c.prop("href"))?"xlink:href":"href";c.on("click",function(a){c.attr(f)||a.preventDefault()})}}}),Hb={};q(nb,function(a,c){if("multiple"!=a){var d=na("ng-"+c);Hb[d]=function(){return{priority:100,link:function(a,f,g){a.$watch(g[d],function(a){g.$set(c, +!!a)})}}}}});q(["src","srcset","href"],function(a){var c=na("ng-"+a);Hb[c]=function(){return{priority:99,link:function(d,e,f){var g=a,h=a;"href"===a&&"[object SVGAnimatedString]"===xa.call(e.prop("href"))&&(h="xlinkHref",f.$attr[h]="xlink:href",g=null);f.$observe(c,function(a){a&&(f.$set(h,a),P&&g&&e.prop(g,f[h]))})}}}});var zb={$addControl:D,$removeControl:D,$setValidity:D,$setDirty:D,$setPristine:D};Xc.$inject=["$element","$attrs","$scope","$animate"];var Yc=function(a){return["$timeout",function(c){return{name:"form", +restrict:a?"EAC":"E",controller:Xc,compile:function(){return{pre:function(a,e,f,g){if(!f.action){var h=function(a){a.preventDefault?a.preventDefault():a.returnValue=!1};sb(e[0],"submit",h);e.on("$destroy",function(){c(function(){Ua(e[0],"submit",h)},0,!1)})}var m=e.parent().controller("form"),k=f.name||f.ngForm;k&&ub(a,k,g,k);if(m)e.on("$destroy",function(){m.$removeControl(g);k&&ub(a,k,s,k);w(g,zb)})}}}}}]},qd=Yc(),Dd=Yc(!0),cf=/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/, +df=/^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(\.[a-z0-9-]+)*$/i,ef=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/,Zc=/^(\d{4})-(\d{2})-(\d{2})$/,$c=/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)$/,Yb=/^(\d{4})-W(\d\d)$/,ad=/^(\d{4})-(\d\d)$/,bd=/^(\d\d):(\d\d)$/,ff=/(\b|^)default(\b|$)/,cd={text:ab,date:bb("date",Zc,Bb(Zc,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":bb("datetimelocal",$c,Bb($c,["yyyy","MM","dd","HH","mm"]),"yyyy-MM-ddTHH:mm"),time:bb("time",bd,Bb(bd,["HH","mm"]),"HH:mm"),week:bb("week",Yb,function(a){if(qa(a))return a; +if(x(a)){Yb.lastIndex=0;var c=Yb.exec(a);if(c){a=+c[1];var d=+c[2],c=Vc(a),d=7*(d-1);return new Date(a,0,c.getDate()+d)}}return NaN},"yyyy-Www"),month:bb("month",ad,Bb(ad,["yyyy","MM"]),"yyyy-MM"),number:function(a,c,d,e,f,g){ab(a,c,d,e,f,g);e.$parsers.push(function(a){var c=e.$isEmpty(a);if(c||ef.test(a))return e.$setValidity("number",!0),""===a?null:c?a:parseFloat(a);e.$setValidity("number",!1);return s});Ye(e,"number",c);e.$formatters.push(function(a){return e.$isEmpty(a)?"":""+a});d.min&&(a=function(a){var c= +parseFloat(d.min);return pa(e,"min",e.$isEmpty(a)||a>=c,a)},e.$parsers.push(a),e.$formatters.push(a));d.max&&(a=function(a){var c=parseFloat(d.max);return pa(e,"max",e.$isEmpty(a)||a<=c,a)},e.$parsers.push(a),e.$formatters.push(a));e.$formatters.push(function(a){return pa(e,"number",e.$isEmpty(a)||Cb(a),a)})},url:function(a,c,d,e,f,g){ab(a,c,d,e,f,g);a=function(a){return pa(e,"url",e.$isEmpty(a)||cf.test(a),a)};e.$formatters.push(a);e.$parsers.push(a)},email:function(a,c,d,e,f,g){ab(a,c,d,e,f,g); +a=function(a){return pa(e,"email",e.$isEmpty(a)||df.test(a),a)};e.$formatters.push(a);e.$parsers.push(a)},radio:function(a,c,d,e){A(d.name)&&c.attr("name",db());var f=function(f){c[0].checked&&a.$apply(function(){e.$setViewValue(d.value,f&&f.type)})};if(e.$options&&e.$options.updateOn)c.on(e.$options.updateOn,f);if(!e.$options||e.$options.updateOnDefault)c.on("click",f);e.$render=function(){c[0].checked=d.value==e.$viewValue};d.$observe("value",e.$render)},checkbox:function(a,c,d,e){var f=d.ngTrueValue, +g=d.ngFalseValue;x(f)||(f=!0);x(g)||(g=!1);d=function(d){a.$apply(function(){e.$setViewValue(c[0].checked,d&&d.type)})};if(e.$options&&e.$options.updateOn)c.on(e.$options.updateOn,d);if(!e.$options||e.$options.updateOnDefault)c.on("click",d);e.$render=function(){c[0].checked=e.$viewValue};e.$isEmpty=function(a){return a!==f};e.$formatters.push(function(a){return a===f});e.$parsers.push(function(a){return a?f:g})},hidden:D,button:D,submit:D,reset:D,file:D},lc=["$browser","$sniffer","$filter",function(a, +c,d){return{restrict:"E",require:["?ngModel"],link:function(e,f,g,h){h[0]&&(cd[u(g.type)]||cd.text)(e,f,g,h[0],c,a,d)}}}],yb="ng-valid",xb="ng-invalid",Ma="ng-pristine",Ab="ng-dirty",gf=["$scope","$exceptionHandler","$attrs","$element","$parse","$animate","$timeout",function(a,c,d,e,f,g,h){function m(a,c){c=c?"-"+ib(c,"-"):"";g.removeClass(e,(a?xb:yb)+c);g.addClass(e,(a?yb:xb)+c)}this.$modelValue=this.$viewValue=Number.NaN;this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$pristine= +!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$name=d.name;var k=f(d.ngModel),l=k.assign,p=null,n=this;if(!l)throw I("ngModel")("nonassign",d.ngModel,ga(e));this.$render=D;this.$isEmpty=function(a){return A(a)||""===a||null===a||a!==a};var r=e.inheritedData("$formController")||zb,t=0,s=this.$error={};e.addClass(Ma);m(!0);this.$setValidity=function(a,c){s[a]!==!c&&(c?(s[a]&&t--,t||(m(!0),n.$valid=!0,n.$invalid=!1)):(m(!1),n.$invalid=!0,n.$valid=!1,t++),s[a]=!c,m(c,a),r.$setValidity(a,c,n))}; +this.$setPristine=function(){n.$dirty=!1;n.$pristine=!0;g.removeClass(e,Ab);g.addClass(e,Ma)};this.$cancelUpdate=function(){h.cancel(p);n.$render()};this.$$realSetViewValue=function(d){n.$viewValue=d;n.$pristine&&(n.$dirty=!0,n.$pristine=!1,g.removeClass(e,Ma),g.addClass(e,Ab),r.$setDirty());q(n.$parsers,function(a){d=a(d)});n.$modelValue!==d&&(n.$modelValue=d,l(a,d),q(n.$viewChangeListeners,function(a){try{a()}catch(d){c(d)}}))};this.$setViewValue=function(a,c){var d=n.$options&&(R(n.$options.debounce)? +n.$options.debounce[c]||n.$options.debounce["default"]||0:n.$options.debounce)||0;h.cancel(p);d?p=h(function(){n.$$realSetViewValue(a)},d):n.$$realSetViewValue(a)};a.$watch(function(){var c=k(a);if(n.$modelValue!==c){var d=n.$formatters,e=d.length;for(n.$modelValue=c;e--;)c=d[e](c);n.$viewValue!==c&&(n.$viewValue=c,n.$render())}return c})}],Sd=function(){return{require:["ngModel","^?form","^?ngModelOptions"],controller:gf,link:function(a,c,d,e){var f=e[0],g=e[1]||zb;g.$addControl(f);e[2]&&(f.$options= +e[2].$options);a.$on("$destroy",function(){g.$removeControl(f)})}}},Ud=$({require:"ngModel",link:function(a,c,d,e){e.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),mc=function(){return{require:"?ngModel",link:function(a,c,d,e){if(e){d.required=!0;var f=function(a){if(d.required&&e.$isEmpty(a))e.$setValidity("required",!1);else return e.$setValidity("required",!0),a};e.$formatters.push(f);e.$parsers.unshift(f);d.$observe("required",function(){f(e.$viewValue)})}}}},Td=function(){return{require:"ngModel", +link:function(a,c,d,e){var f=(a=/\/(.*)\//.exec(d.ngList))&&RegExp(a[1])||d.ngList||",";e.$parsers.push(function(a){if(!A(a)){var c=[];a&&q(a.split(f),function(a){a&&c.push(Y(a))});return c}});e.$formatters.push(function(a){return M(a)?a.join(", "):s});e.$isEmpty=function(a){return!a||!a.length}}}},hf=/^(true|false|\d+)$/,Vd=function(){return{priority:100,compile:function(a,c){return hf.test(c.ngValue)?function(a,c,f){f.$set("value",a.$eval(f.ngValue))}:function(a,c,f){a.$watch(f.ngValue,function(a){f.$set("value", +a)})}}}},Wd=function(){return{controller:["$scope","$attrs",function(a,c){var d=this;this.$options=a.$eval(c.ngModelOptions);this.$options.updateOn?(this.$options.updateOnDefault=!1,this.$options.updateOn=this.$options.updateOn.replace(ff,function(){d.$options.updateOnDefault=!0;return" "})):this.$options.updateOnDefault=!0}]}},vd=wa(function(a,c,d){c.addClass("ng-binding").data("$binding",d.ngBind);a.$watch(d.ngBind,function(a){c.text(a==s?"":a)})}),xd=["$interpolate",function(a){return function(c, +d,e){c=a(d.attr(e.$attr.ngBindTemplate));d.addClass("ng-binding").data("$binding",c);e.$observe("ngBindTemplate",function(a){d.text(a)})}}],wd=["$sce","$parse",function(a,c){return function(d,e,f){e.addClass("ng-binding").data("$binding",f.ngBindHtml);var g=c(f.ngBindHtml);d.$watch(function(){return(g(d)||"").toString()},function(c){e.html(a.getTrustedHtml(g(d))||"")})}}],yd=Xb("",!0),Ad=Xb("Odd",0),zd=Xb("Even",1),Bd=wa({compile:function(a,c){c.$set("ngCloak",s);a.removeClass("ng-cloak")}}),Cd=[function(){return{scope:!0, +controller:"@",priority:500}}],nc={};q("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),function(a){var c=na("ng-"+a);nc[c]=["$parse",function(d){return{compile:function(e,f){var g=d(f[c]);return function(c,d,e){d.on(u(a),function(a){c.$apply(function(){g(c,{$event:a})})})}}}}]});var Fd=["$animate",function(a){return{transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(c, +d,e,f,g){var h,m,k;c.$watch(e.ngIf,function(f){Pa(f)?m||(m=c.$new(),g(m,function(c){c[c.length++]=S.createComment(" end ngIf: "+e.ngIf+" ");h={clone:c};a.enter(c,d.parent(),d)})):(k&&(k.remove(),k=null),m&&(m.$destroy(),m=null),h&&(k=Gb(h.clone),a.leave(k,function(){k=null}),h=null))})}}}],Gd=["$http","$templateCache","$anchorScroll","$animate","$sce",function(a,c,d,e,f){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element",controller:Qa.noop,compile:function(g,h){var m=h.ngInclude|| +h.src,k=h.onload||"",l=h.autoscroll;return function(g,h,q,t,s){var E=0,B,z,y,v=function(){z&&(z.remove(),z=null);B&&(B.$destroy(),B=null);y&&(e.leave(y,function(){z=null}),z=y,y=null)};g.$watch(f.parseAsResourceUrl(m),function(f){var m=function(){!F(l)||l&&!g.$eval(l)||d()},q=++E;f?(a.get(f,{cache:c}).success(function(a){if(q===E){var c=g.$new();t.template=a;a=s(c,function(a){v();e.enter(a,null,h,m)});B=c;y=a;B.$emit("$includeContentLoaded");g.$eval(k)}}).error(function(){q===E&&v()}),g.$emit("$includeContentRequested")): +(v(),t.template=null)})}}}}],Xd=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(c,d,e,f){d.html(f.template);a(d.contents())(c)}}}],Hd=wa({priority:450,compile:function(){return{pre:function(a,c,d){a.$eval(d.ngInit)}}}}),Id=wa({terminal:!0,priority:1E3}),Jd=["$locale","$interpolate",function(a,c){var d=/{}/g;return{restrict:"EA",link:function(e,f,g){var h=g.count,m=g.$attr.when&&f.attr(g.$attr.when),k=g.offset||0,l=e.$eval(m)||{},p={},n=c.startSymbol(), +r=c.endSymbol(),t=/^when(Minus)?(.+)$/;q(g,function(a,c){t.test(c)&&(l[u(c.replace("when","").replace("Minus","-"))]=f.attr(g.$attr[c]))});q(l,function(a,e){p[e]=c(a.replace(d,n+h+"-"+k+r))});e.$watch(function(){var c=parseFloat(e.$eval(h));if(isNaN(c))return"";c in l||(c=a.pluralCat(c-k));return p[c](e)},function(a){f.text(a)})}}}],Kd=["$parse","$animate",function(a,c){var d=I("ngRepeat");return{transclude:"element",priority:1E3,terminal:!0,$$tlb:!0,link:function(e,f,g,h,m){var k=g.ngRepeat,l=k.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/), +p,n,r,t,s,E,B={$id:Ia};if(!l)throw d("iexp",k);g=l[1];h=l[2];(l=l[3])?(p=a(l),n=function(a,c,d){E&&(B[E]=a);B[s]=c;B.$index=d;return p(e,B)}):(r=function(a,c){return Ia(c)},t=function(a){return a});l=g.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);if(!l)throw d("iidexp",g);s=l[3]||l[1];E=l[2];var z={};e.$watchCollection(h,function(a){var g,h,l=f[0],p,B={},F,C,u,x,D,w,A=[];if(cb(a))D=a,p=n||r;else{p=n||t;D=[];for(u in a)a.hasOwnProperty(u)&&"$"!=u.charAt(0)&&D.push(u);D.sort()}F=D.length; +h=A.length=D.length;for(g=0;gC;)w.pop().element.remove()}for(;x.length>A;)x.pop()[0].element.remove()}var k;if(!(k=t.match(d)))throw jf("iexp",t,ga(f));var l=c(k[2]||k[1]),m=k[4]||k[6],n=k[5],p=c(k[3]||""),q=c(k[2]?k[1]:m),y=c(k[7]),v=k[8]?c(k[8]):null,x=[[{element:f,label:""}]];u&&(a(u)(e),u.removeClass("ng-scope"),u.remove()); +f.empty();f.on("change",function(){e.$apply(function(){var a,c=y(e)||[],d={},h,k,l,p,t,w,u;if(r)for(k=[],p=0,w=x.length;p@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide{display:none !important;}ng\\:form{display:block;}'); +//# sourceMappingURL=angular.min.js.map diff --git a/public/js/app.js b/public/js/app.js index e1ff667f..6f603b91 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1,29 +1,41 @@ +var DEFAULT_MAX_SOURCE_TIME = 5000; +var DEFAULT_VOLUME = 80; +var COOKIE_EXPIRES = 604800; + // Begin actual class implementation... var Soundtrack = function() { this.settings = { - notifications: $.cookie('notificationsEnabled') + notifications: $.cookie('notificationsEnabled') + , streaming: $.cookie('streaming') !== 'false' + , avoidVideo: $.cookie('avoidVideo') === 'true' + , maxTimeToPlaySource: $.cookie('maxTimeToPlaySource', Number) || DEFAULT_MAX_SOURCE_TIME }; this.user = { username: $('a[data-for=user-model]').data('username') }; this.room = { - name: '' - , track: { - title: '' - , artist: '' - } + name: '', + track: { + title: '', + artist: '' + } }; this.controls = { volume: {} - } + }; + // stub out the player, since sometimes we don't load it. this.player = { - ready: function(callback) { - callback(); - }, - src: function(src) { - return src; - } - } + ready: function( callback ) { return callback(); }, + src: function( src ) { return src; }, + pause: function() { return this; }, + play: function() { return this; }, + on: function( event , cb ) { return this; }, + one: function( event , cb ) { return this; }, + volume: function( level ) { return this; }, + currentTime: function( t ) { return 0; }, + duration: function( t ) { return 0; }, + error: function( e ) { return this; }, + }; }; Soundtrack.prototype.checkNotificationPermissions = function(callback) { if (window.webkitNotifications.checkPermission() != 0) { @@ -31,7 +43,7 @@ Soundtrack.prototype.checkNotificationPermissions = function(callback) { console.log(e); }); } -} +}; Soundtrack.prototype.notify = function(img, title, content, callback) { if (!this.settings.notifications) { return false; } @@ -41,86 +53,126 @@ Soundtrack.prototype.notify = function(img, title, content, callback) { setTimeout(function() { e.currentTarget.cancel(); }, 15000); - } + }; notification.onclick = function() { window.focus(); this.cancel(); - } + }; notification.show(); }; Soundtrack.prototype.editTrackID = function( trackID ) { $editor = $('form[data-for=edit-track]'); - $.getJSON('/tracks/'+encodeURIComponent(trackID), function(track) { + $.getJSON('/tracks/' + encodeURIComponent(trackID), function(track) { if (!track || !track._id) { return alert('No such track.'); } $editor.data('track-id', track._id ); $editor.data('artist-slug', track._artist.slug ); + $editor.data('artist-id', track._artist._id ); $editor.data('track-slug', track.slug ); + $editor.find('input[name=trackArtistID]').val( track._artist._id ); $editor.find('input[name=artist]').val( track._artist.name ); $editor.find('input[name=title]').val( track.title ); + $editor.find('*[data-context=track]').data('track-id', track._id); + + $editor.find('*[data-context=track][data-action=track-flag-nsfw]').prop('checked', track.flags.nsfw ); + $editor.find('*[data-context=track][data-action=track-flag-live]').prop('checked', track.flags.live ); + + var titles = track.titles; + if (titles.indexOf( track.title ) == -1) { + titles.push( track.title ); + } + + track.sources.youtube.forEach(function(video) { + if (video.data && titles.indexOf( video.data.title ) == -1) { + titles.push( video.data.title ); + } + }); + + track.sources.soundcloud.forEach(function(track) { + if (track.data && titles.indexOf( track.data.title ) == -1) { + titles.push( track.data.title ); + } + }); + + $editor.find('pre[data-for=titles]').html( titles.join('
') ); + + /* $editor.find('input.typeahead').typeahead({ + name: 'artists' + , remote: '/artists?q=%QUERY' + }); */ + $editor.modal(); }); -} -function volumeChangeHandler(e) { - var self = this; +}; - console.log('Handling volume change... ' + $(self).val() + ' => ' + $(self).val() / 100 ); - console.log( typeof($(self).val()) ) +function volumeChangeHandler(e) { + var vol = Number( e.value ); - soundtrack.player.volume( $(self).val() / 100 ); - $.cookie('lastVolume', $(self).val() , { expires: COOKIE_EXPIRES }); + soundtrack.player.volume( vol / 100 ); + $.cookie('lastVolume' , vol, { expires: COOKIE_EXPIRES }); }; -function mutePlayer() { +function mutePlayer(saveState) { // TODO: why doesn't this work with just 0? Why does it only work with 0.001? soundtrack.player.volume( 0.00001 ); - $.cookie('lastVolume', '0'); + $.cookie('lastVolume', 0); $('.slider[data-for=volume]').slider('setValue', 0).val(0); } + function unmutePlayer() { - if (parseInt($.cookie('lastVolume'))) { - soundtrack.player.volume( $.cookie('lastVolume') / 100 ); - $('.slider[data-for=volume]').slider('setValue', $.cookie('lastVolume')).val($.cookie('lastVolume')); + var lastVol = $.cookie('lastVolume', Number); + + if (lastVol) { + soundtrack.player.volume(lastVol / 100); + $('.slider[data-for=volume]').slider('setValue', lastVol).val(lastVol); } else { soundtrack.player.volume( 0.8 ); - $('.slider[data-for=volume]').slider('setValue', 80).val(80); - $.cookie('lastVolume', '80', { expires: COOKIE_EXPIRES }); + $('.slider[data-for=volume]').slider('setValue', DEFAULT_VOLUME).val( DEFAULT_VOLUME ); + $.cookie('lastVolume', DEFAULT_VOLUME, { expires: COOKIE_EXPIRES }); } } + function ensureVolumeCorrect() { - if (registered) { - console.log('last volume... ' + $.cookie('lastVolume') + ' => ' + ($.cookie('lastVolume') / 100) ); - // TODO: why doesn't this work with just 0? Why does it only work with 0.001? - soundtrack.player.volume( ($.cookie('lastVolume') + 0.001) / 100 ); - $('.slider[data-for=volume]').slider('setValue', $.cookie('lastVolume')).val( $.cookie('lastVolume') ); - } else { - mutePlayer(); + var lastVol = $.cookie('lastVolume', Number); + if (soundtrack.debug) console.log('setting volume to ', lastVol / 100); + + soundtrack.player.volume( lastVol / 100 ); + + if ($('.slider[data-for=volume]')[0]) { + $('.slider[data-for=volume]').slider('setValue', lastVol ).val( lastVol ); } } + function updateUserlist() { $.get('/listeners.json', function(data) { $('#userlist').html(''); - $('.user-count').html(''+data.length+' online'); + $('.user-count').html('' + data.length + ' online'); data.forEach(function(user) { // TODO: use template (Blade?) - $('
  • '+user.username+'
  • ').appendTo('#userlist'); + if (user.role != 'listener') { + $('
  • ' + user.username + ' ' + user.role + '
  • ').appendTo('#userlist'); + } else { + $('
  • ' + user.username + '
  • ').appendTo('#userlist'); + } + }); }); } var videoToggled = false; //TODO: Should this use Cookie? function toggleVideo() { - if(videoToggled) + if (videoToggled) toggleVideoOn(); else toggleVideoOff(); } function toggleVideoOn() { - $('#screen-one *').css('height', '300px'); $('#messages').css('height', '256px'); + $('#screen-one *').css('height', '300px'); + $('#messages').css('height', '256px'); $(this).children('i').replaceWith($('')); $("#messages").scrollTop($("#messages")[0].scrollHeight); @@ -128,36 +180,92 @@ function toggleVideoOn() { } function toggleVideoOff() { - $('#screen-one *').css('height', '0px'); $('#messages').css('height', '541px'); + $('#screen-one *').css('height', '0px'); + $('#messages').css('height', '541px'); $(this).children('i').replaceWith($('')); - + videoToggled = true; } + +angular.module('timeFilters', []). + filter('toHHMMSS', function() { + return function(input) { + var sec_num = parseInt(input, 10); // don't forget the second parm + var hours = Math.floor(sec_num / 3600); + var minutes = Math.floor((sec_num - (hours * 3600)) / 60); + var seconds = sec_num - (hours * 3600) - (minutes * 60); + + if (hours < 10) { + hours = "0" + hours; + } + if (minutes < 10) { + minutes = "0" + minutes; + } + if (seconds < 10) { + seconds = "0" + seconds; + } + + if (hours != '00') { + var time = hours + ':' + minutes + ':' + seconds; + } else { + var time = minutes + ':' + seconds; + } + return time; + } + }); + +angular.module('soundtrack-io', ['timeFilters']); + function AppController($scope, $http) { - window.updatePlaylist = function(){ - $http.get('/playlist.json').success(function(data){ - $scope.tracks = data; - soundtrack.room.track = data[0]; + window.updatePlaylist = function() { + $http.get('/playlist.json').success(function(data) { + if (!data) var data = []; + + $scope.tracks = data.map(function(t) { + if (t.images && t.images.thumbnail && t.images.thumbnail.url) { + // strip hardcoded http urls + t.images.thumbnail.url = t.images.thumbnail.url.replace('http:', ''); + } + return t; + }); + + if (data.length) { + $scope.playlistLength = data.map(function(x) { + return x.duration; + }).reduce(function(prev, now) { + return prev + now; + }); + } + + if (typeof(soundtrack) != 'undefined') { + soundtrack.room.track = data[0]; + } }); } updatePlaylist(); } -String.prototype.toHHMMSS = function () { +String.prototype.toHHMMSS = function() { var sec_num = parseInt(this, 10); // don't forget the second parm - var hours = Math.floor(sec_num / 3600); + var hours = Math.floor(sec_num / 3600); var minutes = Math.floor((sec_num - (hours * 3600)) / 60); var seconds = sec_num - (hours * 3600) - (minutes * 60); - if (hours < 10) {hours = "0"+hours;} - if (minutes < 10) {minutes = "0"+minutes;} - if (seconds < 10) {seconds = "0"+seconds;} + if (hours < 10) { + hours = "0" + hours; + } + if (minutes < 10) { + minutes = "0" + minutes; + } + if (seconds < 10) { + seconds = "0" + seconds; + } if (hours != '00') { - var time = hours+':'+minutes+':'+seconds; + var time = hours + ':' + minutes + ':' + seconds; } else { - var time = minutes+':'+seconds; + var time = minutes + ':' + seconds; } return time; } @@ -168,134 +276,255 @@ promise = deferred.promise(); promise.done(function() { - soundtrack.controls.volume = $('.slider[data-for=volume]').slider(); - soundtrack.controls.volume.on('slide', volumeChangeHandler); - soundtrack.controls.volume.on('click', volumeChangeHandler); - - if (!registered) { introJs().start(); } + if ($('.slider[data-for=volume]')[0]) { + soundtrack.controls.volume = $('.slider[data-for=volume]').slider(); + soundtrack.controls.volume.on('slide slideStart', volumeChangeHandler); + if (!$.cookie('lastVolume', Number)) { + $.cookie('lastVolume', DEFAULT_VOLUME); + } + } ensureVolumeCorrect(); setInterval(function() { // TODO: use angularJS for this + var duration = soundtrack.player.duration() || 0; + var time = soundtrack.player.currentTime().toString().toHHMMSS(); - var total = soundtrack.player.duration().toString().toHHMMSS(); - $('#current-track #time').html( time + '/' + total); + var total = duration.toString().toHHMMSS(); + $('#current-track #time').html( time + '/' + total ); var progress = ((soundtrack.player.currentTime() / soundtrack.player.duration()) * 100); $('#track-progress .bar').css('width', progress + '%'); $('#track-progress').attr('title', progress + '%'); + + $('.timestamp').each(function(i , el) { + var $el = $(el); + $el.html( moment( $el.attr('title') ).fromNow() ); + }); + }, 1000); }); -COOKIE_EXPIRES = 604800; - -$(window).load(function(){ +$(window).load(function() { var sockjs = null; - var retryTimes = [1000, 5000, 10000, 30000, 60000, 120000, 300000, 600000]; //in ms + var retryTimes = [100, 1000, 2500, 5000, 10000, 30000, 60000, 120000, 300000, 600000, 86400000]; //in ms var retryIdx = 0; // must be after DOM loads so we have access to the user-model soundtrack = new Soundtrack(); if ($('#main-player').length) { soundtrack.player = videojs('#main-player', { - techOrder: ['html5', 'flash', 'youtube'] - }); - } else { - soundtrack.player = videojs('#secondary-player', { - techOrder: ['html5', 'flash', 'youtube'] + techOrder: ['html5', 'youtube', 'flash'] }); + soundtrack.player.controls( true ); + } //else { + // soundtrack.player = videojs('#secondary-player', { + // techOrder: ['html5', 'youtube'] + // }); + // mutePlayer( false ); + //} + + if (!$.cookie('maxTimeToPlaySource')) { + $.cookie('maxTimeToPlaySource', soundtrack.settings.maxTimeToPlaySource ); } + $('*[data-for=max-source-load-time]').val( $.cookie('maxTimeToPlaySource', Number) ); + soundtrack.player.ready(function() { - console.log('player loaded. :)'); + if (soundtrack.debug) console.log('player loaded. :)'); - startSockJs = function(){ - sockjs = new SockJS('/stream'); + soundtrack.startSockJs = function() { + soundtrack.sockjs = new SockJS('/stream'); - sockjs.onopen = function(){ + soundtrack.sockjs.onopen = function() { //sockjs connection has been opened! - $.post('/socket-auth', {}, function(data){ - sockjs.send(JSON.stringify({type: 'auth', authData: data.authData})); + $.post('/socket-auth', {}, function(data) { + soundtrack.sockjs.send(JSON.stringify({ + type: 'auth', + authData: data.authData + })); }); } - sockjs.onmessage = function(e) { + soundtrack.sockjs.onmessage = function(e) { retryIdx = 0; //reset our retry timeouts var received = new Date(); var msg = JSON.parse(e.data); - console.log(msg); + if (soundtrack.debug) console.log(msg); switch (msg.type) { - default: console.log('unhandled message: ' + msg); break; + default: console.warn('unhandled message: ' + msg); + break; + case 'edit': + updatePlaylist(); + break; case 'track': updatePlaylist(); + // TODO: replace with proper 2-way databinding if (msg.data._artist) { - $('#track-title').attr('href', '/'+msg.data._artist.slug+'/'+msg.data.slug+'/'+msg.data._id); + $('#track-title').attr('href', '/' + msg.data._artist.slug + '/' + msg.data.slug + '/' + msg.data._id); - $('#track-artist').attr('href', '/'+msg.data._artist.slug); - $('#track-artist').html( msg.data._artist.name ); + $('#track-artist').attr('href', '/' + msg.data._artist.slug); + $('#track-artist').html(msg.data._artist.name); } else { - $('#track-artist').html( 'unknown' ); + $('#track-artist').html('unknown'); } - + $('#track-title').html( msg.data.title ); - $('input[name=current-track-id]').val( msg.data._id ); + $('input[name=current-track-id]').val(msg.data._id); + $('*[data-for=current-track-id]').data('track-id', msg.data._id); + if (msg.data.curator) { - $('#track-curator').html(''+msg.data.curator.username+''); - + $('#track-curator').html('' + msg.data.curator.username + ''); + $('#userlist li').removeClass('current-curator'); - $('#userlist li[data-user-id='+msg.data.curator._id+']').addClass('current-curator'); + $('#userlist li[data-user-id=' + msg.data.curator._id + ']').addClass('current-curator'); } else { $('#track-curator').html('the machine'); } - var sources = []; + if (soundtrack.settings.streaming) { + var sources = []; + + // use the new sources array if available + if (msg.sources) { + msg.data.sources = msg.sources + }; + + if (soundtrack.settings.avoidVideo) { + msg.data.sources.soundcloud.forEach(function(item) { + sources.push({ + type: 'audio/mp3', + src: 'https://api.soundcloud.com/tracks/' + item.id + '/stream?client_id=7fbc3f4099d3390415d4c95f16f639ae', + poster: (item.data) ? item.data.artwork_url : undefined + }); + }); + msg.data.sources.youtube.forEach(function(item) { + sources.push({ + type: 'video/youtube', + src: 'https://www.youtube.com/watch?v=' + item.id + }); + }); + } else { + msg.data.sources.youtube.forEach(function(item) { + sources.push({ + type: 'video/youtube', + src: 'https://www.youtube.com/watch?v=' + item.id + }); + }); + msg.data.sources.soundcloud.forEach(function(item) { + sources.push({ + type: 'audio/mp3', + src: 'https://api.soundcloud.com/tracks/' + item.id + '/stream?client_id=7fbc3f4099d3390415d4c95f16f639ae', + poster: (item.data) ? item.data.artwork_url : undefined + }); + }); + } + + if (msg.data.sources.bandcamp) { + msg.data.sources.bandcamp.forEach(function(item) { + if (!item || !item.data) return console.log('bandcamp shit breaking. totally chrisinajar\'s fault. deets: ' , item ); + sources.push({ + type: 'audio/mp3', + src: item.data.url, + poster: (item.data) ? item.data.artwork_url : undefined + }); + }); + } + + if (!sources.length) { + return $.ajax({ + url: '/tracks/' + msg.data._id, + method: 'PUT', + data: { + flags: { + lackingSources: true + } + } + }, function(data) { + if (soundtrack.debug) console.log('submitted the track as needing more sources: ', data); + }); + } - msg.data.sources.youtube.forEach(function( item ) { - sources.push( { type:'video/youtube', src: 'https://www.youtube.com/watch?v=' + item.id } ); - }); + function rollTrack() { + if (soundtrack.debug) console.log('rollTrack()', sources ); + if (soundtrack.debug) console.log('current source:', soundtrack.player.src() ); + if (!sources[0]) return; + + // this is an egregious and terrifying hack + // TODO: not use this hack + soundtrack.player.dispose(); + $('').appendTo('#screen-one'); + soundtrack.player = videojs('#main-player', { + techOrder: ['html5', 'youtube', 'flash'] + }); + soundtrack.player.controls( true ); - msg.data.sources.soundcloud.forEach(function( item ) { - sources.push( { type:'audio/mp3', src: 'https://api.soundcloud.com/tracks/' + item.id + '/stream?client_id=7fbc3f4099d3390415d4c95f16f639ae' } ); - }); + soundtrack.player.error( null ); + soundtrack.player.poster( sources[0].poster ); - soundtrack.player.src( sources ); + soundtrack.player.pause(); + soundtrack.player.src( sources[0] ); + soundtrack.player.load(); - // YouTube doesn't behave well without these two lines... - soundtrack.player.pause(); - soundtrack.player.currentTime( msg.seekTo ); - soundtrack.player.play(); + soundtrack.player.one('playing', function() { + if (soundtrack.debug) console.log('playing event'); + clearInterval( ensureTrackPlaying ); + jumpIfNecessary(); + }); + + // TODO: find a better event to listen for! this is terrible. + soundtrack.player.one('durationchange', function() { + if (soundtrack.debug) console.log('durationchange event'); + if (soundtrack.debug) console.log('setting current time 0 and src...'); + if (soundtrack.debug) console.log('source is now', soundtrack.player.src() ); + soundtrack.player.currentTime( 0 ); + soundtrack.player.play(); + }); + } + + function verifyTrackPlaying() { + if (!sources.length) { + if (soundtrack.debug) console.log('sources length is zero. sad day. failing out.'); + clearInterval( ensureTrackPlaying ); + } else if (soundtrack.player.currentTime() > 0) { + if (soundtrack.debug) console.log('track is playing (yay!). clearing interval.'); + clearInterval( ensureTrackPlaying ) + } else { + if (soundtrack.debug) console.log('track is NOT playing after %dms... advancing to next source', maxTimeToPlayTrack); + if (soundtrack.debug) console.log('failed to load: ', sources[0] ); + + sources.shift(); + rollTrack(); + } + } + + function jumpIfNecessary() { + if (soundtrack.debug) console.log( 'now calling jumpIfNecessary()' ); - // ...and SoundCloud doesn't behave well without these. :/ - var bufferEvaluator = function() { - console.log('evaluating buffer...'); + var now = new Date(); + var estimatedSeekTo = (msg.seekTo * 1000) + (now - received); + var estimatedProgress = estimatedSeekTo / (msg.data.duration * 1000); - var now = new Date(); - var estimatedSeekTo = (msg.seekTo * 1000) + (now - received); - var estimatedProgress = estimatedSeekTo / (msg.data.duration * 1000); + if (estimatedSeekTo / 1000 > 5) { + soundtrack.player.currentTime( estimatedSeekTo / 1000 ); + } - if (soundtrack.player.bufferedPercent() > estimatedProgress) { - soundtrack.player.off('progress', bufferEvaluator); - soundtrack.player.off('loadeddata', bufferEvaluator); - console.log('jumping to ' + msg.seekTo + '...'); - soundtrack.player.pause(); // this breaks soundcloud, wat? - soundtrack.player.currentTime( msg.seekTo ); - soundtrack.player.play(); - } else { - console.log('estimated progress: ' + estimatedProgress ); - console.log( soundtrack.player.bufferedPercent() ) + ensureVolumeCorrect(); } - }; - //soundtrack.player.off('progress', bufferEvaluator); - soundtrack.player.on('progress', bufferEvaluator); - soundtrack.player.on('loadeddata', bufferEvaluator); - ensureVolumeCorrect(); + rollTrack(); + + var maxTimeToPlayTrack = soundtrack.settings.maxTimeToPlaySource; + var ensureTrackPlaying = setInterval( verifyTrackPlaying , maxTimeToPlayTrack ); + + + } if ($('#playlist-list li:first').data('track-id') == msg.data._id) { $('#playlist-list li:first').slideUp('slow', function() { @@ -317,32 +546,39 @@ $(window).load(function(){ updateUserlist(); break; case 'part': - $('#userlist li[data-user-id='+msg.data._id+']').remove(); + $('#userlist li[data-user-id=' + msg.data._id + ']').slideUp(); + updateUserlist(); break; case 'chat': $( msg.data.formatted ).appendTo('#messages'); - $("#messages").scrollTop($("#messages")[0].scrollHeight); - $('.message .message-content').filter(':contains("'+ $('a[data-for=user-model]').data('username') + '")').parent().addClass('highlight'); - - if ( msg.data.message.toLowerCase().indexOf( '@'+ soundtrack.user.username.toLowerCase() ) >= 0 ) { - soundtrack.notify( 'https://soundtrack.io/favicon.ico', 'New Mention in Chat', msg.data.message ); + + setTimeout(function() { + $("#messages").scrollTop($("#messages")[0].scrollHeight); + }, 100); + + $('.message .message-content').filter(':contains("' + $('a[data-for=user-model]').data('username') + '")').parent().addClass('highlight'); + + if (msg.data.message.toLowerCase().indexOf('@' + soundtrack.user.username.toLowerCase()) >= 0) { + soundtrack.notify('https://soundtrack.io/favicon.ico', 'New Mention in Chat', msg.data.message); } break; case 'ping': - sockjs.send(JSON.stringify({type: 'pong'})); - console.log("Ping Pong\'d"); + soundtrack.sockjs.send(JSON.stringify({ + type: 'pong' + })); + if (soundtrack.debug) console.log("Ping Pong\'d"); break; case 'announcement': - $( msg.data.formatted ).appendTo('#messages'); + $( unescape( msg.data.formatted ) ).appendTo('#messages'); $("#messages").scrollTop($("#messages")[0].scrollHeight); break; } }; - sockjs.onclose = function() { - console.log('Lost our connection, lets retry!'); + soundtrack.sockjs.onclose = function() { + if (soundtrack.debug) console.log('Lost our connection, lets retry!'); if (retryIdx < retryTimes.length) { - console.log("Retrying connection in " + retryTimes[retryIdx] + 'ms'); + if (soundtrack.debug) console.log("Retrying connection in " + retryTimes[retryIdx] + 'ms'); setTimeout(restartSockJs, retryTimes[retryIdx++]); } else { alert('Bummer. We lost connection.'); @@ -350,9 +586,9 @@ $(window).load(function(){ }; } - restartSockJs = function(){ - sockjs = null; - startSockJs(); + restartSockJs = function() { + soundtrack.sockjs = null; + soundtrack.startSockJs(); } restartSockJs(); @@ -361,20 +597,18 @@ $(window).load(function(){ }); - $('.message .message-content').filter('.message-content:contains("'+ $('a[data-for=user-model]').data('username') + '")').parent().addClass('highlight'); + $('.message .message-content').filter('.message-content:contains("' + $('a[data-for=user-model]').data('username') + '")').parent().addClass('highlight'); updatePlaylist(); updateUserlist(); // breaks javascript if page doesn't have #messages //$("#messages").scrollTop($("#messages")[0].scrollHeight); - + $('.tablesorter').tablesorter(); $('*[data-action=toggle-volume]').click(function(e) { e.preventDefault(); var self = this; - var currentVolume = parseInt( $('.slider[data-for=volume]').slider('getValue').val() ); - - console.log('currentVolume is a ' + typeof(currentVolume) + ' and is ' + currentVolume); + var currentVolume = Number( $('.slider[data-for=volume]').slider('getValue') ); if (currentVolume) { mutePlayer(); @@ -387,7 +621,7 @@ $(window).load(function(){ return false; }); - OutgoingChatHandler = (function(){ + OutgoingChatHandler = (function() { var listeners = {}; var triggerWord = /^\/(\w+)/i; var CHAT_DEFAULT = '$DEFAULT$'; @@ -401,21 +635,21 @@ $(window).load(function(){ } function notify(key, msg) { - if (listeners[key]) { - listeners[key].forEach(function(l){ - l(msg); - }); + if (listeners[key]) { + listeners[key].forEach(function(l) { + l(msg); + }); - return true; - } + return true; + } - return false; + return false; } function chatSubmit(msg) { var matches = msg.match(triggerWord); if (matches) { - if (notify(matches[1])) { + if (notify(matches[1], msg )) { return; } } @@ -424,8 +658,11 @@ $(window).load(function(){ } //add our default chat handler that actually sends the messages - addListener(CHAT_DEFAULT, function (msg) { - $.post('/chat', { message: msg }, function(data){}); + addListener(CHAT_DEFAULT, function(msg) { + $.post('/chat', { + message: msg + }, function(data) {}); + $("#messages").scrollTop($("#messages")[0].scrollHeight); }); return { @@ -436,16 +673,43 @@ $(window).load(function(){ })(); // /reset -> reset the video player - OutgoingChatHandler.addListener('reset', function(msg){ + OutgoingChatHandler.addListener('reset', function(msg) { var cur = soundtrack.player.currentTime(); - soundtrack.player.stop(); + soundtrack.player.pause(); soundtrack.player.currentTime(cur); soundtrack.player.play(); }); + OutgoingChatHandler.addListener('stream', function(msg) { + if (!msg) return true; + switch ((msg.split(' ')[1] || '').toLowerCase()) { + case 'on': + $('
    Streaming turned on.
    ').appendTo('#messages'); + $("#messages").scrollTop($("#messages")[0].scrollHeight); + + $.cookie('streaming', true); + soundtrack.settings.streaming = true; + soundtrack.sockjs.close(); + soundtrack.startSockJs(); + break; + case 'off': + $.cookie('streaming', false); + soundtrack.settings.streaming = false; + soundtrack.player.pause(); + $('
    Streaming turned off.
    ').appendTo('#messages'); + $("#messages").scrollTop($("#messages")[0].scrollHeight); + break; + default: + var status = (soundtrack.settings.streaming) ? 'on' : 'off'; + $('
    Stream is ' + status + '.
    ').appendTo('#messages'); + $("#messages").scrollTop($("#messages")[0].scrollHeight); + break; + } + }); + // /video -> toggle video OutgoingChatHandler.addListener('video', function(msg) { - switch((msg.split(' ')[1] || '').toLowerCase()) { + switch ((msg.split(' ')[1] || '').toLowerCase()) { case 'on': toggleVideoOn(); break; @@ -463,27 +727,27 @@ $(window).load(function(){ if (partying) { $('.partying').removeClass('partying'); } else { - var d = function () { + var d = function() { var b = Math.floor(Math.random() * 255), a = Math.floor(Math.random() * 255), c = Math.floor(Math.random() * 255); return "rgb(" + b + "," + a + "," + c + ")" - }, h = function () { - jQuery("p, li, h1, h2, h3, div, span, a, input").each(function (b, a) { + }, h = function() { + jQuery("p, li, h1, h2, h3, div, span, a, input").each(function(b, a) { if (jQuery(a).children().size() == 0 && !jQuery(a).hasClass("partying")) { var c = jQuery(a).text().split(" "), - c = jQuery.map(jQuery.makeArray(c), function (a) { + c = jQuery.map(jQuery.makeArray(c), function(a) { return "" + a + "" }); jQuery(a).html(c.join(" ")) } }); - jQuery(".partying").each(function (b, a) { + jQuery(".partying").each(function(b, a) { jQuery(a).css("color", d()) }); partying = true; - }, g = function () { - setTimeout(function () { + }, g = function() { + setTimeout(function() { h(); g(); jQuery("body").css("background-color", d()) @@ -494,7 +758,13 @@ $(window).load(function(){ }); OutgoingChatHandler.addListener('katamari', function(msg) { - var i,s,ss=['http://kathack.com/js/kh.js','http://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js'];for(i=0;i!=ss.length;i++){s=document.createElement('script');s.src=ss[i];document.body.appendChild(s);}void(0); + var i, s, ss = ['http://kathack.com/js/kh.js', 'http://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js']; + for (i = 0; i != ss.length; i++) { + s = document.createElement('script'); + s.src = ss[i]; + document.body.appendChild(s); + } + void(0); }); OutgoingChatHandler.addListener('doge', function(msg) { @@ -504,7 +774,7 @@ $(window).load(function(){ }); OutgoingChatHandler.addListener('dance', function(msg) { - (function () { + (function() { function c() { var e = document.createElement("link"); e.setAttribute("type", "text/css"); @@ -525,7 +795,7 @@ $(window).load(function(){ var e = document.createElement("div"); e.setAttribute("class", a); document.body.appendChild(e); - setTimeout(function () { + setTimeout(function() { document.body.removeChild(e) }, 100) } @@ -579,17 +849,17 @@ $(window).load(function(){ e.setAttribute("class", l); e.src = i; e.loop = false; - setTimeout(function () { + setTimeout(function() { x(k) }, 500); - setTimeout(function () { + setTimeout(function() { N(); p(); for (var e = 0; e < O.length; e++) { T(O[e]) } }, 500); - setTimeout(function(){ + setTimeout(function() { N(); h() }, 30000); @@ -638,7 +908,7 @@ $(window).load(function(){ } } if (A === null) { - console.warn("Could not find a node of the right size. Please try a different page."); + if (soundtrack.debug) console.warn("Could not find a node of the right size. Please try a different page."); return } c(); @@ -674,11 +944,30 @@ $(window).load(function(){ return false; }); + $(document).on('click', '*[data-action=remove-queued-track]', function(e) { + e.preventDefault(); + var self = this; + + $('#playlist-list li[data-track-id=' + $(self).data('track-id') + ']').slideUp(); + + $.ajax({ + url: '/playlist/' + $(self).data('track-id'), + method: 'DELETE', + data: { + index: $(self).data('track-index') + } + }, function(data) { + if (soundtrack.debug) console.log(data); + }); + + return false; + }); + $(document).on('click', '*[data-for=track-search-reset]', function(e) { e.preventDefault(); $('*[data-for=track-search-results]').html(''); $('*[data-for=track-search-query]').val(''); - $('#search-modal *[data-for=track-search-query]').focus(); + $('#search-modal').find('*[data-for=track-search-query]').focus(); $('*[data-for=track-search-select-source]').removeClass('btn-primary'); return false; }); @@ -688,14 +977,14 @@ $(window).load(function(){ var self = this; $('*[data-for=track-search-select-source]').removeClass('btn-primary'); - $( self ).addClass('btn-primary'); + $(self).addClass('btn-primary'); if ($(self).data('data') == 'all') { $('*[data-for=track-search-results] li').slideDown(); } else { - $('*[data-for=track-search-results] li:not(*[data-source='+ $(self).data('data') +'])').slideUp(); - $('*[data-for=track-search-results] li[data-source='+ $(self).data('data') +']').slideDown(); + $('*[data-for=track-search-results] li:not(*[data-source=' + $(self).data('data') + '])').slideUp(); + $('*[data-for=track-search-results] li[data-source=' + $(self).data('data') + ']').slideDown(); } return false; @@ -705,55 +994,154 @@ $(window).load(function(){ e.preventDefault(); var self = this; - $( self ).slideUp(function() { - $( this ).remove(); + $(self).slideUp(function() { + $(this).remove(); }); $.post('/playlist', { - source: $(self).data('source') - , id: $(self).data('id') + source: $(self).data('source'), + id: $(self).data('id') }, function(response) { - console.log(response); + if (soundtrack.debug) console.log(response); }); return false; }, 200, true); + var selectSet = _.debounce(function(e) { + e.preventDefault(); + var $self = $(this); + + $self.slideUp(function() { + $(this).remove(); + }); + + $.getJSON('/' + $self.data('set-slug') , function(set) { + set._tracks.forEach(function(track) { + $.post('/playlist', { + source: 'soundtrack', + id: track._id + }, function(response) { + if (soundtrack.debug) console.log(response); + }); + }); + }); + + return false; + }, 200, true); + + $(document).on('click', '*[data-action=queue-track]', selectTrack); + + $(document).on('click', '*[data-action=queue-set]', selectSet ); + + $(document).on('click', '*[data-action=launch-playlist-editor]', function(e) { + e.preventDefault(); + $('#playlist-modal').modal('show'); + return false; + }); + + $(document).on('click', '*[data-action=launch-playlist-creator]', function(e) { + e.preventDefault(); + var $self = $(this); + + $('#create-playlist-modal').modal('show'); + $('#create-playlist-form').children('input[name=trackID]').val( $self.data('track') ); + $('#create-playlist-form').children('input[name=current-track-id]').val( $self.data('track') ); + + // TODO: replace with local data cache / Maki datastore + $.getJSON('/tracks/'+$self.data('track'), function(track) { + var $track = $('*[data-for=track-name]'); + $track.children('.track-artist').html( track._artist.name ); + $track.children('.track-title').html( track.title ); + + $track.children('*[data-for=track-preview]').html( soundtrack._templates.preview( track ) ); + + }); + + return false; + }); + + soundtrack._templates = { + preview: function( track ) { + if (track.sources && track.sources.youtube && track.sources.youtube.length) { + var video = track.sources.youtube[0]; + return '