├── .gitignore ├── .editorconfig ├── cli.js ├── package.json ├── readme.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var meow = require('meow'); 6 | var ripSubtitles = require('./'); 7 | 8 | var cli = meow({ 9 | help: [ 10 | 'Usage', 11 | ' $ rip-subtitles > ', 12 | '', 13 | 'Options', 14 | ' -l, --lang Subtitle language (eng, etc.)', 15 | ' -f, --format Format of subtitles (srt, ass, etc.)', 16 | '', 17 | 'Example', 18 | ' $rip-subtitles clip.mkv > subs.srt', 19 | '' 20 | ] 21 | }, { 22 | alias: { 23 | lang: 'l', 24 | format: 'f' 25 | } 26 | }); 27 | 28 | ripSubtitles(cli.input[0], cli.flags) 29 | .on('error', console.log) 30 | .pipe(process.stdout); 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rip-subtitles", 3 | "version": "0.0.1", 4 | "description": "Rip subtitles from video files", 5 | "main": "index.js", 6 | "bin": { 7 | "rip-subtitles": "cli.js" 8 | }, 9 | "files": [ 10 | "index.js", 11 | "cli.js" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/callmehiphop/rip-subtitles.git" 16 | }, 17 | "keywords": [ 18 | "rip", 19 | "extract", 20 | "subtitle", 21 | "subtitles", 22 | "mkv" 23 | ], 24 | "author": "Dave Gramlich ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/callmehiphop/rip-subtitles/issues" 28 | }, 29 | "homepage": "https://github.com/callmehiphop/rip-subtitles#readme", 30 | "dependencies": { 31 | "concat-stream": "^1.5.0", 32 | "is-video": "^1.0.1", 33 | "meow": "^3.3.0", 34 | "node-ffprobe": "^1.2.2", 35 | "object-assign": "^3.0.0", 36 | "through2": "^2.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # rip-subtitles 2 | 3 | > Rip subtitles from video files 4 | 5 | This module requires [ffmpeg](https://www.ffmpeg.org/) 6 | 7 | ## Install 8 | 9 | ```sh 10 | $ npm install --save rip-subtitles 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```javascript 16 | var ripSubtitles = require('rip-subtitles'); 17 | var fs = require('fs'); 18 | 19 | ripSubtitles('clip.mkv', function (err, subtitles) { 20 | fs.writeFile('subtitles.srt', subtitles, function (err) {}); 21 | }); 22 | 23 | // or if streams are your thing 24 | ripSubtitles('clip.mkv') 25 | .pipe(fs.createWriteStream('subtitles.srt')); 26 | ``` 27 | 28 | ## API 29 | 30 | ### ripSubtitles(filename, [options], [callback]) 31 | 32 | ## filename 33 | 34 | **Required** 35 | 36 | Type `string` 37 | 38 | Path to the video file 39 | 40 | ## options 41 | 42 | Type `object` 43 | 44 | Subtitle options 45 | 46 | #### lang 47 | 48 | Type `string` 49 | 50 | Default `eng` 51 | 52 | The chosen language - e.g. `eng` 53 | 54 | #### format 55 | 56 | Type `string` 57 | 58 | Default `srt` 59 | 60 | The subtitle format - e.g. `srt`, `webvtt` 61 | 62 | ## callback 63 | 64 | Type `function` 65 | 66 | A callback function - if not present a stream is returned 67 | 68 | ## CLI 69 | 70 | ```sh 71 | Usage 72 | $ rip-subtitles > 73 | 74 | Options 75 | -l, --lang Subtitle language (eng, etc.) 76 | -f, --format Format of subtitles (srt, ass, etc.) 77 | 78 | Example 79 | $rip-subtitles clip.mkv > subs.srt 80 | 81 | ``` 82 | 83 | ## Formats 84 | 85 | Formats available depend on your `ffmpeg` version, for a list use the following command 86 | 87 | ```sh 88 | $ ffmpeg -formats | grep "subtitle" 89 | ``` 90 | 91 | ## License 92 | 93 | MIT 94 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var probe = require('node-ffprobe'); 4 | var assign = require('object-assign'); 5 | var spawn = require('child_process').spawn; 6 | var through = require('through2'); 7 | var concat = require('concat-stream'); 8 | var isVideo = require('is-video'); 9 | 10 | var defaults = { 11 | lang: 'eng', 12 | format: 'srt' 13 | }; 14 | 15 | /** 16 | * Gets stream info for supplied video then tries to locate 17 | * the desired subtitle stream and spits it out in the specified format 18 | * 19 | * @throws {TypeError} if filename is not a valid video 20 | * @param {string} filename - path to the video 21 | * @param {object=} options - subtitle options 22 | * @param {string} options.lang - desired language 23 | * @param {string} options.format - desired format 24 | * @param {function=} callback - callback function 25 | * @return {stream} stream - only if a callback is not provided 26 | */ 27 | module.exports = function (filename, options, callback) { 28 | if (!isVideo(filename)) { 29 | throw new TypeError('invalid parameter `filename`'); 30 | } 31 | 32 | if (typeof options !== 'object') { 33 | callback = options; 34 | options = {}; 35 | } 36 | 37 | options = assign({}, defaults, options); 38 | 39 | var stream = through(); 40 | 41 | getSubtitleIndex(filename, options.lang, function (err, index) { 42 | if (err) { 43 | stream.emit('error', err); 44 | return stream.end(); 45 | } 46 | 47 | getSubtitleStream(filename, index, options.format) 48 | .on('error', stream.emit.bind(stream, 'error')) 49 | .pipe(stream); 50 | }); 51 | 52 | if (typeof callback !== 'function') { 53 | return stream; 54 | } 55 | 56 | stream 57 | .on('error', callback) 58 | .pipe(concat(function (subtitles) { 59 | callback(null, subtitles); 60 | })); 61 | }; 62 | 63 | /** 64 | * Attempts to get the index of the subtitle stream 65 | * The callback will pass back an error if we can't find it 66 | * 67 | * @param {string} filename - video file name 68 | * @param {string} lang - language of desired subtitle 69 | * @param {function} callback - rly 70 | */ 71 | function getSubtitleIndex (filename, lang, callback) { 72 | probe(filename, function (err, data) { 73 | if (err) { 74 | return callback(err); 75 | } 76 | 77 | var streams = data.streams.filter(function (stream) { 78 | var isSubtitle = stream.codec_type === 'subtitle'; 79 | var hasLang = stream['TAG:language'] === lang; 80 | 81 | return isSubtitle && hasLang; 82 | }); 83 | 84 | if (!streams.length) { 85 | return callback(new Error('subtitle not found for lang "' + lang + '"')); 86 | } 87 | 88 | callback(null, data.streams.indexOf(streams[0])); 89 | }); 90 | } 91 | 92 | /** 93 | * Streams the subtitle file in the desired format 94 | * 95 | * @param {string} filename - video filename 96 | * @param {number} index - index of desired subtitle stream 97 | * @param {string} format - subtitle format (srt, ass, etc.) 98 | * @return {stream} 99 | */ 100 | function getSubtitleStream (filename, index, format) { 101 | var stream = through(); 102 | 103 | var ffmpeg = spawn('ffmpeg', [ 104 | '-i', filename, 105 | '-an', '-vn', 106 | '-map', '0:' + index, 107 | '-c:s:0', format, 108 | '-f', format, 109 | 'pipe:1' 110 | ]); 111 | 112 | ffmpeg.on('error', stream.emit.bind(stream, 'error')); 113 | ffmpeg.on('exit', stream.end.bind(stream)); 114 | 115 | ffmpeg.stdout.pipe(stream); 116 | 117 | return stream; 118 | } 119 | --------------------------------------------------------------------------------