opensubtitles
Read-only OpenSubtitles skill to search and download subtitles via API, then extract scene context by timestamp to answer user questions regarding a show in context and avoid spoilers. Use when the user asks “what’s happening at this timestamp” or needs subtitle context; pairs well with trakt-readonly skill for playback progress. Requires an OpenSubtitles API key and User-Agent.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install openclaw-skills-opensubtitles
Repository
Skill path: skills/dennisooki/opensubtitles
Read-only OpenSubtitles skill to search and download subtitles via API, then extract scene context by timestamp to answer user questions regarding a show in context and avoid spoilers. Use when the user asks “what’s happening at this timestamp” or needs subtitle context; pairs well with trakt-readonly skill for playback progress. Requires an OpenSubtitles API key and User-Agent.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack, Backend.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: openclaw.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install opensubtitles into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding opensubtitles to shared team environments
- Use opensubtitles for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: opensubtitles
description: Read-only OpenSubtitles skill to search and download subtitles via API, then extract scene context by timestamp to answer user questions regarding a show in context and avoid spoilers. Use when the user asks “what’s happening at this timestamp” or needs subtitle context; pairs well with trakt-readonly skill for playback progress. Requires an OpenSubtitles API key and User-Agent.
metadata: {"openclaw":{"requires":{"env":["OPENSUBTITLES_API_KEY","OPENSUBTITLES_USER_AGENT"],"bins":["curl","jq","awk"]},"primaryEnv":"OPENSUBTITLES_API_KEY","emoji":"📝"}}
---
# OpenSubtitles
Use this skill to fetch subtitle context around a timestamp. This is **read‑only**: no uploads or modifications.
## Setup
Users should obtain an API key at: https://www.opensubtitles.com/consumers
Required env vars:
- `OPENSUBTITLES_API_KEY`
- `OPENSUBTITLES_USER_AGENT` (e.g., `OpenClaw 1.0`)
Optional (for downloads):
- `OPENSUBTITLES_USERNAME`
- `OPENSUBTITLES_PASSWORD`
- `OPENSUBTITLES_TOKEN` (if already logged in)
- `OPENSUBTITLES_BASE_URL` (hostname from login response, e.g., `api.opensubtitles.com`)
## Commands
Scripts live in `{baseDir}/scripts/`.
### Search subtitles
```
{baseDir}/scripts/opensubtitles-api.sh search --query "Show Name" --season 3 --episode 5 --languages en
```
Prefer IDs when available (imdb/tmdb). Use parent IDs for TV episodes. Follow redirects for search (script already uses `-L`).
### Login (token)
```
{baseDir}/scripts/opensubtitles-api.sh login
```
Note: Login is rate limited (1/sec, 10/min, 30/hour). If you get 401, stop retrying.
Use `base_url` from the response as `OPENSUBTITLES_BASE_URL` for subsequent requests.
### Request a download link
Before downloading, check the local subtitle cache (see below). Only call the download API if the file is not already cached.
```
OPENSUBTITLES_TOKEN=... {baseDir}/scripts/opensubtitles-api.sh download-link --file-id 123
```
### Extract context at timestamp
After downloading an `.srt` file (default window: 10 minutes before timestamp):
On Windows, use `findstr` or PowerShell `Select-String` to replicate the awk logic (use the shell script as a guide). The agent should pick the best option available on that system.
```
{baseDir}/scripts/subtitle-context.sh ./subtitle.srt 00:12:34,500
```
Custom window:
```
{baseDir}/scripts/subtitle-context.sh ./subtitle.srt 00:12:34,500 --window-mins 5
```
## Trakt synergy
Pair with `trakt-readonly` to identify current episode; when Trakt adds playback progress support, update the Trakt skill to supply a precise timestamp for context‑aware, spoiler‑safe responses.
## Cache
Store downloaded subtitles under `{baseDir}/storage/subtitles/` (create if missing). Use a stable filename like:
`{baseDir}/storage/subtitles/<file_id>__<language>.srt`
Check this cache before calling `/download` to avoid wasting limited daily downloads.
## Guardrails
- Never log or expose API keys, passwords, or tokens.
- Only call `https://api.opensubtitles.com/api/v1` (or the `base_url` returned by login).
- Do not cache download links.
- Prefer IDs over fuzzy queries for accuracy.
- **Avoid wasting downloads:** store downloaded subtitle files locally and check the cache before calling the download API again.
- If a download is requested, always append the remaining download quota (from the `remaining` field) in the user response.
- Subtitle context should include the window from 10 minutes before the timestamp to the timestamp (default), unless the user specifies otherwise.
- The agent can adjust `--window-mins` to get more or less context as needed.
- Only read subtitle files from `{baseDir}/storage/subtitles/` to avoid arbitrary file access.
## References
- `references/opensubtitles-api.md`
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/opensubtitles-api.md
```markdown
# OpenSubtitles API (Read-only) Reference
Base URL: `https://api.opensubtitles.com/api/v1`
## Required headers
- `Api-Key: <OPENSUBTITLES_API_KEY>`
- `User-Agent: <APP_NAME> <APP_VERSION>`
- For download requests: `Authorization: Bearer <TOKEN>`
## Key endpoints
### Search subtitles
`GET /subtitles`
Useful params:
- `query` (text)
- `languages` (comma-separated, alphabetical; e.g., `en,fr`)
- `imdb_id`, `tmdb_id`
- For TV: `parent_imdb_id` or `parent_tmdb_id` + `season_number` + `episode_number`
Notes:
- If you can provide IDs, prefer them over query strings.
- If a moviehash is available, include it.
- Avoid ordering unless needed; it reduces cache hits.
- Follow HTTP redirects for search (use `curl -L`).
- To reduce redirects, send sorted, lowercase parameters without defaults.
### Download link
`POST /download`
Body:
- `file_id` (required)
- Optional: `sub_format`, `file_name`, `in_fps`, `out_fps`, `timeshift`, `force_download`
Important:
- Must include **both** `Api-Key` and `Authorization` headers.
- Download link is temporary (~3 hours). Do not cache.
- Download count is incremented when requesting the link.
### Login / Logout (token)
`POST /login` (username + password) returns `token` and `base_url`.
`DELETE /logout` destroys token.
Rate limits for login: **1 req/sec, 10/min, 30/hour**. If 401, stop retrying.
If `base_url` is `vip-api.opensubtitles.com`, include JWT token on all requests.
## Formats
`GET /infos/formats` lists available subtitle formats.
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### _meta.json
```json
{
"owner": "dennisooki",
"slug": "opensubtitles",
"displayName": "OpenSubtitles Read-only",
"latest": {
"version": "1.0.4",
"publishedAt": 1772164943995,
"commit": "https://github.com/openclaw/skills/commit/2655955a49e3b12128dc2131179624007b23ba2c"
},
"history": []
}
```
### scripts/opensubtitles-api.sh
```bash
#!/bin/bash
set -euo pipefail
API_ROOT="https://api.opensubtitles.com/api/v1"
base_url() {
if [[ -n "${OPENSUBTITLES_BASE_URL:-}" ]]; then
echo "https://${OPENSUBTITLES_BASE_URL}/api/v1"
else
echo "${API_ROOT}"
fi
}
print_usage() {
cat <<EOF
Usage: opensubtitles-api.sh <command> [options]
Commands:
search Search subtitles (read-only)
login Get user token (required for downloads)
download-link Request a temporary download URL (requires token)
Environment:
OPENSUBTITLES_API_KEY (required)
OPENSUBTITLES_USER_AGENT (required, e.g., "OpenClaw 1.0")
OPENSUBTITLES_USERNAME (required for login)
OPENSUBTITLES_PASSWORD (required for login)
OPENSUBTITLES_TOKEN (optional; use instead of login)
OPENSUBTITLES_BASE_URL (optional; from login response)
Examples:
OPENSUBTITLES_API_KEY=xxx OPENSUBTITLES_USER_AGENT="OpenClaw 1.0" \
./opensubtitles-api.sh search --query "Game of Thrones" --season 3 --episode 5 --languages en
OPENSUBTITLES_API_KEY=xxx OPENSUBTITLES_USER_AGENT="OpenClaw 1.0" \
OPENSUBTITLES_USERNAME=user OPENSUBTITLES_PASSWORD=pass ./opensubtitles-api.sh login
OPENSUBTITLES_API_KEY=xxx OPENSUBTITLES_USER_AGENT="OpenClaw 1.0" \
OPENSUBTITLES_TOKEN=token ./opensubtitles-api.sh download-link --file-id 123
EOF
}
check_bins() {
local bins=("curl" "jq")
for b in "${bins[@]}"; do
if ! command -v "$b" >/dev/null 2>&1; then
echo "Error: missing dependency '$b'" >&2
exit 1
fi
done
}
check_api_key() {
if [[ -z "${OPENSUBTITLES_API_KEY:-}" ]]; then
echo "Error: OPENSUBTITLES_API_KEY not set" >&2
exit 1
fi
}
check_user_agent() {
if [[ -z "${OPENSUBTITLES_USER_AGENT:-}" ]]; then
echo "Error: OPENSUBTITLES_USER_AGENT not set" >&2
exit 1
fi
}
api_get() {
local base="$1"
local path="$2"
shift 2
local qs="$*"
local url="${base}${path}"
if [[ -n "$qs" ]]; then
url+="?${qs}"
fi
curl -s -L \
-H "Accept: application/json" \
-H "Api-Key: ${OPENSUBTITLES_API_KEY}" \
-H "User-Agent: ${OPENSUBTITLES_USER_AGENT}" \
"$url"
}
api_post() {
local base="$1"
local path="$2"
local body="$3"
local auth_header="$4"
curl -s \
-H "Accept: application/json" \
-H "Api-Key: ${OPENSUBTITLES_API_KEY}" \
-H "User-Agent: ${OPENSUBTITLES_USER_AGENT}" \
-H "Content-Type: application/json" \
${auth_header:+-H "Authorization: Bearer ${auth_header}"} \
-d "$body" \
"${base}${path}"
}
parse_args() {
local -n _query=$1
local -n _languages=$2
local -n _imdb_id=$3
local -n _tmdb_id=$4
local -n _parent_imdb_id=$5
local -n _parent_tmdb_id=$6
local -n _season=$7
local -n _episode=$8
local -n _file_id=$9
shift 9
while [[ $# -gt 0 ]]; do
case "$1" in
--query) _query="$2"; shift 2;;
--languages) _languages="$2"; shift 2;;
--imdb-id) _imdb_id="$2"; shift 2;;
--tmdb-id) _tmdb_id="$2"; shift 2;;
--parent-imdb-id) _parent_imdb_id="$2"; shift 2;;
--parent-tmdb-id) _parent_tmdb_id="$2"; shift 2;;
--season) _season="$2"; shift 2;;
--episode) _episode="$2"; shift 2;;
--file-id) _file_id="$2"; shift 2;;
*) echo "Unknown arg: $1" >&2; exit 1;;
esac
done
}
format_search() {
local json="$1"
if echo "$json" | jq -e '.error' >/dev/null 2>&1; then
echo "Error: $(echo "$json" | jq -r '.message // .error')" >&2
exit 1
fi
echo "🎬 Subtitles (top 5):"
echo ""
echo "$json" | jq -r '.data[:5][] | "- \(.attributes.release) | \(.attributes.language) | file_id=\(.attributes.files[0].file_id) | \(.attributes.files[0].file_name)"'
}
main() {
if [[ $# -lt 1 ]]; then
print_usage
exit 1
fi
check_bins
check_api_key
check_user_agent
local command="$1"
shift
case "$command" in
search)
local query="" languages="" imdb_id="" tmdb_id="" parent_imdb_id="" parent_tmdb_id="" season="" episode=""
local file_id=""
parse_args query languages imdb_id tmdb_id parent_imdb_id parent_tmdb_id season episode file_id "$@"
local qs=()
[[ -n "$query" ]] && qs+=("query=$(jq -nr --arg s "$query" '$s|@uri')")
[[ -n "$languages" ]] && qs+=("languages=$(jq -nr --arg s "$languages" '$s|@uri')")
[[ -n "$imdb_id" ]] && qs+=("imdb_id=${imdb_id}")
[[ -n "$tmdb_id" ]] && qs+=("tmdb_id=${tmdb_id}")
[[ -n "$parent_imdb_id" ]] && qs+=("parent_imdb_id=${parent_imdb_id}")
[[ -n "$parent_tmdb_id" ]] && qs+=("parent_tmdb_id=${parent_tmdb_id}")
[[ -n "$season" ]] && qs+=("season_number=${season}")
[[ -n "$episode" ]] && qs+=("episode_number=${episode}")
local json
local qs_str=""
if (( ${#qs[@]} )); then
qs_str="${qs[0]}"
for ((i=1;i<${#qs[@]};i++)); do
qs_str+="&${qs[i]}"
done
fi
json=$(api_get "$(base_url)" "/subtitles" "$qs_str")
format_search "$json"
;;
login)
if [[ -z "${OPENSUBTITLES_USERNAME:-}" || -z "${OPENSUBTITLES_PASSWORD:-}" ]]; then
echo "Error: OPENSUBTITLES_USERNAME and OPENSUBTITLES_PASSWORD required for login" >&2
exit 1
fi
local body
body=$(jq -nr --arg u "$OPENSUBTITLES_USERNAME" --arg p "$OPENSUBTITLES_PASSWORD" '{username:$u,password:$p}')
api_post "$API_ROOT" "/login" "$body" "" | jq
;;
download-link)
local query="" languages="" imdb_id="" tmdb_id="" parent_imdb_id="" parent_tmdb_id="" season="" episode=""
local file_id=""
parse_args query languages imdb_id tmdb_id parent_imdb_id parent_tmdb_id season episode file_id "$@"
if [[ -z "$file_id" ]]; then
echo "Error: --file-id required" >&2
exit 1
fi
local token="${OPENSUBTITLES_TOKEN:-}"
if [[ -z "$token" ]]; then
echo "Error: OPENSUBTITLES_TOKEN not set. Run login and pass the token." >&2
exit 1
fi
local body
body=$(jq -nr --argjson id "$file_id" '{file_id:$id}')
api_post "$(base_url)" "/download" "$body" "$token" | jq
;;
help|--help|-h)
print_usage
;;
*)
echo "Error: Unknown command '$command'" >&2
print_usage
exit 1
;;
esac
}
main "$@"
```
### scripts/subtitle-context.sh
```bash
#!/bin/bash
set -euo pipefail
usage() {
cat <<EOF
Usage: subtitle-context.sh <srt_file> <timestamp> [--window-mins N]
Timestamp formats: HH:MM:SS,mmm or HH:MM:SS.mmm
Default window: 10 minutes before the timestamp (inclusive) to the timestamp.
EOF
}
if [[ $# -lt 2 ]]; then
usage
exit 1
fi
srt_file="$1"
shift
timestamp="$1"
shift
# Enforce cache directory + .srt extension to avoid arbitrary file reads
base_dir="$(cd "$(dirname "$0")/.." && pwd)"
cache_dir="${base_dir}/storage/subtitles"
case "$srt_file" in
*.srt) ;;
*) echo "Error: subtitle file must have .srt extension" >&2; exit 1;;
esac
# Normalize to absolute path
if [[ "$srt_file" != /* ]]; then
srt_file="$PWD/$srt_file"
fi
if [[ "$srt_file" != "$cache_dir"/* ]]; then
echo "Error: subtitle file must be inside $cache_dir" >&2
exit 1
fi
if [[ ! -f "$srt_file" ]]; then
echo "Error: subtitle file not found" >&2
exit 1
fi
window_mins=10
while [[ $# -gt 0 ]]; do
case "$1" in
--window-mins)
window_mins="$2"
shift 2
;;
*)
echo "Unknown arg: $1" >&2
exit 1
;;
esac
done
# Normalize timestamp
norm_ts="${timestamp/./,}"
awk -v target="$norm_ts" -v wmins="$window_mins" '
function to_ms(ts, h,m,s,ms) {
split(ts, a, ":");
h=a[1]; m=a[2];
split(a[3], b, ",");
s=b[1]; ms=b[2];
return (h*3600000)+(m*60000)+(s*1000)+ms;
}
BEGIN {
target_ms = to_ms(target);
window_ms = wmins * 60 * 1000;
start_window = target_ms - window_ms;
if (start_window < 0) start_window = 0;
}
/^[0-9]+$/ { idx=$0; next }
/^[0-9][0-9]:[0-9][0-9]:[0-9][0-9],[0-9][0-9][0-9] --> / {
start=$1; end=$3;
start_ms=to_ms(start); end_ms=to_ms(end);
in_block=1; text=""; next
}
/^$/ {
if (in_block) {
if (end_ms >= start_window && start_ms <= target_ms) {
printf("[%s --> %s]\n%s\n\n", start, end, text);
found=1;
}
}
in_block=0; text=""; next
}
{
if (in_block) {
if (text == "") text=$0; else text=text"\n"$0;
}
}
END {
if (!found) print "No subtitles found in window";
}
' "$srt_file"
```