Skip to content

Download Subtitle

Download the actual subtitle file for a specific subtitle.

Endpoint

GET /api/subtitles/{subtitle_id}/download

Authentication

Authentication required - Include your API key in the Authorization header.

Description

This endpoint downloads the actual subtitle file (typically .srt, .sub, or .zip format). The file is either served from cache if previously downloaded, or fetched from the source and cached for future requests.

Key Features: - Automatic caching for faster subsequent downloads - Downloads from source if not cached - Increments download counter - Returns the file with appropriate content-disposition headers

Path Parameters

Parameter Type Required Description
subtitle_id string (UUID) Yes Unique subtitle identifier

Request Examples

=== "cURL"

# Download and save to file
curl -X GET "https://subx-api.duckdns.org/api/subtitles/550e8400-e29b-41d4-a716-446655440000/download" \
  -H "Authorization: Bearer {YOUR_API_KEY_HERE}" \
  -o subtitle.srt

# Download with original filename
curl -X GET "https://subx-api.duckdns.org/api/subtitles/550e8400-e29b-41d4-a716-446655440000/download" \
  -H "Authorization: Bearer {YOUR_API_KEY_HERE}" \
  -O -J
import requests

subtitle_id = "550e8400-e29b-41d4-a716-446655440000"
headers = {"Authorization": "Bearer {YOUR_API_KEY_HERE}"}

response = requests.get(
    f"https://subx-api.duckdns.org/api/subtitles/{subtitle_id}/download",
    headers=headers
)

# Save with original filename from headers
filename = "subtitle.srt"
if 'content-disposition' in response.headers:
    # Extract filename from Content-Disposition header
    import re
    cd = response.headers['content-disposition']
    filename_match = re.findall('filename="(.+)"', cd)
    if filename_match:
        filename = filename_match[0]

with open(filename, "wb") as f:
    f.write(response.content)

print(f"Downloaded: {filename}")
package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "regexp"
)

func main() {
    apiKey := "{YOUR_API_KEY_HERE}"
    subtitleID := "550e8400-e29b-41d4-a716-446655440000"
    url := fmt.Sprintf("https://subx-api.duckdns.org/api/subtitles/%s/download", subtitleID)

    // Create request
    req, _ := http.NewRequest("GET", url, nil)
    req.Header.Set("Authorization", "Bearer "+apiKey)

    // Send request
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    // Extract filename from Content-Disposition
    filename := "subtitle.srt"
    cd := resp.Header.Get("Content-Disposition")
    if cd != "" {
        re := regexp.MustCompile(`filename="(.+)"`)
        matches := re.FindStringSubmatch(cd)
        if len(matches) > 1 {
            filename = matches[1]
        }
    }

    // Save to file
    file, _ := os.Create(filename)
    defer file.Close()

    io.Copy(file, resp.Body)
    fmt.Printf("Downloaded: %s\n", filename)
}
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

var subtitleId = "550e8400-e29b-41d4-a716-446655440000";
var client = new HttpClient();

client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY_HERE}");

var url = $"https://subx-api.duckdns.org/api/subtitles/{subtitleId}/download";
var response = await client.GetAsync(url);

// Extract filename from Content-Disposition
var filename = "subtitle.srt";
if (response.Content.Headers.ContentDisposition?.FileName != null)
{
    filename = response.Content.Headers.ContentDisposition.FileName.Trim('"');
}

var content = await response.Content.ReadAsByteArrayAsync();
await File.WriteAlleBytesAsync(filename, content);

Console.WriteLine($"Downloaded: {filename}");
const subtitleId = '550e8400-e29b-41d4-a716-446655440000';

const response = await fetch(
  `https://subx-api.duckdns.org/api/subtitles/${subtitleId}/download`,
  {
    headers: {
      'Authorization': 'Bearer {YOUR_API_KEY_HERE}'
    }
  }
);

if (!response.ok) {
  throw new Error(`Download failed: ${response.status}`);
}

// Extract filename from Content-Disposition
let filename = 'subtitle.srt';
const cd = response.headers.get('content-disposition');
if (cd) {
  const match = cd.match(/filename="(.+)"/);
  if (match) filename = match[1];
}

// Save to file (Node.js)
const fs = require('fs').promises;
const buffer = await response.arrayBuffer();
await fs.writeFile(filename, Buffer.from(buffer));

console.log(`Downloaded: ${filename}`);

Response

Success Response (200 OK)

The response is the binary subtitle file content with appropriate headers:

Headers:

HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="Dexter.S01E01.HDTV.XviD-LOL.srt"
Content-Length: 45678

Body: Binary file content (subtitle file)

Common File Extensions

Extension Description
.srt SubRip format (most common)
.sub MicroDVD or SubViewer format
.ssa / .ass SubStation Alpha / Advanced SubStation Alpha
.zip Compressed archive (may contain multiple files)
.rar Compressed archive

Handling ZIP Files

If the downloaded file is a .zip archive, extract it to find the subtitle file inside. The archive may contain multiple subtitle files or additional metadata. :::

Status Codes

Code Description
200 Success - File downloaded
400 Bad Request - Missing source ID or download failed
401 Unauthorized - Invalid or missing API key
404 Not Found - Subtitle doesn't exist
429 Too Many Requests - Rate limit exceeded
500 Internal Server Error
502 Bad Gateway - Failed to fetch from source

Error Responses

404 Not Found

{
  "detail": "Subtitle not found"
}

400 Bad Request - Missing Source ID

{
  "detail": "Subtitle missing source_release_id to download"
}

This occurs when the subtitle record doesn't have the necessary metadata to download from the source.

502 Bad Gateway - Source Fetch Failed

{
  "detail": "Failed to fetch subtitle from source"
}

This can occur when: - The source website is temporarily unavailable - The source file has been removed - Network connectivity issues

Download Workflow

graph TD
    A[Client requests download] --> B{File cached?}
    B -->|Yes| C[Return cached file]
    B -->|No| D[Fetch from source]
    D --> E{Fetch successful?}
    E -->|Yes| F[Save to cache]
    F --> G[Return file]
    E -->|No| H[Return 502 error]
    C --> I[Increment download counter]
    G --> I

Advanced Examples

1. Download with Progress Tracking

import requests
from tqdm import tqdm

def download_with_progress(subtitle_id, output_path):
    """Download subtitle with progress bar."""
    headers = {"Authorization": "Bearer {YOUR_API_KEY_HERE}"}
    url = f"https://subx-api.duckdns.org/api/subtitles/{subtitle_id}/download"

    response = requests.get(url, headers=headers, stream=True)
    response.raise_for_status()

    total_size = int(response.headers.get('content-length', 0))

    with open(output_path, 'wb') as f:
        with tqdm(total=total_size, unit='B', unit_scale=True) as pbar:
            for chunk in response.iter_content(chunk_size=8192):
                f.write(chunk)
                pbar.update(len(chunk))

    print(f"✓ Downloaded: {output_path}")

# Usage
download_with_progress(
    "550e8400-e29b-41d4-a716-446655440000",
    "dexter_s01e01.srt"
)

2. Batch Download Multiple Subtitles

import requests
import os
from concurrent.futures import ThreadPoolExecutor, as_completed

def download_subtitle(subtitle_id, output_dir, headers):
    """Download a single subtitle."""
    try:
        url = f"https://subx-api.duckdns.org/api/subtitles/{subtitle_id}/download"
        response = requests.get(url, headers=headers, timeout=30)
        response.raise_for_status()

        # Extract filename
        filename = f"{subtitle_id}.srt"
        cd = response.headers.get('content-disposition', '')
        if 'filename=' in cd:
            import re
            match = re.search(r'filename="(.+)"', cd)
            if match:
                filename = match.group(1)

        # Save file
        filepath = os.path.join(output_dir, filename)
        with open(filepath, 'wb') as f:
            f.write(response.content)

        return {'id': subtitle_id, 'success': True, 'filename': filename}

    except Exception as e:
        return {'id': subtitle_id, 'success': False, 'error': str(e)}

def batch_download(subtitle_ids, output_dir="subtitles", max_workers=3):
    """Download multiple subtitles concurrently."""
    os.makedirs(output_dir, exist_ok=True)
    headers = {"Authorization": "Bearer {YOUR_API_KEY_HERE}"}

    results = []
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {
            executor.submit(download_subtitle, sub_id, output_dir, headers): sub_id
            for sub_id in subtitle_ids
        }

        for future in as_completed(futures):
            result = future.result()
            results.append(result)

            if result['success']:
                print(f"✓ Downloaded: {result['filename']}")
            else:
                print(f"✗ Failed {result['id']}: {result['error']}")

    success_count = sum(1 for r in results if r['success'])
    print(f"\nCompleted: {success_count}/{len(subtitle_ids)} successful")

    return results

# Usage
ids = [
    "550e8400-e29b-41d4-a716-446655440000",
    "660e9500-f39c-52e5-b827-557766551111"
]

