Saltar a contenido

Descargar Subtítulo

Descarga el archivo de subtítulo actual para un subtítulo específico.

Endpoint

GET /api/subtitles/{subtitle_id}/download

Autenticación

Autenticación requerida - Incluye tu clave de API en el header Authorization.

Descripción

Este endpoint descarga el archivo de subtítulo actual (típicamente formato .srt, .sub, o .zip). El archivo se sirve desde caché si fue descargado previamente, o se obtiene de la fuente y se cachea para solicitudes futuras.

Características Clave: - Caché automático para descargas subsecuentes más rápidas - Descarga desde la fuente si no está cacheado - Incrementa contador de descargas - Devuelve el archivo con headers content-disposition apropiados

Parámetros de Path

Parámetro Tipo Requerido Descripción
subtitle_id string (UUID) Identificador único del subtítulo

Ejemplos de Solicitud

=== "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 {TU_CLAVE_API}" \
  -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 {TU_CLAVE_API}" \
  -O -J
import requests

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

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 := "{TU_CLAVE_API}"
    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", "{TU_CLAVE_API}");

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 {TU_CLAVE_API}'
    }
  }
);

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}`);

Respuesta

Respuesta Exitosa (200 OK)

La respuesta es el contenido del archivo de subtítulo binario con headers apropiados:

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: Contenido del archivo binario (archivo de subtítulo)

Extensiones de Archivo Comunes

Extensión Descripción
.srt Formato SubRip (más común)
.sub Formato MicroDVD o SubViewer
.ssa / .ass SubStation Alpha / Advanced SubStation Alpha
.zip Archivo comprimido (puede contener múltiples archivos)
.rar Archivo comprimido

Manejo de Archivos ZIP

Si el archivo descargado es un archivo .zip, extráelo para encontrar el archivo de subtítulo dentro. El archivo puede contener múltiples archivos de subtítulos o metadatos adicionales. :::

Códigos de Estado

Código Descripción
200 Éxito - Archivo descargado
400 Solicitud Incorrecta - ID de fuente faltante o descarga fallida
401 No Autorizado - Clave de API inválida o faltante
404 No Encontrado - El subtítulo no existe
429 Demasiadas Solicitudes - Límite de tasa excedido
500 Error Interno del Servidor
502 Bad Gateway - Fallo al obtener de la fuente

Respuestas de Error

404 No Encontrado

{
  "detail": "Subtitle not found"
}

400 Solicitud Incorrecta - ID de Fuente Faltante

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

Esto ocurre cuando el registro del subtítulo no tiene los metadatos necesarios para descargar de la fuente.

502 Bad Gateway - Fallo al Obtener de la Fuente

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

Esto puede ocurrir cuando: - El sitio web de la fuente está temporalmente no disponible - El archivo de la fuente ha sido eliminado - Problemas de conectividad de red

Flujo de Descarga

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

Ejemplos Avanzados

1. Descarga con Seguimiento de Progreso

import requests
from tqdm import tqdm

def download_with_progress(subtitle_id, output_path):
    """Download subtitle with progress bar."""
    headers = {"Authorization": "Bearer {TU_CLAVE_API}"}
    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. Descarga por Lotes de Múltiples Subtítulos

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 {TU_CLAVE_API}"}

    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. Descarga con Reintento Automático

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 {TU_CLAVE_API}"}
    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. Descarga y Extracción de Archivos ZIP

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 {TU_CLAVE_API}"}
    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")

Límite de Tasa y Seguimiento de Uso

  • Cada descarga incrementa tu contador de uso de la API
  • Las descargas se rastrean por clave de API para monitoreo
  • Se aplican límites de tasa estándar (ver Guía de Límites de Tasa)
  • Las descargas cacheadas aún cuentan hacia tu uso

Mejores Prácticas

✅ Hacer

  • Cachear descargas localmente para evitar llamadas repetidas a la API
  • Verificar extensión de archivo antes de procesar (manejar archivos ZIP apropiadamente)
  • Implementar lógica de reintento para errores 502 (fuente temporalmente no disponible)
  • Usar seguimiento de progreso para mejor experiencia de usuario con archivos grandes
  • Verificar integridad del archivo después de descargar (verificar tamaño de archivo, intentar parsear)

❌ No Hacer

  • No descargar el mismo archivo repetidamente - cachealo localmente
  • No ignorar errores - maneja 404, 400, y 502 apropiadamente
  • No asumir formato - siempre verifica la extensión del archivo
  • No omitir configuración de timeout - archivos grandes pueden tomar tiempo

Consideraciones de Tamaño de Archivo

Tamaños típicos de archivos de subtítulos: - Archivos SRT: 50-200 KB - Archivos ASS/SSA: 100-500 KB - Archivos ZIP: Varía (100 KB - 5 MB)

Configura timeouts apropiados basados en el tamaño del archivo y velocidad de conexión:

# 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)

Notas

  • El contador de descargas se incrementa en cada solicitud, incluso para archivos cacheados
  • Los archivos se almacenan con deduplicación SHA256 (archivos idénticos comparten almacenamiento)
  • La API maneja automáticamente la compresión (gzip, brotli) para transferencias más rápidas
  • El nombre de archivo original se preserva en el header Content-Disposition
  • Las descargas cuentan tanto hacia el uso de la API como hacia las estadísticas de descarga

Próximos Pasos