Files
silo-server/internal/subtitles/subdl/subdl.go
T

216 lines
5.5 KiB
Go

// internal/subtitles/subdl/subdl.go
package subdl
import (
"archive/zip"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path/filepath"
"strings"
"time"
"github.com/Silo-Server/silo-server/internal/subtitles"
)
const (
defaultBaseURL = "https://api.subdl.com/api/v1"
defaultDLBaseURL = "https://dl.subdl.com"
)
// Config holds the configuration for the SubDL provider.
type Config struct {
APIKey string
BaseURL string // override for testing
DLBaseURL string // override for testing
}
// Provider implements subtitles.Provider for SubDL.com.
type Provider struct {
client *http.Client
baseURL string
dlBaseURL string
apiKey string
limiter *subtitles.RateLimiter
}
// New creates a new SubDL provider.
func New(cfg Config) *Provider {
base := cfg.BaseURL
if base == "" {
base = defaultBaseURL
}
dlBase := cfg.DLBaseURL
if dlBase == "" {
dlBase = defaultDLBaseURL
}
return &Provider{
client: &http.Client{Timeout: 15 * time.Second},
baseURL: base,
dlBaseURL: dlBase,
apiKey: cfg.APIKey,
limiter: subtitles.NewRateLimiter(subtitles.RateLimiterConfig{
MaxRequests: 40,
Window: 10 * time.Second,
}),
}
}
func (p *Provider) Name() string { return "subdl" }
func (p *Provider) Search(ctx context.Context, req subtitles.SearchRequest) ([]subtitles.SubtitleResult, error) {
if err := p.limiter.Wait(ctx); err != nil {
return nil, err
}
params := url.Values{}
params.Set("api_key", p.apiKey)
if req.IMDbID != "" {
params.Set("imdb_id", req.IMDbID)
}
if len(req.Languages) > 0 {
params.Set("languages", strings.Join(req.Languages, ","))
}
if req.Season > 0 {
params.Set("type", "tv")
params.Set("season_number", fmt.Sprintf("%d", req.Season))
params.Set("episode_number", fmt.Sprintf("%d", req.Episode))
} else {
params.Set("type", "movie")
}
if req.Filename != "" {
params.Set("file_name", req.Filename)
}
if req.Title != "" && req.IMDbID == "" {
params.Set("film_name", req.Title)
}
httpReq, err := http.NewRequestWithContext(ctx, "GET", p.baseURL+"/subtitles?"+params.Encode(), nil)
if err != nil {
return nil, fmt.Errorf("subdl: build request: %w", err)
}
resp, err := p.client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("subdl: search request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("subdl: search returned %d: %s", resp.StatusCode, string(body))
}
var searchResp searchResponse
if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil {
return nil, fmt.Errorf("subdl: decode response: %w", err)
}
results := make([]subtitles.SubtitleResult, 0, len(searchResp.Subtitles))
for _, s := range searchResp.Subtitles {
// Skip results that don't match the requested episode
if req.Episode > 0 && s.Episode > 0 && s.Episode != req.Episode {
continue
}
format := detectFormat(s.ReleaseName)
results = append(results, subtitles.SubtitleResult{
ID: s.URL, // relative download path
Provider: "subdl",
Language: s.Lang,
ReleaseName: s.ReleaseName,
Format: format,
Downloads: s.DownloadCount,
HearingImpaired: s.HearingImpaired,
})
}
return results, nil
}
func (p *Provider) Download(ctx context.Context, id string) ([]byte, subtitles.SubtitleFormat, error) {
if err := p.limiter.Wait(ctx); err != nil {
return nil, "", err
}
// id is the relative download URL from search results
downloadURL := p.dlBaseURL + id
httpReq, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil)
if err != nil {
return nil, "", fmt.Errorf("subdl: build download request: %w", err)
}
resp, err := p.client.Do(httpReq)
if err != nil {
return nil, "", fmt.Errorf("subdl: download request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, "", fmt.Errorf("subdl: download returned %d: %s", resp.StatusCode, string(body))
}
zipData, err := io.ReadAll(resp.Body)
if err != nil {
return nil, "", fmt.Errorf("subdl: read zip: %w", err)
}
return extractSubtitleFromZip(zipData)
}
// extractSubtitleFromZip finds the first subtitle file in a ZIP archive.
func extractSubtitleFromZip(data []byte) ([]byte, subtitles.SubtitleFormat, error) {
reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return nil, "", fmt.Errorf("subdl: open zip: %w", err)
}
subtitleExts := map[string]subtitles.SubtitleFormat{
".srt": subtitles.FormatSRT,
".ass": subtitles.FormatASS,
".ssa": subtitles.FormatSSA,
".vtt": subtitles.FormatVTT,
".sub": subtitles.FormatSUB,
}
for _, f := range reader.File {
ext := strings.ToLower(filepath.Ext(f.Name))
if format, ok := subtitleExts[ext]; ok {
rc, err := f.Open()
if err != nil {
return nil, "", fmt.Errorf("subdl: open zip entry: %w", err)
}
content, err := io.ReadAll(rc)
rc.Close()
if err != nil {
return nil, "", fmt.Errorf("subdl: read zip entry: %w", err)
}
return content, format, nil
}
}
return nil, "", fmt.Errorf("subdl: no subtitle file found in zip archive")
}
func detectFormat(filename string) subtitles.SubtitleFormat {
ext := strings.ToLower(filepath.Ext(filename))
switch ext {
case ".srt":
return subtitles.FormatSRT
case ".ass":
return subtitles.FormatASS
case ".ssa":
return subtitles.FormatSSA
case ".vtt":
return subtitles.FormatVTT
case ".sub":
return subtitles.FormatSUB
default:
return subtitles.FormatSRT
}
}