├── .gitattributes ├── compression_server ├── .gitignore ├── .dockerignore ├── nodemon.json ├── config │ ├── server.config.js │ └── aws.config.js ├── test │ ├── testFiles │ │ └── .gitignore │ └── compressionTest.js ├── temp_audio │ ├── wav_temp │ │ └── .gitignore │ ├── hi_res_inbox │ │ └── .gitignore │ └── low_res_outbox │ │ └── .gitignore ├── helper_functions │ └── serverAuth.js ├── Dockerfile ├── package.json ├── routes │ └── index.js ├── README.md ├── app.js ├── bin │ └── www └── run.sh ├── .bowerrc ├── .eslintignore ├── client ├── favicon.ico ├── assets │ ├── pin.png │ ├── bands │ │ ├── safety1.JPG │ │ ├── safety2.JPG │ │ ├── safety3.JPG │ │ ├── safety4.JPG │ │ ├── safety5.JPG │ │ ├── evergreen1.jpg │ │ ├── evergreen2.jpg │ │ ├── evergreen3.jpg │ │ └── evergreen4.jpg │ └── pin-selected.png ├── app │ ├── modal │ │ └── modal.html │ ├── services │ │ ├── infoFactory.js │ │ ├── usersFactory.js │ │ ├── uploadsFactory.js │ │ └── groupsFactory.js │ ├── info │ │ ├── info.js │ │ └── info.html │ ├── styles │ │ ├── _colors.scss │ │ └── _sliders.scss │ ├── songs │ │ ├── songView.html │ │ ├── songs.html │ │ └── songs.js │ ├── profile │ │ ├── profile.js │ │ └── profile.html │ ├── player │ │ ├── player.html │ │ └── player.js │ ├── song │ │ ├── song.html │ │ └── song.js │ ├── nav │ │ └── nav.html │ ├── auth │ │ ├── auth.js │ │ └── login.html │ ├── groups │ │ ├── settings.js │ │ ├── settings.html │ │ ├── groups.html │ │ └── groups.js │ ├── upload │ │ ├── upload.html │ │ └── upload.js │ └── playlist │ │ ├── playlist.html │ │ └── playlist.js └── index.html ├── nodemon.json ├── .travis.yml ├── .dockerignore ├── server ├── config │ ├── aws.config.js │ ├── middleware.js │ ├── config.js │ ├── helpers.js │ └── routes.js ├── models │ ├── infoModel.js │ ├── commentModel.js │ ├── userModel.js │ ├── playlistModel.js │ ├── songModel.js │ └── groupModel.js ├── controllers │ ├── infoController.js │ ├── commentController.js │ ├── compressionServer.js │ ├── playlistController.js │ ├── upload.js │ ├── userController.js │ ├── groupController.js │ └── songController.js ├── server.js └── db │ └── database.js ├── .editorconfig ├── Dockerfile ├── .eslintrc.json ├── docker-compose.example.yml ├── bower.json ├── LICENSE ├── .gitignore ├── docker_info.md ├── backup └── backupAudiopile.sh ├── test ├── client │ └── unit │ │ └── servicesTest.js └── server │ ├── unit │ ├── schemaTest.js │ ├── songTest.js │ └── playlistTest.js │ ├── testHelpers.js │ └── integration │ └── apiTest.js ├── sessionSetup.js ├── pomander.sh ├── package.json ├── gulpfile.js ├── README.md ├── we need to do these! └── _PRESS-RELEASE.md └── CONTRIBUTING.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /compression_server/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "client/lib" 3 | } -------------------------------------------------------------------------------- /compression_server/.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules/ -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/lib 3 | **/dist 4 | **/*config.js 5 | -------------------------------------------------------------------------------- /client/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuoyantPyramid/Audiopile/HEAD/client/favicon.ico -------------------------------------------------------------------------------- /compression_server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "ignore": ["temp_audio/*"] 4 | } -------------------------------------------------------------------------------- /client/assets/pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuoyantPyramid/Audiopile/HEAD/client/assets/pin.png -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "ignore": ["compression_server/temp_audio/*", "dist/*"] 4 | } -------------------------------------------------------------------------------- /client/assets/bands/safety1.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuoyantPyramid/Audiopile/HEAD/client/assets/bands/safety1.JPG -------------------------------------------------------------------------------- /client/assets/bands/safety2.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuoyantPyramid/Audiopile/HEAD/client/assets/bands/safety2.JPG -------------------------------------------------------------------------------- /client/assets/bands/safety3.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuoyantPyramid/Audiopile/HEAD/client/assets/bands/safety3.JPG -------------------------------------------------------------------------------- /client/assets/bands/safety4.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuoyantPyramid/Audiopile/HEAD/client/assets/bands/safety4.JPG -------------------------------------------------------------------------------- /client/assets/bands/safety5.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuoyantPyramid/Audiopile/HEAD/client/assets/bands/safety5.JPG -------------------------------------------------------------------------------- /client/assets/pin-selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuoyantPyramid/Audiopile/HEAD/client/assets/pin-selected.png -------------------------------------------------------------------------------- /client/assets/bands/evergreen1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuoyantPyramid/Audiopile/HEAD/client/assets/bands/evergreen1.jpg -------------------------------------------------------------------------------- /client/assets/bands/evergreen2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuoyantPyramid/Audiopile/HEAD/client/assets/bands/evergreen2.jpg -------------------------------------------------------------------------------- /client/assets/bands/evergreen3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuoyantPyramid/Audiopile/HEAD/client/assets/bands/evergreen3.jpg -------------------------------------------------------------------------------- /client/assets/bands/evergreen4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuoyantPyramid/Audiopile/HEAD/client/assets/bands/evergreen4.jpg -------------------------------------------------------------------------------- /compression_server/config/server.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | primaryServer: process.env.JAMRECORD_PRIMARY_SERVER 3 | } -------------------------------------------------------------------------------- /compression_server/test/testFiles/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /compression_server/temp_audio/wav_temp/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /compression_server/temp_audio/hi_res_inbox/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /compression_server/temp_audio/low_res_outbox/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /compression_server/helper_functions/serverAuth.js: -------------------------------------------------------------------------------- 1 | var serverAuth = function(req, res, next) { 2 | var passcode = req.headers; 3 | console.log('Passcode! : ', passcode); 4 | } 5 | 6 | 7 | 8 | 9 | 10 | module.exports = serverAuth; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'stable' 4 | addons: 5 | postgresql: "9.3" 6 | 7 | #services: 8 | # - postgresql 9 | 10 | before_script: 11 | - psql --version 12 | - psql -c 'create database jamstest;' -U postgres -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | compression_server/ 3 | client/lib/ 4 | client/dist/ 5 | test/ 6 | .git 7 | .gitignore 8 | .editorconfig 9 | .eslintignore 10 | eslintrc.json 11 | .travis.yml 12 | CONTRIBUTING.md 13 | pomander.sh 14 | sessionsSetup.js 15 | STYLE-GUIDE.md -------------------------------------------------------------------------------- /server/config/aws.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "accessKeyId": process.env.AWS_JAMRECORD_ACCESS_KEY_ID, 3 | "secretAccessKey": process.env.AWS_JAMRECORD_SECRET_ACCESS_KEY, 4 | "region": process.env.AWS_JAMRECORD_REGION, 5 | "bucket": process.env.AWS_JAMRECORD_BUCKET 6 | } -------------------------------------------------------------------------------- /compression_server/config/aws.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "accessKeyId": process.env.AWS_JAMRECORD_ACCESS_KEY_ID, 3 | "secretAccessKey": process.env.AWS_JAMRECORD_SECRET_ACCESS_KEY, 4 | "region": process.env.AWS_JAMRECORD_REGION, 5 | "bucket": process.env.AWS_JAMRECORD_BUCKET 6 | } -------------------------------------------------------------------------------- /client/app/modal/modal.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /server/models/infoModel.js: -------------------------------------------------------------------------------- 1 | var db = require('../db/database'); 2 | var User = db.User; 3 | var Song = db.Song; 4 | var Promise = require('bluebird'); 5 | 6 | 7 | var countUsers = function() { 8 | return User.count(); 9 | }; 10 | 11 | var countSongs = function() { 12 | return Song.count(); 13 | }; 14 | 15 | module.exports = { 16 | countUsers: countUsers, 17 | countSongs: countSongs 18 | }; -------------------------------------------------------------------------------- /client/app/services/infoFactory.js: -------------------------------------------------------------------------------- 1 | angular.module('jam.infoFactory', []) 2 | 3 | .factory('Info', ['$http', '$q', function (http, q) { 4 | 5 | var getStats = function() { 6 | return http({ 7 | method: 'GET', 8 | url: '/api/info/' 9 | }).then(function(res) { 10 | return res; 11 | }); 12 | }; 13 | 14 | return { 15 | getStats: getStats 16 | }; 17 | 18 | }]); 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:argon 2 | 3 | ENV cachebust=840762987412734 4 | 5 | RUN npm install gulp -g 6 | RUN npm install bower -g 7 | 8 | # Create app directory 9 | RUN mkdir -p /usr/src/app/ 10 | WORKDIR /usr/src/app/ 11 | 12 | # Install app dependencies 13 | COPY . /usr/src/app/ 14 | 15 | RUN npm install 16 | RUN cd client 17 | RUN bower install --allow-root 18 | RUN cd .. 19 | 20 | RUN gulp build 21 | 22 | EXPOSE 3000 23 | 24 | CMD [ "node", "server/server.js" ] 25 | -------------------------------------------------------------------------------- /compression_server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntuffmpegnode3 2 | 3 | # ENV cachebust = 148988344445 4 | 5 | # Create app directory 6 | 7 | RUN mkdir -p /usr/src/app/compression_server 8 | WORKDIR /usr/src/app/compression_server 9 | 10 | # Install app dependencies 11 | # COPY . /usr/src/app/ 12 | COPY ./package.json /usr/src/app/compression_server/package.json 13 | 14 | 15 | RUN npm install 16 | 17 | EXPOSE 4000 18 | 19 | # CMD ["npm", "run", "gulp"] 20 | 21 | CMD ["node", "/bin/www"] -------------------------------------------------------------------------------- /compression_server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compression_server", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "nodemon ./bin/www" 7 | }, 8 | "dependencies": { 9 | "aws-sdk": "^2.2.46", 10 | "bluebird": "^3.3.4", 11 | "body-parser": "~1.13.2", 12 | "cookie-parser": "~1.3.5", 13 | "debug": "~2.2.0", 14 | "express": "~4.13.1", 15 | "fluent-ffmpeg": "^2.0.1", 16 | "morgan": "~1.6.1", 17 | "queue": "^3.1.0", 18 | "request": "^2.69.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/app/info/info.js: -------------------------------------------------------------------------------- 1 | angular.module('jam.info', []) 2 | .controller('InfoController', ['$scope', '$location', 'Songs', 'Users', 'Groups', 'Info', '$anchorScroll', function($scope, $location, Songs, Users, GR, Info, $anchorScroll) { 3 | 4 | $scope.scrollTo = function(id) { 5 | console.log('scroll to :', id, $location); 6 | $location.hash(id); 7 | $anchorScroll(); 8 | }; 9 | 10 | Info.getStats().then(function(res) { 11 | console.log('then!', res); 12 | $scope.userCount = res.data.userCount; 13 | $scope.songCount = res.data.songCount; 14 | }); 15 | 16 | }]); -------------------------------------------------------------------------------- /compression_server/routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var audioProcessing = require('../helper_functions/audioProcessing.js'); 4 | var serverAuth = require('../helper_functions/serverAuth.js'); 5 | 6 | 7 | /* GET home page. */ 8 | router.get('/', function(req, res, next) { 9 | console.log('---Recieves request to root route'); 10 | res.send('Server online'); 11 | }); 12 | 13 | // Handle compression request 14 | // add 'serverAuth' middleware in here 15 | router.post('/compress', audioProcessing.addToQueue); 16 | 17 | module.exports = router; -------------------------------------------------------------------------------- /server/config/middleware.js: -------------------------------------------------------------------------------- 1 | var bodyParser = require('body-parser'); 2 | var methodOverride = require('method-override'); 3 | var morgan = require('morgan'); 4 | var path = require('path'); 5 | 6 | var middleware = function (app, express) { 7 | // override with the X-HTTP-Method-Override header in the request. simulate DELETE/PUT 8 | app.use(methodOverride('X-HTTP-Method-Override')); 9 | app.use(morgan('dev')); 10 | app.use(bodyParser.urlencoded({extended: true})); 11 | app.use(bodyParser.json()); 12 | app.use(express.static(path.join(__dirname + '/../../client'))); 13 | }; 14 | 15 | module.exports = middleware; -------------------------------------------------------------------------------- /client/app/styles/_colors.scss: -------------------------------------------------------------------------------- 1 | /************************************************** 2 | * COLOR VARIABLES 3 | **************************************************/ 4 | $black: #000; // black 5 | $white: #fff; // white 6 | $reallylightgrey: #f2f2f2; 7 | $lightgrey: #d1d1d1; 8 | $midgrey: #999; 9 | $reallydarkgrey: #222; 10 | $darkgrey: #555; 11 | $bgcolor: #FAFCFC; // light desaturated bluegreen 12 | $bgcolor-2: #385E8A; // dark greyblue 13 | $textcolor: $darkgrey; 14 | $textcolor-2: #FBFBFB; // super light grey 15 | $textcolor-3: #5C86B8; // lighter greyblue 16 | $linkcolor: #99D1B2; // greygreen 17 | $red: #ed5e5e; // greyred 18 | -------------------------------------------------------------------------------- /server/models/commentModel.js: -------------------------------------------------------------------------------- 1 | var db = require('../db/database'); 2 | var Comment = db.Comment; 3 | var Song = db.Song; 4 | var User = db.User; 5 | 6 | var addComment = function (userId, songId, time, note) { 7 | return Comment.create({ 8 | time: time, 9 | note: note, 10 | userId: userId, 11 | songId: songId 12 | }, { 13 | include: { 14 | model: Song, 15 | model: User 16 | } 17 | }); 18 | }; 19 | 20 | var getComment = function (commentId) { 21 | return Comment.findById(commentId); 22 | }; 23 | 24 | module.exports = { 25 | addComment: addComment, 26 | getComment: getComment 27 | }; 28 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | /** 2 | * These rules enforce the Hack Reactor Style Guide: 3 | * http://bookstrap.hackreactor.com/wiki/Style-Guide 4 | * 5 | * Visit this repo for more information: 6 | * https://github.com/hackreactor-labs/eslint-config-hackreactor 7 | */ 8 | 9 | { 10 | "env": { 11 | "browser": true, 12 | "node": true, 13 | "es6": true 14 | }, 15 | "extends": "./node_modules/eslint-config-hackreactor/index.js", 16 | "plugins": [ 17 | "react" 18 | ], 19 | "parserOptions": { 20 | "ecmaVersion": 6, 21 | "sourceType": "module", 22 | "ecmaFeatures": { 23 | "jsx": true 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/controllers/infoController.js: -------------------------------------------------------------------------------- 1 | var Info = require('../models/infoModel'); 2 | 3 | var audioPileStatusUpdate = function(req, res) { 4 | var stats = {}; 5 | 6 | Info.countUsers() 7 | .then(function(userCount) { 8 | return userCount; 9 | }) 10 | .then(function(userCount) { 11 | stats.userCount = userCount; 12 | return Info.countSongs(); 13 | }) 14 | .then(function(songCount) { 15 | stats.songCount = songCount; 16 | res.json(stats); 17 | }) 18 | .catch(function(err) { 19 | console.log('Error getting info: ', err); 20 | }); 21 | }; 22 | 23 | module.exports = { 24 | audioPileStatusUpdate: audioPileStatusUpdate 25 | }; -------------------------------------------------------------------------------- /docker-compose.example.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | 5 | db: 6 | image: postgres 7 | expose: 8 | - 5432 9 | environment: 10 | POSTGRES_USER: '----------' 11 | POSTGRES_PASSWORD: '------------' 12 | POSTGRES_DB: '--------' 13 | 14 | compression: 15 | build: ./compression_server 16 | # image: compression 17 | environment: 18 | PORT: 4000 19 | ports: 20 | - "4000:4000" 21 | 22 | primary: 23 | build: . 24 | volumes: 25 | - . 26 | links: 27 | - db 28 | - compression 29 | ports: 30 | - "3000:3000" 31 | environment: 32 | SEQ_DB: '----------' 33 | SEQ_USER: '----------' 34 | SEQ_PW: '---------' 35 | PORT: 3000 36 | DATABASE_URL: --------------------- 37 | COMPRESSION_SERVER: 'http://compression:4000' -------------------------------------------------------------------------------- /server/controllers/commentController.js: -------------------------------------------------------------------------------- 1 | var Comment = require('../models/commentModel'); 2 | 3 | var addComment = function (req, res, next) { 4 | var time = req.body.time; 5 | var note = req.body.note; 6 | var userId = req.body.userId; 7 | var songId = req.params.id; 8 | 9 | Comment.addComment(userId, songId, time, note) 10 | .then(function(comment) { 11 | res.json(comment); 12 | }) 13 | .catch(function(err) { 14 | next(err); 15 | }); 16 | }; 17 | 18 | var deleteComment = function (req, res, next) { 19 | var commentId = req.params.id; 20 | 21 | Comment.getComment(commentId) 22 | .then(function(comment) { 23 | comment.destroy(); 24 | res.json(comment); 25 | }) 26 | .catch(function(err) { 27 | next(err); 28 | }); 29 | }; 30 | 31 | module.exports = { 32 | addComment: addComment, 33 | deleteComment: deleteComment 34 | }; 35 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jamrecord", 3 | "version": "0.0.0", 4 | "authors": [ 5 | "Brian Fogg", 6 | "Sondra Silverhawk", 7 | "Erick Paepke", 8 | "Nick Echols" 9 | ], 10 | "license": "MIT", 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "client/lib" 16 | ], 17 | "dependencies": { 18 | "angular": "1.5.2", 19 | "angular-route": "~1.3.16", 20 | "angular-mocks": "~1.3.16", 21 | "angular-animate": "~1.5.0", 22 | "angular-file-upload": "^2.2.0", 23 | "ng-file-upload-shim": "^12.0.4", 24 | "ngprogress": "^1.1.3", 25 | "underscore": "^1.8.3", 26 | "ng-img-crop-full-extended": "ngImgCropFullExtended#~0.5.4", 27 | "d3": "^3.5.16", 28 | "angularjs-slider": "^2.10.4", 29 | "angular-drag-and-drop-lists": "^1.4.0", 30 | "ng-focus-if": "^1.0.5" 31 | }, 32 | "resolutions": { 33 | "angular": "1.5.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Audiopile 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /compression_server/README.md: -------------------------------------------------------------------------------- 1 | ### Compression server README 2 | 3 | 4 | ## Install 5 | 6 | - npm install 7 | 8 | 9 | ## Other requirements 10 | - Need to find and change important server urls. It's all 'localhost' right now 11 | 12 | - install ffmpeg 13 | - libmp3lame or libx264... not sure which yet 14 | 15 | 'brew install ffmpeg' 16 | 17 | -this should do it 18 | 19 | ---------------------- 20 | SOcket.io to client 21 | 22 | - I'll need to take responses from server2, send them back to server1. 23 | - these responses correspoond to upload jobs. 24 | I should store the socket id number with each job so that when the responses come back they know who to direct to downstream. This is not high priority at this time. 25 | 26 | -------------- 27 | Collisions incoming 28 | 29 | - There's a lot of stuff commented out in here. I'm still working on it. 30 | 31 | - WHen pull request comes through I will expect conflicts: 32 | 33 | - Server/song db add. This is where secondary server request is included and triggered 34 | - Seconddary server will have trouble with new filenames. Get rid of .wav appending in audio processing. Files have this now by default. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 BuoyantPyramid 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 | -------------------------------------------------------------------------------- /server/config/config.js: -------------------------------------------------------------------------------- 1 | var port = process.env.JAMRECORD_PORT; 2 | 3 | var connectionString; 4 | 5 | if (process.env.JAMRUN === 'test') { 6 | connectionString = process.env.JAMRECORD_TEST_CONNECTION_STRING; 7 | } else if (process.env.JAMRUN === 'production') { 8 | connectionString = process.env.JAMRECORD_PRODUCTION_CONNECTION_STRING; 9 | } else if (process.env.JAMRUN === 'development') { 10 | connectionString = process.env.JAMRECORD_DEV_CONNECTION_STRING; 11 | } 12 | 13 | var COMPRESSION_SERVER = process.env.COMPRESSION_SERVER; 14 | var ZENCODER_COMPRESSION_SERVER = process.env.ZENCODER_COMPRESSION_SERVER; 15 | var ZENCODER_API_KEY = process.env.ZENCODER_API_KEY; 16 | 17 | var JWT_SECRET = process.env.JAMRECORD_JWT_SECRET; 18 | var mailgun = { 19 | 'api_key': process.env.JAMRECORD_MAILGUN_API_KEY, 20 | domain: process.env.JAMRECORD_MAILGUN_DOMAIN 21 | }; 22 | 23 | module.exports = { 24 | mailgun: mailgun, 25 | port: port, 26 | JWT_SECRET: JWT_SECRET, 27 | connectionString: connectionString, 28 | COMPRESSION_SERVER: COMPRESSION_SERVER, 29 | ZENCODER_COMPRESSION_SERVER: ZENCODER_COMPRESSION_SERVER, 30 | ZENCODER_API_KEY: ZENCODER_API_KEY 31 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | node_modules/ 36 | bower_components/ 37 | client/lib 38 | *.log 39 | 40 | build/ 41 | client/dist/ 42 | 43 | # Ignore Mac DS_Store files 44 | .DS_Store 45 | 46 | # Ignore config file 47 | docker-compose.yml 48 | 49 | # ignore uploadInbox 50 | server/uploadInbox/* 51 | 52 | 53 | # ignore compression_server audio folder 54 | # compression_server/temp_audio/* 55 | 56 | # ignore testFiles 57 | test/compression_server/testFiles/* 58 | 59 | # ignore db backup folder 60 | backup/postgres-backup/* 61 | 62 | 63 | -------------------------------------------------------------------------------- /server/controllers/compressionServer.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var crypto = require('crypto'); 3 | var config = require('../config/config.js'); 4 | 5 | 6 | // TODO: this can probably go in a factory 7 | var requestFileCompression = function(song) { 8 | console.log('=============== Make request to compression server'); 9 | 10 | 11 | var url = ''; 12 | var params = { 13 | songID: song.id, 14 | s3UniqueHash: song.uniqueHash 15 | }; 16 | 17 | console.log('------------Call to compreeion server-------------------'); 18 | console.log('Url: ', url); 19 | console.log('Params: ', params); 20 | 21 | 22 | // request.post( 23 | // config.compressionServer + '/compress', 24 | // { json: params }, 25 | // function (error, response, body) { 26 | // // socket messages trigger to unique downstream user here 27 | // if (error) { 28 | // console.log('Request compression error: ', error); 29 | // } else if (!error && response.statusCode === 200) { 30 | // console.log('Successful request to compression server: ', body); 31 | // } 32 | // } 33 | // ); 34 | 35 | }; 36 | 37 | module.exports = { 38 | requestFileCompression: requestFileCompression 39 | }; -------------------------------------------------------------------------------- /compression_server/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var logger = require('morgan'); 4 | var cookieParser = require('cookie-parser'); 5 | var bodyParser = require('body-parser'); 6 | var routes = require('./routes/index'); 7 | 8 | var app = express(); 9 | 10 | app.use(logger('dev')); 11 | app.use(bodyParser.json()); 12 | app.use(bodyParser.urlencoded({ extended: false })); 13 | app.use(cookieParser()); 14 | 15 | app.use('/', routes); 16 | 17 | // catch 404 and forward to error handler 18 | app.use(function(req, res, next) { 19 | var err = new Error('Not Found'); 20 | err.status = 404; 21 | next(err); 22 | }); 23 | 24 | // error handlers 25 | 26 | // development error handler 27 | // will print stacktrace 28 | if (app.get('env') === 'development') { 29 | app.use(function(err, req, res, next) { 30 | res.status(err.status || 500); 31 | res.send({ 32 | message: err.message, 33 | error: err 34 | }); 35 | }); 36 | } 37 | 38 | // production error handler 39 | // no stacktraces leaked to user 40 | app.use(function(err, req, res, next) { 41 | res.status(err.status || 500); 42 | res.send({ 43 | message: err.message, 44 | error: {} 45 | }); 46 | }); 47 | 48 | 49 | module.exports = app; 50 | -------------------------------------------------------------------------------- /client/app/songs/songView.html: -------------------------------------------------------------------------------- 1 |
2 | 4 | 5 | 6 |
7 | 8 | 9 |
10 | 11 |
12 | {{ song.title || 'Untitled' }} 13 | 14 | 15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /docker_info.md: -------------------------------------------------------------------------------- 1 | ### --- Using Docker --- 2 | 3 | install with these instructions: 4 | https://docs.docker.com/engine/installation/mac/ 5 | 6 | start docker CLI 7 | bash --login '/Applications/Docker/Docker Quickstart Terminal.app/Contents/Resources/Scripts/start.sh' 8 | 9 | display images 10 | docker images 11 | docker images -a 12 | 13 | list running containers 14 | docker ps 15 | 16 | show latest created container 17 | docker ps -l 18 | 19 | --- helpful commands --- 20 | Delete all containers 21 | docker rm $(docker ps -a -q) 22 | 23 | Delete all images 24 | docker rmi $(docker images -q) 25 | 26 | build image 27 | docker build -t brian/testapp . 28 | 29 | run container 30 | docker run 31 | -p - port settings 32 | 49160:8080 33 | -d 34 | test_app 35 | 36 | docker run -p 3000:3000 -d brian/testapp 37 | 38 | POSTGRES 39 | 40 | pull it down 41 | docker pull postgres 42 | 43 | start it 44 | docker run --name some-postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres 45 | 46 | connect to it from an app 47 | docker run --link some-postgres:postgres -d brian/testapp 48 | 49 | ########################## 50 | 51 | --------------- Docker Environment Setup --------------- 52 | Docker Quickstart 53 | 54 | docker-compose up 55 | 56 | Wait for the long download! 57 | 58 | visit: 192.168.99.100 -------------------------------------------------------------------------------- /backup/backupAudiopile.sh: -------------------------------------------------------------------------------- 1 | todaysDate=$(date +"%Y-%m-%d") 2 | filename="AudioPile_dumpAll__"$todaysDate".sql" 3 | pg_dumpall -v -h localhost -f ./postgres-backup/$filename 4 | 5 | # You don't need Fog in Ruby or some other library to upload to S3 -- shell works perfectly fine 6 | # This is how I upload my new Sol Trader builds (http://soltrader.net) 7 | # Based on a modified script from here: http://tmont.com/blargh/2014/1/uploading-to-s3-in-bash 8 | 9 | function putS3 10 | { 11 | path=$1 12 | file=$2 13 | aws_path=$3 14 | bucket=$BUCKET 15 | date=$(date +"%a, %d %b %Y %T %z") 16 | acl="x-amz-acl:public-read" 17 | content_type='application/x-compressed-tar' 18 | string="PUT\n\n$content_type\n$date\n$acl\n/$bucket$aws_path$file" 19 | signature=$(echo -en "${string}" | openssl sha1 -hmac "${S3SECRET}" -binary | base64) 20 | 21 | curl -X PUT -T "$path/$file" \ 22 | -H "Host: $bucket.s3.amazonaws.com" \ 23 | -H "Date: $date" \ 24 | -H "Content-Type: $content_type" \ 25 | -H "$acl" \ 26 | -H "Authorization: AWS ${S3KEY}:$signature" \ 27 | "https://$bucket.s3.amazonaws.com$aws_path$file" 28 | } 29 | 30 | S3KEY=$AWS_JAMRECORD_ACCESS_KEY_ID 31 | S3SECRET=$AWS_JAMRECORD_SECRET_ACCESS_KEY # pass these in 32 | BUCKET=$AWS_JAMRECORD_BUCKET 33 | S3_PATH='/backup/' 34 | 35 | putS3 ./postgres-backup $filename $S3_PATH 36 | 37 | rm ./postgres-backup/$filename 38 | 39 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var config = require('./config/config.js'); 3 | var database = require('./db/database'); 4 | var http = require('http'); 5 | var app = express(); 6 | var path = require('path'); 7 | 8 | // var server = http.createServer(app); 9 | // var io = require('socket.io').listen(server); //pass a http.Server instance 10 | // server.listen(8080); //listen on port 80 11 | 12 | 13 | var normalizePort = function(val) { 14 | var port = parseInt(val, 10); 15 | if (isNaN(port)) { 16 | return val; 17 | } 18 | if (port >= 0) { 19 | return port; 20 | } 21 | return false; 22 | }; 23 | 24 | // Get port from environment and store in Express. 25 | config.port = normalizePort(process.env.PORT || config.port); 26 | config.port = process.env.NODE_ENV === 'test' ? 9000 : config.port; 27 | 28 | // configure our server with all the middleware and routing 29 | require('./config/middleware.js')(app, express); 30 | require('./config/routes.js')(app, express); 31 | 32 | // io.on('connection', function (socket) { 33 | // socket.emit('news', { hello: 'world' }); 34 | // // socket.on('my other event', function (data) { 35 | // // console.log(data); 36 | // // }); 37 | // }); 38 | 39 | // start listening to requests on port 8000 40 | app.listen(config.port, function() { console.log('listening on port: ', config.port); }); 41 | 42 | // export our app for testing and flexibility, required by index.js 43 | module.exports = app; -------------------------------------------------------------------------------- /compression_server/test/compressionTest.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var assert = chai.assert; 3 | var fs = require('fs'); 4 | var request = require('request'); 5 | var path = require('path'); 6 | 7 | // console.log('--- 1 --- Run Compression Server Tests!'); 8 | 9 | 10 | describe('hooks', function() { 11 | before(function() { 12 | // runs before all tests in this block 13 | // console.log('--- 1.1 --- Before: download test.wav'); 14 | 15 | var testWavUrl = 'https://s3-us-west-1.amazonaws.com/jamrecordtest/indextest/testSourceFiles/test1.wav'; 16 | var destinationPath = path.join(__dirname + '/testFiles/test1.wav'); 17 | // console.log(destinationPath); 18 | 19 | request.get(testWavUrl) 20 | .on('data', function(chunk) { 21 | // console.log(chunk); 22 | }) 23 | .pipe( fs.createWriteStream(destinationPath) ); 24 | // console.log('--- 1.2 --- Download completed'); 25 | 26 | }); 27 | 28 | after(function() { 29 | // runs after all tests in this block 30 | }); 31 | 32 | beforeEach(function() { 33 | // runs before each test in this block 34 | }); 35 | 36 | afterEach(function() { 37 | // runs after each test in this block 38 | }); 39 | 40 | it('it should work', function () { 41 | assert.equal(1, 1); 42 | }); 43 | 44 | // test cases 45 | }); 46 | 47 | 48 | // describe('Download test.wav', function() { 49 | 50 | 51 | 52 | // // describe('#indexOf()', function () { 53 | // // it('should return -1 when the value is not present', function () { 54 | // // assert.equal(1, 1); 55 | // // }); 56 | // // }); 57 | // }); 58 | 59 | // https://s3-us-west-1.amazonaws.com/jamrecordtest/indextest/testSourceFiles/test1.wav 60 | -------------------------------------------------------------------------------- /client/app/profile/profile.js: -------------------------------------------------------------------------------- 1 | angular.module('jam.profile', []) 2 | 3 | .controller('ProfileController', ['$scope', '$location', '$window', '$timeout', 'Users', 'Upload', 'UploadFactory', 'Songs', 4 | function ($scope, loc, win, to, Users, Up, UploadFactory, Songs) { 5 | $scope.avatarURL = ''; 6 | $scope.playable = Songs.getPlayable(); 7 | 8 | Users.getUserData() 9 | .then(function (userData) { 10 | $scope.user = userData; 11 | // $scope.avatarURL = '/api/users/' + $scope.user.id + '/avatar?rev=' + (++avatarRev); 12 | }) 13 | .catch(console.error); 14 | 15 | 16 | $scope.showAvatarModal = function (file) { 17 | $scope.file = file; 18 | $scope.avatarModalShown = true; 19 | }; 20 | 21 | $scope.hideAvatarModal = function () { 22 | $scope.avatarModalShown = false; 23 | }; 24 | 25 | 26 | var progressCallback = function(file, evt) { 27 | file.progressPercentage = Math.min(100, parseInt(100.0 * evt.loaded / evt.total)); 28 | }; 29 | 30 | var errorCallback = function (response) { 31 | $scope.errorMsg = response.status + ': ' + response.data; 32 | }; 33 | 34 | var successCallback = function (file, response) { 35 | file.result = response.data; 36 | $scope.user.avatarURL = file.s3url; 37 | $scope.hideAvatarModal(); 38 | $scope.updateProfile(); 39 | }; 40 | 41 | $scope.upload = function(dataUrl, name) { 42 | var file = Up.dataUrltoBlob(dataUrl, name); 43 | $scope.file = file; 44 | if (file) { 45 | UploadFactory.upload(file, 'images', successCallback, errorCallback, progressCallback); 46 | } 47 | }; 48 | 49 | $scope.updateProfile = function () { 50 | Users.updateProfile($scope.user) 51 | .then(function (res) { 52 | $scope.user = res.data.user; 53 | }) 54 | .catch(function (error) { 55 | console.error(error); 56 | }); 57 | }; 58 | }]); 59 | -------------------------------------------------------------------------------- /test/client/unit/servicesTest.js: -------------------------------------------------------------------------------- 1 | // 'use strict'; 2 | // var expect = require('chai').expect; 3 | // var dbModels = require('../../../client/services/services.js'); 4 | 5 | // describe('Services', function () { 6 | // beforeEach(module('jam.services')); 7 | 8 | // afterEach(inject(function ($httpBackend) { 9 | // $httpBackend.verifyNoOutstandingExpectation(); 10 | // $httpBackend.verifyNoOutstandingRequest(); 11 | // })); 12 | 13 | // describe('Songs Factory', function () { 14 | // var $httpBackend, Songs; 15 | 16 | // beforeEach(inject(function (_$httpBackend_, _Songs_) { 17 | // $httpBackend = _$httpBackend_; 18 | // Songs = _Songs_; 19 | // })); 20 | 21 | // it('should exist', function () { 22 | // expect(Songs).to.exist; 23 | // }); 24 | 25 | // it('should have a method `getAllSongs`', function () { 26 | // expect(Songs.getAllSongs).to.be.a('function'); 27 | // }); 28 | 29 | // it('should have a method `addSong`', function () { 30 | // expect(Songs.addSong).to.be.a('function'); 31 | // }); 32 | 33 | // it('should get all songs with `getAllSongs`', function () { 34 | // var mockResponse = [ 35 | // { title: 'Some Song', 36 | // url: 'https://song1.com' }, 37 | // { title: 'Another Song', 38 | // url: 'https://song2.com' } 39 | // ]; 40 | 41 | // $httpBackend.expect('GET', '/api/groups/:id/songs/').respond(mockResponse); 42 | 43 | // Songs.getAllSongs().then(function (songs) { 44 | // expect(songs).to.deep.equal(mockResponse); 45 | // }); 46 | 47 | // $httpBackend.flush(); 48 | // }); 49 | 50 | // it('should add a new song with `addSong`', function () { 51 | // // Send in ??? get back a url? 52 | // expect(23).to.be.a('number'); 53 | // }); 54 | 55 | // }); 56 | 57 | // }); 58 | 59 | 60 | -------------------------------------------------------------------------------- /server/config/helpers.js: -------------------------------------------------------------------------------- 1 | var jwt = require('jsonwebtoken'); 2 | var config = require('../config/config.js'); 3 | var db = require('../db/database'); 4 | var User = db.User; 5 | var JWT_SECRET = config.JWT_SECRET || 's00p3R53kritt'; 6 | 7 | var errorLogger = function (error, req, res, next) { 8 | // log the error then send it to the next middleware in 9 | console.error(error.stack); 10 | next(error); 11 | }; 12 | 13 | var errorHandler = function (error, req, res, next) { 14 | // send error message to client 15 | // message for gracefull error handling on app 16 | res.status(500).json({error: error.message}); 17 | }; 18 | 19 | var verifyToken = function (req, res, next) { 20 | 21 | // check header or url parameters or post parameters for token 22 | var token = req.body.token || req.query.token || req.headers['x-access-token']; 23 | var tokenUser; 24 | 25 | if (!token) { 26 | res.status(401).json('No authentication token'); 27 | } else { 28 | // decode token and attach user to the request 29 | // for use inside our controllers 30 | jwt.verify(token, JWT_SECRET, function(err, tokenUser) { 31 | if (err) { 32 | res.status(401).json('Bad authentication token'); 33 | } else { 34 | // check against database 35 | if (tokenUser.id && tokenUser.email) { 36 | User.findOne({ 37 | where: {id: tokenUser.id, email: tokenUser.email}, 38 | attributes: { exclude: ['password'] } 39 | }) 40 | .then(function(user) { 41 | req.user = user; 42 | next(); 43 | }); 44 | } else { 45 | res.status(404).json('no user associated with that authentication token'); 46 | } 47 | } 48 | }); 49 | } 50 | }; 51 | 52 | 53 | module.exports = { 54 | errorLogger: errorLogger, 55 | errorHandler: errorHandler, 56 | verifyToken: verifyToken 57 | }; 58 | -------------------------------------------------------------------------------- /sessionSetup.js: -------------------------------------------------------------------------------- 1 | // Each of these commands will run in a new tab! 2 | var commandsList = []; 3 | 4 | commandsList[0] = 'npm install; bower install; cd ./compression_server; npm install; cd ..'; 5 | commandsList[1] = 'pg_ctl -D /usr/local/var/postgres -l /usr/local/var/postgres/server.log start; psql; drop table users cascade; drop table groups cascade; drop table users; drop table groups; drop table "userGroups"; drop table playlists; drop table songs cascade; drop table "playlistSongs";'; 6 | commandsList[2] = 'gulp'; 7 | commandsList[3] = 'cd ./compression_server; npm start'; 8 | commandsList[4] = 'chrome https://github.com/BuoyantPyramid/buoyantpyramid http://localhost:5000'; 9 | 10 | 11 | var exec = require('child_process').exec; 12 | var dir = __dirname; 13 | 14 | for ( var i = 0; i < commandsList.length; i++ ) { 15 | 16 | var thisCommand = commandsList[i]; 17 | 18 | (function (thisCommand) { 19 | setTimeout(function() { 20 | // console.log(thisCommand); 21 | exec('runInTab ' + thisCommand); 22 | }, i * 1000); 23 | })(thisCommand); 24 | } 25 | 26 | // Save below script to /usr/local/bin as "runInTab" 27 | // ensure this is executeable 28 | // add commands to list below and run 'node sessionSetup.js' 29 | // Hope this works... 30 | ////////////////////////////////////////////////////////////////////////// 31 | 32 | // #!/usr/bin/env osascript 33 | 34 | // on run argv 35 | // set AppleScript's text item delimiters to {" "} 36 | // tell application "iTerm" 37 | // make new terminal 38 | // tell the current terminal 39 | // activate current session 40 | // launch session "Default Session" 41 | // tell the last session 42 | // write text argv as string 43 | // end tell 44 | // end tell 45 | // end tell 46 | // end run 47 | 48 | //////////////////////////////////////////////////////////////////////////// 49 | 50 | 51 | -------------------------------------------------------------------------------- /test/server/unit/schemaTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var Sequelize = require('sequelize'); 3 | var dbModels = require('../../../server/db/database.js'); 4 | var helpers = require('../testHelpers'); 5 | var Song = dbModels.Song; 6 | var User = dbModels.User; 7 | var Group = dbModels.Group; 8 | var Playlist = dbModels.Playlist; 9 | var UserGroups = dbModels.UserGroups; 10 | 11 | 12 | 13 | describe('User Model', function () { 14 | // rebuild test database 15 | before(function (done) { 16 | helpers.rebuildDb(done); 17 | }); 18 | 19 | 20 | it('User should be a Sequelize model', function () { 21 | expect(User).to.be.instanceOf(Sequelize.Model); 22 | }); 23 | 24 | it('should have a schema with fields: id, email, displayName, password', function (done) { 25 | User.describe().then(function(schema) { 26 | expect(schema.id).to.exist; 27 | expect(schema.email).to.exist; 28 | expect(schema.displayName).to.exist; 29 | expect(schema.password).to.exist; 30 | done(); 31 | }); 32 | }); 33 | }); 34 | 35 | describe('Group Model', function () { 36 | it('should be a Sequelize model', function () { 37 | expect(Group).to.be.instanceOf(Sequelize.Model); 38 | }); 39 | 40 | it('should have a schema with fields: name, bannerUrl', function (done) { 41 | Group.describe().then(function(schema) { 42 | expect(schema).to.include.keys('name', 'bannerUrl'); 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | describe('Song Model', function () { 49 | it('should be a Sequelize model', function () { 50 | expect(Song).to.be.instanceOf(Sequelize.Model); 51 | }); 52 | 53 | it('should have a schema with fields: title, description, dateRecorded, duration, groupId', function (done) { 54 | Song.describe().then(function(schema) { 55 | expect(schema).to.include.keys('title', 'description', 'dateRecorded', 'duration', 'groupId'); 56 | done(); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /compression_server/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('compression_server:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.COMPRESSIONPORT || '4000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /server/models/userModel.js: -------------------------------------------------------------------------------- 1 | var db = require('../db/database'); 2 | var Group = db.Group; 3 | var UserGroups = db.UserGroups; 4 | var Song = db.Song; 5 | var User = db.User; 6 | var Promise = require('bluebird'); 7 | 8 | var sanitizeUser = function (user) { 9 | var sanitizedUser = user.toJSON(); 10 | delete sanitizedUser.password; 11 | return sanitizedUser; 12 | }; 13 | 14 | var compileUserData = function(user) { 15 | return Group.findById(user.currentGroupId, {include: [{model: Song}]}) 16 | .then(function(currentGroup) { 17 | // Get rid of the password; 18 | user = sanitizeUser(user); 19 | user.currentGroup = currentGroup; 20 | return user; 21 | }); 22 | }; 23 | 24 | var createUser = function (email, displayName, password) { 25 | return new Promise(function (resolve, reject) { 26 | Group.create({ 27 | name: displayName, 28 | }) 29 | .then(function (group) { 30 | this.group = group; 31 | return User.create({ 32 | displayName: displayName, 33 | email: email, 34 | password: password, 35 | currentGroupId: group.id 36 | }); 37 | }) 38 | .then(function (user) { 39 | this.user = user; 40 | return this.group.addUser(user, {role: 'admin'}); 41 | }) 42 | .then(function () { 43 | resolve(this.user); 44 | }) 45 | .catch(function (error) { 46 | reject(error); 47 | }); 48 | }).bind({}); 49 | }; 50 | 51 | var getGroups = function(userId) { 52 | return User.findById(userId, { 53 | include: [{ 54 | model: Group, 55 | include: [{ 56 | model: User, 57 | attributes: { exclude: ['password'] } 58 | }] 59 | }] 60 | }) 61 | .then(function(user) { 62 | return user.groups; 63 | }); 64 | }; 65 | 66 | var getUser = function (query) { 67 | return User.findOne({where: query}); 68 | }; 69 | 70 | module.exports = { 71 | compileUserData: compileUserData, 72 | createUser: createUser, 73 | getGroups: getGroups, 74 | getUser: getUser, 75 | sanitizeUser: sanitizeUser 76 | }; 77 | -------------------------------------------------------------------------------- /client/app/player/player.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
{{song.title}}
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 | 16 | 19 | 22 |
23 |
24 |
25 | 26 |
27 | 30 |
31 |
32 | 33 |
{{audio.playbackRate}}x
34 | 35 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /client/app/song/song.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 |
7 | Download: 8 | Original 9 | 10 |
11 |
12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |

