feat: change method to monitor episodes

This commit is contained in:
Simon Ding
2024-08-02 12:52:54 +08:00
parent 3aeecac4fb
commit 5132714247
14 changed files with 219 additions and 68 deletions

View File

@@ -31,6 +31,8 @@ type Episode struct {
AirDate string `json:"air_date,omitempty"`
// Status holds the value of the "status" field.
Status episode.Status `json:"status,omitempty"`
// Monitored holds the value of the "monitored" field.
Monitored bool `json:"monitored"`
// 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"`
@@ -62,6 +64,8 @@ func (*Episode) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case episode.FieldMonitored:
values[i] = new(sql.NullBool)
case episode.FieldID, episode.FieldMediaID, episode.FieldSeasonNumber, episode.FieldEpisodeNumber:
values[i] = new(sql.NullInt64)
case episode.FieldTitle, episode.FieldOverview, episode.FieldAirDate, episode.FieldStatus:
@@ -129,6 +133,12 @@ func (e *Episode) assignValues(columns []string, values []any) error {
} else if value.Valid {
e.Status = episode.Status(value.String)
}
case episode.FieldMonitored:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field monitored", values[i])
} else if value.Valid {
e.Monitored = value.Bool
}
default:
e.selectValues.Set(columns[i], values[i])
}
@@ -190,6 +200,9 @@ func (e *Episode) String() string {
builder.WriteString(", ")
builder.WriteString("status=")
builder.WriteString(fmt.Sprintf("%v", e.Status))
builder.WriteString(", ")
builder.WriteString("monitored=")
builder.WriteString(fmt.Sprintf("%v", e.Monitored))
builder.WriteByte(')')
return builder.String()
}

View File

