Skip to content

API de Vídeos

A API de vídeos gerencia o conteúdo principal da plataforma de streaming: metadados de vídeos, thumbnails, publicação e controle de acesso por assinatura.


🔐 Autenticação

Todos os endpoints de vídeos exigem autenticação JWT.

Middleware de Assinatura

⚠️ IMPORTANTE: Usuários do tipo customer precisam de assinatura ativa para acessar vídeos.

# Middleware aplicado automaticamente em GET /api/v1/videos
async def verify_subscription(auth: TokenData) -> TokenData:
    """
    Valida assinatura ativa antes de permitir acesso aos vídeos.

    - Admin users: bypass (sem validação)
    - Customer users: require current_subscription_id not null + status='active'
    """

Fluxo de Validação:

sequenceDiagram
    participant Client
    participant API
    participant Auth
    participant Middleware
    participant DB

    Client->>API: GET /api/v1/videos<br/>Authorization: Bearer <token>
    API->>Auth: Validar JWT
    Auth->>Middleware: verify_subscription(auth)

    alt User Type = Admin
        Middleware-->>API: ✅ Bypass (admin)
    else User Type = Customer
        Middleware->>DB: Buscar user.current_subscription_id
        alt current_subscription_id exists
            Middleware->>DB: Buscar subscription (status)
            alt subscription.status = 'active'
                Middleware-->>API: ✅ Autorizado
            else status != 'active'
                Middleware-->>Client: ❌ 403 Subscription not active
            end
        else current_subscription_id is null
            Middleware-->>Client: ❌ 403 No active subscription
        end
    end

    API->>DB: Buscar vídeos
    DB-->>API: Lista de vídeos
    API-->>Client: 200 OK + docs

📋 Endpoints

Streaming HLS: Para obter URLs assinadas de streaming direto (player customizado), ver API de Streaming HLS.

1. GET /api/v1/videos - Listar Vídeos

Buscar vídeos publicados com filtros, paginação e ordenação.

Headers:

Authorization: Bearer <jwt_token>
Content-Type: application/json

Query Parameters:

Parâmetro Tipo Padrão Descrição
_id OID - ID específico do vídeo
query str (JSON) null Query MongoDB customizada
sort str (JSON) {"register_update_date":-1} Ordenação
qty_docs_page int 10 Documentos por página
current_page int 0 Página atual (zero-indexed)
docs_range str (tuple) "(0, 0)" Range customizado

Exemplos de Query:

# Buscar vídeo por ID
GET /api/v1/videos?_id=507f1f77bcf86cd799439011

# Buscar vídeos publicados (enable=true)
GET /api/v1/videos?query={"enable":true}

# Buscar por categoria
GET /api/v1/videos?query={"category_id":"507f1f77bcf86cd799439012"}

# Buscar por tag
GET /api/v1/videos?query={"tags":"tutorial"}

# Buscar destaques principais
GET /api/v1/videos?query={"main_highlight":true}

# Buscar vídeos recentes (últimos 30 dias)
GET /api/v1/videos?query={"release_date":{"$gte":"2026-01-01T00:00:00Z"}}

# Combinar filtros: publicados + categoria + tag
GET /api/v1/videos?query={"enable":true,"category_id":"507f...","tags":"tutorial"}

# Paginação: página 2, 20 itens por página
GET /api/v1/videos?current_page=1&qty_docs_page=20

# Ordenar por data de release (mais recentes primeiro)
GET /api/v1/videos?sort={"release_date":-1}

Response (200 OK):

