mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
199 lines
5.4 KiB
Go
199 lines
5.4 KiB
Go
// Package service — UNIT3D site adapter.
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
// ─── UNIT3D 适配器 ───────────────────────────────────────────────────────────
|
|
|
|
// UNIT3DAdapter UNIT3D 框架适配器。
|
|
type UNIT3DAdapter struct {
|
|
client *http.Client
|
|
}
|
|
|
|
// NewUNIT3DAdapter 创建 UNIT3D 适配器。
|
|
func NewUNIT3DAdapter() *UNIT3DAdapter {
|
|
return &UNIT3DAdapter{
|
|
client: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
}
|
|
|
|
func (a *UNIT3DAdapter) Authenticate(ctx context.Context, cfg SiteConfig) error {
|
|
u := cfg.URL + "/api/torrents?limit=1"
|
|
data, status, err := doRequest(ctx, a.client, "GET", u, cfg, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("authenticate: %w", err)
|
|
}
|
|
if status == http.StatusUnauthorized || status == http.StatusForbidden {
|
|
return fmt.Errorf("authentication failed: status %d", status)
|
|
}
|
|
if status != http.StatusOK {
|
|
return fmt.Errorf("authenticate failed: status %d", status)
|
|
}
|
|
var resp map[string]interface{}
|
|
if err := json.Unmarshal(data, &resp); err == nil {
|
|
if errMsg, ok := resp["message"].(string); ok {
|
|
return fmt.Errorf("authentication failed: %s", errMsg)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *UNIT3DAdapter) Search(ctx context.Context, cfg SiteConfig, keyword string, page int) (*SiteSearchResult, error) {
|
|
params := url.Values{}
|
|
params.Set("search", keyword)
|
|
params.Set("page", strconv.Itoa(page))
|
|
|
|
u := cfg.URL + "/api/torrents?" + 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 parseUNIT3DJSON(data, cfg.Name, cfg.URL)
|
|
}
|
|
|
|
func (a *UNIT3DAdapter) Browse(ctx context.Context, cfg SiteConfig, category string, page int) (*SiteSearchResult, error) {
|
|
params := url.Values{}
|
|
if category != "" {
|
|
params.Set("category", category)
|
|
}
|
|
params.Set("page", strconv.Itoa(page))
|
|
|
|
u := cfg.URL + "/api/torrents?" + 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 parseUNIT3DJSON(data, cfg.Name, cfg.URL)
|
|
}
|
|
|
|
func (a *UNIT3DAdapter) GetDetail(ctx context.Context, cfg SiteConfig, id string) (*TorrentDetail, error) {
|
|
u := cfg.URL + "/api/torrents/" + id
|
|
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 torrent map[string]interface{}
|
|
if err := json.Unmarshal(data, &torrent); err != nil {
|
|
return nil, fmt.Errorf("parse response: %w", err)
|
|
}
|
|
|
|
detail := &TorrentDetail{
|
|
ID: id,
|
|
DetailURL: cfg.URL + "/torrents/" + id,
|
|
}
|
|
|
|
if v, ok := torrent["name"].(string); ok {
|
|
detail.Title = v
|
|
}
|
|
if v, ok := torrent["description"].(string); ok {
|
|
detail.Description = stripHTML(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["times_completed"].(float64); ok {
|
|
detail.Snatched = int(v)
|
|
}
|
|
if v, ok := torrent["free"].(bool); ok {
|
|
detail.Free = v
|
|
}
|
|
if v, ok := torrent["info_hash"].(string); ok {
|
|
detail.InfoHash = v
|
|
}
|
|
|
|
detail.DownloadURL = cfg.URL + "/api/torrents/" + id + "/download"
|
|
return detail, nil
|
|
}
|
|
|
|
func (a *UNIT3DAdapter) GetDownloadURL(ctx context.Context, cfg SiteConfig, id string) (string, error) {
|
|
return cfg.URL + "/api/torrents/" + id + "/download", nil
|
|
}
|
|
|
|
// parseUNIT3DJSON 解析 UNIT3D JSON 响应。
|
|
func parseUNIT3DJSON(data []byte, siteName, baseURL string) (*SiteSearchResult, error) {
|
|
var resp struct {
|
|
Data []map[string]interface{} `json:"data"`
|
|
Meta struct {
|
|
Total int `json:"total"`
|
|
CurrentPage int `json:"current_page"`
|
|
} `json:"meta"`
|
|
}
|
|
if err := json.Unmarshal(data, &resp); err != nil {
|
|
return nil, fmt.Errorf("parse JSON: %w", err)
|
|
}
|
|
|
|
result := &SiteSearchResult{
|
|
SiteName: siteName,
|
|
Items: []TorrentItem{},
|
|
Page: resp.Meta.CurrentPage,
|
|
Total: resp.Meta.Total,
|
|
}
|
|
|
|
for _, t := range resp.Data {
|
|
item := TorrentItem{}
|
|
if v, ok := t["id"].(float64); ok {
|
|
item.ID = strconv.Itoa(int(v))
|
|
}
|
|
if v, ok := t["name"].(string); ok {
|
|
item.Title = v
|
|
}
|
|
if v, ok := t["category"].(map[string]interface{}); ok {
|
|
if name, ok := v["name"].(string); ok {
|
|
item.Category = name
|
|
}
|
|
}
|
|
if v, ok := t["size"].(float64); ok {
|
|
item.Size = int64(v)
|
|
}
|
|
if v, ok := t["seeders"].(float64); ok {
|
|
item.Seeders = int(v)
|
|
}
|
|
if v, ok := t["leechers"].(float64); ok {
|
|
item.Leechers = int(v)
|
|
}
|
|
if v, ok := t["times_completed"].(float64); ok {
|
|
item.Snatched = int(v)
|
|
}
|
|
if v, ok := t["free"].(bool); ok {
|
|
item.Free = v
|
|
}
|
|
if v, ok := t["created_at"].(string); ok {
|
|
if t2, err := time.Parse(time.RFC3339, v); err == nil {
|
|
item.UploadTime = t2
|
|
}
|
|
}
|
|
|
|
item.DetailURL = baseURL + "/torrents/" + item.ID
|
|
item.DownloadURL = baseURL + "/api/torrents/" + item.ID + "/download"
|
|
result.Items = append(result.Items, item)
|
|
}
|
|
|
|
return result, nil
|
|
}
|