@@ -28,6 +28,8 @@ const (
FieldAirDate = "air_date"
// FieldStatus holds the string denoting the status field in the database.
FieldStatus = "status"
// FieldMonitored holds the string denoting the monitored field in the database.
FieldMonitored = "monitored"
// EdgeMedia holds the string denoting the media edge name in mutations.
EdgeMedia = "media"
// Table holds the table name of the episode in the database.
@@ -51,6 +53,7 @@ var Columns = []string{
FieldOverview,
FieldAirDate,
FieldStatus,
FieldMonitored,
}
// ValidColumn reports if the column name is valid (part of the table columns).
@@ -63,6 +66,11 @@ func ValidColumn(column string) bool {
return false
}
var (
// DefaultMonitored holds the default value on creation for the "monitored" field.
DefaultMonitored bool
)
// Status defines the type for the "status" enum field.
type Status string
@@ -133,6 +141,11 @@ func ByStatus(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldStatus, opts...).ToFunc()
}
// ByMonitored orders the results by the monitored field.
func ByMonitored(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldMonitored, opts...).ToFunc()
}
// ByMediaField orders the results by media field.
func ByMediaField(field string, opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {

View File

@@ -84,6 +84,11 @@ func AirDate(v string) predicate.Episode {
return predicate.Episode(sql.FieldEQ(FieldAirDate, v))
}
// Monitored applies equality check predicate on the "monitored" field. It's identical to MonitoredEQ.
func Monitored(v bool) predicate.Episode {
return predicate.Episode(sql.FieldEQ(FieldMonitored, v))
}
// MediaIDEQ applies the EQ predicate on the "media_id" field.
func MediaIDEQ(v int) predicate.Episode {
return predicate.Episode(sql.FieldEQ(FieldMediaID, v))
@@ -409,6 +414,16 @@ func StatusNotIn(vs ...Status) predicate.Episode {
return predicate.Episode(sql.FieldNotIn(FieldStatus, vs...))
}
// MonitoredEQ applies the EQ predicate on the "monitored" field.
func MonitoredEQ(v bool) predicate.Episode {
return predicate.Episode(sql.FieldEQ(FieldMonitored, v))
}
// MonitoredNEQ applies the NEQ predicate on the "monitored" field.
func MonitoredNEQ(v bool) predicate.Episode {
return predicate.Episode(sql.FieldNEQ(FieldMonitored, v))
}
// HasMedia applies the HasEdge predicate on the "media" edge.
func HasMedia() predicate.Episode {
return predicate.Episode(func(s *sql.Selector) {

View File

@@ -78,6 +78,20 @@ func (ec *EpisodeCreate) SetNillableStatus(e *episode.Status) *EpisodeCreate {
return ec
}
// SetMonitored sets the "monitored" field.
func (ec *EpisodeCreate) SetMonitored(b bool) *EpisodeCreate {
ec.mutation.SetMonitored(b)
return ec
}
// SetNillableMonitored sets the "monitored" field if the given value is not nil.
func (ec *EpisodeCreate) SetNillableMonitored(b *bool) *EpisodeCreate {
if b != nil {
ec.SetMonitored(*b)
}
return ec
}
// SetMedia sets the "media" edge to the Media entity.
func (ec *EpisodeCreate) SetMedia(m *Media) *EpisodeCreate {
return ec.SetMediaID(m.ID)
@@ -122,6 +136,10 @@ func (ec *EpisodeCreate) defaults() {
v := episode.DefaultStatus
ec.mutation.SetStatus(v)
}
if _, ok := ec.mutation.Monitored(); !ok {
v := episode.DefaultMonitored
ec.mutation.SetMonitored(v)
}
}
// check runs all checks and user-defined validators on the builder.
@@ -149,6 +167,9 @@ func (ec *EpisodeCreate) check() error {
return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Episode.status": %w`, err)}
}
}
if _, ok := ec.mutation.Monitored(); !ok {
return &ValidationError{Name: "monitored", err: errors.New(`ent: missing required field "Episode.monitored"`)}
}
return nil
}
@@ -199,6 +220,10 @@ func (ec *EpisodeCreate) createSpec() (*Episode, *sqlgraph.CreateSpec) {
_spec.SetField(episode.FieldStatus, field.TypeEnum, value)
_node.Status = value
}
if value, ok := ec.mutation.Monitored(); ok {
_spec.SetField(episode.FieldMonitored, field.TypeBool, value)
_node.Monitored = value
}
if nodes := ec.mutation.MediaIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,

View File

@@ -146,6 +146,20 @@ func (eu *EpisodeUpdate) SetNillableStatus(e *episode.Status) *EpisodeUpdate {
return eu
}
// SetMonitored sets the "monitored" field.
func (eu *EpisodeUpdate) SetMonitored(b bool) *EpisodeUpdate {
eu.mutation.SetMonitored(b)
return eu
}
// SetNillableMonitored sets the "monitored" field if the given value is not nil.
func (eu *EpisodeUpdate) SetNillableMonitored(b *bool) *EpisodeUpdate {
if b != nil {
eu.SetMonitored(*b)
}
return eu
}
// SetMedia sets the "media" edge to the Media entity.
func (eu *EpisodeUpdate) SetMedia(m *Media) *EpisodeUpdate {
return eu.SetMediaID(m.ID)
@@ -235,6 +249,9 @@ 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.Monitored(); ok {
_spec.SetField(episode.FieldMonitored, field.TypeBool, value)
}
if eu.mutation.MediaCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
@@ -402,6 +419,20 @@ func (euo *EpisodeUpdateOne) SetNillableStatus(e *episode.Status) *EpisodeUpdate
return euo
}
// SetMonitored sets the "monitored" field.
func (euo *EpisodeUpdateOne) SetMonitored(b bool) *EpisodeUpdateOne {
euo.mutation.SetMonitored(b)
return euo
}
// SetNillableMonitored sets the "monitored" field if the given value is not nil.
func (euo *EpisodeUpdateOne) SetNillableMonitored(b *bool) *EpisodeUpdateOne {
if b != nil {
euo.SetMonitored(*b)
}
return euo
}
// SetMedia sets the "media" edge to the Media entity.
func (euo *EpisodeUpdateOne) SetMedia(m *Media) *EpisodeUpdateOne {
return euo.SetMediaID(m.ID)
@@ -521,6 +552,9 @@ 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.Monitored(); ok {
_spec.SetField(episode.FieldMonitored, field.TypeBool, value)
}
if euo.mutation.MediaCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,

View File

@@ -38,6 +38,7 @@ 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: "monitored", Type: field.TypeBool, Default: false},
{Name: "media_id", Type: field.TypeInt, Nullable: true},
}
// EpisodesTable holds the schema information for the "episodes" table.
@@ -48,7 +49,7 @@ var (
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "episodes_media_episodes",
Columns: []*schema.Column{EpisodesColumns[7]},
Columns: []*schema.Column{EpisodesColumns[8]},
RefColumns: []*schema.Column{MediaColumns[0]},
OnDelete: schema.SetNull,
},

View File

@@ -921,6 +921,7 @@ type EpisodeMutation struct {
overview *string
air_date *string
status *episode.Status
monitored *bool
clearedFields map[string]struct{}
media *int
clearedmedia bool
@@ -1332,6 +1333,42 @@ func (m *EpisodeMutation) ResetStatus() {
m.status = nil
}
// SetMonitored sets the "monitored" field.
func (m *EpisodeMutation) SetMonitored(b bool) {
m.monitored = &b
}
// Monitored returns the value of the "monitored" field in the mutation.
func (m *EpisodeMutation) Monitored() (r bool, exists bool) {
v := m.monitored
if v == nil {
return
}
return *v, true
}
// OldMonitored returns the old "monitored" 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) OldMonitored(ctx context.Context) (v bool, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldMonitored is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldMonitored requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldMonitored: %w", err)
}
return oldValue.Monitored, nil
}
// ResetMonitored resets all changes to the "monitored" field.
func (m *EpisodeMutation) ResetMonitored() {
m.monitored = nil
}
// ClearMedia clears the "media" edge to the Media entity.
func (m *EpisodeMutation) ClearMedia() {
m.clearedmedia = true
@@ -1393,7 +1430,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.media != nil {
fields = append(fields, episode.FieldMediaID)
}
@@ -1415,6 +1452,9 @@ func (m *EpisodeMutation) Fields() []string {
if m.status != nil {
fields = append(fields, episode.FieldStatus)
}
if m.monitored != nil {
fields = append(fields, episode.FieldMonitored)
}
return fields
}
@@ -1437,6 +1477,8 @@ func (m *EpisodeMutation) Field(name string) (ent.Value, bool) {
return m.AirDate()
case episode.FieldStatus:
return m.Status()
case episode.FieldMonitored:
return m.Monitored()
}
return nil, false
}
@@ -1460,6 +1502,8 @@ func (m *EpisodeMutation) OldField(ctx context.Context, name string) (ent.Value,
return m.OldAirDate(ctx)
case episode.FieldStatus:
return m.OldStatus(ctx)
case episode.FieldMonitored:
return m.OldMonitored(ctx)
}
return nil, fmt.Errorf("unknown Episode field %s", name)
}
@@ -1518,6 +1562,13 @@ func (m *EpisodeMutation) SetField(name string, value ent.Value) error {
}
m.SetStatus(v)
return nil
case episode.FieldMonitored:
v, ok := value.(bool)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetMonitored(v)
return nil
}
return fmt.Errorf("unknown Episode field %s", name)
}
@@ -1624,6 +1675,9 @@ func (m *EpisodeMutation) ResetField(name string) error {
case episode.FieldStatus:
m.ResetStatus()
return nil
case episode.FieldMonitored:
m.ResetMonitored()
return nil
}
return fmt.Errorf("unknown Episode field %s", name)
}

