diff --git a/db/db.go b/db/db.go index d75f248..e0447f1 100644 --- a/db/db.go +++ b/db/db.go @@ -65,12 +65,12 @@ func (c *Client) GetLanguage() string { return lang } -func (c *Client) AddWatchlist(path string, detail *tmdb.TVDetails) error { +func (c *Client) AddWatchlist(path string, detail *tmdb.TVDetails) (*ent.Series, error) { count := c.ent.Series.Query().Where(series.TmdbID(int(detail.ID))).CountX(context.Background()) if (count > 0) { - return fmt.Errorf("tv series %s already in watchlist", detail.Name) + return nil, fmt.Errorf("tv series %s already in watchlist", detail.Name) } - _, err := c.ent.Series.Create(). + r, err := c.ent.Series.Create(). SetTmdbID(int(detail.ID)). SetPath(path). SetOverview(detail.Overview). @@ -78,7 +78,7 @@ func (c *Client) AddWatchlist(path string, detail *tmdb.TVDetails) error { SetOriginalName(detail.OriginalName). SetPosterPath(detail.PosterPath). Save(context.TODO()) - return err + return r, err } func (c *Client) GetWatchlist() []*ent.Series { @@ -90,14 +90,14 @@ func (c *Client) GetWatchlist() []*ent.Series { return list } -func (c *Client) SaveEposideDetail(tmdbId int, d *tmdb.TVEpisodeDetails) error { +func (c *Client) SaveEposideDetail(d *ent.Epidodes) error { _, err := c.ent.Epidodes.Create(). SetAirDate(d.AirDate). SetSeasonNumber(d.SeasonNumber). SetEpisodeNumber(d.EpisodeNumber). - SetSeriesID(tmdbId). + SetSeriesID(d.SeriesID). SetOverview(d.Overview). - SetTitle(d.Name).Save(context.TODO()) + SetTitle(d.Title).Save(context.TODO()) return err } diff --git a/ent/migrate/schema.go b/ent/migrate/schema.go index c65d66f..62f9bcc 100644 --- a/ent/migrate/schema.go +++ b/ent/migrate/schema.go @@ -84,6 +84,7 @@ var ( {Name: "overview", Type: field.TypeString}, {Name: "path", Type: field.TypeString}, {Name: "poster_path", Type: field.TypeString, Nullable: true}, + {Name: "created_at", Type: field.TypeTime}, } // SeriesTable holds the schema information for the "series" table. SeriesTable = &schema.Table{ diff --git a/ent/mutation.go b/ent/mutation.go index 5cddf80..b898a0e 100644 --- a/ent/mutation.go +++ b/ent/mutation.go @@ -2750,6 +2750,7 @@ type SeriesMutation struct { overview *string _path *string poster_path *string + created_at *time.Time clearedFields map[string]struct{} done bool oldValue func(context.Context) (*Series, error) @@ -3152,6 +3153,42 @@ func (m *SeriesMutation) ResetPosterPath() { delete(m.clearedFields, series.FieldPosterPath) } +// SetCreatedAt sets the "created_at" field. +func (m *SeriesMutation) SetCreatedAt(t time.Time) { + m.created_at = &t +} + +// CreatedAt returns the value of the "created_at" field in the mutation. +func (m *SeriesMutation) CreatedAt() (r time.Time, exists bool) { + v := m.created_at + if v == nil { + return + } + return *v, true +} + +// OldCreatedAt returns the old "created_at" field's value of the Series entity. +// If the Series 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 *SeriesMutation) OldCreatedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedAt: %w", err) + } + return oldValue.CreatedAt, nil +} + +// ResetCreatedAt resets all changes to the "created_at" field. +func (m *SeriesMutation) ResetCreatedAt() { + m.created_at = nil +} + // Where appends a list predicates to the SeriesMutation builder. func (m *SeriesMutation) Where(ps ...predicate.Series) { m.predicates = append(m.predicates, ps...) @@ -3186,7 +3223,7 @@ func (m *SeriesMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *SeriesMutation) Fields() []string { - fields := make([]string, 0, 7) + fields := make([]string, 0, 8) if m.tmdb_id != nil { fields = append(fields, series.FieldTmdbID) } @@ -3208,6 +3245,9 @@ func (m *SeriesMutation) Fields() []string { if m.poster_path != nil { fields = append(fields, series.FieldPosterPath) } + if m.created_at != nil { + fields = append(fields, series.FieldCreatedAt) + } return fields } @@ -3230,6 +3270,8 @@ func (m *SeriesMutation) Field(name string) (ent.Value, bool) { return m.Path() case series.FieldPosterPath: return m.PosterPath() + case series.FieldCreatedAt: + return m.CreatedAt() } return nil, false } @@ -3253,6 +3295,8 @@ func (m *SeriesMutation) OldField(ctx context.Context, name string) (ent.Value, return m.OldPath(ctx) case series.FieldPosterPath: return m.OldPosterPath(ctx) + case series.FieldCreatedAt: + return m.OldCreatedAt(ctx) } return nil, fmt.Errorf("unknown Series field %s", name) } @@ -3311,6 +3355,13 @@ func (m *SeriesMutation) SetField(name string, value ent.Value) error { } m.SetPosterPath(v) return nil + case series.FieldCreatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedAt(v) + return nil } return fmt.Errorf("unknown Series field %s", name) } @@ -3411,6 +3462,9 @@ func (m *SeriesMutation) ResetField(name string) error { case series.FieldPosterPath: m.ResetPosterPath() return nil + case series.FieldCreatedAt: + m.ResetCreatedAt() + return nil } return fmt.Errorf("unknown Series field %s", name) } diff --git a/ent/runtime.go b/ent/runtime.go index 6c80c48..c4c1672 100644 --- a/ent/runtime.go +++ b/ent/runtime.go @@ -6,6 +6,8 @@ import ( "polaris/ent/downloadclients" "polaris/ent/indexers" "polaris/ent/schema" + "polaris/ent/series" + "time" ) // The init function reads all schema descriptors with runtime code @@ -48,4 +50,10 @@ func init() { indexersDescEnableRss := indexersFields[3].Descriptor() // indexers.DefaultEnableRss holds the default value on creation for the enable_rss field. indexers.DefaultEnableRss = indexersDescEnableRss.Default.(bool) + seriesFields := schema.Series{}.Fields() + _ = seriesFields + // seriesDescCreatedAt is the schema descriptor for created_at field. + seriesDescCreatedAt := seriesFields[7].Descriptor() + // series.DefaultCreatedAt holds the default value on creation for the created_at field. + series.DefaultCreatedAt = seriesDescCreatedAt.Default.(time.Time) } diff --git a/ent/schema/series.go b/ent/schema/series.go index 293b131..01efec9 100644 --- a/ent/schema/series.go +++ b/ent/schema/series.go @@ -1,6 +1,8 @@ package schema import ( + "time" + "entgo.io/ent" "entgo.io/ent/schema/field" ) @@ -20,6 +22,7 @@ func (Series) Fields() []ent.Field { field.String("overview"), field.String("path"), field.String("poster_path").Optional(), + field.Time("created_at").Default(time.Now()), } } diff --git a/ent/series.go b/ent/series.go index c953b8e..dca5239 100644 --- a/ent/series.go +++ b/ent/series.go @@ -6,6 +6,7 @@ import ( "fmt" "polaris/ent/series" "strings" + "time" "entgo.io/ent" "entgo.io/ent/dialect/sql" @@ -29,7 +30,9 @@ type Series struct { // Path holds the value of the "path" field. Path string `json:"path,omitempty"` // PosterPath holds the value of the "poster_path" field. - PosterPath string `json:"poster_path,omitempty"` + PosterPath string `json:"poster_path,omitempty"` + // CreatedAt holds the value of the "created_at" field. + CreatedAt time.Time `json:"created_at,omitempty"` selectValues sql.SelectValues } @@ -42,6 +45,8 @@ func (*Series) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullInt64) case series.FieldImdbID, series.FieldTitle, series.FieldOriginalName, series.FieldOverview, series.FieldPath, series.FieldPosterPath: values[i] = new(sql.NullString) + case series.FieldCreatedAt: + values[i] = new(sql.NullTime) default: values[i] = new(sql.UnknownType) } @@ -105,6 +110,12 @@ func (s *Series) assignValues(columns []string, values []any) error { } else if value.Valid { s.PosterPath = value.String } + case series.FieldCreatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created_at", values[i]) + } else if value.Valid { + s.CreatedAt = value.Time + } default: s.selectValues.Set(columns[i], values[i]) } @@ -161,6 +172,9 @@ func (s *Series) String() string { builder.WriteString(", ") builder.WriteString("poster_path=") builder.WriteString(s.PosterPath) + builder.WriteString(", ") + builder.WriteString("created_at=") + builder.WriteString(s.CreatedAt.Format(time.ANSIC)) builder.WriteByte(')') return builder.String() } diff --git a/ent/series/series.go b/ent/series/series.go index ee091b4..ee25573 100644 --- a/ent/series/series.go +++ b/ent/series/series.go @@ -3,6 +3,8 @@ package series import ( + "time" + "entgo.io/ent/dialect/sql" ) @@ -25,6 +27,8 @@ const ( FieldPath = "path" // FieldPosterPath holds the string denoting the poster_path field in the database. FieldPosterPath = "poster_path" + // FieldCreatedAt holds the string denoting the created_at field in the database. + FieldCreatedAt = "created_at" // Table holds the table name of the series in the database. Table = "series" ) @@ -39,6 +43,7 @@ var Columns = []string{ FieldOverview, FieldPath, FieldPosterPath, + FieldCreatedAt, } // ValidColumn reports if the column name is valid (part of the table columns). @@ -51,6 +56,11 @@ func ValidColumn(column string) bool { return false } +var ( + // DefaultCreatedAt holds the default value on creation for the "created_at" field. + DefaultCreatedAt time.Time +) + // OrderOption defines the ordering options for the Series queries. type OrderOption func(*sql.Selector) @@ -93,3 +103,8 @@ func ByPath(opts ...sql.OrderTermOption) OrderOption { func ByPosterPath(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldPosterPath, opts...).ToFunc() } + +// ByCreatedAt orders the results by the created_at field. +func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() +} diff --git a/ent/series/where.go b/ent/series/where.go index c45524f..f758733 100644 --- a/ent/series/where.go +++ b/ent/series/where.go @@ -4,6 +4,7 @@ package series import ( "polaris/ent/predicate" + "time" "entgo.io/ent/dialect/sql" ) @@ -88,6 +89,11 @@ func PosterPath(v string) predicate.Series { return predicate.Series(sql.FieldEQ(FieldPosterPath, v)) } +// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. +func CreatedAt(v time.Time) predicate.Series { + return predicate.Series(sql.FieldEQ(FieldCreatedAt, v)) +} + // TmdbIDEQ applies the EQ predicate on the "tmdb_id" field. func TmdbIDEQ(v int) predicate.Series { return predicate.Series(sql.FieldEQ(FieldTmdbID, v)) @@ -538,6 +544,46 @@ func PosterPathContainsFold(v string) predicate.Series { return predicate.Series(sql.FieldContainsFold(FieldPosterPath, v)) } +// CreatedAtEQ applies the EQ predicate on the "created_at" field. +func CreatedAtEQ(v time.Time) predicate.Series { + return predicate.Series(sql.FieldEQ(FieldCreatedAt, v)) +} + +// CreatedAtNEQ applies the NEQ predicate on the "created_at" field. +func CreatedAtNEQ(v time.Time) predicate.Series { + return predicate.Series(sql.FieldNEQ(FieldCreatedAt, v)) +} + +// CreatedAtIn applies the In predicate on the "created_at" field. +func CreatedAtIn(vs ...time.Time) predicate.Series { + return predicate.Series(sql.FieldIn(FieldCreatedAt, vs...)) +} + +// CreatedAtNotIn applies the NotIn predicate on the "created_at" field. +func CreatedAtNotIn(vs ...time.Time) predicate.Series { + return predicate.Series(sql.FieldNotIn(FieldCreatedAt, vs...)) +} + +// CreatedAtGT applies the GT predicate on the "created_at" field. +func CreatedAtGT(v time.Time) predicate.Series { + return predicate.Series(sql.FieldGT(FieldCreatedAt, v)) +} + +// CreatedAtGTE applies the GTE predicate on the "created_at" field. +func CreatedAtGTE(v time.Time) predicate.Series { + return predicate.Series(sql.FieldGTE(FieldCreatedAt, v)) +} + +// CreatedAtLT applies the LT predicate on the "created_at" field. +func CreatedAtLT(v time.Time) predicate.Series { + return predicate.Series(sql.FieldLT(FieldCreatedAt, v)) +} + +// CreatedAtLTE applies the LTE predicate on the "created_at" field. +func CreatedAtLTE(v time.Time) predicate.Series { + return predicate.Series(sql.FieldLTE(FieldCreatedAt, v)) +} + // And groups predicates with the AND operator between them. func And(predicates ...predicate.Series) predicate.Series { return predicate.Series(sql.AndPredicates(predicates...)) diff --git a/ent/series_create.go b/ent/series_create.go index f538e89..45df5d0 100644 --- a/ent/series_create.go +++ b/ent/series_create.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "polaris/ent/series" + "time" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" @@ -77,6 +78,20 @@ func (sc *SeriesCreate) SetNillablePosterPath(s *string) *SeriesCreate { return sc } +// SetCreatedAt sets the "created_at" field. +func (sc *SeriesCreate) SetCreatedAt(t time.Time) *SeriesCreate { + sc.mutation.SetCreatedAt(t) + return sc +} + +// SetNillableCreatedAt sets the "created_at" field if the given value is not nil. +func (sc *SeriesCreate) SetNillableCreatedAt(t *time.Time) *SeriesCreate { + if t != nil { + sc.SetCreatedAt(*t) + } + return sc +} + // Mutation returns the SeriesMutation object of the builder. func (sc *SeriesCreate) Mutation() *SeriesMutation { return sc.mutation @@ -84,6 +99,7 @@ func (sc *SeriesCreate) Mutation() *SeriesMutation { // Save creates the Series in the database. func (sc *SeriesCreate) Save(ctx context.Context) (*Series, error) { + sc.defaults() return withHooks(ctx, sc.sqlSave, sc.mutation, sc.hooks) } @@ -109,6 +125,14 @@ func (sc *SeriesCreate) ExecX(ctx context.Context) { } } +// defaults sets the default values of the builder before save. +func (sc *SeriesCreate) defaults() { + if _, ok := sc.mutation.CreatedAt(); !ok { + v := series.DefaultCreatedAt + sc.mutation.SetCreatedAt(v) + } +} + // check runs all checks and user-defined validators on the builder. func (sc *SeriesCreate) check() error { if _, ok := sc.mutation.TmdbID(); !ok { @@ -126,6 +150,9 @@ func (sc *SeriesCreate) check() error { if _, ok := sc.mutation.Path(); !ok { return &ValidationError{Name: "path", err: errors.New(`ent: missing required field "Series.path"`)} } + if _, ok := sc.mutation.CreatedAt(); !ok { + return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "Series.created_at"`)} + } return nil } @@ -180,6 +207,10 @@ func (sc *SeriesCreate) createSpec() (*Series, *sqlgraph.CreateSpec) { _spec.SetField(series.FieldPosterPath, field.TypeString, value) _node.PosterPath = value } + if value, ok := sc.mutation.CreatedAt(); ok { + _spec.SetField(series.FieldCreatedAt, field.TypeTime, value) + _node.CreatedAt = value + } return _node, _spec } @@ -201,6 +232,7 @@ func (scb *SeriesCreateBulk) Save(ctx context.Context) ([]*Series, error) { for i := range scb.builders { func(i int, root context.Context) { builder := scb.builders[i] + builder.defaults() var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { mutation, ok := m.(*SeriesMutation) if !ok { diff --git a/ent/series_update.go b/ent/series_update.go index eedfe39..b222f45 100644 --- a/ent/series_update.go +++ b/ent/series_update.go @@ -8,6 +8,7 @@ import ( "fmt" "polaris/ent/predicate" "polaris/ent/series" + "time" "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" @@ -144,6 +145,20 @@ func (su *SeriesUpdate) ClearPosterPath() *SeriesUpdate { return su } +// SetCreatedAt sets the "created_at" field. +func (su *SeriesUpdate) SetCreatedAt(t time.Time) *SeriesUpdate { + su.mutation.SetCreatedAt(t) + return su +} + +// SetNillableCreatedAt sets the "created_at" field if the given value is not nil. +func (su *SeriesUpdate) SetNillableCreatedAt(t *time.Time) *SeriesUpdate { + if t != nil { + su.SetCreatedAt(*t) + } + return su +} + // Mutation returns the SeriesMutation object of the builder. func (su *SeriesUpdate) Mutation() *SeriesMutation { return su.mutation @@ -215,6 +230,9 @@ func (su *SeriesUpdate) sqlSave(ctx context.Context) (n int, err error) { if su.mutation.PosterPathCleared() { _spec.ClearField(series.FieldPosterPath, field.TypeString) } + if value, ok := su.mutation.CreatedAt(); ok { + _spec.SetField(series.FieldCreatedAt, field.TypeTime, value) + } if n, err = sqlgraph.UpdateNodes(ctx, su.driver, _spec); err != nil { if _, ok := err.(*sqlgraph.NotFoundError); ok { err = &NotFoundError{series.Label} @@ -352,6 +370,20 @@ func (suo *SeriesUpdateOne) ClearPosterPath() *SeriesUpdateOne { return suo } +// SetCreatedAt sets the "created_at" field. +func (suo *SeriesUpdateOne) SetCreatedAt(t time.Time) *SeriesUpdateOne { + suo.mutation.SetCreatedAt(t) + return suo +} + +// SetNillableCreatedAt sets the "created_at" field if the given value is not nil. +func (suo *SeriesUpdateOne) SetNillableCreatedAt(t *time.Time) *SeriesUpdateOne { + if t != nil { + suo.SetCreatedAt(*t) + } + return suo +} + // Mutation returns the SeriesMutation object of the builder. func (suo *SeriesUpdateOne) Mutation() *SeriesMutation { return suo.mutation @@ -453,6 +485,9 @@ func (suo *SeriesUpdateOne) sqlSave(ctx context.Context) (_node *Series, err err if suo.mutation.PosterPathCleared() { _spec.ClearField(series.FieldPosterPath, field.TypeString) } + if value, ok := suo.mutation.CreatedAt(); ok { + _spec.SetField(series.FieldCreatedAt, field.TypeTime, value) + } _node = &Series{config: suo.config} _spec.Assign = _node.assignValues _spec.ScanValues = _node.scanValues diff --git a/pkg/tmdb/tmdb.go b/pkg/tmdb/tmdb.go index 3260e6b..0fcb94d 100644 --- a/pkg/tmdb/tmdb.go +++ b/pkg/tmdb/tmdb.go @@ -44,6 +44,10 @@ func (c *Client) GetEposideDetail(id, seasonNumber, eposideNumber int, language return d, err } +func (c *Client) GetSeasonDetails(id, seasonNumber int, language string) (*tmdb.TVSeasonDetails, error) { + return c.tmdbClient.GetTVSeasonDetails(id, seasonNumber, withLangOption(language)) +} + func wrapLanguage(lang string) string { if lang == "" { lang = "zh-CN" diff --git a/server/watchlist.go b/server/watchlist.go index c17637a..6b5856f 100644 --- a/server/watchlist.go +++ b/server/watchlist.go @@ -1,6 +1,7 @@ package server import ( + "polaris/ent" "polaris/log" "github.com/gin-gonic/gin" @@ -25,7 +26,7 @@ func (s *Server) SearchTvSeries(c *gin.Context) (interface{}, error) { } type addWatchlistIn struct { - ID int `json:"id" binding:"required"` + ID int `json:"id" binding:"required"` RootFolder string `json:"folder" binding:"required"` } @@ -39,23 +40,32 @@ func (s *Server) AddWatchlist(c *gin.Context) (interface{}, error) { return nil, errors.Wrap(err, "get tv detail") } log.Infof("find detail for tv id %d: %v", in.ID, detail) - - if err := s.db.AddWatchlist(in.RootFolder, detail); err != nil { + + wl, err := s.db.AddWatchlist(in.RootFolder, detail) + if err != nil { return nil, errors.Wrap(err, "add to list") } log.Infof("save watchlist success: %s", detail.Name) for _, season := range detail.Seasons { seasonId := season.SeasonNumber - for i := 1; i <= season.EpisodeCount; i++ { - ep, err := s.MustTMDB().GetEposideDetail(int(detail.ID), seasonId, i, s.language) + se, err := s.MustTMDB().GetSeasonDetails(int(detail.ID), seasonId, s.language) + if err != nil { + log.Errorf("get season detail (%s) error: %v", detail.Name, err) + continue + } + for _, ep := range se.Episodes { + err = s.db.SaveEposideDetail(&ent.Epidodes{ + SeriesID: wl.ID, + SeasonNumber: seasonId, + EpisodeNumber: ep.EpisodeNumber, + Title: ep.Name, + Overview: ep.Overview, + AirDate: ep.AirDate, + }) if err != nil { - log.Errorf("get eposide detail: %v", err) - return nil, errors.Wrap(err, "get eposide detail") - } - err = s.db.SaveEposideDetail(int(detail.ID), ep) - if err != nil { - return nil, errors.Wrap(err, "save episode") + log.Errorf("save episode info error: %v", err) + continue } } } @@ -66,4 +76,4 @@ func (s *Server) AddWatchlist(c *gin.Context) (interface{}, error) { func (s *Server) GetWatchlist(c *gin.Context) (interface{}, error) { list := s.db.GetWatchlist() return list, nil -} \ No newline at end of file +} diff --git a/ui/lib/search.dart b/ui/lib/search.dart index 2735415..72ac904 100644 --- a/ui/lib/search.dart +++ b/ui/lib/search.dart @@ -69,7 +69,7 @@ class _SearchPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - m.name!, + "${m.name} (${m.firstAirDate?.split("-")[0]})", style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold), ), diff --git a/ui/lib/system_settings.dart b/ui/lib/system_settings.dart index 21be19f..f020cdf 100644 --- a/ui/lib/system_settings.dart +++ b/ui/lib/system_settings.dart @@ -37,7 +37,7 @@ class _SystemSettingsPageState extends State { labelText: "TMDB Api Key", icon: Icon(Icons.key), ), - // 校验用户名 + // validator: (v) { return v!.trim().isNotEmpty ? null : "ApiKey 不能为空"; },