batch_download(ids)

3. Download with Automatic Retry

import requests
import time

def download_with_retry(subtitle_id, output_path, max_retries=3):
    """Download subtitle with automatic retry on failure."""
    headers = {"Authorization": "Bearer {YOUR_API_KEY_HERE}"}
    url = f"https://subx-api.duckdns.org/api/subtitles/{subtitle_id}/download"

    for attempt in range(max_retries):
        try:
            response = requests.get(url, headers=headers, timeout=30)

            if response.status_code == 502:
                # Source fetch failed - retry with backoff
                if attempt < max_retries - 1:
                    wait_time = 60 * (attempt + 1)
                    print(f"Source unavailable. Retrying in {wait_time}s...")
                    time.sleep(wait_time)
                    continue
                else:
                    raise Exception("Failed to fetch from source after retries")

            response.raise_for_status()

            # Success - save file
            with open(output_path, 'wb') as f:
                f.write(response.content)

            print(f"✓ Downloaded: {output_path}")
            return True

        except requests.exceptions.RequestException as e:
            if attempt == max_retries - 1:
                print(f"✗ Download failed: {e}")
                return False

            wait_time = 60 * (attempt + 1)
            print(f"Attempt {attempt + 1} failed. Retrying in {wait_time}s...")
            time.sleep(wait_time)

    return False

# Usage
success = download_with_retry(
    "550e8400-e29b-41d4-a716-446655440000",
    "subtitle.srt"
)

4. Download and Extract ZIP Archives

import requests
import zipfile
import io
import os

def download_and_extract(subtitle_id, output_dir="subtitles"):
    """Download subtitle and extract if it's a ZIP archive."""
    headers = {"Authorization": "Bearer {YOUR_API_KEY_HERE}"}
    url = f"https://subx-api.duckdns.org/api/subtitles/{subtitle_id}/download"

    response = requests.get(url, headers=headers)
    response.raise_for_status()

    # Get filename
    filename = "subtitle.srt"
    cd = response.headers.get('content-disposition', '')
    if 'filename=' in cd:
        import re
        match = re.search(r'filename="(.+)"', cd)
        if match:
            filename = match.group(1)

    os.makedirs(output_dir, exist_ok=True)

    # Check if ZIP file
    if filename.endswith('.zip'):
        print(f"Extracting ZIP archive: {filename}")
        with zipfile.ZipFile(io.BytesIO(response.content)) as zf:
            # Extract all files
            zf.extractall(output_dir)
            extracted = zf.namelist()
            print(f"✓ Extracted {len(extracted)} files:")
            for file in extracted:
                print(f"  - {file}")
            return extracted
    else:
        # Not a ZIP - save directly
        filepath = os.path.join(output_dir, filename)
        with open(filepath, 'wb') as f:
            f.write(response.content)
        print(f"✓ Downloaded: {filename}")
        return [filename]

# Usage
files = download_and_extract("550e8400-e29b-41d4-a716-446655440000")

Rate Limiting & Usage Tracking

  • Each download increments your API usage counter
  • Downloads are tracked per API key for monitoring
  • Standard rate limits apply (see Rate Limits Guide)
  • Cached downloads still count toward your usage

Best Practices

✅ Do's

  • Cache downloads locally to avoid repeated API calls
  • Check file extension before processing (handle ZIP files appropriately)
  • Implement retry logic for 502 errors (source temporarily unavailable)
  • Use progress tracking for better user experience with large files
  • Verify file integrity after download (check file size, try to parse)

❌ Don'ts

  • Don't download the same file repeatedly - cache it locally
  • Don't ignore errors - handle 404, 400, and 502 appropriately
  • Don't assume format - always check the file extension
  • Don't skip timeout configuration - large files may take time

File Size Considerations

Typical subtitle file sizes: - SRT files: 50-200 KB - ASS/SSA files: 100-500 KB - ZIP archives: Varies (100 KB - 5 MB)

Set appropriate timeouts based on file size and connection speed:

# For typical subtitle files
response = requests.get(url, headers=headers, timeout=30)

# For potentially large ZIP files
response = requests.get(url, headers=headers, timeout=60)

Notes

  • The download counter increments on every request, even for cached files
  • Files are stored with SHA256 deduplication (identical files share storage)
  • The API automatically handles compression (gzip, brotli) for faster transfers
  • Original filename is preserved in the Content-Disposition header
  • Downloads count toward both API usage and download statistics

Next Steps