feat: episode status

This commit is contained in:
Simon Ding
2024-07-15 14:41:49 +08:00
parent 68897e85f9
commit 9a3a4addeb
15 changed files with 288 additions and 50 deletions

View File

@@ -431,3 +431,8 @@ func (c *Client) UpdateEpisodeFile(seriesID int, seasonNum, episodeNum int, file
}
return ep.Update().SetFileInStorage(file).Exec(context.TODO())
}
func (c *Client) SetEpisodeStatus(id int, status episode.Status) error {
return c.ent.Episode.Update().Where(episode.ID(id)).SetStatus(status).Exec(context.TODO())
}

View File

@@ -29,6 +29,8 @@ type Episode struct {
Overview string `json:"overview,omitempty"`
// AirDate holds the value of the "air_date" field.
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.
@@ -64,7 +66,7 @@ func (*Episode) scanValues(columns []string) ([]any, error) {
switch columns[i] {
case episode.FieldID, episode.FieldSeriesID, episode.FieldSeasonNumber, episode.FieldEpisodeNumber:
values[i] = new(sql.NullInt64)
case episode.FieldTitle, episode.FieldOverview, episode.FieldAirDate, episode.FieldFileInStorage:
case episode.FieldTitle, episode.FieldOverview, episode.FieldAirDate, episode.FieldStatus, episode.FieldFileInStorage:
values[i] = new(sql.NullString)
default:
values[i] = new(sql.UnknownType)
@@ -123,6 +125,12 @@ func (e *Episode) assignValues(columns []string, values []any) error {
} else if value.Valid {
e.AirDate = value.String
}
case episode.FieldStatus:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field status", values[i])
} 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])
@@ -188,6 +196,9 @@ func (e *Episode) String() string {
builder.WriteString("air_date=")
builder.WriteString(e.AirDate)
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(')')

View File

@@ -3,6 +3,8 @@
package episode
import (
"fmt"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
)
@@ -24,6 +26,8 @@ const (
FieldOverview = "overview"
// FieldAirDate holds the string denoting the air_date field in the database.
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"
// EdgeSeries holds the string denoting the series edge name in mutations.
@@ -48,6 +52,7 @@ var Columns = []string{
FieldTitle,
FieldOverview,
FieldAirDate,
FieldStatus,
FieldFileInStorage,
}
@@ -61,6 +66,33 @@ func ValidColumn(column string) bool {
return false
}
// Status defines the type for the "status" enum field.
type Status string
// StatusMissing is the default value of the Status enum.
const DefaultStatus = StatusMissing
// Status values.
const (
StatusMissing Status = "missing"
StatusDownloading Status = "downloading"
StatusDownloaded Status = "downloaded"
)
func (s Status) String() string {
return string(s)
}
// StatusValidator is a validator for the "status" field enum values. It is called by the builders before save.
func StatusValidator(s Status) error {
switch s {
case StatusMissing, StatusDownloading, StatusDownloaded:
return nil
default:
return fmt.Errorf("episode: invalid enum value for status field: %q", s)
}
}
// OrderOption defines the ordering options for the Episode queries.
type OrderOption func(*sql.Selector)
@@ -99,6 +131,11 @@ func ByAirDate(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldAirDate, opts...).ToFunc()
}
// ByStatus orders the results by the status field.
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()

View File

@@ -394,6 +394,26 @@ func AirDateContainsFold(v string) predicate.Episode {
return predicate.Episode(sql.FieldContainsFold(FieldAirDate, v))
}
// StatusEQ applies the EQ predicate on the "status" field.
func StatusEQ(v Status) predicate.Episode {
return predicate.Episode(sql.FieldEQ(FieldStatus, v))
}
// StatusNEQ applies the NEQ predicate on the "status" field.
func StatusNEQ(v Status) predicate.Episode {
return predicate.Episode(sql.FieldNEQ(FieldStatus, v))
}
// StatusIn applies the In predicate on the "status" field.
func StatusIn(vs ...Status) predicate.Episode {
return predicate.Episode(sql.FieldIn(FieldStatus, vs...))
}
// StatusNotIn applies the NotIn predicate on the "status" field.
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))