{
  "docs": [
    {
      "_id": "507f1f77bcf86cd799439011",
      "title": "Introdução ao FastAPI",
      "slug": "intro-fastapi",
      "video_id": "https://youtube.com/watch?v=xyz",
      "description": "Aprenda os fundamentos do FastAPI para criar APIs modernas",
      "category_id": ["507f1f77bcf86cd799439012"],
      "category_name": ["Python"],
      "tags_id": ["507f1f77bcf86cd799439021"],
      "tags_name": ["FastAPI"],
      "tags": ["tutorial", "python", "fastapi"],
      "author": "John Doe",
      "duration": 45,
      "thumbnail_horizontal": "507f1f77bcf86cd799439013",
      "thumbnail_horizontal_name_less": "507f1f77bcf86cd799439015",
      "thumbnail_vertical": "507f1f77bcf86cd799439014",
      "thumbnail_vertical_name_less": "507f1f77bcf86cd799439016",
      "release_date": "2026-01-01T00:00:00Z",
      "expiration_date": null,
      "enable": true,
      "main_highlight": false,
      "section_highlight": false,
      "is_show_in_slide_carousel": false,
      "season_id": "69ac725620019bcb4cce5f4a",
      "season_name": "No Divã - Temporada 1",
      "season_number": 1,
      "episode_number": 3,
      "video_gridfs_id": null,
      "document_created_by_id": "507f1f77bcf86cd799439015",
      "register_date": "2025-12-01T10:00:00Z",
      "register_update_date": "2026-01-08T14:30:00Z"
    }
  ],
  "links": [
    {"link_type": "GET", "rel": "self", "href": "/api/v1/videos?current_page=0&qty_docs_page=10"},
    {"link_type": "GET", "rel": "get document", "href": "/api/v1/videos/{id}"},
    {"link_type": "DELETE", "rel": "delete document", "href": "/api/v1/videos/{id}"},
    {"link_type": "POST", "rel": "insert document", "href": "/api/v1/videos"},
    {"link_type": "PUT", "rel": "update document", "href": "/api/v1/videos/{id}"}
  ],
  "msg": "ok",
  "pagination": {
    "current_page": 0,
    "qty_docs_page": 10,
    "qty_of_pages": 15,
    "qty_total_docs": 150
  }
}

Response (403 Forbidden - Sem Assinatura Ativa):

{
  "detail": "No active subscription found. Please subscribe to access videos."
}

2. POST /api/v1/videos - Criar Vídeo

Criar novos vídeos na plataforma (admin only).

Headers:

Authorization: Bearer <jwt_token>
Content-Type: application/json

Request Body:

[
  {
    "title": "Introdução ao FastAPI",
    "slug": "intro-fastapi",
    "video_id": "https://youtube.com/watch?v=xyz",
    "description": "Aprenda os fundamentos do FastAPI",
    "category_id": ["507f1f77bcf86cd799439012"],
    "tags": ["tutorial", "python"],
    "author": "John Doe",
    "duration": 45,
    "thumbnail_horizontal": "507f1f77bcf86cd799439013",
    "thumbnail_vertical": "507f1f77bcf86cd799439014",
    "release_date": "2026-01-01T00:00:00Z",
    "enable": true
  }
]

Campos Obrigatórios:

Campo Tipo Descrição
title str Titulo do video (1-200 chars)
slug str URL-friendly identifier (unico, lowercase + hifens)
video_id str (URL) URL do video (YouTube, Vimeo, etc.)
duration int Duracao em minutos (>= 0)
release_date datetime Data de publicacao (ISO 8601)

Campos Opcionais:

Campo Tipo Padrão Descrição
description str "" Descricao detalhada
category_id list[OID] [] IDs de categorias
tags_id list[OID] [] IDs das tags (referencias)
tags list[str] [] Tags como strings (busca/compatibilidade)
author str "" Nome do autor/instrutor
thumbnail_horizontal OID null GridFS file ID (16:9). URL: GET /api/v1/archive-records/{id}
thumbnail_horizontal_name_less OID null GridFS file ID (16:9) sem nome sobreposto
thumbnail_vertical OID null GridFS file ID (9:16). URL: GET /api/v1/archive-records/{id}
thumbnail_vertical_name_less OID null GridFS file ID (9:16) sem nome sobreposto

Thumbnails — Como montar a URL

Os campos thumbnail_horizontal e thumbnail_vertical contem apenas o ObjectId do GridFS.