Comments

24 |
25 |
26 |
27 | avatar 28 |

{{ comment.note }} @ {{ formatTime(comment.time) }}

29 |
30 |
31 |
32 |
33 |
Add Comment
34 |
35 |
36 | 37 |
38 | 39 |
40 | 41 |
42 |
Click the pin to comment on a specific part of the song
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 53 |
-------------------------------------------------------------------------------- /test/server/testHelpers.js: -------------------------------------------------------------------------------- 1 | var dbModels = require('../../server/db/database.js'); 2 | var Song = dbModels.Song; 3 | var User = dbModels.User; 4 | var Group = dbModels.Group; 5 | var Playlist = dbModels.Playlist; 6 | var PlaylistSongs = dbModels.PlaylistSongs; 7 | var UserGroups = dbModels.UserGroups; 8 | 9 | 10 | dbModels.db.options.logging = false; 11 | 12 | // rebuild test database 13 | var rebuildDb = function (done) { 14 | User.sync() 15 | .then(function () { 16 | return Group.sync(); 17 | }) 18 | .then(function() { 19 | return UserGroups.sync(); 20 | }) 21 | .then(function() { 22 | return Playlist.sync(); 23 | }) 24 | .then(function() { 25 | return Song.sync(); 26 | }) 27 | .then(function() { 28 | return PlaylistSongs.sync(); 29 | }) 30 | .then(function () { 31 | return PlaylistSongs.destroy({where: {}}); 32 | }) 33 | .then(function () { 34 | return Playlist.destroy({where: {}}); 35 | }) 36 | .then(function () { 37 | return Song.destroy({where: {}}); 38 | }) 39 | .then(function () { 40 | return UserGroups.destroy({where: {}}); 41 | }) 42 | .then(function () { 43 | return User.destroy({where: {}}); 44 | }) 45 | .then(function() { 46 | return Group.destroy({where: {}}); 47 | }) 48 | .then(function() { 49 | done(); 50 | }); 51 | }; 52 | 53 | 54 | // Define api request bodies 55 | var songReq = { 56 | body: { 57 | name: 'Margaritaville', 58 | description: 'Wasted again', 59 | uniqueHash: 'asdfbbrkdjgf.wav', 60 | address: ' https://jamrecordtest.s3.amazonaws.com/audio/88408bec-a2b3-464a-9108-a3284da79f65.mp3', 61 | size: 9238148 62 | }, 63 | params: { 64 | id: 1 65 | }, 66 | }; 67 | 68 | var groupReq = { 69 | body: { 70 | name: 'Safety Talk' 71 | } 72 | }; 73 | 74 | var addSongReq = { 75 | params: { 76 | sid: 1, 77 | pid: 1 78 | } 79 | }; 80 | 81 | var playlistReq = { 82 | body: { 83 | title: 'Chill Vibes', 84 | description: 'Indie Electronic', 85 | groupId: 1 86 | } 87 | }; 88 | 89 | 90 | 91 | 92 | module.exports = { 93 | rebuildDb: rebuildDb, 94 | songReq: songReq, 95 | groupReq: groupReq, 96 | addSongReq: addSongReq, 97 | playlistReq: playlistReq 98 | }; -------------------------------------------------------------------------------- /server/controllers/playlistController.js: -------------------------------------------------------------------------------- 1 | var Playlist = require('../models/playlistModel'); 2 | 3 | var addSong = function (req, res, next) { 4 | var songId = req.params.sid; 5 | var playlistId = req.params.pid; 6 | 7 | Playlist.addSong(songId, playlistId) 8 | .then(function (song) { 9 | res.json(song); 10 | }) 11 | .catch(function (error) { 12 | next(error); 13 | }); 14 | }; 15 | 16 | var createPlaylist = function (req, res, next) { 17 | var groupId = req.body.groupId; 18 | var title = req.body.title; 19 | var description = req.body.description; 20 | 21 | Playlist.createPlaylist(groupId, title, description) 22 | .then(function (playlist) { 23 | res.json(playlist); 24 | }) 25 | .catch(function (error) { 26 | next(error); 27 | }); 28 | }; 29 | 30 | var deletePlaylist = function(req, res, next) { 31 | var playlistId = req.params.id; 32 | 33 | Playlist.deletePlaylist(playlistId) 34 | .then(function(playlist) { 35 | playlist.destroy(); 36 | res.json(playlist); 37 | }) 38 | .catch(function (error) { 39 | next(error); 40 | }); 41 | }; 42 | 43 | var getSongs = function (req, res, next) { 44 | var playlistId = req.params.id; 45 | 46 | Playlist.getSongs(playlistId) 47 | .then(function (songs) { 48 | res.json(songs); 49 | }) 50 | .catch(function (error) { 51 | next(error); 52 | }); 53 | }; 54 | 55 | var updatePositions = function(req, res, next) { 56 | var playlistId = req.params.id; 57 | var positions = req.body; 58 | 59 | Playlist.updatePositions(playlistId, positions) 60 | .then(function(resp) { 61 | res.json(resp); 62 | }) 63 | .catch(function (error) { 64 | next(error); 65 | }); 66 | }; 67 | 68 | var removeSong = function(req, res, next) { 69 | var playlistId = req.params.pid; 70 | var songId = req.params.sid; 71 | 72 | Playlist.removeSong(songId, playlistId) 73 | .then(function(resp) { 74 | res.json(resp); 75 | }) 76 | .catch(function (error) { 77 | next(error); 78 | }); 79 | }; 80 | 81 | module.exports = { 82 | addSong: addSong, 83 | createPlaylist: createPlaylist, 84 | deletePlaylist: deletePlaylist, 85 | getSongs: getSongs, 86 | updatePositions: updatePositions, 87 | removeSong: removeSong 88 | }; 89 | -------------------------------------------------------------------------------- /pomander.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # /pōˈmandər,ˈpōˌmandər/ (noun) - 4 | # a ball or perforated container of sweet-smelling substances such as herbs and spices, 5 | # placed in a closet, drawer, room, or codebase to perfume the air or (formerly) 6 | # carried as a supposed protection against infection. 7 | 8 | # This is a self-installing git hook. Install me by executing: 9 | # `bash pomander.sh` 10 | # in the root directory of a git repo. 11 | 12 | # Pretty colors 13 | # Use `echo -e` for these to work 14 | ESC_SEQ="\033[" 15 | COLOR_RESET="${ESC_SEQ}0m" 16 | COLOR_RED="${ESC_SEQ}1;31m" 17 | COLOR_GREEN="${ESC_SEQ}1;32m" 18 | COLOR_YELLOW="${ESC_SEQ}1;33m" 19 | COLOR_MAGENTA="${ESC_SEQ}1;35m" 20 | 21 | # Install mode 22 | NAME=`basename $0` 23 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 24 | 25 | if [ "$NAME" != "pre-commit" ]; then 26 | cd $DIR/.git/hooks 27 | rm -f pre-commit 28 | ln -s ../../$NAME pre-commit 29 | chmod +x pre-commit 30 | echo "Installed pre-commit hook." 31 | exit 0 32 | fi 33 | 34 | # Hook mode 35 | STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep ".jsx\{0,1\}$") 36 | PASSED=true 37 | 38 | if [[ "$STAGED_FILES" = "" ]]; then 39 | echo -e "${COLOR_YELLOW}No JavaScript files staged, skipping Pomander${COLOR_RESET}" 40 | exit 0 41 | fi 42 | 43 | # Check for ESLint 44 | which eslint &> /dev/null 45 | if [[ "$?" == 1 ]]; then 46 | echo -e "${COLOR_RED}Pomander failed:${COLOR_RESET} ESLint not found! Please install ESLint" 47 | echo -e "Make sure you have Node installed then run:" 48 | echo -e " ${COLOR_YELLOW}npm install -g eslint${COLOR_RESET}" 49 | exit 1 50 | fi 51 | 52 | # Lint files 53 | echo -e "${COLOR_MAGENTA}Sniffing your code, hang tight...${COLOR_RESET}" 54 | 55 | for FILE in $STAGED_FILES 56 | do 57 | eslint "$FILE" 58 | 59 | if [[ "$?" == 0 ]]; then 60 | echo -e "👍 Passed: $FILE" 61 | else 62 | echo -e "❌ Failed: $FILE" 63 | PASSED=false 64 | fi 65 | done 66 | 67 | # Report results 68 | if ! $PASSED; then 69 | echo -e "\n${COLOR_RED}Pomander failed:${COLOR_RESET} Your commit contains files that do not contain proper syntax or violate the Hack Reactor style guide. Please fix the errors and try again. Don't forget to 'git add' the fixed files!" 70 | exit 1 71 | else 72 | echo -e "${COLOR_GREEN}All files passed!${COLOR_RESET}" 73 | fi 74 | 75 | exit $? 76 | -------------------------------------------------------------------------------- /test/server/unit/songTest.js: -------------------------------------------------------------------------------- 1 | var sinon = require('sinon'); 2 | var chai = require('chai'); 3 | var expect = chai.expect; 4 | var Promise = require('bluebird'); 5 | var Sequelize = require('sequelize'); 6 | var dbModels = require('../../../server/db/database.js'); 7 | var helpers = require('../testHelpers'); 8 | var Song = dbModels.Song; 9 | var Group = dbModels.Group; 10 | var GroupController = require('../../../server/controllers/groupController.js'); 11 | var SongModel = require('../../../server/models/songModel.js'); 12 | var SongController = require('../../../server/controllers/songController.js'); 13 | 14 | 15 | 16 | var songReq = helpers.songReq; 17 | var groupReq = helpers.groupReq; 18 | 19 | var compressStub; 20 | 21 | describe('Song Controller', function () { 22 | 23 | 24 | before(function(done) { 25 | compressStub = sinon.stub(SongModel, 'requestFileCompression', function() { 26 | return new Promise(function(resolve, reject) { 27 | resolve(true); 28 | }); 29 | }); 30 | done(); 31 | }); 32 | 33 | after(function (done) { 34 | compressStub.restore(); 35 | done(); 36 | }); 37 | 38 | 39 | // Connect to database before any tests 40 | beforeEach(function (done) { 41 | helpers.rebuildDb(function() { 42 | Group.create({name: 'Safety Talk'}) 43 | .then(function(group) { 44 | songReq.params.id = group.id; 45 | done(); 46 | }); 47 | }); 48 | }); 49 | 50 | describe ('add song', function() { 51 | 52 | it('should call res.json to return a json object', function (done) { 53 | var res = {}; 54 | res.json = function(jsonresponse) { 55 | expect(jsonresponse).to.have.property('title'); 56 | done(); 57 | }; 58 | 59 | res.send = function(err) { 60 | console.error(err); 61 | }; 62 | SongController.addSong(songReq, res, console.error); 63 | }); 64 | 65 | 66 | it('should create a new song in the database', function (done) { 67 | var res = {}; 68 | res.send = function(err) { 69 | console.error(err); 70 | }; 71 | res.json = function(jsonresponse) { 72 | dbModels.db.query('SELECT * FROM songs WHERE title = :title ', { replacements: {title: songReq.body.name}, type: Sequelize.QueryTypes.SELECT}) 73 | .then( function(songs) { 74 | expect(songs[0].title).to.equal(songReq.body.name); 75 | done(); 76 | }); 77 | }; 78 | SongController.addSong(songReq, res, function() {}); 79 | }); 80 | }); 81 | }); -------------------------------------------------------------------------------- /server/models/playlistModel.js: -------------------------------------------------------------------------------- 1 | var db = require('../db/database'); 2 | var Group = db.Group; 3 | var Playlist = db.Playlist; 4 | var Song = db.Song; 5 | var PlaylistSongs = db.PlaylistSongs; 6 | var Promise = require('bluebird'); 7 | 8 | var addSong = function(songId, playlistId) { 9 | return Song.findById(songId) 10 | .then(function(song) { 11 | return PlaylistSongs.count({ where: { playlistId: playlistId } }); 12 | }) 13 | .then(function(count) { 14 | return PlaylistSongs.create({ 15 | songId: songId, 16 | playlistId: playlistId, 17 | listPosition: count 18 | }); 19 | }); 20 | }; 21 | 22 | var createPlaylist = function(groupId, title, description) { 23 | return Playlist.create({ 24 | title: title, 25 | description: description, 26 | groupId: groupId 27 | }, { 28 | include: { 29 | model: Group 30 | } 31 | }); 32 | }; 33 | 34 | var deletePlaylist = function(playlistId) { 35 | return Playlist.findOne({ where: { id: playlistId } }); 36 | }; 37 | 38 | var getSongs = function(playlistId) { 39 | return new Promise(function(resolve, reject) { 40 | Playlist.findById(playlistId) 41 | .then(function(playlist) { 42 | if (playlist) { 43 | resolve(playlist.getSongs()); 44 | } else { 45 | resolve([]); 46 | } 47 | }) 48 | .catch(function(err) { 49 | reject(err); 50 | }); 51 | }); 52 | }; 53 | 54 | var updatePositions = function(playlistId, positions) { 55 | return new Promise(function(resolve, reject) { 56 | PlaylistSongs.findAll({ where: { playlistId: playlistId } }) 57 | .then(function(playlistSongs) { 58 | for (var i = 0; i < playlistSongs.length; i++) { 59 | PlaylistSongs.update( 60 | { 61 | listPosition: positions[i].listPosition 62 | }, 63 | { where: { songId: positions[i].songId } 64 | } 65 | ).then(function(response) { 66 | resolve(response); 67 | }) 68 | .catch(function(error) { 69 | reject(error); 70 | }); 71 | } 72 | }); 73 | }); 74 | }; 75 | 76 | var removeSong = function(songId, playlistId) { 77 | return PlaylistSongs.destroy({ where: { songId: songId, playlistId: playlistId } }); 78 | }; 79 | 80 | module.exports = { 81 | addSong: addSong, 82 | createPlaylist: createPlaylist, 83 | deletePlaylist: deletePlaylist, 84 | getSongs: getSongs, 85 | updatePositions: updatePositions, 86 | removeSong: removeSong 87 | }; 88 | -------------------------------------------------------------------------------- /client/app/nav/nav.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Audiopile
4 |
5 | 76 |
77 | 78 | -------------------------------------------------------------------------------- /client/app/auth/auth.js: -------------------------------------------------------------------------------- 1 | angular.module('jam.auth', []) 2 | 3 | .controller('AuthController', ['$scope', '$window', '$location', '$routeParams', 'Users', 4 | function ($scope, $window, $location, $routeParams, Users) { 5 | $scope.confirm = false; 6 | $scope.passMismatch = false; 7 | $scope.loginError = ''; 8 | $scope.signupError = ''; 9 | $scope.user = {}; 10 | 11 | $scope.toggleLogin = function () { 12 | $scope.user.email = $routeParams.email || ''; 13 | $scope.user.password = ''; 14 | $scope.loginForm.$setPristine(); 15 | $scope.showLogin = !$scope.showLogin; 16 | if ($scope.showSignup && $scope.showLogin) { 17 | $scope.showSignup = false; 18 | } 19 | }; 20 | 21 | $scope.toggleSignup = function () { 22 | $scope.user.email = $scope.user.password = ''; 23 | $scope.signupForm.$setPristine(); 24 | $scope.showSignup = !$scope.showSignup; 25 | if ($scope.showSignup && $scope.showLogin) { 26 | $scope.showLogin = false; 27 | } 28 | }; 29 | 30 | $scope.login = function () { 31 | $scope.loginError = ''; 32 | Users.login($scope.user) 33 | .then(function (data) { 34 | $location.path('/songs'); 35 | }) 36 | .catch(function (error) { 37 | console.error(error.data); 38 | $scope.loginError = error.data; 39 | }); 40 | }; 41 | 42 | $scope.signup = function (pass) { 43 | $scope.signupError = ''; 44 | if (pass === $scope.user.password) { 45 | $scope.passMismatch = false; 46 | $scope.user.displayName = 'anonymous'; 47 | Users.signup($scope.user) 48 | .then(function (data) { 49 | $location.path('/profile'); 50 | }) 51 | .catch(function (error) { 52 | console.error(error.data); 53 | $scope.signupError = error.data; 54 | }); 55 | } else { 56 | $scope.newPassword = ''; 57 | $scope.passMismatch = true; 58 | $scope.user.password = ''; 59 | } 60 | }; 61 | 62 | $scope.logout = function () { 63 | Users.logout(); 64 | $scope.user = null; 65 | }; 66 | 67 | $scope.images = [ 68 | 'evergreen1.jpg', 69 | 'evergreen2.jpg', 70 | 'evergreen3.jpg', 71 | 'evergreen4.jpg', 72 | 'safety1.JPG', 73 | 'safety2.JPG', 74 | 'safety3.JPG', 75 | 'safety4.JPG', 76 | 'safety5.JPG' 77 | ]; 78 | 79 | $scope.backImage = { 80 | 'background-image': 'url(assets/bands/' + $scope.images[Math.floor(Math.random() * $scope.images.length)] + ')' 81 | }; 82 | 83 | $scope.$on('$viewContentLoaded', function() { 84 | if ($routeParams.email) { 85 | $scope.toggleLogin(); 86 | } 87 | }); 88 | }]); 89 | -------------------------------------------------------------------------------- /client/app/profile/profile.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 |
6 |
7 | 8 |
{{file.name}} {{errFile.name}} {{errFile.$error}} {{errFile.$errorParam}}
9 |
10 |
11 | 12 |
13 |

