소스 검색

refacto and deploy

Benoit Sida 3 년 전
부모
커밋
a94da9680f

+ 1
- 0
.dockerignore 파일 보기

1
+uploaded/

+ 8
- 12
.meteor/packages 파일 보기

6
 
6
 
7
 meteor-base@1.0.4             # Packages every Meteor app needs to have
7
 meteor-base@1.0.4             # Packages every Meteor app needs to have
8
 mobile-experience@1.0.4       # Packages for a great mobile UX
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
 blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views
10
 blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views
11
 reactive-var@1.0.11            # Reactive variable for tracker
11
 reactive-var@1.0.11            # Reactive variable for tracker
12
 jquery@1.11.10                  # Helpful client-side library
12
 jquery@1.11.10                  # Helpful client-side library
13
 tracker@1.1.2                 # Meteor's client-side reactive programming library
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
 es5-shim@4.6.15                # ECMAScript 5 compatibility for older browsers.
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
 react-meteor-data
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
 fortawesome:fontawesome
24
 fortawesome:fontawesome
26
 numeral:numeral
25
 numeral:numeral
27
-meteorhacks:npm
28
-
29
-
30
-npm-container
31
 momentjs:moment
26
 momentjs:moment
32
 iron:router
27
 iron:router
33
 udondan:jszip
28
 udondan:jszip
29
+erasaur:meteor-lodash

+ 1
- 1
.meteor/release 파일 보기

1
-METEOR@1.4.3.1
1
+METEOR@1.4.4.1

+ 34
- 36
.meteor/versions 파일 보기

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
 accounts-ui@1.1.9
3
 accounts-ui@1.1.9
4
-accounts-ui-unstyled@1.2.0
4
+accounts-ui-unstyled@1.2.1
5
 allow-deny@1.0.5
5
 allow-deny@1.0.5
6
 autoupdate@1.3.12
6
 autoupdate@1.3.12
7
-babel-compiler@6.14.1
7
+babel-compiler@6.18.1
8
 babel-runtime@1.0.1
8
 babel-runtime@1.0.1
9
 base64@1.0.10
9
 base64@1.0.10
10
 binary-heap@1.0.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
 blaze-tools@1.0.10
13
 blaze-tools@1.0.10
14
 boilerplate-generator@1.0.11
14
 boilerplate-generator@1.0.11
15
 caching-compiler@1.1.9
15
 caching-compiler@1.1.9
16
-caching-html-compiler@1.1.0
16
+caching-html-compiler@1.1.2
17
 callback-hook@1.0.10
17
 callback-hook@1.0.10
18
-check@1.2.4
18
+check@1.2.5
19
 ddp@1.2.5
19
 ddp@1.2.5
20
-ddp-client@1.3.3
20
+ddp-client@1.3.4
21
 ddp-common@1.2.8
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
 deps@1.0.12
24
 deps@1.0.12
25
 diff-sequence@1.0.7
25
 diff-sequence@1.0.7
26
-ecmascript@0.6.3
26
+ecmascript@0.7.2
27
 ecmascript-runtime@0.3.15
27
 ecmascript-runtime@0.3.15
28
 ejson@1.0.13
28
 ejson@1.0.13
29
-email@1.1.18
29
+email@1.2.0
30
+erasaur:meteor-lodash@4.0.0
30
 es5-shim@4.6.15
31
 es5-shim@4.6.15
31
 fastclick@1.0.13
32
 fastclick@1.0.13
32
 fortawesome:fontawesome@4.7.0
33
 fortawesome:fontawesome@4.7.0
34
 hot-code-push@1.0.4
35
 hot-code-push@1.0.4
35
 html-tools@1.0.11
36
 html-tools@1.0.11
36
 htmljs@1.0.11
37
 htmljs@1.0.11
37
-http@1.2.11
38
+http@1.2.12
38
 id-map@1.0.9
39
 id-map@1.0.9
39
 iron:controller@1.0.12
40
 iron:controller@1.0.12
40
 iron:core@1.0.11
41
 iron:core@1.0.11
52
 logging@1.1.17
53
 logging@1.1.17
53
 meteor@1.6.1
54
 meteor@1.6.1
54
 meteor-base@1.0.4
55
 meteor-base@1.0.4
55
-meteorhacks:async@1.0.0
56
-meteorhacks:npm@1.5.0
57
 minifier-css@1.2.16
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
 mobile-experience@1.0.4
59
 mobile-experience@1.0.4
61
 mobile-status-bar@1.0.14
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
 momentjs:moment@2.18.1
63
 momentjs:moment@2.18.1
65
-mongo@1.1.15
64
+mongo@1.1.16
66
 mongo-id@1.0.6
65
 mongo-id@1.0.6