Opção 1 — Endpoint público com archive_token (recomendado para Image.network, <img>):

$baseUrl/api/v1/archive-records-public/{archive_token}?f={file_id}
O archive_token é retornado no login. Não requer header Authorization.

Opção 2 — Endpoint autenticado com ?token= (quando archive_token indisponível):

$baseUrl/api/v1/archive-records/{file_id}?token={access_token}

Exemplo (opção 2): https://fdplay-api.infraifd.com/api/v1/archive-records/69bea66a4a2410cd09d8c33b?token=eyJ...

Os campos thumbnail_url_horizontal e thumbnail_url_vertical (URLs externas Supabase) foram removidos. Todas as thumbnails sao servidas via GridFS.

| expiration_date | datetime | null | Data de expiracao (ISO 8601) | | enable | bool | true | Video publicado/visivel | | main_highlight | bool | false | Destaque principal (homepage) | | section_highlight | bool | false | Destaque na secao de categoria | | is_show_in_slide_carousel | bool | false | Indica se o frontend deve mostrar o video no slide carousel | | season_id | OID | null | ID da temporada (ref. collection seasons) | | season_name | str | null | Nome da temporada (via agregacao) | | season_number | int | null | Numero da temporada (1, 2, 3...) | | episode_number | int | null | Numero do episodio (>= 1) | | video_gridfs_id | OID | null | ID do video no GridFS (storage interno) | | legacy_id | str | null | UUID do sistema legado |

Campos Auto-Gerados:

# Gerados automaticamente pelo sistema:
_id: ObjectId                     # MongoDB ID
created_by: OID                  # User ID (do token JWT)
register_date: datetime          # Timestamp de criação
register_update_date: datetime    # Timestamp de última atualização

Response (200 OK):

{
  "docs": [
    {
      "_id": "507f1f77bcf86cd799439011",
      "title": "Introdução ao FastAPI",
      "slug": "intro-fastapi",
      ...
    }
  ],
  "links": [
    {"link_type": "GET", "rel": "self", "href": "/api/v1/videos"},
    {"link_type": "POST", "rel": "insert document", "href": "/api/v1/videos"}
  ],
  "msg": "ok",
  "pagination": {
    "current_page": 0,
    "qty_docs_page": 1,
    "qty_of_pages": 1,
    "qty_total_docs": 1
  }
}

3. PUT /api/v1/videos - Atualizar Vídeo

Atualizar vídeos existentes (admin only).

Headers:

Authorization: Bearer <jwt_token>
Content-Type: application/json

Request Body:

[
  {
    "_id": "507f1f77bcf86cd799439011",
    "title": "Introdução ao FastAPI - Atualizado",
    "enable": false
  }
]

⚠️ Campo _id obrigatório para identificar o documento a atualizar.

Atualização Parcial:

Apenas campos fornecidos são atualizados. Exemplo:

{
  "_id": "507f...",
  "enable": false  // Apenas despublica o vídeo, mantém demais campos
}

Response (200 OK):

{
  "docs": [
    {
      "_id": "507f1f77bcf86cd799439011",
      "title": "Introdução ao FastAPI - Atualizado",
      "enable": false,
      "register_update_date": "2026-01-08T15:00:00Z"  // Atualizado automaticamente
    }
  ],
  "links": [
    {"link_type": "GET", "rel": "self", "href": "/api/v1/videos"},
    {"link_type": "POST", "rel": "insert document", "href": "/api/v1/videos"}
  ],
  "msg": "ok",
  "pagination": {
    "current_page": 0,
    "qty_docs_page": 1,
    "qty_of_pages": 1,
    "qty_total_docs": 1
  }
}

4. DELETE /api/v1/videos - Deletar Vídeo

Remover vídeos do banco de dados (admin only).

Headers:

Authorization: Bearer <jwt_token>
Content-Type: application/json

Opção 1: Deletar por ID (Query String)

DELETE /api/v1/videos?_id=507f1f77bcf86cd799439011

Opção 2: Deletar múltiplos (Request Body)