View File

@@ -4,6 +4,7 @@ package ent
import (
"polaris/ent/downloadclients"
"polaris/ent/episode"
"polaris/ent/history"
"polaris/ent/indexers"
"polaris/ent/media"
@@ -49,6 +50,10 @@ func init() {
downloadclients.DefaultTags = downloadclientsDescTags.Default.(string)
episodeFields := schema.Episode{}.Fields()
_ = episodeFields
// episodeDescMonitored is the schema descriptor for monitored field.
episodeDescMonitored := episodeFields[7].Descriptor()
// episode.DefaultMonitored holds the default value on creation for the monitored field.
episode.DefaultMonitored = episodeDescMonitored.Default.(bool)
historyFields := schema.History{}.Fields()
_ = historyFields
// historyDescSize is the schema descriptor for size field.

View File

@@ -21,7 +21,7 @@ func (Episode) Fields() []ent.Field {
field.String("overview"),
field.String("air_date"),
field.Enum("status").Values("missing", "downloading", "downloaded").Default("missing"),
field.Bool("monitored").Default(true).StructTag("json:\"monitored\""), //whether this episode is monitored
field.Bool("monitored").Default(false).StructTag("json:\"monitored\""), //whether this episode is monitored
}
}

View File

@@ -112,7 +112,7 @@ func (c *Client) DownloadEpisodeTorrent(r1 torznab.Result, seriesId, seasonNum,
}
func (c *Client) SearchAndDownload(seriesId, seasonNum, episodeNum int) (*string, error) {
res, err := SearchEpisode(c.db, seriesId, seasonNum, episodeNum, true)
res, err := SearchTvSeries(c.db, seriesId, seasonNum, []int{episodeNum}, true)
if err != nil {
return nil, err
}

View File

@@ -11,7 +11,6 @@ import (
"polaris/pkg"
"polaris/pkg/notifier/message"
"polaris/pkg/utils"
"time"
"github.com/pkg/errors"
)
@@ -217,16 +216,9 @@ func (c *Client) downloadTvSeries() {
for _, series := range allSeries {
tvDetail := c.db.GetMediaDetails(series.ID)
for _, ep := range tvDetail.Episodes {
if !series.DownloadHistoryEpisodes { //设置不下载历史已播出剧集,只下载将来剧集
t, err := time.Parse("2006-01-02", ep.AirDate)
if err != nil {
log.Error("air date not known, skip: %v", ep.Title)
if !ep.Monitored { //未监控的剧集不去下载
continue
}
if series.CreatedAt.Sub(t) > 24*time.Hour { //剧集在加入watchlist之前不去下载
continue
}
}
if ep.Status != episode.StatusMissing { //已经下载的不去下载
continue
@@ -338,6 +330,7 @@ func (c *Client) checkSeiesNewSeason(media *ent.Media) error {
Overview: ep.Overview,
AirDate: ep.AirDate,
Status: episode.StatusMissing,
Monitored: true,
}
c.db.SaveEposideDetail2(episode)
}

View File

@@ -7,6 +7,7 @@ import (
"polaris/pkg/metadata"
"polaris/pkg/torznab"
"polaris/pkg/utils"
"slices"
"sort"
"strconv"
"strings"
@@ -15,30 +16,12 @@ import (
"github.com/pkg/errors"
)
func SearchSeasonPackage(db1 *db.Client, seriesId, seasonNum int, checkResolution bool) ([]torznab.Result, error) {
return SearchEpisode(db1, seriesId, seasonNum, -1, checkResolution)
}
func isNumberedSeries(detail *db.MediaDetails) bool {
hasSeason2 := false
season2HasEpisode1 := false
for _, ep := range detail.Episodes {
if ep.SeasonNumber == 2 {
hasSeason2 = true
if ep.EpisodeNumber == 1 {
season2HasEpisode1 = true
}
}
}
return hasSeason2 && !season2HasEpisode1 //only one 1st episode
}
func SearchEpisode(db1 *db.Client, seriesId, seasonNum, episodeNum int, checkResolution bool) ([]torznab.Result, error) {
func SearchTvSeries(db1 *db.Client, seriesId, seasonNum int, episodes []int, checkResolution bool) ([]torznab.Result, error) {
series := db1.GetMediaDetails(seriesId)
if series == nil {
return nil, fmt.Errorf("no tv series of id %v", seriesId)
}
slices.Contains(episodes, 1)
res := searchWithTorznab(db1, series.NameEn)
resCn := searchWithTorznab(db1, series.NameCn)
@@ -56,14 +39,14 @@ func SearchEpisode(db1 *db.Client, seriesId, seasonNum, episodeNum int, checkRes
continue
}
}
if isNumberedSeries(series) && episodeNum == -1 {
if isNumberedSeries(series) && len(episodes) == 0 {
//should not want season
continue
}
if episodeNum != -1 && meta.Episode != episodeNum { //not season pack, episode number equals
if len(episodes) > 0 && slices.Contains(episodes, meta.Episode) { //not season pack, episode number equals
continue
}else if episodeNum == -1 && !meta.IsSeasonPack { //want season pack, but not season pack
}else if len(episodes) == 0 && !meta.IsSeasonPack { //want season pack, but not season pack
continue
}
if checkResolution && meta.Resolution != series.Resolution.String() {
@@ -82,6 +65,21 @@ func SearchEpisode(db1 *db.Client, seriesId, seasonNum, episodeNum int, checkRes
}
func isNumberedSeries(detail *db.MediaDetails) bool {
hasSeason2 := false
season2HasEpisode1 := false
for _, ep := range detail.Episodes {
if ep.SeasonNumber == 2 {
hasSeason2 = true
if ep.EpisodeNumber == 1 {
season2HasEpisode1 = true
}
}
}
return hasSeason2 && !season2HasEpisode1 //only one 1st episode
}
func SearchMovie(db1 *db.Client, movieId int, checkResolution bool) ([]torznab.Result, error) {
movieDetail := db1.GetMediaDetails(movieId)
if movieDetail == nil {

View File

@@ -13,7 +13,7 @@ import (
func (s *Server) searchAndDownloadSeasonPackage(seriesId, seasonNum int) (*string, error) {
res, err := core.SearchSeasonPackage(s.db, seriesId, seasonNum, true)
res, err := core.SearchTvSeries(s.db, seriesId, seasonNum, nil, true)
if err != nil {
return nil, err
}
@@ -46,13 +46,13 @@ func (s *Server) SearchAvailableTorrents(c *gin.Context) (interface{}, error) {
if in.Episode == 0 {
//search season package
log.Infof("search series season package S%02d", in.Season)
res, err = core.SearchSeasonPackage(s.db, in.ID, in.Season, false)
res, err = core.SearchTvSeries(s.db, in.ID, in.Season, nil, false)
if err != nil {
return nil, errors.Wrap(err, "search season package")
}
} else {
log.Infof("search series episode S%02dE%02d", in.Season, in.Episode)
res, err = core.SearchEpisode(s.db, in.ID, in.Season, in.Episode, false)
res, err = core.SearchTvSeries(s.db, in.ID, in.Season, []int{in.Episode}, false)
if err != nil {
if err.Error() == "no resource found" {
return []string{}, nil

View File

@@ -102,12 +102,29 @@ func (s *Server) AddTv2Watchlist(c *gin.Context) (interface{}, error) {
continue
}
for _, ep := range se.Episodes {
shouldMonitor := false
//如果设置下载往期剧集则监控所有剧集。如果没有则监控未上映的剧集考虑时差等问题留24h余量
if in.DownloadHistoryEpisodes {
shouldMonitor = true
} else {
t, err := time.Parse("2006-01-02", ep.AirDate)
if err != nil {
log.Error("air date not known, will not monitor: %v", ep.AirDate)
} else {
if time.Since(t) < 24*time.Hour { //monitor episode air 24h before now
shouldMonitor = true
}
}
}
epid, err := s.db.SaveEposideDetail(&ent.Episode{
SeasonNumber: seasonId,
EpisodeNumber: ep.EpisodeNumber,
Title: ep.Name,
Overview: ep.Overview,
AirDate: ep.AirDate,
Monitored: shouldMonitor,
})
if err != nil {
log.Errorf("save episode info error: %v", err)
@@ -176,6 +193,7 @@ func (s *Server) AddMovie2Watchlist(c *gin.Context) (interface{}, error) {
Title: "dummy episode for movies",
Overview: "dummy episode for movies",
AirDate: detail.ReleaseDate,
Monitored: true,
})
if err != nil {
return nil, errors.Wrap(err, "add dummy episode")
@@ -271,31 +289,13 @@ func (s *Server) GetTvWatchlist(c *gin.Context) (interface{}, error) {
details := s.db.GetMediaDetails(item.ID)
for _, ep := range details.Episodes {
monitored := false
if ep.SeasonNumber == 0 {
continue
}
if item.DownloadHistoryEpisodes {
monitored = true
} else {
t, err := time.Parse("2006-01-02", ep.AirDate)
if err != nil { //airdate not exist, maybe airdate not set yet
monitored = true
} else {
if item.CreatedAt.Sub(t) > 24*time.Hour { //剧集在加入watchlist之前不去下载
continue
}
monitored = true
}
}
if monitored {
if ep.Monitored {
ms.MonitoredNum++
}
if ep.Status == episode.StatusDownloaded {
ms.DownloadedNum++
}
}
}
res[i] = ms
}
return res, nil