Benoit Sida 3 år sedan
förälder
incheckning
65c5e1e49c

+ 5
- 1
build/Dockerfile Visa fil

@@ -1,4 +1,8 @@
1
-FROM node
1
+FROM ubuntu:16.04
2
+RUN apt-get update
3
+RUN apt-get install -y curl ffmpeg build-essential python libssl-dev
4
+RUN curl -sL https://deb.nodesource.com/setup_6.x | bash -
5
+RUN apt-get install -y nodejs
2 6
 RUN mkdir -p /usr/src/app
3 7
 COPY bundle/ /usr/src/app/
4 8
 WORKDIR /usr/src/app/

Binär
build/SoundWave.tar.gz Visa fil


+ 6
- 0
imports/api/security.js Visa fil

@@ -0,0 +1,6 @@
1
+import { Meteor } from 'meteor/meteor';
2
+
3
+if (Meteor.isServer) {
4
+	if (!process.env.API_KEY) throw new Error("You must fill the API_KEY env variable !");
5
+	Meteor.methods({ 'security.key'() { return { key: process.env.API_KEY}; } });
6
+}

+ 2
- 10
imports/ui/App.jsx Visa fil

@@ -13,16 +13,8 @@ class App extends Component {
13 13
 		this.state = { result: [], links: [], value: '' };
14 14
 		this.timeout = undefined;
15 15
 		this.options = [
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
-			},
16
+			{ type: 'MP3', handler: v => window.open('/mp3/'+ (v ? v.id : `batch/${this.batchLinks()}`)) },
17
+			{ type: 'MP4', handler: v => window.open('/mp4/'+ (v ? v.id : `batch/${this.batchLinks()}`)) },
26 18
 		];
27 19
 		_.each(['inputChange', 'selectResult', 'removeLink'], f => this[f] = this[f].bind(this));
28 20
 	}

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

@@ -18,7 +18,6 @@ const AppLayout = (props) =>
18 18
 		</div>
19 19
 	</div>;
20 20
 
