diff --git a/db/db.go b/db/db.go index 8eef08a..828a8d0 100644 --- a/db/db.go +++ b/db/db.go @@ -67,7 +67,7 @@ func (c *Client) GetLanguage() string { func (c *Client) AddWatchlist(path string, detail *tmdb.TVDetails, episodes []int) (*ent.Series, error) { count := c.ent.Series.Query().Where(series.TmdbID(int(detail.ID))).CountX(context.Background()) - if (count > 0) { + if count > 0 { return nil, fmt.Errorf("tv series %s already in watchlist", detail.Name) } r, err := c.ent.Series.Create(). @@ -91,6 +91,20 @@ func (c *Client) GetWatchlist() []*ent.Series { return list } +type SeriesDetails struct { + *ent.Series + Episodes []*ent.Episode `json:"episodes"` +} + +func (c *Client) GetSeriesDetails(id int) *SeriesDetails { + se := c.ent.Series.Query().Where(series.ID(id)).FirstX(context.TODO()) + ep := se.QueryEpisodes().AllX(context.Background()) + return &SeriesDetails{ + Series: se, + Episodes: ep, + } +} + func (c *Client) SaveEposideDetail(d *ent.Episode) (int, error) { ep, err := c.ent.Episode.Create(). SetAirDate(d.AirDate). @@ -99,7 +113,7 @@ func (c *Client) SaveEposideDetail(d *ent.Episode) (int, error) { SetOverview(d.Overview). SetTitle(d.Title).Save(context.TODO()) - return ep.ID,err + return ep.ID, err } type TorznabSetting struct { diff --git a/ent/episode.go b/ent/episode.go index 827280f..5b61187 100644 --- a/ent/episode.go +++ b/ent/episode.go @@ -17,8 +17,10 @@ type Episode struct { config `json:"-"` // ID of the ent. ID int `json:"id,omitempty"` + // SeriesID holds the value of the "series_id" field. + SeriesID int `json:"series_id,omitempty"` // SeasonNumber holds the value of the "season_number" field. - SeasonNumber int `json:"season_number,omitempty"` + SeasonNumber int `json:"season_number"` // EpisodeNumber holds the value of the "episode_number" field. EpisodeNumber int `json:"episode_number,omitempty"` // Title holds the value of the "title" field. @@ -29,9 +31,8 @@ type Episode struct { AirDate string `json:"air_date,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"` - series_episodes *int - selectValues sql.SelectValues + Edges EpisodeEdges `json:"edges"` + selectValues sql.SelectValues } // EpisodeEdges holds the relations/edges for other nodes in the graph. @@ -59,12 +60,10 @@ func (*Episode) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { - case episode.FieldID, episode.FieldSeasonNumber, episode.FieldEpisodeNumber: + case episode.FieldID, episode.FieldSeriesID, episode.FieldSeasonNumber, episode.FieldEpisodeNumber: values[i] = new(sql.NullInt64) case episode.FieldTitle, episode.FieldOverview, episode.FieldAirDate: values[i] = new(sql.NullString) - case episode.ForeignKeys[0]: // series_episodes - values[i] = new(sql.NullInt64) default: values[i] = new(sql.UnknownType) } @@ -86,6 +85,12 @@ func (e *Episode) assignValues(columns []string, values []any) error { return fmt.Errorf("unexpected type %T for field id", value) } e.ID = int(value.Int64) + case episode.FieldSeriesID: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field series_id", values[i]) + } else if value.Valid { + e.SeriesID = int(value.Int64) + } case episode.FieldSeasonNumber: if value, ok := values[i].(*sql.NullInt64); !ok { return fmt.Errorf("unexpected type %T for field season_number", values[i]) @@ -116,13 +121,6 @@ func (e *Episode) assignValues(columns []string, values []any) error { } else if value.Valid { e.AirDate = value.String } - case episode.ForeignKeys[0]: - if value, ok := values[i].(*sql.NullInt64); !ok { - return fmt.Errorf("unexpected type %T for edge-field series_episodes", value) - } else if value.Valid { - e.series_episodes = new(int) - *e.series_episodes = int(value.Int64) - } default: e.selectValues.Set(columns[i], values[i]) } @@ -164,6 +162,9 @@ func (e *Episode) String() string { var builder strings.Builder builder.WriteString("Episode(") builder.WriteString(fmt.Sprintf("id=%v, ", e.ID)) + builder.WriteString("series_id=") + builder.WriteString(fmt.Sprintf("%v", e.SeriesID)) + builder.WriteString(", ") builder.WriteString("season_number=") builder.WriteString(fmt.Sprintf("%v", e.SeasonNumber)) builder.WriteString(", ") diff --git a/ent/episode/episode.go b/ent/episode/episode.go index fa3e0f9..1abcdd2 100644 --- a/ent/episode/episode.go +++ b/ent/episode/episode.go @@ -12,6 +12,8 @@ const ( Label = "episode" // FieldID holds the string denoting the id field in the database. FieldID = "id" + // FieldSeriesID holds the string denoting the series_id field in the database. + FieldSeriesID = "series_id" // FieldSeasonNumber holds the string denoting the season_number field in the database. FieldSeasonNumber = "season_number" // FieldEpisodeNumber holds the string denoting the episode_number field in the database. @@ -32,12 +34,13 @@ const ( // It exists in this package in order to avoid circular dependency with the "series" package. SeriesInverseTable = "series" // SeriesColumn is the table column denoting the series relation/edge. - SeriesColumn = "series_episodes" + SeriesColumn = "series_id" ) // Columns holds all SQL columns for episode fields. var Columns = []string{ FieldID, + FieldSeriesID, FieldSeasonNumber, FieldEpisodeNumber, FieldTitle, @@ -45,12 +48,6 @@ var Columns = []string{ FieldAirDate, } -// ForeignKeys holds the SQL foreign-keys that are owned by the "episodes" -// table and are not defined as standalone fields in the schema. -var ForeignKeys = []string{ - "series_episodes", -} - // ValidColumn reports if the column name is valid (part of the table columns). func ValidColumn(column string) bool { for i := range Columns { @@ -58,11 +55,6 @@ func ValidColumn(column string) bool { return true } } - for i := range ForeignKeys { - if column == ForeignKeys[i] { - return true - } - } return false } @@ -74,6 +66,11 @@ func ByID(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldID, opts...).ToFunc() } +// BySeriesID orders the results by the series_id field. +func BySeriesID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSeriesID, opts...).ToFunc() +} + // BySeasonNumber orders the results by the season_number field. func BySeasonNumber(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldSeasonNumber, opts...).ToFunc() diff --git a/ent/episode/where.go b/ent/episode/where.go index 7fe00a5..bf00b43 100644 --- a/ent/episode/where.go +++ b/ent/episode/where.go @@ -54,6 +54,11 @@ func IDLTE(id int) predicate.Episode { return predicate.Episode(sql.FieldLTE(FieldID, id)) } +// SeriesID applies equality check predicate on the "series_id" field. It's identical to SeriesIDEQ. +func SeriesID(v int) predicate.Episode { + return predicate.Episode(sql.FieldEQ(FieldSeriesID, v)) +} + // SeasonNumber applies equality check predicate on the "season_number" field. It's identical to SeasonNumberEQ. func SeasonNumber(v int) predicate.Episode { return predicate.Episode(sql.FieldEQ(FieldSeasonNumber, v)) @@ -79,6 +84,36 @@ func AirDate(v string) predicate.Episode { return predicate.Episode(sql.FieldEQ(FieldAirDate, v)) } +// SeriesIDEQ applies the EQ predicate on the "series_id" field. +func SeriesIDEQ(v int) predicate.Episode { + return predicate.Episode(sql.FieldEQ(FieldSeriesID, v)) +} + +// SeriesIDNEQ applies the NEQ predicate on the "series_id" field. +func SeriesIDNEQ(v int) predicate.Episode { + return predicate.Episode(sql.FieldNEQ(FieldSeriesID, v)) +} + +// SeriesIDIn applies the In predicate on the "series_id" field. +func SeriesIDIn(vs ...int) predicate.Episode { + return predicate.Episode(sql.FieldIn(FieldSeriesID, vs...)) +} + +// SeriesIDNotIn applies the NotIn predicate on the "series_id" field. +func SeriesIDNotIn(vs ...int) predicate.Episode { + return predicate.Episode(sql.FieldNotIn(FieldSeriesID, vs...)) +} + +// SeriesIDIsNil applies the IsNil predicate on the "series_id" field. +func SeriesIDIsNil() predicate.Episode { + return predicate.Episode(sql.FieldIsNull(FieldSeriesID)) +} + +// SeriesIDNotNil applies the NotNil predicate on the "series_id" field. +func SeriesIDNotNil() predicate.Episode { + return predicate.Episode(sql.FieldNotNull(FieldSeriesID)) +} + // SeasonNumberEQ applies the EQ predicate on the "season_number" field. func SeasonNumberEQ(v int) predicate.Episode { return predicate.Episode(sql.FieldEQ(FieldSeasonNumber, v)) diff --git a/ent/episode_create.go b/ent/episode_create.go index f615d8d..18a5a4c 100644 --- a/ent/episode_create.go +++ b/ent/episode_create.go @@ -20,6 +20,20 @@ type EpisodeCreate struct { hooks []Hook } +// SetSeriesID sets the "series_id" field. +func (ec *EpisodeCreate) SetSeriesID(i int) *EpisodeCreate { + ec.mutation.SetSeriesID(i) + return ec +} + +// SetNillableSeriesID sets the "series_id" field if the given value is not nil. +func (ec *EpisodeCreate) SetNillableSeriesID(i *int) *EpisodeCreate { + if i != nil { + ec.SetSeriesID(*i) + } + return ec +} + // SetSeasonNumber sets the "season_number" field. func (ec *EpisodeCreate) SetSeasonNumber(i int) *EpisodeCreate { ec.mutation.SetSeasonNumber(i) @@ -50,20 +64,6 @@ func (ec *EpisodeCreate) SetAirDate(s string) *EpisodeCreate { return ec } -// SetSeriesID sets the "series" edge to the Series entity by ID. -func (ec *EpisodeCreate) SetSeriesID(id int) *EpisodeCreate { - ec.mutation.SetSeriesID(id) - return ec -} - -// SetNillableSeriesID sets the "series" edge to the Series entity by ID if the given value is not nil. -func (ec *EpisodeCreate) SetNillableSeriesID(id *int) *EpisodeCreate { - if id != nil { - ec = ec.SetSeriesID(*id) - } - return ec -} - // SetSeries sets the "series" edge to the Series entity. func (ec *EpisodeCreate) SetSeries(s *Series) *EpisodeCreate { return ec.SetSeriesID(s.ID) @@ -178,7 +178,7 @@ func (ec *EpisodeCreate) createSpec() (*Episode, *sqlgraph.CreateSpec) { for _, k := range nodes { edge.Target.Nodes = append(edge.Target.Nodes, k) } - _node.series_episodes = &nodes[0] + _node.SeriesID = nodes[0] _spec.Edges = append(_spec.Edges, edge) } return _node, _spec diff --git a/ent/episode_query.go b/ent/episode_query.go index 045dd1b..592a09e 100644 --- a/ent/episode_query.go +++ b/ent/episode_query.go @@ -23,7 +23,6 @@ type EpisodeQuery struct { inters []Interceptor predicates []predicate.Episode withSeries *SeriesQuery - withFKs bool // intermediate query (i.e. traversal path). sql *sql.Selector path func(context.Context) (*sql.Selector, error) @@ -298,12 +297,12 @@ func (eq *EpisodeQuery) WithSeries(opts ...func(*SeriesQuery)) *EpisodeQuery { // Example: // // var v []struct { -// SeasonNumber int `json:"season_number,omitempty"` +// SeriesID int `json:"series_id,omitempty"` // Count int `json:"count,omitempty"` // } // // client.Episode.Query(). -// GroupBy(episode.FieldSeasonNumber). +// GroupBy(episode.FieldSeriesID). // Aggregate(ent.Count()). // Scan(ctx, &v) func (eq *EpisodeQuery) GroupBy(field string, fields ...string) *EpisodeGroupBy { @@ -321,11 +320,11 @@ func (eq *EpisodeQuery) GroupBy(field string, fields ...string) *EpisodeGroupBy // Example: // // var v []struct { -// SeasonNumber int `json:"season_number,omitempty"` +// SeriesID int `json:"series_id,omitempty"` // } // // client.Episode.Query(). -// Select(episode.FieldSeasonNumber). +// Select(episode.FieldSeriesID). // Scan(ctx, &v) func (eq *EpisodeQuery) Select(fields ...string) *EpisodeSelect { eq.ctx.Fields = append(eq.ctx.Fields, fields...) @@ -369,18 +368,11 @@ func (eq *EpisodeQuery) prepareQuery(ctx context.Context) error { func (eq *EpisodeQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Episode, error) { var ( nodes = []*Episode{} - withFKs = eq.withFKs _spec = eq.querySpec() loadedTypes = [1]bool{ eq.withSeries != nil, } ) - if eq.withSeries != nil { - withFKs = true - } - if withFKs { - _spec.Node.Columns = append(_spec.Node.Columns, episode.ForeignKeys...) - } _spec.ScanValues = func(columns []string) ([]any, error) { return (*Episode).scanValues(nil, columns) } @@ -412,10 +404,7 @@ func (eq *EpisodeQuery) loadSeries(ctx context.Context, query *SeriesQuery, node ids := make([]int, 0, len(nodes)) nodeids := make(map[int][]*Episode) for i := range nodes { - if nodes[i].series_episodes == nil { - continue - } - fk := *nodes[i].series_episodes + fk := nodes[i].SeriesID if _, ok := nodeids[fk]; !ok { ids = append(ids, fk) } @@ -432,7 +421,7 @@ func (eq *EpisodeQuery) loadSeries(ctx context.Context, query *SeriesQuery, node for _, n := range neighbors { nodes, ok := nodeids[n.ID] if !ok { - return fmt.Errorf(`unexpected foreign-key "series_episodes" returned %v`, n.ID) + return fmt.Errorf(`unexpected foreign-key "series_id" returned %v`, n.ID) } for i := range nodes { assign(nodes[i], n) @@ -466,6 +455,9 @@ func (eq *EpisodeQuery) querySpec() *sqlgraph.QuerySpec { _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) } } + if eq.withSeries != nil { + _spec.Node.AddColumnOnce(episode.FieldSeriesID) + } } if ps := eq.predicates; len(ps) > 0 { _spec.Predicate = func(selector *sql.Selector) { diff --git a/ent/episode_update.go b/ent/episode_update.go index 4c2b7b0..e0e9150 100644 --- a/ent/episode_update.go +++ b/ent/episode_update.go @@ -28,6 +28,26 @@ func (eu *EpisodeUpdate) Where(ps ...predicate.Episode) *EpisodeUpdate { return eu } +// SetSeriesID sets the "series_id" field. +func (eu *EpisodeUpdate) SetSeriesID(i int) *EpisodeUpdate { + eu.mutation.SetSeriesID(i) + return eu +} + +// SetNillableSeriesID sets the "series_id" field if the given value is not nil. +func (eu *EpisodeUpdate) SetNillableSeriesID(i *int) *EpisodeUpdate { + if i != nil { + eu.SetSeriesID(*i) + } + return eu +} + +// ClearSeriesID clears the value of the "series_id" field. +func (eu *EpisodeUpdate) ClearSeriesID() *EpisodeUpdate { + eu.mutation.ClearSeriesID() + return eu +} + // SetSeasonNumber sets the "season_number" field. func (eu *EpisodeUpdate) SetSeasonNumber(i int) *EpisodeUpdate { eu.mutation.ResetSeasonNumber() @@ -112,20 +132,6 @@ func (eu *EpisodeUpdate) SetNillableAirDate(s *string) *EpisodeUpdate { return eu } -// SetSeriesID sets the "series" edge to the Series entity by ID. -func (eu *EpisodeUpdate) SetSeriesID(id int) *EpisodeUpdate { - eu.mutation.SetSeriesID(id) - return eu -} - -// SetNillableSeriesID sets the "series" edge to the Series entity by ID if the given value is not nil. -func (eu *EpisodeUpdate) SetNillableSeriesID(id *int) *EpisodeUpdate { - if id != nil { - eu = eu.SetSeriesID(*id) - } - return eu -} - // SetSeries sets the "series" edge to the Series entity. func (eu *EpisodeUpdate) SetSeries(s *Series) *EpisodeUpdate { return eu.SetSeriesID(s.ID) @@ -248,6 +254,26 @@ type EpisodeUpdateOne struct { mutation *EpisodeMutation } +// SetSeriesID sets the "series_id" field. +func (euo *EpisodeUpdateOne) SetSeriesID(i int) *EpisodeUpdateOne { + euo.mutation.SetSeriesID(i) + return euo +} + +// SetNillableSeriesID sets the "series_id" field if the given value is not nil. +func (euo *EpisodeUpdateOne) SetNillableSeriesID(i *int) *EpisodeUpdateOne { + if i != nil { + euo.SetSeriesID(*i) + } + return euo +} + +// ClearSeriesID clears the value of the "series_id" field. +func (euo *EpisodeUpdateOne) ClearSeriesID() *EpisodeUpdateOne { + euo.mutation.ClearSeriesID() + return euo +} + // SetSeasonNumber sets the "season_number" field. func (euo *EpisodeUpdateOne) SetSeasonNumber(i int) *EpisodeUpdateOne { euo.mutation.ResetSeasonNumber() @@ -332,20 +358,6 @@ func (euo *EpisodeUpdateOne) SetNillableAirDate(s *string) *EpisodeUpdateOne { return euo } -// SetSeriesID sets the "series" edge to the Series entity by ID. -func (euo *EpisodeUpdateOne) SetSeriesID(id int) *EpisodeUpdateOne { - euo.mutation.SetSeriesID(id) - return euo -} - -// SetNillableSeriesID sets the "series" edge to the Series entity by ID if the given value is not nil. -func (euo *EpisodeUpdateOne) SetNillableSeriesID(id *int) *EpisodeUpdateOne { - if id != nil { - euo = euo.SetSeriesID(*id) - } - return euo -} - // SetSeries sets the "series" edge to the Series entity. func (euo *EpisodeUpdateOne) SetSeries(s *Series) *EpisodeUpdateOne { return euo.SetSeriesID(s.ID) diff --git a/ent/migrate/schema.go b/ent/migrate/schema.go index 23a790b..e0b1238 100644 --- a/ent/migrate/schema.go +++ b/ent/migrate/schema.go @@ -37,7 +37,7 @@ var ( {Name: "title", Type: field.TypeString}, {Name: "overview", Type: field.TypeString}, {Name: "air_date", Type: field.TypeString}, - {Name: "series_episodes", Type: field.TypeInt, Nullable: true}, + {Name: "series_id", Type: field.TypeInt, Nullable: true}, } // EpisodesTable holds the schema information for the "episodes" table. EpisodesTable = &schema.Table{ diff --git a/ent/mutation.go b/ent/mutation.go index 6e499d0..4496c83 100644 --- a/ent/mutation.go +++ b/ent/mutation.go @@ -1022,6 +1022,55 @@ func (m *EpisodeMutation) IDs(ctx context.Context) ([]int, error) { } } +// SetSeriesID sets the "series_id" field. +func (m *EpisodeMutation) SetSeriesID(i int) { + m.series = &i +} + +// SeriesID returns the value of the "series_id" field in the mutation. +func (m *EpisodeMutation) SeriesID() (r int, exists bool) { + v := m.series + if v == nil { + return + } + return *v, true +} + +// OldSeriesID returns the old "series_id" 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) OldSeriesID(ctx context.Context) (v int, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSeriesID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSeriesID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSeriesID: %w", err) + } + return oldValue.SeriesID, nil +} + +// ClearSeriesID clears the value of the "series_id" field. +func (m *EpisodeMutation) ClearSeriesID() { + m.series = nil + m.clearedFields[episode.FieldSeriesID] = struct{}{} +} + +// SeriesIDCleared returns if the "series_id" field was cleared in this mutation. +func (m *EpisodeMutation) SeriesIDCleared() bool { + _, ok := m.clearedFields[episode.FieldSeriesID] + return ok +} + +// ResetSeriesID resets all changes to the "series_id" field. +func (m *EpisodeMutation) ResetSeriesID() { + m.series = nil + delete(m.clearedFields, episode.FieldSeriesID) +} + // SetSeasonNumber sets the "season_number" field. func (m *EpisodeMutation) SetSeasonNumber(i int) { m.season_number = &i @@ -1242,27 +1291,15 @@ func (m *EpisodeMutation) ResetAirDate() { m.air_date = nil } -// SetSeriesID sets the "series" edge to the Series entity by id. -func (m *EpisodeMutation) SetSeriesID(id int) { - m.series = &id -} - // ClearSeries clears the "series" edge to the Series entity. func (m *EpisodeMutation) ClearSeries() { m.clearedseries = true + m.clearedFields[episode.FieldSeriesID] = struct{}{} } // SeriesCleared reports if the "series" edge to the Series entity was cleared. func (m *EpisodeMutation) SeriesCleared() bool { - return m.clearedseries -} - -// SeriesID returns the "series" edge ID in the mutation. -func (m *EpisodeMutation) SeriesID() (id int, exists bool) { - if m.series != nil { - return *m.series, true - } - return + return m.SeriesIDCleared() || m.clearedseries } // SeriesIDs returns the "series" edge IDs in the mutation. @@ -1315,7 +1352,10 @@ 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, 5) + fields := make([]string, 0, 6) + if m.series != nil { + fields = append(fields, episode.FieldSeriesID) + } if m.season_number != nil { fields = append(fields, episode.FieldSeasonNumber) } @@ -1339,6 +1379,8 @@ func (m *EpisodeMutation) Fields() []string { // schema. func (m *EpisodeMutation) Field(name string) (ent.Value, bool) { switch name { + case episode.FieldSeriesID: + return m.SeriesID() case episode.FieldSeasonNumber: return m.SeasonNumber() case episode.FieldEpisodeNumber: @@ -1358,6 +1400,8 @@ func (m *EpisodeMutation) Field(name string) (ent.Value, bool) { // database failed. func (m *EpisodeMutation) OldField(ctx context.Context, name string) (ent.Value, error) { switch name { + case episode.FieldSeriesID: + return m.OldSeriesID(ctx) case episode.FieldSeasonNumber: return m.OldSeasonNumber(ctx) case episode.FieldEpisodeNumber: @@ -1377,6 +1421,13 @@ func (m *EpisodeMutation) OldField(ctx context.Context, name string) (ent.Value, // type. func (m *EpisodeMutation) SetField(name string, value ent.Value) error { switch name { + case episode.FieldSeriesID: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSeriesID(v) + return nil case episode.FieldSeasonNumber: v, ok := value.(int) if !ok { @@ -1468,7 +1519,11 @@ func (m *EpisodeMutation) AddField(name string, value ent.Value) error { // ClearedFields returns all nullable fields that were cleared during this // mutation. func (m *EpisodeMutation) ClearedFields() []string { - return nil + var fields []string + if m.FieldCleared(episode.FieldSeriesID) { + fields = append(fields, episode.FieldSeriesID) + } + return fields } // FieldCleared returns a boolean indicating if a field with the given name was @@ -1481,6 +1536,11 @@ func (m *EpisodeMutation) FieldCleared(name string) bool { // ClearField clears the value of the field with the given name. It returns an // error if the field is not defined in the schema. func (m *EpisodeMutation) ClearField(name string) error { + switch name { + case episode.FieldSeriesID: + m.ClearSeriesID() + return nil + } return fmt.Errorf("unknown Episode nullable field %s", name) } @@ -1488,6 +1548,9 @@ func (m *EpisodeMutation) ClearField(name string) error { // It returns an error if the field is not defined in the schema. func (m *EpisodeMutation) ResetField(name string) error { switch name { + case episode.FieldSeriesID: + m.ResetSeriesID() + return nil case episode.FieldSeasonNumber: m.ResetSeasonNumber() return nil diff --git a/ent/schema/episode.go b/ent/schema/episode.go index 2beb165..b3e2d22 100644 --- a/ent/schema/episode.go +++ b/ent/schema/episode.go @@ -14,8 +14,8 @@ type Episode struct { // Fields of the Episode. func (Episode) Fields() []ent.Field { return []ent.Field{ - //field.Int("series_id"), - field.Int("season_number"), + field.Int("series_id").Optional(), + field.Int("season_number").StructTag("json:\"season_number\""), field.Int("episode_number"), field.String("title"), field.String("overview"), @@ -28,7 +28,8 @@ func (Episode) Edges() []ent.Edge { return []ent.Edge{ edge.From("series", Series.Type). Ref("episodes"). - Unique(), + Unique(). + Field("series_id"), } } diff --git a/ent/series/series.go b/ent/series/series.go index b658478..f76348b 100644 --- a/ent/series/series.go +++ b/ent/series/series.go @@ -40,7 +40,7 @@ const ( // It exists in this package in order to avoid circular dependency with the "episode" package. EpisodesInverseTable = "episodes" // EpisodesColumn is the table column denoting the episodes relation/edge. - EpisodesColumn = "series_episodes" + EpisodesColumn = "series_id" ) // Columns holds all SQL columns for series fields. diff --git a/ent/series_query.go b/ent/series_query.go index 61e6480..09a7045 100644 --- a/ent/series_query.go +++ b/ent/series_query.go @@ -412,7 +412,9 @@ func (sq *SeriesQuery) loadEpisodes(ctx context.Context, query *EpisodeQuery, no init(nodes[i]) } } - query.withFKs = true + if len(query.ctx.Fields) > 0 { + query.ctx.AppendFieldOnce(episode.FieldSeriesID) + } query.Where(predicate.Episode(func(s *sql.Selector) { s.Where(sql.InValues(s.C(series.EpisodesColumn), fks...)) })) @@ -421,13 +423,10 @@ func (sq *SeriesQuery) loadEpisodes(ctx context.Context, query *EpisodeQuery, no return err } for _, n := range neighbors { - fk := n.series_episodes - if fk == nil { - return fmt.Errorf(`foreign-key "series_episodes" is nil for node %v`, n.ID) - } - node, ok := nodeids[*fk] + fk := n.SeriesID + node, ok := nodeids[fk] if !ok { - return fmt.Errorf(`unexpected referenced foreign-key "series_episodes" returned %v for node %v`, *fk, n.ID) + return fmt.Errorf(`unexpected referenced foreign-key "series_id" returned %v for node %v`, fk, n.ID) } assign(node, n) } diff --git a/server/server.go b/server/server.go index 0ba9ceb..a98a618 100644 --- a/server/server.go +++ b/server/server.go @@ -56,6 +56,7 @@ func (s *Server) Serve() error { tv.GET("/search", HttpHandler(s.SearchTvSeries)) tv.POST("/watchlist", HttpHandler(s.AddWatchlist)) tv.GET("/watchlist", HttpHandler(s.GetWatchlist)) + tv.GET("/series/:id", HttpHandler(s.GetTvDetails)) } indexer := api.Group("/indexer") { diff --git a/server/watchlist.go b/server/watchlist.go index 38a16da..79b5c34 100644 --- a/server/watchlist.go +++ b/server/watchlist.go @@ -3,6 +3,7 @@ package server import ( "polaris/ent" "polaris/log" + "strconv" "github.com/gin-gonic/gin" "github.com/pkg/errors" @@ -81,5 +82,11 @@ func (s *Server) GetWatchlist(c *gin.Context) (interface{}, error) { func (s *Server) GetTvDetails(c *gin.Context) (interface{}, error) { - return nil, nil + ids := c.Param("id") + id, err := strconv.Atoi(ids) + if err != nil { + return nil, errors.Wrap(err, "convert") + } + detail := s.db.GetSeriesDetails(id) + return detail, nil } \ No newline at end of file diff --git a/ui/lib/APIs.dart b/ui/lib/APIs.dart index e14a91f..211b99a 100644 --- a/ui/lib/APIs.dart +++ b/ui/lib/APIs.dart @@ -3,6 +3,7 @@ class APIs { static const searchUrl = "$_baseUrl/api/v1/tv/search"; static const settingsUrl = "$_baseUrl/api/v1/setting/do"; static const watchlistUrl = "$_baseUrl/api/v1/tv/watchlist"; + static const seriesDetailUrl = "$_baseUrl/api/v1/tv/series/"; static const tmdbImgBaseUrl = "https://image.tmdb.org/t/p/w500/"; diff --git a/ui/lib/main.dart b/ui/lib/main.dart index fab739b..64ede81 100644 --- a/ui/lib/main.dart +++ b/ui/lib/main.dart @@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart'; import 'package:ui/navdrawer.dart'; import 'package:ui/search.dart'; import 'package:ui/system_settings.dart'; +import 'package:ui/tv_details.dart'; import 'package:ui/weclome.dart'; void main() { @@ -57,6 +58,10 @@ class MyApp extends StatelessWidget { GoRoute( path: SystemSettingsPage.route, builder: (context, state) => SystemSettingsPage(), + ), + GoRoute( + path: TvDetailsPage.route, + builder: (context, state) => TvDetailsPage(seriesId: state.pathParameters['id']!), ) ], ); diff --git a/ui/lib/navdrawer.dart b/ui/lib/navdrawer.dart index 762f8c3..300870b 100644 --- a/ui/lib/navdrawer.dart +++ b/ui/lib/navdrawer.dart @@ -42,7 +42,7 @@ class _NavDrawerState extends State { destinations: const [ NavigationRailDestination( icon: Icon(Icons.live_tv), - label: Text(' 电视剧'), + label: Text('电视剧'), ), NavigationRailDestination( icon: Icon(Icons.download), diff --git a/ui/lib/tv_details.dart b/ui/lib/tv_details.dart index 6c3f723..0e87f27 100644 --- a/ui/lib/tv_details.dart +++ b/ui/lib/tv_details.dart @@ -1,24 +1,197 @@ +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; +import 'package:ui/APIs.dart'; +import 'package:ui/server_response.dart'; +import 'package:ui/utils.dart'; class TvDetailsPage extends StatefulWidget { - @override - State createState() { - // TODO: implement createState - throw UnimplementedError(); + static const route = "/series/:id"; + + static String toRoute(int id) { + return "/series/$id"; } + final String seriesId; + + const TvDetailsPage({super.key, required this.seriesId}); + + @override + State createState() { + return _TvDetailsPageState(seriesId: seriesId); + } } class _TvDetailsPageState extends State { - final int tvId = 1; + final String seriesId; + + _TvDetailsPageState({required this.seriesId}); + + SeriesDetails? details; @override Widget build(BuildContext context) { - // TODO: implement build - throw Column( - children: [ + _querySeriesDetails(context); + if (details == null) { + return const Center( + child: Text("nothing here"), + ); + } + + Map> m = Map(); + for (final ep in details!.episodes!) { + var w = Container( + alignment: Alignment.topLeft, + child: Text( + "第 ${ep.seasonNumber} 季,第 ${ep.episodeNumber} 集:${ep.title}", + textAlign: TextAlign.left, + ), + ); + if (m[ep.seasonNumber] == null) { + m[ep.seasonNumber!] = List.empty(growable: true); + } + m[ep.seasonNumber!]!.add(w); + } + List list = List.empty(growable: true); + for (final k in m.keys) { + bool _customTileExpanded = false; + var seasonList = ExpansionTile( + initiallyExpanded: true, + title: Text("第 $k 季"), + trailing: Icon( + _customTileExpanded + ? Icons.arrow_drop_down_circle + : Icons.arrow_drop_down, + ), + children: m[k]!, + onExpansionChanged: (bool expanded) { + setState(() { + _customTileExpanded = expanded; + }); + }, + ); + list.add(seasonList); + } + + return Column( + children: [ + Card( + margin: const EdgeInsets.all(4), + clipBehavior: Clip.hardEdge, + child: Row( + children: [ + Flexible( + child: SizedBox( + width: 150, + height: 200, + child: Image.network( + APIs.tmdbImgBaseUrl + details!.posterPath!, + fit: BoxFit.contain, + ), + ), + ), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${details!.name}", + style: const TextStyle( + fontSize: 14, fontWeight: FontWeight.bold), + ), + const Text(""), + Text(details!.overview!) + ], + ), + ), + ], + ), + ), + Expanded( + child: ListView( + children: list, + ), + ), ], - ) + ); } - -} \ No newline at end of file + + void _querySeriesDetails(BuildContext context) async { + var resp = await Dio().get("${APIs.seriesDetailUrl}$seriesId"); + var rsp = ServerResponse.fromJson(resp.data); + if (rsp.code != 0 && context.mounted) { + Utils.showAlertDialog(context, rsp.message); + } + setState(() { + details = SeriesDetails.fromJson(rsp.data); + }); + } +} + +class SeriesDetails { + int? id; + int? tmdbId; + String? name; + String? originalName; + String? overview; + String? path; + String? posterPath; + String? createdAt; + List? episodes; + + SeriesDetails( + {this.id, + this.tmdbId, + this.name, + this.originalName, + this.overview, + this.path, + this.posterPath, + this.createdAt, + this.episodes}); + + SeriesDetails.fromJson(Map json) { + id = json['id']; + tmdbId = json['tmdb_id']; + name = json['name']; + originalName = json['original_name']; + overview = json['overview']; + path = json['path']; + posterPath = json['poster_path']; + createdAt = json['created_at']; + if (json['episodes'] != null) { + episodes = []; + json['episodes'].forEach((v) { + episodes!.add(Episodes.fromJson(v)); + }); + } + } +} + +class Episodes { + int? id; + int? seriesId; + int? episodeNumber; + String? title; + String? airDate; + int? seasonNumber; + String? overview; + + Episodes( + {this.id, + this.seriesId, + this.episodeNumber, + this.title, + this.airDate, + this.seasonNumber, + this.overview}); + + Episodes.fromJson(Map json) { + id = json['id']; + seriesId = json['series_id']; + episodeNumber = json['episode_number']; + title = json['title']; + airDate = json['air_date']; + seasonNumber = json['season_number']; + overview = json['overview']; + } +} diff --git a/ui/lib/weclome.dart b/ui/lib/weclome.dart index a44d956..f381916 100644 --- a/ui/lib/weclome.dart +++ b/ui/lib/weclome.dart @@ -1,7 +1,9 @@ import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:ui/APIs.dart'; import 'package:ui/server_response.dart'; +import 'package:ui/tv_details.dart'; class WelcomePage extends StatefulWidget { const WelcomePage({super.key}); @@ -20,44 +22,45 @@ class _WeclomePageState extends State { Widget build(BuildContext context) { _onRefresh(); return GridView.builder( - itemCount: favList.length, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4), - itemBuilder: (context, i) { - var item = TvSeries.fromJson(favList[i]); - return Container( - child: Card( - margin: const EdgeInsets.all(4), - clipBehavior: Clip.hardEdge, - child: InkWell( - //splashColor: Colors.blue.withAlpha(30), - onTap: () { - //showDialog(context: context, builder: builder) - }, - child: Column( - children: [ - Flexible( - child: SizedBox( - width: 300, - height: 600, - child: Image.network( - APIs.tmdbImgBaseUrl + item.posterPath!, - fit: BoxFit.contain, - ), - ), + itemCount: favList.length, + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4), + itemBuilder: (context, i) { + var item = TvSeries.fromJson(favList[i]); + return Container( + child: Card( + margin: const EdgeInsets.all(4), + clipBehavior: Clip.hardEdge, + child: InkWell( + //splashColor: Colors.blue.withAlpha(30), + onTap: () { + context.go(TvDetailsPage.toRoute(item.id!)); + //showDialog(context: context, builder: builder) + }, + child: Column( + children: [ + Flexible( + child: SizedBox( + width: 300, + height: 600, + child: Image.network( + APIs.tmdbImgBaseUrl + item.posterPath!, + fit: BoxFit.contain, ), - Flexible( - child: Text( - item.name!, - style: const TextStyle( - fontSize: 14, fontWeight: FontWeight.bold), - ), - ) - ], + ), ), - )), - ); - }); + Flexible( + child: Text( + item.name!, + style: const TextStyle( + fontSize: 14, fontWeight: FontWeight.bold), + ), + ) + ], + ), + )), + ); + }); } Future _onRefresh() async {