├── .gitignore
├── README.md
├── bower.json
├── css
└── style.css
├── gulpfile.js
├── img
└── icons
│ └── arrow_forward.svg
├── index.html
├── main.js
├── package.json
└── scripts
├── app.js
├── config.js
├── formattedTimestamps
├── formattedTimestamps.html
├── formattedTimestampsController.js
└── formattedTimestampsService.js
├── pipelines.js
├── pipelines
├── mkvPipeline.js
└── mp4Pipeline.js
├── timestampsFiltering
├── timestampsFiltering.html
├── timestampsFilteringController.js
└── timestampsFilteringService.js
├── videoProcessing
├── videoProcessing.html
├── videoProcessingController.js
└── videoProcessingService.js
├── videoSelect
├── videoSelect.html
├── videoSelectController.js
└── videoSelectService.js
└── wizard
├── wizard.html
└── wizardController.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 | bower_components
4 | tmp
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | cooptional-timestamps
2 | =====================
3 |
4 | A small tool to generate a table for Reddit with timestamps to specific topics in Co-optional Podcast episodes (based on the text at the bottom of the video updated by TB).
5 |
6 | Prerequisites
7 | =============
8 |
9 | * [ffmpeg](https://www.ffmpeg.org/)
10 | * [Tesseract OCR](https://github.com/tesseract-ocr/tesseract)
11 |
12 | You need both those utilites to be installed on your system and have [PATH](https://en.wikipedia.org/wiki/PATH_(variable)) variable configured with them.
13 |
14 | Usage
15 | =====
16 |
17 | Run the app, provide the link, fiddle with settings, clean up the output.
18 |
19 | Tutorial: http://imgur.com/a/PwYEk
20 |
21 | License
22 | =======
23 |
24 | MIT
25 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cooptional-timestamps",
3 | "description": "",
4 | "main": "main.js",
5 | "authors": [
6 | "Xylem"
7 | ],
8 | "license": "MIT",
9 | "homepage": "https://github.com/Xylem/cooptional-timestamps",
10 | "moduleType": [
11 | "node"
12 | ],
13 | "ignore": [
14 | "**/.*",
15 | "node_modules",
16 | "bower_components",
17 | "test",
18 | "tests"
19 | ],
20 | "dependencies": {
21 | "angular-route": "~1.4.7",
22 | "angular-material": "1.0.0-rc4",
23 | "angular-messages": "~1.4.7",
24 | "angular-motion": "~0.4.3"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/css/style.css:
--------------------------------------------------------------------------------
1 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
2 | display: none !important;
3 | }
4 |
5 | main {
6 | height: calc(100vh - 48px);
7 | }
8 |
9 | textarea {
10 | resize: none;
11 | white-space: nowrap;
12 | }
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var childProcess = require('child_process');
3 | var electron = require('electron-prebuilt');
4 |
5 | gulp.task('run', function () {
6 | childProcess.spawn(electron, [ '.' ], {
7 | stdio: 'inherit'
8 | });
9 | });
--------------------------------------------------------------------------------
/img/icons/arrow_forward.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Co-optional Timestamps
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | var app = require('app');
2 | var BrowserWindow = require('browser-window');
3 |
4 | var mainWindow = null;
5 |
6 | app.on('window-all-closed', function() {
7 | if (process.platform != 'darwin') {
8 | app.quit();
9 | }
10 | });
11 |
12 | app.on('ready', function() {
13 | mainWindow = new BrowserWindow({width: 800, height: 600});
14 |
15 | mainWindow.loadUrl('file://' + __dirname + '/index.html');
16 |
17 | mainWindow.setMenu(null);
18 |
19 | mainWindow.on('closed', function() {
20 | mainWindow = null;
21 | });
22 | });
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cooptional-timestamps",
3 | "version": "1.0.1",
4 | "description": "",
5 | "main": "main.js",
6 | "author": "Xylem",
7 | "license": "MIT",
8 | "repository": "Xylem/cooptional-timestamps",
9 | "dependencies": {
10 | "bluebird": "2.10.1",
11 | "bluebird-retry": "0.5.1",
12 | "fast-levenshtein": "1.0.7",
13 | "fluent-ffmpeg": "2.0.1",
14 | "lodash": "3.10.1",
15 | "matroska": "2.2.2",
16 | "mkdirp": "0.5.1",
17 | "node-tesseract": "0.2.7",
18 | "printf": "0.2.3",
19 | "rimraf": "2.4.3",
20 | "stream-to-promise": "1.0.4",
21 | "ytdl-core": "0.6.0"
22 | },
23 | "devDependencies": {
24 | "electron-prebuilt": "^0.34.1",
25 | "gulp": "3.9.0"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/scripts/app.js:
--------------------------------------------------------------------------------
1 | (function (angular) {
2 | 'use strict';
3 |
4 | angular.module('app', ['ngRoute', 'ngMaterial', 'ngAnimate', 'ngMessages']).
5 | config(['$routeProvider', '$mdThemingProvider', function ($routeProvider, $mdThemingProvider) {
6 | $mdThemingProvider.theme('default')
7 | .primaryPalette('blue')
8 | .accentPalette('orange');
9 |
10 | $routeProvider.when('/', {
11 | templateUrl: './scripts/wizard/wizard.html' ,
12 | controller: 'wizardController'
13 | });
14 |
15 | $routeProvider.otherwise({ redirectTo: '/' });
16 | }
17 | ]);
18 |
19 | })(window.angular);
--------------------------------------------------------------------------------
/scripts/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | temporaryDirectory: './tmp',
5 | headerPath: './tmp/header.mp4',
6 | videoPath: './tmp/video.mp4'
7 | };
--------------------------------------------------------------------------------
/scripts/formattedTimestamps/formattedTimestamps.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/scripts/formattedTimestamps/formattedTimestampsController.js:
--------------------------------------------------------------------------------
1 | (function (angular) {
2 | 'use strict';
3 | angular.module('app')
4 | .controller('formattedTimestampsController', ['$scope', 'formattedTimestampsService',
5 | FormattedTimestampsController]);
6 |
7 | function FormattedTimestampsController($scope, formattedTimestampsService) {
8 | var listHeader = 'Approximate timestamps to specific topics\n\n \n\nTopic|Timestamp\n-|-\n';
9 | var listFooter = '\n\n \n\n^^Prepared ^^using ^^https://github.com/Xylem/cooptional-timestamps';
10 |
11 | var timestamps = formattedTimestampsService.format($scope.videoData.filteredTimestamps, $scope.videoData.url);
12 |
13 | $scope.timestampList = listHeader + timestamps + listFooter;
14 | }
15 |
16 | })(window.angular);
--------------------------------------------------------------------------------
/scripts/formattedTimestamps/formattedTimestampsService.js:
--------------------------------------------------------------------------------
1 | (function (angular) {
2 | 'use strict';
3 |
4 | var url = require('url');
5 | var printf = require('printf');
6 |
7 | angular.module('app').service('formattedTimestampsService', FormattedTimestampsService);
8 |
9 | function FormattedTimestampsService() {
10 | return {
11 | format: format
12 | };
13 |
14 | function format(timestamps, videoUrl) {
15 | var rawVideoUrl = url.parse(videoUrl, true);
16 |
17 | return timestamps.map(function (timestamp) {
18 | var timestampUrl = angular.copy(rawVideoUrl);
19 | timestampUrl.query.t = timestamp.timestamp;
20 | delete timestampUrl.search;
21 |
22 | return printf('%s|[%s](%s)', timestamp.text, timestamp.time, url.format(timestampUrl));
23 | }).join('\n');
24 | }
25 | }
26 | })(window.angular);
--------------------------------------------------------------------------------
/scripts/pipelines.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _ = require('lodash');
4 |
5 | var PIPELINES = [
6 | require('./pipelines/mkvPipeline'),
7 | require('./pipelines/mp4Pipeline')
8 | ];
9 |
10 | exports.getBestPipeline = function (videoDescriptor) {
11 | return _.find(PIPELINES, function (pipeline) {
12 | return _.find(videoDescriptor.formats, pipeline.requirements);
13 | });
14 | };
--------------------------------------------------------------------------------
/scripts/pipelines/mkvPipeline.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Promise = require('bluebird');
4 |
5 | var fs = Promise.promisifyAll(require('fs'));
6 | var path = require('path');
7 |
8 | var _ = require('lodash');
9 | var ffmpeg = require('fluent-ffmpeg');
10 | var matroska = Promise.promisifyAll(require('matroska'));
11 | var mkdirp = Promise.promisify(require('mkdirp'));
12 | var retry = require('bluebird-retry');
13 | var rimraf = require('rimraf');
14 | var streamToPromise = require('stream-to-promise');
15 | var tesseract = Promise.promisifyAll(require('node-tesseract'));
16 | var ytdl = Promise.promisifyAll(require('ytdl-core'));
17 |
18 | var config = require('../config');
19 |
20 | exports.process = function (url, progressCallback) {
21 | var timestampPromise = mkdirp(config.temporaryDirectory).then(function () {
22 | return ytdl.getInfoAsync(url);
23 | }).then(function(info) {
24 | var length = info.length_seconds;
25 | var webmInfo = _.find(info.formats, {
26 | itag: '247'
27 | });
28 |
29 | return {
30 | headerEnd: parseInt(webmInfo.index.split('-')[1], 10),
31 | size: parseInt(webmInfo.clen, 10),
32 | length: parseInt(length, 10)
33 | }
34 | }).then(function (videoInfo) {
35 | progressCallback('register', {
36 | name: 'Downloading video header',
37 | maxProgress: videoInfo.headerEnd
38 | });
39 |
40 | var downloadStream = ytdl(url, {
41 | quality: '247',
42 | range: '0-' + (videoInfo.headerEnd + 1)
43 | });
44 |
45 | downloadStream.on('data', function (chunk) {
46 | progressCallback('progress', chunk.length);
47 | });
48 |
49 | var stream = fs.createWriteStream(config.headerPath);
50 |
51 | return streamToPromise(downloadStream.pipe(stream));
52 | }).then(function () {
53 | var decoder = new matroska.Decoder();
54 |
55 | return decoder.parseAsync(config.headerPath);
56 | }).then(function (document) {
57 | var segment = document.getFirstChildByName('Segment');
58 |
59 | var totalSize = segment.getDataSize();
60 |
61 | return segment.getFirstChildByName('Cues')
62 | .listChildrenByName('CuePoint').map(function (cuePoint) {
63 | return {
64 | time: cuePoint.getFirstChildByName('CueTime').getUInt(),
65 | byte: cuePoint.getFirstChildByName('CueTrackPositions')
66 | .getFirstChildByName('CueClusterPosition')
67 | .getUInt()
68 | };
69 | }).map(function (chunk, index, array) {
70 | var nextChunk = array[index + 1];
71 |
72 | chunk.end = nextChunk ? nextChunk.byte : totalSize;
73 |
74 | return chunk;
75 | }).filter(function (chunk, index) {
76 | return index % 2 === 0;
77 | });
78 | }).then(function (chunks) {
79 | progressCallback('register', {
80 | name: 'Downloading and processing video chunks',
81 | maxProgress: chunks.length
82 | });
83 |
84 | return chunks;
85 | }).map(function (chunk) {
86 | return retry(function () {
87 | var PATH = config.temporaryDirectory + '/' + chunk.time + '.webm';
88 | var OUTPUT = config.temporaryDirectory + '/' + chunk.time + '.png';
89 |
90 | var stream = fs.createWriteStream(PATH);
91 |
92 | fs.createReadStream(config.headerPath).pipe(stream);
93 |
94 | return streamToPromise(stream).then(function () {
95 | stream = fs.createWriteStream(PATH, {
96 | flags: 'a'
97 | });
98 |
99 | var ytdlStream = ytdl(url, {
100 | quality: '247',
101 | range: chunk.byte + '-' + chunk.end
102 | });
103 |
104 | var streamPromise = streamToPromise(ytdlStream);
105 |
106 | ytdlStream.pipe(stream);
107 |
108 | return streamPromise;
109 | }).then(function () {
110 | return new Promise(function (resolve, reject) {
111 | ffmpeg(PATH)
112 | .complexFilter([
113 | 'crop=in_w:30:0:in_h-30[cropped]',
114 | '[cropped]lutrgb=r=negval:g=negval:b=negval[inverted]',
115 | '[inverted]scale=4*in_w:4*in_h'
116 | ])
117 | .on('end', resolve)
118 | .on('error', resolve)
119 | .output(OUTPUT)
120 | .run();
121 | });
122 | }).then(function () {
123 | return tesseract.processAsync(OUTPUT, {
124 | psm: 7
125 | }).then(function (text) {
126 | return {
127 | timestamp: _.floor(chunk.time / 1000),
128 | text: text
129 | };
130 | }).error(function () {});
131 | });
132 | }).then(function (data) {
133 | progressCallback('progress', 1);
134 |
135 | return data;
136 | });
137 | }, {
138 | concurrency: 10
139 | }).reduce(function (a, b) {
140 | return a.concat(b);
141 | }, []).then(function (timestamps) {
142 | return _.chain(timestamps)
143 | .filter(function (value) {
144 | return value !== undefined;
145 | })
146 | .each(function (value) {
147 | value.text = value.text.replace(/\n/g, '');
148 | })
149 | .sortBy('timestamp')
150 | .value();
151 | });
152 |
153 | timestampPromise.then(function () {
154 | rimraf.sync(config.temporaryDirectory);
155 | });
156 |
157 | return timestampPromise;
158 | };
159 |
160 | exports.requirements = {
161 | itag: '247'
162 | };
163 |
164 | exports.name = "WebM/VP9 720p";
--------------------------------------------------------------------------------
/scripts/pipelines/mp4Pipeline.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Promise = require('bluebird');
4 |
5 | var fs = Promise.promisifyAll(require('fs'));
6 | var path = require('path');
7 |
8 | var _ = require('lodash');
9 | var ffmpeg = require('fluent-ffmpeg');
10 | var mkdirp = Promise.promisify(require('mkdirp'));
11 | var rimraf = require('rimraf');
12 | var streamToPromise = require('stream-to-promise');
13 | var tesseract = Promise.promisifyAll(require('node-tesseract'));
14 | var ytdl = Promise.promisifyAll(require('ytdl-core'));
15 |
16 | var config = require('../config');
17 |
18 | exports.process = function (url, progressCallback) {
19 | var timestampPromise = mkdirp(config.temporaryDirectory)
20 | .then(function () {
21 | return ytdl.getInfoAsync(url);
22 | }).then(function (info) {
23 | var videoInfo = _.find(info.formats, {
24 | itag: '136'
25 | });
26 |
27 | progressCallback('register', {
28 | name: 'Downloading video',
29 | maxProgress: parseInt(videoInfo.clen, 10)
30 | });
31 |
32 | var downloadStream = ytdl(url, {
33 | quality: '136'
34 | });
35 |
36 | downloadStream.on('data', function (chunk) {
37 | progressCallback('progress', chunk.length);
38 | });
39 |
40 | var videoStream = fs.createWriteStream(config.videoPath);
41 |
42 | return streamToPromise(downloadStream.pipe(videoStream));
43 | }).then(function () {
44 | progressCallback('register', {
45 | name: 'Generating frames'
46 | });
47 |
48 | return new Promise(function (resolve, reject) {
49 | ffmpeg(config.videoPath)
50 | .fps(0.1)
51 | .complexFilter([
52 | 'crop=in_w:30:0:in_h-30[cropped]',
53 | '[cropped]lutrgb=r=negval:g=negval:b=negval[inverted]',
54 | '[inverted]scale=4*in_w:4*in_h'
55 | ])
56 | .output(config.temporaryDirectory + '/%d0.png')
57 | .on('end', resolve)
58 | .on('error', reject)
59 | .run();
60 | });
61 | }).then(function () {
62 | var framesPromise = fs.readdirAsync(config.temporaryDirectory).filter(function (file) {
63 | return path.extname(file) === '.png';
64 | });
65 |
66 | framesPromise.then(function (frames) {
67 | progressCallback('register', {
68 | name: 'Reading data from frames',
69 | maxProgress: frames.length
70 | });
71 | });
72 |
73 | return framesPromise.map(function (file) {
74 | var timestamp = path.basename(file, '.png');
75 | var filePath = path.join(config.temporaryDirectory, file);
76 |
77 | return tesseract.processAsync(filePath, {
78 | psm: 7
79 | }).then(function (text) {
80 | progressCallback('progress', 1);
81 |
82 | return {
83 | timestamp: parseInt(timestamp, 10),
84 | text: text
85 | };
86 | });
87 | }, {
88 | concurrency: 10
89 | }).reduce(function (a, b) {
90 | return a.concat(b);
91 | }, []);
92 | })
93 | .then(function (timestamps) {
94 | return _.chain(timestamps)
95 | .filter(function (value) {
96 | return value !== undefined;
97 | })
98 | .each(function (value) {
99 | value.text = value.text.replace(/\n/g, '');
100 | })
101 | .sortBy('timestamp')
102 | .value();
103 | });
104 |
105 | timestampPromise.then(function () {
106 | rimraf.sync(config.temporaryDirectory);
107 | });
108 |
109 | return timestampPromise;
110 | };
111 |
112 | exports.requirements = {
113 | itag: '136'
114 | };
115 |
116 | exports.name = "MP4/H.264 720p";
--------------------------------------------------------------------------------
/scripts/timestampsFiltering/timestampsFiltering.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ setting.name }}
6 |
7 | {{ setting.tooltip }}
8 |
9 |
10 |
11 |
12 |
13 |
14 | {{setting.unit}}
15 |
16 |
17 |
18 |
19 |
20 |
21 | Remaining timestamps
22 |
23 |
24 |
25 |
{{ item.text }}
26 |
{{ formatAsTime(item.timestamp) }}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/scripts/timestampsFiltering/timestampsFilteringController.js:
--------------------------------------------------------------------------------
1 | (function (angular) {
2 | 'use strict';
3 | angular.module('app')
4 | .controller('timestampsFilteringController', ['$scope', 'timestampsFilteringService', TimestampsFilteringController]);
5 |
6 | function TimestampsFilteringController($scope, timestampsFilteringService) {
7 | $scope.filterSettings = {
8 | minimalTextLength: 10,
9 | minimalLetterContent: 70,
10 | minimalLevenshteinDistance: 9,
11 | timestampsOffset: 40
12 | };
13 |
14 | $scope.settingsList = [
15 | {
16 | name: 'Minimal text length',
17 | tooltip: 'Minimal length of the timestamp text for it to be considered valid',
18 | property: 'minimalTextLength',
19 | rangeMin: 0,
20 | rangeMax: 30
21 | },
22 | {
23 | name: 'Minimal letter content',
24 | tooltip: 'Minimal ratio of letters in timestamp to the entire timestamp text length (removes garbage)',
25 | property: 'minimalLetterContent',
26 | rangeMin: 0,
27 | rangeMax: 100,
28 | unit: '%'
29 | },
30 | {
31 | name: 'Minimal Levenshtein distance',
32 | tooltip: 'Minimal level of difference between subsequent timestamps required to detect topic change',
33 | property: 'minimalLevenshteinDistance',
34 | rangeMin: 0,
35 | rangeMax: 30
36 | },
37 | {
38 | name: 'Timestamps offset',
39 | tooltip: 'Number of seconds to offset timestamps backwards to account for lag between topic indicator updates and actual topic changes',
40 | property: 'timestampsOffset',
41 | rangeMin: 0,
42 | rangeMax: 300,
43 | unit: 's'
44 | }
45 | ];
46 |
47 | $scope.remainingTimestamps = function () {
48 | var filteredTimestamps = timestampsFilteringService.filterTimestamps($scope.videoData.timestamps,
49 | $scope.filterSettings);
50 |
51 | $scope.videoData.filteredTimestamps = angular.copy(filteredTimestamps);
52 |
53 | $scope.videoData.filteredTimestamps.forEach(function (timestamp) {
54 | timestamp.time = timestampsFilteringService.formatAsTime(timestamp.timestamp,
55 | $scope.filterSettings.timestampsOffset);
56 | timestamp.timestamp -= $scope.filterSettings.timestampsOffset;
57 | });
58 |
59 | return filteredTimestamps;
60 | };
61 |
62 | $scope.formatAsTime = function (timestamp) {
63 | return timestampsFilteringService.formatAsTime(timestamp, $scope.filterSettings.timestampsOffset);
64 | };
65 | }
66 |
67 | })(window.angular);
--------------------------------------------------------------------------------
/scripts/timestampsFiltering/timestampsFilteringService.js:
--------------------------------------------------------------------------------
1 | (function (angular) {
2 | 'use strict';
3 |
4 | var _ = require('lodash');
5 | var levenshtein = require('fast-levenshtein');
6 | var printf = require('printf');
7 |
8 | var LETTERS = /[a-zA-Z]/g;
9 |
10 | angular.module('app').service('timestampsFilteringService', TimestampsFilteringService);
11 |
12 | function TimestampsFilteringService() {
13 | return {
14 | filterTimestamps: filterTimestamps,
15 | formatAsTime: formatAsTime
16 | };
17 |
18 | function filterTimestamps(timestamps, settings) {
19 | var lastTopic = '';
20 |
21 | return _.chain(timestamps)
22 | .filter(function (value) {
23 | return value.text.length >= settings.minimalTextLength;
24 | })
25 | .filter(function (value) {
26 | var lettersInLine = value.text.match(LETTERS);
27 |
28 | return lettersInLine &&
29 | lettersInLine.length / value.text.length >= settings.minimalLetterContent / 100;
30 | })
31 | .filter(function (value) {
32 | if (levenshtein.get(lastTopic, value.text) > settings.minimalLevenshteinDistance) {
33 | lastTopic = value.text;
34 | return true;
35 | }
36 | })
37 | .value();
38 | }
39 |
40 | function formatAsTime(timestamp, offset) {
41 | var time = Math.max(0, timestamp - offset);
42 |
43 | var second = time % 60;
44 | var minute = ((time - second) % 3600) / 60;
45 | var hour = (time - second - 60 * minute) / 3600;
46 |
47 | return printf('%02d:%02d:%02d', hour, minute, second);
48 | }
49 | }
50 | })(window.angular);
--------------------------------------------------------------------------------
/scripts/videoProcessing/videoProcessing.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | There is no appropriate video format available to start processing. Please try again later.
4 |
5 |
6 | Using {{ pipeline.name }} pipeline
7 | {{ progressText }}
8 |
9 |
10 |
--------------------------------------------------------------------------------
/scripts/videoProcessing/videoProcessingController.js:
--------------------------------------------------------------------------------
1 | (function (angular) {
2 | 'use strict';
3 | angular.module('app')
4 | .controller('videoProcessingController', ['$scope', 'videoProcessingService', VideoProcessingController]);
5 |
6 | function VideoProcessingController($scope, videoProcessingService) {
7 | $scope.videoData.timestamps = null;
8 | $scope.pipeline = videoProcessingService.getBestPipeline($scope.videoData.descriptors);
9 |
10 | var progress = 0;
11 | $scope.maxProgress = 0;
12 |
13 | function progressUpdate(type, data) {
14 | $scope.$apply(function () {
15 | switch (type) {
16 | case 'register':
17 | $scope.progressText = data.name;
18 | $scope.progressValue = 0;
19 | progress = 0;
20 | $scope.maxProgress = data.maxProgress;
21 | break;
22 | case 'progress':
23 | progress += data;
24 | $scope.progressValue = progress * 100 / $scope.maxProgress;
25 | break;
26 | }
27 | });
28 | }
29 |
30 | if ($scope.pipeline) {
31 | $scope.pipeline.process($scope.videoData.url, progressUpdate).then(function (timestamps) {
32 | $scope.$apply(function () {
33 | $scope.videoData.timestamps = timestamps;
34 | });
35 | }).catch(function () {
36 | $scope.$apply(function () {
37 | $scope.videoData.timestamps = null;
38 | });
39 | });
40 | }
41 | }
42 |
43 | })(window.angular);
--------------------------------------------------------------------------------
/scripts/videoProcessing/videoProcessingService.js:
--------------------------------------------------------------------------------
1 | (function (angular) {
2 | 'use strict';
3 |
4 | var pipelines = require('./scripts/pipelines');
5 |
6 | angular.module('app').service('videoProcessingService', VideoProcessingService);
7 |
8 | function VideoProcessingService() {
9 | return {
10 | getBestPipeline: getBestPipeline
11 | };
12 |
13 | function getBestPipeline(videoDescriptor) {
14 | return pipelines.getBestPipeline(videoDescriptor);
15 | }
16 | }
17 | })(window.angular);
--------------------------------------------------------------------------------
/scripts/videoSelect/videoSelect.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/videoSelect/videoSelectController.js:
--------------------------------------------------------------------------------
1 | (function (angular) {
2 | 'use strict';
3 | angular.module('app')
4 | .controller('videoSelectController', ['$scope', 'videoSelectService', VideoSelectController]);
5 |
6 | function VideoSelectController($scope, videoSelectService) {
7 | $scope.loadVideoData = function () {
8 | videoSelectService.getVideoData($scope.videoData.url).then(function (data) {
9 | $scope.$apply(function () {
10 | $scope.videoData.descriptors = data;
11 | });
12 | }).catch(function () {
13 | $scope.$apply(function () {
14 | $scope.videoData.descriptors = null;
15 | });
16 | });
17 | };
18 | }
19 |
20 | })(window.angular);
--------------------------------------------------------------------------------
/scripts/videoSelect/videoSelectService.js:
--------------------------------------------------------------------------------
1 | (function (angular) {
2 | 'use strict';
3 |
4 | var Promise = require('bluebird');
5 |
6 | var ytdl = Promise.promisifyAll(require('ytdl-core'));
7 |
8 | angular.module('app').service('videoSelectService', VideoSelectService);
9 |
10 | function VideoSelectService() {
11 | return {
12 | getVideoData: getVideoData
13 | };
14 |
15 | function getVideoData(url) {
16 | return ytdl.getInfoAsync(url);
17 | }
18 | }
19 | })(window.angular);
--------------------------------------------------------------------------------
/scripts/wizard/wizard.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/scripts/wizard/wizardController.js:
--------------------------------------------------------------------------------
1 | (function (angular) {
2 | 'use strict';
3 |
4 | var _ = require('lodash');
5 |
6 | angular.module('app')
7 | .controller('wizardController', ['$scope', WizardController]);
8 |
9 | function WizardController($scope) {
10 | $scope.tabs = [
11 | {
12 | title: 'Select video',
13 | content: 'videoSelect',
14 | provides: [ 'descriptors' ]
15 | },
16 | {
17 | title: 'Video processing',
18 | content: 'videoProcessing',
19 | provides: [ 'timestamps' ]
20 | },
21 | {
22 | title: 'Timestamps filtering',
23 | content: 'timestampsFiltering',
24 | provides: [ 'filteredTimestamps' ]
25 | },
26 | {
27 | title: 'Formatted timestamps',
28 | content: 'formattedTimestamps',
29 | provides: [ ]
30 | }
31 | ];
32 |
33 | $scope.videoData = {};
34 |
35 | $scope.tabReady = function (tabIndex) {
36 | var tab = $scope.tabs[tabIndex];
37 |
38 | return tab && _.every(tab.provides, function (property) {
39 | var videoDataProperty = $scope.videoData[property];
40 | return videoDataProperty !== null && videoDataProperty !== undefined;
41 | });
42 | };
43 |
44 | $scope.nextStep = function () {
45 | ++$scope.selectedIndex;
46 | };
47 | }
48 |
49 | })(window.angular);
--------------------------------------------------------------------------------