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¶
400 Bad Request - Missing Source ID¶
This occurs when the subtitle record doesn't have the necessary metadata to download from the source.
502 Bad Gateway - Source Fetch Failed¶
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-Dispositionheader - Downloads count toward both API usage and download statistics
Next Steps¶
- Search Subtitles - Find subtitles to download
- Get Subtitle - Check subtitle details before downloading
- Error Handling - Handle download errors gracefully