67
 npm-bcrypt@0.9.2
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
 numeral:numeral@1.5.3_1
68
 numeral:numeral@1.5.3_1
71
-observe-sequence@1.0.15
69
+observe-sequence@1.0.16
72
 ordered-dict@1.0.9
70
 ordered-dict@1.0.9
73
 promise@0.8.8
71
 promise@0.8.8
74
 random@1.0.10
72
 random@1.0.10
75
-rate-limit@1.0.6
73
+rate-limit@1.0.8
76
 react-meteor-data@0.2.9
74
 react-meteor-data@0.2.9
77
 reactive-dict@1.1.8
75
 reactive-dict@1.1.8
78
 reactive-var@1.0.11
76
 reactive-var@1.0.11
82
 service-configuration@1.0.11
80
 service-configuration@1.0.11
83
 session@1.1.7
81
 session@1.1.7
84
 sha@1.0.9
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
 srp@1.0.10
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
 tmeasday:check-npm-versions@0.2.0
93
 tmeasday:check-npm-versions@0.2.0
96
 tracker@1.1.2
94
 tracker@1.1.2
97
 udondan:jszip@2.4.0_1
95
 udondan:jszip@2.4.0_1
98
-ui@1.0.12
96
+ui@1.0.13
99
 underscore@1.0.10
97
 underscore@1.0.10
100
 url@1.1.0
98
 url@1.1.0
101
-webapp@1.3.13
99
+webapp@1.3.15
102
 webapp-hashing@1.0.9
100
 webapp-hashing@1.0.9

+ 1
- 0
Dockerfile 파일 보기

1
+FROM jshimko/meteor-launchpad:latest

+ 11
- 0
build/Dockerfile 파일 보기

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 파일 보기

4
 import App from '../imports/ui/App.jsx';
4
 import App from '../imports/ui/App.jsx';
5
 import '../imports/startup/routes';
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 파일 보기

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 파일 보기

1
 import { Meteor } from 'meteor/meteor';
1
 import { Meteor } from 'meteor/meteor';
2
 import { Mongo } from 'meteor/mongo';
2
 import { Mongo } from 'meteor/mongo';
3
 import { check, Match } from 'meteor/check';
3
 import { check, Match } from 'meteor/check';
4
+import { convertVideo, convertAudio} from './converter';
4
 
5
 
5
 export const Converted = new Mongo.Collection('links');
6
 export const Converted = new Mongo.Collection('links');
6
 
7
 
7
 const YTExp = /http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-_]*)/;
8
 const YTExp = /http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-_]*)/;
8
 
9
 