Profile - update your information

14 |
15 |
16 | 17 |
18 |
19 | avatar 20 |
current avatar
21 | 23 | 24 | 25 | 28 | {{errorMsg}} 29 | 30 |
31 |
32 |
33 |
34 | info 35 | 38 | 41 | 42 |
43 |
44 |
45 |
46 |
47 | 50 |
51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "clean": "rimraf dist/*", 8 | "open:dev": "opener http://localhost:3000", 9 | "test": "NODE_ENV=test JAMRUN=test mocha --recursive test", 10 | "test_webserver": "NODE_ENV=test JAMRUN=test mocha --recursive test/server/", 11 | "testserver2": " mocha test/compression_server/compressionTest.js", 12 | "setup": "node sessionSetup.js", 13 | "lint": "./node_modules/eslint/bin/eslint.js .", 14 | "start": "gulp", 15 | "build": "gulp build", 16 | "startForever": "forever start --spinSleepTime 10000 server/server.js", 17 | "deploy": "npm run build && npm run startForever", 18 | "restartForever": "forever restart server/server.js && forever restart compression_server/bin/www", 19 | "killnode": "killall -9 node", 20 | "startPSQL": "pg_ctl -D /usr/local/var/postgres -l /usr/local/var/postgres/server.log start", 21 | "stopPSQL": "pg_ctl -D /usr/local/var/postgres stop -s -m fast" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/BuoyantPyramid/buoyantpyramid.git" 26 | }, 27 | "author": "buoyantPyramid", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/BuoyantPyramid/buoyantpyramid/issues" 31 | }, 32 | "homepage": "https://github.com/BuoyantPyramid/buoyantpyramid#readme", 33 | "dependencies": { 34 | "aws-sdk": "2.2.45", 35 | "bcrypt-nodejs": "0.0.3", 36 | "bluebird": "3.3.4", 37 | "body-parser": "1.15.0", 38 | "busboy": "0.2.12", 39 | "crypto": "0.0.3", 40 | "crypto-js": "3.1.6", 41 | "express": "4.13.4", 42 | "gulp-size": "2.1.0", 43 | "jsonwebtoken": "5.7.0", 44 | "mailgun-js": "0.7.7", 45 | "method-override": "2.3.5", 46 | "morgan": "1.7.0", 47 | "opener": "1.4.1", 48 | "pg": "4.5.1", 49 | "q": "1.4.1", 50 | "request": "2.69.0", 51 | "rimraf": "2.5.2", 52 | "sequelize": "3.19.3", 53 | "sinon": "1.17.3", 54 | "socket.io": "1.4.5", 55 | "supertest": "1.2.0" 56 | }, 57 | "devDependencies": { 58 | "browser-sync": "2.11.1", 59 | "chai": "3.5.0", 60 | "eslint": "2.4.0", 61 | "eslint-config-hackreactor": "git://github.com/hackreactor-labs/eslint-config-hackreactor", 62 | "eslint-plugin-react": "4.2.1", 63 | "gulp": "3.9.1", 64 | "gulp-autoprefixer": "3.1.0", 65 | "gulp-closure-compiler": "0.4.0", 66 | "gulp-concat": "2.6.0", 67 | "gulp-ngmin": "0.3.0", 68 | "gulp-nodemon": "2.0.6", 69 | "gulp-rename": "1.2.2", 70 | "gulp-sass": "2.2.0", 71 | "gulp-uglify": "1.5.3", 72 | "mocha": "2.4.5", 73 | "sinon": "1.17.3" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /client/app/groups/settings.js: -------------------------------------------------------------------------------- 1 | angular.module('jam.groupSettings', []) 2 | 3 | .controller('SettingsController', ['$scope', '$timeout', 'Upload', 'Users', 'Groups', 'UploadFactory', 'Songs', function ($scope, $timeout, Up, Users, Groups, UploadFactory, Songs) { 4 | $scope.user = {}; 5 | $scope.group = {}; 6 | $scope.sendingInvite = false; 7 | $scope.playable = Songs.getPlayable(); 8 | $scope.inviteError = ''; 9 | 10 | Users.getUserData() 11 | .then(function (user) { 12 | $scope.user = user; 13 | $scope.group = user.currentGroup; 14 | }) 15 | .catch(console.error); 16 | 17 | $scope.showBannerModal = function (file) { 18 | $scope.file = file; 19 | $scope.bannerModalShown = true; 20 | }; 21 | 22 | $scope.hideBannerModal = function () { 23 | $scope.bannerModalShown = false; 24 | }; 25 | 26 | $scope.sendInvite = function () { 27 | $scope.inviteError = ''; 28 | $scope.sendingInvite = true; 29 | Groups.sendInvite($scope.group, $scope.invite) 30 | .then(function (res) { 31 | $scope.invite = ""; 32 | $scope.inviteForm.$setPristine(); 33 | Groups.getGroupsData($scope.user, true) 34 | .then(function () { 35 | $scope.sendingInvite = false; 36 | }) 37 | .catch(console.error); 38 | }) 39 | .catch(function (error) { 40 | $scope.inviteError = error.data; 41 | $scope.sendingInvite = false; 42 | console.error(error); 43 | }); 44 | }; 45 | 46 | $scope.updateGroupProfile = function (triggerButton) { 47 | if (triggerButton) { 48 | $scope.updatingName = true; 49 | $timeout(function() { 50 | $scope.updatingName = false; 51 | }, 300); 52 | } 53 | Groups.updateInfo($scope.group) 54 | .then(function (updatedGroup) { 55 | _.extend($scope.user.currentGroup, updatedGroup); 56 | }) 57 | .catch(console.error); 58 | }; 59 | 60 | $scope.removeBanner = function () { 61 | Groups.updateInfo({ 62 | id: $scope.group.id, 63 | bannerUrl: '' 64 | }) 65 | .then(function (res) { 66 | _.extend($scope.user.currentGroup, res.data); 67 | }) 68 | .catch(console.error); 69 | }; 70 | 71 | var progressCallback = function (file, evt) { 72 | file.progress = Math.min(100, parseInt(100.0 * evt.loaded / evt.total)); 73 | }; 74 | 75 | var errorCallback = function (response) { 76 | $scope.errorMsg = response.status + ': ' + response.data; 77 | }; 78 | 79 | var successCallback = function (file, response) { 80 | $scope.result = response.data; 81 | $scope.group.bannerUrl = file.s3url; 82 | $scope.hideBannerModal(); 83 | $scope.updateGroupProfile(); 84 | }; 85 | 86 | $scope.upload = function (dataUrl, name) { 87 | var file = Up.dataUrltoBlob(dataUrl, name); 88 | $scope.file = file; 89 | if (file) { 90 | UploadFactory.upload(file, 'images', successCallback, errorCallback, progressCallback); 91 | } 92 | }; 93 | 94 | }]); 95 | -------------------------------------------------------------------------------- /client/app/songs/songs.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 |
6 |
7 | {{commentSong.title}} 8 |

at time: {{timeFormat}}

9 | 10 | 11 |
12 |
13 |
14 | 15 | 16 | 17 |
18 |
19 | Choose a playlist: 20 |
    21 |
  • {{playlist.title}}
  • 22 |
23 |
24 |
25 | 26 |
27 | 28 | 29 |
30 |
31 | Delete this song? 32 |

{{pendingSong.title}}

33 | 34 |
35 |
36 |
37 | 38 | 44 |
{{message}}
45 |

Songs

46 | 49 | 50 |
51 | your group doesn't have any songs yet. you should upload some 52 |
53 |
54 |
55 |
56 | 59 | 60 | 61 |
62 | 63 |
64 |
65 |
66 | 69 |
-------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var sync = require('browser-sync'); 5 | var reload = sync.reload; 6 | var nodemon = require('gulp-nodemon'); 7 | // var closureCompiler = require('gulp-closure-compiler'); 8 | var uglify = require('gulp-uglify'); 9 | var sass = require('gulp-sass'); 10 | var concat = require('gulp-concat'); 11 | var rename = require('gulp-rename'); 12 | var ngmin = require('gulp-ngmin'); 13 | var prefix = require('gulp-autoprefixer'); 14 | var size = require('gulp-size'); 15 | 16 | // the paths to our app files 17 | var paths = { 18 | scripts: ['client/app/**/*.js'], 19 | libsrc: [ 'client/lib/angular/angular.min.js', 20 | 'client/lib/angular-route/angular-route.min.js', 21 | 'client/lib/angular-animate/angular-animate.min.js', 22 | 'client/lib/ng-file-upload/ng-file-upload.min.js', 23 | 'client/lib/ng-img-crop-full-extended/compile/minified/ng-img-crop.js', 24 | 'client/lib/ngprogress/build/ngprogress.min.js', 25 | 'client/lib/underscore/underscore-min.js', 26 | 'client/lib/d3/d3.min.js', 27 | 'client/lib/angular-drag-and-drop-lists/angular-drag-and-drop-lists.min.js', 28 | 'client/lib/ng-focus-if/focusIf.min.js' 29 | ], 30 | html: ['client/app/**/*.html', 'client/index.html'], 31 | styles: ['client/app/styles/*.scss'], 32 | test: ['tests/**/*.js'] 33 | }; 34 | 35 | // Sass and css injecting 36 | gulp.task('sass', function () { 37 | return gulp.src(paths.styles) 38 | .pipe(sass({outputStyle: 'compressed', sourceComments: 'map'}, {errLogToConsole: true})) 39 | .pipe(prefix('last 2 versions', '> 1%', 'ie 8', 'Android 2', 'Firefox ESR')) 40 | .pipe(gulp.dest('client/dist')); 41 | }); 42 | 43 | gulp.task('browser-sync', ['nodemon'], function() { 44 | sync.init(null, { 45 | proxy: 'http://localhost:3000', 46 | port: 5000 47 | }); 48 | }); 49 | 50 | // Minify the things 51 | gulp.task('buildlib', function() { 52 | return gulp.src(paths.libsrc) 53 | .pipe(size({showFiles: true})) 54 | // .pipe(ngmin()) 55 | .pipe(concat('lib.min.js')) // Combine into 1 file 56 | .pipe(gulp.dest('client/dist')); // Write non-minified to disk 57 | }); 58 | 59 | gulp.task('build', ['sass', 'buildlib'], function() { 60 | return gulp.src(paths.scripts) 61 | .pipe(ngmin()) 62 | .pipe(concat('main.js')) // Combine into 1 file 63 | .pipe(gulp.dest('client/dist')) // Write non-minified to disk 64 | .pipe(uglify()) // Minify 65 | .pipe(rename({extname: '.min.js'})) // Rename to ng-quick-date.min.js 66 | .pipe(gulp.dest('client/dist')); // Write minified to disk 67 | }); 68 | 69 | gulp.task('nodemon', function (cb) { 70 | var called = false; 71 | return nodemon({script: 'server/server.js'}).on('start', function () { 72 | if (!called) { 73 | called = true; 74 | cb(); 75 | } 76 | }); 77 | }); 78 | 79 | gulp.task('default', ['build', 'browser-sync'], function () { 80 | gulp.watch(paths.styles, ['sass']); 81 | gulp.watch([paths.scripts, paths.html], ['build', reload]); 82 | }); 83 | -------------------------------------------------------------------------------- /client/app/services/usersFactory.js: -------------------------------------------------------------------------------- 1 | angular.module('jam.usersFactory', []) 2 | 3 | .factory('Users', ['$http', '$location', '$window', '$q', 'Groups', function (http, loc, win, q, Groups) { 4 | // stores users token as com.jam 5 | var userData = null; 6 | 7 | var login = function (user) { 8 | return http({ 9 | method: 'POST', 10 | url: '/api/users/login', 11 | data: user 12 | }) 13 | .then(function (resp) { 14 | userData = resp.data.user; 15 | win.localStorage.setItem('com.jam', resp.data.token); 16 | return resp.data; 17 | }); 18 | }; 19 | 20 | var signup = function (user) { 21 | return http({ 22 | method: 'POST', 23 | url: '/api/users/signup', 24 | data: user 25 | }) 26 | .then(function (resp) { 27 | userData = resp.data.user; 28 | win.localStorage.setItem('com.jam', resp.data.token); 29 | return resp.data; 30 | }); 31 | }; 32 | 33 | var getGroups = function (userId) { 34 | return http({ 35 | method: 'GET', 36 | url: '/api/users/' + userId + '/groups/' 37 | }) 38 | .then(function (res) { 39 | return res.data; 40 | }); 41 | }; 42 | 43 | var getUser = function(userId) { 44 | return http({ 45 | method: 'GET', 46 | url: '/api/users/' + userId 47 | }) 48 | .then(function(resp) { 49 | return resp.data.user; 50 | }); 51 | }; 52 | 53 | var logout = function () { 54 | win.localStorage.removeItem('com.jam'); 55 | userData = null; 56 | Groups.setGroupsData(null); 57 | loc.path('/login'); 58 | }; 59 | 60 | var updateProfile = function(profile) { 61 | return http({ 62 | method: 'PUT', 63 | url: '/api/users/profile', 64 | data: profile 65 | }) 66 | .then(function(res) { 67 | _.extend(userData, res.data.user); 68 | win.localStorage.setItem('com.jam', res.data.token); 69 | return res; 70 | }) 71 | .catch(console.error); 72 | }; 73 | 74 | var getProfile = function(profile) { 75 | return http({ 76 | method: 'GET', 77 | url: '/api/users/profile' 78 | }); 79 | }; 80 | 81 | var getUserData = function( force ) { 82 | force = force || false; 83 | return q(function(resolve, reject) { 84 | if (userData && !force) { 85 | resolve(userData); 86 | } 87 | getProfile() 88 | .then(function(res) { 89 | if (userData) { 90 | _.extend(userData, res.data.user); 91 | } else { 92 | userData = res.data.user; 93 | } 94 | resolve(userData); 95 | }) 96 | .catch(function (error) { 97 | reject(error); 98 | }); 99 | }); 100 | }; 101 | 102 | var isAuth = function () { 103 | return !!win.localStorage.getItem('com.jam'); 104 | }; 105 | 106 | 107 | return { 108 | updateProfile: updateProfile, 109 | getGroups: getGroups, 110 | getProfile: getProfile, 111 | login: login, 112 | signup: signup, 113 | getUser: getUser, 114 | isAuth: isAuth, 115 | logout: logout, 116 | getUserData: getUserData 117 | }; 118 | }]); 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Stories in Backlog](https://badge.waffle.io/BuoyantPyramid/buoyantpyramid.svg?label=backlogy&title=Backlog)](http://waffle.io/BuoyantPyramid/buoyantpyramid) 2 | [![Stories in Ready](https://badge.waffle.io/BuoyantPyramid/buoyantpyramid.svg?label=ready&title=Ready)](http://waffle.io/BuoyantPyramid/buoyantpyramid) 3 | [![Stories in In Progress](https://badge.waffle.io/BuoyantPyramid/buoyantpyramid.svg?label=In%20Progress&title=In%20Progress)](http://waffle.io/BuoyantPyramid/buoyantpyramid) 4 | [![Stories in Done](https://badge.waffle.io/BuoyantPyramid/buoyantpyramid.svg?label=done&title=Done)](http://waffle.io/BuoyantPyramid/buoyantpyramid) 5 | [![Build Status](https://travis-ci.org/BuoyantPyramid/buoyantpyramid.svg?branch=master)](https://travis-ci.org/BuoyantPyramid/buoyantpyramid) 6 | 7 | # [Audiopile](http://audiopile.rocks) 8 | 9 | Audio organization platform for team collaboration among recording artists 10 | 11 | ## Brief Demo 12 | Click the image below to access a demo video 13 | [![Demo](http://i.imgur.com/GhqG14C.png)](https://www.youtube.com/watch?v=eFnGx2wbRRE "Demo - Playing Songs") 14 | 15 | ## Team 16 | - __Product Owner__: Brian Fogg 17 | - __Scrum Master__: Nick Echols 18 | - __Development Team Members__: Sondra Silverhawk, Erick Paepke 19 | 20 | ## Table of Contents 21 | 22 | 1. [Usage](#Usage) 23 | 1. [Requirements](#requirements) 24 | 1. [Development](#development) 25 | 1. [Installing Dependencies](#installing-dependencies) 26 | 1. [Tasks](#tasks) 27 | 1. [Team](#team) 28 | 1. [Contributing](#contributing) 29 | 30 | 31 | ## Requirements 32 | 33 | - Node 34 | - Nodemon 35 | - Eslint 36 | 37 | ## Development 38 | 39 | ### Installing Dependencies 40 | 41 | From within the main directory of the repo: 42 | 43 | ```sh 44 | npm install -g nodemon 45 | npm install -g eslint 46 | npm install -g eslint-plugin-react 47 | npm install -g webpack-cli 48 | npm install 49 | sh ./pomander.sh 50 | ``` 51 | 52 | 53 | 54 | ## Usage 55 | ### Server configuration 56 | 57 | 58 | 1. Update config files on primary server 59 | (do this manually) 60 | 61 | from project root 62 | 3. Install dependencies 63 | 64 | npm install; 65 | bower install; 66 | cd ./compression_server; 67 | npm install; 68 | cd ..; 69 | 70 | 71 | 5. Start postgres database 72 | 73 | Start: 74 | pg_ctl -D /usr/local/var/postgres -l /usr/local/var/postgres/server.log start 75 | 76 | Stop: 77 | pg_ctl -D /usr/local/var/postgres stop -s -m fast 78 | 79 | 6. Clear all databases 80 | 81 | drop table users cascade; drop table groups cascade; drop table users; drop table groups; drop table "userGroups"; drop table playlists; drop table songs cascade; drop table "playlistSongs"; 82 | 83 | 7. Start primary server 84 | gulp 85 | 86 | ### Roadmap 87 | 88 | View the project roadmap [here](https://github.com/BuoyantPyramid/buoyantpyramid/issues) 89 | 90 | 91 | ## Contributing 92 | 93 | See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines. 94 | 95 | ## Dropping all tables 96 | drop table songs cascade; 97 | drop table groups cascade; 98 | drop table playlists cascade; 99 | drop table userGroups cascade; 100 | drop table users cascade; 101 | -------------------------------------------------------------------------------- /client/app/upload/upload.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 6 | 7 |
8 | click the button above to add files for uploading 9 |
10 | 36 | 37 | 42 | 43 | 44 | 45 |
46 | 49 |
50 | -------------------------------------------------------------------------------- /server/controllers/upload.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Busboy = require('busboy'); 3 | var path = require('path'); 4 | var os = require('os'); 5 | var fs = require('fs'); 6 | var AWS = require('aws-sdk'); 7 | var crypto = require('crypto'); 8 | var config = require('../config/aws.config.js'); 9 | 10 | var getExpiryTime = function () { 11 | var _date = new Date(); 12 | return '' + (_date.getFullYear()) + '-' + (_date.getMonth() + 1) + '-' + 13 | (_date.getDate() + 1) + 'T' + (_date.getHours() + 3) + ':' + '00:00.000Z'; 14 | }; 15 | 16 | var createS3Policy = function (contentType, callback) { 17 | var date = new Date(); 18 | var s3Policy = { 19 | 'expiration': getExpiryTime(), 20 | 'conditions': [ 21 | ['starts-with', '$key', ''], 22 | {'bucket': config.bucket}, 23 | {'acl': 'public-read'}, 24 | ['starts-with', '$Content-Type', contentType], 25 | {'success_action_status': '201'} 26 | ] 27 | }; 28 | 29 | // stringify and encode the policy 30 | var stringPolicy = JSON.stringify(s3Policy); 31 | var base64Policy = new Buffer(stringPolicy, 'utf-8').toString('base64'); 32 | 33 | // sign the base64 encoded policy 34 | var signature = crypto 35 | .createHmac('sha1', config.secretAccessKey) 36 | .update(new Buffer(base64Policy, 'utf-8')) 37 | .digest('base64'); 38 | 39 | // build the results object 40 | var s3Credentials = { 41 | s3Policy: base64Policy, 42 | s3Signature: signature, 43 | AWSAccessKeyId: config.accessKeyId, 44 | bucketName: config.bucket, 45 | region: config.region 46 | }; 47 | 48 | callback(s3Credentials); 49 | }; 50 | 51 | var getS3Policy = function (req, res) { 52 | createS3Policy(req.body.fileType, function (creds, err) { 53 | if (!err) { 54 | // console.log('No error creating s3 policy: ', creds); 55 | return res.status(200).send(creds); 56 | } else { 57 | // console.log('Error creating s3 policy'); 58 | return res.status(500).send(err); 59 | } 60 | }); 61 | }; 62 | 63 | var catchUpload = function (req, res, next) { 64 | var busboy = new Busboy({ headers: req.headers }); 65 | 66 | busboy.on('file', function (fieldname, file, filename, encoding, mimetype) { 67 | var saveTo = path.join(__dirname + '/../uploadInbox/' + filename); 68 | file.pipe(fs.createWriteStream(saveTo)); 69 | 70 | // console.log('File [' + fieldname + ']: filename: ' + filename + ', encoding: ' + encoding + ', mimetype: ' + mimetype); 71 | 72 | // file.on('data', function(data) { 73 | // console.log('File [' + fieldname + '] got ' + data.length + ' bytes'); 74 | // }); 75 | 76 | file.on('end', function () { 77 | // console.log('File [' + fieldname + '] Finished'); 78 | req.filename = filename; 79 | next(); 80 | }); 81 | }); 82 | 83 | // busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) { 84 | // console.log('Field [' + fieldname + ']: value: ' + inspect(val)); 85 | // }); 86 | 87 | busboy.on('finish', function () { 88 | // console.log('Done parsing form!'); 89 | res.writeHead(303, { Connection: 'close', Location: '/' }); 90 | res.end(); 91 | }); 92 | req.pipe(busboy); 93 | }; 94 | 95 | module.exports = { 96 | catchUpload: catchUpload, 97 | getS3Data: getS3Policy, 98 | createS3Policy: createS3Policy 99 | }; -------------------------------------------------------------------------------- /client/app/playlist/playlist.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
6 |
7 | Create a new playlist 8 | 14 | 17 | 18 |
19 |
20 |
21 | 22 | 23 |
24 |
25 | Delete this playlist? 26 |

{{pendingPlaylist.title}}

27 | 28 |
29 |
30 |
31 |
32 |
33 |

Playlists

34 | 35 |
36 |
37 |
{{playlist.title}}
38 |
39 |
40 |
41 |
42 | Your group doesn't have any playlists. Click the button above to create one. 43 |
44 |
45 |

{{models.currentPlaylist.title}}

46 |

{{models.currentPlaylist.description}}

47 |
48 | There are no songs in this playlist. 49 |
50 | 65 |
66 |
67 | 70 | -------------------------------------------------------------------------------- /server/config/routes.js: -------------------------------------------------------------------------------- 1 | var helpers = require('./helpers.js'); 2 | var Song = require('../controllers/songController'); 3 | var Group = require('../controllers/groupController'); 4 | var Playlist = require('../controllers/playlistController'); 5 | var User = require('../controllers/userController'); 6 | var Upload = require('../controllers/upload'); 7 | var Comment = require('../controllers/commentController'); 8 | var Info = require('../controllers/infoController'); 9 | 10 | var http = require('http'); 11 | var express = require('express'); 12 | var app = express(); 13 | var server = http.createServer(app); 14 | var io = require('socket.io').listen(server); //pass a http.Server instance 15 | 16 | var routing = function (app, express) { 17 | 18 | var apiRoutes = express.Router(); 19 | 20 | apiRoutes.post('/users/signup', User.signup); 21 | apiRoutes.post('/users/login', User.login); 22 | 23 | 24 | // EVERYTHING BELOW THIS WILL NEED A JWT TOKEN!!! 25 | apiRoutes.use(helpers.verifyToken); 26 | 27 | // info 28 | apiRoutes.get('/info', Info.audioPileStatusUpdate); 29 | 30 | // Song related requests 31 | apiRoutes.delete('/songs/:id', Song.deleteSong); 32 | apiRoutes.get('/songs/:id', Song.getSong); 33 | apiRoutes.put('/songs/:id', Song.updateSong); 34 | 35 | apiRoutes.post('/songs/:id/comments', Comment.addComment); 36 | apiRoutes.get('/songs/:id/comments', Song.getComments); 37 | 38 | // User related requests 39 | apiRoutes.post('/users/avatar', Upload.catchUpload, User.setAvatar); 40 | apiRoutes.put('/users/profile', User.updateProfile); 41 | apiRoutes.get('/users/profile', User.getProfile); 42 | apiRoutes.get('/users/:id', User.getUser); 43 | apiRoutes.get('/users/:id/groups', User.getGroups); 44 | 45 | 46 | // Add, update and retrieve groups 47 | apiRoutes.put('/groups/info', Group.updateGroupInfo); 48 | apiRoutes.put('/groups/:gid/users/:uid', Group.updateUserRole); 49 | apiRoutes.post('/groups/', Group.createGroup); 50 | apiRoutes.post('/groups/:id/users/', Group.addUser); 51 | // apiRoutes.get('/groups/:id/users/', Group.getUsers); 52 | apiRoutes.get('/groups/:id/playlists/', Group.getPlaylists); 53 | apiRoutes.delete('/groups/:gid/users/:uid', Group.removeUser); 54 | apiRoutes.delete('/groups/:id', Group.deleteGroup); 55 | 56 | // Add and retrieve songs 57 | apiRoutes.post('/groups/:id/songs/', Song.addSong); 58 | apiRoutes.get('/groups/:id/songs/', Group.getSongs); 59 | 60 | // Remove song comments 61 | apiRoutes.delete('/comments/:id', Comment.deleteComment); 62 | 63 | // Add and retrieve playlists 64 | apiRoutes.post('/playlists/', Playlist.createPlaylist); 65 | apiRoutes.post('/playlists/:sid/:pid/', Playlist.addSong); 66 | apiRoutes.get('/playlists/:id/', Playlist.getSongs); 67 | apiRoutes.put('/playlists/:id', Playlist.updatePositions); 68 | apiRoutes.delete('/playlists/:sid/:pid', Playlist.removeSong); 69 | apiRoutes.delete('/playlists/:id/', Playlist.deletePlaylist); 70 | 71 | // Upload handling 72 | apiRoutes.post('/s3/', Upload.getS3Data); 73 | apiRoutes.post('/upload/', Upload.catchUpload); 74 | 75 | // Send email invites 76 | apiRoutes.post('/groups/:id/invite', Group.sendInvite); 77 | 78 | // Handle error logging of requests that are destined for above routes 79 | apiRoutes.use(helpers.errorLogger); 80 | apiRoutes.use(helpers.errorHandler); 81 | 82 | 83 | app.use('/api', apiRoutes); 84 | }; 85 | 86 | module.exports = routing; 87 | -------------------------------------------------------------------------------- /client/app/groups/settings.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 14 | 15 | 16 |

Group Settings for {{ user.currentGroup.name }}

17 |
18 |
19 |
20 | group info 21 | 24 |
25 | 26 |
27 |
28 |
29 |
30 |
31 | invite someone 32 | 35 |
36 | 39 |
40 |
41 | {{inviteError}} 42 |
43 |
44 |
45 | 46 |
47 |
48 | Change your banner 49 |
50 | 51 | 52 | 53 | 54 | 55 |
56 | 65 |
66 | 67 |
68 |
69 |
70 |
71 |
72 | 75 |
76 | -------------------------------------------------------------------------------- /we need to do these!/_PRESS-RELEASE.md: -------------------------------------------------------------------------------- 1 | # Project Name # 2 | 3 | 18 | 19 | ## Heading ## 20 | > Name the product in a way the reader (i.e. your target customers) will understand. 21 | 22 | ## Sub-Heading ## 23 | > Describe who the market for the product is and what benefit they get. One sentence only underneath the title. 24 | 25 | ## Summary ## 26 | > Give a summary of the product and the benefit. Assume the reader will not read anything else so make this paragraph good. 27 | 28 | ## Problem ## 29 | > Describe the problem your product solves. 30 | 31 | ## Solution ## 32 | > Describe how your product elegantly solves the problem. 33 | 34 | ## Quote from You ## 35 | > A quote from a spokesperson in your company. 36 | 37 | ## How to Get Started ## 38 | > Describe how easy it is to get started. 39 | 40 | ## Customer Quote ## 41 | > Provide a quote from a hypothetical customer that describes how they experienced the benefit. 42 | 43 | ## Closing and Call to Action ## 44 | > Wrap it up and give pointers where the reader should go next. 45 | -------------------------------------------------------------------------------- /server/models/songModel.js: -------------------------------------------------------------------------------- 1 | var db = require('../db/database'); 2 | var Song = db.Song; 3 | var Comment = db.Comment; 4 | var request = require('request'); 5 | var Promise = require('bluebird'); 6 | var awsConfig = require('../config/aws.config.js'); 7 | var config = require('../config/config.js'); 8 | var uploadController = require('../controllers/upload.js'); 9 | 10 | 11 | var addSong = function (songData) { 12 | return Song.create(songData); 13 | }; 14 | 15 | var addCompressedLink = function(songID, compressedID, amplitudeData) { 16 | // console.log('--- --- Add compressed link Song.find() update DB', songID, compressedID); 17 | return new Promise(function(resolve, reject) { 18 | Song.find({where: {id: songID}}) 19 | .then(function(song) { 20 | if (song) { 21 | song.updateAttributes({ 22 | 'compressedAddress': compressedID, 23 | 'amplitudeData': amplitudeData 24 | }); 25 | resolve(); 26 | } 27 | }) 28 | .catch(function(err) { 29 | reject(err); 30 | }); 31 | }); 32 | }; 33 | 34 | var getComments = function(songId) { 35 | return new Promise(function (resolve, reject) { 36 | Song.findById(songId) 37 | .then(function(song) { 38 | if (song) { 39 | resolve(song.getComments()); 40 | } else { 41 | resolve([]); 42 | } 43 | }) 44 | .catch(function(err) { 45 | reject(err); 46 | }); 47 | }); 48 | }; 49 | 50 | var getSong = function(songId) { 51 | return Song.findById(songId); 52 | }; 53 | 54 | var updateSong = function(song) { 55 | return Song.update(song, { 56 | where: { id: song.id }, 57 | fields: ['title', 'description', 'dateRecorded', 'imageUrl'], 58 | returning: true 59 | }); 60 | }; 61 | 62 | var replaceAt = function(string, index, character) { 63 | return string.substr(0, index) + character + string.substr(index + character.length); 64 | }; 65 | 66 | // TODO: why is this a promise? 67 | var requestFileCompression = function(song) { 68 | return new Promise(function (resolve, reject) { 69 | var url = config.ZENCODER_COMPRESSION_SERVER; 70 | var fileSource = song.dataValues.address; 71 | var fileDestination = 'https://' + awsConfig.bucket + '.s3.amazonaws.com/audio/'; 72 | 73 | var compressedFileName = song.dataValues.uniqueHash; 74 | var period = compressedFileName.lastIndexOf('.'); 75 | if (period) { 76 | compressedFileName = replaceAt(compressedFileName, period, '_'); 77 | } 78 | 79 | // console.log('New file name zencoder: ', compressedFileName, period); 80 | 81 | var params = { 82 | 'api_key': config.ZENCODER_API_KEY, 83 | 'input': song.dataValues.address, 84 | 'outputs': [ 85 | { 86 | 'public': true, 87 | 'url': fileDestination + compressedFileName + '.mp3', 88 | // this credential correspondes to zencoder credential nickname 89 | 'credentials': awsConfig.bucket, 90 | 'audio_normalize': true 91 | } 92 | ], 93 | }; 94 | // console.log('url is ' + url); 95 | request.post( 96 | url, 97 | { json: params }, 98 | function (error, response, body) { 99 | if (error) { 100 | // console.log('--- --- Request compression error: ', error); 101 | reject(error); 102 | } else if (!error && response.statusCode === 201) { 103 | // console.log(' --- --- Successful creation of new audio on zencoder: ', body); 104 | resolve(body); 105 | } 106 | } 107 | ); 108 | 109 | }); 110 | }; 111 | 112 | module.exports = { 113 | addCompressedLink: addCompressedLink, 114 | addSong: addSong, 115 | getComments: getComments, 116 | getSong: getSong, 117 | updateSong: updateSong, 118 | requestFileCompression: requestFileCompression 119 | }; -------------------------------------------------------------------------------- /client/app/songs/songs.js: -------------------------------------------------------------------------------- 1 | angular.module('jam.songs', []) 2 | 3 | .controller('SongsController', ['$scope', '$location', 'Songs', 'Users', 'Groups', function ($scope, loc, Songs, Users, GR) { 4 | // When user adds a new link, put it in the collection 5 | $scope.data = {}; 6 | $scope.user = {}; 7 | $scope.time = null; 8 | $scope.timeFormat = '00:00'; 9 | $scope.comment = {}; 10 | $scope.message = ''; 11 | $scope.commentSong = {}; 12 | $scope.where = 'songs'; 13 | Songs.setViewLocation($scope.where); 14 | $scope.playable = Songs.getPlayable(); 15 | 16 | // $scope.$on('audioPlayerEvent', function(event, data) { 17 | // $scope.broadcastTest = event + ' ' + data; 18 | // console.log('EVENT'); 19 | // }); 20 | 21 | $scope.updateIndex = function(index) { 22 | console.log('Update index: ', index, $scope.where); 23 | 24 | Songs.choose(index, $scope.where); 25 | }; 26 | 27 | Users.getUserData() 28 | .then(function (user) { 29 | $scope.user = user; 30 | $scope.refreshSongs(); 31 | GR.getPlaylistsByGroupId($scope.user.currentGroup.id) 32 | .then(function (playlists) { 33 | $scope.data.playlists = playlists; 34 | }); 35 | }) 36 | .catch(console.error); 37 | 38 | $scope.addToPlaylist = function(playlist) { 39 | $scope.newSong.playlistId = playlist.id; 40 | var index = playlist.length; 41 | console.log('the playlist: ', playlist); 42 | Songs.addSongToPlaylist($scope.newSong.id, playlist.id, index) 43 | .then(function (resp) { 44 | // tell user song was added 45 | console.log(resp); 46 | }) 47 | .catch(console.error); 48 | }; 49 | 50 | $scope.getTime = function () { 51 | $scope.time = Songs.getPlayer().currentTime; 52 | $scope.timeFormat = Songs.timeFormat($scope.time); 53 | }; 54 | 55 | $scope.toggleCommentModal = function (song, userId) { 56 | $scope.commentSong = song; 57 | var playingSong = Songs.getCurrentSong(); 58 | if (playingSong && playingSong.id === song.id) { 59 | $scope.getTime(); 60 | } 61 | $scope.commentModalShown = !$scope.commentModalShown; 62 | }; 63 | 64 | $scope.addComment = function() { 65 | $scope.comment.time = $scope.time; 66 | $scope.comment.userId = $scope.user.id; 67 | Songs.addComment($scope.comment, $scope.commentSong.id) 68 | .then(function(comment) { 69 | $scope.comment = {}; 70 | $scope.time = null; 71 | $scope.commentModalShown = false; 72 | $scope.message = 'comment posted: ' + comment; 73 | }); 74 | }; 75 | 76 | 77 | $scope.toggleAddModal = function () { 78 | $scope.addModalShown = !$scope.addModalShown; 79 | }; 80 | 81 | $scope.refreshSongs = function() { 82 | Songs.getAllSongs($scope.user.currentGroupId) 83 | .then(function(songs) { 84 | console.log('Refresh all songs: ', songs); 85 | $scope.data.songs = songs.sort(function(a, b) { 86 | if ( a.createdAt > b.createdAt ) { 87 | return -1; 88 | } else if ( a.createdAt < b.createdAt ) { 89 | return 1; 90 | } else { 91 | return 0; 92 | } 93 | }); 94 | }) 95 | .catch(console.error); 96 | }; 97 | 98 | $scope.makeSongPending = function (song, index) { 99 | $scope.deleteModalShown = true; 100 | $scope.pendingSong = song; 101 | $scope.pendingSong.index = index; 102 | }; 103 | 104 | $scope.deleteSong = function(index) { 105 | var song = $scope.data.songs[index]; 106 | Songs.checkReset(song.id, 'songs'); 107 | Songs.deleteSong(song) 108 | .then(function() { 109 | $scope.deleteModalShown = false; 110 | $scope.data.songs = _.filter($scope.data.songs, function(currentSong) { 111 | return currentSong.id !== song.id; 112 | }); 113 | }) 114 | .catch(function (err) { 115 | $scope.message = 'error: ' + err; 116 | }); 117 | }; 118 | }]); 119 | -------------------------------------------------------------------------------- /client/app/services/uploadsFactory.js: -------------------------------------------------------------------------------- 1 | angular.module('jam.uploadsFactory', ['jam.usersFactory']) 2 | .factory('UploadFactory', ['$http', '$window', '$q', 'Upload', 'Users', 'Songs', 3 | function ($http, win, q, Upload, Users, Songs) { 4 | var audioQueue = []; 5 | var uploadedAudio = []; 6 | 7 | // upload on file select or drop 8 | var upload = function(file, directory, successCallback, errorCallback, progressCallback) { 9 | 10 | var postData = { 11 | uniqueFilename: file.name, 12 | fileType: file.type 13 | }; 14 | 15 | $http.post('/api/s3', postData) 16 | .then(function(res) { 17 | var s3Credentials = res.data; 18 | beginDirectS3Upload(s3Credentials, file); 19 | }, function(res) { 20 | // AWS Signature API Error 21 | console.log('Error', res); 22 | }); 23 | 24 | String.prototype.uuid = function() { 25 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 26 | var r = Math.random() * 16 | 0; 27 | var v = (c === 'x') ? r : r & 0x3 | 0x8; 28 | return v.toString(16); 29 | }); 30 | }; 31 | 32 | var beginDirectS3Upload = function(s3Credentials, file) { 33 | 34 | console.log('Begin s3 upload', s3Credentials); 35 | var groupId; 36 | 37 | Users.getUserData() 38 | .then(function(user) { 39 | var fileExtension = file.name.replace(/^.*\./, ''); 40 | var uniqueFilename = ( Date.now() + file.name ).uuid() + '.' + fileExtension; 41 | var dataObj = { 42 | 'key': directory + '/' + uniqueFilename, 43 | 'acl': 'public-read', 44 | 'Content-Type': file.type, 45 | 'AWSAccessKeyId': s3Credentials.AWSAccessKeyId, 46 | 'success_action_status': '201', 47 | 'Policy': s3Credentials.s3Policy, 48 | 'Signature': s3Credentials.s3Signature 49 | }; 50 | 51 | // init upload bar on this element 52 | // var divId = 'progressBar' + file.queueId; 53 | // $scope[divId] = ngProgressFactory.createInstance(); 54 | // console.log('Progress bar test!', $scope[divId]); 55 | // $scope[divId].setParent(document.getElementById(divId)); 56 | // $scope[divId].start(); 57 | // $scope[divId].setAbsolute(); 58 | file.status = 'UPLOADING'; 59 | file.progressPercentage = 0; 60 | file.uploader = Upload.upload({ 61 | url: 'https://' + s3Credentials.bucketName + '.s3.amazonaws.com/', 62 | method: 'POST', 63 | transformRequest: function (data, headersGetter) { 64 | var headers = headersGetter(); 65 | delete headers['Authorization']; 66 | return data; 67 | }, 68 | data: dataObj, 69 | file: file, 70 | }); 71 | file.uploader.then(function(response) { 72 | // On upload confirmation 73 | file.status = 'COMPLETE'; 74 | file.progressPercentage = parseInt(100); 75 | console.log('Upload confirmed'); 76 | if (response.status === 201) { 77 | var escapedUrl = new DOMParser().parseFromString(response.data, 'application/xml').getElementsByTagName('Location')[0].textContent; 78 | file.s3url = unescape(escapedUrl); 79 | file.uniqueFilename = uniqueFilename; 80 | if (successCallback) { 81 | successCallback(file, response); 82 | } 83 | } else { 84 | file.status = 'ERROR'; 85 | if (errorCallback) { 86 | errorCallback(response); 87 | } 88 | } 89 | }, 90 | null, // WHAT IS THIS? 91 | function(evt) { 92 | if (progressCallback) { 93 | progressCallback(file, evt); 94 | } 95 | 96 | }); 97 | file.uploader.catch(errorCallback); 98 | }); 99 | }; 100 | }; 101 | return { 102 | upload: upload, 103 | audioQueue: audioQueue 104 | 105 | }; 106 | }]); 107 | -------------------------------------------------------------------------------- /client/app/services/groupsFactory.js: -------------------------------------------------------------------------------- 1 | angular.module('jam.groupsFactory', []) 2 | 3 | .factory('Groups', ['$http', '$q', function (http, q) { 4 | 5 | // Each group has an array of members 6 | var groupsData = null; 7 | 8 | var createGroup = function (newGroup) { 9 | return http({ 10 | method: 'POST', 11 | url: '/api/groups/', 12 | data: newGroup 13 | }) 14 | .then(function (res) { 15 | return res.data; 16 | }); 17 | }; 18 | 19 | var addUser = function (groupId, userId, role) { 20 | var data = {userId: userId, role: role}; 21 | return http({ 22 | method: 'POST', 23 | url: '/api/groups/' + groupId + '/users/', 24 | data: data 25 | }) 26 | .then(function (res) { 27 | return res.data; 28 | }); 29 | }; 30 | 31 | var getGroupsByUserId = function (userId) { 32 | return http({ 33 | method: 'GET', 34 | url: '/api/users/' + userId + '/groups/' 35 | }) 36 | .then(function (res) { 37 | _.extend(groupsData, res.data); 38 | return res.data; 39 | }); 40 | }; 41 | 42 | var getPlaylistsByGroupId = function (groupId) { 43 | return http({ 44 | method: 'GET', 45 | url: '/api/groups/' + groupId + '/playlists/' 46 | }) 47 | .then(function (res) { 48 | return res.data; 49 | }); 50 | }; 51 | 52 | var updateInfo = function(group) { 53 | return http({ 54 | method: 'PUT', 55 | url: '/api/groups/info', 56 | data: group 57 | }) 58 | .then(function(res) { 59 | return res.data; 60 | }) 61 | .catch(console.error); 62 | }; 63 | 64 | var sendInvite = function (group, email) { 65 | var data = {email: email, group: group}; 66 | 67 | return http({ 68 | method: 'post', 69 | url: '/api/groups/' + group.id + '/invite', 70 | data: data 71 | }) 72 | .then(function (res) { 73 | return res.data; 74 | }); 75 | }; 76 | 77 | var updateUserRole = function (groupId, userId, role) { 78 | var data = {role: role}; 79 | return http({ 80 | method: 'PUT', 81 | url: '/api/groups/' + groupId + '/users/' + userId, 82 | data: data 83 | }) 84 | .then(function (res) { 85 | return res.data; 86 | }); 87 | }; 88 | 89 | var removeUser = function (groupId, userId) { 90 | return http({ 91 | method: 'DELETE', 92 | url: '/api/groups/' + groupId + '/users/' + userId, 93 | }) 94 | .then(function (res) { 95 | return res.data; 96 | }); 97 | }; 98 | 99 | var getGroupsData = function(user, force) { 100 | force = force || false; 101 | return q(function(resolve, reject) { 102 | if (groupsData && !force) { 103 | resolve(groupsData); 104 | } 105 | getGroupsByUserId(user.id) 106 | .then(function(groups) { 107 | if (groupsData) { 108 | _.extend(groupsData, groups); 109 | } else { 110 | groupsData = groups; 111 | } 112 | resolve(groupsData); 113 | }) 114 | .catch(function (error) { 115 | reject(error); 116 | }); 117 | }); 118 | }; 119 | 120 | var setGroupsData = function(data) { 121 | if (!data) { 122 | groupsData = null; 123 | } else { 124 | _.extend(groupsData, data); 125 | } 126 | }; 127 | 128 | var deleteGroup = function(id) { 129 | return http({ 130 | method: 'DELETE', 131 | url: '/api/groups/' + id 132 | }) 133 | .then(function (res) { 134 | return res.data; 135 | }); 136 | }; 137 | 138 | return { 139 | createGroup: createGroup, 140 | addUser: addUser, 141 | getGroupsByUserId: getGroupsByUserId, 142 | getPlaylistsByGroupId: getPlaylistsByGroupId, 143 | updateInfo: updateInfo, 144 | sendInvite: sendInvite, 145 | updateUserRole: updateUserRole, 146 | removeUser: removeUser, 147 | getGroupsData: getGroupsData, 148 | setGroupsData: setGroupsData, 149 | deleteGroup: deleteGroup 150 | }; 151 | }]); 152 | -------------------------------------------------------------------------------- /test/server/unit/playlistTest.js: -------------------------------------------------------------------------------- 1 | var sinon = require('sinon'); 2 | var chai = require('chai'); 3 | var expect = chai.expect; 4 | var Sequelize = require('sequelize'); 5 | var dbModels = require('../../../server/db/database.js'); 6 | var PlaylistSchema = dbModels.Playlist; 7 | var Group = dbModels.Group; 8 | var SongModel = require('../../../server/models/songModel.js'); 9 | var SongController = require('../../../server/controllers/songController.js'); 10 | var GroupController = require('../../../server/controllers/groupController.js'); 11 | var PlaylistController = require('../../../server/controllers/playlistController.js'); 12 | var helpers = require('../testHelpers'); 13 | 14 | // Define api request bodies 15 | var songReq = helpers.songReq; 16 | var addSongReq = helpers.addSongReq; 17 | var playlistReq = helpers.playlistReq; 18 | 19 | var compressStub; 20 | 21 | describe('Playlist Controller', function() { 22 | before(function(done) { 23 | compressStub = sinon.stub(SongModel, 'requestFileCompression', function() { 24 | return new Promise(function(resolve, reject) { 25 | resolve(true); 26 | }); 27 | }); 28 | done(); 29 | }); 30 | 31 | after(function (done) { 32 | compressStub.restore(); 33 | done(); 34 | }); 35 | 36 | // The `clearDB` helper function, when invoked, will clear the database 37 | var clearDB = function(done) { 38 | var res = { 39 | json: function(playlist) { 40 | addSongReq.params.pid = playlist.id; 41 | done(); 42 | } 43 | }; 44 | 45 | var addedSongCallback = function(song) { 46 | addSongReq.params.sid = song.id; 47 | PlaylistController.createPlaylist(playlistReq, res, console.error); 48 | }; 49 | 50 | helpers.rebuildDb(function() { 51 | Group.create({name: 'Buoyant Pyramid'}) 52 | .then(function(group) { 53 | songReq.params.id = group.id; 54 | playlistReq.body.groupId = group.id; 55 | SongController.addSong(songReq, {json: addedSongCallback}, console.error); 56 | }); 57 | }); 58 | 59 | }; 60 | 61 | 62 | describe('create playlist', function() { 63 | // Clear database before each test and then seed it with example `users` so that you can run tests 64 | beforeEach(function(done) { 65 | clearDB(done); 66 | }); 67 | 68 | it('should call res.json to return a json object', function (done) { 69 | var res = {}; 70 | 71 | res.json = function(jsonresponse) { 72 | expect(jsonresponse).to.have.property('title'); 73 | done(); 74 | }; 75 | 76 | res.send = function(err) { 77 | console.error(err); 78 | }; 79 | PlaylistController.createPlaylist(playlistReq, res, console.error); 80 | }); 81 | }); 82 | describe('songs and playlists', function() { 83 | it('should add a song to a playlist', function (done) { 84 | var res = {}; 85 | 86 | res.json = function(jsonresponse) { 87 | expect(jsonresponse.songId).to.equal(addSongReq.params.sid); 88 | expect(jsonresponse.playlistId).to.equal(addSongReq.params.pid); 89 | done(); 90 | }; 91 | 92 | res.send = function(err) { 93 | console.error(err); 94 | }; 95 | PlaylistController.addSong(addSongReq, res, console.error); 96 | }); 97 | 98 | it('should fetch songs from a playlist', function (done) { 99 | before(function(done) { 100 | dbModels.Playlist.addSong(addSongReq.params.pid, addSongReq.params.sid) 101 | .then(function () { 102 | done(); 103 | }).catch(console.error); 104 | }); 105 | 106 | var res = {}; 107 | 108 | res.json = function(jsonresponse) { 109 | // console.log('jsonresponse is: ' + JSON.stringify(jsonresponse)); 110 | expect(jsonresponse[0].title).to.eql('Margaritaville'); 111 | done(); 112 | }; 113 | 114 | res.send = function(err) { 115 | console.error(err); 116 | }; 117 | PlaylistController.getSongs({params: {id: addSongReq.params.pid}}, res, console.error); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /server/models/groupModel.js: -------------------------------------------------------------------------------- 1 | var db = require('../db/database'); 2 | var Group = db.Group; 3 | var Song = db.Song; 4 | var User = db.User; 5 | var UserGroups = db.UserGroups; 6 | var UserModel = require('./userModel.js'); 7 | var config = require('../config/config'); 8 | var mailgun = require('mailgun-js')({apiKey: config.mailgun.api_key, domain: config.mailgun.domain}); 9 | var Promise = require('bluebird'); 10 | 11 | 12 | var addUser = function (groupId, userId, role) { 13 | return new Promise(function (resolve, reject) { 14 | getGroup(groupId) 15 | .then(function (group) { 16 | this.group = group; 17 | return User.findOne({ 18 | where: { id: userId }, 19 | attributes: { exclude: ['password'] } 20 | }); 21 | }) 22 | .then(function (user) { 23 | this.user = user; 24 | return this.group.addUser(user, {role: role}); 25 | }) 26 | .then(function () { 27 | resolve(this.user); 28 | }) 29 | .catch(function(error) { 30 | reject(error); 31 | }); 32 | }).bind({}); 33 | }; 34 | 35 | var createGroup = function (name) { 36 | // TODO: Add banner 37 | return Group.create({name: name}); 38 | }; 39 | 40 | var getGroup = function (groupId) { 41 | // TODO: Add banner 42 | return Group.findById(groupId); 43 | }; 44 | 45 | var getUsers = function(groupId) { 46 | return Group.findById(groupId, { 47 | include: [{ 48 | model: User, 49 | attributes: { exclude: ['password'] } 50 | }] 51 | }) 52 | .then(function(group) { 53 | return group.users; 54 | }); 55 | }; 56 | 57 | var getUserGroup = function(userId, groupId) { 58 | return UserGroups.findOne({ 59 | where: {userId: userId, groupId: groupId} 60 | }); 61 | }; 62 | 63 | var removeUser = function (groupId, userId) { 64 | return UserGroups.findOne({where: {groupId: groupId, userId: userId}}) 65 | .then(function (userGroup) { 66 | return userGroup.destroy(); 67 | }); 68 | }; 69 | 70 | var deleteGroup = function (groupId) { 71 | return Group.findOne({where: {id: groupId}}) 72 | .then(function (group) { 73 | // get and destroy all songs uploaded by the group 74 | return group.destroy(); 75 | }) 76 | .catch(console.error); 77 | }; 78 | 79 | var sendEmailInvite = function(group, email) { 80 | var password = Math.random().toString(36).substring(5); 81 | return new Promise(function (resolve, reject) { 82 | UserModel.createUser(email, 'anonymous', password) 83 | .then(function (user) { 84 | return group.addUser(user, {role: 'pending'}); 85 | }) 86 | .then(function() { 87 | var data = { 88 | from: 'Audiopile ', 89 | to: email, 90 | subject: 'Hello', 91 | text: 'You\'ve been invited to join ' + group.name + ' at Audiopile!\n\n' + 92 | 'Click the link below and login with the following credentials:\n' + 93 | '\temail: ' + email + '\n' + 94 | '\tpassword: ' + password + '\n\n' + 95 | 'http://www.audiopile.rocks/#/login/' + email + '\n' 96 | }; 97 | mailgun.messages().send(data, function (error, body) { 98 | if (error) { 99 | reject(error); 100 | } else { 101 | resolve('Email sent successfully'); 102 | } 103 | }); 104 | }) 105 | .catch(function (error) { 106 | reject(error); 107 | }); 108 | }); 109 | }; 110 | 111 | var updateUserRole = function (groupId, userId, role) { 112 | return UserGroups.update({role: role}, {where: {groupId: groupId, userId: userId}}); 113 | }; 114 | 115 | var updateInfo = function(groupId, fields) { 116 | return Group.update(fields, { 117 | where: { 118 | id: groupId 119 | }, 120 | returning: true 121 | }); 122 | }; 123 | 124 | module.exports = { 125 | addUser: addUser, 126 | createGroup: createGroup, 127 | getGroup: getGroup, 128 | getUserGroup: getUserGroup, 129 | getUsers: getUsers, 130 | removeUser: removeUser, 131 | deleteGroup: deleteGroup, 132 | sendEmailInvite: sendEmailInvite, 133 | updateUserRole: updateUserRole, 134 | updateInfo: updateInfo 135 | }; -------------------------------------------------------------------------------- /client/app/playlist/playlist.js: -------------------------------------------------------------------------------- 1 | angular.module('jam.playlist', []) 2 | .controller('PlaylistController', ['$scope', 'Users', 'Songs', 'Groups', function ($scope, Users, Songs, GR) { 3 | $scope.newPlaylist = {}; 4 | $scope.models = { 5 | selected: null 6 | }; 7 | $scope.models.currentPlaylist = Songs.getCurrentPlaylist(); 8 | $scope.models.playlists = []; 9 | $scope.user = {}; 10 | $scope.where = 'playlist'; 11 | Songs.setViewLocation($scope.where); 12 | $scope.playable = Songs.getPlayable(); 13 | 14 | $scope.updateIndex = function(index) { 15 | Songs.choose(index, $scope.where, $scope.models.currentPlaylist); 16 | }; 17 | 18 | $scope.dropCallback = function(event, index, item, external, type, allowedType) { 19 | $scope.reorderPlaylist($scope.models.selected, index); 20 | return item; 21 | }; 22 | 23 | $scope.reorderPlaylist = function(song, newIndex) { 24 | var oldIndex = song.playlistSongs.listPosition; 25 | if (oldIndex < newIndex) { 26 | newIndex--; 27 | } 28 | if (oldIndex !== newIndex) { 29 | var targetSong = $scope.models.currentPlaylist.songs[oldIndex]; 30 | $scope.models.currentPlaylist.songs.splice(oldIndex, 1); 31 | $scope.models.currentPlaylist.songs.splice(newIndex, 0, targetSong); 32 | // rekey all the list positions: 33 | var updateArray = []; 34 | for (var i = 0, l = $scope.models.currentPlaylist.songs.length; i < l; i++) { 35 | $scope.models.currentPlaylist.songs[i].playlistSongs.listPosition = i; 36 | updateArray.push({songId: $scope.models.currentPlaylist.songs[i].id, listPosition: i}); 37 | } 38 | Songs.updatePlaylistPosition($scope.models.currentPlaylist.id, updateArray) 39 | .then(function(resp) { 40 | // anything? 41 | }) 42 | .catch(console.error); 43 | } 44 | }; 45 | 46 | Users.getUserData() 47 | .then(function (user) { 48 | $scope.user = user; 49 | $scope.newPlaylist.groupId = $scope.user.currentGroup.id; 50 | GR.getPlaylistsByGroupId($scope.user.currentGroup.id) 51 | .then(function (playlists) { 52 | $scope.models.playlists = playlists; 53 | playlists.length && $scope.makeCurrent(playlists[0]); 54 | }); 55 | }) 56 | .catch(console.error); 57 | 58 | $scope.toggleCreateModal = function () { 59 | $scope.createModalShown = !$scope.createModalShown; 60 | }; 61 | 62 | $scope.pendingDeletePlaylist = function (playlist) { 63 | $scope.pendingPlaylist = playlist; 64 | $scope.destroyModalShown = true; 65 | }; 66 | 67 | $scope.makeCurrent = function (playlist) { 68 | $scope.models.currentPlaylist = playlist; 69 | Songs.getPlaylistSongs(playlist.id) 70 | .then(function (songs) { 71 | $scope.models.currentPlaylist.songs = songs; 72 | }) 73 | .catch(console.error); 74 | }; 75 | 76 | $scope.createPlaylist = function () { 77 | Songs.createPlaylist($scope.newPlaylist) 78 | .then(function (playlist) { 79 | $scope.createModalShown = false; 80 | GR.getPlaylistsByGroupId($scope.user.currentGroup.id) 81 | .then(function (playlists) { 82 | $scope.models.playlists = playlists; 83 | }) 84 | .catch(console.error); 85 | }); 86 | }; 87 | 88 | $scope.deleteSong = function (index) { 89 | var songId = $scope.models.currentPlaylist.songs[index].id; 90 | $scope.models.currentPlaylist.songs.splice(index, 1); 91 | Songs.deleteFromPlaylist(songId, $scope.models.currentPlaylist.id) 92 | .then(function(resp) { 93 | console.log(resp); 94 | }) 95 | .catch(console.error); 96 | }; 97 | 98 | $scope.deletePlaylist = function () { 99 | var playlist = $scope.pendingPlaylist; 100 | if ($scope.models.currentPlaylist.id === playlist.id) { 101 | $scope.models.currentPlaylist = {}; 102 | } 103 | Songs.deletePlaylist(playlist.id) 104 | .then(function(resp) { 105 | $scope.destroyModalShown = false; 106 | $scope.models.playlists = _.filter($scope.models.playlists, function (currentPlaylist) { 107 | return currentPlaylist.id !== playlist.id; 108 | }); 109 | }) 110 | .catch(console.error); 111 | }; 112 | 113 | }]); -------------------------------------------------------------------------------- /test/server/integration/apiTest.js: -------------------------------------------------------------------------------- 1 | var request = require('supertest'); 2 | var express = require('express'); 3 | var app = require('../../../server/server.js'); 4 | var chai = require('chai'); 5 | var expect = chai.expect; 6 | var Sequelize = require('sequelize'); 7 | var dbModels = require('../../../server/db/database.js'); 8 | var Song = dbModels.Song; 9 | var User = dbModels.User; 10 | var Group = dbModels.Group; 11 | var Playlist = dbModels.Playlist; 12 | var UserGroups = dbModels.UserGroups; 13 | var UserController = require('../../../server/controllers/userController.js'); 14 | var helpers = require('../testHelpers'); 15 | 16 | var dbUser, jwtToken; 17 | 18 | // Initialize db with one user before every test 19 | var clearDB = function(done) { 20 | var req = { 21 | body: { 22 | email: 'finn@ooo.com', 23 | displayName: 'Finn', 24 | password: 'thehoomun' 25 | } 26 | }; 27 | 28 | var res = { 29 | json: function(response) { 30 | dbUser = response.user; 31 | jwtToken = response.token; 32 | done(); 33 | } 34 | }; 35 | dbModels.db.query('DELETE from USERS where true') 36 | .spread(function(results, metadata) { 37 | UserController.signup(req, res, console.error); 38 | }); 39 | }; 40 | 41 | 42 | describe('API', function() { 43 | // rebuild test database 44 | 45 | before(function (done) { 46 | helpers.rebuildDb(done); 47 | }); 48 | 49 | 50 | beforeEach(function(done) { 51 | clearDB(function() { 52 | done(); 53 | }); 54 | }); 55 | 56 | describe('user', function() { 57 | it('should be able to sign up a new user with a valid email and password', function(done) { 58 | request(app) 59 | .post('/api/users/signup') 60 | .send({ 61 | 'email': 'jake@ooo.com', 62 | 'displayName': 'Jake', 63 | 'password': 'thedog' 64 | }) 65 | .expect(200) 66 | .expect(function(res) { 67 | expect(res.body.token).to.exist; 68 | expect(res.body.user).to.exist; 69 | expect(res.body.user.currentGroup).to.exist; 70 | expect(res.body.user.currentGroup.songs).to.exist; 71 | }) 72 | .end(done); 73 | }); 74 | 75 | it('should not accept an invalid email on signup', function(done) { 76 | request(app) 77 | .post('/api/users/signup') 78 | .send({ 79 | 'email': 'jake', 80 | 'displayName': 'Jake', 81 | 'password': 'thedog' 82 | }) 83 | .expect(400) 84 | .end(done); 85 | }); 86 | 87 | it('should login an existing user', function(done) { 88 | request(app) 89 | .post('/api/users/login') 90 | .send({ 91 | email: 'finn@ooo.com', 92 | password: 'thehoomun' 93 | }) 94 | .expect(200) 95 | .expect(function(res) { 96 | expect(res.body.token).to.exist; 97 | expect(res.body.user).to.exist; 98 | expect(res.body.user.currentGroup).to.exist; 99 | expect(res.body.user.currentGroup.songs).to.exist; 100 | }) 101 | .end(done); 102 | }); 103 | 104 | it('should send a 404 status when logging in a nonexistent user', function(done) { 105 | request(app) 106 | .post('/api/users/login') 107 | .send({ 108 | email: 'jake@ooo.com', 109 | password: 'thedog' 110 | }) 111 | .expect(404) 112 | .end(done); 113 | }); 114 | 115 | it('should send a 401 status when logging in with an incorrect password', function(done) { 116 | request(app) 117 | .post('/api/users/login') 118 | .send({ 119 | email: 'finn@ooo.com', 120 | password: 'wrongpassword' 121 | }) 122 | .expect(401) 123 | .end(done); 124 | }); 125 | 126 | }); 127 | 128 | xdescribe('profile', function() { 129 | it('should be able to fetch own profile', function(done) { 130 | }); 131 | 132 | it('should throw a 401 with no jwt in header', function(done) { 133 | }); 134 | 135 | it('should throw a 401 with a nonsense token', function(done) { 136 | 137 | }); 138 | 139 | it('should be able to update own profile', function(done) { 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /client/app/upload/upload.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('jam.upload', []) 3 | .controller('UploadController', ['$scope', 'Upload', 'UploadFactory', 'ngProgressFactory', 'Users', 'Songs', '$http', function($scope, Upload, UploadFactory, ngProgressFactory, Users, Songs, $http) { 4 | 5 | $scope.progressbar = ngProgressFactory.createInstance(); 6 | $scope.queue = UploadFactory.audioQueue; 7 | $scope.playable = Songs.getPlayable(); 8 | 9 | var totalToUpload = 0; 10 | var totalUploaded = 0; 11 | var totalPercent = 0; 12 | 13 | var findTotalPercent = function() { 14 | var total = 0; 15 | for (var i = 0; i < $scope.queue.length; i++) { 16 | if ($scope.queue[i].progressPercentage) { 17 | total += $scope.queue[i].progressPercentage; 18 | } 19 | } 20 | 21 | totalPercent = Math.ceil(total / (totalToUpload)); 22 | // console.log('Total percent progress bar: ========== ', totalPercent); 23 | 24 | $scope.progressbar.set(totalPercent); 25 | if (totalPercent === 100) { 26 | $scope.progressbar.complete(); 27 | } 28 | }; 29 | 30 | var throttledTotal = _.throttle(findTotalPercent, 250); 31 | 32 | $scope.addToQueue = function(files) { 33 | for (file in files) { 34 | var thisFile = files[file]; 35 | thisFile['queueId'] = Math.floor( Math.random() * 10000000 ); 36 | thisFile.status = 'READY'; 37 | thisFile.editing = false; 38 | thisFile.displayName = thisFile.name; 39 | $scope.queue.push(thisFile); 40 | } 41 | }; 42 | 43 | 44 | $scope.removeFile = function(index) { 45 | if (index > -1) { 46 | $scope.queue.splice(index, 1); 47 | } 48 | }; 49 | 50 | var getAudioLength = function(file, cb) { 51 | var objectUrl; 52 | var a = document.createElement('audio'); 53 | a.addEventListener('canplaythrough', function(e) { 54 | var seconds = e.currentTarget.duration; 55 | cb(seconds); 56 | URL.revokeObjectURL(objectUrl); 57 | }); 58 | 59 | objectUrl = URL.createObjectURL(file); 60 | a.setAttribute('src', objectUrl); 61 | }; 62 | 63 | var progressCallback = function(file, evt) { 64 | var progressPercentage = parseInt(100.0 * evt.loaded / evt.total); 65 | file['progressPercentage'] = progressPercentage; 66 | throttledTotal(); 67 | }; 68 | 69 | var successCallback = function (file, response) { 70 | getAudioLength(file, function(duration) { 71 | file.duration = duration; 72 | Songs.addSong(file, file.groupId) 73 | .then(function(data) { 74 | // console.log('Song added: ', data); 75 | }) 76 | .catch(console.error); 77 | }); 78 | }; 79 | 80 | $scope.cancelUpload = function(file) { 81 | if (file.uploader) { 82 | file.uploader.abort(); 83 | file.status = 'CANCELLED'; 84 | } 85 | }; 86 | 87 | $scope.upload = function(file) { 88 | Users.getUserData() 89 | .then(function(user) { 90 | file.groupId = user.currentGroupId; 91 | file.editing = false; 92 | UploadFactory.upload(file, 'audio', successCallback, console.error, progressCallback); 93 | file.progressPercentage = 0; 94 | }) 95 | .catch(console.error); 96 | }; 97 | 98 | // for multiple files: 99 | $scope.cancelAll = function() { 100 | $scope.progressbar.set(0); 101 | if ($scope.queue && $scope.queue.length) { 102 | totalToUpload = 0; 103 | totalUploaded = 0; 104 | for (var i = 0; i < $scope.queue.length; i++) { 105 | if ($scope.queue[i].status === 'UPLOADING') { 106 | $scope.cancelUpload($scope.queue[i]); 107 | } 108 | } 109 | } 110 | }; 111 | 112 | // for multiple files: 113 | $scope.uploadFiles = function() { 114 | $scope.progressbar.set(0); 115 | if ($scope.queue && $scope.queue.length) { 116 | totalToUpload = $scope.queue.length; 117 | totalUploaded = 0; 118 | for (var i = 0; i < $scope.queue.length; i++) { 119 | if ($scope.queue[i].status === 'READY') { 120 | $scope.upload($scope.queue[i]); 121 | } 122 | } 123 | } 124 | }; 125 | }]); 126 | 127 | -------------------------------------------------------------------------------- /client/app/styles/_sliders.scss: -------------------------------------------------------------------------------- 1 | @import "colors"; 2 | 3 | // The draghandle 4 | $thumb-color: $linkcolor !default; 5 | 6 | $thumb-radius: 1 !default; 7 | $thumb-height: 1.5rem !default; 8 | $thumb-width: 0.5rem !default; 9 | 10 | $thumb-border-width: 0 !default; 11 | $thumb-border-color: $midgrey !default; 12 | 13 | $thumb-shadow-size: 0 !default; 14 | $thumb-shadow-blur: 1px !default; 15 | $thumb-shadow-color: rgba(0, 0, 0, 0) !default; 16 | 17 | // The range 18 | $track-color: rgba(33, 33, 33, 0.5) !default; 19 | 20 | $track-radius: 0 !default; 21 | $track-width: auto !default; 22 | $track-height: 0.5rem !default; 23 | 24 | $track-border-width: 0 !default; 25 | $track-border-color: $linkcolor !default; 26 | 27 | $track-shadow-size: 1px !default; 28 | $track-shadow-blur: 1px !default; 29 | $track-shadow-color: rgba(0, 0, 0, 0) !default; 30 | 31 | @mixin shadow($shadow-size, $shadow-blur, $shadow-color) { 32 | box-shadow: $shadow-size $shadow-size $shadow-blur $shadow-color, 0px 0px $shadow-size lighten($shadow-color, 5%); 33 | } 34 | 35 | @mixin track ($width:$track-width, $height:$track-height) { 36 | width: $width; 37 | height: $height; 38 | cursor: pointer; 39 | animate: 0.2s; 40 | outline:0; 41 | } 42 | 43 | @mixin thumb ($width:$thumb-width, $height:$thumb-height, $color:$thumb-color) { 44 | @include shadow($thumb-shadow-size, $thumb-shadow-blur, $thumb-shadow-color); 45 | 46 | border: $thumb-border-width solid $thumb-border-color; 47 | height: $height; 48 | width: $width; 49 | border-radius: $thumb-radius; 50 | background: $color; 51 | cursor: pointer; 52 | } 53 | 54 | @mixin input-type-range ($thumbwidth:$thumb-width, $thumbheight:$thumb-height, $thumbcolor:$thumb-color, $trackwidth:$track-width, $trackheight:$track-height, $trackcolor:$track-color) { 55 | -webkit-appearance: none; 56 | background: transparent; 57 | width: $trackwidth; 58 | 59 | &:focus { 60 | outline: none; 61 | } 62 | 63 | &::-webkit-slider-runnable-track { 64 | @include track($trackwidth, $trackheight); 65 | 66 | @include shadow($track-shadow-size, $track-shadow-blur, $track-shadow-color); 67 | 68 | background: $trackcolor; 69 | border-radius: $track-radius; 70 | border: $track-border-width solid $track-border-color; 71 | } 72 | 73 | &::-webkit-slider-thumb { 74 | @include thumb($thumbwidth, $thumbheight); 75 | 76 | -webkit-appearance: none; 77 | margin-top: (-$track-border-width * 2 + $trackheight) / 2 - $thumbheight / 2; 78 | } 79 | 80 | &:focus::-webkit-slider-runnable-track { 81 | background: $trackcolor; 82 | } 83 | 84 | &::-moz-range-track { 85 | @include track($trackwidth, $trackheight); 86 | 87 | @include shadow($track-shadow-size, $track-shadow-blur, $track-shadow-color); 88 | 89 | background: $trackcolor; 90 | border-radius: $track-radius; 91 | border: $track-border-width solid $track-border-color; 92 | &:focus { 93 | outline: none; 94 | } 95 | } 96 | 97 | &::-moz-range-thumb { 98 | @include thumb($thumbwidth, $thumbheight); 99 | } 100 | 101 | &::-ms-track { 102 | @include track($trackwidth, $trackheight); 103 | 104 | background: transparent; 105 | border-color: transparent; 106 | border-width: $thumb-width 0; 107 | color: transparent; 108 | } 109 | 110 | &::-ms-fill-lower { 111 | background: $trackcolor; 112 | border: $track-border-width solid $track-border-color; 113 | border-radius: $track-radius * 2; 114 | 115 | @include shadow($track-shadow-size, $track-shadow-blur, $track-shadow-color); 116 | } 117 | 118 | &::-ms-fill-upper { 119 | background: $trackcolor; 120 | border: $track-border-width solid $track-border-color; 121 | border-radius: $track-radius * 2; 122 | 123 | @include shadow($track-shadow-size, $track-shadow-blur, $track-shadow-color); 124 | } 125 | 126 | &::-ms-thumb { 127 | @include thumb($thumbwidth, $thumbheight); 128 | } 129 | 130 | &:focus::-ms-fill-lower { 131 | background: $trackcolor; 132 | } 133 | 134 | &:focus::-ms-fill-upper { 135 | background: $trackcolor; 136 | } 137 | } -------------------------------------------------------------------------------- /client/app/auth/login.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Audiopile

5 | 6 |
7 | 8 | 9 |
10 |
11 |
12 |
13 | 14 |
15 |
    16 |
    17 |
  • Upload uncompressed audio in any format
  • 18 |
  • Automatic audio transcoding and normalization
  • 19 |
    20 |
    21 |
  • Backup source files, stream lower resolution mp3
  • 22 |
  • Collaborate and share audio with your group
  • 23 |
    24 |
25 | 26 | 51 | 52 | 84 | 85 |

AudioPile

86 | 87 |
88 |
89 |
90 | -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | var jwt = require('jsonwebtoken'); 2 | var config = require('../config/config'); 3 | var path = require('path'); 4 | var User = require('../models/userModel'); 5 | 6 | var JWT_SECRET = config.JWT_SECRET || 's00p3R53kritt'; 7 | 8 | var getGroups = function(req, res, next) { 9 | var userId = parseInt(req.params.id); 10 | 11 | User.getGroups(userId) 12 | .then(function (groups) { 13 | res.json(groups); 14 | }) 15 | .catch(function (error) { 16 | next(error); 17 | }); 18 | }; 19 | 20 | var getProfile = function(req, res, next) { 21 | var user = req.user; 22 | User.compileUserData(user) 23 | .then(function(compiledUser) { 24 | res.json({ 25 | user: compiledUser 26 | }); 27 | }) 28 | .catch(function (error) { 29 | next(error); 30 | }); 31 | }; 32 | 33 | var getUser = function(req, res, next) { 34 | var userId = parseInt(req.params.id); 35 | User.getUser({id: userId}) 36 | .then(function(foundUser) { 37 | if (foundUser) { 38 | // INCLUDE GROUPS TOO??? 39 | res.json({ 40 | user: { 41 | id: foundUser.id, 42 | displayName: foundUser.displayName, 43 | avatarUrl: foundUser.avatarUrl 44 | } 45 | }); 46 | } else { 47 | res.status(404).json('user doesn\'t exist'); 48 | } 49 | }) 50 | .catch(function(error) { 51 | next(error); 52 | }); 53 | }; 54 | 55 | var login = function (req, res, next) { 56 | var email = req.body.email; 57 | var password = req.body.password; 58 | 59 | User.getUser({email: email}) 60 | .then(function (user) { 61 | this.user = user; 62 | if (!user) { 63 | res.status(404).json('User does not exist'); 64 | } else { 65 | return user.comparePassword(password); 66 | } 67 | }) 68 | .then(function (didMatch) { 69 | if (didMatch) { 70 | return User.compileUserData(this.user); 71 | } else { 72 | res.status(401).json('Incorrect password'); 73 | } 74 | }) 75 | .then(function(compiledUser) { 76 | var token = jwt.sign(this.user.toJSON(), JWT_SECRET, { expiresIn: 60 * 60 * 24 }); 77 | res.json({ 78 | token: token, 79 | user: compiledUser 80 | }); 81 | }) 82 | .catch(function (error) { 83 | next(error); 84 | }) 85 | .bind({}); 86 | }; 87 | 88 | var setAvatar = function(req, res, next) { 89 | var user = req.user; 90 | 91 | user.update({avatarUrl: req.filename}) 92 | .then(function(user) { 93 | this.user = user; 94 | return User.compileUserData(user); 95 | }) 96 | .then(function(compiledUser) { 97 | var token = jwt.sign(this.user.toJSON(), JWT_SECRET, { expiresIn: 60 * 60 * 24 }); 98 | res.json({ 99 | user: compiledUser, 100 | token: token 101 | }); 102 | }) 103 | .catch(function(error) { 104 | next(error); 105 | }) 106 | .bind({}); 107 | }; 108 | 109 | var signup = function (req, res, next) { 110 | var displayName = req.body.displayName; 111 | var email = req.body.email; 112 | var password = req.body.password; 113 | 114 | User.getUser({email: email}) 115 | .then(function(user) { 116 | if (user) { 117 | res.status(400).json('User already exists'); 118 | } else { 119 | return User.createUser(email, displayName, password); 120 | } 121 | }) 122 | .then(function (user) { 123 | this.user = user; 124 | return User.compileUserData(user); 125 | }) 126 | .then(function (compiledUser) { 127 | var token = jwt.sign(this.user.toJSON(), JWT_SECRET, { expiresIn: 60 * 60 * 24 }); 128 | res.json({ 129 | token: token, 130 | user: compiledUser 131 | }); 132 | }) 133 | .catch(function (error) { 134 | next(error); 135 | }) 136 | .bind({}); 137 | }; 138 | 139 | 140 | var updateProfile = function(req, res, next) { 141 | var user = req.user; 142 | user.update(req.body) 143 | .then(function(user) { 144 | this.user = user; 145 | return User.compileUserData(user); 146 | }) 147 | .then(function(compiledUser) { 148 | var token = jwt.sign(this.user.toJSON(), JWT_SECRET, { expiresIn: 60 * 60 * 24 }); 149 | res.json({ 150 | user: compiledUser, 151 | token: token 152 | }); 153 | }) 154 | .catch(function(error) { 155 | next(error); 156 | }) 157 | .bind({}); 158 | }; 159 | 160 | 161 | module.exports = { 162 | getUser: getUser, 163 | getGroups: getGroups, 164 | getProfile: getProfile, 165 | login: login, 166 | setAvatar: setAvatar, 167 | signup: signup, 168 | updateProfile: updateProfile 169 | }; 170 | -------------------------------------------------------------------------------- /server/controllers/groupController.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var UserModel = require('../models/userModel'); 3 | var Group = require('../models/groupModel'); 4 | 5 | var addUser = function(req, res, next) { 6 | // roles: 7 | // admin, member, pending 8 | var groupId = req.params.id; 9 | var userId = req.body.userId; 10 | var role = req.body.role; 11 | 12 | Group.addUser(groupId, userId, role) 13 | .then(function(user) { 14 | res.json(user); 15 | }) 16 | .catch(function(error) { 17 | next(error); 18 | }); 19 | }; 20 | 21 | var createGroup = function(req, res, next) { 22 | var name = req.body.name; 23 | 24 | Group.createGroup(name) 25 | .then(function(group) { 26 | res.json(group); 27 | }) 28 | .catch(function(error) { 29 | next(error); 30 | }); 31 | }; 32 | 33 | var getUsers = function(req, res, next) { 34 | // roles: 35 | // admin, member, pending 36 | var groupId = req.params.id; 37 | 38 | Group.getUsers(groupId) 39 | .then(function(users) { 40 | res.json(users); 41 | }) 42 | .catch(function(err) { 43 | next(err); 44 | }); 45 | }; 46 | 47 | var getPlaylists = function(req, res, next) { 48 | var groupId = req.params.id; 49 | 50 | Group.getGroup(groupId) 51 | .then(function(group) { 52 | return group.getPlaylists(); 53 | }) 54 | .then(function(playlists) { 55 | res.json(playlists); 56 | }) 57 | .catch(function(err) { 58 | next(err); 59 | }); 60 | }; 61 | 62 | var getSongs = function(req, res, next) { 63 | var groupId = req.params.id; 64 | 65 | console.log('--- --- Get songs first: ', groupId); 66 | 67 | 68 | Group.getGroup(groupId) 69 | .then(function(group) { 70 | return group.getSongs(); 71 | }) 72 | .then(function(songs) { 73 | res.json(songs); 74 | }) 75 | .catch(function(err) { 76 | next(err); 77 | }); 78 | }; 79 | 80 | var removeUser = function(req, res, next) { 81 | var groupId = req.params.gid; 82 | var userId = req.params.uid; 83 | 84 | Group.removeUser(groupId, userId) 85 | .then(function(response) { 86 | res.json(response); 87 | }) 88 | .catch(function(error) { 89 | next(error); 90 | }); 91 | }; 92 | 93 | var deleteGroup = function(req, res, next) { 94 | var groupId = req.params.id; 95 | 96 | Group.deleteGroup(groupId) 97 | .then(function(response) { 98 | res.json(response); 99 | }) 100 | .catch(function(error) { 101 | next(error); 102 | }); 103 | }; 104 | 105 | // Redirects to either send email invite or add user function 106 | var sendInvite = function(req, res, next) { 107 | var email = req.body.email; 108 | var groupId = req.params.id; 109 | 110 | UserModel.getUser({email: email}) 111 | .then(function(user) { 112 | if (user) { 113 | return Group.getUserGroup(user.id, groupId) 114 | .then(function(userGroup) { 115 | if (userGroup) { 116 | res.status(400).json('User is already a member'); 117 | } else { 118 | Group.addUser(groupId, user.id, 'pending') 119 | .then(function(user) { 120 | res.json(user); 121 | }); 122 | } 123 | }); 124 | } else { 125 | return Group.getGroup(groupId) 126 | .then(function(group) { 127 | return Group.sendEmailInvite(group, email) 128 | .then(function(result) { 129 | res.json(result); 130 | }); 131 | }); 132 | } 133 | }) 134 | .catch(function(error) { 135 | res.json(error); 136 | }); 137 | }; 138 | 139 | var updateGroupInfo = function(req, res, next) { 140 | var groupId = req.body.id; 141 | var fields = req.body; 142 | 143 | Group.updateInfo(groupId, fields) 144 | .then(function(result) { 145 | res.json(result[1][0]); 146 | }) 147 | .catch(function(error) { 148 | next(error); 149 | }); 150 | }; 151 | 152 | var updateUserRole = function(req, res, next) { 153 | var groupId = req.params.gid; 154 | var userId = req.params.uid; 155 | var role = req.body.role; 156 | 157 | Group.updateUserRole(groupId, userId, role) 158 | .then(function(response) { 159 | res.json(response); 160 | }) 161 | .catch(function(error) { 162 | next(error); 163 | }); 164 | }; 165 | 166 | module.exports = { 167 | addUser: addUser, 168 | createGroup: createGroup, 169 | getUsers: getUsers, 170 | getPlaylists: getPlaylists, 171 | getSongs: getSongs, 172 | removeUser: removeUser, 173 | deleteGroup: deleteGroup, 174 | sendInvite: sendInvite, 175 | updateGroupInfo: updateGroupInfo, 176 | updateUserRole: updateUserRole 177 | }; 178 | -------------------------------------------------------------------------------- /client/app/groups/groups.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 | 7 | 8 |
9 |
10 | New Group: 11 | 15 | 20 | 21 |
22 | {{invitees[invitees.length - 1]}} will get an invite! 23 |
24 |
25 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | {{clickedMember.displayName}} 33 | 34 |
{{clickedMember.email}}
35 | 36 |
{{clickedMember.userGroups.role}}
37 | 38 |
39 | 43 | 47 | 48 | 49 |
50 |
51 |
52 |
53 |
54 | 55 | 56 |
57 |
58 | Request: {{ group.name }} 59 | 60 | 61 | 62 | 63 |
64 |
65 | 66 | 67 | 68 | 69 |
70 |
71 | {{user.currentGroup.name}} 72 |
73 | 74 | 75 | 76 |
77 |
78 |
79 | 80 |
81 |
{{ member.displayName }}
82 |
83 |
84 |
85 | 86 | 87 |
88 | 89 | 90 | 91 |
92 | 93 | 94 | 95 | 96 |
97 |
98 |
{{ group.name }}
99 | 100 |
101 |
102 | 103 |
104 |
105 | 106 |
107 |
-------------------------------------------------------------------------------- /client/app/info/info.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

AudioPile Information

5 | 6 |

Files uploaded to AudioPile undergo a multi-step process which allows them to be easily shared and listened to by groups or individuals.

7 | 8 |

When a file is uploaded the source is stored inside Amazon’s S3 file storage system. This source file remains untouched and can be downloaded in it’s original format at any time. After upload is complete the second step begins. Zencoder, a third party transcoding service downloads the source file from S3 and creates it’s own copy. This copy is then volume normalized (gain raised to maximum possible without clipping) and transcoded to 256kbps MP3. This compressed version is then uploaded back to S3 and stored alongside the original.

9 | 10 |

When listen to your audio on AudioPile you are hearing the volume normalized, MP3 version of the file you originally uploaded. This allows streaming on mobile devices with a capable internet connection and compatibility with most browsers.

11 | 12 |

I hope you get some use out of this tool! Please read the FAQ below and if you still have any questions, suggestions or criticisms please send them my way!

13 | 14 |

-Brian Fogg

15 | 25 | 26 | 27 |

What is this for?

28 |

AudioPile is a web utility for easily uploading and delivering audio files.

29 | 30 | 31 |

What can I upload?

32 |

Audio is transcoded through Zencoder. They claim to be able to process just about any audio format but we have not tested this extensively. Wav and mp3's definitely work.

33 | 34 | 35 |

How much does this cost?

36 |

Lucky you, it's free right now. This project has very few users and I'm personally footing the bill for now. Financials will eventually be published.

37 | 38 | 39 |

I can't change my password! Where is the button for that?

40 |

It's on the todo list.

41 | 42 | 43 |

How many people are using this service?

44 |

As of this moment, {{userCount}} users have uploaded {{songCount}} pieces of audio!

45 | 46 | 47 |

Who's building all this?

48 | 49 | 75 | 76 | 77 |

TodoList

78 |
    79 |
  • Download both original and compressed
  • 80 |
  • Usage stats in this FAQ
  • 81 |
  • Utility based monetization with extreme transparency. We have to charge eventually.
  • 82 |
  • 83 |
  • 84 |
85 |
86 | 89 |
-------------------------------------------------------------------------------- /client/app/player/player.js: -------------------------------------------------------------------------------- 1 | angular.module('jam.player', []) 2 | .controller('PlayerController', ['$scope', '$rootScope', '$timeout', 'Songs', function($scope, $rootScope, timeout, Songs) { 3 | 4 | $scope.isTouchDevice = 'ontouchstart' in document.documentElement; 5 | $scope.audio = Songs.getPlayer(); 6 | $scope.song = Songs.getCurrentSong(); 7 | $scope.playlist = Songs.getSoundsAndIndex(); 8 | $scope.muted = Songs.getMuted(); 9 | $scope.timeFormat = '00:00'; 10 | $scope.playable = Songs.getPlayable(); 11 | $scope.timeSliderDisabled = !$scope.playable || $scope.isTouchDevice; 12 | 13 | // $scope.modelTime = 0; 14 | 15 | $scope.snapVol = function() { 16 | if ($scope.audio.volume < 0.05) { 17 | $scope.mute(); 18 | } 19 | }; 20 | 21 | $scope.speeds = [0.5, 0.8, 1.0, 1.5, 2.0]; 22 | 23 | setInterval(function() { $scope.$apply(); }, 200); 24 | 25 | // $scope.audio.addEventListener('timeupdate', function(data) { 26 | 27 | // // console.log('TIME UPDATE', data.path[0].currentTime); 28 | // console.log('TIME UPDATE', $scope.audio); 29 | // var time = data.path[0].currentTime; 30 | 31 | // $scope.modelTime = time; 32 | // // $scope.$apply(); 33 | // // $rootScope.$broadcast('audio.currentTime', this); 34 | // }); 35 | 36 | $scope.showSpeed = false; 37 | 38 | $scope.$watch(function(scope) { 39 | return scope.audio.volume; 40 | }, function(newV, oldV) { 41 | if (newV) { 42 | if ($scope.muted) { 43 | Songs.setMuted(false); 44 | $scope.muted = false; 45 | } 46 | } 47 | }); 48 | 49 | // $scope.$watch(function(scope) { 50 | // return scope.audio.currentTime; 51 | // }, function(newV, oldV) { 52 | // $scope.timeFormat = Songs.timeFormat(newV); 53 | // }); 54 | 55 | $scope.stop = function () { 56 | $scope.audio.pause(); 57 | $scope.audio.currentTime = 0; 58 | 59 | }; 60 | 61 | $scope.mute = function () { 62 | if ($scope.muted) { 63 | $scope.audio.volume = Songs.getVolume(); 64 | } else { 65 | $scope.audio.volume = 0; 66 | } 67 | $scope.muted = !$scope.muted; 68 | Songs.setMuted($scope.muted); 69 | }; 70 | 71 | $scope.changeTrack = function (direction) { 72 | var currentSongIndex = Songs.getSongIndex(); 73 | if (direction === 'back') { 74 | if (currentSongIndex === 0) { 75 | $scope.stop(); 76 | $scope.audio.play(); 77 | } else { 78 | Songs.setSongIndex(currentSongIndex - 1); 79 | } 80 | } 81 | if (direction === 'forward') { 82 | if (currentSongIndex === $scope.playlist.songs.length - 1) { 83 | Songs.resetPlayer(); 84 | $scope.song = Songs.getCurrentSong(); 85 | } else { 86 | Songs.setSongIndex(currentSongIndex + 1); 87 | } 88 | } 89 | }; 90 | 91 | $scope.setSpeed = function (inc) { 92 | var currentIndex = $scope.speeds.indexOf($scope.audio.playbackRate); 93 | nextIndex = currentIndex + inc; 94 | if (nextIndex < 0) { 95 | nextIndex = 0; 96 | } 97 | if (nextIndex >= $scope.speeds.length) { 98 | nextIndex = $scope.speeds.length - 1; 99 | } 100 | $scope.audio.playbackRate = $scope.speeds[nextIndex]; 101 | }; 102 | 103 | $scope.togglePlay = function () { 104 | if ($scope.audio.paused) { 105 | $scope.audio.play(); 106 | } else { 107 | $scope.audio.pause(); 108 | } 109 | // TODO: use broadcastEvent function here 110 | // broadcastEvent('TOGGLE_PLAY'); 111 | $rootScope.$broadcast('audioPlayerEvent', 'TOGGLE_PLAY'); 112 | }; 113 | 114 | var changeSong = function() { 115 | $scope.playlist = Songs.getSoundsAndIndex(); 116 | $scope.song = $scope.playlist.songs[$scope.playlist.index]; 117 | $scope.audio.src = $scope.song.compressedAddress || null; 118 | $scope.audio.onended = Songs.nextIndex; 119 | $scope.audio.ondurationchange = function(e) { 120 | $scope.timeSliderDisabled = !$scope.audio.duration || $scope.isTouchDevice; 121 | Songs.setPlayable(!!$scope.audio.duration); 122 | $scope.playable = Songs.getPlayable(); 123 | }; 124 | $scope.audio.play(); 125 | }; 126 | 127 | var resetPlayer = function() { 128 | $scope.stop(); 129 | $scope.audio = Songs.getPlayer(); 130 | $scope.playlist = Songs.getSoundsAndIndex(); 131 | $scope.timeSliderDisabled = !$scope.audio.duration; 132 | }; 133 | 134 | var refreshList = function() { 135 | $scope.playlist = Songs.getSoundsAndIndex(); 136 | $scope.timeSliderDisabled = !$scope.audio.duration; 137 | }; 138 | 139 | // broadcast audio events to all views 140 | var broadcastEvent = function(event) { 141 | $rootScope.$broadcast('audioPlayerEvent', event); 142 | }; 143 | 144 | Songs.registerObserverCallback('CHANGE_SONG', changeSong); 145 | Songs.registerObserverCallback('TOGGLE_PLAY', $scope.togglePlay); 146 | Songs.registerObserverCallback('RESET_PLAYER', resetPlayer); 147 | Songs.registerObserverCallback('REFRESH_LIST', refreshList); 148 | Songs.registerObserverCallback('ANY_AUDIO_EVENT', broadcastEvent); 149 | }]); 150 | -------------------------------------------------------------------------------- /client/app/groups/groups.js: -------------------------------------------------------------------------------- 1 | angular.module('jam.groups', []) 2 | 3 | .controller('GroupsController', ['$scope', '$route', 'Users', 'Groups', 'Songs', function($scope, $route, Users, Groups, Songs) { 4 | $scope.user = {}; 5 | $scope.newGroup = {}; 6 | $scope.data = {}; 7 | $scope.chooseRole = { 8 | role: 'admin' 9 | }; 10 | $scope.playable = Songs.getPlayable(); 11 | $scope.invitees = []; 12 | 13 | $scope.toggleCreateModal = function () { 14 | $scope.createModalShown = !$scope.createModalShown; 15 | }; 16 | 17 | $scope.memberInfo = function (member, index) { 18 | $scope.clickedMember = member; 19 | $scope.clickedMember.isAdmin = member.userGroups.role === 'admin' ? true : false; 20 | $scope.clickedMember.index = index; 21 | $scope.chooseRole.role = member.userGroups.role; 22 | $scope.memberModalShown = true; 23 | }; 24 | 25 | $scope.updateRole = function (userId) { 26 | if ($scope.chooseRole.role !== $scope.clickedMember.role) { 27 | Groups.updateUserRole($scope.user.currentGroupId, userId, $scope.chooseRole.role) 28 | .then(function () { 29 | $scope.data.members[$scope.clickedMember.index].userGroups.role = $scope.chooseRole.role; 30 | $scope.memberModalShown = false; 31 | }) 32 | .catch(console.error); 33 | } 34 | }; 35 | 36 | $scope.removeMember = function (userId) { 37 | Groups.removeUser($scope.user.currentGroupId, userId) 38 | .then(function () { 39 | // tell the user that the member is no more! 40 | $scope.data.members.splice($scope.clickedMember.index, 1); 41 | $scope.memberModalShown = false; 42 | }) 43 | .catch(console.error); 44 | }; 45 | 46 | $scope.acceptInvite = function (group, index) { 47 | Groups.updateUserRole(group.id, $scope.user.id, 'member') 48 | .then(function() { 49 | group.userGroups.role = 'member'; 50 | _.each(group.users, function(user) { 51 | if ($scope.user.id === user.id) { 52 | user.userGroups.role = "member"; 53 | } 54 | }); 55 | $scope.data.groups.push(group); 56 | $scope.data.pendingGroups.splice(index, 1); 57 | }) 58 | .catch(console.error); 59 | }; 60 | 61 | $scope.rejectInvite = function (group, index) { 62 | Groups.removeUser(group.id, $scope.user.id) 63 | .then(function (data) { 64 | $scope.data.pendingGroups.splice(index, 1); 65 | }) 66 | .catch(console.error); 67 | }; 68 | 69 | $scope.createGroup = function () { 70 | Groups.createGroup($scope.newGroup) 71 | .then(function (group) { 72 | Groups.addUser(group.id, $scope.user.id, 'admin') 73 | .then(function (user) { 74 | $scope.createModalShown = false; 75 | $scope.refreshGroups(user, true); 76 | $scope.sendInvites(group); 77 | }); 78 | }); 79 | }; 80 | 81 | $scope.sendInvites = function (group) { 82 | _.each($scope.invitees, function (invitee) { 83 | Groups.sendInvite(group, invitee) 84 | .then(function (res) { 85 | console.log(invitee + ' invited! '); 86 | }) 87 | .catch(console.error); 88 | }); 89 | }; 90 | 91 | $scope.setCurrentGroup = function(group) { 92 | Users.updateProfile({currentGroupId: group.id}) 93 | .then(function (res) { 94 | $scope.user = res.data.user; 95 | $scope.user.currentGroup = group; 96 | $scope.user.isAdmin = $scope.user.currentGroup.userGroups.role === 'admin' ? true : false; 97 | $scope.data.members = $scope.user.currentGroup.users; 98 | Songs.resetPlayer(); 99 | }) 100 | .catch(function (error) { 101 | console.error(error); 102 | }); 103 | }; 104 | 105 | $scope.updateProfile = function () { 106 | return Users.updateProfile($scope.user) 107 | .then(function (res) { 108 | // $scope.user = res.data.user; 109 | }) 110 | .catch(function (error) { 111 | console.error(error); 112 | }); 113 | }; 114 | 115 | $scope.isNotCurrentGroup = function (group) { 116 | return group.id !== $scope.user.currentGroup.id; 117 | }; 118 | 119 | $scope.refreshGroups = function (userId, force) { 120 | Groups.getGroupsData(userId, force) 121 | .then(function (groups) { 122 | $scope.data.groups = []; 123 | $scope.data.pendingGroups = []; 124 | _.each(groups, function (group) { 125 | if (group.id === $scope.user.currentGroupId) { 126 | $scope.user.currentGroup = group; 127 | $scope.user.isAdmin = $scope.user.currentGroup.userGroups.role === 'admin' ? true : false; 128 | $scope.data.members = $scope.user.currentGroup.users; 129 | } 130 | if (group.userGroups.role === 'pending') { 131 | $scope.data.pendingGroups.push(group); 132 | } else { 133 | $scope.data.groups.push(group); 134 | } 135 | }); 136 | }); 137 | }; 138 | 139 | $scope.deleteGroup = function(id) { 140 | Groups.deleteGroup(id) 141 | .then(function(resp) { 142 | console.log(resp); 143 | }) 144 | .catch(console.error); 145 | }; 146 | 147 | // Load groups and group users 148 | Users.getUserData() 149 | .then(function (userData) { 150 | $scope.user = userData; 151 | $scope.refreshGroups(userData); 152 | }) 153 | .catch(console.error); 154 | }]); -------------------------------------------------------------------------------- /server/controllers/songController.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var SongModel = require('../models/songModel'); 4 | var awsConfig = require('../config/aws.config.js'); 5 | var Promise = require('bluebird'); 6 | 7 | var AWS = require('aws-sdk'); 8 | AWS.config.update({ 9 | 'accessKeyId': awsConfig.accessKeyId, 10 | 'secretAccessKey': awsConfig.secretAccessKey, 11 | 'region': awsConfig.region 12 | }); 13 | var s3 = new AWS.S3(); 14 | var bucketAddress = s3.endpoint.protocol + '//' + awsConfig.bucket + '.' + s3.endpoint.hostname + s3.endpoint.pathname; 15 | 16 | // var addCompressedLink = function (req, res, next) { 17 | // console.log('add compressed link'); 18 | 19 | // var songID = req.body.songID; 20 | // var compressedID = req.body.compressedID; 21 | // var amplitudeData = req.body.amplitudeData; 22 | 23 | // SongModel.addCompressedLink(songID, compressedID, amplitudeData) 24 | // .then(function(data) { 25 | // console.log('Did it!'); 26 | // res.sendStatus(200); 27 | // }) 28 | // .catch(function(err) { 29 | // console.log('Fail!'); 30 | // res.sendStatus(500); 31 | // // TODO: figure out the correct code here! 32 | // }); 33 | 34 | // }; 35 | 36 | var addSong = function (req, res, next) { 37 | 38 | 39 | var dbSongEntry = {}; 40 | dbSongEntry.title = req.body.name || ''; 41 | dbSongEntry.description = req.body.description || ''; 42 | dbSongEntry.dateRecorded = req.body.lastModified || null; 43 | dbSongEntry.dateUploaded = Date.now(); //TODO: make a db entry for this data 44 | dbSongEntry.groupId = req.params.id; 45 | dbSongEntry.size = req.body.size; 46 | dbSongEntry.address = req.body.address; 47 | dbSongEntry.uniqueHash = req.body.uniqueHash; 48 | dbSongEntry.duration = req.body.duration || 300; 49 | 50 | // initialize dbsong 51 | var dbSong; 52 | 53 | SongModel.addSong(dbSongEntry) 54 | .then(function(songDbReturn) { 55 | dbSong = songDbReturn; 56 | 57 | // tell client song is added to database 58 | res.json(dbSong); 59 | // carry on with compression... 60 | 61 | return SongModel.requestFileCompression(songDbReturn); 62 | }) 63 | .then(function(zencoderBodyResponse) { 64 | var compressedID = zencoderBodyResponse.outputs[0].url; 65 | var songID = dbSong.id; 66 | var dummyAmplitudeData = { 67 | 'max': [.5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5, .5], 68 | 'duration': 10 69 | }; 70 | SongModel.addCompressedLink(songID, compressedID, dummyAmplitudeData); 71 | }) 72 | .catch(function(err) { 73 | next(err); 74 | }); 75 | }; 76 | 77 | var s3delete = function (song) { 78 | // delete both original and compressed files from aws 79 | song.address = song.address || 'dummy'; 80 | song.compressedAddress = song.compressedAddress || 'dummy'; 81 | 82 | 83 | var source = song.address.replace(bucketAddress, ''); 84 | var mini = song.compressedAddress.replace(bucketAddress, ''); 85 | 86 | // console.log(' ------------ Songs to delete: ---------- '); 87 | // console.log('source: ', source); 88 | // console.log('mini: ', mini); 89 | // console.log('song:', song); 90 | 91 | var params = { 92 | Bucket: awsConfig.bucket, /* required */ 93 | Delete: { /* required */ 94 | Objects: [ /* required */ 95 | { 96 | Key: source, /* required */ 97 | }, 98 | { 99 | Key: mini, /* required */ 100 | } 101 | ], 102 | }, 103 | }; 104 | return new Promise(function (resolve, reject) { 105 | 106 | // console.log('--- --- Delete Songs: ', params); 107 | 108 | s3.deleteObjects(params, function(err, res) { 109 | if (err) { 110 | reject(err); 111 | } else { 112 | resolve(res); 113 | } 114 | }); 115 | }); 116 | }; 117 | 118 | var deleteSong = function (req, res, next) { 119 | var songId = req.params.id; 120 | 121 | SongModel.getSong(songId) 122 | .then(function(song) { 123 | this.song = song; 124 | return s3delete(song); 125 | }) 126 | .then(function(s3response) { 127 | // console.log('s3 delete response is ' + JSON.stringify(s3response)); 128 | return this.song.destroy(); 129 | }) 130 | .then(function() { 131 | res.json(this.song); 132 | }) 133 | .catch(function(err) { 134 | next(err); 135 | }) 136 | .bind({}); 137 | }; 138 | 139 | var getComments = function (req, res, next) { 140 | var songId = req.params.id; 141 | SongModel.getComments(songId) 142 | .then(function (comments) { 143 | res.json(comments); 144 | }) 145 | .catch(function (error) { 146 | res.status(500).json('error retreiving comments'); 147 | }); 148 | }; 149 | 150 | var getSong = function (req, res, next) { 151 | var songId = req.params.id; 152 | SongModel.getSong(songId) 153 | .then(function (song) { 154 | res.json(song); 155 | }) 156 | .catch(function (error) { 157 | res.status(500).json('error retreiving song'); 158 | }); 159 | }; 160 | 161 | var updateSong = function (req, res, next) { 162 | SongModel.updateSong(req.body) 163 | .then(function (array) { 164 | if (array[0] > 0) { 165 | res.json(array[1][0]); 166 | } else { 167 | res.status(404).json('no songs updated'); 168 | } 169 | }) 170 | .catch(function (error) { 171 | res.status(500).json('error retreiving song'); 172 | }); 173 | }; 174 | 175 | module.exports = { 176 | // addCompressedLink: addCompressedLink, 177 | addSong: addSong, 178 | deleteSong: deleteSong, 179 | getComments: getComments, 180 | getSong: getSong, 181 | updateSong: updateSong 182 | }; 183 | -------------------------------------------------------------------------------- /server/db/database.js: -------------------------------------------------------------------------------- 1 | var config = require('../config/config.js'); 2 | var pg = require('pg'); 3 | var Sequelize = require('sequelize'); 4 | var bcrypt = require('bcrypt-nodejs'); 5 | var Promise = require('bluebird'); 6 | 7 | 8 | var sqldebug = process.env.SQL_DEBUG || false; 9 | var db = new Sequelize(config.connectionString, {logging: sqldebug}); 10 | 11 | // Define table schemas 12 | var User = db.define('user', { 13 | email: { 14 | type: Sequelize.STRING, 15 | unique: true, 16 | allowNull: false, 17 | validate: {isEmail: {msg: 'Invalid email'}} 18 | }, 19 | displayName: { 20 | type: Sequelize.STRING, 21 | allowNull: false 22 | }, 23 | password: { 24 | type: Sequelize.STRING, 25 | allowNull: false 26 | }, 27 | avatarURL: { 28 | type: Sequelize.STRING, 29 | defaultValue: 'http://www.murketing.com/journal/wp-content/uploads/2009/04/vimeo.jpg' // Change to random default on signup 30 | }, 31 | currentGroupId: { 32 | type: Sequelize.INTEGER, 33 | allowNull: false 34 | } 35 | }, { 36 | classMethods: { 37 | hashPassword: function(password) { 38 | var hashAsync = Promise.promisify(bcrypt.hash); 39 | return hashAsync(password, null, null).bind(this); 40 | } 41 | }, 42 | instanceMethods: { 43 | comparePassword: function(attemptedPassword) { 44 | var compareAsync = Promise.promisify(bcrypt.compare); 45 | return compareAsync(attemptedPassword, this.password); 46 | } 47 | }, 48 | hooks: { 49 | beforeCreate: function(user, options) { 50 | return this.hashPassword(user.password) 51 | .then(function(hash) { 52 | user.password = hash; 53 | }); 54 | }, 55 | } 56 | }); 57 | 58 | var Group = db.define('group', { 59 | name: { 60 | type: Sequelize.STRING, 61 | allowNull: false 62 | }, 63 | bannerUrl: { 64 | type: Sequelize.STRING, 65 | defaultValue: '' // Update 66 | } 67 | }); 68 | 69 | var Song = db.define('song', { 70 | title: { 71 | type: Sequelize.STRING, 72 | allowNull: false 73 | }, 74 | description: { 75 | type: Sequelize.STRING, 76 | allowNull: false 77 | }, 78 | dateRecorded: { 79 | type: Sequelize.DATE, 80 | allowNull: true 81 | }, 82 | dateUploaded: { 83 | type: Sequelize.DATE, 84 | allowNull: true 85 | }, 86 | size: { 87 | type: Sequelize.INTEGER, 88 | allowNull: false 89 | }, 90 | duration: { 91 | type: Sequelize.INTEGER, 92 | allowNull: false 93 | }, 94 | uniqueHash: { 95 | type: Sequelize.STRING, 96 | allowNull: false 97 | }, 98 | address: { 99 | type: Sequelize.STRING, 100 | allowNull: false, 101 | defaultValue: 'http://www.stephaniequinn.com/Music/Canon.mp3' 102 | }, 103 | compressedAddress: { 104 | type: Sequelize.STRING, 105 | allowNull: true 106 | }, 107 | imageUrl: { 108 | type: Sequelize.STRING, 109 | defaultValue: 'http://lorempixel.com/200/200/' // Update 110 | }, 111 | amplitudeData: { 112 | type: Sequelize.JSON, 113 | allowNull: true 114 | } 115 | }); 116 | 117 | var Playlist = db.define('playlist', { 118 | title: { 119 | type: Sequelize.STRING, 120 | allowNull: false 121 | }, 122 | description: { 123 | type: Sequelize.STRING, 124 | } 125 | }); 126 | 127 | var Comment = db.define('comment', { 128 | time: { 129 | type: Sequelize.INTEGER 130 | }, 131 | note: { 132 | type: Sequelize.STRING 133 | } 134 | }); 135 | 136 | // Define join tables 137 | var UserGroups = db.define('userGroups', { 138 | role: { 139 | type: Sequelize.STRING, 140 | }}, 141 | {timestamps: false} 142 | ); 143 | 144 | var PlaylistSongs = db.define('playlistSongs', { 145 | listPosition: { 146 | type: Sequelize.INTEGER, 147 | }} 148 | ); 149 | 150 | // Define associations 151 | Group.belongsToMany(User, {through: 'userGroups'}); 152 | User.belongsToMany(Group, {through: 'userGroups'}); 153 | 154 | User.belongsToMany(Group, { 155 | through: { 156 | model: UserGroups, 157 | scope: { 158 | role: 'admin' 159 | } 160 | }, 161 | as: 'adminGroups' 162 | }); 163 | 164 | User.belongsToMany(Group, { 165 | through: { 166 | model: UserGroups, 167 | scope: { 168 | role: 'member' 169 | } 170 | }, 171 | as: 'memberGroups' 172 | }); 173 | 174 | User.belongsToMany(Group, { 175 | through: { 176 | model: UserGroups, 177 | scope: { 178 | role: 'pending' 179 | } 180 | }, 181 | as: 'pendingGroups' 182 | }); 183 | 184 | Group.belongsToMany(User, { 185 | through: { 186 | model: UserGroups, 187 | scope: { 188 | role: 'admin' 189 | } 190 | }, 191 | as: 'adminUsers' 192 | }); 193 | 194 | Group.belongsToMany(User, { 195 | through: { 196 | model: UserGroups, 197 | scope: { 198 | role: 'member' 199 | } 200 | }, 201 | as: 'memberUsers' 202 | }); 203 | 204 | Group.belongsToMany(User, { 205 | through: { 206 | model: UserGroups, 207 | scope: { 208 | role: 'pending' 209 | } 210 | }, 211 | as: 'pendingUsers' 212 | }); 213 | 214 | Group.hasMany(Song, {onDelete: 'cascade'}); 215 | Song.belongsTo(Group); 216 | 217 | Group.hasMany(Playlist, {onDelete: 'cascade'}); 218 | Playlist.belongsTo(Group); 219 | 220 | Playlist.belongsToMany(Song, {through: 'playlistSongs'}); 221 | Song.belongsToMany(Playlist, {through: 'playlistSongs'}); 222 | 223 | User.hasMany(Comment, {onDelete: 'cascade'}); 224 | Comment.belongsTo(User); 225 | 226 | Song.hasMany(Comment, {onDelete: 'cascade'}); 227 | Comment.belongsTo(Song); 228 | 229 | 230 | var logSync = false; //(process.env.NODE_ENV === 'test') ? false : console.log; 231 | 232 | // Sync models to define postgres tables and capture associations 233 | User.sync() 234 | .then(function() { 235 | return Group.sync(); 236 | }) 237 | .then(function() { 238 | return Playlist.sync(); 239 | }) 240 | .then(function() { 241 | return Song.sync(); 242 | }) 243 | .then(function() { 244 | return UserGroups.sync(); 245 | }) 246 | .then(function() { 247 | return PlaylistSongs.sync(); 248 | }) 249 | .then(function() { 250 | return Comment.sync(); 251 | }); 252 | 253 | module.exports = { 254 | db: db, 255 | User: User, 256 | Group: Group, 257 | UserGroups: UserGroups, 258 | Song: Song, 259 | Playlist: Playlist, 260 | PlaylistSongs: PlaylistSongs, 261 | Comment: Comment 262 | }; 263 | 264 | // Command to drop all tables in postgres 265 | // drop table users cascade; drop table groups cascade; drop table users; drop table groups; drop table "userGroups"; drop table playlists; drop table songs cascade; drop table "playlistSongs"; drop table comments; drop table playlists; 266 | -------------------------------------------------------------------------------- /compression_server/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | echo "/usr/local/lib" > /etc/ld.so.conf.d/libc.conf 6 | 7 | export MAKEFLAGS="-j$[$(nproc) + 1]" 8 | export SRC=/usr/local 9 | export PKG_CONFIG_PATH=${SRC}/lib/pkgconfig 10 | 11 | yum install -y autoconf automake gcc gcc-c++ git libtool make nasm zlib-devel \ 12 | openssl-devel tar cmake perl which bzip2 13 | 14 | # yasm 15 | DIR=$(mktemp -d) && cd ${DIR} && \ 16 | curl -s http://www.tortall.net/projects/yasm/releases/yasm-${YASM_VERSION}.tar.gz | \ 17 | tar zxvf - -C . && \ 18 | cd ${DIR}/yasm-${YASM_VERSION} && \ 19 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --docdir=${DIR} -mandir=${DIR}&& \ 20 | make && \ 21 | make install && \ 22 | make distclean && \ 23 | rm -rf ${DIR} 24 | 25 | # x264 TODO : pin version 26 | DIR=$(mktemp -d) && cd ${DIR} && \ 27 | git clone --depth 1 git://git.videolan.org/x264 && \ 28 | cd x264 && \ 29 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --enable-static && \ 30 | make && \ 31 | make install && \ 32 | make distclean && \ 33 | rm -rf ${DIR} 34 | 35 | # x265 36 | DIR=$(mktemp -d) && cd ${DIR} && \ 37 | curl -s https://bitbucket.org/multicoreware/x265/get/${X265_VERSION}.tar.gz | tar zxvf - -C . && \ 38 | cd multicoreware-*/source && \ 39 | cmake -G "Unix Makefiles" . && \ 40 | cmake . && \ 41 | make && \ 42 | make install && \ 43 | rm -rf ${DIR} 44 | 45 | # libogg 46 | DIR=$(mktemp -d) && cd ${DIR} && \ 47 | curl -s http://downloads.xiph.org/releases/ogg/libogg-${OGG_VERSION}.tar.gz | tar zxvf - -C . && \ 48 | cd libogg-${OGG_VERSION} && \ 49 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --disable-shared --docdir=/dev/null && \ 50 | make && \ 51 | make install && \ 52 | make distclean && \ 53 | rm -rf ${DIR} 54 | 55 | # libopus 56 | DIR=$(mktemp -d) && cd ${DIR} && \ 57 | curl -s http://downloads.xiph.org/releases/opus/opus-${OPUS_VERSION}.tar.gz | tar zxvf - -C . && \ 58 | cd opus-${OPUS_VERSION} && \ 59 | autoreconf -fiv && \ 60 | ./configure --prefix="${SRC}" --disable-shared && \ 61 | make && \ 62 | make install && \ 63 | make distclean && \ 64 | rm -rf ${DIR} 65 | 66 | # libvorbis 67 | DIR=$(mktemp -d) && cd ${DIR} && \ 68 | curl -s http://downloads.xiph.org/releases/vorbis/libvorbis-${VORBIS_VERSION}.tar.gz | tar zxvf - -C . && \ 69 | cd libvorbis-${VORBIS_VERSION} && \ 70 | ./configure --prefix="${SRC}" --with-ogg="${SRC}" --bindir="${SRC}/bin" \ 71 | --disable-shared --datadir=${DIR} && \ 72 | make && \ 73 | make install && \ 74 | make distclean && \ 75 | rm -rf ${DIR} 76 | 77 | # libtheora 78 | DIR=$(mktemp -d) && cd ${DIR} && \ 79 | curl -s http://downloads.xiph.org/releases/theora/libtheora-${THEORA_VERSION}.tar.bz2 | tar jxvf - -C . && \ 80 | cd libtheora-${THEORA_VERSION} && \ 81 | ./configure --prefix="${SRC}" --with-ogg="${SRC}" --bindir="${SRC}/bin" \ 82 | --disable-shared --datadir=${DIR} && \ 83 | make && \ 84 | make install && \ 85 | make distclean && \ 86 | rm -rf ${DIR} 87 | 88 | # libvpx 89 | DIR=$(mktemp -d) && cd ${DIR} && \ 90 | curl -s https://codeload.github.com/webmproject/libvpx/tar.gz/v${VPX_VERSION} | tar zxvf - -C . && \ 91 | cd libvpx-${VPX_VERSION} && \ 92 | ./configure --prefix="${SRC}" --enable-vp8 --enable-vp9 --disable-examples && \ 93 | make && \ 94 | make install && \ 95 | make clean && \ 96 | rm -rf ${DIR} 97 | 98 | # libmp3lame 99 | DIR=$(mktemp -d) && cd ${DIR} && \ 100 | curl -L -s http://downloads.sourceforge.net/project/lame/lame/${LAME_VERSION%.*}/lame-${LAME_VERSION}.tar.gz | tar zxvf - -C . && \ 101 | cd lame-${LAME_VERSION} && \ 102 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --disable-shared --enable-nasm && \ 103 | make && \ 104 | make install && \ 105 | make distclean&& \ 106 | rm -rf ${DIR} 107 | 108 | # faac + http://stackoverflow.com/a/4320377 109 | DIR=$(mktemp -d) && cd ${DIR} && \ 110 | curl -L -s http://downloads.sourceforge.net/faac/faac-${FAAC_VERSION}.tar.gz | tar zxvf - -C . && \ 111 | cd faac-${FAAC_VERSION} && \ 112 | sed -i '126d' common/mp4v2/mpeg4ip.h && \ 113 | ./bootstrap && \ 114 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" && \ 115 | make && \ 116 | make install &&\ 117 | rm -rf ${DIR} 118 | 119 | # xvid 120 | DIR=$(mktemp -d) && cd ${DIR} && \ 121 | curl -L -s http://downloads.xvid.org/downloads/xvidcore-${XVID_VERSION}.tar.gz | tar zxvf - -C .&& \ 122 | cd xvidcore/build/generic && \ 123 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" && \ 124 | make && \ 125 | make install&& \ 126 | rm -rf ${DIR} 127 | 128 | # fdk-aac 129 | DIR=$(mktemp -d) && cd ${DIR} && \ 130 | curl -s https://codeload.github.com/mstorsjo/fdk-aac/tar.gz/v${FDKAAC_VERSION} | tar zxvf - -C . && \ 131 | cd fdk-aac-${FDKAAC_VERSION} && \ 132 | autoreconf -fiv && \ 133 | ./configure --prefix="${SRC}" --disable-shared && \ 134 | make && \ 135 | make install && \ 136 | make distclean && \ 137 | rm -rf ${DIR} 138 | 139 | # ffmpeg 140 | DIR=$(mktemp -d) && cd ${DIR} && \ 141 | curl -s http://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz | tar zxvf - -C . && \ 142 | cd ffmpeg-${FFMPEG_VERSION} && \ 143 | ./configure --prefix="${SRC}" --extra-cflags="-I${SRC}/include" \ 144 | --extra-ldflags="-L${SRC}/lib" --bindir="${SRC}/bin" \ 145 | --extra-libs=-ldl --enable-version3 --enable-libfaac --enable-libmp3lame \ 146 | --enable-libx264 --enable-libxvid --enable-gpl \ 147 | --enable-postproc --enable-nonfree --enable-avresample --enable-libfdk_aac \ 148 | --disable-debug --enable-small --enable-openssl --enable-libtheora \ 149 | --enable-libx265 --enable-libopus --enable-libvorbis --enable-libvpx && \ 150 | make && \ 151 | make install && \ 152 | make distclean && \ 153 | hash -r && \ 154 | cd tools && \ 155 | make qt-faststart && \ 156 | cp qt-faststart ${SRC}/bin && \ 157 | rm -rf ${DIR} 158 | 159 | # mplayer 160 | DIR=$(mktemp -d) && cd ${DIR} && \ 161 | curl -s http://mplayerhq.hu/MPlayer/releases/MPlayer-${MPLAYER_VERSION}.tar.gz | tar zxvf - -C . && \ 162 | cd MPlayer-${MPLAYER_VERSION} && \ 163 | ./configure --prefix="${SRC}" --extra-cflags="-I${SRC}/include" --extra-ldflags="-L${SRC}/lib" --bindir="${SRC}/bin" && \ 164 | make && \ 165 | make install && \ 166 | rm -rf ${DIR} 167 | 168 | yum history -y undo last && yum clean all && rm -rf /var/lib/yum/* 169 | 170 | # nodejs 171 | yum install make gcc gcc-c++ -y 172 | DIR=$(mktemp -d) && cd ${DIR} && \ 173 | curl -s http://nodejs.org/dist/v${NODEJS_VERSION}/node-v${NODEJS_VERSION}.tar.gz | tar zxvf - -C . && \ 174 | cd node-v* && \ 175 | ./configure && \ 176 | make && \ 177 | make install && \ 178 | rm -rf ${DIR} 179 | 180 | yum clean all 181 | rm -rf /var/lib/yum/yumdb/* 182 | echo "/usr/local/lib" > /etc/ld.so.conf.d/libc.conf -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## General Workflow 4 | 5 | 1. Fork the repo 6 | 1. Cut a namespaced feature branch from master 7 | - bug/... 8 | - feat/... 9 | - test/... 10 | - doc/... 11 | - refactor/... 12 | - setup/... 13 | 1. Make commits to your feature branch. Prefix each commit like so: 14 | - (feat) Added a new feature 15 | - (fix) Fixed inconsistent tests [Fixes #0] 16 | - (refactor) ... 17 | - (cleanup) ... 18 | - (test) ... 19 | - (doc) ... 20 | - (setup) .. 21 | 1. When you've finished with your fix or feature, Rebase upstream changes into your branch. submit a pull request 22 | directly to master. Include a description of your changes. 23 | 1. Your pull request will be reviewed by another maintainer. The point of code 24 | reviews is to help keep the codebase clean and of high quality and, equally 25 | as important, to help you grow as a programmer. If your code reviewer 26 | requests you make a change you don't understand, ask them why. 27 | 1. Fix any issues raised by your code reviwer, and push your fixes as a single 28 | new commit. 29 | 1. Once the pull request has been reviewed, it will be merged by another member of the team. Do not merge your own commits. 30 | 31 | ## Detailed Workflow 32 | 33 | ### Fork the repo 34 | 35 | Use github’s interface to make a fork of the repo, then add that repo as an upstream remote: 36 | 37 | ``` 38 | git remote add upstream https://github.com/BuoyantPyramid/buoyantpyramid.git 39 | ``` 40 | 41 | ### Cut a namespaced feature branch from master 42 | 43 | Your branch should follow this naming convention: 44 | - bug/... 45 | - feat/... 46 | - test/... 47 | - doc/... 48 | - refactor/... 49 | 50 | For example: 51 | 52 | ``` bash 53 | 54 | # Creates your branch and brings you there 55 | git checkout -b `feat/livesearch` 56 | ``` 57 | 58 | ### Make commits to your feature branch. 59 | 60 | Prefix each commit like so 61 | - (feat) Added a new feature 62 | - (fix) Fixed inconsistent tests [Fixes #0] 63 | - (refactor) ... 64 | - (cleanup) ... 65 | - (test) ... 66 | - (doc) ... 67 | 68 | Make changes and commits on your branch, and make sure that you 69 | only make changes that are relevant to this branch. If you find 70 | yourself making unrelated changes, make a new branch for those 71 | changes. 72 | 73 | #### Commit Message Guidelines 74 | 75 | - Commit messages should be written in the present tense; e.g. "Fix continuous 76 | integration script". 77 | - The first line of your commit message should be a brief summary of what the 78 | commit changes. Aim for about 70 characters max. Remember: This is a summary, 79 | not a detailed description of everything that changed. 80 | - If you want to explain the commit in more depth, following the first line should 81 | be a blank line and then a more detailed description of the commit. This can be 82 | as detailed as you want, so dig into details here and keep the first line short. 83 | 84 | ### Rebase upstream changes into your branch 85 | 86 | Once you are done making changes, you can begin the process of getting 87 | your code merged into the main repo. Step 1 is to rebase upstream 88 | changes to the master branch into yours by running this command 89 | from your branch: 90 | 91 | ```bash 92 | git pull --rebase upstream master 93 | ``` 94 | 95 | This will start the rebase process. You must commit all of your changes 96 | before doing this. If there are no conflicts, this should just roll all 97 | of your changes back on top of the changes from upstream, leading to a 98 | nice, clean, linear commit history. 99 | 100 | If there are conflicting changes, git will start yelling at you part way 101 | through the rebasing process. Git will pause rebasing to allow you to sort 102 | out the conflicts. You do this the same way you solve merge conflicts, 103 | by checking all of the files git says have been changed in both histories 104 | and picking the versions you want. Be aware that these changes will show 105 | up in your pull request, so try and incorporate upstream changes as much 106 | as possible. 107 | 108 | You pick a file by `git add`ing it - you do not make commits during a 109 | rebase. 110 | 111 | Once you are done fixing conflicts for a specific commit, run: 112 | 113 | ```bash 114 | git rebase --continue 115 | ``` 116 | 117 | This will continue the rebasing process. Once you are done fixing all 118 | conflicts you should run the existing tests to make sure you didn’t break 119 | anything, then run your new tests (there are new tests, right?) and 120 | make sure they work also. 121 | 122 | If rebasing broke anything, fix it, then repeat the above process until 123 | you get here again and nothing is broken and all the tests pass. 124 | 125 | ### Make a pull request 126 | 127 | Make a clear pull request from your fork and branch to the upstream master 128 | branch, detailing exactly what changes you made and what feature this 129 | should add. The clearer your pull request is the faster you can get 130 | your changes incorporated into this repo. 131 | 132 | At least one other person MUST give your changes a code review, and once 133 | they are satisfied they will merge your changes into upstream. Alternatively, 134 | they may have some requested changes. You should make more commits to your 135 | branch to fix these, then follow this process again from rebasing onwards. 136 | 137 | Once you get back here, make a comment requesting further review and 138 | someone will look at your code again. If they like it, it will get merged, 139 | else, just repeat again. 140 | 141 | Thanks for contributing! 142 | 143 | ### Guidelines 144 | 145 | 1. Uphold the current code standard: 146 | - Keep your code [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). 147 | - Apply the [boy scout rule](http://programmer.97things.oreilly.com/wiki/index.php/The_Boy_Scout_Rule). 148 | - Follow [STYLE-GUIDE.md](STYLE-GUIDE.md) 149 | 1. Run the tests before submitting a pull request. 150 | 1. Tests are very, very important. Submit tests if your pull request contains new, testable behavior. 151 | 152 | ## Checklist: 153 | 154 | This is just to help you organize your process 155 | 156 | - [ ] Did I cut my work branch off of master (don't cut new branches from existing feature brances)? 157 | - [ ] Did I follow the correct naming convention for my branch? 158 | - [ ] Is my branch focused on a single main change? 159 | - [ ] Do all of my changes directly relate to this change? 160 | - [ ] Did I rebase the upstream master branch after I finished all my 161 | work? 162 | - [ ] Did I write a clear pull request message detailing what changes I made? 163 | - [ ] Did I get a code review? 164 | - [ ] Did I make any requested changes from that code review? 165 | 166 | If you follow all of these guidelines and make good changes, you should have 167 | no problem getting your changes merged in. 168 | -------------------------------------------------------------------------------- /client/app/song/song.js: -------------------------------------------------------------------------------- 1 | angular.module('jam.song', []) 2 | 3 | .controller('SongController', ['$scope', '$location', '$route', 'Songs', 'Users', function ($scope, loc, $route, Songs, Users) { 4 | $scope.song = {}; 5 | $scope.audio = Songs.getPlayer(); 6 | $scope.commentTime = null; 7 | $scope.pinningComment = false; 8 | $scope.user = {}; 9 | $scope.comments = []; 10 | $scope.selectedComment = [{}]; 11 | $scope.songInPlayer = false; 12 | $scope.alreadyPlaying = false; 13 | $scope.playable = Songs.getPlayable(); 14 | $scope.fromUrl = '/#' + loc.search().from; 15 | 16 | var pageWidth = document.getElementsByClassName('page-content')[0].offsetWidth; 17 | var waveHeight = 100; 18 | var waveWidth = pageWidth * 0.9; 19 | var waveRadius = 2; 20 | var pinWidth = 12; 21 | var pinHeight = 20; 22 | var barPadding = 1; 23 | var initialDelay = 750; 24 | var scaledAmplitudes; 25 | 26 | $scope.width = waveWidth + 'px'; 27 | 28 | // Obtain song information and display comments 29 | Users.getUserData() 30 | .then(function (user) { 31 | $scope.user = user; 32 | }) 33 | .then(function() { 34 | return Songs.getSong(loc.path().split('/')[2]); 35 | }) 36 | .then(function (song) { 37 | $scope.song = song; 38 | var rawAmplitudes = song.amplitudeData.max; 39 | var max = _.max(rawAmplitudes); 40 | var scale = 100 / max; 41 | scaledAmplitudes = rawAmplitudes.map(function(amp) { 42 | // return amp * scale; 43 | 44 | // UNTIL WE ACTUALLY HAVE WAVFORMS: 45 | return Math.random() * 90 + 10; 46 | }); 47 | }) 48 | .then(function () { 49 | return Songs.getComments($scope.song.id); 50 | }) 51 | .then(function (comments) { 52 | $scope.comments = comments; 53 | renderComments(comments); 54 | $scope.songInPlayer = Songs.getCurrentSong() && $scope.song.id === Songs.getCurrentSong().id; 55 | if ($scope.songInPlayer && !$scope.audio.paused) { 56 | $scope.$broadcast('audioPlayerEvent', 'ALREADY_PLAYING'); 57 | } 58 | initialRender(); 59 | }); 60 | 61 | 62 | var createSvg = function (parent, height, width) { 63 | return d3.select(parent).append('svg').attr('height', height).attr('width', width); 64 | }; 65 | 66 | // D3 67 | var svg = createSvg('.waveform-container', waveHeight, waveWidth); 68 | 69 | var selectedComment = d3.select('body').selectAll('.selected-comment'); 70 | var comment = d3.select('body').selectAll('.comment-icon'); 71 | 72 | var commentPins = d3.select('body').selectAll('.pin-container') 73 | .style('height', pinHeight + 'px') 74 | .style('width', waveWidth + 'px'); 75 | 76 | var initialRender = function() { 77 | svg.attr('class', 'visualizer') 78 | .selectAll('rect') 79 | .data(scaledAmplitudes) 80 | .enter() 81 | .append('rect') 82 | .attr('rx', waveRadius + 'px') 83 | .attr('ry', waveRadius + 'px') 84 | .attr('x', function (d, i) { 85 | return i * (waveWidth / scaledAmplitudes.length); 86 | }) 87 | .attr('y', waveHeight) 88 | .attr('height', 0) 89 | .transition() 90 | .delay(function(d, i) { 91 | return initialDelay * i / scaledAmplitudes.length; 92 | }) 93 | .attr('width', waveWidth / scaledAmplitudes.length - barPadding) 94 | .attr('y', function(d) { 95 | return waveHeight - d; 96 | }) 97 | .attr('height', function(d) { 98 | return d; 99 | }) 100 | .attr('fill', function(d, i) { 101 | if ((i / scaledAmplitudes.length) < ($scope.audio.currentTime / $scope.song.duration) && $scope.songInPlayer) { 102 | return '#99D1B2'; 103 | } else { 104 | return '#999'; 105 | } 106 | }); 107 | 108 | d3.select('body').selectAll('.comment-pin-container') 109 | .style('height', pinHeight * 2 + 'px') 110 | .style('width', waveWidth + 'px'); 111 | 112 | d3.select('body').selectAll('.selected-comment-container') 113 | .style('height', pinHeight + 'px') 114 | .style('width', waveWidth + 'px'); 115 | 116 | _.delay(setInterval, initialDelay, renderFlow, 300); 117 | }; 118 | 119 | $scope.addComment = function (comment) { 120 | var time = Math.floor($scope.commentTime * $scope.song.duration); 121 | Songs.addComment({note: comment, time: time, userId: $scope.user.id}, $scope.song.id) 122 | .then(function (comment) { 123 | $scope.comments.push(comment); 124 | renderComments($scope.comments); 125 | $scope.pinningComment = false; 126 | $scope.comment = ''; 127 | }); 128 | }; 129 | 130 | $scope.commentSelected = function () { 131 | return !!Object.keys($scope.selectedComment[0]).length; 132 | }; 133 | 134 | $scope.formatTime = function (time) { 135 | return Songs.timeFormat(time); 136 | }; 137 | 138 | $scope.pinComment = function () { 139 | $scope.commentTime = $scope.audio.currentTime / $scope.song.duration; 140 | $scope.pinningComment = true; 141 | $scope.selectedComment = [{}]; 142 | }; 143 | 144 | var renderComments = function() { 145 | commentPins.selectAll('div') 146 | .data($scope.comments) 147 | .enter() 148 | .append('div') 149 | .style('left', function (d) { 150 | var left = Math.floor(d.time / $scope.song.duration * waveWidth) - pinWidth / 2; 151 | return left + 'px'; 152 | }) 153 | .attr('class', 'pin') 154 | .on('mouseover', function(d, i) { 155 | $scope.setSelectedComment(d); 156 | }); 157 | }; 158 | 159 | var renderSelectedComment = function() { 160 | var left = Math.floor($scope.selectedComment[0].time / $scope.song.duration * waveWidth); 161 | var onLeft = left < waveWidth / 2; 162 | selectedComment.data($scope.selectedComment) 163 | .style('left', function() { 164 | if (onLeft) { 165 | return left + 'px'; 166 | } else { 167 | return left - waveWidth / 2 + 'px'; 168 | } 169 | }) 170 | .classed('left', onLeft) 171 | .classed('right', !onLeft) 172 | .style('width', waveWidth / 2 + 'px') 173 | .text(function (d) { 174 | return d.note; 175 | }); 176 | }; 177 | 178 | var renderFlow = function () { 179 | svg.selectAll('rect') 180 | .data(scaledAmplitudes) 181 | .transition() 182 | .duration(600) 183 | .attr('fill', function(d, i) { 184 | if ((i / scaledAmplitudes.length) < ($scope.audio.currentTime / $scope.song.duration) && $scope.songInPlayer) { 185 | return '#99D1B2'; 186 | } else { 187 | return '#999'; 188 | } 189 | }); 190 | }; 191 | 192 | $scope.setSelectedComment = function(comment) { 193 | $scope.selectedComment = [comment]; 194 | renderSelectedComment(); 195 | }; 196 | 197 | $scope.setPlayTime = function (e) { 198 | if ($scope.songInPlayer) { 199 | var visualizer = document.getElementsByClassName('visualizer')[0]; 200 | var rect = visualizer.getBoundingClientRect(); 201 | 202 | var x = e.clientX - rect.left; 203 | $scope.audio.currentTime = $scope.song.duration * x / waveWidth; 204 | } 205 | }; 206 | 207 | // hack to work with songview for now 208 | $scope.updateIndex = function() { 209 | var currentSong = Songs.getCurrentSong(); 210 | if (currentSong && currentSong.id === $scope.song.id) { 211 | Songs.togglePlay(); 212 | } else { 213 | Songs.playFromAllSongs($scope.song.id, $scope.user.currentGroupId); 214 | } 215 | $scope.songInPlayer = true; 216 | }; 217 | 218 | }]); 219 | --------------------------------------------------------------------------------