diff --git a/ent/history.go b/ent/history.go index acee6c0..4b45a6f 100644 --- a/ent/history.go +++ b/ent/history.go @@ -31,6 +31,8 @@ type History struct { Size int `json:"size,omitempty"` // DownloadClientID holds the value of the "download_client_id" field. DownloadClientID int `json:"download_client_id,omitempty"` + // IndexerID holds the value of the "indexer_id" field. + IndexerID int `json:"indexer_id,omitempty"` // Status holds the value of the "status" field. Status history.Status `json:"status,omitempty"` // Saved holds the value of the "saved" field. @@ -43,7 +45,7 @@ func (*History) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { - case history.FieldID, history.FieldMediaID, history.FieldEpisodeID, history.FieldSize, history.FieldDownloadClientID: + case history.FieldID, history.FieldMediaID, history.FieldEpisodeID, history.FieldSize, history.FieldDownloadClientID, history.FieldIndexerID: values[i] = new(sql.NullInt64) case history.FieldSourceTitle, history.FieldTargetDir, history.FieldStatus, history.FieldSaved: values[i] = new(sql.NullString) @@ -112,6 +114,12 @@ func (h *History) assignValues(columns []string, values []any) error { } else if value.Valid { h.DownloadClientID = int(value.Int64) } + case history.FieldIndexerID: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field indexer_id", values[i]) + } else if value.Valid { + h.IndexerID = int(value.Int64) + } case history.FieldStatus: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field status", values[i]) @@ -181,6 +189,9 @@ func (h *History) String() string { builder.WriteString("download_client_id=") builder.WriteString(fmt.Sprintf("%v", h.DownloadClientID)) builder.WriteString(", ") + builder.WriteString("indexer_id=") + builder.WriteString(fmt.Sprintf("%v", h.IndexerID)) + builder.WriteString(", ") builder.WriteString("status=") builder.WriteString(fmt.Sprintf("%v", h.Status)) builder.WriteString(", ") diff --git a/ent/history/history.go b/ent/history/history.go index ef5ff08..044c040 100644 --- a/ent/history/history.go +++ b/ent/history/history.go @@ -27,6 +27,8 @@ const ( FieldSize = "size" // FieldDownloadClientID holds the string denoting the download_client_id field in the database. FieldDownloadClientID = "download_client_id" + // FieldIndexerID holds the string denoting the indexer_id field in the database. + FieldIndexerID = "indexer_id" // FieldStatus holds the string denoting the status field in the database. FieldStatus = "status" // FieldSaved holds the string denoting the saved field in the database. @@ -45,6 +47,7 @@ var Columns = []string{ FieldTargetDir, FieldSize, FieldDownloadClientID, + FieldIndexerID, FieldStatus, FieldSaved, } @@ -132,6 +135,11 @@ func ByDownloadClientID(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldDownloadClientID, opts...).ToFunc() } +// ByIndexerID orders the results by the indexer_id field. +func ByIndexerID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldIndexerID, opts...).ToFunc() +} + // ByStatus orders the results by the status field. func ByStatus(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldStatus, opts...).ToFunc() diff --git a/ent/history/where.go b/ent/history/where.go index 8d561d8..27aa2e6 100644 --- a/ent/history/where.go +++ b/ent/history/where.go @@ -89,6 +89,11 @@ func DownloadClientID(v int) predicate.History { return predicate.History(sql.FieldEQ(FieldDownloadClientID, v)) } +// IndexerID applies equality check predicate on the "indexer_id" field. It's identical to IndexerIDEQ. +func IndexerID(v int) predicate.History { + return predicate.History(sql.FieldEQ(FieldIndexerID, v)) +} + // Saved applies equality check predicate on the "saved" field. It's identical to SavedEQ. func Saved(v string) predicate.History { return predicate.History(sql.FieldEQ(FieldSaved, v)) @@ -444,6 +449,56 @@ func DownloadClientIDNotNil() predicate.History { return predicate.History(sql.FieldNotNull(FieldDownloadClientID)) } +// IndexerIDEQ applies the EQ predicate on the "indexer_id" field. +func IndexerIDEQ(v int) predicate.History { + return predicate.History(sql.FieldEQ(FieldIndexerID, v)) +} + +// IndexerIDNEQ applies the NEQ predicate on the "indexer_id" field. +func IndexerIDNEQ(v int) predicate.History { + return predicate.History(sql.FieldNEQ(FieldIndexerID, v)) +} + +// IndexerIDIn applies the In predicate on the "indexer_id" field. +func IndexerIDIn(vs ...int) predicate.History { + return predicate.History(sql.FieldIn(FieldIndexerID, vs...)) +} + +// IndexerIDNotIn applies the NotIn predicate on the "indexer_id" field. +func IndexerIDNotIn(vs ...int) predicate.History { + return predicate.History(sql.FieldNotIn(FieldIndexerID, vs...)) +} + +// IndexerIDGT applies the GT predicate on the "indexer_id" field. +func IndexerIDGT(v int) predicate.History { + return predicate.History(sql.FieldGT(FieldIndexerID, v)) +} + +// IndexerIDGTE applies the GTE predicate on the "indexer_id" field. +func IndexerIDGTE(v int) predicate.History { + return predicate.History(sql.FieldGTE(FieldIndexerID, v)) +} + +// IndexerIDLT applies the LT predicate on the "indexer_id" field. +func IndexerIDLT(v int) predicate.History { + return predicate.History(sql.FieldLT(FieldIndexerID, v)) +} + +// IndexerIDLTE applies the LTE predicate on the "indexer_id" field. +func IndexerIDLTE(v int) predicate.History { + return predicate.History(sql.FieldLTE(FieldIndexerID, v)) +} + +// IndexerIDIsNil applies the IsNil predicate on the "indexer_id" field. +func IndexerIDIsNil() predicate.History { + return predicate.History(sql.FieldIsNull(FieldIndexerID)) +} + +// IndexerIDNotNil applies the NotNil predicate on the "indexer_id" field. +func IndexerIDNotNil() predicate.History { + return predicate.History(sql.FieldNotNull(FieldIndexerID)) +} + // StatusEQ applies the EQ predicate on the "status" field. func StatusEQ(v Status) predicate.History { return predicate.History(sql.FieldEQ(FieldStatus, v)) diff --git a/ent/history_create.go b/ent/history_create.go index ba2e520..cb8d11b 100644 --- a/ent/history_create.go +++ b/ent/history_create.go @@ -86,6 +86,20 @@ func (hc *HistoryCreate) SetNillableDownloadClientID(i *int) *HistoryCreate { return hc } +// SetIndexerID sets the "indexer_id" field. +func (hc *HistoryCreate) SetIndexerID(i int) *HistoryCreate { + hc.mutation.SetIndexerID(i) + return hc +} + +// SetNillableIndexerID sets the "indexer_id" field if the given value is not nil. +func (hc *HistoryCreate) SetNillableIndexerID(i *int) *HistoryCreate { + if i != nil { + hc.SetIndexerID(*i) + } + return hc +} + // SetStatus sets the "status" field. func (hc *HistoryCreate) SetStatus(h history.Status) *HistoryCreate { hc.mutation.SetStatus(h) @@ -226,6 +240,10 @@ func (hc *HistoryCreate) createSpec() (*History, *sqlgraph.CreateSpec) { _spec.SetField(history.FieldDownloadClientID, field.TypeInt, value) _node.DownloadClientID = value } + if value, ok := hc.mutation.IndexerID(); ok { + _spec.SetField(history.FieldIndexerID, field.TypeInt, value) + _node.IndexerID = value + } if value, ok := hc.mutation.Status(); ok { _spec.SetField(history.FieldStatus, field.TypeEnum, value) _node.Status = value diff --git a/ent/history_update.go b/ent/history_update.go index eb7df8a..b952606 100644 --- a/ent/history_update.go +++ b/ent/history_update.go @@ -166,6 +166,33 @@ func (hu *HistoryUpdate) ClearDownloadClientID() *HistoryUpdate { return hu } +// SetIndexerID sets the "indexer_id" field. +func (hu *HistoryUpdate) SetIndexerID(i int) *HistoryUpdate { + hu.mutation.ResetIndexerID() + hu.mutation.SetIndexerID(i) + return hu +} + +// SetNillableIndexerID sets the "indexer_id" field if the given value is not nil. +func (hu *HistoryUpdate) SetNillableIndexerID(i *int) *HistoryUpdate { + if i != nil { + hu.SetIndexerID(*i) + } + return hu +} + +// AddIndexerID adds i to the "indexer_id" field. +func (hu *HistoryUpdate) AddIndexerID(i int) *HistoryUpdate { + hu.mutation.AddIndexerID(i) + return hu +} + +// ClearIndexerID clears the value of the "indexer_id" field. +func (hu *HistoryUpdate) ClearIndexerID() *HistoryUpdate { + hu.mutation.ClearIndexerID() + return hu +} + // SetStatus sets the "status" field. func (hu *HistoryUpdate) SetStatus(h history.Status) *HistoryUpdate { hu.mutation.SetStatus(h) @@ -293,6 +320,15 @@ func (hu *HistoryUpdate) sqlSave(ctx context.Context) (n int, err error) { if hu.mutation.DownloadClientIDCleared() { _spec.ClearField(history.FieldDownloadClientID, field.TypeInt) } + if value, ok := hu.mutation.IndexerID(); ok { + _spec.SetField(history.FieldIndexerID, field.TypeInt, value) + } + if value, ok := hu.mutation.AddedIndexerID(); ok { + _spec.AddField(history.FieldIndexerID, field.TypeInt, value) + } + if hu.mutation.IndexerIDCleared() { + _spec.ClearField(history.FieldIndexerID, field.TypeInt) + } if value, ok := hu.mutation.Status(); ok { _spec.SetField(history.FieldStatus, field.TypeEnum, value) } @@ -460,6 +496,33 @@ func (huo *HistoryUpdateOne) ClearDownloadClientID() *HistoryUpdateOne { return huo } +// SetIndexerID sets the "indexer_id" field. +func (huo *HistoryUpdateOne) SetIndexerID(i int) *HistoryUpdateOne { + huo.mutation.ResetIndexerID() + huo.mutation.SetIndexerID(i) + return huo +} + +// SetNillableIndexerID sets the "indexer_id" field if the given value is not nil. +func (huo *HistoryUpdateOne) SetNillableIndexerID(i *int) *HistoryUpdateOne { + if i != nil { + huo.SetIndexerID(*i) + } + return huo +} + +// AddIndexerID adds i to the "indexer_id" field. +func (huo *HistoryUpdateOne) AddIndexerID(i int) *HistoryUpdateOne { + huo.mutation.AddIndexerID(i) + return huo +} + +// ClearIndexerID clears the value of the "indexer_id" field. +func (huo *HistoryUpdateOne) ClearIndexerID() *HistoryUpdateOne { + huo.mutation.ClearIndexerID() + return huo +} + // SetStatus sets the "status" field. func (huo *HistoryUpdateOne) SetStatus(h history.Status) *HistoryUpdateOne { huo.mutation.SetStatus(h) @@ -617,6 +680,15 @@ func (huo *HistoryUpdateOne) sqlSave(ctx context.Context) (_node *History, err e if huo.mutation.DownloadClientIDCleared() { _spec.ClearField(history.FieldDownloadClientID, field.TypeInt) } + if value, ok := huo.mutation.IndexerID(); ok { + _spec.SetField(history.FieldIndexerID, field.TypeInt, value) + } + if value, ok := huo.mutation.AddedIndexerID(); ok { + _spec.AddField(history.FieldIndexerID, field.TypeInt, value) + } + if huo.mutation.IndexerIDCleared() { + _spec.ClearField(history.FieldIndexerID, field.TypeInt) + } if value, ok := huo.mutation.Status(); ok { _spec.SetField(history.FieldStatus, field.TypeEnum, value) } diff --git a/ent/migrate/schema.go b/ent/migrate/schema.go index 4857628..ed20a19 100644 --- a/ent/migrate/schema.go +++ b/ent/migrate/schema.go @@ -64,6 +64,7 @@ var ( {Name: "target_dir", Type: field.TypeString}, {Name: "size", Type: field.TypeInt, Default: 0}, {Name: "download_client_id", Type: field.TypeInt, Nullable: true}, + {Name: "indexer_id", Type: field.TypeInt, Nullable: true}, {Name: "status", Type: field.TypeEnum, Enums: []string{"running", "success", "fail", "uploading"}}, {Name: "saved", Type: field.TypeString, Nullable: true}, } diff --git a/ent/mutation.go b/ent/mutation.go index e7f31c6..f06b5fc 100644 --- a/ent/mutation.go +++ b/ent/mutation.go @@ -1719,6 +1719,8 @@ type HistoryMutation struct { addsize *int download_client_id *int adddownload_client_id *int + indexer_id *int + addindexer_id *int status *history.Status saved *string clearedFields map[string]struct{} @@ -2185,6 +2187,76 @@ func (m *HistoryMutation) ResetDownloadClientID() { delete(m.clearedFields, history.FieldDownloadClientID) } +// SetIndexerID sets the "indexer_id" field. +func (m *HistoryMutation) SetIndexerID(i int) { + m.indexer_id = &i + m.addindexer_id = nil +} + +// IndexerID returns the value of the "indexer_id" field in the mutation. +func (m *HistoryMutation) IndexerID() (r int, exists bool) { + v := m.indexer_id + if v == nil { + return + } + return *v, true +} + +// OldIndexerID returns the old "indexer_id" field's value of the History entity. +// If the History object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HistoryMutation) OldIndexerID(ctx context.Context) (v int, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldIndexerID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldIndexerID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldIndexerID: %w", err) + } + return oldValue.IndexerID, nil +} + +// AddIndexerID adds i to the "indexer_id" field. +func (m *HistoryMutation) AddIndexerID(i int) { + if m.addindexer_id != nil { + *m.addindexer_id += i + } else { + m.addindexer_id = &i + } +} + +// AddedIndexerID returns the value that was added to the "indexer_id" field in this mutation. +func (m *HistoryMutation) AddedIndexerID() (r int, exists bool) { + v := m.addindexer_id + if v == nil { + return + } + return *v, true +} + +// ClearIndexerID clears the value of the "indexer_id" field. +func (m *HistoryMutation) ClearIndexerID() { + m.indexer_id = nil + m.addindexer_id = nil + m.clearedFields[history.FieldIndexerID] = struct{}{} +} + +// IndexerIDCleared returns if the "indexer_id" field was cleared in this mutation. +func (m *HistoryMutation) IndexerIDCleared() bool { + _, ok := m.clearedFields[history.FieldIndexerID] + return ok +} + +// ResetIndexerID resets all changes to the "indexer_id" field. +func (m *HistoryMutation) ResetIndexerID() { + m.indexer_id = nil + m.addindexer_id = nil + delete(m.clearedFields, history.FieldIndexerID) +} + // SetStatus sets the "status" field. func (m *HistoryMutation) SetStatus(h history.Status) { m.status = &h @@ -2304,7 +2376,7 @@ func (m *HistoryMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *HistoryMutation) Fields() []string { - fields := make([]string, 0, 9) + fields := make([]string, 0, 10) if m.media_id != nil { fields = append(fields, history.FieldMediaID) } @@ -2326,6 +2398,9 @@ func (m *HistoryMutation) Fields() []string { if m.download_client_id != nil { fields = append(fields, history.FieldDownloadClientID) } + if m.indexer_id != nil { + fields = append(fields, history.FieldIndexerID) + } if m.status != nil { fields = append(fields, history.FieldStatus) } @@ -2354,6 +2429,8 @@ func (m *HistoryMutation) Field(name string) (ent.Value, bool) { return m.Size() case history.FieldDownloadClientID: return m.DownloadClientID() + case history.FieldIndexerID: + return m.IndexerID() case history.FieldStatus: return m.Status() case history.FieldSaved: @@ -2381,6 +2458,8 @@ func (m *HistoryMutation) OldField(ctx context.Context, name string) (ent.Value, return m.OldSize(ctx) case history.FieldDownloadClientID: return m.OldDownloadClientID(ctx) + case history.FieldIndexerID: + return m.OldIndexerID(ctx) case history.FieldStatus: return m.OldStatus(ctx) case history.FieldSaved: @@ -2443,6 +2522,13 @@ func (m *HistoryMutation) SetField(name string, value ent.Value) error { } m.SetDownloadClientID(v) return nil + case history.FieldIndexerID: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetIndexerID(v) + return nil case history.FieldStatus: v, ok := value.(history.Status) if !ok { @@ -2477,6 +2563,9 @@ func (m *HistoryMutation) AddedFields() []string { if m.adddownload_client_id != nil { fields = append(fields, history.FieldDownloadClientID) } + if m.addindexer_id != nil { + fields = append(fields, history.FieldIndexerID) + } return fields } @@ -2493,6 +2582,8 @@ func (m *HistoryMutation) AddedField(name string) (ent.Value, bool) { return m.AddedSize() case history.FieldDownloadClientID: return m.AddedDownloadClientID() + case history.FieldIndexerID: + return m.AddedIndexerID() } return nil, false } @@ -2530,6 +2621,13 @@ func (m *HistoryMutation) AddField(name string, value ent.Value) error { } m.AddDownloadClientID(v) return nil + case history.FieldIndexerID: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddIndexerID(v) + return nil } return fmt.Errorf("unknown History numeric field %s", name) } @@ -2544,6 +2642,9 @@ func (m *HistoryMutation) ClearedFields() []string { if m.FieldCleared(history.FieldDownloadClientID) { fields = append(fields, history.FieldDownloadClientID) } + if m.FieldCleared(history.FieldIndexerID) { + fields = append(fields, history.FieldIndexerID) + } if m.FieldCleared(history.FieldSaved) { fields = append(fields, history.FieldSaved) } @@ -2567,6 +2668,9 @@ func (m *HistoryMutation) ClearField(name string) error { case history.FieldDownloadClientID: m.ClearDownloadClientID() return nil + case history.FieldIndexerID: + m.ClearIndexerID() + return nil case history.FieldSaved: m.ClearSaved() return nil @@ -2599,6 +2703,9 @@ func (m *HistoryMutation) ResetField(name string) error { case history.FieldDownloadClientID: m.ResetDownloadClientID() return nil + case history.FieldIndexerID: + m.ResetIndexerID() + return nil case history.FieldStatus: m.ResetStatus() return nil diff --git a/ent/schema/history.go b/ent/schema/history.go index 846509e..cdaa9c9 100644 --- a/ent/schema/history.go +++ b/ent/schema/history.go @@ -20,6 +20,7 @@ func (History) Fields() []ent.Field { field.String("target_dir"), field.Int("size").Default(0), field.Int("download_client_id").Optional(), + field.Int("indexer_id").Optional(), field.Enum("status").Values("running", "success", "fail", "uploading"), field.String("saved").Optional(), } diff --git a/pkg/torznab/torznab.go b/pkg/torznab/torznab.go index 723a940..9f02f92 100644 --- a/pkg/torznab/torznab.go +++ b/pkg/torznab/torznab.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "net/url" + "polaris/db" "polaris/log" "strconv" "time" @@ -71,17 +72,22 @@ func (i *Item) GetAttr(key string) string { } return "" } -func (r *Response) ToResults() []Result { +func (r *Response) ToResults(indexer *db.TorznabInfo) []Result { var res []Result for _, item := range r.Channel.Item { r := Result{ - Name: item.Title, - Link: 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, + Name: item.Title, + Link: item.Link, + Size: mustAtoI(item.Size), + Seeders: mustAtoI(item.GetAttr("seeders")), + Peers: mustAtoI(item.GetAttr("peers")), + Category: mustAtoI(item.GetAttr("category")), + DownloadVolumeFactor: tryParseFloat(item.GetAttr("downloadvolumefactor")), + UploadVolumeFactor: tryParseFloat(item.GetAttr("uploadvolumefactor")), + Source: indexer.Name, + IndexerId: indexer.ID, + Priority: indexer.Priority, + IsPrivate: item.Type == "private", } res = append(res, r) } @@ -96,11 +102,21 @@ func mustAtoI(key string) int { } return i } -func Search(torznabUrl, api, keyWord string) ([]Result, error) { + +func tryParseFloat(s string) float32 { + r, err := strconv.ParseFloat(s, 32) + if err != nil { + log.Warnf("parse float error: %v", err) + return 0 + } + return float32(r) +} + +func Search(indexer *db.TorznabInfo, api, keyWord string) ([]Result, error) { ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second) defer cancel() - req, err := http.NewRequestWithContext(ctx, http.MethodGet, torznabUrl, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, indexer.URL, nil) if err != nil { return nil, errors.Wrap(err, "new request") } @@ -124,15 +140,20 @@ func Search(torznabUrl, api, keyWord string) ([]Result, error) { if err != nil { return nil, errors.Wrap(err, "json unmarshal") } - return res.ToResults(), nil + return res.ToResults(indexer), nil } type Result struct { - Name string - Link string - Size int - Seeders int - Peers int - Category int - Source string + Name string `json:"name"` + Link string `json:"link"` + Size int `json:"size"` + Seeders int `json:"seeders"` + Peers int `json:"peers"` + Category int `json:"category"` + Source string `json:"source"` + DownloadVolumeFactor float32 `json:"download_volume_factor"` + UploadVolumeFactor float32 `json:"upload_volume_factor"` + IndexerId int `json:"indexer_id"` + Priority int `json:"priority"` + IsPrivate bool `json:"is_private"` } diff --git a/server/core/torrent.go b/server/core/torrent.go index 4f6c111..a31288a 100644 --- a/server/core/torrent.go +++ b/server/core/torrent.go @@ -77,7 +77,7 @@ func SearchEpisode(db1 *db.Client, seriesId, seasonNum, episodeNum int, checkRes if len(filtered) == 0 { return nil, errors.New("no resource found") } - + filtered = dedup(filtered) return filtered, nil } @@ -117,6 +117,7 @@ func SearchMovie(db1 *db.Client, movieId int, checkResolution bool) ([]torznab.R if len(filtered) == 0 { return nil, errors.New("no resource found") } + filtered = dedup(filtered) return filtered, nil @@ -134,7 +135,7 @@ func searchWithTorznab(db *db.Client, q string) []torznab.Result { go func() { log.Debugf("search torznab %v with %v", tor.Name, q) defer wg.Done() - resp, err := torznab.Search(tor.URL, tor.ApiKey, q) + resp, err := torznab.Search(tor, tor.ApiKey, q) if err != nil { log.Errorf("search %s error: %v", tor.Name, err) return @@ -152,11 +153,54 @@ func searchWithTorznab(db *db.Client, q string) []torznab.Result { res = append(res, result...) } - sort.Slice(res, func(i, j int) bool { + //res = dedup(res) + + sort.SliceStable(res, func(i, j int) bool { //先按做种人数排序 var s1 = res[i] var s2 = res[j] return s1.Seeders > s2.Seeders }) + sort.SliceStable(res, func(i, j int) bool { //再按优先级排序,优先级高的种子排前面 + var s1 = res[i] + var s2 = res[j] + return s1.Priority > s2.Priority + }) + + //pt资源中,同一indexer内部,优先下载free的资源 + sort.SliceStable(res, func(i, j int) bool { + var s1 = res[i] + var s2 = res[j] + if s1.IndexerId == s2.IndexerId && s1.IsPrivate { + return s1.DownloadVolumeFactor < s2.DownloadVolumeFactor + } + return false + }) + + //同一indexer内部,如果下载消耗一样,则优先下载上传奖励较多的 + sort.SliceStable(res, func(i, j int) bool { + var s1 = res[i] + var s2 = res[j] + if s1.IndexerId == s2.IndexerId && s1.IsPrivate && s1.DownloadVolumeFactor == s2.DownloadVolumeFactor{ + return s1.UploadVolumeFactor > s2.UploadVolumeFactor + } + return false + }) + return res } + + +func dedup(list []torznab.Result) []torznab.Result { + var res = make([]torznab.Result, 0, len(list)) + seen := make(map[string]bool, 0) + for _, r := range list { + key := fmt.Sprintf("%s%s%d%d", r.Name, r.Source, r.Seeders,r.Peers) + if seen[key] { + continue + } + seen[key] = true + res = append(res, r) + } + return res +} \ No newline at end of file diff --git a/server/resources.go b/server/resources.go index 6607edb..38cd974 100644 --- a/server/resources.go +++ b/server/resources.go @@ -62,6 +62,7 @@ func (s *Server) downloadSeasonPackage(r1 torznab.Result, seriesId, seasonNum in Size: r1.Size, Saved: torrent.Save(), DownloadClientID: dlClient.ID, + IndexerID: r1.IndexerId, }) if err != nil { return nil, errors.Wrap(err, "save record") @@ -110,6 +111,7 @@ func (s *Server) downloadEpisodeTorrent(r1 torznab.Result, seriesId, seasonNum, Size: r1.Size, Saved: torrent.Save(), DownloadClientID: dlc.ID, + IndexerID: r1.IndexerId, }) if err != nil { return nil, errors.Wrap(err, "save record") @@ -165,7 +167,7 @@ func (s *Server) SearchAvailableTorrents(c *gin.Context) (interface{}, error) { res, err = core.SearchEpisode(s.db, in.ID, in.Season, in.Episode, false) if err != nil { if err.Error() == "no resource found" { - return []TorznabSearchResult{}, nil + return gin.H{}, nil } return nil, errors.Wrap(err, "search episode") } @@ -176,22 +178,12 @@ func (s *Server) SearchAvailableTorrents(c *gin.Context) (interface{}, error) { res, err = core.SearchMovie(s.db, in.ID, false) if err != nil { if err.Error() == "no resource found" { - return []TorznabSearchResult{}, nil + return gin.H{}, nil } return nil, err } } - var searchResults []TorznabSearchResult - for _, r := range res { - searchResults = append(searchResults, TorznabSearchResult{ - Name: r.Name, - Size: r.Size, - Seeders: r.Seeders, - Peers: r.Peers, - Link: r.Link, - }) - } - return searchResults, nil + return res, nil } func (s *Server) SearchTvAndDownload(c *gin.Context) (interface{}, error) { @@ -223,19 +215,11 @@ func (s *Server) SearchTvAndDownload(c *gin.Context) (interface{}, error) { }, nil } -type TorznabSearchResult struct { - Name string `json:"name"` - Size int `json:"size"` - Link string `json:"link"` - Seeders int `json:"seeders"` - Peers int `json:"peers"` - Source string `json:"source"` -} type downloadTorrentIn struct { MediaID int `json:"id" binding:"required"` Season int `json:"season"` Episode int `json:"episode"` - TorznabSearchResult + torznab.Result } func (s *Server) DownloadTorrent(c *gin.Context) (interface{}, error) { @@ -263,7 +247,7 @@ func (s *Server) DownloadTorrent(c *gin.Context) (interface{}, error) { if name == "" { name = fmt.Sprintf("%v S%02dE%02d", m.OriginalName, in.Season, in.Episode) } - res := torznab.Result{Name: name, Link: in.Link, Size: in.Size} + res := torznab.Result{Name: name, Link: in.Link, Size: in.Size, IndexerId: in.IndexerId} return s.downloadEpisodeTorrent(res, in.MediaID, in.Season, in.Episode) } else { //movie @@ -292,6 +276,7 @@ func (s *Server) DownloadTorrent(c *gin.Context) (interface{}, error) { Size: in.Size, Saved: torrent.Save(), DownloadClientID: dlc.ID, + IndexerID: in.IndexerId, }) if err != nil { log.Errorf("save history error: %v", err) diff --git a/ui/lib/providers/series_details.dart b/ui/lib/providers/series_details.dart index 3f13f10..41acac7 100644 --- a/ui/lib/providers/series_details.dart +++ b/ui/lib/providers/series_details.dart @@ -98,7 +98,7 @@ class SeriesDetails { mediaType = json["media_type"]; storage = Storage.fromJson(json["storage"]); targetDir = json["target_dir"]; - downloadHistoryEpisodes = json["download_history_episodes"]??false; + downloadHistoryEpisodes = json["download_history_episodes"] ?? false; if (json['episodes'] != null) { episodes = []; json['episodes'].forEach((v) { @@ -195,13 +195,27 @@ class MediaTorrentResource extends AutoDisposeFamilyAsyncNotifier< } class TorrentResource { - TorrentResource({this.name, this.size, this.seeders, this.peers, this.link}); + TorrentResource( + {this.name, + this.size, + this.seeders, + this.peers, + this.link, + this.source, + this.indexerId, + this.downloadFactor, + this.uploadFactor, this.isPrivate}); String? name; int? size; int? seeders; int? peers; String? link; + String? source; + int? indexerId; + double? downloadFactor; + double? uploadFactor; + bool? isPrivate; factory TorrentResource.fromJson(Map json) { return TorrentResource( @@ -209,13 +223,20 @@ class TorrentResource { size: json["size"], seeders: json["seeders"], peers: json["peers"], - link: json["link"]); + link: json["link"], + source: json["source"], + indexerId: json["indexer_id"], + isPrivate: json["is_private"]??false, + downloadFactor: json["download_volume_factor"], + uploadFactor: json["upload_volume_factor"]); } Map toJson() { final Map data = {}; data['name'] = name; data['size'] = size; data["link"] = link; + data["indexer_id"] = indexerId; + data["source"] = source; return data; } } diff --git a/ui/lib/tv_details.dart b/ui/lib/tv_details.dart index 91f87b5..4ce8090 100644 --- a/ui/lib/tv_details.dart +++ b/ui/lib/tv_details.dart @@ -71,7 +71,8 @@ class _TvDetailsPageState extends ConsumerState { .read(mediaDetailsProvider(widget.seriesId) .notifier) .searchAndDownload(widget.seriesId, - ep.seasonNumber!, ep.episodeNumber!).then((v) => showSnakeBar("开始下载: $v")); + ep.seasonNumber!, ep.episodeNumber!) + .then((v) => showSnakeBar("开始下载: $v")); showLoadingWithFuture(f); }, icon: const Icon(Icons.download)), @@ -118,7 +119,8 @@ class _TvDetailsPageState extends ConsumerState { final f = ref .read(mediaDetailsProvider(widget.seriesId) .notifier) - .searchAndDownload(widget.seriesId, k, 0).then((v) => showSnakeBar("开始下载: $v")); + .searchAndDownload(widget.seriesId, k, 0) + .then((v) => showSnakeBar("开始下载: $v")); showLoadingWithFuture(f); }, icon: const Icon(Icons.download)), @@ -165,47 +167,71 @@ class _TvDetailsPageState extends ConsumerState { //title: Text("资源"), content: SelectionArea( child: SizedBox( - width: MediaQuery.of(context).size.width*0.7, - height: MediaQuery.of(context).size.height*0.6, + width: MediaQuery.of(context).size.width * 0.7, + height: MediaQuery.of(context).size.height * 0.6, child: torrents.when( data: (v) { + bool hasPrivate = false; + for (final item in v) { + if (item.isPrivate == true) { + hasPrivate = true; + } + } + final columns = [ + const DataColumn(label: Text("名称")), + const DataColumn(label: Text("大小")), + const DataColumn(label: Text("S/P")), + const DataColumn(label: Text("来源")), + ]; + if (hasPrivate) { + columns.add(const DataColumn(label: Text("消耗"))); + } + columns.add(const DataColumn(label: Text("下载"))); + return SingleChildScrollView( child: DataTable( - dataTextStyle: - const TextStyle(fontSize: 12), - columns: const [ - DataColumn(label: Text("名称")), - DataColumn(label: Text("大小")), - DataColumn(label: Text("seeders")), - DataColumn(label: Text("peers")), - DataColumn(label: Text("操作")) - ], + dataTextStyle: const TextStyle(fontSize: 12), + columns: columns, rows: List.generate(v.length, (i) { final torrent = v[i]; - return DataRow(cells: [ + final rows = [ DataCell(Text("${torrent.name}")), DataCell(Text( "${torrent.size?.readableFileSize()}")), - DataCell(Text("${torrent.seeders}")), - DataCell(Text("${torrent.peers}")), - DataCell(IconButton( - icon: const Icon(Icons.download), - onPressed: () async { - var f = ref - .read(mediaTorrentsDataProvider(( - mediaId: id, - seasonNumber: season, - episodeNumber: episode - )).notifier) - .download(torrent).then((v) => showSnakeBar("开始下载:${torrent.name}")); - showLoadingWithFuture(f); - }, - )) - ]); + DataCell(Text( + "${torrent.seeders}/${torrent.peers}")), + DataCell(Text(torrent.source ?? "-")), + ]; + if (hasPrivate) { + rows.add(DataCell(Text(torrent.isPrivate == true + ? "${torrent.downloadFactor}dl/${torrent.uploadFactor}up" + : "-"))); + } + + rows.add(DataCell(IconButton( + icon: const Icon(Icons.download), + onPressed: () async { + var f = ref + .read(mediaTorrentsDataProvider(( + mediaId: id, + seasonNumber: season, + episodeNumber: episode + )).notifier) + .download(torrent) + .then((v) => + showSnakeBar("开始下载:${torrent.name}")); + showLoadingWithFuture(f); + }, + ))); + return DataRow(cells: rows); }))); }, error: (err, trace) { - return Text("$err"); + return "$err".contains("no resource found") + ? const Center( + child: Text("没有资源"), + ) + : Text("$err"); }, loading: () => const MyProgressIndicator()), ),