Files
MeBox/internal/service/site_adapter_gazelle.go
T

228 lines
6.1 KiB
Go

// Package service — Gazelle site adapter.
package service
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
)
// ─── Gazelle 适配器 ──────────────────────────────────────────────────────────
// GazelleAdapter Gazelle 框架适配器(What.cd 开源)。
type GazelleAdapter struct {
client *http.Client
}
// NewGazelleAdapter 创建 Gazelle 适配器。
func NewGazelleAdapter() *GazelleAdapter {
return &GazelleAdapter{
client: &http.Client{Timeout: 30 * time.Second},
}
}
func (a *GazelleAdapter) Authenticate(ctx context.Context, cfg SiteConfig) error {
u := cfg.URL + "/ajax.php?action=index"
data, status, err := doRequest(ctx, a.client, "GET", u, cfg, nil)
if err != nil {
return fmt.Errorf("authenticate: %w", err)
}
if status != http.StatusOK {
return fmt.Errorf("authenticate failed: status %d", status)
}
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
return fmt.Errorf("parse response: %w", err)
}
if statusMsg, ok := result["status"].(string); ok && statusMsg == "failure" {
return fmt.Errorf("authentication failed: %v", result["error"])
}
return nil
}
func (a *GazelleAdapter) Search(ctx context.Context, cfg SiteConfig, keyword string, page int) (*SiteSearchResult, error) {
params := url.Values{}
params.Set("action", "browse")
params.Set("searchstr", keyword)
params.Set("page", strconv.Itoa(page))
u := cfg.URL + "/ajax.php?" + params.Encode()
data, status, err := doRequest(ctx, a.client, "GET", u, 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)
}
return parseGazelleJSON(data, cfg.Name, cfg.URL)
}
func (a *GazelleAdapter) Browse(ctx context.Context, cfg SiteConfig, category string, page int) (*SiteSearchResult, error) {
params := url.Values{}
params.Set("action", "browse")
if category != "" {
params.Set("filter_cat["+category+"]", "1")
}
params.Set("page", strconv.Itoa(page))
u := cfg.URL + "/ajax.php?" + params.Encode()
data, status, err := doRequest(ctx, a.client, "GET", u, cfg, nil)
if err != nil {
return nil, fmt.Errorf("browse: %w", err)
}
if status != http.StatusOK {
return nil, fmt.Errorf("browse failed: status %d", status)
}
return parseGazelleJSON(data, cfg.Name, cfg.URL)
}
func (a *GazelleAdapter) GetDetail(ctx context.Context, cfg SiteConfig, id string) (*TorrentDetail, error) {
params := url.Values{}
params.Set("action", "torrent")
params.Set("id", id)
u := cfg.URL + "/ajax.php?" + params.Encode()
data, status, err := doRequest(ctx, a.client, "GET", u, cfg, nil)
if err != nil {
return nil, fmt.Errorf("detail: %w", err)
}
if status != http.StatusOK {
return nil, fmt.Errorf("detail failed: status %d", status)
}
var resp map[string]interface{}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, fmt.Errorf("parse response: %w", err)
}
torrent, ok := resp["torrent"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("torrent not found")
}
detail := &TorrentDetail{
ID: id,
DetailURL: cfg.URL + "/torrents.php?torrentid=" + id,
DownloadURL: cfg.URL + "/torrents.php?action=download&id=" + id,
}
if v, ok := torrent["groupName"].(string); ok {
detail.Title = v
}
if v, ok := torrent["subName"].(string); ok {
detail.Subtitle = v
}
if v, ok := torrent["size"].(float64); ok {
detail.Size = int64(v)
}
if v, ok := torrent["seeders"].(float64); ok {
detail.Seeders = int(v)
}
if v, ok := torrent["leechers"].(float64); ok {
detail.Leechers = int(v)
}
if v, ok := torrent["snatched"].(float64); ok {
detail.Snatched = int(v)
}
if v, ok := torrent["freeTorrent"].(string); ok && v == "1" {
detail.Free = true
}
if v, ok := torrent["freeTorrent"].(bool); ok {
detail.Free = v
}
if v, ok := torrent["infoHash"].(string); ok {
detail.InfoHash = v
}
if v, ok := torrent["groupDesc"].(string); ok {
detail.Description = stripHTML(v)
}
return detail, nil
}
func (a *GazelleAdapter) GetDownloadURL(ctx context.Context, cfg SiteConfig, id string) (string, error) {
return cfg.URL + "/torrents.php?action=download&id=" + id, nil
}
// parseGazelleJSON 解析 Gazelle JSON 响应。
func parseGazelleJSON(data []byte, siteName, baseURL string) (*SiteSearchResult, error) {
var resp map[string]interface{}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, fmt.Errorf("parse JSON: %w", err)
}
result := &SiteSearchResult{
SiteName: siteName,
Items: []TorrentItem{},
}
if status, ok := resp["status"].(string); ok && status == "failure" {
return result, nil
}
results, ok := resp["results"].([]interface{})
if !ok {
return result, nil
}
for _, r := range results {
torrent, ok := r.(map[string]interface{})
if !ok {
continue
}
item := TorrentItem{}
if v, ok := torrent["torrentId"].(float64); ok {
item.ID = strconv.Itoa(int(v))
}
if v, ok := torrent["groupName"].(string); ok {
item.Title = v
}
if v, ok := torrent["artist"].(string); ok {
item.Subtitle = v
}
if v, ok := torrent["category"].(string); ok {
item.Category = v
}
if v, ok := torrent["size"].(float64); ok {
item.Size = int64(v)
}
if v, ok := torrent["seeders"].(float64); ok {
item.Seeders = int(v)
}
if v, ok := torrent["leechers"].(float64); ok {
item.Leechers = int(v)
}
if v, ok := torrent["snatched"].(float64); ok {
item.Snatched = int(v)
}
if v, ok := torrent["freeTorrent"].(string); ok && v == "1" {
item.Free = true
}
if v, ok := torrent["freeTorrent"].(bool); ok {
item.Free = v
}
if v, ok := torrent["time"].(string); ok {
if t, err := time.Parse(time.RFC3339, v); err == nil {
item.UploadTime = t
}
}
item.DetailURL = baseURL + "/torrents.php?torrentid=" + item.ID
item.DownloadURL = baseURL + "/torrents.php?action=download&id=" + item.ID
result.Items = append(result.Items, item)
}
if total, ok := resp["totalResults"].(float64); ok {
result.Total = int(total)
} else {
result.Total = len(result.Items)
}
return result, nil
}