9
 if (Meteor.isServer) {
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
 Meteor.methods({
14
 Meteor.methods({
79
 
15
 
80
   'links.add'({video, type}) {
16
   'links.add'({video, type}) {
81
   	check(video.url, Match.Where(url => YTExp.test(url)));
17
   	check(video.url, Match.Where(url => YTExp.test(url)));
82
 	check(type, Match.Where(t => ['audio'].includes(t)));
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
 	if (!converted) {
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 파일 보기

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

+ 5
- 5
imports/startup/routes.js 파일 보기

1
 import { Converted } from '../api/links';
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
 Router.route('/');
5
 Router.route('/');
6
 
6
 
7
 Router.route("/:type/batch/:ids", function() {
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
 	let { type, ids } = this.params;
11
 	let { type, ids } = this.params;
12
 	ids = ids.split(',');
12
 	ids = ids.split(',');
27
 
27
 
28
 
28
 
29
 Router.route("/:type/:id", function() {
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
 	let { type, id } = this.params;
33
 	let { type, id } = this.params;
34
 	let file = (type === 'mp3' ? 'audio' : 'video');
34
 	let file = (type === 'mp3' ? 'audio' : 'video');

+ 0
- 20
imports/ui/AccountsUIWrapper.jsx 파일 보기

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 파일 보기

2
 import { Meteor } from 'meteor/meteor';
2
 import { Meteor } from 'meteor/meteor';
3
 import { createContainer } from 'meteor/react-meteor-data';
3
 import { createContainer } from 'meteor/react-meteor-data';
4
 import { Converted } from '../api/links';
4
 import { Converted } from '../api/links';
5
-import Links from './Links';
6
 import { YouTubeSearch, YouTubeVideoInfo } from './YouTubeAPI';
5
 import { YouTubeSearch, YouTubeVideoInfo } from './YouTubeAPI';
7
 import Video from './Video';
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
 class App extends Component {
10
 class App extends Component {
13
 	constructor(props) {
11
 	constructor(props) {
14
 		super(props);
12
 		super(props);
15
 		this.state = { result: [], links: [], value: '' };
13
 		this.state = { result: [], links: [], value: '' };
14
+		this.timeout = undefined;
16
 		this.options = [
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
 	batchLinks() {
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
 	inputChange(text) {
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
 	removeLink(link) {
39
 	removeLink(link) {
55
 
43
 
56
 	selectResult(video) {
44
 	selectResult(video) {
57
 		if (_.every(this.state.links, e => e.id !== video.id)) {
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
 				video.converted = _.find(this.props.converted, e => e.id === video.id)._id;
47
 				video.converted = _.find(this.props.converted, e => e.id === video.id)._id;
60
-			} else
48
+			else
61
 				Meteor.call('links.add', {video, type: 'audio'}, (err, res) => {
49
 				Meteor.call('links.add', {video, type: 'audio'}, (err, res) => {
62
 					if (err)
50
 					if (err)
63
 						console.error(err);
51
 						console.error(err);
70
 	}
58
 	}
71
 
59
 
72
 	search() {
60
 	search() {
73
-		if (!this.state.value) {
61
+		if (!this.state.value)
74
 			this.setState({ result: [] });
62
 			this.setState({ result: [] });
75
-		} else {
63
+		else
76
 			YouTubeSearch(this.state.value)
64
 			YouTubeSearch(this.state.value)
77
 				.then(e => e.items.map(e => new Video(e)))
65
 				.then(e => e.items.map(e => new Video(e)))
78
 				.then(e => { this.setState({ result: e }); return e; })
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
 					.then(() => this.setState({ result: e }))
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
 export default createContainer(() => {
85
 export default createContainer(() => {
92
 	Meteor.subscribe('links');
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 파일 보기

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 파일 보기

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
 Input.propTypes = {
7
 Input.propTypes = {
17
 	change: PropTypes.func.isRequired,
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 파일 보기

1
-import React, {Component, PropTypes} from 'react';
1
+import React, { Component, PropTypes } from 'react';
2
 import Video from './Video';
2
 import Video from './Video';
3
 import classnames from 'classnames';
3
 import classnames from 'classnames';
4
 
4
 
5
 export default class Options extends Component {
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
 	getProgress(type) {
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
 	isDisabled(type) {
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
 	getClasses(type) {
29
 	getClasses(type) {
23
 	}
35
 	}
24
 
36
 
25
 	getErrorMessage(type) {
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 파일 보기

22
 				<div className="label">ALL: {this.renderCount()}</div>
22
 				<div className="label">ALL: {this.renderCount()}</div>
23
 				<div>
23
 				<div>
24
 					{ _.map(this.props.handlers, (h) => (
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
 							<i className="fa fa-download"/> {h.type}
26
 							<i className="fa fa-download"/> {h.type}
27
 						</button>
27
 						</button>
28
 					)) }
28
 					)) }
38
 	converted: PropTypes.arrayOf(PropTypes.object),
38
 	converted: PropTypes.arrayOf(PropTypes.object),
39
 	handlers: PropTypes.arrayOf(PropTypes.shape({
39
 	handlers: PropTypes.arrayOf(PropTypes.shape({
40
 		type: PropTypes.string,
40
 		type: PropTypes.string,
41
-		handler: PropTypes.func
41
+		handlerAll: PropTypes.func,
42
 	})).isRequired,
42
 	})).isRequired,
43
 };
43
 };

+ 30
- 35
imports/ui/Result.jsx 파일 보기

1
-import React, {Component, PropTypes} from 'react';
1
+import React, { PropTypes } from 'react';
2
 import Video from './Video';
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
 					</div>
26
 					</div>
33
-				</div>
34
-			</a>
35
-		);
36
-	}
37
-}
27
+				) }
28
+			</div>
29
+		</div>
30
+	</a>;
38
 
31
 
39
 Result.propTypes = {
32
 Result.propTypes = {
40
 	result: PropTypes.instanceOf(Video).isRequired,
33
 	result: PropTypes.instanceOf(Video).isRequired,
41
 	click: PropTypes.func
34
 	click: PropTypes.func
42
-};
35
+};
36
+
37
+export default Result;

+ 1
- 0
imports/ui/YouTubeAPI.js 파일 보기

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

+ 1
- 0
launchpad.conf 파일 보기

1
+INSTALL_MONGO=true

+ 2
- 1
package.json 파일 보기

15
     "react": "^15.4.2",
15
     "react": "^15.4.2",
16
     "react-addons-pure-render-mixin": "^15.4.2",
16
     "react-addons-pure-render-mixin": "^15.4.2",
17
     "react-dom": "^15.4.2",
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
   "devDependencies": {
21
   "devDependencies": {
21
     "babel-plugin-transform-class-properties": "^6.23.0"
22
     "babel-plugin-transform-class-properties": "^6.23.0"

+ 0
- 4
packages.json 파일 보기

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

+ 64
- 0
server/Converter.js 파일 보기

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 파일 보기

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