mirror of
https://github.com/simon-ding/polaris.git
synced 2026-02-21 22:40:52 +08:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58e65b21fb | ||
|
|
520933085d | ||
|
|
5cc88986d2 | ||
|
|
d63a923589 | ||
|
|
bca68befb1 | ||
|
|
1be44bff9e | ||
|
|
3998270cbd | ||
|
|
73e76c2185 | ||
|
|
c72a460509 | ||
|
|
912293d8e8 | ||
|
|
7f2e84ad52 | ||
|
|
e52ad612c1 | ||
|
|
45a212fec5 | ||
|
|
39bfda4cda | ||
|
|
24a4d3152d | ||
|
|
6c6670a8c0 | ||
|
|
63fc4f277b | ||
|
|
45d2a4fb79 | ||
|
|
5e337871c9 | ||
|
|
803dcfeacd | ||
|
|
c26e61bbee | ||
|
|
e334acba32 | ||
|
|
1359df599b | ||
|
|
16ca00d19c | ||
|
|
f4b8d03cfc | ||
|
|
8811b89889 |
37
db/db.go
37
db/db.go
@@ -162,6 +162,10 @@ func (c *Client) UpdateEpiode(episodeId int, name, overview string) error {
|
||||
return c.ent.Episode.Update().Where(episode.ID(episodeId)).SetTitle(name).SetOverview(overview).Exec(context.TODO())
|
||||
}
|
||||
|
||||
func (c *Client) UpdateEpiode2(episodeId int, name, overview, airdate string) error {
|
||||
return c.ent.Episode.Update().Where(episode.ID(episodeId)).SetTitle(name).SetOverview(overview).SetAirDate(airdate).Exec(context.TODO())
|
||||
}
|
||||
|
||||
type MediaDetails struct {
|
||||
*ent.Media
|
||||
Episodes []*ent.Episode `json:"episodes"`
|
||||
@@ -207,6 +211,19 @@ func (c *Client) SaveEposideDetail(d *ent.Episode) (int, error) {
|
||||
return ep.ID, err
|
||||
}
|
||||
|
||||
func (c *Client) SaveEposideDetail2(d *ent.Episode) (int, error) {
|
||||
ep, err := c.ent.Episode.Create().
|
||||
SetAirDate(d.AirDate).
|
||||
SetSeasonNumber(d.SeasonNumber).
|
||||
SetEpisodeNumber(d.EpisodeNumber).
|
||||
SetMediaID(d.MediaID).
|
||||
SetStatus(d.Status).
|
||||
SetOverview(d.Overview).
|
||||
SetTitle(d.Title).Save(context.TODO())
|
||||
|
||||
return ep.ID, err
|
||||
}
|
||||
|
||||
type TorznabSetting struct {
|
||||
URL string `json:"url"`
|
||||
ApiKey string `json:"api_key"`
|
||||
@@ -299,9 +316,9 @@ func (c *Client) DeleteDownloadCLient(id int) {
|
||||
|
||||
// Storage is the model entity for the Storage schema.
|
||||
type StorageInfo struct {
|
||||
Name string `json:"name"`
|
||||
Implementation string `json:"implementation"`
|
||||
Settings map[string]string `json:"settings"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Implementation string `json:"implementation" binding:"required"`
|
||||
Settings map[string]string `json:"settings" binding:"required"`
|
||||
Default bool `json:"default"`
|
||||
}
|
||||
|
||||
@@ -310,16 +327,15 @@ func (s *StorageInfo) ToWebDavSetting() WebdavSetting {
|
||||
panic("not webdav storage")
|
||||
}
|
||||
return WebdavSetting{
|
||||
URL: s.Settings["url"],
|
||||
TvPath: s.Settings["tv_path"],
|
||||
MoviePath: s.Settings["movie_path"],
|
||||
User: s.Settings["user"],
|
||||
Password: s.Settings["password"],
|
||||
URL: s.Settings["url"],
|
||||
TvPath: s.Settings["tv_path"],
|
||||
MoviePath: s.Settings["movie_path"],
|
||||
User: s.Settings["user"],
|
||||
Password: s.Settings["password"],
|
||||
ChangeFileHash: s.Settings["change_file_hash"],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type LocalDirSetting struct {
|
||||
TvPath string `json:"tv_path"`
|
||||
MoviePath string `json:"movie_path"`
|
||||
@@ -501,7 +517,6 @@ func (c *Client) TmdbIdInWatchlist(tmdb_id int) bool {
|
||||
return c.ent.Media.Query().Where(media.TmdbID(tmdb_id)).CountX(context.TODO()) > 0
|
||||
}
|
||||
|
||||
|
||||
func (c *Client) GetDownloadHistory(mediaID int) ([]*ent.History, error) {
|
||||
return c.ent.History.Query().Where(history.MediaID(mediaID)).All(context.TODO())
|
||||
}
|
||||
}
|
||||
|
||||
6
go.mod
6
go.mod
@@ -11,7 +11,7 @@ require (
|
||||
golang.org/x/net v0.25.0
|
||||
)
|
||||
|
||||
require github.com/adrg/strutil v0.3.1 // indirect
|
||||
require github.com/adrg/strutil v0.3.1
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.19.1-0.20240203083654-5948b60a8e43 // indirect
|
||||
@@ -56,9 +56,9 @@ require (
|
||||
github.com/zclconf/go-cty v1.8.0 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.23.0
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/sys v0.21.0
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
||||
10
go.sum
10
go.sum
@@ -91,8 +91,6 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
|
||||
@@ -104,8 +102,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -129,8 +125,6 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
@@ -177,8 +171,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -190,8 +182,6 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
|
||||
@@ -15,6 +15,7 @@ type MovieMetadata struct {
|
||||
}
|
||||
|
||||
func ParseMovie(name string) *MovieMetadata {
|
||||
name = strings.Join(strings.Fields(name), " ") //remove unnessary spaces
|
||||
name = strings.ToLower(strings.TrimSpace(name))
|
||||
var meta = &MovieMetadata{}
|
||||
yearRe := regexp.MustCompile(`\(\d{4}\)`)
|
||||
|
||||
@@ -9,28 +9,26 @@ import (
|
||||
)
|
||||
|
||||
type Metadata struct {
|
||||
NameEn string
|
||||
NameCn string
|
||||
Season int
|
||||
Episode int
|
||||
Resolution string
|
||||
NameEn string
|
||||
NameCn string
|
||||
Season int
|
||||
Episode int
|
||||
Resolution string
|
||||
IsSeasonPack bool
|
||||
}
|
||||
|
||||
func ParseTv(name string) *Metadata {
|
||||
name = strings.ToLower(name)
|
||||
if utils.IsASCII(name) { //english name
|
||||
return parseEnglishName(name)
|
||||
}
|
||||
name = strings.ReplaceAll(name, "\u200b", "") //remove unicode hidden character
|
||||
if utils.ContainsChineseChar(name) {
|
||||
return parseChineseName(name)
|
||||
}
|
||||
return nil
|
||||
return parseEnglishName(name)
|
||||
}
|
||||
|
||||
func parseEnglishName(name string) *Metadata {
|
||||
re := regexp.MustCompile(`[^\p{L}\w\s]`)
|
||||
name = re.ReplaceAllString(strings.ToLower(name), "")
|
||||
name = re.ReplaceAllString(strings.ToLower(name), " ")
|
||||
|
||||
splits := strings.Split(strings.TrimSpace(name), " ")
|
||||
var newSplits []string
|
||||
@@ -71,11 +69,6 @@ func parseEnglishName(name string) *Metadata {
|
||||
}
|
||||
if seasonIndex != -1 {
|
||||
//season exists
|
||||
if seasonIndex != 0 {
|
||||
//name exists
|
||||
names := newSplits[0:seasonIndex]
|
||||
meta.NameEn = strings.Join(names, " ")
|
||||
}
|
||||
ss := seasonRe.FindAllString(newSplits[seasonIndex], -1)
|
||||
if len(ss) != 0 {
|
||||
//season info
|
||||
@@ -87,6 +80,24 @@ func parseEnglishName(name string) *Metadata {
|
||||
}
|
||||
meta.Season = n
|
||||
}
|
||||
} else { //maybe like Season 1?
|
||||
seasonRe := regexp.MustCompile(`season \d{1,2}`)
|
||||
matches := seasonRe.FindAllString(name, -1)
|
||||
if len(matches) > 0 {
|
||||
for i, s := range newSplits {
|
||||
if s == "season" {
|
||||
seasonIndex = i
|
||||
}
|
||||
}
|
||||
numRe := regexp.MustCompile(`\d{1,2}`)
|
||||
seNum := numRe.FindAllString(matches[0], -1)[0]
|
||||
n, err := strconv.Atoi(seNum)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("convert %s error: %v", seNum, err))
|
||||
}
|
||||
meta.Season = n
|
||||
|
||||
}
|
||||
}
|
||||
if episodeIndex != -1 {
|
||||
ep := episodeRe.FindAllString(newSplits[episodeIndex], -1)
|
||||
@@ -99,14 +110,46 @@ func parseEnglishName(name string) *Metadata {
|
||||
}
|
||||
meta.Episode = n
|
||||
}
|
||||
} else { //no episode, maybe like One Punch Man S2 - 08 [1080p].mkv
|
||||
|
||||
numRe := regexp.MustCompile(`^\d{1,2}$`)
|
||||
for i, p := range newSplits {
|
||||
if numRe.MatchString(p) {
|
||||
if i > 0 && strings.Contains(newSplits[i-1], "season") { //last word cannot be season
|
||||
continue
|
||||
}
|
||||
if i < seasonIndex {
|
||||
//episode number most likely should comes alfter season number
|
||||
continue
|
||||
}
|
||||
//episodeIndex = i
|
||||
n, err := strconv.Atoi(p)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("convert %s error: %v", p, err))
|
||||
}
|
||||
meta.Episode = n
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if resIndex != -1 {
|
||||
//resolution exists
|
||||
meta.Resolution = newSplits[resIndex]
|
||||
}
|
||||
if meta.Episode == -1 {
|
||||
if meta.Episode == -1 || strings.Contains(name, "complete") {
|
||||
meta.Episode = -1
|
||||
meta.IsSeasonPack = true
|
||||
}
|
||||
|
||||
if seasonIndex > 0 {
|
||||
//name exists
|
||||
names := newSplits[0:seasonIndex]
|
||||
meta.NameEn = strings.TrimSpace(strings.Join(names, " "))
|
||||
} else {
|
||||
meta.NameEn = name
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
@@ -144,10 +187,10 @@ func parseChineseName(name string) *Metadata {
|
||||
}
|
||||
meta.Episode = n
|
||||
} else { //【第09話】
|
||||
re2 := regexp.MustCompile(`第\d{1,2}(话|話|集)`)
|
||||
re2 := regexp.MustCompile(`第\d{1,4}(话|話|集)`)
|
||||
episodeMatches1 := re2.FindAllString(name, -1)
|
||||
if len(episodeMatches1) > 0 {
|
||||
re := regexp.MustCompile(`\d{1,2}`)
|
||||
re := regexp.MustCompile(`\d{1,4}`)
|
||||
epNum := re.FindAllString(episodeMatches1[0], -1)[0]
|
||||
n, err := strconv.Atoi(epNum)
|
||||
if err != nil {
|
||||
@@ -158,9 +201,9 @@ func parseChineseName(name string) *Metadata {
|
||||
re3 := regexp.MustCompile(`[^\d\w]\d{1,2}[^\d\w]`)
|
||||
epNums := re3.FindAllString(name, -1)
|
||||
if len(epNums) > 0 {
|
||||
|
||||
|
||||
re3 := regexp.MustCompile(`\d{1,2}`)
|
||||
epNum := re3.FindAllString(epNums[0],-1)[0]
|
||||
epNum := re3.FindAllString(epNums[0], -1)[0]
|
||||
n, err := strconv.Atoi(epNum)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
|
||||
@@ -203,10 +246,10 @@ func parseChineseName(name string) *Metadata {
|
||||
}
|
||||
}
|
||||
|
||||
if meta.IsSeasonPack && meta.Episode != 0{
|
||||
if meta.IsSeasonPack && meta.Episode != 0 {
|
||||
meta.Season = meta.Episode
|
||||
meta.Episode = -1
|
||||
}
|
||||
}
|
||||
|
||||
//tv name
|
||||
title := name
|
||||
|
||||
@@ -28,7 +28,7 @@ type LocalStorage struct {
|
||||
|
||||
func (l *LocalStorage) Move(src, dest string) error {
|
||||
targetDir := filepath.Join(l.dir, dest)
|
||||
os.MkdirAll(targetDir, 0655)
|
||||
os.MkdirAll(filepath.Dir(targetDir), 0655)
|
||||
err := filepath.Walk(src, func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -28,6 +28,9 @@ func NewClient(apiKey string) (*Client, error) {
|
||||
|
||||
func (c *Client) GetTvDetails(id int, language string) (*tmdb.TVDetails, error) {
|
||||
d, err := c.tmdbClient.GetTVDetails(id, withLangOption(language))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get tv detail")
|
||||
}
|
||||
|
||||
log.Infof("tv id %d, language %s", id, language)
|
||||
if !episodeNameUseful(d.LastEpisodeToAir.Name) {
|
||||
@@ -38,13 +41,12 @@ func (c *Client) GetTvDetails(id int, language string) (*tmdb.TVDetails, error)
|
||||
if err != nil {
|
||||
return d, nil
|
||||
}
|
||||
|
||||
}
|
||||
if episodeNameUseful(detailEN.LastEpisodeToAir.Name) {
|
||||
d.LastEpisodeToAir.Name = detailEN.LastEpisodeToAir.Name
|
||||
d.LastEpisodeToAir.Overview = detailEN.LastEpisodeToAir.Overview
|
||||
d.NextEpisodeToAir.Name = detailEN.NextEpisodeToAir.Name
|
||||
d.NextEpisodeToAir.Overview = detailEN.NextEpisodeToAir.Overview
|
||||
if episodeNameUseful(detailEN.LastEpisodeToAir.Name) {
|
||||
d.LastEpisodeToAir.Name = detailEN.LastEpisodeToAir.Name
|
||||
d.LastEpisodeToAir.Overview = detailEN.LastEpisodeToAir.Overview
|
||||
d.NextEpisodeToAir.Name = detailEN.NextEpisodeToAir.Name
|
||||
d.NextEpisodeToAir.Overview = detailEN.NextEpisodeToAir.Overview
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ func (r *Response) ToResults() []Result {
|
||||
for _, item := range r.Channel.Item {
|
||||
r := Result{
|
||||
Name: item.Title,
|
||||
Magnet: item.Link,
|
||||
Link: item.Link,
|
||||
Size: mustAtoI(item.Size),
|
||||
Seeders: mustAtoI(item.GetAttr("seeders")),
|
||||
Peers: mustAtoI(item.GetAttr("peers")),
|
||||
@@ -129,7 +129,7 @@ func Search(torznabUrl, api, keyWord string) ([]Result, error) {
|
||||
|
||||
type Result struct {
|
||||
Name string
|
||||
Magnet string
|
||||
Link string
|
||||
Size int
|
||||
Seeders int
|
||||
Peers int
|
||||
|
||||
@@ -3,8 +3,11 @@ package transmission
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"polaris/log"
|
||||
"strings"
|
||||
|
||||
"github.com/hekmon/transmissionrpc/v3"
|
||||
"github.com/pkg/errors"
|
||||
@@ -25,6 +28,10 @@ func NewClient(c Config) (*Client, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "connect transmission")
|
||||
}
|
||||
_, err = tbt.TorrentGetAll(context.TODO())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "transmission cannot connect")
|
||||
}
|
||||
return &Client{c: tbt, cfg: c}, nil
|
||||
}
|
||||
|
||||
@@ -34,20 +41,42 @@ type Config struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
type Client struct {
|
||||
c *transmissionrpc.Client
|
||||
c *transmissionrpc.Client
|
||||
cfg Config
|
||||
}
|
||||
|
||||
func (c *Client) Download(magnet, dir string) (*Torrent, error) {
|
||||
func (c *Client) Download(link, dir string) (*Torrent, error) {
|
||||
if strings.HasPrefix(link, "http") {
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
resp, err:=client.Get(link)
|
||||
if err == nil {
|
||||
if resp.StatusCode == http.StatusFound {
|
||||
loc, err := resp.Location()
|
||||
if err == nil {
|
||||
link = loc.String()
|
||||
log.Warnf("transimision redirect to url: %v", link)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
t, err := c.c.TorrentAdd(context.TODO(), transmissionrpc.TorrentAddPayload{
|
||||
Filename: &magnet,
|
||||
Filename: &link,
|
||||
DownloadDir: &dir,
|
||||
})
|
||||
log.Infof("get torrent info: %+v", t)
|
||||
if t.ID == nil {
|
||||
return nil, fmt.Errorf("download torrent error: %v", link)
|
||||
}
|
||||
|
||||
return &Torrent{
|
||||
ID: *t.ID,
|
||||
c: c.c,
|
||||
ID: *t.ID,
|
||||
c: c.c,
|
||||
Config: c.cfg,
|
||||
}, err
|
||||
}
|
||||
@@ -95,7 +124,7 @@ func (t *Torrent) Progress() int {
|
||||
if t.getTorrent().PercentComplete != nil && *t.getTorrent().PercentComplete >= 1 {
|
||||
return 100
|
||||
}
|
||||
|
||||
|
||||
if t.getTorrent().PercentComplete != nil {
|
||||
p := int(*t.getTorrent().PercentComplete * 100)
|
||||
if p == 100 {
|
||||
@@ -143,4 +172,4 @@ func ReloadTorrent(s string) (*Torrent, error) {
|
||||
return nil, errors.Wrap(err, "reload client")
|
||||
}
|
||||
return &torrent, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,10 +57,14 @@ func RandString(n int) string {
|
||||
}
|
||||
|
||||
func IsNameAcceptable(name1, name2 string) bool {
|
||||
|
||||
re := regexp.MustCompile(`[^\p{L}\w\s]`)
|
||||
name1 = re.ReplaceAllString(strings.ToLower(name1), "")
|
||||
name2 = re.ReplaceAllString(strings.ToLower(name2), "")
|
||||
name1 = re.ReplaceAllString(strings.ToLower(name1), " ")
|
||||
name2 = re.ReplaceAllString(strings.ToLower(name2), " ")
|
||||
name1 = strings.Join(strings.Fields(name1), " ")
|
||||
name2 = strings.Join(strings.Fields(name2), " ")
|
||||
if strings.Contains(name1, name2) || strings.Contains(name2, name1) {
|
||||
return true
|
||||
}
|
||||
return strutil.Similarity(name1, name2, metrics.NewHamming()) > 0.4
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,21 @@ func SearchSeasonPackage(db1 *db.Client, seriesId, seasonNum int, checkResolutio
|
||||
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) {
|
||||
series := db1.GetMediaDetails(seriesId)
|
||||
if series == nil {
|
||||
@@ -31,11 +46,24 @@ func SearchEpisode(db1 *db.Client, seriesId, seasonNum, episodeNum int, checkRes
|
||||
|
||||
var filtered []torznab.Result
|
||||
for _, r := range res {
|
||||
//log.Infof("torrent resource: %+v", r)
|
||||
meta := metadata.ParseTv(r.Name)
|
||||
if meta.Season != seasonNum {
|
||||
if meta == nil { //cannot parse name
|
||||
continue
|
||||
}
|
||||
if episodeNum != -1 && meta.Episode != episodeNum { //not season pack
|
||||
if !isNumberedSeries(series) { //do not check season on series that only rely on episode number
|
||||
if meta.Season != seasonNum {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if isNumberedSeries(series) && episodeNum == -1 {
|
||||
//should not want season
|
||||
continue
|
||||
}
|
||||
|
||||
if episodeNum != -1 && meta.Episode != episodeNum { //not season pack, episode number equals
|
||||
continue
|
||||
} else if seasonNum == -1 && !meta.IsSeasonPack { //want season pack, but not season pack
|
||||
continue
|
||||
}
|
||||
if checkResolution && meta.Resolution != series.Resolution.String() {
|
||||
|
||||
@@ -2,12 +2,10 @@ package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"polaris/db"
|
||||
"polaris/ent"
|
||||
"polaris/ent/episode"
|
||||
"polaris/ent/history"
|
||||
"polaris/log"
|
||||
"polaris/pkg/transmission"
|
||||
"polaris/pkg/utils"
|
||||
"polaris/server/core"
|
||||
"strconv"
|
||||
@@ -16,58 +14,6 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type addTorznabIn struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
ApiKey string `json:"api_key"`
|
||||
}
|
||||
|
||||
func (s *Server) AddTorznabInfo(c *gin.Context) (interface{}, error) {
|
||||
var in addTorznabIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
err := s.db.SaveTorznabInfo(in.Name, db.TorznabSetting{
|
||||
URL: in.URL,
|
||||
ApiKey: in.ApiKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "add ")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteTorznabInfo(c *gin.Context) (interface{}, error) {
|
||||
var ids = c.Param("id")
|
||||
id, err := strconv.Atoi(ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("id is not correct: %v", ids)
|
||||
}
|
||||
s.db.DeleteTorznab(id)
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
func (s *Server) GetAllIndexers(c *gin.Context) (interface{}, error) {
|
||||
indexers := s.db.GetAllTorznabInfo()
|
||||
if len(indexers) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return indexers, nil
|
||||
}
|
||||
|
||||
func (s *Server) getDownloadClient() (*transmission.Client, error) {
|
||||
tr := s.db.GetTransmission()
|
||||
trc, err := transmission.NewClient(transmission.Config{
|
||||
URL: tr.URL,
|
||||
User: tr.User,
|
||||
Password: tr.Password,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "connect transmission")
|
||||
}
|
||||
return trc, nil
|
||||
}
|
||||
|
||||
func (s *Server) searchAndDownloadSeasonPackage(seriesId, seasonNum int) (*string, error) {
|
||||
trc, err := s.getDownloadClient()
|
||||
if err != nil {
|
||||
@@ -80,7 +26,7 @@ func (s *Server) searchAndDownloadSeasonPackage(seriesId, seasonNum int) (*strin
|
||||
}
|
||||
|
||||
r1 := res[0]
|
||||
log.Infof("found resource to download: %v", r1)
|
||||
log.Infof("found resource to download: %+v", r1)
|
||||
|
||||
downloadDir := s.db.GetDownloadDir()
|
||||
size := utils.AvailableSpace(downloadDir)
|
||||
@@ -89,7 +35,7 @@ func (s *Server) searchAndDownloadSeasonPackage(seriesId, seasonNum int) (*strin
|
||||
return nil, errors.New("no enough space")
|
||||
}
|
||||
|
||||
torrent, err := trc.Download(r1.Magnet, s.db.GetDownloadDir())
|
||||
torrent, err := trc.Download(r1.Link, s.db.GetDownloadDir())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "downloading")
|
||||
}
|
||||
@@ -99,7 +45,7 @@ func (s *Server) searchAndDownloadSeasonPackage(seriesId, seasonNum int) (*strin
|
||||
if series == nil {
|
||||
return nil, fmt.Errorf("no tv series of id %v", seriesId)
|
||||
}
|
||||
dir := fmt.Sprintf("%s/Season %02d", series.TargetDir, seasonNum)
|
||||
dir := fmt.Sprintf("%s/Season %02d/", series.TargetDir, seasonNum)
|
||||
|
||||
history, err := s.db.SaveHistoryRecord(ent.History{
|
||||
MediaID: seriesId,
|
||||
@@ -143,14 +89,14 @@ func (s *Server) searchAndDownload(seriesId, seasonNum, episodeNum int) (*string
|
||||
return nil, err
|
||||
}
|
||||
r1 := res[0]
|
||||
log.Infof("found resource to download: %v", r1)
|
||||
torrent, err := trc.Download(r1.Magnet, s.db.GetDownloadDir())
|
||||
log.Infof("found resource to download: %+v", r1)
|
||||
torrent, err := trc.Download(r1.Link, s.db.GetDownloadDir())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "downloading")
|
||||
}
|
||||
torrent.Start()
|
||||
|
||||
dir := fmt.Sprintf("%s/Season %02d", series.TargetDir, seasonNum)
|
||||
dir := fmt.Sprintf("%s/Season %02d/", series.TargetDir, seasonNum)
|
||||
|
||||
history, err := s.db.SaveHistoryRecord(ent.History{
|
||||
MediaID: ep.MediaID,
|
||||
@@ -195,7 +141,7 @@ func (s *Server) SearchAvailableEpisodeResource(c *gin.Context) (interface{}, er
|
||||
Size: r.Size,
|
||||
Seeders: r.Seeders,
|
||||
Peers: r.Peers,
|
||||
Link: r.Magnet,
|
||||
Link: r.Link,
|
||||
})
|
||||
}
|
||||
if len(searchResults) == 0 {
|
||||
@@ -268,7 +214,7 @@ func (s *Server) SearchAvailableMovies(c *gin.Context) (interface{}, error) {
|
||||
Size: r.Size,
|
||||
Seeders: r.Seeders,
|
||||
Peers: r.Peers,
|
||||
Link: r.Magnet,
|
||||
Link: r.Link,
|
||||
})
|
||||
}
|
||||
if len(searchResults) == 0 {
|
||||
@@ -278,7 +224,7 @@ func (s *Server) SearchAvailableMovies(c *gin.Context) (interface{}, error) {
|
||||
}
|
||||
|
||||
type downloadTorrentIn struct {
|
||||
MediaID int `json:"media_id" binding:"required"`
|
||||
MediaID int `json:"media_id" binding:"required"`
|
||||
TorznabSearchResult
|
||||
}
|
||||
|
||||
@@ -303,13 +249,16 @@ func (s *Server) DownloadMovieTorrent(c *gin.Context) (interface{}, error) {
|
||||
return nil, errors.Wrap(err, "downloading")
|
||||
}
|
||||
torrent.Start()
|
||||
|
||||
name := in.Name
|
||||
if name == "" {
|
||||
name = media.OriginalName
|
||||
}
|
||||
go func() {
|
||||
ep := media.Episodes[0]
|
||||
history, err := s.db.SaveHistoryRecord(ent.History{
|
||||
MediaID: media.ID,
|
||||
EpisodeID: ep.ID,
|
||||
SourceTitle: media.NameCn,
|
||||
SourceTitle: name,
|
||||
TargetDir: "./",
|
||||
Status: history.StatusRunning,
|
||||
Size: in.Size,
|
||||
@@ -329,39 +278,3 @@ func (s *Server) DownloadMovieTorrent(c *gin.Context) (interface{}, error) {
|
||||
|
||||
}
|
||||
|
||||
type downloadClientIn struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
Implementation string `json:"implementation"`
|
||||
}
|
||||
|
||||
func (s *Server) AddDownloadClient(c *gin.Context) (interface{}, error) {
|
||||
var in downloadClientIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
if err := s.db.SaveTransmission(in.Name, in.URL, in.User, in.Password); err != nil {
|
||||
return nil, errors.Wrap(err, "save transmission")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetAllDonloadClients(c *gin.Context) (interface{}, error) {
|
||||
res := s.db.GetAllDonloadClients()
|
||||
if len(res) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteDownloadCLient(c *gin.Context) (interface{}, error) {
|
||||
var ids = c.Param("id")
|
||||
id, err := strconv.Atoi(ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("id is not correct: %v", ids)
|
||||
}
|
||||
s.db.DeleteDownloadCLient(id)
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ func (s *Server) scheduler() {
|
||||
s.downloadTvSeries()
|
||||
s.downloadMovie()
|
||||
})
|
||||
s.mustAddCron("@every 12h", s.checkAllSeriesNewSeason)
|
||||
s.cron.Start()
|
||||
}
|
||||
|
||||
@@ -294,7 +295,7 @@ func (s *Server) downloadMovieSingleEpisode(ep *ent.Episode) error {
|
||||
}
|
||||
r1 := res[0]
|
||||
log.Infof("begin download torrent resource: %v", r1.Name)
|
||||
torrent, err := trc.Download(r1.Magnet, s.db.GetDownloadDir())
|
||||
torrent, err := trc.Download(r1.Link, s.db.GetDownloadDir())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "downloading")
|
||||
}
|
||||
@@ -318,3 +319,49 @@ func (s *Server) downloadMovieSingleEpisode(ep *ent.Episode) error {
|
||||
s.db.SetEpisodeStatus(ep.ID, episode.StatusDownloading)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) checkAllSeriesNewSeason() {
|
||||
log.Infof("begin checking series all new season")
|
||||
allSeries := s.db.GetMediaWatchlist(media.MediaTypeTv)
|
||||
for _, series := range allSeries {
|
||||
err := s.checkSeiesNewSeason(series)
|
||||
if err != nil {
|
||||
log.Errorf("check series new season error: series name %v, error: %v", series.NameEn, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) checkSeiesNewSeason(media *ent.Media) error{
|
||||
d, err := s.MustTMDB().GetTvDetails(media.TmdbID, s.language)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tmdb")
|
||||
}
|
||||
lastsSason := d.NumberOfSeasons
|
||||
seasonDetail, err := s.MustTMDB().GetSeasonDetails(media.TmdbID, lastsSason, s.language)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tmdb season")
|
||||
}
|
||||
|
||||
for _, ep := range seasonDetail.Episodes {
|
||||
epDb, err := s.db.GetEpisode(media.ID, ep.SeasonNumber, ep.EpisodeNumber)
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
log.Infof("add new episode: %+v", ep)
|
||||
episode := &ent.Episode{
|
||||
MediaID: media.ID,
|
||||
SeasonNumber: ep.SeasonNumber,
|
||||
EpisodeNumber: ep.EpisodeNumber,
|
||||
Title: ep.Name,
|
||||
Overview: ep.Overview,
|
||||
AirDate: ep.AirDate,
|
||||
Status: episode.StatusMissing,
|
||||
}
|
||||
s.db.SaveEposideDetail2(episode)
|
||||
}
|
||||
} else {//update episode
|
||||
log.Infof("update new episode: %+v", ep)
|
||||
s.db.UpdateEpiode2(epDb.ID, ep.Name, ep.Overview, ep.AirDate)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"polaris/db"
|
||||
"polaris/log"
|
||||
"polaris/pkg/transmission"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
||||
type GeneralSettings struct {
|
||||
TmdbApiKey string `json:"tmdb_api_key"`
|
||||
TmdbApiKey string `json:"tmdb_api_key"`
|
||||
DownloadDir string `json:"download_dir"`
|
||||
}
|
||||
|
||||
func (s *Server) SetSetting(c *gin.Context) (interface{}, error) {
|
||||
var in GeneralSettings
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
@@ -35,9 +38,108 @@ func (s *Server) SetSetting(c *gin.Context) (interface{}, error) {
|
||||
func (s *Server) GetSetting(c *gin.Context) (interface{}, error) {
|
||||
tmdb := s.db.GetSetting(db.SettingTmdbApiKey)
|
||||
downloadDir := s.db.GetSetting(db.SettingDownloadDir)
|
||||
|
||||
|
||||
return &GeneralSettings{
|
||||
TmdbApiKey: tmdb,
|
||||
TmdbApiKey: tmdb,
|
||||
DownloadDir: downloadDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type addTorznabIn struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
URL string `json:"url" binding:"required"`
|
||||
ApiKey string `json:"api_key" binding:"required"`
|
||||
}
|
||||
|
||||
func (s *Server) AddTorznabInfo(c *gin.Context) (interface{}, error) {
|
||||
var in addTorznabIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
err := s.db.SaveTorznabInfo(in.Name, db.TorznabSetting{
|
||||
URL: in.URL,
|
||||
ApiKey: in.ApiKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "add ")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteTorznabInfo(c *gin.Context) (interface{}, error) {
|
||||
var ids = c.Param("id")
|
||||
id, err := strconv.Atoi(ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("id is not correct: %v", ids)
|
||||
}
|
||||
s.db.DeleteTorznab(id)
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
func (s *Server) GetAllIndexers(c *gin.Context) (interface{}, error) {
|
||||
indexers := s.db.GetAllTorznabInfo()
|
||||
if len(indexers) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return indexers, nil
|
||||
}
|
||||
|
||||
func (s *Server) getDownloadClient() (*transmission.Client, error) {
|
||||
tr := s.db.GetTransmission()
|
||||
trc, err := transmission.NewClient(transmission.Config{
|
||||
URL: tr.URL,
|
||||
User: tr.User,
|
||||
Password: tr.Password,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "connect transmission")
|
||||
}
|
||||
return trc, nil
|
||||
}
|
||||
|
||||
|
||||
type downloadClientIn struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
URL string `json:"url" binding:"required"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
Implementation string `json:"implementation" binding:"required"`
|
||||
}
|
||||
|
||||
func (s *Server) AddDownloadClient(c *gin.Context) (interface{}, error) {
|
||||
var in downloadClientIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
//test connection
|
||||
_, err := transmission.NewClient(transmission.Config{
|
||||
URL: in.URL,
|
||||
User: in.User,
|
||||
Password: in.Password,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tranmission setting")
|
||||
}
|
||||
if err := s.db.SaveTransmission(in.Name, in.URL, in.User, in.Password); err != nil {
|
||||
return nil, errors.Wrap(err, "save transmission")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetAllDonloadClients(c *gin.Context) (interface{}, error) {
|
||||
res := s.db.GetAllDonloadClients()
|
||||
if len(res) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteDownloadCLient(c *gin.Context) (interface{}, error) {
|
||||
var ids = c.Param("id")
|
||||
id, err := strconv.Atoi(ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("id is not correct: %v", ids)
|
||||
}
|
||||
s.db.DeleteDownloadCLient(id)
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ class _ActivityPageState extends ConsumerState<ActivityPage>
|
||||
ref
|
||||
.read(activitiesDataProvider("active").notifier)
|
||||
.deleteActivity(id)
|
||||
.whenComplete(() => Utils.showSnakeBar("删除成功"))
|
||||
.then((v) => Utils.showSnakeBar("删除成功"))
|
||||
.onError((error, trace) => Utils.showSnakeBar("删除失败:$error"));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ class _MovieDetailsPageState extends ConsumerState<MovieDetailsPage> {
|
||||
widget.id)
|
||||
.notifier)
|
||||
.delete()
|
||||
.whenComplete(() => context
|
||||
.then((v) => context
|
||||
.go(WelcomePage.routeMoivie))
|
||||
.onError((error, trace) =>
|
||||
Utils.showSnakeBar(
|
||||
@@ -252,8 +252,8 @@ class _NestedTabBarState extends ConsumerState<NestedTabBar>
|
||||
ref
|
||||
.read(movieTorrentsDataProvider(widget.id)
|
||||
.notifier)
|
||||
.download(torrent.link!)
|
||||
.whenComplete(() =>
|
||||
.download(torrent)
|
||||
.then((v) =>
|
||||
Utils.showSnakeBar("开始下载:${torrent.name}"))
|
||||
.onError((error, trace) =>
|
||||
Utils.showSnakeBar("操作失败: $error"));
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:ui/providers/APIs.dart';
|
||||
import 'package:ui/providers/server_response.dart';
|
||||
|
||||
var activitiesDataProvider =
|
||||
AsyncNotifierProvider.autoDispose.family<ActivityData, List<Activity>, String>(
|
||||
AsyncNotifierProvider.family<ActivityData, List<Activity>, String>(
|
||||
ActivityData.new);
|
||||
|
||||
var mediaHistoryDataProvider = FutureProvider.autoDispose.family(
|
||||
@@ -24,8 +24,7 @@ var mediaHistoryDataProvider = FutureProvider.autoDispose.family(
|
||||
},
|
||||
);
|
||||
|
||||
class ActivityData
|
||||
extends AutoDisposeFamilyAsyncNotifier<List<Activity>, String> {
|
||||
class ActivityData extends FamilyAsyncNotifier<List<Activity>, String> {
|
||||
@override
|
||||
FutureOr<List<Activity>> build(String arg) async {
|
||||
if (arg == "active") {
|
||||
@@ -35,7 +34,8 @@ class ActivityData
|
||||
}
|
||||
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.get(APIs.activityUrl, queryParameters: {"status": arg});
|
||||
var resp =
|
||||
await dio.get(APIs.activityUrl, queryParameters: {"status": arg});
|
||||
final sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
|
||||
@@ -41,10 +41,11 @@ class AuthSettingData extends AutoDisposeAsyncNotifier<AuthSetting> {
|
||||
class AuthSetting {
|
||||
bool enable;
|
||||
String user;
|
||||
String password;
|
||||
|
||||
AuthSetting({required this.enable, required this.user});
|
||||
AuthSetting({required this.enable, required this.user, required this.password});
|
||||
|
||||
factory AuthSetting.fromJson(Map<String, dynamic> json) {
|
||||
return AuthSetting(enable: json["enable"], user: json["user"]);
|
||||
return AuthSetting(enable: json["enable"], user: json["user"], password: json["password"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,24 +181,23 @@ class MediaDetail {
|
||||
}
|
||||
|
||||
class SearchResult {
|
||||
SearchResult({
|
||||
required this.backdropPath,
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.originalName,
|
||||
required this.overview,
|
||||
required this.posterPath,
|
||||
required this.mediaType,
|
||||
required this.adult,
|
||||
required this.originalLanguage,
|
||||
required this.genreIds,
|
||||
required this.popularity,
|
||||
required this.firstAirDate,
|
||||
required this.voteAverage,
|
||||
required this.voteCount,
|
||||
required this.originCountry,
|
||||
this.inWatchlist
|
||||
});
|
||||
SearchResult(
|
||||
{required this.backdropPath,
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.originalName,
|
||||
required this.overview,
|
||||
required this.posterPath,
|
||||
required this.mediaType,
|
||||
required this.adult,
|
||||
required this.originalLanguage,
|
||||
required this.genreIds,
|
||||
required this.popularity,
|
||||
required this.firstAirDate,
|
||||
required this.voteAverage,
|
||||
required this.voteCount,
|
||||
required this.originCountry,
|
||||
this.inWatchlist});
|
||||
|
||||
final String? backdropPath;
|
||||
final int? id;
|
||||
@@ -258,10 +257,12 @@ class MovieTorrentResource
|
||||
return (rsp.data as List).map((v) => TorrentResource.fromJson(v)).toList();
|
||||
}
|
||||
|
||||
Future<void> download(String link) async {
|
||||
Future<void> download(TorrentResource res) async {
|
||||
var m = res.toJson();
|
||||
m["media_id"] = int.parse(mediaId!);
|
||||
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.post(APIs.availableMoviesUrl,
|
||||
data: {"media_id": int.parse(mediaId!), "link": link});
|
||||
var resp = await dio.post(APIs.availableMoviesUrl, data: m);
|
||||
var rsp = ServerResponse.fromJson(resp.data);
|
||||
if (rsp.code != 0) {
|
||||
throw rsp.message;
|
||||
@@ -286,4 +287,11 @@ class TorrentResource {
|
||||
peers: json["peers"],
|
||||
link: json["link"]);
|
||||
}
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['name'] = name;
|
||||
data['size'] = size;
|
||||
data["link"] = link;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:ui/providers/APIs.dart';
|
||||
import 'package:ui/providers/settings.dart';
|
||||
import 'package:ui/providers/welcome_data.dart';
|
||||
import 'package:ui/utils.dart';
|
||||
import 'package:ui/widgets/progress_indicator.dart';
|
||||
|
||||
class SearchPage extends ConsumerStatefulWidget {
|
||||
@@ -257,13 +258,14 @@ class _SearchPageState extends ConsumerState<SearchPage> {
|
||||
ref
|
||||
.read(searchPageDataProvider(widget.query ?? "")
|
||||
.notifier)
|
||||
.submit2Watchlist(
|
||||
item.id!,
|
||||
storageSelected,
|
||||
resSelected,
|
||||
item.mediaType!,
|
||||
pathController.text);
|
||||
Navigator.of(context).pop();
|
||||
.submit2Watchlist(item.id!, storageSelected,
|
||||
resSelected, item.mediaType!, pathController.text)
|
||||
.then((v) {
|
||||
Utils.showSnakeBar("添加成功");
|
||||
Navigator.of(context).pop();
|
||||
}).onError((error, trace) {
|
||||
Utils.showSnakeBar("添加失败:$error");
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:quiver/strings.dart';
|
||||
import 'package:ui/providers/login.dart';
|
||||
@@ -6,6 +7,7 @@ import 'package:ui/providers/settings.dart';
|
||||
import 'package:ui/utils.dart';
|
||||
import 'package:ui/widgets/progress_indicator.dart';
|
||||
import 'package:ui/widgets/widgets.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
|
||||
class SystemSettingsPage extends ConsumerStatefulWidget {
|
||||
static const route = "/settings";
|
||||
@@ -18,10 +20,8 @@ class SystemSettingsPage extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
|
||||
final GlobalKey _formKey = GlobalKey<FormState>();
|
||||
|
||||
final _tmdbApiController = TextEditingController();
|
||||
final _downloadDirController = TextEditingController();
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
final _formKey2 = GlobalKey<FormBuilderState>();
|
||||
bool? _enableAuth;
|
||||
|
||||
@override
|
||||
@@ -30,62 +30,56 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
|
||||
|
||||
var tmdbSetting = settings.when(
|
||||
data: (v) {
|
||||
_tmdbApiController.text = v.tmdbApiKey!;
|
||||
_downloadDirController.text = v.downloadDIr!;
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(40, 10, 40, 0),
|
||||
child: Form(
|
||||
child: FormBuilder(
|
||||
key: _formKey, //设置globalKey,用于后面获取FormState
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
initialValue: {
|
||||
"tmdb_api": v.tmdbApiKey,
|
||||
"download_dir": v.downloadDIr
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
FormBuilderTextField(
|
||||
name: "tmdb_api",
|
||||
autofocus: true,
|
||||
controller: _tmdbApiController,
|
||||
decoration: Commons.requiredTextFieldStyle(
|
||||
text: "TMDB Api Key", icon: const Icon(Icons.key)),
|
||||
//
|
||||
validator: (v) {
|
||||
return v!.trim().isNotEmpty ? null : "ApiKey 不能为空";
|
||||
},
|
||||
onSaved: (newValue) {},
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
TextFormField(
|
||||
FormBuilderTextField(
|
||||
name: "download_dir",
|
||||
autofocus: true,
|
||||
controller: _downloadDirController,
|
||||
decoration: Commons.requiredTextFieldStyle(
|
||||
text: "下载路径", icon: const Icon(Icons.folder)),
|
||||
//
|
||||
validator: (v) {
|
||||
return v!.trim().isNotEmpty ? null : "下载路径不能为空";
|
||||
},
|
||||
onSaved: (newValue) {},
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 28.0),
|
||||
child: ElevatedButton(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Text("保存"),
|
||||
),
|
||||
onPressed: () {
|
||||
if ((_formKey.currentState as FormState)
|
||||
.validate()) {
|
||||
var f = ref
|
||||
.read(settingProvider.notifier)
|
||||
.updateSettings(GeneralSetting(
|
||||
tmdbApiKey: _tmdbApiController.text,
|
||||
downloadDIr:
|
||||
_downloadDirController.text));
|
||||
f.whenComplete(() {
|
||||
Utils.showSnakeBar("更新成功");
|
||||
}).onError((e, s) {
|
||||
Utils.showSnakeBar("更新失败:$e");
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Text("保存"),
|
||||
),
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.saveAndValidate()) {
|
||||
var values = _formKey.currentState!.value;
|
||||
var f = ref
|
||||
.read(settingProvider.notifier)
|
||||
.updateSettings(GeneralSetting(
|
||||
tmdbApiKey: values["tmdb_api"],
|
||||
downloadDIr: values["download_dir"]));
|
||||
f.then((v) {
|
||||
Utils.showSnakeBar("更新成功");
|
||||
}).onError((e, s) {
|
||||
Utils.showSnakeBar("更新失败:$e");
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
],
|
||||
@@ -103,7 +97,7 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
|
||||
var indexer = value[i];
|
||||
return SettingsCard(
|
||||
onTap: () => showIndexerDetails(indexer),
|
||||
child: Text(indexer.name!));
|
||||
child: Text(indexer.name ?? ""));
|
||||
}
|
||||
return SettingsCard(
|
||||
onTap: () => showIndexerDetails(Indexer()),
|
||||
@@ -121,7 +115,7 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
|
||||
var client = value[i];
|
||||
return SettingsCard(
|
||||
onTap: () => showDownloadClientDetails(client),
|
||||
child: Text(client.name!));
|
||||
child: Text(client.name ?? ""));
|
||||
}
|
||||
return SettingsCard(
|
||||
onTap: () => showDownloadClientDetails(DownloadClient()),
|
||||
@@ -138,7 +132,7 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
|
||||
var storage = value[i];
|
||||
return SettingsCard(
|
||||
onTap: () => showStorageDetails(storage),
|
||||
child: Text(storage.name!));
|
||||
child: Text(storage.name ?? ""));
|
||||
}
|
||||
return SettingsCard(
|
||||
onTap: () => showStorageDetails(Storage()),
|
||||
@@ -159,53 +153,69 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
|
||||
});
|
||||
}
|
||||
userController.text = data.user;
|
||||
return Column(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: const Text("开启认证"),
|
||||
value: _enableAuth!,
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
_enableAuth = v;
|
||||
});
|
||||
}),
|
||||
_enableAuth!
|
||||
? Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: userController,
|
||||
decoration: Commons.requiredTextFieldStyle(
|
||||
text: "用户名",
|
||||
icon: const Icon(Icons.account_box),
|
||||
)),
|
||||
TextFormField(
|
||||
obscureText: true,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
controller: passController,
|
||||
decoration: Commons.requiredTextFieldStyle(
|
||||
text: "密码",
|
||||
icon: const Icon(Icons.password),
|
||||
))
|
||||
],
|
||||
)
|
||||
: const Column(),
|
||||
Center(
|
||||
child: ElevatedButton(
|
||||
child: const Text("保存"),
|
||||
onPressed: () {
|
||||
var f = ref
|
||||
.read(authSettingProvider.notifier)
|
||||
.updateAuthSetting(_enableAuth!,
|
||||
userController.text, passController.text);
|
||||
f.whenComplete(() {
|
||||
Utils.showSnakeBar("更新成功");
|
||||
}).onError((e, s) {
|
||||
Utils.showSnakeBar("更新失败:$e");
|
||||
return FormBuilder(
|
||||
key: _formKey2,
|
||||
initialValue: {
|
||||
"user": data.user,
|
||||
"password": data.password,
|
||||
"enable": data.enable
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
FormBuilderSwitch(
|
||||
name: "enable",
|
||||
title: const Text("开启认证"),
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
_enableAuth = v;
|
||||
});
|
||||
}))
|
||||
],
|
||||
);
|
||||
}),
|
||||
_enableAuth!
|
||||
? Column(
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
name: "user",
|
||||
autovalidateMode:
|
||||
AutovalidateMode.onUserInteraction,
|
||||
validator: FormBuilderValidators.required(),
|
||||
decoration: Commons.requiredTextFieldStyle(
|
||||
text: "用户名",
|
||||
icon: const Icon(Icons.account_box),
|
||||
)),
|
||||
FormBuilderTextField(
|
||||
name: "password",
|
||||
obscureText: true,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
autovalidateMode:
|
||||
AutovalidateMode.onUserInteraction,
|
||||
validator: FormBuilderValidators.required(),
|
||||
decoration: Commons.requiredTextFieldStyle(
|
||||
text: "密码",
|
||||
icon: const Icon(Icons.password),
|
||||
))
|
||||
],
|
||||
)
|
||||
: const Column(),
|
||||
Center(
|
||||
child: ElevatedButton(
|
||||
child: const Text("保存"),
|
||||
onPressed: () {
|
||||
if (_formKey2.currentState!.saveAndValidate()) {
|
||||
var values = _formKey2.currentState!.value;
|
||||
var f = ref
|
||||
.read(authSettingProvider.notifier)
|
||||
.updateAuthSetting(_enableAuth!,
|
||||
values["user"], values["password"]);
|
||||
f.then((v) {
|
||||
Utils.showSnakeBar("更新成功");
|
||||
}).onError((e, s) {
|
||||
Utils.showSnakeBar("更新失败:$e");
|
||||
});
|
||||
}
|
||||
}))
|
||||
],
|
||||
));
|
||||
},
|
||||
error: (err, trace) => Text("$err"),
|
||||
loading: () => const MyProgressIndicator());
|
||||
@@ -256,109 +266,149 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
|
||||
}
|
||||
|
||||
Future<void> showIndexerDetails(Indexer indexer) {
|
||||
var nameController = TextEditingController(text: indexer.name);
|
||||
var urlController = TextEditingController(text: indexer.url);
|
||||
var apiKeyController = TextEditingController(text: indexer.apiKey);
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
var selectImpl = "torznab";
|
||||
final children = <Widget>[
|
||||
DropdownMenu(
|
||||
label: const Text("类型"),
|
||||
onSelected: (value) {
|
||||
setState(() {
|
||||
selectImpl = value!;
|
||||
});
|
||||
},
|
||||
initialSelection: selectImpl,
|
||||
dropdownMenuEntries: const [
|
||||
DropdownMenuEntry(value: "torznab", label: "Torznab"),
|
||||
var body = FormBuilder(
|
||||
key: _formKey,
|
||||
initialValue: {
|
||||
"name": indexer.name,
|
||||
"url": indexer.url,
|
||||
"api_key": indexer.apiKey,
|
||||
"impl": "torznab"
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
FormBuilderDropdown(
|
||||
name: "impl",
|
||||
decoration: const InputDecoration(labelText: "类型"),
|
||||
items: const [
|
||||
DropdownMenuItem(value: "torznab", child: Text("Torznab")),
|
||||
],
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: "name",
|
||||
decoration: Commons.requiredTextFieldStyle(text: "名称"),
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: "url",
|
||||
decoration: Commons.requiredTextFieldStyle(text: "地址"),
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: "api_key",
|
||||
decoration: Commons.requiredTextFieldStyle(text: "API Key"),
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
],
|
||||
),
|
||||
TextField(
|
||||
decoration: Commons.requiredTextFieldStyle(text: "名称"),
|
||||
controller: nameController,
|
||||
),
|
||||
TextField(
|
||||
decoration: Commons.requiredTextFieldStyle(text: "地址"),
|
||||
controller: urlController,
|
||||
),
|
||||
TextField(
|
||||
decoration: Commons.requiredTextFieldStyle(text: "API Key"),
|
||||
controller: apiKeyController,
|
||||
),
|
||||
];
|
||||
);
|
||||
onDelete() async {
|
||||
return ref.read(indexersProvider.notifier).deleteIndexer(indexer.id!);
|
||||
}
|
||||
|
||||
onSubmit() async {
|
||||
return ref.read(indexersProvider.notifier).addIndexer(Indexer(
|
||||
name: nameController.text,
|
||||
url: urlController.text,
|
||||
apiKey: apiKeyController.text));
|
||||
if (_formKey.currentState!.saveAndValidate()) {
|
||||
var values = _formKey.currentState!.value;
|
||||
return ref.read(indexersProvider.notifier).addIndexer(Indexer(
|
||||
name: values["name"],
|
||||
url: values["url"],
|
||||
apiKey: values["api_key"]));
|
||||
} else {
|
||||
throw "数据校验失败";
|
||||
}
|
||||
}
|
||||
|
||||
return showSettingDialog(
|
||||
"索引器", indexer.id != null, children, onSubmit, onDelete);
|
||||
"索引器", indexer.id != null, body, onSubmit, onDelete);
|
||||
}
|
||||
|
||||
Future<void> showDownloadClientDetails(DownloadClient client) {
|
||||
var nameController = TextEditingController(text: client.name);
|
||||
var urlController = TextEditingController(text: client.url);
|
||||
var userController = TextEditingController(text: client.user);
|
||||
var passController = TextEditingController(text: client.password);
|
||||
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
var _enableAuth = isNotBlank(client.user);
|
||||
String selectImpl = "transmission";
|
||||
var body = <Widget>[
|
||||
DropdownMenu(
|
||||
label: const Text("类型"),
|
||||
onSelected: (value) {
|
||||
setState(() {
|
||||
selectImpl = value!;
|
||||
});
|
||||
},
|
||||
initialSelection: selectImpl,
|
||||
dropdownMenuEntries: const [
|
||||
DropdownMenuEntry(value: "transmission", label: "Transmission"),
|
||||
],
|
||||
),
|
||||
TextField(
|
||||
decoration: Commons.requiredTextFieldStyle(text: "名称"),
|
||||
controller: nameController,
|
||||
),
|
||||
TextField(
|
||||
decoration: Commons.requiredTextFieldStyle(text: "地址"),
|
||||
controller: urlController,
|
||||
),
|
||||
StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
return Column(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: const Text("需要认证"),
|
||||
value: _enableAuth,
|
||||
onChanged: (v) {
|
||||
|
||||
final body =
|
||||
StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
return FormBuilder(
|
||||
key: _formKey,
|
||||
initialValue: {
|
||||
"name": client.name,
|
||||
"url": client.url,
|
||||
"user": client.user,
|
||||
"password": client.password,
|
||||
"impl": "transmission"
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
FormBuilderDropdown<String>(
|
||||
name: "impl",
|
||||
decoration: const InputDecoration(labelText: "类型"),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_enableAuth = v;
|
||||
selectImpl = value!;
|
||||
});
|
||||
}),
|
||||
_enableAuth
|
||||
? Column(
|
||||
children: [
|
||||
TextField(
|
||||
decoration: Commons.requiredTextFieldStyle(text: "用户"),
|
||||
controller: userController,
|
||||
),
|
||||
TextField(
|
||||
decoration: Commons.requiredTextFieldStyle(text: "密码"),
|
||||
controller: passController,
|
||||
),
|
||||
],
|
||||
)
|
||||
: Container()
|
||||
],
|
||||
);
|
||||
})
|
||||
];
|
||||
},
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: "transmission", child: Text("Transmission")),
|
||||
],
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: "name",
|
||||
decoration: const InputDecoration(labelText: "名称"),
|
||||
validator: FormBuilderValidators.required(),
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction),
|
||||
FormBuilderTextField(
|
||||
name: "url",
|
||||
decoration: const InputDecoration(
|
||||
labelText: "地址", hintText: "http://127.0.0.1:9091"),
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return Column(
|
||||
children: [
|
||||
FormBuilderSwitch(
|
||||
name: "auth",
|
||||
title: const Text("需要认证"),
|
||||
initialValue: _enableAuth,
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
_enableAuth = v!;
|
||||
});
|
||||
}),
|
||||
_enableAuth
|
||||
? Column(
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
name: "user",
|
||||
decoration: Commons.requiredTextFieldStyle(
|
||||
text: "用户"),
|
||||
validator: FormBuilderValidators.required(),
|
||||
autovalidateMode:
|
||||
AutovalidateMode.onUserInteraction),
|
||||
FormBuilderTextField(
|
||||
name: "password",
|
||||
decoration: Commons.requiredTextFieldStyle(
|
||||
text: "密码"),
|
||||
validator: FormBuilderValidators.required(),
|
||||
obscureText: true,
|
||||
autovalidateMode:
|
||||
AutovalidateMode.onUserInteraction),
|
||||
],
|
||||
)
|
||||
: Container()
|
||||
],
|
||||
);
|
||||
})
|
||||
],
|
||||
));
|
||||
});
|
||||
onDelete() async {
|
||||
return ref
|
||||
.read(dwonloadClientsProvider.notifier)
|
||||
@@ -366,13 +416,18 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
|
||||
}
|
||||
|
||||
onSubmit() async {
|
||||
return ref.read(dwonloadClientsProvider.notifier).addDownloadClients(
|
||||
DownloadClient(
|
||||
name: nameController.text,
|
||||
implementation: "transmission",
|
||||
url: urlController.text,
|
||||
user: _enableAuth ? userController.text : null,
|
||||
password: _enableAuth ? passController.text : null));
|
||||
if (_formKey.currentState!.saveAndValidate()) {
|
||||
var values = _formKey.currentState!.value;
|
||||
return ref.read(dwonloadClientsProvider.notifier).addDownloadClients(
|
||||
DownloadClient(
|
||||
name: values["name"],
|
||||
implementation: values["impl"],
|
||||
url: values["url"],
|
||||
user: _enableAuth ? values["user"] : null,
|
||||
password: _enableAuth ? values["password"] : null));
|
||||
} else {
|
||||
throw "数据校验不通过";
|
||||
}
|
||||
}
|
||||
|
||||
return showSettingDialog(
|
||||
@@ -380,112 +435,137 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
|
||||
}
|
||||
|
||||
Future<void> showStorageDetails(Storage s) {
|
||||
var nameController = TextEditingController(text: s.name);
|
||||
var tvPathController = TextEditingController();
|
||||
var moviePathController = TextEditingController();
|
||||
var urlController = TextEditingController();
|
||||
var userController = TextEditingController();
|
||||
var passController = TextEditingController();
|
||||
bool enablingChangeFileHash = false;
|
||||
if (s.settings != null) {
|
||||
tvPathController.text = s.settings!["tv_path"] ?? "";
|
||||
moviePathController.text = s.settings!["movie_path"] ?? "";
|
||||
urlController.text = s.settings!["url"] ?? "";
|
||||
userController.text = s.settings!["user"] ?? "";
|
||||
passController.text = s.settings!["password"] ?? "";
|
||||
enablingChangeFileHash =
|
||||
s.settings!["change_file_hash"] == "true" ? true : false;
|
||||
}
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
|
||||
String selectImpl = s.implementation == null ? "local" : s.implementation!;
|
||||
final widgets =
|
||||
StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
DropdownMenu(
|
||||
label: const Text("类型"),
|
||||
onSelected: (value) {
|
||||
setState(() {
|
||||
selectImpl = value!;
|
||||
});
|
||||
},
|
||||
initialSelection: selectImpl,
|
||||
dropdownMenuEntries: const [
|
||||
DropdownMenuEntry(value: "local", label: "本地存储"),
|
||||
DropdownMenuEntry(value: "webdav", label: "webdav")
|
||||
return FormBuilder(
|
||||
key: _formKey,
|
||||
autovalidateMode: AutovalidateMode.disabled,
|
||||
initialValue: {
|
||||
"name": s.name,
|
||||
"impl": s.implementation == null ? "local" : s.implementation!,
|
||||
"user": s.settings != null ? s.settings!["user"] ?? "" : "",
|
||||
"password": s.settings != null ? s.settings!["password"] ?? "" : "",
|
||||
"tv_path": s.settings != null ? s.settings!["tv_path"] ?? "" : "",
|
||||
"url": s.settings != null ? s.settings!["url"] ?? "" : "",
|
||||
"movie_path":
|
||||
s.settings != null ? s.settings!["movie_path"] ?? "" : "",
|
||||
"change_file_hash": s.settings != null
|
||||
? s.settings!["change_file_hash"] == "true"
|
||||
? true
|
||||
: false
|
||||
: false,
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
FormBuilderDropdown<String>(
|
||||
name: "impl",
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
decoration: const InputDecoration(labelText: "类型"),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectImpl = value!;
|
||||
});
|
||||
},
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: "local",
|
||||
child: Text("本地存储"),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: "webdav",
|
||||
child: Text("webdav"),
|
||||
)
|
||||
],
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: "name",
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
initialValue: s.name,
|
||||
decoration: const InputDecoration(labelText: "名称"),
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
selectImpl != "local"
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
name: "url",
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
decoration:
|
||||
const InputDecoration(labelText: "Webdav地址"),
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: "user",
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
decoration: const InputDecoration(labelText: "用户"),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: "password",
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
decoration: const InputDecoration(labelText: "密码"),
|
||||
obscureText: true,
|
||||
),
|
||||
FormBuilderCheckbox(
|
||||
name: "change_file_hash",
|
||||
title: const Text(
|
||||
"上传时更改文件哈希",
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Container(),
|
||||
FormBuilderTextField(
|
||||
name: "tv_path",
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
decoration: const InputDecoration(labelText: "电视剧路径"),
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: "movie_path",
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
decoration: const InputDecoration(labelText: "电影路径"),
|
||||
validator: FormBuilderValidators.required(),
|
||||
)
|
||||
],
|
||||
),
|
||||
TextField(
|
||||
decoration: const InputDecoration(labelText: "名称"),
|
||||
controller: nameController,
|
||||
),
|
||||
selectImpl != "local"
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
decoration: const InputDecoration(labelText: "Webdav地址"),
|
||||
controller: urlController,
|
||||
),
|
||||
TextField(
|
||||
decoration: const InputDecoration(labelText: "用户"),
|
||||
controller: userController,
|
||||
),
|
||||
TextField(
|
||||
decoration: const InputDecoration(labelText: "密码"),
|
||||
controller: passController,
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: const Text("上传时更改文件哈希", style: TextStyle(fontSize: 14),),
|
||||
value: enablingChangeFileHash,
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
enablingChangeFileHash = v??false;
|
||||
});
|
||||
}),
|
||||
],
|
||||
)
|
||||
: Container(),
|
||||
TextField(
|
||||
decoration: const InputDecoration(labelText: "电视剧路径"),
|
||||
controller: tvPathController,
|
||||
),
|
||||
TextField(
|
||||
decoration: const InputDecoration(labelText: "电影路径"),
|
||||
controller: moviePathController,
|
||||
)
|
||||
],
|
||||
);
|
||||
));
|
||||
});
|
||||
onSubmit() async {
|
||||
return ref.read(storageSettingProvider.notifier).addStorage(Storage(
|
||||
name: nameController.text,
|
||||
implementation: selectImpl,
|
||||
settings: {
|
||||
"tv_path": tvPathController.text,
|
||||
"movie_path": moviePathController.text,
|
||||
"url": urlController.text,
|
||||
"user": userController.text,
|
||||
"password": passController.text,
|
||||
"change_file_hash": enablingChangeFileHash ? "true" : "false"
|
||||
},
|
||||
));
|
||||
if (_formKey.currentState!.saveAndValidate()) {
|
||||
final values = _formKey.currentState!.value;
|
||||
return ref.read(storageSettingProvider.notifier).addStorage(Storage(
|
||||
name: values["name"],
|
||||
implementation: selectImpl,
|
||||
settings: {
|
||||
"tv_path": values["tv_path"],
|
||||
"movie_path": values["movie_path"],
|
||||
"url": values["url"],
|
||||
"user": values["user"],
|
||||
"password": values["password"],
|
||||
"change_file_hash":
|
||||
values["change_file_hash"] as bool ? "true" : "false"
|
||||
},
|
||||
));
|
||||
} else {
|
||||
throw "数据校验位未通过";
|
||||
}
|
||||
}
|
||||
|
||||
onDelete() async {
|
||||
return ref.read(storageSettingProvider.notifier).deleteStorage(s.id!);
|
||||
}
|
||||
|
||||
return showSettingDialog('存储', s.id != null, [widgets], onSubmit, onDelete);
|
||||
return showSettingDialog('存储', s.id != null, widgets, onSubmit, onDelete);
|
||||
}
|
||||
|
||||
Future<void> showSettingDialog(
|
||||
String title,
|
||||
bool showDelete,
|
||||
List<Widget> children,
|
||||
Future Function() onSubmit,
|
||||
Future Function() onDelete) {
|
||||
Future<void> showSettingDialog(String title, bool showDelete, Widget body,
|
||||
Future Function() onSubmit, Future Function() onDelete) {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
@@ -495,9 +575,7 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
|
||||
content: SingleChildScrollView(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 200),
|
||||
child: ListBody(
|
||||
children: children,
|
||||
),
|
||||
child: body,
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
@@ -505,7 +583,7 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
|
||||
? TextButton(
|
||||
onPressed: () {
|
||||
final f = onDelete();
|
||||
f.whenComplete(() {
|
||||
f.then((v) {
|
||||
Utils.showSnakeBar("删除成功");
|
||||
Navigator.of(context).pop();
|
||||
}).onError((e, s) {
|
||||
@@ -524,7 +602,7 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
|
||||
child: const Text('确定'),
|
||||
onPressed: () {
|
||||
final f = onSubmit();
|
||||
f.whenComplete(() {
|
||||
f.then((v) {
|
||||
Utils.showSnakeBar("操作成功");
|
||||
Navigator.of(context).pop();
|
||||
}).onError((e, s) {
|
||||
|
||||
@@ -211,7 +211,7 @@ class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
|
||||
widget.seriesId)
|
||||
.notifier)
|
||||
.delete()
|
||||
.whenComplete(() =>
|
||||
.then((v) =>
|
||||
context.go(WelcomePage.routeTv))
|
||||
.onError((error, trace) =>
|
||||
Utils.showSnakeBar(
|
||||
|
||||
@@ -118,6 +118,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.1.11+1"
|
||||
flutter_form_builder:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_form_builder
|
||||
sha256: "447f8808f68070f7df968e8063aada3c9d2e90e789b5b70f3b44e4b315212656"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.3.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -126,6 +134,11 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
flutter_localizations:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_login:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -160,6 +173,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "10.7.0"
|
||||
form_builder_validators:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: form_builder_validators
|
||||
sha256: c61ed7b1deecf0e1ebe49e2fa79e3283937c5a21c7e48e3ed9856a4a14e1191a
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "11.0.0"
|
||||
go_router:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -44,6 +44,8 @@ dependencies:
|
||||
percent_indicator: ^4.2.3
|
||||
intl: ^0.19.0
|
||||
flutter_adaptive_scaffold: ^0.1.11+1
|
||||
flutter_form_builder: ^9.3.0
|
||||
form_builder_validators: ^11.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user