Your subs, now — from your console.
subcinode is a command-line tool that automatically finds and downloads the right subtitles for
your local video files. Point it at a folder, it scans for video files, identifies each one by its
content hash, asks OpenSubtitles for the best matching subtitle in
the languages you want, and drops the .srt next to the video.
- Content-hash matching — subtitles are matched to the exact release, not guessed from the filename.
- Batch + recursive — process a whole library in one run.
- Multi-language — download several languages per file in a single pass.
- Idempotent — files that already have a subtitle are skipped.
- Pluggable back-ends — OpenSubtitles today, other providers via a small interface.
For every video file found under the target path, subcinode:
- Hashes it with the OpenSubtitles/OSDb algorithm (file size + checksums of the first and last 64 KiB).
- Searches the provider for subtitles matching that hash, filtered to your languages.
- Downloads the most-downloaded subtitle per language to
Movie.<lang>.srt(or into asubs/sub-folder with--use-subs). - Tags the downloaded
.srtwith a single short caption in a silent gap (Downloaded with subcinode …).
A language is skipped when its target file already exists, so re-running is cheap.
- Node.js ≥ 20
- A free OpenSubtitles API key (see below). Anonymous search works, but downloads require a key, and the per-day download quota is tied to a (free) OpenSubtitles account.
-
Create a free account at https://www.opensubtitles.com.
-
Go to https://www.opensubtitles.com/consumers and register a new consumer — this gives you an API key.
-
Make the key (and, for downloads, your account login) available to
subcinodevia environment variables:export OPENSUBTITLES_API_KEY=your_api_key export OPENSUBTITLES_USERNAME=your_username # needed for the download quota export OPENSUBTITLES_PASSWORD=your_password
Put these in your shell profile (
~/.zshrc,~/.bashrc, …) to make them permanent.subcinodenever writes credentials to disk.
npm install --global subcinodeOr run it without installing:
npx subcinode --langs eng,itacd ~/Movies
export OPENSUBTITLES_API_KEY=your_api_key
subcinode --langs eng,itaThis scans ~/Movies (recursively), and for each .mp4 / .mkv / .avi downloads the best English
and Italian subtitles next to the video.
subcinode [--langs <list>] [--extensions <list>] [--path <dir>]
[--recursive | --no-recursive] [--use-subs]
[--provider <name>] [--save] [--settings] [--debug]| Option | Type | Default | Description |
|---|---|---|---|
--langs <list> |
string | all |
Comma-separated language codes to download. 2- and 3-letter codes are both accepted (en = eng). all downloads every available language. |
--extensions <list> |
string | mp4,mkv,avi |
Comma-separated video extensions to look for. |
--path <dir> |
string | current directory | Directory to scan. |
--recursive / --no-recursive |
boolean | true |
Descend into sub-folders. The output subs/ folder is always skipped. |
--use-subs |
flag | off | Save subtitles into a subs/ sub-folder instead of next to the video. |
--provider <name> |
string | opensubtitles |
Subtitle back-end to use. |
--save |
flag | – | Persist the supplied options to ./settings.json as the new defaults, then continue. |
--settings |
flag | – | Print the effective settings and exit. |
--debug |
flag | – | Verbose logging. |
--version / --help |
flag | – | Print version / help and exit. |
subcinode exits 0 on success and 1 if any download failed or a fatal error occurred (for
example, a missing API key).
The single-dash style from older versions still works and is mapped to the options above:
subcinode -langs=eng,ita -recursive=false -useSubs -path=/movies -save -debug -settingsRunning with --save writes the current options to settings.json in the working directory:
subcinode --save --langs eng,ita --no-recursive --extensions mp4After that, a bare subcinode in the same directory reuses those defaults. Precedence is:
built-in defaults < ./settings.json < command-line flags
settings.json is git-ignored by this repo and should not contain secrets — credentials always come
from the environment.
# Every language, default settings, current folder (recursive)
subcinode
# English + Italian for MP4/AVI in a specific folder, non-recursive
subcinode --langs eng,ita --no-recursive --extensions mp4,avi --path "/Users/me/Downloads"
# Keep subtitles in a subs/ folder
subcinode --langs eng --use-subs
# Save these as the defaults for this folder, then run
subcinode --save --langs eng,ita --no-recursive --extensions mp4
# Show what settings would be used
subcinode --settings--langs accepts ISO 639 2- or 3-letter codes. Common values:
| Language | Code | Language | Code | Language | Code |
|---|---|---|---|---|---|
| English | eng |
Italian | ita |
French | fre |
| German | ger |
Spanish | spa |
Portuguese | por |
| Portuguese (BR) | pob |
Dutch | dut |
Polish | pol |
| Russian | rus |
Arabic | ara |
Hebrew | heb |
| Greek | ell |
Turkish | tur |
Czech | cze |
| Danish | dan |
Finnish | fin |
Swedish | swe |
| Norwegian | nor |
Romanian | rum |
Hungarian | hun |
| Chinese | chi |
Japanese | jpn |
Korean | kor |
| Hindi | hin |
Thai | tha |
Vietnamese | vie |
| Indonesian | ind |
Ukrainian | ukr |
Croatian | hr |
The full list of codes OpenSubtitles supports is at https://www.opensubtitles.com/en/languages.
subcinode is plain ESM and can be driven from code:
import { run } from 'subcinode';
const result = await run(
{ cli: { langs: ['eng'], path: '/movies', useSubs: true } },
{ version: '2.0.0', env: process.env }
);
console.log(result.downloaded); // string[] of written paths
console.log(result.errors); // [{ file, error }]A provider is a class implementing three async methods:
class MyProvider {
get name() { return 'myprovider'; }
// Validate credentials, obtain tokens, etc.
async init() {}
// fileInfo: { moviehash, moviebytesize }
// opts: { languages: string[] | string } ("all" / [] means every language)
// returns: [{ langId, fileId, fileName, downloadCount }]
async search(fileInfo, opts) {}
// result: one entry from search()
// returns: { url, fileName }
async resolveDownloadUrl(result) {}
}Register it, then select it with --provider:
import { registerProvider } from 'subcinode/providers';
import { MyProvider } from './my-provider.js';
registerProvider('myprovider', MyProvider);git clone https://github.com/alexis89x/subcinode.git
cd subcinode
npm install
npm test # node:test suite, no test framework neededLayout:
bin/subcinode.js CLI entry point (arg parsing + wiring)
src/run.js orchestrator: scan → hash → search → download → tag
src/config.js defaults, settings.json, credentials from env
src/args.js commander setup + legacy-flag shim
src/files.js directory walking, filename helpers, language normalisation
src/hash.js OSDb movie hash
src/download.js streaming HTTPS download (redirects, gzip, atomic-ish)
src/promo.js the "Downloaded with subcinode" caption
src/providers/ subtitle back-ends (opensubtitles.js) + registry
test/ one *.test.js per module
- Rewritten as ESM with
async/await, split into small single-purpose modules; requires Node ≥ 20. - Switched to the OpenSubtitles REST API — the legacy XML-RPC endpoint was retired. An API key is now required.
- Pluggable provider layer (
src/providers/,subcinode/providers). - Modern
--flag valueCLI viacommander; legacy single-dash flags still accepted. - Dropped the
async,http,jsonfileandsubtitles-parserdependencies. Movie hashing is now built in; SRT handling uses the maintainedsubtitlepackage. - Fixed: crash on the default
langs: allpath; an always-true language filter; a broken-extensions=parser; HTTP downloads of HTTPS links; a read-before-write race on the subtitle tag; mutation of the shared defaults object. - Removed references to the defunct
www.subcino.com. - Added a
node:testtest suite.
- Fixed dependency problems
- Fixed
endsWithproblem for some users
- Removed unnecessary files
- Added subtitle parsing
- Added
-settingsparameter
- Minor documentation fixes
- Major version bump
- Fixed
-langssettings bug - Added the ability to save default settings
- Initial development: directory-tree navigation, full workflow, global install, license/docs.
- Alessandro Piana (@alexis89x) — lead
- Matteo Silvestri (@matteosilv)
- Paola Piatti
MIT © 2015 Alessandro Piana