21
-
22 21
 AppLayout.propTypes = {
23 22
 	links: Links.propTypes.links,
24 23
 	linkRemove: Links.propTypes.remove,

+ 4
- 4
imports/ui/Input.jsx Visa fil

@@ -1,13 +1,13 @@
1
-import React, { PropTypes } from 'react';
1
+import React from 'react';
2 2
 
3 3
 const Input = ({ value, change, placeholder }) =>
4 4
 	<input type="text" className="new-link" placeholder={placeholder} autoFocus
5 5
 		   value={value} onChange={e => change(e.target.value)} />;
6 6
 
7 7
 Input.propTypes = {
8
-	change: PropTypes.func.isRequired,
9
-	value: PropTypes.string.isRequired,
10
-	placeholder: PropTypes.string
8
+	change: React.PropTypes.func.isRequired,
9
+	value: React.PropTypes.string.isRequired,
10
+	placeholder: React.PropTypes.string
11 11
 };
12 12
 
13 13
 export default Input;

+ 17
- 23
imports/ui/Link.jsx Visa fil

@@ -1,33 +1,27 @@
1
-import React, {Component, PropTypes} from 'react';
1
+import React from 'react';
2 2
 import { createContainer } from 'meteor/react-meteor-data';
3 3
 import { Converted } from '../api/links';
4 4
 import Options from './Options';
5 5
 import Video from './Video';
6 6
 
7
-class Link extends Component {
8
-	render() {
9
-		return (
10
-			<li className="link">
11
-				<span className="link-text" title={this.props.link.title}>{this.props.link.title}</span>
12
-				<Options className="link-options" handlers={this.props.options}
13
-						 video={this.props.link} converted={this.props.converted}/>
14
-				<button className="link-delete" onClick={this.props.remove}>&times;</button>
15
-			</li>
16
-		);
17
-	}
18
-}
7
+const Link = ({ link, remove, options, converted }) =>
8
+	<li className="link">
9
+		<span className="link-text" title={link.title}>{link.title}</span>
10
+		<Options className="link-options" handlers={options} video={link} converted={converted}/>
11
+		<button className="link-delete" onClick={remove}>&times;</button>
12
+	</li>;
19 13
 
20 14
 Link.propTypes = {
21
-	link: PropTypes.instanceOf(Video).isRequired,
22
-	remove: PropTypes.func.isRequired,
23
-	options: PropTypes.arrayOf(PropTypes.shape({
24
-		type: PropTypes.string,
25
-		handler: PropTypes.func
15
+	link: React.PropTypes.instanceOf(Video).isRequired,
16
+	remove: React.PropTypes.func.isRequired,
17
+	options: React.PropTypes.arrayOf(React.PropTypes.shape({
18
+		type: React.PropTypes.string,
19
+		handler: React.PropTypes.func
26 20
 	})).isRequired,
27
-	converted: PropTypes.object
21
+	converted: React.PropTypes.object
28 22
 };
29 23
 
30
-export default createContainer(({link, converted_id, remove, options}) => {
31
-	const converted = Converted.findOne(converted_id);
32
-	return { link, remove, options, converted }
33
-}, Link);
24
+export default createContainer(({ link, converted_id, remove, options }) => ({
25
+	converted: Converted.findOne(converted_id),
26
+	link, remove, options,
27
+}), Link);

+ 17
- 20
imports/ui/Links.jsx Visa fil

@@ -1,27 +1,24 @@
1
-import React, {Component, PropTypes} from 'react';
1
+import React from 'react';
2 2
 import { createContainer } from 'meteor/react-meteor-data';
3 3
 import Link from './Link';
4 4
 import Video from './Video';
5 5
 
6
-export default class Links extends Component {
7
-	render() {
8
-		return (
9
-			<ul>
10
-				{ this.props.links.map(link => (
11
-					<Link key={link.id} converted_id={link.converted}
12
-						  link={link} options={this.props.handlers}
13
-						  remove={() => this.props.remove(link)}/>
14
-				)) }
15
-			</ul>
16
-		);
17
-	}
18
-}
6
+const Links = ({ links, remove, handlers }) =>
7
+	<ul>
8
+		{ links.map(link => (
9
+			<Link key={link.id} converted_id={link.converted}
10
+				  link={link} options={handlers}
11
+				  remove={() => remove(link)}/>
12
+		)) }
13
+	</ul>;
19 14
 
20 15
 Links.propTypes = {
21
-	links: PropTypes.arrayOf(PropTypes.instanceOf(Video)).isRequired,
22
-	remove: PropTypes.func.isRequired,
23
-	handlers: PropTypes.arrayOf(PropTypes.shape({
24
-		type: PropTypes.string,
25
-		handler: PropTypes.func
16
+	links: React.PropTypes.arrayOf(React.PropTypes.instanceOf(Video)).isRequired,
17
+	remove: React.PropTypes.func.isRequired,
18
+	handlers: React.PropTypes.arrayOf(React.PropTypes.shape({
19
+		type: React.PropTypes.string,
20
+		handler: React.PropTypes.func
26 21
 	})).isRequired
27
-};
22
+};
23
+
24
+export default Links;

+ 15
- 15
imports/ui/Options.jsx Visa fil

@@ -3,21 +3,6 @@ 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
-	}
20
-
21 6
 	getProgress(type) {
22 7
 		return !this.props.converted ? 0 : this.props.converted[type === 'MP3' ? 'audio_progress' : 'video_progress'];
23 8
 	}
@@ -38,6 +23,21 @@ export default class Options extends Component {
38 23
 		let typestr = (type === 'MP3' ? 'audio' : 'video');
39 24
 		return this.props.converted && this.props.converted[`error_${typestr}`] || null;
40 25
 	}
26
+
27
+	render() {
28
+		return (
29
+			<div>
30
+				{ _.map(this.props.handlers, (h) => (
31
+					<button key={h.type} className={this.getClasses(h.type)}
32
+							onClick={() => h.handler(this.props.video)}
33
+							disabled={this.isDisabled(h.type)} title={this.getErrorMessage(h.type)}>
34
+						<div className="option-progress" style={{ width: this.getProgress(h.type) + '%' }} />
35
+						<i className="fa fa-download" /> {h.type}
36
+					</button>
37
+				)) }
38
+			</div>
39
+		);
40
+	}
41 41
 }
42 42
 
43 43
 Options.propTypes = {

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

@@ -2,7 +2,6 @@ import React, {Component, PropTypes} from 'react';
2 2
 import Video from './Video';
3 3
 
4 4
 export default class OptionsAll extends Component {
5
-
6 5
 	renderCount() {
7 6
 		return (this.props.count || 0) + ' link' + (this.props.count > 1 ? 's' : '' );
8 7
 	}
@@ -22,7 +21,7 @@ export default class OptionsAll extends Component {
22 21
 				<div className="label">ALL: {this.renderCount()}</div>
23 22
 				<div>
24 23
 					{ _.map(this.props.handlers, (h) => (
25
-						<button key={h.type} className="option" onClick={h.handlerAll} disabled={this.isDisabled(h.type)}>
24
+						<button key={h.type} className="option" onClick={() => h.handler()} disabled={this.isDisabled(h.type)}>
26 25
 							<i className="fa fa-download"/> {h.type}
27 26
 						</button>
28 27
 					)) }
@@ -38,6 +37,6 @@ OptionsAll.propTypes = {
38 37
 	converted: PropTypes.arrayOf(PropTypes.object),
39 38
 	handlers: PropTypes.arrayOf(PropTypes.shape({
40 39
 		type: PropTypes.string,
41
-		handlerAll: PropTypes.func,
40
+		handler: PropTypes.func,
42 41
 	})).isRequired,
43 42
 };

+ 5
- 19
imports/ui/Result.jsx Visa fil

@@ -1,14 +1,6 @@
1
-import React, { PropTypes } from 'react';
1
+import React from 'react';
2 2
 import Video from './Video';
3
-_ = lodash;
4
-
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
-};
3
+import Stats from './Stats';
12 4
 
13 5
 const Result = ({ click, result }) =>
14 6
 	<a className="result-link" onClick={click}>
@@ -19,19 +11,13 @@ const Result = ({ click, result }) =>
19 11
 				<div className="result-title">{result.title}</div>
20 12
 				<div className="result-duration">{result.duration}</div>
21 13
 			</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}
26
-					</div>
27
-				) }
28
-			</div>
14
+			<Stats stats={result.stats} prefix="result"/>
29 15
 		</div>
30 16
 	</a>;
31 17
 
32 18
 Result.propTypes = {
33
-	result: PropTypes.instanceOf(Video).isRequired,
34
-	click: PropTypes.func
19
+	result: React.PropTypes.instanceOf(Video).isRequired,
20
+	click: React.PropTypes.func
35 21
 };
36 22
 
37 23
 export default Result;

+ 11
- 16
imports/ui/Results.jsx Visa fil

@@ -1,21 +1,16 @@
1
-import React, {Component, PropTypes} from 'react';
1
+import React from 'react';
2 2
 import Video from './Video';
3 3
 import Result from './Result';
4 4
 
5
-export default class Results extends Component {
6
-	render() {
7
-		if (!this.props.results.length) return null;
8
-		return (
9
-			<ul id="search-result">
10
-				{ this.props.results.map(res => (
11
-					<Result key={res.id} result={res} click={() => this.props.select(res) }/>
12
-				)) }
13
-			</ul>
14
-		);
15
-	}
16
-}
5
+const Results = ({ results, select }) => (!results.length ? null :
6
+	<ul id="search-result">
7
+		{ results.map(res => <Result key={res.id} result={res} click={() => select(res) }/>) }
8
+	</ul>
9
+);
17 10
 
18 11
 Results.propTypes = {
19
-	results: PropTypes.arrayOf(PropTypes.instanceOf(Video)).isRequired,
20
-	select: PropTypes.func.isRequired
21
-};
12
+	results: React.PropTypes.arrayOf(React.PropTypes.instanceOf(Video)).isRequired,
13
+	select: React.PropTypes.func.isRequired
14
+};
15
+
16
+export default Results;

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

@@ -0,0 +1,34 @@
1
+import React from 'react';
2
+
3
+const Stats = ({ stats, prefix }) =>
4
+	<div className={`${prefix}-stats`}>
5
+		<div className={`${prefix}-views`} title={stats.viewCount} >
6
+			<i className="fa fa-eye"/> {stats.viewCount}
7
+		</div>
8
+		<div className={`${prefix}-likes`} title={stats.likeCount} >
9
+			<i className="fa fa-thumbs-o-up"/> {stats.likeCount}
10
+		</div>
11
+		<div className={`${prefix}-dislikes`} title={stats.dislikeCount} >
12
+			<i className="fa fa-thumbs-o-down"/> {stats.dislikeCount}
13
+		</div>
14
+		<div className={`${prefix}-comments`} title={stats.commentCount} >
15
+			<i className="fa fa-comments-o"/> {stats.commentCount}
16
+		</div>
17
+		<div className={`${prefix}-favorites`} title={stats.favoriteCount} >
18
+			<i className="fa fa-star-o"/> {stats.favoriteCount}
19
+		</div>
20
+	</div>;
21
+
22
+const CountType = React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]);
23
+Stats.propTypes = {
24
+	prefix: React.PropTypes.string.isRequired,
25
+	stats: React.PropTypes.shape({
26
+		viewCount: CountType,
27
+		likeCount: CountType,
28
+		dislikeCount: CountType,
29
+		commentCount: CountType,
30
+		favoriteCount: CountType,
31
+	}).isRequired
32
+};
33
+
34
+export default Stats;

