├── .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 | 4 | -------------------------------------------------------------------------------- /static/images/encore-cover.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /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 = $('{{msg}}
144 |Find your favourite songs :)
38 | 39 | 44 |