diff --git a/cmd/main.go b/cmd/main.go index 5de9047..8be993c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,7 +6,6 @@ import ( "polaris/server" ) - func main() { log.Infof("------------------- Starting Polaris ---------------------") dbClient, err := db.Open() diff --git a/db/db.go b/db/db.go index ccbc716..eabe43d 100644 --- a/db/db.go +++ b/db/db.go @@ -201,6 +201,10 @@ func (c *Client) GetMediaDetails(id int) *MediaDetails { return md } +func (c *Client) GetMedia(id int) (*ent.Media, error) { + return c.ent.Media.Query().Where(media.ID(id)).First(context.TODO()) +} + func (c *Client) DeleteMedia(id int) error { _, err := c.ent.Episode.Delete().Where(episode.MediaID(id)).Exec(context.TODO()) if err != nil { @@ -506,13 +510,13 @@ func (c *Client) GetDownloadDir() string { return r.Value } -func (c *Client) UpdateEpisodeFile(mediaID int, seasonNum, episodeNum int, file string) error { +func (c *Client) UpdateEpisodeStatus(mediaID int, seasonNum, episodeNum int) error { ep, err := c.ent.Episode.Query().Where(episode.MediaID(mediaID)).Where(episode.EpisodeNumber(episodeNum)). Where(episode.SeasonNumber(seasonNum)).First(context.TODO()) if err != nil { return errors.Wrap(err, "finding episode") } - return ep.Update().SetFileInStorage(file).SetStatus(episode.StatusDownloaded).Exec(context.TODO()) + return ep.Update().SetStatus(episode.StatusDownloaded).Exec(context.TODO()) } func (c *Client) SetEpisodeStatus(id int, status episode.Status) error { diff --git a/ent/episode.go b/ent/episode.go index 2227c16..8a662ad 100644 --- a/ent/episode.go +++ b/ent/episode.go @@ -31,8 +31,6 @@ type Episode struct { AirDate string `json:"air_date,omitempty"` // Status holds the value of the "status" field. Status episode.Status `json:"status,omitempty"` - // FileInStorage holds the value of the "file_in_storage" field. - FileInStorage string `json:"file_in_storage,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the EpisodeQuery when eager-loading is set. Edges EpisodeEdges `json:"edges"` @@ -66,7 +64,7 @@ func (*Episode) scanValues(columns []string) ([]any, error) { switch columns[i] { case episode.FieldID, episode.FieldMediaID, episode.FieldSeasonNumber, episode.FieldEpisodeNumber: values[i] = new(sql.NullInt64) - case episode.FieldTitle, episode.FieldOverview, episode.FieldAirDate, episode.FieldStatus, episode.FieldFileInStorage: + case episode.FieldTitle, episode.FieldOverview, episode.FieldAirDate, episode.FieldStatus: values[i] = new(sql.NullString) default: values[i] = new(sql.UnknownType) @@ -131,12 +129,6 @@ func (e *Episode) assignValues(columns []string, values []any) error { } else if value.Valid { e.Status = episode.Status(value.String) } - case episode.FieldFileInStorage: - if value, ok := values[i].(*sql.NullString); !ok { - return fmt.Errorf("unexpected type %T for field file_in_storage", values[i]) - } else if value.Valid { - e.FileInStorage = value.String - } default: e.selectValues.Set(columns[i], values[i]) } @@ -198,9 +190,6 @@ func (e *Episode) String() string { builder.WriteString(", ") builder.WriteString("status=") builder.WriteString(fmt.Sprintf("%v", e.Status)) - builder.WriteString(", ") - builder.WriteString("file_in_storage=") - builder.WriteString(e.FileInStorage) builder.WriteByte(')') return builder.String() } diff --git a/ent/episode/episode.go b/ent/episode/episode.go index 1c720c9..f3d2c00 100644 --- a/ent/episode/episode.go +++ b/ent/episode/episode.go @@ -28,8 +28,6 @@ const ( FieldAirDate = "air_date" // FieldStatus holds the string denoting the status field in the database. FieldStatus = "status" - // FieldFileInStorage holds the string denoting the file_in_storage field in the database. - FieldFileInStorage = "file_in_storage" // EdgeMedia holds the string denoting the media edge name in mutations. EdgeMedia = "media" // Table holds the table name of the episode in the database. @@ -53,7 +51,6 @@ var Columns = []string{ FieldOverview, FieldAirDate, FieldStatus, - FieldFileInStorage, } // ValidColumn reports if the column name is valid (part of the table columns). @@ -136,11 +133,6 @@ func ByStatus(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldStatus, opts...).ToFunc() } -// ByFileInStorage orders the results by the file_in_storage field. -func ByFileInStorage(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldFileInStorage, opts...).ToFunc() -} - // ByMediaField orders the results by media field. func ByMediaField(field string, opts ...sql.OrderTermOption) OrderOption { return func(s *sql.Selector) { diff --git a/ent/episode/where.go b/ent/episode/where.go index 909de91..64fb4a8 100644 --- a/ent/episode/where.go +++ b/ent/episode/where.go @@ -84,11 +84,6 @@ func AirDate(v string) predicate.Episode { return predicate.Episode(sql.FieldEQ(FieldAirDate, v)) } -// FileInStorage applies equality check predicate on the "file_in_storage" field. It's identical to FileInStorageEQ. -func FileInStorage(v string) predicate.Episode { - return predicate.Episode(sql.FieldEQ(FieldFileInStorage, v)) -} - // MediaIDEQ applies the EQ predicate on the "media_id" field. func MediaIDEQ(v int) predicate.Episode { return predicate.Episode(sql.FieldEQ(FieldMediaID, v)) @@ -414,81 +409,6 @@ func StatusNotIn(vs ...Status) predicate.Episode { return predicate.Episode(sql.FieldNotIn(FieldStatus, vs...)) } -// FileInStorageEQ applies the EQ predicate on the "file_in_storage" field. -func FileInStorageEQ(v string) predicate.Episode { - return predicate.Episode(sql.FieldEQ(FieldFileInStorage, v)) -} - -// FileInStorageNEQ applies the NEQ predicate on the "file_in_storage" field. -func FileInStorageNEQ(v string) predicate.Episode { - return predicate.Episode(sql.FieldNEQ(FieldFileInStorage, v)) -} - -// FileInStorageIn applies the In predicate on the "file_in_storage" field. -func FileInStorageIn(vs ...string) predicate.Episode { - return predicate.Episode(sql.FieldIn(FieldFileInStorage, vs...)) -} - -// FileInStorageNotIn applies the NotIn predicate on the "file_in_storage" field. -func FileInStorageNotIn(vs ...string) predicate.Episode { - return predicate.Episode(sql.FieldNotIn(FieldFileInStorage, vs...)) -} - -// FileInStorageGT applies the GT predicate on the "file_in_storage" field. -func FileInStorageGT(v string) predicate.Episode { - return predicate.Episode(sql.FieldGT(FieldFileInStorage, v)) -} - -// FileInStorageGTE applies the GTE predicate on the "file_in_storage" field. -func FileInStorageGTE(v string) predicate.Episode { - return predicate.Episode(sql.FieldGTE(FieldFileInStorage, v)) -} - -// FileInStorageLT applies the LT predicate on the "file_in_storage" field. -func FileInStorageLT(v string) predicate.Episode { - return predicate.Episode(sql.FieldLT(FieldFileInStorage, v)) -} - -// FileInStorageLTE applies the LTE predicate on the "file_in_storage" field. -func FileInStorageLTE(v string) predicate.Episode { - return predicate.Episode(sql.FieldLTE(FieldFileInStorage, v)) -} - -// FileInStorageContains applies the Contains predicate on the "file_in_storage" field. -func FileInStorageContains(v string) predicate.Episode { - return predicate.Episode(sql.FieldContains(FieldFileInStorage, v)) -} - -// FileInStorageHasPrefix applies the HasPrefix predicate on the "file_in_storage" field. -func FileInStorageHasPrefix(v string) predicate.Episode { - return predicate.Episode(sql.FieldHasPrefix(FieldFileInStorage, v)) -} - -// FileInStorageHasSuffix applies the HasSuffix predicate on the "file_in_storage" field. -func FileInStorageHasSuffix(v string) predicate.Episode { - return predicate.Episode(sql.FieldHasSuffix(FieldFileInStorage, v)) -} - -// FileInStorageIsNil applies the IsNil predicate on the "file_in_storage" field. -func FileInStorageIsNil() predicate.Episode { - return predicate.Episode(sql.FieldIsNull(FieldFileInStorage)) -} - -// FileInStorageNotNil applies the NotNil predicate on the "file_in_storage" field. -func FileInStorageNotNil() predicate.Episode { - return predicate.Episode(sql.FieldNotNull(FieldFileInStorage)) -} - -// FileInStorageEqualFold applies the EqualFold predicate on the "file_in_storage" field. -func FileInStorageEqualFold(v string) predicate.Episode { - return predicate.Episode(sql.FieldEqualFold(FieldFileInStorage, v)) -} - -// FileInStorageContainsFold applies the ContainsFold predicate on the "file_in_storage" field. -func FileInStorageContainsFold(v string) predicate.Episode { - return predicate.Episode(sql.FieldContainsFold(FieldFileInStorage, v)) -} - // HasMedia applies the HasEdge predicate on the "media" edge. func HasMedia() predicate.Episode { return predicate.Episode(func(s *sql.Selector) { diff --git a/ent/episode_create.go b/ent/episode_create.go index 28a17bc..55187ac 100644 --- a/ent/episode_create.go +++ b/ent/episode_create.go @@ -78,20 +78,6 @@ func (ec *EpisodeCreate) SetNillableStatus(e *episode.Status) *EpisodeCreate { return ec } -// SetFileInStorage sets the "file_in_storage" field. -func (ec *EpisodeCreate) SetFileInStorage(s string) *EpisodeCreate { - ec.mutation.SetFileInStorage(s) - return ec -} - -// SetNillableFileInStorage sets the "file_in_storage" field if the given value is not nil. -func (ec *EpisodeCreate) SetNillableFileInStorage(s *string) *EpisodeCreate { - if s != nil { - ec.SetFileInStorage(*s) - } - return ec -} - // SetMedia sets the "media" edge to the Media entity. func (ec *EpisodeCreate) SetMedia(m *Media) *EpisodeCreate { return ec.SetMediaID(m.ID) @@ -213,10 +199,6 @@ func (ec *EpisodeCreate) createSpec() (*Episode, *sqlgraph.CreateSpec) { _spec.SetField(episode.FieldStatus, field.TypeEnum, value) _node.Status = value } - if value, ok := ec.mutation.FileInStorage(); ok { - _spec.SetField(episode.FieldFileInStorage, field.TypeString, value) - _node.FileInStorage = value - } if nodes := ec.mutation.MediaIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, diff --git a/ent/episode_update.go b/ent/episode_update.go index 9899c6c..f8a21af 100644 --- a/ent/episode_update.go +++ b/ent/episode_update.go @@ -146,26 +146,6 @@ func (eu *EpisodeUpdate) SetNillableStatus(e *episode.Status) *EpisodeUpdate { return eu } -// SetFileInStorage sets the "file_in_storage" field. -func (eu *EpisodeUpdate) SetFileInStorage(s string) *EpisodeUpdate { - eu.mutation.SetFileInStorage(s) - return eu -} - -// SetNillableFileInStorage sets the "file_in_storage" field if the given value is not nil. -func (eu *EpisodeUpdate) SetNillableFileInStorage(s *string) *EpisodeUpdate { - if s != nil { - eu.SetFileInStorage(*s) - } - return eu -} - -// ClearFileInStorage clears the value of the "file_in_storage" field. -func (eu *EpisodeUpdate) ClearFileInStorage() *EpisodeUpdate { - eu.mutation.ClearFileInStorage() - return eu -} - // SetMedia sets the "media" edge to the Media entity. func (eu *EpisodeUpdate) SetMedia(m *Media) *EpisodeUpdate { return eu.SetMediaID(m.ID) @@ -255,12 +235,6 @@ func (eu *EpisodeUpdate) sqlSave(ctx context.Context) (n int, err error) { if value, ok := eu.mutation.Status(); ok { _spec.SetField(episode.FieldStatus, field.TypeEnum, value) } - if value, ok := eu.mutation.FileInStorage(); ok { - _spec.SetField(episode.FieldFileInStorage, field.TypeString, value) - } - if eu.mutation.FileInStorageCleared() { - _spec.ClearField(episode.FieldFileInStorage, field.TypeString) - } if eu.mutation.MediaCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, @@ -428,26 +402,6 @@ func (euo *EpisodeUpdateOne) SetNillableStatus(e *episode.Status) *EpisodeUpdate return euo } -// SetFileInStorage sets the "file_in_storage" field. -func (euo *EpisodeUpdateOne) SetFileInStorage(s string) *EpisodeUpdateOne { - euo.mutation.SetFileInStorage(s) - return euo -} - -// SetNillableFileInStorage sets the "file_in_storage" field if the given value is not nil. -func (euo *EpisodeUpdateOne) SetNillableFileInStorage(s *string) *EpisodeUpdateOne { - if s != nil { - euo.SetFileInStorage(*s) - } - return euo -} - -// ClearFileInStorage clears the value of the "file_in_storage" field. -func (euo *EpisodeUpdateOne) ClearFileInStorage() *EpisodeUpdateOne { - euo.mutation.ClearFileInStorage() - return euo -} - // SetMedia sets the "media" edge to the Media entity. func (euo *EpisodeUpdateOne) SetMedia(m *Media) *EpisodeUpdateOne { return euo.SetMediaID(m.ID) @@ -567,12 +521,6 @@ func (euo *EpisodeUpdateOne) sqlSave(ctx context.Context) (_node *Episode, err e if value, ok := euo.mutation.Status(); ok { _spec.SetField(episode.FieldStatus, field.TypeEnum, value) } - if value, ok := euo.mutation.FileInStorage(); ok { - _spec.SetField(episode.FieldFileInStorage, field.TypeString, value) - } - if euo.mutation.FileInStorageCleared() { - _spec.ClearField(episode.FieldFileInStorage, field.TypeString) - } if euo.mutation.MediaCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, diff --git a/ent/migrate/schema.go b/ent/migrate/schema.go index 5e88cc3..635e552 100644 --- a/ent/migrate/schema.go +++ b/ent/migrate/schema.go @@ -38,7 +38,6 @@ var ( {Name: "overview", Type: field.TypeString}, {Name: "air_date", Type: field.TypeString}, {Name: "status", Type: field.TypeEnum, Enums: []string{"missing", "downloading", "downloaded"}, Default: "missing"}, - {Name: "file_in_storage", Type: field.TypeString, Nullable: true}, {Name: "media_id", Type: field.TypeInt, Nullable: true}, } // EpisodesTable holds the schema information for the "episodes" table. @@ -49,7 +48,7 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "episodes_media_episodes", - Columns: []*schema.Column{EpisodesColumns[8]}, + Columns: []*schema.Column{EpisodesColumns[7]}, RefColumns: []*schema.Column{MediaColumns[0]}, OnDelete: schema.SetNull, }, diff --git a/ent/mutation.go b/ent/mutation.go index fe85d72..47205fa 100644 --- a/ent/mutation.go +++ b/ent/mutation.go @@ -919,7 +919,6 @@ type EpisodeMutation struct { overview *string air_date *string status *episode.Status - file_in_storage *string clearedFields map[string]struct{} media *int clearedmedia bool @@ -1331,55 +1330,6 @@ func (m *EpisodeMutation) ResetStatus() { m.status = nil } -// SetFileInStorage sets the "file_in_storage" field. -func (m *EpisodeMutation) SetFileInStorage(s string) { - m.file_in_storage = &s -} - -// FileInStorage returns the value of the "file_in_storage" field in the mutation. -func (m *EpisodeMutation) FileInStorage() (r string, exists bool) { - v := m.file_in_storage - if v == nil { - return - } - return *v, true -} - -// OldFileInStorage returns the old "file_in_storage" field's value of the Episode entity. -// If the Episode 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 *EpisodeMutation) OldFileInStorage(ctx context.Context) (v string, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldFileInStorage is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldFileInStorage requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldFileInStorage: %w", err) - } - return oldValue.FileInStorage, nil -} - -// ClearFileInStorage clears the value of the "file_in_storage" field. -func (m *EpisodeMutation) ClearFileInStorage() { - m.file_in_storage = nil - m.clearedFields[episode.FieldFileInStorage] = struct{}{} -} - -// FileInStorageCleared returns if the "file_in_storage" field was cleared in this mutation. -func (m *EpisodeMutation) FileInStorageCleared() bool { - _, ok := m.clearedFields[episode.FieldFileInStorage] - return ok -} - -// ResetFileInStorage resets all changes to the "file_in_storage" field. -func (m *EpisodeMutation) ResetFileInStorage() { - m.file_in_storage = nil - delete(m.clearedFields, episode.FieldFileInStorage) -} - // ClearMedia clears the "media" edge to the Media entity. func (m *EpisodeMutation) ClearMedia() { m.clearedmedia = true @@ -1441,7 +1391,7 @@ func (m *EpisodeMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *EpisodeMutation) Fields() []string { - fields := make([]string, 0, 8) + fields := make([]string, 0, 7) if m.media != nil { fields = append(fields, episode.FieldMediaID) } @@ -1463,9 +1413,6 @@ func (m *EpisodeMutation) Fields() []string { if m.status != nil { fields = append(fields, episode.FieldStatus) } - if m.file_in_storage != nil { - fields = append(fields, episode.FieldFileInStorage) - } return fields } @@ -1488,8 +1435,6 @@ func (m *EpisodeMutation) Field(name string) (ent.Value, bool) { return m.AirDate() case episode.FieldStatus: return m.Status() - case episode.FieldFileInStorage: - return m.FileInStorage() } return nil, false } @@ -1513,8 +1458,6 @@ func (m *EpisodeMutation) OldField(ctx context.Context, name string) (ent.Value, return m.OldAirDate(ctx) case episode.FieldStatus: return m.OldStatus(ctx) - case episode.FieldFileInStorage: - return m.OldFileInStorage(ctx) } return nil, fmt.Errorf("unknown Episode field %s", name) } @@ -1573,13 +1516,6 @@ func (m *EpisodeMutation) SetField(name string, value ent.Value) error { } m.SetStatus(v) return nil - case episode.FieldFileInStorage: - v, ok := value.(string) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetFileInStorage(v) - return nil } return fmt.Errorf("unknown Episode field %s", name) } @@ -1640,9 +1576,6 @@ func (m *EpisodeMutation) ClearedFields() []string { if m.FieldCleared(episode.FieldMediaID) { fields = append(fields, episode.FieldMediaID) } - if m.FieldCleared(episode.FieldFileInStorage) { - fields = append(fields, episode.FieldFileInStorage) - } return fields } @@ -1660,9 +1593,6 @@ func (m *EpisodeMutation) ClearField(name string) error { case episode.FieldMediaID: m.ClearMediaID() return nil - case episode.FieldFileInStorage: - m.ClearFileInStorage() - return nil } return fmt.Errorf("unknown Episode nullable field %s", name) } @@ -1692,9 +1622,6 @@ func (m *EpisodeMutation) ResetField(name string) error { case episode.FieldStatus: m.ResetStatus() return nil - case episode.FieldFileInStorage: - m.ResetFileInStorage() - return nil } return fmt.Errorf("unknown Episode field %s", name) } diff --git a/ent/schema/media.go b/ent/schema/media.go index 759aac9..48db8e2 100644 --- a/ent/schema/media.go +++ b/ent/schema/media.go @@ -28,6 +28,7 @@ func (Media) Fields() []ent.Field { field.Enum("resolution").Values("720p", "1080p", "4k").Default("1080p"), field.Int("storage_id").Optional(), field.String("target_dir").Optional(), + //field.Bool("download_history_episodes").Optional().Default(false).Comment("tv series only"), } } diff --git a/go.sum b/go.sum index ca68294..598f9ca 100644 --- a/go.sum +++ b/go.sum @@ -93,6 +93,8 @@ 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.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= @@ -106,6 +108,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= +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/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -129,6 +133,8 @@ 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= @@ -177,6 +183,8 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -188,6 +196,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= diff --git a/pkg/metadata/tv.go b/pkg/metadata/tv.go index d622cfe..d8dbe52 100644 --- a/pkg/metadata/tv.go +++ b/pkg/metadata/tv.go @@ -112,25 +112,25 @@ func parseEnglishName(name string) *Metadata { } } else { //no episode, maybe like One Punch Man S2 - 08 [1080p].mkv - numRe := regexp.MustCompile(`^\d{1,2}$`) - for i, p := range newSplits { - if numRe.MatchString(p) { - if i > 0 && strings.Contains(newSplits[i-1], "season") { //last word cannot be season - continue - } - if i < seasonIndex { - //episode number most likely should comes alfter season number - continue - } - //episodeIndex = i - n, err := strconv.Atoi(p) - if err != nil { - panic(fmt.Sprintf("convert %s error: %v", p, err)) - } - meta.Episode = n + // numRe := regexp.MustCompile(`^\d{1,2}$`) + // for i, p := range newSplits { + // if numRe.MatchString(p) { + // if i > 0 && strings.Contains(newSplits[i-1], "season") { //last word cannot be season + // continue + // } + // if i < seasonIndex { + // //episode number most likely should comes alfter season number + // continue + // } + // //episodeIndex = i + // n, err := strconv.Atoi(p) + // if err != nil { + // panic(fmt.Sprintf("convert %s error: %v", p, err)) + // } + // meta.Episode = n - } - } + // } + // } } if resIndex != -1 { diff --git a/server/resources.go b/server/resources.go index 5325896..00cfa60 100644 --- a/server/resources.go +++ b/server/resources.go @@ -5,10 +5,11 @@ import ( "polaris/ent" "polaris/ent/episode" "polaris/ent/history" + "polaris/ent/media" "polaris/log" + "polaris/pkg/torznab" "polaris/pkg/utils" "polaris/server/core" - "strconv" "github.com/gin-gonic/gin" "github.com/pkg/errors" @@ -65,7 +66,7 @@ func (s *Server) searchAndDownloadSeasonPackage(seriesId, seasonNum int) (*strin return &r1.Name, nil } -func (s *Server) searchAndDownload(seriesId, seasonNum, episodeNum int) (*string, error) { +func (s *Server) downloadEpisodeTorrent(r1 torznab.Result, seriesId, seasonNum, episodeNum int) (*string, error) { trc, err := s.getDownloadClient() if err != nil { return nil, errors.Wrap(err, "connect transmission") @@ -83,13 +84,6 @@ func (s *Server) searchAndDownload(seriesId, seasonNum, episodeNum int) (*string if ep == nil { return nil, errors.Errorf("no episode of season %d episode %d", seasonNum, episodeNum) } - - res, err := core.SearchEpisode(s.db, seriesId, seasonNum, episodeNum, true) - if err != nil { - return nil, err - } - r1 := res[0] - log.Infof("found resource to download: %+v", r1) torrent, err := trc.Download(r1.Link, s.db.GetDownloadDir()) if err != nil { return nil, errors.Wrap(err, "downloading") @@ -116,6 +110,17 @@ func (s *Server) searchAndDownload(seriesId, seasonNum, episodeNum int) (*string log.Infof("success add %s to download task", r1.Name) return &r1.Name, nil + +} +func (s *Server) searchAndDownload(seriesId, seasonNum, episodeNum int) (*string, error) { + + res, err := core.SearchEpisode(s.db, seriesId, seasonNum, episodeNum, true) + if err != nil { + return nil, err + } + r1 := res[0] + log.Infof("found resource to download: %+v", r1) + return s.downloadEpisodeTorrent(r1, seriesId, seasonNum, episodeNum) } type searchAndDownloadIn struct { @@ -124,15 +129,34 @@ type searchAndDownloadIn struct { Episode int `json:"episode"` } -func (s *Server) SearchAvailableEpisodeResource(c *gin.Context) (interface{}, error) { +func (s *Server) SearchAvailableTorrents(c *gin.Context) (interface{}, error) { var in searchAndDownloadIn if err := c.ShouldBindJSON(&in); err != nil { return nil, errors.Wrap(err, "bind json") } - log.Infof("search episode resources link: %v", in) - res, err := core.SearchEpisode(s.db, in.ID, in.Season, in.Episode, true) + m, err := s.db.GetMedia(in.ID) if err != nil { - return nil, errors.Wrap(err, "search episode") + return nil, errors.Wrap(err, "get media") + } + log.Infof("search torrents resources link: %+v", in) + + var res []torznab.Result + if m.MediaType == media.MediaTypeTv { + 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 nil, errors.Wrap(err, "search episode") + } + } else { + res, err = core.SearchMovie(s.db, in.ID, false) + if err != nil { + if err.Error() == "no resource found" { + return []TorznabSearchResult{}, nil + } + return nil, err + } } var searchResults []TorznabSearchResult for _, r := range res { @@ -144,9 +168,6 @@ func (s *Server) SearchAvailableEpisodeResource(c *gin.Context) (interface{}, er Link: r.Link, }) } - if len(searchResults) == 0 { - return nil, errors.New("no resource found") - } return searchResults, nil } @@ -187,59 +208,36 @@ type TorznabSearchResult struct { Peers int `json:"peers"` Source string `json:"source"` } - -func (s *Server) SearchAvailableMovies(c *gin.Context) (interface{}, error) { - ids := c.Param("id") - id, err := strconv.Atoi(ids) - if err != nil { - return nil, errors.Wrap(err, "convert") - } - - res, err := core.SearchMovie(s.db, id, false) - if err != nil { - if err.Error() == "no resource found" { - return []TorznabSearchResult{}, 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, - Source: r.Source, - }) - } - if len(searchResults) == 0 { - return []TorznabSearchResult{}, nil - } - return searchResults, nil -} - type downloadTorrentIn struct { - MediaID int `json:"media_id" binding:"required"` + MediaID int `json:"id" binding:"required"` + Season int `json:"season"` + Episode int `json:"episode"` TorznabSearchResult } -func (s *Server) DownloadMovieTorrent(c *gin.Context) (interface{}, error) { +func (s *Server) DownloadTorrent(c *gin.Context) (interface{}, error) { var in downloadTorrentIn if err := c.ShouldBindJSON(&in); err != nil { return nil, errors.Wrap(err, "bind json") } log.Infof("download torrent input: %+v", in) + m, err := s.db.GetMedia(in.MediaID) + if err != nil { + return nil, fmt.Errorf("no tv series of id %v", in.MediaID) + } + if m.MediaType == media.MediaTypeTv { + name := in.Name + 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} + return s.downloadEpisodeTorrent(res, in.MediaID, in.Season, in.Episode) + } trc, err := s.getDownloadClient() if err != nil { return nil, errors.Wrap(err, "connect transmission") } - media := s.db.GetMediaDetails(in.MediaID) - if media == nil { - return nil, fmt.Errorf("no tv series of id %v", in.MediaID) - } torrent, err := trc.Download(in.Link, s.db.GetDownloadDir()) if err != nil { @@ -248,12 +246,12 @@ func (s *Server) DownloadMovieTorrent(c *gin.Context) (interface{}, error) { torrent.Start() name := in.Name if name == "" { - name = media.OriginalName + name = m.OriginalName } go func() { - ep := media.Episodes[0] + ep, _ := s.db.GetMovieDummyEpisode(m.ID) history, err := s.db.SaveHistoryRecord(ent.History{ - MediaID: media.ID, + MediaID: m.ID, EpisodeID: ep.ID, SourceTitle: name, TargetDir: "./", diff --git a/server/scheduler.go b/server/scheduler.go index efb1ea8..f6de489 100644 --- a/server/scheduler.go +++ b/server/scheduler.go @@ -192,12 +192,18 @@ func (s *Server) checkDownloadedSeriesFiles(m *ent.Media) error { log.Errorf("find season episode num error: %v", err) continue } - var dirname = filepath.Join(in.Name(), ep.Name()) log.Infof("found match, season num %d, episode num %d", seNum, epNum) - err = s.db.UpdateEpisodeFile(m.ID, seNum, epNum, dirname) + ep, err := s.db.GetEpisode(m.ID, seNum, epNum) if err != nil { log.Error("update episode: %v", err) + continue } + err = s.db.SetEpisodeStatus(ep.ID, episode.StatusDownloaded) + if err != nil { + log.Error("update episode: %v", err) + continue + } + } } return nil diff --git a/server/server.go b/server/server.go index 88dc981..654da03 100644 --- a/server/server.go +++ b/server/server.go @@ -83,11 +83,10 @@ func (s *Server) Serve() error { tv.GET("/search", HttpHandler(s.SearchMedia)) tv.POST("/tv/watchlist", HttpHandler(s.AddTv2Watchlist)) tv.GET("/tv/watchlist", HttpHandler(s.GetTvWatchlist)) - tv.POST("/tv/torrents", HttpHandler(s.SearchAvailableEpisodeResource)) + tv.POST("/torrents", HttpHandler(s.SearchAvailableTorrents)) + tv.POST("/torrents/download/", HttpHandler(s.DownloadTorrent)) tv.POST("/movie/watchlist", HttpHandler(s.AddMovie2Watchlist)) tv.GET("/movie/watchlist", HttpHandler(s.GetMovieWatchlist)) - tv.GET("/movie/resources/:id", HttpHandler(s.SearchAvailableMovies)) - tv.POST("/movie/resources/", HttpHandler(s.DownloadMovieTorrent)) tv.GET("/record/:id", HttpHandler(s.GetMediaDetails)) tv.DELETE("/record/:id", HttpHandler(s.DeleteFromWatchlist)) tv.GET("/resolutions", HttpHandler(s.GetAvailableResolutions)) diff --git a/ui/lib/movie_watchlist.dart b/ui/lib/movie_watchlist.dart index becee56..5f0ef2f 100644 --- a/ui/lib/movie_watchlist.dart +++ b/ui/lib/movie_watchlist.dart @@ -172,7 +172,7 @@ class _NestedTabBarState extends ConsumerState @override Widget build(BuildContext context) { - var torrents = ref.watch(movieTorrentsDataProvider(widget.id)); + var torrents = ref.watch(mediaTorrentsDataProvider(TorrentQuery(mediaId: widget.id))); var histories = ref.watch(mediaHistoryDataProvider(widget.id)); return Column( @@ -250,7 +250,7 @@ class _NestedTabBarState extends ConsumerState icon: const Icon(Icons.download), onPressed: () { ref - .read(movieTorrentsDataProvider(widget.id) + .read(mediaTorrentsDataProvider(TorrentQuery(mediaId: widget.id)) .notifier) .download(torrent) .then((v) => diff --git a/ui/lib/providers/APIs.dart b/ui/lib/providers/APIs.dart index 829d9c9..a28fe83 100644 --- a/ui/lib/providers/APIs.dart +++ b/ui/lib/providers/APIs.dart @@ -13,7 +13,8 @@ class APIs { static final settingsGeneralUrl = "$_baseUrl/api/v1/setting/general"; static final watchlistTvUrl = "$_baseUrl/api/v1/media/tv/watchlist"; static final watchlistMovieUrl = "$_baseUrl/api/v1/media/movie/watchlist"; - static final availableMoviesUrl = "$_baseUrl/api/v1/media/movie/resources/"; + static final availableTorrentsUrl = "$_baseUrl/api/v1/media/torrents/"; + static final downloadTorrentUrl = "$_baseUrl/api/v1/media/torrents/download"; static final seriesDetailUrl = "$_baseUrl/api/v1/media/record/"; static final suggestedTvName = "$_baseUrl/api/v1/media/suggest/"; static final searchAndDownloadUrl = "$_baseUrl/api/v1/indexer/download"; diff --git a/ui/lib/providers/series_details.dart b/ui/lib/providers/series_details.dart index 5550a86..1c9eecd 100644 --- a/ui/lib/providers/series_details.dart +++ b/ui/lib/providers/series_details.dart @@ -97,7 +97,7 @@ class SeriesDetails { class Episodes { int? id; - int? seriesId; + int? mediaId; int? episodeNumber; String? title; String? airDate; @@ -107,7 +107,7 @@ class Episodes { Episodes( {this.id, - this.seriesId, + this.mediaId, this.episodeNumber, this.title, this.airDate, @@ -117,7 +117,7 @@ class Episodes { Episodes.fromJson(Map json) { id = json['id']; - seriesId = json['series_id']; + mediaId = json['media_id']; episodeNumber = json['episode_number']; title = json['title']; airDate = json['air_date']; @@ -126,3 +126,75 @@ class Episodes { overview = json['overview']; } } + +var mediaTorrentsDataProvider = AsyncNotifierProvider.autoDispose + .family, TorrentQuery>( + MediaTorrentResource.new); + +class TorrentQuery { + final String mediaId; + final int seasonNumber; + final int episodeNumber; + TorrentQuery( + {required this.mediaId, this.seasonNumber = 0, this.episodeNumber = 0}); + Map toJson() { + final Map data = {}; + data["id"] = int.parse(mediaId); + data["season"] = seasonNumber; + data["episode"] = episodeNumber; + return data; + } +} + +class MediaTorrentResource extends AutoDisposeFamilyAsyncNotifier< + List, TorrentQuery> { + TorrentQuery? query; + @override + FutureOr> build(TorrentQuery arg) async { + query = arg; + final dio = await APIs.getDio(); + var resp = await dio.post(APIs.availableTorrentsUrl, data: arg.toJson()); + var rsp = ServerResponse.fromJson(resp.data); + if (rsp.code != 0) { + throw rsp.message; + } + return (rsp.data as List).map((v) => TorrentResource.fromJson(v)).toList(); + } + + Future download(TorrentResource res) async { + final data = res.toJson(); + data.addAll(query!.toJson()); + final dio = await APIs.getDio(); + var resp = await dio.post(APIs.downloadTorrentUrl, data: data); + var rsp = ServerResponse.fromJson(resp.data); + if (rsp.code != 0) { + throw rsp.message; + } + } +} + +class TorrentResource { + TorrentResource({this.name, this.size, this.seeders, this.peers, this.link}); + + String? name; + int? size; + int? seeders; + int? peers; + String? link; + + factory TorrentResource.fromJson(Map json) { + return TorrentResource( + name: json["name"], + size: json["size"], + seeders: json["seeders"], + peers: json["peers"], + link: json["link"]); + } + Map toJson() { + final Map data = {}; + data['name'] = name; + data['size'] = size; + data["link"] = link; + return data; + } +} diff --git a/ui/lib/providers/welcome_data.dart b/ui/lib/providers/welcome_data.dart index b8bd1bf..68e953d 100644 --- a/ui/lib/providers/welcome_data.dart +++ b/ui/lib/providers/welcome_data.dart @@ -44,10 +44,6 @@ final movieWatchlistDataProvider = FutureProvider.autoDispose((ref) async { var searchPageDataProvider = AsyncNotifierProvider.autoDispose .family, String>(SearchPageData.new); -var movieTorrentsDataProvider = AsyncNotifierProvider.autoDispose - .family, String>( - MovieTorrentResource.new); - class SearchPageData extends AutoDisposeFamilyAsyncNotifier, String> { List list = List.empty(growable: true); @@ -245,56 +241,3 @@ class SearchResult { } } -class MovieTorrentResource - extends AutoDisposeFamilyAsyncNotifier, String> { - String? mediaId; - @override - FutureOr> build(String id) async { - mediaId = id; - final dio = await APIs.getDio(); - var resp = await dio.get(APIs.availableMoviesUrl + id); - var rsp = ServerResponse.fromJson(resp.data); - if (rsp.code != 0) { - throw rsp.message; - } - return (rsp.data as List).map((v) => TorrentResource.fromJson(v)).toList(); - } - - Future download(TorrentResource res) async { - var m = res.toJson(); - m["media_id"] = int.parse(mediaId!); - - final dio = await APIs.getDio(); - var resp = await dio.post(APIs.availableMoviesUrl, data: m); - var rsp = ServerResponse.fromJson(resp.data); - if (rsp.code != 0) { - throw rsp.message; - } - } -} - -class TorrentResource { - TorrentResource({this.name, this.size, this.seeders, this.peers, this.link}); - - String? name; - int? size; - int? seeders; - int? peers; - String? link; - - factory TorrentResource.fromJson(Map json) { - return TorrentResource( - name: json["name"], - size: json["size"], - seeders: json["seeders"], - peers: json["peers"], - link: json["link"]); - } - Map toJson() { - final Map data = {}; - data['name'] = name; - data['size'] = size; - data["link"] = link; - return data; - } -} diff --git a/ui/lib/settings.dart b/ui/lib/settings.dart index 71a0fb7..60b79ac 100644 --- a/ui/lib/settings.dart +++ b/ui/lib/settings.dart @@ -591,8 +591,8 @@ class _SystemSettingsPageState extends ConsumerState { return AlertDialog( title: Text(title), content: SingleChildScrollView( - child: Container( - constraints: const BoxConstraints(maxWidth: 200), + child: SizedBox( + width: 300, child: body, ), ), diff --git a/ui/lib/tv_details.dart b/ui/lib/tv_details.dart index fc1615b..a673b03 100644 --- a/ui/lib/tv_details.dart +++ b/ui/lib/tv_details.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -26,7 +28,6 @@ class TvDetailsPage extends ConsumerStatefulWidget { } class _TvDetailsPageState extends ConsumerState { - @override void initState() { super.initState(); @@ -45,7 +46,7 @@ class _TvDetailsPageState extends ConsumerState { DataCell(Text("${ep.title}")), DataCell(Opacity( opacity: 0.5, - child: Text(ep.airDate??"-"), + child: Text(ep.airDate ?? "-"), )), DataCell( Opacity( @@ -86,7 +87,9 @@ class _TvDetailsPageState extends ConsumerState { width: 10, ), IconButton( - onPressed: () {}, icon: const Icon(Icons.manage_search)) + onPressed: () => showAvailableTorrents(widget.seriesId, + ep.seasonNumber ?? 0, ep.episodeNumber ?? 0), + icon: const Icon(Icons.manage_search)) ], )) ]); @@ -198,7 +201,7 @@ class _TvDetailsPageState extends ConsumerState { ), const Text(""), Text( - details.overview??"", + details.overview ?? "", ), ], )), @@ -239,4 +242,86 @@ class _TvDetailsPageState extends ConsumerState { }, loading: () => const MyProgressIndicator()); } + + Future showAvailableTorrents(String id, int season, int episode) { + final torrents = ref.watch(mediaTorrentsDataProvider(TorrentQuery( + mediaId: id, seasonNumber: season, episodeNumber: episode)) + .future); + + return showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return AlertDialog( + title: Text("资源"), + content: FutureBuilder( + future: torrents, + builder: (context, snapshot) { + return SelectionArea( + child: Container( + constraints: + BoxConstraints(maxHeight: 400, maxWidth: 1000), + child: () { + if (snapshot.connectionState == + ConnectionState.done) { + if (snapshot.hasError) { + // 请求失败,显示错误 + return Text("Error: ${snapshot.error}"); + } else { + // 请求成功,显示数据 + final v = snapshot.data; + return SingleChildScrollView( + child: DataTable( + dataTextStyle: + TextStyle(fontSize: 14, height: 0), + columns: const [ + DataColumn(label: Text("名称")), + DataColumn(label: Text("大小")), + DataColumn(label: Text("seeders")), + DataColumn(label: Text("peers")), + DataColumn(label: Text("操作")) + ], + rows: List.generate(v!.length, (i) { + final torrent = v[i]; + return DataRow(cells: [ + 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 { + await ref + .read(mediaTorrentsDataProvider( + TorrentQuery( + mediaId: id, + seasonNumber: + season, + episodeNumber: + episode)) + .notifier) + .download(torrent) + .then((v) { + Navigator.of(context).pop(); + Utils.showSnakeBar( + "开始下载:${torrent.name}"); + }).onError((error, trace) => + Utils.showSnakeBar( + "下载失败:$error")); + }, + )) + ]); + }))); + } + } else { + // 请求未结束,显示loading + return MyProgressIndicator(); + } + }())); + })); + }, + ); + } }