View File

@@ -64,6 +64,20 @@ func (ec *EpisodeCreate) SetAirDate(s string) *EpisodeCreate {
return ec
}
// SetStatus sets the "status" field.
func (ec *EpisodeCreate) SetStatus(e episode.Status) *EpisodeCreate {
ec.mutation.SetStatus(e)
return ec
}
// SetNillableStatus sets the "status" field if the given value is not nil.
func (ec *EpisodeCreate) SetNillableStatus(e *episode.Status) *EpisodeCreate {
if e != nil {
ec.SetStatus(*e)
}
return ec
}
// SetFileInStorage sets the "file_in_storage" field.
func (ec *EpisodeCreate) SetFileInStorage(s string) *EpisodeCreate {
ec.mutation.SetFileInStorage(s)
@@ -90,6 +104,7 @@ func (ec *EpisodeCreate) Mutation() *EpisodeMutation {
// Save creates the Episode in the database.
func (ec *EpisodeCreate) Save(ctx context.Context) (*Episode, error) {
ec.defaults()
return withHooks(ctx, ec.sqlSave, ec.mutation, ec.hooks)
}
@@ -115,6 +130,14 @@ func (ec *EpisodeCreate) ExecX(ctx context.Context) {
}
}
// defaults sets the default values of the builder before save.
func (ec *EpisodeCreate) defaults() {
if _, ok := ec.mutation.Status(); !ok {
v := episode.DefaultStatus
ec.mutation.SetStatus(v)
}
}
// check runs all checks and user-defined validators on the builder.
func (ec *EpisodeCreate) check() error {
if _, ok := ec.mutation.SeasonNumber(); !ok {
@@ -132,6 +155,14 @@ func (ec *EpisodeCreate) check() error {
if _, ok := ec.mutation.AirDate(); !ok {
return &ValidationError{Name: "air_date", err: errors.New(`ent: missing required field "Episode.air_date"`)}
}
if _, ok := ec.mutation.Status(); !ok {
return &ValidationError{Name: "status", err: errors.New(`ent: missing required field "Episode.status"`)}
}
if v, ok := ec.mutation.Status(); ok {
if err := episode.StatusValidator(v); err != nil {
return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Episode.status": %w`, err)}
}
}
return nil
}
@@ -178,6 +209,10 @@ func (ec *EpisodeCreate) createSpec() (*Episode, *sqlgraph.CreateSpec) {
_spec.SetField(episode.FieldAirDate, field.TypeString, value)
_node.AirDate = value
}
if value, ok := ec.mutation.Status(); ok {
_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
@@ -220,6 +255,7 @@ func (ecb *EpisodeCreateBulk) Save(ctx context.Context) ([]*Episode, error) {
for i := range ecb.builders {
func(i int, root context.Context) {
builder := ecb.builders[i]
builder.defaults()
var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
mutation, ok := m.(*EpisodeMutation)
if !ok {

View File

@@ -132,6 +132,20 @@ func (eu *EpisodeUpdate) SetNillableAirDate(s *string) *EpisodeUpdate {
return eu
}
// SetStatus sets the "status" field.
func (eu *EpisodeUpdate) SetStatus(e episode.Status) *EpisodeUpdate {
eu.mutation.SetStatus(e)
return eu
}
// SetNillableStatus sets the "status" field if the given value is not nil.
func (eu *EpisodeUpdate) SetNillableStatus(e *episode.Status) *EpisodeUpdate {
if e != nil {
eu.SetStatus(*e)
}
return eu
}
// SetFileInStorage sets the "file_in_storage" field.
func (eu *EpisodeUpdate) SetFileInStorage(s string) *EpisodeUpdate {
eu.mutation.SetFileInStorage(s)
@@ -195,7 +209,20 @@ func (eu *EpisodeUpdate) ExecX(ctx context.Context) {
}
}
// check runs all checks and user-defined validators on the builder.
func (eu *EpisodeUpdate) check() error {
if v, ok := eu.mutation.Status(); ok {
if err := episode.StatusValidator(v); err != nil {
return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Episode.status": %w`, err)}
}
}
return nil
}
func (eu *EpisodeUpdate) sqlSave(ctx context.Context) (n int, err error) {
if err := eu.check(); err != nil {
return n, err
}
_spec := sqlgraph.NewUpdateSpec(episode.Table, episode.Columns, sqlgraph.NewFieldSpec(episode.FieldID, field.TypeInt))
if ps := eu.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
@@ -225,6 +252,9 @@ func (eu *EpisodeUpdate) sqlSave(ctx context.Context) (n int, err error) {
if value, ok := eu.mutation.AirDate(); ok {
_spec.SetField(episode.FieldAirDate, field.TypeString, value)
}
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)
}
@@ -384,6 +414,20 @@ func (euo *EpisodeUpdateOne) SetNillableAirDate(s *string) *EpisodeUpdateOne {
return euo
}
// SetStatus sets the "status" field.
func (euo *EpisodeUpdateOne) SetStatus(e episode.Status) *EpisodeUpdateOne {
euo.mutation.SetStatus(e)
return euo
}
// SetNillableStatus sets the "status" field if the given value is not nil.
func (euo *EpisodeUpdateOne) SetNillableStatus(e *episode.Status) *EpisodeUpdateOne {
if e != nil {
euo.SetStatus(*e)
}
return euo
}
// SetFileInStorage sets the "file_in_storage" field.
func (euo *EpisodeUpdateOne) SetFileInStorage(s string) *EpisodeUpdateOne {
euo.mutation.SetFileInStorage(s)
@@ -460,7 +504,20 @@ func (euo *EpisodeUpdateOne) ExecX(ctx context.Context) {
}
}
// check runs all checks and user-defined validators on the builder.
func (euo *EpisodeUpdateOne) check() error {
if v, ok := euo.mutation.Status(); ok {
if err := episode.StatusValidator(v); err != nil {
return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Episode.status": %w`, err)}
}
}
return nil
}
func (euo *EpisodeUpdateOne) sqlSave(ctx context.Context) (_node *Episode, err error) {
if err := euo.check(); err != nil {
return _node, err
}
_spec := sqlgraph.NewUpdateSpec(episode.Table, episode.Columns, sqlgraph.NewFieldSpec(episode.FieldID, field.TypeInt))
id, ok := euo.mutation.ID()
if !ok {
@@ -507,6 +564,9 @@ func (euo *EpisodeUpdateOne) sqlSave(ctx context.Context) (_node *Episode, err e
if value, ok := euo.mutation.AirDate(); ok {
_spec.SetField(episode.FieldAirDate, field.TypeString, value)
}
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)
}

View File

@@ -37,6 +37,7 @@ var (
{Name: "title", Type: field.TypeString},
{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: "series_id", Type: field.TypeInt, Nullable: true},
}
@@ -48,7 +49,7 @@ var (
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "episodes_series_episodes",
Columns: []*schema.Column{EpisodesColumns[7]},
Columns: []*schema.Column{EpisodesColumns[8]},
RefColumns: []*schema.Column{SeriesColumns[0]},
OnDelete: schema.SetNull,
},

View File

@@ -918,6 +918,7 @@ type EpisodeMutation struct {
title *string
overview *string
air_date *string
status *episode.Status
file_in_storage *string
clearedFields map[string]struct{}
series *int
@@ -1294,6 +1295,42 @@ func (m *EpisodeMutation) ResetAirDate() {
m.air_date = nil
}
// SetStatus sets the "status" field.
func (m *EpisodeMutation) SetStatus(e episode.Status) {
m.status = &e
}
// Status returns the value of the "status" field in the mutation.
func (m *EpisodeMutation) Status() (r episode.Status, exists bool) {
v := m.status
if v == nil {
return
}
return *v, true
}
// OldStatus returns the old "status" 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) OldStatus(ctx context.Context) (v episode.Status, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldStatus is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldStatus requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldStatus: %w", err)
}
return oldValue.Status, nil
}
// ResetStatus resets all changes to the "status" field.
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
@@ -1404,7 +1441,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, 7)
fields := make([]string, 0, 8)
if m.series != nil {
fields = append(fields, episode.FieldSeriesID)
}
@@ -1423,6 +1460,9 @@ func (m *EpisodeMutation) Fields() []string {
if m.air_date != nil {
fields = append(fields, episode.FieldAirDate)
}
if m.status != nil {
fields = append(fields, episode.FieldStatus)
}
if m.file_in_storage != nil {
fields = append(fields, episode.FieldFileInStorage)
}
@@ -1446,6 +1486,8 @@ func (m *EpisodeMutation) Field(name string) (ent.Value, bool) {
return m.Overview()
case episode.FieldAirDate:
return m.AirDate()
case episode.FieldStatus:
return m.Status()
case episode.FieldFileInStorage:
return m.FileInStorage()
}
@@ -1469,6 +1511,8 @@ func (m *EpisodeMutation) OldField(ctx context.Context, name string) (ent.Value,
return m.OldOverview(ctx)
case episode.FieldAirDate:
return m.OldAirDate(ctx)
case episode.FieldStatus:
return m.OldStatus(ctx)
case episode.FieldFileInStorage:
return m.OldFileInStorage(ctx)
}
@@ -1522,6 +1566,13 @@ func (m *EpisodeMutation) SetField(name string, value ent.Value) error {
}
m.SetAirDate(v)
return nil
case episode.FieldStatus:
v, ok := value.(episode.Status)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetStatus(v)
return nil
case episode.FieldFileInStorage:
v, ok := value.(string)
if !ok {
@@ -1638,6 +1689,9 @@ func (m *EpisodeMutation) ResetField(name string) error {
case episode.FieldAirDate:
m.ResetAirDate()
return nil
case episode.FieldStatus:
m.ResetStatus()
return nil
case episode.FieldFileInStorage:
m.ResetFileInStorage()
return nil

View File

@@ -45,6 +45,8 @@ func init() {
downloadclientsDescTags := downloadclientsFields[10].Descriptor()
// downloadclients.DefaultTags holds the default value on creation for the tags field.
downloadclients.DefaultTags = downloadclientsDescTags.Default.(string)
episodeFields := schema.Episode{}.Fields()
_ = episodeFields
indexersFields := schema.Indexers{}.Fields()
_ = indexersFields
// indexersDescEnableRss is the schema descriptor for enable_rss field.

View File

@@ -20,6 +20,7 @@ func (Episode) Fields() []ent.Field {
field.String("title"),
field.String("overview"),
field.String("air_date"),
field.Enum("status").Values("missing", "downloading", "downloaded").Default("missing"),
field.String("file_in_storage").Optional(),
}
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"polaris/db"
"polaris/ent"
"polaris/ent/episode"
"polaris/ent/history"
"polaris/log"
"polaris/pkg/torznab"
@@ -123,6 +124,7 @@ func (s *Server) searchAndDownload(seriesId, seasonNum, episodeNum int) (*string
Status: history.StatusRunning,
Saved: torrent.Save(),
})
s.db.SetEpisodeStatus(ep.ID, episode.StatusDownloading)
if err != nil {
return nil, errors.Wrap(err, "save record")
}

View File

@@ -3,6 +3,7 @@ package server
import (
"path/filepath"
"polaris/ent"
"polaris/ent/episode"
"polaris/ent/history"
storage1 "polaris/ent/storage"
"polaris/log"
@@ -17,7 +18,7 @@ import (
func (s *Server) scheduler() {
s.mustAddCron("@every 1m", s.checkTasks)
s.mustAddCron("@every 10m", s.checkAllFiles)
//s.mustAddCron("@every 1h", s.checkAllFiles)
s.cron.Start()
}
@@ -59,10 +60,13 @@ func (s *Server) moveCompletedTask(id int) (err error) {
defer func () {
if err != nil {
s.db.SetHistoryStatus(r.ID, history.StatusFail)
s.db.SetEpisodeStatus(r.EpisodeID, episode.StatusMissing)
} else {
torrent.Remove()
delete(s.tasks, r.ID)
s.db.SetHistoryStatus(r.ID, history.StatusSuccess)
s.db.SetEpisodeStatus(r.EpisodeID, episode.StatusDownloaded)
torrent.Remove()
}
}()

View File

@@ -32,9 +32,10 @@ class ActivityPage extends ConsumerWidget {
DataCell(Text("${activity.date!.toLocal()}")),
DataCell(() {
if (activity.status == "uploading") {
return const MyProgressIndicator(
size: 20,
);
return const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator());
} else if (activity.status == "fail") {
return const Icon(
Icons.close,

View File

@@ -102,6 +102,7 @@ class Episodes {
String? airDate;
int? seasonNumber;
String? overview;
String? status;
Episodes(
{this.id,
@@ -110,6 +111,7 @@ class Episodes {
this.title,
this.airDate,
this.seasonNumber,
this.status,
this.overview});
Episodes.fromJson(Map<String, dynamic> json) {
@@ -119,6 +121,7 @@ class Episodes {
title = json['title'];
airDate = json['air_date'];
seasonNumber = json['season_number'];
status = json['status'];
overview = json['overview'];
}
}

View File

@@ -46,30 +46,24 @@ class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
builder: (context, snapshot) {
return seriesDetails.when(
data: (details) {
Map<int, List<Widget>> m = Map();
Map<int, List<DataRow>> m = Map();
for (final ep in details.episodes!) {
var w = Container(
alignment: Alignment.topLeft,
child: Row(
children: [
SizedBox(
width: 70,
child: Text("${ep.episodeNumber}"),
),
SizedBox(
width: 100,
child: Opacity(
var row = DataRow(cells: [
DataCell(Text("${ep.episodeNumber}")),
DataCell(Text("${ep.title}")),
DataCell(Opacity(
opacity: 0.5,
child: Text("${ep.airDate}"),
),
),
Text("${ep.title}", textAlign: TextAlign.left),
const Expanded(child: Text("")),
IconButton(
)),
DataCell(ep.status == "dwnloading"
? const Icon(Icons.cloud_download)
: (ep.status == "dwnloaded"
? const Icon(Icons.cloud_done)
: const Icon(Icons.cloud_off))),
DataCell(IconButton(
onPressed: () async {
var f = ref
.read(
seriesDetailsProvider(seriesId).notifier)
.read(seriesDetailsProvider(seriesId).notifier)
.searchAndDownload(seriesId, ep.seasonNumber!,
ep.episodeNumber!);
setState(() {
@@ -80,23 +74,31 @@ class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
Utils.showSnakeBar(context, "开始下载: $name");
}
},
icon: const Icon(Icons.search))
],
),
);
icon: const Icon(Icons.search)))
]);
if (m[ep.seasonNumber] == null) {
m[ep.seasonNumber!] = List.empty(growable: true);
}
m[ep.seasonNumber!]!.add(w);
m[ep.seasonNumber!]!.add(row);
}
List<ExpansionTile> list = List.empty(growable: true);
for (final k in m.keys.toList().reversed) {
var seasonList = ExpansionTile(
tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
initiallyExpanded: k == 0 ? false : true,
//childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
initiallyExpanded: false,
title: k == 0 ? const Text("特集") : Text("$k"),
children: m[k]!,
expandedCrossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DataTable(columns: const [
DataColumn(label: Text("#")),
DataColumn(label: SizedBox(width: 500, child: Text("标题"),)),
DataColumn(label: Text("播出时间")),
DataColumn(label: Text("状态")),
DataColumn(label: Text("操作"))
], rows: m[k]!),
],
);
list.add(seasonList);
}
@@ -145,7 +147,6 @@ class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
Text("$error"),
loading: () =>
const MyProgressIndicator()),
],
),
const Divider(thickness: 1, height: 1),