Bläddra i källkod

refacto and deploy

Benoit Sida 3 år sedan
förälder
incheckning
a94da9680f

+ 1
- 0
.dockerignore Visa fil

@@ -0,0 +1 @@
1
+uploaded/

+ 8
- 12
.meteor/packages Visa fil

@@ -6,28 +6,24 @@
6 6
 
7 7
 meteor-base@1.0.4             # Packages every Meteor app needs to have
8 8
 mobile-experience@1.0.4       # Packages for a great mobile UX
9
-mongo@1.1.15                   # The database Meteor supports right now
9
+mongo@1.1.16                   # The database Meteor supports right now
10 10
 blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views
11 11
 reactive-var@1.0.11            # Reactive variable for tracker
12 12
 jquery@1.11.10                  # Helpful client-side library
13 13
 tracker@1.1.2                 # Meteor's client-side reactive programming library
14 14
 
15
-standard-minifier-css@1.3.3   # CSS minifier run for production mode
16
-standard-minifier-js@1.2.2    # JS minifier run for production mode
15
+standard-minifier-css@1.3.4   # CSS minifier run for production mode
16
+standard-minifier-js@2.0.0    # JS minifier run for production mode
17 17
 es5-shim@4.6.15                # ECMAScript 5 compatibility for older browsers.
18
-ecmascript@0.6.3              # Enable ECMAScript2015+ syntax in app code
19
-shell-server@0.2.2            # Server-side component of the `meteor shell` command
18
+ecmascript@0.7.2              # Enable ECMAScript2015+ syntax in app code
19
+shell-server@0.2.3            # Server-side component of the `meteor shell` command
20 20
 
21 21
 react-meteor-data
22
-accounts-ui
23
-accounts-password
24
-underscore
22
+accounts-ui@1.1.9
23
+accounts-password@1.3.5
25 24
 fortawesome:fontawesome
26 25
 numeral:numeral
27
-meteorhacks:npm
28
-
29
-
30
-npm-container
31 26
 momentjs:moment
32 27
 iron:router
33 28
 udondan:jszip
29
+erasaur:meteor-lodash

+ 1
- 1
.meteor/release Visa fil

@@ -1 +1 @@
1
-METEOR@1.4.3.1
1
+METEOR@1.4.4.1

+ 34
- 36
.meteor/versions Visa fil

@@ -1,32 +1,33 @@
1
-accounts-base@1.2.14
2
-accounts-password@1.3.4
1
+accounts-base@1.2.16
2
+accounts-password@1.3.5
3 3
 accounts-ui@1.1.9
4
-accounts-ui-unstyled@1.2.0
4
+accounts-ui-unstyled@1.2.1
5 5
 allow-deny@1.0.5
6 6
 autoupdate@1.3.12
7
-babel-compiler@6.14.1
7
+babel-compiler@6.18.1
8 8
 babel-runtime@1.0.1
9 9
 base64@1.0.10
10 10
 binary-heap@1.0.10
11
-blaze@2.3.0
12
-blaze-html-templates@1.1.0
11
+blaze@2.3.2
12
+blaze-html-templates@1.1.2
13 13
 blaze-tools@1.0.10
14 14
 boilerplate-generator@1.0.11
15 15
 caching-compiler@1.1.9
16
-caching-html-compiler@1.1.0
16
+caching-html-compiler@1.1.2
17 17
 callback-hook@1.0.10
18
-check@1.2.4
18
+check@1.2.5
19 19
 ddp@1.2.5
20
-ddp-client@1.3.3
20
+ddp-client@1.3.4
21 21
 ddp-common@1.2.8
22
-ddp-rate-limiter@1.0.6
23
-ddp-server@1.3.13
22
+ddp-rate-limiter@1.0.7
23
+ddp-server@1.3.14
24 24
 deps@1.0.12
25 25
 diff-sequence@1.0.7
26
-ecmascript@0.6.3
26
+ecmascript@0.7.2
27 27
 ecmascript-runtime@0.3.15
28 28
 ejson@1.0.13
29
-email@1.1.18
29
+email@1.2.0
30
+erasaur:meteor-lodash@4.0.0
30 31
 es5-shim@4.6.15
31 32
 fastclick@1.0.13
32 33
 fortawesome:fontawesome@4.7.0
@@ -34,7 +35,7 @@ geojson-utils@1.0.10
34 35
 hot-code-push@1.0.4
35 36
 html-tools@1.0.11
36 37
 htmljs@1.0.11
37
-http@1.2.11
38
+http@1.2.12
38 39
 id-map@1.0.9
39 40
 iron:controller@1.0.12
40 41
 iron:core@1.0.11
@@ -52,27 +53,24 @@ localstorage@1.0.12
52 53
 logging@1.1.17
53 54
 meteor@1.6.1
54 55
 meteor-base@1.0.4
55
-meteorhacks:async@1.0.0
56
-meteorhacks:npm@1.5.0
57 56
 minifier-css@1.2.16
