├── .gitignore ├── .jscsrc ├── Gruntfile.js ├── LICENSE ├── README.md ├── app.js ├── bower.json ├── config.js ├── db.js ├── electron.js ├── electronres ├── buildpackages.sh ├── ffmpeg32.exe ├── ffmpeg64.exe ├── icon.icns └── icon.ico ├── library_functions.js ├── package.json ├── patches.js ├── routes └── index.js ├── static ├── css │ ├── main.css │ ├── main1.css │ ├── mobile.css │ ├── preloader │ │ └── preloader.css │ └── style.css ├── images │ ├── cross.svg │ ├── encore-cover.svg │ ├── favicon.ico │ ├── milkyway.jpg │ ├── particle.jpg │ └── unknown.jpg └── js │ ├── player │ ├── backbone_setup.js │ ├── coverbox.js │ ├── detailview.js │ ├── footer.js │ ├── infoview.js │ ├── models.js │ ├── music-search.js │ ├── onload.js │ ├── player.js │ ├── selection_options.js │ ├── settingsview.js │ ├── sidebarview.js │ ├── socket.js │ ├── songview.js │ ├── syncview.js │ └── utils.js │ └── preloader │ ├── pace.js │ └── preloader.js ├── util.js ├── views ├── admin.html ├── base │ └── main_base.html ├── experimental-ui.html ├── index.html ├── index1.html └── mobile.html └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | bower_components/* 3 | static/lib/* 4 | dbs/* 5 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "airbnb", 3 | "requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties", 4 | "safeContextKeyword": null, 5 | "requireCamelCaseOrUpperCaseIdentifiers": null, 6 | "requirePaddingNewLinesAfterBlocks": false, 7 | "requireSpacesInsideObjectBrackets": false, 8 | } 9 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | 'bower-install-simple': { 6 | options: { 7 | color: true, 8 | directory: 'bower_components', 9 | }, 10 | def: { 11 | production: true, 12 | }, 13 | }, 14 | 15 | // because grunt-bower-task doesn't correctly move font-awesome 16 | copy: { 17 | fontawesome_fonts: { 18 | src: 'bower_components/font-awesome-bower/fonts/*', 19 | dest: 'static/lib/font-awesome-bower/fonts/', 20 | expand: true, flatten: true, filter: 'isFile', 21 | }, 22 | fontawesome_css: { 23 | src: 'bower_components/font-awesome-bower/css/*', 24 | dest: 'static/lib/font-awesome-bower/css/', 25 | expand: true, flatten: true, filter: 'isFile', 26 | }, 27 | headjs: { 28 | src: 'bower_components/headjs/dist/1.0.0/head.min.js', 29 | dest: 'static/lib/head.min.js', 30 | }, 31 | }, 32 | uglify: { 33 | options: { 34 | mangle: false, 35 | }, 36 | my_target: { 37 | files: { 38 | 'static/lib/libs.min.js': [ 39 | 'bower_components/jquery/dist/jquery.min.js', 40 | 'bower_components/bootstrap/dist/js/bootstrap.min.js', 41 | 'bower_components/jquery.ui/ui/core.js', 42 | 'bower_components/jquery.ui/ui/widget.js', 43 | 'bower_components/jquery.ui/ui/position.js', 44 | 'bower_components/jquery.ui/ui/menu.js', 45 | 'bower_components/jquery.ui/ui/mouse.js', 46 | 'bower_components/jquery.ui/ui/sortable.js', 47 | 'bower_components/underscore/underscore-min.js', 48 | 'bower_components/backbone/backbone.js', 49 | 'bower_components/backbone.babysitter/lib/backbone.babysitter.min.js', 50 | 'bower_components/backbone.wreqr/lib/backbone.wreqr.min.js', 51 | 'bower_components/marionette/lib/backbone.marionette.min.js', 52 | 'bower_components/seiyria-bootstrap-slider/dist/bootstrap-slider.min.js', 53 | 'bower_components/bootbox/bootbox.js', 54 | 'bower_components/messenger/build/js/messenger.min.js', 55 | 'bower_components/civswig/swig.min.js', 56 | 'bower_components/DateJS/build/production/date.min.js', 57 | 'bower_components/lastfm-api/lastfm-api.js', 58 | ], 59 | }, 60 | }, 61 | }, 62 | cssmin: { 63 | options: { 64 | shorthandCompacting: false, 65 | roundingPrecision: -1, 66 | }, 67 | target: { 68 | files: { 69 | 'static/lib/libs.min.css': [ 70 | 'bower_components/bootswatch/cerulean/bootstrap.min.css', 71 | 'bower_components/seiyria-bootstrap-slider/dist/css/bootstrap-slider.min.css', 72 | 'bower_components/messenger/build/css/messenger.css', 73 | 'bower_components/messenger/build/css/messenger-theme-air.css', 74 | ], 75 | }, 76 | }, 77 | }, 78 | }); 79 | 80 | grunt.loadNpmTasks('grunt-bower-install-simple'); 81 | grunt.loadNpmTasks('grunt-contrib-copy'); 82 | grunt.loadNpmTasks('grunt-contrib-uglify'); 83 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 84 | 85 | grunt.registerTask('default', ['bower-install-simple:def', 'copy', 'uglify', 'cssmin']); 86 | }; 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 OSD Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Encore 2 | * Intranet Music Player 3 | * Access IP: http://10.4.8.248 4 | 5 | ## TODO 6 | * Music Recommender 7 | * UI Upgrade 8 | 9 | ## Installation Instructions 10 | 11 | * Clone the repository: 12 | `$ git clone https://github.com/OSDLabs/Encore/` 13 | * Change the directory: 14 | `$ cd Encore` 15 | * Install packages and start the app: 16 | `$ npm install && npm start` 17 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var async = require('async'); 6 | var express = require('express.oi'); 7 | var favicon = require('serve-favicon'); 8 | var bodyParser = require('body-parser'); 9 | var cookieParser = require('cookie-parser'); 10 | var errorhandler = require('errorhandler'); 11 | var http = require('http'); 12 | var path = require('path'); 13 | var util = require(__dirname + '/util.js'); 14 | var mkdirp = require('mkdirp'); 15 | var proxy = require('express-http-proxy'); 16 | var basicAuth = require('basic-auth-connect'); 17 | var passport = require('passport'); 18 | var Strategy = require('passport-local').Strategy; 19 | 20 | var app = express().http().io(); 21 | 22 | var sessionOpts = { 23 | secret: 'keyboard cat', 24 | resave: true, 25 | saveUninitialized: true, 26 | }; 27 | app.io.session(sessionOpts); 28 | 29 | app.io.set('authorization', function handleAuth(handshakeData, accept) { 30 | // accept all requests 31 | accept(null, true); 32 | }); 33 | 34 | // fetch the config directory 35 | app.set('configDir', process.env.configDir || __dirname); 36 | 37 | // all variables to be shared throughout the app 38 | app.set('port', process.env.PORT || 2000); 39 | app.set('views', path.join(__dirname, 'views')); 40 | app.set('view engine', 'html'); 41 | app.set('root', __dirname); 42 | app.set('started', Date.now()); 43 | app.engine('html', require('swig').renderFile); 44 | 45 | async.series([function createDatabaseDirectory(next) { 46 | // make sure the dbs directory is present 47 | mkdirp(app.get('configDir') + '/dbs/covers', next); 48 | }, function databaseDirectoryCreated(next) { 49 | // attach the db to the app 50 | require(__dirname + '/db.js')(app); 51 | 52 | // patch the app 53 | require(__dirname + '/patches.js')(app); 54 | 55 | // attach the config 56 | app.set('config', require(__dirname + '/config')(app)); 57 | 58 | next(); 59 | }, function setupAuth(next) { 60 | var config = app.get('config'); 61 | 62 | // auth is only intended for use outside of electron 63 | if (config.auth && 64 | config.auth.username !== undefined && 65 | config.auth.password !== undefined && 66 | !process.env.ELECTRON_ENABLED) { 67 | app.use(basicAuth(config.auth.username, config.auth.password)); 68 | } 69 | next(); 70 | }, function setupPassport(next) { 71 | 72 | passport.use(new Strategy( 73 | function(username, password, cb) { 74 | app.db.users.findOne({username: username}, function(err, user) { 75 | if (err) { return cb(err); } 76 | if (!user) { return cb(null, false, {message:"Invalid credentials"}); } 77 | if (user.password != password) { return cb(null, false, {message:"Invalid credentials"}); } 78 | return cb(null, user); 79 | }); 80 | })); 81 | 82 | // Configure Passport persistence. 83 | passport.serializeUser(function(user, cb) { 84 | var sessionUser = user; 85 | cb(null, sessionUser); 86 | }); 87 | 88 | passport.deserializeUser(function(user, cb) { 89 | cb(null, user); 90 | }); 91 | next(); 92 | }, function setupEverythingElse(next) { 93 | // middleware to use in the app 94 | app.use(favicon(__dirname + '/static/images/favicon.ico')); 95 | app.use(bodyParser.urlencoded({extended: false})); 96 | app.use(bodyParser.json()); 97 | app.use(cookieParser()); 98 | app.use(express.session(sessionOpts)); 99 | app.use('/static', express.static(__dirname + '/static')); 100 | 101 | //initialize passport 102 | app.use(passport.initialize()); 103 | app.use(passport.session()); 104 | app.use(require('connect-flash')()); //for error flashes 105 | 106 | // proxy for itunes requests 107 | app.use('/proxy', proxy('https://itunes.apple.com', { 108 | forwardPath: function (req, res) { 109 | return require('url').parse(req.url).path; 110 | }, 111 | })); 112 | 113 | // development only 114 | if (app.get('env') == 'development') { 115 | app.use(errorhandler()); 116 | } 117 | 118 | require(__dirname + '/routes').createRoutes(app); 119 | 120 | app.listen(app.get('port'), function () { 121 | console.log('Express server listening on port ' + app.get('port')); 122 | }); 123 | 124 | next(); 125 | }]); 126 | 127 | module.exports = app; 128 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stretto", 3 | "version": "0.0.2", 4 | "description": "Web based music player", 5 | "main": "server.js", 6 | "private": true, 7 | "dependencies": { 8 | "DateJS": "~1.0.0-rc3", 9 | "backbone": "~1.1.2", 10 | "bootbox": "~4.3.0", 11 | "bootstrap": "~3.3.0", 12 | "bootswatch": "v3.3.0", 13 | "civswig": "civitaslearning/swig-bower#~1.3.2", 14 | "font-awesome-bower": "~4.3.0", 15 | "headjs": "~1.0.3", 16 | "jquery": "~2.1.1", 17 | "jquery.ui": "~1.11.3", 18 | "lastfm-api": "~0.0.1", 19 | "marionette": "~2.2.2", 20 | "messenger": "~1.4.1", 21 | "mustache.js": "~1.1.0", 22 | "seiyria-bootstrap-slider": "~4.5.1", 23 | "tablesort": "https://github.com/tristen/tablesort.git#~3.0.2", 24 | "underscore": "~1.7.0", 25 | "materialize": "^0.97.8" 26 | }, 27 | "resolutions": { 28 | "underscore": "~1.7.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var request = require('request'); 3 | var async = require('async'); 4 | 5 | // export json of config 6 | module.exports = function(app, callback) { 7 | return new Config(app); 8 | }; 9 | 10 | // construct the object 11 | function Config(app) { 12 | var self = this; 13 | this.demo = Boolean(process.env.DEMO || false); 14 | this.initialized = false; 15 | this.music_dir = ''; 16 | this.music_dir_set = false; 17 | this.soundcloud = { 18 | dl_dir: 'soundcloud', 19 | client_id: '062e8dac092fe1ed9e16ee10dd88d566', 20 | }; 21 | this.youtube = { 22 | dl_dir: 'youtube', 23 | quality: 'highest', 24 | api: 'AIzaSyBzVTAJq_j_WsltEa45EUaMHHmXlz8F_PM', 25 | parallel_download: 5, 26 | }; 27 | 28 | // // Uncomment this block to set your own basic http authentication 29 | // this.auth = { 30 | // username: 'username', 31 | // password: 'password', 32 | // }; 33 | 34 | // used for itunes metadata fetching (to select the store to search) 35 | this.country_code = 'us'; 36 | 37 | // work out a default for the music directory 38 | // based on iTunes defaults https://support.apple.com/en-au/HT1391 39 | var home_dir = getUserHome(); 40 | 41 | // this works on all platforms 42 | this.music_dir = path.join(home_dir, 'Music'); 43 | 44 | // execute the two db calls in parrallel 45 | async.parallel([ 46 | function(cb) { 47 | // override the music_dir if it is in the database 48 | app.db.settings.findOne({key: 'music_dir'}, function(err, item) { 49 | if (err) { 50 | console.log('Error fetching music_dir settings: ' + err); 51 | } else if (item && item.value) { 52 | // the music directory has been set in the database 53 | self.music_dir = item.value; 54 | self.music_dir_set = true; 55 | } 56 | 57 | // remove trailing seperator 58 | if (self.music_dir.lastIndexOf(path.sep) == self.music_dir.length - 1) { 59 | self.music_dir = self.music_dir.substr(0, self.music_dir.length - 1); 60 | } 61 | 62 | cb(); 63 | }); 64 | }, 65 | 66 | function(cb) { 67 | // fetch the country code to override 68 | app.db.settings.findOne({key: 'country_code'}, function(err, item) { 69 | if (err) { 70 | console.log('Error fetching country_code settings: ' + err); 71 | cb(); 72 | } else if (item && item.value) { 73 | // if the value returned, override the country_code 74 | self.country_code = item.value; 75 | cb(); 76 | } else { 77 | // fetch the country code 78 | request({url:'http://ipinfo.io', json: true}, function(error, response, body) { 79 | // if the country was returned 80 | if (body && body.country) { 81 | // set it in the database 82 | var country = body.country.toLowerCase(); 83 | app.db.settings.insert({key: 'country_code', value: country}); 84 | self.country_code = country; 85 | } 86 | 87 | cb(); 88 | }); 89 | } 90 | }); 91 | }, 92 | 93 | function (cb) { 94 | app.db.users.findOne({username:'admin'}, function (err, item) { 95 | if(!item) 96 | app.db.users.insert({username:'admin',password:'admin'}); 97 | }); 98 | 99 | cb(); 100 | }, 101 | ], function() { 102 | // set config as initialised and only call the callback if set 103 | self.initialized = true; 104 | if (self.initializedFn) { 105 | self.initializedFn(); 106 | } 107 | } 108 | ); 109 | } 110 | 111 | function getUserHome() { 112 | return process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH; 113 | } 114 | -------------------------------------------------------------------------------- /db.js: -------------------------------------------------------------------------------- 1 | var Datastore = require('nedb'); 2 | 3 | module.exports = function(app) { 4 | app.db = {}; 5 | app.db.songs = new Datastore({ filename: app.get('configDir') + '/dbs/songs.db', autoload: true }); 6 | app.db.playlists = new Datastore({ filename: app.get('configDir') + '/dbs/playlists.db', autoload: true }); 7 | app.db.settings = new Datastore({ filename: app.get('configDir') + '/dbs/settings.db', autoload: true }); 8 | app.db.users = new Datastore({ filename: app.get('configDir') + '/dbs/users.db', autoload: true }); 9 | }; 10 | -------------------------------------------------------------------------------- /electron.js: -------------------------------------------------------------------------------- 1 | var electronApp = require('electron').app; // Module to control application life. 2 | var BrowserWindow = require('electron').BrowserWindow; // Module to create native browser window. 3 | var globalShortcut = require('electron').globalShortcut; 4 | 5 | var mainWindow = null; 6 | electronApp.on('window-all-closed', function() { 7 | // On OS X it is common for applications and their menu bar 8 | // to stay active until the user quits explicitly with Cmd + Q 9 | if (process.platform != 'darwin') { 10 | electronApp.quit(); 11 | } 12 | }); 13 | 14 | // This method will be called when Electron has finished 15 | // initialization and is ready to create browser windows. 16 | electronApp.on('ready', function() { 17 | // Create the browser window. 18 | mainWindow = new BrowserWindow({ 19 | width: 800, 20 | height: 600, 21 | webPreferences: { 22 | nodeIntegration: false, 23 | }, 24 | }); 25 | 26 | // disable the menu entirely 27 | mainWindow.setMenu(null); 28 | 29 | // set the config directory in the users applicatin settings location 30 | process.env.configDir = electronApp.getPath('userData'); 31 | 32 | // let the app know via an env variable we are running electron 33 | process.env.ELECTRON_ENABLED = true; 34 | 35 | // init the server 36 | var app = require(__dirname + '/app.js'); 37 | 38 | // and load the music player page on the server 39 | mainWindow.loadURL('http://localhost:' + app.get('port') + '/'); 40 | 41 | // show the dev tool 42 | if (process.env.DEVTOOLS) { 43 | mainWindow.toggleDevTools(); 44 | } 45 | 46 | // attach the keyboard shortucts 47 | globalShortcut.register('MediaPlayPause', function() { app.io.sockets.emit('command', {command: 'playpause'}); }); 48 | globalShortcut.register('MediaNextTrack', function() { app.io.sockets.emit('command', {command: 'next'}); }); 49 | globalShortcut.register('MediaPreviousTrack', function() { app.io.sockets.emit('command', {command: 'prev'}); }); 50 | 51 | // Emitted when the window is closed. 52 | mainWindow.on('closed', function() { 53 | // Dereference the window object, usually you would store windows 54 | // in an array if your app supports multi windows, this is the time 55 | // when you should delete the corresponding element. 56 | mainWindow = null; 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /electronres/buildpackages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # first run electron-packager and put the output in tmp 3 | echo 'Building Stretto for all platforms' 4 | electron-packager ./ "Stretto" --platform=win32,linux,darwin --arch=all --version=0.37.6 --out=/tmp --overwrite --ignore="dbs|bower_components|electronres" --icon electronres/icon --prune 5 | 6 | # then copy the ffmpeg binaries into them 7 | echo 'Copying ffmpeg binaries to windows builds' 8 | cp electronres/ffmpeg32.exe /tmp/Stretto-win32-ia32/resources/app/ffmpeg.exe 9 | cp electronres/ffmpeg64.exe /tmp/Stretto-win32-x64/resources/app/ffmpeg.exe 10 | 11 | # zip the resulting Stretto folders 12 | echo 'Zipping packages for uploading' 13 | cd /tmp 14 | for d in Stretto-*/; do target=${d%/}; echo "Zipping $target"; zip -qry9 "$target.zip" $d; done; 15 | -------------------------------------------------------------------------------- /electronres/ffmpeg32.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OSDLabs/Encore/ffbfe924621dc3bb7feae081b6319f49001c3b33/electronres/ffmpeg32.exe -------------------------------------------------------------------------------- /electronres/ffmpeg64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OSDLabs/Encore/ffbfe924621dc3bb7feae081b6319f49001c3b33/electronres/ffmpeg64.exe -------------------------------------------------------------------------------- /electronres/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OSDLabs/Encore/ffbfe924621dc3bb7feae081b6319f49001c3b33/electronres/icon.icns -------------------------------------------------------------------------------- /electronres/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OSDLabs/Encore/ffbfe924621dc3bb7feae081b6319f49001c3b33/electronres/icon.ico -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "encore", 3 | "version": "0.0.5", 4 | "main": "electron.js", 5 | "private": true, 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "node app.js", 9 | "nodemon": "nodemon app.js -e 'html css js'", 10 | "postinstall": "./node_modules/grunt-cli/bin/grunt", 11 | "build": "./electronres/buildpackages.sh" 12 | }, 13 | "dependencies": { 14 | "archiver": "^1.0.0", 15 | "async": "*", 16 | "basic-auth-connect": "^1.0.0", 17 | "body-parser": "^1.14.1", 18 | "connect-flash": "^0.1.1", 19 | "cookie-parser": "^1.4.0", 20 | "errorhandler": "^1.4.2", 21 | "express-http-proxy": "^0.3.0", 22 | "express.oi": "0.0.19", 23 | "md5": "*", 24 | "mkdirp": "*", 25 | "mobile-detect": "^0.4.3", 26 | "musicmetadata": "*", 27 | "nedb": "*", 28 | "opener": "*", 29 | "passport": "^0.3.2", 30 | "passport-local": "^1.0.0", 31 | "request": "*", 32 | "serve-favicon": "^2.3.0", 33 | "similar-songs": "^0.1.3", 34 | "song-search": "^0.1.0", 35 | "soundcloud-resolver": "*", 36 | "swig": "*", 37 | "youtube-playlist-info": "^0.1.0", 38 | "ytdl-core": "*" 39 | }, 40 | "optionalDependencies": { 41 | "ffmetadata-ohnx-fork": "*", 42 | "fluent-ffmpeg": "~2.0.0-rc1" 43 | }, 44 | "devDependencies": { 45 | "electron-packager": "^8.2.0", 46 | "grunt": "^0.4.5", 47 | "grunt-bower-install-simple": "^1.2.0", 48 | "grunt-cli": "^0.1.13", 49 | "grunt-contrib-copy": "^0.8.0", 50 | "grunt-contrib-cssmin": "^0.12.2", 51 | "grunt-contrib-uglify": "^0.8.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /patches.js: -------------------------------------------------------------------------------- 1 | /** 2 | * this script patches the db for if you are running a version <= e84e38c (2014-08-14) 3 | * it will run automatically on every launch, but should have relatively 4 | * little overhead. 5 | * 6 | * The comment above applies to the date_added and date_modified components, 7 | * however the below components regarding the disc / track fixes are much 8 | * slower (requires reading all the tracks again). 9 | * This patch relatest to versions <= 367bdeb (2015-11-09) 10 | */ 11 | var async = require('async'); 12 | var MM = require('musicmetadata'); 13 | var fs = require('fs'); 14 | 15 | module.exports = function(app) { 16 | // update the dates 17 | var now_milli = Date.now(); 18 | 19 | // fix the date_added values 20 | app.db.songs.update( 21 | {date_added: {$exists: false}}, 22 | {$set: {date_added: now_milli}}, {multi: true}, function(err, numReplaced) { 23 | if (err) { 24 | console.log(err); 25 | } else { 26 | if (numReplaced > 0) { 27 | console.log('Date added patch applied to: ' + numReplaced + ' rows'); 28 | } 29 | } 30 | }); 31 | 32 | // fix the date_modified values 33 | app.db.songs.update( 34 | {date_modified: {$exists: false}}, 35 | {$set: {date_modified: now_milli}}, {multi: true}, function(err, numReplaced) { 36 | if (err) { 37 | console.log(err); 38 | } else { 39 | if (numReplaced > 0) { 40 | console.log('Date modified patch applied to: ' + numReplaced + ' rows'); 41 | } 42 | } 43 | }); 44 | 45 | // fix the disc / track number fields 46 | app.db.songs.find( 47 | { 48 | $and: [ 49 | {disc: {$exists: false}}, 50 | {track: {$exists: false}}, 51 | ], 52 | }, function(err, documents) { 53 | if (err) { 54 | console.log(err); 55 | } else if (documents.length !== 0) { 56 | // fix the disc and track numbers 57 | // use a queue to run 10 of them in parellel at a time 58 | var queue = async.queue(function eachDocument(document, callback) { 59 | var readStream = fs.createReadStream(app.get('config').music_dir + document.location); 60 | var parser = new MM(readStream, function(err, result) { 61 | // don't wait for nedb to commit, run the next one! 62 | callback(); 63 | console.log('done one' + document._id); 64 | 65 | // force close the read stream to increase performance 66 | // I think musicmetadata must not close it off 67 | readStream.close(); 68 | 69 | // update the attributes 70 | // the object containing attributes to update 71 | var setObj; 72 | 73 | if (!err) { 74 | setObj = { 75 | disc: (result.disk || {no:0}).no || 0, 76 | track: (result.track || {no:0}).no || 0, 77 | }; 78 | } else { 79 | setObj = { 80 | disc: 0, 81 | track: 0, 82 | }; 83 | } 84 | 85 | // commit the update to the database 86 | app.db.songs.update({ _id: document._id }, {$set: setObj}); 87 | }); 88 | }, 4); 89 | 90 | queue.drain = function onFinished(err) { 91 | if (err) { 92 | console.log('Failed to fix disc numbers and durations: ' + err); 93 | } else { 94 | console.log('All disc numbers and durations added'); 95 | } 96 | }; 97 | 98 | // load all the documents into the queue 99 | queue.push(documents); 100 | 101 | // notify the console 102 | console.log('Adding track and disc numbers for ' + documents.length + ' tracks. This will re-read those tracks from the filesystem.'); 103 | } 104 | }); 105 | }; 106 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var util = require(__dirname + '/../util.js'); 2 | var lib_func = require(__dirname + '/../library_functions.js'); 3 | var async = require('async'); 4 | var archiver = require('archiver'); 5 | var os = require('os'); 6 | var opener = require('opener'); 7 | var md5 = require('md5'); 8 | var request = require('request').defaults({ encoding: null }); 9 | var fs = require('fs'); 10 | var path = require('path'); 11 | var passport = require('passport'); 12 | var MobileDetect = require('mobile-detect'); 13 | var similarSongs = require('similar-songs'); 14 | var songSearch = require('song-search'); 15 | 16 | /* 17 | * GET home page. 18 | */ 19 | 20 | app = null; 21 | 22 | exports.createRoutes = function(app_ref) { 23 | app = app_ref; 24 | 25 | // pass the app onto the library_functions module 26 | lib_func.setApp(app); 27 | 28 | app.get('/', musicRoute); 29 | app.get('/remote/:name', musicRoute); 30 | app.get('/songs/:id', sendSong); 31 | app.get('/cover/:id', sendCover); 32 | app.get('/downloadplaylist/:id', downloadPlaylist); 33 | 34 | //adds login facility 35 | app.get('/admin',function (req,res) { res.render('admin',{ msg:req.flash('error'), log: req.user? true : false});}); 36 | //auth using passport 37 | app.post('/admin', passport.authenticate('local', { failureRedirect: '/admin', failureFlash: true }), function(req, res) { 38 | res.redirect('/'); 39 | }); 40 | app.post('/logout', function(req, res) { 41 | req.session.destroy(); 42 | res.redirect('/'); 43 | }); 44 | // remote control commands 45 | app.get('/command/:name/:command', remoteCommand); 46 | app.io.route('player_page_connected', function(req) { req.socket.join('players'); }); 47 | 48 | // remote functions 49 | app.io.route('set_comp_name', setCompName); 50 | 51 | // routes protected in demo 52 | if (!app.get('config').demo) { 53 | // scanning signals 54 | app.io.route('start_scan', function(req) { lib_func.scanLibrary(false); }); 55 | 56 | app.io.route('start_scan_hard', function(req) { lib_func.scanLibrary(true); }); 57 | 58 | app.io.route('hard_rescan', rescanItem); 59 | 60 | // rewrite tags of a track to the file 61 | app.io.route('rewrite_tags', rewriteTags); 62 | 63 | // sync routes 64 | app.io.route('sync_page_connected', syncPageConnected); 65 | app.io.route('sync_playlists', syncPlaylists); 66 | 67 | // soundcloud downloading 68 | app.io.route('soundcloud_download', soundcloudDownload); 69 | 70 | // youtube downloading 71 | app.io.route('youtube_download', youtubeDownload); 72 | 73 | // youtube downloading for bunch of songs with pre-filled info 74 | // (i.e. they were viewing from a mix) 75 | app.io.route('youtube_import', youtubeImport); 76 | 77 | // settings updating 78 | app.io.route('update_settings', updateSettings); 79 | } 80 | 81 | // send the songs to the client 82 | app.io.route('fetch_songs', returnSongs); 83 | 84 | // playlist modifications 85 | app.io.route('fetch_playlists', returnPlaylists); 86 | app.io.route('create_playlist', createPlaylist); 87 | app.io.route('rename_playlist', renamePlaylist); 88 | app.io.route('delete_playlist', deletePlaylist); 89 | app.io.route('add_to_playlist', addToPlaylist); 90 | app.io.route('remove_from_playlist', removeFromPlaylist); 91 | app.io.route('song_moved_in_playlist', songMovedInPlaylist); 92 | 93 | // play count 94 | app.io.route('update_play_count', updatePlayCount); 95 | 96 | // remote control routes 97 | app.io.route('get_receivers', getReceiversMinusThis); 98 | 99 | // open file manager to location 100 | app.io.route('open_dir', function(req) { opener(req.data.substring(0, req.data.lastIndexOf(path.sep))); }); 101 | 102 | // update the info of a song 103 | app.io.route('update_song_info', updateSongInfo); 104 | 105 | // get similar songs 106 | app.io.route('similar_songs', getSimilarSongs); 107 | app.io.route('youtube_search', getYoutubeSongs); 108 | }; 109 | 110 | function musicRoute(req, res) { 111 | // test if client is mobile 112 | md = new MobileDetect(req.headers['user-agent']); 113 | 114 | // get ip (for syncing functions) 115 | util.getip(function(ip) { 116 | // the function to send the homepage only when the config is finished initalizing 117 | var sendHome = function() { 118 | var config = app.get('config'); 119 | var user = req.user ? req.user.username : undefined; 120 | // render the view 121 | res.render((md.mobile() ? 'mobile' : 'index'), { 122 | menu: !md.mobile(), 123 | music_dir: config.music_dir, 124 | music_dir_set: config.music_dir_set, 125 | country_code: config.country_code, 126 | ip: ip + ':' + app.get('port'), 127 | remote_name: req.params.name, 128 | demo: config.demo, 129 | user: user, 130 | log: req.user? true : false, 131 | }); 132 | }; 133 | 134 | // logic to wait for config to initialise 135 | var config = app.get('config'); 136 | if (config.initialized) { 137 | sendHome(); 138 | } else { 139 | config.initializedFn = sendHome; 140 | } 141 | }); 142 | } 143 | 144 | function sendSong(req, res) { 145 | app.db.songs.findOne({_id: req.params.id}, function(err, song) { 146 | if (err || !song) { 147 | res.status(404).send(); 148 | } else { 149 | res.sendFile(path.join(app.get('config').music_dir, song.location)); 150 | } 151 | }); 152 | } 153 | 154 | function sendCover(req, res) { 155 | // if they passed the location of the cover, fetch it 156 | if (req.params.id.length > 0) { 157 | res.sendFile(app.get('configDir') + '/dbs/covers/' + req.params.id); 158 | } else { 159 | res.sendFile(app.get('root') + '/static/images/unknown.png'); 160 | } 161 | } 162 | 163 | function downloadPlaylist(req, res) { 164 | app.db.playlists.findOne({_id: req.params.id}, function(err, playlist) { 165 | if (err) throw err; 166 | 167 | // create zip 168 | var filename = os.tmpdir() + '/download.zip'; 169 | var archive = archiver('zip'); 170 | async.forEach(playlist.songs, function(item, callback) { 171 | app.db.songs.findOne({_id: item._id}, function(err, song) { 172 | if (err) throw err; 173 | var zip_location = song.location; 174 | 175 | var song_location = path.join(app.get('config').music_dir, song.location); 176 | 177 | archive.append(fs.createReadStream(song_location), {name: zip_location}); 178 | callback(); 179 | }); 180 | }, function(err) { 181 | // set the response headers and set the archiver to pipe on finish 182 | res.setHeader('Content-Type', 'application/zip'); 183 | res.setHeader('Content-disposition', 'attachment; filename=download.zip'); 184 | archive.pipe(res); 185 | 186 | // trigger the piping of the archive 187 | archive.finalize(); 188 | }); 189 | }); 190 | } 191 | 192 | function returnSongs(req) { 193 | get_songs(function(songs) { 194 | req.socket.emit('songs', {songs: songs}); 195 | }); 196 | } 197 | 198 | function get_songs(callback) { 199 | app.db.songs.find({}, function(err, songs) { 200 | if (!err) { 201 | callback(songs); 202 | } else { 203 | callback([]); 204 | } 205 | }); 206 | } 207 | 208 | function returnPlaylists(req) { 209 | // send the playlists back to the user 210 | get_playlists(function(playlists) { 211 | req.socket.emit('playlists', {playlists: playlists}); 212 | }); 213 | } 214 | 215 | function get_playlists(callback) { 216 | app.db.playlists.find({}).sort({ title: 1 }).exec(function(err, docs) { 217 | playlists = docs; 218 | 219 | // create a new playlist for the library 220 | getLibraryIds(function(result) { 221 | // add library 222 | playlists.unshift({ 223 | _id: 'LIBRARY', 224 | title: 'Library', 225 | songs: result, 226 | editable: false, 227 | }); 228 | 229 | // add queue 230 | playlists.unshift({ 231 | _id: 'QUEUE', 232 | title: 'Queue', 233 | songs: [], // populated by client 234 | editable: false, 235 | }); 236 | 237 | // send the playlists back to the user 238 | callback(playlists); 239 | }); 240 | }); 241 | } 242 | 243 | // get the _id's of every song in the library 244 | function getLibraryIds(callback) { 245 | app.db.songs.find({}).sort({ title: 1 }).exec(function(err, docs) { 246 | for (var i = 0; i < docs.length; i++) { 247 | docs[i] = {_id: docs[i]._id}; 248 | } 249 | 250 | callback(docs); 251 | }); 252 | } 253 | 254 | function createPlaylist(req) { 255 | plist = { 256 | title: req.data.title, 257 | songs: req.data.songs, 258 | editable: true, 259 | }; 260 | app.db.playlists.insert(plist, function(err, doc) { 261 | req.io.route('fetch_playlists'); 262 | }); 263 | } 264 | 265 | function renamePlaylist(req) { 266 | var id = req.data.id; 267 | var title = req.data.title; 268 | app.db.playlists.update({_id: id}, { $set: {title: title} }, function() { 269 | req.io.route('fetch_playlists'); 270 | }); 271 | } 272 | 273 | function deletePlaylist(req) { 274 | del = req.data.del; 275 | app.db.playlists.remove({_id: del}, {}, function(err, numRemoved) { 276 | req.io.route('fetch_playlists'); 277 | }); 278 | } 279 | 280 | function addToPlaylist(req) { 281 | addItems = req.data.add; 282 | to = req.data.playlist; 283 | 284 | // pull the playlists database 285 | app.db.playlists.findOne({ _id: to}, function(err, doc) { 286 | // used as a counter to count how many still need to be added 287 | waitingOn = 0; 288 | 289 | // prep function for use in loop 290 | var checkFinish = function() { 291 | if (--waitingOn === 0) { 292 | req.io.route('fetch_playlists'); 293 | } 294 | }; 295 | 296 | // run loop 297 | for (var i = 0; i < addItems.length; i++) { 298 | var found = false; 299 | for (var j = 0; j < doc.songs.length; j++) { 300 | if (doc.songs[j]._id == addItems[i]) { 301 | found = true; 302 | break; 303 | } 304 | } 305 | 306 | if (!found) { 307 | waitingOn++; 308 | app.db.playlists.update({_id: to}, { $push:{songs: {_id: addItems[i]}}}, checkFinish); 309 | } 310 | } 311 | }); 312 | } 313 | 314 | function removeFromPlaylist(req) { 315 | removeItems = req.data.remove; 316 | to = req.data.playlist; 317 | app.db.playlists.findOne({ _id: to}, function(err, doc) { 318 | var tmpSongs = []; 319 | for (var i = 0; i < doc.songs.length; i++) { 320 | if (removeItems.indexOf(doc.songs[i]._id) == -1) { 321 | tmpSongs.push(doc.songs[i]); 322 | } 323 | } 324 | 325 | app.db.playlists.update({_id: to}, { $set:{songs: tmpSongs}}, function() { 326 | req.io.route('fetch_playlists'); 327 | }); 328 | }); 329 | } 330 | 331 | function songMovedInPlaylist(req) { 332 | var playlist_id = req.data.playlist_id; 333 | var oldIndex = req.data.oldIndex; 334 | var newIndex = req.data.newIndex; 335 | app.db.playlists.findOne({ _id: playlist_id}, function(err, playlist) { 336 | // remove the item from it's old place 337 | var item = playlist.songs.splice(oldIndex, 1)[0]; 338 | 339 | // add the item into it's new place 340 | playlist.songs.splice(newIndex, 0, item); 341 | 342 | // update the playlist with the new order 343 | app.db.playlists.update({_id: playlist_id}, { $set:{songs: playlist.songs}}); 344 | }); 345 | } 346 | 347 | // update song play count 348 | function updatePlayCount(req) { 349 | var _id = req.data.track_id; 350 | var plays = req.data.plays; 351 | 352 | // apply the new play_count to the database 353 | app.db.songs.update({_id: _id}, { $set: { play_count: plays } }); 354 | } 355 | 356 | // force rescan of a set of items 357 | function rescanItem(req) { 358 | var items = req.data.items; 359 | var songLocArr = []; 360 | app.db.songs.find({ _id: { $in: items }}, function(err, songs) { 361 | if (!err && songs) 362 | 363 | // add the location to the list of songs to scan 364 | for (var i = 0; i < songs.length; i++) { 365 | songLocArr.push(songs[i].location); 366 | } 367 | 368 | lib_func.scanItems(songLocArr); 369 | }); 370 | } 371 | 372 | // update the details for a song 373 | function updateSongInfo(req) { 374 | // update the details about the song in the database (minus the cover) 375 | app.db.songs.update({_id: req.data._id}, { 376 | $set: { 377 | title: req.data.title, 378 | display_artist: req.data.artist, 379 | album: req.data.album, 380 | 381 | // it has been modified, update it 382 | date_modified: Date.now(), 383 | }, 384 | }); 385 | 386 | // update the cover photo 387 | var cover = req.data.cover; 388 | if (cover !== null) { 389 | // function to be called by both download file and file upload methods 390 | var process_cover = function(type, content_buffer) { 391 | var cover_filename = md5(content_buffer) + '.' + type; 392 | var location = app.get('configDir') + '/dbs/covers/' + cover_filename; 393 | fs.exists(location, function(exists) { 394 | if (!exists) { 395 | fs.writeFile(location, content_buffer, function(err) { 396 | if (err) { 397 | console.log(err); 398 | } else { 399 | console.log('Wrote cover here: ' + location); 400 | } 401 | }); 402 | } 403 | 404 | // update the songs cover even if the image already exists 405 | app.db.songs.update({display_artist: req.data.artist, album: req.data.album}, { 406 | $set: { 407 | cover_location: cover_filename, 408 | }, 409 | }, { multi: true }, function(err, numReplaced) { 410 | app.db.songs.find({display_artist: req.data.artist, album: req.data.album}, function(err, tracks) { 411 | app.io.sockets.emit('song_update', tracks); 412 | }); 413 | }); 414 | }); 415 | }; 416 | 417 | // different methods of setting cover 418 | if (req.data.cover_is_url && req.data.cover_is_lastfm) { 419 | // they fetched the cover from lastfm 420 | request(cover, function(error, response, body) { 421 | if (!error && response.statusCode == 200) { 422 | var type = response.headers['content-type'].split('/').pop(); 423 | process_cover(type, body); 424 | } 425 | }); 426 | } else { 427 | // they dropped a file to upload 428 | var image = util.decodeBase64Image(cover); 429 | process_cover(image.type, image.data); 430 | } 431 | } 432 | } 433 | 434 | // write the tags (metadata) from the database to the files for the given items 435 | function rewriteTags(req) { 436 | var items = req.data.items; 437 | app.db.songs.find({ _id: { $in: items }}, function(err, songs) { 438 | if (!err && songs) { 439 | // write the tags for all the given files 440 | for (var i in songs) { 441 | lib_func.saveID3(songs[i]); 442 | } 443 | } 444 | }); 445 | } 446 | 447 | // controller routes 448 | 449 | function remoteCommand(req, res) { 450 | // get params 451 | var command = req.params.command; 452 | var name = req.params.name; 453 | 454 | // bool to see if server was found 455 | var command_sent = false; 456 | 457 | // find destination machine and send the command 458 | var receivers = getReceiverList(); 459 | for (var index in receivers) { 460 | var client = receivers[index]; 461 | if (client.name && 462 | client.name.length > 0 && 463 | client.name == name) { 464 | client.emit('command', {command: command}); 465 | 466 | // mark it as sent 467 | command_sent = true; 468 | } 469 | } 470 | 471 | if (command_sent) { 472 | res.send('OK'); 473 | } else { 474 | res.send('NAME_NOT_FOUND'); 475 | } 476 | } 477 | 478 | function setCompName(req) { 479 | req.socket.join('receivers'); 480 | req.socket.name = req.data.name; 481 | } 482 | 483 | function getReceiverList(req) { 484 | var namespace = '/'; 485 | var receiversRoom = 'receivers'; 486 | var users = []; 487 | 488 | for (var id in app.io.of(namespace).adapter.rooms[receiversRoom].sockets) { 489 | users.push(app.io.of(namespace).adapter.nsp.connected[id]); 490 | } 491 | 492 | return users; 493 | } 494 | 495 | function getReceiversMinusThis(req) { 496 | var receivers = getReceiverList(); 497 | var validReceivers = []; 498 | for (var index in receivers) { 499 | var client = receivers[index]; 500 | if (client.name && client.name.length > 0) { 501 | validReceivers.push({ 502 | id: client.id, 503 | name: client.name, 504 | }); 505 | } 506 | } 507 | 508 | req.socket.emit('recievers', {recievers: validReceivers}); 509 | } 510 | 511 | // sync routes 512 | function syncPageConnected(req) { 513 | // get the playlist data 514 | get_playlists(function(playlists) { 515 | // get the songs data 516 | get_songs(function(songs) { 517 | req.socket.emit('alldata', {playlists: playlists, songs: songs}); 518 | }); 519 | }); 520 | } 521 | 522 | // the playlists to sync have been selected, sync them 523 | function syncPlaylists(req) { 524 | var lists = req.data.playlists; 525 | 526 | // function for within loop 527 | var playlistResult = function(err, numReplaced, newDoc) { 528 | if (newDoc) { 529 | console.log('Inserted playlist: ' + newDoc.title); 530 | } else { 531 | console.log('Updated playlist'); 532 | } 533 | }; 534 | 535 | // loop over lists to join editable lists 536 | for (var list_cnt = 0; list_cnt < lists.length; list_cnt++) { 537 | // attempt to replace the playlist if it is editable 538 | if (lists[list_cnt].editable) { 539 | app.db.playlists.update({_id: lists[list_cnt]._id}, lists[list_cnt], {upsert: true}, playlistResult); 540 | } 541 | } 542 | 543 | var songs = req.data.songs; 544 | var remote_url = req.data.remote_url; 545 | lib_func.sync_import(songs, remote_url); 546 | } 547 | 548 | // download the soundcloud songs from the requested url 549 | function soundcloudDownload(req) { 550 | lib_func.scDownload(req.data.url); 551 | } 552 | 553 | // download the youtube song 554 | function youtubeDownload(req) { 555 | lib_func.ytDownload({url: req.data.url}); 556 | } 557 | 558 | function youtubeImport(req) { 559 | var queue = async.queue(function(result, next) { 560 | // augment the song info object with the url needed 561 | result.url = 'https://www.youtube.com/watch?v=' + result.youtube_id; 562 | lib_func.ytDownload(result, next); 563 | }, app.get('config').youtube.parallel_download); 564 | 565 | // add all the items to the queue 566 | queue.push(req.data.songs); 567 | } 568 | 569 | // update the app settings 570 | function updateSettings(req) { 571 | // emit the settings updated message to the client 572 | req.socket.emit('message', {message: 'Settings Updated'}); 573 | 574 | // update the settings 575 | if (req.data.music_dir) { 576 | // first remove all music_dir settings 577 | app.db.settings.remove({key: 'music_dir'}, {multi: true}, function() { 578 | // add a new one in 579 | app.db.settings.insert({key: 'music_dir', value: req.data.music_dir}, function() { 580 | // update config 581 | app.get('config').music_dir = req.data.music_dir; 582 | app.get('config').music_dir_set = true; 583 | }); 584 | }); 585 | } 586 | 587 | if (req.data.country_code) { 588 | // first remove all country_code settings 589 | app.db.settings.remove({key: 'country_code'}, {multi: true}, function() { 590 | // add a new one in 591 | app.db.settings.insert({key: 'country_code', value: req.data.country_code}, function() { 592 | // update config 593 | app.get('config').country_code = req.data.country_code; 594 | }); 595 | }); 596 | } 597 | } 598 | 599 | // fetch the similar songs for a given title and artist 600 | function getSimilarSongs(req) { 601 | var title = req.data.title; 602 | var artist = req.data.artist; 603 | 604 | similarSongs.find({ 605 | title: title, 606 | artist: artist, 607 | limit: req.data.limit || 50, 608 | lastfmAPIKey: '4795cbddcdec3a6b3f622235caa4b504', 609 | lastfmAPISecret: 'cbe22daa03f35df599746f590bf015a5', 610 | youtubeAPIKey: app.get('config').youtube.api, 611 | }, function(err, songs) { 612 | if (err) { 613 | console.log(err); 614 | 615 | // if it couldn't find it, just pass that through 616 | if (err.message == 'Track not found') { 617 | err = null; 618 | songs = []; 619 | } 620 | } 621 | 622 | req.socket.emit('similar_songs', { 623 | error: err, 624 | songs: songs, 625 | url: 'mix/' + encodeURIComponent(title), 626 | title: "Instant mix for: '" + title + "' by '" + artist + "'", 627 | }); 628 | }); 629 | } 630 | 631 | function getYoutubeSongs(req) { 632 | var search = req.data.search; 633 | 634 | songSearch.search({ 635 | search: search, 636 | limit: req.data.limit || 50, 637 | itunesCountry: app.get('config').country_code, 638 | youtubeAPIKey: app.get('config').youtube.api, 639 | }, function(err, songs) { 640 | if (err) { 641 | console.log(err); 642 | songs = []; 643 | return; 644 | } 645 | 646 | req.socket.emit('similar_songs', { 647 | error: err, 648 | songs: songs, 649 | url: 'searchyt/' + encodeURIComponent(search), 650 | title: "Youtube listings for: '" + search + "'", 651 | }); 652 | }); 653 | } 654 | -------------------------------------------------------------------------------- /static/css/main.css: -------------------------------------------------------------------------------- 1 | body, 2 | html {} 3 | body { 4 | padding-top: 32px; 5 | font-family: 'Open Sans', sans-serif; 6 | } 7 | #wrapper { 8 | width: 100%; 9 | height: 100%; 10 | position: fixed; 11 | top: 0; 12 | left: 0; 13 | outline: 0; 14 | } 15 | .content-body { 16 | padding: 20px; 17 | } 18 | #content { 19 | height: 90%; 20 | height: calc(100% - 93px); 21 | margin-top: 31px; 22 | /*margin-left: 200px;*/ 23 | margin-left: 0px; 24 | padding: 20px; 25 | padding-bottom: 0px; 26 | } 27 | #content > div { 28 | height: 100%; 29 | overflow-y: auto; 30 | } 31 | #content > div > h1 { 32 | margin-top: 10px; 33 | } 34 | #sidebar { 35 | margin-top: 50px; 36 | padding-bottom: 60px; 37 | /*width: 200px;*/ 38 | width: 0px; 39 | height: 100%; 40 | position: fixed; 41 | left: 0; 42 | box-shadow: 0 0 20px #ccc; 43 | } 44 | .sidebar_controls { 45 | text-align: center; 46 | width: 100%; 47 | } 48 | #sidebar .input-group { 49 | margin: 10px; 50 | margin-bottom: 0; 51 | } 52 | #sidebar h3 { 53 | padding-left: 10px; 54 | } 55 | #sidebar_height_wrapper { 56 | height: 80%; 57 | height: calc(100% - 150px); 58 | } 59 | #sidebar_height_wrapper > ul { 60 | height: 90%; 61 | height: calc(100% - 50px); 62 | overflow-y: hidden; 63 | } 64 | #sidebar_height_wrapper > ul:hover { 65 | overflow-y: scroll; 66 | } 67 | #bottom_bar { 68 | position: fixed; 69 | bottom: 0; 70 | height: 61px; 71 | width: 100%; 72 | background-color: rgb(235, 235, 235); 73 | box-shadow: 0 0 30px #ccc; 74 | padding-top: 2px; 75 | -webkit-transform: translateZ(0); 76 | } 77 | #bottom_bar:focus { 78 | outline: 0; 79 | } 80 | .duration, 81 | .current_time { 82 | position: fixed; 83 | pointer-events: none; 84 | display: none; 85 | bottom: 63px; 86 | } 87 | .duration { 88 | right: 0; 89 | } 90 | .current_time { 91 | left: 0; 92 | } 93 | #bottom_bar:hover .current_time, 94 | #bottom_bar:hover .duration { 95 | display: block; 96 | background-color: rgb(235, 235, 235); 97 | ; 98 | z-index: 2; 99 | padding: 2px; 100 | color: #fff; 101 | } 102 | #bottom_bar:hover .duration { 103 | padding-left: 5px; 104 | border-top-left-radius: 5px; 105 | } 106 | #bottom_bar:hover .current_time { 107 | padding-right: 5px; 108 | border-top-right-radius: 5px; 109 | } 110 | .slider-track { 111 | border-radius: 0px; 112 | } 113 | .disable_selection { 114 | -webkit-touch-callout: none; 115 | -webkit-user-select: none; 116 | -moz-user-select: none; 117 | -ms-user-select: none; 118 | user-select: none; 119 | } 120 | .song_table { 121 | table-layout: fixed; 122 | margin-bottom: 0; 123 | } 124 | .song_table tbody > tr > td { 125 | padding: 0 !important; 126 | line-height: 40px; 127 | vertical-align: middle; 128 | overflow: hidden; 129 | white-space: nowrap; 130 | text-overflow: ellipsis; 131 | cursor: pointer; 132 | } 133 | tr:hover { 134 | background-color: #eeeeee; 135 | } 136 | .light-blue:hover { 137 | background-color: #aaa; 138 | } 139 | tr:hover .options { 140 | display: inline-block; 141 | } 142 | .options { 143 | display: none; 144 | } 145 | .option_div { 146 | float: right; 147 | vertical-align: middle; 148 | margin-right: 10px; 149 | position: relative; 150 | z-index: 1; 151 | } 152 | .options:hover { 153 | color: #444; 154 | } 155 | .options-menu { 156 | max-height: 300px; 157 | overflow-y: auto; 158 | } 159 | .song_text { 160 | z-index: 0; 161 | } 162 | .colsearch:hover { 163 | text-decoration: underline; 164 | } 165 | .unknown_cover { 166 | width: 40px; 167 | height: 40px; 168 | margin-right: 10px; 169 | } 170 | .cover { 171 | width: 40px; 172 | height: 40px; 173 | margin-right: 10px; 174 | } 175 | .popover_cover { 176 | max-width: 400px; 177 | max-height: 400px; 178 | border-radius: 20px; 179 | } 180 | .drop_placeholder { 181 | float: right; 182 | width: 270px; 183 | height: 40px; 184 | line-height: 38px; 185 | text-align: center; 186 | border: 2px dashed #ccc; 187 | } 188 | .dropping_file { 189 | position: absolute; 190 | left: 0; 191 | top: 0; 192 | width: 100%; 193 | height: 100%; 194 | z-index: 10000; 195 | font-size: 40px; 196 | padding-top: 100px; 197 | background-color: rgba(0, 0, 0, 0.5); 198 | color: white; 199 | text-align: center; 200 | } 201 | 202 | /*** bottom bar items ***/ 203 | 204 | .controls { 205 | float: left; 206 | width: 323px; 207 | height: 100%; 208 | margin: 0; 209 | margin-left: -161px; 210 | padding: 0; 211 | background-color: rgb(235, 235, 235); 212 | box-shadow: -10px 0px 5px #fff; 213 | } 214 | .controls .fa { 215 | padding-left: 10px; 216 | padding-right: 10px; 217 | padding-top: 10px; 218 | height: 100%; 219 | } 220 | .controls .fa:hover { 221 | background-color: #ddd; 222 | } 223 | #settings_bar { 224 | float: right; 225 | padding: 9px; 226 | } 227 | #repeat_badge { 228 | position: absolute; 229 | margin-left: -25px; 230 | margin-top: 5px; 231 | background-color: #444; 232 | } 233 | #scrub_bar_slider { 234 | width: 100%; 235 | position: fixed; 236 | bottom: 55px; 237 | left: 0px; 238 | z-index: 1; 239 | } 240 | #vol_bar_slider { 241 | margin-right: 20px; 242 | } 243 | .volume_icon { 244 | vertical-align: middle; 245 | margin-right: 5px; 246 | } 247 | 248 | /*** slider changes ***/ 249 | 250 | .slider.slider-horizontal:hover .slider-track { 251 | background: #ddd; 252 | } 253 | .slider.slider-horizontal .slider-track { 254 | height: 5px; 255 | background: #eee; 256 | -webkit-transition: background 1s; 257 | transition: background 1s; 258 | margin-top: 2px; 259 | margin-bottom: 2px; 260 | } 261 | .slider-selection { 262 | background: #444; 263 | } 264 | .slider.slider-horizontal .slider-handle { 265 | opacity: 1; 266 | margin-top: -7px; 267 | background: #ddd; 268 | } 269 | 270 | /*** info ***/ 271 | 272 | #current_info { 273 | float: left; 274 | width: 50%; 275 | text-align: left; 276 | white-space: nowrap; 277 | } 278 | .info_cover { 279 | float: left; 280 | width: 60px; 281 | height: 60px; 282 | margin-right: 10px; 283 | } 284 | .detailed_info_cover { 285 | max-width: 200px; 286 | max-height: 200px; 287 | } 288 | 289 | /* override popove max width */ 290 | 291 | .popover { 292 | max-width: 1000px; 293 | } 294 | .info_unknown_cover { 295 | float: left; 296 | font-size: 60px; 297 | margin-right: 10px; 298 | } 299 | .info_title { 300 | margin-bottom: 0; 301 | font-size: 18px; 302 | text-align: center; 303 | padding-top: 3px; 304 | } 305 | .info_detail { 306 | font-size: 12px; 307 | color: #999; 308 | padding-top: 3px; 309 | } 310 | .info_wrapper { 311 | display: inline-block; 312 | background-color: rgb(235, 235, 235); 313 | } 314 | .info_options { 315 | margin-left: 20px; 316 | } 317 | .info_options:hover { 318 | color: #444; 319 | } 320 | .modal-footer { 321 | margin-top: 0; 322 | } 323 | 324 | /*** options dialog ***/ 325 | 326 | .options_container { 327 | position: fixed; 328 | z-index: 1030; 329 | } 330 | 331 | /*** color definitions ***/ 332 | 333 | .blue { 334 | color: #444; 335 | } 336 | .light-blue { 337 | background-color: #aaa; 338 | } 339 | .selected { 340 | background-color: #aaa; 341 | } 342 | 343 | /*** custom scrollbar ***/ 344 | 345 | .custom_scrollbar::-webkit-scrollbar { 346 | width: 10px; 347 | height: 10px; 348 | } 349 | .custom_scrollbar::-webkit-scrollbar-track { 350 | background-color: rgba(113, 112, 107, 0.1); 351 | -webkit-border-radius: 5px; 352 | } 353 | .custom_scrollbar::-webkit-scrollbar-thumb:vertical { 354 | background-color: rgba(0, 0, 0, .2); 355 | -webkit-border-radius: 6px; 356 | } 357 | .custom_scrollbar::-webkit-scrollbar-thumb:vertical:hover, 358 | .custom_scrollbar::-webkit-scrollbar-thumb:horizontal:hover { 359 | background: #444; 360 | } 361 | .custom_scrollbar::-webkit-scrollbar-thumb:horizontal { 362 | background-color: rgba(0, 0, 0, .2); 363 | -webkit-border-radius: 6px; 364 | } 365 | 366 | /*** th widths ***/ 367 | 368 | .duration_th { 369 | width: 50px; 370 | } 371 | .play_count_th { 372 | width: 100px; 373 | } 374 | .index_th { 375 | width: 55px; 376 | text-align: right; 377 | } 378 | .index_text { 379 | padding-right: 10px; 380 | } 381 | 382 | /*** sync page ***/ 383 | 384 | .form-container { 385 | max-width: 1000px; 386 | } 387 | .sync_div { 388 | visibility: hidden; 389 | } 390 | .back_to_songs { 391 | margin-right: 10px; 392 | } 393 | 394 | /*** custom generic classes ***/ 395 | 396 | .pointer { 397 | cursor: pointer; 398 | } 399 | #search-overlay { 400 | position: fixed; 401 | width: 100%; 402 | height: 100%; 403 | top: 0; 404 | left: 0; 405 | bottom: 0; 406 | right: 0; 407 | z-index: 1005; 408 | background-color: transparent; 409 | display: none; 410 | -webkit-transition: display 1s; 411 | transition: display 1s; 412 | overflow-y: scroll; 413 | } 414 | #search-overlay > #input { 415 | background: none; 416 | border: none; 417 | margin-left: 15%; 418 | font-size: 6em; 419 | margin-top: 10%; 420 | width: 70%; 421 | outline: none; 422 | } 423 | #search-overlay > #instructions { 424 | margin-top: 3%; 425 | margin-left: 15%; 426 | color: #4d4d4d; 427 | font-family: Helvetica, sans-serif; 428 | } 429 | .blur { 430 | -webkit-filter: url(#blue-tint) blur(5px); 431 | filter: url(#blue-tint) blur(5px); 432 | } 433 | #search-button { 434 | -webkit-filter: none !important; 435 | filter: none !important; 436 | } 437 | #overlay-search-button { 438 | position: absolute; 439 | top: 10%; 440 | right: 15%; 441 | z-index: 1010; 442 | display: inline; 443 | } 444 | #link { 445 | background: url(../images/cross.svg); 446 | background-repeat: no-repeat; 447 | background-size: contain; 448 | background-position: center; 449 | width: 50px; 450 | height: 50px; 451 | display: block; 452 | /* A little hack to displace the text, so that only the SVG is seen*/ 453 | text-indent: -10000px; 454 | -webkit-transition: background 1s; 455 | transition: background 1s; 456 | } 457 | -------------------------------------------------------------------------------- /static/css/main1.css: -------------------------------------------------------------------------------- 1 | body, 2 | html {} 3 | body { 4 | padding-top: 32px; 5 | font-family: 'Open Sans', sans-serif; 6 | } 7 | #wrapper { 8 | width: 100%; 9 | height: 100%; 10 | position: fixed; 11 | top: 0; 12 | left: 0; 13 | outline: 0; 14 | } 15 | .content-body { 16 | padding: 20px; 17 | } 18 | #content { 19 | height: 90%; 20 | height: calc(100% - 93px); 21 | margin-top: 31px; 22 | margin-left: 200px; 23 | padding: 20px; 24 | padding-bottom: 0px; 25 | } 26 | #content > div { 27 | height: 100%; 28 | overflow-y: auto; 29 | } 30 | #content > div > h1 { 31 | margin-top: 10px; 32 | } 33 | #sidebar { 34 | margin-top: 50px; 35 | padding-bottom: 60px; 36 | width: 200px; 37 | height: 100%; 38 | position: fixed; 39 | left: 0; 40 | box-shadow: 0 0 20px #ccc; 41 | } 42 | .sidebar_controls { 43 | text-align: center; 44 | width: 100%; 45 | } 46 | #sidebar .input-group { 47 | margin: 10px; 48 | margin-bottom: 0; 49 | } 50 | #sidebar h3 { 51 | padding-left: 10px; 52 | } 53 | #sidebar_height_wrapper { 54 | height: 80%; 55 | height: calc(100% - 150px); 56 | } 57 | #sidebar_height_wrapper > ul { 58 | height: 90%; 59 | height: calc(100% - 50px); 60 | overflow-y: hidden; 61 | } 62 | #sidebar_height_wrapper > ul:hover { 63 | overflow-y: scroll; 64 | } 65 | #bottom_bar { 66 | position: fixed; 67 | bottom: 0; 68 | height: 61px; 69 | width: 100%; 70 | background-color: rgb(235, 235, 235); 71 | box-shadow: 0 0 30px #ccc; 72 | padding-top: 2px; 73 | -webkit-transform: translateZ(0); 74 | } 75 | #bottom_bar:focus { 76 | outline: 0; 77 | } 78 | .duration, 79 | .current_time { 80 | position: fixed; 81 | pointer-events: none; 82 | display: none; 83 | bottom: 63px; 84 | } 85 | .duration { 86 | right: 0; 87 | } 88 | .current_time { 89 | left: 0; 90 | } 91 | #bottom_bar:hover .current_time, 92 | #bottom_bar:hover .duration { 93 | display: block; 94 | background-color: rgb(235, 235, 235); 95 | ; 96 | z-index: 2; 97 | padding: 2px; 98 | color: #fff; 99 | } 100 | #bottom_bar:hover .duration { 101 | padding-left: 5px; 102 | border-top-left-radius: 5px; 103 | } 104 | #bottom_bar:hover .current_time { 105 | padding-right: 5px; 106 | border-top-right-radius: 5px; 107 | } 108 | .slider-track { 109 | border-radius: 0px; 110 | } 111 | .disable_selection { 112 | -webkit-touch-callout: none; 113 | -webkit-user-select: none; 114 | -moz-user-select: none; 115 | -ms-user-select: none; 116 | user-select: none; 117 | } 118 | .song_table { 119 | table-layout: fixed; 120 | margin-bottom: 0; 121 | } 122 | .song_table tbody > tr > td { 123 | padding: 0 !important; 124 | line-height: 40px; 125 | vertical-align: middle; 126 | overflow: hidden; 127 | white-space: nowrap; 128 | text-overflow: ellipsis; 129 | } 130 | tr:hover { 131 | background-color: #eeeeee; 132 | } 133 | .light-blue:hover { 134 | background-color: #aaa; 135 | } 136 | tr:hover .options { 137 | display: inline-block; 138 | } 139 | .options { 140 | display: none; 141 | } 142 | .option_div { 143 | float: right; 144 | vertical-align: middle; 145 | margin-right: 10px; 146 | position: relative; 147 | z-index: 1; 148 | } 149 | .options:hover { 150 | color: #444; 151 | } 152 | .options-menu { 153 | max-height: 300px; 154 | overflow-y: auto; 155 | } 156 | .song_text { 157 | z-index: 0; 158 | } 159 | .colsearch:hover { 160 | text-decoration: underline; 161 | } 162 | .unknown_cover { 163 | width: 40px; 164 | height: 40px; 165 | margin-right: 10px; 166 | } 167 | .cover { 168 | width: 40px; 169 | height: 40px; 170 | margin-right: 10px; 171 | } 172 | .popover_cover { 173 | max-width: 400px; 174 | max-height: 400px; 175 | border-radius: 20px; 176 | } 177 | .drop_placeholder { 178 | float: right; 179 | width: 270px; 180 | height: 40px; 181 | line-height: 38px; 182 | text-align: center; 183 | border: 2px dashed #ccc; 184 | } 185 | .dropping_file { 186 | position: absolute; 187 | left: 0; 188 | top: 0; 189 | width: 100%; 190 | height: 100%; 191 | z-index: 10000; 192 | font-size: 40px; 193 | padding-top: 100px; 194 | background-color: rgba(0, 0, 0, 0.5); 195 | color: white; 196 | text-align: center; 197 | } 198 | 199 | /*** bottom bar items ***/ 200 | 201 | .controls { 202 | float: left; 203 | width: 323px; 204 | height: 100%; 205 | margin: 0; 206 | margin-left: -161px; 207 | padding: 0; 208 | background-color: rgb(235, 235, 235); 209 | box-shadow: -10px 0px 5px #fff; 210 | } 211 | .controls .fa { 212 | padding-left: 10px; 213 | padding-right: 10px; 214 | padding-top: 10px; 215 | height: 100%; 216 | } 217 | .controls .fa:hover { 218 | background-color: #ddd; 219 | } 220 | #settings_bar { 221 | float: right; 222 | padding: 9px; 223 | } 224 | #repeat_badge { 225 | position: absolute; 226 | margin-left: -25px; 227 | margin-top: 5px; 228 | background-color: #444; 229 | } 230 | #scrub_bar_slider { 231 | width: 100%; 232 | position: fixed; 233 | bottom: 55px; 234 | left: 0px; 235 | z-index: 1; 236 | } 237 | #vol_bar_slider { 238 | margin-right: 20px; 239 | } 240 | .volume_icon { 241 | vertical-align: middle; 242 | margin-right: 5px; 243 | } 244 | 245 | /*** slider changes ***/ 246 | 247 | .slider.slider-horizontal:hover .slider-track { 248 | background: #ddd; 249 | } 250 | .slider.slider-horizontal .slider-track { 251 | height: 5px; 252 | background: #eee; 253 | -webkit-transition: background 1s; 254 | transition: background 1s; 255 | margin-top: 2px; 256 | margin-bottom: 2px; 257 | } 258 | .slider-selection { 259 | background: #444; 260 | } 261 | .slider.slider-horizontal .slider-handle { 262 | opacity: 1; 263 | margin-top: -7px; 264 | background: #ddd; 265 | } 266 | 267 | /*** info ***/ 268 | 269 | #current_info { 270 | float: left; 271 | width: 50%; 272 | text-align: left; 273 | white-space: nowrap; 274 | } 275 | .info_cover { 276 | float: left; 277 | width: 60px; 278 | height: 60px; 279 | margin-right: 10px; 280 | } 281 | .detailed_info_cover { 282 | max-width: 200px; 283 | max-height: 200px; 284 | } 285 | 286 | /* override popove max width */ 287 | 288 | .popover { 289 | max-width: 1000px; 290 | } 291 | .info_unknown_cover { 292 | float: left; 293 | font-size: 60px; 294 | margin-right: 10px; 295 | } 296 | .info_title { 297 | margin-bottom: 0; 298 | font-size: 18px; 299 | text-align: center; 300 | padding-top: 3px; 301 | } 302 | .info_detail { 303 | font-size: 12px; 304 | color: #999; 305 | padding-top: 3px; 306 | } 307 | .info_wrapper { 308 | display: inline-block; 309 | background-color: rgb(235, 235, 235); 310 | } 311 | .info_options { 312 | margin-left: 20px; 313 | } 314 | .info_options:hover { 315 | color: #444; 316 | } 317 | .modal-footer { 318 | margin-top: 0; 319 | } 320 | 321 | /*** options dialog ***/ 322 | 323 | .options_container { 324 | position: fixed; 325 | z-index: 1030; 326 | } 327 | 328 | /*** color definitions ***/ 329 | 330 | .blue { 331 | color: #444; 332 | } 333 | .light-blue { 334 | background-color: #aaa; 335 | } 336 | .selected { 337 | background-color: #aaa; 338 | } 339 | 340 | /*** custom scrollbar ***/ 341 | 342 | .custom_scrollbar::-webkit-scrollbar { 343 | width: 10px; 344 | height: 10px; 345 | } 346 | .custom_scrollbar::-webkit-scrollbar-track { 347 | background-color: rgba(113, 112, 107, 0.1); 348 | -webkit-border-radius: 5px; 349 | } 350 | .custom_scrollbar::-webkit-scrollbar-thumb:vertical { 351 | background-color: rgba(0, 0, 0, .2); 352 | -webkit-border-radius: 6px; 353 | } 354 | .custom_scrollbar::-webkit-scrollbar-thumb:vertical:hover, 355 | .custom_scrollbar::-webkit-scrollbar-thumb:horizontal:hover { 356 | background: #444; 357 | } 358 | .custom_scrollbar::-webkit-scrollbar-thumb:horizontal { 359 | background-color: rgba(0, 0, 0, .2); 360 | -webkit-border-radius: 6px; 361 | } 362 | 363 | /*** th widths ***/ 364 | 365 | .duration_th { 366 | width: 50px; 367 | } 368 | .play_count_th { 369 | width: 100px; 370 | } 371 | .index_th { 372 | width: 55px; 373 | text-align: right; 374 | } 375 | .index_text { 376 | padding-right: 10px; 377 | } 378 | 379 | /*** sync page ***/ 380 | 381 | .form-container { 382 | max-width: 1000px; 383 | } 384 | .sync_div { 385 | visibility: hidden; 386 | } 387 | .back_to_songs { 388 | margin-right: 10px; 389 | } 390 | 391 | /*** custom generic classes ***/ 392 | 393 | .pointer { 394 | cursor: pointer; 395 | } 396 | #search-overlay { 397 | position: fixed; 398 | width: 100%; 399 | height: 100%; 400 | top: 0; 401 | left: 0; 402 | bottom: 0; 403 | right: 0; 404 | z-index: 1005; 405 | background-color: transparent; 406 | display: none; 407 | -webkit-transition: display 1s; 408 | transition: display 1s; 409 | overflow-y: scroll; 410 | } 411 | #search-overlay > #input { 412 | background: none; 413 | border: none; 414 | margin-left: 15%; 415 | font-size: 6em; 416 | margin-top: 10%; 417 | width: 70%; 418 | outline: none; 419 | } 420 | #search-overlay > #instructions { 421 | margin-top: 3%; 422 | margin-left: 15%; 423 | color: #4d4d4d; 424 | font-family: Helvetica, sans-serif; 425 | } 426 | .blur { 427 | -webkit-filter: url(#blue-tint) blur(5px); 428 | filter: url(#blue-tint) blur(5px); 429 | } 430 | #search-button { 431 | -webkit-filter: none !important; 432 | filter: none !important; 433 | } 434 | #overlay-search-button { 435 | position: absolute; 436 | top: 10%; 437 | right: 15%; 438 | z-index: 1010; 439 | display: inline; 440 | } 441 | #link { 442 | background: url(../images/cross.svg); 443 | background-repeat: no-repeat; 444 | background-size: contain; 445 | background-position: center; 446 | width: 50px; 447 | height: 50px; 448 | display: block; 449 | /* A little hack to displace the text, so that only the SVG is seen*/ 450 | text-indent: -10000px; 451 | -webkit-transition: background 1s; 452 | transition: background 1s; 453 | } 454 | -------------------------------------------------------------------------------- /static/css/mobile.css: -------------------------------------------------------------------------------- 1 | #wrapper{ 2 | width: 100%; 3 | position: fixed; 4 | overflow-x: none; 5 | overflow-y: scroll; 6 | top: 0; 7 | left: 0; 8 | outline: 0; 9 | } 10 | .controls{ 11 | float: none; 12 | width: 323px; 13 | height: 100%; 14 | margin: 0 auto; 15 | } 16 | #sidebar{ 17 | clear: both; 18 | margin-top: 60px; 19 | width: 100%; 20 | height: auto; 21 | position: relative; 22 | padding-bottom: 10px; 23 | box-shadow: none; 24 | border-bottom: 1px solid #eee; 25 | border-top: 1px solid #eee; 26 | } 27 | #sidebar_height_wrapper{ 28 | height: auto; 29 | } 30 | #sidebar_height_wrapper > ul{ 31 | height: auto; 32 | overflow-y: inherit; 33 | } 34 | #content{ 35 | height: auto; 36 | margin-left: 0; 37 | margin-bottom: 60px; 38 | margin-top: 0; 39 | padding: 10px; 40 | } 41 | .sidebar_controls{ 42 | position: relative; 43 | text-align: left; 44 | } 45 | #current_info{ 46 | position: fixed; 47 | height: 60px; 48 | width: 100%; 49 | z-index: 3; 50 | overflow: hidden; 51 | background-color: #cef; 52 | box-shadow: 0 0 30px #ccc; 53 | -webkit-transform: translateZ(0); 54 | } 55 | .info_wrapper{ 56 | width: calc(100% - 70px); /* cover width plus left padding */ 57 | } 58 | .info_title{ 59 | width: 1000%; 60 | } 61 | .info_detail{ 62 | line-height: 1.7; 63 | width: 1000%; 64 | } 65 | .duration, .current_time{ 66 | display: block; 67 | bottom: 63px; 68 | background-color: #4BF; 69 | z-index: 2; 70 | padding: 2px; 71 | color: #fff; 72 | } 73 | #bottom_bar:hover .current_time, .current_time{ 74 | padding-right: 5px; 75 | border-top-right-radius: 5px; 76 | border-bottom-right-radius: 0px; 77 | } 78 | #bottom_bar:hover .duration, .duration{ 79 | padding-left: 5px; 80 | border-top-left-radius: 5px; 81 | border-bottom-left-radius: 0px; 82 | } 83 | -------------------------------------------------------------------------------- /static/css/preloader/preloader.css: -------------------------------------------------------------------------------- 1 | #preloader-overlay { 2 | position: fixed; 3 | /* Offset values added to prevent ugly gaps at the edges*/ 4 | width: 110%; 5 | height: 100%; 6 | top: 0; 7 | left: -5%; 8 | bottom: 0; 9 | right: 0; 10 | z-index: 2000; 11 | background: url(../../images/encore-cover.svg); 12 | background-position: center; 13 | background-size: cover; 14 | } 15 | .preloader-show { 16 | max-height: 100%; 17 | opacity: 1; 18 | transition: max-height 0, opacity 300ms; 19 | -webkit-transition: max-height 0, opacity 300ms; 20 | } 21 | .preloader-hide { 22 | display: block; 23 | overflow: hidden; 24 | max-height: 0; 25 | opacity: 0; 26 | /* Fade transitions are fucked up. If you prefer something else, be my guest.*/ 27 | transition: max-height 2s ease-in-out, opacity 50s; 28 | -webkit-transition: max-height 2s ease-in-out, opacity 50s; 29 | } 30 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | /*background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAMAAAAp4XiDAAAAUVBMVEWFhYWDg4N3d3dtbW17e3t1dXWBgYGHh4d5eXlzc3OLi4ubm5uVlZWPj4+NjY19fX2JiYl/f39ra2uRkZGZmZlpaWmXl5dvb29xcXGTk5NnZ2c8TV1mAAAAG3RSTlNAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAvEOwtAAAFVklEQVR4XpWWB67c2BUFb3g557T/hRo9/WUMZHlgr4Bg8Z4qQgQJlHI4A8SzFVrapvmTF9O7dmYRFZ60YiBhJRCgh1FYhiLAmdvX0CzTOpNE77ME0Zty/nWWzchDtiqrmQDeuv3powQ5ta2eN0FY0InkqDD73lT9c9lEzwUNqgFHs9VQce3TVClFCQrSTfOiYkVJQBmpbq2L6iZavPnAPcoU0dSw0SUTqz/GtrGuXfbyyBniKykOWQWGqwwMA7QiYAxi+IlPdqo+hYHnUt5ZPfnsHJyNiDtnpJyayNBkF6cWoYGAMY92U2hXHF/C1M8uP/ZtYdiuj26UdAdQQSXQErwSOMzt/XWRWAz5GuSBIkwG1H3FabJ2OsUOUhGC6tK4EMtJO0ttC6IBD3kM0ve0tJwMdSfjZo+EEISaeTr9P3wYrGjXqyC1krcKdhMpxEnt5JetoulscpyzhXN5FRpuPHvbeQaKxFAEB6EN+cYN6xD7RYGpXpNndMmZgM5Dcs3YSNFDHUo2LGfZuukSWyUYirJAdYbF3MfqEKmjM+I2EfhA94iG3L7uKrR+GdWD73ydlIB+6hgref1QTlmgmbM3/LeX5GI1Ux1RWpgxpLuZ2+I+IjzZ8wqE4nilvQdkUdfhzI5QDWy+kw5Wgg2pGpeEVeCCA7b85BO3F9DzxB3cdqvBzWcmzbyMiqhzuYqtHRVG2y4x+KOlnyqla8AoWWpuBoYRxzXrfKuILl6SfiWCbjxoZJUaCBj1CjH7GIaDbc9kqBY3W/Rgjda1iqQcOJu2WW+76pZC9QG7M00dffe9hNnseupFL53r8F7YHSwJWUKP2q+k7RdsxyOB11n0xtOvnW4irMMFNV4H0uqwS5ExsmP9AxbDTc9JwgneAT5vTiUSm1E7BSflSt3bfa1tv8Di3R8n3Af7MNWzs49hmauE2wP+ttrq+AsWpFG2awvsuOqbipWHgtuvuaAE+A1Z/7gC9hesnr+7wqCwG8c5yAg3AL1fm8T9AZtp/bbJGwl1pNrE7RuOX7PeMRUERVaPpEs+yqeoSmuOlokqw49pgomjLeh7icHNlG19yjs6XXOMedYm5xH2YxpV2tc0Ro2jJfxC50ApuxGob7lMsxfTbeUv07TyYxpeLucEH1gNd4IKH2LAg5TdVhlCafZvpskfncCfx8pOhJzd76bJWeYFnFciwcYfubRc12Ip/ppIhA1/mSZ/RxjFDrJC5xifFjJpY2Xl5zXdguFqYyTR1zSp1Y9p+tktDYYSNflcxI0iyO4TPBdlRcpeqjK/piF5bklq77VSEaA+z8qmJTFzIWiitbnzR794USKBUaT0NTEsVjZqLaFVqJoPN9ODG70IPbfBHKK+/q/AWR0tJzYHRULOa4MP+W/HfGadZUbfw177G7j/OGbIs8TahLyynl4X4RinF793Oz+BU0saXtUHrVBFT/DnA3ctNPoGbs4hRIjTok8i+algT1lTHi4SxFvONKNrgQFAq2/gFnWMXgwffgYMJpiKYkmW3tTg3ZQ9Jq+f8XN+A5eeUKHWvJWJ2sgJ1Sop+wwhqFVijqWaJhwtD8MNlSBeWNNWTa5Z5kPZw5+LbVT99wqTdx29lMUH4OIG/D86ruKEauBjvH5xy6um/Sfj7ei6UUVk4AIl3MyD4MSSTOFgSwsH/QJWaQ5as7ZcmgBZkzjjU1UrQ74ci1gWBCSGHtuV1H2mhSnO3Wp/3fEV5a+4wz//6qy8JxjZsmxxy5+4w9CDNJY09T072iKG0EnOS0arEYgXqYnXcYHwjTtUNAcMelOd4xpkoqiTYICWFq0JSiPfPDQdnt+4/wuqcXY47QILbgAAAABJRU5ErkJggg==) fixed; */ 3 | background: url('../images/milkyway.jpg') no-repeat center center fixed; 4 | -webkit-background-size: cover; 5 | -moz-background-size: cover; 6 | -o-background-size: cover; 7 | background-size: cover; 8 | /*background-color: #999;*/ 9 | } 10 | 11 | 12 | /*.input-field input[type=text]:focus { 13 | color: #404040; 14 | } 15 | 16 | .input-field input[type=text] { 17 | color: #404040; 18 | }*/ 19 | 20 | /*.input-field input[type=text]:focus { 21 | border-bottom: 1px solid #0095dd; 22 | box-shadow: 0 1px 0 0 #0095dd; 23 | }*/ 24 | 25 | .desc { 26 | color: #404040 !important; 27 | padding: 10px; 28 | } 29 | 30 | .card-title { 31 | color: #eee; 32 | font-size: 60px; 33 | } 34 | 35 | .osdlogo { 36 | color: #333; 37 | text-shadow: -1px -1px 0px rgba(255, 255, 255, 0.3), 1px 1px 0px rgba(0, 0, 0, 0.8); 38 | } 39 | 40 | .controlbar { 41 | width: 100%; 42 | position: absolute; 43 | bottom: 0; 44 | } 45 | 46 | .control { 47 | padding: 4px 15px; 48 | } 49 | 50 | .prev, .next { 51 | padding: 10px; 52 | } 53 | 54 | .picker__box{ 55 | background-color: #e7e5de !important; 56 | } 57 | 58 | .picker__weekday-display { 59 | background-color: #008bce !important; 60 | } 61 | 62 | .radio { 63 | display:block; 64 | padding-top: 30px; 65 | font-size: 8pt; 66 | text-align: left !important; 67 | text-decoration: none; 68 | } 69 | 70 | .bmo { 71 | background-color: #222 !important; 72 | height: 85vh !important; 73 | } 74 | 75 | .bmobtn { 76 | background-color: #0095dd !important; 77 | } 78 | 79 | .music { 80 | /*position: fixed;*/ 81 | opacity: 0.95; 82 | margin-top: 5vh; 83 | margin-bottom: 5vh; 84 | width: 90%; 85 | } -------------------------------------------------------------------------------- /static/images/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/images/encore-cover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | encore cover 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OSDLabs/Encore/ffbfe924621dc3bb7feae081b6319f49001c3b33/static/images/favicon.ico -------------------------------------------------------------------------------- /static/images/milkyway.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OSDLabs/Encore/ffbfe924621dc3bb7feae081b6319f49001c3b33/static/images/milkyway.jpg -------------------------------------------------------------------------------- /static/images/particle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OSDLabs/Encore/ffbfe924621dc3bb7feae081b6319f49001c3b33/static/images/particle.jpg -------------------------------------------------------------------------------- /static/images/unknown.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OSDLabs/Encore/ffbfe924621dc3bb7feae081b6319f49001c3b33/static/images/unknown.jpg -------------------------------------------------------------------------------- /static/js/player/backbone_setup.js: -------------------------------------------------------------------------------- 1 | // setup the backbone app and router 2 | MusicApp = new Backbone.Marionette.Application(); 3 | 4 | MusicApp.addRegions({ 5 | sideBarRegion: '#sidebar', 6 | contentRegion: '#content', 7 | infoRegion: '#current_info', 8 | settingBarRegion: '#settings_bar', 9 | }); 10 | 11 | items = ['playlists', 'songs']; 12 | itemsLoaded = []; 13 | MusicAppRouter = Backbone.Router.extend({ 14 | sb: null, 15 | songview: null, 16 | settingbar: null, 17 | routes: { 18 | 'playlist/:id': 'playlist', 19 | 'search/:search': 'search', 20 | 'searchyt/:search': 'searchyt', 21 | }, 22 | playlist: function(id) { 23 | findId = player.playlist_collection.getBy_Id(id); 24 | if (findId === false) { 25 | findId = player.playlist_collection.getBy_Id('LIBRARY'); 26 | } 27 | 28 | player.playlist = findId.attributes; 29 | 30 | // update the currently viewed songs 31 | player.songs = player.song_collection.getByIds(player.playlist); 32 | 33 | // block for when this function is called on page load 34 | if (player.songs === false) { 35 | return; 36 | } 37 | 38 | // if they haven't selected a queue yet, make this playlist the queue 39 | // this is used when they are loading a page and haven't clicked a song yet 40 | if (player.queue_pool.length === 0) { 41 | player.queue_pool = player.songs.slice(0); 42 | player.genShufflePool(); 43 | } 44 | 45 | // reset the sorting variables 46 | player.sort_asc = player.sort_col = null; 47 | 48 | // if the songs were found, update the songview 49 | if (player.songs) { 50 | this.songview = new SongView(); 51 | MusicApp.contentRegion.show(this.songview); 52 | } 53 | }, 54 | 55 | search: function(search) { 56 | player.searchItems(search); 57 | }, 58 | 59 | sidebar: function(id) { 60 | // cache the scroll if it's already loaded 61 | var playlistScroll = 0; 62 | if (this.sb !== null) { 63 | playlistScroll = $('#sidebar .custom_scrollbar').scrollTop(); 64 | } 65 | 66 | // load the new view 67 | this.sb = new SidebarView(); 68 | MusicApp.sideBarRegion.show(this.sb); 69 | 70 | // refresh the scroll if needed 71 | if (playlistScroll !== 0) { 72 | _.defer(function() { 73 | $('#sidebar .custom_scrollbar').scrollTop(playlistScroll); 74 | }); 75 | } 76 | }, 77 | 78 | settingsbar: function(id) { 79 | this.settingbar = new SettingsBarView(); 80 | MusicApp.settingBarRegion.show(this.settingbar); 81 | }, 82 | }); 83 | 84 | MusicApp.addInitializer(function(options) { 85 | this.router = new MusicAppRouter(); 86 | 87 | // setup the settings bar section only if not on mobile 88 | if (!on_mobile) { 89 | MusicApp.router.settingsbar(); 90 | } 91 | 92 | // load the history api 93 | Backbone.history.start({pushState: false}); 94 | }); 95 | -------------------------------------------------------------------------------- /static/js/player/coverbox.js: -------------------------------------------------------------------------------- 1 | var element = null; 2 | var padding_factor = 100; 3 | function CoverBox(url, deactivatedCB) { 4 | this.url = url; 5 | this.activate = function() { 6 | var imgCSS = 'position: fixed; z-index: 1031; border-radius: 20px; box-shadow: #333 0px 0px 30px 5px;'; 7 | $(document.body).append('
'); 8 | $('.modal-backdrop').click(this.deactivate); 9 | element = $(''); 10 | element.on('load', function() { 11 | var docW = $(window).width(); 12 | var docH = $(window).height(); 13 | 14 | // work out the maximum height (smaller out of width and height) 15 | var maxw = docW - padding_factor; 16 | var maxh = docH - padding_factor; 17 | 18 | // calculate the maximum ratio of available space to image height / width 19 | var ratio_w = maxw / this.width; 20 | var ratio_h = maxh / this.height; 21 | var max_ratio = (ratio_w > ratio_h) ? ratio_h : ratio_w; 22 | 23 | // calculate the width and height to use 24 | // if the max_ratio is greater than 1, it means we can show it at full size, 25 | // otherwise we must scale it down by the max_ratio 26 | var imgW = (max_ratio > 1) ? this.width : this.width * max_ratio; 27 | var imgH = (max_ratio > 1) ? this.height : this.height * max_ratio; 28 | element.css({ 29 | left: ((docW / 2) - (imgW / 2)) + 'px', 30 | top: ((docH / 2) - (imgH / 2)) + 'px', 31 | 'max-width:': imgW + 'px', 32 | 'max-height': imgH + 'px', 33 | }); 34 | $(document.body).append(element); 35 | }); 36 | }; 37 | 38 | this.deactivate = function() { 39 | $('.modal-backdrop').remove(); 40 | $('.modal-picture').remove(); 41 | deactivatedCB(); 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /static/js/player/detailview.js: -------------------------------------------------------------------------------- 1 | // view for showing / editing the detailed info of a selected track 2 | 3 | function showInfoView(items) { 4 | // input checking 5 | if (items.length === 0) { 6 | Messenger().post('Error: 0 items selected'); 7 | return; 8 | } 9 | 10 | // process the edit for the item 11 | var track = player.song_collection.findBy_Id(items[0]); 12 | 13 | // if they are currently on the computer running the app 14 | var onserver = (window.location.hostname == 'localhost') ? true : false; 15 | 16 | // has the user dropped a file 17 | var filedropdata = null; 18 | 19 | // has the cover been fetched from lastfm 20 | var islastfm = false; 21 | 22 | // generate the content of the modal 23 | var message = render('#info_template', {track: track, music_dir: music_dir, onserver: onserver}); 24 | 25 | // function for saving data 26 | var save_func = function() { 27 | var cover_is_url = true; 28 | if (filedropdata !== null) { 29 | cover_is_url = false; 30 | } 31 | 32 | // change the data 33 | var data = { 34 | _id: $('#edit_id').val(), 35 | title: $('#title').val(), 36 | artist: $('#artist').val(), 37 | album: $('#album').val(), 38 | cover: $('.lfm_cover').attr('src') || filedropdata, 39 | cover_is_lastfm: islastfm, 40 | cover_is_url: cover_is_url, 41 | }; 42 | if (data.cover == track.attributes.cover_location) { 43 | data.cover = null; 44 | } 45 | 46 | // get the data to change on the server 47 | socket.emit('update_song_info', data); 48 | 49 | // get the data to change on the client 50 | track.attributes.title = data.title; 51 | track.attributes.display_artist = data.artist; 52 | track.attributes.album = data.album; 53 | 54 | // redraw the song 55 | MusicApp.router.songview.redrawSong(data._id); 56 | 57 | if (player.playing_id === data._id) { 58 | MusicApp.infoRegion.currentView.render(); 59 | } 60 | }; 61 | 62 | bootbox.dialog({ 63 | title: track.attributes.title, 64 | message: message, 65 | buttons: { 66 | success: { 67 | label: 'Save', 68 | className: 'btn-success', 69 | callback: save_func, 70 | }, 71 | main: { 72 | label: 'Close Without Saving', 73 | className: 'btn-default', 74 | }, 75 | }, 76 | }); 77 | 78 | // show the img in full when mousove 79 | var info_cover_popover = function() { 80 | $('.info_cover, .detailed').popover({ 81 | html: true, 82 | trigger: 'hover', 83 | placement: 'bottom', 84 | content: function() { 85 | return '

'; 86 | }, 87 | }); 88 | }; 89 | 90 | info_cover_popover(); 91 | 92 | // they want to find the cover for the song 93 | $('.find_cover_art').click(function(ev) { 94 | $('.find_cover_art').replaceWith(''); 95 | var lastfm = new LastFM({ 96 | apiKey: '4795cbddcdec3a6b3f622235caa4b504', 97 | apiSecret: 'cbe22daa03f35df599746f590bf015a5', 98 | }); 99 | 100 | // fetch the cover from last fm by the artist and album 101 | lastfm.album.getInfo({artist: $('#artist').val(), album: $('#album').val()}, {success: function(data) { 102 | if (data.album.image.length > 0 && data.album.image[data.album.image.length - 1]['#text'] !== '') { 103 | // set the image as the preview image 104 | var cover_url = data.album.image[data.album.image.length - 1]['#text']; 105 | $('.img_block').html(''); 106 | info_cover_popover(); 107 | islastfm = true; 108 | } else { 109 | // cover art could not be found 110 | bootbox.alert('Artwork not found on LastFM'); 111 | $('.lastfm_spinner').replaceWith('
Find Cover Art
'); 112 | } 113 | }, error: function(code, message) { 114 | 115 | bootbox.alert('Error fetching album artwork: ' + message); 116 | $('.lastfm_spinner').replaceWith('
Find Cover Art
'); 117 | }, }); 118 | }); 119 | 120 | // auto correct info button 121 | $('.correct_info').click(function(ev) { 122 | // build the search string 123 | var title = $('#title').val(); 124 | var album = $('#album').val(); 125 | var artist = $('#artist').val(); 126 | 127 | // defailts 128 | var prefix = '/proxy/search?term='; 129 | var suffix = '&entity=song&limit=10' + (country_code) ? '&country=' + country_code : ''; 130 | 131 | // call the api with all three first 132 | $.getJSON(prefix + encodeURI([title, album, artist].join(' ')) + suffix, function(data) { 133 | console.log('searched with title and artist and album and found ' + data.resultCount + ' tracks'); 134 | 135 | // were there results? 136 | if (data.resultCount) { 137 | foundInfo(data.results); 138 | } else { 139 | // call the api with title and artist 140 | $.getJSON(prefix + encodeURI([title, artist].join(' ')) + suffix, function(data) { 141 | console.log('searched with title and artist and found ' + data.resultCount + ' tracks'); 142 | 143 | // were there results? 144 | if (data.resultCount) { 145 | foundInfo(data.results); 146 | } else { 147 | // call the api with only title 148 | $.getJSON(prefix + encodeURI([title].join(' ')) + suffix, function(data) { 149 | console.log('searched with only title and found ' + data.resultCount + ' tracks'); 150 | if (data.resultCount) { 151 | foundInfo(data.results); 152 | } else { 153 | bootbox.alert('Unable to find song info on itunes. Maybe try tweaking the title/album/artist so it can be found better.'); 154 | } 155 | }); 156 | } 157 | }); 158 | } 159 | }); 160 | 161 | // function to run when song info found 162 | var foundInfo = function(results) { 163 | console.log('Found results:'); 164 | console.log(results); 165 | 166 | // only use the first result 167 | var result = results[0]; 168 | $('#title').val(result.trackName); 169 | $('#album').val(result.collectionName); 170 | $('#artist').val(result.artistName); 171 | 172 | // set the image as the preview image and upgrade it's size 173 | var cover_url = result.artworkUrl100.replace('100x100', '600x600'); 174 | $('.img_block').html(''); 175 | info_cover_popover(); 176 | islastfm = true; 177 | }; 178 | }); 179 | 180 | // tie the enter key on the inputs to the save function 181 | $('.edit_form :input').keydown(function(ev) { 182 | // if enter key 183 | if (ev.keyCode == 13) { 184 | save_func(); 185 | 186 | // manually hide the modal, because it wasn't called from the button press 187 | $('.bootbox').modal('hide'); 188 | } 189 | }); 190 | 191 | // open the directory in a file manager 192 | $('.open_dir').click(function(ev) { 193 | var location = $(this).attr('title'); 194 | socket.emit('open_dir', location); 195 | }); 196 | 197 | // dismiss the modal on backdrop click 198 | $('.bootbox').click(function(ev) { 199 | if (ev.target != this) return; 200 | $('.bootbox').modal('hide'); 201 | }); 202 | 203 | dropZone = $('body'); 204 | var drop_visible = false; 205 | var dragover_handler = function(e) { 206 | e.stopPropagation(); 207 | e.preventDefault(); 208 | if (!drop_visible) { 209 | $('body').append('
Drop File
'); 210 | drop_visible = true; 211 | } 212 | }; 213 | 214 | var dropped_handler = function(e) { 215 | drop_visible = false; 216 | dropZone.unbind('dragover', dragover_handler); 217 | dropZone.unbind('drop', dropped_handler); 218 | 219 | // remove the drop file div 220 | $('.dropping_file').remove(); 221 | 222 | // stop the event propogating (i.e. don't load the image as the page in the browser) 223 | e.preventDefault(); 224 | e.stopPropagation(); 225 | if (e.originalEvent.dataTransfer) { 226 | // only if there was more than one file 227 | if (e.originalEvent.dataTransfer.files.length) { 228 | // get the first file 229 | var file = e.originalEvent.dataTransfer.files[0]; 230 | 231 | // only if it is an image 232 | if (file.type.indexOf('image') === 0) { 233 | // read the file and set the data onto the preview image 234 | var reader = new FileReader(); 235 | reader.onload = function(e) { 236 | filedropdata = e.target.result; 237 | $('.img_block').html(''); 238 | info_cover_popover(); 239 | }; 240 | 241 | reader.readAsDataURL(file); 242 | } 243 | } 244 | } 245 | }; 246 | 247 | dropZone.on('dragover', dragover_handler); 248 | dropZone.on('drop', dropped_handler); 249 | } 250 | -------------------------------------------------------------------------------- /static/js/player/footer.js: -------------------------------------------------------------------------------- 1 | MusicApp.start(); 2 | -------------------------------------------------------------------------------- /static/js/player/infoview.js: -------------------------------------------------------------------------------- 1 | InfoView = Backbone.View.extend({ 2 | template: '#current_info_template', 3 | render: function() { 4 | this.$el.html(render(this.template, {song: player.current_song})); 5 | }, 6 | 7 | events: { 8 | 'click .colsearch': 'triggerSearch', 9 | 'click .info_cover': 'triggerCover', 10 | 'click .info_options': 'triggerOptions', 11 | }, 12 | triggerCover: function(ev) { 13 | showCover($(ev.target).attr('src')); 14 | return false; 15 | }, 16 | 17 | triggerOptions: function(ev) { 18 | if (!optionsVisible) { 19 | // clear the options, they selected this indiviual item 20 | clearSelection(); 21 | 22 | // add the current selection and display the options 23 | addToSelection(player.playing_id, false); 24 | createOptions(ev.clientX, ev.clientY); 25 | } 26 | else { 27 | hideOptions(); 28 | } 29 | 30 | return false; 31 | }, 32 | 33 | triggerSearch: function(ev) { 34 | search = $(ev.target).text(); 35 | player.updateSearch(search); 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /static/js/player/models.js: -------------------------------------------------------------------------------- 1 | // this file contains the models/collections in needed by the player 2 | 3 | var PlaylistCollection = Backbone.Collection.extend({ 4 | comparator: function(playlist) { 5 | return [playlist.get('editable'), playlist.get('title')]; 6 | }, 7 | 8 | fetch: function(options) { 9 | socket.emit('fetch_playlists'); 10 | }, 11 | 12 | getByTitle: function(name) { 13 | for (var i = 0; i < this.models.length; i++) { 14 | if (this.models[i].attributes.title == name) { 15 | return this.models[i]; 16 | } 17 | } 18 | 19 | return false; 20 | }, 21 | 22 | getBy_Id: function(id) { 23 | for (var i = 0; i < this.models.length; i++) { 24 | if (this.models[i].attributes._id == id) { 25 | return this.models[i]; 26 | } 27 | } 28 | 29 | return false; 30 | }, 31 | }); 32 | 33 | var SongModel = Backbone.Model.extend({ 34 | getCover: function() { 35 | if (this.get('is_youtube')) { 36 | return this.attributes.cover_location; 37 | } else if (this.get('cover_location')) { 38 | return 'cover/' + this.attributes.cover_location; 39 | } else { 40 | return 'https://unsplash.it/g/512?random&'+this.attributes._id; 41 | } 42 | }, 43 | 44 | isYoutube: function() { 45 | return this.get('is_youtube') === true; 46 | }, 47 | }); 48 | 49 | var SongCollection = Backbone.Collection.extend({ 50 | model: SongModel, 51 | 52 | fetch: function(options) { 53 | socket.emit('fetch_songs'); 54 | }, 55 | 56 | findBy_Id: function(id) { 57 | for (var i = 0; i < this.models.length; i++) { 58 | if (this.models[i].attributes._id == id) { 59 | return this.models[i]; 60 | } 61 | } 62 | 63 | return false; 64 | }, 65 | 66 | getByIds: function(playlist) { 67 | if (playlist !== undefined && playlist.songs !== undefined) { 68 | if (playlist._id == 'QUEUE') { 69 | return (player.shuffle_state) ? player.shuffle_pool : player.queue_pool; 70 | } 71 | 72 | songs = []; 73 | for (var i = 0; i < playlist.songs.length; i++) { 74 | var song = this.findBy_Id(playlist.songs[i]._id); 75 | if (song) { 76 | // set the index in the playlist 77 | song.attributes.index = i + 1; 78 | songs.push(song); 79 | } 80 | } 81 | 82 | return songs; 83 | } 84 | 85 | return false; 86 | }, 87 | 88 | findItem: function(item) { 89 | for (var i = 0; i < this.models.length; i++) { 90 | if (this.models[i].attributes.title == item.attributes.title && 91 | this.models[i].attributes.album == item.attributes.album && 92 | this.models[i].attributes.display_artist == item.attributes.display_artist) { 93 | return this.models[i]; 94 | } 95 | } 96 | 97 | return false; 98 | }, 99 | }); 100 | -------------------------------------------------------------------------------- /static/js/player/music-search.js: -------------------------------------------------------------------------------- 1 | var search_button = document.getElementById("search-button"); 2 | var overlay_search_button = document.getElementById("overlay-search-button"); 3 | // Default search state 4 | var search_state = false; 5 | var search_overlay = document.getElementById("search-overlay"); 6 | var content = document.getElementById("content"); 7 | var wrapper = document.getElementById("wrapper"); 8 | 9 | function activateSearch() { 10 | // Show search overlay 11 | search_overlay.style.display = "block"; 12 | // Keep search initialised with previously saved search value 13 | // player.updateSearch($("#input").val()); 14 | // Keep focus on search 15 | $('#input').val(''); 16 | $('#input').focus(); 17 | $('#music_ui').addClass('blur'); 18 | } 19 | 20 | function deactivateSearch() { 21 | // Hide search overlay 22 | search_overlay.style.display = "none"; 23 | // Redirect to our main library 24 | window.location.href = "/#playlist/LIBRARY"; 25 | $('#music_ui').removeClass('blur'); 26 | } 27 | 28 | // Add instant search to our custom search feature 29 | $(".search-input").on("keyup", function (evt) { 30 | player.updateSearch(evt.target.value); 31 | }); 32 | 33 | search_button.addEventListener("click", function () { 34 | 35 | // Display search results under our search input 36 | search_overlay.appendChild(content); 37 | search_state = true; 38 | activateSearch(); 39 | }); 40 | overlay_search_button.addEventListener("click", function () { 41 | search_overlay.removeChild(content); 42 | wrapper.appendChild(content); 43 | search_state = false; 44 | deactivateSearch(); 45 | }); 46 | -------------------------------------------------------------------------------- /static/js/player/onload.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | player.setScubElem($('#scrub_bar')); 3 | 4 | // variable for keeping track of seeking 5 | var is_seeking = false; 6 | /* 7 | * use this flag to mark a keydown has happened before a keyup happens 8 | * this is to work with a stupid bug in windows that lets the desktop-level 9 | * workspace change shortcuts leak keys (like right/left arrow) though to the 10 | * application... 11 | */ 12 | var prevNextDown = false; 13 | var seek_interval = 0; 14 | var HOLD_TIME = 500; 15 | $('body').keydown(function(event) { 16 | // don't fire the controls if the user is editing an input 17 | if (event.target.localName == 'input') { 18 | return; 19 | } 20 | 21 | if (event.which == 37 || event.which == 39) { 22 | prevNextDown = true; 23 | } 24 | 25 | switch (event.which){ 26 | case 191: // '?' key 27 | case 83: // 's' key 28 | // focus search box 29 | $('.search-input').select(); 30 | return false; 31 | case 32: // space key 32 | player.togglePlayState(); 33 | event.preventDefault(); 34 | break; 35 | case 39: // right key 36 | // block to only run on first key down 37 | if (!seek_interval) { 38 | is_seeking = false; 39 | 40 | // call function every HOLD_TIME, if we don't get called, then it wasn't held down 41 | seek_interval = setInterval(function() { 42 | is_seeking = true; 43 | player.PlayMethodAbstracter.setCurrentTime(player.PlayMethodAbstracter.getCurrentTime() + 5); 44 | }, HOLD_TIME); 45 | } 46 | 47 | event.preventDefault(); 48 | break; 49 | case 37: // left key 50 | // block to only run on first key down 51 | if (!seek_interval) { 52 | is_seeking = false; 53 | 54 | // call function every HOLD_TIME, if we don't get called, then it wasn't held down 55 | seek_interval = setInterval(function() { 56 | is_seeking = true; 57 | player.PlayMethodAbstracter.setCurrentTime(player.PlayMethodAbstracter.getCurrentTime() - 5); 58 | }, HOLD_TIME); 59 | } 60 | 61 | event.preventDefault(); 62 | break; 63 | case 38: // up key 64 | MusicApp.router.songview.moveSelection('up'); 65 | event.preventDefault(); 66 | break; 67 | case 40: // down key 68 | MusicApp.router.songview.moveSelection('down'); 69 | event.preventDefault(); 70 | break; 71 | case 13: // enter key 72 | // play the last selected item 73 | player.playSong(lastSelection); 74 | 75 | // add it to the history and reset the history 76 | player.play_history.unshift(lastSelection); 77 | player.play_history_idx = 0; 78 | } 79 | }); 80 | 81 | $('body').keyup(function(event) { 82 | // don't fire the controls if the user is editing an input 83 | if (event.target.localName == 'input') { 84 | return; 85 | } 86 | 87 | // left or right repectively 88 | if (event.which == 37 || event.which == 39) { 89 | // clear the seeking interval 90 | clearInterval(seek_interval); 91 | seek_interval = 0; 92 | 93 | // decide if we should move to the next/prev track 94 | if (prevNextDown) { 95 | prevNextDown = false; 96 | 97 | // don't go next if we are seeking 98 | if (!is_seeking) { 99 | // based on the keycode, call prev/next on the player 100 | event.which == 37 ? player.prevTrack() : player.nextTrack(); 101 | } 102 | } 103 | 104 | // prevent the event from propagating 105 | event.preventDefault(); 106 | } 107 | }); 108 | 109 | // disable the options on scroll 110 | $('#content').scroll(hideOptions); 111 | 112 | // add click handler on menu items 113 | $('#soundcloud_fetch').click(function() { 114 | bootbox.prompt({ 115 | title: 'Enter the SoundCloud URL', 116 | callback: function(result) { 117 | if (result !== null) { 118 | socket.emit('soundcloud_download', {url: result}); 119 | } 120 | }, 121 | }); 122 | }); 123 | 124 | $('#youtube_fetch').click(function() { 125 | bootbox.prompt({ 126 | title: 'Enter the Youtube URL (also works for playlist downloads)', 127 | callback: function(result) { 128 | if (result !== null) { 129 | socket.emit('youtube_download', {url: result}); 130 | } 131 | }, 132 | }); 133 | }); 134 | 135 | $('#open_settings').click(function() { 136 | showSettings(); 137 | }); 138 | 139 | // ask them if they would like to view the settings on first load 140 | if (!music_dir_set) { 141 | showSettings('Welcome! Please ensure your music directory is correct.'); 142 | } 143 | 144 | // scan library handlers 145 | $('#soft_scan').click(function() { 146 | socket.emit('start_scan'); 147 | }); 148 | 149 | $('#hard_scan').click(function() { 150 | socket.emit('start_scan_hard'); 151 | }); 152 | 153 | // sync button handlers 154 | $('#load_sync_view').click(function() { 155 | MusicApp.router.syncview = new SyncView(); 156 | MusicApp.contentRegion.show(MusicApp.router.syncview); 157 | }); 158 | 159 | // setup messenger 160 | Messenger.options = { 161 | extraClasses: 'messenger-fixed messenger-on-top', 162 | theme: 'air', 163 | messageDefaults: { 164 | showCloseButton: true, 165 | }, 166 | }; 167 | }); 168 | -------------------------------------------------------------------------------- /static/js/player/selection_options.js: -------------------------------------------------------------------------------- 1 | // these functions handle selection of songs and drawing the (right click) context menu 2 | 3 | var optionsVisible = false; 4 | var selectedItems = []; 5 | var recentPlaylists = []; 6 | var lastSelection = ''; 7 | function createOptions(x, y) { 8 | // calculate if the menu should 'drop up' 9 | var dropup = ''; 10 | if (y + 300 > $(window).height()) { 11 | dropup = 'dropup'; 12 | } 13 | 14 | var foundYoutube = false; 15 | var foundNormal = false; 16 | selectedItems.forEach(function(item) { 17 | var model = player.song_collection.findBy_Id(item); 18 | if (model.isYoutube()) { 19 | foundYoutube = true; 20 | } else { 21 | foundNormal = true; 22 | } 23 | }); 24 | 25 | var type = foundYoutube ? (foundNormal ? 'mix' : 'youtube') : 'normal'; 26 | 27 | $('.options_container').html(render('#options_template', { 28 | playlists: player.playlist_collection.models, 29 | current_playlist: player.playlist, 30 | recents: recentPlaylists, 31 | dropup: dropup, 32 | type: type, 33 | numSelected: selectedItems.length, 34 | })) 35 | .css({top: y + 'px', left: x + 'px'}); 36 | $('.add_to_queue').click(function(ev) { 37 | for (var x = 0; x < selectedItems.length; x++) { 38 | player.play_history.unshift(selectedItems[x]); 39 | player.play_history_idx++; 40 | } 41 | 42 | hideOptions(); 43 | }); 44 | 45 | $('.add_to_playlist').click(function(ev) { 46 | id = $(ev.target).closest('li').attr('id'); 47 | socket.emit('add_to_playlist', {add: selectedItems, playlist: id}); 48 | hideOptions(); 49 | 50 | // add to recents 51 | var this_playlist = player.playlist_collection.getBy_Id(id); 52 | 53 | // take it out if it's in the recents 54 | for (var x = 0; x < recentPlaylists.length; x++) { 55 | if (recentPlaylists[x].attributes._id == this_playlist.attributes._id) { 56 | // remove it from recents 57 | recentPlaylists.splice(x, 1); 58 | } 59 | } 60 | 61 | // add to the start of recents 62 | recentPlaylists.unshift(this_playlist); 63 | 64 | // remove if too many 65 | if (recentPlaylists.length > 3) { 66 | recentPlaylists.pop(); 67 | } 68 | }); 69 | 70 | $('.remove_from_playlist').click(function(ev) { 71 | id = $(ev.target).closest('li').attr('id'); 72 | 73 | // get a handle on the playlist 74 | for (var i = 0; i < selectedItems.length; i++) { 75 | $('#' + selectedItems[i]).remove(); 76 | 77 | // remove the song from the queue_pool 78 | for (var j = 0; j < player.songs.length; j++) { 79 | if (player.songs[j].attributes._id == selectedItems[i]) { 80 | // remove the element 81 | player.songs.splice(j, 1); 82 | } 83 | } 84 | } 85 | 86 | // fix bug with infinite scroll having elements removed 87 | if (MusicApp.router.songview.how_many_drawn > player.songs.length) { 88 | MusicApp.router.songview.how_many_drawn = player.songs.length; 89 | } 90 | 91 | // update the server 92 | socket.emit('remove_from_playlist', {remove: selectedItems, playlist: id}); 93 | hideOptions(); 94 | }); 95 | 96 | $('.hard_rescan').click(function(ev) { 97 | socket.emit('hard_rescan', {items: selectedItems}); 98 | hideOptions(); 99 | }); 100 | 101 | $('.rewrite_tags').click(function(ev) { 102 | socket.emit('rewrite_tags', {items: selectedItems}); 103 | hideOptions(); 104 | }); 105 | 106 | $('.view_info').click(function(ev) { 107 | showInfoView(selectedItems); 108 | hideOptions(); 109 | }); 110 | 111 | $('.similar_songs').click(function(ev) { 112 | // use the first selected option 113 | var song = player.song_collection.findBy_Id(lastSelection); 114 | 115 | // send it to the server to start the search 116 | socket.emit('similar_songs', {title: song.attributes.title, artist: song.attributes.display_artist, _id: song.attributes._id}); 117 | 118 | // notify the user that we are looking for a mix 119 | Messenger().post('Searching for similar songs...'); 120 | 121 | // hide the context menu 122 | hideOptions(); 123 | }); 124 | 125 | $('.save_youtube').click(function(ev) { 126 | var results = selectedItems.map(function(id) { 127 | return player.song_collection.findBy_Id(id).attributes; 128 | }); 129 | 130 | socket.emit('youtube_import', {songs: results}); 131 | 132 | // hide the context menu 133 | hideOptions(); 134 | }); 135 | 136 | $('.hide_options').click(function(ev) { 137 | console.log("closing"); 138 | hideOptions(); 139 | }); 140 | 141 | optionsVisible = true; 142 | } 143 | 144 | function hideOptions() { 145 | $('.options_container').css({'top:': '-1000px', left: '-1000px'}); 146 | optionsVisible = false; 147 | } 148 | 149 | function addToSelection(id, clearIfIn) { 150 | lastSelection = id; 151 | for (var i = 0; i < selectedItems.length; i++) { 152 | if (selectedItems[i] == id) { 153 | if (clearIfIn) { 154 | selectedItems.splice(i, 1); 155 | $('#' + id).removeClass('selected'); 156 | } 157 | 158 | return; 159 | } 160 | } 161 | 162 | selectedItems.push(id); 163 | $('#' + id).addClass('selected'); 164 | } 165 | 166 | function delFromSelection(id) { 167 | for (var i = 0; i < selectedItems.length; i++) { 168 | if (selectedItems[i] == id) { 169 | selectedItems.splice(i, 1); 170 | $('#' + id).removeClass('selected'); 171 | } 172 | } 173 | } 174 | 175 | function selectBetween(id, id2) { 176 | loc1 = indexInSongView(id); 177 | loc2 = indexInSongView(id2); 178 | 179 | // make sure loc1 is less than loc2 180 | if (loc1 > loc2) { 181 | temp = loc1; 182 | loc1 = loc2; 183 | loc2 = temp; 184 | } 185 | 186 | for (var i = loc1; i <= loc2; i++) { 187 | addToSelection(player.songs[i].attributes._id, false); 188 | } 189 | } 190 | 191 | function indexInSongView(id) { 192 | for (var i = 0; i < player.songs.length; i++) { 193 | if (player.songs[i].attributes._id == id) { 194 | return i; 195 | } 196 | } 197 | 198 | return -1; 199 | } 200 | 201 | function clearSelection() { 202 | selectedItems = []; 203 | $('tr').removeClass('selected'); 204 | } 205 | -------------------------------------------------------------------------------- /static/js/player/settingsview.js: -------------------------------------------------------------------------------- 1 | var save_func = function() { 2 | music_dir = $('#music_dir_val').val(); 3 | country_code = $('#country_code').val(); 4 | socket.emit('update_settings', {music_dir: music_dir, country_code: country_code}); 5 | }; 6 | 7 | var showSettings = function(message) { 8 | // render the template 9 | var options = { 10 | music_dir: music_dir, 11 | country_code: country_code, 12 | message: message, 13 | }; 14 | var body = render('#settings_template', options); 15 | 16 | // show the dialog 17 | bootbox.dialog({ 18 | title: 'Settings', 19 | message: body, 20 | buttons: { 21 | success: { 22 | label: 'Save', 23 | className: 'btn-success', 24 | callback: save_func, 25 | }, 26 | main: { 27 | label: 'Close Without Saving', 28 | className: 'btn-default', 29 | }, 30 | }, 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /static/js/player/sidebarview.js: -------------------------------------------------------------------------------- 1 | SidebarView = Backbone.View.extend({ 2 | template: '#sidebar_template', 3 | render: function () { 4 | var editable = player.playlist_collection.where({ 5 | editable: true 6 | }); 7 | var fixed = player.playlist_collection.where({ 8 | editable: false 9 | }); 10 | this.setElement(render(this.template, { 11 | title: 'Playlists', 12 | search: player.searchText, 13 | editable: editable, 14 | fixed: fixed 15 | })); 16 | }, 17 | 18 | events: { 19 | 'click .add_playlist': 'addPlaylist', 20 | 'click .search-youtube': 'searchYoutube', 21 | 'keyup .search-input': 'searchItems', 22 | }, 23 | addPlaylist: function () { 24 | bootbox.prompt('Playlist title?', function (result) { 25 | if (result !== null && result !== '') { 26 | socket.emit('create_playlist', { 27 | title: result, 28 | songs: [] 29 | }); 30 | } 31 | }); 32 | }, 33 | 34 | searchItems: function () { 35 | searchText = $('.search-input').val(); 36 | player.updateSearch(searchText); 37 | return true; 38 | }, 39 | 40 | searchYoutube: function () { 41 | var searchText = $('.search-input').val(); 42 | socket.emit('youtube_search', { 43 | search: searchText 44 | }); 45 | Messenger().post('Searching youtube for songs...'); 46 | }, 47 | }); 48 | 49 | SettingsBarView = Backbone.View.extend({ 50 | template: '#settings_bar_template', 51 | render: function () { 52 | this.$el.html(render(this.template, { 53 | vol: player.getVolume() 54 | })); 55 | _.defer(function () { 56 | var volElem = $('#vol_bar'); 57 | if (volElem.length > 0) { 58 | player.setVolElem(volElem); 59 | } 60 | }); 61 | }, 62 | 63 | events: { 64 | 'click #remote_setup': 'openOptions', 65 | }, 66 | openOptions: function () { 67 | bootbox.dialog({ 68 | message: render('#control_template', { 69 | comp_name: player.comp_name, 70 | host: window.location.host 71 | }), 72 | title: 'Setup Remote Control', 73 | buttons: { 74 | danger: { 75 | label: 'Cancel', 76 | className: 'btn-danger', 77 | }, 78 | success: { 79 | label: 'Save', 80 | className: 'btn-success', 81 | callback: function () { 82 | comp_name = $('#comp_name_input').val(); 83 | player.setCompName(comp_name); 84 | }, 85 | }, 86 | }, 87 | }); 88 | }, 89 | }); 90 | -------------------------------------------------------------------------------- /static/js/player/socket.js: -------------------------------------------------------------------------------- 1 | // soccet connection and events 2 | socket.on('connect', function() { 3 | console.log('Socket connected'); 4 | 5 | // notify the server we have connected 6 | socket.emit('player_page_connected'); 7 | 8 | // let the server know our remote name (if set) 9 | if (player.comp_name) { 10 | socket.emit('set_comp_name', {name: player.comp_name}); 11 | } 12 | }); 13 | 14 | socket.on('songs', function(data) { 15 | player.song_collection.add(data.songs); 16 | loadedRestart('songs'); 17 | }); 18 | 19 | socket.on('playlists', function(data) { 20 | player.playlist_collection.reset(); 21 | player.playlist_collection.add(data.playlists); 22 | 23 | // redraw the sidebar 24 | MusicApp.router.sidebar(); 25 | 26 | // when they are viewing a playlist that has been updated, refresh it 27 | data.playlists.forEach(function(playlist, index) { 28 | if (player.playlist && 29 | player.playlist.title === playlist.title && 30 | player.playlist.songs.length !== playlist.songs.length) { 31 | // trigger a re-route, this will refresh the songview 32 | MusicApp.router.playlist(playlist._id); 33 | } 34 | }); 35 | 36 | // restart the router if we are loading for the first time 37 | loadedRestart('playlists'); 38 | }); 39 | 40 | // mask to either update or initialise a messenger 41 | var updateMask = function(messenger, message) { 42 | if (messenger) { 43 | messenger.update(message); 44 | } else { 45 | messenger = Messenger().post(message); 46 | } 47 | 48 | return messenger; 49 | }; 50 | 51 | var SCMessenger = null; 52 | socket.on('sc_update', function(data) { 53 | console.log(data); 54 | if (data.type == 'started') { 55 | SCMessenger = Messenger().post('Starting download from SoundCloud of ' + data.count + ' tracks' + (data.not_streamable > 0 ? ' (' + data.not_streamable + ' not streamable)' : '')); 56 | } else if (data.type == 'added') { 57 | // it came with a song 58 | player.song_collection.add(data.content); 59 | 60 | // show an updated message 61 | var msg = 'song ' + data.completed + ' out of ' + data.count; 62 | if (data.complete == data.count) { 63 | msg = 'last song of SoundCloud download'; 64 | } 65 | 66 | SCMessenger = updateMask(SCMessenger, 'Added ' + msg + ' (' + data.content.title + ')'); 67 | 68 | // grab the soundcloud playlist from the collection 69 | var sc_plist = player.playlist_collection.getByTitle('SoundCloud'); 70 | 71 | // if it existed, do something, otherwise wait for the addPlaylist event 72 | if (sc_plist) { 73 | sc_plist = sc_plist.attributes; 74 | 75 | // add the track to the soundcloud playlist 76 | sc_plist.songs.push({_id: data.content._id}); 77 | 78 | // render if we are on the SoundCloud playlist 79 | if (player.playlist.title == 'SoundCloud') { 80 | // update the model data 81 | player.playlist = sc_plist; 82 | player.songs = player.song_collection.getByIds(player.playlist); 83 | player.queue_pool = player.songs.slice(0); 84 | player.genShufflePool(); 85 | 86 | // resort the model data 87 | player.resortSongs(); 88 | redrawSongsChangedModel(); 89 | } 90 | } 91 | 92 | // add the track to the library playlist 93 | addToLibraryPlaylist(data.content._id); 94 | } else if (data.type == 'skipped') { 95 | SCMessenger = updateMask(SCMessenger, 'Skipped song ' + data.completed + '. Unable to read from SoundCloud'); 96 | } else if (data.type == 'error') { 97 | SCMessenger = updateMask(SCMessenger, 'Soundcloud track already exists'); 98 | } 99 | }); 100 | 101 | var YTMessenger = null; 102 | socket.on('yt_update', function(data) { 103 | console.log(data); 104 | if (data.type == 'started') { 105 | YTMessenger = Messenger().post('Starting download from Youtube'); 106 | } else if (data.type == 'added') { 107 | player.song_collection.add(data.content); 108 | YTMessenger = updateMask(YTMessenger, 'Added ' + data.content.title); 109 | var yt_plist = player.playlist_collection.getByTitle('Youtube'); 110 | if (yt_plist) { 111 | yt_plist = yt_plist.attributes; 112 | 113 | // add the track to the youtube playlist 114 | yt_plist.songs.push({_id: data.content._id}); 115 | 116 | // if the user currently has the youtube playlist focussed, refresh it 117 | if (player.playlist.title == 'Youtube') { 118 | // update the model data 119 | player.playlist = yt_plist; 120 | player.songs = player.song_collection.getByIds(player.playlist); 121 | player.queue_pool = player.songs.slice(0); 122 | player.genShufflePool(); 123 | 124 | // resort the model data 125 | player.resortSongs(); 126 | 127 | // redraw the songs view 128 | redrawSongsChangedModel(); 129 | } 130 | } 131 | 132 | // add the track to the library playlist 133 | addToLibraryPlaylist(data.content._id); 134 | } else if (data.type == 'error') { 135 | YTMessenger = updateMask(YTMessenger, data.content); 136 | } else if (data.type === 'updated') { 137 | YTMessenger = updateMask(YTMessenger, 'Updated ' + data.content.title); 138 | songUpdated([data]); 139 | } 140 | }); 141 | 142 | var SongMessenger = null; 143 | socket.on('song_update', function(data) { 144 | console.log('Updating song info'); 145 | songUpdated(data); 146 | }); 147 | 148 | var ScanMessenger = null; 149 | var ScanTemplate = 'Scanned {{completed}} out of {{count}}{% if details %}: {{details}}{% endif %}'; 150 | socket.on('scan_update', function(data) { 151 | console.log(data); 152 | if (ScanMessenger === null) { 153 | ScanMessenger = Messenger().post(swig.render(ScanTemplate, {locals: data})); 154 | } else { 155 | ScanMessenger.update(swig.render(ScanTemplate, {locals: data})); 156 | } 157 | 158 | // do we need to remove it first? 159 | if (data.type == 'update') { 160 | // remove the track 161 | player.song_collection.remove(player.song_collection.where({_id: data.doc._id})); 162 | } 163 | 164 | // add the track 165 | player.song_collection.add(data.doc); 166 | 167 | // add the id to the library 168 | if (data.type == 'add') { 169 | addToLibraryPlaylist(data.doc._id); 170 | } 171 | }); 172 | 173 | // for when the sync between servers has progressed 174 | var SyncMessenger = null; 175 | var SyncTemplate = 'Synced {{completed}} out of {{count}}{% if content %} - Added {{content.title}}{% endif %}'; 176 | socket.on('sync_update', function(data) { 177 | // log the sync data to the developer tools 178 | console.log(data); 179 | 180 | if (SyncMessenger === null) { 181 | SyncMessenger = Messenger().post(swig.render(SyncTemplate, {locals: data})); 182 | } else { 183 | SyncMessenger.update(swig.render(SyncTemplate, {locals: data})); 184 | } 185 | 186 | // add the track to the songs collection 187 | player.song_collection.add(data.content); 188 | 189 | // add the id to the library 190 | if (data.type == 'add') { 191 | addToLibraryPlaylist(data.content._id); 192 | } 193 | 194 | // if it's completed, refresh the playlists 195 | if (data.completed == data.count) { 196 | socket.emit('fetch_playlists'); 197 | } 198 | }); 199 | 200 | // for when the durations are added 201 | socket.on('duration_update', function(data) { 202 | player.song_collection.where({_id: data._id})[0].attributes.duration = data.new_duration; 203 | }); 204 | 205 | // remote controll events 206 | var CommandMessenger = null; 207 | var CommandTemplate = 'Received command \'{{command}}\'.'; 208 | socket.on('command', function(data) { 209 | var command = data.command; 210 | 211 | // show command 212 | if (CommandMessenger === null) { 213 | CommandMessenger = Messenger().post(swig.render(CommandTemplate, {locals: data})); 214 | } else { 215 | CommandMessenger.update(swig.render(CommandTemplate, {locals: data})); 216 | } 217 | 218 | // run command 219 | switch (command){ 220 | case 'next': 221 | player.nextTrack(); 222 | break; 223 | case 'prev': 224 | player.prevTrack(); 225 | break; 226 | case 'playpause': 227 | player.togglePlayState(); 228 | break; 229 | default: 230 | break; 231 | } 232 | }); 233 | 234 | // similar songs data received 235 | socket.on('similar_songs', function(data) { 236 | if (data.error) { 237 | Messenger().post('Error generating mix, see log for more info'); 238 | } else { 239 | if (data.songs.length === 0) { 240 | Messenger().post('Error generating mix, lastfm failed to find similar songs'); 241 | } else { 242 | // map them to models we understand 243 | var songsConverted = data.songs.map(function (song) { 244 | // remove the song from the song collection if it already exists 245 | if (song.youtubeId && song.youtubeId.length > 0) { 246 | player.song_collection.remove( 247 | player.song_collection.where({youtube_id: song.youtubeId}) 248 | ); 249 | } 250 | 251 | return { 252 | _id: 'YOUTUBE_' + song.youtubeId, 253 | title: song.title, 254 | album: song.album, 255 | artist: song.artist, 256 | albumartist: song.artist, 257 | display_artist: song.artist, 258 | genre: song.genre, 259 | cover_location: song.coverUrl, 260 | disc: song.discNumber, 261 | track: song.trackNumber, 262 | duration: 0, 263 | play_count: 0, 264 | is_youtube: true, 265 | youtube_id: song.youtubeId, 266 | }; 267 | }); 268 | 269 | // add the tracks into the song_collection 270 | // NOTE: this doesn't add them to the library playlist 271 | player.song_collection.add(songsConverted); 272 | 273 | // instruct the player to create a view for the mix 274 | player.showMix(data.title, songsConverted); 275 | 276 | // update the url without navigating 277 | MusicApp.router.navigate(data.url, true); 278 | } 279 | } 280 | }); 281 | 282 | // add a new playlist 283 | socket.on('addPlaylist', function(data) { 284 | // add the playlist to the collection 285 | player.playlist_collection.add(data); 286 | 287 | // redraw the sidebar 288 | MusicApp.router.sidebar(); 289 | }); 290 | 291 | // generic info message reciever 292 | socket.on('message', function(data) { 293 | Messenger().post(data.message); 294 | }); 295 | 296 | function redrawSongsChangedModel() { 297 | MusicApp.router.songview.render(); 298 | } 299 | 300 | function redrawInfoView() { 301 | MusicApp.infoRegion.currentView.render(); 302 | } 303 | 304 | function addToLibraryPlaylist(id) { 305 | var libraryPlaylist = player.playlist_collection.where({_id: 'LIBRARY'})[0].attributes; 306 | libraryPlaylist.songs.push({_id: id}); 307 | 308 | // if they are focussed on the Library playlist, refresh it 309 | if (player.playlist.title == 'Library') { 310 | // update the model data 311 | player.playlist = libraryPlaylist; 312 | player.songs = player.song_collection.getByIds(player.playlist); 313 | player.queue_pool = player.songs.slice(0); 314 | player.genShufflePool(); 315 | 316 | // resort the model data 317 | player.resortSongs(); 318 | redrawSongsChangedModel(); 319 | } 320 | } 321 | 322 | function songUpdated(data) { 323 | 324 | // iterate over tracks and update them 325 | for (var x = 0; x < data.length; x++) { 326 | // remove the track 327 | player.song_collection.remove(player.song_collection.where({_id: data[x]._id})); 328 | 329 | // add the track 330 | player.song_collection.add(data[x]); 331 | 332 | // if it was the current track, update the player reference 333 | if (data[x]._id == player.current_song.attributes._id) { 334 | // refetch it from the song collection 335 | player.current_song = player.song_collection.where({_id: player.current_song.attributes._id})[0]; 336 | } 337 | } 338 | 339 | // regenerate the list of current songs 340 | player.songs = player.song_collection.getByIds(player.playlist); 341 | player.queue_pool = player.songs.slice(0); 342 | player.genShufflePool(); 343 | 344 | // resort the model data 345 | player.resortSongs(); 346 | 347 | // redraw the view 348 | redrawSongsChangedModel(); 349 | 350 | // redraw info view 351 | redrawInfoView(); 352 | } 353 | -------------------------------------------------------------------------------- /static/js/player/songview.js: -------------------------------------------------------------------------------- 1 | SongView = Backbone.View.extend({ 2 | template: '#song_template', 3 | render: function() { 4 | var self = this; 5 | 6 | // cache scrolltop (incase this is the second render, see bottom of function) 7 | var tmp_scrolltop = this.scrollTop; 8 | 9 | // calculate the duration 10 | var totalDuration = 0; 11 | for (var song = 0; song < player.songs.length; song++) { 12 | totalDuration += parseInt(player.songs[song].attributes.duration) || 0; 13 | } 14 | 15 | // render the view 16 | this.$el.html(render(this.template, { 17 | title: player.playlist.title, 18 | editable: player.playlist.editable, 19 | is_youtube: player.playlist.is_youtube, 20 | _id: player.playlist._id, 21 | sort_col: player.sort_col, 22 | sort_asc: player.sort_asc, 23 | songs: player.songs, 24 | numSongs: player.songs.length, 25 | totalDuration: prettyPrintSecondsorNA(totalDuration), 26 | })); 27 | this.$el.addClass('custom_scrollbar'); 28 | 29 | // add scroll event handler 30 | this.scrollElem = player.onMobile ? $('#wrapper') : this.$el; 31 | this.scrollElem.scroll(function() { 32 | MusicApp.router.songview.renderSong(); 33 | }); 34 | 35 | // logic to manually order the songs in a playlist 36 | if (player.playlist.editable && // playlist is editable 37 | // it is sorted in a way that makes sense for sorting 38 | (player.sort_col === null && player.sort_asc === null)) { 39 | this.$el.find('.song_table tbody').sortable({ 40 | delay: 100, 41 | items: 'tr', 42 | helper: fixHelper, 43 | update: function(event, ui) { 44 | // get where the item has moved from - to 45 | var item = player.song_collection.findBy_Id(ui.item.attr('id')); 46 | var oldIndex = item.attributes.index - 1; 47 | var newIndex = self.lastmin + ui.item.index() - 1; 48 | 49 | // remove the item from it's old place 50 | item = player.playlist.songs.splice(oldIndex, 1)[0]; 51 | 52 | // add the item into it's new place 53 | player.playlist.songs.splice(newIndex, 0, item); 54 | 55 | // refresh the songs array from the playlist 56 | player.songs = player.song_collection.getByIds(player.playlist); 57 | 58 | // send the data back to the server 59 | socket.emit('song_moved_in_playlist', { 60 | playlist_id: player.playlist._id, 61 | oldIndex: oldIndex, 62 | newIndex: newIndex, 63 | }); 64 | }, 65 | }); 66 | } 67 | 68 | // set the defaults and start rendering songs 69 | // init the processing variable 70 | this.processing = false; 71 | 72 | // content height 73 | this.contentHeight = $('#content').height(); 74 | 75 | // get the spacers 76 | this.top_spacer = this.$el.find('#top-spacer'); 77 | this.bottom_spacer = this.$el.find('#bottom-spacer'); 78 | 79 | // height of one item 80 | this.individual_height = 42; 81 | 82 | // how many to show at a time 83 | this.how_many_drawn = Math.ceil(window.innerHeight / this.individual_height) * 2; 84 | if (this.how_many_drawn > player.songs.length) { 85 | this.how_many_drawn = player.songs.length; 86 | } 87 | 88 | // initialise the lastmin and lastmax 89 | this.lastmin = 0; 90 | if (player.songs.length > 0) { 91 | this.lastmin = 1; 92 | } 93 | 94 | this.lastmax = 0; 95 | 96 | // if the viewport has been drawn at least once 97 | self.drawn_full = false; 98 | 99 | // height of number drawn 100 | this.height_of_drawn = this.how_many_drawn * this.individual_height; 101 | 102 | // how high is the table in total 103 | this.total_table_height = player.songs.length * this.individual_height; 104 | 105 | // precompile the song rendering tempalte 106 | this.song_template = swig.compile($('#song_item').html()); 107 | 108 | // default meta height until the defered function below evaluates 109 | this.meta_height = 40; 110 | _.defer(function() { 111 | // get hight of elems above table body incl header row 112 | self.meta_height = $('.playlist_meta').height() + 40; 113 | if (player.onMobile) { 114 | // mobile view has the playlists section in the scrollable view, add it to the meta_height 115 | self.meta_height += $('#sidebar').height(); 116 | } 117 | 118 | // draw the songs 119 | self.renderSong(); 120 | }); 121 | 122 | // if they have already rendered this, scroll to last postition 123 | if (this.scrollTop) { 124 | _.defer(function() { 125 | self.scrollElem.animate({scrollTop: tmp_scrolltop + 'px'}, 126 | 0); 127 | }); 128 | } 129 | }, 130 | 131 | events: { 132 | 'click .colsearch': 'triggerSearch', 133 | 'click thead > tr': 'triggerSort', 134 | 'click tbody > tr': 'triggerSong', 135 | 'click .options': 'triggerOptions', 136 | 'contextmenu td': 'triggerOptions', 137 | 'click .cover': 'triggerCover', 138 | 'click .rename_playlist': 'renamePlaylist', 139 | 'click .delete_playlist': 'deletePlaylist', 140 | }, 141 | 142 | triggerSearch: function(ev) { 143 | var search = $(ev.target).text(); 144 | player.updateSearch(search); 145 | }, 146 | 147 | triggerSort: function(ev) { 148 | var column_name = $(ev.target).closest('th').attr('class').replace('_th', ''); 149 | console.log(column_name); 150 | player.sortSongs(column_name); 151 | this.render(); 152 | }, 153 | 154 | triggerSong: function(ev) { 155 | if ($(ev.target).hasClass('options') || $(ev.target).hasClass('colsearch')) { 156 | return; 157 | } 158 | 159 | id = $(ev.target).closest('tr').attr('id'); 160 | hideOptions(); 161 | if (ev.ctrlKey) { 162 | // ctrlKey pressed, add to selection 163 | addToSelection(id, true); 164 | } else if (ev.shiftKey) { 165 | // shiftkey pressed, add to selection 166 | selectBetween(id, lastSelection); 167 | } else { 168 | // just play the song 169 | clearSelection(); 170 | player.queue_pool = player.songs.slice(0); 171 | player.playSong(id, false); 172 | player.genShufflePool(); 173 | 174 | // add the song to the history and reset it to the top 175 | player.play_history.unshift(id); 176 | player.play_history_idx = 0; 177 | } 178 | }, 179 | 180 | triggerOptions: function(ev) { 181 | if (!optionsVisible) { 182 | id = $(ev.target).closest('tr').attr('id'); 183 | if ($.inArray(id, selectedItems) == -1) { 184 | // right click on non-selected item should select only that item 185 | clearSelection(); 186 | } 187 | 188 | addToSelection(id, false); 189 | createOptions(ev.clientX, ev.clientY); 190 | } else { 191 | hideOptions(); 192 | } 193 | 194 | return false; 195 | }, 196 | 197 | triggerCover: function(ev) { 198 | showCover($(ev.target).attr('src')); 199 | return false; 200 | }, 201 | 202 | // move the selection up and down 203 | moveSelection: function(direction) { 204 | var index_in_queue = player.findSongIndex(lastSelection); 205 | var new_index = index_in_queue || 0; 206 | if (direction == 'up' && index_in_queue - 1 >= 0) { 207 | new_index--; 208 | } else if (direction == 'down' && index_in_queue + 1 < player.queue_pool.length) { 209 | new_index++; 210 | } 211 | 212 | var newly_selected = player.queue_pool[new_index]; 213 | 214 | // get the offset 215 | var new_item = $('#' + newly_selected.attributes._id); 216 | var offset = new_item.offset(); 217 | 218 | // it is actually in the view 219 | if (offset !== undefined) { 220 | // do we need to scroll to show the full item? 221 | var doc_height = $(document).height(); 222 | 223 | // how far past the top minus navbar 224 | var top_diff = offset.top - 51; 225 | 226 | // how far past the bottom plus control bar and height of element 227 | var bottom_diff = offset.top - doc_height + 63 + this.individual_height; 228 | if (top_diff <= 0) { 229 | // move one item up 230 | this.$el.scrollTop(this.$el.scrollTop() - this.individual_height); 231 | } else if (bottom_diff > 0) { 232 | // move one item down 233 | this.$el.scrollTop(this.$el.scrollTop() + this.individual_height); 234 | } 235 | } 236 | 237 | // remove the old selection 238 | if (index_in_queue !== null) { 239 | delFromSelection(player.queue_pool[index_in_queue].attributes._id); 240 | } 241 | 242 | // select the new item 243 | addToSelection(newly_selected.attributes._id, false); 244 | }, 245 | 246 | renamePlaylist: function(ev) { 247 | bootbox.prompt({ 248 | title: 'What would you like to rename "' + player.playlist.title + 249 | '" to?', 250 | value: player.playlist.title, 251 | callback: function(result) { 252 | if (result !== null) { 253 | socket.emit('rename_playlist', { 254 | title: result, 255 | id: player.playlist._id, 256 | }); 257 | $('#playlist_header').text(result); 258 | } 259 | }, 260 | }); 261 | }, 262 | 263 | deletePlaylist: function(ev) { 264 | bootbox.dialog({ 265 | message: 'Do you really want to delete this playlist?', 266 | title: 'Delete Playlist', 267 | buttons: { 268 | cancel: { 269 | label: 'Cancel', 270 | className: 'btn-default', 271 | }, 272 | 273 | del: { 274 | label: 'Delete', 275 | className: 'btn-danger', 276 | callback: function() { 277 | socket.emit('delete_playlist', {del: player.playlist._id}); 278 | MusicApp.router.playlist('LIBRARY'); 279 | }, 280 | }, 281 | }, 282 | }); 283 | }, 284 | 285 | renderSong: function() { 286 | // simple block to make sure it is only processing a render once at any point in time 287 | if (!this.processing) { 288 | this.processing = true; 289 | 290 | // cache scroll top variable 291 | this.scrollTop = this.scrollElem.scrollTop(); 292 | 293 | // get the index of the item in the center of the screen 294 | // if contentHeight is 0, contentHeight / 2 is NaN, so default to 0 295 | var middle_of_viewport = this.scrollTop + (this.contentHeight / 2 || 0) - this.meta_height; 296 | this.middle_item = Math.floor(middle_of_viewport / this.individual_height); 297 | 298 | // get the bounds of items to draw 299 | var min = Math.floor(this.middle_item - this.how_many_drawn / 2); 300 | var max = Math.floor(this.middle_item + this.how_many_drawn / 2); 301 | 302 | // if the min is less than 0, set it to 0 303 | if (min < 0) { 304 | min = 0; 305 | max = Math.min(this.how_many_drawn, player.songs.length - 1); 306 | } else if (max > player.songs.length - 1) { 307 | // set the max item to the last item 308 | max = player.songs.length - 1; 309 | 310 | // only let it go as low as 0 311 | min = Math.max(max - this.how_many_drawn, 0); 312 | } 313 | 314 | if (min != this.lastmin || max != this.lastmax) { 315 | // shortcut to remove all items easily 316 | if (min > this.lastmax || max < this.lastmin) { 317 | this.$el.find('.song_row').remove(); 318 | this.drawn_full = false; 319 | } 320 | 321 | // add the elements from the side they can be added from 322 | var index = 0; 323 | var diff; 324 | var render_item; 325 | if (max > this.lastmax) { 326 | // add them to the bottom 327 | diff = Math.min(max - this.lastmax, this.how_many_drawn) - 1; 328 | while (diff >= 0) { 329 | index = max - diff; 330 | render_item = this.song_template({ 331 | song: player.songs[index], 332 | selected: (player.songs[index] ? selectedItems.indexOf(player.songs[index].attributes._id) != -1 : false), 333 | index: index, 334 | }); 335 | 336 | // if we are redrawing, remove from the other side 337 | if (this.drawn_full) { 338 | this.top_spacer.next().remove(); 339 | } 340 | 341 | this.bottom_spacer.before(render_item); 342 | diff--; 343 | } 344 | } 345 | 346 | if (min < this.lastmin) { 347 | // add them to the top 348 | diff = Math.min(this.lastmin - min, this.how_many_drawn) - 1; 349 | while (diff >= 0) { 350 | index = min + diff; 351 | render_item = this.song_template({ 352 | song: player.songs[index], 353 | selected: (selectedItems.indexOf(player.songs[index].attributes._id) != -1), 354 | index: index, 355 | }); 356 | 357 | // if we are redrawing, remove from the other side 358 | if (this.drawn_full) { 359 | this.bottom_spacer.prev().remove(); 360 | } 361 | 362 | this.top_spacer.after(render_item); 363 | diff--; 364 | } 365 | } 366 | 367 | this.lastmax = max; 368 | this.lastmin = min; 369 | } 370 | 371 | // calculate the spacer heights based off what is missing 372 | var top = this.individual_height * this.lastmin; 373 | var bottom = this.individual_height * (player.songs.length - this.lastmax); 374 | 375 | // set the spacing heights 376 | this.top_spacer.css('height', top); 377 | this.bottom_spacer.css('height', bottom); 378 | 379 | // set some finish variables 380 | this.processing = false; 381 | this.drawn_full = true; 382 | } 383 | }, 384 | 385 | redrawSong: function(_id) { 386 | // check if the song is already visible 387 | var song_tr = this.$el.find('#' + _id); 388 | if (song_tr.length !== 0) { 389 | // now replace the item 390 | this.$el.find('#' + _id).replaceWith(this.song_template({ song: player.song_collection.findBy_Id(_id)})); 391 | } 392 | }, 393 | }); 394 | -------------------------------------------------------------------------------- /static/js/player/syncview.js: -------------------------------------------------------------------------------- 1 | SyncView = Backbone.View.extend({ 2 | template: '#sync_body_template', 3 | events: { 4 | 'click #fetch_data': 'connectB', 5 | 'click .sync_div': 'sync', 6 | 'click .back_to_songs': 'goBack', 7 | }, 8 | initialize: function() { 9 | this.a = { 10 | socket: socket, 11 | server_ip: this_ip, 12 | song_collection: player.song_collection, 13 | playlist_collection: player.playlist_collection, 14 | loaded: true, 15 | }; 16 | this.b = { 17 | socket: null, 18 | server_ip: '', 19 | song_collection: new SongCollection(), 20 | playlist_collection: new PlaylistCollection(), 21 | loaded: false, 22 | }; 23 | }, 24 | 25 | render: function() { 26 | this.b.server_ip = localStorage.getItem('server_b_ip') || ''; 27 | 28 | this.setElement(render(this.template, {this_ip: this_ip, remote_ip: this.b.server_ip})); 29 | 30 | if (this.b.server_ip.length > 0) { 31 | this.connectB(); 32 | } 33 | }, 34 | 35 | connectB: function() { 36 | var self = this; 37 | 38 | // save the setting 39 | this.b.server_ip = this.$el.find('#remote_b').val(); 40 | localStorage.setItem('server_b_ip', this.b.server_ip); 41 | 42 | // connect 43 | this.b.socket = io.connect(this.b.server_ip, {'force new connection': true}); 44 | this.b.socket.on('connect', function() { 45 | self.b.socket.emit('sync_page_connected'); 46 | console.log('Socket_b connected'); 47 | }); 48 | 49 | this.b.socket.on('alldata', function(data) { 50 | // reset the playlists 51 | self.b.playlist_collection.reset(); 52 | self.b.playlist_collection.add(data.playlists); 53 | 54 | // reset the songs 55 | self.b.song_collection.reset(); 56 | self.b.song_collection.add(data.songs); 57 | self.b.loaded = true; 58 | self.drawSync(); 59 | }); 60 | }, 61 | 62 | drawSync: function() { 63 | if (this.b.loaded) { 64 | // render the left and right panes 65 | this.left = new SyncListView({ 66 | el: this.$el.find('.playlists_left')[0], 67 | model: { 68 | playlists: this.a.playlist_collection, 69 | title: 'This Server', 70 | side: 'left', 71 | }, 72 | }); 73 | this.left.render(); 74 | this.right = new SyncListView({ 75 | el: this.$el.find('.playlists_right')[0], 76 | model: { 77 | playlists: this.b.playlist_collection, 78 | title: 'Remote Server', 79 | side: 'right', 80 | }, 81 | }); 82 | this.right.render(); 83 | 84 | // show the sync button 85 | this.$el.find('.sync_div').css('visibility', 'visible'); 86 | } 87 | }, 88 | 89 | sync: function() { 90 | var left_items = this.left.getSelected(); 91 | var right_items = this.right.getSelected(); 92 | 93 | var data_to_send = null; 94 | if (left_items.length > 0) { 95 | // filter the lists and work out the new songs 96 | data_to_send = this.filter_out(left_items, this.a, this.b); 97 | data_to_send.remote_url = this.a.server_ip; 98 | 99 | // sync the playlists 100 | console.log(data_to_send); 101 | this.b.socket.emit('sync_playlists', data_to_send); 102 | } 103 | 104 | if (right_items.length > 0) { 105 | // filter the lists and work out the new songs 106 | data_to_send = this.filter_out(right_items, this.b, this.a); 107 | data_to_send.remote_url = this.b.server_ip; 108 | 109 | // sync the playlists 110 | console.log(data_to_send); 111 | this.a.socket.emit('sync_playlists', data_to_send); 112 | } 113 | }, 114 | /* this function replaces the uids of songs that share the same title + artist 115 | * + album on the playlists that are being synced across. This allows for syncing 116 | * to be skipped for items that seem to be the same track. 117 | */ 118 | filter_out: function(selected_lists, from, to) { 119 | var new_songs = []; 120 | for (var list_cnt = 0; list_cnt < selected_lists.length; list_cnt++) { 121 | for (var item_cnt = 0; item_cnt < selected_lists[list_cnt].songs.length; item_cnt++) { 122 | // if the song is already in the library, set the correct _id 123 | var uid = selected_lists[list_cnt].songs[item_cnt]._id; 124 | var from_song = from.song_collection.findBy_Id(uid); 125 | if (from_song) { 126 | var match_song = to.song_collection.findItem(from_song); 127 | if (match_song) { 128 | selected_lists[list_cnt].songs[item_cnt]._id = match_song.attributes._id; 129 | } else { 130 | new_songs.push(from_song.attributes); 131 | } 132 | } 133 | } 134 | 135 | // check if the playlist is a dupe, if so merge it 136 | var match_playlist = to.playlist_collection.getByTitle(selected_lists[list_cnt].title); 137 | if (match_playlist) { 138 | selected_lists[list_cnt] = this.mergePlaylists(selected_lists[list_cnt], match_playlist.attributes); 139 | } 140 | } 141 | 142 | return {songs: new_songs, playlists: selected_lists}; 143 | }, 144 | 145 | mergePlaylists: function(list_one, list_two) { 146 | list_one._id = list_two._id; 147 | for (var song_cnt = 0; song_cnt < list_two.songs.length; song_cnt++) { 148 | // see if we can find the song in the first playlist 149 | var found = false; 150 | for (var inner_cnt = 0; inner_cnt < list_one.songs.length; inner_cnt++) { 151 | if (list_one.songs[inner_cnt]._id == list_two.songs[song_cnt]._id) { 152 | found = true; 153 | } 154 | } 155 | 156 | // if it isn't found, add it 157 | if (!found) { 158 | list_one.songs.push(list_two.songs[song_cnt]); 159 | } 160 | } 161 | 162 | return list_one; 163 | }, 164 | 165 | goBack: function() { 166 | MusicApp.contentRegion.show(MusicApp.router.songview); 167 | MusicApp.router.songview.delegateEvents(); 168 | }, 169 | }); 170 | 171 | // viewing the playlists from each server 172 | SyncListView = Backbone.View.extend({ 173 | template: '#sync_template', 174 | render: function() { 175 | this.$el.html(render(this.template, { 176 | playlists: this.model.playlists.models, 177 | side: this.model.side, 178 | title: this.model.title, 179 | })); 180 | }, 181 | 182 | getSelected: function() { 183 | var selected = []; 184 | var self = this; 185 | this.$el.find(':input').each(function(index) { 186 | if ($(this).is(':checked')) { 187 | // add the playlist by _id 188 | selected.push(self.model.playlists.getBy_Id($(this).attr('id').replace(self.model.side + '_', '')).attributes); 189 | } 190 | }); 191 | 192 | return selected; 193 | }, 194 | }); 195 | -------------------------------------------------------------------------------- /static/js/player/utils.js: -------------------------------------------------------------------------------- 1 | // this file stores utility functions and misc setup that happens 2 | // before the rest of the JS is loaded 3 | 4 | // define swig functions before swig is used at all 5 | function prettyPrintSeconds(seconds) { 6 | var pretty = ''; 7 | 8 | // days 9 | if (seconds > 86400) { 10 | pretty += Math.floor(seconds / 86400) + ' day'; 11 | 12 | // is it plural 13 | if (Math.floor(seconds / 86400) != 1.0) { 14 | pretty += 's'; 15 | } 16 | 17 | pretty += ' '; 18 | } 19 | 20 | // hours 21 | if (seconds > 3600) { 22 | pretty += Math.floor(seconds % 86400 / 3600) + ':'; 23 | } 24 | 25 | // minutes 26 | if (seconds > 60) { 27 | pretty += ('0' + Math.floor(seconds % 3600 / 60)).slice(-2) + ':'; 28 | } else { 29 | pretty += '0:'; 30 | } 31 | 32 | // seconds 33 | pretty += ('0' + Math.floor(seconds % 60)).slice(-2); 34 | return pretty; 35 | } 36 | 37 | // pretty print the date_added milliseconds 38 | function prettyPrintDateAdded(milliseconds) { 39 | // convert it to a javascript date object 40 | var date = new Date(milliseconds); 41 | 42 | // get the format of it 43 | var dateFormat = date.toString('yyyy-MM-dd HH:mm:ss'); 44 | return dateFormat; 45 | } 46 | 47 | function prettyPrintSecondsorNA(seconds) { 48 | if (seconds <= 0) { 49 | return 'N/A'; 50 | } else { 51 | return prettyPrintSeconds(seconds); 52 | } 53 | } 54 | 55 | // make it usable in swig 56 | swig.setFilter('prettyPrintSeconds', prettyPrintSecondsorNA); 57 | swig.setFilter('prettyPrintDateAdded', prettyPrintDateAdded); 58 | 59 | var cover_is_current = false; 60 | var cover_is_visible = false; 61 | var box; // coverbox ref 62 | function showCover(src) { 63 | // deactivate it if it's visible already 64 | if (cover_is_visible && box) { 65 | box.deactivate(); 66 | } 67 | 68 | // check if the new cover art is from the current track 69 | cover_is_current = ('cover/' + player.current_song.attributes.cover_location == src); 70 | 71 | // create and activate the cover art 72 | box = new CoverBox(src, function() { 73 | cover_is_visible = false; 74 | }); 75 | 76 | box.activate(); 77 | cover_is_visible = true; 78 | } 79 | 80 | // utility functions 81 | function render(template, data) { 82 | return swig.render($(template).html(), {locals: data}); 83 | } 84 | 85 | function loadedRestart(item) { 86 | itemsLoaded.push(item); 87 | if (arraysEqual(items, itemsLoaded)) { 88 | Backbone.history.stop(); 89 | Backbone.history.start(); 90 | 91 | // if the player is showing nothing, show the default place 92 | if (MusicApp.router.songview === null) { 93 | MusicApp.router.playlist(); 94 | } 95 | 96 | // if they last played a song, continue playing 97 | var play_state = localStorage.getItem('last_play_state'); // must be fetched before the song is played 98 | var song_id = localStorage.getItem('last_playing_id'); 99 | if (song_id) { 100 | player.playSong(song_id); 101 | 102 | // set the currentTime 103 | var currentTime = parseInt(localStorage.getItem('currentTime')); 104 | if (currentTime) { 105 | player.PlayMethodAbstracter.setCurrentTime(currentTime); 106 | } 107 | } 108 | 109 | // it is stored as a string 110 | if (play_state == 'false') { 111 | player.togglePlayState(); 112 | } 113 | } 114 | } 115 | 116 | function arraysEqual(a, b) { 117 | if (a === b) return true; 118 | if (a === null || b === null) return false; 119 | if (a.length != b.length) return false; 120 | 121 | a.sort(); 122 | b.sort(); 123 | 124 | for (var i = 0; i < a.length; ++i) { 125 | if (a[i] !== b[i]) return false; 126 | } 127 | 128 | return true; 129 | } 130 | 131 | function searchMatchesSong(songString, searchWords) { 132 | for (var i = 0; i < searchWords.length; i++) { 133 | if (songString.indexOf(searchWords[i]) == -1) { 134 | return false; 135 | } 136 | } 137 | 138 | return true; 139 | } 140 | 141 | function imageToBase64(img) { 142 | // create the canvas 143 | var canvas = document.createElement('canvas'); 144 | canvas.height = img.height; 145 | canvas.width = img.width; 146 | 147 | // grab the context for drawing 148 | var ctx = canvas.getContext('2d'); 149 | 150 | // draw the image to the context 151 | ctx.drawImage(img, 0, 0, img.width, img.height); 152 | 153 | // return the dataURL 154 | return canvas.toDataURL('image/png'); 155 | } 156 | 157 | // implementation of knuth-shuffle by @coolaj86 158 | // https://github.com/coolaj86/knuth-shuffle 159 | function shuffle_array(array) { 160 | var currentIndex = array.length; 161 | var temporaryValue; 162 | var randomIndex; 163 | 164 | while (currentIndex !== 0) { 165 | randomIndex = Math.floor(Math.random() * currentIndex); 166 | currentIndex -= 1; 167 | temporaryValue = array[currentIndex]; 168 | array[currentIndex] = array[randomIndex]; 169 | array[randomIndex] = temporaryValue; 170 | } 171 | 172 | return array; 173 | } 174 | 175 | function randomIntFromInterval(min, max) { 176 | return Math.floor(Math.random() * (max - min + 1) + min); 177 | } 178 | 179 | function deAttribute(collection) { 180 | var newCollection = []; 181 | for (var i = 0; i < collection.length; i++) { 182 | newCollection.push(collection[i].attributes); 183 | } 184 | 185 | return newCollection; 186 | } 187 | 188 | // make table row widths be correct when dragging 189 | var fixHelper = function(e, ui) { 190 | ui.children().each(function() { 191 | $(this).width($(this).width()); 192 | }); 193 | 194 | return ui; 195 | }; 196 | 197 | // define things before they are used 198 | var socket = io.connect('//' + window.location.host, {path: window.location.pathname + 'socket.io'}); 199 | 200 | -------------------------------------------------------------------------------- /static/js/preloader/preloader.js: -------------------------------------------------------------------------------- 1 | //State to check whether app has loaded 2 | var state_reached = false; 3 | Pace.on("change", function (progress) { 4 | // Clock our load progress 5 | console.log(progress); 6 | if (progress >= 90 && !(state_reached)) { 7 | state_reached = true; 8 | var preloader = document.getElementById("preloader-overlay"); 9 | // Add hide class 10 | preloader.className += " preloader-hide"; 11 | } 12 | 13 | }); 14 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | // This file is a bunch of utility functions 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | 5 | var walk = function(dir, done) { 6 | var results = []; 7 | var strip_results = []; 8 | fs.readdir(dir, function(err, list) { 9 | if (err) return done(err); 10 | var pending = list.length; 11 | if (!pending) return done(null, results); 12 | list.forEach(function(file) { 13 | if (file[0] === '.') { 14 | if (!--pending) done(null, results, strip_results); 15 | return; 16 | } 17 | 18 | file = path.join(dir, file); 19 | fs.stat(file, function(err, stat) { 20 | if (stat && stat.isDirectory()) { 21 | walk(file, function(err, res) { 22 | results = results.concat(res); 23 | if (!--pending) done(null, results, strip_results); 24 | }); 25 | } else { 26 | results.push(file); 27 | if (!--pending) done(null, results, strip_results); 28 | } 29 | }); 30 | }); 31 | }); 32 | }; 33 | 34 | exports.walk = walk; 35 | 36 | var contains = function(a, obj) { 37 | var i = a.length; 38 | while (i--) { 39 | if (a[i] === obj) { 40 | return true; 41 | } 42 | } 43 | 44 | return false; 45 | }; 46 | 47 | exports.contains = contains; 48 | 49 | // function to get the IP of the current machine on the network 50 | var cached_ip = null; 51 | var getip = function(callback) { 52 | if (cached_ip === null) { 53 | require('dns').lookup(require('os').hostname(), function(err, add, fam) { 54 | callback(add); 55 | 56 | // save it for the next call 57 | cached_ip = add; 58 | }); 59 | } else { 60 | callback(cached_ip); 61 | } 62 | }; 63 | 64 | exports.getip = getip; 65 | 66 | exports.decodeBase64Image = function(dataString) { 67 | var matches = dataString.match(/^data:image\/([A-Za-z-+\/]+);base64,(.+)$/); 68 | var response = {}; 69 | 70 | if (matches === null) { 71 | console.log(dataString); 72 | console.log('Invalid file uploaded'); 73 | } 74 | 75 | response.type = matches[1]; 76 | response.data = new Buffer(matches[2], 'base64'); 77 | 78 | return response; 79 | }; 80 | -------------------------------------------------------------------------------- /views/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Admin Login 7 | 136 | 137 | 138 | 139 |
140 |

Login

141 | {% if msg %} 142 |
143 |

{{msg}}

144 |
145 | {% endif %} 146 |
147 | 154 | {% if log %} 155 | 165 |
166 |
167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /views/base/main_base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block title %}Desktop Command Remote{% endblock %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 28 | {% block head %} {% endblock %} 29 | 30 | 31 | 32 |
33 |
34 | 35 |
36 | 37 |

Find your favourite songs :)

