// Package service — custom RSS site adapter.
package service
import (
"context"
"fmt"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
)
// ─── Custom RSS 适配器 ───────────────────────────────────────────────────────
// CustomRSSAdapter 自定义 RSS 源适配器。
type CustomRSSAdapter struct {
client *http.Client
}
// NewCustomRSSAdapter 创建 Custom RSS 适配器。
func NewCustomRSSAdapter() *CustomRSSAdapter {
return &CustomRSSAdapter{
client: &http.Client{Timeout: 30 * time.Second},
}
}
func (a *CustomRSSAdapter) Authenticate(ctx context.Context, cfg SiteConfig) error {
// RSS 源通常不需要认证,或者认证通过 URL 参数
if cfg.URL == "" {
return fmt.Errorf("RSS URL is required")
}
_, status, err := doRequest(ctx, a.client, "GET", cfg.URL, cfg, nil)
if err != nil {
return fmt.Errorf("authenticate: %w", err)
}
if status != http.StatusOK {
return fmt.Errorf("authenticate failed: status %d", status)
}
return nil
}
func (a *CustomRSSAdapter) Search(ctx context.Context, cfg SiteConfig, keyword string, page int) (*SiteSearchResult, error) {
searchURL := cfg.URL
// If extra has search URL template, use it
if searchTpl, ok := cfg.Extra["search_url"]; ok && searchTpl != "" {
searchURL = strings.ReplaceAll(searchTpl, "{keyword}", url.QueryEscape(keyword))
searchURL = strings.ReplaceAll(searchURL, "{page}", strconv.Itoa(page))
}
data, status, err := doRequest(ctx, a.client, "GET", searchURL, cfg, nil)
if err != nil {
return nil, fmt.Errorf("search: %w", err)
}
if status != http.StatusOK {
return nil, fmt.Errorf("search failed: status %d", status)
}
result, err := parseRSSXML(data, cfg.Name, keyword)
if err != nil {
return nil, fmt.Errorf("parse RSS: %w", err)
}
if page > 1 {
// Simple pagination for RSS: skip items already seen
start := (page - 1) * 50
if start < len(result.Items) {
result.Items = result.Items[start:]
} else {
result.Items = []TorrentItem{}
}
}
result.Page = page
return result, nil
}
func (a *CustomRSSAdapter) Browse(ctx context.Context, cfg SiteConfig, category string, page int) (*SiteSearchResult, error) {
// RSS browse is essentially the same as search with empty keyword
return a.Search(ctx, cfg, "", page)
}
func (a *CustomRSSAdapter) GetDetail(ctx context.Context, cfg SiteConfig, id string) (*TorrentDetail, error) {
// RSS typically doesn't support detail page; return basic info
return &TorrentDetail{
ID: id,
Title: id,
}, nil
}
func (a *CustomRSSAdapter) GetDownloadURL(ctx context.Context, cfg SiteConfig, id string) (string, error) {
return id, nil // In RSS, the ID is often the download URL
}
// parseRSSXML 解析 RSS XML 内容。
func parseRSSXML(data []byte, siteName, keyword string) (*SiteSearchResult, error) {
result := &SiteSearchResult{
SiteName: siteName,
Items: []TorrentItem{},
}
html := string(data)
// Simple regex-based XML parsing for RSS items
itemRegex := regexp.MustCompile(`