diff --git a/db/const.go b/db/const.go index 3104153..5184054 100644 --- a/db/const.go +++ b/db/const.go @@ -17,6 +17,7 @@ const ( SetttingSizeLimiter = "size_limiter" SettingTvNamingFormat = "tv_naming_format" SettingMovieNamingFormat = "movie_naming_format" + SettingProwlarrInfo = "prowlarr_info" ) const ( @@ -60,3 +61,8 @@ type Limiter struct { Max int `json:"max"` Min int `json:"min"` } + +type ProwlarrSetting struct { + ApiKey string `json:"api_key"` + URL string `json:"url"` +} diff --git a/db/db.go b/db/db.go index b1077ef..edd1eec 100644 --- a/db/db.go +++ b/db/db.go @@ -660,3 +660,24 @@ func (c *Client) CleanAllDanglingEpisodes() error { func (c *Client) AddBlacklistItem(item *ent.Blacklist) error { return c.ent.Blacklist.Create().SetType(item.Type).SetValue(item.Value).SetNotes(item.Notes).Exec(context.Background()) } + + +func (c *Client) GetProwlarrSetting() (*ProwlarrSetting, error) { + s := c.GetSetting(SettingProwlarrInfo) + if s == "" { + return nil, errors.New("prowlarr setting not set") + } + var se ProwlarrSetting + if err := json.Unmarshal([]byte(s), &se); err != nil { + return nil, err + } + return &se, nil +} + +func (c *Client) SaveProwlarrSetting(se *ProwlarrSetting) error { + data, err := json.Marshal(se) + if err != nil { + return err + } + return c.SetSetting(SettingProwlarrInfo, string(data)) +} \ No newline at end of file diff --git a/go.mod b/go.mod index 7c8d605..0e71546 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/ncruces/go-sqlite3 v0.18.4 github.com/nikoksr/notify v1.0.0 github.com/stretchr/testify v1.9.0 + golift.io/starr v1.0.0 ) require ( diff --git a/go.sum b/go.sum index 4741904..a441397 100644 --- a/go.sum +++ b/go.sum @@ -216,8 +216,6 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -251,8 +249,6 @@ github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= github.com/nikoksr/notify v1.0.0 h1:qe9/6FRsWdxBgQgWcpvQ0sv8LRGJZDpRB4TkL2uNdO8= github.com/nikoksr/notify v1.0.0/go.mod h1:hPaaDt30d6LAA7/5nb0e48Bp/MctDfycCSs8VEgN29I= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -310,8 +306,6 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= @@ -453,10 +447,10 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golift.io/starr v1.0.0 h1:IDSaSL+ZYxdLT/Lg//dg/iwZ39LHO3D5CmbLCOgSXbI= +golift.io/starr v1.0.0/go.mod h1:xnUwp4vK62bDvozW9QHUYc08m6kjwaZnGw3Db65fQHw= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/pkg/importlist/douban/douban.go b/pkg/importlist/douban/douban.go index b1c378c..94f661d 100644 --- a/pkg/importlist/douban/douban.go +++ b/pkg/importlist/douban/douban.go @@ -37,6 +37,7 @@ func ParseDoulist(doulistUrl string) (*importlist.Response, error) { if err != nil { return nil, err } + var items []importlist.Item doc.Find("div[class=doulist-item]").Each(func(i int, selection *goquery.Selection) { titleDiv := selection.Find("div[class=title]") link := titleDiv.Find("div>a") @@ -64,18 +65,26 @@ func ParseDoulist(doulistUrl string) (*importlist.Response, error) { } } } + _, err := parseDetailPage(strings.TrimSpace(href)) + if err != nil { + log.Errorf("get detail page: %v", err) + return + } item := importlist.Item{ Title: strings.TrimSpace(link.Text()), Year: year, } + items = append(items, item) _ = item - println(link.Text(), href) + //println(link.Text(), href) }) - return nil, nil + + return &importlist.Response{Items: items}, nil } func parseDetailPage(url string) (string, error) { + println(url) req, err := http.NewRequest("GET", url, nil) if err != nil { return "", err @@ -95,6 +104,14 @@ func parseDetailPage(url string) (string, error) { if err != nil { return "", err } + + doc.Find("div[class='subject clearfix']").Each(func(i int, se *goquery.Selection) { + println(se.Text()) + se.Children().Get(1) + imdb := se.Find("div[class='info']").First().Children().Last() + println(imdb.Text()) + }) + _ = doc return "", nil } diff --git a/pkg/importlist/douban/douban_test.go b/pkg/importlist/douban/douban_test.go index d469e62..5b71064 100644 --- a/pkg/importlist/douban/douban_test.go +++ b/pkg/importlist/douban/douban_test.go @@ -6,6 +6,6 @@ import ( ) func TestParseDoulist(t *testing.T) { - r, err := ParseDoulist("https://www.douban.com/doulist/166422/") + r, err := ParseDoulist("https://www.douban.com/doulist/81580/") log.Info(r, err) } diff --git a/pkg/prowlarr/prowlarr.go b/pkg/prowlarr/prowlarr.go new file mode 100644 index 0000000..0429d28 --- /dev/null +++ b/pkg/prowlarr/prowlarr.go @@ -0,0 +1,65 @@ +package prowlarr + +import ( + "encoding/json" + "fmt" + "polaris/db" + "polaris/ent" + "strings" + "time" + + "golift.io/starr" + "golift.io/starr/prowlarr" +) + +type Client struct { + p *prowlarr.Prowlarr + apiKey string + url string +} + +func New(apiKey, url string) *Client { + c := starr.New(apiKey, url, 10*time.Second) + p := prowlarr.New(c) + return &Client{p: p, apiKey: apiKey, url: url} +} + +func (c *Client) GetIndexers() ([]*db.TorznabInfo, error) { + ins, err := c.p.GetIndexers() + if err != nil { + return nil, err + } + var indexers []*db.TorznabInfo + for _, in := range ins { + if !in.Enable { + continue + } + seedRatio := 0.0 + for _, f := range in.Fields { + if f.Name == "torrentBaseSettings.seedRatio" && f.Value != nil { + if r, ok := f.Value.(float64); ok { + seedRatio = r + } + } + } + setting := db.TorznabSetting{ + URL: fmt.Sprintf("%s/%d/api", strings.TrimSuffix(c.url, "/"), in.ID), + ApiKey: c.apiKey, + } + data, _ := json.Marshal(&setting) + + entIndexer := ent.Indexers{ + Name: in.Name, + Implementation: "torznab", + Priority: int(in.Priority), + SeedRatio: float32(seedRatio), + Settings: string(data), + } + + indexers = append(indexers, &db.TorznabInfo{ + Indexers: &entIndexer, + TorznabSetting: setting, + }) + } + return indexers, nil +} diff --git a/pkg/prowlarr/prowlarr_test.go b/pkg/prowlarr/prowlarr_test.go new file mode 100644 index 0000000..be55052 --- /dev/null +++ b/pkg/prowlarr/prowlarr_test.go @@ -0,0 +1,13 @@ +package prowlarr + +import ( + "polaris/log" + "testing" +) + +func Test111(t *testing.T) { + c := New("", "http://10.0.0.8:9696/") + apis , err := c.GetIndexers() + log.Infof("errors: %v", err) + log.Infof("indexers: %+v", apis[0]) +} diff --git a/server/core/torrent.go b/server/core/torrent.go index 55ffc12..c5144f5 100644 --- a/server/core/torrent.go +++ b/server/core/torrent.go @@ -6,6 +6,7 @@ import ( "polaris/ent/media" "polaris/log" "polaris/pkg/metadata" + "polaris/pkg/prowlarr" "polaris/pkg/torznab" "slices" "sort" @@ -136,7 +137,7 @@ func torrentSizeOk(detail *db.MediaDetails, torrentSize int, param *SearchParam) } } } - return torrentSize > defaultMinSize * multiplier + return torrentSize > defaultMinSize*multiplier } func seasonEpisodeCount(detail *db.MediaDetails, seasonNum int) int { @@ -230,6 +231,17 @@ func searchWithTorznab(db *db.Client, queries ...string) []torznab.Result { var res []torznab.Result allTorznab := db.GetAllTorznabInfo() + + p, err := db.GetProwlarrSetting() + if err == nil { //prowlarr exists + c := prowlarr.New(p.ApiKey, p.URL) + all, err := c.GetIndexers() + if err != nil { + log.Warnf("get prowlarr all indexer error: %v", err) + } else { + allTorznab = append(allTorznab, all...) + } + } resChan := make(chan []torznab.Result) var wg sync.WaitGroup diff --git a/server/server.go b/server/server.go index 912edd6..dd5cbbb 100644 --- a/server/server.go +++ b/server/server.go @@ -70,6 +70,8 @@ func (s *Server) Serve() error { setting.POST("/parse/movie", HttpHandler(s.ParseMovie)) setting.POST("/monitoring", HttpHandler(s.ChangeEpisodeMonitoring)) setting.POST("/cron/trigger", HttpHandler(s.TriggerCronJob)) + setting.GET("/prowlarr", HttpHandler(s.GetProwlarrSetting)) + setting.POST("/prowlarr", HttpHandler(s.SaveProwlarrSetting)) } activity := api.Group("/activity") { diff --git a/server/setting.go b/server/setting.go index 081ee05..e3ca482 100644 --- a/server/setting.go +++ b/server/setting.go @@ -8,6 +8,7 @@ import ( "polaris/ent" "polaris/ent/downloadclients" "polaris/log" + "polaris/pkg/prowlarr" "polaris/pkg/qbittorrent" "polaris/pkg/torznab" "polaris/pkg/transmission" @@ -303,3 +304,26 @@ func (s *Server) TriggerCronJob(c *gin.Context) (interface{}, error) { } return "success", nil } + +func (s *Server) GetProwlarrSetting(c *gin.Context) (interface{}, error) { + se, err :=s.db.GetProwlarrSetting() + if err != nil { + return &db.ProwlarrSetting{}, nil + } + return se, nil +} +func (s *Server) SaveProwlarrSetting(c *gin.Context) (interface{}, error) { + var in db.ProwlarrSetting + if err := c.ShouldBindJSON(&in); err != nil { + return nil, err + } + client := prowlarr.New(in.ApiKey, in.URL) + if _, err := client.GetIndexers(); err != nil { + return nil, errors.Wrap(err, "connect to prowlarr error") + } + err := s.db.SaveProwlarrSetting(&in) + if err != nil { + return nil, err + } + return "success", nil +} diff --git a/ui/lib/providers/APIs.dart b/ui/lib/providers/APIs.dart index 411acdc..8d18ae7 100644 --- a/ui/lib/providers/APIs.dart +++ b/ui/lib/providers/APIs.dart @@ -40,6 +40,7 @@ class APIs { static final addImportlistUrl = "$_baseUrl/api/v1/importlist/add"; static final deleteImportlistUrl = "$_baseUrl/api/v1/importlist/delete"; static final getAllImportlists = "$_baseUrl/api/v1/importlist/"; + static final prowlarrUrl = "$_baseUrl/api/v1/setting/prowlarr"; static final notifierAllUrl = "$_baseUrl/api/v1/notifier/all"; static final notifierDeleteUrl = "$_baseUrl/api/v1/notifier/id/"; diff --git a/ui/lib/providers/settings.dart b/ui/lib/providers/settings.dart index 5507eda..0592762 100644 --- a/ui/lib/providers/settings.dart +++ b/ui/lib/providers/settings.dart @@ -25,6 +25,10 @@ var importlistProvider = AsyncNotifierProvider.autoDispose>( ImportListData.new); +var prowlarrSettingDataProvider = + AsyncNotifierProvider.autoDispose( + ProwlarrSettingData.new); + class EditSettingData extends AutoDisposeAsyncNotifier { @override FutureOr build() async { @@ -503,3 +507,38 @@ class ImportListData extends AutoDisposeAsyncNotifier> { ref.invalidateSelf(); } } + +class ProwlarrSetting { + final String apiKey; + final String url; + ProwlarrSetting({required this.apiKey, required this.url}); + factory ProwlarrSetting.fromJson(Map json) { + return ProwlarrSetting(apiKey: json["api_key"], url: json["url"]); + } + + Map tojson() => {"api_key": apiKey, "url": url}; +} + +class ProwlarrSettingData extends AutoDisposeAsyncNotifier { + @override + FutureOr build() async { + final dio = APIs.getDio(); + var resp = await dio.get(APIs.prowlarrUrl); + var sp = ServerResponse.fromJson(resp.data); + if (sp.code != 0) { + throw sp.message; + } + var se = ProwlarrSetting.fromJson(sp.data); + return se; + } + + Future save(ProwlarrSetting ps) async { + final dio = APIs.getDio(); + var resp = await dio.post(APIs.prowlarrUrl, data: ps.tojson()); + var sp = ServerResponse.fromJson(resp.data); + if (sp.code != 0) { + throw sp.message; + } + ref.invalidateSelf(); + } +} diff --git a/ui/lib/settings/prowlarr.dart b/ui/lib/settings/prowlarr.dart new file mode 100644 index 0000000..917e6f7 --- /dev/null +++ b/ui/lib/settings/prowlarr.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:ui/providers/settings.dart'; +import 'package:ui/widgets/progress_indicator.dart'; +import 'package:ui/widgets/utils.dart'; +import 'package:ui/widgets/widgets.dart'; + +class ProwlarrSettingPage extends ConsumerStatefulWidget { + const ProwlarrSettingPage({super.key}); + @override + ConsumerState createState() { + return ProwlarrSettingState(); + } +} + +class ProwlarrSettingState extends ConsumerState { + final _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + var ps = ref.watch(prowlarrSettingDataProvider); + return ps.when( + data: (v) => FormBuilder( + key: _formKey, //设置globalKey,用于后面获取FormState + autovalidateMode: AutovalidateMode.onUserInteraction, + initialValue: {"api_key": v.apiKey, "url": v.url}, + child: Column( + children: [ + FormBuilderTextField( + name: "url", + decoration: const InputDecoration( + labelText: "Prowlarr URL", + icon: Icon(Icons.web), + hintText: "http://10.0.0.8:9696"), + validator: FormBuilderValidators.required(), + ), + FormBuilderTextField( + name: "api_key", + decoration: const InputDecoration( + labelText: "API Key", + icon: Icon(Icons.web), + helperText: "Prowlarr 设置 -> 通用设置 -> 接口密钥"), + validator: FormBuilderValidators.required(), + ), + Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: ElevatedButton( + onPressed: () { + if (_formKey.currentState!.saveAndValidate()) { + var values = _formKey.currentState!.value; + var f = ref + .read(prowlarrSettingDataProvider.notifier) + .save(ProwlarrSetting( + apiKey: values["api_key"], + url: values["url"])) + .then((v) => showSnakeBar("更新成功")); + showLoadingWithFuture(f); + } + }, + child: const Padding(padding: EdgeInsets.all(10), child: Text("保存"),)), + ), + ) + ], + ), + ), + error: (err, trace) => Text("$err"), + loading: () => const MyProgressIndicator()); + } +} diff --git a/ui/lib/settings/settings.dart b/ui/lib/settings/settings.dart index 94172c1..8da8662 100644 --- a/ui/lib/settings/settings.dart +++ b/ui/lib/settings/settings.dart @@ -6,6 +6,7 @@ import 'package:ui/settings/general.dart'; import 'package:ui/settings/importlist.dart'; import 'package:ui/settings/indexer.dart'; import 'package:ui/settings/notifier.dart'; +import 'package:ui/settings/prowlarr.dart'; import 'package:ui/settings/storage.dart'; class SystemSettingsPage extends ConsumerStatefulWidget { @@ -25,6 +26,7 @@ class _SystemSettingsPageState extends ConsumerState { children: [ getExpansionTile("常规", const GeneralSettings()), getExpansionTile("索引器", const IndexerSettings()), + getExpansionTile("Prowlarr 设置", const ProwlarrSettingPage()), getExpansionTile("下载器", const DownloaderSettings()), getExpansionTile("存储", const StorageSettings()), getExpansionTile("通知客户端", const NotifierSettings()),