58
-minifier-js@1.2.17
59
-minimongo@1.0.20
57
+minifier-js@2.0.0
58
+minimongo@1.0.21
60 59
 mobile-experience@1.0.4
61 60
 mobile-status-bar@1.0.14
62
-modules@0.7.9
63
-modules-runtime@0.7.9
61
+modules@0.8.1
62
+modules-runtime@0.7.10
64 63
 momentjs:moment@2.18.1
65
-mongo@1.1.15
64
+mongo@1.1.16
66 65
 mongo-id@1.0.6
67 66
 npm-bcrypt@0.9.2
68
-npm-container@1.2.0
69
-npm-mongo@2.2.16_1
67
+npm-mongo@2.2.24
70 68
 numeral:numeral@1.5.3_1
71
-observe-sequence@1.0.15
69
+observe-sequence@1.0.16
72 70
 ordered-dict@1.0.9
73 71
 promise@0.8.8
74 72
 random@1.0.10
75
-rate-limit@1.0.6
73
+rate-limit@1.0.8
76 74
 react-meteor-data@0.2.9
77 75
 reactive-dict@1.1.8
78 76
 reactive-var@1.0.11
@@ -82,21 +80,21 @@ routepolicy@1.0.12
82 80
 service-configuration@1.0.11
83 81
 session@1.1.7
84 82
 sha@1.0.9
85
-shell-server@0.2.2
86
-spacebars@1.0.13
87
-spacebars-compiler@1.1.0
83
+shell-server@0.2.3
84
+spacebars@1.0.15
85
+spacebars-compiler@1.1.2
88 86
 srp@1.0.10
89
-standard-minifier-css@1.3.3
90
-standard-minifier-js@1.2.2
91
-templating@1.3.0
92
-templating-compiler@1.3.0
93
-templating-runtime@1.3.0
94
-templating-tools@1.1.0
87
+standard-minifier-css@1.3.4
88
+standard-minifier-js@2.0.0
89
+templating@1.3.2
90
+templating-compiler@1.3.2
91
+templating-runtime@1.3.2
92
+templating-tools@1.1.2
95 93
 tmeasday:check-npm-versions@0.2.0
96 94
 tracker@1.1.2
97 95
 udondan:jszip@2.4.0_1
98
-ui@1.0.12
96
+ui@1.0.13
99 97
 underscore@1.0.10
100 98
 url@1.1.0
101
-webapp@1.3.13
99
+webapp@1.3.15
102 100
 webapp-hashing@1.0.9

+ 1
- 0
Dockerfile Visa fil

@@ -0,0 +1 @@
1
+FROM jshimko/meteor-launchpad:latest

+ 11
- 0
build/Dockerfile Visa fil

@@ -0,0 +1,11 @@
1
+FROM node
2
+RUN mkdir -p /usr/src/app
3
+COPY bundle/ /usr/src/app/
4
+WORKDIR /usr/src/app/
5
+RUN cd programs/server && npm install && npm uninstall fibers && npm install fibers
6
+EXPOSE 3000
7
+ENV MONGO_URL mongodb://mongo:27017/soundwave
8
+ENV ROOT_URL http://localhost
9
+ENV UPLOAD_DIR /usr/src/upload
10
+ENV PORT 3000
11
+CMD ["node", "main.js"]

+ 1
- 3
client/main.js Visa fil

@@ -4,6 +4,4 @@ import { render } from 'react-dom';
4 4
 import App from '../imports/ui/App.jsx';
5 5
 import '../imports/startup/routes';
6 6
  
7
-Meteor.startup(() => {
8
-	render(<App />, document.getElementById('main'));
9
-});
7
+Meteor.startup(() => render(<App />, document.getElementById('main')));

+ 54
- 0
imports/api/converter.js Visa fil

