mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 03:36:37 +08:00
176 lines
4.4 KiB
Go
176 lines
4.4 KiB
Go
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"
|
|
}
|