feat: code refactor and support season pack write .plexmatch file

This commit is contained in:
Simon Ding
2024-08-12 22:19:57 +08:00
parent 9632ca45b3
commit 081338df24
11 changed files with 374 additions and 31 deletions

View File

@@ -571,3 +571,11 @@ func (c *Client) EditMediaMetadata(in EditMediaData) error {
return c.ent.Media.Update().Where(media.ID(in.ID)).SetResolution(in.Resolution).SetTargetDir(in.TargetDir).SetLimiter(in.Limiter).
Exec(context.Background())
}
func (c *Client) UpdateEpisodeTargetFile(id int, filename string) error {
return c.ent.Episode.Update().Where(episode.ID(id)).SetTargetFile(filename).Exec(context.Background())
}
func (c *Client) GetSeasonEpisodes(mediaId, seasonNum int) ([]*ent.Episode, error) {
return c.ent.Episode.Query().Where(episode.MediaID(mediaId), episode.SeasonNumber(seasonNum)).All(context.Background())
}

View File

@@ -33,6 +33,8 @@ type Episode struct {
Status episode.Status `json:"status,omitempty"`
// Monitored holds the value of the "monitored" field.
Monitored bool `json:"monitored"`
// TargetFile holds the value of the "target_file" field.
TargetFile string `json:"target_file,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"`
@@ -68,7 +70,7 @@ func (*Episode) scanValues(columns []string) ([]any, error) {
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:
case episode.FieldTitle, episode.FieldOverview, episode.FieldAirDate, episode.FieldStatus, episode.FieldTargetFile:
values[i] = new(sql.NullString)
default:
values[i] = new(sql.UnknownType)
@@ -139,6 +141,12 @@ func (e *Episode) assignValues(columns []string, values []any) error {
} else if value.Valid {
e.Monitored = value.Bool
}
case episode.FieldTargetFile:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field target_file", values[i])
} else if value.Valid {
e.TargetFile = value.String
}
default:
e.selectValues.Set(columns[i], values[i])
}
@@ -203,6 +211,9 @@ func (e *Episode) String() string {
builder.WriteString(", ")
builder.WriteString("monitored=")
builder.WriteString(fmt.Sprintf("%v", e.Monitored))
builder.WriteString(", ")
builder.WriteString("target_file=")
builder.WriteString(e.TargetFile)
builder.WriteByte(')')
return builder.String()
}

View File

