basic find and download ability

This commit is contained in:
Simon Ding
2024-07-01 11:34:54 +08:00
parent ab8e653c5b
commit 3b7a444164
13 changed files with 2304 additions and 0 deletions

25
pkg/downloader/torrent.go Normal file
View File

@@ -0,0 +1,25 @@
package downloader
import (
"github.com/anacrolix/torrent"
"github.com/pkg/errors"
)
func DownloadByMagnet(magnet string, dir string) (*torrent.Torrent, error) {
c, err := torrent.NewClient(nil)
if err != nil {
return nil, errors.Wrap(err, "new torrent")
}
defer c.Close()
t, err := c.AddMagnet(magnet)
if err != nil {
return nil, errors.Wrap(err, "add torrent")
}
<-t.GotInfo()
t.DownloadAll()
c.WaitAll()
return t, nil
}

206
pkg/jackett/category.go Normal file
View File

@@ -0,0 +1,206 @@
package jackett
import "fmt"
type Category struct {
ID int
Name string
}
func (c Category) String() string {
return fmt.Sprintf("%s[%d]", c.Name, c.ID)
}
const (
CustomCategoryOffset = 100000
)
// Categories from the Newznab spec
// https://github.com/nZEDb/nZEDb/blob/0.x/docs/newznab_api_specification.txt#L627
var (
CategoryOther = Category{0, "Other"}
CategoryOther_Misc = Category{10, "Other/Misc"}
CategoryOther_Hashed = Category{20, "Other/Hashed"}
CategoryConsole = Category{1000, "Console"}
CategoryConsole_NDS = Category{1010, "Console/NDS"}
CategoryConsole_PSP = Category{1020, "Console/PSP"}
CategoryConsole_Wii = Category{1030, "Console/Wii"}
CategoryConsole_XBOX = Category{1040, "Console/Xbox"}
CategoryConsole_XBOX360 = Category{1050, "Console/Xbox360"}
CategoryConsole_WiiwareVC = Category{1060, "Console/Wiiware/V"}
CategoryConsole_XBOX360DLC = Category{1070, "Console/Xbox360"}
CategoryConsole_PS3 = Category{1080, "Console/PS3"}
CategoryConsole_Other = Category{1999, "Console/Other"}
CategoryConsole_3DS = Category{1110, "Console/3DS"}
CategoryConsole_PSVita = Category{1120, "Console/PS Vita"}
CategoryConsole_WiiU = Category{1130, "Console/WiiU"}
CategoryConsole_XBOXOne = Category{1140, "Console/XboxOne"}
CategoryConsole_PS4 = Category{1180, "Console/PS4"}
CategoryMovies = Category{2000, "Movies"}
CategoryMovies_Foreign = Category{2010, "Movies/Foreign"}
CategoryMovies_Other = Category{2020, "Movies/Other"}
CategoryMovies_SD = Category{2030, "Movies/SD"}
CategoryMovies_HD = Category{2040, "Movies/HD"}
CategoryMovies_3D = Category{2050, "Movies/3D"}
CategoryMovies_BluRay = Category{2060, "Movies/BluRay"}
CategoryMovies_DVD = Category{2070, "Movies/DVD"}
CategoryMovies_WEBDL = Category{2080, "Movies/WEBDL"}
CategoryAudio = Category{3000, "Audio"}
CategoryAudio_MP3 = Category{3010, "Audio/MP3"}
CategoryAudio_Video = Category{3020, "Audio/Video"}
CategoryAudio_Audiobook = Category{3030, "Audio/Audiobook"}
CategoryAudio_Lossless = Category{3040, "Audio/Lossless"}
CategoryAudio_Other = Category{3999, "Audio/Other"}
CategoryAudio_Foreign = Category{3060, "Audio/Foreign"}
CategoryPC = Category{4000, "PC"}
CategoryPC_0day = Category{4010, "PC/0day"}
CategoryPC_ISO = Category{4020, "PC/ISO"}
CategoryPC_Mac = Category{4030, "PC/Mac"}
CategoryPC_PhoneOther = Category{4040, "PC/Phone-Other"}
CategoryPC_Games = Category{4050, "PC/Games"}
CategoryPC_PhoneIOS = Category{4060, "PC/Phone-IOS"}
CategoryPC_PhoneAndroid = Category{4070, "PC/Phone-Android"}
CategoryTV = Category{5000, "TV"}
CategoryTV_WEBDL = Category{5010, "TV/WEB-DL"}
CategoryTV_FOREIGN = Category{5020, "TV/Foreign"}
CategoryTV_SD = Category{5030, "TV/SD"}
CategoryTV_HD = Category{5040, "TV/HD"}
CategoryTV_Other = Category{5999, "TV/Other"}
CategoryTV_Sport = Category{5060, "TV/Sport"}
CategoryTV_Anime = Category{5070, "TV/Anime"}
CategoryTV_Documentary = Category{5080, "TV/Documentary"}
CategoryXXX = Category{6000, "XXX"}
CategoryXXX_DVD = Category{6010, "XXX/DVD"}
CategoryXXX_WMV = Category{6020, "XXX/WMV"}
CategoryXXX_XviD = Category{6030, "XXX/XviD"}
CategoryXXX_x264 = Category{6040, "XXX/x264"}
CategoryXXX_Other = Category{6999, "XXX/Other"}
CategoryXXX_Imageset = Category{6060, "XXX/Imageset"}
CategoryXXX_Packs = Category{6070, "XXX/Packs"}
CategoryBooks = Category{7000, "Books"}
CategoryBooks_Magazines = Category{7010, "Books/Magazines"}
CategoryBooks_Ebook = Category{7020, "Books/Ebook"}
CategoryBooks_Comics = Category{7030, "Books/Comics"}
CategoryBooks_Technical = Category{7040, "Books/Technical"}
CategoryBooks_Foreign = Category{7060, "Books/Foreign"}
CategoryBooks_Unknown = Category{7999, "Books/Unknown"}
)
var AllCategories = Categories{
CategoryOther,
CategoryOther_Misc,
CategoryOther_Hashed,
CategoryConsole,
CategoryConsole_NDS,
CategoryConsole_PSP,
CategoryConsole_Wii,
CategoryConsole_XBOX,
CategoryConsole_XBOX360,
CategoryConsole_WiiwareVC,
CategoryConsole_XBOX360DLC,
CategoryConsole_PS3,
CategoryConsole_Other,
CategoryConsole_3DS,
CategoryConsole_PSVita,
CategoryConsole_WiiU,
CategoryConsole_XBOXOne,
CategoryConsole_PS4,
CategoryMovies,
CategoryMovies_Foreign,
CategoryMovies_Other,
CategoryMovies_SD,
CategoryMovies_HD,
CategoryMovies_3D,
CategoryMovies_BluRay,
CategoryMovies_DVD,
CategoryMovies_WEBDL,
CategoryAudio,
CategoryAudio_MP3,
CategoryAudio_Video,
CategoryAudio_Audiobook,
CategoryAudio_Lossless,
CategoryAudio_Other,
CategoryAudio_Foreign,
CategoryPC,
CategoryPC_0day,
CategoryPC_ISO,
CategoryPC_Mac,
CategoryPC_PhoneOther,
CategoryPC_Games,
CategoryPC_PhoneIOS,
CategoryPC_PhoneAndroid,
CategoryTV,
CategoryTV_WEBDL,
CategoryTV_FOREIGN,
CategoryTV_SD,
CategoryTV_HD,
CategoryTV_Other,
CategoryTV_Sport,
CategoryTV_Anime,
CategoryTV_Documentary,
CategoryXXX,
CategoryXXX_DVD,
CategoryXXX_WMV,
CategoryXXX_XviD,
CategoryXXX_x264,
CategoryXXX_Other,
CategoryXXX_Imageset,
CategoryXXX_Packs,
CategoryBooks,
CategoryBooks_Magazines,
CategoryBooks_Ebook,
CategoryBooks_Comics,
CategoryBooks_Technical,
CategoryBooks_Foreign,
CategoryBooks_Unknown,
}
func ParentCategory(c Category) Category {
switch {
case c.ID < 1000:
return CategoryOther
case c.ID < 2000:
return CategoryConsole
case c.ID < 3000:
return CategoryMovies
case c.ID < 4000:
return CategoryAudio
case c.ID < 5000:
return CategoryPC
case c.ID < 6000:
return CategoryTV
case c.ID < 7000:
return CategoryXXX
case c.ID < 8000:
return CategoryBooks
}
return CategoryOther
}
type Categories []Category
func (slice Categories) Subset(ids ...int) Categories {
cats := Categories{}
for _, cat := range AllCategories {
for _, id := range ids {
if cat.ID == id {
cats = append(cats, cat)
}
}
}
return cats
}
func (slice Categories) Len() int {
return len(slice)
}
func (slice Categories) Less(i, j int) bool {
return slice[i].ID < slice[j].ID
}
func (slice Categories) Swap(i, j int) {
slice[i], slice[j] = slice[j], slice[i]
}