+ 16
- 30
imports/ui/YouTubeAPI.js Visa fil

@@ -1,43 +1,29 @@
1
+import { Meteor } from 'meteor/meteor';
1 2
 import 'whatwg-fetch';
2 3
 import querystring from 'querystring';
3
-_ = lodash;
4 4
 
5
-const KEY = 'AIzaSyDh641dLzxYexCASA3TdYMlc6zzS793ppQ';
5
+let KEY = '';
6
+Meteor.call('security.key', (err, res) => KEY = res.key);
6 7
 const ENDPOINT = 'https://www.googleapis.com/youtube/v3';
7 8
 
8
-const checkStatus = (response) => {
9
-	if (response.status >= 200 && response.status < 300) {
10
-		return response
11
-	} else {
12
-		let error = new Error(response.statusText);
13
-		error.response = response;
14
-		throw error
15
-	}
9
+const checkStatus = response => {
10
+	if (response.status >= 200 && response.status < 300) return response;
11
+	let error = new Error(response.statusText);
12
+	error.response = response;
13
+	throw error
16 14
 };
17 15
 
18 16
 const parseJson = response => response.json();
19 17
 
20
-export function YouTubeSearch(query) {
21
-	const params = {
22
-		part: 'snippet',
23
-		type: 'video',
24
-		key: KEY,
25
-		q: query
26
-	};
27
-
28
-	return fetch(`${ENDPOINT}/search?${querystring.stringify(params)}`)
18
+const YouTubeAPIQuery = ({ action, params}) =>
19
+	fetch(`${ENDPOINT}/${action}?${querystring.stringify(params)}`)
29 20
 		.then(checkStatus)
30 21
 		.then(parseJson);
31
-}
32 22
 
33
-export function YouTubeVideoInfo(id) {
34
-	const params = {
35
-		part: 'statistics,contentDetails',
36
-		key: KEY,
37
-		id: id
38
-	};
23
+const YouTubeSearch = query => YouTubeAPIQuery({ action: 'search',
24
+	params: { part: 'snippet', type: 'video', key: KEY, q: query }});
39 25
 
40
-	return fetch(`${ENDPOINT}/videos?${querystring.stringify(params)}`)
41
-		.then(checkStatus)
42
-		.then(parseJson);
43
-}
26
+const YouTubeVideoInfo = id => YouTubeAPIQuery({ action: 'videos',
27
+	params: { part: 'statistics,contentDetails', key: KEY, id: id }});
28
+
29
+export { YouTubeSearch, YouTubeVideoInfo };

+ 1
- 0
server/main.js Visa fil

@@ -1,5 +1,6 @@
1 1
 import { Meteor } from 'meteor/meteor';
2 2
 import '../imports/api/links.js';
3
+import '../imports/api/security.js';
3 4
 import '../imports/startup/routes';
4 5
 
5 6
 Meteor.startup(() => {});