From 1746157a9a1dfb2ac17f53548708a25158dc2822 Mon Sep 17 00:00:00 2001 From: Eric Martindale Date: Tue, 31 Dec 2013 21:39:01 +0000 Subject: [PATCH 001/352] Track data, editor permissions, chat, refactor. --- controllers/artists.js | 27 ++- controllers/chat.js | 10 +- controllers/pages.js | 22 +-- controllers/tracks.js | 112 +++++++----- models/Artist.js | 3 +- models/Chat.js | 2 + models/Person.js | 2 +- models/Track.js | 11 +- public/js/app.js | 20 +- soundtrack.js | 353 ++++++++---------------------------- util.js | 324 +++++++++++++++++++++++++++++++++ views/history.jade | 3 +- views/index.jade | 2 +- views/layout.jade | 14 +- views/partials/message.jade | 5 +- views/track.jade | 6 + 16 files changed, 563 insertions(+), 353 deletions(-) create mode 100644 util.js diff --git a/controllers/artists.js b/controllers/artists.js index 820a753f..01023a1d 100644 --- a/controllers/artists.js +++ b/controllers/artists.js @@ -3,17 +3,34 @@ var rest = require('restler') module.exports = { list: function(req, res, next) { - Artist.find({}).sort('name').exec(function(err, artists) { - res.render('artists', { - artists: artists - }); + var query = (req.param('q')) ? { name: new RegExp('(.*)'+req.param('q')+'(.*)', 'i') } : undefined; + console.log( query ); + Artist.find( query ).sort('name').limit(100).exec(function(err, artists) { + res.format({ + json: function() { + res.send( artists.map(function(x) { + x = x.toObject(); + //x.value = x._id; + x.value = x.name; + return x; + }) ); + }, + html: function() { + res.render('artists', { + artists: artists + }); + } + }) }); }, view: function(req, res, next) { Artist.findOne({ slug: req.param('artistSlug') }).exec(function(err, artist) { if (!artist) { return next(); } - Track.find({ _artist: artist._id }).exec(function(err, tracks) { + Track.find({ $or: [ + { _artist: artist._id } + , { _credits: artist._id } + ] }).exec(function(err, tracks) { Play.aggregate([ { $match: { _track: { $in: tracks.map(function(x) { return x._id; }) } } }, diff --git a/controllers/chat.js b/controllers/chat.js index 3eaeb0f3..285cfee5 100644 --- a/controllers/chat.js +++ b/controllers/chat.js @@ -2,9 +2,13 @@ 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 + Chat.find({}).sort('-_id').populate('_author _track _play').limit(20).exec(function(err, chats) { + Artist.populate( chats , { + path: '_track._artist' + }, function(err, chats) { + res.render('chats', { + chats: chats + }); }); }); }, diff --git a/controllers/pages.js b/controllers/pages.js index defca662..3eef7293 100644 --- a/controllers/pages.js +++ b/controllers/pages.js @@ -1,17 +1,19 @@ module.exports = { index: function(req, res, next) { - Chat.find({}).limit(10).sort('-created').populate('_author').exec(function(err, messages) { + Chat.find({}).limit(10).sort('-created').populate('_author _track _play').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 - }); + Artist.populate( messages, { + path: '_track._artist' + }, function(err, messages) { + res.render('index', { + messages: messages.reverse() + , backup: [] + , playlists: playlists + , room: req.app.room + }); + }) }); }); @@ -24,8 +26,6 @@ module.exports = { Artist.populate(plays, { path: '_track._artist' }, function(err, plays) { - console.log(plays); - res.render('history', { plays: plays }); diff --git a/controllers/tracks.js b/controllers/tracks.js index 55f64d8b..e8539e3c 100644 --- a/controllers/tracks.js +++ b/controllers/tracks.js @@ -10,22 +10,39 @@ module.exports = { ], 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 + res.format({ + json: function() { + res.send( tracks ); + }, + html: function() { + res.render('tracks', { + tracks: tracks + }); + } }); }); - }); }, edit: function(req, res, next) { Track.findOne({ _id: req.param('trackID') }).exec(function(err, track) { if (err || !track) { return next(); } + Artist.findOne({ name: req.param('artistName') }).exec(function(err, artist) { + if (err) { console.log(err); } + + if (!artist ) { var artist = new Artist({ name: req.param('artistName') }); } + + track._artist = (artist && artist._id.toString() != track._artist.toString()) ? artist._id : track._artist; + track.title = (req.param('title')) ? req.param('title') : track.title; - track.title = (req.param('title')) ? req.param('title') : track.title; - track.save(function(err) { - res.send({ - status: 'success' - , message: 'Track edited successfully.' + artist.save(function(err) { + if (err) { console.log(err); } + track.save(function(err) { + if (err) { console.log(err); } + res.send({ + status: 'success' + , message: 'Track edited successfully.' + }); + }); }); }); }); @@ -35,49 +52,54 @@ 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); } + ] }).populate('_artist _credits').exec(function(err, track) { + if (!track) { return next(); } + + Chat.find({ _track: track._id }).sort('-_id').limit(20).populate('_author').exec(function(err, chats) { + 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 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 start = new Date(); + start.setHours('0'); + start.setMinutes('0'); + start.setSeconds('0'); + start.setMilliseconds('0'); - var end = new Date( start.getTime() ); + var end = new Date( start.getTime() ); - start = new Date( start - (d * 1000 * 60 * 60 * 24) ); - end = new Date( start.getTime() + 1000 * 60 * 60 * 24 ); + 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 - }); + 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 + , chats: chats + }); + } + ); + }); + } + }); }); }); } diff --git a/models/Artist.js b/models/Artist.js index 4eb0a9df..c3ce4ad9 100644 --- a/models/Artist.js +++ b/models/Artist.js @@ -8,7 +8,8 @@ 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' } } diff --git a/models/Chat.js b/models/Chat.js index b842714f..2b9d72d5 100644 --- a/models/Chat.js +++ b/models/Chat.js @@ -6,6 +6,8 @@ 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' } + , _play: { type: ObjectId, ref: 'Play' } }); ChatSchema.virtual('isoDate').get(function() { diff --git a/models/Person.js b/models/Person.js index e83c0592..b8cfc1d1 100644 --- a/models/Person.js +++ b/models/Person.js @@ -8,7 +8,7 @@ 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'] } ] , avatar: { url: { type: String, default: '/img/user-avatar.png' } } diff --git a/models/Track.js b/models/Track.js index 7b93c334..38bb2a39 100644 --- a/models/Track.js +++ b/models/Track.js @@ -8,9 +8,11 @@ var mongoose = require('mongoose') // and moreover, their type. var TrackSchema = new Schema({ title: { type: String, required: true } + , titles: [ { type: String } ] , _artist: { type: ObjectId, ref: 'Artist' } , _credits: [ { type: ObjectId, ref: 'Artist' } ] - , duration: { type: Number } + , duration: { type: Number } // time in seconds + , description: { type: String } , images: { thumbnail: { url: { type: String } } } @@ -19,12 +21,15 @@ var TrackSchema = new Schema({ id: { type: String, required: true } , start: { type: Number, default: 0 } , duration: { type: Number } + , data: {} })], soundcloud: [ new Schema({ - id: { type: String, required: true } + id: { type: String, required: true } + , data: {} })], vimeo: [ new Schema({ - id: { type: String, required: true } + id: { type: String, required: true } + , data: {} })] } , _remixes: [ new Schema({ diff --git a/public/js/app.js b/public/js/app.js index 1117ce18..1291abd7 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -56,11 +56,18 @@ Soundtrack.prototype.editTrackID = function( trackID ) { $editor.data('track-id', track._id ); $editor.data('artist-slug', track._artist.slug ); + $editor.data('artist-id', track._artist._d ); $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('input.typeahead').typeahead({ + name: 'artists' + , remote: '/artists?q=%QUERY' + }); + $editor.modal(); }); } @@ -106,7 +113,12 @@ function updateUserlist() { $('.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'); + } + }); }); } @@ -206,6 +218,7 @@ $(window).load(function(){ soundtrack.player = videojs('#secondary-player', { techOrder: ['html5', 'flash', 'youtube'] }); + mutePlayer(); } soundtrack.player.ready(function() { console.log('player loaded. :)'); @@ -787,10 +800,13 @@ $(window).load(function(){ var trackID = $(self).data('track-id') , artistSlug = $(self).data('artist-slug') + , artistID = $(self).data('artist-id') , trackSlug = $(self).data('track-slug'); $.post('/'+artistSlug+'/'+trackSlug+'/'+trackID, { - title: $(self).find('input[name=title]').val() + title: $(self).find('input[name=title]').val() + , artistName: $(self).find('input[name=artist]').val() + , artistID: $(self).find('input[name=trackArtistID]').val() }, function(data) { console.log(data); $(self).modal('hide'); diff --git a/soundtrack.js b/soundtrack.js index 639f16ee..844aafb8 100644 --- a/soundtrack.js +++ b/soundtrack.js @@ -1,5 +1,6 @@ var config = require('./config') , database = require('./db') + , util = require('./util') , express = require('express') , app = express() , sys = require('sys') @@ -67,7 +68,7 @@ passport.deserializeUser(function(userID, done) { }); app.use(function(req, res, next) { res.setHeader("X-Powered-By", 'beer.'); - app.locals.user = req.user; + res.locals.user = req.user; if (req.user && !req.user.username) { return res.redirect('/set-username'); @@ -257,103 +258,6 @@ app.forEachClient = function(fn) { } } -function getYoutubeVideo(videoID, internalCallback) { - rest.get('http://gdata.youtube.com/feeds/api/videos/'+videoID+'?v=2&alt=jsonc').on('complete', function(data) { - if (data && data.data) { - var video = data.data; - Track.findOne({ - 'sources.youtube.id': video.id - }).exec(function(err, track) { - if (!track) { var track = new Track({}); } - - // this is bad for now, until we have an importer... - // it'll be slow. - rest.get('http://codingsoundtrack.org/songs/1:'+video.id+'.json').on('complete', function(data) { - - // hack to allow title re-parsing - // be cautious here if we ever store the video titles - //video.title = track.title || video.title; - - // TODO: load from datafile - var baddies = ['[hd]', '[dubstep]', '[electro]', '[edm]', '[house music]', '[glitch hop]', '[video]', '[official video]', '(official video)', '[ official video ]', '[free download]', '[free DL]', '[monstercat release]']; - baddies.forEach(function(token) { - video.title = video.title.replace(token + ' - ', '').trim(); - video.title = video.title.replace(token.toUpperCase() + ' - ', '').trim(); - video.title = video.title.replace(token.capitalize() + ' - ', '').trim(); - - video.title = video.title.replace(token, '').trim(); - video.title = video.title.replace(token.toUpperCase(), '').trim(); - video.title = video.title.replace(token.capitalize(), '').trim(); - }); - - // if codingsoundtrack.org isn't aware of it... - if (!data.author) { - var parts = video.title.split(' - '); - data = { - author: parts[0].trim() - , title: (track.title) ? track.title : video.title - }; - if (parts.length == 2) { - data.title = data.title.replace(data.author + ' - ', '').trim(); - - } - } - - Artist.findOne({ $or: [ - { _id: track._artist } - , { slug: slug( data.author ) } - , { name: data.author } - ] }).exec(function(err, artist) { - - if (!artist) { var artist = new Artist({ - name: data.author - }); } - - track._artist = artist._id; - - 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 - }); - } - - // if the track doesn't already have a title, set it from - if (!track.title) { - track.title = data.title || video.title; - } - - track.duration = (track.duration) ? track.duration : video.duration; - track.images.thumbnail.url = video.thumbnail.hqDefault; - - // TODO: use CodingSoundtrack.org's lookup for artist creation - //Author.findOne() - artist.save(function(err) { - if (err) { console.log(err); } - track.save(function(err) { - if (err) { console.log(err); } - - Artist.populate(track, { - path: '_artist' - }, function(err, track) { - internalCallback( track ); - }); - - }); - }); - }); - }); - }); - } else { - console.log('waaaaaaaaaaat videoID: ' + videoID); - console.log(data); - - internalCallback(); - } - }); -}; - function ensureQueue(callback) { // remove the first track in the playlist... var lastTrack = app.room.playlist.shift(); @@ -369,7 +273,7 @@ function ensureQueue(callback) { Play.find( query ).limit(100).sort('timestamp').exec(function(err, plays) { if (err || !plays) { - getYoutubeVideo( 'dQw4w9WgXcQ‎' , function(track) { + util.getYoutubeVideo( 'dQw4w9WgXcQ‎' , function(track) { if (track) { backupTracks.push( track.toObject() ); } callback(); }); @@ -465,43 +369,46 @@ function startMusic() { } -function scrobbleActive(track, cb) { +function scrobbleActive(requestedTrack, cb) { console.log('scrobbling to active listeners...'); - console.log(track); - if (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 LastFM({ - api_key: config.lastfm.key - , secret: config.lastfm.secret - }); - var creds = { - username: user.profiles.lastfm.username - , key: user.profiles.lastfm.key - }; + 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 LastFM({ + api_key: config.lastfm.key + , secret: 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); } + 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(); + console.log(scrobbles); + cb(); + }); }); }); }); + } function sortPlaylist() { @@ -547,7 +454,7 @@ async.parallel([ var fallbackVideos = ['meBNMk7xKL4', 'KrVC5dm5fFc', '3vC5TsSyNjU', 'vZyenjZseXA', 'QK8mJJJvaes', 'wsUQKw4ByVg', 'PVzljDmoPVs', 'YJVmu6yttiw', '7-tNUur2YoU', '7n3aHR1qgKM', 'lG5aSZBAuPs']; async.series(fallbackVideos.map(function(videoID) { return function(callback) { - getYoutubeVideo(videoID, function(track) { + util.getYoutubeVideo(videoID, function(track) { if (track) { backupTracks.push( track.toObject() ); } callback(); }); @@ -603,6 +510,7 @@ sock.on('connection', function(conn) { , slug: matches[0].user.slug , username: matches[0].user.username , id: conn.id + , role: (matches[0].user.roles && matches[0].user.roles.indexOf('editor') >= 0) ? 'editor' : 'listener' }; app.broadcast({ @@ -684,6 +592,7 @@ app.post('/chat', requireLogin, function(req, res) { var chat = new Chat({ _author: req.user._id , message: req.param('message') + , _track: (app.room.playlist[0]) ? app.room.playlist[0]._id : undefined }); chat.save(function(err) { res.render('partials/message', { @@ -691,6 +600,7 @@ app.post('/chat', requireLogin, function(req, res) { _author: req.user , message: req.param('message') , created: chat.created + , _track: app.room.playlist[0] } }, function(err, html) { app.broadcast({ @@ -705,6 +615,7 @@ app.post('/chat', requireLogin, function(req, res) { , message: req.param('message') , formatted: html , created: new Date() + , _track: app.room.playlist[0] } }); res.send({ status: 'success' }); @@ -743,162 +654,51 @@ app.post('/playlist/:trackID', requireLogin, function(req, res, next) { }); function queueTrack(track, curator, queueCallback) { - app.room.playlist.push( _.extend( track.toObject() , { - score: 0 - , votes: {} // TODO: auto-upvote? - , timestamp: new Date() - , curator: { - _id: curator._id - , id: (app.room.listeners[ curator._id.toString() ]) ? app.room.listeners[ curator._id.toString() ].connId : undefined - , username: curator.username - , slug: curator.slug - } - } ) ); - - sortPlaylist(); + Track.findOne({ _id: track._id }).populate('_artist _credits').exec(function(err, realTrack) { - app.redis.set(config.database.name + ':playlist', JSON.stringify( app.room.playlist ) ); + var playlistItem = realTrack.toObject(); - app.broadcast({ - type: 'playlist:add' - , data: track - }); - - queueCallback(); -} - -var TRACK_SEPARATOR = ' - '; -function parseTitleString(string, partsCallback) { - var artist, title, credits = []; - var string = string || ''; - - console.log('parseTitleString(): ' + string); - - // TODO: load from datafile - var baddies = ['[hd]', '[dubstep]', '[electro]', '[edm]', '[house music]', - '[glitch hop]', '[video]', '[official video]', '(official video)', - '[ official video ]', '[official music video]', '[free download]', - '[free DL]', '( 1080p )', '(with lyrics)', '(High Res / Official video)', - '[monstercat release]', '(hd)']; - baddies.forEach(function(token) { - string = string.replace(token + ' - ', '').trim(); - string = string.replace(token.toUpperCase() + ' - ', '').trim(); - string = string.replace(token.capitalize() + ' - ', '').trim(); - - string = string.replace(token, '').trim(); - string = string.replace(token.toUpperCase(), '').trim(); - string = string.replace(token.capitalize(), '').trim(); - }); - console.log('baddies parsed: ' + string); - - var parts = string.split( TRACK_SEPARATOR ); - - if (parts.length == 2) { - artist = parts[0]; - title = parts[1]; - } else if (parts.length > 2) { - // uh... - artist = parts[0]; - title = parts[1]; - } else { - artist = parts[0]; - title = parts[0]; - } - - // look for certain patterns in the string - credits.push( title.replace(/(.*)\((.*) remix\)/i, '$2') ); - credits.push( artist.replace(/ ft\. (.*)/i, '$1') ); - credits.push( artist.replace(/ feat\. (.*)/i, '$1') ); - - var output = { - artist: artist - , title: title - , credits: credits - }; - - console.log('output parts: ' + output); - console.log('artist: ' + artist); - console.log('title: ' + title); - console.log('credits: ' + credits); - - partsCallback(output); -} - -function trackFromSource(source, id, sourceCallback) { - switch (source) { - default: - callback('Unknown source: ' + source); - break; - case 'soundcloud': - rest.get('https://api.soundcloud.com/tracks/'+parseInt(id)+'.json?client_id='+config.soundcloud.id).on('complete', function(data) { - console.log(data); - if (!data.title) { return sourceCallback('No video found.'); } - - var stringToParse = (data.title.split( TRACK_SEPARATOR ).length > 1) ? data.title : data.user.username + ' - ' + data.title; - - parseTitleString( stringToParse , function(parts) { - - //console.log('parts: ' + JSON.stringify(parts) ); - - Track.findOne({ $or: [ - { 'sources.soundcloud.id': data.id } - ] }).exec(function(err, track) { - if (!track) { var track = new Track({}); } - - 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({}); } - - artist.name = artist.name || parts.artist; - - artist.save(function(err) { - if (err) { console.log(err); } + playlistItem._artist = { + _id: playlistItem._artist._id + , name: playlistItem._artist.name + , slug: playlistItem._artist.slug + }; - track.title = track.title || parts.title; - track._artist = track._artist || artist._id; - track.duration = track.duration || data.duration / 1000; + for (var source in playlistItem.sources) { + console.log(source); + console.log(playlistItem.sources[ source ]); + for (var i = 0; i 2) { + // uh... + artist = parts[0]; + title = parts[1]; + } else { + artist = parts[0]; + title = parts[0]; + } + + + // one last pass + baddies.forEach(function(baddy) { + title = title.replace( new RegExp( escapeRegExp(baddy) , 'i') , '').trim(); + artist = artist.replace( new RegExp( escapeRegExp(baddy) , 'i') , '').trim(); + }); + + // look for certain patterns in the string + credits.push( title.replace(/(.*)\((.*) remix\)/i, '$2').trim() ); + credits.push( title.replace(/(.*) ft\.? (.*)/i, '$1').trim() ); + credits.push( title.replace(/(.*) ft\.? (.*)/i, '$2').trim() ); + credits.push( title.replace(/(.*) feat\.? (.*)/i, '$1').trim() ); + credits.push( title.replace(/(.*) feat\.? (.*)/i, '$2').trim() ); + credits.push( title.replace(/(.*) featuring (.*)/i, '$2').trim() ); + credits.push( title.replace(/(.*) \(ft (.*)\)/i, '$1').trim() ); + credits.push( title.replace(/(.*) \(ft (.*)\)/i, '$2').trim() ); + credits.push( title.replace(/(.*) \(featuring (.*)\)/i, '$2').trim() ); + credits.push( artist.replace(/(.*) ft\.? (.*)/i, '$1').trim() ); + credits.push( artist.replace(/(.*) ft\.? (.*)/i, '$2').trim() ); + credits.push( artist.replace(/(.*) feat\.? (.*)/i, '$1').trim() ); + credits.push( artist.replace(/(.*) feat\.? (.*)/i, '$2').trim() ); + credits.push( artist.replace(/(.*) featuring (.*)/i, '$2').trim() ); + credits.push( artist.replace(/(.*) \(ft (.*)\)/i, '$1').trim() ); + credits.push( artist.replace(/(.*) \(ft (.*)\)/i, '$2').trim() ); + credits.push( artist.replace(/(.*) \(featuring (.*)\)/i, '$2').trim() ); + credits.push( artist.replace(/(.*) & (.*)/ig, '$1').trim() ); + credits.push( artist.replace(/(.*) & (.*)/ig, '$2').trim() ); + credits.push( artist.replace(/(.*) vs\.? (.*)/i, '$1').trim() ); + credits.push( artist.replace(/(.*) vs\.? (.*)/i, '$2').trim() ); + credits.push( artist.replace(/(.*) x (.*)/i, '$1').trim() ); + credits.push( artist.replace(/(.*) x (.*)/i, '$2').trim() ); + + var creditMap = {}; + credits.forEach(function(credit) { + if (credit !== title) { // temp., until we find out why title is in credits + creditMap[ credit ] = credit; + } + }); + + var output = { + artist: artist + , title: title + , credits: Object.keys(creditMap) + }; + + console.log('output parts: ' + JSON.stringify(output) ); + /* console.log('artist: ' + artist); + console.log('title: ' + title); + console.log('credits: ' + credits);*/ + + partsCallback(output); +} + +String.prototype.capitalize = function(){ + return this.replace( /(^|\s)([a-z])/g , function(m,p1,p2){ return p1+p2.toUpperCase(); } ); +}; + +module.exports = { + getYoutubeVideo: function(videoID, internalCallback) { + 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) { + var video = data.data; + Track.findOne({ + 'sources.youtube.id': video.id + }).exec(function(err, track) { + if (!track) { var track = new Track({ title: video.title }); } + + parseTitleString( video.title , function(parts) { + + console.log( video.title + ' was parsed into:'); + 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.hqDefault; + + 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()) { + console.log(source); + console.log(track.sources[ source ]); + for (var i = 0; i 1) ? data.title : data.user.username + ' - ' + data.title; + + parseTitleString( stringToParse , function(parts) { + + //console.log('parts: ' + JSON.stringify(parts) ); + + Track.findOne({ $or: [ + { 'sources.soundcloud.id': data.id } + ] }).exec(function(err, track) { + if (!track) { var track = new Track({}); } + + 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({}); } + + 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) { + sourceCallback(null, track); + } else { + sourceCallback('No track returned.'); + } + }); + break; + } + } +} \ No newline at end of file diff --git a/views/history.jade b/views/history.jade index 3627ee12..6f4d318f 100644 --- a/views/history.jade +++ b/views/history.jade @@ -15,4 +15,5 @@ block content if (play._curator) a(href="#{play._curator.slug}") #{play._curator.username} else - span random entropy (chosen by the machine) \ No newline at end of file + span random entropy (chosen by the machine) + | with #{play.messageCount} messages \ No newline at end of file diff --git a/views/index.jade b/views/index.jade index a643aa9f..0193cc5b 100644 --- a/views/index.jade +++ b/views/index.jade @@ -45,7 +45,7 @@ block content //-h3#playlist-summary(ng-bind="'Playlist (' + tracks.length + ')'") #playlist ul#playlist-list.nav.nav-list(data-intro="If the playlist ever reaches zero, soundtrack.io will automatically choose song based on what's been played in the past.", data-step="3", data-position="right") - li(ng-repeat="track in tracks", ng-class="{active: $first}", data-track-id="{{track._id}}") + li(ng-repeat="track in tracks", ng-class="{active: $first}", ng-style="{top: ((track.order * 40) + 'px')}", data-track-id="{{track._id}}") .playlist-controls .score.badge {{track.score}} i.icon-chevron-up.icon-white(data-action="upvote-track", data-track-id="{{track._id}}") diff --git a/views/layout.jade b/views/layout.jade index 1628bb07..4b1e8aeb 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -9,6 +9,7 @@ html(lang="en-US", ng-app) link(rel="stylesheet", href="/css/darkstrap-v0.9.0.css") //-link(rel="stylesheet", href="/css/bootstrap-custom.css") link(rel="stylesheet", href="/css/bootstrap-responsive.min.css") + link(rel="stylesheet", href="/css/typeahead.js-bootstrap.css") link(rel="stylesheet", href="/css/slider.css") link(rel="stylesheet", href="/css/introjs.css") link(rel="stylesheet", href="/video-js/video-js.min.css") @@ -20,6 +21,7 @@ html(lang="en-US", ng-app) script(src="/js/bootstrap.min.js") script(src="/js/angular.min.js") script(src="/js/d3.min.js") + script(src="/js/typeahead.min.js") script(src="/js/bootstrap.slider.js") script(src="/js/jquery.cookie.js") script(src="/js/intro.js") @@ -114,6 +116,12 @@ html(lang="en-US", ng-app) footer .container p ♫ soundtrack.io is a collaborative online jukebox. It's a way to listen to music with your friends. It's also open-source, as is our Android Client (source). + p + a(class="coinbase-button", data-code="316e0eac837e964996b403a0a6e04930", data-button-style="custom_small", href="https://coinbase.com/checkouts/316e0eac837e964996b403a0a6e04930") Donate + i.glyiphicon.glyphicon-bitcoin + | BitCoin + script(src="https://coinbase.com/assets/button.js", type="text/javascript") + #settings-modal.modal.hide.fade .modal-header @@ -166,12 +174,14 @@ html(lang="en-US", ng-app) registered = true; form#edit-track-modal.modal.hide.fade(data-for="edit-track") + input(type="hidden", name="trackArtistID") + .modal-header button.close(data-dismiss="modal") × h3 Edit Track - .modal-body + .modal-body(style="min-height: 20em;") label(for="artist") Track Artist - input.input-block-level.disabled(type="text", name="artist", disabled, title="This feature isn't finished yet...") + input.input-block-level.typeahead(type="text", name="artist", data-remote="/artists?q=%QUERY") label(for="title") Track Title input.input-block-level(type="text", name="title") diff --git a/views/partials/message.jade b/views/partials/message.jade index c6b5a7fc..3c13603f 100644 --- a/views/partials/message.jade +++ b/views/partials/message.jade @@ -1,5 +1,8 @@ .message(data-message-id="#{message._id}") - abbr.timestamp.pull-right(title="#{moment(message.created).format()}") #{moment(message.created).format('HH:mm:ss')} + if (message._track && message._track._id) + abbr.timestamp.pull-right(title="#{moment(message.created).format()}\n\"#{message._track.title}\" by #{message._track._artist.name} was playing.") #{moment(message.created).format('HH:mm:ss')} + else + abbr.timestamp.pull-right(title="#{moment(message.created).format()}") #{moment(message.created).format('HH:mm:ss')} img.user-avatar-small(src="#{message._author.avatar.url}") - if (message.message.substring(0, 4) == '/me ') strong(data-role="author", data-user-username="#{message._author.username}") #{message._author.username} diff --git a/views/track.jade b/views/track.jade index 993e3007..2a9c4ed8 100644 --- a/views/track.jade +++ b/views/track.jade @@ -12,6 +12,7 @@ block content a(href="/#{track._artist.slug}") #{track._artist.name} | — a(href="/#{track._artist.slug}/#{track.slug}/#{track._id}") #{track.title} + h4 !{track._credits.map(function(x) { return ''+x.name+''; }).join(', ')} p #{track.duration} seconds of brain-melting audio h2 played #{history.length} times @@ -41,6 +42,11 @@ block content default li #{link} + h3 Conversation + each message in chats + include partials/message + + block scripts style .chart rect { From 658a931bf05c6ecf898a4872e4f75639f80b9868 Mon Sep 17 00:00:00 2001 From: Eric Martindale Date: Tue, 11 Mar 2014 23:28:01 +0000 Subject: [PATCH 002/352] General layout improvements, code re-organization. --- config.js | 8 +- controllers/artists.js | 2 +- controllers/people.js | 9 +- controllers/tracks.js | 34 ++++- lib/last.fm.js | 85 +++++++++++ lib/soundtrack.js | 182 ++++++++++++++++++++++++ public/css/main.css | 69 ++++++++- public/js/app.js | 55 +++++++- soundtrack.js | 312 ++++++----------------------------------- views/artist.jade | 8 +- views/index.jade | 19 ++- views/layout.jade | 14 +- views/people.jade | 2 +- views/person.jade | 12 +- views/register.jade | 2 +- views/track.jade | 13 +- 16 files changed, 521 insertions(+), 305 deletions(-) create mode 100644 lib/last.fm.js create mode 100644 lib/soundtrack.js diff --git a/config.js b/config.js index da9b35d7..a2a22676 100644 --- a/config.js +++ b/config.js @@ -16,11 +16,11 @@ module.exports = { , checkInterval: 30 * 1000 }, lastfm: { - key: process.env.SOUNDTRACK_LASTFM_KEY || 'key here' - , secret: process.env.SOUNDTRACK_LASTFM_SECRET || 'secret here' + key: process.env.SOUNDTRACK_LASTFM_KEY || '89a54d8c58f533944fee0196aa227341' + , secret: process.env.SOUNDTRACK_LASTFM_SECRET || 'bd39b0b60cd7cfe82d5dff3747b08dd6' }, soundcloud: { - id: process.env.SOUNDTRACK_SOUNDCLOUD_ID || 'id here' - , secret: process.env.SOUNDTRACK_SOUNDCLOUD_SECRET || 'secret here' + id: process.env.SOUNDTRACK_SOUNDCLOUD_ID || '7fbc3f4099d3390415d4c95f16f639ae' + , secret: process.env.SOUNDTRACK_SOUNDCLOUD_SECRET || '28f12a65a5e84e853732e3bc49aefe2e' } } \ No newline at end of file diff --git a/controllers/artists.js b/controllers/artists.js index 01023a1d..d0e4601e 100644 --- a/controllers/artists.js +++ b/controllers/artists.js @@ -30,7 +30,7 @@ module.exports = { Track.find({ $or: [ { _artist: artist._id } , { _credits: artist._id } - ] }).exec(function(err, tracks) { + ] }).populate('_artist').exec(function(err, tracks) { Play.aggregate([ { $match: { _track: { $in: tracks.map(function(x) { return x._id; }) } } }, diff --git a/controllers/people.js b/controllers/people.js index b3ecef8d..4c809117 100644 --- a/controllers/people.js +++ b/controllers/people.js @@ -16,7 +16,14 @@ module.exports = { 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 ).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 ); diff --git a/controllers/tracks.js b/controllers/tracks.js index e8539e3c..c74fa7d2 100644 --- a/controllers/tracks.js +++ b/controllers/tracks.js @@ -42,11 +42,27 @@ module.exports = { status: 'success' , message: 'Track edited successfully.' }); + + track = track.toObject(); + track._artist = artist; + + req.app.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) { + + }); + }); + }, view: function(req, res, next) { // TODO: use artist in the lookup Track.findOne({ $or: [ @@ -89,11 +105,19 @@ module.exports = { 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 - , chats: chats + + Track.find({ + _artist: track._artist._id + , slug: track.slug + }).exec(function(err, dupes) { + + res.render('track', { + track: track + , history: history + , playsPerDay: playsPerDay + , chats: chats + , dupes: dupes + }); }); } ); diff --git a/lib/last.fm.js b/lib/last.fm.js new file mode 100644 index 00000000..de81dbfd --- /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: ((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 LastFM({ + api_key: config.lastfm.key + , secret: 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..b3db2b4f --- /dev/null +++ b/lib/soundtrack.js @@ -0,0 +1,182 @@ +var _ = require('underscore'); + +module.exports = { + sortPlaylist: function() { + 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; + } + }) ); + }, + broadcast: function(msg) { + switch (msg.type) { + case 'edit': + for (var i = 0; i < app.room.playlist.length; i++) { + + console.log( 'comparing ' + app.room.playlist[ i ]._id + ' to ' + msg.track._id ); + console.log( app.room.playlist[ i ]._id == msg.track._id ); + + if ( app.room.playlist[ i ]._id.toString() == msg.track._id.toString() ) { + app.room.playlist[ i ].title = msg.track.title; + app.room.playlist[ i ].slug = msg.track.slug; + app.room.playlist[ i ]._artist.name = msg.track._artist.name; + app.room.playlist[ i ]._artist.slug = msg.track._artist.slug; + } + } + break; + } + + var json = JSON.stringify(msg); + for (var id in app.clients) { + app.clients[id].write(json); + } + }, + whisper: function(id, msg) { + var json = JSON.stringify(msg); + app.clients[id].write(json); + }, + markAndSweep: function(){ + app.broadcast({type: 'ping'}); // we should probably not do this globally... instead, start interval after client connect? + var time = (new Date()).getTime(); + app.forEachClient(function(client, id){ + if (client.pongTime < time - app.config.connection.clientTimeout) { + client.close('', 'Timeout'); + // TODO: broadcast part message + + app.broadcast({ + type: 'part' + , data: { + id: id + , _id: (app.clients[id] && app.clients[id].user) ? app.clients[id].user._id : undefined + } + }); + + delete app.clients[id]; + + /*/app.broadcast({ + type: 'part' + , data: { + id: conn.id + } + });/**/ + } + }); + }, + forEachClient: function(fn) { + for (var id in app.clients) { + fn(app.clients[id], id) + } + }, + ensureQueue: function(callback) { + // remove the first track in the playlist... + var lastTrack = app.room.playlist.shift(); + console.log(app.room.playlist.length); + + if (app.room.playlist.length == 0) { + var query = { + _curator: { $exists: true } + , timestamp: { $gte: new Date((Math.floor((new Date()).getTime() / 1000) - 604800) * 1000) } + }; + console.log('!!!!!!!!!!!!!!!!!!!!! QUERY !!!!!!!!!!!!!!!!!!!!!') + console.log( query ); + + Play.find( query ).limit(100).sort('timestamp').exec(function(err, plays) { + if (err || !plays) { + util.getYoutubeVideo( 'dQw4w9WgXcQ‎' , function(track) { + if (track) { backupTracks.push( track.toObject() ); } + callback(); + }); + } + + console.log('plays are ' + plays.length + ' long.'); + + var randomSelection = plays[ _.random(0, plays.length - 1 ) ]; + console.log('random selection is ') + console.log(randomSelection); + + Track.findOne({ _id: randomSelection._track }).populate('_artist').exec(function(err, track) { + + console.log('track is: ') + console.log( track ); + + app.room.playlist.push( _.extend( track , { + score: 0 + , votes: {} + } ) ); + callback(); + }); + }); + } else { + callback(); + } + }, + nextSong: function() { + app.ensureQueue(function() { + app.room.playlist[0].startTime = Date.now(); + app.room.track = app.room.playlist[0]; + + app.redis.set(app.config.database.name + ':playlist', JSON.stringify( app.room.playlist ) ); + + var play = new Play({ + _track: app.room.playlist[0]._id + , _curator: (app.room.playlist[0].curator) ? app.room.playlist[0].curator._id : undefined + }); + play.save(function(err) { + // ...then start the music. + app.startMusic(); + }); + }); + }, + startMusic: function() { + console.log('startMusic() called, current playlist is: ' + JSON.stringify(app.room.playlist)); + + console.log('current playlist lead is...') + console.log( app.room.playlist[0] ) + var firstTrack = app.room.playlist[0]; + + if (!app.room.playlist[0]) { + app.broadcast({ + type: 'announcement' + , data: { + formatted: '
    No tracks in playlist. Please add at least one! Waiting 5 seconds...
    ' + , created: new Date() + } + }); + return setTimeout(app.startMusic, 5000); + } + + var seekTo = (Date.now() - app.room.playlist[0].startTime) / 1000; + app.room.track = app.room.playlist[0]; + + Track.findOne({ _id: app.room.playlist[0]._id }).populate('_artist _artists').lean().exec(function(err, track) { + + console.log('extending...') + console.log( app.room.playlist[0] ) + console.log('with...'); + console.log( track ); + + if (track) { + app.broadcast({ + type: 'track' + , data: _.extend( firstTrack , track ) + , seekTo: seekTo + }); + } else { + console.log('uhhh... broken: ' + app.room.playlist[0].sources['youtube'][0].id + ' and ' +track); + } + }); + + clearTimeout( app.timeout ); + + app.timeout = setTimeout( app.nextSong , (app.room.playlist[0].duration - seekTo) * 1000 ); + + if (app.lastfm) { + app.lastfm.scrobbleActive( app.room.playlist[0] , function() { + console.log('scrobbling complete!'); + }); + } + + } +} \ No newline at end of file diff --git a/public/css/main.css b/public/css/main.css index cbd42263..6039ec30 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -283,8 +283,7 @@ footer { .playlist-controls { float:left; - padding-right: 5px; - padding-left: 5px; + margin-right: 5px; } .active .playlist-controls { visibility: hidden; @@ -308,6 +307,7 @@ footer { text-transform: uppercase; font-weight: bold; font-size: 0.8em; + padding-left: 0.2em; } #playlist-list li.active { display: none; @@ -329,4 +329,67 @@ footer { .fadeout-top { 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))); */ -} \ No newline at end of file +} + +/* 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%; + } + } + + @media (max-width: 979px) and (min-width: 945px) { + .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; + } + + #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; + } +} +/* end le Dizzcode */ diff --git a/public/js/app.js b/public/js/app.js index 1291abd7..513c175a 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -63,10 +63,24 @@ Soundtrack.prototype.editTrackID = function( trackID ) { $editor.find('input[name=artist]').val( track._artist.name ); $editor.find('input[name=title]').val( track.title ); - $editor.find('input.typeahead').typeahead({ + 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 ); + } + }); + + + $editor.find('pre[data-for=titles]').html( titles.join('
    ') ); + + /* $editor.find('input.typeahead').typeahead({ name: 'artists' , remote: '/artists?q=%QUERY' - }); + }); */ $editor.modal(); }); @@ -114,9 +128,9 @@ function updateUserlist() { data.forEach(function(user) { // TODO: use template (Blade?) if (user.role != 'listener') { - $('
  • '+user.username+' '+user.role+'
  • ').appendTo('#userlist'); + $('
  • '+user.username+' '+user.role+'
  • ').appendTo('#userlist'); } else { - $('
  • '+user.username+'
  • ').appendTo('#userlist'); + $('
  • '+user.username+'
  • ').appendTo('#userlist'); } }); @@ -145,6 +159,30 @@ function toggleVideoOff() { 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){ @@ -242,6 +280,9 @@ $(window).load(function(){ switch (msg.type) { default: console.log('unhandled message: ' + msg); break; + case 'edit': + updatePlaylist(); + break; case 'track': updatePlaylist(); @@ -680,8 +721,10 @@ $(window).load(function(){ $(document).on('click', '*[data-action=skip-track]', function(e) { e.preventDefault(); - soundtrack.player.pause(); - $.post('/skip'); + if (confirm("Are you sure you want to skip?")) { + soundtrack.player.pause(); + $.post('/skip'); + } return false; }); diff --git a/soundtrack.js b/soundtrack.js index 844aafb8..a7526942 100644 --- a/soundtrack.js +++ b/soundtrack.js @@ -2,7 +2,7 @@ var config = require('./config') , database = require('./db') , util = require('./util') , express = require('express') - , app = express() +// , app = express() , sys = require('sys') , http = require('http') , rest = require('restler') @@ -25,6 +25,8 @@ var config = require('./config') , marked = require('marked') , validator = require('validator'); +app = express(); + app.set('views', __dirname + '/views'); app.set('view engine', 'jade'); app.set('strict routing', true); @@ -107,46 +109,11 @@ if (config.lastfm && config.lastfm.key && config.lastfm.secret) { api_key: config.lastfm.key , secret: config.lastfm.secret }); - app.get('/auth/lastfm', function(req, res) { - //var authUrl = lastfm.getAuthenticationUrl({ cb: ((config.app.safe) ? 'http://' : 'http://') + config.app.host + '/auth/lastfm/callback' }); - var authUrl = lastfm.getAuthenticationUrl({ cb: ((config.app.safe) ? 'http://' : 'http://') + 'soundtrack.io/auth/lastfm/callback' }); - res.redirect(authUrl); - }); - app.get('/auth/lastfm/callback', 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('/'); - }); - - }); - - }); - }); + app.lastfm = require('./lib/last.fm.js'); + + app.get('/auth/lastfm', app.lastfm.authSetup ); + app.get('/auth/lastfm/callback', app.lastfm.authCallback ); } var auth = require('./controllers/auth') @@ -191,236 +158,26 @@ var server = http.createServer(app); app.clients = {}; var backupTracks = []; -app.redis = redis.createClient(); -app.redis.get(config.database.name + ':playlist', function(err, playlist) { - playlist = JSON.parse(playlist); - - if (!playlist || !playlist.length) { - playlist = []; - } - - app.room = { - track: undefined - , playlist: playlist - , listeners: {} - }; - - startMusic(); - -}); app.socketAuthTokens = []; -app.broadcast = function(msg) { - var json = JSON.stringify(msg); - for (var id in app.clients) { - app.clients[id].write(json); - } -}; +app.config = config; -app.whisper = function(id, msg) { - var json = JSON.stringify(msg); - app.clients[id].write(json); -} -app.markAndSweep = function(){ - app.broadcast({type: 'ping'}); // we should probably not do this globally... instead, start interval after client connect? - var time = (new Date()).getTime(); - app.forEachClient(function(client, id){ - if (client.pongTime < time - config.connection.clientTimeout) { - client.close('', 'Timeout'); - // TODO: broadcast part message +var soundtrackController = require('./lib/soundtrack'); - app.broadcast({ - type: 'part' - , data: { - id: id - , _id: (app.clients[id] && app.clients[id].user) ? app.clients[id].user._id : undefined - } - }); +app.sortPlaylist = soundtrackController.sortPlaylist; - delete app.clients[id]; +app.broadcast = soundtrackController.broadcast; +app.whisper = soundtrackController.whisper; +app.markAndSweep = soundtrackController.markAndSweep; +app.forEachClient = soundtrackController.forEachClient; - /*/app.broadcast({ - type: 'part' - , data: { - id: conn.id - } - });/**/ - } - }); -} +app.ensureQueue = soundtrackController.ensureQueue; +app.nextSong = soundtrackController.nextSong; +app.startMusic = soundtrackController.startMusic; setInterval(app.markAndSweep, config.connection.checkInterval); -app.forEachClient = function(fn) { - for (var id in app.clients) { - fn(app.clients[id], id) - } -} - -function ensureQueue(callback) { - // remove the first track in the playlist... - var lastTrack = app.room.playlist.shift(); - console.log(app.room.playlist.length); - - if (app.room.playlist.length == 0) { - var query = { - _curator: { $exists: true } - , timestamp: { $gte: new Date((Math.floor((new Date()).getTime() / 1000) - 604800) * 1000) } - }; - console.log('!!!!!!!!!!!!!!!!!!!!! QUERY !!!!!!!!!!!!!!!!!!!!!') - console.log( query ); - - Play.find( query ).limit(100).sort('timestamp').exec(function(err, plays) { - if (err || !plays) { - util.getYoutubeVideo( 'dQw4w9WgXcQ‎' , function(track) { - if (track) { backupTracks.push( track.toObject() ); } - callback(); - }); - } - - console.log('plays are ' + plays.length + ' long.'); - - var randomSelection = plays[ _.random(0, plays.length - 1 ) ]; - console.log('random selection is ') - console.log(randomSelection); - - Track.findOne({ _id: randomSelection._track }).populate('_artist').exec(function(err, track) { - - console.log('track is: ') - console.log( track ); - - app.room.playlist.push( _.extend( track , { - score: 0 - , votes: {} - } ) ); - callback(); - }); - }); - } else { - callback(); - } -} - -function nextSong() { - ensureQueue(function() { - app.room.playlist[0].startTime = Date.now(); - app.room.track = app.room.playlist[0]; - - app.redis.set(config.database.name + ':playlist', JSON.stringify( app.room.playlist ) ); - - var play = new Play({ - _track: app.room.playlist[0]._id - , _curator: (app.room.playlist[0].curator) ? app.room.playlist[0].curator._id : undefined - }); - play.save(function(err) { - // ...then start the music. - startMusic(); - }); - }); -} - -function startMusic() { - console.log('startMusic() called, current playlist is: ' + JSON.stringify(app.room.playlist)); - - console.log('current playlist lead is...') - console.log( app.room.playlist[0] ) - var firstTrack = app.room.playlist[0]; - - if (!app.room.playlist[0]) { - app.broadcast({ - type: 'announcement' - , data: { - formatted: '
    No tracks in playlist. Please add at least one! Waiting 5 seconds...
    ' - , created: new Date() - } - }); - return setTimeout(startMusic, 5000); - } - - var seekTo = (Date.now() - app.room.playlist[0].startTime) / 1000; - app.room.track = app.room.playlist[0]; - - Track.findOne({ _id: app.room.playlist[0]._id }).populate('_artist _artists').lean().exec(function(err, track) { - - console.log('extending...') - console.log( app.room.playlist[0] ) - console.log('with...'); - console.log( track ); - - if (track) { - app.broadcast({ - type: 'track' - , data: _.extend( firstTrack , track ) - , seekTo: seekTo - }); - } else { - console.log('uhhh... broken: ' + app.room.playlist[0].sources['youtube'][0].id + ' and ' +track); - } - }); - - clearTimeout( app.timeout ); - - app.timeout = setTimeout( nextSong , (app.room.playlist[0].duration - seekTo) * 1000 ); - - scrobbleActive( app.room.playlist[0] , function() { - console.log('scrobbling complete!'); - }); - -} - -function scrobbleActive(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 LastFM({ - api_key: config.lastfm.key - , secret: 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(); - }); - }); - }); - }); - -} - -function sortPlaylist() { - 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; - } - }) ); -} - app.post('/skip', /*/requireLogin,/**/ function(req, res) { console.log('skip received:'); console.log(req.user); @@ -443,7 +200,7 @@ app.post('/skip', /*/requireLogin,/**/ function(req, res) { } ); - nextSong(); + app.nextSong(); res.send({ status: 'success' }); }); @@ -470,7 +227,7 @@ async.parallel([ }); } ], function(err, trackLists) { - //nextSong(); + //app.nextSong(); }); sock.on('connection', function(conn) { @@ -511,6 +268,7 @@ sock.on('connection', function(conn) { , username: matches[0].user.username , id: conn.id , role: (matches[0].user.roles && matches[0].user.roles.indexOf('editor') >= 0) ? 'editor' : 'listener' + , avatar: matches[0].user.avatar }; app.broadcast({ @@ -641,7 +399,7 @@ app.post('/playlist/:trackID', requireLogin, function(req, res, next) { console.log('track score: ' + app.room.playlist[ index].score); console.log('track votes: ' + JSON.stringify(app.room.playlist[ index].votes)); - sortPlaylist(); + app.sortPlaylist(); app.broadcast({ type: 'playlist:update' @@ -684,7 +442,7 @@ function queueTrack(track, curator, queueCallback) { } } ) ); - sortPlaylist(); + app.sortPlaylist(); app.redis.set(config.database.name + ':playlist', JSON.stringify( app.room.playlist ) ); @@ -814,8 +572,26 @@ function getTop100FromCodingSoundtrack(done) { }); } -server.listen(config.app.port, function(err) { - console.log('Listening on port ' + config.app.port + ' for HTTP'); - console.log('Must have redis listening on port 6379'); - console.log('Must have mongodb listening on port 27017'); +app.redis = redis.createClient(); +app.redis.get(config.database.name + ':playlist', function(err, playlist) { + playlist = JSON.parse(playlist); + + if (!playlist || !playlist.length) { + playlist = []; + } + + app.room = { + track: undefined + , playlist: playlist + , listeners: {} + }; + + server.listen(config.app.port, function(err) { + console.log('Listening on port ' + config.app.port + ' for HTTP'); + console.log('Must have redis listening on port 6379'); + console.log('Must have mongodb listening on port 27017'); + + app.startMusic(); + }); + }); \ No newline at end of file diff --git a/views/artist.jade b/views/artist.jade index 0226b8be..3d4dad7e 100644 --- a/views/artist.jade +++ b/views/artist.jade @@ -11,5 +11,11 @@ block content ul for track in tracks li + a(href="/#{track._artist.slug}") #{track._artist.name} + | — a(href="/#{artist.slug}/#{track.slug}/#{track._id}") #{track.title} - | played #{track.plays} times \ No newline at end of file + | played #{track.plays} times + if (user && user.roles.indexOf('editor') >= 0) + | + a.btn.btn-mini(href="#", data-action="launch-track-editor", data-track-id="#{track._id}") + | edit \ No newline at end of file diff --git a/views/index.jade b/views/index.jade index 0193cc5b..bb6927f4 100644 --- a/views/index.jade +++ b/views/index.jade @@ -50,11 +50,20 @@ block content .score.badge {{track.score}} i.icon-chevron-up.icon-white(data-action="upvote-track", data-track-id="{{track._id}}") i.icon-chevron-down.icon-white(data-action="downvote-track", data-track-id="{{track._id}}") - img.thumbnail-medium.pull-left(ng-src="{{track.images.thumbnail.url}}") - span(style="font-size: 0.75em;") {{track.curator.username}} wants to play - a(href="/{{track._artist.slug}}/{{track.slug}}/{{track._id}}") - small.pull-right {{track.duration.toString().toHHMMSS()}} - span {{track._artist.name}} — {{track.title}} + if (user && user.roles.indexOf('editor') >= 0) + .editor-controls + a.btn.btn-mini(href="#", data-action="launch-track-editor", data-track-id="{{track._id}}") + | edit + div(style="font-size: 0.75em;") + span.pull-right {{track.duration | toHHMMSS }} + a(href="{{track.curator.slug}}") {{track.curator.username}} + | wants to play + .track-details(style="font-size: 1.1em;") + img.thumbnail-medium.pull-left(ng-src="{{track.images.thumbnail.url}}") + a(href="/{{track._artist.slug}}/{{track.slug}}/{{track._id}}") {{track.title}} + .track-details.text-right + | by + a(href="/{{track._artist.slug}}") {{track._artist.name}} h5 Add Track form#search-form.input-append(style="display:block;", data-for="track-search") diff --git a/views/layout.jade b/views/layout.jade index 4b1e8aeb..76ffcaa6 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -1,5 +1,5 @@ !!! 5 -html(lang="en-US", ng-app) +html(lang="en-US", ng-app="soundtrack-io") head title soundtrack.io | music for all. @@ -31,17 +31,17 @@ html(lang="en-US", ng-app) script(src="/js/app.js") if (user) - script + script. window.pendo_options = { apiKey: 'c4a237f5-e4e2-40eb-4a79-5a96fdc45d82', visitorId: '#{user._id}' // NOTE: THIS IS OPTIONAL. SEE IDENTIFY }; else - script + script. window.pendo_options = { apiKey: 'c4a237f5-e4e2-40eb-4a79-5a96fdc45d82' }; - script + script. (function() { var script = document.createElement('script'); script.type = 'text/javascript'; @@ -137,7 +137,7 @@ html(lang="en-US", ng-app) input(type="checkbox", data-action="toggle-scrobble", checked=user.preferences.scrobble) | Enable Scrobbling else - a.btn.btn-danger(href="/auth/lastfm") Enable Scrobbling » + a.btn.btn-danger(href="/auth/lastfm") Enable Scrobbling » h4 Content label(title="Prefer SFW videos / audio sources for the currently playing track.") @@ -191,6 +191,10 @@ html(lang="en-US", ng-app) //-.row-fluid input.span3(type="number", name="startTime") input.span3(type="number", name="endTime") + + label(for="titles") Known Titles (for easy copypasta) + pre(data-for="titles") + .modal-footer button.btn.btn(data-dismiss="modal") Cancel button.btn.btn-primary.btn-large(type="submit") Save Changes » \ No newline at end of file diff --git a/views/people.jade b/views/people.jade index d7600b66..48c315e0 100644 --- a/views/people.jade +++ b/views/people.jade @@ -17,4 +17,4 @@ block content if (person.profiles && person.profiles.lastfm && person.profiles.lastfm.username) | a(href="http://last.fm/user/#{person.profiles.lastfm.username}") - img(src="http://last.fm/favicon.ico", title="#{person.profiles.lastfm.username}") \ No newline at end of file + img(src="/img/last.fm.ico", title="#{person.profiles.lastfm.username}") \ No newline at end of file diff --git a/views/person.jade b/views/person.jade index c448f259..dd4f8725 100644 --- a/views/person.jade +++ b/views/person.jade @@ -2,20 +2,26 @@ extends layout block content .well.content-well - - if (typeof(user) != 'undefined' && user._id.toString() == person._id.toString()) + if (typeof(user) != 'undefined' && user._id.toString() == person._id.toString()) .pull-right a.btn(data-action="enable-profile-editor") EDIT! if (!person.profiles || !person.profiles.lastfm || !person.profiles.lastfm.username) a.btn.btn-mini(href="/auth/lastfm") add last.fm » form#profile-editor(action="/#{user.slug}", method="post", style="display:none;") - textarea.input-block-level(name="bio", rows="3") #{person.bio} + h4 Bio (markdown supported) + textarea.input-block-level(name="bio", rows="5") #{person.bio} + + h4 Email Address (used for avatars, using gravatar) + input.input-block-level(name="email", value="#{((person.email) ? person.email : '')}") + button.btn.btn-primary(type="submit") Make Changes » + img.avatar-large.pull-left(src="#{person.avatar.url}") h1 #{person.username} if (person.profiles && person.profiles.lastfm && person.profiles.lastfm.username) a(href="http://last.fm/user/#{person.profiles.lastfm.username}") - img(src="http://last.fm/favicon.ico", title="#{person.profiles.lastfm.username}") + img(src="/img/last.fm.ico", title="#{person.profiles.lastfm.username}") .bio | !{marked(person.bio)} diff --git a/views/register.jade b/views/register.jade index 23b40508..40667599 100644 --- a/views/register.jade +++ b/views/register.jade @@ -13,4 +13,4 @@ block content label(for="passwordAgain") Password (again) input(type="password", name="passwordAgain") - input.btn.btn-primary(type="submit", value="Le Register") + input.btn.btn-primary(type="submit", value="Le Register") \ No newline at end of file diff --git a/views/track.jade b/views/track.jade index 2a9c4ed8..017e26e6 100644 --- a/views/track.jade +++ b/views/track.jade @@ -46,7 +46,18 @@ block content each message in chats include partials/message - + h3 Possible Sources + ul + each source in dupes + li + code #{source} + + each source in dupes + if (source.sources.youtube && source.sources.youtube.length) + iframe(width="540", height="295", src="https://www.youtube.com/embed/#{source.sources.youtube[0].id}?enablejsapi=1", frameborder="0") + if (source.sources.soundcloud && source.sources.soundcloud.length) + iframe(width="100%", height="166", src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/#{source.sources.soundcloud[0].id}", frameborder="0") + block scripts style .chart rect { From 8b368e13cc35780c37b84cda4bf75ac130f78bf7 Mon Sep 17 00:00:00 2001 From: DecisiveDesign Date: Tue, 11 Mar 2014 18:37:57 -0500 Subject: [PATCH 003/352] Adding old responsive tweaks to actual repo Conflicts: public/css/main.css --- public/css/main.css | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/public/css/main.css b/public/css/main.css index 6039ec30..e491ee31 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -158,10 +158,16 @@ footer { } @media (max-width: 979px) { - body { - padding-top: 0px; - padding-bottom: 40px; - } + body {padding:0;} + + .container { + width: 100%; + margin-left:0; + margin-right:0; + } + + footer {margin:0;} + #chat-form .input-block-level { width: 275px; } @@ -331,6 +337,9 @@ footer { /* 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 #### */ +/* ######################################### */ /* begin le Dizzcode */ @media (max-width:1199px) and (min-width:980px){ #chat-form .input-block-level { From 959c504bd01e13ab235b19a1c7b6d93541864f4a Mon Sep 17 00:00:00 2001 From: Eric Martindale Date: Wed, 12 Mar 2014 17:30:11 +0000 Subject: [PATCH 004/352] Bugfixes. --- lib/last.fm.js | 8 +++---- lib/soundtrack.js | 46 ++++++++++++++++++++++++++++++++++++++++ public/js/app.js | 2 +- soundtrack.js | 53 ++++++----------------------------------------- util.js | 31 +++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 52 deletions(-) diff --git a/lib/last.fm.js b/lib/last.fm.js index de81dbfd..d7a25731 100644 --- a/lib/last.fm.js +++ b/lib/last.fm.js @@ -3,7 +3,7 @@ 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: ((config.app.safe) ? 'http://' : 'http://') + 'soundtrack.io/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) { @@ -56,9 +56,9 @@ module.exports = { console.log('listener available:'); console.log(user); - var lastfm = new LastFM({ - api_key: config.lastfm.key - , secret: config.lastfm.secret + var lastfm = new app.LastFM({ + api_key: app.config.lastfm.key + , secret: app.config.lastfm.secret }); var creds = { diff --git a/lib/soundtrack.js b/lib/soundtrack.js index b3db2b4f..8bdbff49 100644 --- a/lib/soundtrack.js +++ b/lib/soundtrack.js @@ -1,4 +1,5 @@ var _ = require('underscore'); +var util = require('../util'); module.exports = { sortPlaylist: function() { @@ -69,6 +70,49 @@ module.exports = { fn(app.clients[id], id) } }, + queueTrack: function(track, curator, queueCallback) { + Track.findOne({ _id: track._id }).populate('_artist _credits').exec(function(err, realTrack) { + + var playlistItem = realTrack.toObject(); + + playlistItem._artist = { + _id: playlistItem._artist._id + , name: playlistItem._artist.name + , slug: playlistItem._artist.slug + }; + + for (var source in playlistItem.sources) { + console.log(source); + console.log(playlistItem.sources[ source ]); + for (var i = 0; i Date: Wed, 12 Mar 2014 21:54:12 +0000 Subject: [PATCH 005/352] Change to class structure. --- README.md | 5 + lib/soundtrack.js | 481 ++++++++++++++++++++++++++++------------------ soundtrack.js | 95 +++++---- util.js | 14 +- 4 files changed, 368 insertions(+), 227 deletions(-) diff --git a/README.md b/README.md index de61b680..39a589bb 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,11 @@ 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 [Fork. Commit. Pull request.](https://help.github.com/articles/fork-a-repo) diff --git a/lib/soundtrack.js b/lib/soundtrack.js index 8bdbff49..249f54ea 100644 --- a/lib/soundtrack.js +++ b/lib/soundtrack.js @@ -1,228 +1,341 @@ var _ = require('underscore'); var util = require('../util'); -module.exports = { - sortPlaylist: function() { - 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; - } - }) ); - }, - broadcast: function(msg) { - switch (msg.type) { - case 'edit': - for (var i = 0; i < app.room.playlist.length; i++) { - - console.log( 'comparing ' + app.room.playlist[ i ]._id + ' to ' + msg.track._id ); - console.log( app.room.playlist[ i ]._id == msg.track._id ); - - if ( app.room.playlist[ i ]._id.toString() == msg.track._id.toString() ) { - app.room.playlist[ i ].title = msg.track.title; - app.room.playlist[ i ].slug = msg.track.slug; - app.room.playlist[ i ]._artist.name = msg.track._artist.name; - app.room.playlist[ i ]._artist.slug = msg.track._artist.slug; - } - } - break; +var Soundtrack = function(app) { + //this.app = app; + this.rooms = {}; + //setInterval( this.markAndSweep, app.config.connection.checkInterval ); +} + +var Room = function() { + +} + +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) { + case 'edit': + for (var i = 0; i < app.room.playlist.length; i++) { + + console.log( 'comparing ' + app.room.playlist[ i ]._id + ' to ' + msg.track._id ); + console.log( app.room.playlist[ i ]._id == msg.track._id ); + + if ( app.room.playlist[ i ]._id.toString() == msg.track._id.toString() ) { + app.room.playlist[ i ].title = msg.track.title; + app.room.playlist[ i ].slug = msg.track.slug; + app.room.playlist[ i ]._artist.name = msg.track._artist.name; + app.room.playlist[ i ]._artist.slug = msg.track._artist.slug; + } + } + break; + } - var json = JSON.stringify(msg); - for (var id in app.clients) { - app.clients[id].write(json); - } - }, - whisper: function(id, msg) { - var json = JSON.stringify(msg); + var json = JSON.stringify(msg); + for (var id in app.clients) { app.clients[id].write(json); - }, - markAndSweep: function(){ - app.broadcast({type: 'ping'}); // we should probably not do this globally... instead, start interval after client connect? - var time = (new Date()).getTime(); - app.forEachClient(function(client, id){ - if (client.pongTime < time - app.config.connection.clientTimeout) { - client.close('', 'Timeout'); - // TODO: broadcast part message - - app.broadcast({ - type: 'part' - , data: { - id: id - , _id: (app.clients[id] && app.clients[id].user) ? app.clients[id].user._id : undefined - } - }); + } +}; +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'); + // TODO: broadcast part message + + self.broadcast({ + type: 'part' + , data: { + id: id + , _id: (app.clients[id] && app.clients[id].user) ? app.clients[id].user._id : undefined + } + }); - delete app.clients[id]; + delete app.clients[id]; - /*/app.broadcast({ - type: 'part' - , data: { - id: conn.id - } - });/**/ + /*/self.broadcast({ + type: 'part' + , data: { + id: conn.id + } + });/**/ + } + }); +}; +Soundtrack.prototype.forEachClient = function(fn) {235 + var self = this; + for (var id in app.clients) { + fn(app.clients[id], id) + } +}; +Soundtrack.prototype.queueTrack = function(track, curator, queueCallback) { + var self = this; + var app = this.app; + Track.findOne({ _id: track._id }).populate('_artist _credits').exec(function(err, realTrack) { + + var playlistItem = realTrack.toObject(); + + playlistItem._artist = { + _id: playlistItem._artist._id + , name: playlistItem._artist.name + , slug: playlistItem._artist.slug + }; + + for (var source in playlistItem.sources) { + console.log(source); + console.log(playlistItem.sources[ source ]); + for (var i = 0; iNo tracks in playlist. Please add at least one! Waiting 5 seconds...' + , created: new Date() + } + }); + return setTimeout(app.startMusic, 5000); + } - console.log('track is: ') - console.log( track ); + var seekTo = (Date.now() - app.room.playlist[0].startTime) / 1000; + app.room.track = app.room.playlist[0]; - app.room.playlist.push( _.extend( track , { - score: 0 - , votes: {} - } ) ); - callback(); - }); + Track.findOne({ _id: app.room.playlist[0]._id }).populate('_artist _artists').lean().exec(function(err, track) { + if (track) { + self.broadcast({ + type: 'track' + , data: _.extend( firstTrack , track ) + , seekTo: seekTo }); } else { - callback(); + console.log('uhhh... broken: ' + app.room.playlist[0].sources['youtube'][0].id + ' and ' +track); } - }, - nextSong: function() { - app.ensureQueue(function() { - app.room.playlist[0].startTime = Date.now(); - app.room.track = app.room.playlist[0]; + }); - app.redis.set(app.config.database.name + ':playlist', JSON.stringify( app.room.playlist ) ); + clearTimeout( self.timeout ); - var play = new Play({ - _track: app.room.playlist[0]._id - , _curator: (app.room.playlist[0].curator) ? app.room.playlist[0].curator._id : undefined - }); - play.save(function(err) { - // ...then start the music. - app.startMusic(); - }); + self.timeout = setTimeout(function() { + self.nextSong + }, (app.room.playlist[0].duration - seekTo) * 1000 ); + + if (app.lastfm) { + app.lastfm.scrobbleActive( app.room.playlist[0] , function() { + console.log('scrobbling complete!'); }); - }, - startMusic: function() { - console.log('startMusic() called, current playlist is: ' + JSON.stringify(app.room.playlist)); + } - console.log('current playlist lead is...') - console.log( app.room.playlist[0] ) - var firstTrack = app.room.playlist[0]; +}; - if (!app.room.playlist[0]) { - app.broadcast({ - type: 'announcement' - , data: { - formatted: '
    No tracks in playlist. Please add at least one! Waiting 5 seconds...
    ' - , created: new Date() - } - }); - return setTimeout(app.startMusic, 5000); - } +Soundtrack.prototype.lastfmAuthSetup = function(req, res) { + var self = this; + var app = this.app; - var seekTo = (Date.now() - app.room.playlist[0].startTime) / 1000; - app.room.track = app.room.playlist[0]; + //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); +}; +Soundtrack.prototype.lastfmAuthCallback = function(req, res) { + var self = this; + var app = this.app; - Track.findOne({ _id: app.room.playlist[0]._id }).populate('_artist _artists').lean().exec(function(err, track) { + lastfm.authenticate( req.param('token') , function(err, session) { + console.log(session); - console.log('extending...') - console.log( app.room.playlist[0] ) - console.log('with...'); - console.log( track ); + if (err) { + console.log(err); + req.flash('error', 'Something went wrong with authentication.'); + return res.redirect('/'); + } - if (track) { - app.broadcast({ - type: 'track' - , data: _.extend( firstTrack , track ) - , seekTo: seekTo - }); - } else { - console.log('uhhh... broken: ' + app.room.playlist[0].sources['youtube'][0].id + ' and ' +track); + 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 ' }); } - }); - clearTimeout( app.timeout ); + 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('/'); + }); + + }); - app.timeout = setTimeout( app.nextSong , (app.room.playlist[0].duration - seekTo) * 1000 ); + }); +}; +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:'); + console.log(user); + + var lastfm = new app.LastFM({ + api_key: app.config.lastfm.key + , secret: app.config.lastfm.secret + }); - if (app.lastfm) { - app.lastfm.scrobbleActive( app.room.playlist[0] , function() { - console.log('scrobbling complete!'); + 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 +module.exports = Soundtrack; diff --git a/soundtrack.js b/soundtrack.js index 8b395397..d1da721c 100644 --- a/soundtrack.js +++ b/soundtrack.js @@ -2,7 +2,7 @@ var config = require('./config') , database = require('./db') , util = require('./util') , express = require('express') -// , app = express() + , app = express() , sys = require('sys') , http = require('http') , rest = require('restler') @@ -25,8 +25,6 @@ var config = require('./config') , marked = require('marked') , validator = require('validator'); -app = express(); - app.set('views', __dirname + '/views'); app.set('view engine', 'jade'); app.set('strict routing', true); @@ -104,20 +102,6 @@ String.prototype.capitalize = function(){ return this.replace( /(^|\s)([a-z])/g , function(m,p1,p2){ return p1+p2.toUpperCase(); } ); }; -if (config.lastfm && config.lastfm.key && config.lastfm.secret) { - - app.LastFM = LastFM; - var lastfm = new LastFM({ - api_key: config.lastfm.key - , secret: config.lastfm.secret - }); - - app.lastfm = require('./lib/last.fm.js'); - - app.get('/auth/lastfm', app.lastfm.authSetup ); - app.get('/auth/lastfm/callback', app.lastfm.authCallback ); -} - var auth = require('./controllers/auth') , pages = require('./controllers/pages') , people = require('./controllers/people') @@ -140,6 +124,7 @@ function requireLogin(req, res, next) { function authorize(role) { switch (role) { case 'editor': + case 'admin': return function(req, res, next) { if (!req.user || !req.user.roles || req.user.roles.indexOf( role ) == -1) { res.status(401).send({ @@ -164,22 +149,36 @@ app.socketAuthTokens = []; app.config = config; +var Soundtrack = require('./lib/soundtrack'); +var soundtrack = new Soundtrack(app); +soundtrack.app = app; -var soundtrackController = require('./lib/soundtrack'); +/*/ +app.sortPlaylist = soundtrack.sortPlaylist; -app.sortPlaylist = soundtrackController.sortPlaylist; +soundtrack.broadcast = soundtrack.broadcast; +app.whisper = soundtrack.whisper; +app.markAndSweep = soundtrack.markAndSweep; +app.forEachClient = soundtrack.forEachClient; -app.broadcast = soundtrackController.broadcast; -app.whisper = soundtrackController.whisper; -app.markAndSweep = soundtrackController.markAndSweep; -app.forEachClient = soundtrackController.forEachClient; +app.queueTrack = soundtrack.queueTrack; +app.ensureQueue = soundtrack.ensureQueue; +app.nextSong = soundtrack.nextSong; +app.startMusic = soundtrack.startMusic; +/**/ -app.queueTrack = soundtrackController.queueTrack; -app.ensureQueue = soundtrackController.ensureQueue; -app.nextSong = soundtrackController.nextSong; -app.startMusic = soundtrackController.startMusic; -setInterval(app.markAndSweep, config.connection.checkInterval); +if (config.lastfm && config.lastfm.key && config.lastfm.secret) { + + app.LastFM = LastFM; + var lastfm = new LastFM({ + api_key: config.lastfm.key + , secret: config.lastfm.secret + }); + + app.get('/auth/lastfm', soundtrack.lastfmAuthSetup ); + app.get('/auth/lastfm/callback', soundtrack.lastfmAuthCallback ); +} app.post('/skip', /*/requireLogin,/**/ function(req, res) { console.log('skip received:'); @@ -193,7 +192,7 @@ app.post('/skip', /*/requireLogin,/**/ function(req, res) { , created: new Date() } }, function(err, html) { - app.broadcast({ + soundtrack.broadcast({ type: 'announcement' , data: { formatted: html @@ -203,7 +202,7 @@ app.post('/skip', /*/requireLogin,/**/ function(req, res) { } ); - app.nextSong(); + soundtrack.nextSong(); res.send({ status: 'success' }); }); @@ -265,7 +264,7 @@ sock.on('connection', function(conn) { // TODO: strip salt, hash, etc. // We do this on /listeners.json, but if nothing else, we save memory. - app.room.listeners[ matches[0].user._id ] = { + soundtrack.app.room.listeners[ matches[0].user._id ] = { _id: matches[0].user._id , slug: matches[0].user.slug , username: matches[0].user.username @@ -274,7 +273,7 @@ sock.on('connection', function(conn) { , avatar: matches[0].user.avatar }; - app.broadcast({ + soundtrack.broadcast({ type: 'join' , data: { _id: matches[0].user._id @@ -316,7 +315,7 @@ sock.on('connection', function(conn) { }; } - /*app.broadcast({ + /*soundtrack.broadcast({ type: 'part' , data: { id: conn.id @@ -364,7 +363,7 @@ app.post('/chat', requireLogin, function(req, res) { , _track: app.room.playlist[0] } }, function(err, html) { - app.broadcast({ + soundtrack.broadcast({ type: 'chat' , data: { _id: chat._id @@ -384,6 +383,23 @@ app.post('/chat', requireLogin, function(req, res) { }); }); +app.del('/playlist/:trackID', requireLogin, authorize('admin'), function(req, res, next) { + if (!req.param('index') || req.param('index') == 0) { return next(); } + + app.room.playlist.splice( req.param('index') , 1 ); + app.sortPlaylist(); + app.redis.set( app.config.database.name + ':playlist', JSON.stringify( app.room.playlist ) ); + + soundtrack.broadcast({ + type: 'playlist:update' + }); + + res.send({ + status: 'success' + }); + +}); + app.post('/playlist/:trackID', requireLogin, function(req, res, next) { var playlistMap = app.room.playlist.map(function(x) { @@ -404,7 +420,7 @@ app.post('/playlist/:trackID', requireLogin, function(req, res, next) { app.sortPlaylist(); - app.broadcast({ + soundtrack.broadcast({ type: 'playlist:update' }); @@ -417,7 +433,7 @@ app.post('/playlist/:trackID', requireLogin, function(req, res, next) { app.post('/playlist', requireLogin, function(req, res) { util.trackFromSource( req.param('source') , req.param('id') , function(err, track) { if (!err && track) { - app.queueTrack(track, req.user, function() { + soundtrack.queueTrack(track, req.user, function() { res.send({ status: 'success', message: 'Track added successfully!' }); }); } else { @@ -550,7 +566,12 @@ app.redis.get(config.database.name + ':playlist', function(err, playlist) { console.log('Must have redis listening on port 6379'); console.log('Must have mongodb listening on port 27017'); - app.startMusic(); + soundtrack.startMusic(function(err, track) { + /*/var seekTo = (Date.now() - app.room.playlist[0].startTime) / 1000; + + clearTimeout( soundtrack.timeout ); + soundtrack.timeout = setTimeout( soundtrack.nextSong , (app.room.playlist[0].duration - seekTo) * 1000 ); /**/ + }); }); }); \ No newline at end of file diff --git a/util.js b/util.js index 06766b45..c64d7025 100644 --- a/util.js +++ b/util.js @@ -104,12 +104,16 @@ String.prototype.capitalize = function(){ }; module.exports = { - timeSeries: function(field, interval) { + timeSeries: function(field, interval, skip, limit) { var queries = []; if (!interval) { var interval = 24; } + if (!skip) { var skip = 1; } + if (!limit) { var limit = 24; } - for (var d = 0; d <= 29; d++) { + var halfTime = interval / 2; // moving window + + for (var i = 0; i <= limit; i++) { var start = new Date(); start.setHours('0'); @@ -119,8 +123,8 @@ module.exports = { var end = new Date( start.getTime() ); - start = new Date( start - (d * 1000 * 60 * 60 * interval) ); - end = new Date( start.getTime() + 1000 * 60 * 60 * interval ); + start = new Date( start - ((i+1) * 1000 * 60 * 60 * 24) ); + end = new Date( start.getTime() + interval ); var query = {}; query[ field ] = { @@ -131,8 +135,6 @@ module.exports = { queries.push( query ); } - console.log(queries); - return queries; }, getYoutubeVideo: function(videoID, internalCallback) { From 1fd5fb385dbe391699e29bf1d71605d067598dc7 Mon Sep 17 00:00:00 2001 From: Eric Martindale Date: Wed, 12 Mar 2014 22:03:58 +0000 Subject: [PATCH 006/352] Fix crash on track voting. --- soundtrack.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soundtrack.js b/soundtrack.js index d1da721c..9ec988af 100644 --- a/soundtrack.js +++ b/soundtrack.js @@ -418,7 +418,7 @@ app.post('/playlist/:trackID', requireLogin, function(req, res, next) { console.log('track score: ' + app.room.playlist[ index].score); console.log('track votes: ' + JSON.stringify(app.room.playlist[ index].votes)); - app.sortPlaylist(); + soundtrack.sortPlaylist(); soundtrack.broadcast({ type: 'playlist:update' From 677801e5a3ed6f96629509afbc9256c794c20f0d Mon Sep 17 00:00:00 2001 From: Eric Martindale Date: Wed, 12 Mar 2014 22:19:04 +0000 Subject: [PATCH 007/352] Fix track advancement bug. Thanks, @ging. --- lib/soundtrack.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/soundtrack.js b/lib/soundtrack.js index 249f54ea..88d69f2d 100644 --- a/lib/soundtrack.js +++ b/lib/soundtrack.js @@ -235,9 +235,8 @@ Soundtrack.prototype.startMusic = function() { }); clearTimeout( self.timeout ); - self.timeout = setTimeout(function() { - self.nextSong + self.nextSong(); }, (app.room.playlist[0].duration - seekTo) * 1000 ); if (app.lastfm) { From fa81f9e82ef5e40aaca19eaa8cf214f56a8c9daf Mon Sep 17 00:00:00 2001 From: Eric Martindale Date: Wed, 12 Mar 2014 22:40:03 +0000 Subject: [PATCH 008/352] Various bugfixes. --- controllers/tracks.js | 2 +- lib/soundtrack.js | 2 +- soundtrack.js | 10 ++++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/controllers/tracks.js b/controllers/tracks.js index c74fa7d2..994064a3 100644 --- a/controllers/tracks.js +++ b/controllers/tracks.js @@ -46,7 +46,7 @@ module.exports = { track = track.toObject(); track._artist = artist; - req.app.broadcast({ + req.soundtrack.broadcast({ type: 'edit' , track: track }); diff --git a/lib/soundtrack.js b/lib/soundtrack.js index 88d69f2d..476db94f 100644 --- a/lib/soundtrack.js +++ b/lib/soundtrack.js @@ -83,7 +83,7 @@ Soundtrack.prototype.markAndSweep = function(){ } }); }; -Soundtrack.prototype.forEachClient = function(fn) {235 +Soundtrack.prototype.forEachClient = function(fn) { var self = this; for (var id in app.clients) { fn(app.clients[id], id) diff --git a/soundtrack.js b/soundtrack.js index 9ec988af..96b82804 100644 --- a/soundtrack.js +++ b/soundtrack.js @@ -514,10 +514,16 @@ app.get('/tracks', tracks.list); app.get('/chat', chat.view); app.get('/chat/since.json', chat.since); + +var soundtracker = function(req, res, next) { + req.soundtrack = soundtrack; + next(); +}; + app.get('/:artistSlug/:trackSlug/:trackID', tracks.view); -app.post('/:artistSlug/:trackSlug/:trackID', authorize('editor') , tracks.edit); +app.post('/:artistSlug/:trackSlug/:trackID', authorize('editor') , soundtracker , tracks.edit); app.get('/tracks/:trackID', tracks.view ); -app.post('/tracks/:trackID', authorize('editor') , tracks.edit); +app.post('/tracks/:trackID', authorize('editor') , soundtracker , tracks.edit); app.get('/:artistSlug', artists.view); From b65888fd9ad074532b636febe5645edc533b1595 Mon Sep 17 00:00:00 2001 From: Eric Martindale Date: Thu, 13 Mar 2014 18:30:13 +0000 Subject: [PATCH 009/352] Fix double-queue bug for youtube. --- lib/soundtrack.js | 8 +- soundtrack.js | 8 +- util.js | 191 +++++++++++++++++----------------------------- 3 files changed, 83 insertions(+), 124 deletions(-) diff --git a/lib/soundtrack.js b/lib/soundtrack.js index 476db94f..d9413356 100644 --- a/lib/soundtrack.js +++ b/lib/soundtrack.js @@ -92,7 +92,13 @@ Soundtrack.prototype.forEachClient = function(fn) { Soundtrack.prototype.queueTrack = function(track, curator, queueCallback) { var self = this; var app = this.app; + + console.log('queueTrack() : ' + track ); Track.findOne({ _id: track._id }).populate('_artist _credits').exec(function(err, realTrack) { + if (err || !realTrack) { + console.log(err); + return queueCallback(); + } var playlistItem = realTrack.toObject(); @@ -184,8 +190,6 @@ Soundtrack.prototype.nextSong = function() { var self = this; var app = this.app; - console.log( self ); - self.ensureQueue(function() { app.room.playlist[0].startTime = Date.now(); app.room.track = app.room.playlist[0]; diff --git a/soundtrack.js b/soundtrack.js index 96b82804..0c2410d2 100644 --- a/soundtrack.js +++ b/soundtrack.js @@ -387,7 +387,7 @@ app.del('/playlist/:trackID', requireLogin, authorize('admin'), function(req, re if (!req.param('index') || req.param('index') == 0) { return next(); } app.room.playlist.splice( req.param('index') , 1 ); - app.sortPlaylist(); + soundtrack.sortPlaylist(); app.redis.set( app.config.database.name + ':playlist', JSON.stringify( app.room.playlist ) ); soundtrack.broadcast({ @@ -431,9 +431,15 @@ app.post('/playlist/:trackID', requireLogin, function(req, res, next) { }); app.post('/playlist', requireLogin, function(req, res) { + console.log('playlist endpoint hit with POST...') + util.trackFromSource( req.param('source') , req.param('id') , function(err, track) { + + console.log('trackFromSource() callback executing...') + if (!err && track) { soundtrack.queueTrack(track, req.user, function() { + console.log( 'queueTrack() callback executing... '); res.send({ status: 'success', message: 'Track added successfully!' }); }); } else { diff --git a/util.js b/util.js index c64d7025..31d74760 100644 --- a/util.js +++ b/util.js @@ -140,140 +140,87 @@ module.exports = { getYoutubeVideo: function(videoID, internalCallback) { 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) { - var video = data.data; - Track.findOne({ - 'sources.youtube.id': video.id - }).exec(function(err, track) { - if (!track) { var track = new Track({ title: video.title }); } + if (!data || !data.data) { return internalCallback('error retrieving video from youtube: ' + JSON.stringify(data) ); } - parseTitleString( video.title , function(parts) { + var video = data.data; + Track.findOne({ + 'sources.youtube.id': video.id + }).exec(function(err, track) { + if (!track) { var track = new Track({ title: video.title }); } - console.log( video.title + ' was parsed into:'); - console.log(parts); + parseTitleString( video.title , function(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.hqDefault; - - 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()) { - console.log(source); - console.log(track.sources[ source ]); - for (var i = 0; i Date: Thu, 13 Mar 2014 20:41:35 +0000 Subject: [PATCH 010/352] Further code reorganization. --- lib/soundtrack.js | 183 ++++++++++++++++++++++++++++++- package.json | 3 +- soundtrack.js | 29 +++-- util.js | 167 +--------------------------- views/partials/announcement.jade | 2 +- 5 files changed, 197 insertions(+), 187 deletions(-) diff --git a/lib/soundtrack.js b/lib/soundtrack.js index d9413356..6dcb748c 100644 --- a/lib/soundtrack.js +++ b/lib/soundtrack.js @@ -1,8 +1,11 @@ var _ = require('underscore'); var util = require('../util'); +var rest = require('restler'); +var async = require('async'); +var slug = require('speakingurl'); var Soundtrack = function(app) { - //this.app = app; + this.app = app; this.rooms = {}; //setInterval( this.markAndSweep, app.config.connection.checkInterval ); } @@ -93,7 +96,7 @@ Soundtrack.prototype.queueTrack = function(track, curator, queueCallback) { var self = this; var app = this.app; - console.log('queueTrack() : ' + track ); + console.log('queueTrack() : ' + track._id ); Track.findOne({ _id: track._id }).populate('_artist _credits').exec(function(err, realTrack) { if (err || !realTrack) { console.log(err); @@ -158,7 +161,7 @@ Soundtrack.prototype.ensureQueue = function(callback) { Play.find( query ).limit(4096).sort('timestamp').exec(function(err, plays) { if (err || !plays) { - return util.getYoutubeVideo( 'dQw4w9WgXcQ‎' , function(track) { + return self.getYoutubeVideo( 'dQw4w9WgXcQ‎' , function(track) { if (track) { backupTracks.push( track.toObject() ); } callback(); }); @@ -251,6 +254,180 @@ Soundtrack.prototype.startMusic = function() { }; +Soundtrack.prototype.trackFromSource = function(source, id, sourceCallback) { + var self = this; + var app = self.app; + + console.log('trackFromSource() : ' + source + ' ' + id ); + + switch (source) { + default: + callback('Unknown source: ' + source); + break; + case 'soundcloud': + rest.get('https://api.soundcloud.com/tracks/'+parseInt(id)+'.json?client_id='+app.config.soundcloud.id).on('complete', function(data, response) { + + if (!data.title) { return sourceCallback('No video found.'); } + + 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 (!track) { var track = new Track({ title: video.title }); } + + util.parseTitleString( video.title , function(parts) { + + console.log( video.title + ' was parsed into:'); + 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.hqDefault; + + 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()) { + console.log(source); + console.log(track.sources[ source ]); + for (var i = 0; i 1) ? data.title : data.user.username + ' - ' + data.title; - - 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; - } - } + parseTitleString: parseTitleString } \ No newline at end of file diff --git a/views/partials/announcement.jade b/views/partials/announcement.jade index 3e2d87b1..35cfd62a 100644 --- a/views/partials/announcement.jade +++ b/views/partials/announcement.jade @@ -1,3 +1,3 @@ .message abbr.timestamp.pull-right(title="#{moment(message.created).format()}") #{moment(message.created).format('HH:mm:ss')} - strong#announcement #{message.message} \ No newline at end of file + strong#announcement !{message.message} \ No newline at end of file From ddac5fe0376e2fdf70245420fbebaf20055f58f0 Mon Sep 17 00:00:00 2001 From: Eric Martindale Date: Thu, 13 Mar 2014 16:52:37 -0400 Subject: [PATCH 011/352] Make soundtrack work on first-boot. --- lib/soundtrack.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/soundtrack.js b/lib/soundtrack.js index 6dcb748c..1d62d881 100644 --- a/lib/soundtrack.js +++ b/lib/soundtrack.js @@ -7,6 +7,7 @@ var slug = require('speakingurl'); var Soundtrack = function(app) { this.app = app; this.rooms = {}; + this.backupTracks = []; //setInterval( this.markAndSweep, app.config.connection.checkInterval ); } @@ -160,9 +161,15 @@ Soundtrack.prototype.ensureQueue = function(callback) { console.log( query ); Play.find( query ).limit(4096).sort('timestamp').exec(function(err, plays) { - if (err || !plays) { - return self.getYoutubeVideo( 'dQw4w9WgXcQ‎' , function(track) { - if (track) { backupTracks.push( track.toObject() ); } + if (err || !plays || !plays.length) { + return self.getYoutubeVideo( 'dQw4w9WgXcQ' , function(track) { + if (track) { + self.backupTracks.push( track.toObject() ); + app.room.playlist.push( _.extend( track , { + score: 0 + , votes: {} + } ) ); + } callback(); }); } From 44bc5656c10bdccbd835b715ba94664e2fc30a18 Mon Sep 17 00:00:00 2001 From: Eric Martindale Date: Sat, 15 Mar 2014 21:22:19 +0000 Subject: [PATCH 012/352] Fix scrobbling, increase redis resilience. --- lib/soundtrack.js | 5 ++-- soundtrack.js | 73 +++++++++++++++++++++++++++++++---------------- 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/lib/soundtrack.js b/lib/soundtrack.js index 6dcb748c..7aa87972 100644 --- a/lib/soundtrack.js +++ b/lib/soundtrack.js @@ -247,7 +247,7 @@ Soundtrack.prototype.startMusic = function() { }, (app.room.playlist[0].duration - seekTo) * 1000 ); if (app.lastfm) { - app.lastfm.scrobbleActive( app.room.playlist[0] , function() { + self.scrobbleActive( app.room.playlist[0] , function() { console.log('scrobbling complete!'); }); } @@ -269,6 +269,7 @@ Soundtrack.prototype.trackFromSource = function(source, id, sourceCallback) { if (!data.title) { return sourceCallback('No video found.'); } + var TRACK_SEPARATOR = ' - '; var stringToParse = (data.title.split( TRACK_SEPARATOR ).length > 1) ? data.title : data.user.username + ' - ' + data.title; util.parseTitleString( stringToParse , function(parts) { @@ -433,7 +434,7 @@ Soundtrack.prototype.lastfmAuthSetup = function(req, res) { var app = this.app; //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' }); + var authUrl = app.lastfm.getAuthenticationUrl({ cb: 'https://soundtrack.io/auth/lastfm/callback' }); res.redirect(authUrl); }; Soundtrack.prototype.lastfmAuthCallback = function(req, res) { diff --git a/soundtrack.js b/soundtrack.js index 4aa20c21..edb312f1 100644 --- a/soundtrack.js +++ b/soundtrack.js @@ -6,7 +6,7 @@ var config = require('./config') , sys = require('sys') , http = require('http') , rest = require('restler') - , slug = require('slug-component') + , slug = require('speakingurl') , async = require('async') , redis = require('redis') , sockjs = require('sockjs') @@ -149,37 +149,58 @@ app.socketAuthTokens = []; app.config = config; -var Soundtrack = require('./lib/soundtrack'); -var soundtrack = new Soundtrack(app); -//soundtrack.app = app; - -/*/ -app.sortPlaylist = soundtrack.sortPlaylist; - -soundtrack.broadcast = soundtrack.broadcast; -app.whisper = soundtrack.whisper; -app.markAndSweep = soundtrack.markAndSweep; -app.forEachClient = soundtrack.forEachClient; - -app.queueTrack = soundtrack.queueTrack; -app.ensureQueue = soundtrack.ensureQueue; -app.nextSong = soundtrack.nextSong; -app.startMusic = soundtrack.startMusic; -/**/ - - if (config.lastfm && config.lastfm.key && config.lastfm.secret) { - - app.LastFM = LastFM; var lastfm = new LastFM({ api_key: config.lastfm.key , secret: config.lastfm.secret }); + app.LastFM = LastFM; + app.lastfm = lastfm; + app.get('/auth/lastfm', function(req, res) { + //var authUrl = lastfm.getAuthenticationUrl({ cb: ((config.app.safe) ? 'http://' : 'http://') + config.app.host + '/auth/lastfm/callback' }); + var authUrl = lastfm.getAuthenticationUrl({ cb: ((config.app.safe) ? 'http://' : 'http://') + 'soundtrack.io/auth/lastfm/callback' }); + res.redirect(authUrl); + }); + app.get('/auth/lastfm/callback', 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) { - app.get('/auth/lastfm', soundtrack.lastfmAuthSetup ); - app.get('/auth/lastfm/callback', soundtrack.lastfmAuthCallback ); + 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('/'); + }); + + }); + + }); + }); } +var Soundtrack = require('./lib/soundtrack'); +var soundtrack = new Soundtrack(app); + app.post('/skip', requireLogin, function(req, res) { console.log('skip received from ' +req.user.username); @@ -557,6 +578,10 @@ function getTop100FromCodingSoundtrack(done) { } app.redis = redis.createClient(); +app.redis.on('error', function(err) { + console.error("Error connecting to redis", err); +}); + app.redis.get(config.database.name + ':playlist', function(err, playlist) { playlist = JSON.parse(playlist); From 8eeec083648eafb4b984035152793fbe711c2272 Mon Sep 17 00:00:00 2001 From: Eric Martindale Date: Sun, 16 Mar 2014 01:21:52 +0000 Subject: [PATCH 013/352] Add some new states, data structure for _sources. --- controllers/artists.js | 21 +++++++++++++++----- controllers/people.js | 43 +++++++++++++++++++++++++++++++++++++--- controllers/tracks.js | 38 ++++++++++++++++++++++++++++++++--- models/Artist.js | 8 ++++++-- models/Source.js | 20 +++++++++++++++++++ models/Track.js | 12 +++++++++++ soundtrack.js | 3 +++ views/artists.jade | 7 ++++++- views/history.jade | 12 +---------- views/partials/play.jade | 12 +++++++++++ views/person-plays.jade | 7 +++++++ views/person.jade | 4 ++++ views/pool.jade | 17 ++++++++++++++++ views/tracks.jade | 3 ++- 14 files changed, 181 insertions(+), 26 deletions(-) create mode 100644 models/Source.js create mode 100644 views/partials/play.jade create mode 100644 views/person-plays.jade create mode 100644 views/pool.jade diff --git a/controllers/artists.js b/controllers/artists.js index d0e4601e..97a6352d 100644 --- a/controllers/artists.js +++ b/controllers/artists.js @@ -1,11 +1,20 @@ var rest = require('restler') - , _ = require('underscore'); + , _ = require('underscore') + , async = require('async'); module.exports = { list: function(req, res, next) { + var limit = (req.param('limit')) ? req.param('limit') : 100; var query = (req.param('q')) ? { name: new RegExp('(.*)'+req.param('q')+'(.*)', 'i') } : undefined; - console.log( query ); - Artist.find( query ).sort('name').limit(100).exec(function(err, artists) { + + async.parallel([ + function(done) { + Artist.count().exec( done ); + }, + function(done) { + Artist.find( query ).sort('name').limit( limit ).exec( done ); + } + ], function(err, results) { res.format({ json: function() { res.send( artists.map(function(x) { @@ -17,10 +26,12 @@ module.exports = { }, html: function() { res.render('artists', { - artists: artists + count: results[0] + , limit: limit + , artists: results[1] }); } - }) + }); }); }, view: function(req, res, next) { diff --git a/controllers/people.js b/controllers/people.js index 4c809117..89da238e 100644 --- a/controllers/people.js +++ b/controllers/people.js @@ -1,15 +1,28 @@ +var async = require('async'); + module.exports = { profile: function(req, res, next) { Person.findOne({ slug: req.param('usernameSlug') }).exec(function(err, person) { if (!person) { return next(); } - Playlist.find({ _creator: person._id, public: true }).exec(function(err, playlists) { + async.parallel([ + function(done) { + Playlist.find({ _creator: person._id, public: true }).exec( done ); + }, + function(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(err, results) { res.render('person', { person: person - , playlists: playlists + , playlists: results[0] + , plays: results[1] }); }); - }); }, edit: function(req, res, next) { @@ -38,6 +51,30 @@ module.exports = { }); }); }, + listPlays: function(req, res, next) { + 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 }).sort('-timestamp').populate('_track _curator').exec(function(err, plays) { + Artist.populate( plays , { + path: '_track._artist _track._credits' + }, done ); + }); + } + ], function(err, results) { + 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('/'); diff --git a/controllers/tracks.js b/controllers/tracks.js index 994064a3..32da9a2a 100644 --- a/controllers/tracks.js +++ b/controllers/tracks.js @@ -1,21 +1,53 @@ var async = require('async') - , _ = require('underscore'); + , _ = require('underscore') + , util = require('../util'); module.exports = { list: function(req, res, next) { + var limit = (req.param('limit')) ? req.param('limit') : 100; + var query = (req.param('q')) ? { name: new RegExp('(.*)'+req.param('q')+'(.*)', 'i') } : undefined; + Play.aggregate([ + { $match: query }, { $group: { _id: '$_track', count: { $sum: 1 } } }, { $sort: { 'count': -1 } }, - { $limit: 100 } + { $limit: limit } ], function(err, tracks) { Track.find({ _id: { $in: tracks.map(function(x) { return x._id; }) }}).populate('_artist').exec(function(err, tracks) { + + Track.count( query ).exec(function(err, count) { + res.format({ + json: function() { + res.send( tracks ); + }, + html: function() { + res.render('tracks', { + tracks: tracks + , count: count + , limit: limit + }); + } + }); + }); + }); + }); + }, + pool: function(req, res, next) { + var query = { _curator: { $exists: true } }; + + query = _.extend( query , { + $or: util.timeSeries('timestamp', 3600*3*1000, 24*60*1000*60, 7) + }); + + Play.find( query ).limit(4096).exec(function(err, 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('tracks', { + res.render('pool', { tracks: tracks }); } diff --git a/models/Artist.js b/models/Artist.js index c3ce4ad9..0c033f30 100644 --- a/models/Artist.js +++ b/models/Artist.js @@ -16,16 +16,20 @@ var ArtistSchema = new Schema({ , bio: String }); -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']; } + + next(); }); + } else { + next(); } }); diff --git a/models/Source.js b/models/Source.js new file mode 100644 index 00000000..01c077da --- /dev/null +++ b/models/Source.js @@ -0,0 +1,20 @@ +var mongoose = require('mongoose') + , Schema = mongoose.Schema + , ObjectId = mongoose.SchemaTypes.ObjectId; + +// this defines the fields associated with the model, +// and moreover, their type. +var SourceSchema = new Schema({ + id: { type: String } + , type: { type: String, enum: ['video/youtube', 'audio/mp3'] } + , uri: { type: String } + , start: { type: Number } + , end: { type: Number } +}); + +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 38bb2a39..3fad6ade 100644 --- a/models/Track.js +++ b/models/Track.js @@ -16,6 +16,7 @@ var TrackSchema = new Schema({ , images: { thumbnail: { url: { type: String } } } + , _sources: [ { type: ObjectId, ref: 'Source' } ] , sources: { youtube: [ new Schema({ id: { type: String, required: true } @@ -38,6 +39,17 @@ var TrackSchema = new Schema({ }) ] }); +TrackSchema.pre('save', function(next) { + var self = this; + + /** for (var source in self.sources) { + self._sources.push({ }); + } **/ + + next(); + +}); + TrackSchema.post('init', function() { var self = this; diff --git a/soundtrack.js b/soundtrack.js index edb312f1..1a32c91b 100644 --- a/soundtrack.js +++ b/soundtrack.js @@ -53,6 +53,7 @@ Track = require('./models/Track').Track; Artist = require('./models/Artist').Artist; Play = require('./models/Play').Play; Playlist = require('./models/Playlist').Playlist; +Source = require('./models/Source').Source; Chat = require('./models/Chat').Chat; passport.use(Person.createStrategy()); @@ -535,6 +536,7 @@ app.get('/history', pages.history); app.get('/people', people.list); app.get('/artists', artists.list); app.get('/tracks', tracks.list); +app.get('/pool', tracks.pool); app.get('/chat', chat.view); app.get('/chat/since.json', chat.since); @@ -552,6 +554,7 @@ app.post('/tracks/:trackID', authorize('editor') , soundtracker app.get('/:artistSlug', artists.view); app.get('/:usernameSlug/:playlistSlug', playlists.view); +app.get('/:usernameSlug/plays', people.listPlays); app.get('/:usernameSlug', people.profile); app.post('/:usernameSlug', people.edit); diff --git a/views/artists.jade b/views/artists.jade index efa6000b..452ad5b8 100644 --- a/views/artists.jade +++ b/views/artists.jade @@ -2,7 +2,12 @@ extends layout block content .well.content-well - h1 #{artists.length} Artists Known + .pull-right + form + input(type="text", name="q") + + h1 #{count} Artists Known + p Display limited to #{limit} artists. Pass limit to change this. ul for artist in artists li diff --git a/views/history.jade b/views/history.jade index 6f4d318f..94191935 100644 --- a/views/history.jade +++ b/views/history.jade @@ -6,14 +6,4 @@ block content h1 Track History ul for play in plays - if (typeof(play._track._artist) != 'undefined' && play._track._artist) - li - a(href="/#{play._track._artist.slug}") #{play._track._artist.name} - | — - a(href="/#{play._track._artist.slug}/#{play._track.slug}/#{play._track._id}") #{play._track.title} - small queued by - if (play._curator) - a(href="#{play._curator.slug}") #{play._curator.username} - else - span random entropy (chosen by the machine) - | with #{play.messageCount} messages \ No newline at end of file + include partials/play \ No newline at end of file diff --git a/views/partials/play.jade b/views/partials/play.jade new file mode 100644 index 00000000..e00262e6 --- /dev/null +++ b/views/partials/play.jade @@ -0,0 +1,12 @@ +if (typeof(play._track._artist) != 'undefined' && play._track._artist) + li + a(href="/#{play._track._artist.slug}") #{play._track._artist.name} + | — + a(href="/#{play._track._artist.slug}/#{play._track.slug}/#{play._track._id}") #{play._track.title} + small queued by + if (play._curator) + a(href="#{play._curator.slug}") #{play._curator.username} + else + span random entropy (chosen by the machine) + | on #{moment(play.timestamp).format('LLLL')} + | with #{play.messageCount} messages \ No newline at end of file diff --git a/views/person-plays.jade b/views/person-plays.jade new file mode 100644 index 00000000..529c5d01 --- /dev/null +++ b/views/person-plays.jade @@ -0,0 +1,7 @@ +extends layout + +block content + h1 All #{plays.length} Plays By #{person.username} + + for play in plays + include partials/play \ No newline at end of file diff --git a/views/person.jade b/views/person.jade index dd4f8725..a4edccb2 100644 --- a/views/person.jade +++ b/views/person.jade @@ -34,3 +34,7 @@ block content a(href="/#{person.slug}/#{playlist.slug}") #{playlist.name} p #{playlist.description} + h3 Recent Plays (view all ») + for play in plays + include partials/play + diff --git a/views/pool.jade b/views/pool.jade new file mode 100644 index 00000000..79a04d90 --- /dev/null +++ b/views/pool.jade @@ -0,0 +1,17 @@ +extends layout + +block content + + .well.content-well + h1 #{tracks.length} Tracks Currently in Queue Pool + p Chosen from tracks played during the previous 7 days at the present time of day (3 hour rolling window). + + ul + for track in tracks + li + if (track._artist) + a(href="/#{track._artist.slug}") #{track._artist.name} + | — + a(href="/#{track._artist.slug}/#{track.slug}/#{track._id}") #{track.title} + else + h5 ಠ_ಠ #{track} \ No newline at end of file diff --git a/views/tracks.jade b/views/tracks.jade index ee788dd5..7ebc4f36 100644 --- a/views/tracks.jade +++ b/views/tracks.jade @@ -3,7 +3,8 @@ extends layout block content .well.content-well - h1 Tracks + h1 #{count} Tracks Known + p Ordered by number of plays. Display limited to #{limit} tracks. Pass limit to change this. ul for track in tracks From 85bb50f6b87419f45428c8e8d41539380f550144 Mon Sep 17 00:00:00 2001 From: Eric Martindale Date: Wed, 2 Apr 2014 22:48:04 +0000 Subject: [PATCH 014/352] Fix crash on artist JSON, add debug to pool. --- controllers/artists.js | 2 +- controllers/tracks.js | 23 +++++++++++++++-------- views/pool.jade | 5 ++++- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/controllers/artists.js b/controllers/artists.js index 97a6352d..8b30a267 100644 --- a/controllers/artists.js +++ b/controllers/artists.js @@ -17,7 +17,7 @@ module.exports = { ], function(err, results) { res.format({ json: function() { - res.send( artists.map(function(x) { + res.send( results[1].map(function(x) { x = x.toObject(); //x.value = x._id; x.value = x.name; diff --git a/controllers/tracks.js b/controllers/tracks.js index 32da9a2a..910ac8fb 100644 --- a/controllers/tracks.js +++ b/controllers/tracks.js @@ -4,19 +4,24 @@ var async = require('async') module.exports = { list: function(req, res, next) { - var limit = (req.param('limit')) ? req.param('limit') : 100; - var query = (req.param('q')) ? { name: new RegExp('(.*)'+req.param('q')+'(.*)', 'i') } : undefined; + var limit = (req.param('limit')) ? parseInt(req.param('limit')) : 100; + var query = (req.param('q')) ? { name: new RegExp('(.*)'+req.param('q')+'(.*)', 'i') } : {}; - Play.aggregate([ - { $match: query }, + /*/Play.aggregate([ { $group: { _id: '$_track', count: { $sum: 1 } } }, { $sort: { 'count': -1 } }, { $limit: limit } ], function(err, tracks) { + if (err) { console.log(err); }/**/ + + //Track.find({ _id: { $in: tracks.map(function(x) { return x._id; }) }}).populate('_artist').exec(function(err, tracks) { + Track.find( query ).populate('_artist').limit( limit ).exec(function(err, tracks) { + if (err) { console.log(err); } - Track.find({ _id: { $in: tracks.map(function(x) { return x._id; }) }}).populate('_artist').exec(function(err, tracks) { Track.count( query ).exec(function(err, count) { + if (err) { console.log(err); } + res.format({ json: function() { res.send( tracks ); @@ -26,12 +31,13 @@ module.exports = { tracks: tracks , count: count , limit: limit + , query: query }); } }); }); }); - }); + /*/});/**/ }, pool: function(req, res, next) { var query = { _curator: { $exists: true } }; @@ -48,7 +54,8 @@ module.exports = { }, html: function() { res.render('pool', { - tracks: tracks + tracks: tracks + , query: query }); } }); @@ -159,4 +166,4 @@ module.exports = { }); }); } -} \ No newline at end of file +} diff --git a/views/pool.jade b/views/pool.jade index 79a04d90..7e63ff2a 100644 --- a/views/pool.jade +++ b/views/pool.jade @@ -6,6 +6,9 @@ block content h1 #{tracks.length} Tracks Currently in Queue Pool p Chosen from tracks played during the previous 7 days at the present time of day (3 hour rolling window). + h2 Query used (for debug purposes) + pre #{JSON.stringify(query)} + ul for track in tracks li @@ -14,4 +17,4 @@ block content | — a(href="/#{track._artist.slug}/#{track.slug}/#{track._id}") #{track.title} else - h5 ಠ_ಠ #{track} \ No newline at end of file + h5 ಠ_ಠ #{track} From ad076abbb95e789e7dd09e966272474993a281b1 Mon Sep 17 00:00:00 2001 From: Eric Martindale Date: Wed, 16 Apr 2014 20:48:01 +0000 Subject: [PATCH 015/352] Remove time resets from timeSeries. --- util.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/util.js b/util.js index 48053c39..617c33a8 100644 --- a/util.js +++ b/util.js @@ -116,11 +116,6 @@ module.exports = { for (var i = 0; i <= limit; i++) { 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 - ((i+1) * 1000 * 60 * 60 * 24) ); From 23d4e65bf2b00d16fab44a5a0abecf684c833a1d Mon Sep 17 00:00:00 2001 From: Eric Martindale Date: Tue, 22 Apr 2014 06:10:37 +0000 Subject: [PATCH 016/352] Fix various streaming issues. --- lib/soundtrack.js | 2 +- models/Play.js | 2 + public/js/app.js | 21 +- public/js/vjs.youtube.js | 42 +- public/video-js/video-js.css | 831 +++++++++++++-------------- public/video-js/video-js.min.css | 5 +- public/video-js/video-js.swf | Bin 14329 -> 14059 bytes public/video-js/video.dev.js | 947 +++++++++---------------------- public/video-js/video.js | 246 ++++---- views/layout.jade | 10 +- 10 files changed, 837 insertions(+), 1269 deletions(-) diff --git a/lib/soundtrack.js b/lib/soundtrack.js index 27e1d1a9..bc14e7cf 100644 --- a/lib/soundtrack.js +++ b/lib/soundtrack.js @@ -392,7 +392,7 @@ Soundtrack.prototype.getYoutubeVideo = function(videoID, internalCallback) { 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.hqDefault; + 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 ); diff --git a/models/Play.js b/models/Play.js index 9808c698..69d43814 100644 --- a/models/Play.js +++ b/models/Play.js @@ -8,6 +8,8 @@ var PlaySchema = new Schema({ _track: { type: ObjectId, ref: 'Track' } , _curator: { type: ObjectId, ref: 'Person' } , timestamp: { type: Date, default: Date.now } + , length: { type: Number } + , played: { type: Number } }); PlaySchema.virtual('isoDate').get(function() { diff --git a/public/js/app.js b/public/js/app.js index 97188556..587224a4 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -186,7 +186,13 @@ angular.module('soundtrack-io', ['timeFilters']); function AppController($scope, $http) { window.updatePlaylist = function(){ $http.get('/playlist.json').success(function(data){ - $scope.tracks = 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; + }); soundtrack.room.track = data[0]; }); } @@ -250,11 +256,14 @@ $(window).load(function(){ soundtrack = new Soundtrack(); if ($('#main-player').length) { soundtrack.player = videojs('#main-player', { - techOrder: ['html5', 'flash', 'youtube'] + techOrder: ['html5', 'youtube'] + , forceHTML5: true + , forceSSL: true + , playsInline: true }); } else { soundtrack.player = videojs('#secondary-player', { - techOrder: ['html5', 'flash', 'youtube'] + techOrder: ['html5', 'youtube'] }); mutePlayer(); } @@ -315,7 +324,7 @@ $(window).load(function(){ }); msg.data.sources.soundcloud.forEach(function( item ) { - sources.push( { type:'audio/mp3', src: 'https://api.soundcloud.com/tracks/' + item.id + '/stream?client_id=7fbc3f4099d3390415d4c95f16f639ae' } ); + sources.push( { type:'audio/mp3', src: 'https://api.soundcloud.com/tracks/' + item.id + '/stream?start='+msg.data.duration+'&client_id=7fbc3f4099d3390415d4c95f16f639ae' } ); }); soundtrack.player.src( sources ); @@ -324,6 +333,7 @@ $(window).load(function(){ soundtrack.player.pause(); soundtrack.player.currentTime( msg.seekTo ); soundtrack.player.play(); + soundtrack.player.currentTime( msg.seekTo ); // ...and SoundCloud doesn't behave well without these. :/ var bufferEvaluator = function() { @@ -337,7 +347,7 @@ $(window).load(function(){ 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.pause(); // this breaks soundcloud, wat? soundtrack.player.currentTime( msg.seekTo ); soundtrack.player.play(); } else { @@ -348,7 +358,6 @@ $(window).load(function(){ //soundtrack.player.off('progress', bufferEvaluator); soundtrack.player.on('progress', bufferEvaluator); soundtrack.player.on('loadeddata', bufferEvaluator); - ensureVolumeCorrect(); if ($('#playlist-list li:first').data('track-id') == msg.data._id) { diff --git a/public/js/vjs.youtube.js b/public/js/vjs.youtube.js index 78d25a1c..0fccdb3c 100644 --- a/public/js/vjs.youtube.js +++ b/public/js/vjs.youtube.js @@ -1,16 +1,28 @@ -videojs.Youtube=videojs.MediaTechController.extend({init:function(a,b,c){videojs.MediaTechController.call(this,a,b,c);this.features.fullscreenResize=!0;this.player_=a;this.player_el_=document.getElementById(this.player_.id());if("undefined"!=typeof b.source)for(var e in b.source)this.player_.options()[e]=b.source[e];this.player_.options().ytcontrols&&this.player_.controls(!1);this.videoId=videojs.Youtube.parseVideoId(this.player_.options().src);if("undefined"!=typeof this.videoId&&!this.player_.options().ytcontrols){"undefined"== -typeof this.player_.poster()&&this.player_.poster("https://img.youtube.com/vi/"+this.videoId+"/0.jpg");var d=this;setTimeout(function(){d.player_.posterImage.el().style.backgroundSize="cover"},50)}this.id_=this.player_.id()+"_youtube_api";this.el_=videojs.Component.prototype.createEl("iframe",{id:this.id_,className:"vjs-tech",scrolling:"no",marginWidth:0,marginHeight:0,frameBorder:0,webkitAllowFullScreen:"",mozallowfullscreen:"",allowFullScreen:""});this.player_el_.insertBefore(this.el_,this.player_el_.firstChild); -a={enablejsapi:1,iv_load_policy:3,playerapiid:this.id(),disablekb:1,wmode:"transparent",controls:this.player_.options().ytcontrols?1:0,showinfo:0,modestbranding:1,rel:0,autoplay:this.player_.options().autoplay?1:0,loop:this.player_.options().loop?1:0,list:videojs.Youtube.parsePlaylist(this.player_.options().src)};"undefined"==typeof a.list&&delete a.list;this.player_.options().autoplay&&(this.playOnReady=!0);"file:"!=window.location.protocol&&(a.origin=window.location.protocol+"//"+window.location.host); -this.el_.src="https://www.youtube.com/embed/"+this.videoId+"?"+videojs.Youtube.makeQueryString(a);this.player_.options().ytcontrols&&(d=this,setTimeout(function(){var a=d.player_.bigPlayButton.el();a.parentNode.removeChild(a);a=d.player_.controlBar.el();a.parentNode.removeChild(a)},50));videojs.Youtube.apiReady?this.loadYoutube():(videojs.Youtube.loadingQueue.push(this),videojs.Youtube.apiLoading||(a=document.createElement("script"),a.src="https://www.youtube.com/iframe_api",b=document.getElementsByTagName("script")[0], -b.parentNode.insertBefore(a,b),videojs.Youtube.apiLoading=!0))}});videojs.Youtube.prototype.dispose=function(){this.el_&&this.el_.parentNode.removeChild(this.el_);this.ytplayer&&this.ytplayer.destroy();videojs.MediaTechController.prototype.dispose.call(this)};videojs.Youtube.prototype.src=function(a){this.ytplayer.loadVideoById({videoId:videojs.Youtube.parseVideoId(a),list:videojs.Youtube.parsePlaylist(a)})}; -videojs.Youtube.prototype.play=function(){this.isReady_?this.ytplayer.playVideo():this.playOnReady=!0};videojs.Youtube.prototype.pause=function(){this.ytplayer.pauseVideo()};videojs.Youtube.prototype.paused=function(){return this.lastState!==YT.PlayerState.PLAYING&&this.lastState!==YT.PlayerState.BUFFERING};videojs.Youtube.prototype.currentTime=function(){return this.ytplayer.getCurrentTime()};videojs.Youtube.prototype.setCurrentTime=function(a){this.ytplayer.seekTo(a,!0);this.player_.trigger("timeupdate")}; -videojs.Youtube.prototype.duration=function(){return this.ytplayer.getDuration()};videojs.Youtube.prototype.buffered=function(){var a=this.ytplayer.getVideoBytesLoaded(),b=this.ytplayer.getVideoBytesTotal();if(!a||!b)return 0;var c=this.ytplayer.getDuration(),a=a/b*c,b=this.ytplayer.getVideoStartBytes()/b*c;return videojs.createTimeRange(b,b+a)};videojs.Youtube.prototype.volume=function(){isNaN(this.volumeVal)&&(this.volumeVal=this.ytplayer.getVolume()/100);return this.volumeVal}; -videojs.Youtube.prototype.setVolume=function(a){a&&a!=this.volumeVal&&(this.ytplayer.setVolume(100*a),this.volumeVal=a,this.player_.trigger("volumechange"))};videojs.Youtube.prototype.muted=function(){return this.ytplayer.isMuted()};videojs.Youtube.prototype.setMuted=function(a){a?this.ytplayer.mute():this.ytplayer.unMute();var b=this;setTimeout(function(){b.player_.trigger("volumechange")},50)}; -videojs.Youtube.prototype.onReady=function(){this.isReady_=!0;this.player_.trigger("techready");this.triggerReady();this.player_.trigger("durationchange");this.playOnReady&&this.ytplayer.playVideo()}; -videojs.Youtube.prototype.onStateChange=function(a){if(a!=this.lastState){switch(a){case -1:this.player_.trigger("durationchange");break;case YT.PlayerState.ENDED:this.player_.trigger("ended");break;case YT.PlayerState.PLAYING:this.player_.trigger("timeupdate");this.player_.trigger("durationchange");this.player_.trigger("playing");this.player_.trigger("play");break;case YT.PlayerState.PAUSED:this.player_.trigger("pause");break;case YT.PlayerState.BUFFERING:this.player_.trigger("timeupdate"),this.player_.trigger("waiting")}this.lastState= -a}}; -videojs.Youtube.prototype.onPlaybackQualityChange=function(a){switch(a){case "medium":this.player_.videoWidth=480;this.player_.videoHeight=360;break;case "large":this.player_.videoWidth=640;this.player_.videoHeight=480;break;case "hd720":this.player_.videoWidth=960;this.player_.videoHeight=720;break;case "hd1080":this.player_.videoWidth=1440;this.player_.videoHeight=1080;break;case "highres":this.player_.videoWidth=1920;this.player_.videoHeight=1080;break;case "small":this.player_.videoWidth=320; -this.player_.videoHeight=240;break;default:this.player_.videoWidth=0,this.player_.videoHeight=0}this.player_.trigger("ratechange")};videojs.Youtube.prototype.onError=function(a){this.player_.error=a;this.player_.trigger("error")};videojs.Youtube.isSupported=function(){return!0};videojs.Youtube.prototype.supportsFullScreen=function(){return!1};videojs.Youtube.canPlaySource=function(a){return"video/youtube"==a.type};videojs.Youtube.loadingQueue=[]; +videojs.Youtube=videojs.MediaTechController.extend({init:function(a,b,c){videojs.MediaTechController.call(this,a,b,c);this.features.progressEvents=!1;this.features.timeupdateEvents=!1;if("undefined"!=typeof b.source)for(var e in b.source)a.options()[e]=b.source[e];this.userQuality=videojs.Youtube.convertQualityName(a.options().quality);this.player_=a;this.player_el_=document.getElementById(a.id());this.player_el_.className+=" vjs-youtube";this.qualityButton=document.createElement("div");this.qualityButton.setAttribute("class", +"vjs-quality-button vjs-menu-button vjs-control");this.qualityButton.setAttribute("tabindex",0);b=document.createElement("div");this.qualityButton.appendChild(b);this.qualityTitle=document.createElement("span");b.appendChild(this.qualityTitle);b=document.createElement("div");b.setAttribute("class","vjs-menu");this.qualityButton.appendChild(b);this.qualityMenuContent=document.createElement("ul");this.qualityMenuContent.setAttribute("class","vjs-menu-content");b.appendChild(this.qualityMenuContent); +this.id_=this.player_.id()+"_youtube_api";this.el_=videojs.Component.prototype.createEl("iframe",{id:this.id_,className:"vjs-tech",scrolling:"no",marginWidth:0,marginHeight:0,frameBorder:0,webkitAllowFullScreen:"true",mozallowfullscreen:"true",allowFullScreen:"true"});this.iframeblocker=videojs.Component.prototype.createEl("div",{className:"iframeblocker"});var d=this;(this.toggleOnClick=!!this.player_.options().toggleOnClick)?this.iframeblocker.addEventListener("click",function(){d.paused()?d.play(): +d.pause()}):this.iframeblocker.addEventListener("click",function(){!0===d.player_.userActive()?d.player_.userActive(!1):d.player_.userActive(!0)});this.iframeblocker.addEventListener("mousemove",function(a){d.player_.userActive()||d.player_.userActive(!0);a.stopPropagation();a.preventDefault()});this.iframeblocker.addEventListener("tap",function(){!0===d.player_.userActive()?d.player_.userActive(!1):d.player_.userActive(!0)});this.player_.options().ytcontrols||(this.iframeblocker.style.display="block"); +this.player_el_.insertBefore(this.iframeblocker,this.player_el_.firstChild);this.player_el_.insertBefore(this.el_,this.iframeblocker);this.parseSrc(a.options().src);this.playOnReady=this.player_.options().autoplay||!1;this.forceSSL=this.player_.options().forceSSL||!1;b={enablejsapi:1,iv_load_policy:3,playerapiid:this.id(),disablekb:1,wmode:"transparent",controls:this.player_.options().ytcontrols?1:0,html5:this.player_.options().forceHTML5?1:null,playsinline:this.player_.options().playsInline?1:0, +showinfo:0,modestbranding:1,rel:0,autoplay:this.playOnReady?1:0,loop:this.player_.options().loop?1:0,list:this.playlistId,vq:this.userQuality};for(var f in b)!b.hasOwnProperty(f)||"undefined"!==typeof b[f]&&null!==b[f]||delete b[f];"file:"!=window.location.protocol?this.forceSSL?this.el_.src="https://www.youtube.com/embed/"+this.videoId+"?"+videojs.Youtube.makeQueryString(b):(b.origin=window.location.protocol+"//"+window.location.host,this.el_.src=window.location.protocol+"//www.youtube.com/embed/"+ +this.videoId+"?"+videojs.Youtube.makeQueryString(b)):this.el_.src="https://www.youtube.com/embed/"+this.videoId+"?"+videojs.Youtube.makeQueryString(b);a.ready(function(){d.player_el_.getElementsByClassName("vjs-control-bar")[0].appendChild(d.qualityButton);d.playOnReady&&!d.player_.options().ytcontrols&&("undefined"!=typeof d.player_.loadingSpinner&&d.player_.loadingSpinner.show(),"undefined"!=typeof d.player_.bigPlayButton&&d.player_.bigPlayButton.hide())});this.player_.options().ytcontrols?this.player_.controls(!1): +this.player_.poster()||(null==this.videoId?this.iframeblocker.style.backgroundColor="black":this.player_.poster("https://img.youtube.com/vi/"+this.videoId+"/0.jpg"));videojs.Youtube.apiReady?this.loadYoutube():(videojs.Youtube.loadingQueue.push(this),videojs.Youtube.apiLoading||(a=document.createElement("script"),a.onerror=function(a){d.onError(a)},a.src=this.forceSSL||"file:"===window.location.protocol?"https://www.youtube.com/iframe_api":"//www.youtube.com/iframe_api",f=document.getElementsByTagName("script")[0], +f.parentNode.insertBefore(a,f),videojs.Youtube.apiLoading=!0));this.on("dispose",function(){this.el_.parentNode.removeChild(this.el_);this.iframeblocker.parentNode.removeChild(this.iframeblocker);this.qualityButton.parentNode.removeChild(this.qualityButton);"undefined"!=typeof this.player_.loadingSpinner&&this.player_.loadingSpinner.hide();"undefined"!=typeof this.player_.bigPlayButton&&this.player_.bigPlayButton.hide()})}}); +videojs.Youtube.prototype.parseSrc=function(a){if(this.srcVal=a){var b=a.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/);this.videoId=b&&11==b[2].length?b[2]:null;b=a.match(/[?&]list=([^#\&\?]+)/);null!=b&&1 div:first-child > span:first-child { position:relative;top:7px } ");document.getElementsByTagName("head")[0].appendChild(a)})(); diff --git a/public/video-js/video-js.css b/public/video-js/video-js.css index 513c9c90..3058edbc 100644 --- a/public/video-js/video-js.css +++ b/public/video-js/video-js.css @@ -1,134 +1,250 @@ /*! Video.js Default Styles (http://videojs.com) -Version 4.2.2 -Create your own skin at http://designer.videojs.com +Version 4.1.0 */ -/* SKIN -================================================================================ -The main class name for all skin-specific styles. To make your own skin, -replace all occurances of 'vjs-default-skin' with a new name. Then add your new -skin name to your video tag instead of the default skin. -e.g.