38 | 39 |
40 | 41 | Search 42 | 43 |
44 |
45 | 46 | 47 | 48 | 49 | 53 | 54 | 55 |
56 | {% if menu %} 57 | 91 | {% endif %} {% block content %} {% endblock %} 92 |
93 | 117 | {% block scripts %} {% endblock %} 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /views/experimental-ui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 |
18 |
19 |
20 |
21 |
22 | 23 |
24 | Encore 25 |
26 |
27 |
28 |
29 | 30 |
31 |
32 | 33 | 34 | 35 |
36 | 37 |
38 |
39 |
40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/main_base.html' %} {% block title %}Music{% endblock %} {% block extra_nav %} 2 | {% if !log %} 3 |
  • OSDLabs
  • 4 | {% endif %} 5 | {% if log %} 6 | 21 | {% endif %} 22 | 23 | {% if log %} 24 |
  • Settings
  • 25 | {% endif %} {% endblock %} {% block content %} 26 |
    27 | 29 |
    30 |
    31 | 32 |
    33 |
    34 |
    35 | 36 |
    37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
    46 |
     
    47 |
    48 |
    49 |
    50 |
    51 | 54 |
    55 | 58 | {% raw %} 59 | 83 | 121 | 143 | 150 | 193 | 225 | 230 | 241 | 242 | 263 | 264 | 297 | 306 | {% endraw %} 307 | 313 | {% endblock %} {% block scripts %} {% endblock %} 314 | -------------------------------------------------------------------------------- /views/index1.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/main_base.html' %} 2 | 3 | {% block title %}Music{% endblock %} 4 | 5 | {% block extra_nav %} 6 | 21 | {% if !demo %} 22 |
  • Settings
  • 23 | {% endif %} 24 | {% endblock %} 25 | 26 | {% block content %} 27 |
    28 | 30 |
    31 |
    32 |
    33 |
    34 |
    35 | 36 |
     
    37 |
    38 | 39 | 40 | 41 | 42 | 43 | 44 |
    45 |
    46 |
    47 |
    48 |
    49 | 50 |
    51 | 54 | {% raw %} 55 | 80 | 123 | 145 | 152 | 194 | 232 | 237 | 248 | 249 | 270 | 271 | 298 | 306 | {% endraw %} 307 | 312 | {% endblock %} 313 | {% block scripts %} 314 | {% endblock %} 315 | -------------------------------------------------------------------------------- /views/mobile.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/main_base.html' %} 2 | 3 | {% block title %}Music{% endblock %} 4 | 5 | {% block content %} 6 |
    7 |
    8 | 10 |
    11 |
    12 |
    13 |
    14 |
    15 | 16 |
    17 | 18 | 19 | 20 | 21 | 22 | 23 |
    24 |
    25 |
    26 | 27 |
    28 | 31 | {% raw %} 32 | 51 | 80 | 92 | 99 | 137 | 142 | 153 | {% endraw %} 154 | 159 | {% endblock %} 160 | {% block scripts %} 161 | 166 | {% endblock %} 167 | --------------------------------------------------------------------------------