[
  {"_id": "507f1f77bcf86cd799439011"},
  {"_id": "507f1f77bcf86cd799439012"}
]

⚠️ ATENÇÃO: Deleção é permanente. Thumbnails no GridFS devem ser deletados separadamente via endpoints de arquivos.

Response (200 OK):

{
  "docs": [],
  "msg": "2 documents deleted successfully",
  "links": [
    {"link_type": "GET", "rel": "self", "href": "/api/v1/videos"},
    {"link_type": "POST", "rel": "insert document", "href": "/api/v1/videos"}
  ],
  "pagination": {
    "current_page": 0,
    "qty_docs_page": 0,
    "qty_of_pages": 0,
    "qty_total_docs": 0
  }
}

🎨 Gerenciamento de Thumbnails (GridFS)

Thumbnails são armazenados no GridFS (MongoDB file storage) e referenciados nos campos:

  • thumbnail_horizontal (OID) - Thumbnail landscape (16:9)
  • thumbnail_horizontal_name_less (OID) - Thumbnail landscape sem nome (16:9)
  • thumbnail_vertical (OID) - Thumbnail portrait (9:16)
  • thumbnail_vertical_name_less (OID) - Thumbnail portrait sem nome (9:16)

Fluxo de Upload:

sequenceDiagram
    participant Admin
    participant API
    participant GridFS
    participant VideosDB

    Admin->>API: POST /api/v1/upload-file/archive-records/{id}<br/>(multipart/form-data)
    API->>GridFS: Salvar arquivo
    GridFS-->>API: file_id (OID)
    API-->>Admin: 200 OK {file_id: "507f..."}

    Admin->>API: POST /api/v1/videos<br/>{"thumbnail_horizontal": "507f..."}
    API->>VideosDB: Salvar vídeo
    VideosDB-->>API: Vídeo criado
    API-->>Admin: 200 OK

Endpoints Relacionados:

# Upload de thumbnail
POST /api/v1/upload-file/archive-records/{id}
Content-Type: multipart/form-data

file: <binary>

# Response:
{
  "file_id": "507f1f77bcf86cd799439013",
  "filename": "thumbnail.jpg",
  "content_type": "image/jpeg",
  "size": 245678
}

# Download de thumbnail
GET /api/v1/archive-records/{file_id}

# Deletar thumbnail
DELETE /api/v1/upload-file/archive-records/{id}/{file_id}

🔍 Buscas Avançadas

Exemplos de Queries MongoDB

Buscar vídeos por múltiplas tags (OR):

{
  "tags": {"$in": ["tutorial", "python", "fastapi"]}
}

Buscar vídeos publicados após data específica:

{
  "enable": true,
  "release_date": {"$gte": "2026-01-01T00:00:00Z"}
}

Buscar vídeos de categoria + tag + publicados:

{
  "$and": [
    {"enable": true},
    {"category_id": "507f1f77bcf86cd799439012"},
    {"tags": "tutorial"}
  ]
}

Buscar vídeos por range de duração (30-60 minutos):

{
  "duration": {"$gte": 30, "$lte": 60}
}

Buscar vídeos por autor:

{
  "author": {"$regex": "John", "$options": "i"}
}

🎯 Fluxo Frontend: Listagem de Vídeos

Exemplo de implementação Flutter/Dart:

import 'dart:convert';
import 'package:http/http.dart' as http;

/// Buscar vídeos publicados (homepage)
Future<VideoListResponse> fetchPublishedVideos({int page = 0}) async {
    final token = prefs.getString('access_token');

    final params = {
        'query': jsonEncode({'enable': true}),
        'sort': jsonEncode({'release_date': -1}),
        'current_page': page.toString(),
        'qty_docs_page': '12',
    };

    final uri = Uri.parse('${AppConfig.baseUrl}/videos')
        .replace(queryParameters: params);

    final response = await http.get(
        uri,
        headers: {
            'Authorization': 'Bearer $token',
            'Content-Type': 'application/json',
        },
    );

    if (response.statusCode == 403) {
        // Redirecionar para página de assinatura
        throw Exception('Subscription required');
    }

    if (response.statusCode != 200) {
        throw Exception('HTTP error! status: ${response.statusCode}');
    }

    return VideoListResponse.fromJson(jsonDecode(response.body));
}

