mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-10 15:26:38 +08:00
refactor site service by responsibility
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||
)
|
||||
|
||||
// ResolveDownloadURL converts tracker-specific search result URLs into a URL
|
||||
// that a downloader can fetch directly. M-Team, NexusPHP and similar sites
|
||||
// often expose a signed/detail endpoint in search results; qBittorrent cannot
|
||||
// call those APIs with the configured site credentials, so subscriptions need
|
||||
// the same resolution path as the manual download button.
|
||||
func (s *SiteService) ResolveDownloadURL(ctx context.Context, raw string) string {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return raw
|
||||
}
|
||||
matched := s.matchSiteForURL(ctx, raw)
|
||||
if matched == nil {
|
||||
return raw
|
||||
}
|
||||
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil || u.Host == "" {
|
||||
return raw
|
||||
}
|
||||
id := u.Query().Get("id")
|
||||
if id == "" {
|
||||
return raw
|
||||
}
|
||||
adapter := GetAdapterForType(matched.Type)
|
||||
if adapter == nil {
|
||||
return raw
|
||||
}
|
||||
cfg := s.siteModelToConfig(matched)
|
||||
timeout := cfg.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 15 * time.Second
|
||||
}
|
||||
resolveCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
resolved, err := adapter.GetDownloadURL(resolveCtx, cfg, id)
|
||||
if err != nil || resolved == "" {
|
||||
if s.log != nil {
|
||||
s.log.Warn("resolve PT download URL failed",
|
||||
zap.String("site", matched.Name),
|
||||
zap.String("raw", redactSensitiveDownloadURL(raw)),
|
||||
zap.Error(err))
|
||||
}
|
||||
return raw
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
func redactSensitiveDownloadURL(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(raw), "magnet:") {
|
||||
return "magnet:?xt=***"
|
||||
}
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil || u.Host == "" {
|
||||
return "[redacted-download-url]"
|
||||
}
|
||||
u.RawQuery = ""
|
||||
u.Fragment = ""
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (s *SiteService) FetchTorrentFile(ctx context.Context, raw string) ([]byte, string, error) {
|
||||
matched := s.matchSiteForURL(ctx, raw)
|
||||
if matched == nil {
|
||||
return nil, "", errors.New("no matching PT site for torrent URL")
|
||||
}
|
||||
cfg := s.siteModelToConfig(matched)
|
||||
timeout := cfg.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 30 * time.Second
|
||||
}
|
||||
req, err := buildRequest(ctx, http.MethodGet, raw, cfg, nil)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
req.Header.Set("Accept", "application/x-bittorrent,application/octet-stream,*/*")
|
||||
client := newHTTPClient(cfg, timeout)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, "", fmt.Errorf("torrent fetch: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
const maxTorrentSize = 32 << 20
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, maxTorrentSize+1))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, "", errors.New("torrent fetch: empty body")
|
||||
}
|
||||
if len(data) > maxTorrentSize {
|
||||
return nil, "", errors.New("torrent fetch: body too large")
|
||||
}
|
||||
if strings.Contains(strings.ToLower(resp.Header.Get("Content-Type")), "text/html") {
|
||||
return nil, "", errors.New("torrent fetch: upstream returned HTML")
|
||||
}
|
||||
if torrentInfoHash(data) == "" {
|
||||
return nil, "", errors.New("torrent fetch: upstream did not return a valid torrent")
|
||||
}
|
||||
return data, torrentFilename(raw, resp.Header.Get("Content-Disposition")), nil
|
||||
}
|
||||
|
||||
func (s *SiteService) matchSiteForURL(ctx context.Context, raw string) *model.Site {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil || u.Host == "" {
|
||||
return nil
|
||||
}
|
||||
host := strings.ToLower(u.Host)
|
||||
|
||||
sites, err := s.List(ctx)
|
||||
if err != nil || len(sites) == 0 {
|
||||
return nil
|
||||
}
|
||||
for i := range sites {
|
||||
if siteHostMatches(host, sites[i].URL) || siteHostMatches(host, sites[i].RSSURL) {
|
||||
return &sites[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func siteHostMatches(host, raw string) bool {
|
||||
if raw == "" {
|
||||
return false
|
||||
}
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil || u.Host == "" {
|
||||
return false
|
||||
}
|
||||
siteHost := strings.ToLower(u.Host)
|
||||
return strings.EqualFold(siteHost, host) || strings.HasSuffix(host, "."+siteHost)
|
||||
}
|
||||
|
||||
func torrentFilename(rawURL, disposition string) string {
|
||||
if disposition != "" {
|
||||
if _, params, err := mime.ParseMediaType(disposition); err == nil {
|
||||
if filename := strings.TrimSpace(params["filename"]); filename != "" {
|
||||
return filename
|
||||
}
|
||||
}
|
||||
}
|
||||
if u, err := url.Parse(rawURL); err == nil {
|
||||
if name := strings.TrimSpace(path.Base(u.Path)); name != "" && name != "." && name != "/" {
|
||||
if !strings.HasSuffix(strings.ToLower(name), ".torrent") {
|
||||
name += ".torrent"
|
||||
}
|
||||
return name
|
||||
}
|
||||
}
|
||||
return "download.torrent"
|
||||
}
|
||||
Reference in New Issue
Block a user