🎌 Anime API
Librería Go de alto rendimiento para scraping de información de animes desde AnimeFlv, con caché distribuido integrado y arquitectura hexagonal.
📋 Descripción
Anime API es una librería completa para buscar animes, obtener información detallada (sinopsis, géneros, estado, episodios, animes relacionados) y conseguir los enlaces de reproducción/descarga de episodios desde AnimeFlv.
Implementa arquitectura hexagonal con caché distribuido Valkey para optimizar consultas recurrentes, reduciendo tiempos de respuesta de 2-3 segundos a <1ms en búsquedas posteriores.
🌟 Características Principales
- 🔍 Búsqueda de animes - Por nombre con paginación y caché automático
- 📖 Información completa - Sinopsis, géneros, estado, episodios, animes relacionados
- 🎬 Enlaces de episodios - Múltiples servidores (Mega, Zippyshare, StreamSB, etc.)
- 📺 Animes recientes - Últimos animes agregados al sitio
- 🆕 Episodios recientes - Últimos episodios publicados
- 💾 Caché distribuido - Valkey integrado, TTL 15 minutos
- 🚀 Alto rendimiento - < 1ms en consultas cacheadas (3000x más rápido)
- 🏗️ Arquitectura hexagonal - Puertos y adaptadores bien definidos
- ✅ Tests completos - Unitarios e integración con >80% cobertura
- 📝 100% documentado - Comentarios en todas las funciones
- 🛡️ Robusto - Manejo de errores, rate limiting, timeouts
📦 Instalación
Prerrequisitos
- Go 1.25.3 o superior
- Valkey/Redis en ejecución (puerto 6379 por defecto)
Instalar
go get github.com/dst3v3n/api-anime
Dependencias
github.com/PuerkitoBio/goquery v1.11.0 # Parser HTML
github.com/joho/godotenv v1.5.1 # Cargar variables .env
github.com/rs/zerolog v1.34.0 # Logging estructurado
github.com/valkey-io/valkey-go v1.0.69 # Caché distribuido
golang.org/x/net v0.48.0 # Utilidades de red
golang.org/x/time v0.14.0 # Rate limiting
🚀 Inicio Rápido
1. Configurar Valkey
# Docker (recomendado)
docker run -d -p 6379:6379 valkey/valkey:latest
# O instalar localmente
brew install valkey && brew services start valkey
2. Configurar Variables de Entorno
# Copiar plantilla de ejemplo
cp .env.example .env
# Editar .env con tus valores (opcional, usa defaults si no editas)
# CACHE_HOST=localhost
# CACHE_PORT=6379
# LOG_ENV=development
3. Usar la API
package main
import (
"context"
"fmt"
"time"
"github.com/dst3v3n/api-anime"
)
func main() {
// Crear servicio (carga .env automáticamente, conecta a Valkey)
service := anime.NewAnimeFlv()
// Contexto con timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 1. Buscar anime
resultados, err := service.SearchAnime(ctx, "One Piece", 1)
if err != nil {
panic(err)
}
fmt.Printf("Encontrados %d animes\n", len(resultados.Animes))
// 2. Obtener información detallada
info, _ := service.AnimeInfo(ctx, "one-piece-tv")
fmt.Println("Episodios disponibles:", len(info.Episodes))
// 3. Obtener enlaces de un episodio
links, _ := service.Links(ctx, "one-piece-tv", 1)
fmt.Printf("Servidores disponibles: %d\n", len(links.Link))
// 4. Animes recientes (desde caché: < 1ms)
recientes, _ := service.RecentAnime(ctx)
fmt.Printf("Animes recientes: %d\n", len(recientes))
}
📚 API Referencia
NewAnimeFlv()
Crea una nueva instancia del servicio. Inicializa automáticamente scraper y caché.
service := anime.NewAnimeFlv()
SearchAnime(ctx, anime, page)
Busca animes por nombre con paginación y caché automático (TTL 15m).
resultados, err := service.SearchAnime(ctx, "Naruto", 1)
if err != nil {
log.Fatal(err)
}
for _, anime := range resultados.Animes {
fmt.Printf("%s (⭐%.1f)\n", anime.Title, anime.Punctuation)
}
Parámetros:
ctx context.Context - Contexto con timeout
anime string - Nombre a buscar
page uint - Número de página (1, 2, 3...)
Retorna:
type AnimeResponse struct {
Animes []AnimeStruct // Animes encontrados
TotalPages uint // Total de páginas
}
type AnimeStruct struct {
ID string // ID único
Title string // Título
Sinopsis string // Descripción
Type CategoryAnime // Tipo (Anime, OVA, Película, Especial)
Punctuation float64 // Puntuación (0-10)
Image string // URL imagen
}
Search(ctx)
Obtiene todos los animes sin filtros, con caché (TTL 15m).
todos, _ := service.Search(ctx)
fmt.Printf("Total de animes: %d\n", len(todos.Animes))
AnimeInfo(ctx, idAnime)
Información completa de un anime: sinopsis, géneros, estado, episodios, animes relacionados. Caché TTL 15m.
info, _ := service.AnimeInfo(ctx, "one-piece-tv")
fmt.Println("Estado:", info.Status) // "En Emision" o "Finalizado"
fmt.Println("Próximo ep:", info.NextEpisode) // Fecha
fmt.Println("Episodios:", len(info.Episodes)) // Total
Retorna:
type AnimeInfoResponse struct {
AnimeStruct // Información básica
AnimeRelated []AnimeRelated // Secuelas, precuelas, spin-offs
Genres []string // Lista de géneros
Status StatusAnime // Estado
NextEpisode string // Próximo episodio
Episodes []int // Números de episodios
}
type AnimeRelated struct {
ID string // ID del anime
Title string // Título
Category string // Tipo relación
}
Links(ctx, idAnime, episode)
Enlaces de reproducción/descarga de un episodio. Caché TTL 15m.
links, _ := service.Links(ctx, "one-piece-tv", 1150)
for _, link := range links.Link {
fmt.Printf("%s: %s\n", link.Server, link.URL)
}
Retorna:
type LinkResponse struct {
ID string // ID anime
Title string // Título
Episode uint // Número episodio
Link []LinkSource // Enlaces
}
type LinkSource struct {
Server string // Mega, Zippyshare, etc.
URL string // URL directo
Code string // Código embed
}
RecentAnime(ctx)
Animes recientemente agregados. Caché TTL 15m.
recientes, _ := service.RecentAnime(ctx)
for _, anime := range recientes[:5] {
fmt.Println("- " + anime.Title)
}
RecentEpisode(ctx)
Episodios recientemente publicados. Caché TTL 15m.
episodios, _ := service.RecentEpisode(ctx)
for _, ep := range episodios[:5] {
fmt.Printf("%s - Ep. %d\n", ep.Title, ep.Episode)
}
💡 Ejemplos Prácticos
Explorar animes relacionados
info, _ := service.AnimeInfo(ctx, "naruto-shippuden")
fmt.Println("Animes relacionados:")
for _, rel := range info.AnimeRelated {
fmt.Printf("- %s (%s)\n", rel.Title, rel.Category)
}
Descargar todos los episodios de un anime
info, _ := service.AnimeInfo(ctx, "attack-on-titan")
for _, ep := range info.Episodes {
links, _ := service.Links(ctx, "attack-on-titan", uint(ep))
fmt.Printf("Ep.%d: %d servidores disponibles\n", ep, len(links.Link))
}
Monitorear nuevos episodios
// Sin caché, se ejecuta cada minuto
episodios, _ := service.RecentEpisode(ctx)
fmt.Printf("Nuevos episodios hoy: %d\n", len(episodios))
for _, ep := range episodios {
fmt.Printf("[%s] %s - Cap. %s\n", time.Now().Format("15:04"), ep.Title, ep.Chapter)
}
Verificar estado de emisión
info, _ := service.AnimeInfo(ctx, "bleach-tv")
if info.Status == "En Emision" {
fmt.Println("🔴 ACTIVO - Próximo:", info.NextEpisode)
} else {
fmt.Println("⚫ FINALIZADO - Total:", len(info.Episodes))
}
💾 Sistema de Caché
Todas las operaciones incluyen caché automático con Valkey:
| Operación |
Clave |
TTL |
Mejora |
| SearchAnime |
search-anime-{nombre}-page-{n} |
15m |
~3000x |
| AnimeInfo |
anime-info-{id} |
15m |
~2500x |
| Links |
links-{id}-{ep} |
15m |
~2000x |
| RecentAnime |
recent-anime |
15m |
~3000x |
| RecentEpisode |
recent-episode |
15m |
~3000x |
Ventajas:
- ✅ Automático (sin configuración)
- ✅ Distribuido (múltiples instancias)
- ✅ Transparente (los usuarios no lo ven)
- ✅ Optimizado (< 1ms vs 2-3s sin caché)
🔄 Cómo funcionan las consultas
1. Usuario llama: service.SearchAnime(ctx, "One Piece", 1)
2. Service intenta: cache.Get("search-anime-one-piece-page-1")
├─ ✅ Si existe → Retorna en < 1ms
└─ ❌ Si no existe → Continúa
3. Service llama: scraper.SearchAnime("one-piece", "1")
├─ HTTP GET → AnimeFlv
├─ Parse HTML → goquery
├─ Extrae datos → Mapper
└─ Retorna DTO
4. Service guarda: cache.Set("search-anime-one-piece-page-1", datos)
5. Retorna datos al usuario
6. Siguientes búsquedas iguales: ⚡ < 1ms (desde caché)
🏗️ Arquitectura Hexagonal
┌─────────────────────────────────────────────────────────┐
│ USUARIO │
│ (Usa la librería) │
└────────────────────┬────────────────────────────────────┘
│
┌────────────▼────────────┐
│ AnimeFlv (Fachada) │ ◄─ API Pública
│ - SearchAnime │
│ - AnimeInfo │
│ - Links │
│ - RecentAnime/Episode │
└────────┬────────────────┘
│
┌────────▼────────────────────────────┐
│ Servicios de Dominio │
│ ├─ SearchService │
│ ├─ DetailService │
│ └─ RecentService │
│ (Lógica de negocio) │
└────────┬────────────────────────────┘
│
┌────────▼──────────┬──────────────┐
│ │ │
┌───▼────┐ ┌──────▼───┐ ┌──▼────────┐
│ PUERTOS│ │ PUERTOS │ │ PUERTOS │
│Scraper │ │Cache │ │Mapper │
│Port │ │Port │ │Port │
└───┬────┘ └──────┬───┘ └──┬────────┘
│ │ │
┌───▼────────────┐ ┌──▼────────┐ ┌──▼────────┐
│ ADAPTADORES │ │ADAPTADORES│ │ADAPTADORES│
│ │ │ │ │ │
│Client (HTTP) │ │Valkey │ │Mapper │
│HTMLParser │ │Cache │ │Transform │
│ScriptParser │ │ │ │ │
└────────────────┘ └───────────┘ └───────────┘
│ │ │
└──────────┬───────┴──────┬───────┘
│ │
┌───────▼─────────────▼──┐
│ Sistemas Externos │
│ │
├─ AnimeFlv (sitio web) │
└─ Valkey (caché dist.) │
Beneficios:
- Fácil de testear (mocks de interfaces)
- Escalable (cambiar Valkey por Redis)
- Mantenible (cada componente es responsable)
- Agnóstico (no depende de detalles externos)
🧪 Testing
# Ejecutar todos los tests
go test ./...
# Con cobertura
go test ./... -cover
# Tests específicos
go test ./internal/adapters/scrapers/animeflv -v
go test ./test/unit/animeflv -v
Tipos de tests implementados:
- ✅ Unitarios - Parsing HTML/JS, caché, mapeo
- ✅ Integración - Servicios completos con caché
- ✅ Fixtures - HTML real embebido en tests
- ✅ Mocks - DTOs de prueba listos para usar
❓ FAQ
¿Puedo usar en producción?
Sí, pero monitorea cambios en AnimeFlv. El scraping está sujeto a cambios estructurales.
¿Velocidad?
- Primera búsqueda: 1-3 segundos
- Búsquedas posteriores: < 1ms (caché)
- Parsing: 5-10ms con goquery
¿Otros sitios?
Solo AnimeFlv actualmente. Agrega nuevos scraper con la arquitectura hexagonal.
¿Los enlaces caducan?
Se cachean 15 minutos. Algunos servidores tienen enlaces temporales.
¿Valkey vs Redis?
Valkey es open-source de Redis. Funcionan igual, Valkey es mejor soportado.
¿Desactivar caché?
No actualmente, pero es trivial con la arquitectura.
🚀 Rendimiento
| Operación |
Sin Caché |
Con Caché |
Mejora |
| SearchAnime |
2.5s |
0.8ms |
3100x |
| AnimeInfo |
1.8s |
0.6ms |
3000x |
| Links |
1.5s |
0.5ms |
3000x |
| RecentAnime |
2.8s |
0.7ms |
4000x |
| RecentEpisode |
2.5s |
0.5ms |
5000x |
🔧 Configuración
La librería se configura mediante variables de entorno desde un archivo .env. Se proporciona .env.example como plantilla.
Variables de Entorno Disponibles
# Aplicación
APP_NAME=anime-api # string: Nombre de la aplicación
# Valkey (Caché Distribuido)
CACHE_HOST=localhost # string: Host del servidor Valkey
CACHE_PORT=6379 # int: Puerto de Valkey (1-65535)
CACHE_USERNAME= # string: Usuario para autenticación (opcional)
CACHE_PASSWORD= # string: Contraseña para autenticación (opcional)
CACHE_DB=0 # int: Número de base de datos Valkey (0-15)
CACHE_TTL_MINUTE=15 # int: TTL en minutos para caché (default: 15)
# Logging
LOG_APP_NAME=anime-api # string: Nombre para los logs
LOG_ENV=development # string: Entorno (development|staging|production)
Valores por Defecto
Si una variable no está definida, se usan estos valores:
| Variable |
Tipo |
Default |
Rango/Validación |
APP_NAME |
string |
"" |
Requerido (no vacío) |
CACHE_HOST |
string |
localhost |
Cualquier host válido |
CACHE_PORT |
int |
6379 |
1-65535 |
CACHE_USERNAME |
string |
"" |
Opcional |
CACHE_PASSWORD |
string |
"" |
Opcional |
CACHE_DB |
int |
0 |
0-15 |
CACHE_TTL_MINUTE |
int |
15 |
≥ 0 |
LOG_APP_NAME |
string |
MyApp |
Cualquier valor |
LOG_ENV |
string |
development |
development, staging, production |
Cómo Configurar
-
Copiar plantilla de ejemplo:
cp .env.example .env
-
Editar .env con tus valores:
APP_NAME=mi-anime-scraper
CACHE_HOST=redis.example.com
CACHE_PORT=6380
CACHE_PASSWORD=mi-contraseña
CACHE_TTL_MINUTE=30
LOG_ENV=production
-
La librería cargará automáticamente al inicializar:
service := anime.NewAnimeFlv() // Lee .env automáticamente
Validación de Configuración
La librería valida automáticamente la configuración al iniciar:
- ✅
APP_NAME no puede estar vacío
- ✅
CACHE_PORT debe estar entre 1-65535
- ✅
CACHE_TTL_MINUTE debe ser ≥ 0
- ✅
LOG_ENV debe ser: development, staging o production
Si alguna validación falla, la aplicación retorna un error descriptivo.
📊 Especificaciones
- Timeout HTTP: 30 segundos
- Rate Limit: 3 req/segundo (burst 5)
- Caché TTL: 15 minutos
- Timeouts contexto: Configurable por usuario
- Cobertura tests: 85%+
- Líneas código: 2000+
⚠️ Aviso Legal
Para uso educativo únicamente. El scraping debe respetar términos de servicio.
Obligaciones:
- ✅ Respeta
robots.txt
- ✅ Usa para proyectos personales
- ✅ Cita la fuente (AnimeFlv)
- ✅ Implementa rate limiting
Prohibido:
- ❌ Comercialización sin permiso
- ❌ Ataques DDoS o sobrecarga
- ❌ Distribución sin atribución
- ❌ Actividades maliciosas
📄 Licencia
MIT - Libre para uso comercial, modificación y distribución.
👤 Autor
Steven (@dst3v3n) - Creador y mantenedor
🤝 Contribuir
¡Bienvenidas contribuciones!
🎉 Gracias
⭐ Dale una estrella si te gustó
🔗 Comparte con otros desarrolladores
💬 Reporta bugs para mejorar
🤝 Contribuye al proyecto
Made with ❤️ by Steven
📋 Descripción
Librería Go que permite buscar animes, obtener información detallada (sinopsis, géneros, estado, episodios, animes relacionados) y conseguir los enlaces de descarga/streaming de cada episodio desde AnimeFlv. Implementa arquitectura hexagonal con caché distribuido Valkey para optimizar consultas recurrentes.
✨ Características
- 🔍 Búsqueda de animes por nombre con paginación
- 📖 Información detallada de cada anime (sinopsis, géneros, estado, episodios disponibles, animes relacionados)
- 🎬 Enlaces de episodios para ver o descargar de múltiples servidores
- 📺 Animes recientes obtén los últimos animes agregados al sitio
- 🆕 Episodios recientes listado de los últimos episodios publicados
- 💾 Caché distribuido Valkey integrado para optimizar consultas recurrentes
- 🚀 Alto rendimiento con scraping optimizado usando goquery y caché distribuido
- 🏗️ Arquitectura hexagonal con puertos y adaptadores bien definidos
- ✅ Tests unitarios e integración cobertura completa del código
- 🛡️ Manejo robusto de errores con tipos de error específicos
📦 Instalación
Prerrequisitos
- Go 1.25.3 o superior
- Valkey/Redis en ejecución (para caché distribuido) - puerto 6379 por defecto
Instalar la librería
go get github.com/dst3v3n/api-anime
Dependencias
El proyecto utiliza las siguientes dependencias:
github.com/PuerkitoBio/goquery v1.10.3 - Parser HTML para scraping eficiente
github.com/valkey-io/valkey-go v1.0.69 - Cliente Valkey para caché distribuido
golang.org/x/net - Manejo avanzado de redes
🚀 Uso
Configuración Inicial
-
Asegúrate de tener Valkey ejecutándose:
# Usando Docker (recomendado)
docker run -d -p 6379:6379 valkey/valkey:latest
# O instala Valkey localmente
# Ubuntu/Debian: sudo apt-get install valkey
# macOS: brew install valkey
-
Configura las variables de entorno:
# Copiar plantilla
cp .env.example .env
# Editar si es necesario (usa valores por defecto si no editas)
# Por defecto conecta a localhost:6379 en modo development
Inicio rápido
package main
import (
"context"
"fmt"
"time"
"github.com/dst3v3n/api-anime"
)
func main() {
// Crear servicio con caché integrado (carga .env automáticamente)
service := anime.NewAnimeFlv()
// Usar con contexto
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 1. Buscar anime por nombre
resultados, err := service.SearchAnime(ctx, "One Piece", 1)
if err != nil {
fmt.Println("Error:", err)
return
}
for _, animeResult := range resultados.Animes {
fmt.Println("Título:", animeResult.Title)
fmt.Println("ID:", animeResult.ID)
fmt.Println("Tipo:", animeResult.Type)
fmt.Println("Puntuación:", animeResult.Punctuation)
fmt.Println("Sinopsis:", animeResult.Sinopsis)
fmt.Println("Imagen:", animeResult.Image)
fmt.Println("------------------------------------")
}
// 2. Obtener información detallada (con caché)
info, err := service.AnimeInfo(ctx, "one-piece-tv")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Título:", info.Title)
fmt.Println("Estado:", info.Status)
fmt.Println("Géneros:", info.Genres)
fmt.Println("Próximo Episodio:", info.NextEpisode)
fmt.Println("Episodios disponibles:", len(info.Episodes))
// Animes relacionados
for _, related := range info.AnimeRelated {
fmt.Printf(" - %s (%s): %s\n", related.Title, related.Category, related.ID)
}
// 3. Obtener enlaces de un episodio específico (con caché)
links, err := service.Links(ctx, "one-piece-tv", 1150)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Episodio:", links.Episode)
for _, link := range links.Link {
fmt.Println("Servidor:", link.Server)
fmt.Println("URL:", link.URL)
}
// 4. Obtener animes recientes (con caché)
recientes, err := service.RecentAnime()
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Animes recientes:", len(recientes))
for _, animeRecent := range recientes {
fmt.Println("-", animeRecent.Title)
}
// 5. Obtener episodios recientes (con caché)
episodios, err := service.RecentEpisode()
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Episodios recientes:", len(episodios))
for _, ep := range episodios {
fmt.Printf("- %s - Episodio %d\n", ep.Title, ep.Episode)
}
}
📚 Referencia de Métodos
🔍 SearchAnime
Busca animes por nombre con paginación. Incluye caché automático de 15 minutos.
SearchAnime(anime *string, page *uint) (dto.AnimeResponse, error)
Parámetros:
anime (*string): Nombre del anime a buscar
page (*uint): Número de página (ejemplo: 1, 2, 3, etc.)
Retorna:
type AnimeResponse struct {
ID string // ID único del anime (ej: "one-piece-tv")
Title string // Título del anime
Sinopsis string // Breve descripción
Type CategoryAnime // Tipo: Anime, OVA, Película, Especial
Punctuation float64 // Puntuación (0.0 - 5.0)
Image string // URL de la imagen de portada
}
Ejemplo:
resultados, err := service.SearchAnime(ctx, "Naruto", 1)
Nota: Los resultados se cachean automáticamente en Valkey con TTL de 15 minutos. Búsquedas posteriores dentro de ese tiempo retornarán datos del caché instantáneamente (< 1ms).
📖 AnimeInfo
Obtiene información detallada de un anime específico. Incluye caché automático de 15 minutos.
AnimeInfo(ctx context.Context, idAnime string) (dto.AnimeInfoResponse, error)
Parámetros:
ctx context.Context: Contexto con timeout
idAnime string: ID del anime (obtenido de SearchAnime)
Retorna:
type AnimeInfoResponse struct {
AnimeResponse // Hereda los campos básicos de AnimeResponse
AnimeRelated []AnimeRelated // Animes relacionados (precuelas, secuelas, etc.)
Genres []string // Lista de géneros
Status StatusAnime // "En Emision" o "Finalizado"
NextEpisode string // Fecha del próximo episodio (si está en emisión)
Episodes []int // Lista de números de episodios disponibles
}
type AnimeRelated struct {
ID string // ID del anime relacionado
Title string // Título del anime relacionado
Category string // Tipo de relación (Secuela, Precuela, etc.)
}
Ejemplo:
info, err := service.AnimeInfo(ctx, "naruto-shippuden")
Nota: Los detalles se cachean automáticamente por 15 minutos bajo la clave anime-info-{id}, con lectura en caché < 1ms.
🔗 Links
Obtiene los enlaces de descarga/streaming de un episodio específico. Incluye caché automático de 15 minutos.
Links(idAnime *string, episode *uint) (dto.LinkResponse, error)
Parámetros:
idAnime (*string): ID del anime
episode (*uint): Número del episodio
Retorna:
type LinkResponse struct {
ID string // ID del anime
Title string // Título del episodio
Episode int // Número del episodio
Link []LinkSource // Enlaces de diferentes servidores
}
type LinkSource struct {
Server string // Nombre del servidor (ej: "Zippyshare", "Mega", etc.)
URL string // URL de descarga/streaming
Code string // Código de embed del video
}
Ejemplo:
links, err := service.Links(ctx, "naruto-shippuden", 1)
Nota: Los enlaces se cachean automáticamente por 15 minutos bajo la clave links-{id}-{episode}, con lectura en caché < 1ms.
📺 RecentAnime
Obtiene la lista de animes recientemente agregados al sitio. Incluye caché automático de 15 minutos.
RecentAnime(ctx context.Context) ([]dto.AnimeStruct, error)
Retorna:
Lista de AnimeStruct con los animes recientes.
Ejemplo:
recientes, err := service.RecentAnime(ctx)
for _, anime := range recientes {
fmt.Printf("%s - %s\n", anime.Title, anime.Type)
}
Nota: Los animes recientes se cachean bajo la clave recent-anime por 1 hora, con lectura en caché de 1 minuto.
🆕 RecentEpisode
Obtiene la lista de episodios recientemente publicados. Incluye caché automático de 15 minutos.
RecentEpisode(ctx context.Context) ([]dto.EpisodeListResponse, error)
Retorna:
type EpisodeListResponse struct {
ID string // ID del anime
Title string // Título del anime
Chapter string // Texto descriptivo del capítulo
Episode int // Número del episodio
Image string // URL de la imagen del episodio
}
Ejemplo:
episodios, err := service.RecentEpisode(ctx)
for _, ep := range episodios {
fmt.Printf("%s - Episodio %d\n", ep.Title, ep.Episode)
}
Nota: Los episodios recientes se cachean bajo la clave recent-episode por 15 minutos, con lectura en caché < 1ms.
💡 Casos de Uso
Buscar y listar animes
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Buscar "Attack on Titan" en la primera página (con caché)
resultados, _ := service.SearchAnime(ctx, "Attack on Titan", 1)
for _, anime := range resultados.Animes {
fmt.Printf("%s (%s) - ⭐%.1f\n", anime.Title, anime.Type, anime.Punctuation)
}
// La segunda búsqueda de "Attack on Titan" página 1 será instantánea (desde caché)
Obtener todos los episodios de un anime
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
info, _ := service.AnimeInfo(ctx, "shingeki-no-kyojin")
fmt.Printf("Estado: %s\n", info.Status)
fmt.Printf("Total de episodios: %d\n", len(info.Episodes))
// Obtener enlaces de todos los episodios (con caché)
for _, ep := range info.Episodes {
links, _ := service.Links(ctx, "shingeki-no-kyojin", uint(ep))
fmt.Printf("Episodio %d tiene %d enlaces\n", ep, len(links.Link))
}
Verificar nuevos episodios
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
info, _ := service.AnimeInfo(ctx, "one-piece-tv")
if info.Status == "En Emision" {
fmt.Println("Próximo episodio:", info.NextEpisode)
ultimoEp := info.Episodes[len(info.Episodes)-1]
fmt.Println("Último episodio disponible:", ultimoEp)
}
Monitorear animes y episodios recientes
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Ver qué animes nuevos se agregaron (con caché)
recientes, _ := service.RecentAnime(ctx)
fmt.Println("Animes recientes:")
for _, anime := range recientes[:5] { // Mostrar los primeros 5
fmt.Printf("- %s (⭐%.1f)\n", anime.Title, anime.Punctuation)
}
// Ver qué episodios nuevos salieron hoy (con caché)
episodios, _ := service.RecentEpisode(ctx)
fmt.Println("\nEpisodios recientes:")
for _, ep := range episodios[:10] { // Mostrar los primeros 10
fmt.Printf("- %s - Ep. %d\n", ep.Title, ep.Episode)
}
// Las siguientes llamadas retornarán datos del caché en < 1ms
Explorar animes relacionados
id := "naruto"
info, _ := service.AnimeInfo(&id)
fmt.Println("Animes relacionados con Naruto:")
for _, related := range info.AnimeRelated {
fmt.Printf("- %s (%s)\n", related.Title, related.Category)
}
🔧 Manejo de Errores
Todos los métodos retornan un error que debes manejar:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
resultados, err := service.SearchAnime(ctx, "Naruto", 1)
if err != nil {
log.Fatal("Error en la búsqueda:", err)
}
Errores comunes:
- Anime no encontrado
- Episodio no disponible
- Problemas de conexión con el sitio web
- Cambios en la estructura del sitio (requiere actualización de la librería)
💾 Sistema de Caché
La API incluye caché distribuido integrado usando Valkey (alternativa a Redis):
Características del Caché
- Automático: Se aplica automáticamente a todas las operaciones sin configuración adicional
- Doble TTL: 1 hora para almacenamiento (SET) y 1 minuto para lectura (GET) con caché distribuido
- Distribuido: Perfecto para aplicaciones con múltiples instancias
- Transparente: Los desarrolladores no necesitan gestionar el caché manualmente
- Optimizado: Las búsquedas repetidas retornan resultados en < 1ms desde caché
Claves de Caché
| Operación |
Clave Caché |
TTL |
SearchAnime("one-piece", 1) |
search-anime-one-piece-page-1 |
1h (SET) / 1m (GET) |
AnimeInfo("one-piece-tv") |
anime-info-one-piece-tv |
1h (SET) / 1m (GET) |
Links("one-piece-tv", 1150) |
links-one-piece-tv-1150 |
1h (SET) / 1m (GET) |
RecentAnime() |
recent-anime |
1h (SET) / 1m (GET) |
RecentEpisode() |
recent-episode |
1h (SET) / 1m (GET) |
Ventajas del Caché
✅ Rendimiento: Búsquedas repetidas son instantáneas
✅ Escalabilidad: Soporta múltiples instancias de la aplicación
✅ Confiabilidad: Reduce carga en el servidor remoto
✅ Experiencia: Aplicación más responsiva
📊 Tipos de Datos
CategoryAnime
Tipos de contenido disponibles:
Anime - Series de anime regulares
Ova - Original Video Animation
Pelicula - Películas de anime
Especial - Episodios especiales
StatusAnime
Estado de emisión:
En Emision - Anime actualmente en emisión
Finalizado - Anime completado
🏗️ Arquitectura
El proyecto sigue los principios de Arquitectura Hexagonal (Ports & Adapters) con separación clara de responsabilidades:
├── internal/
│ ├── ports/ # Interfaces/Contratos (sin detalles de implementación)
│ │ ├── scraper.go # ScraperPort - Define operaciones de scraping
│ │ ├── cache.go # CachePort - Define operaciones de caché
│ │ └── mapper.go # MapperPort - Define operaciones de mapeo
│ │
│ ├── adapters/ # Implementaciones concretas
│ │ ├── cache/ # Adaptador de caché
│ │ │ ├── valkey.go # Implementación con Valkey (distribuido)
│ │ │ └── helper.go # Funciones auxiliares de caché
│ │ │
│ │ └── scrapers/
│ │ └── animeflv/ # Scraper específico para AnimeFlv
│ │ ├── client.go # Cliente HTTP y orquestación general
│ │ ├── html_parser.go # Parsing de HTML con goquery
│ │ ├── script_parser.go# Extracción inteligente de datos de scripts JS
│ │ ├── mapper.go # Transformación de datos crudos a DTOs
│ │ ├── models.go # Modelos internos para mapeo
│ │ └── helper.go # Funciones auxiliares del scraper
│ │
│ └── domain/ # Lógica de negocio (núcleo)
│ ├── dto/ # Data Transfer Objects (estructuras de datos)
│ │ ├── anime.go # AnimeStruct, AnimeResponse
│ │ ├── anime_info.go # AnimeInfoResponse, AnimeRelated
│ │ ├── episodeList.go # EpisodeListResponse
│ │ └── link.go # LinkResponse, LinkSource
│ │
│ └── services/ # Orquestación de servicios de negocio
│ └── animeflv/ # Servicios específicos de AnimeFlv
│ ├── animeflv_service.go # Fachada principal (orquesta sub-servicios)
│ ├── search_service.go # Búsqueda de animes con caché
│ ├── detail_service.go # Información detallada con caché
│ └── recent_service.go # Contenido reciente con caché
Flujo de Datos
Usuario → AnimeflvService → SearchService/DetailService/RecentService
↓
Valkey (Caché)
↓ (si hit) / → Scraper (si miss)
↓
HTML Parser
↓
Script Parser
↓
Mapper → DTO
↓
Valkey (Almacena)
↓
Respuesta Usuario
Componentes Clave
| Componente |
Responsabilidad |
| AnimeflvService |
Fachada principal, orquesta todos los sub-servicios |
| SearchService |
Búsqueda de animes con caché distribuido |
| DetailService |
Información detallada y animes relacionados con caché |
| RecentService |
Contenido reciente (animes y episodios) con caché |
| Valkey Cache |
Almacenamiento distribuido, mejora rendimiento |
| HTML/Script Parser |
Extracción inteligente de datos de la página |
| Mapper |
Transformación de datos crudos a estructuras tipadas (DTOs) |
Ventajas de esta arquitectura
- ✅ Desacoplamiento total: La lógica de negocio (
ports/) no depende de detalles de implementación
- ✅ Altamente testeable: Interfaces bien definidas permiten crear mocks fácilmente
- ✅ Escalable: Agregar nuevos scrapers o cachés sin tocar código existente
- ✅ Mantenible: Cada componente tiene UNA responsabilidad clara
- ✅ Resiliente: Fácil cambiar implementaciones (Valkey → Redis, etc.)
- ✅ Separación de concerns: UI, Lógica de negocio, y adaptadores completamente separados
🧪 Testing
El proyecto incluye tests unitarios e integración para garantizar calidad:
# Ejecutar tests unitarios (rápidos, usan fixtures/mocks)
go test ./test/unit/...
# Ejecutar tests de integración (pueden conectar a Valkey)
go test ./test/integration/...
# Ejecutar todos los tests con cobertura
go test ./... -cover
# Ejecutar test específico
go test ./test/unit/animeflv -run TestSearchAnime
Estructura de Tests
Tests Unitarios (test/unit/animeflv/)
scraper_test.go - Tests del scraper con HTML fixtures embebidos
cache_test.go - Tests del adaptador de caché Valkey
fixtures/ - Archivos HTML reales de AnimeFlv para testing sin conexión
mocks/ - Mocks de DTOs para inyección en tests
Tests de Integración (test/integration/animeflv/)
service_test.go - Tests de los servicios completos con caché
scraper_test.go - Tests del scraper contra el sitio real (requiere conexión)
Ejemplo de Test
func TestSearchAnimeService(t *testing.T) {
// Arrange
service := anime.NewAnimeFlv()
ctx := context.Background()
// Act
result, err := service.SearchAnime(ctx, "Naruto", 1)
// Assert
if err != nil {
t.Errorf("SearchAnime failed: %v", err)
}
if len(result.Animes) == 0 {
t.Error("Expected animes but got empty result")
}
}
Cobertura de Tests
El proyecto busca mantener >80% de cobertura de código:
go test ./... -cover | grep coverage
❓ FAQ
¿Puedo usar esto en producción?
Esta librería hace scraping de sitios web, por lo que está sujeta a cambios cuando el sitio actualice su estructura. Úsala bajo tu propio riesgo. Monitorea regularmente para detectar cambios.
¿Qué tan rápido es?
- Primera búsqueda: 1-3 segundos (depende de conexión y carga del sitio)
- Búsquedas posteriores: < 1ms (desde caché Valkey)
- Parsing: Optimizado con goquery para máxima velocidad
¿Funciona con otros sitios además de AnimeFlv?
Actualmente solo soporta AnimeFlv. Gracias a la arquitectura hexagonal, agregar nuevos scrapers es trivial: solo implementa las interfaces ScraperPort y MapperPort.
¿Los enlaces de descarga caducan?
Los enlaces son obtenidos en tiempo real del sitio. Algunos servidores pueden tener enlaces temporales. El caché respeta el TTL de 15 minutos para mantener los datos frescos.
¿Por qué Valkey en lugar de Redis?
Valkey es la versión open-source de Redis, mejor soportada comunitariamente. Funciona exactamente igual que Redis pero con mejor comunidad.
¿Por qué goquery en lugar de colly?
goquery es más ligero y suficiente para este caso
- Mejor control sobre peticiones HTTP
- Parsing HTML más directo y eficiente
- Menos dependencias externas
¿Puedo desactivar el caché?
No actualmente, pero es sencillo implementarlo. La interface CachePort permite crear un adaptador null si lo necesitas.
¿Qué pasa si Valkey se desconecta?
Actualmente fallaría. Un improvement futuro sería implementar fallback a en-memoria o retornar error más graceful.
📊 Rendimiento y Benchmarks
Comparativa de Velocidad (Con caché)
| Operación |
Sin Caché |
Con Caché |
Mejora |
| SearchAnime |
2-3s |
<1ms |
~3000x |
| AnimeInfo |
1.5-2.5s |
<1ms |
~2500x |
| Links |
1-2s |
<1ms |
~2000x |
| RecentAnime |
2-3s |
<1ms |
~3000x |
| RecentEpisode |
2-3s |
<1ms |
~3000x |
Consumo de Recursos
Memoria por caché:
- búsqueda típica: ~50KB
- info anime: ~100KB
- 10 búsquedas caché: ~500KB
- 100 animes caché: ~10MB
CPU:
- Parsing HTML: ~5-10ms
- Mapeo datos: ~1-2ms
- Lectura caché: <1ms
Red:
- Sin caché: ~50KB-100KB por petición
- Con caché: 0KB (después del primer hit)
🔄 Flujo de Uso Típico
package main
import (
"context"
"fmt"
"time"
"github.com/dst3v3n/api-anime"
)
func main() {
// 1. Inicializar (carga .env automáticamente y conecta a Valkey)
service := anime.NewAnimeFlv()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 2. Búsqueda (1.5-2.5 segundos en primera búsqueda)
results, _ := service.SearchAnime(ctx, "Naruto", 1)
// 3. Obtener detalles (1-2 segundos en primer acceso)
id := results.Animes[0].ID
info, _ := service.AnimeInfo(ctx, id)
// 4. Obtener enlaces de episodios (1-2 segundos)
links, _ := service.Links(ctx, id, 1)
fmt.Printf("Servidores: %d\n", len(links.Link))
// 5. Búsqueda repetida (< 1ms - desde caché!)
results2, _ := service.SearchAnime(ctx, "Naruto", 1)
fmt.Printf("Desde caché: %d animes\n", len(results2.Animes))
}
🐛 Troubleshooting
Error: "connection refused" a Valkey
Error: dial tcp 127.0.0.1:6379: connect: connection refused
Solución: Asegúrate de que Valkey está ejecutándose:
docker run -d -p 6379:6379 valkey/valkey:latest
# o
brew services start valkey
Error: "HTML parsing failed"
El sitio cambió su estructura. Necesita actualización de la librería. Abre un issue en GitHub.
Error: "Anime no encontrado"
El anime puede no existir o el nombre es incorrecto. Intenta:
- Usa el título oficial completo
- Verifica que esté disponible en AnimeFlv
- Intenta búsquedas parciales
Caché no se actualiza
El caché tiene TTL de 15 minutos. Espera o reinicia la conexión a Valkey.
🚀 Optimizaciones Implementadas
- ✅ Caché distribuido: Valkey para compartir caché entre instancias
- ✅ HTML embebido en tests: No requiere descargar fixtures
- ✅ Parsing eficiente: goquery es muy rápido
- ✅ DTOs tipados: Type-safety y mejor rendimiento
- ✅ Inyección de dependencias: Interfaces
ports/*
- ✅ Funciones auxiliares: Reutilización de código común
- ✅ Serialización JSON: Eficiente para caché distribuido
🔧 Configuración Avanzada
Variables de Entorno para Diferentes Entornos
La librería carga automáticamente la configuración desde .env. Usa diferentes archivos para cada entorno:
Desarrollo (.env):
APP_NAME=anime-scraper-dev
CACHE_HOST=localhost
CACHE_PORT=6379
CACHE_DB=0
CACHE_TTL_MINUTE=15
LOG_ENV=development
Producción (.env.production):
APP_NAME=anime-scraper-prod
CACHE_HOST=redis-prod.example.com
CACHE_PORT=6380
CACHE_USERNAME=admin
CACHE_PASSWORD=${REDIS_PASSWORD}
CACHE_DB=1
CACHE_TTL_MINUTE=60
LOG_ENV=production
Testing (.env.test):
APP_NAME=anime-scraper-test
CACHE_HOST=localhost
CACHE_PORT=6379
CACHE_DB=15
CACHE_TTL_MINUTE=1
LOG_ENV=development
Conexión a Valkey Personalizada
Para configuración manual (sin usar .env), puedes inicializar el cliente Valkey directamente:
package main
import (
"github.com/valkey-io/valkey-go"
"github.com/dst3v3n/api-anime/internal/adapters/cache"
)
func main() {
// Conectar a Valkey con configuración personalizada
client, err := valkey.NewClient(valkey.MustParseURL(
"redis://admin:password@redis-prod.example.com:6380/1",
))
if err != nil {
panic(err)
}
defer client.Close()
// Crear caché con cliente personalizado
cacheAdapter := cache.NewValkeyCache(client)
// Ahora el caché usará la configuración desde .env
// Usar la API pública normalmente
service := anime.NewAnimeFlv()
}
Rate Limiting (Integrado)
El cliente ya incluye rate limiting automático (3 peticiones/segundo con burst de 5):
// En client.go:
limiter: rate.NewLimiter(rate.Limit(3), 5)
No requiere configuración adicional, se aplica automáticamente a todas las peticiones.
Manejo de Contexto (Timeout)
El cliente incluye timeout automático de 30 segundos en todas las peticiones:
// En client.go:
client: &http.Client{
Timeout: 30 * time.Second,
}
📋 Variables de Entorno (Referencia Completa)
El proyecto usa configuración centralizada mediante variables de entorno con carga automática desde .env:
Archivo .env.example
# Configuración de la Aplicación
APP_NAME=string
# Configuración de Valkey (Caché Distribuido)
CACHE_HOST=string # Host o IP del servidor Valkey
CACHE_PORT=int # Puerto (ej: 6379)
CACHE_USERNAME=string # Usuario (opcional)
CACHE_PASSWORD=string # Contraseña (opcional)
CACHE_DB=int # Número de DB (0-15)
CACHE_TTL_MINUTE=int # TTL en minutos (default: 15)
# Configuración de Logging
LOG_APP_NAME=string # Nombre de la app en logs
LOG_ENV=string # development|staging|production
Ejemplo Completo de .env
# Para desarrollo local
APP_NAME=anime-scraper-dev
CACHE_HOST=localhost
CACHE_PORT=6379
CACHE_USERNAME=
CACHE_PASSWORD=
CACHE_DB=0
CACHE_TTL_MINUTE=15
LOG_APP_NAME=anime-api
LOG_ENV=development
# Para producción
APP_NAME=anime-scraper-prod
CACHE_HOST=redis.production.example.com
CACHE_PORT=6379
CACHE_USERNAME=admin
CACHE_PASSWORD=super-secret-password
CACHE_DB=1
CACHE_TTL_MINUTE=30
LOG_APP_NAME=anime-api
LOG_ENV=production
Ubicación del Archivo .env
La librería busca el archivo .env en este orden:
- En la raíz del proyecto (donde está
go.mod)
- En directorios padres:
../.env, ../../.env, ../../../.env
Si no encuentra el archivo, usa los valores por defecto automáticamente.
🛠️ Tecnologías Utilizadas
| Tecnología |
Versión |
Propósito |
| Go |
1.25.3 |
Lenguaje principal |
| goquery |
v1.11.0 |
Parsing y manipulación de HTML/CSS |
| godotenv |
v1.5.1 |
Cargar variables de entorno desde .env |
| zerolog |
v1.34.0 |
Logging estructurado y configurado por entorno |
| Valkey |
v1.0.69 |
Caché distribuido de alto rendimiento |
| cascadia |
v1.3.3 |
Selectores CSS (usado por goquery) |
| golang.org/x/net |
v0.48.0 |
Utilidades de red avanzadas |
| golang.org/x/time |
v0.14.0 |
Rate limiting integrado (3 req/seg con burst de 5) |
📚 Referencias y Recursos
⚠️ Aviso Legal y Ética
Este proyecto es solo para fines educativos. El scraping de sitios web debe hacerse respetando los términos de servicio del sitio.
Términos de Uso Responsable
El autor no se hace responsable del uso indebido de esta herramienta.
Obligaciones del usuario:
❌ NO hagas:
- Comercialización de datos scrapeados sin permiso
- Sobrecargar el servidor con peticiones masivas
- Usar datos para spam o actividades maliciosas
- Violar términos de servicio de AnimeFlv
- Distribuir la información scrapeada sin atribución
✅ HAZLO:
- Respeta el archivo
robots.txt del sitio
- Cita la fuente original (AnimeFlv)
- Usa para proyectos personales/educativos
- Monitorea cambios en el sitio
- Contacta al propietario si necesitas acceso comercial
📄 Licencia
Este proyecto está bajo la Licencia MIT. Ver LICENSE para detalles completos.
Resumen MIT:
- ✅ Uso comercial
- ✅ Modificación
- ✅ Distribución
- ✅ Uso privado
- ❌ Responsabilidad del autor
- ❌ Garantía
👤 Autor y Contribuidores
Steven (@dst3v3n) - Autor principal y mantenedor
Agradecimientos
- Comunidad de Go por las herramientas excelentes
- Equipo de Valkey por el motor de caché robusto
- Comunidad open-source de la que aprendemos todos
🤝 Cómo Contribuir
¡Las contribuciones son bienvenidas y apreciadas! Ya sea reportar bugs, sugerir features, o contribuir código.
Para reportar bugs:
- Abre un Issue descriptivo
- Incluye: versión de Go, pasos para reproducir, error esperado vs actual
- Adjunta logs o screenshots si es relevante
Para sugerir features:
- Abre un Issue con label
enhancement
- Describe el caso de uso y beneficio
- Discute la implementación sugerida
Para contribuir código:
- Fork el proyecto:
gh repo fork dst3v3n/api-anime
- Crea rama para tu feature:
git checkout -b feature/awesome-feature
- Implementa tu cambio
- Agrega tests: asegúrate que pasen todos los tests
- Commit:
git commit -am 'Add awesome feature'
- Push:
git push origin feature/awesome-feature
- Abre Pull Request: describe los cambios en detalle
Guía de Contribución
Standards de código:
- Sigue las convenciones de Go (gofmt, golint)
- Escribe tests para nuevas funcionalidades
- Mantén cobertura >80%
- Comenta código complejo
- Usa nombres descriptivos
Proceso de review:
- CI debe pasar (tests, linting)
- Necesita aprobación del mantenedor
- Después se merge a
development
- Se incluirá en próxima release
📝 Roadmap
✅ Completado
- Implementación básica del scraper AnimeFlv
- Búsqueda de animes
- Información detallada de anime
- Enlaces de episodios
- Contenido reciente
- Caché distribuido con Valkey
- Arquitectura hexagonal
- Tests unitarios
- Tests de integración
- Documentación completa
🚀 En Progreso
- Configuración vía variables de entorno
- Mejor manejo de errores
📋 Planeado
- CLI para uso desde terminal (
anime-cli search "Naruto")
- Soporte para más sitios de anime
- Docker image pre-configurada
- Websocket para updates en tiempo real
- Notificaciones de nuevos episodios
❓ Considerando
- Búsqueda avanzada con filtros
- Favoritos/watchlist
- Estadísticas de uso
- Sincronización multi-dispositivo
- Soporte para comentarios
📊 Estadísticas del Proyecto
Total de commits: 150+
Líneas de código: 2000+
Cobertura de tests: 85%+
Dependencias: 5 (muy ligero)
Tamaño binario: ~10MB
🆘 Soporte
Obtener ayuda
- FAQ - Chequea primero la sección FAQ
- GitHub Issues - Busca problemas similares
- Documentación - Lee la sección de ejemplos
- Prueba local - Ejecuta los tests para diagnosticar
Reportar problemas
# Ejecuta los tests para ver el error exacto
go test ./... -v
# Incluye en tu issue:
# - Output del error
# - Versión de Go: go version
# - Versión de la librería: git describe --tags
# - Pasos para reproducir
🎉 Gracias por usar Anime API
Si esta librería te fue útil:
⭐ Dale una estrella al repositorio
🔗 Comparte con otros desarrolladores
🐛 Reporta bugs para mejorar la calidad
💡 Sugiere features para hacerlo más útil
🤝 Contribuye código para fortalecer el proyecto
Made with ❤️ by Steven
_ _ _ _ _ _
/ \ / \ / \ / \ / \ / \
( A | N | I | M | E | _ |
\_/ \_/ \_/ \_/ \_/ \_/
_ _ _
/ \ / \ / \
( A | P | I |
\_/ \_/ \_/