/// Buscar vídeos por categoria
Future<VideoListResponse> fetchVideosByCategory(String categoryId) async {
    final token = prefs.getString('access_token');

    final params = {
        'query': jsonEncode({
            'enable': true,
            'category_id': categoryId,
        }),
        'sort': jsonEncode({'release_date': -1}),
    };

    final uri = Uri.parse('${AppConfig.baseUrl}/videos')
        .replace(queryParameters: params);

    final response = await http.get(
        uri,
        headers: {
            'Authorization': 'Bearer $token',
            'Content-Type': 'application/json',
        },
    );

    return VideoListResponse.fromJson(jsonDecode(response.body));
}

/// Buscar vídeo específico por slug
Future<Video?> fetchVideoBySlug(String slug) async {
    final token = prefs.getString('access_token');

    final params = {
        'query': jsonEncode({'slug': slug}),
    };

    final uri = Uri.parse('${AppConfig.baseUrl}/videos')
        .replace(queryParameters: params);

    final response = await http.get(
        uri,
        headers: {
            'Authorization': 'Bearer $token',
            'Content-Type': 'application/json',
        },
    );

    final data = VideoListResponse.fromJson(jsonDecode(response.body));
    return data.docs.isNotEmpty ? data.docs[0] : null;
}

Dart Models:

class Video {
    final String id;
    final String title;
    final String slug;
    final String videoId;
    final String? description;
    final List<String>? categoryId;
    final List<String>? categoryName;
    final List<String>? tagsId;
    final List<String>? tagsName;
    final List<String>? tags;
    final String? author;
    final int duration;
    final String? thumbnailHorizontal;
    final String? thumbnailHorizontalNameLess;
    final String? thumbnailVertical;
    final String? thumbnailVerticalNameLess;
    final String releaseDate;
    final String? expirationDate;
    final bool enable;
    final bool mainHighlight;
    final bool sectionHighlight;
    final bool isShowInSlideCarousel;
    final String? seasonId;
    final String? seasonName;
    final int? seasonNumber;
    final int? episodeNumber;
    final String? videoGridfsId;
    final String? documentCreatedById;
    final String? registerDate;
    final String? registerUpdateDate;

    Video({
        required this.id,
        required this.title,
        required this.slug,
        required this.videoId,
        this.description,
        this.categoryId,
        this.categoryName,
        this.tagsId,
        this.tagsName,
        this.tags,
        this.author,
        required this.duration,
        this.thumbnailHorizontal,
        this.thumbnailHorizontalNameLess,
        this.thumbnailVertical,
        this.thumbnailVerticalNameLess,
        required this.releaseDate,
        this.expirationDate,
        required this.enable,
        this.mainHighlight = false,
        this.sectionHighlight = false,
        this.isShowInSlideCarousel = false,
        this.seasonId,
        this.seasonName,
        this.seasonNumber,
        this.episodeNumber,
        this.videoGridfsId,
        this.documentCreatedById,
        this.registerDate,
        this.registerUpdateDate,
    });