168
pkg/jackett/jackett.go Normal file
View File

@@ -0,0 +1,168 @@
package jackett
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/pkg/errors"
"golang.org/x/net/context"
)
var (
apiURL string
apiKey string
)
type Settings struct {
ApiURL string
ApiKey string
Client *http.Client
}
type FetchRequest struct {
Query string
Trackers []string
Categories []uint
}
type FetchResponse struct {
Results []Result
Indexers []Indexer
}
type jackettTime struct {
time.Time
}
func (jt *jackettTime) UnmarshalJSON(b []byte) (err error) {
str := strings.Trim(string(b), `"`)
if str == "0001-01-01T00:00:00" {
} else if len(str) == 19 {
jt.Time, err = time.Parse(time.RFC3339, str+"Z")
} else {
jt.Time, err = time.Parse(time.RFC3339, str)
}
return
}
type Result struct {
BannerUrl string
BlackholeLink string
Category []uint
CategoryDesc string
Comments string
Description string
DownloadVolumeFactor float32
Files uint
FirstSeen jackettTime
Gain float32
Grabs uint
Guid string
Imdb uint
InfoHash string
Link string
MagnetUri string
MinimumRatio float32
MinimumSeedTime uint
Peers uint
PublishDate jackettTime
RageID uint
Seeders uint
Size uint
TMDb uint
TVDBId uint
Title string
Tracker string
TrackerId string
UploadVolumeFactor float32
}
type Indexer struct {
Error string
ID string
Name string
Results uint
Status uint
}
type Jackett struct {
settings *Settings
}
func NewJackett(s *Settings) *Jackett {
if s.ApiURL == "" && apiURL != "" {
s.ApiURL = apiURL
}
if s.ApiKey == "" && apiKey != "" {
s.ApiKey = apiKey
}
if s.Client == nil {
s.Client = http.DefaultClient
}
return &Jackett{settings: s}
}
func (j *Jackett) generateFetchURL(fr *FetchRequest) (string, error) {
u, err := url.Parse(j.settings.ApiURL)
if err != nil {
return "", errors.Wrapf(err, "failed to parse apiURL %q", j.settings.ApiURL)
}
u.Path = "/api/v2.0/indexers/all/results"
q := u.Query()
q.Set("apikey", j.settings.ApiKey)
for _, t := range fr.Trackers {
q.Add("Tracker[]", t)
}
for _, c := range fr.Categories {
q.Add("Category[]", fmt.Sprintf("%v", c))
}
if fr.Query != "" {
q.Add("Query", fr.Query)
}
u.RawQuery = q.Encode()
return u.String(), nil
}
func (j *Jackett) Fetch(ctx context.Context, fr *FetchRequest) (*FetchResponse, error) {
u, err := j.generateFetchURL(fr)
if err != nil {
return nil, errors.Wrap(err, "failed to generate fetch url")
}
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
if err != nil {
return nil, errors.Wrap(err, "failed to make fetch request")
}
res, err := j.settings.Client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "failed to invoke fetch request")
}
defer res.Body.Close()
data, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, errors.Wrap(err, "failed to read fetch data")
}
var fres FetchResponse
err = json.Unmarshal(data, &fres)
if err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal fetch data with url=%v and data=%v", u, string(data))
}
return &fres, nil
}
func SearchTvWithJacket(q string, url, apiKey string) (*FetchResponse, error) {
j := NewJackett(&Settings{
ApiURL: url,
ApiKey: apiKey,
})
resp, err := j.Fetch(context.TODO(), &FetchRequest{
//Categories: []uint{uint(CategoryTV.ID)},
Query: q,
})
return resp, err
}

