├── .cursor └── rules │ ├── command-system-events.mdc │ ├── container-events.mdc │ ├── conventions-go.mdc │ ├── project-management-events.mdc │ ├── testing-events-go.mdc │ └── troubleshooting-events.mdc ├── .fcai ├── README.md ├── commands.md ├── features │ ├── video-processing │ │ ├── completed │ │ │ ├── 001-video-entity │ │ │ │ ├── 001-video-entity.md │ │ │ │ └── 001-video-entity.result.md │ │ │ ├── 002-video-repository │ │ │ │ ├── 002-video-repository.md │ │ │ │ └── 002-video-repository.result.md │ │ │ ├── 003-video-repository-postgres │ │ │ │ ├── 003-video-repository-postgres.md │ │ │ │ └── 003-video-repository-postgres_result.md │ │ │ ├── 004-database-migrations │ │ │ │ ├── 004-database-migrations.md │ │ │ │ └── 004-database-migrations_result.md │ │ │ ├── 005-ffmpeg-service │ │ │ │ ├── 005-ffmpeg-service.md │ │ │ │ └── 005-ffmpeg-service_result.md │ │ │ └── 006-video-converter-service │ │ │ │ ├── 006-video-converter-service.md │ │ │ │ └── 006-video-converter-service_result.md │ │ └── documentation │ │ │ └── overview.md │ └── worker-pool │ │ └── completed │ │ └── 001-worker-pool-update │ │ ├── 001-worker-pool-update.md │ │ └── 001-worker-pool-update_result.md ├── project │ ├── architecture │ │ ├── docker.md │ │ ├── env-vars.md │ │ ├── ffmpeg-video-converter.md │ │ ├── go-libs.md │ │ ├── layered-architecture.md │ │ └── overview.md │ └── documentation │ │ └── context.md ├── state.md └── structure.md ├── .gitignore ├── Dockerfile ├── cmd ├── app │ └── main.go └── server │ └── main.go ├── config └── config.go ├── docker-compose.yaml ├── go.mod ├── go.sum ├── internal ├── application │ └── service │ │ ├── ffmpeg_service.go │ │ ├── ffmpeg_service_test.go │ │ ├── video_converter.go │ │ └── video_converter_test.go ├── domain │ ├── entity │ │ ├── video.go │ │ └── video_test.go │ └── repository │ │ └── video_repository.go └── infra │ └── database │ ├── db.go │ ├── migrations │ ├── 000001_create_videos_table.down.sql │ └── 000001_create_videos_table.up.sql │ └── repository │ ├── video_repository.go │ └── video_repository_test.go ├── main.go └── pkg └── workerpool ├── doc.go ├── errors.go ├── workerpool.go └── workerpool_test.go /.cursor/rules/command-system-events.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Quando falar da de comandos e da pasta .fcai 3 | globs: 4 | alwaysApply: false 5 | --- 6 | Leia o arquivo de [commands.md](mdc:.fcai/commands.md) para entender a regra de comandos -------------------------------------------------------------------------------- /.cursor/rules/container-events.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Para inicializar a aplicação ou executar a aplicação com docker 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # Quando precisar iniciar a aplicação pela primeira vez ou para retomar o projeto onde parou 7 | 8 | - Para inicializar a aplicação, você precisa utilizar o comando docker comopose up -d 9 | - Caso esteja usando o podman: podman-compose up -d 10 | 11 | - Todos os comandos devem ser executados sempre dentro dos containers através do docker comopse exec 12 | 13 | - Nome dos serviços no @docker-compose.yaml -------------------------------------------------------------------------------- /.cursor/rules/conventions-go.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Convenções de código e práticas de desenvolvimento 3 | globs: 4 | alwaysApply: true 5 | --- 6 | ## Convenções para Go 7 | 8 | ### 1. Estrutura de Pastas e Arquivos 9 | - **snake_case**: Para arquivos Go, use nomes em snake_case (letras minúsculas separadas por underscores). 10 | - **CamelCase**: Para nomes de pacotes exportados, siga a convenção CamelCase. 11 | - **lowercase**: Para pacotes (diretórios), use nomes em minúsculas sem underscores. 12 | 13 | ### 2. Estrutura do Projeto (baseada em golang-standards/project-layout) 14 | ``` 15 | / 16 | ├── cmd/ # Aplicações principais do projeto 17 | │ ├── app/ # Nome da aplicação 18 | │ │ └── main.go # Ponto de entrada da aplicação 19 | │ └── cli/ # Ferramentas de linha de comando 20 | ├── internal/ # Código privado da aplicação 21 | ├── pkg/ # Bibliotecas que podem ser usadas por aplicações externas 22 | ├── api/ # Especificações de API, protobuf, etc. 23 | ├── web/ # Componentes web (templates, assets, etc.) 24 | ├── configs/ # Arquivos de configuração 25 | ├── deployments/ # Configurações de implantação (docker, k8s, etc.) 26 | └── docs/ # Documentação do projeto 27 | ``` 28 | 29 | ### 3. Nomes de Tipos e Interfaces 30 | - **PascalCase** para tipos exportados (UserService, OrderRepository). 31 | - **camelCase** para variáveis e funções não exportadas (calculatePrice, getUserByID). 32 | - Evite redundância: Não use UserServiceStruct, apenas UserService. 33 | - Interfaces com um único método geralmente são nomeadas com o sufixo "er" (Reader, Writer). 34 | 35 | ### 4. Nomes de Variáveis e Funções 36 | - **camelCase** para variáveis e funções não exportadas (calculatePrice, getUserByID). 37 | - **PascalCase** para variáveis e funções exportadas (ProcessPayment, SendEmail). 38 | - Use nomes claros e descritivos, evitando abreviações excessivas (GetUserData e não gud). 39 | - Acrônimos em nomes devem ser tratados como uma palavra (HttpServer → HTTPServer, Api → API). 40 | - Nunca use Impl como sufixo para dizer que está implementando uma interface. 41 | 42 | ### 5. Convenções para Testes 43 | - Arquivos de teste têm o sufixo _test.go (user_service_test.go). 44 | - Funções de teste começam com Test seguido do nome da função testada (TestUserService_Process). 45 | - Benchmarks começam com Benchmark (BenchmarkUserService_Process). 46 | - Exemplos começam com Example (ExampleUserService_Process). 47 | - Os testes ficam no mesmo pacote que o código testado. 48 | 49 | ### 6. Importações 50 | - Organize as importações em grupos: 51 | 1. Pacotes da biblioteca padrão 52 | 2. Pacotes de terceiros 53 | 3. Pacotes internos do projeto 54 | - Use o caminho completo de importação baseado no módulo Go. 55 | 56 | Exemplo: 57 | ```go 58 | import ( 59 | "context" 60 | "fmt" 61 | 62 | "github.com/gin-gonic/gin" 63 | "go.uber.org/zap" 64 | 65 | "github.com/seu-usuario/seu-projeto/internal/domain" 66 | ) 67 | ``` 68 | 69 | ### 7. Tratamento de Erros 70 | - Retorne erros explicitamente em vez de usar panics. 71 | - Use pacotes como "errors" ou "github.com/pkg/errors" para criar e enriquecer erros. 72 | - Verifique erros imediatamente após a chamada que pode gerá-los. 73 | - Evite usar _ para ignorar erros, a menos que seja absolutamente necessário. 74 | 75 | ### 8. Documentação 76 | - Todos os pacotes e funções/tipos exportados devem ter comentários de documentação. 77 | - Comentários de documentação começam com o nome do elemento que estão documentando em inglês. 78 | - Use frases completas com ponto final. 79 | 80 | Exemplo: 81 | ```go 82 | // UserService providers methods... 83 | type UserService struct { 84 | // ... 85 | } 86 | 87 | // Process process the payment 88 | // Returns a response 89 | func (s *UserService) Process(ctx context.Context, req Request) (Response, error) { 90 | // ... 91 | } 92 | ``` 93 | -------------------------------------------------------------------------------- /.cursor/rules/project-management-events.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Quando gerenciar tarefas, documentar resultados ou organizar a estrutura do projeto 3 | globs: .fcai/*, .fcai/**/* 4 | alwaysApply: true 5 | --- 6 | # Gerenciamento de Projeto (.fcai) 7 | 8 | > **Nota**: Esta regra fornece orientações sobre quando e como gerenciar tarefas, documentar resultados e organizar a estrutura do projeto. A documentação completa está disponível em `.fcai/project/documentation/` e o estado atual do projeto em [state.md](mdc:.fcai/state.md). 9 | 10 | ## Quando iniciar uma nova tarefa 11 | - Verifique a estrutura atual do projeto em [README.md](mdc:.fcai/README.md) 12 | - Identifique o componente relacionado à tarefa 13 | - Crie a tarefa na pasta apropriada seguindo as convenções do projeto 14 | - **Atualize o arquivo [state.md](mdc:.fcai/state.md) para incluir a nova tarefa** 15 | - Para detalhes completos, consulte [command-implementation.md](mdc:.fcai/project/documentation/command-implementation.md) 16 | 17 | ## Quando documentar resultados de uma tarefa 18 | - Crie um arquivo de resultados com o sufixo `_result` 19 | - Inclua resumo, desafios, soluções, resultados de testes e próximos passos 20 | - Marque as tarefas como concluídas no arquivo original 21 | - Para detalhes completos, consulte [command-implementation.md](mdc:.fcai/project/documentation/command-implementation.md) 22 | 23 | ## Quando concluir uma tarefa 24 | - Mova a tarefa para a pasta `completed/` 25 | - **Sempre valide a tarefa de forma prática antes de marcá-la como concluída**: 26 | - Execute todos os testes automatizados relacionados 27 | - Verifique o funcionamento no container 28 | - Confirme que todos os critérios de aceitação foram atendidos 29 | - **Atualize o arquivo [state.md](mdc:.fcai/state.md)** para refletir a conclusão 30 | - Para detalhes completos, consulte [command-implementation.md](mdc:.fcai/project/documentation/command-implementation.md) 31 | 32 | ## Quando fazer ajustes em tarefas já finalizadas 33 | - Atualize o arquivo de resultados correspondente 34 | - Documente claramente quais ajustes foram feitos e por quê 35 | - **Atualize o arquivo [state.md](mdc:.fcai/state.md)** se os ajustes alterarem significativamente o estado do projeto 36 | 37 | ## Quando documentar aspectos do projeto 38 | - Use as pastas apropriadas em `.fcai/project/` para diferentes tipos de documentação 39 | - Nomeie os arquivos de forma descritiva, usando kebab-case 40 | - Mantenha a documentação atualizada conforme o projeto evolui 41 | 42 | ## Quando revisar o progresso do projeto 43 | - Consulte as pastas de tarefas concluídas, em andamento e backlog 44 | - Use a documentação em `.fcai/project/` para entender o contexto geral 45 | - **Atualize o arquivo [state.md](mdc:.fcai/state.md)** após a revisão 46 | - Use os comandos do projeto para facilitar a visualização do estado 47 | 48 | ## Quando atualizar o estado do projeto 49 | - **Sempre atualize o arquivo [state.md](mdc:.fcai/state.md)** quando houver mudanças significativas 50 | - O arquivo deve conter informações sobre componentes, tarefas e estrutura 51 | - Use os comandos do projeto para facilitar a atualização e visualização do estado 52 | - Para detalhes completos, consulte [command-implementation.md](mdc:.fcai/project/documentation/command-implementation.md) 53 | 54 | ## Quando trabalhar com containers 55 | - Utilize preferencialmente Docker neste projeto 56 | - Todos os comandos devem ser executados dentro dos containers utilizando docker compose exec 57 | - Para mais detalhes, consulte [container-events.mdc](mdc:.cursor/rules/container-events.mdc) 58 | 59 | ## Quando trabalhar com a arquitetura do projeto 60 | - Siga os princípios de Domain Driven Design e Clean Architecture 61 | - Mantenha o modelo de domínio separado das entidades do banco de dados 62 | - Para mais detalhes, consulte [architecture-events.mdc](mdc:.cursor/rules/architecture-events.mdc) 63 | 64 | ## Referências 65 | 66 | Para informações detalhadas, consulte: 67 | 68 | - **Estrutura do projeto**: [README.md](mdc:.fcai/README.md) 69 | - **Estado atual**: [state.md](mdc:.fcai/state.md) 70 | - **Sistema de comandos**: `.fcai/commands.md` e `.fcai/project/documentation/command-implementation.md` 71 | - **Mapeamento de migração**: `.fcai/project/documentation/migracao.md` 72 | - **Regras do sistema de comandos**: `.cursor/rules/command-system-events.mdc` 73 | 74 | -------------------------------------------------------------------------------- /.cursor/rules/testing-events-go.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Convenções e práticas para testes automatizados em Go 3 | globs: **/*_test.go 4 | alwaysApply: true 5 | --- 6 | # Ao trabalhar com testes automatizados em Go 7 | 8 | ## Estrutura e Organização 9 | - Os arquivos de teste devem estar no mesmo pacote que o código testado, com o sufixo `_test.go` 10 | - Para testes de integração ou testes que precisam de isolamento, use o sufixo `_test` no nome do pacote 11 | - Espelhe a estrutura de diretórios do código fonte nos testes 12 | - Não implemente testes para interfaces, apenas para implementações concretas 13 | 14 | ## Nomenclatura 15 | - Arquivos de teste devem seguir o padrão `nome_do_arquivo_test.go` (ex: `user_service_test.go`) 16 | - Funções de teste devem começar com `Test` seguido do nome da função ou método testado em PascalCase 17 | - Use nomes descritivos que indiquem o que está sendo testado (ex: `TestUserService_Create_ValidUser`) 18 | - Para subtestes, use `t.Run()` com nomes descritivos em formato de frase (ex: `t.Run("should return error when email is invalid", func(t *testing.T) {...})`) 19 | 20 | ## Execução 21 | - Sempre execute os testes a partir do diretório raiz do projeto 22 | - Sempre execute os testes a partir do container com o docker compose exec 23 | - Use o comando `go test ./...` para executar todos os testes do projeto 24 | - Para testes específicos, use `go test ./caminho/para/pacote` 25 | - Para testes com cobertura, use `go test ./... -coverprofile=coverage.out` 26 | 27 | ## Práticas Recomendadas 28 | - Use tabelas de testes (`table-driven tests`) para testar múltiplos cenários 29 | - Utilize subtestes com `t.Run()` para organizar melhor os testes 30 | - Use `testify` ou pacotes similares para asserções mais expressivas 31 | - Implemente testes de benchmark quando relevante com o prefixo `Benchmark` 32 | - Crie exemplos executáveis com o prefixo `Example` para documentação 33 | 34 | ## Execução com Docker 35 | - Use o Docker para garantir ambiente consistente: `docker compose exec app go test ./...` 36 | - Para testes com cobertura: `docker compose exec app go test ./... -coverprofile=coverage.out` 37 | - Para visualizar a cobertura: `docker compose exec app go tool cover -html=coverage.out -o coverage.html` 38 | 39 | ## Mocks e Stubs 40 | - Use interfaces para facilitar a criação de mocks 41 | - Prefira ferramentas como `gomock` ou `testify/mock` para geração de mocks 42 | - Mantenha os mocks em um subdiretório `mocks/` dentro do pacote testado 43 | 44 | ## Testes de Integração 45 | - Mantenha testes de integração separados dos testes unitários 46 | - Use tags de compilação para separar testes de integração: `// +build integration` 47 | - Execute testes de integração explicitamente: `go test ./... -tags=integration` 48 | -------------------------------------------------------------------------------- /.cursor/rules/troubleshooting-events.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: when having trouble to solve a problem make sure 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # Quando encontrar muitos erros para executar um script 7 | - Garanta que você esteja utilizando docker compose exec 8 | - Nunca faça upgrade ou downgrade de bibliotecas perguntar antes 9 | - Verifique se você já não fez algo similar antes 10 | -------------------------------------------------------------------------------- /.fcai/README.md: -------------------------------------------------------------------------------- 1 | # Guia de Organização e Estrutura 2 | 3 | ## Estrutura Principal 4 | 5 | .fcai/ 6 | ├── README.md # Este arquivo de orientação 7 | ├── commands.md # Lista de comandos disponíveis 8 | ├── state.md # Estado atual do projeto 9 | ├── structure.md # Estrutura de pastas detalhada 10 | ├── features/ # Features do sistema 11 | └── project/ # Documentação geral do projeto 12 | 13 | ## Instruções de Operação 14 | 15 | ### Inicialização 16 | 1. Ler o [contexto do projeto](.fcai/project/documentation/context.md) para compreender objetivos e escopo 17 | 2. Verificar o [estado atual](.fcai/state.md) para identificar progresso e tarefas em andamento 18 | 3. Explorar as features existentes na pasta [features/](.fcai/features/) 19 | 20 | ### Comandos Disponíveis 21 | Utilize comandos prefixados com `!` para interagir com o projeto: 22 | 23 | - `!read all` - Lê todos os arquivos do projeto dentro da pasta .fcai 24 | - `!task list` - Lista todas as tarefas do projeto 25 | - `!show progress` - Mostra o progresso atual do projeto 26 | 27 | Consulte [commands.md](.fcai/commands.md) para a lista completa. 28 | 29 | ### Fluxo de Trabalho para Tarefas 30 | 1. Verificar tarefas no backlog 31 | 2. Iniciar uma tarefa: `!task start ` 32 | 3. Atualizar progresso: `!task update ` 33 | 4. Completar tarefa: `!task complete ` 34 | 35 | ### Documentação de Referência 36 | - [Visão geral da arquitetura](.fcai/project/architecture/overview.md) 37 | - [Roadmap do projeto](.fcai/project/planning/roadmap.md) 38 | 39 | --- 40 | 41 | Esta estrutura foi projetada para facilitar a organização de documentação e gerenciamento de tarefas, permitindo uma compreensão clara do estado atual e dos próximos passos do projeto. -------------------------------------------------------------------------------- /.fcai/commands.md: -------------------------------------------------------------------------------- 1 | # Comandos Disponíveis 2 | 3 | Este documento lista todos os comandos disponíveis para interagir com o projeto através da IA. Os comandos são prefixados com `!` para distingui-los de mensagens normais. 4 | 5 | ## Comandos de Leitura 6 | 7 | ### !read [arquivo] 8 | Lê o conteúdo de um arquivo específico. 9 | 10 | **Exemplo:** `!read .fcai/state.md` 11 | 12 | ### !read all 13 | Lê todos os arquivos principais do projeto para obter uma visão geral completa. 14 | 15 | **Exemplo:** `!read all` 16 | 17 | ## Comandos de Tarefas 18 | 19 | ### !task list 20 | Lista todas as tarefas do projeto, organizadas por status (backlog, em andamento, concluídas). 21 | 22 | **Exemplo:** `!task list` 23 | 24 | ### !task start [número] 25 | Inicia uma nova tarefa do backlog, movendo-a para o status "em andamento". 26 | 27 | **Exemplo:** `!task start 5` 28 | 29 | ### !task update [número] 30 | Atualiza o progresso de uma tarefa em andamento. 31 | 32 | **Exemplo:** `!task update 3` 33 | 34 | ### !task complete [número] 35 | Marca uma tarefa como concluída, movendo-a para o status "concluída". 36 | 37 | **Exemplo:** `!task complete 2` 38 | 39 | ### !task move [número] [status] 40 | Move uma tarefa para um status específico (backlog, in-progress, completed). 41 | 42 | **Exemplo:** `!task move 4 backlog` 43 | 44 | ## Comandos de Atualização 45 | 46 | ### !update state 47 | Atualiza o arquivo de estado do projeto com as informações mais recentes. 48 | 49 | **Exemplo:** `!update state` 50 | 51 | ### !update all 52 | Atualiza todos os arquivos principais do projeto. 53 | 54 | **Exemplo:** `!update all` 55 | 56 | ## Comandos de Criação 57 | 58 | ### !create feature [nome] 59 | Cria uma nova feature com a estrutura de pastas padrão. 60 | 61 | **Exemplo:** `!create feature user-authentication` 62 | 63 | ### !create task [feature] [nome] 64 | Cria uma nova tarefa no backlog de uma feature específica. 65 | 66 | **Exemplo:** `!create task user-interface login-page` 67 | 68 | ## Comandos de Análise 69 | 70 | ### !show progress 71 | Mostra o progresso geral do projeto, incluindo tarefas concluídas e pendentes. 72 | 73 | **Exemplo:** `!show progress` 74 | 75 | ### !show features 76 | Lista todas as features do projeto com uma breve descrição. 77 | 78 | **Exemplo:** `!show features` 79 | 80 | ## Aliases 81 | 82 | Alguns comandos têm aliases (atalhos) para facilitar o uso: 83 | 84 | - `!r` = `!read` 85 | - `!t` = `!task` 86 | - `!u` = `!update` 87 | - `!c` = `!create` 88 | - `!s` = `!show` 89 | 90 | **Exemplo:** `!t list` é equivalente a `!task list` 91 | 92 | ## Comandos Básicos 93 | - `!help` - Mostra esta lista de comandos 94 | - `!read context` - Lê o contexto geral do projeto 95 | - `!read state` - Lê o estado atual do projeto 96 | 97 | ## Leitura de Features e Tarefas 98 | - `!read feature ` - Lê a documentação de uma feature 99 | - `!read task ` - Lê a documentação de uma tarefa 100 | - `!read result ` - Lê os resultados de uma tarefa 101 | 102 | ## Listagem 103 | - `!list features` - Lista todas as features 104 | - `!list tasks` - Lista todas as tarefas 105 | - `!list tasks active` - Lista tarefas em andamento 106 | - `!list tasks done` - Lista tarefas concluídas 107 | - `!list tasks pending` - Lista tarefas pendentes 108 | 109 | ## Visualização 110 | - `!show structure` - Mostra a estrutura do projeto 111 | - `!show history` - Mostra o histórico recente de alterações 112 | 113 | ## Gerenciamento de Tarefas 114 | - `!task start ` - Inicia uma nova tarefa 115 | - `!task complete ` - Marca uma tarefa como concluída 116 | - `!task update ` - Atualiza o estado de uma tarefa 117 | - `!task move ` - Move uma tarefa para outro status (backlog, in-progress, completed) 118 | 119 | ## Atualização do Estado 120 | - `!update state` - Atualiza o arquivo de estado do projeto 121 | - `!update all` - Atualiza todos os arquivos de documentação 122 | 123 | ## Como Usar 124 | Quando precisar recuperar informações sobre o projeto, basta digitar um desses comandos no chat. Por exemplo: 125 | 126 | ``` 127 | !read context 128 | ``` 129 | 130 | Isso fará com que a IA leia o contexto geral do projeto para recuperar informações importantes. 131 | 132 | ## Fluxo de Trabalho 133 | 134 | ### Iniciando uma Nova Sessão 135 | 1. `!read all` - Para carregar o contexto completo do projeto, lendo todos os arquivos da pasta .fcai 136 | 2. `!list tasks active` - Para ver quais tarefas estão em andamento 137 | 3. `!read task ` - Para ler a tarefa específica que você vai trabalhar 138 | 139 | ### Trabalhando em uma Tarefa 140 | 1. `!read task ` - Para entender os requisitos da tarefa 141 | 2. Desenvolver a solução 142 | 3. `!task update ` - Para atualizar o estado da tarefa 143 | 4. `!update state` - Para atualizar o estado geral do projeto 144 | 145 | ### Concluindo uma Tarefa 146 | 1. `!task complete ` - Para marcar a tarefa como concluída 147 | 2. `!update all` - Para atualizar toda a documentação 148 | 149 | ### Quando Perder o Contexto 150 | 1. `!help` - Para ver a lista de comandos disponíveis 151 | 2. `!read state` - Para entender o estado atual do projeto 152 | 3. `!list tasks active` - Para ver quais tarefas estão em andamento -------------------------------------------------------------------------------- /.fcai/features/video-processing/completed/001-video-entity/001-video-entity.md: -------------------------------------------------------------------------------- 1 | # Tarefa: Implementação da Entidade Video 2 | 3 | ## Descrição 4 | Implementar a entidade de domínio Video, que será responsável por gerenciar o ciclo de vida completo do processamento de vídeos, desde o recebimento até a disponibilização no S3 no formato HLS. 5 | 6 | ## Objetivos 7 | - [x] Criar a estrutura básica da entidade Video com todos os campos necessários 8 | - [x] Definir as constantes para os possíveis estados do vídeo 9 | - [x] Garantir que a entidade siga os princípios de Domain-Driven Design 10 | - [x] Preparar a estrutura para futura implementação dos métodos 11 | - [x] Implementar métodos para gerenciar o estado do vídeo 12 | - [x] Implementar métodos para gerenciar URLs e caminhos de arquivos 13 | 14 | ## Campos Necessários 15 | - [x] ID: Identificador único do vídeo 16 | - [x] Title: Título do vídeo 17 | - [x] FilePath: Caminho do arquivo original no sistema de arquivos 18 | - [x] HLSPath: Caminho onde os arquivos HLS serão armazenados temporariamente 19 | - [x] ManifestPath: Caminho do arquivo de manifesto (.m3u8) 20 | - [x] S3URL: URL final do vídeo no S3 após o upload 21 | - [x] S3ManifestURL: URL do manifesto no S3 22 | - [x] Status: Estado atual do vídeo (pendente, em processamento, concluído, erro) 23 | - [x] UploadStatus: Status do upload para o S3 24 | - [x] ErrorMessage: Mensagem de erro, se houver 25 | - [x] CreatedAt: Data de criação do registro 26 | - [x] UpdatedAt: Data da última atualização do registro 27 | 28 | ## Métodos Implementados 29 | - [x] NewVideo(title, filePath string): Construtor com geração automática de UUID 30 | - [x] MarkAsProcessing(): Atualiza o status para "processing" 31 | - [x] MarkAsCompleted(hslPath, manifestPath string): Atualiza o status para "completed" 32 | - [x] MarkAsFailed(errorMessage string): Atualiza o status para "failed" 33 | - [x] SetS3URL(url string): Define a URL final do vídeo no S3 34 | - [x] SetS3ManifestURL(url string): Define a URL do manifesto no S3 35 | - [x] IsCompleted(): Verifica se o vídeo foi processado com sucesso 36 | - [x] GetHLSDirectory(): Retorna o diretório dos arquivos HLS 37 | - [x] GetManifestPath(): Retorna o caminho do manifesto 38 | - [x] GenerateOutputPath(baseDir string): Gera o caminho base para os arquivos convertidos 39 | 40 | ## Estrutura de Diretórios 41 | - [x] Criar diretório para o domínio (internal/domain) 42 | - [x] Criar arquivo para a entidade Video (internal/domain/entity/video.go) 43 | - [x] Criar arquivo para testes da entidade (internal/domain/entity/video_test.go) -------------------------------------------------------------------------------- /.fcai/features/video-processing/completed/001-video-entity/001-video-entity.result.md: -------------------------------------------------------------------------------- 1 | # Resultado: Implementação da Entidade Video 2 | 3 | ## Resumo 4 | A implementação da entidade Video foi concluída com sucesso. Criamos a estrutura básica da entidade com todos os campos necessários, definimos as constantes para os possíveis estados do vídeo e implementamos os métodos para gerenciar o estado do vídeo durante o processamento. 5 | 6 | ## Implementação 7 | 8 | ### Estrutura de Diretórios 9 | Criamos a seguinte estrutura de diretórios: 10 | ``` 11 | internal/ 12 | └── domain/ 13 | └── entity/ 14 | ├── video.go 15 | └── video_test.go 16 | ``` 17 | 18 | ### Entidade Video 19 | A entidade Video foi implementada com os seguintes campos: 20 | - ID: Identificador único do vídeo (gerado automaticamente como UUID) 21 | - Title: Título do vídeo 22 | - FilePath: Caminho do arquivo original no sistema de arquivos 23 | - HLSPath: Caminho onde os arquivos HLS serão armazenados temporariamente 24 | - ManifestPath: Caminho do arquivo de manifesto (.m3u8) 25 | - S3ManifestURL: URL do manifesto no S3 26 | - S3URL: URL final do vídeo no S3 após o upload 27 | - Status: Estado atual do vídeo 28 | - UploadStatus: Status do upload para o S3 29 | - ErrorMessage: Mensagem de erro, se houver 30 | - CreatedAt: Data de criação do registro 31 | - UpdatedAt: Data da última atualização do registro 32 | 33 | ### Constantes de Status 34 | Definimos as seguintes constantes para os possíveis estados do vídeo: 35 | - StatusPending: Vídeo registrado mas ainda não processado 36 | - StatusProcessing: Vídeo em processamento 37 | - StatusCompleted: Vídeo processado com sucesso 38 | - StatusError: Vídeo com erro durante o processamento (definido como "failed") 39 | 40 | ### Constantes de Status de Upload 41 | Definimos as seguintes constantes para os possíveis estados de upload: 42 | - UploadStatusNone: Nenhum upload iniciado 43 | - UploadStatusPendingS3: Upload pendente para o S3 44 | - UploadStatusUploadingS3: Upload em andamento para o S3 45 | - UploadStatusCompletedS3: Upload concluído para o S3 46 | - UploadStatusFailedS3: Erro durante o upload para o S3 47 | 48 | ### Constantes de Tipo de Arquivo 49 | Definimos as seguintes constantes para os tipos de arquivo: 50 | - FileTypeManifest: Arquivo de manifesto (.m3u8) 51 | - FileTypeSegment: Segmento de vídeo (.ts) 52 | 53 | ### Método Construtor 54 | Implementamos o método `NewVideo` que cria uma nova instância de Video com valores padrão: 55 | - Recebe apenas o título e o caminho do arquivo como parâmetros 56 | - Gera automaticamente um UUID como ID 57 | - Define o status inicial como "pending" e o status de upload como "none" 58 | - Inicializa as datas de criação e atualização 59 | 60 | ### Métodos de Gerenciamento de Estado 61 | Implementamos os seguintes métodos para gerenciar o estado do vídeo: 62 | - `MarkAsProcessing()`: Atualiza o status do vídeo para "processing" 63 | - `MarkAsCompleted(hslPath, manifestPath string)`: Atualiza o status do vídeo para "completed" e define os caminhos dos arquivos HLS 64 | - `MarkAsFailed(errorMessage string)`: Atualiza o status do vídeo para "failed" e registra a mensagem de erro 65 | - `IsCompleted()`: Verifica se o vídeo foi processado com sucesso 66 | 67 | ### Métodos de Gerenciamento de URLs 68 | Implementamos os seguintes métodos para gerenciar as URLs do vídeo: 69 | - `SetS3URL(url string)`: Define a URL final do vídeo no S3 70 | - `SetS3ManifestURL(url string)`: Define a URL do manifesto no S3 71 | 72 | ### Métodos de Gerenciamento de Caminhos 73 | Implementamos os seguintes métodos para gerenciar os caminhos dos arquivos: 74 | - `GetHLSDirectory()`: Retorna o diretório onde os arquivos HLS estão armazenados 75 | - `GetManifestPath()`: Retorna o caminho do arquivo de manifesto 76 | - `GenerateOutputPath(baseDir string) string`: Gera e retorna o caminho base para os arquivos convertidos, seguindo o padrão `baseDir/converted/videoID` 77 | 78 | ### Modificações Importantes 79 | 1. O método `MarkAsCompleted` agora recebe os parâmetros `hslPath` e `manifestPath` para definir os caminhos dos arquivos HLS no momento em que o vídeo é marcado como concluído. 80 | 81 | 2. O método `GenerateOutputPath` foi modificado para: 82 | - Retornar uma string com o caminho de saída 83 | - Incluir um subdiretório "converted" na estrutura do caminho 84 | - Não atualizar diretamente os campos da entidade, apenas gerar e retornar o caminho 85 | 86 | Essas modificações tornam a API da entidade mais flexível, permitindo que o serviço de conversão tenha mais controle sobre os caminhos dos arquivos e quando eles são definidos na entidade. 87 | 88 | ### Testes 89 | Implementamos testes unitários para verificar: 90 | - Se o ID é gerado automaticamente 91 | - Se o construtor inicializa corretamente os campos obrigatórios 92 | - Se os status iniciais estão corretos 93 | - Se as datas são preenchidas corretamente 94 | - Se os campos opcionais estão vazios inicialmente 95 | - Se os métodos de gerenciamento de estado funcionam corretamente 96 | - Se os métodos de gerenciamento de URLs funcionam corretamente 97 | - Se os métodos de gerenciamento de caminhos funcionam corretamente 98 | 99 | ## Próximos Passos 100 | - Implementar o serviço de conversão para HLS 101 | - Implementar o serviço de upload para S3 102 | - Implementar a API REST para upload de vídeos -------------------------------------------------------------------------------- /.fcai/features/video-processing/completed/002-video-repository/002-video-repository.md: -------------------------------------------------------------------------------- 1 | # Tarefa: Implementação da Interface de Repositório de Vídeos 2 | 3 | ## Descrição 4 | Implementar a interface de repositório de vídeos dentro do domínio, seguindo os princípios de Clean Architecture e Domain-Driven Design. Esta interface será responsável por definir as operações que podem ser realizadas em um repositório de vídeos, permitindo diferentes implementações (PostgreSQL, MongoDB, em memória, etc.) sem alterar o domínio. 5 | 6 | ## Objetivos 7 | - [x] Criar a estrutura de diretórios para o repositório 8 | - [x] Definir a interface VideoRepository com todos os métodos necessários 9 | - [x] Garantir que a interface siga os princípios de Clean Architecture 10 | - [x] Preparar a estrutura para futura implementação concreta 11 | 12 | ## Métodos Necessários 13 | - [x] Create(ctx context.Context, video *entity.Video) error 14 | - [x] FindByID(ctx context.Context, id string) (*entity.Video, error) 15 | - [x] List(ctx context.Context, page, pageSize int) ([]*entity.Video, error) 16 | - [x] UpdateStatus(ctx context.Context, id string, status string, errorMessage string) error 17 | - [x] UpdateHLSPath(ctx context.Context, id string, hlsPath, manifestPath string) error 18 | - [x] UpdateS3Status(ctx context.Context, id string, uploadStatus string) error 19 | - [x] UpdateS3URLs(ctx context.Context, id string, s3URL, s3ManifestURL string) error 20 | - [x] UpdateS3Keys(ctx context.Context, id string, segmentKey string, manifestKey string) error 21 | - [x] Delete(ctx context.Context, id string) error 22 | 23 | ## Considerações de Design 24 | - Todos os métodos devem receber um `context.Context` como primeiro parâmetro 25 | - O método `Create` não deve retornar a entidade, apenas um erro se a operação falhar 26 | - O método `List` deve incluir parâmetros de paginação (page e pageSize) e retornar apenas a lista de vídeos e um erro 27 | - O método `UpdateStatus` deve incluir um parâmetro para a mensagem de erro, útil quando o status é "failed" 28 | - Métodos de atualização retornam apenas `error`, não a entidade atualizada 29 | - O método `UpdateS3Keys` recebe `segmentKey` e `manifestKey` em vez de uma lista de chaves 30 | - Métodos específicos para cada tipo de atualização em vez de um único método `Update` 31 | 32 | ## Estrutura de Diretórios 33 | - [x] Criar diretório para o repositório (internal/domain/repository) 34 | - [x] Criar arquivo para a interface VideoRepository (internal/domain/repository/video_repository.go) 35 | 36 | ## Critérios de Aceitação 37 | - A interface deve seguir os princípios de Clean Architecture 38 | - A interface deve ser clara e fácil de implementar 39 | - A interface deve permitir diferentes implementações sem alterar o domínio 40 | - A documentação dos métodos deve ser clara e completa 41 | 42 | ## Dependências 43 | - Entidade Video (já implementada) 44 | 45 | ## Estimativa 46 | - 1 hora 47 | 48 | ## Responsável 49 | - Equipe de desenvolvimento -------------------------------------------------------------------------------- /.fcai/features/video-processing/completed/002-video-repository/002-video-repository.result.md: -------------------------------------------------------------------------------- 1 | # Resultado: Implementação da Interface de Repositório de Vídeos 2 | 3 | ## Resumo 4 | A implementação da interface de repositório de vídeos foi concluída com sucesso. Criamos a interface `VideoRepository` que define todas as operações necessárias para persistir e recuperar vídeos, seguindo os princípios de Clean Architecture e Domain-Driven Design. 5 | 6 | ## Implementação 7 | 8 | ### Estrutura de Diretórios 9 | Criamos a seguinte estrutura de diretórios: 10 | ``` 11 | internal/ 12 | └── domain/ 13 | └── repository/ 14 | └── video_repository.go 15 | ``` 16 | 17 | ### Interface VideoRepository 18 | A interface `VideoRepository` foi implementada com os seguintes métodos: 19 | 20 | ```go 21 | type VideoRepository interface { 22 | Create(ctx context.Context, video *entity.Video) error 23 | FindByID(ctx context.Context, id string) (*entity.Video, error) 24 | List(ctx context.Context, page, pageSize int) ([]*entity.Video, error) 25 | UpdateStatus(ctx context.Context, id string, status string, errorMessage string) error 26 | UpdateHLSPath(ctx context.Context, id string, hlsPath, manifestPath string) error 27 | UpdateS3Status(ctx context.Context, id string, uploadStatus string) error 28 | UpdateS3URLs(ctx context.Context, id string, s3URL, s3ManifestURL string) error 29 | UpdateS3Keys(ctx context.Context, id string, segmentKey string, manifestKey string) error 30 | Delete(ctx context.Context, id string) error 31 | } 32 | ``` 33 | 34 | ### Características da Interface 35 | 36 | 1. **Uso de Context**: Todos os métodos recebem um `context.Context` como primeiro parâmetro, permitindo controle de timeout, cancelamento e passagem de valores entre camadas. 37 | 38 | 2. **Métodos de Criação e Consulta**: 39 | - `Create`: Persiste um novo vídeo e retorna apenas um erro se a operação falhar 40 | - `FindByID`: Busca um vídeo pelo ID e retorna a entidade encontrada ou um erro 41 | - `List`: Retorna uma lista paginada de vídeos 42 | 43 | 3. **Métodos de Atualização**: 44 | - Todos os métodos de atualização retornam apenas `error`, não a entidade atualizada 45 | - `UpdateStatus`: Inclui um parâmetro para a mensagem de erro, útil quando o status é "failed" 46 | - `UpdateHLSPath`: Atualiza os caminhos dos arquivos HLS 47 | - `UpdateS3Status`: Atualiza o status de upload para S3 48 | - `UpdateS3URLs`: Atualiza as URLs do S3 49 | - `UpdateS3Keys`: Atualiza as chaves do S3 (segmentKey e manifestKey) 50 | 51 | 4. **Método de Remoção**: 52 | - `Delete`: Remove um vídeo do repositório 53 | 54 | ### Considerações de Design 55 | 56 | - **Separação de Responsabilidades**: Cada método tem uma responsabilidade clara e específica 57 | - **Métodos Específicos**: Em vez de um único método `Update`, temos métodos específicos para cada tipo de atualização 58 | - **Simplicidade**: Os métodos de atualização retornam apenas `error`, simplificando a interface 59 | - **Flexibilidade**: A interface permite diferentes implementações (PostgreSQL, MongoDB, em memória, etc.) sem alterar o domínio 60 | 61 | ## Próximos Passos 62 | - Implementar a interface em uma camada de infraestrutura (por exemplo, PostgreSQL) 63 | - Criar testes para a implementação 64 | - Injetar a implementação nos serviços que precisam dela -------------------------------------------------------------------------------- /.fcai/features/video-processing/completed/003-video-repository-postgres/003-video-repository-postgres.md: -------------------------------------------------------------------------------- 1 | # Tarefa: Implementação do Repositório PostgreSQL para Vídeos 2 | 3 | ## Descrição 4 | Implementar a versão concreta do repositório de vídeos utilizando PostgreSQL, seguindo a interface definida em `internal/domain/repository/video_repository.go`. Esta implementação será responsável por persistir e recuperar vídeos do banco de dados PostgreSQL. 5 | 6 | ## Objetivos 7 | - [x] Criar a estrutura de migrations para o banco de dados 8 | - [x] Implementar a migration para criação da tabela de vídeos 9 | - [x] Implementar a conexão com o banco de dados PostgreSQL 10 | - [x] Implementar a versão concreta do repositório de vídeos 11 | - [x] Implementar testes de integração para o repositório 12 | 13 | ## Subtarefas 14 | 15 | ### 1. Estrutura de Migrations 16 | - [x] Criar diretório para migrations (`internal/infra/database/migrations`) 17 | - [x] Configurar o pacote `golang-migrate/migrate` para gerenciar as migrations 18 | - [x] Usar o `migrate` para criar as migrations 19 | 20 | ### 2. Criação da Tabela de Vídeos 21 | - [x] Criar migration para criação da tabela `videos` com os seguintes campos: 22 | ``` 23 | CREATE TABLE IF NOT EXISTS videos ( 24 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 25 | title VARCHAR(255) NOT NULL, 26 | description TEXT, 27 | file_path VARCHAR(255) NOT NULL, 28 | status VARCHAR(50) NOT NULL DEFAULT 'pending', 29 | upload_status VARCHAR(50) NOT NULL DEFAULT 'none', 30 | error_message TEXT, 31 | hls_path VARCHAR(255), 32 | manifest_path VARCHAR(255), 33 | s3_url VARCHAR(255), 34 | s3_manifest_url VARCHAR(255), 35 | segment_key VARCHAR(255), 36 | manifest_key VARCHAR(255), 37 | created_at TIMESTAMP NOT NULL DEFAULT NOW(), 38 | updated_at TIMESTAMP NOT NULL DEFAULT NOW(), 39 | deleted_at TIMESTAMP 40 | ) 41 | ``` 42 | - [x] Criar migration para rollback (drop table) 43 | 44 | ### 3. Implementação do Repositório 45 | - [x] Criar estrutura de diretórios (`internal/infra/database/repository`) 46 | - [x] Implementar a conexão com o banco de dados 47 | - [x] Implementar o método `Create` 48 | - [x] Implementar o método `FindByID` 49 | - [x] Implementar o método `List` 50 | - [x] Implementar o método `UpdateStatus` 51 | - [x] Implementar o método `UpdateHLSPath` 52 | - [x] Implementar o método `UpdateS3Status` 53 | - [x] Implementar o método `UpdateS3URLs` 54 | - [x] Implementar o método `UpdateS3Keys` 55 | - [x] Implementar o método `Delete` 56 | 57 | ### 4. Testes de Integração 58 | - [x] Configurar ambiente de teste com banco de dados de teste 59 | - [x] Implementar testes para o método `Create` 60 | - [x] Implementar testes para o método `FindByID` 61 | - [x] Implementar testes para o método `List` 62 | - [x] Implementar testes para os métodos de atualização 63 | - [x] Implementar testes para o método `Delete` 64 | 65 | ## Estrutura de Diretórios 66 | - [x] `internal/infra/database/migrations` - Migrations do banco de dados 67 | - [x] `internal/infra/database/repository` - Implementação do repositório PostgreSQL 68 | - [x] `internal/infra/database/repository/video_repository_test.go` - Testes de integração 69 | 70 | ## Critérios de Aceitação 71 | - A implementação deve seguir a interface definida em `internal/domain/repository/video_repository.go` 72 | - As migrations devem criar corretamente a tabela de vídeos com todos os campos necessários 73 | - Os testes de integração devem verificar o funcionamento correto de todos os métodos 74 | - O código deve seguir as boas práticas de programação em Go 75 | - A implementação deve ser thread-safe e lidar corretamente com erros de banco de dados 76 | 77 | ## Dependências 78 | - Interface VideoRepository (já implementada) 79 | - Entidade Video (já implementada) 80 | - PostgreSQL (disponível via Docker) 81 | 82 | ## Pacotes Necessários 83 | - `database/sql` - Pacote padrão do Go para SQL 84 | - `github.com/lib/pq` - Driver PostgreSQL para Go 85 | - `github.com/golang-migrate/migrate/v4` - Ferramenta para migrations 86 | -------------------------------------------------------------------------------- /.fcai/features/video-processing/completed/003-video-repository-postgres/003-video-repository-postgres_result.md: -------------------------------------------------------------------------------- 1 | # Resultado: Implementação do Repositório PostgreSQL para Vídeos 2 | 3 | ## Resumo 4 | Nesta tarefa, implementamos a versão concreta do repositório de vídeos utilizando PostgreSQL, seguindo a interface definida em `internal/domain/repository/video_repository.go`. A implementação inclui todos os métodos necessários para persistir e recuperar vídeos do banco de dados, bem como testes de integração para verificar o funcionamento correto. 5 | 6 | ## Ações Realizadas 7 | 1. Implementamos a conexão com o banco de dados PostgreSQL em `internal/infra/database/db.go` 8 | 2. Criamos a estrutura de diretórios para o repositório em `internal/infra/database/repository` 9 | 3. Implementamos o repositório PostgreSQL em `internal/infra/database/repository/video_repository.go` 10 | 4. Implementamos testes de integração em `internal/infra/database/repository/video_repository_test.go` 11 | 5. Configuramos os testes de integração com a tag `integration` para separar dos testes unitários 12 | 6. Executamos os testes para verificar o funcionamento correto do repositório 13 | 14 | ## Implementações 15 | ### Conexão com o Banco de Dados 16 | - Implementamos a função `NewConnection` para criar uma conexão com o banco de dados PostgreSQL 17 | - Configuramos o pool de conexões para otimizar o desempenho 18 | - Implementamos a função `Close` para fechar a conexão com o banco de dados 19 | 20 | ### Repositório PostgreSQL 21 | - Implementamos a estrutura `VideoRepositoryPostgres` que implementa a interface `VideoRepository` 22 | - Implementamos todos os métodos da interface: 23 | - `Create`: Persiste um novo vídeo no banco de dados 24 | - `FindByID`: Busca um vídeo pelo seu ID 25 | - `List`: Retorna uma lista de vídeos com paginação 26 | - `UpdateStatus`: Atualiza o status de um vídeo 27 | - `UpdateHLSPath`: Atualiza os caminhos HLS de um vídeo 28 | - `UpdateS3Status`: Atualiza o status de upload para S3 de um vídeo 29 | - `UpdateS3URLs`: Atualiza as URLs do S3 de um vídeo 30 | - `UpdateS3Keys`: Atualiza as chaves do S3 de um vídeo 31 | - `Delete`: Remove um vídeo do repositório (soft delete) 32 | 33 | ### Testes de Integração 34 | - Implementamos testes para todos os métodos do repositório 35 | - Configuramos o ambiente de teste para usar o banco de dados real 36 | - Adicionamos a tag de compilação `//go:build integration` para identificar os testes de integração 37 | - Separamos os testes de integração dos testes unitários conforme as regras do projeto 38 | - Verificamos o funcionamento correto de todos os métodos 39 | - Todos os testes passaram com sucesso 40 | 41 | ## Resultados 42 | - O repositório PostgreSQL foi implementado com sucesso e está funcionando corretamente 43 | - Todos os testes de integração passaram, confirmando o funcionamento correto do repositório 44 | - A implementação segue as boas práticas de programação em Go 45 | - A implementação é thread-safe e lida corretamente com erros de banco de dados 46 | 47 | ## Próximos Passos 48 | 1. Implementar o serviço de conversão para HLS 49 | 2. Implementar o serviço de upload para S3 50 | 3. Implementar a API REST para upload de vídeos 51 | 52 | ## Comandos Utilizados 53 | ```bash 54 | # Executar os testes de integração 55 | docker compose exec app go test -tags=integration -v ./internal/infra/database/repository 56 | ``` -------------------------------------------------------------------------------- /.fcai/features/video-processing/completed/004-database-migrations/004-database-migrations.md: -------------------------------------------------------------------------------- 1 | # Tarefa: Configuração do Banco de Dados e Migrations 2 | 3 | ## Descrição 4 | Configurar o banco de dados PostgreSQL e criar as migrations necessárias para a aplicação, garantindo que a estrutura do banco de dados esteja corretamente definida para suportar o armazenamento e recuperação de vídeos. 5 | 6 | ## Objetivos 7 | - [x] Verificar a estrutura de migrations existente 8 | - [x] Executar as migrations para criar a tabela de vídeos 9 | - [x] Testar a reversão das migrations 10 | - [x] Documentar o processo e os comandos utilizados 11 | 12 | ## Subtarefas 13 | 14 | ### 1. Verificação da Estrutura Existente 15 | - [x] Verificar os arquivos de migration existentes 16 | - [x] Verificar a estrutura da tabela de vídeos 17 | 18 | ### 2. Execução das Migrations 19 | - [x] Instalar o pacote `golang-migrate/migrate` 20 | - [x] Executar o comando de migração para aplicar as migrations (up) 21 | - [x] Verificar a criação correta da tabela `videos` 22 | 23 | ### 3. Teste de Reversão 24 | - [x] Executar o comando de migração para reverter as migrations (down) 25 | - [x] Verificar a remoção correta da tabela `videos` 26 | - [x] Aplicar novamente as migrations para deixar o banco de dados no estado correto 27 | 28 | ### 4. Documentação 29 | - [x] Documentar o processo de execução das migrations 30 | - [x] Documentar os comandos utilizados 31 | - [x] Documentar a estrutura da tabela de vídeos 32 | 33 | ## Critérios de Aceitação 34 | - As migrations devem criar corretamente a tabela de vídeos com todos os campos necessários 35 | - As migrations devem ser reversíveis (up/down) 36 | - O processo deve ser documentado para facilitar a execução por outros desenvolvedores 37 | 38 | ## Dependências 39 | - PostgreSQL (disponível via Docker) 40 | - Pacote `github.com/golang-migrate/migrate/v4` 41 | 42 | ## Pacotes Necessários 43 | - `github.com/golang-migrate/migrate/v4` - Ferramenta para migrations 44 | - `github.com/golang-migrate/migrate/v4/database/postgres` - Driver PostgreSQL para o migrate 45 | - `github.com/golang-migrate/migrate/v4/source/file` - Driver de arquivo para o migrate 46 | 47 | ## Estimativa 48 | - 2 horas 49 | 50 | ## Responsável 51 | - Equipe de desenvolvimento -------------------------------------------------------------------------------- /.fcai/features/video-processing/completed/004-database-migrations/004-database-migrations_result.md: -------------------------------------------------------------------------------- 1 | # Resultado: Configuração do Banco de Dados e Migrations 2 | 3 | ## Resumo 4 | Nesta tarefa, verificamos a configuração do banco de dados e executamos as migrations para criar e testar a tabela de vídeos no PostgreSQL. 5 | 6 | ## Ações Realizadas 7 | 1. Verificamos a estrutura do projeto e identificamos os arquivos de migration existentes 8 | 2. Instalamos o pacote `github.com/golang-migrate/migrate/v4` e seus drivers 9 | 3. Executamos o comando de migração para aplicar as migrations (up) 10 | 4. Verificamos a criação correta da tabela `videos` com todos os campos necessários 11 | 5. Executamos o comando de migração para reverter as migrations (down) 12 | 6. Verificamos a remoção correta da tabela `videos` 13 | 7. Aplicamos novamente as migrations para deixar o banco de dados no estado correto 14 | 15 | ## Resultados 16 | - A tabela `videos` foi criada com sucesso com todos os campos necessários: 17 | - `id` (UUID, chave primária) 18 | - `title` (VARCHAR) 19 | - `description` (TEXT) 20 | - `file_path` (VARCHAR) 21 | - `status` (VARCHAR, default 'pending') 22 | - `upload_status` (VARCHAR, default 'none') 23 | - `error_message` (TEXT) 24 | - `hls_path` (VARCHAR) 25 | - `manifest_path` (VARCHAR) 26 | - `s3_url` (VARCHAR) 27 | - `s3_manifest_url` (VARCHAR) 28 | - `segment_key` (VARCHAR) 29 | - `manifest_key` (VARCHAR) 30 | - `created_at` (TIMESTAMP) 31 | - `updated_at` (TIMESTAMP) 32 | - `deleted_at` (TIMESTAMP) 33 | 34 | - As migrations funcionam corretamente tanto para criar quanto para remover a tabela 35 | 36 | ## Próximos Passos 37 | 1. Implementar o repositório PostgreSQL para vídeos 38 | 2. Implementar os testes de integração para o repositório 39 | 3. Integrar o repositório com o restante da aplicação 40 | 41 | ## Comandos Utilizados 42 | ```bash 43 | # Instalação do CLI do migrate 44 | docker compose exec app go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest 45 | 46 | # Aplicação das migrations (up) 47 | docker compose exec app /go/bin/migrate -path=/app/internal/infra/database/migrations -database "postgres://postgres:postgres@postgres:5432/conversorgo?sslmode=disable" up 48 | 49 | # Reversão das migrations (down) 50 | docker compose exec app /go/bin/migrate -path=/app/internal/infra/database/migrations -database "postgres://postgres:postgres@postgres:5432/conversorgo?sslmode=disable" down 51 | ``` -------------------------------------------------------------------------------- /.fcai/features/video-processing/completed/005-ffmpeg-service/005-ffmpeg-service.md: -------------------------------------------------------------------------------- 1 | # Tarefa 005: Implementação do Serviço de Conversão de Vídeo (FFmpeg) 2 | 3 | ## Descrição 4 | Implementar um serviço para conversão de vídeos para o formato HLS (HTTP Live Streaming) utilizando FFmpeg. O serviço deve fornecer uma interface clara para converter vídeos e coletar os arquivos gerados. 5 | 6 | ## Objetivos 7 | - [x] Criar a interface `FFmpegService` com o método `ConvertToHLS` 8 | - [x] Implementar a estrutura `FFmpegConverter` que implementa a interface 9 | - [x] Implementar o método de execução do FFmpeg com parâmetros configuráveis 10 | - [x] Implementar o método para coletar os arquivos gerados 11 | - [x] Criar testes de integração para validar a conversão 12 | 13 | ## Especificações Técnicas 14 | 15 | ### Interface do Serviço 16 | ```go 17 | type FFmpegService interface { 18 | ConvertToHLS(ctx context.Context, input string, outputDir string) ([]OutputFile, error) 19 | } 20 | ``` 21 | 22 | ### Estrutura do Conversor 23 | ```go 24 | type FFmpegConverter struct { 25 | videoCodec string 26 | audioCodec string 27 | videoBitrate string 28 | audioBitrate string 29 | } 30 | ``` 31 | 32 | ### Estrutura de Saída 33 | ```go 34 | type OutputFile struct { 35 | Path string // Caminho completo do arquivo 36 | Type string // Tipo do arquivo (manifest, segment) 37 | } 38 | ``` 39 | 40 | ### Parâmetros de Configuração 41 | - **videoCodec**: Codec de vídeo a ser utilizado (ex: "libx264") 42 | - **audioCodec**: Codec de áudio a ser utilizado (ex: "aac") 43 | - **videoBitrate**: Taxa de bits para o vídeo (ex: "1000k") 44 | - **audioBitrate**: Taxa de bits para o áudio (ex: "128k") 45 | 46 | ### Localização dos Arquivos 47 | - **Serviço**: `internal/application/service/ffmpeg_service.go` 48 | - **Testes**: `internal/application/service/ffmpeg_service_test.go` 49 | 50 | ## Critérios de Aceitação 51 | 1. O serviço deve converter com sucesso um arquivo de vídeo para o formato HLS 52 | 2. Os arquivos gerados devem incluir um manifesto (.m3u8) e segmentos (.ts) 53 | 3. O serviço deve retornar a lista de arquivos gerados com seus respectivos tipos 54 | 4. Os testes de integração devem passar utilizando um arquivo de vídeo real 55 | 56 | ## Dependências 57 | - FFmpeg instalado no ambiente de execução 58 | - Estrutura de diretórios para armazenar os arquivos temporários 59 | 60 | ## Notas Adicionais 61 | - Para os testes de integração, serão utilizados arquivos de vídeo de teste localizados na pasta `upload/` 62 | - O serviço deve ser implementado de forma a facilitar a integração com o serviço de upload para S3 que será implementado posteriormente -------------------------------------------------------------------------------- /.fcai/features/video-processing/completed/005-ffmpeg-service/005-ffmpeg-service_result.md: -------------------------------------------------------------------------------- 1 | # Resultado da Tarefa 005: Implementação do Serviço de Conversão de Vídeo (FFmpeg) 2 | 3 | ## Resumo 4 | 5 | Foi implementado com sucesso o serviço de conversão de vídeo para o formato HLS utilizando a biblioteca `ffmpeg-go`. O serviço fornece uma interface clara para converter vídeos e coletar os arquivos gerados, seguindo as especificações definidas na tarefa. 6 | 7 | ## Implementação 8 | 9 | ### Estruturas de Dados 10 | 11 | Foram implementadas as seguintes estruturas: 12 | 13 | 1. **OutputFile**: Representa um arquivo gerado pelo processo de conversão, contendo o caminho e o tipo do arquivo. 14 | ```go 15 | type OutputFile struct { 16 | Path string // Caminho completo do arquivo 17 | Type string // Tipo do arquivo (manifest, segment) 18 | } 19 | ``` 20 | 21 | 2. **FFmpegServiceInterface**: Interface que define o contrato para o serviço de conversão. 22 | ```go 23 | type FFmpegServiceInterface interface { 24 | ConvertToHLS(ctx context.Context, input string, outputDir string) ([]OutputFile, error) 25 | } 26 | ``` 27 | 28 | 3. **FFmpegService**: Implementação da interface FFmpegServiceInterface. 29 | ```go 30 | type FFmpegService struct{} 31 | ``` 32 | 33 | ### Métodos Principais 34 | 35 | 1. **ConvertToHLS**: Método principal que converte um vídeo para o formato HLS. 36 | 2. **executeFFmpegConversion**: Método interno que executa a conversão usando a biblioteca ffmpeg-go. 37 | 3. **collectOutputFiles**: Método interno que coleta os arquivos gerados pelo processo de conversão. 38 | 39 | ### Uso da Biblioteca ffmpeg-go 40 | 41 | A implementação utiliza a biblioteca `github.com/u2takey/ffmpeg-go` para interagir com o FFmpeg, oferecendo uma API mais amigável e segura: 42 | 43 | ```go 44 | // Configura os parâmetros para a conversão HLS 45 | hlsParams := ffmpeg.KwArgs{ 46 | // Parâmetros essenciais 47 | "f": "hls", // Formato de saída: HLS 48 | "hls_time": 10, // Duração de cada segmento em segundos 49 | "hls_list_size": 0, // 0 = incluir todos os segmentos no manifesto 50 | 51 | // Codecs de áudio e vídeo 52 | "c:v": "h264", // Codec de vídeo H.264 (amplamente suportado) 53 | "c:a": "aac", // Codec de áudio AAC (amplamente suportado) 54 | "b:a": "128k", // Taxa de bits do áudio: 128 kbps 55 | } 56 | 57 | // Executa o comando FFmpeg 58 | err := ffmpeg.Input(input). 59 | Output(manifestPath, hlsParams). 60 | ErrorToStdOut(). 61 | Run() 62 | ``` 63 | 64 | ### Tratamento de Cancelamento 65 | 66 | O serviço implementa suporte a cancelamento de operações através do contexto: 67 | 68 | ```go 69 | // Verifica se a operação já foi cancelada 70 | if ctx.Err() != nil { 71 | return nil, fmt.Errorf("operação cancelada: %w", ctx.Err()) 72 | } 73 | 74 | // Verifica se a operação foi cancelada durante a execução 75 | if ctx.Err() != nil { 76 | return ctx.Err() 77 | } 78 | ``` 79 | 80 | ### Testes de Integração 81 | 82 | Foi implementado um teste de integração que verifica: 83 | - A conversão bem-sucedida de um vídeo para o formato HLS 84 | - A geração de um arquivo de manifesto (.m3u8) 85 | - A geração de segmentos de vídeo (.ts) 86 | 87 | ## Desafios Encontrados 88 | 89 | 1. **Configuração do FFmpeg**: Foi necessário definir os parâmetros corretos para a conversão HLS com uma única resolução. 90 | 2. **Coleta de Arquivos**: Foi implementada uma lógica para identificar e classificar os arquivos gerados pelo processo de conversão. 91 | 3. **Integração com a Biblioteca**: Foi necessário adaptar a implementação para utilizar a biblioteca ffmpeg-go em vez de chamar o FFmpeg diretamente. 92 | 4. **Tratamento de Cancelamento**: Foi implementado suporte a cancelamento de operações através do contexto. 93 | 5. **Documentação Detalhada**: Foi adicionada documentação detalhada para facilitar o uso e manutenção do serviço. 94 | 95 | ## Soluções Adotadas 96 | 97 | 1. **Uso de Interface**: Foi definida uma interface clara para o serviço, facilitando a criação de mocks para testes. 98 | 2. **Configuração Padrão**: Foram definidos valores padrão para os parâmetros de conversão (codecs, bitrates, etc.), simplificando a API do serviço. 99 | 3. **Tratamento de Erros Robusto**: Foi implementado um tratamento de erros robusto, incluindo suporte a cancelamento de operações. 100 | 4. **Documentação Detalhada**: Foi adicionada documentação detalhada para facilitar o uso e manutenção do serviço. 101 | 102 | ## Resultados 103 | 104 | O serviço implementado atende a todos os critérios de aceitação definidos na tarefa: 105 | 1. Converte com sucesso um arquivo de vídeo para o formato HLS 106 | 2. Gera arquivos de manifesto (.m3u8) e segmentos (.ts) 107 | 3. Retorna a lista de arquivos gerados com seus respectivos tipos 108 | 4. Inclui testes de integração que validam o funcionamento do serviço 109 | 5. Implementa suporte a cancelamento de operações 110 | 111 | ## Próximos Passos 112 | 113 | 1. Integrar o serviço de conversão com o serviço de upload para S3 114 | 2. Implementar suporte a múltiplas resoluções (qualidades de vídeo) 115 | 3. Adicionar monitoramento de progresso da conversão para vídeos longos -------------------------------------------------------------------------------- /.fcai/features/video-processing/completed/006-video-converter-service/006-video-converter-service.md: -------------------------------------------------------------------------------- 1 | # Tarefa 006: Implementação do Serviço de Conversão de Vídeo com Worker Pool 2 | 3 | ## Descrição 4 | Implementar um serviço de conversão de vídeo que utilize o worker pool para processar múltiplas solicitações de conversão simultaneamente. O serviço deve atualizar o status do vídeo no banco de dados conforme o processamento avança. 5 | 6 | ## Objetivos 7 | - [x] Definir as estruturas `ConversionJob` e `ConversionResult` 8 | - [x] Implementar o serviço `VideoConverterService` com as dependências necessárias 9 | - [x] Implementar a função de processamento `processFunc` para o worker pool 10 | - [x] Implementar o método `StartConversion` para iniciar o processo de conversão 11 | - [x] Implementar a atualização de status do vídeo no banco de dados 12 | - [ ] Criar testes unitários e de integração para o serviço 13 | 14 | ## Especificações Técnicas 15 | 16 | ### Estrutura do Job de Conversão 17 | ```go 18 | type ConversionJob struct { 19 | VideoID string 20 | InputPath string 21 | OutputDir string 22 | } 23 | ``` 24 | 25 | ### Estrutura do Resultado da Conversão 26 | ```go 27 | type ConversionResult struct { 28 | VideoID string 29 | Success bool 30 | Error error 31 | OutputFiles []service.OutputFile 32 | Duration time.Duration 33 | } 34 | ``` 35 | 36 | ### Interface do Serviço 37 | ```go 38 | type VideoConverterService interface { 39 | StartConversion(ctx context.Context, inputCh <-chan ConversionJob) (<-chan ConversionResult, error) 40 | Stop() error 41 | IsRunning() bool 42 | } 43 | ``` 44 | 45 | ### Dependências do Serviço 46 | - FFmpegService: Para realizar a conversão de vídeos 47 | - VideoRepository: Para atualizar o status dos vídeos no banco de dados 48 | - WorkerPool: Para processar múltiplas conversões simultaneamente 49 | - Logger: Para registrar eventos e erros durante o processamento 50 | 51 | ### Localização dos Arquivos 52 | - **Serviço**: `internal/application/service/video_converter.go` 53 | - **Testes**: `internal/application/service/video_converter_test.go` 54 | 55 | ## Critérios de Aceitação 56 | 1. O serviço deve processar múltiplas solicitações de conversão simultaneamente 57 | 2. O status do vídeo deve ser atualizado no banco de dados conforme o processamento 58 | 3. O serviço deve retornar os resultados da conversão através de um canal 59 | 4. Os testes devem validar o funcionamento correto do serviço com mocks para as dependências 60 | 61 | ## Dependências 62 | - Serviço FFmpeg implementado (Tarefa 005) 63 | - Repositório de vídeos implementado (Tarefa 002/003) 64 | - Worker Pool implementado (pkg/workerpool) 65 | 66 | ## Notas Adicionais 67 | - O serviço deve ser implementado seguindo os princípios de Clean Architecture 68 | - A implementação deve garantir o tratamento adequado de erros durante a conversão 69 | - O serviço deve ser configurável quanto ao número de workers utilizados -------------------------------------------------------------------------------- /.fcai/features/video-processing/completed/006-video-converter-service/006-video-converter-service_result.md: -------------------------------------------------------------------------------- 1 | # Resultado da Tarefa 006: Implementação do Serviço de Conversão de Vídeo com Worker Pool 2 | 3 | ## Resumo 4 | 5 | Implementamos o serviço de conversão de vídeo utilizando o worker pool para processar múltiplas solicitações de conversão simultaneamente. O serviço é responsável por: 6 | 7 | 1. Receber solicitações de conversão de vídeo 8 | 2. Atualizar o status do vídeo no banco de dados 9 | 3. Converter o vídeo para o formato HLS usando o FFmpeg 10 | 4. Atualizar os caminhos dos arquivos gerados no banco de dados 11 | 5. Retornar os resultados da conversão 12 | 13 | ## Implementação 14 | 15 | ### Estruturas Principais 16 | 17 | - `ConversionJob`: Representa um trabalho de conversão de vídeo 18 | - `ConversionResult`: Representa o resultado de uma conversão 19 | - `VideoConverterService`: Implementa o serviço de conversão de vídeos 20 | 21 | ### Métodos Principais 22 | 23 | - `NewVideoConverter`: Cria uma nova instância do serviço 24 | - `StartConversion`: Inicia o processo de conversão de vídeos 25 | - `StopConversion`: Interrompe o serviço de conversão 26 | - `IsRunning`: Verifica se o serviço está em execução 27 | - `processJob`: Processa um trabalho de conversão de vídeo 28 | 29 | ### Fluxo de Processamento 30 | 31 | 1. O serviço recebe um canal de entrada com jobs de conversão 32 | 2. Para cada job, o serviço: 33 | - Atualiza o status do vídeo para "processing" 34 | - Prepara o diretório de saída 35 | - Converte o vídeo para HLS usando o FFmpeg 36 | - Processa os arquivos de saída 37 | - Atualiza o status do vídeo para "completed" 38 | - Retorna o resultado da conversão 39 | 40 | ## Desafios Encontrados 41 | 42 | ### 1. Integração com o Worker Pool 43 | 44 | A integração com o worker pool foi desafiadora devido à necessidade de adaptar os tipos específicos do serviço para os tipos genéricos do worker pool. Implementamos adaptadores para os canais de entrada e saída. 45 | 46 | ### 2. Testes Automatizados 47 | 48 | Os testes automatizados estão travando, provavelmente devido a problemas com o worker pool. Algumas possíveis causas: 49 | 50 | - Deadlocks nos canais de comunicação 51 | - Problemas de sincronização entre goroutines 52 | - Falta de encerramento adequado dos recursos 53 | 54 | ### 3. Tratamento de Erros 55 | 56 | Implementamos um tratamento de erros robusto, garantindo que: 57 | - Erros durante a atualização do status sejam registrados 58 | - Erros durante a conversão sejam registrados e o status do vídeo seja atualizado 59 | - O serviço não falhe completamente se houver erros em operações não críticas 60 | 61 | ## Próximos Passos 62 | 63 | 1. **Corrigir os testes automatizados**: Resolver os problemas de travamento nos testes 64 | 2. **Melhorar a cobertura de testes**: Adicionar mais casos de teste para cobrir diferentes cenários 65 | 3. **Otimizar o desempenho**: Ajustar os parâmetros do worker pool para melhor desempenho 66 | 4. **Integrar com o serviço de upload para S3**: Preparar a integração com o próximo componente do sistema 67 | 68 | ## Conclusão 69 | 70 | O serviço de conversão de vídeo com worker pool foi implementado com sucesso, seguindo os princípios de Clean Architecture e boas práticas de programação em Go. A implementação é thread-safe e escalável, permitindo o processamento de múltiplas conversões simultaneamente. 71 | 72 | Ainda há trabalho a ser feito para corrigir os testes automatizados e otimizar o desempenho, mas a estrutura básica está pronta e funcional. -------------------------------------------------------------------------------- /.fcai/features/video-processing/documentation/overview.md: -------------------------------------------------------------------------------- 1 | # Feature: Processamento de Vídeo 2 | 3 | ## Descrição 4 | Esta feature é responsável por todo o ciclo de vida do processamento de vídeos, desde o recebimento do arquivo até a disponibilização do vídeo convertido para HLS no S3. 5 | 6 | ## Componentes Principais 7 | 8 | ### 1. Entidade de Domínio: Video 9 | Representa o vídeo e seu estado durante todo o processo de conversão e upload. Contém todas as informações necessárias para rastrear e gerenciar o ciclo de vida do vídeo. 10 | 11 | ### 2. Serviço de Conversão 12 | Responsável por converter o vídeo para o formato HLS utilizando o ffmpeg. 13 | 14 | ### 3. Serviço de Upload 15 | Responsável por fazer o upload dos segmentos HLS e do manifesto para o AWS S3. 16 | 17 | ### 4. API REST 18 | Endpoints para upload de vídeos, consulta de status e obtenção da URL final. 19 | 20 | ## Fluxo de Processamento 21 | 1. Recebimento do vídeo via API 22 | 2. Armazenamento temporário no servidor 23 | 3. Registro no banco de dados 24 | 4. Conversão para HLS (processamento concorrente) 25 | 5. Upload para S3 26 | 6. Atualização do status e URL final 27 | 7. Disponibilização do link para o cliente 28 | 29 | ## Tecnologias Utilizadas 30 | - Go 1.24 31 | - ffmpeg para conversão 32 | - AWS SDK para Go 33 | - PostgreSQL para persistência 34 | - Goroutines e channels para concorrência -------------------------------------------------------------------------------- /.fcai/features/worker-pool/completed/001-worker-pool-update/001-worker-pool-update.md: -------------------------------------------------------------------------------- 1 | # Tarefa: Atualização do Worker Pool 2 | 3 | ## Objetivos 4 | 5 | - [x] Analisar o código atualizado do worker pool em `pkg/workerpool/workerpool.go` 6 | - [x] Identificar as mudanças na API do worker pool 7 | - [x] Atualizar o serviço de conversão de vídeo para ser compatível com o worker pool atualizado 8 | - [x] Atualizar os testes do serviço de conversão de vídeo 9 | - [x] Garantir que todos os testes passem 10 | 11 | ## Descrição 12 | 13 | O worker pool foi atualizado com melhorias na implementação e na API. É necessário atualizar o serviço de conversão de vídeo para ser compatível com essas mudanças. 14 | 15 | ## Critérios de Aceitação 16 | 17 | - O serviço de conversão de vídeo deve ser compatível com o worker pool atualizado 18 | - Os testes do serviço de conversão de vídeo devem passar 19 | - O código deve seguir as boas práticas de programação em Go -------------------------------------------------------------------------------- /.fcai/features/worker-pool/completed/001-worker-pool-update/001-worker-pool-update_result.md: -------------------------------------------------------------------------------- 1 | # Resultado: Atualização do Worker Pool 2 | 3 | ## Resumo 4 | 5 | Atualizamos o serviço de conversão de vídeo para ser compatível com a nova implementação do worker pool. As principais mudanças incluíram: 6 | 7 | 1. Criação de uma estrutura de configuração para o serviço de conversão de vídeo 8 | 2. Atualização do construtor para receber a interface FFmpegService como dependência 9 | 3. Melhoria no tratamento de erros e na conversão de tipos 10 | 4. Atualização dos testes para refletir as mudanças na API 11 | 12 | ## Mudanças Realizadas 13 | 14 | ### 1. Criação de uma estrutura de configuração 15 | 16 | Criamos a estrutura `VideoConverterConfig` para encapsular as configurações do serviço: 17 | 18 | ```go 19 | // VideoConverterConfig representa a configuração do serviço de conversão de vídeo 20 | type VideoConverterConfig struct { 21 | WorkerCount int // Número de workers para processamento paralelo 22 | Logger *slog.Logger // Logger para registro de eventos 23 | } 24 | ``` 25 | 26 | Também implementamos uma função para retornar uma configuração padrão: 27 | 28 | ```go 29 | // DefaultVideoConverterConfig retorna uma configuração padrão para o serviço 30 | func DefaultVideoConverterConfig() VideoConverterConfig { 31 | return VideoConverterConfig{ 32 | WorkerCount: 3, 33 | Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 34 | Level: slog.LevelInfo, 35 | })), 36 | } 37 | } 38 | ``` 39 | 40 | ### 2. Atualização do construtor 41 | 42 | Atualizamos o construtor para receber a interface FFmpegService como dependência e usar a nova estrutura de configuração: 43 | 44 | ```go 45 | // NewVideoConverter cria uma nova instância do serviço de conversão de vídeos 46 | func NewVideoConverter(ffmpeg FFmpegServiceInterface, videoRepo repository.VideoRepository, config VideoConverterConfig) *VideoConverterService { 47 | // ... 48 | } 49 | ``` 50 | 51 | ### 3. Melhoria no tratamento de erros e na conversão de tipos 52 | 53 | Adicionamos verificações de tipo seguras ao processar jobs e resultados: 54 | 55 | ```go 56 | // Conversão de tipo segura 57 | conversionJob, ok := job.(ConversionJob) 58 | if !ok { 59 | return ConversionResult{ 60 | Success: false, 61 | Error: fmt.Errorf("job inválido: esperado ConversionJob"), 62 | } 63 | } 64 | ``` 65 | 66 | E também ao processar resultados: 67 | 68 | ```go 69 | // Conversão de tipo segura 70 | convResult, ok := result.(ConversionResult) 71 | if !ok { 72 | c.logger.Error("resultado inválido do worker pool", "error", "tipo incompatível") 73 | continue 74 | } 75 | ``` 76 | 77 | ### 4. Prevenção de bloqueios em canais 78 | 79 | Adicionamos tratamento de contexto para evitar bloqueios em canais: 80 | 81 | ```go 82 | select { 83 | case jobCh <- job: 84 | // Job enviado com sucesso 85 | case <-ctx.Done(): 86 | return 87 | } 88 | ``` 89 | 90 | E também ao enviar resultados: 91 | 92 | ```go 93 | select { 94 | case conversionResultCh <- convResult: 95 | // Resultado enviado com sucesso 96 | case <-ctx.Done(): 97 | return 98 | } 99 | ``` 100 | 101 | ### 5. Atualização dos testes 102 | 103 | Atualizamos os testes para refletir as mudanças na API: 104 | 105 | - Uso da estrutura `VideoConverterConfig` 106 | - Uso da função `DefaultVideoConverterConfig()` 107 | - Passagem explícita do mock do FFmpegService 108 | - Uso de contextos com timeout em vez de contextos com cancel 109 | - Melhoria nas asserções para verificar os resultados 110 | 111 | ### 6. Correção de problemas nos testes 112 | 113 | Identificamos e corrigimos um problema nos testes onde estávamos tentando parar o serviço após o processamento, mas o serviço já havia sido encerrado automaticamente quando o canal de entrada foi fechado. Adicionamos uma verificação para parar o serviço apenas se ele ainda estiver em execução: 114 | 115 | ```go 116 | // Parar o serviço apenas se ainda estiver em execução 117 | if converter.IsRunning() { 118 | err = converter.StopConversion() 119 | assert.NoError(t, err) 120 | } 121 | ``` 122 | 123 | ## Resultados 124 | 125 | Todos os testes estão passando, confirmando que o serviço de conversão de vídeo está funcionando corretamente com o worker pool atualizado. 126 | 127 | ``` 128 | === RUN TestNewVideoConverter 129 | --- PASS: TestNewVideoConverter (0.00s) 130 | === RUN TestVideoConverterService_StartConversion_Success 131 | --- PASS: TestVideoConverterService_StartConversion_Success (0.00s) 132 | === RUN TestVideoConverterService_StartConversion_FFmpegError 133 | --- PASS: TestVideoConverterService_StartConversion_FFmpegError (0.00s) 134 | === RUN TestVideoConverterService_StartConversion_UpdateStatusError 135 | --- PASS: TestVideoConverterService_StartConversion_UpdateStatusError (0.00s) 136 | === RUN TestVideoConverterService_StartConversion_AlreadyRunning 137 | --- PASS: TestVideoConverterService_StartConversion_AlreadyRunning (0.00s) 138 | === RUN TestVideoConverterService_StopConversion_NotRunning 139 | --- PASS: TestVideoConverterService_StopConversion_NotRunning (0.00s) 140 | === RUN TestVideoConverterService_IsRunning 141 | --- PASS: TestVideoConverterService_IsRunning (0.00s) 142 | PASS 143 | ``` 144 | 145 | ## Conclusão 146 | 147 | A atualização do serviço de conversão de vídeo para ser compatível com o worker pool atualizado foi concluída com sucesso. As melhorias incluíram: 148 | 149 | 1. Melhor gerenciamento de configuração 150 | 2. Tratamento de erros mais robusto 151 | 3. Prevenção de bloqueios em canais 152 | 4. Testes mais abrangentes 153 | 5. Correção de problemas nos testes 154 | 155 | Essas mudanças tornam o serviço mais confiável e mais fácil de manter. -------------------------------------------------------------------------------- /.fcai/project/architecture/docker.md: -------------------------------------------------------------------------------- 1 | # Configuração do Ambiente com Docker e Docker Compose 2 | 3 | ## **Visão Geral** 4 | Este projeto será executado inteiramente dentro de contêineres Docker, eliminando a necessidade de instalar **Go** e **PostgreSQL** localmente. Para isso, utilizaremos **Docker Compose** para orquestrar os serviços da aplicação e do banco de dados. 5 | 6 | ## **Tecnologias Utilizadas** 7 | - **Docker** para contêinerização do ambiente. 8 | - **Docker Compose** para gerenciar múltiplos serviços. 9 | - **Imagem Base da Aplicação:** `golang:1.24-alpine` para otimização e leveza. 10 | - **Banco de Dados:** PostgreSQL 14 rodando em contêiner. 11 | 12 | ## **Estrutura dos Serviços no Docker Compose** 13 | - **`app`**: Serviço principal onde a aplicação Go será executada dentro do contêiner. 14 | - **`db`**: Serviço PostgreSQL para armazenamento das informações de conversão. 15 | - **`localstack`**: Serviço Localstack para testar o upload para S3 localmente. 16 | 17 | ## **Execução Totalmente Dentro do Container** 18 | - O contêiner da aplicação terá o ambiente Go configurado para rodar diretamente dentro dele. 19 | - O código será montado como um volume para desenvolvimento dinâmico sem necessidade de reconstrução manual do container. 20 | - O serviço será executado com **TTY habilitado** (`tty: true`) para manter o processo ativo. 21 | - O serviço **`localstack`** será executado com **TTY habilitado** (`tty: true`) para manter o processo ativo. 22 | 23 | ## **Bind Mounts** 24 | - O código da aplicação será montado como um bind mount para desenvolvimento, logo, qualquer alteração no código será refletida automaticamente no container. 25 | - O banco de dados será montado como um volume para persistência dos dados. 26 | 27 | ## **Arquivo `docker-compose.yaml` (Descrição Geral)** 28 | - Define os serviços **`app`** e **`db`** e **`localstack`**. 29 | - Configura um volume para persistência dos dados do PostgreSQL. 30 | - Exibe logs da aplicação e do banco em tempo real. -------------------------------------------------------------------------------- /.fcai/project/architecture/env-vars.md: -------------------------------------------------------------------------------- 1 | # Uso de Variáveis de Ambiente no Projeto 2 | 3 | ## **Visão Geral** 4 | 5 | O projeto utilizará **variáveis de ambiente** para tornar a configuração mais flexível, segura e adaptável a diferentes ambientes de execução. Em vez de definir valores sensíveis diretamente no código, utilizaremos variáveis para armazenar informações como **parâmetros do banco de dados**, **chaves de API** e **credenciais de serviços externos**. 6 | 7 | ## **Motivação para Uso de Variáveis de Ambiente** 8 | 9 | - **Flexibilidade**: Permite configurar o sistema para diferentes ambientes (desenvolvimento, teste, produção) sem modificar o código. 10 | - **Segurança**: Evita armazenar credenciais sensíveis no código-fonte, reduzindo riscos de exposição acidental. 11 | - **Facilidade de Deploy**: As configurações podem ser alteradas diretamente via `.env` 12 | 13 | ## **Principais Configurações Usadas no Projeto** 14 | 15 | ### **Exemplos de Banco de Dados** 16 | 17 | - `DB_HOST`: Endereço do servidor PostgreSQL 18 | - `DB_PORT`: Porta do banco de dados 19 | - `DB_USER`: Usuário do banco de dados 20 | - `DB_PASSWORD`: Senha do banco de dados 21 | - `DB_NAME`: Nome do banco de dados 22 | 23 | ### **Exemplos de Serviços Externos e APIs** 24 | 25 | - `AWS_ACCESS_KEY_ID`: Chave de acesso AWS 26 | - `AWS_SECRET_ACCESS_KEY`: Chave secreta AWS 27 | - `S3_BUCKET_NAME`: Nome do bucket S3 28 | - `OPENAI_API_KEY`: Chave de API para uso do OpenAI 29 | - `EMAIL_SERVICE_API_KEY`: Chave de API para serviços de e-mail (ex: SendGrid, Mailgun) 30 | 31 | ### **Configurações Gerais** 32 | 33 | - `APP_ENV`: Define o ambiente de execução (`development`, `staging`, `production`) 34 | - `LOG_LEVEL`: Define o nível de log da aplicação (`debug`, `info`, `warn`, `error`) 35 | 36 | ## **Carregamento das Variáveis no Ambiente Docker** 37 | 38 | - As variáveis serão carregadas automaticamente a partir de um arquivo `.env`, garantindo que a configuração possa ser facilmente alterada sem modificar os arquivos do código. 39 | - No **Docker Compose**, as variáveis serão passadas para os contêineres, os valores default podem ser especificados no serviço através da seção: environment  40 | -------------------------------------------------------------------------------- /.fcai/project/architecture/ffmpeg-video-converter.md: -------------------------------------------------------------------------------- 1 | # Configuração do FFmpeg para Conversão de Vídeos 2 | 3 | ## **Visão Geral** 4 | O projeto utilizará o **FFmpeg** para processar e converter vídeos para o formato **HLS (HTTP Live Streaming)**. O FFmpeg será instalado dentro do **contêiner da aplicação**, garantindo que a conversão seja executada diretamente no ambiente containerizado. 5 | 6 | ## **Instalação do FFmpeg no Dockerfile** 7 | - O **FFmpeg** será instalado no contêiner da aplicação, baseado na imagem **Golang 1.24 Alpine**. 8 | - A instalação ocorrerá diretamente no **Dockerfile**, garantindo que todos os serviços tenham o FFmpeg disponível sem necessidade de instalação manual. 9 | 10 | ## **Execução do FFmpeg dentro do Contêiner** 11 | - A conversão de vídeos será feita **inteiramente dentro do container**, eliminando dependências externas no ambiente local. 12 | - O binário do **FFmpeg** será chamado diretamente dentro do código Go para processar os vídeos. 13 | -------------------------------------------------------------------------------- /.fcai/project/architecture/go-libs.md: -------------------------------------------------------------------------------- 1 | # Tecnologias e Pacotes Go Utilizados no Projeto 2 | 3 | ## **Principais Recursos da Biblioteca Padrão do Go** 4 | 5 | ### **1. Logging** 6 | 7 | - `log/slog`: Biblioteca moderna para logging estruturado. 8 | 9 | ## **Principais Pacotes Externos Utilizados** 10 | 11 | ### **1. Banco de Dados** 12 | 13 | - SQL puro usando `database/sql` 14 | - `github.com/golang-migrate/migrate/v4`: Ferramenta para controle de **migrations** do banco de dados. 15 | 16 | ### **2. Web Framework e API REST** 17 | 18 | - `github.com/go-chi/chi/v5`: Roteador minimalista e eficiente para APIs HTTP. 19 | 20 | ### **3. Upload para AWS S3** 21 | 22 | - `github.com/aws/aws-sdk-go-v2`: SDK oficial da AWS para integração com o S3. 23 | - `github.com/localstack/localstack-go`: Para testar o upload para S3 localmente. 24 | 25 | ### **4. Variáveis de Ambiente** 26 | 27 | - `github.com/joho/godotenv`: Para carregar variáveis de ambiente de um arquivo `.env`. 28 | 29 | ### **5. Identificadores Únicos (UUID)** 30 | 31 | - `github.com/google/uuid`: Biblioteca para geração de UUIDs, garantindo identificadores únicos para registros no banco de dados e outras entidades da aplicação. 32 | 33 | ### **6. Testes automatizados** 34 | 35 | - `github.com/stretchr/testify`: Framework de testes para facilitar a escrita de testes unitários e de integração. 36 | 37 | -------------------------------------------------------------------------------- /.fcai/project/architecture/layered-architecture.md: -------------------------------------------------------------------------------- 1 | # Estrutura de Camadas e Responsabilidades do ConversorGo 2 | 3 | ## Visão Geral 4 | 5 | O ConversorGo segue uma arquitetura em camadas para organizar seu código, garantindo separação de responsabilidades, testabilidade e manutenibilidade. Este documento descreve a estrutura arquitetural do projeto, as responsabilidades de cada camada e as diretrizes para implementação. 6 | 7 | ## Princípios Fundamentais 8 | 9 | 1. **Separação de Responsabilidades**: Cada camada tem um propósito específico e bem definido 10 | 2. **Testabilidade**: Todas as camadas podem ser testadas de forma isolada 11 | 3. **Independência de Detalhes Técnicos**: O núcleo da aplicação não depende de detalhes de implementação 12 | 4. **Inversão de Dependência**: Dependências apontam para dentro, não para fora 13 | 5. **Substituibilidade**: Componentes podem ser substituídos sem afetar o restante do sistema 14 | 15 | ## Estrutura de Camadas 16 | 17 | O ConversorGo é organizado nas seguintes camadas: 18 | 19 | ``` 20 | internal/ 21 | ├── domain/ # Regras de negócio e entidades 22 | ├── application/ # Casos de uso e orquestração de serviços 23 | └── infra/ # Implementações técnicas e interfaces com o mundo externo 24 | ``` 25 | 26 | ### 1. Domain (Domínio) 27 | 28 | A camada de domínio contém as entidades e regras de negócio centrais da aplicação, independentes de qualquer detalhe de implementação. 29 | 30 | #### Responsabilidades: 31 | - Definir entidades de negócio 32 | - Definir interfaces de repositórios 33 | - Implementar regras de negócio puras 34 | 35 | #### Estrutura: 36 | ``` 37 | domain/ 38 | ├── entity/ # Entidades de negócio 39 | └── repository/ # Interfaces de repositórios 40 | ``` 41 | 42 | #### Diretrizes: 43 | - Não deve depender de nenhuma outra camada 44 | - Não deve importar pacotes externos exceto os da biblioteca padrão Go 45 | - Deve conter apenas regras de negócio puras 46 | - Deve definir interfaces que serão implementadas por camadas externas 47 | 48 | #### Exemplo: 49 | ```go 50 | // domain/entity/video.go 51 | package entity 52 | 53 | type Video struct { 54 | ID string 55 | Title string 56 | FilePath string 57 | Status string 58 | // ... 59 | } 60 | 61 | func (v *Video) CanBeProcessed() bool { 62 | return v.Status == "pending" || v.Status == "failed" 63 | } 64 | 65 | // domain/repository/video_repository.go 66 | package repository 67 | 68 | type VideoRepository interface { 69 | FindByID(ctx context.Context, id string) (*entity.Video, error) 70 | UpdateStatus(ctx context.Context, id, status, errorMessage string) error 71 | // ... 72 | } 73 | ``` 74 | 75 | ### 2. Application (Aplicação) 76 | 77 | A camada de aplicação contém os casos de uso da aplicação, orquestrando o fluxo entre entidades e serviços de infraestrutura. 78 | 79 | #### Responsabilidades: 80 | - Implementar casos de uso 81 | - Orquestrar entre domínio e infraestrutura 82 | - Gerenciar transações e fluxo de dados 83 | - Implementar serviços de aplicação 84 | 85 | #### Estrutura: 86 | ``` 87 | application/ 88 | ├── service/ # Serviços de aplicação 89 | └── usecase/ # Casos de uso específicos 90 | ``` 91 | 92 | #### Diretrizes: 93 | - Pode depender apenas da camada de domínio 94 | - Não deve conter regras de negócio, apenas orquestração 95 | - Pode receber implementações de infraestrutura via injeção de dependência 96 | 97 | #### Exemplo: 98 | ```go 99 | // application/service/video_converter.go 100 | package service 101 | 102 | import ( 103 | "github.com/devfullcycle/golangtechweek/internal/domain/repository" 104 | ) 105 | 106 | // VideoConverter é um serviço de aplicação para conversão de vídeos 107 | type VideoConverter struct { 108 | ffmpeg FFmpegWrapper 109 | videoRepo repository.VideoRepository 110 | // ... 111 | } 112 | 113 | // Interface para o wrapper FFmpeg (definida na aplicação) 114 | type FFmpegWrapper interface { 115 | ConvertToHLS(ctx context.Context, input, outputDir string) ([]OutputFile, error) 116 | GetVideoInfo(ctx context.Context, input string) (VideoInfo, error) 117 | } 118 | 119 | // StartConversion inicia o serviço de conversão 120 | func (c *VideoConverter) StartConversion(ctx context.Context, inputCh <-chan ConversionJob) (<-chan ConversionResult, error) { 121 | // Implementação... 122 | } 123 | ``` 124 | 125 | ### 3. Infrastructure (infra) 126 | 127 | A camada de infraestrutura contém implementações concretas de interfaces definidas no domínio e na aplicação, bem como todos os componentes que interagem com o mundo externo. 128 | 129 | #### Responsabilidades: 130 | - Implementar repositórios 131 | - Integrar com serviços externos 132 | - Fornecer adaptadores para frameworks 133 | - Implementar detalhes técnicos 134 | - Fornecer interfaces com o mundo externo (API, CLI) 135 | 136 | #### Estrutura: 137 | ``` 138 | infra/ 139 | ├── database/ # Implementações de banco de dados 140 | ├── repository/ # Implementações de repositórios 141 | ├── s3/ # Serviços de armazenamento 142 | ├── api/ # API HTTP 143 | │ ├── handler/ # Handlers HTTP 144 | │ └── router.go # Configuração de rotas 145 | ``` 146 | 147 | #### Diretrizes: 148 | - Pode depender das camadas de domínio e aplicação 149 | - Deve implementar interfaces definidas no domínio e na aplicação 150 | - Deve encapsular detalhes técnicos 151 | - Deve ser substituível sem afetar as camadas internas 152 | 153 | 154 | 155 | ## Fluxo de Dependências 156 | 157 | O fluxo de dependências segue a regra da dependência: as camadas internas não conhecem as camadas externas. 158 | 159 | ``` 160 | Infrastructure → Application → Domain 161 | ``` 162 | 163 | ## Injeção de Dependências 164 | 165 | A injeção de dependências é utilizada para fornecer implementações concretas para interfaces: 166 | 167 | ```go 168 | // cmd/app/main.go 169 | func main() { 170 | // Infraestrutura 171 | db := postgres.NewConnection() 172 | videoRepo := postgres.NewVideoRepository(db) 173 | ffmpegWrapper := ffmpeg.NewFFmpegWrapper() 174 | 175 | // Aplicação 176 | videoConverter := service.NewVideoConverter(ffmpegWrapper, videoRepo) 177 | uploadUseCase := usecase.NewUploadVideo(videoRepo, videoConverter) 178 | 179 | // API (parte da infraestrutura) 180 | videoHandler := handler.NewVideoHandler(uploadUseCase) 181 | router := api.NewRouter(videoHandler) 182 | 183 | // Iniciar servidor 184 | http.ListenAndServe(":8080", router) 185 | } 186 | ``` 187 | -------------------------------------------------------------------------------- /.fcai/project/architecture/overview.md: -------------------------------------------------------------------------------- 1 | # Fluxo Completo da Aplicação de Conversão de Vídeos para HLS com Upload para S3 2 | 3 | ## **Fase 1 4 | ### **1. Recebimento do Vídeo pela API** 5 | - O cliente (frontend ou outra aplicação) faz uma requisição **HTTP multipart/form-data** enviando o arquivo de vídeo para a API. 6 | - A API: 7 | 1. Salva o vídeo temporariamente no servidor (ex: `/tmp/uploads`). 8 | 2. Registra os metadados no **banco de dados PostgreSQL**, incluindo: 9 | - Nome do arquivo original. 10 | - Status inicial (`pendente`). 11 | - Timestamp da solicitação. 12 | - ID único do vídeo. 13 | 3. Insere a tarefa na **fila de processamento** usando **channels**. 14 | 15 | ### **2. Processamento Assíncrono da Conversão** 16 | - Um **worker** (goroutine) monitora a fila de processamento e aguarda novos vídeos. 17 | - Quando um novo vídeo entra na fila: 18 | 1. O worker lê o arquivo do disco. 19 | 2. Executa a conversão para **HLS**, gerando: 20 | - Segmentos `.ts`. 21 | - Manifesto `.m3u8`. 22 | 3. Atualiza o **banco de dados** para `em processamento`. 23 | 24 | ### **3. Upload dos Arquivos HLS para S3** 25 | - Após a conversão, uma goroutine inicia o **upload para S3**: 26 | 1. Envia cada segmento `.ts` e o arquivo `.m3u8`. 27 | 2. Após o sucesso do upload, remove os arquivos temporários do servidor. 28 | 3. Atualiza o **banco de dados** com o status `concluído` e a URL do arquivo `.m3u8`. 29 | 30 | ### **4. Consulta do Status e Acesso ao Vídeo Convertido** 31 | - **API HTTP** permite: 32 | - Consultar status do vídeo (`pendente`, `em processamento`, `concluído`). 33 | - Obter a URL do vídeo convertido no S3. 34 | 35 | --- 36 | 37 | ## **Contexto Geral do Projeto** 38 | - **Nome do Módulo:** `github.com/devfullcycle/golangtechweek` 39 | - **Versão do Go:** `1.24` 40 | - **Objetivo do Projeto:** 41 | - Criar um sistema eficiente e escalável para conversão de vídeos em **HLS**, permitindo **upload via API HTTP**, processamento concorrente e armazenamento final na **AWS S3**. 42 | - A solução permite que aplicações enviem vídeos para serem convertidos automaticamente para streaming, facilitando a distribuição de mídia de forma otimizada. 43 | - Além da conversão, o sistema fornece um **endpoint HTTP** para consultar o status das conversões e recuperar a URL do vídeo convertido. 44 | 45 | - **Principais Tecnologias:** 46 | - **Linguagem:** Go 1.24 47 | - **Processamento de Mídia:** `ffmpeg` para conversão de vídeos para HLS 48 | - **Armazenamento:** AWS S3 para guardar os segmentos e manifestos gerados 49 | - **Banco de Dados:** PostgreSQL para rastrear status das conversões 50 | - **Container:** Docker para criar o ambiente de execução tanto para aplicação em Go quanto para o banco de dados PostgreSQL através do docker-compose.yml 51 | - **Localstack**: Para testar o upload para S3 localmente 52 | -------------------------------------------------------------------------------- /.fcai/project/documentation/context.md: -------------------------------------------------------------------------------- 1 | # Contexto Geral do Projeto: Conversão de Vídeos para HLS com Upload para S3 2 | 3 | ## **Descrição do Projeto** 4 | O projeto tem como objetivo criar uma **API REST escalável e eficiente** para processamento de vídeos, convertendo-os para o formato **HLS (HTTP Live Streaming)** e armazenando os segmentos no **AWS S3**. A aplicação será desenvolvida em **Go 1.24**, utilizando um modelo baseado em **concorrência eficiente com goroutines e channels** para processar e gerenciar múltiplas conversões simultaneamente. 5 | 6 | A solução permitirá que aplicações externas enviem vídeos via **API HTTP**, iniciando automaticamente a conversão e disponibilizando os arquivos no S3. Os usuários poderão consultar o status da conversão e obter a URL final para visualização do vídeo convertido. 7 | 8 | ## **Objetivo do Projeto** 9 | - Fornecer um serviço de conversão de vídeos **altamente eficiente** e **escalável**. 10 | - Utilizar **concorrência em Go** para permitir múltiplas conversões simultâneas sem impactar o desempenho do sistema. 11 | - Implementar uma API **simples e intuitiva** para upload de vídeos e consulta de status. 12 | - Armazenar os vídeos processados em **AWS S3**, otimizando o acesso e distribuição dos arquivos convertidos. Utilizar o Localstack para testar o upload para S3 localmente. 13 | 14 | ## **Principais Tecnologias Utilizadas** 15 | - **Linguagem:** Go 1.24 16 | - **Processamento de Mídia:** `ffmpeg` para conversão dos vídeos para HLS 17 | - **Banco de Dados:** PostgreSQL para rastrear status e metadados das conversões 18 | - **Armazenamento:** AWS S3 para guardar os segmentos `.ts` e os manifestos `.m3u8` 19 | - **Concorrência:** Goroutines e Channels para gerenciar múltiplas conversões simultaneamente 20 | - **APIs:** RESTful HTTP para upload, consulta de status e obtenção do link final do vídeo 21 | 22 | ## **Fluxo Geral da Aplicação** 23 | 1. **Recebimento do Vídeo**: O usuário faz upload de um vídeo via API HTTP. 24 | 2. **Armazenamento Temporário**: O vídeo é salvo temporariamente no servidor. 25 | 3. **Registro no Banco de Dados**: A API registra a entrada do vídeo e seu status inicial (`pendente`). 26 | 4. **Processamento Concorrente**: Um worker em Go processa a conversão para HLS usando `ffmpeg`. 27 | 5. **Upload para AWS S3**: Os segmentos `.ts` e o manifesto `.m3u8` são enviados para um bucket na AWS. 28 | 6. **Atualização do Status**: Após o upload, o banco de dados é atualizado com o status `concluído` e a URL do vídeo. 29 | 7. **Consulta de Status**: O usuário pode consultar via API se a conversão foi concluída e obter o link final. 30 | 31 | ## **Desafios Técnicos e Soluções** 32 | ### **1. Gerenciamento de Concorrência** 33 | - Uso de **worker pools** para processar múltiplos vídeos simultaneamente. 34 | - Uso de **channels** para comunicação eficiente entre processos de conversão e upload. 35 | 36 | ## **Nome do Módulo** 37 | `github.com/devfullcycle/golangtechweek` 38 | -------------------------------------------------------------------------------- /.fcai/state.md: -------------------------------------------------------------------------------- 1 | # Estado Atual do Projeto 2 | 3 | Este arquivo fornece uma referência rápida do contexto atual do projeto, detalhando features ativas, tarefas em andamento, tarefas concluídas e prioridades futuras. 4 | 5 | ## Features Ativas 6 | 7 | ### Video Processing 8 | - **Descrição**: Feature responsável pelo processamento de vídeos, conversão para HLS e upload para S3 9 | - **Status**: Em desenvolvimento 10 | - **Documentação**: [Visão Geral](.fcai/features/video-processing/documentation/overview.md) 11 | - **Componentes Implementados**: 12 | - Entidade Video com métodos para gerenciamento de estado, URLs e caminhos 13 | - Interface VideoRepository para persistência e recuperação de vídeos 14 | - Implementação PostgreSQL do VideoRepository 15 | - Serviço FFmpeg para conversão de vídeos para o formato HLS 16 | - Serviço de conversão de vídeo com worker pool atualizado 17 | 18 | ### Worker Pool 19 | - **Descrição**: Componente responsável pelo processamento paralelo de tarefas 20 | - **Status**: Implementado 21 | - **Componentes Implementados**: 22 | - Worker Pool com configuração flexível 23 | - Tratamento de erros robusto 24 | - Prevenção de bloqueios em canais 25 | - Integração com o serviço de conversão de vídeo 26 | 27 | ## Tarefas no Backlog 28 | 29 | ### Video Processing 30 | // Nenhuma tarefa no backlog no momento 31 | 32 | ## Tarefas em Andamento 33 | 34 | ### Video Processing 35 | // Nenhuma tarefa em andamento no momento 36 | 37 | ## Tarefas Concluídas 38 | 39 | ### Video Processing 40 | 1. [001-video-entity](.fcai/features/video-processing/completed/001-video-entity/001-video-entity.md) - Implementação da entidade Video com todos os métodos necessários 41 | 2. [002-video-repository](.fcai/features/video-processing/completed/002-video-repository/002-video-repository.md) - Implementação da interface de repositório de vídeos 42 | 3. [003-video-repository-postgres](.fcai/features/video-processing/completed/003-video-repository-postgres/003-video-repository-postgres.md) - Implementação do repositório PostgreSQL para vídeos 43 | 4. [004-database-migrations](.fcai/features/video-processing/completed/004-database-migrations/004-database-migrations.md) - Configuração do banco de dados e migrations 44 | 5. [005-ffmpeg-service](.fcai/features/video-processing/completed/005-ffmpeg-service/005-ffmpeg-service.md) - Implementação do serviço de conversão de vídeo usando FFmpeg 45 | 6. [006-video-converter-service](.fcai/features/video-processing/completed/006-video-converter-service/006-video-converter-service.md) - Implementação do serviço de conversão de vídeo com worker pool 46 | 47 | ### Worker Pool 48 | 1. [001-worker-pool-update](.fcai/features/worker-pool/completed/001-worker-pool-update/001-worker-pool-update.md) - Atualização do worker pool e integração com o serviço de conversão de vídeo 49 | 50 | ## Próximos Passos 51 | 1. Planejar a implementação do serviço de upload para S3 52 | 2. Planejar a implementação da API REST para upload de vídeos 53 | 3. Planejar a integração entre o serviço de conversão e o serviço de upload -------------------------------------------------------------------------------- /.fcai/structure.md: -------------------------------------------------------------------------------- 1 | # Estrutura de Pastas do Projeto 2 | 3 | Este documento descreve a estrutura de pastas recomendada para projetos que utilizam este template. A estrutura foi projetada para facilitar a organização de documentação, tarefas e features do projeto. 4 | 5 | ## Visão Geral 6 | 7 | ``` 8 | .fcai/ 9 | ├── README.md # Visão geral do projeto e instruções iniciais 10 | ├── commands.md # Lista de comandos disponíveis para interação 11 | ├── state.md # Estado atual do projeto e progresso 12 | ├── structure.md # Este arquivo (estrutura de pastas) 13 | ├── features/ # Pasta para features do sistema 14 | │ └── [feature-name]/ # Um diretório para cada feature 15 | │ ├── documentation/ # Documentação da feature 16 | │ │ └── overview.md # Visão geral da feature 17 | │ ├── backlog/ # Tarefas planejadas para o futuro 18 | │ │ └── [number]-[name]/ # Pasta para cada tarefa no backlog 19 | │ │ └── [number]-[name].md # Descrição da tarefa 20 | │ ├── in-progress/ # Tarefas em andamento 21 | │ │ └── [number]-[name]/ # Pasta para cada tarefa em andamento 22 | │ │ ├── [number]-[name].md # Descrição da tarefa 23 | │ │ └── [number]-[name].result.md # Resultados parciais da tarefa 24 | │ └── completed/ # Tarefas concluídas 25 | │ └── [number]-[name]/ # Pasta para cada tarefa concluída 26 | │ ├── [number]-[name].md # Descrição da tarefa 27 | │ └── [number]-[name].result.md # Resultados da tarefa 28 | └── project/ # Documentação geral do projeto 29 | ├── documentation/ # Documentação principal 30 | │ └── context.md # Contexto geral do projeto 31 | ├── planning/ # Planejamento do projeto 32 | │ └── roadmap.md # Roadmap e cronograma 33 | ├── architecture/ # Documentação de arquitetura 34 | │ └── overview.md # Visão geral da arquitetura 35 | └── analysis/ # Análises e estudos 36 | └── [analysis-name].md # Documentos de análise específicos 37 | ``` 38 | 39 | ## Detalhamento das Pastas 40 | 41 | ### Arquivos Principais 42 | 43 | - **README.md**: Contém uma visão geral do projeto, instruções de como começar e referências para documentação mais detalhada. 44 | - **commands.md**: Lista todos os comandos disponíveis para interagir com o projeto através da IA. 45 | - **state.md**: Mantém o estado atual do projeto, incluindo progresso, features ativas e tarefas em andamento. 46 | - **structure.md**: Este arquivo, que descreve a estrutura de pastas do projeto. 47 | 48 | ### Pasta `features/` 49 | 50 | Esta pasta contém um diretório para cada feature do sistema. Cada feature segue a mesma estrutura interna: 51 | 52 | - **documentation/**: Contém a documentação específica da feature. 53 | - **overview.md**: Visão geral da feature, incluindo propósito, funcionalidades e arquitetura interna. 54 | 55 | - **backlog/**: Contém tarefas planejadas para o futuro. 56 | - **[number]-[name]/**: Uma pasta para cada tarefa no backlog. 57 | - **task.md**: Descrição detalhada da tarefa. 58 | 59 | - **in-progress/**: Contém tarefas que estão sendo trabalhadas atualmente. 60 | - **[number]-[name]/**: Uma pasta para cada tarefa em andamento. 61 | - **task.md**: Descrição detalhada da tarefa. 62 | - **result.md**: Resultados parciais da tarefa. 63 | 64 | - **completed/**: Contém tarefas que foram concluídas. 65 | - **[number]-[name]/**: Uma pasta para cada tarefa concluída. 66 | - **task.md**: Descrição detalhada da tarefa. 67 | - **result.md**: Resultados finais da tarefa. 68 | 69 | ### Pasta `project/` 70 | 71 | Esta pasta contém a documentação geral do projeto: 72 | 73 | - **documentation/**: Contém a documentação principal do projeto. 74 | - **context.md**: Descreve o contexto geral do projeto, incluindo objetivos, escopo e visão geral. 75 | 76 | - **planning/**: Contém documentos relacionados ao planejamento do projeto. 77 | - **roadmap.md**: Descreve o roadmap e cronograma do projeto. 78 | 79 | - **architecture/**: Contém documentação relacionada à arquitetura do sistema. 80 | - **overview.md**: Visão geral da arquitetura do sistema. 81 | 82 | - **analysis/**: Contém análises e estudos relacionados ao projeto. 83 | - **[analysis-name].md**: Documentos específicos de análise. 84 | 85 | ## Convenções de Nomenclatura 86 | 87 | 1. **Pastas de Tarefas**: Sempre use o formato `[number]-[task-name]`, onde: 88 | - `[number]` é um identificador numérico único para a tarefa 89 | - `[task-name]` é um nome curto e descritivo, usando hífens para separar palavras 90 | 91 | 2. **Arquivos Markdown**: Use nomes em minúsculas, com hífens para separar palavras (ex: `command-implementation.md`). 92 | 93 | 3. **Features**: Use nomes em minúsculas, com hífens para separar palavras (ex: `user-interface`). 94 | 95 | ## Gerenciamento de Tarefas 96 | 97 | As tarefas seguem um fluxo de trabalho específico: 98 | 99 | 1. Inicialmente, as tarefas são criadas no `backlog/` da feature relevante. 100 | 2. Quando o trabalho começa, a tarefa é movida para `in-progress/`. 101 | 3. Quando a tarefa é concluída, ela é movida para `completed/`. 102 | 103 | Este fluxo pode ser gerenciado usando os comandos definidos em `commands.md`, como `!task start`, `!task update` e `!task complete`. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binários compilados 2 | /conversorgo 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Arquivos de teste 10 | *.test 11 | *.tmp 12 | 13 | # Arquivos de saída de cobertura 14 | *.out 15 | *.prof 16 | coverage.html 17 | 18 | # Dependências 19 | /vendor/ 20 | 21 | # Arquivos de IDE e editores 22 | .idea/ 23 | .vscode/ 24 | *.swp 25 | *.swo 26 | *~ 27 | 28 | # Arquivos de sistema operacional 29 | .DS_Store 30 | Thumbs.db 31 | 32 | # Arquivos de ambiente 33 | .env.local 34 | .env.development.local 35 | .env.test.local 36 | .env.production.local 37 | .env 38 | # Diretório de uploads 39 | /uploads/* 40 | !/uploads/.gitkeep 41 | 42 | # Arquivos de log 43 | *.log 44 | 45 | # Arquivos temporários 46 | /tmp/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine AS builder 2 | 3 | WORKDIR /app 4 | 5 | # Copiar arquivos de dependências 6 | COPY go.mod go.sum* ./ 7 | 8 | # Baixar dependências 9 | RUN go mod download 10 | 11 | # Copiar o código fonte 12 | COPY . . 13 | 14 | # Compilar a aplicação 15 | RUN CGO_ENABLED=0 GOOS=linux go build -o /conversorgo ./cmd/app 16 | 17 | # Imagem final 18 | FROM golang:1.24-alpine 19 | 20 | # Instalar FFmpeg e dependências 21 | RUN apk add --no-cache ffmpeg ca-certificates tzdata 22 | 23 | # Criar diretório para uploads 24 | RUN mkdir -p /uploads 25 | 26 | # Copiar o binário compilado 27 | COPY --from=builder /conversorgo /usr/local/bin/conversorgo 28 | 29 | # Definir diretório de trabalho 30 | WORKDIR /app 31 | 32 | # Copiar o código fonte para permitir desenvolvimento dentro do container 33 | COPY . . 34 | 35 | # Expor porta 36 | EXPOSE 8080 37 | 38 | # Comando para executar a aplicação 39 | # Usando tail -f /dev/null como fallback para manter o container em execução 40 | CMD ["sh", "-c", "tail -f /dev/null"] -------------------------------------------------------------------------------- /cmd/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func main() { 8 | fmt.Println("Aplicação iniciada com sucesso!") 9 | } 10 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | ) 8 | 9 | type Product struct { 10 | ID string `json:"id"` 11 | Name string 12 | Price float64 13 | } 14 | 15 | func (p Product) IncreasePrice(amount float64) *Product { 16 | if amount < 0 { 17 | return nil 18 | } 19 | return &p 20 | } 21 | 22 | func main() { 23 | product := Product{ 24 | ID: "1", 25 | Name: "Product 1", 26 | Price: 10.0, 27 | } 28 | jsonProduct, err := json.Marshal(product) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | fmt.Println(string(jsonProduct)) 33 | 34 | var product2 Product 35 | json.Unmarshal(jsonProduct, &product2) 36 | 37 | fmt.Println(product2) 38 | } 39 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Config struct { 4 | Port string 5 | } 6 | 7 | func NewConfig() *Config { 8 | return &Config{ 9 | Port: "8081", 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | container_name: conversorgo-app 7 | ports: 8 | - "8080:8080" 9 | volumes: 10 | - ./uploads:/uploads 11 | - .:/app 12 | environment: 13 | - DB_HOST=postgres 14 | - DB_PORT=5432 15 | - DB_USER=postgres 16 | - DB_PASSWORD=postgres 17 | - DB_NAME=conversorgo 18 | - DB_SSL_MODE=disable 19 | - AWS_REGION=us-east-1 20 | - S3_BUCKET=conversorgo-videos 21 | - AWS_ACCESS_KEY_ID=test 22 | - AWS_SECRET_ACCESS_KEY=test 23 | - S3_ENDPOINT=http://localstack:4566 24 | depends_on: 25 | - postgres 26 | - localstack 27 | networks: 28 | - conversorgo-network 29 | restart: unless-stopped 30 | tty: true 31 | stdin_open: true 32 | 33 | postgres: 34 | image: postgres:14-alpine 35 | container_name: conversorgo-postgres 36 | ports: 37 | - "5433:5432" 38 | volumes: 39 | - postgres-data:/var/lib/postgresql/data 40 | - ./internal/infra/database/migrations:/migrations 41 | environment: 42 | - POSTGRES_USER=postgres 43 | - POSTGRES_PASSWORD=postgres 44 | - POSTGRES_DB=conversorgo 45 | networks: 46 | - conversorgo-network 47 | restart: unless-stopped 48 | 49 | localstack: 50 | image: localstack/localstack:1.4 51 | container_name: conversorgo-localstack 52 | ports: 53 | - "4566:4566" 54 | environment: 55 | - SERVICES=s3 56 | - DEFAULT_REGION=us-east-1 57 | - AWS_ACCESS_KEY_ID=test 58 | - AWS_SECRET_ACCESS_KEY=test 59 | networks: 60 | - conversorgo-network 61 | restart: unless-stopped 62 | 63 | networks: 64 | conversorgo-network: 65 | driver: bridge 66 | 67 | volumes: 68 | postgres-data: 69 | driver: local -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/devfullcycle/golangtechweek 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.49.6 // indirect 7 | github.com/davecgh/go-spew v1.1.1 // indirect 8 | github.com/golang-migrate/migrate/v4 v4.18.2 // indirect 9 | github.com/google/uuid v1.6.0 // indirect 10 | github.com/hashicorp/errwrap v1.1.0 // indirect 11 | github.com/hashicorp/go-multierror v1.1.1 // indirect 12 | github.com/jmespath/go-jmespath v0.4.0 // indirect 13 | github.com/lib/pq v1.10.9 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | github.com/stretchr/objx v0.5.2 // indirect 16 | github.com/stretchr/testify v1.10.0 // indirect 17 | github.com/u2takey/ffmpeg-go v0.5.0 // indirect 18 | github.com/u2takey/go-utils v0.3.1 // indirect 19 | go.uber.org/atomic v1.7.0 // indirect 20 | gopkg.in/yaml.v3 v3.0.1 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= 2 | github.com/aws/aws-sdk-go v1.49.6 h1:yNldzF5kzLBRvKlKz1S0bkvc2+04R1kt13KfBWQBfFA= 3 | github.com/aws/aws-sdk-go v1.49.6/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= 8 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 9 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 10 | github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 11 | github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8= 12 | github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk= 13 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 14 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 15 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 16 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 17 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 18 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 19 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 20 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 21 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 22 | github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 23 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 24 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 25 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 26 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 27 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 28 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 29 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 30 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 31 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 32 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 33 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 34 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 35 | github.com/panjf2000/ants/v2 v2.4.2/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A= 36 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 37 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 38 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 39 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 40 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 41 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 42 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 43 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 44 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 45 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 46 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 47 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 48 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 49 | github.com/u2takey/ffmpeg-go v0.5.0 h1:r7d86XuL7uLWJ5mzSeQ03uvjfIhiJYvsRAJFCW4uklU= 50 | github.com/u2takey/ffmpeg-go v0.5.0/go.mod h1:ruZWkvC1FEiUNjmROowOAps3ZcWxEiOpFoHCvk97kGc= 51 | github.com/u2takey/go-utils v0.3.1 h1:TaQTgmEZZeDHQFYfd+AdUT1cT4QJgJn/XVPELhHw4ys= 52 | github.com/u2takey/go-utils v0.3.1/go.mod h1:6e+v5vEZ/6gu12w/DC2ixZdZtCrNokVxD0JUklcqdCs= 53 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 54 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 55 | gocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs= 56 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 57 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 58 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 59 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 60 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 61 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 62 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 63 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 64 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 65 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 66 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 67 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 68 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 69 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 70 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 71 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 72 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 73 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 74 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 75 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 76 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 77 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 78 | -------------------------------------------------------------------------------- /internal/application/service/ffmpeg_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/devfullcycle/golangtechweek/internal/domain/entity" 11 | ffmpeg "github.com/u2takey/ffmpeg-go" 12 | ) 13 | 14 | // OutputFile representa um arquivo gerado pela conversão 15 | type OutputFile struct { 16 | Path string // Caminho completo do arquivo 17 | Type string // Tipo do arquivo (manifest, segment) 18 | } 19 | 20 | // FFmpegServiceInterface define a interface para o serviço de conversão de vídeos usando FFmpeg. 21 | // Esta interface facilita a criação de mocks para testes e segue o princípio de inversão de dependência. 22 | // 23 | // Exemplo de uso com mock em testes: 24 | // 25 | // mockFFmpeg := new(MockFFmpegService) 26 | // mockFFmpeg.On("ConvertToHLS", ctx, inputPath, outputDir).Return(expectedFiles, nil) 27 | // // Use o mock no seu teste 28 | type FFmpegServiceInterface interface { 29 | // ConvertToHLS converte um arquivo de vídeo para o formato HLS (HTTP Live Streaming). 30 | // Retorna uma lista de arquivos gerados (manifesto e segmentos) e um possível erro. 31 | ConvertToHLS(ctx context.Context, input string, outputDir string) ([]OutputFile, error) 32 | } 33 | 34 | // FFmpegService implementa a interface FFmpegServiceInterface usando o pacote ffmpeg-go. 35 | // Esta estrutura não possui campos, pois não precisa armazenar estado. 36 | type FFmpegService struct{} 37 | 38 | // NewFFmpegService cria uma nova instância do serviço FFmpeg. 39 | // 40 | // Exemplo de uso: 41 | // 42 | // ffmpegService := NewFFmpegService() 43 | // outputFiles, err := ffmpegService.ConvertToHLS(ctx, "video.mp4", "./output") 44 | func NewFFmpegService() *FFmpegService { 45 | return &FFmpegService{} 46 | } 47 | 48 | // Sobre permissões de arquivos em notação octal (como 0o755): 49 | // 50 | // As permissões em sistemas Unix/Linux são representadas por 9 bits, organizados em 3 grupos: 51 | // - Proprietário (dono do arquivo) 52 | // - Grupo (usuários do mesmo grupo) 53 | // - Outros (todos os demais usuários) 54 | // 55 | // Cada grupo tem 3 tipos de permissão: 56 | // - r (read/leitura): Valor 4 57 | // - w (write/escrita): Valor 2 58 | // - x (execute/execução): Valor 1 59 | // 60 | // A notação octal é usada porque cada dígito octal representa exatamente 3 bits: 61 | // - 7 (4+2+1) = rwx = leitura + escrita + execução 62 | // - 5 (4+0+1) = r-x = leitura + execução 63 | // - 6 (4+2+0) = rw- = leitura + escrita 64 | // - 4 (4+0+0) = r-- = apenas leitura 65 | // 66 | // Exemplos comuns: 67 | // - 0o755 (rwxr-xr-x): Diretórios padrão, scripts executáveis 68 | // - 0o644 (rw-r--r--): Arquivos regulares (documentos, imagens) 69 | // - 0o600 (rw-------): Arquivos privados (chaves SSH, senhas) 70 | // 71 | // No código abaixo, usamos 0o755 para o diretório de saída porque queremos que: 72 | // 1. O processo que executa o código possa criar, modificar e listar arquivos 73 | // 2. Outros usuários possam listar e acessar os arquivos, mas não modificar o diretório 74 | 75 | // ConvertToHLS converte um vídeo para o formato HLS (HTTP Live Streaming). 76 | // Parâmetros: 77 | // - ctx: Contexto que permite cancelamento da operação 78 | // - input: Caminho do arquivo de vídeo de entrada 79 | // - outputDir: Diretório onde os arquivos HLS serão salvos 80 | // 81 | // Retorna: 82 | // - Uma lista de arquivos gerados (manifesto e segmentos) 83 | // - Um erro, se ocorrer 84 | // 85 | // Exemplo de uso: 86 | // 87 | // ctx := context.Background() 88 | // outputFiles, err := ffmpegService.ConvertToHLS(ctx, "video.mp4", "./output") 89 | // if err != nil { 90 | // log.Fatalf("Erro ao converter vídeo: %v", err) 91 | // } 92 | // fmt.Printf("Arquivos gerados: %d\n", len(outputFiles)) 93 | func (s *FFmpegService) ConvertToHLS(ctx context.Context, input string, outputDir string) ([]OutputFile, error) { 94 | // Verifica se a operação já foi cancelada 95 | if ctx.Err() != nil { 96 | return nil, fmt.Errorf("operação cancelada: %w", ctx.Err()) 97 | } 98 | 99 | // Cria o diretório de saída (e diretórios pai, se necessário) 100 | if err := os.MkdirAll(outputDir, 0o755); err != nil { 101 | return nil, fmt.Errorf("erro ao criar diretório de saída: %w", err) 102 | } 103 | 104 | // Executa a conversão do vídeo para o formato HLS 105 | if err := s.executeFFmpegConversion(ctx, input, outputDir); err != nil { 106 | return nil, fmt.Errorf("erro na conversão FFmpeg: %w", err) 107 | } 108 | 109 | // Coleta os arquivos gerados pela conversão 110 | return s.collectOutputFiles(outputDir) 111 | } 112 | 113 | // executeFFmpegConversion executa o comando FFmpeg para converter o vídeo para HLS. 114 | // Esta função configura e executa o FFmpeg com os parâmetros necessários para 115 | // criar um stream HLS a partir do vídeo de entrada. 116 | func (s *FFmpegService) executeFFmpegConversion(ctx context.Context, input string, outputDir string) error { 117 | // Define o caminho do arquivo de manifesto (playlist principal) 118 | manifestPath := filepath.Join(outputDir, "playlist.m3u8") 119 | 120 | // Configura os parâmetros para a conversão HLS 121 | hlsParams := ffmpeg.KwArgs{ 122 | // Parâmetros essenciais 123 | "f": "hls", // Formato de saída: HLS 124 | "hls_time": 10, // Duração de cada segmento em segundos 125 | "hls_list_size": 0, // 0 = incluir todos os segmentos no manifesto 126 | 127 | // Codecs de áudio e vídeo 128 | "c:v": "h264", // Codec de vídeo H.264 (amplamente suportado) 129 | "c:a": "aac", // Codec de áudio AAC (amplamente suportado) 130 | "b:a": "128k", // Taxa de bits do áudio: 128 kbps 131 | } 132 | 133 | // Executa o comando FFmpeg 134 | err := ffmpeg.Input(input). 135 | Output(manifestPath, hlsParams). 136 | ErrorToStdOut(). 137 | Run() 138 | 139 | // Verifica se a operação foi cancelada durante a execução 140 | if ctx.Err() != nil { 141 | return ctx.Err() 142 | } 143 | 144 | // Retorna o erro do FFmpeg, se houver 145 | if err != nil { 146 | return err 147 | } 148 | 149 | return nil 150 | } 151 | 152 | // collectOutputFiles lista e categoriza os arquivos gerados pela conversão. 153 | // Esta função percorre o diretório de saída e identifica os arquivos de manifesto (.m3u8) 154 | // e os segmentos de vídeo (.ts) gerados pelo FFmpeg. 155 | func (s *FFmpegService) collectOutputFiles(outputDir string) ([]OutputFile, error) { 156 | // Lista todos os arquivos no diretório de saída 157 | files, err := os.ReadDir(outputDir) 158 | if err != nil { 159 | return nil, fmt.Errorf("erro ao listar arquivos gerados: %w", err) 160 | } 161 | 162 | // Cria um slice para armazenar os arquivos de saída 163 | outputFiles := make([]OutputFile, 0, len(files)) 164 | 165 | // Processa cada arquivo encontrado 166 | for _, file := range files { 167 | fileName := file.Name() 168 | filePath := filepath.Join(outputDir, fileName) 169 | 170 | // Determina o tipo do arquivo com base na extensão 171 | var fileType string 172 | if strings.HasSuffix(fileName, ".m3u8") { 173 | // Arquivos .m3u8 são manifestos (playlists) 174 | fileType = entity.FileTypeManifest 175 | } else { 176 | // Outros arquivos (geralmente .ts) são segmentos de vídeo 177 | fileType = entity.FileTypeSegment 178 | } 179 | 180 | // Adiciona o arquivo à lista de saída 181 | outputFiles = append(outputFiles, OutputFile{ 182 | Path: filePath, 183 | Type: fileType, 184 | }) 185 | } 186 | 187 | return outputFiles, nil 188 | } 189 | -------------------------------------------------------------------------------- /internal/application/service/ffmpeg_service_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package service_test 5 | 6 | import ( 7 | "context" 8 | "os" 9 | "testing" 10 | 11 | "github.com/devfullcycle/golangtechweek/internal/application/service" 12 | "github.com/devfullcycle/golangtechweek/internal/domain/entity" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | // TestFFmpegService_ConvertToHLS_Integration é um teste de integração 18 | // que requer a presença de um arquivo de vídeo real e o FFmpeg instalado 19 | func TestFFmpegService_ConvertToHLS_Integration(t *testing.T) { 20 | // Criar instância do serviço 21 | ffmpegService := service.NewFFmpegService() 22 | 23 | // Definir diretório de saída 24 | outputDir := "/app/uploads/converted" 25 | 26 | // Limpar o diretório de saída antes do teste 27 | os.RemoveAll(outputDir) 28 | 29 | // Criar o diretório de saída 30 | err := os.MkdirAll(outputDir, 0755) 31 | require.NoError(t, err) 32 | 33 | // Garantir que o diretório será limpo após o teste 34 | defer os.RemoveAll(outputDir) 35 | 36 | // Caminho para o vídeo de teste na pasta uploads 37 | testVideoPath := "/app/uploads/44444444-4444-4444-4444-444444444444.mp4" 38 | 39 | // Verificar se o arquivo de teste existe 40 | _, err = os.Stat(testVideoPath) 41 | if os.IsNotExist(err) { 42 | t.Logf("Arquivo de teste não encontrado: %s", testVideoPath) 43 | t.Skip("Arquivo de vídeo de teste não encontrado") 44 | } 45 | 46 | // Executar a conversão 47 | ctx := context.Background() 48 | outputFiles, err := ffmpegService.ConvertToHLS(ctx, testVideoPath, outputDir) 49 | 50 | // Verificar se não houve erro 51 | require.NoError(t, err) 52 | 53 | // Verificar se foram gerados arquivos 54 | assert.NotEmpty(t, outputFiles) 55 | 56 | // Verificar se o manifesto foi gerado 57 | var hasManifest bool 58 | for _, file := range outputFiles { 59 | if file.Type == entity.FileTypeManifest { 60 | hasManifest = true 61 | break 62 | } 63 | } 64 | assert.True(t, hasManifest, "O manifesto não foi gerado") 65 | 66 | // Verificar se foram gerados segmentos 67 | var segmentCount int 68 | for _, file := range outputFiles { 69 | if file.Type == entity.FileTypeSegment { 70 | segmentCount++ 71 | } 72 | } 73 | assert.Greater(t, segmentCount, 0, "Nenhum segmento foi gerado") 74 | 75 | // Imprimir informações sobre os arquivos gerados 76 | t.Logf("Arquivos gerados no diretório: %s", outputDir) 77 | for _, file := range outputFiles { 78 | t.Logf("- %s (tipo: %s)", file.Path, file.Type) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /internal/application/service/video_converter.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "os" 8 | "path/filepath" 9 | "time" 10 | 11 | "github.com/devfullcycle/golangtechweek/internal/domain/entity" 12 | "github.com/devfullcycle/golangtechweek/internal/domain/repository" 13 | "github.com/devfullcycle/golangtechweek/pkg/workerpool" 14 | ) 15 | 16 | // ConversionJob representa um trabalho de conversão de vídeo 17 | type ConversionJob struct { 18 | VideoID string // ID do vídeo no banco de dados 19 | InputPath string // Caminho do arquivo de entrada 20 | OutputDir string // Diretório de saída para os arquivos convertidos 21 | } 22 | 23 | // ConversionResult representa o resultado de uma conversão 24 | type ConversionResult struct { 25 | VideoID string // ID do vídeo no banco de dados 26 | Success bool // Indica se a conversão foi bem-sucedida 27 | Error error // Erro ocorrido durante a conversão, se houver 28 | OutputFiles []OutputFile // Lista de arquivos gerados pela conversão 29 | Duration time.Duration // Duração do processo de conversão 30 | } 31 | 32 | // VideoConverterService implementa o serviço de conversão de vídeos 33 | type VideoConverterService struct { 34 | ffmpeg FFmpegServiceInterface 35 | videoRepo repository.VideoRepository 36 | workerPool workerpool.WorkerPool 37 | logger *slog.Logger 38 | } 39 | 40 | // VideoConverterConfig representa a configuração do serviço de conversão de vídeo 41 | type VideoConverterConfig struct { 42 | WorkerCount int // Número de workers para processamento paralelo 43 | Logger *slog.Logger // Logger para registro de eventos 44 | } 45 | 46 | // DefaultVideoConverterConfig retorna uma configuração padrão para o serviço 47 | func DefaultVideoConverterConfig() VideoConverterConfig { 48 | return VideoConverterConfig{ 49 | WorkerCount: 3, 50 | Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 51 | Level: slog.LevelInfo, 52 | })), 53 | } 54 | } 55 | 56 | // NewVideoConverter cria uma nova instância do serviço de conversão de vídeos 57 | func NewVideoConverter(ffmpeg FFmpegServiceInterface, videoRepo repository.VideoRepository, config VideoConverterConfig) *VideoConverterService { 58 | if config.WorkerCount <= 0 { 59 | config.WorkerCount = 1 60 | } 61 | 62 | if config.Logger == nil { 63 | config.Logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 64 | Level: slog.LevelInfo, 65 | })) 66 | } 67 | 68 | service := &VideoConverterService{ 69 | ffmpeg: ffmpeg, 70 | videoRepo: videoRepo, 71 | logger: config.Logger, 72 | } 73 | 74 | // Cria a função de processamento para o worker pool 75 | processFunc := func(ctx context.Context, job workerpool.Job) workerpool.Result { 76 | // Conversão de tipo segura 77 | conversionJob, ok := job.(ConversionJob) 78 | if !ok { 79 | return ConversionResult{ 80 | Success: false, 81 | Error: fmt.Errorf("job inválido: esperado ConversionJob"), 82 | } 83 | } 84 | 85 | return service.processJob(ctx, conversionJob) 86 | } 87 | 88 | // Cria a configuração para o worker pool 89 | wpConfig := workerpool.Config{ 90 | WorkerCount: config.WorkerCount, 91 | Logger: config.Logger, 92 | } 93 | 94 | // Cria o worker pool 95 | service.workerPool = workerpool.New(processFunc, wpConfig) 96 | 97 | return service 98 | } 99 | 100 | // StartConversion inicia o serviço de conversão de vídeos 101 | func (c *VideoConverterService) StartConversion(ctx context.Context, inputCh <-chan ConversionJob) (<-chan ConversionResult, error) { 102 | // Verifica se o serviço já está em execução 103 | if c.workerPool.IsRunning() { 104 | return nil, fmt.Errorf("o serviço de conversão já está em execução") 105 | } 106 | 107 | // Cria um canal para adaptar o canal de entrada genérico para o tipo específico 108 | jobCh := make(chan workerpool.Job) 109 | 110 | // Goroutine para adaptar o canal de entrada 111 | go func() { 112 | defer close(jobCh) 113 | for job := range inputCh { 114 | select { 115 | case jobCh <- job: 116 | // Job enviado com sucesso 117 | case <-ctx.Done(): 118 | return 119 | } 120 | } 121 | }() 122 | 123 | // Inicia o worker pool 124 | resultCh, err := c.workerPool.Start(ctx, jobCh) 125 | if err != nil { 126 | return nil, fmt.Errorf("erro ao iniciar o worker pool: %w", err) 127 | } 128 | 129 | // Cria um canal para adaptar o canal de saída genérico para o tipo específico 130 | conversionResultCh := make(chan ConversionResult) 131 | 132 | // Goroutine para adaptar o canal de saída 133 | go func() { 134 | defer close(conversionResultCh) 135 | for result := range resultCh { 136 | // Conversão de tipo segura 137 | convResult, ok := result.(ConversionResult) 138 | if !ok { 139 | c.logger.Error("resultado inválido do worker pool", "error", "tipo incompatível") 140 | continue 141 | } 142 | 143 | select { 144 | case conversionResultCh <- convResult: 145 | // Resultado enviado com sucesso 146 | case <-ctx.Done(): 147 | return 148 | } 149 | } 150 | }() 151 | 152 | return conversionResultCh, nil 153 | } 154 | 155 | // StopConversion interrompe o serviço de conversão 156 | func (c *VideoConverterService) StopConversion() error { 157 | // Verifica se o serviço está em execução 158 | if !c.workerPool.IsRunning() { 159 | return fmt.Errorf("o serviço de conversão não está em execução") 160 | } 161 | 162 | // Para o worker pool 163 | return c.workerPool.Stop() 164 | } 165 | 166 | // IsRunning retorna true se o serviço estiver em execução 167 | func (c *VideoConverterService) IsRunning() bool { 168 | return c.workerPool.IsRunning() 169 | } 170 | 171 | // processJob processa um trabalho de conversão de vídeo 172 | func (c *VideoConverterService) processJob(ctx context.Context, job ConversionJob) ConversionResult { 173 | startTime := time.Now() 174 | c.logger.Info("Iniciando processamento de vídeo", "video_id", job.VideoID) 175 | 176 | // Inicializa o resultado com falha por padrão 177 | result := ConversionResult{ 178 | VideoID: job.VideoID, 179 | Success: false, 180 | Duration: time.Since(startTime), 181 | } 182 | 183 | // Etapa 1: Atualiza o status do vídeo para "processing" 184 | if err := c.updateVideoStatusToProcessing(ctx, job.VideoID); err != nil { 185 | result.Error = err 186 | return result 187 | } 188 | 189 | // Etapa 2: Prepara o diretório de saída 190 | outputDir := c.prepareOutputDirectory(job) 191 | 192 | // Etapa 3: Converte o vídeo para HLS 193 | outputFiles, err := c.convertVideoToHLS(ctx, job.VideoID, job.InputPath, outputDir) 194 | if err != nil { 195 | result.Error = err 196 | return result 197 | } 198 | 199 | // Etapa 4: Atualiza o resultado com sucesso 200 | result.Success = true 201 | result.OutputFiles = outputFiles 202 | result.Duration = time.Since(startTime) 203 | 204 | // Etapa 5: Processa os arquivos de saída e atualiza o banco de dados 205 | c.processOutputFiles(ctx, job.VideoID, outputFiles) 206 | 207 | c.logger.Info("Processamento de vídeo concluído com sucesso", 208 | "video_id", job.VideoID, 209 | "duration", result.Duration.String(), 210 | "output_files", len(result.OutputFiles)) 211 | 212 | return result 213 | } 214 | 215 | // updateVideoStatusToProcessing atualiza o status do vídeo para "processing" 216 | func (c *VideoConverterService) updateVideoStatusToProcessing(ctx context.Context, videoID string) error { 217 | err := c.videoRepo.UpdateStatus(ctx, videoID, entity.StatusProcessing, "") 218 | if err != nil { 219 | errWithContext := fmt.Errorf("erro ao atualizar status do vídeo para processing: %w", err) 220 | c.logger.Error("Erro ao atualizar status do vídeo", "video_id", videoID, "error", err) 221 | c.videoRepo.UpdateStatus(ctx, videoID, entity.StatusError, errWithContext.Error()) 222 | return errWithContext 223 | } 224 | return nil 225 | } 226 | 227 | // prepareOutputDirectory prepara o diretório de saída para os arquivos convertidos 228 | func (c *VideoConverterService) prepareOutputDirectory(job ConversionJob) string { 229 | outputDir := job.OutputDir 230 | if outputDir == "" { 231 | outputDir = filepath.Join("uploads", "converted", job.VideoID) 232 | } 233 | return outputDir 234 | } 235 | 236 | // convertVideoToHLS converte o vídeo para o formato HLS 237 | func (c *VideoConverterService) convertVideoToHLS(ctx context.Context, videoID, inputPath, outputDir string) ([]OutputFile, error) { 238 | outputFiles, err := c.ffmpeg.ConvertToHLS(ctx, inputPath, outputDir) 239 | if err != nil { 240 | errWithContext := fmt.Errorf("erro ao converter vídeo para HLS: %w", err) 241 | c.logger.Error("Erro ao converter vídeo para HLS", "video_id", videoID, "error", err) 242 | c.videoRepo.UpdateStatus(ctx, videoID, entity.StatusError, errWithContext.Error()) 243 | return nil, errWithContext 244 | } 245 | return outputFiles, nil 246 | } 247 | 248 | // processOutputFiles processa os arquivos de saída e atualiza o banco de dados 249 | func (c *VideoConverterService) processOutputFiles(ctx context.Context, videoID string, outputFiles []OutputFile) { 250 | // Encontra o manifesto e os segmentos 251 | manifestPath, hlsPath := c.findManifestAndHLSPaths(outputFiles) 252 | 253 | // Atualiza os caminhos HLS e Manifest no banco de dados 254 | if manifestPath != "" && hlsPath != "" { 255 | c.updateHLSPaths(ctx, videoID, hlsPath, manifestPath) 256 | } 257 | 258 | // Atualiza o status do vídeo para "completed" 259 | c.updateVideoStatusToCompleted(ctx, videoID) 260 | } 261 | 262 | // findManifestAndHLSPaths encontra os caminhos do manifesto e do diretório HLS 263 | // Retorna o caminho do manifesto e o caminho do diretório HLS 264 | func (c *VideoConverterService) findManifestAndHLSPaths(outputFiles []OutputFile) (string, string) { 265 | var manifestPath string 266 | var hlsPath string 267 | 268 | // Itera sobre os arquivos até encontrar tanto o manifesto quanto o primeiro segmento 269 | for _, file := range outputFiles { 270 | // Se ainda não encontramos o manifesto e este arquivo é um manifesto 271 | if manifestPath == "" && file.Type == entity.FileTypeManifest { 272 | manifestPath = file.Path 273 | } 274 | 275 | // Se ainda não encontramos o caminho HLS e este arquivo é um segmento 276 | if hlsPath == "" && file.Type == entity.FileTypeSegment { 277 | // Usa o diretório do primeiro segmento como caminho HLS 278 | hlsPath = filepath.Dir(file.Path) 279 | } 280 | 281 | // Se já encontramos ambos, podemos sair do loop 282 | if manifestPath != "" && hlsPath != "" { 283 | break 284 | } 285 | } 286 | 287 | return manifestPath, hlsPath 288 | } 289 | 290 | // updateHLSPaths atualiza os caminhos HLS e Manifest no banco de dados 291 | func (c *VideoConverterService) updateHLSPaths(ctx context.Context, videoID, hlsPath, manifestPath string) { 292 | err := c.videoRepo.UpdateHLSPath(ctx, videoID, hlsPath, manifestPath) 293 | if err != nil { 294 | c.logger.Error("Erro ao atualizar caminhos HLS", "video_id", videoID, "error", err) 295 | // Não falha a conversão por erro na atualização dos caminhos 296 | } 297 | } 298 | 299 | // updateVideoStatusToCompleted atualiza o status do vídeo para "completed" 300 | func (c *VideoConverterService) updateVideoStatusToCompleted(ctx context.Context, videoID string) { 301 | err := c.videoRepo.UpdateStatus(ctx, videoID, entity.StatusCompleted, "") 302 | if err != nil { 303 | c.logger.Error("Erro ao atualizar status do vídeo para completed", "video_id", videoID, "error", err) 304 | // Não falha a conversão por erro na atualização do status 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /internal/application/service/video_converter_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log/slog" 7 | "os" 8 | "testing" 9 | "time" 10 | 11 | "github.com/devfullcycle/golangtechweek/internal/domain/entity" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/mock" 14 | ) 15 | 16 | // MockFFmpegService é um mock para o serviço FFmpeg 17 | type MockFFmpegService struct { 18 | mock.Mock 19 | } 20 | 21 | func (m *MockFFmpegService) ConvertToHLS(ctx context.Context, input string, outputDir string) ([]OutputFile, error) { 22 | args := m.Called(ctx, input, outputDir) 23 | return args.Get(0).([]OutputFile), args.Error(1) 24 | } 25 | 26 | // MockVideoRepository é um mock para o repositório de vídeos 27 | type MockVideoRepository struct { 28 | mock.Mock 29 | } 30 | 31 | func (m *MockVideoRepository) Create(ctx context.Context, video *entity.Video) error { 32 | args := m.Called(ctx, video) 33 | return args.Error(0) 34 | } 35 | 36 | func (m *MockVideoRepository) FindByID(ctx context.Context, id string) (*entity.Video, error) { 37 | args := m.Called(ctx, id) 38 | if args.Get(0) == nil { 39 | return nil, args.Error(1) 40 | } 41 | return args.Get(0).(*entity.Video), args.Error(1) 42 | } 43 | 44 | func (m *MockVideoRepository) List(ctx context.Context, page, pageSize int) ([]*entity.Video, error) { 45 | args := m.Called(ctx, page, pageSize) 46 | return args.Get(0).([]*entity.Video), args.Error(1) 47 | } 48 | 49 | func (m *MockVideoRepository) UpdateStatus(ctx context.Context, id string, status string, errorMessage string) error { 50 | args := m.Called(ctx, id, status, errorMessage) 51 | return args.Error(0) 52 | } 53 | 54 | func (m *MockVideoRepository) UpdateHLSPath(ctx context.Context, id string, hlsPath, manifestPath string) error { 55 | args := m.Called(ctx, id, hlsPath, manifestPath) 56 | return args.Error(0) 57 | } 58 | 59 | func (m *MockVideoRepository) UpdateS3Status(ctx context.Context, id string, uploadStatus string) error { 60 | args := m.Called(ctx, id, uploadStatus) 61 | return args.Error(0) 62 | } 63 | 64 | func (m *MockVideoRepository) UpdateS3URLs(ctx context.Context, id string, s3URL, s3ManifestURL string) error { 65 | args := m.Called(ctx, id, s3URL, s3ManifestURL) 66 | return args.Error(0) 67 | } 68 | 69 | func (m *MockVideoRepository) UpdateS3Keys(ctx context.Context, id string, segmentKey string, manifestKey string) error { 70 | args := m.Called(ctx, id, segmentKey, manifestKey) 71 | return args.Error(0) 72 | } 73 | 74 | func (m *MockVideoRepository) Delete(ctx context.Context, id string) error { 75 | args := m.Called(ctx, id) 76 | return args.Error(0) 77 | } 78 | 79 | // Substituir o FFmpegService no VideoConverterService para testes 80 | func replaceFFmpegService(converter *VideoConverterService, ffmpeg FFmpegServiceInterface) { 81 | converter.ffmpeg = ffmpeg 82 | } 83 | 84 | func TestNewVideoConverter(t *testing.T) { 85 | // Arrange 86 | mockRepo := new(MockVideoRepository) 87 | mockFFmpeg := new(MockFFmpegService) 88 | config := VideoConverterConfig{ 89 | WorkerCount: 3, 90 | Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 91 | Level: slog.LevelInfo, 92 | })), 93 | } 94 | 95 | // Act 96 | converter := NewVideoConverter(mockFFmpeg, mockRepo, config) 97 | 98 | // Assert 99 | assert.NotNil(t, converter) 100 | assert.Equal(t, mockRepo, converter.videoRepo) 101 | assert.Equal(t, mockFFmpeg, converter.ffmpeg) 102 | assert.NotNil(t, converter.workerPool) 103 | assert.NotNil(t, converter.logger) 104 | } 105 | 106 | func TestVideoConverterService_StartConversion_Success(t *testing.T) { 107 | // Arrange 108 | mockRepo := new(MockVideoRepository) 109 | mockFFmpeg := new(MockFFmpegService) 110 | config := DefaultVideoConverterConfig() 111 | config.WorkerCount = 1 // Usar apenas 1 worker para simplificar o teste 112 | 113 | converter := NewVideoConverter(mockFFmpeg, mockRepo, config) 114 | 115 | // Configurar o mock do repositório para retornar sucesso ao atualizar o status 116 | mockRepo.On("UpdateStatus", mock.Anything, "test-video-id", entity.StatusProcessing, "").Return(nil) 117 | mockRepo.On("UpdateStatus", mock.Anything, "test-video-id", entity.StatusCompleted, "").Return(nil) 118 | mockRepo.On("UpdateHLSPath", mock.Anything, "test-video-id", "output/dir", "output/dir/manifest.m3u8").Return(nil) 119 | 120 | // Configurar o mock do FFmpeg para retornar arquivos de saída simulados 121 | outputFiles := []OutputFile{ 122 | {Path: "output/dir/manifest.m3u8", Type: entity.FileTypeManifest}, 123 | {Path: "output/dir/segment_0.ts", Type: entity.FileTypeSegment}, 124 | } 125 | mockFFmpeg.On("ConvertToHLS", mock.Anything, "input/path", mock.Anything).Return(outputFiles, nil) 126 | 127 | // Criar um canal de entrada com capacidade para evitar bloqueio 128 | inputCh := make(chan ConversionJob, 1) 129 | 130 | // Criar um contexto com timeout para evitar que o teste fique preso 131 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 132 | defer cancel() 133 | 134 | // Act 135 | resultCh, err := converter.StartConversion(ctx, inputCh) 136 | 137 | // Assert - verificar se o serviço iniciou corretamente 138 | assert.NoError(t, err) 139 | assert.NotNil(t, resultCh) 140 | 141 | // Enviar um job para o canal de entrada 142 | inputCh <- ConversionJob{ 143 | VideoID: "test-video-id", 144 | InputPath: "input/path", 145 | OutputDir: "output/dir", 146 | } 147 | 148 | // Fechar o canal de entrada para sinalizar que não há mais jobs 149 | close(inputCh) 150 | 151 | // Ler o resultado do canal de saída 152 | result := <-resultCh 153 | 154 | // Verificar o resultado 155 | assert.True(t, result.Success) 156 | assert.Nil(t, result.Error) 157 | assert.Equal(t, "test-video-id", result.VideoID) 158 | assert.NotEmpty(t, result.OutputFiles) 159 | assert.Greater(t, result.Duration, time.Duration(0)) 160 | 161 | // Verificar se os mocks foram chamados conforme esperado 162 | mockRepo.AssertExpectations(t) 163 | mockFFmpeg.AssertExpectations(t) 164 | 165 | // Parar o serviço apenas se ainda estiver em execução 166 | if converter.IsRunning() { 167 | err = converter.StopConversion() 168 | assert.NoError(t, err) 169 | } 170 | } 171 | 172 | func TestVideoConverterService_StartConversion_FFmpegError(t *testing.T) { 173 | // Arrange 174 | mockRepo := new(MockVideoRepository) 175 | mockFFmpeg := new(MockFFmpegService) 176 | config := DefaultVideoConverterConfig() 177 | config.WorkerCount = 1 // Usar apenas 1 worker para simplificar o teste 178 | 179 | converter := NewVideoConverter(mockFFmpeg, mockRepo, config) 180 | 181 | // Configurar o mock do repositório 182 | mockRepo.On("UpdateStatus", mock.Anything, "test-video-id", entity.StatusProcessing, "").Return(nil) 183 | mockRepo.On("UpdateStatus", mock.Anything, "test-video-id", entity.StatusError, mock.Anything).Return(nil) 184 | 185 | // Configurar o mock do FFmpeg para retornar erro 186 | ffmpegError := errors.New("erro na conversão") 187 | mockFFmpeg.On("ConvertToHLS", mock.Anything, "input/path", mock.Anything).Return([]OutputFile{}, ffmpegError) 188 | 189 | // Criar um canal de entrada com capacidade para evitar bloqueio 190 | inputCh := make(chan ConversionJob, 1) 191 | 192 | // Criar um contexto com timeout para evitar que o teste fique preso 193 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 194 | defer cancel() 195 | 196 | // Act 197 | resultCh, err := converter.StartConversion(ctx, inputCh) 198 | 199 | // Assert - verificar se o serviço iniciou corretamente 200 | assert.NoError(t, err) 201 | assert.NotNil(t, resultCh) 202 | 203 | // Enviar um job para o canal de entrada 204 | inputCh <- ConversionJob{ 205 | VideoID: "test-video-id", 206 | InputPath: "input/path", 207 | OutputDir: "output/dir", 208 | } 209 | 210 | // Fechar o canal de entrada para sinalizar que não há mais jobs 211 | close(inputCh) 212 | 213 | // Ler o resultado do canal de saída 214 | result := <-resultCh 215 | 216 | // Verificar o resultado 217 | assert.False(t, result.Success) 218 | assert.NotNil(t, result.Error) 219 | assert.Equal(t, "test-video-id", result.VideoID) 220 | assert.Contains(t, result.Error.Error(), "erro na conversão") 221 | 222 | // Verificar se os mocks foram chamados conforme esperado 223 | mockRepo.AssertExpectations(t) 224 | mockFFmpeg.AssertExpectations(t) 225 | 226 | // Parar o serviço apenas se ainda estiver em execução 227 | if converter.IsRunning() { 228 | err = converter.StopConversion() 229 | assert.NoError(t, err) 230 | } 231 | } 232 | 233 | func TestVideoConverterService_StartConversion_UpdateStatusError(t *testing.T) { 234 | // Arrange 235 | mockRepo := new(MockVideoRepository) 236 | mockFFmpeg := new(MockFFmpegService) 237 | config := DefaultVideoConverterConfig() 238 | config.WorkerCount = 1 // Usar apenas 1 worker para simplificar o teste 239 | 240 | converter := NewVideoConverter(mockFFmpeg, mockRepo, config) 241 | 242 | // Configurar o mock do repositório para retornar erro ao atualizar o status 243 | updateError := errors.New("erro ao atualizar status") 244 | mockRepo.On("UpdateStatus", mock.Anything, "test-video-id", entity.StatusProcessing, "").Return(updateError) 245 | mockRepo.On("UpdateStatus", mock.Anything, "test-video-id", entity.StatusError, mock.Anything).Return(nil) 246 | 247 | // Criar um canal de entrada com capacidade para evitar bloqueio 248 | inputCh := make(chan ConversionJob, 1) 249 | 250 | // Criar um contexto com timeout para evitar que o teste fique preso 251 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 252 | defer cancel() 253 | 254 | // Act 255 | resultCh, err := converter.StartConversion(ctx, inputCh) 256 | 257 | // Assert - verificar se o serviço iniciou corretamente 258 | assert.NoError(t, err) 259 | assert.NotNil(t, resultCh) 260 | 261 | // Enviar um job para o canal de entrada 262 | inputCh <- ConversionJob{ 263 | VideoID: "test-video-id", 264 | InputPath: "input/path", 265 | OutputDir: "output/dir", 266 | } 267 | 268 | // Fechar o canal de entrada para sinalizar que não há mais jobs 269 | close(inputCh) 270 | 271 | // Ler o resultado do canal de saída 272 | result := <-resultCh 273 | 274 | // Verificar o resultado 275 | assert.False(t, result.Success) 276 | assert.NotNil(t, result.Error) 277 | assert.Equal(t, "test-video-id", result.VideoID) 278 | assert.Contains(t, result.Error.Error(), "erro ao atualizar status") 279 | 280 | // Verificar se os mocks foram chamados conforme esperado 281 | mockRepo.AssertExpectations(t) 282 | 283 | // Parar o serviço apenas se ainda estiver em execução 284 | if converter.IsRunning() { 285 | err = converter.StopConversion() 286 | assert.NoError(t, err) 287 | } 288 | } 289 | 290 | func TestVideoConverterService_StartConversion_AlreadyRunning(t *testing.T) { 291 | // Arrange 292 | mockRepo := new(MockVideoRepository) 293 | mockFFmpeg := new(MockFFmpegService) 294 | config := DefaultVideoConverterConfig() 295 | 296 | converter := NewVideoConverter(mockFFmpeg, mockRepo, config) 297 | 298 | // Criar canais de entrada 299 | inputCh1 := make(chan ConversionJob, 1) 300 | inputCh2 := make(chan ConversionJob, 1) 301 | 302 | // Criar um contexto com timeout para evitar que o teste fique preso 303 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 304 | defer cancel() 305 | 306 | // Act - Iniciar o serviço pela primeira vez 307 | resultCh1, err1 := converter.StartConversion(ctx, inputCh1) 308 | 309 | // Assert - verificar se o serviço iniciou corretamente 310 | assert.NoError(t, err1) 311 | assert.NotNil(t, resultCh1) 312 | 313 | // Act - Tentar iniciar o serviço novamente 314 | resultCh2, err2 := converter.StartConversion(ctx, inputCh2) 315 | 316 | // Assert - verificar se o serviço retornou erro ao tentar iniciar novamente 317 | assert.Error(t, err2) 318 | assert.Nil(t, resultCh2) 319 | assert.Contains(t, err2.Error(), "o serviço de conversão já está em execução") 320 | 321 | // Parar o serviço 322 | err := converter.StopConversion() 323 | assert.NoError(t, err) 324 | 325 | // Fechar os canais de entrada 326 | close(inputCh1) 327 | close(inputCh2) 328 | } 329 | 330 | func TestVideoConverterService_StopConversion_NotRunning(t *testing.T) { 331 | // Arrange 332 | mockRepo := new(MockVideoRepository) 333 | mockFFmpeg := new(MockFFmpegService) 334 | config := DefaultVideoConverterConfig() 335 | 336 | converter := NewVideoConverter(mockFFmpeg, mockRepo, config) 337 | 338 | // Act - Tentar parar o serviço que não está em execução 339 | err := converter.StopConversion() 340 | 341 | // Assert - verificar se o serviço retornou erro ao tentar parar 342 | assert.Error(t, err) 343 | assert.Contains(t, err.Error(), "o serviço de conversão não está em execução") 344 | } 345 | 346 | func TestVideoConverterService_IsRunning(t *testing.T) { 347 | // Arrange 348 | mockRepo := new(MockVideoRepository) 349 | mockFFmpeg := new(MockFFmpegService) 350 | config := DefaultVideoConverterConfig() 351 | 352 | converter := NewVideoConverter(mockFFmpeg, mockRepo, config) 353 | 354 | // Assert - verificar se o serviço não está em execução inicialmente 355 | assert.False(t, converter.IsRunning()) 356 | 357 | // Criar um canal de entrada 358 | inputCh := make(chan ConversionJob, 1) 359 | 360 | // Criar um contexto com timeout para evitar que o teste fique preso 361 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 362 | defer cancel() 363 | 364 | // Act - Iniciar o serviço 365 | resultCh, err := converter.StartConversion(ctx, inputCh) 366 | 367 | // Assert - verificar se o serviço iniciou corretamente 368 | assert.NoError(t, err) 369 | assert.NotNil(t, resultCh) 370 | 371 | // Assert - verificar se o serviço está em execução após iniciar 372 | assert.True(t, converter.IsRunning()) 373 | 374 | // Act - Parar o serviço 375 | err = converter.StopConversion() 376 | 377 | // Assert - verificar se o serviço parou corretamente 378 | assert.NoError(t, err) 379 | 380 | // Assert - verificar se o serviço não está mais em execução após parar 381 | assert.False(t, converter.IsRunning()) 382 | 383 | // Fechar o canal de entrada 384 | close(inputCh) 385 | } 386 | -------------------------------------------------------------------------------- /internal/domain/entity/video.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | // Status do vídeo durante o ciclo de processamento 10 | const ( 11 | // StatusPending representa um vídeo que foi registrado mas ainda não começou a ser processado 12 | StatusPending = "pending" 13 | // StatusProcessing representa um vídeo que está sendo processado 14 | StatusProcessing = "processing" 15 | 16 | // StatusCompleted representa um vídeo que foi processado com sucesso 17 | StatusCompleted = "completed" 18 | 19 | // StatusError representa um vídeo que encontrou um erro durante o processamento 20 | StatusError = "failed" 21 | ) 22 | 23 | const ( 24 | UploadStatusNone = "none" 25 | UploadStatusPendingS3 = "pending_s3" 26 | UploadStatusUploadingS3 = "uploading_s3" 27 | UploadStatusCompletedS3 = "completed_s3" 28 | UploadStatusFailedS3 = "failed_s3" 29 | ) 30 | 31 | const ( 32 | FileTypeManifest = "manifest" 33 | FileTypeSegment = "segment" 34 | ) 35 | 36 | // Video representa a entidade de domínio para um vídeo que será processado 37 | type Video struct { 38 | ID string // Identificador único do vídeo 39 | Title string // Título do vídeo 40 | FilePath string // Caminho do arquivo original no sistema de arquivos 41 | HLSPath string // Caminho onde os arquivos HLS serão armazenados temporariamente 42 | ManifestPath string // Caminho do arquivo de manifesto (.m3u8) 43 | S3ManifestURL string // URL do manifesto no S3 44 | S3URL string // URL final do vídeo no S3 após o upload 45 | Status string // Estado atual do vídeo 46 | UploadStatus string 47 | ErrorMessage string // Mensagem de erro, se houver 48 | CreatedAt time.Time // Data de criação do registro 49 | UpdatedAt time.Time // Data da última atualização do registro 50 | } 51 | 52 | // NewVideo cria uma nova instância de Video com valores padrão 53 | func NewVideo(title, filePath string) *Video { 54 | now := time.Now() 55 | 56 | return &Video{ 57 | ID: uuid.New().String(), 58 | Title: title, 59 | FilePath: filePath, 60 | Status: StatusPending, 61 | UploadStatus: UploadStatusNone, 62 | CreatedAt: now, 63 | UpdatedAt: now, 64 | } 65 | } 66 | 67 | // MarkAsProcessing atualiza o status do vídeo para "processing" 68 | func (v *Video) MarkAsProcessing() { 69 | v.Status = StatusProcessing 70 | v.UpdatedAt = time.Now() 71 | } 72 | 73 | // MarkAsCompleted atualiza o status do vídeo para "completed" 74 | func (v *Video) MarkAsCompleted(hslPath, manifestPath string) { 75 | v.Status = StatusCompleted 76 | v.HLSPath = hslPath 77 | v.ManifestPath = manifestPath 78 | v.UpdatedAt = time.Now() 79 | } 80 | 81 | // MarkAsFailed atualiza o status do vídeo para "failed" e registra a mensagem de erro 82 | func (v *Video) MarkAsFailed(errorMessage string) { 83 | v.Status = StatusError 84 | v.ErrorMessage = errorMessage 85 | v.UpdatedAt = time.Now() 86 | } 87 | 88 | // SetS3URL define a URL final do vídeo no S3 89 | func (v *Video) SetS3URL(url string) { 90 | v.S3URL = url 91 | v.UpdatedAt = time.Now() 92 | } 93 | 94 | // SetS3ManifestURL define a URL do manifesto no S3 95 | func (v *Video) SetS3ManifestURL(url string) { 96 | v.S3ManifestURL = url 97 | v.UpdatedAt = time.Now() 98 | } 99 | 100 | // IsCompleted verifica se o vídeo foi processado com sucesso 101 | func (v *Video) IsCompleted() bool { 102 | return v.Status == StatusCompleted 103 | } 104 | 105 | // GetHLSDirectory retorna o diretório onde os arquivos HLS estão armazenados 106 | func (v *Video) GetHLSDirectory() string { 107 | return v.HLSPath 108 | } 109 | 110 | // GetManifestPath retorna o caminho do arquivo de manifesto 111 | func (v *Video) GetManifestPath() string { 112 | return v.ManifestPath 113 | } 114 | 115 | // GenerateOutputPath gera os caminhos de saída para os arquivos HLS 116 | func (v *Video) GenerateOutputPath(baseDir string) string { 117 | return baseDir + "/converted/" + v.ID 118 | } 119 | -------------------------------------------------------------------------------- /internal/domain/entity/video_test.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func NewVideTest(t *testing.T) { 9 | title := "Meu Vídeo de Teste" 10 | filePath := "/tmp/video.mp4" 11 | 12 | video := NewVideo(title, filePath) 13 | 14 | // Verifica se o ID foi gerado 15 | if video.ID == "" { 16 | t.Error("ID não deveria ser vazio") 17 | } 18 | 19 | // Verifica se os campos foram preenchidos corretamente 20 | if video.Title != title { 21 | t.Errorf("Esperado Title %s, obtido %s", title, video.Title) 22 | } 23 | 24 | if video.FilePath != filePath { 25 | t.Errorf("Esperado FilePath %s, obtido %s", filePath, video.FilePath) 26 | } 27 | 28 | // Verifica se o status inicial está correto 29 | if video.Status != StatusPending { 30 | t.Errorf("Esperado Status %s, obtido %s", StatusPending, video.Status) 31 | } 32 | 33 | // Verifica se o status de upload inicial está correto 34 | if video.UploadStatus != UploadStatusNone { 35 | t.Errorf("Esperado UploadStatus %s, obtido %s", UploadStatusNone, video.UploadStatus) 36 | } 37 | 38 | // Verifica se as datas foram preenchidas 39 | if video.CreatedAt.IsZero() { 40 | t.Error("CreatedAt não deveria ser zero") 41 | } 42 | 43 | if video.UpdatedAt.IsZero() { 44 | t.Error("UpdatedAt não deveria ser zero") 45 | } 46 | 47 | // Verifica se os campos opcionais estão vazios 48 | if video.HLSPath != "" { 49 | t.Errorf("Esperado HLSPath vazio, obtido %s", video.HLSPath) 50 | } 51 | 52 | if video.ManifestPath != "" { 53 | t.Errorf("Esperado ManifestPath vazio, obtido %s", video.ManifestPath) 54 | } 55 | 56 | if video.S3URL != "" { 57 | t.Errorf("Esperado S3URL vazio, obtido %s", video.S3URL) 58 | } 59 | 60 | if video.S3ManifestURL != "" { 61 | t.Errorf("Esperado S3ManifestURL vazio, obtido %s", video.S3ManifestURL) 62 | } 63 | 64 | if video.ErrorMessage != "" { 65 | t.Errorf("Esperado ErrorMessage vazio, obtido %s", video.ErrorMessage) 66 | } 67 | } 68 | 69 | func TestMarkAsProcessing(t *testing.T) { 70 | video := NewVideo("Test Video", "/tmp/video.mp4") 71 | oldUpdatedAt := video.UpdatedAt 72 | 73 | // Aguarda um momento para garantir que o timestamp seja diferente 74 | time.Sleep(1 * time.Millisecond) 75 | 76 | video.MarkAsProcessing() 77 | 78 | if video.Status != StatusProcessing { 79 | t.Errorf("Esperado Status %s, obtido %s", StatusProcessing, video.Status) 80 | } 81 | 82 | if !video.UpdatedAt.After(oldUpdatedAt) { 83 | t.Error("UpdatedAt deveria ter sido atualizado") 84 | } 85 | } 86 | 87 | func TestMarkAsCompleted(t *testing.T) { 88 | video := NewVideo("Test Video", "/tmp/video.mp4") 89 | oldUpdatedAt := video.UpdatedAt 90 | 91 | // Aguarda um momento para garantir que o timestamp seja diferente 92 | time.Sleep(1 * time.Millisecond) 93 | 94 | hlsPath := "/tmp/output/123" 95 | manifestPath := "/tmp/output/123/playlist.m3u8" 96 | 97 | video.MarkAsCompleted(hlsPath, manifestPath) 98 | 99 | if video.Status != StatusCompleted { 100 | t.Errorf("Esperado Status %s, obtido %s", StatusCompleted, video.Status) 101 | } 102 | 103 | if video.HLSPath != hlsPath { 104 | t.Errorf("Esperado HLSPath %s, obtido %s", hlsPath, video.HLSPath) 105 | } 106 | 107 | if video.ManifestPath != manifestPath { 108 | t.Errorf("Esperado ManifestPath %s, obtido %s", manifestPath, video.ManifestPath) 109 | } 110 | 111 | if !video.UpdatedAt.After(oldUpdatedAt) { 112 | t.Error("UpdatedAt deveria ter sido atualizado") 113 | } 114 | } 115 | 116 | func TestMarkAsFailed(t *testing.T) { 117 | video := NewVideo("Test Video", "/tmp/video.mp4") 118 | oldUpdatedAt := video.UpdatedAt 119 | errorMsg := "Erro ao processar vídeo" 120 | 121 | // Aguarda um momento para garantir que o timestamp seja diferente 122 | time.Sleep(1 * time.Millisecond) 123 | 124 | video.MarkAsFailed(errorMsg) 125 | 126 | if video.Status != StatusError { 127 | t.Errorf("Esperado Status %s, obtido %s", StatusError, video.Status) 128 | } 129 | 130 | if video.ErrorMessage != errorMsg { 131 | t.Errorf("Esperado ErrorMessage %s, obtido %s", errorMsg, video.ErrorMessage) 132 | } 133 | 134 | if !video.UpdatedAt.After(oldUpdatedAt) { 135 | t.Error("UpdatedAt deveria ter sido atualizado") 136 | } 137 | } 138 | 139 | func TestSetS3URL(t *testing.T) { 140 | video := NewVideo("Test Video", "/tmp/video.mp4") 141 | oldUpdatedAt := video.UpdatedAt 142 | url := "https://bucket.s3.amazonaws.com/videos/123/video.m3u8" 143 | 144 | // Aguarda um momento para garantir que o timestamp seja diferente 145 | time.Sleep(1 * time.Millisecond) 146 | 147 | video.SetS3URL(url) 148 | 149 | if video.S3URL != url { 150 | t.Errorf("Esperado S3URL %s, obtido %s", url, video.S3URL) 151 | } 152 | 153 | if !video.UpdatedAt.After(oldUpdatedAt) { 154 | t.Error("UpdatedAt deveria ter sido atualizado") 155 | } 156 | } 157 | 158 | func TestSetS3ManifestURL(t *testing.T) { 159 | video := NewVideo("Test Video", "/tmp/video.mp4") 160 | oldUpdatedAt := video.UpdatedAt 161 | url := "https://bucket.s3.amazonaws.com/videos/123/playlist.m3u8" 162 | 163 | // Aguarda um momento para garantir que o timestamp seja diferente 164 | time.Sleep(1 * time.Millisecond) 165 | 166 | video.SetS3ManifestURL(url) 167 | 168 | if video.S3ManifestURL != url { 169 | t.Errorf("Esperado S3ManifestURL %s, obtido %s", url, video.S3ManifestURL) 170 | } 171 | 172 | if !video.UpdatedAt.After(oldUpdatedAt) { 173 | t.Error("UpdatedAt deveria ter sido atualizado") 174 | } 175 | } 176 | 177 | func TestIsCompleted(t *testing.T) { 178 | video := NewVideo("Test Video", "/tmp/video.mp4") 179 | 180 | if video.IsCompleted() { 181 | t.Error("Vídeo não deveria estar completo inicialmente") 182 | } 183 | 184 | hlsPath := "/tmp/output/123" 185 | manifestPath := "/tmp/output/123/playlist.m3u8" 186 | 187 | video.MarkAsCompleted(hlsPath, manifestPath) 188 | 189 | if !video.IsCompleted() { 190 | t.Error("Vídeo deveria estar completo após MarkAsCompleted") 191 | } 192 | } 193 | 194 | func TestGetHLSDirectory(t *testing.T) { 195 | video := NewVideo("Test Video", "/tmp/video.mp4") 196 | hlsPath := "/tmp/output/123" 197 | video.HLSPath = hlsPath 198 | 199 | if video.GetHLSDirectory() != hlsPath { 200 | t.Errorf("Esperado HLSPath %s, obtido %s", hlsPath, video.GetHLSDirectory()) 201 | } 202 | } 203 | 204 | func TestGetManifestPath(t *testing.T) { 205 | video := NewVideo("Test Video", "/tmp/video.mp4") 206 | manifestPath := "/tmp/output/123/playlist.m3u8" 207 | video.ManifestPath = manifestPath 208 | 209 | if video.GetManifestPath() != manifestPath { 210 | t.Errorf("Esperado ManifestPath %s, obtido %s", manifestPath, video.GetManifestPath()) 211 | } 212 | } 213 | 214 | func TestGenerateOutputPath(t *testing.T) { 215 | video := NewVideo("Test Video", "/tmp/video.mp4") 216 | baseDir := "/tmp/output" 217 | 218 | oldUpdatedAt := video.UpdatedAt 219 | 220 | // Aguarda um momento para garantir que o timestamp seja diferente 221 | time.Sleep(1 * time.Millisecond) 222 | 223 | outputPath := video.GenerateOutputPath(baseDir) 224 | 225 | expectedOutputPath := baseDir + "/converted/" + video.ID 226 | if outputPath != expectedOutputPath { 227 | t.Errorf("Esperado outputPath %s, obtido %s", expectedOutputPath, outputPath) 228 | } 229 | 230 | // Não deve mais atualizar os campos da entidade 231 | if video.HLSPath != "" { 232 | t.Errorf("HLSPath não deveria ter sido atualizado, obtido %s", video.HLSPath) 233 | } 234 | 235 | if video.ManifestPath != "" { 236 | t.Errorf("ManifestPath não deveria ter sido atualizado, obtido %s", video.ManifestPath) 237 | } 238 | 239 | // Não deve mais atualizar o timestamp 240 | if video.UpdatedAt != oldUpdatedAt { 241 | t.Error("UpdatedAt não deveria ter sido atualizado") 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /internal/domain/repository/video_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/devfullcycle/golangtechweek/internal/domain/entity" 7 | ) 8 | 9 | // VideoRepository define as operações que podem ser realizadas em um repositório de vídeos 10 | type VideoRepository interface { 11 | // Create persiste um novo vídeo no repositório 12 | // Retorna um erro se a operação falhar 13 | Create(ctx context.Context, video *entity.Video) error 14 | 15 | // FindByID busca um vídeo pelo seu ID 16 | // Retorna o vídeo encontrado ou um erro se não for encontrado ou se a operação falhar 17 | FindByID(ctx context.Context, id string) (*entity.Video, error) 18 | 19 | // List retorna uma lista de vídeos com paginação 20 | // page começa em 1, pageSize é o número de itens por página 21 | // Retorna a lista de vídeos ou um erro se a operação falhar 22 | List(ctx context.Context, page, pageSize int) ([]*entity.Video, error) 23 | 24 | // UpdateStatus atualiza o status de um vídeo e a mensagem de erro (quando aplicável) 25 | // Retorna um erro se a operação falhar 26 | UpdateStatus(ctx context.Context, id string, status string, errorMessage string) error 27 | 28 | // UpdateHLSPath atualiza os caminhos HLS de um vídeo 29 | // Retorna um erro se a operação falhar 30 | UpdateHLSPath(ctx context.Context, id string, hlsPath, manifestPath string) error 31 | 32 | // UpdateS3Status atualiza o status de upload para S3 de um vídeo 33 | // Retorna um erro se a operação falhar 34 | UpdateS3Status(ctx context.Context, id string, uploadStatus string) error 35 | 36 | // UpdateS3URLs atualiza as URLs do S3 de um vídeo 37 | // Retorna um erro se a operação falhar 38 | UpdateS3URLs(ctx context.Context, id string, s3URL, s3ManifestURL string) error 39 | 40 | // UpdateS3Keys atualiza as chaves do S3 de um vídeo (segmentKey e manifestKey) 41 | // Retorna um erro se a operação falhar 42 | UpdateS3Keys(ctx context.Context, id string, segmentKey string, manifestKey string) error 43 | 44 | // Delete remove um vídeo do repositório 45 | // Retorna um erro se a operação falhar 46 | Delete(ctx context.Context, id string) error 47 | } 48 | -------------------------------------------------------------------------------- /internal/infra/database/db.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "time" 7 | 8 | _ "github.com/lib/pq" 9 | ) 10 | 11 | // Config contém as configurações para conexão com o banco de dados 12 | type Config struct { 13 | Host string 14 | Port int 15 | User string 16 | Password string 17 | DBName string 18 | SSLMode string 19 | } 20 | 21 | // NewConnection cria uma nova conexão com o banco de dados PostgreSQL 22 | func NewConnection(config Config) (*sql.DB, error) { 23 | dsn := fmt.Sprintf( 24 | "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", 25 | config.Host, config.Port, config.User, config.Password, config.DBName, config.SSLMode, 26 | ) 27 | 28 | db, err := sql.Open("postgres", dsn) 29 | if err != nil { 30 | return nil, fmt.Errorf("erro ao abrir conexão com o banco de dados: %w", err) 31 | } 32 | 33 | // Configurar o pool de conexões 34 | db.SetMaxOpenConns(25) 35 | db.SetMaxIdleConns(25) 36 | db.SetConnMaxLifetime(5 * time.Minute) 37 | 38 | // Verificar se a conexão está funcionando 39 | if err := db.Ping(); err != nil { 40 | return nil, fmt.Errorf("erro ao verificar conexão com o banco de dados: %w", err) 41 | } 42 | 43 | return db, nil 44 | } 45 | 46 | // Close fecha a conexão com o banco de dados 47 | func Close(db *sql.DB) error { 48 | if db != nil { 49 | return db.Close() 50 | } 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /internal/infra/database/migrations/000001_create_videos_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS videos; -------------------------------------------------------------------------------- /internal/infra/database/migrations/000001_create_videos_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS videos ( 2 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 3 | title VARCHAR(255) NOT NULL, 4 | description TEXT, 5 | file_path VARCHAR(255) NOT NULL, 6 | status VARCHAR(50) NOT NULL DEFAULT 'pending', 7 | upload_status VARCHAR(50) NOT NULL DEFAULT 'none', 8 | error_message TEXT, 9 | hls_path VARCHAR(255), 10 | manifest_path VARCHAR(255), 11 | s3_url VARCHAR(255), 12 | s3_manifest_url VARCHAR(255), 13 | segment_key VARCHAR(255), 14 | manifest_key VARCHAR(255), 15 | created_at TIMESTAMP NOT NULL DEFAULT NOW(), 16 | updated_at TIMESTAMP NOT NULL DEFAULT NOW(), 17 | deleted_at TIMESTAMP 18 | ); -------------------------------------------------------------------------------- /internal/infra/database/repository/video_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "fmt" 8 | "time" 9 | 10 | "github.com/devfullcycle/golangtechweek/internal/domain/entity" 11 | domainRepository "github.com/devfullcycle/golangtechweek/internal/domain/repository" 12 | ) 13 | 14 | var ErrVideoNotFound = errors.New("vídeo não encontrado") 15 | 16 | // VideoRepositoryPostgres implementa a interface VideoRepository usando PostgreSQL 17 | type VideoRepositoryPostgres struct { 18 | db *sql.DB 19 | } 20 | 21 | // NewVideoRepositoryPostgres cria uma nova instância de VideoRepositoryPostgres 22 | func NewVideoRepositoryPostgres(db *sql.DB) *VideoRepositoryPostgres { 23 | return &VideoRepositoryPostgres{ 24 | db: db, 25 | } 26 | } 27 | 28 | // Create persiste um novo vídeo no banco de dados 29 | func (r *VideoRepositoryPostgres) Create(ctx context.Context, video *entity.Video) error { 30 | query := ` 31 | INSERT INTO videos ( 32 | id, title, file_path, status, upload_status, hls_path, manifest_path, 33 | s3_url, s3_manifest_url, error_message, created_at, updated_at 34 | ) VALUES ( 35 | $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 36 | ) 37 | ` 38 | 39 | _, err := r.db.ExecContext( 40 | ctx, 41 | query, 42 | video.ID, 43 | video.Title, 44 | video.FilePath, 45 | video.Status, 46 | video.UploadStatus, 47 | video.HLSPath, 48 | video.ManifestPath, 49 | video.S3URL, 50 | video.S3ManifestURL, 51 | video.ErrorMessage, 52 | video.CreatedAt, 53 | video.UpdatedAt, 54 | ) 55 | if err != nil { 56 | return fmt.Errorf("erro ao criar vídeo: %w", err) 57 | } 58 | 59 | return nil 60 | } 61 | 62 | // FindByID busca um vídeo pelo seu ID 63 | func (r *VideoRepositoryPostgres) FindByID(ctx context.Context, id string) (*entity.Video, error) { 64 | query := ` 65 | SELECT 66 | id, title, file_path, status, upload_status, hls_path, manifest_path, 67 | s3_url, s3_manifest_url, error_message, created_at, updated_at 68 | FROM videos 69 | WHERE id = $1 AND deleted_at IS NULL 70 | ` 71 | 72 | var video entity.Video 73 | var createdAt, updatedAt time.Time 74 | 75 | err := r.db.QueryRowContext(ctx, query, id).Scan( 76 | &video.ID, 77 | &video.Title, 78 | &video.FilePath, 79 | &video.Status, 80 | &video.UploadStatus, 81 | &video.HLSPath, 82 | &video.ManifestPath, 83 | &video.S3URL, 84 | &video.S3ManifestURL, 85 | &video.ErrorMessage, 86 | &createdAt, 87 | &updatedAt, 88 | ) 89 | if err != nil { 90 | if errors.Is(err, sql.ErrNoRows) { 91 | return nil, ErrVideoNotFound 92 | } 93 | return nil, fmt.Errorf("erro ao buscar vídeo: %w", err) 94 | } 95 | 96 | video.CreatedAt = createdAt 97 | video.UpdatedAt = updatedAt 98 | 99 | return &video, nil 100 | } 101 | 102 | // List retorna uma lista de vídeos com paginação 103 | func (r *VideoRepositoryPostgres) List(ctx context.Context, page, pageSize int) ([]*entity.Video, error) { 104 | if page < 1 { 105 | page = 1 106 | } 107 | if pageSize < 1 { 108 | pageSize = 10 109 | } 110 | 111 | offset := (page - 1) * pageSize 112 | 113 | query := ` 114 | SELECT 115 | id, title, file_path, status, upload_status, hls_path, manifest_path, 116 | s3_url, s3_manifest_url, error_message, created_at, updated_at 117 | FROM videos 118 | WHERE deleted_at IS NULL 119 | ORDER BY created_at DESC 120 | LIMIT $1 OFFSET $2 121 | ` 122 | 123 | rows, err := r.db.QueryContext(ctx, query, pageSize, offset) 124 | if err != nil { 125 | return nil, fmt.Errorf("erro ao listar vídeos: %w", err) 126 | } 127 | defer rows.Close() 128 | 129 | var videos []*entity.Video 130 | 131 | for rows.Next() { 132 | var video entity.Video 133 | var createdAt, updatedAt time.Time 134 | 135 | err := rows.Scan( 136 | &video.ID, 137 | &video.Title, 138 | &video.FilePath, 139 | &video.Status, 140 | &video.UploadStatus, 141 | &video.HLSPath, 142 | &video.ManifestPath, 143 | &video.S3URL, 144 | &video.S3ManifestURL, 145 | &video.ErrorMessage, 146 | &createdAt, 147 | &updatedAt, 148 | ) 149 | if err != nil { 150 | return nil, fmt.Errorf("erro ao escanear vídeo: %w", err) 151 | } 152 | 153 | video.CreatedAt = createdAt 154 | video.UpdatedAt = updatedAt 155 | 156 | videos = append(videos, &video) 157 | } 158 | 159 | if err := rows.Err(); err != nil { 160 | return nil, fmt.Errorf("erro ao iterar sobre os resultados: %w", err) 161 | } 162 | 163 | return videos, nil 164 | } 165 | 166 | // UpdateStatus atualiza o status de um vídeo e a mensagem de erro (quando aplicável) 167 | func (r *VideoRepositoryPostgres) UpdateStatus(ctx context.Context, id string, status string, errorMessage string) error { 168 | query := ` 169 | UPDATE videos 170 | SET status = $1, error_message = $2, updated_at = $3 171 | WHERE id = $4 AND deleted_at IS NULL 172 | ` 173 | 174 | result, err := r.db.ExecContext(ctx, query, status, errorMessage, time.Now(), id) 175 | if err != nil { 176 | return fmt.Errorf("erro ao atualizar status do vídeo: %w", err) 177 | } 178 | 179 | rowsAffected, err := result.RowsAffected() 180 | if err != nil { 181 | return fmt.Errorf("erro ao obter linhas afetadas: %w", err) 182 | } 183 | 184 | if rowsAffected == 0 { 185 | return ErrVideoNotFound 186 | } 187 | 188 | return nil 189 | } 190 | 191 | // UpdateHLSPath atualiza os caminhos HLS de um vídeo 192 | func (r *VideoRepositoryPostgres) UpdateHLSPath(ctx context.Context, id string, hlsPath, manifestPath string) error { 193 | query := ` 194 | UPDATE videos 195 | SET hls_path = $1, manifest_path = $2, updated_at = $3 196 | WHERE id = $4 AND deleted_at IS NULL 197 | ` 198 | 199 | result, err := r.db.ExecContext(ctx, query, hlsPath, manifestPath, time.Now(), id) 200 | if err != nil { 201 | return fmt.Errorf("erro ao atualizar caminhos HLS do vídeo: %w", err) 202 | } 203 | 204 | rowsAffected, err := result.RowsAffected() 205 | if err != nil { 206 | return fmt.Errorf("erro ao obter linhas afetadas: %w", err) 207 | } 208 | 209 | if rowsAffected == 0 { 210 | return ErrVideoNotFound 211 | } 212 | 213 | return nil 214 | } 215 | 216 | // UpdateS3Status atualiza o status de upload para S3 de um vídeo 217 | func (r *VideoRepositoryPostgres) UpdateS3Status(ctx context.Context, id string, uploadStatus string) error { 218 | query := ` 219 | UPDATE videos 220 | SET upload_status = $1, updated_at = $2 221 | WHERE id = $3 AND deleted_at IS NULL 222 | ` 223 | 224 | result, err := r.db.ExecContext(ctx, query, uploadStatus, time.Now(), id) 225 | if err != nil { 226 | return fmt.Errorf("erro ao atualizar status de upload do vídeo: %w", err) 227 | } 228 | 229 | rowsAffected, err := result.RowsAffected() 230 | if err != nil { 231 | return fmt.Errorf("erro ao obter linhas afetadas: %w", err) 232 | } 233 | 234 | if rowsAffected == 0 { 235 | return ErrVideoNotFound 236 | } 237 | 238 | return nil 239 | } 240 | 241 | // UpdateS3URLs atualiza as URLs do S3 de um vídeo 242 | func (r *VideoRepositoryPostgres) UpdateS3URLs(ctx context.Context, id string, s3URL, s3ManifestURL string) error { 243 | query := ` 244 | UPDATE videos 245 | SET s3_url = $1, s3_manifest_url = $2, updated_at = $3 246 | WHERE id = $4 AND deleted_at IS NULL 247 | ` 248 | 249 | result, err := r.db.ExecContext(ctx, query, s3URL, s3ManifestURL, time.Now(), id) 250 | if err != nil { 251 | return fmt.Errorf("erro ao atualizar URLs do S3 do vídeo: %w", err) 252 | } 253 | 254 | rowsAffected, err := result.RowsAffected() 255 | if err != nil { 256 | return fmt.Errorf("erro ao obter linhas afetadas: %w", err) 257 | } 258 | 259 | if rowsAffected == 0 { 260 | return ErrVideoNotFound 261 | } 262 | 263 | return nil 264 | } 265 | 266 | // UpdateS3Keys atualiza as chaves do S3 de um vídeo 267 | func (r *VideoRepositoryPostgres) UpdateS3Keys(ctx context.Context, id string, segmentKey string, manifestKey string) error { 268 | query := ` 269 | UPDATE videos 270 | SET segment_key = $1, manifest_key = $2, updated_at = $3 271 | WHERE id = $4 AND deleted_at IS NULL 272 | ` 273 | 274 | result, err := r.db.ExecContext(ctx, query, segmentKey, manifestKey, time.Now(), id) 275 | if err != nil { 276 | return fmt.Errorf("erro ao atualizar chaves do S3 do vídeo: %w", err) 277 | } 278 | 279 | rowsAffected, err := result.RowsAffected() 280 | if err != nil { 281 | return fmt.Errorf("erro ao obter linhas afetadas: %w", err) 282 | } 283 | 284 | if rowsAffected == 0 { 285 | return ErrVideoNotFound 286 | } 287 | 288 | return nil 289 | } 290 | 291 | // Delete remove um vídeo do repositório (soft delete) 292 | func (r *VideoRepositoryPostgres) Delete(ctx context.Context, id string) error { 293 | query := ` 294 | UPDATE videos 295 | SET deleted_at = $1 296 | WHERE id = $2 AND deleted_at IS NULL 297 | ` 298 | 299 | result, err := r.db.ExecContext(ctx, query, time.Now(), id) 300 | if err != nil { 301 | return fmt.Errorf("erro ao excluir vídeo: %w", err) 302 | } 303 | 304 | rowsAffected, err := result.RowsAffected() 305 | if err != nil { 306 | return fmt.Errorf("erro ao obter linhas afetadas: %w", err) 307 | } 308 | 309 | if rowsAffected == 0 { 310 | return ErrVideoNotFound 311 | } 312 | 313 | return nil 314 | } 315 | 316 | // Ensure VideoRepositoryPostgres implements VideoRepository 317 | var _ domainRepository.VideoRepository = (*VideoRepositoryPostgres)(nil) 318 | -------------------------------------------------------------------------------- /internal/infra/database/repository/video_repository_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | // Este arquivo contém testes de integração que acessam o banco de dados real. 5 | // Para executar estes testes, use o comando: 6 | // go test -tags=integration ./internal/infra/database/repository 7 | 8 | package repository 9 | 10 | import ( 11 | "context" 12 | "database/sql" 13 | "fmt" 14 | "os" 15 | "testing" 16 | 17 | "github.com/devfullcycle/golangtechweek/internal/domain/entity" 18 | "github.com/devfullcycle/golangtechweek/internal/infra/database" 19 | "github.com/google/uuid" 20 | _ "github.com/lib/pq" 21 | "github.com/stretchr/testify/assert" 22 | "github.com/stretchr/testify/suite" 23 | ) 24 | 25 | type VideoRepositoryTestSuite struct { 26 | suite.Suite 27 | db *sql.DB 28 | repository *VideoRepositoryPostgres 29 | ctx context.Context 30 | } 31 | 32 | func (suite *VideoRepositoryTestSuite) SetupSuite() { 33 | // Configuração do banco de dados de teste 34 | dbConfig := database.Config{ 35 | Host: getEnv("DB_HOST", "postgres"), 36 | Port: 5432, 37 | User: getEnv("DB_USER", "postgres"), 38 | Password: getEnv("DB_PASSWORD", "postgres"), 39 | DBName: getEnv("DB_NAME", "conversorgo"), 40 | SSLMode: getEnv("DB_SSL_MODE", "disable"), 41 | } 42 | 43 | var err error 44 | suite.db, err = database.NewConnection(dbConfig) 45 | if err != nil { 46 | suite.T().Fatalf("Erro ao conectar ao banco de dados: %v", err) 47 | } 48 | 49 | // Limpar a tabela de vídeos antes de iniciar os testes 50 | _, err = suite.db.Exec("DELETE FROM videos") 51 | if err != nil { 52 | suite.T().Fatalf("Erro ao limpar a tabela de vídeos: %v", err) 53 | } 54 | 55 | suite.repository = NewVideoRepositoryPostgres(suite.db) 56 | suite.ctx = context.Background() 57 | } 58 | 59 | func (suite *VideoRepositoryTestSuite) TearDownSuite() { 60 | // Limpar a tabela de vídeos após os testes 61 | _, err := suite.db.Exec("DELETE FROM videos") 62 | if err != nil { 63 | suite.T().Fatalf("Erro ao limpar a tabela de vídeos: %v", err) 64 | } 65 | 66 | if suite.db != nil { 67 | suite.db.Close() 68 | } 69 | } 70 | 71 | func (suite *VideoRepositoryTestSuite) TestCreate() { 72 | video := entity.NewVideo("Teste de Vídeo", "/path/to/video.mp4") 73 | err := suite.repository.Create(suite.ctx, video) 74 | assert.NoError(suite.T(), err) 75 | 76 | // Verificar se o vídeo foi criado corretamente 77 | var count int 78 | err = suite.db.QueryRow("SELECT COUNT(*) FROM videos WHERE id = $1", video.ID).Scan(&count) 79 | assert.NoError(suite.T(), err) 80 | assert.Equal(suite.T(), 1, count) 81 | } 82 | 83 | func (suite *VideoRepositoryTestSuite) TestFindByID() { 84 | // Criar um vídeo para o teste 85 | video := entity.NewVideo("Teste de Busca", "/path/to/search.mp4") 86 | err := suite.repository.Create(suite.ctx, video) 87 | assert.NoError(suite.T(), err) 88 | 89 | // Buscar o vídeo pelo ID 90 | foundVideo, err := suite.repository.FindByID(suite.ctx, video.ID) 91 | assert.NoError(suite.T(), err) 92 | assert.NotNil(suite.T(), foundVideo) 93 | assert.Equal(suite.T(), video.ID, foundVideo.ID) 94 | assert.Equal(suite.T(), video.Title, foundVideo.Title) 95 | assert.Equal(suite.T(), video.FilePath, foundVideo.FilePath) 96 | assert.Equal(suite.T(), video.Status, foundVideo.Status) 97 | } 98 | 99 | func (suite *VideoRepositoryTestSuite) TestFindByIDNotFound() { 100 | // Buscar um vídeo com ID inexistente, mas com formato UUID válido 101 | _, err := suite.repository.FindByID(suite.ctx, uuid.New().String()) 102 | assert.Error(suite.T(), err) 103 | assert.Equal(suite.T(), ErrVideoNotFound, err) 104 | } 105 | 106 | func (suite *VideoRepositoryTestSuite) TestList() { 107 | // Limpar a tabela para garantir um estado conhecido 108 | _, err := suite.db.Exec("DELETE FROM videos") 109 | assert.NoError(suite.T(), err) 110 | 111 | // Criar vários vídeos para o teste 112 | for i := 1; i <= 15; i++ { 113 | video := entity.NewVideo(fmt.Sprintf("Vídeo %d", i), fmt.Sprintf("/path/to/video%d.mp4", i)) 114 | err := suite.repository.Create(suite.ctx, video) 115 | assert.NoError(suite.T(), err) 116 | } 117 | 118 | // Testar a primeira página 119 | videos, err := suite.repository.List(suite.ctx, 1, 10) 120 | assert.NoError(suite.T(), err) 121 | assert.Len(suite.T(), videos, 10) 122 | 123 | // Testar a segunda página 124 | videos, err = suite.repository.List(suite.ctx, 2, 10) 125 | assert.NoError(suite.T(), err) 126 | assert.Len(suite.T(), videos, 5) 127 | } 128 | 129 | func (suite *VideoRepositoryTestSuite) TestUpdateStatus() { 130 | // Criar um vídeo para o teste 131 | video := entity.NewVideo("Teste de Atualização de Status", "/path/to/status.mp4") 132 | err := suite.repository.Create(suite.ctx, video) 133 | assert.NoError(suite.T(), err) 134 | 135 | // Atualizar o status 136 | err = suite.repository.UpdateStatus(suite.ctx, video.ID, entity.StatusProcessing, "") 137 | assert.NoError(suite.T(), err) 138 | 139 | // Verificar se o status foi atualizado 140 | foundVideo, err := suite.repository.FindByID(suite.ctx, video.ID) 141 | assert.NoError(suite.T(), err) 142 | assert.Equal(suite.T(), entity.StatusProcessing, foundVideo.Status) 143 | } 144 | 145 | func (suite *VideoRepositoryTestSuite) TestUpdateHLSPath() { 146 | // Criar um vídeo para o teste 147 | video := entity.NewVideo("Teste de Atualização de HLS", "/path/to/hls.mp4") 148 | err := suite.repository.Create(suite.ctx, video) 149 | assert.NoError(suite.T(), err) 150 | 151 | // Atualizar os caminhos HLS 152 | hlsPath := "/path/to/hls" 153 | manifestPath := "/path/to/manifest.m3u8" 154 | err = suite.repository.UpdateHLSPath(suite.ctx, video.ID, hlsPath, manifestPath) 155 | assert.NoError(suite.T(), err) 156 | 157 | // Verificar se os caminhos foram atualizados 158 | foundVideo, err := suite.repository.FindByID(suite.ctx, video.ID) 159 | assert.NoError(suite.T(), err) 160 | assert.Equal(suite.T(), hlsPath, foundVideo.HLSPath) 161 | assert.Equal(suite.T(), manifestPath, foundVideo.ManifestPath) 162 | } 163 | 164 | func (suite *VideoRepositoryTestSuite) TestUpdateS3Status() { 165 | // Criar um vídeo para o teste 166 | video := entity.NewVideo("Teste de Atualização de S3 Status", "/path/to/s3status.mp4") 167 | err := suite.repository.Create(suite.ctx, video) 168 | assert.NoError(suite.T(), err) 169 | 170 | // Atualizar o status de upload 171 | err = suite.repository.UpdateS3Status(suite.ctx, video.ID, entity.UploadStatusPendingS3) 172 | assert.NoError(suite.T(), err) 173 | 174 | // Verificar se o status foi atualizado 175 | foundVideo, err := suite.repository.FindByID(suite.ctx, video.ID) 176 | assert.NoError(suite.T(), err) 177 | assert.Equal(suite.T(), entity.UploadStatusPendingS3, foundVideo.UploadStatus) 178 | } 179 | 180 | func (suite *VideoRepositoryTestSuite) TestUpdateS3URLs() { 181 | // Criar um vídeo para o teste 182 | video := entity.NewVideo("Teste de Atualização de S3 URLs", "/path/to/s3urls.mp4") 183 | err := suite.repository.Create(suite.ctx, video) 184 | assert.NoError(suite.T(), err) 185 | 186 | // Atualizar as URLs do S3 187 | s3URL := "https://bucket.s3.amazonaws.com/videos/123" 188 | s3ManifestURL := "https://bucket.s3.amazonaws.com/manifests/123.m3u8" 189 | err = suite.repository.UpdateS3URLs(suite.ctx, video.ID, s3URL, s3ManifestURL) 190 | assert.NoError(suite.T(), err) 191 | 192 | // Verificar se as URLs foram atualizadas 193 | foundVideo, err := suite.repository.FindByID(suite.ctx, video.ID) 194 | assert.NoError(suite.T(), err) 195 | assert.Equal(suite.T(), s3URL, foundVideo.S3URL) 196 | assert.Equal(suite.T(), s3ManifestURL, foundVideo.S3ManifestURL) 197 | } 198 | 199 | func (suite *VideoRepositoryTestSuite) TestUpdateS3Keys() { 200 | // Criar um vídeo para o teste 201 | video := entity.NewVideo("Teste de Atualização de S3 Keys", "/path/to/s3keys.mp4") 202 | err := suite.repository.Create(suite.ctx, video) 203 | assert.NoError(suite.T(), err) 204 | 205 | // Atualizar as chaves do S3 206 | segmentKey := "videos/123" 207 | manifestKey := "manifests/123.m3u8" 208 | err = suite.repository.UpdateS3Keys(suite.ctx, video.ID, segmentKey, manifestKey) 209 | assert.NoError(suite.T(), err) 210 | 211 | // Verificar se as chaves foram atualizadas 212 | var foundSegmentKey, foundManifestKey string 213 | err = suite.db.QueryRow("SELECT segment_key, manifest_key FROM videos WHERE id = $1", video.ID).Scan(&foundSegmentKey, &foundManifestKey) 214 | assert.NoError(suite.T(), err) 215 | assert.Equal(suite.T(), segmentKey, foundSegmentKey) 216 | assert.Equal(suite.T(), manifestKey, foundManifestKey) 217 | } 218 | 219 | func (suite *VideoRepositoryTestSuite) TestDelete() { 220 | // Criar um vídeo para o teste 221 | video := entity.NewVideo("Teste de Exclusão", "/path/to/delete.mp4") 222 | err := suite.repository.Create(suite.ctx, video) 223 | assert.NoError(suite.T(), err) 224 | 225 | // Excluir o vídeo 226 | err = suite.repository.Delete(suite.ctx, video.ID) 227 | assert.NoError(suite.T(), err) 228 | 229 | // Verificar se o vídeo foi marcado como excluído 230 | var deletedAt sql.NullTime 231 | err = suite.db.QueryRow("SELECT deleted_at FROM videos WHERE id = $1", video.ID).Scan(&deletedAt) 232 | assert.NoError(suite.T(), err) 233 | assert.True(suite.T(), deletedAt.Valid) 234 | 235 | // Tentar buscar o vídeo excluído 236 | _, err = suite.repository.FindByID(suite.ctx, video.ID) 237 | assert.Error(suite.T(), err) 238 | assert.Equal(suite.T(), ErrVideoNotFound, err) 239 | } 240 | 241 | func TestVideoRepositoryTestSuite(t *testing.T) { 242 | suite.Run(t, new(VideoRepositoryTestSuite)) 243 | } 244 | 245 | // getEnv retorna o valor da variável de ambiente ou o valor padrão se não estiver definida 246 | func getEnv(key, defaultValue string) string { 247 | value := os.Getenv(key) 248 | if value == "" { 249 | return defaultValue 250 | } 251 | return value 252 | } 253 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "sync" 8 | "time" 9 | 10 | "github.com/devfullcycle/golangtechweek/pkg/workerpool" 11 | ) 12 | 13 | type NumeroJob struct { 14 | Numero int 15 | } 16 | 17 | type ResultadoNumero struct { 18 | Valor int 19 | WorkerID int 20 | Timestamp time.Time 21 | } 22 | 23 | func processarNumero(ctx context.Context, job workerpool.Job) workerpool.Result { 24 | numero := job.(NumeroJob).Numero 25 | workerID := numero % 3 26 | 27 | sleepTime := time.Duration(800+rand.Intn(400)) * time.Millisecond 28 | time.Sleep(sleepTime) 29 | 30 | return ResultadoNumero{ 31 | Valor: numero, 32 | WorkerID: workerID, 33 | Timestamp: time.Now(), 34 | } 35 | } 36 | 37 | func main() { 38 | valorMaximo := 20 39 | bufferSize := 10 40 | 41 | pool := workerpool.New(processarNumero, workerpool.Config{ 42 | WorkerCount: 3, 43 | }) 44 | 45 | inputCh := make(chan workerpool.Job, bufferSize) 46 | ctx := context.Background() 47 | 48 | resultCh, err := pool.Start(ctx, inputCh) 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | var wg sync.WaitGroup 54 | wg.Add(valorMaximo) 55 | 56 | fmt.Println("Iniciando o pool de workers com conta de", valorMaximo, "numeros") 57 | 58 | go func() { 59 | for i := 0; i < valorMaximo; i++ { 60 | inputCh <- NumeroJob{Numero: i} 61 | } 62 | close(inputCh) 63 | }() 64 | 65 | go func() { 66 | for result := range resultCh { 67 | r := result.(ResultadoNumero) 68 | fmt.Printf("Numero: %d, WorkerID: %d, Timestamp: %s\n", r.Valor, r.WorkerID, r.Timestamp.Format(time.RFC3339)) 69 | wg.Done() 70 | } 71 | }() 72 | 73 | wg.Wait() 74 | fmt.Printf("pool de workers finalizado\n Todos os %d numeros foram processados\n", valorMaximo) 75 | } 76 | -------------------------------------------------------------------------------- /pkg/workerpool/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package workerpool implementa um pool de workers genérico para processamento concorrente de tarefas. 3 | 4 | # Visão Geral 5 | 6 | O pacote workerpool fornece uma implementação genérica de um pool de workers que pode ser usado 7 | para processar tarefas de forma concorrente. O pool de workers é configurável e pode ser usado 8 | para processar qualquer tipo de tarefa que possa ser representada como um Job. 9 | 10 | # Diagrama de Funcionamento 11 | 12 | ``` 13 | ┌─────────────┐ ┌─────────────────────────────────────────────────┐ ┌─────────────┐ 14 | │ │ │ Worker Pool │ │ │ 15 | │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ 16 | │ Producer │────▶│ │ Worker 1│ │ Worker 2│ │ Worker n│ │────▶│ Consumer │ 17 | │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ 18 | │ │ │ │ │ │ │ │ │ 19 | └─────────────┘ │ └────────────┼────────────┘ │ └─────────────┘ 20 | 21 | │ │ │ 22 | │ ProcessFunc(job) │ 23 | │ │ 24 | └───────────────────────────────────────────────────┘ 25 | 26 | ``` 27 | 28 | # Fluxo de Execução 29 | 30 | 1. O produtor envia trabalhos (Jobs) para o canal de entrada 31 | 2. Os workers do pool recebem os trabalhos do canal de entrada 32 | 3. Cada worker processa o trabalho usando a função ProcessFunc 33 | 4. O resultado do processamento é enviado para o canal de saída 34 | 5. O consumidor recebe os resultados do canal de saída 35 | 36 | # Exemplo de Uso 37 | 38 | ```go 39 | package main 40 | 41 | import ( 42 | 43 | "context" 44 | "fmt" 45 | "time" 46 | 47 | "github.com/devfullcycle/golangtechweek/internal/pkg/workerpool" 48 | 49 | ) 50 | 51 | // Define um tipo de trabalho personalizado 52 | 53 | type MyJob struct { 54 | ID int 55 | Data string 56 | } 57 | 58 | // Define um tipo de resultado personalizado 59 | 60 | type MyResult struct { 61 | JobID int 62 | Processed string 63 | Success bool 64 | } 65 | 66 | func main() { 67 | // Cria uma função de processamento 68 | processFunc := func(ctx context.Context, job workerpool.Job) workerpool.Result { 69 | // Converte o trabalho genérico para o tipo específico 70 | myJob := job.(MyJob) 71 | 72 | // Processa o trabalho 73 | processed := fmt.Sprintf("Processed: %s", myJob.Data) 74 | 75 | // Retorna o resultado 76 | return MyResult{ 77 | JobID: myJob.ID, 78 | Processed: processed, 79 | Success: true, 80 | } 81 | } 82 | 83 | // Cria uma configuração para o worker pool 84 | config := workerpool.Config{ 85 | WorkerCount: 3, // Número de workers 86 | } 87 | 88 | // Cria um novo worker pool 89 | pool := workerpool.New(processFunc, config) 90 | 91 | // Cria um canal de entrada para os trabalhos 92 | inputCh := make(chan workerpool.Job) 93 | 94 | // Inicia o worker pool 95 | ctx := context.Background() 96 | resultCh, err := pool.Start(ctx, inputCh) 97 | if err != nil { 98 | panic(err) 99 | } 100 | 101 | // Goroutine para enviar trabalhos 102 | go func() { 103 | for i := 0; i < 10; i++ { 104 | job := MyJob{ 105 | ID: i, 106 | Data: fmt.Sprintf("Job %d", i), 107 | } 108 | inputCh <- job 109 | time.Sleep(100 * time.Millisecond) 110 | } 111 | close(inputCh) // Fecha o canal quando não há mais trabalhos 112 | }() 113 | 114 | // Recebe os resultados 115 | for result := range resultCh { 116 | // Converte o resultado genérico para o tipo específico 117 | myResult := result.(MyResult) 118 | fmt.Printf("Result: JobID=%d, Processed=%s, Success=%v\n", 119 | myResult.JobID, myResult.Processed, myResult.Success) 120 | } 121 | 122 | // O worker pool para automaticamente quando o canal de entrada é fechado 123 | // e todos os trabalhos são processados 124 | } 125 | 126 | ``` 127 | 128 | # Estados do Worker Pool 129 | 130 | O worker pool pode estar em um dos seguintes estados: 131 | 132 | 1. **Idle**: O worker pool está ocioso, não está processando trabalhos 133 | 2. **Running**: O worker pool está em execução, processando trabalhos 134 | 3. **Stopping**: O worker pool está sendo interrompido, finalizando os trabalhos em andamento 135 | 136 | # Diagrama de Estados 137 | 138 | ``` 139 | ┌─────────┐ 140 | │ │ 141 | │ Idle │◀───────────────┐ 142 | │ │ │ 143 | └────┬────┘ │ 144 | 145 | │ │ 146 | │ Start() │ All workers finished 147 | ▼ │ 148 | 149 | ┌─────────┐ ┌─────────┐ 150 | │ │ │ │ 151 | │ Running │────────▶│Stopping │ 152 | │ │ Stop() │ │ 153 | └─────────┘ └─────────┘ 154 | ``` 155 | */ 156 | package workerpool 157 | -------------------------------------------------------------------------------- /pkg/workerpool/errors.go: -------------------------------------------------------------------------------- 1 | package workerpool 2 | 3 | import "errors" 4 | 5 | // Erros do worker pool 6 | var ( 7 | // ErrWorkerPoolAlreadyRunning é retornado quando se tenta iniciar um worker pool que já está em execução 8 | ErrWorkerPoolAlreadyRunning = errors.New("worker pool já está em execução") 9 | 10 | // ErrWorkerPoolNotRunning é retornado quando se tenta parar um worker pool que não está em execução 11 | ErrWorkerPoolNotRunning = errors.New("worker pool não está em execução") 12 | ) 13 | -------------------------------------------------------------------------------- /pkg/workerpool/workerpool.go: -------------------------------------------------------------------------------- 1 | package workerpool 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "os" 7 | "sync" 8 | ) 9 | 10 | // Job representa um trabalho genérico a ser processado pelo worker pool 11 | type Job interface{} 12 | 13 | // Result representa o resultado genérico de um trabalho processado 14 | type Result interface{} 15 | 16 | // ProcessFunc é a função que processa um trabalho e retorna um resultado 17 | type ProcessFunc func(ctx context.Context, job Job) Result 18 | 19 | // WorkerPool é a interface para um pool de workers 20 | type WorkerPool interface { 21 | // Start inicia o worker pool 22 | // Recebe um canal de entrada para novos trabalhos 23 | // Retorna um canal de saída para resultados dos trabalhos 24 | Start(ctx context.Context, inputCh <-chan Job) (<-chan Result, error) 25 | 26 | // Stop interrompe o worker pool 27 | Stop() error 28 | 29 | // IsRunning retorna true se o worker pool estiver em execução 30 | IsRunning() bool 31 | } 32 | 33 | // State representa o estado do worker pool 34 | type State int 35 | 36 | // Constantes para os estados do worker pool 37 | const ( 38 | StateIdle State = iota 39 | StateRunning 40 | StateStopping 41 | ) 42 | 43 | // String retorna a representação em string do estado do worker pool 44 | func (s State) String() string { 45 | switch s { 46 | case StateIdle: 47 | return "idle" 48 | case StateRunning: 49 | return "running" 50 | case StateStopping: 51 | return "stopping" 52 | default: 53 | return "unknown" 54 | } 55 | } 56 | 57 | // Config contém as configurações para o worker pool 58 | type Config struct { 59 | // WorkerCount é o número de workers no pool 60 | WorkerCount int 61 | // Logger é o logger a ser usado pelo worker pool 62 | Logger *slog.Logger 63 | } 64 | 65 | // DefaultConfig retorna uma configuração padrão para o worker pool 66 | func DefaultConfig() Config { 67 | return Config{ 68 | WorkerCount: 1, 69 | Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 70 | Level: slog.LevelInfo, 71 | })), 72 | } 73 | } 74 | 75 | // workerPool é a implementação da interface WorkerPool 76 | type workerPool struct { 77 | processFunc ProcessFunc 78 | workerCount int 79 | stopCh chan struct{} 80 | stopWg sync.WaitGroup 81 | state State 82 | stateMutex sync.Mutex 83 | logger *slog.Logger 84 | } 85 | 86 | // New cria um novo worker pool 87 | func New(processFunc ProcessFunc, config Config) WorkerPool { 88 | if config.WorkerCount <= 0 { 89 | config.WorkerCount = 1 90 | } 91 | 92 | if config.Logger == nil { 93 | config.Logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 94 | Level: slog.LevelInfo, 95 | })) 96 | } 97 | 98 | return &workerPool{ 99 | processFunc: processFunc, 100 | workerCount: config.WorkerCount, 101 | stopCh: make(chan struct{}), 102 | state: StateIdle, 103 | logger: config.Logger, 104 | } 105 | } 106 | 107 | // Start inicia o worker pool 108 | func (wp *workerPool) Start(ctx context.Context, inputCh <-chan Job) (<-chan Result, error) { 109 | // Adquire o lock para proteger o acesso ao estado do worker pool 110 | wp.stateMutex.Lock() 111 | defer wp.stateMutex.Unlock() 112 | 113 | // Verifica se o worker pool já está em execução 114 | if wp.state != StateIdle { 115 | return nil, ErrWorkerPoolAlreadyRunning 116 | } 117 | 118 | resultCh := make(chan Result) 119 | // Atualiza o estado para Running 120 | wp.state = StateRunning 121 | wp.stopCh = make(chan struct{}) 122 | 123 | // Inicia os workers 124 | wp.stopWg.Add(wp.workerCount) 125 | for i := 0; i < wp.workerCount; i++ { 126 | go wp.worker(ctx, i, inputCh, resultCh) 127 | } 128 | 129 | // Goroutine para fechar o canal de resultados quando todos os workers terminarem 130 | go func() { 131 | wp.stopWg.Wait() 132 | close(resultCh) 133 | // Atualiza o estado para Idle quando todos os workers terminarem 134 | wp.stateMutex.Lock() 135 | wp.state = StateIdle 136 | wp.stateMutex.Unlock() 137 | }() 138 | 139 | return resultCh, nil 140 | } 141 | 142 | // Stop interrompe o worker pool 143 | func (wp *workerPool) Stop() error { 144 | // Adquire o lock para proteger o acesso ao estado do worker pool 145 | wp.stateMutex.Lock() 146 | defer wp.stateMutex.Unlock() 147 | 148 | // Verifica se o worker pool está em execução 149 | if wp.state != StateRunning { 150 | return ErrWorkerPoolNotRunning 151 | } 152 | 153 | // Atualiza o estado para Stopping 154 | wp.state = StateStopping 155 | // Fecha o canal de parada para sinalizar aos workers que devem parar 156 | close(wp.stopCh) 157 | // Aguarda todos os workers terminarem 158 | wp.stopWg.Wait() 159 | // Atualiza o estado para Idle 160 | wp.state = StateIdle 161 | return nil 162 | } 163 | 164 | // IsRunning retorna true se o worker pool estiver em execução 165 | func (wp *workerPool) IsRunning() bool { 166 | wp.stateMutex.Lock() 167 | defer wp.stateMutex.Unlock() 168 | return wp.state == StateRunning 169 | } 170 | 171 | // worker processa os trabalhos 172 | func (wp *workerPool) worker(ctx context.Context, id int, inputCh <-chan Job, resultCh chan<- Result) { 173 | defer wp.stopWg.Done() 174 | 175 | wp.logger.Info("Iniciando worker", "worker_id", id) 176 | 177 | for { 178 | select { 179 | case <-wp.stopCh: 180 | wp.logger.Info("Worker interrompido", "worker_id", id) 181 | return 182 | case <-ctx.Done(): 183 | wp.logger.Info("Contexto cancelado, interrompendo worker", "worker_id", id) 184 | return 185 | case job, ok := <-inputCh: 186 | if !ok { 187 | wp.logger.Info("Canal de entrada fechado, interrompendo worker", "worker_id", id) 188 | return 189 | } 190 | 191 | result := wp.processFunc(ctx, job) 192 | select { 193 | case resultCh <- result: 194 | // Resultado enviado com sucesso 195 | case <-wp.stopCh: 196 | wp.logger.Info("Worker interrompido durante envio de resultado", "worker_id", id) 197 | return 198 | case <-ctx.Done(): 199 | wp.logger.Info("Contexto cancelado durante envio de resultado", "worker_id", id) 200 | return 201 | } 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /pkg/workerpool/workerpool_test.go: -------------------------------------------------------------------------------- 1 | package workerpool 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | // TestJob é um tipo de trabalho para testes 13 | type TestJob struct { 14 | ID int 15 | Data string 16 | } 17 | 18 | // TestResult é um tipo de resultado para testes 19 | type TestResult struct { 20 | JobID int 21 | Processed string 22 | Success bool 23 | } 24 | 25 | // TestWorkerPool_Start testa o método Start do worker pool 26 | func TestWorkerPool_Start(t *testing.T) { 27 | // Cria uma função de processamento para testes 28 | processFunc := func(ctx context.Context, job Job) Result { 29 | // Converte o trabalho genérico para o tipo específico 30 | testJob := job.(TestJob) 31 | 32 | // Processa o trabalho 33 | processed := "Processed: " + testJob.Data 34 | 35 | // Retorna o resultado 36 | return TestResult{ 37 | JobID: testJob.ID, 38 | Processed: processed, 39 | Success: true, 40 | } 41 | } 42 | 43 | // Cria uma configuração para o worker pool 44 | config := Config{ 45 | WorkerCount: 2, 46 | } 47 | 48 | // Cria um novo worker pool 49 | pool := New(processFunc, config) 50 | 51 | // Verifica se o worker pool está ocioso 52 | assert.False(t, pool.IsRunning()) 53 | 54 | // Cria um canal de entrada para os trabalhos 55 | inputCh := make(chan Job) 56 | 57 | // Inicia o worker pool 58 | ctx := context.Background() 59 | resultCh, err := pool.Start(ctx, inputCh) 60 | assert.NoError(t, err) 61 | assert.NotNil(t, resultCh) 62 | 63 | // Verifica se o worker pool está em execução 64 | assert.True(t, pool.IsRunning()) 65 | 66 | // Envia um trabalho 67 | job := TestJob{ 68 | ID: 1, 69 | Data: "Test Job", 70 | } 71 | inputCh <- job 72 | 73 | // Recebe o resultado 74 | result := <-resultCh 75 | testResult := result.(TestResult) 76 | assert.Equal(t, 1, testResult.JobID) 77 | assert.Equal(t, "Processed: Test Job", testResult.Processed) 78 | assert.True(t, testResult.Success) 79 | 80 | // Para o worker pool 81 | err = pool.Stop() 82 | assert.NoError(t, err) 83 | 84 | // Verifica se o worker pool está ocioso 85 | assert.False(t, pool.IsRunning()) 86 | } 87 | 88 | // TestWorkerPool_Start_AlreadyRunning testa o método Start quando o worker pool já está em execução 89 | func TestWorkerPool_Start_AlreadyRunning(t *testing.T) { 90 | // Cria uma função de processamento para testes 91 | processFunc := func(ctx context.Context, job Job) Result { 92 | return nil 93 | } 94 | 95 | // Cria uma configuração para o worker pool 96 | config := Config{ 97 | WorkerCount: 1, 98 | } 99 | 100 | // Cria um novo worker pool 101 | pool := New(processFunc, config) 102 | 103 | // Cria um canal de entrada para os trabalhos 104 | inputCh := make(chan Job) 105 | 106 | // Inicia o worker pool 107 | ctx := context.Background() 108 | resultCh, err := pool.Start(ctx, inputCh) 109 | assert.NoError(t, err) 110 | assert.NotNil(t, resultCh) 111 | 112 | // Tenta iniciar o worker pool novamente 113 | resultCh2, err := pool.Start(ctx, inputCh) 114 | assert.Error(t, err) 115 | assert.Equal(t, ErrWorkerPoolAlreadyRunning, err) 116 | assert.Nil(t, resultCh2) 117 | 118 | // Para o worker pool 119 | err = pool.Stop() 120 | assert.NoError(t, err) 121 | } 122 | 123 | // TestWorkerPool_Stop_NotRunning testa o método Stop quando o worker pool não está em execução 124 | func TestWorkerPool_Stop_NotRunning(t *testing.T) { 125 | // Cria uma função de processamento para testes 126 | processFunc := func(ctx context.Context, job Job) Result { 127 | return nil 128 | } 129 | 130 | // Cria uma configuração para o worker pool 131 | config := Config{ 132 | WorkerCount: 1, 133 | } 134 | 135 | // Cria um novo worker pool 136 | pool := New(processFunc, config) 137 | 138 | // Tenta parar o worker pool que não está em execução 139 | err := pool.Stop() 140 | assert.Error(t, err) 141 | assert.Equal(t, ErrWorkerPoolNotRunning, err) 142 | } 143 | 144 | // TestWorkerPool_MultipleJobs testa o processamento de múltiplos trabalhos 145 | func TestWorkerPool_MultipleJobs(t *testing.T) { 146 | // Cria uma função de processamento para testes 147 | processFunc := func(ctx context.Context, job Job) Result { 148 | // Converte o trabalho genérico para o tipo específico 149 | testJob := job.(TestJob) 150 | 151 | // Processa o trabalho 152 | processed := "Processed: " + testJob.Data 153 | 154 | // Retorna o resultado 155 | return TestResult{ 156 | JobID: testJob.ID, 157 | Processed: processed, 158 | Success: true, 159 | } 160 | } 161 | 162 | // Cria uma configuração para o worker pool 163 | config := Config{ 164 | WorkerCount: 3, 165 | } 166 | 167 | // Cria um novo worker pool 168 | pool := New(processFunc, config) 169 | 170 | // Cria um canal de entrada para os trabalhos 171 | inputCh := make(chan Job) 172 | 173 | // Inicia o worker pool 174 | ctx := context.Background() 175 | resultCh, err := pool.Start(ctx, inputCh) 176 | assert.NoError(t, err) 177 | 178 | // Número de trabalhos a serem enviados 179 | numJobs := 10 180 | 181 | // Envia os trabalhos 182 | go func() { 183 | for i := 0; i < numJobs; i++ { 184 | job := TestJob{ 185 | ID: i, 186 | Data: "Job " + string(rune('A'+i)), 187 | } 188 | inputCh <- job 189 | } 190 | close(inputCh) 191 | }() 192 | 193 | // Recebe os resultados 194 | var results []TestResult 195 | var resultsMutex sync.Mutex 196 | 197 | for result := range resultCh { 198 | testResult := result.(TestResult) 199 | resultsMutex.Lock() 200 | results = append(results, testResult) 201 | resultsMutex.Unlock() 202 | } 203 | 204 | // Verifica se todos os trabalhos foram processados 205 | assert.Equal(t, numJobs, len(results)) 206 | 207 | // Verifica se o worker pool está ocioso 208 | assert.False(t, pool.IsRunning()) 209 | } 210 | 211 | // TestWorkerPool_ContextCancellation testa o cancelamento do contexto 212 | func TestWorkerPool_ContextCancellation(t *testing.T) { 213 | // Cria uma função de processamento para testes que demora um tempo para executar 214 | processFunc := func(ctx context.Context, job Job) Result { 215 | // Converte o trabalho genérico para o tipo específico 216 | testJob := job.(TestJob) 217 | 218 | // Simula um processamento demorado 219 | select { 220 | case <-time.After(100 * time.Millisecond): // Reduzido de 500ms para 100ms 221 | // Processa o trabalho 222 | processed := "Processed: " + testJob.Data 223 | 224 | // Retorna o resultado 225 | return TestResult{ 226 | JobID: testJob.ID, 227 | Processed: processed, 228 | Success: true, 229 | } 230 | case <-ctx.Done(): 231 | // Contexto cancelado 232 | return TestResult{ 233 | JobID: testJob.ID, 234 | Processed: "Cancelled", 235 | Success: false, 236 | } 237 | } 238 | } 239 | 240 | // Cria uma configuração para o worker pool 241 | config := Config{ 242 | WorkerCount: 2, 243 | } 244 | 245 | // Cria um novo worker pool 246 | pool := New(processFunc, config) 247 | 248 | // Cria um canal de entrada para os trabalhos 249 | inputCh := make(chan Job) 250 | 251 | // Cria um contexto cancelável 252 | ctx, cancel := context.WithCancel(context.Background()) 253 | defer cancel() // Garante que o contexto seja cancelado ao final do teste 254 | 255 | // Inicia o worker pool 256 | resultCh, err := pool.Start(ctx, inputCh) 257 | assert.NoError(t, err) 258 | 259 | // Envia apenas 2 trabalhos para reduzir o tempo do teste 260 | for i := 0; i < 2; i++ { 261 | job := TestJob{ 262 | ID: i, 263 | Data: "Job " + string(rune('A'+i)), 264 | } 265 | inputCh <- job 266 | } 267 | 268 | // Cancela o contexto após um curto período 269 | time.Sleep(50 * time.Millisecond) 270 | cancel() 271 | 272 | // Fecha o canal de entrada 273 | close(inputCh) 274 | 275 | // Para o worker pool explicitamente para garantir que todos os recursos sejam liberados 276 | err = pool.Stop() 277 | // Pode retornar erro se o pool já estiver parado devido ao cancelamento do contexto 278 | // Não verificamos o erro aqui 279 | 280 | // Recebe os resultados disponíveis com um timeout mais curto 281 | var results []TestResult 282 | timeout := time.After(200 * time.Millisecond) 283 | 284 | collectResults: 285 | for { 286 | select { 287 | case result, ok := <-resultCh: 288 | if !ok { 289 | break collectResults 290 | } 291 | testResult := result.(TestResult) 292 | results = append(results, testResult) 293 | case <-timeout: 294 | t.Log("Timeout ao receber resultados") 295 | break collectResults 296 | } 297 | } 298 | 299 | // Verifica se o worker pool está ocioso 300 | assert.False(t, pool.IsRunning()) 301 | } 302 | --------------------------------------------------------------------------------