Back to skills
SkillHub ClubShip Full StackFull StackBackend

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.

Stars
3,087
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
B78.7

Install command

npx @skill-hub/cli install openclaw-skills-opensubtitles

Repository

openclaw/skills

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 repository

Best 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

Claude CodeCodex CLIGemini CLIOpenCode

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"

```

opensubtitles | SkillHub