Added support for muxing streams, specifically for reddit-hosted videos (now also supported).
This commit is contained in:
@@ -5,6 +5,7 @@ const util = require('util');
|
||||
const fs = require('fs-extra');
|
||||
const yargs = require('yargs').argv;
|
||||
const snoowrap = require('snoowrap');
|
||||
const promiseFinally = require('promise.prototype.finally');
|
||||
|
||||
const reddit = new snoowrap(config.reddit.api);
|
||||
|
||||
@@ -20,6 +21,8 @@ const fetchContent = require('./fetch/content.js');
|
||||
const save = require('./save/save.js');
|
||||
const saveProfileDetails = require('./save/profileDetails.js');
|
||||
|
||||
promiseFinally.shim();
|
||||
|
||||
const limit = yargs.limit || config.fetch.limit;
|
||||
|
||||
if(!yargs.user && typeof yargs.users !== 'string') {
|
||||
|
||||
@@ -25,7 +25,7 @@ function curateSubmissions(submissions) {
|
||||
title: submission.title,
|
||||
text: submission.selftext,
|
||||
user: submission.author.name,
|
||||
permalink: submission.permalink,
|
||||
permalink: 'https://reddit.com' + submission.permalink,
|
||||
url: submission.url,
|
||||
datetime: new Date(submission.created_utc * 1000),
|
||||
subreddit: submission.subreddit.display_name,
|
||||
|
||||
@@ -7,13 +7,17 @@ const hosts = [{
|
||||
label: 'self',
|
||||
pattern: new urlPattern('http(s)\\://(www.)reddit.com/r/:subreddit/comments/:id/:uri/')
|
||||
}, {
|
||||
method: 'reddit',
|
||||
method: 'redditImage',
|
||||
label: 'reddit',
|
||||
pattern: new urlPattern('http(s)\\://i.redd.it/:id.:ext')
|
||||
}, {
|
||||
method: 'reddit',
|
||||
method: 'redditImage',
|
||||
label: 'reddit',
|
||||
pattern: new urlPattern('https\\://i.reddituploads.com/:id(?*)')
|
||||
pattern: new urlPattern('http(s)\\://i.reddituploads.com/:id(?*)')
|
||||
}, {
|
||||
method: 'redditVideo',
|
||||
label: 'reddit',
|
||||
pattern: new urlPattern('http(s)\\://v.redd.it/:id')
|
||||
}, {
|
||||
method: 'imgurImage',
|
||||
label: 'imgur',
|
||||
|
||||
@@ -9,6 +9,7 @@ const interpolate = require('../interpolate.js');
|
||||
const save = require('../save/save.js');
|
||||
const textToStream = require('../save/textToStream.js');
|
||||
const saveMeta = require('../save/meta.js');
|
||||
const mux = require('../save/mux.js');
|
||||
|
||||
const exiftool = require('node-exiftool');
|
||||
const exiftoolBin = require('dist-exiftool');
|
||||
@@ -20,19 +21,20 @@ module.exports = function(posts, user) {
|
||||
return ep.open();
|
||||
}).then(() => {
|
||||
return Promise.all(posts.map(post => {
|
||||
return Promise.resolve().then(() => {
|
||||
return Promise.all(post.content.items.map((item, index) => {
|
||||
item.index = index;
|
||||
return Promise.all(post.content.items.map((item, index) => {
|
||||
item.index = index;
|
||||
|
||||
if(item.self) {
|
||||
return Object.assign({}, item, {stream: textToStream(item.text)});
|
||||
}
|
||||
if(item.self) {
|
||||
return Object.assign({}, item, {stream: textToStream(item.text)});
|
||||
}
|
||||
|
||||
return fetchItem(item.url, 0).then(stream => {
|
||||
return Object.assign({}, item, {stream});
|
||||
});
|
||||
}));
|
||||
}).then(items => {
|
||||
// some videos are delivered with separate audio and are fetched separately to be muxed later
|
||||
const sources = item.mux ? [item.url].concat(item.mux) : [item.url];
|
||||
|
||||
return Promise.all(sources.map(source => {
|
||||
return fetchItem(source, 0);
|
||||
})).then(streams => Object.assign({}, item, {streams}));
|
||||
})).then(items => {
|
||||
return Promise.all(items.map(item => {
|
||||
const type = item.type.split('/')[0];
|
||||
const filepath = post.content.album ? interpolate(config.library.album[type], user, post, item) : interpolate(config.library[type], user, post, item);
|
||||
@@ -40,7 +42,11 @@ module.exports = function(posts, user) {
|
||||
return Promise.resolve().then(() => {
|
||||
return fs.ensureDir(path.dirname(filepath));
|
||||
}).then(() => {
|
||||
return save(filepath, item.stream);
|
||||
return save(filepath, item);
|
||||
}).then(sourcePaths => {
|
||||
if(item.mux) {
|
||||
return mux(sourcePaths, filepath, item);
|
||||
}
|
||||
}).then(() => {
|
||||
const meta = Object.entries(config.library.meta).reduce((acc, [key, value]) => {
|
||||
const interpolatedValue = interpolate(value, user, post, item);
|
||||
@@ -59,9 +65,7 @@ module.exports = function(posts, user) {
|
||||
}));
|
||||
});
|
||||
}));
|
||||
}).then(() => {
|
||||
return ep.close();
|
||||
}).catch(error => {
|
||||
}).finally(() => {
|
||||
return ep.close();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const self = require('./self.js');
|
||||
const reddit = require('./reddit.js');
|
||||
const redditImage = require('./redditImage.js');
|
||||
const redditVideo = require('./redditVideo.js');
|
||||
const imgurImage = require('./imgurImage.js');
|
||||
const imgurAlbum = require('./imgurAlbum.js');
|
||||
const gfycat = require('./gfycat.js');
|
||||
@@ -9,7 +10,8 @@ const eroshare = require('./eroshare.js');
|
||||
|
||||
module.exports = {
|
||||
self: self,
|
||||
reddit: reddit,
|
||||
redditImage: redditImage,
|
||||
redditVideo: redditVideo,
|
||||
imgurImage: imgurImage,
|
||||
imgurAlbum: imgurAlbum,
|
||||
gfycat: gfycat,
|
||||
|
||||
@@ -4,7 +4,7 @@ const util = require('util');
|
||||
const config = require('config');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
function reddit(post) {
|
||||
function redditImage(post) {
|
||||
return Promise.resolve({
|
||||
album: null,
|
||||
items: [{
|
||||
@@ -18,4 +18,4 @@ function reddit(post) {
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = reddit;
|
||||
module.exports = redditImage;
|
||||
38
src/methods/redditVideo.js
Normal file
38
src/methods/redditVideo.js
Normal file
@@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
const util = require('util');
|
||||
const config = require('config');
|
||||
const fetch = require('node-fetch');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
function redditVideo(post) {
|
||||
return fetch(`${post.permalink}.json`).then(res => res.json()).then(res => {
|
||||
return res[0].data.children[0].data.media.reddit_video.fallback_url;
|
||||
}).then(videoUrl => {
|
||||
const audioUrl = videoUrl.split('/').slice(0, -1).join('/') + '/audio';
|
||||
|
||||
return fetch(audioUrl, {
|
||||
method: 'HEAD'
|
||||
}).then(res => {
|
||||
const item = {
|
||||
album: null,
|
||||
items: [{
|
||||
id: post.host.id || post.id,
|
||||
url: videoUrl,
|
||||
title: post.title,
|
||||
datetime: post.datetime,
|
||||
type: 'video/mp4',
|
||||
original: post
|
||||
}]
|
||||
};
|
||||
|
||||
if(res.status === 200) {
|
||||
item.items[0].mux = [audioUrl];
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = redditVideo;
|
||||
26
src/save/mux.js
Normal file
26
src/save/mux.js
Normal file
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
function mux(sources, target, item) {
|
||||
return new Promise((resolve, reject) => {
|
||||
return sources.reduce((acc, source) => {
|
||||
return acc.input(source);
|
||||
}, ffmpeg()).videoCodec('copy').audioCodec('copy').on('start', cmd => {
|
||||
console.log('\x1b[36m%s\x1b[0m', `Muxing ${sources.length} streams to '${target}'`);
|
||||
}).on('end', (stdout) => {
|
||||
console.log('\x1b[32m%s\x1b[0m', `Muxed and saved '${target}'`);
|
||||
|
||||
resolve(stdout);
|
||||
}).on('error', error => reject).save(target);
|
||||
}).then(() => {
|
||||
return Promise.all(sources.map(source => {
|
||||
return fs.remove(source);
|
||||
})).then(() => {
|
||||
console.log('\x1b[36m%s\x1b[0m', `Cleaned up temporary files for '${target}'`);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = mux;
|
||||
@@ -2,22 +2,32 @@
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
|
||||
function save(filepath, item) {
|
||||
const pathComponents = path.parse(filepath);
|
||||
|
||||
function save(filepath, stream) {
|
||||
return Promise.resolve().then(() => {
|
||||
return fs.ensureDir(path.dirname(filepath));
|
||||
return fs.ensureDir(path.dirname(pathComponents.dir));
|
||||
}).then(() => {
|
||||
const file = fs.createWriteStream(filepath);
|
||||
return Promise.all(item.streams.map((stream, index) => {
|
||||
const target = item.streams.length > 1 ? path.join(pathComponents.dir, `${pathComponents.name}-${index}${pathComponents.ext}`) : filepath;
|
||||
const file = fs.createWriteStream(target);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.pipe(file).on('error', error => {
|
||||
reject(error);
|
||||
}).on('finish', () => {
|
||||
console.log('\x1b[32m%s\x1b[0m', `Saved '${filepath}'`);
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.pipe(file).on('error', error => {
|
||||
reject(error);
|
||||
}).on('finish', () => {
|
||||
if(item.mux) {
|
||||
console.log(`Temporarily saved '${target}', queued for muxing`);
|
||||
} else {
|
||||
console.log('\x1b[32m%s\x1b[0m', `Saved '${target}'`);
|
||||
}
|
||||
|
||||
resolve(filepath);
|
||||
resolve(target);
|
||||
});
|
||||
});
|
||||
});
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user