@@ -30,6 +30,8 @@ const (
FieldStatus = "status"
// FieldMonitored holds the string denoting the monitored field in the database.
FieldMonitored = "monitored"
// FieldTargetFile holds the string denoting the target_file field in the database.
FieldTargetFile = "target_file"
// EdgeMedia holds the string denoting the media edge name in mutations.
EdgeMedia = "media"
// Table holds the table name of the episode in the database.
@@ -54,6 +56,7 @@ var Columns = []string{
FieldAirDate,
FieldStatus,
FieldMonitored,
FieldTargetFile,
}
// ValidColumn reports if the column name is valid (part of the table columns).
@@ -146,6 +149,11 @@ func ByMonitored(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldMonitored, opts...).ToFunc()
}
// ByTargetFile orders the results by the target_file field.
func ByTargetFile(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldTargetFile, opts...).ToFunc()
}
// ByMediaField orders the results by media field.
func ByMediaField(field string, opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {

View File

@@ -89,6 +89,11 @@ func Monitored(v bool) predicate.Episode {
return predicate.Episode(sql.FieldEQ(FieldMonitored, v))
}
// TargetFile applies equality check predicate on the "target_file" field. It's identical to TargetFileEQ.
func TargetFile(v string) predicate.Episode {
return predicate.Episode(sql.FieldEQ(FieldTargetFile, v))
}
// MediaIDEQ applies the EQ predicate on the "media_id" field.
func MediaIDEQ(v int) predicate.Episode {
return predicate.Episode(sql.FieldEQ(FieldMediaID, v))
@@ -424,6 +429,81 @@ func MonitoredNEQ(v bool) predicate.Episode {
return predicate.Episode(sql.FieldNEQ(FieldMonitored, v))
}
// TargetFileEQ applies the EQ predicate on the "target_file" field.
func TargetFileEQ(v string) predicate.Episode {
return predicate.Episode(sql.FieldEQ(FieldTargetFile, v))
}
// TargetFileNEQ applies the NEQ predicate on the "target_file" field.
func TargetFileNEQ(v string) predicate.Episode {
return predicate.Episode(sql.FieldNEQ(FieldTargetFile, v))
}
// TargetFileIn applies the In predicate on the "target_file" field.
func TargetFileIn(vs ...string) predicate.Episode {
return predicate.Episode(sql.FieldIn(FieldTargetFile, vs...))
}
// TargetFileNotIn applies the NotIn predicate on the "target_file" field.
func TargetFileNotIn(vs ...string) predicate.Episode {
return predicate.Episode(sql.FieldNotIn(FieldTargetFile, vs...))
}
// TargetFileGT applies the GT predicate on the "target_file" field.
func TargetFileGT(v string) predicate.Episode {
return predicate.Episode(sql.FieldGT(FieldTargetFile, v))
}
// TargetFileGTE applies the GTE predicate on the "target_file" field.
func TargetFileGTE(v string) predicate.Episode {
return predicate.Episode(sql.FieldGTE(FieldTargetFile, v))
}
// TargetFileLT applies the LT predicate on the "target_file" field.
func TargetFileLT(v string) predicate.Episode {
return predicate.Episode(sql.FieldLT(FieldTargetFile, v))
}
// TargetFileLTE applies the LTE predicate on the "target_file" field.
func TargetFileLTE(v string) predicate.Episode {
return predicate.Episode(sql.FieldLTE(FieldTargetFile, v))
}
// TargetFileContains applies the Contains predicate on the "target_file" field.
func TargetFileContains(v string) predicate.Episode {
return predicate.Episode(sql.FieldContains(FieldTargetFile, v))
}
// TargetFileHasPrefix applies the HasPrefix predicate on the "target_file" field.
func TargetFileHasPrefix(v string) predicate.Episode {
return predicate.Episode(sql.FieldHasPrefix(FieldTargetFile, v))
}
// TargetFileHasSuffix applies the HasSuffix predicate on the "target_file" field.
func TargetFileHasSuffix(v string) predicate.Episode {
return predicate.Episode(sql.FieldHasSuffix(FieldTargetFile, v))
}
// TargetFileIsNil applies the IsNil predicate on the "target_file" field.
func TargetFileIsNil() predicate.Episode {
return predicate.Episode(sql.FieldIsNull(FieldTargetFile))
}
// TargetFileNotNil applies the NotNil predicate on the "target_file" field.
func TargetFileNotNil() predicate.Episode {
return predicate.Episode(sql.FieldNotNull(FieldTargetFile))
}
// TargetFileEqualFold applies the EqualFold predicate on the "target_file" field.
func TargetFileEqualFold(v string) predicate.Episode {
return predicate.Episode(sql.FieldEqualFold(FieldTargetFile, v))
}
// TargetFileContainsFold applies the ContainsFold predicate on the "target_file" field.
func TargetFileContainsFold(v string) predicate.Episode {
return predicate.Episode(sql.FieldContainsFold(FieldTargetFile, v))
}
// HasMedia applies the HasEdge predicate on the "media" edge.
func HasMedia() predicate.Episode {
return predicate.Episode(func(s *sql.Selector) {

View File

@@ -92,6 +92,20 @@ func (ec *EpisodeCreate) SetNillableMonitored(b *bool) *EpisodeCreate {
return ec
}
// SetTargetFile sets the "target_file" field.
func (ec *EpisodeCreate) SetTargetFile(s string) *EpisodeCreate {
ec.mutation.SetTargetFile(s)
return ec
}
// SetNillableTargetFile sets the "target_file" field if the given value is not nil.
func (ec *EpisodeCreate) SetNillableTargetFile(s *string) *EpisodeCreate {
if s != nil {
ec.SetTargetFile(*s)
}
return ec
}
// SetMedia sets the "media" edge to the Media entity.
func (ec *EpisodeCreate) SetMedia(m *Media) *EpisodeCreate {
return ec.SetMediaID(m.ID)
@@ -224,6 +238,10 @@ func (ec *EpisodeCreate) createSpec() (*Episode, *sqlgraph.CreateSpec) {
_spec.SetField(episode.FieldMonitored, field.TypeBool, value)
_node.Monitored = value
}
if value, ok := ec.mutation.TargetFile(); ok {
_spec.SetField(episode.FieldTargetFile, field.TypeString, value)
_node.TargetFile = value
}
if nodes := ec.mutation.MediaIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,

View File

@@ -160,6 +160,26 @@ func (eu *EpisodeUpdate) SetNillableMonitored(b *bool) *EpisodeUpdate {
return eu
}
// SetTargetFile sets the "target_file" field.
func (eu *EpisodeUpdate) SetTargetFile(s string) *EpisodeUpdate {
eu.mutation.SetTargetFile(s)
return eu
}
// SetNillableTargetFile sets the "target_file" field if the given value is not nil.
func (eu *EpisodeUpdate) SetNillableTargetFile(s *string) *EpisodeUpdate {
if s != nil {
eu.SetTargetFile(*s)
}
return eu
}
// ClearTargetFile clears the value of the "target_file" field.
func (eu *EpisodeUpdate) ClearTargetFile() *EpisodeUpdate {
eu.mutation.ClearTargetFile()
return eu
}
// SetMedia sets the "media" edge to the Media entity.
func (eu *EpisodeUpdate) SetMedia(m *Media) *EpisodeUpdate {
return eu.SetMediaID(m.ID)
@@ -252,6 +272,12 @@ func (eu *EpisodeUpdate) sqlSave(ctx context.Context) (n int, err error) {
if value, ok := eu.mutation.Monitored(); ok {
_spec.SetField(episode.FieldMonitored, field.TypeBool, value)
}
if value, ok := eu.mutation.TargetFile(); ok {
_spec.SetField(episode.FieldTargetFile, field.TypeString, value)
}
if eu.mutation.TargetFileCleared() {
_spec.ClearField(episode.FieldTargetFile, field.TypeString)
}
if eu.mutation.MediaCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
@@ -433,6 +459,26 @@ func (euo *EpisodeUpdateOne) SetNillableMonitored(b *bool) *EpisodeUpdateOne {
return euo
}
// SetTargetFile sets the "target_file" field.
func (euo *EpisodeUpdateOne) SetTargetFile(s string) *EpisodeUpdateOne {
euo.mutation.SetTargetFile(s)
return euo
}
// SetNillableTargetFile sets the "target_file" field if the given value is not nil.
func (euo *EpisodeUpdateOne) SetNillableTargetFile(s *string) *EpisodeUpdateOne {
if s != nil {
euo.SetTargetFile(*s)
}
return euo
}
// ClearTargetFile clears the value of the "target_file" field.
func (euo *EpisodeUpdateOne) ClearTargetFile() *EpisodeUpdateOne {
euo.mutation.ClearTargetFile()
return euo
}
// SetMedia sets the "media" edge to the Media entity.
func (euo *EpisodeUpdateOne) SetMedia(m *Media) *EpisodeUpdateOne {
return euo.SetMediaID(m.ID)
@@ -555,6 +601,12 @@ func (euo *EpisodeUpdateOne) sqlSave(ctx context.Context) (_node *Episode, err e
if value, ok := euo.mutation.Monitored(); ok {
_spec.SetField(episode.FieldMonitored, field.TypeBool, value)
}
if value, ok := euo.mutation.TargetFile(); ok {
_spec.SetField(episode.FieldTargetFile, field.TypeString, value)
}
if euo.mutation.TargetFileCleared() {
_spec.ClearField(episode.FieldTargetFile, field.TypeString)
}
if euo.mutation.MediaCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,

View File

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

View File

@@ -923,6 +923,7 @@ type EpisodeMutation struct {
air_date *string
status *episode.Status
monitored *bool
target_file *string
clearedFields map[string]struct{}
media *int
clearedmedia bool
@@ -1370,6 +1371,55 @@ func (m *EpisodeMutation) ResetMonitored() {
m.monitored = nil
}
// SetTargetFile sets the "target_file" field.
func (m *EpisodeMutation) SetTargetFile(s string) {
m.target_file = &s
}
// TargetFile returns the value of the "target_file" field in the mutation.
func (m *EpisodeMutation) TargetFile() (r string, exists bool) {
v := m.target_file
if v == nil {
return
}
return *v, true
}
// OldTargetFile returns the old "target_file" 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) OldTargetFile(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldTargetFile is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldTargetFile requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldTargetFile: %w", err)
}
return oldValue.TargetFile, nil
}
// ClearTargetFile clears the value of the "target_file" field.
func (m *EpisodeMutation) ClearTargetFile() {
m.target_file = nil
m.clearedFields[episode.FieldTargetFile] = struct{}{}
}
// TargetFileCleared returns if the "target_file" field was cleared in this mutation.
func (m *EpisodeMutation) TargetFileCleared() bool {
_, ok := m.clearedFields[episode.FieldTargetFile]
return ok
}
// ResetTargetFile resets all changes to the "target_file" field.
func (m *EpisodeMutation) ResetTargetFile() {
m.target_file = nil
delete(m.clearedFields, episode.FieldTargetFile)
}
// ClearMedia clears the "media" edge to the Media entity.
func (m *EpisodeMutation) ClearMedia() {
m.clearedmedia = true
@@ -1431,7 +1481,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, 8)
fields := make([]string, 0, 9)
if m.media != nil {
fields = append(fields, episode.FieldMediaID)
}
@@ -1456,6 +1506,9 @@ func (m *EpisodeMutation) Fields() []string {
if m.monitored != nil {
fields = append(fields, episode.FieldMonitored)
}
if m.target_file != nil {
fields = append(fields, episode.FieldTargetFile)
}
return fields
}
@@ -1480,6 +1533,8 @@ func (m *EpisodeMutation) Field(name string) (ent.Value, bool) {
return m.Status()
case episode.FieldMonitored:
return m.Monitored()
case episode.FieldTargetFile:
return m.TargetFile()
}
return nil, false
}
@@ -1505,6 +1560,8 @@ func (m *EpisodeMutation) OldField(ctx context.Context, name string) (ent.Value,
return m.OldStatus(ctx)
case episode.FieldMonitored:
return m.OldMonitored(ctx)
case episode.FieldTargetFile:
return m.OldTargetFile(ctx)
}
return nil, fmt.Errorf("unknown Episode field %s", name)
}
@@ -1570,6 +1627,13 @@ func (m *EpisodeMutation) SetField(name string, value ent.Value) error {
}
m.SetMonitored(v)
return nil
case episode.FieldTargetFile:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetTargetFile(v)
return nil
}
return fmt.Errorf("unknown Episode field %s", name)
}
@@ -1630,6 +1694,9 @@ func (m *EpisodeMutation) ClearedFields() []string {
if m.FieldCleared(episode.FieldMediaID) {
fields = append(fields, episode.FieldMediaID)
}
if m.FieldCleared(episode.FieldTargetFile) {
fields = append(fields, episode.FieldTargetFile)
}
return fields
}
@@ -1647,6 +1714,9 @@ func (m *EpisodeMutation) ClearField(name string) error {
case episode.FieldMediaID:
m.ClearMediaID()
return nil
case episode.FieldTargetFile:
m.ClearTargetFile()
return nil
}
return fmt.Errorf("unknown Episode nullable field %s", name)
}
@@ -1679,6 +1749,9 @@ func (m *EpisodeMutation) ResetField(name string) error {
case episode.FieldMonitored:
m.ResetMonitored()
return nil
case episode.FieldTargetFile:
m.ResetTargetFile()
return nil
}
return fmt.Errorf("unknown Episode field %s", name)
}

View File

@@ -22,16 +22,17 @@ func (Episode) Fields() []ent.Field {
field.String("air_date"),
field.Enum("status").Values("missing", "downloading", "downloaded").Default("missing"),
field.Bool("monitored").Default(false).StructTag("json:\"monitored\""), //whether this episode is monitored
field.String("target_file").Optional(),
}
}
// Edges of the Episode.
func (Episode) Edges() []ent.Edge {
return []ent.Edge{
edge.From("media", Media.Type).
Ref("episodes").
Unique().
edge.From("media", Media.Type).
Ref("episodes").
Unique().
Field("media_id"),
}
}
}

View File

@@ -3,24 +3,30 @@ package core
import (
"bytes"
"fmt"
"github.com/pkg/errors"
"os"
"path/filepath"
"polaris/db"
"polaris/ent/media"
storage1 "polaris/ent/storage"
"polaris/log"
"polaris/pkg/metadata"
"polaris/pkg/notifier"
"polaris/pkg/storage"
"polaris/pkg/utils"
"slices"
"strings"
"github.com/pkg/errors"
)
func (c *Client) writePlexmatch(seriesId int, episodeId int, targetDir, name string) error {
func (c *Client) writePlexmatch(historyId int) error {
if !c.plexmatchEnabled() {
return nil
}
series, err := c.db.GetMedia(seriesId)
his := c.db.GetHistory(historyId)
series, err := c.db.GetMedia(his.MediaID)
if err != nil {
return err
}
@@ -47,24 +53,44 @@ func (c *Client) writePlexmatch(seriesId int, episodeId int, targetDir, name str
}
}
//season plexmatch file
ep, err := c.db.GetEpisodeByID(episodeId)
if err != nil {
return errors.Wrap(err, "query episode")
}
buff := bytes.Buffer{}
seasonPlex := filepath.Join(targetDir, ".plexmatch")
seasonPlex := filepath.Join(his.TargetDir, ".plexmatch")
data, err := st.ReadFile(seasonPlex)
if err != nil {
log.Infof("read season plexmatch: %v", err)
} else {
buff.Write(data)
}
if strings.Contains(buff.String(), name) {
log.Debugf("already write plex episode line: %v", name)
return nil
if his.EpisodeID > 0 {
//single episode download
ep, err := c.db.GetEpisodeByID(his.EpisodeID)
if err != nil {
return errors.Wrap(err, "query episode")
}
if strings.Contains(buff.String(), ep.TargetFile) {
log.Debugf("already write plex episode line: %v", ep.TargetFile)
return nil
}
buff.WriteString(fmt.Sprintf("\nep: %d: %s\n", ep.EpisodeNumber, ep.TargetFile))
} else {
seasonNum, err := utils.SeasonId(his.TargetDir)
if err != nil {
return errors.Wrap(err, "no season id")
}
allEpisodes, err := c.db.GetSeasonEpisodes(his.MediaID, seasonNum)
if err != nil {
return errors.Wrap(err, "query season episode")
}
for _, ep := range allEpisodes {
if strings.Contains(buff.String(), ep.TargetFile) {
log.Debugf("already write plex episode line: %v", ep.TargetFile)
continue
}
buff.WriteString(fmt.Sprintf("\nep: %d: %s\n", ep.EpisodeNumber, ep.TargetFile))
}
}
buff.WriteString(fmt.Sprintf("\nep: %d: %s\n", ep.EpisodeNumber, name))
log.Infof("write season plexmatch file content: %s", buff.String())
return st.WriteFile(seasonPlex, buff.Bytes())
}
@@ -128,3 +154,63 @@ func (c *Client) sendMsg(msg string) {
log.Debugf("send message to %s success, msg is %s", cl.Name, msg)
}
}
func (c *Client) findEpisodeFilesPreMoving(historyId int) error {
his := c.db.GetHistory(historyId)
isSingleEpisode := his.EpisodeID > 0
downloadDir := c.db.GetDownloadDir()
task := c.tasks[historyId]
target := filepath.Join(downloadDir, task.Name())
fi, err := os.Stat(target)
if err != nil {
return errors.Wrapf(err, "read dir %v", target)
}
if isSingleEpisode {
if fi.IsDir() {
//download single episode in dir
//TODO
} else {
//is file
if err := c.db.UpdateEpisodeTargetFile(his.EpisodeID, fi.Name()); err != nil {
log.Errorf("writing downloaded file name to db error: %v", err)
}
}
} else {
if fi.IsDir() {
return fmt.Errorf("not season pack downloaded")
}
seasonNum, err := utils.SeasonId(his.TargetDir)
if err != nil {
return errors.Wrap(err, "no season id")
}
files, err := os.ReadDir(target)
if err != nil {
return err
}
for _, f := range files {
if f.IsDir() { //want media file
continue
}
excludedExt := []string{".txt", ".srt", ".ass", ".sub"}
ext := filepath.Ext(f.Name())
if slices.Contains(excludedExt, strings.ToLower(ext)) {
continue
}
meta := metadata.ParseTv(f.Name())
if meta.Episode > 0 {
//episode exists
ep, err := c.db.GetEpisode(his.MediaID, seasonNum, meta.Episode)
if err != nil {
return err
}
if err := c.db.UpdateEpisodeTargetFile(ep.ID, f.Name()); err != nil {
return errors.Wrap(err, "update episode file")
}
}
}
}
return nil
}

View File

@@ -61,15 +61,25 @@ func (c *Client) checkTasks() {
}
log.Infof("task is done: %v", t.Name())
c.sendMsg(fmt.Sprintf(message.DownloadComplete, t.Name()))
go func() {
if err := c.moveCompletedTask(id); err != nil {
log.Infof("post tasks for id %v fail: %v", id, err)
}
}()
go c.postTaskProcessing(id)
}
}
}
func (c *Client) postTaskProcessing(id int) {
if err := c.findEpisodeFilesPreMoving(id); err != nil {
log.Errorf("finding all episode file error: %v", err)
} else {
if err := c.writePlexmatch(id); err != nil {
log.Errorf("write plexmatch file error: %v", err)
}
}
if err := c.moveCompletedTask(id); err != nil {
log.Infof("post tasks for id %v fail: %v", id, err)
}
}
func (c *Client) moveCompletedTask(id int) (err1 error) {
torrent := c.tasks[id]
r := c.db.GetHistory(id)
@@ -119,11 +129,6 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
return err
}
// .plexmatch file
if err := c.writePlexmatch(r.MediaID, r.EpisodeID, r.TargetDir, torrentName); err != nil {
log.Errorf("create .plexmatch file error: %v", err)
}
//如果种子是路径,则会把路径展开,只移动文件,类似 move dir/* dir2/, 如果种子是文件,则会直接移动文件,类似 move file dir/
if err := stImpl.Copy(filepath.Join(c.db.GetDownloadDir(), torrentName), r.TargetDir); err != nil {
return errors.Wrap(err, "move file")