    factory Video.fromJson(Map<String, dynamic> json) {
        return Video(
            id: json['_id'],
            title: json['title'],
            slug: json['slug'],
            videoId: json['video_id'],
            description: json['description'],
            categoryId: (json['category_id'] as List?)?.cast<String>(),
            categoryName: (json['category_name'] as List?)?.cast<String>(),
            tagsId: (json['tags_id'] as List?)?.cast<String>(),
            tagsName: (json['tags_name'] as List?)?.cast<String>(),
            tags: (json['tags'] as List?)?.cast<String>(),
            author: json['author'],
            duration: json['duration'] ?? 0,
            thumbnailHorizontal: json['thumbnail_horizontal'],
            thumbnailHorizontalNameLess: json['thumbnail_horizontal_name_less'],
            thumbnailVertical: json['thumbnail_vertical'],
            thumbnailVerticalNameLess: json['thumbnail_vertical_name_less'],
            releaseDate: json['release_date'],
            expirationDate: json['expiration_date'],
            enable: json['enable'] ?? false,
            mainHighlight: json['main_highlight'] ?? false,
            sectionHighlight: json['section_highlight'] ?? false,
            isShowInSlideCarousel: json['is_show_in_slide_carousel'] ?? false,
            seasonId: json['season_id'],
            seasonName: json['season_name'],
            seasonNumber: json['season_number'],
            episodeNumber: json['episode_number'],
            videoGridfsId: json['video_gridfs_id'],
            documentCreatedById: json['document_created_by_id'],
            registerDate: json['register_date'],
            registerUpdateDate: json['register_update_date'],
        );
    }
}

class VideoListResponse {
    final List<Video> docs;
    final Map<String, dynamic> pagination;
    final List<dynamic> links;
    final String msg;

    VideoListResponse({
        required this.docs,
        required this.pagination,
        required this.links,
        required this.msg,
    });

    int get currentPage => pagination['current_page'] ?? 0;
    int get qtyDocsPage => pagination['qty_docs_page'] ?? 0;
    int get qtyOfPages => pagination['qty_of_pages'] ?? 0;
    int get qtyTotalDocs => pagination['qty_total_docs'] ?? 0;

    factory VideoListResponse.fromJson(Map<String, dynamic> json) {
        return VideoListResponse(
            docs: (json['docs'] as List)
                .map((e) => Video.fromJson(e))
                .toList(),
            pagination: json['pagination'] ?? {},
            links: json['links'] ?? [],
            msg: json['msg'] ?? '',
        );
    }
}

⚠️ Tratamento de Erros

401 Unauthorized

{
  "detail": "Could not validate credentials"
}

Causa: Token JWT inválido, expirado ou ausente.

Solução: Fazer login novamente via POST /api/v1/token.


403 Forbidden

{
  "detail": "No active subscription found. Please subscribe to access videos."
}

Causa: Customer sem assinatura ativa tentando acessar vídeos.

Solução: Redirecionar para página de assinatura (/api/v1/plans).


404 Not Found

{
  "detail": "Video not found"
}

Causa: ID ou query não encontrou documentos.


422 Unprocessable Entity

