mirror of
https://github.com/simon-ding/polaris.git
synced 2026-06-07 10:37:39 +08:00
basic find and download ability
This commit is contained in:
25
pkg/downloader/torrent.go
Normal file
25
pkg/downloader/torrent.go
Normal 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
206
pkg/jackett/category.go
Normal 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
168
pkg/jackett/jackett.go
Normal 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
134
pkg/torznab/torznab.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user