134
pkg/torznab/torznab.go Normal file
View File

@@ -0,0 +1,134 @@
package torznab
import (
"encoding/xml"
"io"
"net/http"
"net/url"
"polaris/log"
"strconv"
"github.com/pkg/errors"
)
type Response struct {
XMLName xml.Name `xml:"rss"`
Text string `xml:",chardata"`
Version string `xml:"version,attr"`
Atom string `xml:"atom,attr"`
Torznab string `xml:"torznab,attr"`
Channel struct {
Text string `xml:",chardata"`
Link struct {
Text string `xml:",chardata"`
Href string `xml:"href,attr"`
Rel string `xml:"rel,attr"`
Type string `xml:"type,attr"`
} `xml:"link"`
Title string `xml:"title"`
Description string `xml:"description"`
Language string `xml:"language"`
Category string `xml:"category"`
Item []Item `xml:"item"`
} `xml:"channel"`
}
type Item struct {
Text string `xml:",chardata"`
Title string `xml:"title"`
Guid string `xml:"guid"`
Jackettindexer struct {
Text string `xml:",chardata"`
ID string `xml:"id,attr"`
} `xml:"jackettindexer"`
Type string `xml:"type"`
Comments string `xml:"comments"`
PubDate string `xml:"pubDate"`
Size string `xml:"size"`
Description string `xml:"description"`
Link string `xml:"link"`
Category []string `xml:"category"`
Enclosure struct {
Text string `xml:",chardata"`
URL string `xml:"url,attr"`
Length string `xml:"length,attr"`
Type string `xml:"type,attr"`
} `xml:"enclosure"`
Attr []struct {
Text string `xml:",chardata"`
Name string `xml:"name,attr"`
Value string `xml:"value,attr"`
} `xml:"attr"`
}
func (i *Item) GetAttr(key string) string {
for _, a := range i.Attr {
if a.Name == key {
return a.Value
}
}
return ""
}
func (r *Response) ToResults() []Result {
var res []Result
for _, item := range r.Channel.Item {
r := Result{
Name: item.Title,
Magnet: item.Link,
Size: mustAtoI(item.Size),
Seeders: mustAtoI(item.GetAttr("seeders")),
Peers: mustAtoI(item.GetAttr("peers")),
Category: mustAtoI(item.GetAttr("category")),
Source: r.Channel.Title,
}
res = append(res, r)
}
return res
}
func mustAtoI(key string) int {
i, err := strconv.Atoi(key)
if err != nil {
log.Errorf("must atoi error: %v", err)
panic(err)
}
return i
}
func Search(torznabUrl, api, keyWord string) ([]Result, error) {
req, err := http.NewRequest(http.MethodGet, torznabUrl, nil)
if err != nil {
return nil, errors.Wrap(err, "new request")
}
var q = url.Values{}
q.Add("apikey", api)
q.Add("t", "search")
q.Add("q", keyWord)
req.URL.RawQuery = q.Encode()
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, errors.Wrap(err, "do http")
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "read http body")
}
var res Response
err = xml.Unmarshal(data, &res)
if err != nil {
return nil, errors.Wrap(err, "json unmarshal")
}
return res.ToResults(), nil
}
type Result struct {
Name string
Magnet string
Size int
Seeders int
Peers int
Category int
Source string
}