@@ -0,0 +1,54 @@
1
+import { Meteor } from 'meteor/meteor';
2
+import { Converted } from './links';
3
+_ = lodash;
4
+
5
+const bindHandlers = (ressource, type, path, filename, id, url, ) => {
6
+	let capType = _.capitalize(type);
7
+	ressource.on('unavailable', Meteor.bindEnvironment(err => {
8
+		console.error('KO', url, err);
9
+		Converted.update({ _id: id }, { $set: { [`error_${type}`]: `${capType} is unreachable` } });
10
+	}))
11
+			 .on('available', info => {
12
+				 console.info('OK');
13
+				 ressource.download(path);
14
+			 })
15
+			 .on('progress', Meteor.bindEnvironment(percent => {
16
+				 Converted.update({ _id: id }, { $set: { [`${type}_progress`]: percent } })
17
+			 }))
18
+			 .on('error', Meteor.bindEnvironment(err => {
19
+				 console.error(`${capType} download error: ${err.message}`);
20
+				 Converted.update({ _id: id }, { $set: { [`error_${type}`]: `${capType} cannot be saved` } });
21
+			 }))
22
+			 .on('end', Meteor.bindEnvironment(() => {
23
+				 console.info(`${capType} download complete for ${url}`);
24
+				 Converted.update({ _id: id }, { $set: { [type]: filename, [`${type}_progress`]: 100 } });
25
+			 }));
26
+};
27
+
28
+const convertAudio = (url, filename, id) => {
29
+	if (Meteor.isServer) {
30
+		import { AudioConverter } from '../../server/Converter';
31
+		import path from 'path';
32
+
33
+		const filePath = path.resolve(process.env.UPLOAD_DIR, filename);
34
+		console.log(`Converting ${url} to MP3...`);
35
+
36
+		let audio_dl = new AudioConverter(url);
37
+		bindHandlers(audio_dl, 'audio', filePath + '.mp3', filename + '.mp3', id, url);
38
+	}
39
+};
40
+
41
+const convertVideo = (url, filename, id) => {
42
+	if (Meteor.isServer) {
43
+		import { VideoConverter } from '../../server/Converter';
44
+		import path from 'path';
45
+
46
+		const filePath = path.resolve(process.env.UPLOAD_DIR, filename);
47
+		console.log(`Converting ${url} to MP4 ...`);
48
+
49
+		let video_dl = new VideoConverter(url);
50
+		bindHandlers(video_dl, 'video', filePath + '.mp4', filename + '.mp4', id, url);
51
+	}
52
+};
53
+
54
+export { convertAudio, convertVideo };

+ 17
- 85
imports/api/links.js Visa fil

@@ -1,108 +1,40 @@
1 1
 import { Meteor } from 'meteor/meteor';
2 2
 import { Mongo } from 'meteor/mongo';
3 3
 import { check, Match } from 'meteor/check';
4
+import { convertVideo, convertAudio} from './converter';
4 5
 
5 6
 export const Converted = new Mongo.Collection('links');
6 7
 
7 8
 const YTExp = /http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-_]*)/;
8 9
 
9 10
 if (Meteor.isServer) {
10
-	Meteor.publish('links', function linksPublication() {
11
-		return Converted.find();
12
-	});
11
+	Meteor.publish('links', () => Converted.find());
13 12
 }
14 13
 