{
  "detail": [
    {
      "loc": ["body", 0, "slug"],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}

Causa: Validação Pydantic falhou (campo obrigatório ausente, tipo incorreto, etc.).


📊 Modelo de Dados

Schema MongoDB (Video Entity):

class Video(DocBase):
    """
    Entidade de video para plataforma de streaming.
    """
    title: str                              # Titulo do video (1-200 chars)
    slug: str                               # URL-friendly identifier (unico)
    video_id: HttpUrl                       # URL do video (YouTube, Vimeo, etc.)
    description: str = ''                   # Descricao detalhada
    category_id: list[OID] = []             # IDs das categorias
    category_name: list[str] | None = None  # Nomes das categorias (via aggregation)
    tags_id: list[OID] = []                 # IDs das tags (referencias)
    tags_name: list[str] | None = None      # Nomes das tags (via aggregation)
    tags: list[str] = []                    # Tags como strings (busca/compatibilidade)
    author: str = ''                        # Autor/instrutor
    duration: int                           # Duracao em minutos (>= 0)
    thumbnail_horizontal: OID | None = None           # GridFS file ID (16:9)
    thumbnail_horizontal_name_less: OID | None = None # GridFS file ID (16:9) sem nome
    thumbnail_vertical: OID | None = None             # GridFS file ID (9:16)
    thumbnail_vertical_name_less: OID | None = None   # GridFS file ID (9:16) sem nome
    release_date: datetime                  # Data de publicacao (ISO 8601)
    expiration_date: datetime | None = None # Data de expiracao
    enable: bool = True                     # Video publicado/visivel
    main_highlight: bool = False            # Destaque principal (homepage)
    section_highlight: bool = False         # Destaque na secao de categoria
    is_show_in_slide_carousel: bool = False # Flag para exibicao no slide carousel
    season_id: OID | None = None            # ID da temporada (ref. seasons)
    season_name: str | None = None          # Nome da temporada (via agregacao)
    season_number: int | None = None        # Numero da temporada (1, 2, 3...)
    episode_number: int | None = None       # Numero do episodio (>= 1)
    video_gridfs_id: OID | None = None      # ID do video no GridFS (storage interno)
    legacy_id: str | None = None            # UUID do sistema legado

    # Metadados auto-gerados (DocBase):
    # _id: OID
    # document_created_by_id: OID
    # document_created_by_name: str
    # register_date: datetime
    # register_update_date: datetime

Índices MongoDB:

# Índices recomendados para performance:
db.videos.createIndex({ "slug": 1 }, { unique: true })
db.videos.createIndex({ "enable": 1 })
db.videos.createIndex({ "release_date": -1 })
db.videos.createIndex({ "category_id": 1 })
db.videos.createIndex({ "tags": 1 })
db.videos.createIndex({ "main_highlight": 1 })

📺 Modelo de Dados — Season

Temporadas agrupam vídeos episódicos (ex.: "No Divã - Temporada 1"). Endpoints CRUD em /api/v1/seasons. GET requer assinatura ativa (admin bypass; mesma regra de /videos); POST/PUT/DELETE são admin-only.

Schema MongoDB (Season Entity):

class Season(DocBase):
    """
    Temporada para agrupar vídeos episódicos.
    """
    name: str                                          # Nome da temporada (1-200 chars, único)
    description: str = ''                              # Descrição da temporada
    index: int = 0                                     # Índice de ordenação (>= 0)
    thumbnail_vertical: OID | None = None              # GridFS file ID (9:16)
    thumbnail_vertical_name_less: OID | None = None    # GridFS file ID (9:16) sem nome
    thumbnail_horizontal: OID | None = None            # GridFS file ID (16:9)
    thumbnail_horizontal_name_less: OID | None = None  # GridFS file ID (16:9) sem nome

    # Metadados auto-gerados (DocBase):
    # _id: OID
    # document_created_by_id: OID
    # document_created_by_name: str
    # register_date: datetime
    # register_update_date: datetime

Campos:

Campo Tipo Padrão Descrição
name str obrigatório Nome da temporada (1-200 chars, unique)
description str "" Descrição livre
index int 0 Índice de ordenação (>= 0). Frontend ordena temporadas por index ascendente
thumbnail_vertical OID null GridFS file ID (9:16). URL: GET /api/v1/archive-records/{id}
thumbnail_vertical_name_less OID null GridFS file ID (9:16) sem nome sobreposto
thumbnail_horizontal OID null GridFS file ID (16:9). URL: GET /api/v1/archive-records/{id}
thumbnail_horizontal_name_less OID null GridFS file ID (16:9) sem nome sobreposto

Ordenação no frontend:

// Ordenar temporadas por index ascendente
seasons.sort((a, b) => (a['index'] ?? 0).compareTo(b['index'] ?? 0));

Exemplo POST /api/v1/seasons:

[
  {
    "name": "No Divã",
    "description": "Histórias incríveis transformadas no consultório.",
    "index": 0,
    "thumbnail_vertical": null,
    "thumbnail_horizontal": null
  }
]

Nota: A entidade Series foi removida do domínio. Vídeos episódicos referenciam season_id diretamente, sem agrupador adicional.


🚀 Próximos Passos

  1. Implementar busca full-text via MongoDB Atlas Search
  2. Sistema de visualizações (analytics) - rastrear plays
  3. Sistema de favoritos - permitir customers salvarem vídeos
  4. Recomendações personalizadas - ML-based suggestions
  5. Preview de vídeos - clips de 30 segundos sem assinatura