mirror of
https://github.com/simon-ding/polaris.git
synced 2026-02-24 04:30:47 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2683c5dbf2 | ||
|
|
b717885270 | ||
|
|
4e457e99b9 | ||
|
|
ecfe31ea45 | ||
|
|
89104785d7 | ||
|
|
f4ccc69b50 | ||
|
|
40c6e2df5c | ||
|
|
37dfb0fe94 | ||
|
|
9968f9f225 | ||
|
|
f5c977224b | ||
|
|
8af3ffccd3 | ||
|
|
c535dfd714 | ||
|
|
f696b78260 | ||
|
|
4f3e3e399d |
23
db/const.go
23
db/const.go
@@ -14,6 +14,9 @@ const (
|
||||
SettingNfoSupportEnabled = "nfo_support_enabled"
|
||||
SettingAllowQiangban = "filter_qiangban"
|
||||
SettingEnableTmdbAdultContent = "tmdb_adult_content"
|
||||
SetttingSizeLimiter = "size_limiter"
|
||||
SettingTvNamingFormat = "tv_naming_format"
|
||||
SettingMovieNamingFormat = "movie_naming_format"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -34,6 +37,26 @@ const (
|
||||
LanguageCN = "zh-CN"
|
||||
)
|
||||
|
||||
const DefaultNamingFormat = "{{.NameCN}} {{.NameEN}} {{if .Year}} ({{.Year}}) {{end}}"
|
||||
|
||||
type NamingInfo struct {
|
||||
NameCN string
|
||||
NameEN string
|
||||
Year string
|
||||
TmdbID int
|
||||
}
|
||||
|
||||
type ResolutionType string
|
||||
|
||||
const JwtSerectKey = "jwt_secrect_key"
|
||||
|
||||
type SizeLimiter struct {
|
||||
R720p Limiter `json:"720p"`
|
||||
R1080p Limiter `json:"1080p"`
|
||||
R2160p Limiter `json:"2160p"`
|
||||
}
|
||||
|
||||
type Limiter struct {
|
||||
Max int `json:"max"`
|
||||
Min int `json:"min"`
|
||||
}
|
||||
|
||||
41
db/db.go
41
db/db.go
@@ -530,6 +530,16 @@ func (c *Client) SetEpisodeStatus(id int, status episode.Status) error {
|
||||
return c.ent.Episode.Update().Where(episode.ID(id)).SetStatus(status).Exec(context.TODO())
|
||||
}
|
||||
|
||||
func (c *Client) IsEpisodeDownloadingOrDownloaded(id int) bool {
|
||||
his := c.ent.History.Query().Where(history.EpisodeID(id)).AllX(context.Background())
|
||||
for _, h := range his {
|
||||
if h.Status != history.StatusFail {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Client) SetSeasonAllEpisodeStatus(mediaID, seasonNum int, status episode.Status) error {
|
||||
return c.ent.Episode.Update().Where(episode.MediaID(mediaID), episode.SeasonNumber(seasonNum)).SetStatus(status).Exec(context.TODO())
|
||||
}
|
||||
@@ -602,4 +612,35 @@ func (c *Client) AddImportlist(il *ent.ImportList) error {
|
||||
|
||||
func (c *Client) DeleteImportlist(id int) error {
|
||||
return c.ent.ImportList.DeleteOneID(id).Exec(context.TODO())
|
||||
}
|
||||
|
||||
func (c *Client) GetSizeLimiter() (*SizeLimiter, error) {
|
||||
v := c.GetSetting(SetttingSizeLimiter)
|
||||
var limiter SizeLimiter
|
||||
err := json.Unmarshal([]byte(v), &limiter)
|
||||
return &limiter, err
|
||||
}
|
||||
|
||||
func (c *Client) SetSizeLimiter(limiter *SizeLimiter) error {
|
||||
data, err := json.Marshal(limiter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.SetSetting(SetttingSizeLimiter, string(data))
|
||||
}
|
||||
|
||||
func (c *Client) GetTvNamingFormat() string {
|
||||
s := c.GetSetting(SettingTvNamingFormat)
|
||||
if s == "" {
|
||||
return DefaultNamingFormat
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *Client) GetMovingNamingFormat() string {
|
||||
s := c.GetSetting(SettingMovieNamingFormat)
|
||||
if s == "" {
|
||||
return DefaultNamingFormat
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
package pkg
|
||||
|
||||
type Torrent interface {
|
||||
Name() string
|
||||
Progress() int
|
||||
Name() (string, error)
|
||||
Progress() (int, error)
|
||||
Stop() error
|
||||
Start() error
|
||||
Remove() error
|
||||
Save() string
|
||||
Exists() bool
|
||||
SeedRatio() *float64
|
||||
SeedRatio() (float64, error)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ func ParseDoulist(doulistUrl string) (*importlist.Response, error) {
|
||||
continue
|
||||
} else {
|
||||
n := ppp[1]
|
||||
n1, err := strconv.Atoi(n)
|
||||
n1, err := strconv.Atoi(strings.TrimSpace(n))
|
||||
if err != nil {
|
||||
log.Errorf("convert year number %s to int error: %v", n, err)
|
||||
continue
|
||||
|
||||
@@ -2,6 +2,7 @@ package metadata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"polaris/log"
|
||||
"polaris/pkg/utils"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -170,7 +171,7 @@ func parseChineseName(name string) *Metadata {
|
||||
}
|
||||
|
||||
//episode number
|
||||
re1 := regexp.MustCompile(`\[\d{1,2}\]`)
|
||||
re1 := regexp.MustCompile(`\[\d{1,3}\]`)
|
||||
episodeMatches1 := re1.FindAllString(name, -1)
|
||||
if len(episodeMatches1) > 0 { //[11] [1080p]
|
||||
epNum := strings.TrimRight(strings.TrimLeft(episodeMatches1[0], "["), "]")
|
||||
@@ -228,12 +229,17 @@ func parseChineseName(name string) *Metadata {
|
||||
}
|
||||
meta.Season = n
|
||||
} else {
|
||||
seasonRe1 := regexp.MustCompile(`第.{1}季`)
|
||||
seasonRe1 := regexp.MustCompile(`第.{1,2}季`)
|
||||
seasonMatches := seasonRe1.FindAllString(name, -1)
|
||||
if len(seasonMatches) > 0 {
|
||||
se := []rune(seasonMatches[0])
|
||||
seNum := se[1]
|
||||
meta.Season = chinese2Num[string(seNum)]
|
||||
m1 := []rune(seasonMatches[0])
|
||||
seNum := m1[1 : len(m1)-1]
|
||||
n, err := strconv.Atoi(string(seNum))
|
||||
if err != nil {
|
||||
log.Warnf("parse season number %v error: %v, try to parse using chinese", seNum, err)
|
||||
n = chinese2Num[string(seNum)]
|
||||
}
|
||||
meta.Season = n
|
||||
|
||||
}
|
||||
}
|
||||
@@ -245,24 +251,29 @@ func parseChineseName(name string) *Metadata {
|
||||
}
|
||||
|
||||
//tv name
|
||||
title := name
|
||||
|
||||
fields := strings.FieldsFunc(title, func(r rune) bool {
|
||||
fields := strings.FieldsFunc(name, func(r rune) bool {
|
||||
return r == '[' || r == ']' || r == '【' || r == '】'
|
||||
})
|
||||
title = ""
|
||||
titleCn := ""
|
||||
title := ""
|
||||
for _, p := range fields { //寻找匹配的最长的字符串,最有可能是名字
|
||||
if len([]rune(p)) > len([]rune(title)) {
|
||||
if utils.ContainsChineseChar(p) && len([]rune(p)) > len([]rune(titleCn)) { //最长含中文字符串
|
||||
titleCn = p
|
||||
}
|
||||
if len([]rune(p)) > len([]rune(title)) { //最长字符串
|
||||
title = p
|
||||
}
|
||||
}
|
||||
re := regexp.MustCompile(`[^\p{L}\w\s]`)
|
||||
title = re.ReplaceAllString(strings.TrimSpace(strings.ToLower(title)), "")
|
||||
title = re.ReplaceAllString(strings.TrimSpace(strings.ToLower(title)), "") //去除标点符号
|
||||
titleCn = re.ReplaceAllString(strings.TrimSpace(strings.ToLower(titleCn)), "")
|
||||
|
||||
meta.NameCn = title
|
||||
meta.NameCn = titleCn
|
||||
cnRe := regexp.MustCompile(`\p{Han}.*\p{Han}`)
|
||||
cnmatches := cnRe.FindAllString(title, -1)
|
||||
cnmatches := cnRe.FindAllString(titleCn, -1)
|
||||
|
||||
//titleCn中最长的中文字符
|
||||
if len(cnmatches) > 0 {
|
||||
for _, t := range cnmatches {
|
||||
if len([]rune(t)) > len([]rune(meta.NameCn)) {
|
||||
@@ -271,12 +282,13 @@ func parseChineseName(name string) *Metadata {
|
||||
}
|
||||
}
|
||||
|
||||
//匹配title中最长拉丁字符串
|
||||
enRe := regexp.MustCompile(`[[:ascii:]]*`)
|
||||
enM := enRe.FindAllString(title, -1)
|
||||
if len(enM) > 0 {
|
||||
for _, t := range enM {
|
||||
if len(t) > len(meta.NameEn) {
|
||||
meta.NameEn = strings.ToLower(t)
|
||||
meta.NameEn = strings.TrimSpace(strings.ToLower(t))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,11 +137,11 @@ func Test_ParseTV13(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_ParseTV14(t *testing.T) {
|
||||
s1 := ""
|
||||
s1 := "[GM-Team][国漫][斗破苍穹 第5季][Fights Break Sphere Ⅴ][2022][113][HEVC][GB][4K]"
|
||||
m := ParseTv(s1)
|
||||
log.Infof("results: %+v", m)
|
||||
assert.Equal(t, 2, m.Season)
|
||||
assert.Equal(t, 01, m.Episode)
|
||||
assert.Equal(t, 5, m.Season)
|
||||
assert.Equal(t, 113, m.Episode)
|
||||
assert.Equal(t, false, m.IsSeasonPack)
|
||||
assert.Equal(t, "720p", m.Resolution)
|
||||
}
|
||||
//assert.Equal(t, "720p", m.Resolution)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func (c *Client) GetAll() ([]*Torrent, error) {
|
||||
var torrents []*Torrent
|
||||
for _, t := range all {
|
||||
torrents = append(torrents, &Torrent{
|
||||
ID: *t.ID,
|
||||
Hash: *t.HashString,
|
||||
c: c.c,
|
||||
Config: c.cfg,
|
||||
})
|
||||
@@ -86,12 +86,12 @@ func (c *Client) Download(link, dir string) (*Torrent, error) {
|
||||
DownloadDir: &dir,
|
||||
})
|
||||
log.Infof("get torrent info: %+v", t)
|
||||
if t.ID == nil {
|
||||
if t.HashString == nil {
|
||||
return nil, fmt.Errorf("download torrent error: %v", link)
|
||||
}
|
||||
|
||||
return &Torrent{
|
||||
ID: *t.ID,
|
||||
Hash: *t.HashString,
|
||||
c: c.c,
|
||||
Config: c.cfg,
|
||||
}, err
|
||||
@@ -100,7 +100,7 @@ func (c *Client) Download(link, dir string) (*Torrent, error) {
|
||||
type Torrent struct {
|
||||
//t *transmissionrpc.Torrent
|
||||
c *transmissionrpc.Client
|
||||
ID int64 `json:"id"`
|
||||
Hash string `json:"hash"`
|
||||
Config
|
||||
}
|
||||
|
||||
@@ -113,68 +113,92 @@ func (t *Torrent) reloadClient() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Torrent) getTorrent() transmissionrpc.Torrent {
|
||||
r, err := t.c.TorrentGetAllFor(context.TODO(), []int64{t.ID})
|
||||
func (t *Torrent) getTorrent() (transmissionrpc.Torrent, error) {
|
||||
r, err := t.c.TorrentGetAllForHashes(context.TODO(), []string{t.Hash})
|
||||
if err != nil {
|
||||
log.Errorf("get torrent info for error: %v", err)
|
||||
}
|
||||
return r[0]
|
||||
if len(r) == 0 {
|
||||
return transmissionrpc.Torrent{}, fmt.Errorf("no torrent")
|
||||
}
|
||||
return r[0], nil
|
||||
}
|
||||
|
||||
func (t *Torrent) Exists() bool {
|
||||
r, err := t.c.TorrentGetAllFor(context.TODO(), []int64{t.ID})
|
||||
r, err := t.c.TorrentGetAllForHashes(context.TODO(), []string{t.Hash})
|
||||
if err != nil {
|
||||
log.Errorf("get torrent info for error: %v", err)
|
||||
}
|
||||
return len(r) > 0
|
||||
}
|
||||
|
||||
func (t *Torrent) Name() string {
|
||||
if !t.Exists() {
|
||||
return ""
|
||||
|
||||
func (t *Torrent) Name() (string, error) {
|
||||
tt, err := t.getTorrent()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return *t.getTorrent().Name
|
||||
return *tt.Name, nil
|
||||
}
|
||||
|
||||
func (t *Torrent) Progress() int {
|
||||
if t.getTorrent().IsFinished != nil && *t.getTorrent().IsFinished {
|
||||
return 100
|
||||
func (t *Torrent) Progress() (int, error) {
|
||||
tt, err := t.getTorrent()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if t.getTorrent().PercentComplete != nil && *t.getTorrent().PercentComplete >= 1 {
|
||||
return 100
|
||||
if tt.IsFinished != nil && *tt.IsFinished {
|
||||
return 100, nil
|
||||
}
|
||||
if tt.PercentComplete != nil && *tt.PercentComplete >= 1 {
|
||||
return 100, nil
|
||||
}
|
||||
|
||||
if t.getTorrent().PercentComplete != nil {
|
||||
p := int(*t.getTorrent().PercentComplete * 100)
|
||||
if tt.PercentComplete != nil {
|
||||
p := int(*tt.PercentComplete * 100)
|
||||
if p == 100 {
|
||||
p = 99
|
||||
}
|
||||
return p
|
||||
return p, nil
|
||||
}
|
||||
return 0
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (t *Torrent) Stop() error {
|
||||
return t.c.TorrentStopIDs(context.TODO(), []int64{t.ID})
|
||||
return t.c.TorrentStopHashes(context.TODO(), []string{t.Hash})
|
||||
}
|
||||
|
||||
func (t *Torrent) SeedRatio() *float64 {
|
||||
return t.getTorrent().UploadRatio
|
||||
func (t *Torrent) SeedRatio() (float64, error) {
|
||||
tt, err := t.getTorrent()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if tt.UploadRatio == nil {
|
||||
return 0, nil
|
||||
}
|
||||
return *tt.UploadRatio, nil
|
||||
}
|
||||
|
||||
func (t *Torrent) Start() error {
|
||||
return t.c.TorrentStartIDs(context.TODO(), []int64{t.ID})
|
||||
return t.c.TorrentStartHashes(context.TODO(), []string{t.Hash})
|
||||
}
|
||||
|
||||
func (t *Torrent) Remove() error {
|
||||
tt, err := t.getTorrent()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get torrent")
|
||||
}
|
||||
return t.c.TorrentRemove(context.TODO(), transmissionrpc.TorrentRemovePayload{
|
||||
IDs: []int64{t.ID},
|
||||
IDs: []int64{*tt.ID},
|
||||
DeleteLocalData: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (t *Torrent) Size() int {
|
||||
return int(t.getTorrent().TotalSize.Byte())
|
||||
func (t *Torrent) Size() (int, error) {
|
||||
tt, err := t.getTorrent()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "get torrent")
|
||||
}
|
||||
return int(tt.TotalSize.Byte()), nil
|
||||
}
|
||||
|
||||
func (t *Torrent) Save() string {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -147,3 +148,38 @@ func ChangeFileHash(name string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TrimFields(v interface{}) error {
|
||||
bytes, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var mapSI map[string]interface{}
|
||||
if err := json.Unmarshal(bytes, &mapSI); err != nil {
|
||||
return err
|
||||
}
|
||||
mapSI = trimMapStringInterface(mapSI).(map[string]interface{})
|
||||
bytes2, err := json.Marshal(mapSI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(bytes2, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func trimMapStringInterface(data interface{}) interface{} {
|
||||
if values, valid := data.([]interface{}); valid {
|
||||
for i := range values {
|
||||
data.([]interface{})[i] = trimMapStringInterface(values[i])
|
||||
}
|
||||
} else if values, valid := data.(map[string]interface{}); valid {
|
||||
for k, v := range values {
|
||||
data.(map[string]interface{})[k] = trimMapStringInterface(v)
|
||||
}
|
||||
} else if value, valid := data.(string); valid {
|
||||
data = strings.TrimSpace(value)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
type Activity struct {
|
||||
*ent.History
|
||||
Progress int `json:"progress"`
|
||||
SeedRatio float32 `json:"seed_ratio"`
|
||||
SeedRatio float64 `json:"seed_ratio"`
|
||||
}
|
||||
|
||||
func (s *Server) GetAllActivities(c *gin.Context) (interface{}, error) {
|
||||
@@ -30,8 +30,18 @@ func (s *Server) GetAllActivities(c *gin.Context) (interface{}, error) {
|
||||
}
|
||||
for id, task := range s.core.GetTasks() {
|
||||
if h.ID == id && task.Exists() {
|
||||
a.Progress = task.Progress()
|
||||
a.SeedRatio = float32(*task.SeedRatio())
|
||||
p, err := task.Progress()
|
||||
if err != nil {
|
||||
log.Warnf("get task progress error: %v", err)
|
||||
} else {
|
||||
a.Progress = p
|
||||
}
|
||||
r, err := task.SeedRatio()
|
||||
if err != nil {
|
||||
log.Warnf("get task seed ratio error: %v", err)
|
||||
} else {
|
||||
a.SeedRatio = r
|
||||
}
|
||||
}
|
||||
}
|
||||
activities = append(activities, a)
|
||||
@@ -68,9 +78,13 @@ func (s *Server) RemoveActivity(c *gin.Context) (interface{}, error) {
|
||||
if err := s.core.RemoveTaskAndTorrent(his.ID); err != nil {
|
||||
return nil, errors.Wrap(err, "remove torrent")
|
||||
}
|
||||
err = s.db.DeleteHistory(id)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db")
|
||||
}
|
||||
|
||||
if his.EpisodeID != 0 {
|
||||
if his.Status == history.StatusRunning || his.Status == history.StatusUploading {
|
||||
if !s.db.IsEpisodeDownloadingOrDownloaded(his.EpisodeID) {
|
||||
s.db.SetEpisodeStatus(his.EpisodeID, episode.StatusMissing)
|
||||
}
|
||||
|
||||
@@ -85,10 +99,6 @@ func (s *Server) RemoveActivity(c *gin.Context) (interface{}, error) {
|
||||
}
|
||||
}
|
||||
|
||||
err = s.db.DeleteHistory(id)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db")
|
||||
}
|
||||
log.Infof("history record successful deleted: %v", his.SourceTitle)
|
||||
return nil, nil
|
||||
}
|
||||
@@ -107,7 +117,7 @@ func (s *Server) GetMediaDownloadHistory(c *gin.Context) (interface{}, error) {
|
||||
|
||||
type TorrentInfo struct {
|
||||
Name string `json:"name"`
|
||||
ID int64 `json:"id"`
|
||||
ID string `json:"id"`
|
||||
SeedRatio float32 `json:"seed_ratio"`
|
||||
Progress int `json:"progress"`
|
||||
}
|
||||
@@ -126,10 +136,12 @@ func (s *Server) GetAllTorrents(c *gin.Context) (interface{}, error) {
|
||||
if !t.Exists() {
|
||||
continue
|
||||
}
|
||||
name, _ := t.Name()
|
||||
p, _ := t.Progress()
|
||||
infos = append(infos, TorrentInfo{
|
||||
Name: t.Name(),
|
||||
ID: t.ID,
|
||||
Progress: t.Progress(),
|
||||
Name: name,
|
||||
ID: t.Hash,
|
||||
Progress: p,
|
||||
})
|
||||
}
|
||||
return infos, nil
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -16,7 +18,6 @@ import (
|
||||
"polaris/pkg/utils"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tmdb "github.com/cyruzin/golang-tmdb"
|
||||
"github.com/pkg/errors"
|
||||
@@ -132,7 +133,16 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
} else {
|
||||
detail = detailEn
|
||||
}
|
||||
log.Infof("find detail for tv id %d: %v", in.TmdbID, detail)
|
||||
log.Infof("find detail for tv id %d: %+v", in.TmdbID, detail)
|
||||
|
||||
lastSeason := 0
|
||||
for _, season := range detail.Seasons {
|
||||
if season.SeasonNumber > lastSeason && season.EpisodeCount > 0 { //如果最新一季已经有剧集信息,则以最新一季为准
|
||||
lastSeason = season.SeasonNumber
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("latest season is %v", lastSeason)
|
||||
|
||||
var epIds []int
|
||||
for _, season := range detail.Seasons {
|
||||
@@ -142,36 +152,41 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
log.Errorf("get season detail (%s) error: %v", detail.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
shouldMonitor := seasonId >= lastSeason //监控最新的一季
|
||||
|
||||
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 monitor: %v", ep.AirDate)
|
||||
shouldMonitor = true
|
||||
|
||||
} else {
|
||||
if time.Since(t) < 24*time.Hour { //monitor episode air 24h before now
|
||||
shouldMonitor = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// //如果设置下载往期剧集,则监控所有剧集。如果没有则监控未上映的剧集,考虑时差等问题留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 monitor: %v", ep.AirDate)
|
||||
// shouldMonitor = true
|
||||
|
||||
epid, err := c.db.SaveEposideDetail(&ent.Episode{
|
||||
// } else {
|
||||
// if time.Since(t) < 24*time.Hour { //monitor episode air 24h before now
|
||||
// shouldMonitor = true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
ep := ent.Episode{
|
||||
SeasonNumber: seasonId,
|
||||
EpisodeNumber: ep.EpisodeNumber,
|
||||
Title: ep.Name,
|
||||
Overview: ep.Overview,
|
||||
AirDate: ep.AirDate,
|
||||
Monitored: shouldMonitor,
|
||||
})
|
||||
}
|
||||
epid, err := c.db.SaveEposideDetail(&ep)
|
||||
if err != nil {
|
||||
log.Errorf("save episode info error: %v", err)
|
||||
continue
|
||||
}
|
||||
log.Debugf("success save episode %+v", ep)
|
||||
epIds = append(epIds, epid)
|
||||
}
|
||||
}
|
||||
@@ -361,27 +376,35 @@ func (c *Client) SuggestedMovieFolderName(tmdbId int) (string, error) {
|
||||
return javid, nil
|
||||
}
|
||||
}
|
||||
|
||||
//if name is already in english, no need to query again
|
||||
if !utils.IsASCII(name) && c.language == db.LanguageCN {
|
||||
info := db.NamingInfo{TmdbID: tmdbId}
|
||||
if utils.IsASCII(name) {
|
||||
info.NameEN = stripExtraCharacters(name)
|
||||
} else {
|
||||
info.NameCN = stripExtraCharacters(name)
|
||||
en, err := c.MustTMDB().GetMovieDetails(tmdbId, db.LanguageEN)
|
||||
if err != nil {
|
||||
log.Errorf("get en movie detail error: %v", err)
|
||||
log.Errorf("get en tv detail error: %v", err)
|
||||
} else {
|
||||
name = fmt.Sprintf("%s %s", name, en.Title)
|
||||
info.NameEN = stripExtraCharacters(en.Title)
|
||||
}
|
||||
}
|
||||
//remove extra characters
|
||||
re := regexp.MustCompile(`[^\p{L}\w\s]`)
|
||||
name = re.ReplaceAllString(name, " ")
|
||||
name = strings.Join(strings.Fields(name), " ")
|
||||
year := strings.Split(d1.ReleaseDate, "-")[0]
|
||||
if year != "" {
|
||||
name = fmt.Sprintf("%s (%s)", name, year)
|
||||
}
|
||||
info.Year = year
|
||||
movieNamingFormat := c.db.GetMovingNamingFormat()
|
||||
|
||||
log.Infof("tv series of tmdb id %v suggestting name is %v", tmdbId, name)
|
||||
return name, nil
|
||||
tmpl, err := template.New("test").Parse(movieNamingFormat)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "naming format")
|
||||
}
|
||||
buff := &bytes.Buffer{}
|
||||
err = tmpl.Execute(buff, info)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "tmpl exec")
|
||||
}
|
||||
res := strings.TrimSpace(buff.String())
|
||||
|
||||
log.Infof("tv series of tmdb id %v suggestting name is %v", tmdbId, res)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Client) SuggestedSeriesFolderName(tmdbId int) (string, error) {
|
||||
@@ -393,24 +416,42 @@ func (c *Client) SuggestedSeriesFolderName(tmdbId int) (string, error) {
|
||||
|
||||
name := d.Name
|
||||
|
||||
//if name is already in english, no need to query again
|
||||
if !utils.IsASCII(name) && c.language == db.LanguageCN {
|
||||
info := db.NamingInfo{TmdbID: tmdbId}
|
||||
if utils.IsASCII(name) {
|
||||
info.NameEN = stripExtraCharacters(name)
|
||||
} else {
|
||||
info.NameCN = stripExtraCharacters(name)
|
||||
en, err := c.MustTMDB().GetTvDetails(tmdbId, db.LanguageEN)
|
||||
if err != nil {
|
||||
log.Errorf("get en tv detail error: %v", err)
|
||||
} else {
|
||||
name = fmt.Sprintf("%s %s", name, en.Name)
|
||||
if en.Name != name { //sometimes en name is in chinese
|
||||
info.NameEN = stripExtraCharacters(en.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
//remove extra characters
|
||||
re := regexp.MustCompile(`[^\p{L}\w\s]`)
|
||||
name = re.ReplaceAllString(strings.ToLower(name), " ")
|
||||
name = strings.Join(strings.Fields(name), " ")
|
||||
year := strings.Split(d.FirstAirDate, "-")[0]
|
||||
if year != "" {
|
||||
name = fmt.Sprintf("%s (%s)", name, year)
|
||||
}
|
||||
info.Year = year
|
||||
|
||||
log.Infof("tv series of tmdb id %v suggestting name is %v", tmdbId, name)
|
||||
return name, nil
|
||||
tvNamingFormat := c.db.GetTvNamingFormat()
|
||||
|
||||
tmpl, err := template.New("test").Parse(tvNamingFormat)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "naming format")
|
||||
}
|
||||
buff := &bytes.Buffer{}
|
||||
err = tmpl.Execute(buff, info)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "tmpl exec")
|
||||
}
|
||||
res := strings.TrimSpace(buff.String())
|
||||
|
||||
log.Infof("tv series of tmdb id %v suggestting name is %v", tmdbId, res)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func stripExtraCharacters(s string) string {
|
||||
re := regexp.MustCompile(`[^\p{L}\w\s]`)
|
||||
s = re.ReplaceAllString(s, " ")
|
||||
return strings.Join(strings.Fields(s), " ")
|
||||
}
|
||||
|
||||
@@ -257,7 +257,11 @@ func (c *Client) findEpisodeFilesPreMoving(historyId int) error {
|
||||
isSingleEpisode := his.EpisodeID > 0
|
||||
downloadDir := c.db.GetDownloadDir()
|
||||
task := c.tasks[historyId]
|
||||
target := filepath.Join(downloadDir, task.Name())
|
||||
name, err := task.Name()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target := filepath.Join(downloadDir, name)
|
||||
fi, err := os.Stat(target)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "read dir %v", target)
|
||||
|
||||
@@ -63,24 +63,33 @@ func (c *Client) checkTasks() error {
|
||||
delete(c.tasks, id)
|
||||
continue
|
||||
}
|
||||
log.Infof("task (%s) percentage done: %d%%", t.Name(), t.Progress())
|
||||
if t.Progress() == 100 {
|
||||
name, err := t.Name()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get name")
|
||||
}
|
||||
|
||||
progress, err := t.Progress()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get progress")
|
||||
}
|
||||
log.Infof("task (%s) percentage done: %d%%", name, progress)
|
||||
if progress == 100 {
|
||||
|
||||
if r.Status == history.StatusSeeding {
|
||||
//task already success, check seed ratio
|
||||
torrent := c.tasks[id]
|
||||
ok := c.isSeedRatioLimitReached(r.IndexerID, torrent)
|
||||
ratio, ok := c.isSeedRatioLimitReached(r.IndexerID, torrent)
|
||||
if ok {
|
||||
log.Infof("torrent file seed ratio reached, remove: %v, current seed ratio: %v", torrent.Name(), *torrent.SeedRatio())
|
||||
log.Infof("torrent file seed ratio reached, remove: %v, current seed ratio: %v", name, ratio)
|
||||
torrent.Remove()
|
||||
delete(c.tasks, id)
|
||||
} else {
|
||||
log.Infof("torrent file still sedding: %v, current seed ratio: %v", torrent.Name(), *torrent.SeedRatio())
|
||||
log.Infof("torrent file still sedding: %v, current seed ratio: %v", name, ratio)
|
||||
}
|
||||
continue
|
||||
}
|
||||
log.Infof("task is done: %v", t.Name())
|
||||
c.sendMsg(fmt.Sprintf(message.DownloadComplete, t.Name()))
|
||||
log.Infof("task is done: %v", name)
|
||||
c.sendMsg(fmt.Sprintf(message.DownloadComplete, name))
|
||||
|
||||
go c.postTaskProcessing(id)
|
||||
}
|
||||
@@ -122,14 +131,19 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
|
||||
log.Errorf("get task download client error: %v, use default one", err)
|
||||
downloadclient = &ent.DownloadClients{RemoveCompletedDownloads: true, RemoveFailedDownloads: true}
|
||||
}
|
||||
torrentName := torrent.Name()
|
||||
torrentName, err := torrent.Name()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
|
||||
if err1 != nil {
|
||||
c.db.SetHistoryStatus(r.ID, history.StatusFail)
|
||||
if r.EpisodeID != 0 {
|
||||
c.db.SetEpisodeStatus(r.EpisodeID, episode.StatusMissing)
|
||||
if !c.db.IsEpisodeDownloadingOrDownloaded(r.EpisodeID) {
|
||||
c.db.SetEpisodeStatus(r.EpisodeID, episode.StatusMissing)
|
||||
}
|
||||
} else {
|
||||
c.db.SetSeasonAllEpisodeStatus(r.MediaID, seasonNum, episode.StatusMissing)
|
||||
}
|
||||
@@ -167,9 +181,9 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
|
||||
c.sendMsg(fmt.Sprintf(message.ProcessingComplete, torrentName))
|
||||
|
||||
//判断是否需要删除本地文件
|
||||
ok := c.isSeedRatioLimitReached(r.IndexerID, torrent)
|
||||
r1, ok := c.isSeedRatioLimitReached(r.IndexerID, torrent)
|
||||
if downloadclient.RemoveCompletedDownloads && ok {
|
||||
log.Debugf("download complete,remove torrent and files related, torrent: %v, seed ratio: %v", torrentName, *torrent.SeedRatio())
|
||||
log.Debugf("download complete,remove torrent and files related, torrent: %v, seed ratio: %v", torrentName, r1)
|
||||
c.db.SetHistoryStatus(r.ID, history.StatusSuccess)
|
||||
delete(c.tasks, r.ID)
|
||||
torrent.Remove()
|
||||
@@ -426,15 +440,15 @@ func (c *Client) checkSeiesNewSeason(media *ent.Media) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) isSeedRatioLimitReached(indexId int, t pkg.Torrent) bool {
|
||||
func (c *Client) isSeedRatioLimitReached(indexId int, t pkg.Torrent)(float64,bool) {
|
||||
indexer, err := c.db.GetIndexer(indexId)
|
||||
if err != nil {
|
||||
return true
|
||||
return 0, true
|
||||
}
|
||||
currentRatio := t.SeedRatio()
|
||||
if currentRatio == nil {
|
||||
log.Warnf("get current seed ratio error, current ratio is nil")
|
||||
return indexer.SeedRatio == 0
|
||||
currentRatio, err := t.SeedRatio()
|
||||
if err != nil {
|
||||
log.Warnf("get current seed ratio error: %v", err)
|
||||
return currentRatio, indexer.SeedRatio == 0
|
||||
}
|
||||
return *currentRatio >= float64(indexer.SeedRatio)
|
||||
return currentRatio, currentRatio >= float64(indexer.SeedRatio)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"polaris/ent"
|
||||
"polaris/ent/importlist"
|
||||
"polaris/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
@@ -28,6 +29,8 @@ func (s *Server) addImportlist(c *gin.Context) (interface{}, error) {
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "json")
|
||||
}
|
||||
utils.TrimFields(&in)
|
||||
|
||||
st := s.db.GetStorage(in.StorageId)
|
||||
if st == nil {
|
||||
return nil, fmt.Errorf("storage id not exist: %v", in.StorageId)
|
||||
|
||||
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"polaris/ent"
|
||||
"polaris/pkg/utils"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -35,6 +36,7 @@ func (s *Server) AddNotificationClient(c *gin.Context) (interface{}, error) {
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "json")
|
||||
}
|
||||
utils.TrimFields(&in)
|
||||
|
||||
err := s.db.AddNotificationClient(in.Name, in.Service, in.Settings, in.Enabled)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,10 +3,12 @@ package server
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"polaris/db"
|
||||
"polaris/ent"
|
||||
"polaris/log"
|
||||
"polaris/pkg/transmission"
|
||||
"polaris/pkg/utils"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -22,6 +24,8 @@ type GeneralSettings struct {
|
||||
EnableNfo bool `json:"enable_nfo"`
|
||||
AllowQiangban bool `json:"allow_qiangban"`
|
||||
EnableAdultContent bool `json:"enable_adult_content"`
|
||||
TvNamingFormat string `json:"tv_naming_format"`
|
||||
MovieNamingFormat string `json:"movie_naming_format"`
|
||||
}
|
||||
|
||||
func (s *Server) SetSetting(c *gin.Context) (interface{}, error) {
|
||||
@@ -29,6 +33,8 @@ func (s *Server) SetSetting(c *gin.Context) (interface{}, error) {
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
utils.TrimFields(&in)
|
||||
|
||||
log.Infof("set setting input: %+v", in)
|
||||
if in.TmdbApiKey != "" {
|
||||
if err := s.db.SetSetting(db.SettingTmdbApiKey, in.TmdbApiKey); err != nil {
|
||||
@@ -46,7 +52,25 @@ func (s *Server) SetSetting(c *gin.Context) (interface{}, error) {
|
||||
if err := s.db.SetSetting(db.SettingLogLevel, in.LogLevel); err != nil {
|
||||
return nil, errors.Wrap(err, "save log level")
|
||||
}
|
||||
}
|
||||
if in.TvNamingFormat != "" {
|
||||
if _, err := template.New("test").Parse(in.TvNamingFormat); err != nil {
|
||||
return nil, errors.Wrap(err, "tv format")
|
||||
}
|
||||
|
||||
s.db.SetSetting(db.SettingTvNamingFormat, in.TvNamingFormat)
|
||||
} else {
|
||||
s.db.SetSetting(db.SettingTvNamingFormat, "")
|
||||
}
|
||||
|
||||
if in.MovieNamingFormat != "" {
|
||||
if _, err := template.New("test").Parse(in.MovieNamingFormat); err != nil {
|
||||
return nil, errors.Wrap(err, "movie format")
|
||||
}
|
||||
|
||||
s.db.SetSetting(db.SettingMovieNamingFormat, in.MovieNamingFormat)
|
||||
} else {
|
||||
s.db.SetSetting(db.SettingMovieNamingFormat, "")
|
||||
}
|
||||
|
||||
plexmatchEnabled := s.db.GetSetting(db.SettingPlexMatchEnabled)
|
||||
@@ -74,7 +98,6 @@ func (s *Server) SetSetting(c *gin.Context) (interface{}, error) {
|
||||
} else {
|
||||
s.db.SetSetting(db.SettingEnableTmdbAdultContent, "false")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -86,15 +109,19 @@ func (s *Server) GetSetting(c *gin.Context) (interface{}, error) {
|
||||
allowQiangban := s.db.GetSetting(db.SettingAllowQiangban)
|
||||
enableNfo := s.db.GetSetting(db.SettingNfoSupportEnabled)
|
||||
enableAdult := s.db.GetSetting(db.SettingEnableTmdbAdultContent)
|
||||
tvFormat := s.db.GetTvNamingFormat()
|
||||
movieFormat := s.db.GetMovingNamingFormat()
|
||||
return &GeneralSettings{
|
||||
TmdbApiKey: tmdb,
|
||||
DownloadDir: downloadDir,
|
||||
LogLevel: logLevel,
|
||||
Proxy: s.db.GetSetting(db.SettingProxy),
|
||||
EnablePlexmatch: plexmatchEnabled == "true",
|
||||
AllowQiangban: allowQiangban == "true",
|
||||
EnableNfo: enableNfo == "true",
|
||||
TmdbApiKey: tmdb,
|
||||
DownloadDir: downloadDir,
|
||||
LogLevel: logLevel,
|
||||
Proxy: s.db.GetSetting(db.SettingProxy),
|
||||
EnablePlexmatch: plexmatchEnabled == "true",
|
||||
AllowQiangban: allowQiangban == "true",
|
||||
EnableNfo: enableNfo == "true",
|
||||
EnableAdultContent: enableAdult == "true",
|
||||
TvNamingFormat: tvFormat,
|
||||
MovieNamingFormat: movieFormat,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -113,6 +140,7 @@ func (s *Server) AddTorznabInfo(c *gin.Context) (interface{}, error) {
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
utils.TrimFields(&in)
|
||||
|
||||
log.Infof("add indexer settings: %+v", in)
|
||||
setting := db.TorznabSetting{
|
||||
@@ -188,6 +216,7 @@ func (s *Server) AddDownloadClient(c *gin.Context) (interface{}, error) {
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
utils.TrimFields(&in)
|
||||
//test connection
|
||||
_, err := transmission.NewClient(transmission.Config{
|
||||
URL: in.URL,
|
||||
@@ -262,4 +291,4 @@ func (s *Server) TriggerCronJob(c *gin.Context) (interface{}, error) {
|
||||
return nil, err
|
||||
}
|
||||
return "success", nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"polaris/log"
|
||||
"polaris/pkg/storage"
|
||||
"polaris/pkg/utils"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -22,6 +23,7 @@ func (s *Server) AddStorage(c *gin.Context) (interface{}, error) {
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
utils.TrimFields(&in)
|
||||
|
||||
if in.Implementation == "webdav" {
|
||||
//test webdav
|
||||
|
||||
@@ -138,7 +138,7 @@ class _ActivityPageState extends ConsumerState<ActivityPage>
|
||||
trailing: selectedTab == 0
|
||||
? IconButton(
|
||||
tooltip: "删除任务",
|
||||
onPressed: () => onDelete()(ac.id!),
|
||||
onPressed: () => onDelete()(ac.id!.toString()),
|
||||
icon: const Icon(Icons.delete))
|
||||
: const Text("-"),
|
||||
),
|
||||
@@ -155,7 +155,7 @@ class _ActivityPageState extends ConsumerState<ActivityPage>
|
||||
);
|
||||
}
|
||||
|
||||
Function(int) onDelete() {
|
||||
Function(String) onDelete() {
|
||||
return (id) {
|
||||
final f = ref
|
||||
.read(activitiesDataProvider("active").notifier)
|
||||
|
||||
@@ -47,7 +47,7 @@ class ActivityData extends AutoDisposeFamilyAsyncNotifier<List<Activity>, String
|
||||
return activities;
|
||||
}
|
||||
|
||||
Future<void> deleteActivity(int id) async {
|
||||
Future<void> deleteActivity(String id) async {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.delete("${APIs.activityUrl}$id");
|
||||
final sp = ServerResponse.fromJson(resp.data);
|
||||
|
||||
@@ -59,6 +59,8 @@ class GeneralSetting {
|
||||
bool? allowQiangban;
|
||||
bool? enableNfo;
|
||||
bool? enableAdult;
|
||||
String? tvNamingFormat;
|
||||
String? movieNamingFormat;
|
||||
|
||||
GeneralSetting(
|
||||
{this.tmdbApiKey,
|
||||
@@ -68,6 +70,8 @@ class GeneralSetting {
|
||||
this.enablePlexmatch,
|
||||
this.enableNfo,
|
||||
this.allowQiangban,
|
||||
this.tvNamingFormat,
|
||||
this.movieNamingFormat,
|
||||
this.enableAdult});
|
||||
|
||||
factory GeneralSetting.fromJson(Map<String, dynamic> json) {
|
||||
@@ -79,6 +83,8 @@ class GeneralSetting {
|
||||
enableAdult: json["enable_adult_content"] ?? false,
|
||||
allowQiangban: json["allow_qiangban"] ?? false,
|
||||
enableNfo: json["enable_nfo"] ?? false,
|
||||
tvNamingFormat: json["tv_naming_format"],
|
||||
movieNamingFormat: json["movie_naming_format"],
|
||||
enablePlexmatch: json["enable_plexmatch"] ?? false);
|
||||
}
|
||||
|
||||
@@ -92,6 +98,8 @@ class GeneralSetting {
|
||||
data["allow_qiangban"] = allowQiangban;
|
||||
data["enable_nfo"] = enableNfo;
|
||||
data["enable_adult_content"] = enableAdult;
|
||||
data["tv_naming_format"] = tvNamingFormat;
|
||||
data["movie_naming_format"] = movieNamingFormat;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ class SearchPageData
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
ref.invalidate(tvWatchlistDataProvider);
|
||||
//ref.invalidate(tvWatchlistDataProvider);
|
||||
} else {
|
||||
var resp = await dio.post(APIs.watchlistMovieUrl, data: {
|
||||
"tmdb_id": tmdbId,
|
||||
|
||||
@@ -38,6 +38,8 @@ class _GeneralState extends ConsumerState<GeneralSettings> {
|
||||
"allow_qiangban": v.allowQiangban,
|
||||
"enable_nfo": v.enableNfo,
|
||||
"enable_adult": v.enableAdult,
|
||||
"tv_naming_format": v.tvNamingFormat,
|
||||
"movie_naming_format": v.movieNamingFormat,
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -66,6 +68,22 @@ class _GeneralState extends ConsumerState<GeneralSettings> {
|
||||
hintText: "http://10.0.0.1:1080",
|
||||
helperText: "后台联网代理地址,留空表示不启用代理"),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
decoration: const InputDecoration(
|
||||
icon: Icon(Icons.folder),
|
||||
labelText: "电视剧路径命名规则",
|
||||
helperText:
|
||||
"go template语法,可用的变量为:.NameCN, .NameEN, .Year, .TmdbID"),
|
||||
name: "tv_naming_format",
|
||||
),
|
||||
FormBuilderTextField(
|
||||
decoration: const InputDecoration(
|
||||
icon: Icon(Icons.folder),
|
||||
labelText: "电影路径命名规则",
|
||||
helperText:
|
||||
"go template语法,可用的变量为:.NameCN, .NameEN, .Year, .TmdbID"),
|
||||
name: "movie_naming_format",
|
||||
),
|
||||
SizedBox(
|
||||
width: 300,
|
||||
child: FormBuilderDropdown(
|
||||
@@ -137,6 +155,9 @@ class _GeneralState extends ConsumerState<GeneralSettings> {
|
||||
allowQiangban: values["allow_qiangban"],
|
||||
enableAdult: values["enable_adult"],
|
||||
enableNfo: values["enable_nfo"],
|
||||
tvNamingFormat: values["tv_naming_format"],
|
||||
movieNamingFormat:
|
||||
values["movie_naming_format"],
|
||||
enablePlexmatch:
|
||||
values["enable_plexmatch"]))
|
||||
.then((v) => showSnakeBar("更新成功"));
|
||||
|
||||
@@ -98,8 +98,8 @@ class MediaCard extends StatelessWidget {
|
||||
LinearProgressIndicator(
|
||||
value: 1,
|
||||
color: item.downloadedNum! >= item.monitoredNum!
|
||||
? Colors.green
|
||||
: Colors.blue,
|
||||
? Colors.teal
|
||||
: Colors.lightGreen,
|
||||
),
|
||||
Text(
|
||||
item.name!,
|
||||
|
||||
@@ -38,7 +38,7 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
margin: const EdgeInsets.all(4),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxHeight: 400),
|
||||
constraints: const BoxConstraints(maxHeight: 400),
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
@@ -52,7 +52,7 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
screenWidth < 600
|
||||
? SizedBox()
|
||||
? const SizedBox()
|
||||
: Flexible(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
@@ -72,14 +72,14 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
children: [
|
||||
//const Text(""),
|
||||
Text(
|
||||
"${widget.details.name} ${widget.details.name != widget.details.originalName ? widget.details.originalName : ''} (${widget.details.airDate!.split("-")[0]})",
|
||||
"${widget.details.name} ${widget.details.name != widget.details.originalName ? widget.details.originalName : ''} ${widget.details.airDate == null ? "" : (widget.details.airDate!.split("-")[0])}",
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 2.5),
|
||||
),
|
||||
const Divider(thickness: 1, height: 1),
|
||||
Text(
|
||||
const Text(
|
||||
"",
|
||||
style: TextStyle(height: 0.2),
|
||||
),
|
||||
@@ -93,14 +93,14 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
label: Text(
|
||||
"${widget.details.storage!.name}: ${widget.details.mediaType == "tv" ? widget.details.storage!.tvPath : widget.details.storage!.moviePath}"
|
||||
"${widget.details.targetDir}"),
|
||||
padding: EdgeInsets.all(0),
|
||||
padding: const EdgeInsets.all(0),
|
||||
),
|
||||
Chip(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: ContinuousRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(100.0)),
|
||||
label: Text("${widget.details.resolution}"),
|
||||
padding: EdgeInsets.all(0),
|
||||
padding: const EdgeInsets.all(0),
|
||||
),
|
||||
widget.details.limiter != null &&
|
||||
widget.details.limiter!.sizeMax > 0
|
||||
@@ -109,13 +109,13 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
shape: ContinuousRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(100.0)),
|
||||
padding: EdgeInsets.all(0),
|
||||
padding: const EdgeInsets.all(0),
|
||||
label: Text(
|
||||
"${(widget.details.limiter!.sizeMin).readableFileSize()} - ${(widget.details.limiter!.sizeMax).readableFileSize()}"))
|
||||
: const SizedBox(),
|
||||
MenuAnchor(
|
||||
style:
|
||||
MenuStyle(alignment: Alignment.bottomRight),
|
||||
style: const MenuStyle(
|
||||
alignment: Alignment.bottomRight),
|
||||
menuChildren: [
|
||||
ActionChip.elevated(
|
||||
onPressed: () => launchUrl(url),
|
||||
@@ -124,10 +124,10 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
shape: ContinuousRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(100.0)),
|
||||
padding: EdgeInsets.all(0),
|
||||
label: Text("TMDB")),
|
||||
padding: const EdgeInsets.all(0),
|
||||
label: const Text("TMDB")),
|
||||
isBlank(widget.details.imdbid)
|
||||
? SizedBox()
|
||||
? const SizedBox()
|
||||
: ActionChip.elevated(
|
||||
onPressed: () => launchUrl(imdbUrl),
|
||||
backgroundColor: Colors.indigo[700],
|
||||
@@ -135,8 +135,8 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
shape: ContinuousRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(100.0)),
|
||||
padding: EdgeInsets.all(0),
|
||||
label: Text("IMDB"),
|
||||
padding: const EdgeInsets.all(0),
|
||||
label: const Text("IMDB"),
|
||||
)
|
||||
],
|
||||
builder: (context, controller, child) {
|
||||
@@ -152,8 +152,8 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
shape: ContinuousRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(100.0)),
|
||||
padding: EdgeInsets.all(0),
|
||||
label: Text("外部链接"));
|
||||
padding: const EdgeInsets.all(0),
|
||||
label: const Text("外部链接"));
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -170,7 +170,7 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
children: [
|
||||
downloadButton(),
|
||||
editIcon(),
|
||||
deleteIcon(),
|
||||
deleteIcon(context),
|
||||
],
|
||||
)
|
||||
],
|
||||
@@ -185,14 +185,14 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget deleteIcon() {
|
||||
Widget deleteIcon(BuildContext oriContext) {
|
||||
return IconButton(
|
||||
tooltip: widget.details.mediaType == "tv" ? "删除剧集" : "删除电影",
|
||||
onPressed: () => showConfirmDialog(),
|
||||
onPressed: () => showConfirmDialog(oriContext),
|
||||
icon: const Icon(Icons.delete));
|
||||
}
|
||||
|
||||
Future<void> showConfirmDialog() {
|
||||
Future<void> showConfirmDialog(BuildContext oriContext) {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
@@ -210,9 +210,13 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
.read(mediaDetailsProvider(widget.details.id.toString())
|
||||
.notifier)
|
||||
.delete()
|
||||
.then((v) => context.go(widget.details.mediaType == "tv"
|
||||
.then((v) {
|
||||
if (oriContext.mounted) {
|
||||
oriContext.go(widget.details.mediaType == "tv"
|
||||
? WelcomePage.routeTv
|
||||
: WelcomePage.routeMoivie));
|
||||
: WelcomePage.routeMoivie);
|
||||
}
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text("确认"))
|
||||
@@ -292,7 +296,11 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
.notifier)
|
||||
.edit(values["resolution"], values["target_dir"],
|
||||
values["limiter"])
|
||||
.then((v) => Navigator.of(context).pop());
|
||||
.then((v) {
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
label: const Text("确认"))
|
||||
|
||||
Reference in New Issue
Block a user