15
-const convert = (url, filename, id) => {
16
-	if (Meteor.isServer) {
17
-		console.log('Converting ...');
18
-		const fs = Meteor.npmRequire("fs");
19
-		const ytdl = Meteor.npmRequire('ytdl-core');
20
-		const path = Meteor.npmRequire('path');
21
-		const ffmpeg = Meteor.npmRequire('fluent-ffmpeg');
22
-		const filePath = path.resolve('/home/kod3/projects/SoundWave/uploaded/', filename)
23
-		const videoPath = filePath + '.mp4';
24
-		const audioPath = filePath + '.mp3';
25
-		ytdl.getInfo(url, {}, Meteor.bindEnvironment((err, info) => {
26
-			console.info(info ? 'ok' : 'ko');
27
-			if (err) {
28
-				console.log(url);
29
-				console.error(err);
30
-				Converted.update({ _id: id }, { $set: {
31
-					error_video: 'Video is unreachable',
32
-					error_audio: 'Video is unreachable',
33
-				} });
34
-			} else {
35
-				const totalSeconds = info ? info.length_seconds : 0;
36
-				const dl = ytdl(url);
37
-				dl.pipe(fs.createWriteStream(videoPath));
38
-				ffmpeg(dl)
39
-					.noVideo()
40
-					.on('end', Meteor.bindEnvironment(() => {
41
-						Converted.update({ _id: id }, { $set: { audio: filename + '.mp3', audio_progress: 100 } });
42
-					}))
43
-					.on('progress', Meteor.bindEnvironment((params) => {
44
-						const time = moment(new Date(...[0, 0, 0, ...params.timemark.split(':')]));
45
-						const seconds = time.hours() * 3600 + time.minutes() * 60 + time.seconds();
46
-						let percent = ((seconds / totalSeconds) * 100).toFixed(0);
47
-						Converted.update({ _id: id }, { $set: { audio_progress: percent } });
48
-					}))
49
-					.on('error', Meteor.bindEnvironment((err) => {
50
-						console.error('MP3 encoding error: ' + err.message);
51
-						Converted.update({ _id: id }, { $set: { error_audio: 'Audio cannot be extracted' } });
52
-					}))
53
-					.format('mp3')
54
-					.output(fs.createWriteStream(audioPath))
55
-					.run();
56
-				dl.on('info', (info, format) => console.log);
57
-				dl.on('response', Meteor.bindEnvironment((res) => {
58
-					const totalSize = res.headers['content-length'];
59
-					let dataRead = 0;
60
-					res.on('data', Meteor.bindEnvironment((data) => {
61
-						dataRead += data.length;
62
-						let percent = dataRead / totalSize;
63
-						Converted.update({ _id: id }, { $set: { video_progress: (percent * 100).toFixed(0) } });
64
-					}));
65
-					res.on('end', Meteor.bindEnvironment(() => {
66
-						Converted.update({ _id: id }, { $set: { video: filename + '.mp4', video_progress: 100 } });
67
-					}));
68
-					res.on('error', Meteor.bindEnvironment((err) => {
69
-						console.error('Error: ' + err.message)
70
-						Converted.update({ _id: id }, { $set: { error_video: 'Video cannot be saved' } });
71
-					}));
72
-				}));
73
-			}
74
-		}));
75
-	} else return false;
76
-};
77
-
78 14
 Meteor.methods({
79 15
 
80 16
   'links.add'({video, type}) {
81 17
   	check(video.url, Match.Where(url => YTExp.test(url)));
82 18
 	check(type, Match.Where(t => ['audio'].includes(t)));
83 19
 
84
- 	let converted = Converted.findOne({
85
-		url: video.url,
86
-		error_audio: { $ne: null },
87
-		error_video: { $ne: null },
88
-	});
20
+ 	let converted = Converted.findOne({ id: video.id });
89 21
 
90 22
 	if (!converted) {
91
-		const filename = video.title.replace(/ /g, '_');
92
-		Converted.remove({url: video.url});
93
-		var inserted_id = Converted.insert({
94
-			id: video.id,
95
-			url: video.url,
96
-			title: video.title,
97
-			video: '',
98
-			video_progress: 0,
99
-			error_video: '',
100
-			audio: '',
101
-			audio_progress: 0,
102
-			error_audio: '',
103
-			createdAt: new Date()
23
+		let inserted_id = Converted.insert({
24
+			id: video.id, url: video.url,
25
+			title: video.title, createdAt: new Date()
104 26
 		});
105
-		convert(video.url, filename, inserted_id);
27
+		converted = Converted.findOne({_id: inserted_id });
28
+	}
29
+
30
+	if (!converted.audio) {
31
+		Converted.update({ _id: converted._id }, { $set: { audio: '', error_audio: '', audio_progress: 0 } });
32
+		convertAudio(converted.url, converted.title.replace(/ /g, '_'), converted._id);
33
+	}
34
+
35
+	if (!converted.video) {
36
+		Converted.update({ _id: converted._id }, { $set: { video: '', error_video: '', video_progress: 0 } });
37
+		convertVideo(converted.url, converted.title.replace(/ /g, '_'), converted._id);
106 38
 	}
107 39
   },
108 40
 

+ 0
- 6
imports/startup/accounts-config.js Visa fil

@@ -1,6 +0,0 @@
1
-
2
-import { Accounts } from 'meteor/accounts-base';
3
-
4
-Accounts.ui.config({
5
-  passwordSignupFields: 'USERNAME_ONLY',
6
-});

+ 5
- 5
imports/startup/routes.js Visa fil

@@ -1,12 +1,12 @@
1 1
 import { Converted } from '../api/links';
2 2
 
3
-const uploadedPath = '/home/kod3/projects/SoundWave/uploaded/';
3
+const uploadedPath = process.env.UPLOAD_DIR;
4 4
 
5 5
 Router.route('/');
6 6
 
7 7
 Router.route("/:type/batch/:ids", function() {
8
-	const fs = Meteor.npmRequire('fs');
9
-	const path = Meteor.npmRequire('path');
8
+	import fs from 'fs';
9
+	import path from 'path';
10 10
 
11 11
 	let { type, ids } = this.params;
12 12
 	ids = ids.split(',');
@@ -27,8 +27,8 @@ Router.route("/:type/batch/:ids", function() {
27 27
 
28 28
 
29 29
 Router.route("/:type/:id", function() {
30
-	const fs = Meteor.npmRequire('fs');
31
-	const path = Meteor.npmRequire('path');
30
+	import fs from 'fs';
31
+	import path from 'path';
32 32
 
33 33
 	let { type, id } = this.params;
34 34
 	let file = (type === 'mp3' ? 'audio' : 'video');

+ 0
- 20
imports/ui/AccountsUIWrapper.jsx Visa fil

@@ -1,20 +0,0 @@
1
-import React, { Component } from 'react';
2
-import ReactDOM from 'react-dom';
3
-import { Template } from 'meteor/templating';
4
-import { Blaze } from 'meteor/blaze';
5
-
6
-export default class AccountsUIWrapper extends Component {
7
-  componentDidMount() {
8
-    // Use Meteor Blaze to render login buttons
9
-    this.view = Blaze.render(Template.loginButtons,
10
-      ReactDOM.findDOMNode(this.refs.container));
11
-  }
12
-  componentWillUnmount() {
13
-    // Clean up Blaze view
14
-    Blaze.remove(this.view);
15
-  }
16
-  render() {
17
-    // Just render a placeholder container that will be filled in
18
-    return <span id="accounts-ui" ref="container" />;
19
-  }
20
-}

+ 36
- 44
imports/ui/App.jsx Visa fil

@@ -2,50 +2,38 @@ import React, { Component, PropTypes } from 'react';
2 2
 import { Meteor } from 'meteor/meteor';
3 3
 import { createContainer } from 'meteor/react-meteor-data';
4 4
 import { Converted } from '../api/links';
5
-import Links from './Links';
6 5
 import { YouTubeSearch, YouTubeVideoInfo } from './YouTubeAPI';
7 6
 import Video from './Video';
8
-import Input from "./Input";
9
-import Results from "./Results";
10
-import OptionsAll from "./OptionsAll";
7
+import AppLayout from './AppLayout';
8
+_ = lodash;
11 9
 
12 10
 class App extends Component {
13 11
 	constructor(props) {
14 12
 		super(props);
15 13
 		this.state = { result: [], links: [], value: '' };
14
+		this.timeout = undefined;
16 15
 		this.options = [
17
-			{ type: 'MP3', handler: (video) => window.open('/mp3/'+ video.id) },
18
-			{ type: 'MP4', handler: (video) => window.open('/mp4/'+ video.id) },
16
+			{
17
+				type: 'MP3',
18
+				handler: (video) => window.open('/mp3/'+ video.id),
19
+				handlerAll: () => window.open(`/mp3/batch/${this.batchLinks()}`)
20
+			},
21
+			{
22
+				type: 'MP4',
23
+				handler: (video) => window.open('/mp4/'+ video.id),
24
+				handlerAll: () => window.open(`/mp4/batch/${this.batchLinks()}`)
25
+			},
19 26
 		];
20
-		this.optionsAll = [
21
-			{ type: 'MP3', handler: () => window.open(`/mp3/batch/${this.batchLinks()}`) },
22
-			{ type: 'MP4', handler: () => window.open(`/mp4/batch/${this.batchLinks()}`) },
23
-		];
24
-	}
25
-
26
-	render() {
27
-		return (
28
-			<div className="container">
29
-				<h1 className="logo">SoundWave</h1>
30
-				<div className="box-container">
31
-					<header>
32
-						<Input value={this.state.value} change={this.inputChange.bind(this)} />
33
-						<Results results={this.state.result} select={this.selectResult.bind(this)} />
34
-					</header>
35
-					<Links links={this.state.links} remove={this.removeLink.bind(this)} handlers={this.options}/>
36
-					<OptionsAll count={this.state.links.length} handlers={this.optionsAll}
37
-								links={this.state.links} converted={this.props.converted} />
38
-				</div>
39
-			</div>
40
-		);
27
+		_.each(['inputChange', 'selectResult', 'removeLink'], f => this[f] = this[f].bind(this));
41 28
 	}
42 29
 
43 30
 	batchLinks() {
44
-		return this.state.links.map(e => e.id).join(',');
31
+		return _.map(this.state.links, 'id').join(',');
45 32
 	}
46 33
 
47 34
 	inputChange(text) {
48
-		this.setState({ value: text }, this.search);
35
+		clearTimeout(this.timeout);
36
+		this.setState({ value: text }, () => this.timeout = setTimeout(this.search.bind(this), 100));
49 37
 	}
50 38
 
51 39
 	removeLink(link) {
@@ -55,9 +43,9 @@ class App extends Component {
55 43
 
56 44
 	selectResult(video) {
57 45
 		if (_.every(this.state.links, e => e.id !== video.id)) {
58
-			if (_.some(this.props.converted, e => e.id === video.id)) {
46
+			if (_.some(this.props.converted, e => e.id === video.id && e.audio && e.video))
59 47
 				video.converted = _.find(this.props.converted, e => e.id === video.id)._id;
60
-			} else
48
+			else
61 49
 				Meteor.call('links.add', {video, type: 'audio'}, (err, res) => {
62 50
 					if (err)
63 51
 						console.error(err);
@@ -70,27 +58,31 @@ class App extends Component {
70 58
 	}
71 59
 
72 60
 	search() {
73
-		if (!this.state.value) {
61
+		if (!this.state.value)
74 62
 			this.setState({ result: [] });
75
-		} else {
63
+		else
76 64
 			YouTubeSearch(this.state.value)
77 65
 				.then(e => e.items.map(e => new Video(e)))
78 66
 				.then(e => { this.setState({ result: e }); return e; })
79
-				.then(e => e.forEach(i => YouTubeVideoInfo(i.id)
80
-					.then(r => i.update(r.items[0].statistics, r.items[0].contentDetails.duration))
67
+				.then(e => YouTubeVideoInfo(_.map(e, 'id').join(','))
68
+					.then(r => _.each(e, (elem, i) => elem.update(r.items[i].statistics, r.items[i].contentDetails.duration)))
81 69
 					.then(() => this.setState({ result: e }))
82
-				));
83
-		}
70
+				);
71
+	}
72
+
73
+	render() {
74
+		return <AppLayout
75
+			links={this.state.links} linkRemove={this.removeLink} linkHandlers={this.options}
76
+			inputChange={this.inputChange} inputValue={this.state.value}
77
+			resultSelect={this.selectResult} results={this.state.result}
78
+			optionsHandler={this.options} converted={this.props.converted}
79
+		/>;
84 80
 	}
85 81
 }
86 82
 
87
-App.propTypes = {
88
-	converted: PropTypes.array.isRequired,
89
-};
83
+App.propTypes = { converted: PropTypes.array.isRequired };
90 84
 
91 85
 export default createContainer(() => {
92 86
 	Meteor.subscribe('links');
93
-	return {
94
-		converted: Converted.find({}).fetch(),
95
-	};
96
-}, App)
87
+	return { converted: Converted.find({}).fetch(), };
88
+}, App);

+ 34
- 0
imports/ui/AppLayout.jsx Visa fil

@@ -0,0 +1,34 @@
1
+import React from 'react';
2
+import Input from "./Input";
3
+import Results from "./Results";
4
+import OptionsAll from "./OptionsAll";
5
+import Links from './Links';
6
+
7
+const AppLayout = (props) =>
8
+	<div className="container">
9
+		<h1 className="logo">SoundWave</h1>
10
+		<div className="box-container">
11
+			<header>
12
+				<Input placeholder="Search a video" value={props.inputValue} change={props.inputChange} />
13
+				<Results results={props.results} select={props.resultSelect} />
14
+			</header>
15
+			<Links links={props.links} remove={props.linkRemove} handlers={props.linkHandlers}/>
16
+			<OptionsAll count={props.links.length} handlers={props.optionsHandler}
17
+						links={props.links} converted={props.converted} />
18
+		</div>
19
+	</div>;
20
+
21
+
22
+AppLayout.propTypes = {
23
+	links: Links.propTypes.links,
24
+	linkRemove: Links.propTypes.remove,
25
+	linkHandlers: Links.propTypes.handlers,
26
+	inputValue: Input.propTypes.value,
27
+	inputChange: Input.propTypes.change,
28
+	results: Results.propTypes.results,
29
+	resultSelect: Results.propTypes.select,
30
+	optionsHandler: OptionsAll.propTypes.handlers,
31
+	converted: OptionsAll.propTypes.converted
32
+};
33
+
34
+export default AppLayout;

+ 8
- 14
imports/ui/Input.jsx Visa fil

@@ -1,19 +1,13 @@
1
-import React, {Component, PropTypes} from 'react';
1
+import React, { PropTypes } from 'react';
2 2
 
3
-export default class Input extends Component {
4
-	handleChange(e) {
5
-		this.props.change(e.target.value);
6
-	}
7
-
8
-	render() {
9
-		return (
10
-			<input type="text" ref="textInput" placeholder="Search a video" autoFocus
11
-				   value={this.props.value} className="new-link" onChange={this.handleChange.bind(this)} />
12
-		);
13
-	}
14
-}
3
+const Input = ({ value, change, placeholder }) =>
4
+	<input type="text" className="new-link" placeholder={placeholder} autoFocus
5
+		   value={value} onChange={e => change(e.target.value)} />;
15 6
 
16 7
 Input.propTypes = {
17 8
 	change: PropTypes.func.isRequired,
18
-	value: PropTypes.string.isRequired
9
+	value: PropTypes.string.isRequired,
10
+	placeholder: PropTypes.string
19 11
 };
12
+
13
+export default Input;

+ 19
- 21
imports/ui/Options.jsx Visa fil

@@ -1,17 +1,29 @@
1
-import React, {Component, PropTypes} from 'react';
1
+import React, { Component, PropTypes } from 'react';
2 2
 import Video from './Video';
3 3
 import classnames from 'classnames';
4 4
 
5 5
 export default class Options extends Component {
6
+	render() {
7
+		return (
8
+			<div>
9
+				{ _.map(this.props.handlers, (h) => (
10
+					<button key={h.type} className={this.getClasses(h.type)}
11
+							onClick={() => h.handler(this.props.video)}
12
+							disabled={this.isDisabled(h.type)} title={this.getErrorMessage(h.type)}>
13
+						<div className="option-progress" style={{ width: this.getProgress(h.type) + '%' }} />
14
+						<i className="fa fa-download" /> {h.type}
15
+					</button>
16
+				)) }
17
+			</div>
18
+		);
19
+	}
6 20
 
7 21
 	getProgress(type) {
8
-		return !this.props.converted ? 0 :
9
-			this.props.converted[type === 'MP3' ? 'audio_progress' : 'video_progress'];
22
+		return !this.props.converted ? 0 : this.props.converted[type === 'MP3' ? 'audio_progress' : 'video_progress'];
10 23
 	}
11 24
 
12 25
 	isDisabled(type) {
13
-		return !this.props.converted ||
14
-			!this.props.converted[type === 'MP3' ? 'audio' : 'video'];
26
+		return !this.props.converted || !this.props.converted[type === 'MP3' ? 'audio' : 'video'];
15 27
 	}
16 28
 
17 29
 	getClasses(type) {
@@ -23,22 +35,8 @@ export default class Options extends Component {
23 35
 	}
24 36
 
25 37
 	getErrorMessage(type) {
26
-		return (type === 'MP3' ? this.props.converted.error_audio : this.props.converted.error_video) || null;
27
-	}
28
-
29
-	render() {
30
-		return (
31
-			<div>
32
-				{ _.map(this.props.handlers, (h) => (
33
-					<button key={h.type} className={this.getClasses(h.type)}
34
-							onClick={() => h.handler(this.props.video)}
35
-							disabled={this.isDisabled(h.type)} title={this.getErrorMessage(h.type)}>
36
-						<div className="option-progress" style={{width: this.getProgress(h.type) + '%'}}></div>
37
-						<i className="fa fa-download"/> {h.type}
38
-					</button>
39
-				)) }
40
-			</div>
41
-		);
38
+		let typestr = (type === 'MP3' ? 'audio' : 'video');
39
+		return this.props.converted && this.props.converted[`error_${typestr}`] || null;
42 40
 	}
43 41
 }
44 42
 

+ 2
- 2
imports/ui/OptionsAll.jsx Visa fil

@@ -22,7 +22,7 @@ export default class OptionsAll extends Component {
22 22
 				<div className="label">ALL: {this.renderCount()}</div>
23 23
 				<div>
24 24
 					{ _.map(this.props.handlers, (h) => (
25
-						<button key={h.type} className="option" onClick={h.handler} disabled={this.isDisabled(h.type)}>
25
+						<button key={h.type} className="option" onClick={h.handlerAll} disabled={this.isDisabled(h.type)}>
26 26
 							<i className="fa fa-download"/> {h.type}
27 27
 						</button>
28 28
 					)) }
@@ -38,6 +38,6 @@ OptionsAll.propTypes = {
38 38
 	converted: PropTypes.arrayOf(PropTypes.object),
39 39
 	handlers: PropTypes.arrayOf(PropTypes.shape({
40 40
 		type: PropTypes.string,
41
-		handler: PropTypes.func
41
+		handlerAll: PropTypes.func,
42 42
 	})).isRequired,
43 43
 };

+ 30
- 35
imports/ui/Result.jsx Visa fil

@@ -1,42 +1,37 @@
1
-import React, {Component, PropTypes} from 'react';
1
+import React, { PropTypes } from 'react';
2 2
 import Video from './Video';
3
+_ = lodash;
3 4
 
4
-export default class Result extends Component {
5
-	render() {
6
-		return (
7
-			<a className="result-link" onClick={this.props.click}>
8
-				<img src={this.props.result.thumb.url} alt="thumbnail" className="result-thumb"
9
-					 height={this.props.result.thumb.height / 2}
10
-					 width={this.props.result.thumb.width / 2}/>
11
-				<div className="result-info">
12
-					<div className="result-head" title={this.props.result.title}>
13
-						<div className="result-title">{this.props.result.title}</div>
14
-						<div className="result-duration">{this.props.result.duration}</div>
15
-					</div>
16
-					<div className="result-stats">
17
-						<div className="result-views" title={this.props.result.stats.viewCount}>
18
-							<i className="fa fa-eye"/> {this.props.result.stats.viewCount}
19
-						</div>
20
-						<div className="result-likes" title={this.props.result.stats.likeCount}>
21
-							<i className="fa fa-thumbs-o-up"/> {this.props.result.stats.likeCount}
22
-						</div>
23
-						<div className="result-dislikes" title={this.props.result.stats.dislikeCount}>
24
-							<i className="fa fa-thumbs-o-down"/> {this.props.result.stats.dislikeCount}
25
-						</div>
26
-						<div className="result-comments" title={this.props.result.stats.commentCount}>
27
-							<i className="fa fa-comments-o"/> {this.props.result.stats.commentCount}
28
-						</div>
29
-						<div className="result-favorites" title={this.props.result.stats.favoriteCount}>
30
-							<i className="fa fa-star-o"/> {this.props.result.stats.favoriteCount}
31
-						</div>
5
+const iconsClasses = {
6
+	view: "eye",
7
+	like: "thumbs-o-up",
8
+	dislike: "thumbs-o-down",
9
+	comment: "comments-o",
10
+	favorite: "star-o"
11
+};
12
+
13
+const Result = ({ click, result }) =>
14
+	<a className="result-link" onClick={click}>
15
+		<img src={result.thumb.url} alt="thumbnail" className="result-thumb"
16
+			 height={result.thumb.height / 2} width={result.thumb.width / 2}/>
17
+		<div className="result-info">
18
+			<div className="result-head" title={result.title}>
19
+				<div className="result-title">{result.title}</div>
20
+				<div className="result-duration">{result.duration}</div>
21
+			</div>
22
+			<div className="result-stats">
23
+				{ _.map(result.stats, (e, k) =>
24
+					<div key={k} className={`result-${k.slice(0, -5)}s`} title={e}>
25
+						<i className={`fa fa-${iconsClasses[k.slice(0, -5)]}`}/> {e}
32 26
 					</div>
33
-				</div>
34
-			</a>
35
-		);
36
-	}
37
-}
27
+				) }
28
+			</div>
29
+		</div>
30
+	</a>;
38 31
 
39 32
 Result.propTypes = {
40 33
 	result: PropTypes.instanceOf(Video).isRequired,
41 34
 	click: PropTypes.func
42
-};
35
+};
36
+
37
+export default Result;

+ 1
- 0
imports/ui/YouTubeAPI.js Visa fil

@@ -1,5 +1,6 @@
1 1
 import 'whatwg-fetch';
2 2
 import querystring from 'querystring';
3
+_ = lodash;
3 4
 
4 5
 const KEY = 'AIzaSyDh641dLzxYexCASA3TdYMlc6zzS793ppQ';
5 6
 const ENDPOINT = 'https://www.googleapis.com/youtube/v3';

+ 1
- 0
launchpad.conf Visa fil

@@ -0,0 +1 @@
1
+INSTALL_MONGO=true

+ 2
- 1
package.json Visa fil

@@ -15,7 +15,8 @@
15 15
     "react": "^15.4.2",
16 16
     "react-addons-pure-render-mixin": "^15.4.2",
17 17
     "react-dom": "^15.4.2",
18
-    "youtube-dl": "^1.11.1"
18
+    "ytdl-core":"0.12.1",
19
+    "fluent-ffmpeg":"2.1.0"
19 20
   },
20 21
   "devDependencies": {
21 22
     "babel-plugin-transform-class-properties": "^6.23.0"

+ 0
- 4
packages.json Visa fil

@@ -1,4 +0,0 @@
1
-{
2
-  "ytdl-core":"0.7.9",
3
-  "fluent-ffmpeg":"2.1.0"
4
-}

+ 64
- 0
server/Converter.js Visa fil

@@ -0,0 +1,64 @@
1
+import EventEmitter from 'events';
2
+import fs from 'fs';
3
+import ytdl from 'ytdl-core';
4
+import ffmpeg from 'fluent-ffmpeg';
5
+
6
+class Converter extends EventEmitter {
7
+	constructor(url) {
8
+		super();
9
+		this.ytdl = null;
10
+		this.totalSeconds = 0;
11
+		ytdl.getInfo(url, {}, (err, info) => {
12
+			if (err)
13
+				this.emit('unavailable', err);
14
+			else {
15
+				this.totalSeconds = info ? info.length_seconds : 0;
16
+				this.ytdl = ytdl(url);
17
+				this.emit('available', info);
18
+			}
19
+		});
20
+	}
21
+}
22
+
23
+export class VideoConverter extends Converter {
24
+	constructor(url) { super(url); }
25
+
26
+	initResponse(res, path) {
27
+		const totalSize = res.headers['content-length'];
28
+		let dataRead = 0;
29
+		res.on('data', data => {
30
+			dataRead += data.length;
31
+			let percent = dataRead / totalSize;
32
+			this.emit('progress', (percent * 100).toFixed(0));
33
+		})
34
+		   .on('end', () => this.emit('end'))
35
+		   .on('error', () => fs.unlink(path, err => this.emit('error', error)));
36
+	}
37
+
38
+	download(path) {
39
+		if (this.ytdl)
40
+			this.ytdl.on('response', res => this.initResponse(res, path)).pipe(fs.createWriteStream(path));
41
+	}
42
+}
43
+
44
+export class AudioConverter extends Converter {
45
+	constructor(url) { super(url); }
46
+
47
+	audio_percent(date) {
48
+		const time = moment(new Date(...[0, 0, 0, ...date.split(':')]));
49
+		const seconds = time.hours() * 3600 + time.minutes() * 60 + time.seconds();
50
+		return ((seconds / this.totalSeconds) * 100).toFixed(0);
51
+	}
52
+
53
+	download(path) {
54
+		if (this.ytdl)
55
+			ffmpeg(this.ytdl)
56
+				.noVideo().format('mp3')
57
+				.output(fs.createWriteStream(path))
58
+				.on('end', () => this.emit('end'))
59
+				.on('start', () => this.emit('start'))
60
+				.on('progress', params => this.emit('progress', this.audio_percent(params.timemark)))
61
+				.on('error', (err, stdout, stderr) => fs.unlink(path, () => this.emit('error', err, stdout, stderr)))
62
+				.run();
63
+	}
64
+}

+ 1
- 3
server/main.js Visa fil

@@ -2,7 +2,5 @@ import { Meteor } from 'meteor/meteor';
2 2
 import '../imports/api/links.js';
3 3
 import '../imports/startup/routes';
4 4
 
5
-Meteor.startup(() => {
6
-  // code to run on server at startup
7
-});
5
+Meteor.startup(() => {});
8 6