feat: option to download all episodes

This commit is contained in:
Simon Ding
2024-07-28 13:22:12 +08:00
parent 34fa05e7dd
commit fc86a441f4
13 changed files with 280 additions and 44 deletions

View File

@@ -41,6 +41,8 @@ type Media struct {
StorageID int `json:"storage_id,omitempty"`
// TargetDir holds the value of the "target_dir" field.
TargetDir string `json:"target_dir,omitempty"`
// tv series only
DownloadHistoryEpisodes bool `json:"download_history_episodes,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the MediaQuery when eager-loading is set.
Edges MediaEdges `json:"edges"`
@@ -70,6 +72,8 @@ func (*Media) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case media.FieldDownloadHistoryEpisodes:
values[i] = new(sql.NullBool)
case media.FieldID, media.FieldTmdbID, media.FieldStorageID:
values[i] = new(sql.NullInt64)
case media.FieldImdbID, media.FieldMediaType, media.FieldNameCn, media.FieldNameEn, media.FieldOriginalName, media.FieldOverview, media.FieldAirDate, media.FieldResolution, media.FieldTargetDir:
@@ -169,6 +173,12 @@ func (m *Media) assignValues(columns []string, values []any) error {
} else if value.Valid {
m.TargetDir = value.String
}
case media.FieldDownloadHistoryEpisodes:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field download_history_episodes", values[i])
} else if value.Valid {
m.DownloadHistoryEpisodes = value.Bool
}
default:
m.selectValues.Set(columns[i], values[i])
}
@@ -245,6 +255,9 @@ func (m *Media) String() string {
builder.WriteString(", ")
builder.WriteString("target_dir=")
builder.WriteString(m.TargetDir)
builder.WriteString(", ")
builder.WriteString("download_history_episodes=")
builder.WriteString(fmt.Sprintf("%v", m.DownloadHistoryEpisodes))
builder.WriteByte(')')
return builder.String()
}

View File

@@ -39,6 +39,8 @@ const (
FieldStorageID = "storage_id"
// FieldTargetDir holds the string denoting the target_dir field in the database.
FieldTargetDir = "target_dir"
// FieldDownloadHistoryEpisodes holds the string denoting the download_history_episodes field in the database.
FieldDownloadHistoryEpisodes = "download_history_episodes"
// EdgeEpisodes holds the string denoting the episodes edge name in mutations.
EdgeEpisodes = "episodes"
// Table holds the table name of the media in the database.
@@ -67,6 +69,7 @@ var Columns = []string{
FieldResolution,
FieldStorageID,
FieldTargetDir,
FieldDownloadHistoryEpisodes,
}
// ValidColumn reports if the column name is valid (part of the table columns).
@@ -84,6 +87,8 @@ var (
DefaultCreatedAt time.Time
// DefaultAirDate holds the default value on creation for the "air_date" field.
DefaultAirDate string
// DefaultDownloadHistoryEpisodes holds the default value on creation for the "download_history_episodes" field.
DefaultDownloadHistoryEpisodes bool
)
// MediaType defines the type for the "media_type" enum field.
@@ -204,6 +209,11 @@ func ByTargetDir(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldTargetDir, opts...).ToFunc()
}
// ByDownloadHistoryEpisodes orders the results by the download_history_episodes field.
func ByDownloadHistoryEpisodes(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldDownloadHistoryEpisodes, opts...).ToFunc()
}
// ByEpisodesCount orders the results by episodes count.
func ByEpisodesCount(opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {

View File

@@ -105,6 +105,11 @@ func TargetDir(v string) predicate.Media {
return predicate.Media(sql.FieldEQ(FieldTargetDir, v))
}
// DownloadHistoryEpisodes applies equality check predicate on the "download_history_episodes" field. It's identical to DownloadHistoryEpisodesEQ.
func DownloadHistoryEpisodes(v bool) predicate.Media {
return predicate.Media(sql.FieldEQ(FieldDownloadHistoryEpisodes, v))
}
// TmdbIDEQ applies the EQ predicate on the "tmdb_id" field.
func TmdbIDEQ(v int) predicate.Media {
return predicate.Media(sql.FieldEQ(FieldTmdbID, v))
@@ -750,6 +755,26 @@ func TargetDirContainsFold(v string) predicate.Media {
return predicate.Media(sql.FieldContainsFold(FieldTargetDir, v))
}
// DownloadHistoryEpisodesEQ applies the EQ predicate on the "download_history_episodes" field.
func DownloadHistoryEpisodesEQ(v bool) predicate.Media {
return predicate.Media(sql.FieldEQ(FieldDownloadHistoryEpisodes, v))
}
// DownloadHistoryEpisodesNEQ applies the NEQ predicate on the "download_history_episodes" field.
func DownloadHistoryEpisodesNEQ(v bool) predicate.Media {
return predicate.Media(sql.FieldNEQ(FieldDownloadHistoryEpisodes, v))
}
// DownloadHistoryEpisodesIsNil applies the IsNil predicate on the "download_history_episodes" field.
func DownloadHistoryEpisodesIsNil() predicate.Media {
return predicate.Media(sql.FieldIsNull(FieldDownloadHistoryEpisodes))
}
// DownloadHistoryEpisodesNotNil applies the NotNil predicate on the "download_history_episodes" field.
func DownloadHistoryEpisodesNotNil() predicate.Media {
return predicate.Media(sql.FieldNotNull(FieldDownloadHistoryEpisodes))
}
// HasEpisodes applies the HasEdge predicate on the "episodes" edge.
func HasEpisodes() predicate.Media {
return predicate.Media(func(s *sql.Selector) {

View File

@@ -141,6 +141,20 @@ func (mc *MediaCreate) SetNillableTargetDir(s *string) *MediaCreate {
return mc
}
// SetDownloadHistoryEpisodes sets the "download_history_episodes" field.
func (mc *MediaCreate) SetDownloadHistoryEpisodes(b bool) *MediaCreate {
mc.mutation.SetDownloadHistoryEpisodes(b)
return mc
}
// SetNillableDownloadHistoryEpisodes sets the "download_history_episodes" field if the given value is not nil.
func (mc *MediaCreate) SetNillableDownloadHistoryEpisodes(b *bool) *MediaCreate {
if b != nil {
mc.SetDownloadHistoryEpisodes(*b)
}
return mc
}
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by IDs.
func (mc *MediaCreate) AddEpisodeIDs(ids ...int) *MediaCreate {
mc.mutation.AddEpisodeIDs(ids...)
@@ -203,6 +217,10 @@ func (mc *MediaCreate) defaults() {
v := media.DefaultResolution
mc.mutation.SetResolution(v)
}
if _, ok := mc.mutation.DownloadHistoryEpisodes(); !ok {
v := media.DefaultDownloadHistoryEpisodes
mc.mutation.SetDownloadHistoryEpisodes(v)
}
}
// check runs all checks and user-defined validators on the builder.
@@ -318,6 +336,10 @@ func (mc *MediaCreate) createSpec() (*Media, *sqlgraph.CreateSpec) {
_spec.SetField(media.FieldTargetDir, field.TypeString, value)
_node.TargetDir = value
}
if value, ok := mc.mutation.DownloadHistoryEpisodes(); ok {
_spec.SetField(media.FieldDownloadHistoryEpisodes, field.TypeBool, value)
_node.DownloadHistoryEpisodes = value
}
if nodes := mc.mutation.EpisodesIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,

View File

@@ -229,6 +229,26 @@ func (mu *MediaUpdate) ClearTargetDir() *MediaUpdate {
return mu
}
// SetDownloadHistoryEpisodes sets the "download_history_episodes" field.
func (mu *MediaUpdate) SetDownloadHistoryEpisodes(b bool) *MediaUpdate {
mu.mutation.SetDownloadHistoryEpisodes(b)
return mu
}
// SetNillableDownloadHistoryEpisodes sets the "download_history_episodes" field if the given value is not nil.
func (mu *MediaUpdate) SetNillableDownloadHistoryEpisodes(b *bool) *MediaUpdate {
if b != nil {
mu.SetDownloadHistoryEpisodes(*b)
}
return mu
}
// ClearDownloadHistoryEpisodes clears the value of the "download_history_episodes" field.
func (mu *MediaUpdate) ClearDownloadHistoryEpisodes() *MediaUpdate {
mu.mutation.ClearDownloadHistoryEpisodes()
return mu
}
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by IDs.
func (mu *MediaUpdate) AddEpisodeIDs(ids ...int) *MediaUpdate {
mu.mutation.AddEpisodeIDs(ids...)
@@ -375,6 +395,12 @@ func (mu *MediaUpdate) sqlSave(ctx context.Context) (n int, err error) {
if mu.mutation.TargetDirCleared() {
_spec.ClearField(media.FieldTargetDir, field.TypeString)
}
if value, ok := mu.mutation.DownloadHistoryEpisodes(); ok {
_spec.SetField(media.FieldDownloadHistoryEpisodes, field.TypeBool, value)
}
if mu.mutation.DownloadHistoryEpisodesCleared() {
_spec.ClearField(media.FieldDownloadHistoryEpisodes, field.TypeBool)
}
if mu.mutation.EpisodesCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
@@ -640,6 +666,26 @@ func (muo *MediaUpdateOne) ClearTargetDir() *MediaUpdateOne {
return muo
}
// SetDownloadHistoryEpisodes sets the "download_history_episodes" field.
func (muo *MediaUpdateOne) SetDownloadHistoryEpisodes(b bool) *MediaUpdateOne {
muo.mutation.SetDownloadHistoryEpisodes(b)
return muo
}
// SetNillableDownloadHistoryEpisodes sets the "download_history_episodes" field if the given value is not nil.
func (muo *MediaUpdateOne) SetNillableDownloadHistoryEpisodes(b *bool) *MediaUpdateOne {
if b != nil {
muo.SetDownloadHistoryEpisodes(*b)
}
return muo
}
// ClearDownloadHistoryEpisodes clears the value of the "download_history_episodes" field.
func (muo *MediaUpdateOne) ClearDownloadHistoryEpisodes() *MediaUpdateOne {
muo.mutation.ClearDownloadHistoryEpisodes()
return muo
}
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by IDs.
func (muo *MediaUpdateOne) AddEpisodeIDs(ids ...int) *MediaUpdateOne {
muo.mutation.AddEpisodeIDs(ids...)
@@ -816,6 +862,12 @@ func (muo *MediaUpdateOne) sqlSave(ctx context.Context) (_node *Media, err error
if muo.mutation.TargetDirCleared() {
_spec.ClearField(media.FieldTargetDir, field.TypeString)
}
if value, ok := muo.mutation.DownloadHistoryEpisodes(); ok {
_spec.SetField(media.FieldDownloadHistoryEpisodes, field.TypeBool, value)
}
if muo.mutation.DownloadHistoryEpisodesCleared() {
_spec.ClearField(media.FieldDownloadHistoryEpisodes, field.TypeBool)
}
if muo.mutation.EpisodesCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,

View File

@@ -102,6 +102,7 @@ var (
{Name: "resolution", Type: field.TypeEnum, Enums: []string{"720p", "1080p", "4k"}, Default: "1080p"},
{Name: "storage_id", Type: field.TypeInt, Nullable: true},
{Name: "target_dir", Type: field.TypeString, Nullable: true},
{Name: "download_history_episodes", Type: field.TypeBool, Nullable: true, Default: false},
}
// MediaTable holds the schema information for the "media" table.
MediaTable = &schema.Table{

View File

@@ -3129,30 +3129,31 @@ func (m *IndexersMutation) ResetEdge(name string) error {
// MediaMutation represents an operation that mutates the Media nodes in the graph.
type MediaMutation struct {
config
op Op
typ string
id *int
tmdb_id *int
addtmdb_id *int
imdb_id *string
media_type *media.MediaType
name_cn *string
name_en *string
original_name *string
overview *string
created_at *time.Time
air_date *string
resolution *media.Resolution
storage_id *int
addstorage_id *int
target_dir *string
clearedFields map[string]struct{}
episodes map[int]struct{}
removedepisodes map[int]struct{}
clearedepisodes bool
done bool
oldValue func(context.Context) (*Media, error)
predicates []predicate.Media
op Op
typ string
id *int
tmdb_id *int
addtmdb_id *int
imdb_id *string
media_type *media.MediaType
name_cn *string
name_en *string
original_name *string
overview *string
created_at *time.Time
air_date *string
resolution *media.Resolution
storage_id *int
addstorage_id *int
target_dir *string
download_history_episodes *bool
clearedFields map[string]struct{}
episodes map[int]struct{}
removedepisodes map[int]struct{}
clearedepisodes bool
done bool
oldValue func(context.Context) (*Media, error)
predicates []predicate.Media
}
var _ ent.Mutation = (*MediaMutation)(nil)
@@ -3765,6 +3766,55 @@ func (m *MediaMutation) ResetTargetDir() {
delete(m.clearedFields, media.FieldTargetDir)
}
// SetDownloadHistoryEpisodes sets the "download_history_episodes" field.
func (m *MediaMutation) SetDownloadHistoryEpisodes(b bool) {
m.download_history_episodes = &b
}
// DownloadHistoryEpisodes returns the value of the "download_history_episodes" field in the mutation.
func (m *MediaMutation) DownloadHistoryEpisodes() (r bool, exists bool) {
v := m.download_history_episodes
if v == nil {
return
}
return *v, true
}
// OldDownloadHistoryEpisodes returns the old "download_history_episodes" field's value of the Media entity.
// If the Media 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 *MediaMutation) OldDownloadHistoryEpisodes(ctx context.Context) (v bool, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldDownloadHistoryEpisodes is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldDownloadHistoryEpisodes requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldDownloadHistoryEpisodes: %w", err)
}
return oldValue.DownloadHistoryEpisodes, nil
}
// ClearDownloadHistoryEpisodes clears the value of the "download_history_episodes" field.
func (m *MediaMutation) ClearDownloadHistoryEpisodes() {
m.download_history_episodes = nil
m.clearedFields[media.FieldDownloadHistoryEpisodes] = struct{}{}
}
// DownloadHistoryEpisodesCleared returns if the "download_history_episodes" field was cleared in this mutation.
func (m *MediaMutation) DownloadHistoryEpisodesCleared() bool {
_, ok := m.clearedFields[media.FieldDownloadHistoryEpisodes]
return ok
}
// ResetDownloadHistoryEpisodes resets all changes to the "download_history_episodes" field.
func (m *MediaMutation) ResetDownloadHistoryEpisodes() {
m.download_history_episodes = nil
delete(m.clearedFields, media.FieldDownloadHistoryEpisodes)
}
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by ids.
func (m *MediaMutation) AddEpisodeIDs(ids ...int) {
if m.episodes == nil {
@@ -3853,7 +3903,7 @@ func (m *MediaMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *MediaMutation) Fields() []string {
fields := make([]string, 0, 12)
fields := make([]string, 0, 13)
if m.tmdb_id != nil {
fields = append(fields, media.FieldTmdbID)
}
@@ -3890,6 +3940,9 @@ func (m *MediaMutation) Fields() []string {
if m.target_dir != nil {
fields = append(fields, media.FieldTargetDir)
}
if m.download_history_episodes != nil {
fields = append(fields, media.FieldDownloadHistoryEpisodes)
}
return fields
}
@@ -3922,6 +3975,8 @@ func (m *MediaMutation) Field(name string) (ent.Value, bool) {
return m.StorageID()
case media.FieldTargetDir:
return m.TargetDir()
case media.FieldDownloadHistoryEpisodes:
return m.DownloadHistoryEpisodes()
}
return nil, false
}
@@ -3955,6 +4010,8 @@ func (m *MediaMutation) OldField(ctx context.Context, name string) (ent.Value, e
return m.OldStorageID(ctx)
case media.FieldTargetDir:
return m.OldTargetDir(ctx)
case media.FieldDownloadHistoryEpisodes:
return m.OldDownloadHistoryEpisodes(ctx)
}
return nil, fmt.Errorf("unknown Media field %s", name)
}
@@ -4048,6 +4105,13 @@ func (m *MediaMutation) SetField(name string, value ent.Value) error {
}
m.SetTargetDir(v)
return nil
case media.FieldDownloadHistoryEpisodes:
v, ok := value.(bool)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetDownloadHistoryEpisodes(v)
return nil
}
return fmt.Errorf("unknown Media field %s", name)
}
@@ -4114,6 +4178,9 @@ func (m *MediaMutation) ClearedFields() []string {
if m.FieldCleared(media.FieldTargetDir) {
fields = append(fields, media.FieldTargetDir)
}
if m.FieldCleared(media.FieldDownloadHistoryEpisodes) {
fields = append(fields, media.FieldDownloadHistoryEpisodes)
}
return fields
}
@@ -4137,6 +4204,9 @@ func (m *MediaMutation) ClearField(name string) error {
case media.FieldTargetDir:
m.ClearTargetDir()
return nil
case media.FieldDownloadHistoryEpisodes:
m.ClearDownloadHistoryEpisodes()
return nil
}
return fmt.Errorf("unknown Media nullable field %s", name)
}
@@ -4181,6 +4251,9 @@ func (m *MediaMutation) ResetField(name string) error {
case media.FieldTargetDir:
m.ResetTargetDir()
return nil
case media.FieldDownloadHistoryEpisodes:
m.ResetDownloadHistoryEpisodes()
return nil
}
return fmt.Errorf("unknown Media field %s", name)
}

View File

@@ -70,6 +70,10 @@ func init() {
mediaDescAirDate := mediaFields[8].Descriptor()
// media.DefaultAirDate holds the default value on creation for the air_date field.
media.DefaultAirDate = mediaDescAirDate.Default.(string)
// mediaDescDownloadHistoryEpisodes is the schema descriptor for download_history_episodes field.
mediaDescDownloadHistoryEpisodes := mediaFields[12].Descriptor()
// media.DefaultDownloadHistoryEpisodes holds the default value on creation for the download_history_episodes field.
media.DefaultDownloadHistoryEpisodes = mediaDescDownloadHistoryEpisodes.Default.(bool)
storageFields := schema.Storage{}.Fields()
_ = storageFields
// storageDescDeleted is the schema descriptor for deleted field.

View File

@@ -28,7 +28,7 @@ func (Media) Fields() []ent.Field {
field.Enum("resolution").Values("720p", "1080p", "4k").Default("1080p"),
field.Int("storage_id").Optional(),
field.String("target_dir").Optional(),
//field.Bool("download_history_episodes").Optional().Default(false).Comment("tv series only"),
field.Bool("download_history_episodes").Optional().Default(false).Comment("tv series only"),
}
}

View File

@@ -221,14 +221,17 @@ func (s *Server) downloadTvSeries() {
for _, series := range allSeries {
tvDetail := s.db.GetMediaDetails(series.ID)
for _, ep := range tvDetail.Episodes {
t, err := time.Parse("2006-01-02", ep.AirDate)
if err != nil {
log.Error("air date not known, skip: %v", ep.Title)
continue
}
if series.CreatedAt.Sub(t) > 24*time.Hour { //剧集在加入watchlist之前不去下载
continue
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)
continue
}
if series.CreatedAt.Sub(t) > 24*time.Hour { //剧集在加入watchlist之前不去下载
continue
}
}
if ep.Status != episode.StatusMissing { //已经下载的不去下载
continue
}

View File

@@ -57,10 +57,11 @@ func (s *Server) SearchMedia(c *gin.Context) (interface{}, error) {
}
type addWatchlistIn struct {
TmdbID int `json:"tmdb_id" binding:"required"`
StorageID int `json:"storage_id" `
Resolution string `json:"resolution" binding:"required"`
Folder string `json:"folder"`
TmdbID int `json:"tmdb_id" binding:"required"`
StorageID int `json:"storage_id" `
Resolution string `json:"resolution" binding:"required"`
Folder string `json:"folder"`
DownloadHistoryEpisodes bool `json:"download_history_episodes"` //for tv
}
func (s *Server) AddTv2Watchlist(c *gin.Context) (interface{}, error) {
@@ -121,6 +122,7 @@ func (s *Server) AddTv2Watchlist(c *gin.Context) (interface{}, error) {
Resolution: media.Resolution(in.Resolution),
StorageID: in.StorageID,
TargetDir: in.Folder,
DownloadHistoryEpisodes: in.DownloadHistoryEpisodes,
}, epIds)
if err != nil {
return nil, errors.Wrap(err, "add to list")
@@ -273,7 +275,7 @@ func (s *Server) GetTvWatchlist(c *gin.Context) (interface{}, error) {
}
if ep.Status == episode.StatusMissing {
ms.Status = "monitoring"
}
}
}
}
res[i] = ms

View File

@@ -84,14 +84,15 @@ class SearchPageData
}
Future<void> submit2Watchlist(int tmdbId, int storageId, String resolution,
String mediaType, String folder) async {
String mediaType, String folder, bool downloadHistoryEpisodes) async {
final dio = await APIs.getDio();
if (mediaType == "tv") {
var resp = await dio.post(APIs.watchlistTvUrl, data: {
"tmdb_id": tmdbId,
"storage_id": storageId,
"resolution": resolution,
"folder": folder
"folder": folder,
"download_history_episodes":downloadHistoryEpisodes
});
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {

View File

@@ -146,6 +146,8 @@ class _SearchPageState extends ConsumerState<SearchPage> {
int storageSelected = 0;
var storage = ref.watch(storageSettingProvider);
var name = ref.watch(suggestNameDataProvider(item.id!));
bool downloadHistoryEpisodes = false;
bool buttonTapped = false;
var pathController = TextEditingController();
return AlertDialog(
@@ -230,6 +232,19 @@ class _SearchPageState extends ConsumerState<SearchPage> {
),
)
: Text(""),
item.mediaType == "tv"
? SizedBox(
width: 250,
child: CheckboxListTile(
title: const Text("是否下载往期剧集"),
value: downloadHistoryEpisodes,
onChanged: (v) {
setState(() {
downloadHistoryEpisodes = v!;
});
}),
)
: const SizedBox(),
],
);
});
@@ -254,18 +269,33 @@ class _SearchPageState extends ConsumerState<SearchPage> {
textStyle: Theme.of(context).textTheme.labelLarge,
),
child: const Text('确定'),
onPressed: () {
ref
onPressed: () async {
if (buttonTapped) {
return;
}
setState(() {
buttonTapped = true;
});
await ref
.read(searchPageDataProvider(widget.query ?? "")
.notifier)
.submit2Watchlist(item.id!, storageSelected,
resSelected, item.mediaType!, pathController.text)
.submit2Watchlist(
item.id!,
storageSelected,
resSelected,
item.mediaType!,
pathController.text,
downloadHistoryEpisodes)
.then((v) {
Utils.showSnakeBar("添加成功");
Navigator.of(context).pop();
}).onError((error, trace) {
Utils.showSnakeBar("添加失败:$error");
});
setState(() {
buttonTapped = false;
});
},
),
],