mirror of
https://github.com/simon-ding/polaris.git
synced 2026-02-15 03:20:48 +08:00
Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0954632b65 | ||
|
|
03105a1989 | ||
|
|
e169172c68 | ||
|
|
937b035634 | ||
|
|
c639e11b90 | ||
|
|
f2ac688ed8 | ||
|
|
369263a55c | ||
|
|
9d4848129f | ||
|
|
f7e82fa464 | ||
|
|
d2354ab33c | ||
|
|
67014cfb16 | ||
|
|
60edeacd0d | ||
|
|
4c77cf5798 | ||
|
|
3cf48d1f8e | ||
|
|
6d127c6d00 | ||
|
|
22f76e3f57 | ||
|
|
e947396c04 | ||
|
|
1020190c01 | ||
|
|
7c05acd1cf | ||
|
|
76a9183b52 | ||
|
|
6698d368c3 | ||
|
|
acb627d011 | ||
|
|
7c64d964e8 | ||
|
|
bec3b04705 | ||
|
|
990da92b75 | ||
|
|
ee14cc63b8 | ||
|
|
8df7b8665b | ||
|
|
ea90e014b1 | ||
|
|
6372c5c6e6 | ||
|
|
7b6dba1afe | ||
|
|
c833f6fab6 | ||
|
|
b4c2002ad1 | ||
|
|
b2a9f1f83b | ||
|
|
b69881d26b | ||
|
|
be07e457d0 | ||
|
|
2cdd6e3740 | ||
|
|
fa2968f01a | ||
|
|
36f24a7e04 | ||
|
|
ecc7465028 | ||
|
|
3af4ac795e | ||
|
|
af2a30405c | ||
|
|
ba3f6de852 | ||
|
|
7d5ce8ba97 | ||
|
|
b136b9167f | ||
|
|
f0f3281428 | ||
|
|
196ba6635f | ||
|
|
b61b7f082e | ||
|
|
105b296ba2 | ||
|
|
c4d153f15b | ||
|
|
d2619120da | ||
|
|
fbfee65a50 | ||
|
|
c433ccaa0e | ||
|
|
58428405b0 | ||
|
|
45cd94f65b | ||
|
|
53cbca3101 | ||
|
|
576956e271 | ||
|
|
31d20b4f36 | ||
|
|
d026dc4eec | ||
|
|
e472d67c79 | ||
|
|
2165a8c533 | ||
|
|
0c3b5a6907 | ||
|
|
aaa006a322 | ||
|
|
a83f860624 | ||
|
|
b0c325bc4b | ||
|
|
a0431df1ee | ||
|
|
7b02eeac51 | ||
|
|
66a307f202 | ||
|
|
ae611943c3 | ||
|
|
4fd11540cd | ||
|
|
587a28127b | ||
|
|
05ae58030c | ||
|
|
f1c4e306f4 | ||
|
|
949b6e5188 | ||
|
|
0d4b453d0a | ||
|
|
bce4d93ab1 | ||
|
|
36b72e6461 | ||
|
|
62417727f9 | ||
|
|
03f72b9d86 | ||
|
|
c17cf750e5 | ||
|
|
b176253fc4 | ||
|
|
1e2d8b8520 | ||
|
|
3739f2c960 | ||
|
|
bb6da47efb | ||
|
|
c28373bde1 | ||
|
|
8ce7045466 | ||
|
|
0b1bd8226d | ||
|
|
e67413cec2 | ||
|
|
2da02fa706 | ||
|
|
bc50dd888a | ||
|
|
0305c0709d | ||
|
|
5e9e85206e | ||
|
|
3babb9f5c7 | ||
|
|
c9928f10ce | ||
|
|
a2da1e7479 | ||
|
|
038f643ce3 | ||
|
|
cf2fa90d5d | ||
|
|
677923ea8a | ||
|
|
1192d00fe0 | ||
|
|
b317636a8a | ||
|
|
0e6465593b | ||
|
|
2cb8a5b6fb |
13
.github/workflows/go.yml
vendored
13
.github/workflows/go.yml
vendored
@@ -29,6 +29,19 @@ jobs:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GHCR_TOKEN }}
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version: 3
|
||||
|
||||
- name: Build Web
|
||||
run: |
|
||||
cd ui
|
||||
flutter pub get
|
||||
flutter build web --no-web-resources-cdn
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
|
||||
17
.github/workflows/release.yml
vendored
17
.github/workflows/release.yml
vendored
@@ -37,12 +37,25 @@ jobs:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GHCR_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version: 3
|
||||
|
||||
- name: Build Web
|
||||
run: |
|
||||
cd ui
|
||||
flutter pub get
|
||||
flutter build web --no-web-resources-cdn
|
||||
|
||||
- name: Build and push
|
||||
id: push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -50,9 +63,7 @@ jobs:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
push: true
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/s390x,linux/ppc64le
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
FROM instrumentisto/flutter:3 AS flutter
|
||||
WORKDIR /app
|
||||
COPY ./ui/pubspec.yaml ./ui/pubspec.lock ./
|
||||
RUN flutter pub get
|
||||
COPY ./ui/ ./
|
||||
RUN flutter build web --no-web-resources-cdn --web-renderer html
|
||||
|
||||
# 打包依赖阶段使用golang作为基础镜像
|
||||
FROM golang:1.23 as builder
|
||||
|
||||
# 启用go module
|
||||
@@ -20,7 +12,6 @@ RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
COPY --from=flutter /app/build/web ./ui/build/web/
|
||||
# 指定OS等,并go build
|
||||
RUN CGO_ENABLED=0 go build -o polaris -ldflags="-X polaris/db.Version=$(git describe --tags --long)" ./cmd/
|
||||
|
||||
|
||||
46
README.md
46
README.md
@@ -1,4 +1,10 @@
|
||||
# Polaris
|
||||
|
||||

|
||||
|
||||
|
||||
<h1 align="center">Polaris</h1>
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||
@@ -6,11 +12,10 @@
|
||||

|
||||
|
||||
|
||||
Polaris 是一个电视剧和电影的追踪下载软件。对动漫日剧美剧都有良好的匹配,支持webdav或者本地存储。
|
||||
**Polaris 是一个电视剧和电影的追踪下载软件。对美剧动漫日剧都有良好的匹配,支持多种存储方式(webdav、alist、本地存储)。**
|
||||
|
||||
</div>
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
交流群: https://t.me/+8R2nzrlSs2JhMDgx
|
||||
|
||||
@@ -22,24 +27,41 @@ Polaris 是一个电视剧和电影的追踪下载软件。对动漫日剧美剧
|
||||
|
||||
- [x] 电视剧自动追踪下载
|
||||
- [x] 电影自动追踪下载
|
||||
- [x] webdav 存储支持,配合 [alist](https://github.com/alist-org/alist) 或阿里云等实现更多功能
|
||||
- [x] 本地、webdav、 [alist](https://github.com/alist-org/alist) 存储支持,使用 alist 存储支持秒传功能
|
||||
- [x] 事件通知推送,目前支持 Pushover和 Bark,还在扩充中
|
||||
- [x] 后台代理支持
|
||||
- [x] TMDB 代理支持
|
||||
- [x] 用户认证
|
||||
- [x] plex 刮削支持
|
||||
- [x] NFO 刮削文件支持
|
||||
- [x] BT/PT 支持
|
||||
- [x] qbittorrent/transmission客户端支持
|
||||
- [x] 支持导入plex watchlist,plex里标记,自动导入polaris
|
||||
- [x] and more...
|
||||
|
||||
## 支持的平台
|
||||
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
- linux/386
|
||||
- linux/s390x
|
||||
- linux/ppc64le
|
||||
|
||||
## Todos
|
||||
|
||||
|
||||
- [ ] 更多通知客户端支持
|
||||
- [ ] 第三方watchlist导入支持
|
||||
- [ ] 更多第三方watchlist导入支持
|
||||
- [ ] 手机客户端
|
||||
|
||||
|
||||
## 截图
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
## 运行原理
|
||||
|
||||
Polaris本身不提供任何资源,要使其能正常工作,需要同时安装一个下载客户端(transmission)和一个索引客户端(jackett)。
|
||||
@@ -48,13 +70,17 @@ Polaris通过索引客户端查询相关的BT/PT站点,然后把查到的资
|
||||
|
||||

|
||||
|
||||
## 对比 sonarr/radarr
|
||||
<!-- ## 对比 sonarr/radarr
|
||||
* 更好的中文支持
|
||||
* 对于动漫、日剧的良好支持,配合国内站点基本能匹配上对应资源
|
||||
* 支持 webdav 后端存储,可以配合 alist 或者阿里云来实现下载后实时传到云上的功能。这样外出就可以不依靠家里的宽带来看电影了,或者实现个轻 NAS 功能,下载功能放在本地,数据放在云盘
|
||||
* golang 实现后端,相比于 .NET 更节省资源
|
||||
* 一个程序同时实现了电影、电视剧功能,不需要装两个程序
|
||||
* 当然 sonarr/radarr 也是非常优秀的开源项目,目前 Polaris 功能还没有 sonarr/radarr 丰富
|
||||
* 当然 sonarr/radarr 也是非常优秀的开源项目,目前 Polaris 功能还没有 sonarr/radarr 丰富 -->
|
||||
|
||||
|
||||
## Stargazers over time
|
||||
[](https://starchart.cc/simon-ding/polaris)
|
||||
|
||||
-------------
|
||||
|
||||
|
||||
14
cmd/main.go
14
cmd/main.go
@@ -3,9 +3,7 @@ package main
|
||||
import (
|
||||
"polaris/db"
|
||||
"polaris/log"
|
||||
"polaris/pkg/utils"
|
||||
"polaris/server"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -18,13 +16,13 @@ func main() {
|
||||
log.Panicf("init db error: %v", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
if err := utils.OpenURL("http://127.0.0.1:8080"); err != nil {
|
||||
log.Errorf("open url error: %v", err)
|
||||
}
|
||||
// go func() {
|
||||
// time.Sleep(2 * time.Second)
|
||||
// if err := utils.OpenURL("http://127.0.0.1:8080"); err != nil {
|
||||
// log.Errorf("open url error: %v", err)
|
||||
// }
|
||||
|
||||
}()
|
||||
// }()
|
||||
s := server.NewServer(dbClient)
|
||||
if err := s.Serve(); err != nil {
|
||||
log.Errorf("server start error: %v", err)
|
||||
|
||||
61
db/const.go
61
db/const.go
@@ -1,5 +1,7 @@
|
||||
package db
|
||||
|
||||
import "polaris/ent/media"
|
||||
|
||||
var Version = "undefined"
|
||||
|
||||
const (
|
||||
@@ -14,9 +16,16 @@ const (
|
||||
SettingNfoSupportEnabled = "nfo_support_enabled"
|
||||
SettingAllowQiangban = "filter_qiangban"
|
||||
SettingEnableTmdbAdultContent = "tmdb_adult_content"
|
||||
SetttingSizeLimiter = "size_limiter"
|
||||
SettingTvNamingFormat = "tv_naming_format"
|
||||
SettingMovieNamingFormat = "movie_naming_format"
|
||||
SettingProwlarrInfo = "prowlarr_info"
|
||||
|
||||
SettingTvSizeLimiter = "tv_size_limiter"
|
||||
SettingMovieSizeLimiter = "movie_size_limiter"
|
||||
SettingAcceptedVideoFormats = "accepted_video_formats"
|
||||
SettingAcceptedSubtitleFormats = "accepted_subtitle_formats"
|
||||
|
||||
SettingAIConfig = "ai_config"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -39,6 +48,18 @@ const (
|
||||
|
||||
const DefaultNamingFormat = "{{.NameCN}} {{.NameEN}} {{if .Year}} ({{.Year}}) {{end}}"
|
||||
|
||||
// https://en.wikipedia.org/wiki/Video_file_format
|
||||
var defaultAcceptedVideoFormats = []string{
|
||||
".webm", ".mkv", ".flv", ".vob", ".ogv", ".ogg", ".drc", ".mng", ".avi", ".mts", ".m2ts", ".ts",
|
||||
".mov", ".qt", ".wmv", ".yuv", ".rm", ".rmvb", ".viv", ".amv", ".mp4", ".m4p", ".m4v",
|
||||
".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".m2v", ".m4v",
|
||||
".svi", ".3gp", ".3g2", ".nsv",
|
||||
}
|
||||
|
||||
var defaultAcceptedSubtitleFormats = []string{
|
||||
".ass", ".srt", ".vtt", ".webvtt", ".sub", ".idx",
|
||||
}
|
||||
|
||||
type NamingInfo struct {
|
||||
NameCN string
|
||||
NameEN string
|
||||
@@ -50,13 +71,37 @@ type ResolutionType string
|
||||
|
||||
const JwtSerectKey = "jwt_secrect_key"
|
||||
|
||||
type SizeLimiter struct {
|
||||
R720p Limiter `json:"720p"`
|
||||
R1080p Limiter `json:"1080p"`
|
||||
R2160p Limiter `json:"2160p"`
|
||||
type MediaSizeLimiter struct {
|
||||
P720p SizeLimiter `json:"720p"`
|
||||
P1080 SizeLimiter `json:"1080p"`
|
||||
P2160 SizeLimiter `json:"2160p"`
|
||||
}
|
||||
|
||||
type Limiter struct {
|
||||
Max int `json:"max"`
|
||||
Min int `json:"min"`
|
||||
func (m *MediaSizeLimiter) GetLimiter(r media.Resolution) SizeLimiter {
|
||||
if r == media.Resolution1080p {
|
||||
return m.P1080
|
||||
} else if r == media.Resolution720p {
|
||||
return m.P720p
|
||||
} else if r == media.Resolution2160p {
|
||||
return m.P2160
|
||||
}
|
||||
return SizeLimiter{}
|
||||
}
|
||||
|
||||
type SizeLimiter struct {
|
||||
MaxSIze int64 `json:"max_size"`
|
||||
MinSize int64 `json:"min_size"`
|
||||
PreferSIze int64 `json:"prefer_size"`
|
||||
}
|
||||
|
||||
type ProwlarrSetting struct {
|
||||
Disabled bool `json:"disabled"`
|
||||
ApiKey string `json:"api_key"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type AIConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
GeminiApiKey string `json:"gemini_api_key"`
|
||||
GeminiModelName string `json:"gemini_model_name"`
|
||||
}
|
||||
|
||||
136
db/db.go
136
db/db.go
@@ -157,6 +157,7 @@ func (c *Client) AddMediaWatchlist(m *ent.Media, episodes []int) (*ent.Media, er
|
||||
SetDownloadHistoryEpisodes(m.DownloadHistoryEpisodes).
|
||||
SetLimiter(m.Limiter).
|
||||
SetExtras(m.Extras).
|
||||
SetAlternativeTitles(m.AlternativeTitles).
|
||||
AddEpisodeIDs(episodes...).
|
||||
Save(context.TODO())
|
||||
return r, err
|
||||
@@ -251,6 +252,7 @@ func (c *Client) SaveEposideDetail2(d *ent.Episode) (int, error) {
|
||||
SetMediaID(d.MediaID).
|
||||
SetStatus(d.Status).
|
||||
SetOverview(d.Overview).
|
||||
SetMonitored(d.Monitored).
|
||||
SetTitle(d.Title).Save(context.TODO())
|
||||
|
||||
return ep.ID, err
|
||||
@@ -374,6 +376,15 @@ func (s *StorageInfo) ToWebDavSetting() WebdavSetting {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StorageInfo) ToAlistSetting() WebdavSetting {
|
||||
return WebdavSetting{
|
||||
URL: s.Settings["url"],
|
||||
User: s.Settings["user"],
|
||||
Password: s.Settings["password"],
|
||||
ChangeFileHash: s.Settings["change_file_hash"],
|
||||
}
|
||||
}
|
||||
|
||||
type WebdavSetting struct {
|
||||
URL string `json:"url"`
|
||||
User string `json:"user"`
|
||||
@@ -432,7 +443,7 @@ type Storage struct {
|
||||
}
|
||||
|
||||
func (s *Storage) ToWebDavSetting() WebdavSetting {
|
||||
if s.Implementation != storage.ImplementationWebdav {
|
||||
if s.Implementation != storage.ImplementationWebdav && s.Implementation != storage.ImplementationAlist {
|
||||
panic("not webdav storage")
|
||||
}
|
||||
var webdavSetting WebdavSetting
|
||||
@@ -483,7 +494,8 @@ func (c *Client) SaveHistoryRecord(h ent.History) (*ent.History, error) {
|
||||
}
|
||||
return c.ent.History.Create().SetMediaID(h.MediaID).SetEpisodeID(h.EpisodeID).SetDate(time.Now()).
|
||||
SetStatus(h.Status).SetTargetDir(h.TargetDir).SetSourceTitle(h.SourceTitle).SetIndexerID(h.IndexerID).
|
||||
SetDownloadClientID(h.DownloadClientID).SetSize(h.Size).SetSaved(h.Saved).SetLink(h.Link).Save(context.TODO())
|
||||
SetDownloadClientID(h.DownloadClientID).SetSize(h.Size).SetSaved(h.Saved).SetSeasonNum(h.SeasonNum).
|
||||
SetEpisodeNums(h.EpisodeNums).SetLink(h.Link).Save(context.TODO())
|
||||
}
|
||||
|
||||
func (c *Client) SetHistoryStatus(id int, status history.Status) error {
|
||||
@@ -621,19 +633,38 @@ 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
|
||||
func (c *Client) GetSizeLimiter(mediaType string) (*MediaSizeLimiter, error) {
|
||||
var v string
|
||||
if mediaType == "tv" {
|
||||
v = c.GetSetting(SettingTvSizeLimiter)
|
||||
} else if mediaType == "movie" {
|
||||
v = c.GetSetting(SettingMovieSizeLimiter)
|
||||
} else {
|
||||
return nil, errors.Errorf("media type not supported: %v", mediaType)
|
||||
}
|
||||
|
||||
var limiter MediaSizeLimiter
|
||||
if v == "" {
|
||||
return &limiter, nil
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(v), &limiter)
|
||||
return &limiter, err
|
||||
}
|
||||
|
||||
func (c *Client) SetSizeLimiter(limiter *SizeLimiter) error {
|
||||
func (c *Client) SetSizeLimiter(mediaType string, limiter *MediaSizeLimiter) error {
|
||||
data, err := json.Marshal(limiter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.SetSetting(SetttingSizeLimiter, string(data))
|
||||
if mediaType == "tv" {
|
||||
return c.SetSetting(SettingTvSizeLimiter, string(data))
|
||||
} else if mediaType == "movie" {
|
||||
return c.SetSetting(SettingMovieSizeLimiter, string(data))
|
||||
} else {
|
||||
return errors.Errorf("media type not supported: %v", mediaType)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (c *Client) GetTvNamingFormat() string {
|
||||
@@ -660,3 +691,94 @@ func (c *Client) CleanAllDanglingEpisodes() error {
|
||||
func (c *Client) AddBlacklistItem(item *ent.Blacklist) error {
|
||||
return c.ent.Blacklist.Create().SetType(item.Type).SetValue(item.Value).SetNotes(item.Notes).Exec(context.Background())
|
||||
}
|
||||
|
||||
func (c *Client) GetProwlarrSetting() (*ProwlarrSetting, error) {
|
||||
s := c.GetSetting(SettingProwlarrInfo)
|
||||
if s == "" {
|
||||
return nil, errors.New("prowlarr setting not set")
|
||||
}
|
||||
var se ProwlarrSetting
|
||||
if err := json.Unmarshal([]byte(s), &se); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &se, nil
|
||||
}
|
||||
|
||||
func (c *Client) SaveProwlarrSetting(se *ProwlarrSetting) error {
|
||||
data, err := json.Marshal(se)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.SetSetting(SettingProwlarrInfo, string(data))
|
||||
}
|
||||
|
||||
|
||||
func (c *Client) getAcceptedFormats(key string) ([]string, error) {
|
||||
v := c.GetSetting(key)
|
||||
if v == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var res []string
|
||||
|
||||
err := json.Unmarshal([]byte(v), &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (c *Client) setAcceptedFormats(key string, v []string) error {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.SetSetting(key, string(data))
|
||||
}
|
||||
|
||||
func (c *Client) GetAcceptedVideoFormats() ([]string, error) {
|
||||
res, err := c.getAcceptedFormats(SettingAcceptedVideoFormats)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res == nil {
|
||||
return defaultAcceptedVideoFormats, nil
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Client) SetAcceptedVideoFormats(key string, v []string) error {
|
||||
return c.setAcceptedFormats(SettingAcceptedVideoFormats, v)
|
||||
}
|
||||
|
||||
func (c *Client) GetAcceptedSubtitleFormats() ([]string, error) {
|
||||
res, err := c.getAcceptedFormats(SettingAcceptedSubtitleFormats)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res== nil {
|
||||
return defaultAcceptedSubtitleFormats, nil
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Client) SetAcceptedSubtitleFormats(key string, v []string) error {
|
||||
return c.setAcceptedFormats(SettingAcceptedSubtitleFormats, v)
|
||||
}
|
||||
|
||||
func (c *Client) GetAIConfig() (AIConfig, error) {
|
||||
cfg := c.GetSetting(SettingAIConfig)
|
||||
var ai AIConfig
|
||||
if cfg == "" {
|
||||
return ai, nil
|
||||
}
|
||||
err := json.Unmarshal([]byte(cfg), &ai)
|
||||
if err != nil {
|
||||
return AIConfig{}, err
|
||||
}
|
||||
return ai, nil
|
||||
}
|
||||
|
||||
func (c *Client) SetAIConfig(cfg *AIConfig) error {
|
||||
if data, err := json.Marshal(cfg); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return c.SetSetting(SettingAIConfig, string(data))
|
||||
}
|
||||
}
|
||||
13
doc/alist.md
Normal file
13
doc/alist.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# alist 对接
|
||||
|
||||
> 本程序可以把alist作为一个存储后台使用,下载完成的电影电视剧上传到alist对应的文件夹。配合阿里云、夸克云盘等实现云盘NAS功能。目前支持两种对接方式,webdav和直接对接
|
||||
|
||||
## webdav
|
||||
|
||||
使用webdav形式对接本程序,支持程序所有功能,但是不支持秒传,上传会比较慢
|
||||
|
||||
## alist 直接对接
|
||||
|
||||
存储设置里选择 alist,填入对应的信息即可。
|
||||
|
||||
优点:支持秒传,上传速度快。缺点:部分功能无法使用(plex和nfo文件刮削)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 MiB After Width: | Height: | Size: 843 KiB |
@@ -2,7 +2,7 @@
|
||||
|
||||
要正确使用此程序,需要配置好以下设置:
|
||||
|
||||
### TMDB设置
|
||||
### 1. TMDB设置
|
||||
|
||||
1. 因为此程序需要使用到 TMDB 的数据,使用此程序首先要申请一个 TMDB 的 Api Key. 申请教程请 google [tmdb api key申请](https://www.google.com/search?q=tmdb+api+key%E7%94%B3%E8%AF%B7)
|
||||
|
||||
@@ -10,35 +10,16 @@
|
||||
|
||||
**注意:** TMDB可能需要翻墙才能使用,参考 [TMDB 访问问题](./tmdb.md)
|
||||
|
||||
### 索引器
|
||||
### 2. 索引器
|
||||
|
||||
索引器是资源提供者,目前支持 torznab 协议,意味着 polarr 或者 jackett 都可以支持。请自行部署相关程序,或者使用的 docker compose 配置一起拉起
|
||||
使用配置页面索引器配置或者prowlarr设置,其中一个即可。
|
||||
|
||||
推荐使用 linuxserver 的镜像:https://docs.linuxserver.io/images/docker-jackett/
|
||||
#### jackett配置参考 [jackett](./jackett.md)
|
||||
|
||||
#### 索引器配置
|
||||
#### prowlarr设置
|
||||
|
||||
索引器配置这里以 jackett 为例。使用默认 docker compose 配置拉起后以 http://< ip >:9117 可访问 jackett 的主页。
|
||||
|
||||
1. 打开 jackett 主页后,点击页面上面的 Add indexer,会出现 BT/PT 站点列表,选择你需要的站点点击+号添加。如果是PT,请自行配置好相关配置
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
2. 添加后主页即会显示相应的BT/PT站点,点击 *Copy Torznab Feed* 即得到了我们需要的地址
|
||||
|
||||

|
||||
|
||||
3. 回到我们的主程序 Polaris 当中,点击 *设置 -> 索引器设置* -> 点击+号增加新的索引器,输入一个名称,拷贝我们第2步得到的地址到地址栏
|
||||
|
||||

|
||||
|
||||
4. 选相框中我们可以看到,还需要一个 API Key,我们回到 Jackett 中,在页面右上角,复制我们需要的 API Key:
|
||||

|
||||
|
||||
5. 恭喜!你已经成功完成了索引器配置。如需要更多的站点,请重复相同的操作完成配置
|
||||
1) 取得prowlarr的url和api key, api key在 *Prowlarr 设置 -> 通用 -> API 密钥* 处取得
|
||||
2) 对应参数填到 polaris程序,*设置->prowlarr设置*当中
|
||||
|
||||
### 下载器
|
||||
|
||||
|
||||
21
doc/jackett.md
Normal file
21
doc/jackett.md
Normal file
@@ -0,0 +1,21 @@
|
||||
## jackett 索引器配置
|
||||
|
||||
1. 打开 jackett 主页后,点击页面上面的 Add indexer,会出现 BT/PT 站点列表,选择你需要的站点点击+号添加。如果是PT,请自行配置好相关配置
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
2. 添加后主页即会显示相应的BT/PT站点,点击 *Copy Torznab Feed* 即得到了我们需要的地址
|
||||
|
||||

|
||||
|
||||
3. 回到我们的主程序 Polaris 当中,点击 *设置 -> 索引器设置* -> 点击+号增加新的索引器,输入一个名称,拷贝我们第2步得到的地址到地址栏
|
||||
|
||||

|
||||
|
||||
4. 选相框中我们可以看到,还需要一个 API Key,我们回到 Jackett 中,在页面右上角,复制我们需要的 API Key:
|
||||

|
||||
|
||||
5. 恭喜!你已经成功完成了索引器配置。如需要更多的站点,请重复相同的操作完成配置
|
||||
@@ -1,29 +1,31 @@
|
||||
# 快速开始
|
||||
|
||||
## 安装 Polaris
|
||||
## 1. 安装 Polaris
|
||||
|
||||
### Docker Compose 方式安装
|
||||
### 1.1 Docker Compose 方式安装
|
||||
|
||||
最简单使用本程序的方式是通过docker compose,下面内容保存成 docker-compose.yml,然后执行 docker compose up -d, 即可拉起程序。
|
||||
|
||||
```yaml
|
||||
services:
|
||||
polaris:
|
||||
image: ghcr.io/simon-ding/polaris:latest
|
||||
image: ghcr.io/simon-ding/polaris:latest
|
||||
restart: always
|
||||
environment:
|
||||
- PUID=99
|
||||
- PGID=100
|
||||
- TZ=Asia/Shanghai
|
||||
- PUID=99 #程序运行的用户UID
|
||||
- PGID=100 #程序运行的用户GID
|
||||
- TZ=Asia/Shanghai #时区
|
||||
volumes:
|
||||
- <配置文件路径>:/app/data #程序配置文件路径
|
||||
- <下载路径>:/downloads #下载路径,需要和下载客户端配置一致
|
||||
- <媒体文件路径>:/data #媒体数据存储路径,也可以启动自己配置webdav存储
|
||||
ports:
|
||||
- 8080:8080
|
||||
- 8080:8080 #端口映射,冒号前的端口可自行改为需要的
|
||||
```
|
||||
|
||||
### Docker 方式安装
|
||||
> latest为发布版本,如果你追求新功能,且能接受bug,可以使用main tag
|
||||
|
||||
### 1.2 Docker 方式安装
|
||||
|
||||
也可以通过原始 docker 命令的方式安装 Polaris:
|
||||
|
||||
@@ -40,19 +42,19 @@ docker run -d \
|
||||
ghcr.io/simon-ding/polaris:latest
|
||||
```
|
||||
|
||||
### Unraid 安装
|
||||
### 1.3 Unraid 安装
|
||||
|
||||
参考下图进行配置
|
||||
|
||||

|
||||
|
||||
### 访问
|
||||
### 1.4 访问
|
||||
拉起之后访问 http://< ip >:8080 即可访问 Polaris 的主页:
|
||||
|
||||

|
||||
|
||||
|
||||
## 安装下载客户端
|
||||
## 2. 安装下载客户端
|
||||
|
||||
Polaris 需要下载客户端的配合使用,目前支持 Transmission 和 Qbittorrent。推荐使用linuxserver镜像进行安装
|
||||
|
||||
@@ -62,16 +64,18 @@ Polaris 需要下载客户端的配合使用,目前支持 Transmission 和 Qbi
|
||||
|
||||
需要注意的是下载客户端内 /downloads 路径的映射地址要和 Polaris的/downloads路径映射保持一致。也就是说他俩都要映射到同一路径。
|
||||
|
||||
## 安装 Jackett
|
||||
Polaris 如果要正常工作,还需要一个索引客户端的支持,目前支持jackett索引客户端。
|
||||
## 3. 安装 Jackett/Prowlarr
|
||||
|
||||
Polaris 如果要正常工作,还需要一个索引客户端的支持,目前支持jackett/prowlarr索引客户端。推荐使用prowlarr,设置更简单
|
||||
|
||||
安装方式见:
|
||||
|
||||
* [linuxserver/jackett](https://docs.linuxserver.io/images/docker-jackett/)
|
||||
* [linuxserver/prowlarr](https://docs.linuxserver.io/images/docker-prowlarr/)
|
||||
|
||||
|
||||
|
||||
## 联合安装
|
||||
## 4. 联合安装
|
||||
|
||||
如果觉得一个个安装麻烦,也可以使用下面docker compose文件,一键拉起所有组件
|
||||
|
||||
@@ -125,7 +129,7 @@ docker compose up -d
|
||||
```
|
||||
|
||||
|
||||
## 配置
|
||||
## 5. 配置
|
||||
|
||||
详细配置请看 [配置篇](./configuration.md)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package ent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"polaris/ent/history"
|
||||
"strings"
|
||||
@@ -19,8 +20,12 @@ type History struct {
|
||||
ID int `json:"id,omitempty"`
|
||||
// MediaID holds the value of the "media_id" field.
|
||||
MediaID int `json:"media_id,omitempty"`
|
||||
// EpisodeID holds the value of the "episode_id" field.
|
||||
// deprecated
|
||||
EpisodeID int `json:"episode_id,omitempty"`
|
||||
// EpisodeNums holds the value of the "episode_nums" field.
|
||||
EpisodeNums []int `json:"episode_nums,omitempty"`
|
||||
// SeasonNum holds the value of the "season_num" field.
|
||||
SeasonNum int `json:"season_num,omitempty"`
|
||||
// SourceTitle holds the value of the "source_title" field.
|
||||
SourceTitle string `json:"source_title,omitempty"`
|
||||
// Date holds the value of the "date" field.
|
||||
@@ -47,7 +52,9 @@ func (*History) scanValues(columns []string) ([]any, error) {
|
||||
values := make([]any, len(columns))
|
||||
for i := range columns {
|
||||
switch columns[i] {
|
||||
case history.FieldID, history.FieldMediaID, history.FieldEpisodeID, history.FieldSize, history.FieldDownloadClientID, history.FieldIndexerID:
|
||||
case history.FieldEpisodeNums:
|
||||
values[i] = new([]byte)
|
||||
case history.FieldID, history.FieldMediaID, history.FieldEpisodeID, history.FieldSeasonNum, history.FieldSize, history.FieldDownloadClientID, history.FieldIndexerID:
|
||||
values[i] = new(sql.NullInt64)
|
||||
case history.FieldSourceTitle, history.FieldTargetDir, history.FieldLink, history.FieldStatus, history.FieldSaved:
|
||||
values[i] = new(sql.NullString)
|
||||
@@ -86,6 +93,20 @@ func (h *History) assignValues(columns []string, values []any) error {
|
||||
} else if value.Valid {
|
||||
h.EpisodeID = int(value.Int64)
|
||||
}
|
||||
case history.FieldEpisodeNums:
|
||||
if value, ok := values[i].(*[]byte); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field episode_nums", values[i])
|
||||
} else if value != nil && len(*value) > 0 {
|
||||
if err := json.Unmarshal(*value, &h.EpisodeNums); err != nil {
|
||||
return fmt.Errorf("unmarshal field episode_nums: %w", err)
|
||||
}
|
||||
}
|
||||
case history.FieldSeasonNum:
|
||||
if value, ok := values[i].(*sql.NullInt64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field season_num", values[i])
|
||||
} else if value.Valid {
|
||||
h.SeasonNum = int(value.Int64)
|
||||
}
|
||||
case history.FieldSourceTitle:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field source_title", values[i])
|
||||
@@ -182,6 +203,12 @@ func (h *History) String() string {
|
||||
builder.WriteString("episode_id=")
|
||||
builder.WriteString(fmt.Sprintf("%v", h.EpisodeID))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("episode_nums=")
|
||||
builder.WriteString(fmt.Sprintf("%v", h.EpisodeNums))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("season_num=")
|
||||
builder.WriteString(fmt.Sprintf("%v", h.SeasonNum))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("source_title=")
|
||||
builder.WriteString(h.SourceTitle)
|
||||
builder.WriteString(", ")
|
||||
|
||||
@@ -17,6 +17,10 @@ const (
|
||||
FieldMediaID = "media_id"
|
||||
// FieldEpisodeID holds the string denoting the episode_id field in the database.
|
||||
FieldEpisodeID = "episode_id"
|
||||
// FieldEpisodeNums holds the string denoting the episode_nums field in the database.
|
||||
FieldEpisodeNums = "episode_nums"
|
||||
// FieldSeasonNum holds the string denoting the season_num field in the database.
|
||||
FieldSeasonNum = "season_num"
|
||||
// FieldSourceTitle holds the string denoting the source_title field in the database.
|
||||
FieldSourceTitle = "source_title"
|
||||
// FieldDate holds the string denoting the date field in the database.
|
||||
@@ -44,6 +48,8 @@ var Columns = []string{
|
||||
FieldID,
|
||||
FieldMediaID,
|
||||
FieldEpisodeID,
|
||||
FieldEpisodeNums,
|
||||
FieldSeasonNum,
|
||||
FieldSourceTitle,
|
||||
FieldDate,
|
||||
FieldTargetDir,
|
||||
@@ -114,6 +120,11 @@ func ByEpisodeID(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldEpisodeID, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// BySeasonNum orders the results by the season_num field.
|
||||
func BySeasonNum(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldSeasonNum, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// BySourceTitle orders the results by the source_title field.
|
||||
func BySourceTitle(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldSourceTitle, opts...).ToFunc()
|
||||
|
||||
@@ -64,6 +64,11 @@ func EpisodeID(v int) predicate.History {
|
||||
return predicate.History(sql.FieldEQ(FieldEpisodeID, v))
|
||||
}
|
||||
|
||||
// SeasonNum applies equality check predicate on the "season_num" field. It's identical to SeasonNumEQ.
|
||||
func SeasonNum(v int) predicate.History {
|
||||
return predicate.History(sql.FieldEQ(FieldSeasonNum, v))
|
||||
}
|
||||
|
||||
// SourceTitle applies equality check predicate on the "source_title" field. It's identical to SourceTitleEQ.
|
||||
func SourceTitle(v string) predicate.History {
|
||||
return predicate.History(sql.FieldEQ(FieldSourceTitle, v))
|
||||
@@ -194,6 +199,66 @@ func EpisodeIDNotNil() predicate.History {
|
||||
return predicate.History(sql.FieldNotNull(FieldEpisodeID))
|
||||
}
|
||||
|
||||
// EpisodeNumsIsNil applies the IsNil predicate on the "episode_nums" field.
|
||||
func EpisodeNumsIsNil() predicate.History {
|
||||
return predicate.History(sql.FieldIsNull(FieldEpisodeNums))
|
||||
}
|
||||
|
||||
// EpisodeNumsNotNil applies the NotNil predicate on the "episode_nums" field.
|
||||
func EpisodeNumsNotNil() predicate.History {
|
||||
return predicate.History(sql.FieldNotNull(FieldEpisodeNums))
|
||||
}
|
||||
|
||||
// SeasonNumEQ applies the EQ predicate on the "season_num" field.
|
||||
func SeasonNumEQ(v int) predicate.History {
|
||||
return predicate.History(sql.FieldEQ(FieldSeasonNum, v))
|
||||
}
|
||||
|
||||
// SeasonNumNEQ applies the NEQ predicate on the "season_num" field.
|
||||
func SeasonNumNEQ(v int) predicate.History {
|
||||
return predicate.History(sql.FieldNEQ(FieldSeasonNum, v))
|
||||
}
|
||||
|
||||
// SeasonNumIn applies the In predicate on the "season_num" field.
|
||||
func SeasonNumIn(vs ...int) predicate.History {
|
||||
return predicate.History(sql.FieldIn(FieldSeasonNum, vs...))
|
||||
}
|
||||
|
||||
// SeasonNumNotIn applies the NotIn predicate on the "season_num" field.
|
||||
func SeasonNumNotIn(vs ...int) predicate.History {
|
||||
return predicate.History(sql.FieldNotIn(FieldSeasonNum, vs...))
|
||||
}
|
||||
|
||||
// SeasonNumGT applies the GT predicate on the "season_num" field.
|
||||
func SeasonNumGT(v int) predicate.History {
|
||||
return predicate.History(sql.FieldGT(FieldSeasonNum, v))
|
||||
}
|
||||
|
||||
// SeasonNumGTE applies the GTE predicate on the "season_num" field.
|
||||
func SeasonNumGTE(v int) predicate.History {
|
||||
return predicate.History(sql.FieldGTE(FieldSeasonNum, v))
|
||||
}
|
||||
|
||||
// SeasonNumLT applies the LT predicate on the "season_num" field.
|
||||
func SeasonNumLT(v int) predicate.History {
|
||||
return predicate.History(sql.FieldLT(FieldSeasonNum, v))
|
||||
}
|
||||
|
||||
// SeasonNumLTE applies the LTE predicate on the "season_num" field.
|
||||
func SeasonNumLTE(v int) predicate.History {
|
||||
return predicate.History(sql.FieldLTE(FieldSeasonNum, v))
|
||||
}
|
||||
|
||||
// SeasonNumIsNil applies the IsNil predicate on the "season_num" field.
|
||||
func SeasonNumIsNil() predicate.History {
|
||||
return predicate.History(sql.FieldIsNull(FieldSeasonNum))
|
||||
}
|
||||
|
||||
// SeasonNumNotNil applies the NotNil predicate on the "season_num" field.
|
||||
func SeasonNumNotNil() predicate.History {
|
||||
return predicate.History(sql.FieldNotNull(FieldSeasonNum))
|
||||
}
|
||||
|
||||
// SourceTitleEQ applies the EQ predicate on the "source_title" field.
|
||||
func SourceTitleEQ(v string) predicate.History {
|
||||
return predicate.History(sql.FieldEQ(FieldSourceTitle, v))
|
||||
|
||||
@@ -40,6 +40,26 @@ func (hc *HistoryCreate) SetNillableEpisodeID(i *int) *HistoryCreate {
|
||||
return hc
|
||||
}
|
||||
|
||||
// SetEpisodeNums sets the "episode_nums" field.
|
||||
func (hc *HistoryCreate) SetEpisodeNums(i []int) *HistoryCreate {
|
||||
hc.mutation.SetEpisodeNums(i)
|
||||
return hc
|
||||
}
|
||||
|
||||
// SetSeasonNum sets the "season_num" field.
|
||||
func (hc *HistoryCreate) SetSeasonNum(i int) *HistoryCreate {
|
||||
hc.mutation.SetSeasonNum(i)
|
||||
return hc
|
||||
}
|
||||
|
||||
// SetNillableSeasonNum sets the "season_num" field if the given value is not nil.
|
||||
func (hc *HistoryCreate) SetNillableSeasonNum(i *int) *HistoryCreate {
|
||||
if i != nil {
|
||||
hc.SetSeasonNum(*i)
|
||||
}
|
||||
return hc
|
||||
}
|
||||
|
||||
// SetSourceTitle sets the "source_title" field.
|
||||
func (hc *HistoryCreate) SetSourceTitle(s string) *HistoryCreate {
|
||||
hc.mutation.SetSourceTitle(s)
|
||||
@@ -234,6 +254,14 @@ func (hc *HistoryCreate) createSpec() (*History, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(history.FieldEpisodeID, field.TypeInt, value)
|
||||
_node.EpisodeID = value
|
||||
}
|
||||
if value, ok := hc.mutation.EpisodeNums(); ok {
|
||||
_spec.SetField(history.FieldEpisodeNums, field.TypeJSON, value)
|
||||
_node.EpisodeNums = value
|
||||
}
|
||||
if value, ok := hc.mutation.SeasonNum(); ok {
|
||||
_spec.SetField(history.FieldSeasonNum, field.TypeInt, value)
|
||||
_node.SeasonNum = value
|
||||
}
|
||||
if value, ok := hc.mutation.SourceTitle(); ok {
|
||||
_spec.SetField(history.FieldSourceTitle, field.TypeString, value)
|
||||
_node.SourceTitle = value
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||
"entgo.io/ent/dialect/sql/sqljson"
|
||||
"entgo.io/ent/schema/field"
|
||||
)
|
||||
|
||||
@@ -76,6 +77,51 @@ func (hu *HistoryUpdate) ClearEpisodeID() *HistoryUpdate {
|
||||
return hu
|
||||
}
|
||||
|
||||
// SetEpisodeNums sets the "episode_nums" field.
|
||||
func (hu *HistoryUpdate) SetEpisodeNums(i []int) *HistoryUpdate {
|
||||
hu.mutation.SetEpisodeNums(i)
|
||||
return hu
|
||||
}
|
||||
|
||||
// AppendEpisodeNums appends i to the "episode_nums" field.
|
||||
func (hu *HistoryUpdate) AppendEpisodeNums(i []int) *HistoryUpdate {
|
||||
hu.mutation.AppendEpisodeNums(i)
|
||||
return hu
|
||||
}
|
||||
|
||||
// ClearEpisodeNums clears the value of the "episode_nums" field.
|
||||
func (hu *HistoryUpdate) ClearEpisodeNums() *HistoryUpdate {
|
||||
hu.mutation.ClearEpisodeNums()
|
||||
return hu
|
||||
}
|
||||
|
||||
// SetSeasonNum sets the "season_num" field.
|
||||
func (hu *HistoryUpdate) SetSeasonNum(i int) *HistoryUpdate {
|
||||
hu.mutation.ResetSeasonNum()
|
||||
hu.mutation.SetSeasonNum(i)
|
||||
return hu
|
||||
}
|
||||
|
||||
// SetNillableSeasonNum sets the "season_num" field if the given value is not nil.
|
||||
func (hu *HistoryUpdate) SetNillableSeasonNum(i *int) *HistoryUpdate {
|
||||
if i != nil {
|
||||
hu.SetSeasonNum(*i)
|
||||
}
|
||||
return hu
|
||||
}
|
||||
|
||||
// AddSeasonNum adds i to the "season_num" field.
|
||||
func (hu *HistoryUpdate) AddSeasonNum(i int) *HistoryUpdate {
|
||||
hu.mutation.AddSeasonNum(i)
|
||||
return hu
|
||||
}
|
||||
|
||||
// ClearSeasonNum clears the value of the "season_num" field.
|
||||
func (hu *HistoryUpdate) ClearSeasonNum() *HistoryUpdate {
|
||||
hu.mutation.ClearSeasonNum()
|
||||
return hu
|
||||
}
|
||||
|
||||
// SetSourceTitle sets the "source_title" field.
|
||||
func (hu *HistoryUpdate) SetSourceTitle(s string) *HistoryUpdate {
|
||||
hu.mutation.SetSourceTitle(s)
|
||||
@@ -316,6 +362,26 @@ func (hu *HistoryUpdate) sqlSave(ctx context.Context) (n int, err error) {
|
||||
if hu.mutation.EpisodeIDCleared() {
|
||||
_spec.ClearField(history.FieldEpisodeID, field.TypeInt)
|
||||
}
|
||||
if value, ok := hu.mutation.EpisodeNums(); ok {
|
||||
_spec.SetField(history.FieldEpisodeNums, field.TypeJSON, value)
|
||||
}
|
||||
if value, ok := hu.mutation.AppendedEpisodeNums(); ok {
|
||||
_spec.AddModifier(func(u *sql.UpdateBuilder) {
|
||||
sqljson.Append(u, history.FieldEpisodeNums, value)
|
||||
})
|
||||
}
|
||||
if hu.mutation.EpisodeNumsCleared() {
|
||||
_spec.ClearField(history.FieldEpisodeNums, field.TypeJSON)
|
||||
}
|
||||
if value, ok := hu.mutation.SeasonNum(); ok {
|
||||
_spec.SetField(history.FieldSeasonNum, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := hu.mutation.AddedSeasonNum(); ok {
|
||||
_spec.AddField(history.FieldSeasonNum, field.TypeInt, value)
|
||||
}
|
||||
if hu.mutation.SeasonNumCleared() {
|
||||
_spec.ClearField(history.FieldSeasonNum, field.TypeInt)
|
||||
}
|
||||
if value, ok := hu.mutation.SourceTitle(); ok {
|
||||
_spec.SetField(history.FieldSourceTitle, field.TypeString, value)
|
||||
}
|
||||
@@ -432,6 +498,51 @@ func (huo *HistoryUpdateOne) ClearEpisodeID() *HistoryUpdateOne {
|
||||
return huo
|
||||
}
|
||||
|
||||
// SetEpisodeNums sets the "episode_nums" field.
|
||||
func (huo *HistoryUpdateOne) SetEpisodeNums(i []int) *HistoryUpdateOne {
|
||||
huo.mutation.SetEpisodeNums(i)
|
||||
return huo
|
||||
}
|
||||
|
||||
// AppendEpisodeNums appends i to the "episode_nums" field.
|
||||
func (huo *HistoryUpdateOne) AppendEpisodeNums(i []int) *HistoryUpdateOne {
|
||||
huo.mutation.AppendEpisodeNums(i)
|
||||
return huo
|
||||
}
|
||||
|
||||
// ClearEpisodeNums clears the value of the "episode_nums" field.
|
||||
func (huo *HistoryUpdateOne) ClearEpisodeNums() *HistoryUpdateOne {
|
||||
huo.mutation.ClearEpisodeNums()
|
||||
return huo
|
||||
}
|
||||
|
||||
// SetSeasonNum sets the "season_num" field.
|
||||
func (huo *HistoryUpdateOne) SetSeasonNum(i int) *HistoryUpdateOne {
|
||||
huo.mutation.ResetSeasonNum()
|
||||
huo.mutation.SetSeasonNum(i)
|
||||
return huo
|
||||
}
|
||||
|
||||
// SetNillableSeasonNum sets the "season_num" field if the given value is not nil.
|
||||
func (huo *HistoryUpdateOne) SetNillableSeasonNum(i *int) *HistoryUpdateOne {
|
||||
if i != nil {
|
||||
huo.SetSeasonNum(*i)
|
||||
}
|
||||
return huo
|
||||
}
|
||||
|
||||
// AddSeasonNum adds i to the "season_num" field.
|
||||
func (huo *HistoryUpdateOne) AddSeasonNum(i int) *HistoryUpdateOne {
|
||||
huo.mutation.AddSeasonNum(i)
|
||||
return huo
|
||||
}
|
||||
|
||||
// ClearSeasonNum clears the value of the "season_num" field.
|
||||
func (huo *HistoryUpdateOne) ClearSeasonNum() *HistoryUpdateOne {
|
||||
huo.mutation.ClearSeasonNum()
|
||||
return huo
|
||||
}
|
||||
|
||||
// SetSourceTitle sets the "source_title" field.
|
||||
func (huo *HistoryUpdateOne) SetSourceTitle(s string) *HistoryUpdateOne {
|
||||
huo.mutation.SetSourceTitle(s)
|
||||
@@ -702,6 +813,26 @@ func (huo *HistoryUpdateOne) sqlSave(ctx context.Context) (_node *History, err e
|
||||
if huo.mutation.EpisodeIDCleared() {
|
||||
_spec.ClearField(history.FieldEpisodeID, field.TypeInt)
|
||||
}
|
||||
if value, ok := huo.mutation.EpisodeNums(); ok {
|
||||
_spec.SetField(history.FieldEpisodeNums, field.TypeJSON, value)
|
||||
}
|
||||
if value, ok := huo.mutation.AppendedEpisodeNums(); ok {
|
||||
_spec.AddModifier(func(u *sql.UpdateBuilder) {
|
||||
sqljson.Append(u, history.FieldEpisodeNums, value)
|
||||
})
|
||||
}
|
||||
if huo.mutation.EpisodeNumsCleared() {
|
||||
_spec.ClearField(history.FieldEpisodeNums, field.TypeJSON)
|
||||
}
|
||||
if value, ok := huo.mutation.SeasonNum(); ok {
|
||||
_spec.SetField(history.FieldSeasonNum, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := huo.mutation.AddedSeasonNum(); ok {
|
||||
_spec.AddField(history.FieldSeasonNum, field.TypeInt, value)
|
||||
}
|
||||
if huo.mutation.SeasonNumCleared() {
|
||||
_spec.ClearField(history.FieldSeasonNum, field.TypeInt)
|
||||
}
|
||||
if value, ok := huo.mutation.SourceTitle(); ok {
|
||||
_spec.SetField(history.FieldSourceTitle, field.TypeString, value)
|
||||
}
|
||||
|
||||
15
ent/media.go
15
ent/media.go
@@ -49,6 +49,8 @@ type Media struct {
|
||||
Limiter schema.MediaLimiter `json:"limiter,omitempty"`
|
||||
// Extras holds the value of the "extras" field.
|
||||
Extras schema.MediaExtras `json:"extras,omitempty"`
|
||||
// AlternativeTitles holds the value of the "alternative_titles" field.
|
||||
AlternativeTitles []schema.AlternativeTilte `json:"alternative_titles,omitempty"`
|
||||
// Edges holds the relations/edges for other nodes in the graph.
|
||||
// The values are being populated by the MediaQuery when eager-loading is set.
|
||||
Edges MediaEdges `json:"edges"`
|
||||
@@ -78,7 +80,7 @@ func (*Media) scanValues(columns []string) ([]any, error) {
|
||||
values := make([]any, len(columns))
|
||||
for i := range columns {
|
||||
switch columns[i] {
|
||||
case media.FieldLimiter, media.FieldExtras:
|
||||
case media.FieldLimiter, media.FieldExtras, media.FieldAlternativeTitles:
|
||||
values[i] = new([]byte)
|
||||
case media.FieldDownloadHistoryEpisodes:
|
||||
values[i] = new(sql.NullBool)
|
||||
@@ -203,6 +205,14 @@ func (m *Media) assignValues(columns []string, values []any) error {
|
||||
return fmt.Errorf("unmarshal field extras: %w", err)
|
||||
}
|
||||
}
|
||||
case media.FieldAlternativeTitles:
|
||||
if value, ok := values[i].(*[]byte); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field alternative_titles", values[i])
|
||||
} else if value != nil && len(*value) > 0 {
|
||||
if err := json.Unmarshal(*value, &m.AlternativeTitles); err != nil {
|
||||
return fmt.Errorf("unmarshal field alternative_titles: %w", err)
|
||||
}
|
||||
}
|
||||
default:
|
||||
m.selectValues.Set(columns[i], values[i])
|
||||
}
|
||||
@@ -288,6 +298,9 @@ func (m *Media) String() string {
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("extras=")
|
||||
builder.WriteString(fmt.Sprintf("%v", m.Extras))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("alternative_titles=")
|
||||
builder.WriteString(fmt.Sprintf("%v", m.AlternativeTitles))
|
||||
builder.WriteByte(')')
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ const (
|
||||
FieldLimiter = "limiter"
|
||||
// FieldExtras holds the string denoting the extras field in the database.
|
||||
FieldExtras = "extras"
|
||||
// FieldAlternativeTitles holds the string denoting the alternative_titles field in the database.
|
||||
FieldAlternativeTitles = "alternative_titles"
|
||||
// EdgeEpisodes holds the string denoting the episodes edge name in mutations.
|
||||
EdgeEpisodes = "episodes"
|
||||
// Table holds the table name of the media in the database.
|
||||
@@ -76,6 +78,7 @@ var Columns = []string{
|
||||
FieldDownloadHistoryEpisodes,
|
||||
FieldLimiter,
|
||||
FieldExtras,
|
||||
FieldAlternativeTitles,
|
||||
}
|
||||
|
||||
// ValidColumn reports if the column name is valid (part of the table columns).
|
||||
|
||||
@@ -795,6 +795,16 @@ func ExtrasNotNil() predicate.Media {
|
||||
return predicate.Media(sql.FieldNotNull(FieldExtras))
|
||||
}
|
||||
|
||||
// AlternativeTitlesIsNil applies the IsNil predicate on the "alternative_titles" field.
|
||||
func AlternativeTitlesIsNil() predicate.Media {
|
||||
return predicate.Media(sql.FieldIsNull(FieldAlternativeTitles))
|
||||
}
|
||||
|
||||
// AlternativeTitlesNotNil applies the NotNil predicate on the "alternative_titles" field.
|
||||
func AlternativeTitlesNotNil() predicate.Media {
|
||||
return predicate.Media(sql.FieldNotNull(FieldAlternativeTitles))
|
||||
}
|
||||
|
||||
// HasEpisodes applies the HasEdge predicate on the "episodes" edge.
|
||||
func HasEpisodes() predicate.Media {
|
||||
return predicate.Media(func(s *sql.Selector) {
|
||||
|
||||
@@ -184,6 +184,12 @@ func (mc *MediaCreate) SetNillableExtras(se *schema.MediaExtras) *MediaCreate {
|
||||
return mc
|
||||
}
|
||||
|
||||
// SetAlternativeTitles sets the "alternative_titles" field.
|
||||
func (mc *MediaCreate) SetAlternativeTitles(st []schema.AlternativeTilte) *MediaCreate {
|
||||
mc.mutation.SetAlternativeTitles(st)
|
||||
return mc
|
||||
}
|
||||
|
||||
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by IDs.
|
||||
func (mc *MediaCreate) AddEpisodeIDs(ids ...int) *MediaCreate {
|
||||
mc.mutation.AddEpisodeIDs(ids...)
|
||||
@@ -377,6 +383,10 @@ func (mc *MediaCreate) createSpec() (*Media, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(media.FieldExtras, field.TypeJSON, value)
|
||||
_node.Extras = value
|
||||
}
|
||||
if value, ok := mc.mutation.AlternativeTitles(); ok {
|
||||
_spec.SetField(media.FieldAlternativeTitles, field.TypeJSON, value)
|
||||
_node.AlternativeTitles = value
|
||||
}
|
||||
if nodes := mc.mutation.EpisodesIDs(); len(nodes) > 0 {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||
"entgo.io/ent/dialect/sql/sqljson"
|
||||
"entgo.io/ent/schema/field"
|
||||
)
|
||||
|
||||
@@ -290,6 +291,24 @@ func (mu *MediaUpdate) ClearExtras() *MediaUpdate {
|
||||
return mu
|
||||
}
|
||||
|
||||
// SetAlternativeTitles sets the "alternative_titles" field.
|
||||
func (mu *MediaUpdate) SetAlternativeTitles(st []schema.AlternativeTilte) *MediaUpdate {
|
||||
mu.mutation.SetAlternativeTitles(st)
|
||||
return mu
|
||||
}
|
||||
|
||||
// AppendAlternativeTitles appends st to the "alternative_titles" field.
|
||||
func (mu *MediaUpdate) AppendAlternativeTitles(st []schema.AlternativeTilte) *MediaUpdate {
|
||||
mu.mutation.AppendAlternativeTitles(st)
|
||||
return mu
|
||||
}
|
||||
|
||||
// ClearAlternativeTitles clears the value of the "alternative_titles" field.
|
||||
func (mu *MediaUpdate) ClearAlternativeTitles() *MediaUpdate {
|
||||
mu.mutation.ClearAlternativeTitles()
|
||||
return mu
|
||||
}
|
||||
|
||||
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by IDs.
|
||||
func (mu *MediaUpdate) AddEpisodeIDs(ids ...int) *MediaUpdate {
|
||||
mu.mutation.AddEpisodeIDs(ids...)
|
||||
@@ -454,6 +473,17 @@ func (mu *MediaUpdate) sqlSave(ctx context.Context) (n int, err error) {
|
||||
if mu.mutation.ExtrasCleared() {
|
||||
_spec.ClearField(media.FieldExtras, field.TypeJSON)
|
||||
}
|
||||
if value, ok := mu.mutation.AlternativeTitles(); ok {
|
||||
_spec.SetField(media.FieldAlternativeTitles, field.TypeJSON, value)
|
||||
}
|
||||
if value, ok := mu.mutation.AppendedAlternativeTitles(); ok {
|
||||
_spec.AddModifier(func(u *sql.UpdateBuilder) {
|
||||
sqljson.Append(u, media.FieldAlternativeTitles, value)
|
||||
})
|
||||
}
|
||||
if mu.mutation.AlternativeTitlesCleared() {
|
||||
_spec.ClearField(media.FieldAlternativeTitles, field.TypeJSON)
|
||||
}
|
||||
if mu.mutation.EpisodesCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
@@ -779,6 +809,24 @@ func (muo *MediaUpdateOne) ClearExtras() *MediaUpdateOne {
|
||||
return muo
|
||||
}
|
||||
|
||||
// SetAlternativeTitles sets the "alternative_titles" field.
|
||||
func (muo *MediaUpdateOne) SetAlternativeTitles(st []schema.AlternativeTilte) *MediaUpdateOne {
|
||||
muo.mutation.SetAlternativeTitles(st)
|
||||
return muo
|
||||
}
|
||||
|
||||
// AppendAlternativeTitles appends st to the "alternative_titles" field.
|
||||
func (muo *MediaUpdateOne) AppendAlternativeTitles(st []schema.AlternativeTilte) *MediaUpdateOne {
|
||||
muo.mutation.AppendAlternativeTitles(st)
|
||||
return muo
|
||||
}
|
||||
|
||||
// ClearAlternativeTitles clears the value of the "alternative_titles" field.
|
||||
func (muo *MediaUpdateOne) ClearAlternativeTitles() *MediaUpdateOne {
|
||||
muo.mutation.ClearAlternativeTitles()
|
||||
return muo
|
||||
}
|
||||
|
||||
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by IDs.
|
||||
func (muo *MediaUpdateOne) AddEpisodeIDs(ids ...int) *MediaUpdateOne {
|
||||
muo.mutation.AddEpisodeIDs(ids...)
|
||||
@@ -973,6 +1021,17 @@ func (muo *MediaUpdateOne) sqlSave(ctx context.Context) (_node *Media, err error
|
||||
if muo.mutation.ExtrasCleared() {
|
||||
_spec.ClearField(media.FieldExtras, field.TypeJSON)
|
||||
}
|
||||
if value, ok := muo.mutation.AlternativeTitles(); ok {
|
||||
_spec.SetField(media.FieldAlternativeTitles, field.TypeJSON, value)
|
||||
}
|
||||
if value, ok := muo.mutation.AppendedAlternativeTitles(); ok {
|
||||
_spec.AddModifier(func(u *sql.UpdateBuilder) {
|
||||
sqljson.Append(u, media.FieldAlternativeTitles, value)
|
||||
})
|
||||
}
|
||||
if muo.mutation.AlternativeTitlesCleared() {
|
||||
_spec.ClearField(media.FieldAlternativeTitles, field.TypeJSON)
|
||||
}
|
||||
if muo.mutation.EpisodesCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
|
||||
@@ -74,6 +74,8 @@ var (
|
||||
{Name: "id", Type: field.TypeInt, Increment: true},
|
||||
{Name: "media_id", Type: field.TypeInt},
|
||||
{Name: "episode_id", Type: field.TypeInt, Nullable: true},
|
||||
{Name: "episode_nums", Type: field.TypeJSON, Nullable: true},
|
||||
{Name: "season_num", Type: field.TypeInt, Nullable: true},
|
||||
{Name: "source_title", Type: field.TypeString},
|
||||
{Name: "date", Type: field.TypeTime},
|
||||
{Name: "target_dir", Type: field.TypeString},
|
||||
@@ -141,6 +143,7 @@ var (
|
||||
{Name: "download_history_episodes", Type: field.TypeBool, Nullable: true, Default: false},
|
||||
{Name: "limiter", Type: field.TypeJSON, Nullable: true},
|
||||
{Name: "extras", Type: field.TypeJSON, Nullable: true},
|
||||
{Name: "alternative_titles", Type: field.TypeJSON, Nullable: true},
|
||||
}
|
||||
// MediaTable holds the schema information for the "media" table.
|
||||
MediaTable = &schema.Table{
|
||||
@@ -178,7 +181,7 @@ var (
|
||||
StoragesColumns = []*schema.Column{
|
||||
{Name: "id", Type: field.TypeInt, Increment: true},
|
||||
{Name: "name", Type: field.TypeString, Unique: true},
|
||||
{Name: "implementation", Type: field.TypeEnum, Enums: []string{"webdav", "local"}},
|
||||
{Name: "implementation", Type: field.TypeEnum, Enums: []string{"webdav", "local", "alist"}},
|
||||
{Name: "tv_path", Type: field.TypeString, Nullable: true},
|
||||
{Name: "movie_path", Type: field.TypeString, Nullable: true},
|
||||
{Name: "settings", Type: field.TypeString, Nullable: true},
|
||||
|
||||
291
ent/mutation.go
291
ent/mutation.go
@@ -2336,6 +2336,10 @@ type HistoryMutation struct {
|
||||
addmedia_id *int
|
||||
episode_id *int
|
||||
addepisode_id *int
|
||||
episode_nums *[]int
|
||||
appendepisode_nums []int
|
||||
season_num *int
|
||||
addseason_num *int
|
||||
source_title *string
|
||||
date *time.Time
|
||||
target_dir *string
|
||||
@@ -2578,6 +2582,141 @@ func (m *HistoryMutation) ResetEpisodeID() {
|
||||
delete(m.clearedFields, history.FieldEpisodeID)
|
||||
}
|
||||
|
||||
// SetEpisodeNums sets the "episode_nums" field.
|
||||
func (m *HistoryMutation) SetEpisodeNums(i []int) {
|
||||
m.episode_nums = &i
|
||||
m.appendepisode_nums = nil
|
||||
}
|
||||
|
||||
// EpisodeNums returns the value of the "episode_nums" field in the mutation.
|
||||
func (m *HistoryMutation) EpisodeNums() (r []int, exists bool) {
|
||||
v := m.episode_nums
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldEpisodeNums returns the old "episode_nums" field's value of the History entity.
|
||||
// If the History object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *HistoryMutation) OldEpisodeNums(ctx context.Context) (v []int, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldEpisodeNums is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldEpisodeNums requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldEpisodeNums: %w", err)
|
||||
}
|
||||
return oldValue.EpisodeNums, nil
|
||||
}
|
||||
|
||||
// AppendEpisodeNums adds i to the "episode_nums" field.
|
||||
func (m *HistoryMutation) AppendEpisodeNums(i []int) {
|
||||
m.appendepisode_nums = append(m.appendepisode_nums, i...)
|
||||
}
|
||||
|
||||
// AppendedEpisodeNums returns the list of values that were appended to the "episode_nums" field in this mutation.
|
||||
func (m *HistoryMutation) AppendedEpisodeNums() ([]int, bool) {
|
||||
if len(m.appendepisode_nums) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
return m.appendepisode_nums, true
|
||||
}
|
||||
|
||||
// ClearEpisodeNums clears the value of the "episode_nums" field.
|
||||
func (m *HistoryMutation) ClearEpisodeNums() {
|
||||
m.episode_nums = nil
|
||||
m.appendepisode_nums = nil
|
||||
m.clearedFields[history.FieldEpisodeNums] = struct{}{}
|
||||
}
|
||||
|
||||
// EpisodeNumsCleared returns if the "episode_nums" field was cleared in this mutation.
|
||||
func (m *HistoryMutation) EpisodeNumsCleared() bool {
|
||||
_, ok := m.clearedFields[history.FieldEpisodeNums]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ResetEpisodeNums resets all changes to the "episode_nums" field.
|
||||
func (m *HistoryMutation) ResetEpisodeNums() {
|
||||
m.episode_nums = nil
|
||||
m.appendepisode_nums = nil
|
||||
delete(m.clearedFields, history.FieldEpisodeNums)
|
||||
}
|
||||
|
||||
// SetSeasonNum sets the "season_num" field.
|
||||
func (m *HistoryMutation) SetSeasonNum(i int) {
|
||||
m.season_num = &i
|
||||
m.addseason_num = nil
|
||||
}
|
||||
|
||||
// SeasonNum returns the value of the "season_num" field in the mutation.
|
||||
func (m *HistoryMutation) SeasonNum() (r int, exists bool) {
|
||||
v := m.season_num
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldSeasonNum returns the old "season_num" field's value of the History entity.
|
||||
// If the History object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *HistoryMutation) OldSeasonNum(ctx context.Context) (v int, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldSeasonNum is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldSeasonNum requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldSeasonNum: %w", err)
|
||||
}
|
||||
return oldValue.SeasonNum, nil
|
||||
}
|
||||
|
||||
// AddSeasonNum adds i to the "season_num" field.
|
||||
func (m *HistoryMutation) AddSeasonNum(i int) {
|
||||
if m.addseason_num != nil {
|
||||
*m.addseason_num += i
|
||||
} else {
|
||||
m.addseason_num = &i
|
||||
}
|
||||
}
|
||||
|
||||
// AddedSeasonNum returns the value that was added to the "season_num" field in this mutation.
|
||||
func (m *HistoryMutation) AddedSeasonNum() (r int, exists bool) {
|
||||
v := m.addseason_num
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// ClearSeasonNum clears the value of the "season_num" field.
|
||||
func (m *HistoryMutation) ClearSeasonNum() {
|
||||
m.season_num = nil
|
||||
m.addseason_num = nil
|
||||
m.clearedFields[history.FieldSeasonNum] = struct{}{}
|
||||
}
|
||||
|
||||
// SeasonNumCleared returns if the "season_num" field was cleared in this mutation.
|
||||
func (m *HistoryMutation) SeasonNumCleared() bool {
|
||||
_, ok := m.clearedFields[history.FieldSeasonNum]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ResetSeasonNum resets all changes to the "season_num" field.
|
||||
func (m *HistoryMutation) ResetSeasonNum() {
|
||||
m.season_num = nil
|
||||
m.addseason_num = nil
|
||||
delete(m.clearedFields, history.FieldSeasonNum)
|
||||
}
|
||||
|
||||
// SetSourceTitle sets the "source_title" field.
|
||||
func (m *HistoryMutation) SetSourceTitle(s string) {
|
||||
m.source_title = &s
|
||||
@@ -3050,13 +3189,19 @@ func (m *HistoryMutation) Type() string {
|
||||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *HistoryMutation) Fields() []string {
|
||||
fields := make([]string, 0, 11)
|
||||
fields := make([]string, 0, 13)
|
||||
if m.media_id != nil {
|
||||
fields = append(fields, history.FieldMediaID)
|
||||
}
|
||||
if m.episode_id != nil {
|
||||
fields = append(fields, history.FieldEpisodeID)
|
||||
}
|
||||
if m.episode_nums != nil {
|
||||
fields = append(fields, history.FieldEpisodeNums)
|
||||
}
|
||||
if m.season_num != nil {
|
||||
fields = append(fields, history.FieldSeasonNum)
|
||||
}
|
||||
if m.source_title != nil {
|
||||
fields = append(fields, history.FieldSourceTitle)
|
||||
}
|
||||
@@ -3096,6 +3241,10 @@ func (m *HistoryMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.MediaID()
|
||||
case history.FieldEpisodeID:
|
||||
return m.EpisodeID()
|
||||
case history.FieldEpisodeNums:
|
||||
return m.EpisodeNums()
|
||||
case history.FieldSeasonNum:
|
||||
return m.SeasonNum()
|
||||
case history.FieldSourceTitle:
|
||||
return m.SourceTitle()
|
||||
case history.FieldDate:
|
||||
@@ -3127,6 +3276,10 @@ func (m *HistoryMutation) OldField(ctx context.Context, name string) (ent.Value,
|
||||
return m.OldMediaID(ctx)
|
||||
case history.FieldEpisodeID:
|
||||
return m.OldEpisodeID(ctx)
|
||||
case history.FieldEpisodeNums:
|
||||
return m.OldEpisodeNums(ctx)
|
||||
case history.FieldSeasonNum:
|
||||
return m.OldSeasonNum(ctx)
|
||||
case history.FieldSourceTitle:
|
||||
return m.OldSourceTitle(ctx)
|
||||
case history.FieldDate:
|
||||
@@ -3168,6 +3321,20 @@ func (m *HistoryMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetEpisodeID(v)
|
||||
return nil
|
||||
case history.FieldEpisodeNums:
|
||||
v, ok := value.([]int)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetEpisodeNums(v)
|
||||
return nil
|
||||
case history.FieldSeasonNum:
|
||||
v, ok := value.(int)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetSeasonNum(v)
|
||||
return nil
|
||||
case history.FieldSourceTitle:
|
||||
v, ok := value.(string)
|
||||
if !ok {
|
||||
@@ -3245,6 +3412,9 @@ func (m *HistoryMutation) AddedFields() []string {
|
||||
if m.addepisode_id != nil {
|
||||
fields = append(fields, history.FieldEpisodeID)
|
||||
}
|
||||
if m.addseason_num != nil {
|
||||
fields = append(fields, history.FieldSeasonNum)
|
||||
}
|
||||
if m.addsize != nil {
|
||||
fields = append(fields, history.FieldSize)
|
||||
}
|
||||
@@ -3266,6 +3436,8 @@ func (m *HistoryMutation) AddedField(name string) (ent.Value, bool) {
|
||||
return m.AddedMediaID()
|
||||
case history.FieldEpisodeID:
|
||||
return m.AddedEpisodeID()
|
||||
case history.FieldSeasonNum:
|
||||
return m.AddedSeasonNum()
|
||||
case history.FieldSize:
|
||||
return m.AddedSize()
|
||||
case history.FieldDownloadClientID:
|
||||
@@ -3295,6 +3467,13 @@ func (m *HistoryMutation) AddField(name string, value ent.Value) error {
|
||||
}
|
||||
m.AddEpisodeID(v)
|
||||
return nil
|
||||
case history.FieldSeasonNum:
|
||||
v, ok := value.(int)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.AddSeasonNum(v)
|
||||
return nil
|
||||
case history.FieldSize:
|
||||
v, ok := value.(int)
|
||||
if !ok {
|
||||
@@ -3327,6 +3506,12 @@ func (m *HistoryMutation) ClearedFields() []string {
|
||||
if m.FieldCleared(history.FieldEpisodeID) {
|
||||
fields = append(fields, history.FieldEpisodeID)
|
||||
}
|
||||
if m.FieldCleared(history.FieldEpisodeNums) {
|
||||
fields = append(fields, history.FieldEpisodeNums)
|
||||
}
|
||||
if m.FieldCleared(history.FieldSeasonNum) {
|
||||
fields = append(fields, history.FieldSeasonNum)
|
||||
}
|
||||
if m.FieldCleared(history.FieldDownloadClientID) {
|
||||
fields = append(fields, history.FieldDownloadClientID)
|
||||
}
|
||||
@@ -3356,6 +3541,12 @@ func (m *HistoryMutation) ClearField(name string) error {
|
||||
case history.FieldEpisodeID:
|
||||
m.ClearEpisodeID()
|
||||
return nil
|
||||
case history.FieldEpisodeNums:
|
||||
m.ClearEpisodeNums()
|
||||
return nil
|
||||
case history.FieldSeasonNum:
|
||||
m.ClearSeasonNum()
|
||||
return nil
|
||||
case history.FieldDownloadClientID:
|
||||
m.ClearDownloadClientID()
|
||||
return nil
|
||||
@@ -3382,6 +3573,12 @@ func (m *HistoryMutation) ResetField(name string) error {
|
||||
case history.FieldEpisodeID:
|
||||
m.ResetEpisodeID()
|
||||
return nil
|
||||
case history.FieldEpisodeNums:
|
||||
m.ResetEpisodeNums()
|
||||
return nil
|
||||
case history.FieldSeasonNum:
|
||||
m.ResetSeasonNum()
|
||||
return nil
|
||||
case history.FieldSourceTitle:
|
||||
m.ResetSourceTitle()
|
||||
return nil
|
||||
@@ -4918,6 +5115,8 @@ type MediaMutation struct {
|
||||
download_history_episodes *bool
|
||||
limiter *schema.MediaLimiter
|
||||
extras *schema.MediaExtras
|
||||
alternative_titles *[]schema.AlternativeTilte
|
||||
appendalternative_titles []schema.AlternativeTilte
|
||||
clearedFields map[string]struct{}
|
||||
episodes map[int]struct{}
|
||||
removedepisodes map[int]struct{}
|
||||
@@ -5684,6 +5883,71 @@ func (m *MediaMutation) ResetExtras() {
|
||||
delete(m.clearedFields, media.FieldExtras)
|
||||
}
|
||||
|
||||
// SetAlternativeTitles sets the "alternative_titles" field.
|
||||
func (m *MediaMutation) SetAlternativeTitles(st []schema.AlternativeTilte) {
|
||||
m.alternative_titles = &st
|
||||
m.appendalternative_titles = nil
|
||||
}
|
||||
|
||||
// AlternativeTitles returns the value of the "alternative_titles" field in the mutation.
|
||||
func (m *MediaMutation) AlternativeTitles() (r []schema.AlternativeTilte, exists bool) {
|
||||
v := m.alternative_titles
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldAlternativeTitles returns the old "alternative_titles" field's value of the Media entity.
|
||||
// If the Media object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *MediaMutation) OldAlternativeTitles(ctx context.Context) (v []schema.AlternativeTilte, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldAlternativeTitles is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldAlternativeTitles requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldAlternativeTitles: %w", err)
|
||||
}
|
||||
return oldValue.AlternativeTitles, nil
|
||||
}
|
||||
|
||||
// AppendAlternativeTitles adds st to the "alternative_titles" field.
|
||||
func (m *MediaMutation) AppendAlternativeTitles(st []schema.AlternativeTilte) {
|
||||
m.appendalternative_titles = append(m.appendalternative_titles, st...)
|
||||
}
|
||||
|
||||
// AppendedAlternativeTitles returns the list of values that were appended to the "alternative_titles" field in this mutation.
|
||||
func (m *MediaMutation) AppendedAlternativeTitles() ([]schema.AlternativeTilte, bool) {
|
||||
if len(m.appendalternative_titles) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
return m.appendalternative_titles, true
|
||||
}
|
||||
|
||||
// ClearAlternativeTitles clears the value of the "alternative_titles" field.
|
||||
func (m *MediaMutation) ClearAlternativeTitles() {
|
||||
m.alternative_titles = nil
|
||||
m.appendalternative_titles = nil
|
||||
m.clearedFields[media.FieldAlternativeTitles] = struct{}{}
|
||||
}
|
||||
|
||||
// AlternativeTitlesCleared returns if the "alternative_titles" field was cleared in this mutation.
|
||||
func (m *MediaMutation) AlternativeTitlesCleared() bool {
|
||||
_, ok := m.clearedFields[media.FieldAlternativeTitles]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ResetAlternativeTitles resets all changes to the "alternative_titles" field.
|
||||
func (m *MediaMutation) ResetAlternativeTitles() {
|
||||
m.alternative_titles = nil
|
||||
m.appendalternative_titles = nil
|
||||
delete(m.clearedFields, media.FieldAlternativeTitles)
|
||||
}
|
||||
|
||||
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by ids.
|
||||
func (m *MediaMutation) AddEpisodeIDs(ids ...int) {
|
||||
if m.episodes == nil {
|
||||
@@ -5772,7 +6036,7 @@ func (m *MediaMutation) Type() string {
|
||||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *MediaMutation) Fields() []string {
|
||||
fields := make([]string, 0, 15)
|
||||
fields := make([]string, 0, 16)
|
||||
if m.tmdb_id != nil {
|
||||
fields = append(fields, media.FieldTmdbID)
|
||||
}
|
||||
@@ -5818,6 +6082,9 @@ func (m *MediaMutation) Fields() []string {
|
||||
if m.extras != nil {
|
||||
fields = append(fields, media.FieldExtras)
|
||||
}
|
||||
if m.alternative_titles != nil {
|
||||
fields = append(fields, media.FieldAlternativeTitles)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -5856,6 +6123,8 @@ func (m *MediaMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.Limiter()
|
||||
case media.FieldExtras:
|
||||
return m.Extras()
|
||||
case media.FieldAlternativeTitles:
|
||||
return m.AlternativeTitles()
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -5895,6 +6164,8 @@ func (m *MediaMutation) OldField(ctx context.Context, name string) (ent.Value, e
|
||||
return m.OldLimiter(ctx)
|
||||
case media.FieldExtras:
|
||||
return m.OldExtras(ctx)
|
||||
case media.FieldAlternativeTitles:
|
||||
return m.OldAlternativeTitles(ctx)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown Media field %s", name)
|
||||
}
|
||||
@@ -6009,6 +6280,13 @@ func (m *MediaMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetExtras(v)
|
||||
return nil
|
||||
case media.FieldAlternativeTitles:
|
||||
v, ok := value.([]schema.AlternativeTilte)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetAlternativeTitles(v)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown Media field %s", name)
|
||||
}
|
||||
@@ -6084,6 +6362,9 @@ func (m *MediaMutation) ClearedFields() []string {
|
||||
if m.FieldCleared(media.FieldExtras) {
|
||||
fields = append(fields, media.FieldExtras)
|
||||
}
|
||||
if m.FieldCleared(media.FieldAlternativeTitles) {
|
||||
fields = append(fields, media.FieldAlternativeTitles)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -6116,6 +6397,9 @@ func (m *MediaMutation) ClearField(name string) error {
|
||||
case media.FieldExtras:
|
||||
m.ClearExtras()
|
||||
return nil
|
||||
case media.FieldAlternativeTitles:
|
||||
m.ClearAlternativeTitles()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown Media nullable field %s", name)
|
||||
}
|
||||
@@ -6169,6 +6453,9 @@ func (m *MediaMutation) ResetField(name string) error {
|
||||
case media.FieldExtras:
|
||||
m.ResetExtras()
|
||||
return nil
|
||||
case media.FieldAlternativeTitles:
|
||||
m.ResetAlternativeTitles()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown Media field %s", name)
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func init() {
|
||||
historyFields := schema.History{}.Fields()
|
||||
_ = historyFields
|
||||
// historyDescSize is the schema descriptor for size field.
|
||||
historyDescSize := historyFields[5].Descriptor()
|
||||
historyDescSize := historyFields[7].Descriptor()
|
||||
// history.DefaultSize holds the default value on creation for the size field.
|
||||
history.DefaultSize = historyDescSize.Default.(int)
|
||||
indexersFields := schema.Indexers{}.Fields()
|
||||
|
||||
@@ -14,7 +14,9 @@ type History struct {
|
||||
func (History) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.Int("media_id"),
|
||||
field.Int("episode_id").Optional(),
|
||||
field.Int("episode_id").Optional().Comment("deprecated"),
|
||||
field.Ints("episode_nums").Optional(),
|
||||
field.Int("season_num").Optional(),
|
||||
field.String("source_title"),
|
||||
field.Time("date"),
|
||||
field.String("target_dir"),
|
||||
|
||||
@@ -31,6 +31,7 @@ func (Media) Fields() []ent.Field {
|
||||
field.Bool("download_history_episodes").Optional().Default(false).Comment("tv series only"),
|
||||
field.JSON("limiter", MediaLimiter{}).Optional(),
|
||||
field.JSON("extras", MediaExtras{}).Optional(),
|
||||
field.JSON("alternative_titles", []AlternativeTilte{}).Optional(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,9 +42,16 @@ func (Media) Edges() []ent.Edge {
|
||||
}
|
||||
}
|
||||
|
||||
type AlternativeTilte struct {
|
||||
Iso3166_1 string `json:"iso_3166_1"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type MediaLimiter struct {
|
||||
SizeMin int `json:"size_min"` //in B
|
||||
SizeMax int `json:"size_max"` //in B
|
||||
SizeMin int64 `json:"size_min"` //in B
|
||||
SizeMax int64 `json:"size_max"` //in B
|
||||
PreferSize int64 `json:"prefer_max"`
|
||||
}
|
||||
|
||||
type MediaExtras struct {
|
||||
@@ -51,7 +59,7 @@ type MediaExtras struct {
|
||||
JavId string `json:"javid"`
|
||||
//OriginCountry []string `json:"origin_country"`
|
||||
OriginalLanguage string `json:"original_language"`
|
||||
Genres []struct {
|
||||
Genres []struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"genres"`
|
||||
|
||||
@@ -14,7 +14,7 @@ type Storage struct {
|
||||
func (Storage) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.String("name").Unique(),
|
||||
field.Enum("implementation").Values("webdav", "local"),
|
||||
field.Enum("implementation").Values("webdav", "local", "alist"),
|
||||
field.String("tv_path").Optional(),
|
||||
field.String("movie_path").Optional(),
|
||||
field.String("settings").Optional(),
|
||||
|
||||
@@ -67,6 +67,7 @@ type Implementation string
|
||||
const (
|
||||
ImplementationWebdav Implementation = "webdav"
|
||||
ImplementationLocal Implementation = "local"
|
||||
ImplementationAlist Implementation = "alist"
|
||||
)
|
||||
|
||||
func (i Implementation) String() string {
|
||||
@@ -76,7 +77,7 @@ func (i Implementation) String() string {
|
||||
// ImplementationValidator is a validator for the "implementation" field enum values. It is called by the builders before save.
|
||||
func ImplementationValidator(i Implementation) error {
|
||||
switch i {
|
||||
case ImplementationWebdav, ImplementationLocal:
|
||||
case ImplementationWebdav, ImplementationLocal, ImplementationAlist:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("storage: invalid enum value for implementation field: %q", i)
|
||||
|
||||
38
go.mod
38
go.mod
@@ -10,7 +10,7 @@ require (
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/robfig/cron v1.2.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/net v0.27.0
|
||||
golang.org/x/net v0.33.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -21,9 +21,16 @@ require (
|
||||
github.com/ncruces/go-sqlite3 v0.18.4
|
||||
github.com/nikoksr/notify v1.0.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golift.io/starr v1.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.115.0 // indirect
|
||||
cloud.google.com/go/ai v0.8.0 // indirect
|
||||
cloud.google.com/go/auth v0.7.1 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.5.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.10 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect
|
||||
github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca // indirect
|
||||
@@ -32,8 +39,17 @@ require (
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/blinkbean/dingtalk v1.1.3 // indirect
|
||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
|
||||
github.com/go-test/deep v1.0.4 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/generative-ai-go v0.19.0 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.5 // indirect
|
||||
github.com/gregdel/pushover v1.3.1 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||
@@ -46,7 +62,19 @@ require (
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/tetratelabs/wazero v1.8.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/api v0.188.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect
|
||||
google.golang.org/grpc v1.65.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
lukechampine.com/blake3 v1.1.6 // indirect
|
||||
@@ -94,11 +122,11 @@ require (
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/zclconf/go-cty v1.8.0 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.27.0
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/sys v0.25.0
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
golang.org/x/sys v0.28.0
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
116
go.sum
116
go.sum
@@ -2,6 +2,19 @@ ariga.io/atlas v0.19.1-0.20240203083654-5948b60a8e43 h1:GwdJbXydHCYPedeeLt4x/lrl
|
||||
ariga.io/atlas v0.19.1-0.20240203083654-5948b60a8e43/go.mod h1:uj3pm+hUTVN/X5yfdBexHlZv+1Xu5u5ZbZx7+CDavNU=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
|
||||
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
|
||||
cloud.google.com/go/ai v0.8.0 h1:rXUEz8Wp2OlrM8r1bfmpF2+VKqc1VJpafE3HgzRnD/w=
|
||||
cloud.google.com/go/ai v0.8.0/go.mod h1:t3Dfk4cM61sytiggo2UyGsDVW3RF1qGZaUKDrZFyqkE=
|
||||
cloud.google.com/go/auth v0.7.1 h1:Iv1bbpzJ2OIg16m94XI9/tlzZZl3cdeR3nGVGj78N7s=
|
||||
cloud.google.com/go/auth v0.7.1/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
|
||||
cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg=
|
||||
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
|
||||
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
|
||||
cloud.google.com/go/longrunning v0.5.10 h1:eB/BniENNRKhjz/xgiillrdcH3G74TGSl3BXinGlI7E=
|
||||
cloud.google.com/go/longrunning v0.5.10/go.mod h1:tljz5guTr5oc/qhlUjBlk7UAIFMOGuPNxkNDZXlLics=
|
||||
crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk=
|
||||
crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
|
||||
entgo.io/ent v0.13.1 h1:uD8QwN1h6SNphdCCzmkMN3feSUzNnVvV/WIkHKMbzOE=
|
||||
@@ -68,12 +81,14 @@ github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cyruzin/golang-tmdb v1.6.3 h1:TKK9h+uuwiDOaFlsVispG1KxqhsSM5Y4ZELnUF3GlqU=
|
||||
github.com/cyruzin/golang-tmdb v1.6.3/go.mod h1:ZSryJLCcY+9TiKU+LbouXKns++YBrM8Tizannr05c+I=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -86,6 +101,12 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
@@ -111,6 +132,11 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
@@ -135,6 +161,8 @@ github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@@ -145,19 +173,34 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/generative-ai-go v0.19.0 h1:R71szggh8wHMCUlEMsW2A/3T+5LdEIkiaHSYgSpUgdg=
|
||||
github.com/google/generative-ai-go v0.19.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA=
|
||||
github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
@@ -216,8 +259,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.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
@@ -251,8 +292,6 @@ github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt
|
||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/nikoksr/notify v1.0.0 h1:qe9/6FRsWdxBgQgWcpvQ0sv8LRGJZDpRB4TkL2uNdO8=
|
||||
github.com/nikoksr/notify v1.0.0/go.mod h1:hPaaDt30d6LAA7/5nb0e48Bp/MctDfycCSs8VEgN29I=
|
||||
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/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
@@ -275,6 +314,7 @@ github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3O
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
@@ -310,8 +350,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=
|
||||
@@ -356,6 +394,18 @@ github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUA
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
@@ -367,9 +417,10 @@ golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
|
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
@@ -389,18 +440,22 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -409,19 +464,21 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -429,8 +486,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -442,36 +499,58 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golift.io/starr v1.0.0 h1:IDSaSL+ZYxdLT/Lg//dg/iwZ39LHO3D5CmbLCOgSXbI=
|
||||
golift.io/starr v1.0.0/go.mod h1:xnUwp4vK62bDvozW9QHUYc08m6kjwaZnGw3Db65fQHw=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw=
|
||||
google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d h1:/hmn0Ku5kWij/kjGsrcJeC1T/MrJi2iNWwgAqrihFwc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
@@ -497,6 +576,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c=
|
||||
lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
||||
196
pkg/alist/alist.go
Normal file
196
pkg/alist/alist.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package alist
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Resposne[T any] struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data T `json:"data"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Username string
|
||||
Password string
|
||||
URL string
|
||||
}
|
||||
|
||||
func New(cfg *Config) *Client {
|
||||
cfg.URL = strings.Trim(cfg.URL, "/")
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
http: http.DefaultClient,
|
||||
}
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
cfg *Config
|
||||
http *http.Client
|
||||
token string
|
||||
}
|
||||
|
||||
func (c *Client) Login() (string, error) {
|
||||
p := map[string]string{
|
||||
"username": c.cfg.Username,
|
||||
"password": c.cfg.Password,
|
||||
}
|
||||
data, _ := json.Marshal(p)
|
||||
resp, err := c.http.Post(c.cfg.URL+loginUrl, "application/json", bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "login")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
d1, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "read body")
|
||||
}
|
||||
var rp Resposne[map[string]string]
|
||||
|
||||
err = json.Unmarshal(d1, &rp)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "json")
|
||||
}
|
||||
if rp.Code != 200 {
|
||||
return "", errors.Errorf("alist error: code %d, %s", rp.Code, rp.Message)
|
||||
}
|
||||
c.token = rp.Data["token"]
|
||||
return c.token, nil
|
||||
}
|
||||
|
||||
type LsInfo struct {
|
||||
Content []struct {
|
||||
Name string `json:"name"`
|
||||
Size int `json:"size"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
Modified time.Time `json:"modified"`
|
||||
Created time.Time `json:"created"`
|
||||
Sign string `json:"sign"`
|
||||
Thumb string `json:"thumb"`
|
||||
Type int `json:"type"`
|
||||
Hashinfo string `json:"hashinfo"`
|
||||
HashInfo any `json:"hash_info"`
|
||||
} `json:"content"`
|
||||
Total int `json:"total"`
|
||||
Readme string `json:"readme"`
|
||||
Header string `json:"header"`
|
||||
Write bool `json:"write"`
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
|
||||
func (c *Client) Ls(dir string) (*LsInfo, error) {
|
||||
in := map[string]string{
|
||||
"path": dir,
|
||||
}
|
||||
|
||||
resp, err := c.post(c.cfg.URL+lsUrl, in)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "http")
|
||||
}
|
||||
|
||||
var out Resposne[LsInfo]
|
||||
err = json.Unmarshal(resp, &out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if out.Code != 200 {
|
||||
return nil, errors.Errorf("alist error: code %d, %s", out.Code, out.Message)
|
||||
}
|
||||
return &out.Data, nil
|
||||
}
|
||||
|
||||
func (c *Client) Mkdir(dir string) error {
|
||||
in := map[string]string{
|
||||
"path": dir,
|
||||
}
|
||||
resp, err := c.post(c.cfg.URL+mkdirUrl, in)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "http")
|
||||
}
|
||||
var out Resposne[any]
|
||||
err = json.Unmarshal(resp, &out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if out.Code != 200 {
|
||||
return errors.Errorf("alist error: code %d, %s", out.Code, out.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) post(url string, body interface{}) ([]byte, error) {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "new request")
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", c.token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "http")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
d1, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "read body")
|
||||
}
|
||||
return d1, nil
|
||||
}
|
||||
|
||||
type UploadStreamResponse struct {
|
||||
Task struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
State int `json:"state"`
|
||||
Status string `json:"status"`
|
||||
Progress int `json:"progress"`
|
||||
Error string `json:"error"`
|
||||
} `json:"task"`
|
||||
}
|
||||
|
||||
func (c *Client) UploadStream(reader io.Reader, size int64, toDir string) (*UploadStreamResponse, error) {
|
||||
req, err := http.NewRequest(http.MethodPut, c.cfg.URL+streamUploadUrl, reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", c.token)
|
||||
req.Header.Add("File-Path", url.PathEscape(toDir))
|
||||
req.Header.Add("As-Task", "true")
|
||||
req.Header.Add("Content-Type", "application/octet-stream")
|
||||
req.ContentLength = size
|
||||
res, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
d1, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var out Resposne[UploadStreamResponse]
|
||||
err = json.Unmarshal(d1, &out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if out.Code != 200 {
|
||||
return nil, errors.Errorf("alist error: code %d, %s", out.Code, out.Message)
|
||||
}
|
||||
|
||||
return &out.Data, nil
|
||||
}
|
||||
46
pkg/alist/alist_test.go
Normal file
46
pkg/alist/alist_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package alist
|
||||
|
||||
import (
|
||||
"os"
|
||||
"polaris/log"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
c := New(&Config{
|
||||
URL: "http://10.0.0.8:5244/",
|
||||
Username: "",
|
||||
Password: "",
|
||||
})
|
||||
cre, err := c.Login()
|
||||
if err != nil {
|
||||
log.Errorf("login fail: %v", err)
|
||||
t.Fail()
|
||||
} else {
|
||||
log.Errorf("login success: %s", cre)
|
||||
}
|
||||
info, err := c.Ls("/aliyun")
|
||||
if err != nil {
|
||||
log.Errorf("ls fail: %v", err)
|
||||
t.Fail()
|
||||
} else {
|
||||
log.Infof("ls results: %+v", info)
|
||||
}
|
||||
|
||||
f, err := os.Open("/Users/simonding/Downloads/Steam Link_1.3.9_APKPure.apk")
|
||||
if err != nil {
|
||||
log.Errorf("openfile: %v", err)
|
||||
t.Fail()
|
||||
} else {
|
||||
defer f.Close()
|
||||
ss, _ := f.Stat()
|
||||
log.Infof("upload file size %d", ss.Size())
|
||||
info, err := c.UploadStream(f, ss.Size(), "/aliyun/Steam Link_1.3.9_APKPure.apk")
|
||||
if err != nil {
|
||||
log.Errorf("upload error: %v", err)
|
||||
t.Fail()
|
||||
} else {
|
||||
log.Infof("upload success: %+v", info)
|
||||
}
|
||||
}
|
||||
}
|
||||
8
pkg/alist/url.go
Normal file
8
pkg/alist/url.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package alist
|
||||
|
||||
const (
|
||||
loginUrl = "/api/auth/login"
|
||||
lsUrl = "/api/fs/list"
|
||||
mkdirUrl = "/api/fs/mkdir"
|
||||
streamUploadUrl = "/api/fs/put"
|
||||
)
|
||||
266
pkg/gemini/gemini.go
Normal file
266
pkg/gemini/gemini.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"polaris/log"
|
||||
"strings"
|
||||
|
||||
"github.com/google/generative-ai-go/genai"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
func NewClient(apiKey, modelName string) (*Client, error) {
|
||||
ctx := context.Background()
|
||||
client, err := genai.NewClient(ctx, option.WithAPIKey(apiKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{apiKey: apiKey, modelName: modelName, c: client}, nil
|
||||
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
apiKey string
|
||||
modelName string
|
||||
c *genai.Client
|
||||
}
|
||||
|
||||
type TvInfo struct {
|
||||
TitleEnglish string `json:"title_english"`
|
||||
TitleChinses string `json:"title_chinese"`
|
||||
Season int `json:"season"`
|
||||
StartEpisode int `json:"start_episode"`
|
||||
EndEpisode int `json:"end_episode"`
|
||||
Resolution string `json:"resolution"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
ReleaseGroup string `json:"release_group"`
|
||||
Year int `json:"year"`
|
||||
AudioLanguage string `json:"audio_language"`
|
||||
IsCompleteSeason bool `json:"is_complete_season"`
|
||||
}
|
||||
|
||||
func (c *Client) ParseTvInfo(q string) (*TvInfo, error) {
|
||||
log.Info(q)
|
||||
ctx := context.Background()
|
||||
|
||||
model := c.c.GenerativeModel(c.modelName)
|
||||
|
||||
model.ResponseMIMEType = "application/json"
|
||||
model.ResponseSchema = &genai.Schema{
|
||||
Type: genai.TypeObject,
|
||||
Properties: map[string]*genai.Schema{
|
||||
"title_english": {Type: genai.TypeString},
|
||||
"title_chinese": {Type: genai.TypeString},
|
||||
"season": {Type: genai.TypeInteger, Description: "season number"},
|
||||
"start_episode": {Type: genai.TypeInteger},
|
||||
"end_episode": {Type: genai.TypeInteger},
|
||||
//"episodes": {Type: genai.TypeString},
|
||||
"resolution": {Type: genai.TypeString},
|
||||
"subtitle": {Type: genai.TypeString},
|
||||
"release_group": {Type: genai.TypeString},
|
||||
"year": {Type: genai.TypeInteger},
|
||||
"audio_language": {Type: genai.TypeString},
|
||||
"is_complete_season": {Type: genai.TypeBoolean},
|
||||
},
|
||||
Required: []string{"title_english", "title_chinese", "season", "start_episode", "resolution"},
|
||||
}
|
||||
|
||||
resp, err := model.GenerateContent(ctx, genai.Text(q))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, part := range resp.Candidates[0].Content.Parts {
|
||||
if txt, ok := part.(genai.Text); ok {
|
||||
var info TvInfo
|
||||
if err := json.Unmarshal([]byte(txt), &info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("not found")
|
||||
|
||||
}
|
||||
|
||||
type MovieInfo struct {
|
||||
TitleEnglish string `json:"title_english"`
|
||||
TitleChinses string `json:"title_chinese"`
|
||||
Resolution string `json:"resolution"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
ReleaseGroup string `json:"release_group"`
|
||||
Year int `json:"year"`
|
||||
AudioLanguage string `json:"audio_language"`
|
||||
}
|
||||
|
||||
func (c *Client) ParseMovieInfo(q string) (*MovieInfo, error) {
|
||||
log.Info(q)
|
||||
ctx := context.Background()
|
||||
|
||||
model := c.c.GenerativeModel(c.modelName)
|
||||
|
||||
model.ResponseMIMEType = "application/json"
|
||||
model.ResponseSchema = &genai.Schema{
|
||||
Type: genai.TypeObject,
|
||||
Properties: map[string]*genai.Schema{
|
||||
"title_english": {Type: genai.TypeString},
|
||||
"title_chinese": {Type: genai.TypeString},
|
||||
"resolution": {Type: genai.TypeString},
|
||||
"subtitle": {Type: genai.TypeString},
|
||||
"release_group": {Type: genai.TypeString},
|
||||
"year": {Type: genai.TypeInteger},
|
||||
"audio_language": {Type: genai.TypeString},
|
||||
},
|
||||
Required: []string{"title_english", "title_chinese", "resolution"},
|
||||
}
|
||||
|
||||
resp, err := model.GenerateContent(ctx, genai.Text(q))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, part := range resp.Candidates[0].Content.Parts {
|
||||
if txt, ok := part.(genai.Text); ok {
|
||||
var info MovieInfo
|
||||
if err := json.Unmarshal([]byte(txt), &info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("not found")
|
||||
|
||||
}
|
||||
|
||||
func (c *Client) isTvSeries(q string) (bool, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
model := c.c.GenerativeModel(c.modelName)
|
||||
|
||||
model.ResponseMIMEType = "application/json"
|
||||
model.ResponseSchema = &genai.Schema{
|
||||
Type: genai.TypeBoolean, Nullable: true, Description: "whether the input text implies a tv series",
|
||||
}
|
||||
|
||||
resp, err := model.GenerateContent(ctx, genai.Text(q))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, part := range resp.Candidates[0].Content.Parts {
|
||||
if txt, ok := part.(genai.Text); ok {
|
||||
return strings.ToLower(string(txt)) == "true", nil
|
||||
}
|
||||
}
|
||||
return false, fmt.Errorf("error")
|
||||
}
|
||||
|
||||
func (c *Client) ImpliesSameTvOrMovie(torrentName, mediaName string) bool {
|
||||
ctx := context.Background()
|
||||
|
||||
model := c.c.GenerativeModel(c.modelName)
|
||||
|
||||
model.ResponseMIMEType = "application/json"
|
||||
model.ResponseSchema = &genai.Schema{
|
||||
Type: genai.TypeBoolean, Nullable: true,
|
||||
}
|
||||
q := fmt.Sprintf("whether this file name \"%s\" implies the same TV series or movie with name \"%s\"?", torrentName, mediaName)
|
||||
resp, err := model.GenerateContent(ctx, genai.Text(q))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, part := range resp.Candidates[0].Content.Parts {
|
||||
if txt, ok := part.(genai.Text); ok {
|
||||
return strings.ToLower(string(txt)) == "true"
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
func (c *Client) FilterTvOrMovies(resourcesNames []string, titles ...string) ([]string, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
model := c.c.GenerativeModel(c.modelName)
|
||||
|
||||
model.ResponseMIMEType = "application/json"
|
||||
model.ResponseSchema = &genai.Schema{
|
||||
Type: genai.TypeArray,
|
||||
Items: &genai.Schema{Type: genai.TypeString},
|
||||
}
|
||||
for i, s := range titles {
|
||||
titles[i] = "\"" + s + "\""
|
||||
}
|
||||
p := &bytes.Buffer{}
|
||||
p.WriteString(`the following list of file names, list all of which implies the same TV series or movie of name`)
|
||||
p.WriteString(strings.Join(titles, " or "))
|
||||
p.WriteString(":\n")
|
||||
|
||||
for _, r := range resourcesNames {
|
||||
p.WriteString(" * ")
|
||||
p.WriteString(r)
|
||||
p.WriteString("\n")
|
||||
}
|
||||
log.Debugf("FilterTvOrMovies prompt is %s", p.String())
|
||||
|
||||
resp, err := model.GenerateContent(ctx, genai.Text(p.String()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, part := range resp.Candidates[0].Content.Parts {
|
||||
if txt, ok := part.(genai.Text); ok {
|
||||
|
||||
var s []string
|
||||
if err := json.Unmarshal([]byte(txt), &s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("nothing found")
|
||||
|
||||
}
|
||||
|
||||
|
||||
func (c *Client) FilterMovies(resourcesNames []string, year int, titles ...string) ([]string, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
model := c.c.GenerativeModel(c.modelName)
|
||||
|
||||
model.ResponseMIMEType = "application/json"
|
||||
model.ResponseSchema = &genai.Schema{
|
||||
Type: genai.TypeArray,
|
||||
Items: &genai.Schema{Type: genai.TypeString},
|
||||
}
|
||||
for i, s := range titles {
|
||||
titles[i] = "\"" + s + "\""
|
||||
}
|
||||
p := &bytes.Buffer{}
|
||||
p.WriteString( fmt.Sprint("the following list of file names, list all of which match following criteria: 1. Is movie 2. Released in year %d 3. Have name of ", year))
|
||||
p.WriteString(strings.Join(titles, " or "))
|
||||
p.WriteString(":\n")
|
||||
|
||||
for _, r := range resourcesNames {
|
||||
p.WriteString(" * ")
|
||||
p.WriteString(r)
|
||||
p.WriteString("\n")
|
||||
}
|
||||
log.Debugf("FilterTvOrMovies prompt is %s", p.String())
|
||||
|
||||
resp, err := model.GenerateContent(ctx, genai.Text(p.String()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, part := range resp.Candidates[0].Content.Parts {
|
||||
if txt, ok := part.(genai.Text); ok {
|
||||
|
||||
var s []string
|
||||
if err := json.Unmarshal([]byte(txt), &s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("nothing found")
|
||||
|
||||
}
|
||||
8
pkg/gemini/genimi_test.go
Normal file
8
pkg/gemini/genimi_test.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_any1(t *testing.T) {
|
||||
}
|
||||
@@ -37,6 +37,7 @@ func ParseDoulist(doulistUrl string) (*importlist.Response, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var items []importlist.Item
|
||||
doc.Find("div[class=doulist-item]").Each(func(i int, selection *goquery.Selection) {
|
||||
titleDiv := selection.Find("div[class=title]")
|
||||
link := titleDiv.Find("div>a")
|
||||
@@ -64,18 +65,26 @@ func ParseDoulist(doulistUrl string) (*importlist.Response, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
_, err := parseDetailPage(strings.TrimSpace(href))
|
||||
if err != nil {
|
||||
log.Errorf("get detail page: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
item := importlist.Item{
|
||||
Title: strings.TrimSpace(link.Text()),
|
||||
Year: year,
|
||||
}
|
||||
items = append(items, item)
|
||||
_ = item
|
||||
println(link.Text(), href)
|
||||
//println(link.Text(), href)
|
||||
})
|
||||
return nil, nil
|
||||
|
||||
return &importlist.Response{Items: items}, nil
|
||||
}
|
||||
|
||||
func parseDetailPage(url string) (string, error) {
|
||||
println(url)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -95,6 +104,14 @@ func parseDetailPage(url string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
doc.Find("div[class='subject clearfix']").Each(func(i int, se *goquery.Selection) {
|
||||
println(se.Text())
|
||||
se.Children().Get(1)
|
||||
imdb := se.Find("div[class='info']").First().Children().Last()
|
||||
println(imdb.Text())
|
||||
})
|
||||
|
||||
_ = doc
|
||||
return "", nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@ import (
|
||||
)
|
||||
|
||||
func TestParseDoulist(t *testing.T) {
|
||||
r, err := ParseDoulist("https://www.douban.com/doulist/166422/")
|
||||
r, err := ParseDoulist("https://www.douban.com/doulist/81580/")
|
||||
log.Info(r, err)
|
||||
}
|
||||
|
||||
@@ -7,242 +7,104 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Metadata struct {
|
||||
type Info struct {
|
||||
NameEn string
|
||||
NameCn string
|
||||
Year int
|
||||
Season int
|
||||
Episode int
|
||||
StartEpisode int
|
||||
EndEpisode int
|
||||
Resolution string
|
||||
IsSeasonPack bool
|
||||
}
|
||||
|
||||
func (m *Metadata) IsAcceptable(names... string) bool {
|
||||
func (m *Info) ParseExtraDescription(desc string) {
|
||||
if m.IsSeasonPack { //try to parse episode number with description
|
||||
mm := ParseTv(desc)
|
||||
if mm.StartEpisode > 0 { //sometimes they put episode info in desc text
|
||||
m.IsSeasonPack = false
|
||||
m.StartEpisode = mm.StartEpisode
|
||||
m.EndEpisode = mm.EndEpisode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Info) IsAcceptable(names ...string) bool {
|
||||
re := regexp.MustCompile(`[^\p{L}\w\s]`)
|
||||
|
||||
nameCN := re.ReplaceAllString(strings.ToLower(m.NameCn), " ")
|
||||
nameEN := re.ReplaceAllString(strings.ToLower(m.NameEn), " ")
|
||||
nameCN = strings.Join(strings.Fields(nameCN), " ")
|
||||
nameEN = strings.Join(strings.Fields(nameEN), " ")
|
||||
|
||||
for _, name := range names {
|
||||
re := regexp.MustCompile(`[^\p{L}\w\s]`)
|
||||
name = re.ReplaceAllString(strings.ToLower(name), " ")
|
||||
nameCN := re.ReplaceAllString(strings.ToLower(m.NameCn), " ")
|
||||
nameEN := re.ReplaceAllString(strings.ToLower(m.NameEn), " ")
|
||||
name = strings.Join(strings.Fields(name), " ")
|
||||
nameCN = strings.Join(strings.Fields(nameCN), " ")
|
||||
nameEN = strings.Join(strings.Fields(nameEN), " ")
|
||||
if utils.IsASCII(name) { //ascii name should match words
|
||||
re := regexp.MustCompile(`\b` + name + `\b`)
|
||||
return re.MatchString(nameCN) || re.MatchString(nameEN)
|
||||
if re.MatchString(nameCN) || re.MatchString(nameEN) {
|
||||
return true
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(nameCN, name) || strings.Contains(nameEN, name) {
|
||||
if strings.Contains(nameCN, name) || strings.Contains(nameEN, name) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func ParseTv(name string) *Metadata {
|
||||
func ParseTv(name string) *Info {
|
||||
name = strings.ToLower(name)
|
||||
name = strings.ReplaceAll(name, "\u200b", "") //remove unicode hidden character
|
||||
if utils.ContainsChineseChar(name) {
|
||||
return parseChineseName(name)
|
||||
}
|
||||
return parseEnglishName(name)
|
||||
|
||||
return parseName(name)
|
||||
}
|
||||
|
||||
func parseEnglishName(name string) *Metadata {
|
||||
re := regexp.MustCompile(`[^\p{L}\w\s]`)
|
||||
name = re.ReplaceAllString(strings.ToLower(name), " ")
|
||||
newSplits := strings.Split(strings.TrimSpace(name), " ")
|
||||
|
||||
seasonRe := regexp.MustCompile(`^s\d{1,2}`)
|
||||
resRe := regexp.MustCompile(`^\d{3,4}p`)
|
||||
episodeRe := regexp.MustCompile(`e\d{1,3}`)
|
||||
|
||||
var seasonIndex = -1
|
||||
var episodeIndex = -1
|
||||
var resIndex = -1
|
||||
for i, p := range newSplits {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
if seasonRe.MatchString(p) {
|
||||
//season part
|
||||
seasonIndex = i
|
||||
} else if resRe.MatchString(p) {
|
||||
resIndex = i
|
||||
}
|
||||
if i >= seasonIndex && episodeRe.MatchString(p) {
|
||||
episodeIndex = i
|
||||
func adjacentNumber(s string, start int) (n1 int, l int) {
|
||||
runes := []rune(s)
|
||||
if start > len(runes)-1 { //out of bound
|
||||
return -1, -1
|
||||
}
|
||||
var n []rune
|
||||
for i := start; i < len(runes); i++ {
|
||||
k := runes[i]
|
||||
if (k < '0' || k > '9') && !chineseNum[k] { //not digit anymore
|
||||
break
|
||||
}
|
||||
n = append(n, k)
|
||||
}
|
||||
|
||||
meta := &Metadata{
|
||||
Season: -1,
|
||||
Episode: -1,
|
||||
if len(n) == 0 {
|
||||
return -1, -1
|
||||
}
|
||||
if seasonIndex != -1 {
|
||||
//season exists
|
||||
ss := seasonRe.FindAllString(newSplits[seasonIndex], -1)
|
||||
if len(ss) != 0 {
|
||||
//season info
|
||||
|
||||
ssNum := strings.TrimLeft(ss[0], "s")
|
||||
n, err := strconv.Atoi(ssNum)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("convert %s error: %v", ssNum, err))
|
||||
}
|
||||
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
|
||||
|
||||
}
|
||||
m, err := strconv.Atoi(string(n))
|
||||
if err != nil {
|
||||
return chinese2Num[string(n)], len(n)
|
||||
}
|
||||
if episodeIndex != -1 {
|
||||
ep := episodeRe.FindAllString(newSplits[episodeIndex], -1)
|
||||
if len(ep) > 0 {
|
||||
//episode info exists
|
||||
epNum := strings.TrimLeft(ep[0], "e")
|
||||
n, err := strconv.Atoi(epNum)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
|
||||
}
|
||||
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 {
|
||||
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
|
||||
return m, len(n)
|
||||
}
|
||||
|
||||
func parseChineseName(name string) *Metadata {
|
||||
var meta = parseEnglishName(name)
|
||||
if meta.Season != -1 && (meta.Episode != -1 || meta.IsSeasonPack) {
|
||||
return meta
|
||||
}
|
||||
meta = &Metadata{Season: 1}
|
||||
//season pack
|
||||
packRe := regexp.MustCompile(`(\d{1,2}-\d{1,2})|(全集)`)
|
||||
if packRe.MatchString(name) {
|
||||
meta.IsSeasonPack = true
|
||||
}
|
||||
//resolution
|
||||
resRe := regexp.MustCompile(`\d{3,4}p`)
|
||||
resMatches := resRe.FindAllString(name, -1)
|
||||
if len(resMatches) != 0 {
|
||||
meta.Resolution = resMatches[0]
|
||||
} else {
|
||||
if strings.Contains(name, "720") {
|
||||
meta.Resolution = "720p"
|
||||
} else if strings.Contains(name, "1080") {
|
||||
meta.Resolution = "1080p"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//episode number
|
||||
re1 := regexp.MustCompile(`\[\d{1,3}\]`)
|
||||
episodeMatches1 := re1.FindAllString(name, -1)
|
||||
if len(episodeMatches1) > 0 { //[11] [1080p]
|
||||
epNum := strings.TrimRight(strings.TrimLeft(episodeMatches1[0], "["), "]")
|
||||
n, err := strconv.Atoi(epNum)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
|
||||
}
|
||||
meta.Episode = n
|
||||
} else { //【第09話】
|
||||
re2 := regexp.MustCompile(`第\d{1,4}(话|話|集)`)
|
||||
episodeMatches1 := re2.FindAllString(name, -1)
|
||||
if len(episodeMatches1) > 0 {
|
||||
re := regexp.MustCompile(`\d{1,4}`)
|
||||
epNum := re.FindAllString(episodeMatches1[0], -1)[0]
|
||||
n, err := strconv.Atoi(epNum)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
|
||||
}
|
||||
meta.Episode = n
|
||||
} else { //SHY 靦腆英雄 / Shy -05 ( CR 1920x1080 AVC AAC MKV)
|
||||
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]
|
||||
n, err := strconv.Atoi(epNum)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
|
||||
}
|
||||
meta.Episode = n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func findSeason(s string) (n int, p int) {
|
||||
//season numner
|
||||
seasonRe1 := regexp.MustCompile(`s\d{1,2}`)
|
||||
seasonMatches := seasonRe1.FindAllString(name, -1)
|
||||
seasonMatches := seasonRe1.FindAllString(s, -1)
|
||||
if len(seasonMatches) > 0 {
|
||||
seNum := seasonMatches[0][1:]
|
||||
n, err := strconv.Atoi(seNum)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("convert %s error: %v", seNum, err))
|
||||
}
|
||||
meta.Season = n
|
||||
|
||||
return n, strings.Index(s, seNum)
|
||||
} else {
|
||||
seasonRe1 := regexp.MustCompile(`season \d{1,2}`)
|
||||
seasonMatches := seasonRe1.FindAllString(name, -1)
|
||||
seasonMatches := seasonRe1.FindAllString(s, -1)
|
||||
if len(seasonMatches) > 0 {
|
||||
re3 := regexp.MustCompile(`\d{1,2}`)
|
||||
seNum := re3.FindAllString(seasonMatches[0], -1)[0]
|
||||
@@ -250,10 +112,10 @@ func parseChineseName(name string) *Metadata {
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("convert %s error: %v", seNum, err))
|
||||
}
|
||||
meta.Season = n
|
||||
return n, strings.Index(s, seasonMatches[0])
|
||||
} else {
|
||||
seasonRe1 := regexp.MustCompile(`第.{1,2}季`)
|
||||
seasonMatches := seasonRe1.FindAllString(name, -1)
|
||||
seasonMatches := seasonRe1.FindAllString(s, -1)
|
||||
if len(seasonMatches) > 0 {
|
||||
m1 := []rune(seasonMatches[0])
|
||||
seNum := m1[1 : len(m1)-1]
|
||||
@@ -262,58 +124,364 @@ func parseChineseName(name string) *Metadata {
|
||||
log.Warnf("parse season number %v error: %v, try to parse using chinese", seNum, err)
|
||||
n = chinese2Num[string(seNum)]
|
||||
}
|
||||
meta.Season = n
|
||||
return n, strings.Index(s, seasonMatches[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
func findEpisodes(s string) (start int, end int) {
|
||||
var episodeCn = map[rune]bool{
|
||||
'话': true,
|
||||
'話': true,
|
||||
'集': true,
|
||||
}
|
||||
|
||||
rr := []rune(s)
|
||||
for i := 0; i < len(rr); i++ {
|
||||
r := rr[i]
|
||||
if r == 'e' {
|
||||
n, l := adjacentNumber(s, i+1)
|
||||
|
||||
if n > 0 {
|
||||
foundDash := false
|
||||
for j := i + l + 1; j < len(rr); j++ {
|
||||
r1 := rr[j]
|
||||
if r1 == '-' {
|
||||
foundDash = true
|
||||
continue
|
||||
}
|
||||
if r1 == ' ' || r1 == 'e' {
|
||||
continue
|
||||
}
|
||||
|
||||
if foundDash {
|
||||
if r1 == 's' {
|
||||
s1, l1 := adjacentNumber(s, j+1)
|
||||
if s1 > 0 { //S01E01-S01E21
|
||||
n1, _ := adjacentNumber(s, j+l1+2)
|
||||
if n1 > 0 {
|
||||
return n, n1
|
||||
}
|
||||
}
|
||||
}
|
||||
n1, _ := adjacentNumber(s, j)
|
||||
if n1 > 0 {
|
||||
return n, n1
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return n, n
|
||||
}
|
||||
} else if r == '第' {
|
||||
n, l := adjacentNumber(s, i+1)
|
||||
if len(rr) > i+l+1 && episodeCn[rr[i+l+1]] {
|
||||
return n, n
|
||||
} else if len(rr) > i+l+1 {
|
||||
if rr[i+l+1] == '-' {
|
||||
n1, l1 := adjacentNumber(s, i+l+2)
|
||||
if episodeCn[rr[i+l+2+l1]] {
|
||||
return n, n1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
//episode number
|
||||
re1 := regexp.MustCompile(`\[\d{1,4}\]`)
|
||||
episodeMatches1 := re1.FindAllString(s, -1)
|
||||
if len(episodeMatches1) > 0 { //[11] [1080p], [2022][113][HEVC][GB][4K]
|
||||
for _, m := range episodeMatches1 {
|
||||
epNum := strings.TrimRight(strings.TrimLeft(m, "["), "]")
|
||||
n, err := strconv.Atoi(epNum)
|
||||
if err != nil {
|
||||
log.Debugf("convert %s error: %v", epNum, err)
|
||||
continue
|
||||
}
|
||||
nowYear := time.Now().Year()
|
||||
if n > nowYear-50 { //high possibility is year number
|
||||
continue
|
||||
}
|
||||
return n, n
|
||||
}
|
||||
} else { //【第09話】
|
||||
re2 := regexp.MustCompile(`第\d{1,4}([话話集])`)
|
||||
episodeMatches1 := re2.FindAllString(s, -1)
|
||||
if len(episodeMatches1) > 0 {
|
||||
re := regexp.MustCompile(`\d{1,4}`)
|
||||
epNum := re.FindAllString(episodeMatches1[0], -1)[0]
|
||||
n, err := strconv.Atoi(epNum)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
|
||||
}
|
||||
return n, n
|
||||
} else { //The Road Season 2 Episode 12 XviD-AFG
|
||||
re3 := regexp.MustCompile(`episode \d{1,4}`)
|
||||
epNums := re3.FindAllString(s, -1)
|
||||
if len(epNums) > 0 {
|
||||
re3 := regexp.MustCompile(`\d{1,4}`)
|
||||
epNum := re3.FindAllString(epNums[0], -1)[0]
|
||||
n, err := strconv.Atoi(epNum)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
|
||||
}
|
||||
return n, n
|
||||
|
||||
} else { //SHY 靦腆英雄 / Shy -05 ( CR 1920x1080 AVC AAC MKV)
|
||||
if maybeSeasonPack(s) { //avoid miss match, season pack not use this rule
|
||||
return -1, -1
|
||||
}
|
||||
re3 := regexp.MustCompile(`[^(season)][^\d\w]\d{1,2}[^\d\w]`)
|
||||
epNums := re3.FindAllString(s, -1)
|
||||
if len(epNums) > 0 {
|
||||
|
||||
re3 := regexp.MustCompile(`\d{1,2}`)
|
||||
epNum := re3.FindAllString(epNums[0], -1)[0]
|
||||
n, err := strconv.Atoi(epNum)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
|
||||
}
|
||||
return n, n
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if meta.IsSeasonPack && meta.Episode != 0 {
|
||||
meta.Season = meta.Episode
|
||||
meta.Episode = -1
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
func matchResolution(s string) string {
|
||||
//resolution
|
||||
resRe := regexp.MustCompile(`\d{3,4}p`)
|
||||
resMatches := resRe.FindAllString(s, -1)
|
||||
if len(resMatches) != 0 {
|
||||
return resMatches[0]
|
||||
} else {
|
||||
if strings.Contains(s, "720") {
|
||||
return "720p"
|
||||
} else if strings.Contains(s, "1080") {
|
||||
return "1080p"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func maybeSeasonPack(s string) bool {
|
||||
//season pack
|
||||
packRe := regexp.MustCompile(`((\d{1,2}-\d{1,2}))|(complete)|(全集)`)
|
||||
if packRe.MatchString(s) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//func parseEnglishName(name string) *Info {
|
||||
// meta := &Info{
|
||||
// //Season: -1,
|
||||
// Episode: -1,
|
||||
// }
|
||||
//
|
||||
// start, end := findEpisodes(name)
|
||||
// if start > 0 && end > 0 {
|
||||
// meta.Episode = start
|
||||
// }
|
||||
//
|
||||
// re := regexp.MustCompile(`[^\p{L}\w\s]`)
|
||||
// name = re.ReplaceAllString(strings.ToLower(name), " ")
|
||||
// newSplits := strings.Split(strings.TrimSpace(name), " ")
|
||||
//
|
||||
// seasonRe := regexp.MustCompile(`^s\d{1,2}`)
|
||||
// resRe := regexp.MustCompile(`^\d{3,4}p`)
|
||||
// episodeRe := regexp.MustCompile(`e\d{1,3}`)
|
||||
//
|
||||
// var seasonIndex = -1
|
||||
// var episodeIndex = -1
|
||||
// var resIndex = -1
|
||||
// for i, p := range newSplits {
|
||||
// p = strings.TrimSpace(p)
|
||||
// if p == "" {
|
||||
// continue
|
||||
// }
|
||||
// if seasonRe.MatchString(p) {
|
||||
// //season part
|
||||
// seasonIndex = i
|
||||
// } else if resRe.MatchString(p) {
|
||||
// resIndex = i
|
||||
// }
|
||||
// if i >= seasonIndex && episodeRe.MatchString(p) {
|
||||
// episodeIndex = i
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if seasonIndex != -1 {
|
||||
// //season exists
|
||||
// ss := seasonRe.FindAllString(newSplits[seasonIndex], -1)
|
||||
// if len(ss) != 0 {
|
||||
// //season info
|
||||
//
|
||||
// ssNum := strings.TrimLeft(ss[0], "s")
|
||||
// n, err := strconv.Atoi(ssNum)
|
||||
// if err != nil {
|
||||
// panic(fmt.Sprintf("convert %s error: %v", ssNum, err))
|
||||
// }
|
||||
// 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)
|
||||
// //if len(ep) > 0 {
|
||||
// // //episode info exists
|
||||
// // epNum := strings.TrimLeft(ep[0], "e")
|
||||
// // n, err := strconv.Atoi(epNum)
|
||||
// // if err != nil {
|
||||
// // panic(fmt.Sprintf("convert %s error: %v", epNum, err))
|
||||
// // }
|
||||
// // 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 {
|
||||
// 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
|
||||
//}
|
||||
|
||||
func parseName(name string) *Info {
|
||||
meta := &Info{Season: 1}
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return meta
|
||||
}
|
||||
|
||||
season, p := findSeason(name)
|
||||
if season == -1 {
|
||||
log.Debugf("not find season info: %s", name)
|
||||
if !utils.IsASCII(name) {
|
||||
season = 1
|
||||
}
|
||||
p = len(name) - 1
|
||||
}
|
||||
meta.Season = season
|
||||
|
||||
start, end := findEpisodes(name)
|
||||
if start > 0 && end > 0 {
|
||||
meta.StartEpisode = start
|
||||
meta.EndEpisode = end
|
||||
} else {
|
||||
meta.IsSeasonPack = true
|
||||
}
|
||||
|
||||
meta.Resolution = matchResolution(name)
|
||||
|
||||
//if meta.IsSeasonPack && meta.Episode != 0 {
|
||||
// meta.Season = meta.Episode
|
||||
// meta.Episode = -1
|
||||
//}
|
||||
|
||||
//tv name
|
||||
|
||||
fields := strings.FieldsFunc(name, func(r rune) bool {
|
||||
return r == '[' || r == ']' || r == '【' || r == '】'
|
||||
})
|
||||
titleCn := ""
|
||||
title := ""
|
||||
for _, p := range fields { //寻找匹配的最长的字符串,最有可能是名字
|
||||
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)), "") //去除标点符号
|
||||
titleCn = re.ReplaceAllString(strings.TrimSpace(strings.ToLower(titleCn)), "")
|
||||
|
||||
meta.NameCn = titleCn
|
||||
cnRe := regexp.MustCompile(`\p{Han}.*\p{Han}`)
|
||||
cnmatches := cnRe.FindAllString(titleCn, -1)
|
||||
|
||||
//titleCn中最长的中文字符
|
||||
if len(cnmatches) > 0 {
|
||||
for _, t := range cnmatches {
|
||||
if len([]rune(t)) > len([]rune(meta.NameCn)) {
|
||||
meta.NameCn = strings.ToLower(t)
|
||||
if utils.IsASCII(name) && p < len(name) && p-1 > 0 {
|
||||
meta.NameEn = strings.TrimSpace(name[:p-1])
|
||||
meta.NameCn = meta.NameEn
|
||||
} else {
|
||||
fields := strings.FieldsFunc(name, func(r rune) bool {
|
||||
return r == '[' || r == ']' || r == '【' || r == '】'
|
||||
})
|
||||
titleCn := ""
|
||||
title := ""
|
||||
for _, p := range fields { //寻找匹配的最长的字符串,最有可能是名字
|
||||
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)), "") //去除标点符号
|
||||
titleCn = re.ReplaceAllString(strings.TrimSpace(strings.ToLower(titleCn)), "")
|
||||
|
||||
//匹配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.TrimSpace(strings.ToLower(t))
|
||||
meta.NameCn = titleCn
|
||||
cnRe := regexp.MustCompile(`\p{Han}.*\p{Han}`)
|
||||
cnmatches := cnRe.FindAllString(titleCn, -1)
|
||||
|
||||
//titleCn中最长的中文字符
|
||||
if len(cnmatches) > 0 {
|
||||
for _, t := range cnmatches {
|
||||
if len([]rune(t)) > len([]rune(meta.NameCn)) {
|
||||
meta.NameCn = strings.ToLower(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
meta.NameEn = title
|
||||
|
||||
////匹配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.TrimSpace(strings.ToLower(t))
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
}
|
||||
|
||||
return meta
|
||||
@@ -330,3 +498,15 @@ var chinese2Num = map[string]int{
|
||||
"八": 8,
|
||||
"九": 9,
|
||||
}
|
||||
|
||||
var chineseNum = map[rune]bool{
|
||||
'一': true,
|
||||
'二': true,
|
||||
'三': true,
|
||||
'四': true,
|
||||
'五': true,
|
||||
'六': true,
|
||||
'七': true,
|
||||
'八': true,
|
||||
'九': true,
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ func Test_ParseTV2(t *testing.T) {
|
||||
m := ParseTv(s1)
|
||||
log.Infof("results: %+v", m)
|
||||
assert.Equal(t, m.Season, 1)
|
||||
assert.Equal(t, m.Episode, 4)
|
||||
assert.Equal(t, m.StartEpisode, 4)
|
||||
assert.Equal(t, m.IsSeasonPack, false)
|
||||
assert.Equal(t, m.Resolution, "1080p")
|
||||
}
|
||||
@@ -31,7 +31,7 @@ func Test_ParseTV3(t *testing.T) {
|
||||
m := ParseTv(s1)
|
||||
log.Infof("results: %+v", m)
|
||||
assert.Equal(t, m.Season, 37)
|
||||
assert.Equal(t, m.Episode, 219)
|
||||
assert.Equal(t, m.StartEpisode, 219)
|
||||
assert.Equal(t, m.IsSeasonPack, false)
|
||||
//assert.Equal(t, m.Resolution, "1080p")
|
||||
}
|
||||
@@ -41,8 +41,8 @@ func Test_ParseTV4(t *testing.T) {
|
||||
m := ParseTv(s1)
|
||||
log.Infof("results: %+v", m)
|
||||
assert.Equal(t, m.Season, 2)
|
||||
//assert.Equal(t, m.Episode, 219)
|
||||
assert.Equal(t, m.IsSeasonPack, true)
|
||||
assert.Equal(t, m.StartEpisode, 12)
|
||||
assert.Equal(t, m.IsSeasonPack, false)
|
||||
//assert.Equal(t, m.Resolution, "1080p")
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func Test_ParseTV6(t *testing.T) {
|
||||
m := ParseTv(s1)
|
||||
log.Infof("results: %+v", m)
|
||||
assert.Equal(t, m.Season, 1)
|
||||
assert.Equal(t, m.Episode, 3)
|
||||
assert.Equal(t, m.StartEpisode, 3)
|
||||
assert.Equal(t, m.IsSeasonPack, false)
|
||||
assert.Equal(t, m.Resolution, "1080p")
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func Test_ParseTV7(t *testing.T) {
|
||||
m := ParseTv(s1)
|
||||
log.Infof("results: %+v", m)
|
||||
assert.Equal(t, m.Season, 1)
|
||||
assert.Equal(t, m.Episode, 1113)
|
||||
assert.Equal(t, m.StartEpisode, 1113)
|
||||
assert.Equal(t, m.IsSeasonPack, false)
|
||||
assert.Equal(t, m.Resolution, "1080p")
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func Test_ParseTV8(t *testing.T) {
|
||||
m := ParseTv(s1)
|
||||
log.Infof("results: %+v", m)
|
||||
assert.Equal(t, m.Season, 1)
|
||||
assert.Equal(t, m.Episode, 4)
|
||||
assert.Equal(t, m.StartEpisode, 4)
|
||||
assert.Equal(t, m.IsSeasonPack, false)
|
||||
assert.Equal(t, m.Resolution, "1080p")
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func Test_ParseTV9(t *testing.T) {
|
||||
m := ParseTv(s1)
|
||||
log.Infof("results: %+v", m)
|
||||
assert.Equal(t, m.Season, 1)
|
||||
assert.Equal(t, m.Episode, 16)
|
||||
assert.Equal(t, m.StartEpisode, 16)
|
||||
assert.Equal(t, m.IsSeasonPack, false)
|
||||
assert.Equal(t, m.Resolution, "1080p")
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func Test_ParseTV11(t *testing.T) {
|
||||
m := ParseTv(s1)
|
||||
log.Infof("results: %+v", m)
|
||||
assert.Equal(t, 2, m.Season)
|
||||
assert.Equal(t, 4, m.Episode)
|
||||
assert.Equal(t, 4, m.StartEpisode)
|
||||
assert.Equal(t, false, m.IsSeasonPack)
|
||||
assert.Equal(t, "1080p", m.Resolution)
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func Test_ParseTV12(t *testing.T) {
|
||||
m := ParseTv(s1)
|
||||
log.Infof("results: %+v", m)
|
||||
assert.Equal(t, 2, m.Season)
|
||||
assert.Equal(t, 4, m.Episode)
|
||||
assert.Equal(t, 4, m.StartEpisode)
|
||||
assert.Equal(t, false, m.IsSeasonPack)
|
||||
assert.Equal(t, "1080p", m.Resolution)
|
||||
}
|
||||
@@ -131,7 +131,7 @@ func Test_ParseTV13(t *testing.T) {
|
||||
m := ParseTv(s1)
|
||||
log.Infof("results: %+v", m)
|
||||
assert.Equal(t, 2, m.Season)
|
||||
assert.Equal(t, 8, m.Episode)
|
||||
assert.Equal(t, 8, m.StartEpisode)
|
||||
assert.Equal(t, false, m.IsSeasonPack)
|
||||
assert.Equal(t, "1080p", m.Resolution)
|
||||
}
|
||||
@@ -141,10 +141,11 @@ func Test_ParseTV14(t *testing.T) {
|
||||
m := ParseTv(s1)
|
||||
log.Infof("results: %+v", m)
|
||||
assert.Equal(t, 5, m.Season)
|
||||
assert.Equal(t, 113, m.Episode)
|
||||
assert.Equal(t, 113, m.StartEpisode)
|
||||
assert.Equal(t, false, m.IsSeasonPack)
|
||||
//assert.Equal(t, "720p", m.Resolution)
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func Test_ParseTV15(t *testing.T) {
|
||||
@@ -157,4 +158,58 @@ func Test_ParseTV15(t *testing.T) {
|
||||
//assert.Equal(t, 113, m.Episode)
|
||||
//assert.Equal(t, false, m.IsSeasonPack)
|
||||
//assert.Equal(t, "720p", m.Resolution)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ParseTV16(t *testing.T) {
|
||||
s1 := "Romance in the Alley 2024 S01 E24-E25 1080p WEB-DL H.264 AAC-PTerWEB"
|
||||
m := ParseTv(s1)
|
||||
log.Infof("results: %+v", m)
|
||||
|
||||
assert.Equal(t, 1, m.Season)
|
||||
assert.Equal(t, 24, m.StartEpisode)
|
||||
assert.Equal(t, 25, m.EndEpisode)
|
||||
//assert.Equal(t, false, m.IsSeasonPack)
|
||||
//assert.Equal(t, "720p", m.Resolution)
|
||||
}
|
||||
|
||||
func Test_ParseTV17(t *testing.T) {
|
||||
s1 := "小巷人家/Romance in the Alley 第24-25集 "
|
||||
m := ParseTv(s1)
|
||||
log.Infof("results: %+v", m)
|
||||
|
||||
assert.Equal(t, 1, m.Season)
|
||||
assert.Equal(t, 24, m.StartEpisode)
|
||||
assert.Equal(t, 25, m.EndEpisode)
|
||||
//assert.Equal(t, false, m.IsSeasonPack)
|
||||
//assert.Equal(t, "720p", m.Resolution)
|
||||
}
|
||||
|
||||
// Romance in the Alley 2024 S01E01-S01E21 2160p WEB-DL HEVC AAC-UBWEB
|
||||
func Test_ParseTV18(t *testing.T) {
|
||||
s1 := "Romance in the Alley 2024 S01E01-S01E21 2160p WEB-DL HEVC AAC-UBWEB "
|
||||
m := ParseTv(s1)
|
||||
log.Infof("results: %+v", m)
|
||||
|
||||
assert.Equal(t, 1, m.Season)
|
||||
assert.Equal(t, 1, m.StartEpisode)
|
||||
assert.Equal(t, 21, m.EndEpisode)
|
||||
//assert.Equal(t, false, m.IsSeasonPack)
|
||||
//assert.Equal(t, "720p", m.Resolution)
|
||||
}
|
||||
|
||||
// The Day of the Jackal (Season 1) WEB-DL 1080p
|
||||
func Test_ParseTV19(t *testing.T) {
|
||||
s1 := "The Day of the Jackal (Season 1) WEB-DL 1080p "
|
||||
m := ParseTv(s1)
|
||||
log.Infof("results: %+v", m)
|
||||
|
||||
assert.Equal(t, 1, m.Season)
|
||||
assert.Equal(t, true, m.IsSeasonPack)
|
||||
//assert.Equal(t, "720p", m.Resolution)
|
||||
}
|
||||
|
||||
func Test_Name(t *testing.T) {
|
||||
m := Info{NameEn: "word кибердеревня новый год 2023 webrip 1080p"}
|
||||
b := m.IsAcceptable("word")
|
||||
assert.True(t, b)
|
||||
}
|
||||
|
||||
77
pkg/prowlarr/prowlarr.go
Normal file
77
pkg/prowlarr/prowlarr.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package prowlarr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"polaris/db"
|
||||
"polaris/ent"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golift.io/starr"
|
||||
"golift.io/starr/prowlarr"
|
||||
)
|
||||
|
||||
type ProwlarrSupportType string
|
||||
|
||||
const (
|
||||
TV ProwlarrSupportType = "tv"
|
||||
Movie ProwlarrSupportType = "movie"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
p *prowlarr.Prowlarr
|
||||
apiKey string
|
||||
url string
|
||||
}
|
||||
|
||||
func New(apiKey, url string) *Client {
|
||||
c := starr.New(apiKey, url, 10*time.Second)
|
||||
p := prowlarr.New(c)
|
||||
return &Client{p: p, apiKey: apiKey, url: url}
|
||||
}
|
||||
|
||||
func (c *Client) GetIndexers(t ProwlarrSupportType) ([]*db.TorznabInfo, error) {
|
||||
ins, err := c.p.GetIndexers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var indexers []*db.TorznabInfo
|
||||
for _, in := range ins {
|
||||
if !in.Enable {
|
||||
continue
|
||||
}
|
||||
if t == "tv" && len(in.Capabilities.TvSearchParams) == 0 { //no tv resource in this indexer
|
||||
continue
|
||||
} else if t == "movie" && len(in.Capabilities.MovieSearchParams) == 0 { //no movie resource in this indexer
|
||||
continue
|
||||
}
|
||||
seedRatio := 0.0
|
||||
for _, f := range in.Fields {
|
||||
if f.Name == "torrentBaseSettings.seedRatio" && f.Value != nil {
|
||||
if r, ok := f.Value.(float64); ok {
|
||||
seedRatio = r
|
||||
}
|
||||
}
|
||||
}
|
||||
setting := db.TorznabSetting{
|
||||
URL: fmt.Sprintf("%s/%d/api", strings.TrimSuffix(c.url, "/"), in.ID),
|
||||
ApiKey: c.apiKey,
|
||||
}
|
||||
data, _ := json.Marshal(&setting)
|
||||
|
||||
entIndexer := ent.Indexers{
|
||||
Name: in.Name,
|
||||
Implementation: "torznab",
|
||||
Priority: 128 - int(in.Priority),
|
||||
SeedRatio: float32(seedRatio),
|
||||
Settings: string(data),
|
||||
}
|
||||
|
||||
indexers = append(indexers, &db.TorznabInfo{
|
||||
Indexers: &entIndexer,
|
||||
TorznabSetting: setting,
|
||||
})
|
||||
}
|
||||
return indexers, nil
|
||||
}
|
||||
13
pkg/prowlarr/prowlarr_test.go
Normal file
13
pkg/prowlarr/prowlarr_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package prowlarr
|
||||
|
||||
import (
|
||||
"polaris/log"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test111(t *testing.T) {
|
||||
c := New("", "http://10.0.0.8:9696/")
|
||||
apis , err := c.GetIndexers("tv")
|
||||
log.Infof("errors: %v", err)
|
||||
log.Infof("indexers: %+v", apis[0])
|
||||
}
|
||||
78
pkg/storage/alist.go
Normal file
78
pkg/storage/alist.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"polaris/pkg/alist"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
)
|
||||
|
||||
func NewAlist(cfg *alist.Config, dir string, videoFormats []string, subtitleFormats []string) (*Alist, error) {
|
||||
cl := alist.New(cfg)
|
||||
_, err := cl.Login()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Alist{baseDir: dir, cfg: cfg, client: cl, videoFormats: videoFormats, subtitleFormats: subtitleFormats}, nil
|
||||
}
|
||||
|
||||
type Alist struct {
|
||||
baseDir string
|
||||
cfg *alist.Config
|
||||
client *alist.Client
|
||||
progresser func() float64
|
||||
videoFormats []string
|
||||
subtitleFormats []string
|
||||
}
|
||||
|
||||
func (a *Alist) Move(src, dest string) error {
|
||||
if err := a.Copy(src, dest); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.RemoveAll(src)
|
||||
}
|
||||
|
||||
func (a *Alist) Copy(src, dest string) error {
|
||||
b, err := NewBase(src, a.videoFormats, a.subtitleFormats)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.progresser = b.Progress
|
||||
|
||||
uploadFunc := func(destPath string, destInfo fs.FileInfo, srcReader io.Reader, mimeType *mimetype.MIME) error {
|
||||
_, err := a.client.UploadStream(srcReader, destInfo.Size(), destPath)
|
||||
return err
|
||||
}
|
||||
mkdirFunc := func(dir string) error {
|
||||
return a.client.Mkdir(dir)
|
||||
}
|
||||
|
||||
baseDest := filepath.Join(a.baseDir, dest)
|
||||
return b.Upload(baseDest, false, false, false, uploadFunc, mkdirFunc)
|
||||
}
|
||||
|
||||
func (a *Alist) ReadDir(dir string) ([]fs.FileInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (a *Alist) ReadFile(s string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (a *Alist) WriteFile(s string, bytes []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Alist) UploadProgress() float64 {
|
||||
if a.progresser == nil {
|
||||
return 0
|
||||
}
|
||||
return a.progresser()
|
||||
}
|
||||
|
||||
func (a *Alist) RemoveAll(path string) error {
|
||||
return nil
|
||||
}
|
||||
182
pkg/storage/base.go
Normal file
182
pkg/storage/base.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"polaris/log"
|
||||
"polaris/pkg/utils"
|
||||
"strings"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Storage interface {
|
||||
Move(src, dest string) error
|
||||
Copy(src, dest string) error
|
||||
ReadDir(dir string) ([]fs.FileInfo, error)
|
||||
ReadFile(string) ([]byte, error)
|
||||
WriteFile(string, []byte) error
|
||||
UploadProgress() float64
|
||||
RemoveAll(path string) error
|
||||
}
|
||||
|
||||
type uploadFunc func(destPath string, destInfo fs.FileInfo, srcReader io.Reader, mimeType *mimetype.MIME) error
|
||||
|
||||
type Base struct {
|
||||
src string
|
||||
videoFormats []string
|
||||
subtitleFormats []string
|
||||
totalSize int64
|
||||
uploadedSize int64
|
||||
}
|
||||
|
||||
func NewBase(src string, videoFormats []string, subtitleFormats []string) (*Base, error) {
|
||||
b := &Base{src: src, videoFormats: videoFormats, subtitleFormats: subtitleFormats}
|
||||
err := b.calculateSize()
|
||||
return b, err
|
||||
}
|
||||
|
||||
func (b *Base) checkVideoFilesExist() bool {
|
||||
if len(b.videoFormats) == 0 { // do not check
|
||||
return true
|
||||
}
|
||||
hasVideo := false
|
||||
filepath.Walk(b.src, func(path string, info fs.FileInfo, err error) error {
|
||||
ext := filepath.Ext(strings.ToLower(info.Name()))
|
||||
|
||||
for _, f := range b.videoFormats {
|
||||
if f == ext {
|
||||
hasVideo = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return hasVideo
|
||||
}
|
||||
|
||||
func (b *Base) isFileNeeded(name string) bool {
|
||||
ext := filepath.Ext(strings.ToLower(name))
|
||||
if len(b.videoFormats) == 0 {
|
||||
return true
|
||||
} else {
|
||||
for _, f := range b.videoFormats {
|
||||
if f == ext {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(b.subtitleFormats) > 0 {
|
||||
for _, f := range b.subtitleFormats {
|
||||
if f == ext {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
func (b *Base) Upload(destDir string, tryLink, detectMime, changeMediaHash bool, upload uploadFunc, mkdir func(string) error) error {
|
||||
if !b.checkVideoFilesExist() {
|
||||
return errors.Errorf("torrent has no video file(s)")
|
||||
}
|
||||
os.MkdirAll(destDir, os.ModePerm)
|
||||
|
||||
targetBase := filepath.Join(destDir, filepath.Base(b.src)) //文件的场景,要加上文件名, move filename ./dir/
|
||||
info, err := os.Stat(b.src)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read source dir")
|
||||
}
|
||||
if info.IsDir() { //如果是路径,则只移动路径里面的文件,不管当前路径, 行为类似 move dirname/* target_dir/
|
||||
targetBase = destDir
|
||||
}
|
||||
log.Debugf("local storage target base dir is: %v", targetBase)
|
||||
|
||||
err = filepath.Walk(b.src, func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(b.src, path)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "relation between %s and %s", b.src, path)
|
||||
}
|
||||
destName := filepath.Join(targetBase, rel)
|
||||
|
||||
if info.IsDir() {
|
||||
mkdir(destName)
|
||||
} else { //is file
|
||||
if !b.isFileNeeded(info.Name()) {
|
||||
log.Debugf("file is not needed, skip: %s", info.Name())
|
||||
return nil
|
||||
}
|
||||
if tryLink {
|
||||
if err := os.Link(path, destName); err == nil {
|
||||
return nil //link success
|
||||
}
|
||||
log.Warnf("hard link file error: %v, will try copy file, source: %s, dest: %s", err, path, destName)
|
||||
}
|
||||
if changeMediaHash {
|
||||
if err := utils.ChangeFileHash(path); err != nil {
|
||||
log.Errorf("change file %v hash error: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
if f, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm); err != nil {
|
||||
return errors.Wrapf(err, "read file %v", path)
|
||||
} else { //open success
|
||||
defer f.Close()
|
||||
var mtype *mimetype.MIME
|
||||
if detectMime {
|
||||
mtype, err = mimetype.DetectFile(path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "mime type error")
|
||||
}
|
||||
}
|
||||
return upload(destName, info, &progressReader{R: f, Add: func(i int) {
|
||||
b.uploadedSize += int64(i)
|
||||
}}, mtype)
|
||||
}
|
||||
|
||||
}
|
||||
log.Infof("file copy complete: %v", destName)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "move file error")
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (b *Base) calculateSize() error {
|
||||
var size int64
|
||||
err := filepath.Walk(b.src, func(_ string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
size += info.Size()
|
||||
}
|
||||
return err
|
||||
})
|
||||
b.totalSize = size
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *Base) Progress() float64 {
|
||||
return float64(b.uploadedSize) / float64(b.totalSize)
|
||||
}
|
||||
|
||||
type progressReader struct {
|
||||
R io.Reader
|
||||
Add func(int)
|
||||
}
|
||||
|
||||
func (pr *progressReader) Read(p []byte) (int, error) {
|
||||
n, err := pr.R.Read(p)
|
||||
pr.Add(n)
|
||||
return n, err
|
||||
}
|
||||
@@ -6,81 +6,45 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"polaris/log"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Storage interface {
|
||||
Move(src, dest string) error
|
||||
Copy(src, dest string) error
|
||||
ReadDir(dir string) ([]fs.FileInfo, error)
|
||||
ReadFile(string) ([]byte, error)
|
||||
WriteFile(string, []byte) error
|
||||
}
|
||||
|
||||
func NewLocalStorage(dir string) (*LocalStorage, error) {
|
||||
func NewLocalStorage(dir string, videoFormats []string, subtitleFormats []string) (*LocalStorage, error) {
|
||||
os.MkdirAll(dir, 0655)
|
||||
|
||||
return &LocalStorage{dir: dir}, nil
|
||||
return &LocalStorage{dir: dir, videoFormats: videoFormats, subtitleFormats: subtitleFormats}, nil
|
||||
}
|
||||
|
||||
type LocalStorage struct {
|
||||
dir string
|
||||
dir string
|
||||
videoFormats []string
|
||||
subtitleFormats []string
|
||||
}
|
||||
|
||||
func (l *LocalStorage) Copy(src, destDir string) error {
|
||||
os.MkdirAll(filepath.Join(l.dir, destDir), os.ModePerm)
|
||||
|
||||
targetBase := filepath.Join(l.dir, destDir, filepath.Base(src)) //文件的场景,要加上文件名, move filename ./dir/
|
||||
info, err := os.Stat(src)
|
||||
b, err := NewBase(src, l.videoFormats, l.subtitleFormats)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read source dir")
|
||||
return err
|
||||
}
|
||||
if info.IsDir() { //如果是路径,则只移动路径里面的文件,不管当前路径, 行为类似 move dirname/* target_dir/
|
||||
targetBase = filepath.Join(l.dir, destDir)
|
||||
}
|
||||
log.Debugf("local storage target base dir is: %v", targetBase)
|
||||
|
||||
err = filepath.Walk(src, func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(src, path)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "relation between %s and %s", src, path)
|
||||
}
|
||||
destName := filepath.Join(targetBase, rel)
|
||||
|
||||
if info.IsDir() {
|
||||
os.Mkdir(destName, os.ModePerm)
|
||||
} else { //is file
|
||||
if err := os.Link(path, destName); err != nil {
|
||||
log.Warnf("hard link file error: %v, will try copy file, source: %s, dest: %s", err, path, destName)
|
||||
if writer, err := os.OpenFile(destName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.ModePerm); err != nil {
|
||||
return errors.Wrapf(err, "create file %s", destName)
|
||||
} else {
|
||||
defer writer.Close()
|
||||
if f, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm); err != nil {
|
||||
return errors.Wrapf(err, "read file %v", path)
|
||||
} else { //open success
|
||||
defer f.Close()
|
||||
_, err := io.Copy(writer, f)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "transmitting data error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
baseDest := filepath.Join(l.dir, destDir)
|
||||
uploadFunc := func(destPath string, destInfo fs.FileInfo, srcReader io.Reader, mimeType *mimetype.MIME) error {
|
||||
if writer, err := os.OpenFile(destPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.ModePerm); err != nil {
|
||||
return errors.Wrapf(err, "create file %s", destPath)
|
||||
} else {
|
||||
defer writer.Close()
|
||||
_, err := io.Copy(writer, srcReader)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "transmitting data error")
|
||||
}
|
||||
|
||||
}
|
||||
log.Infof("file copy complete: %v", destName)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "move file error")
|
||||
}
|
||||
return nil
|
||||
return b.Upload(baseDest, true, false, false, uploadFunc, func(s string) error {
|
||||
return os.Mkdir(s, os.ModePerm)
|
||||
})
|
||||
}
|
||||
|
||||
func (l *LocalStorage) Move(src, destDir string) error {
|
||||
@@ -103,3 +67,11 @@ func (l *LocalStorage) WriteFile(name string, data []byte) error {
|
||||
os.MkdirAll(filepath.Dir(path), os.ModePerm)
|
||||
return os.WriteFile(path, data, os.ModePerm)
|
||||
}
|
||||
|
||||
func (l *LocalStorage) UploadProgress() float64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (l *LocalStorage) RemoveAll(path string) error {
|
||||
return os.RemoveAll(filepath.Join(l.dir, path))
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"polaris/log"
|
||||
"polaris/pkg/gowebdav"
|
||||
"polaris/pkg/utils"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/pkg/errors"
|
||||
@@ -17,9 +16,12 @@ type WebdavStorage struct {
|
||||
fs *gowebdav.Client
|
||||
dir string
|
||||
changeMediaHash bool
|
||||
progresser func() float64
|
||||
videoFormats []string
|
||||
subtitleFormats []string
|
||||
}
|
||||
|
||||
func NewWebdavStorage(url, user, password, path string, changeMediaHash bool) (*WebdavStorage, error) {
|
||||
func NewWebdavStorage(url, user, password, path string, changeMediaHash bool, videoFormats []string, subtitleFormats []string) (*WebdavStorage, error) {
|
||||
c := gowebdav.NewClient(url, user, password)
|
||||
if err := c.Connect(); err != nil {
|
||||
return nil, errors.Wrap(err, "connect webdav")
|
||||
@@ -27,71 +29,35 @@ func NewWebdavStorage(url, user, password, path string, changeMediaHash bool) (*
|
||||
return &WebdavStorage{
|
||||
fs: c,
|
||||
dir: path,
|
||||
videoFormats: videoFormats,
|
||||
subtitleFormats: subtitleFormats,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *WebdavStorage) Copy(local, remoteDir string) error {
|
||||
remoteBase := filepath.Join(w.dir, remoteDir, filepath.Base(local))
|
||||
info, err := os.Stat(local)
|
||||
b, err := NewBase(local, w.videoFormats, w.subtitleFormats)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read source dir")
|
||||
}
|
||||
if info.IsDir() { //如果是路径,则只移动路径里面的文件,不管当前路径, 行为类似 move dirname/* target_dir/
|
||||
remoteBase = filepath.Join(w.dir, remoteDir)
|
||||
return err
|
||||
}
|
||||
|
||||
//log.Infof("remove all content in %s", remoteBase)
|
||||
//w.fs.RemoveAll(remoteBase)
|
||||
err = filepath.Walk(local, func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "read file %v", path)
|
||||
w.progresser = b.Progress
|
||||
|
||||
uploadFunc := func(destPath string, destInfo fs.FileInfo, srcReader io.Reader, mtype *mimetype.MIME) error {
|
||||
callback := func(r *http.Request) {
|
||||
r.Header.Set("Content-Type", mtype.String())
|
||||
r.ContentLength = destInfo.Size()
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(local, path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "path relation")
|
||||
if err := w.fs.WriteStream(destPath, srcReader, 0666, callback); err != nil {
|
||||
return errors.Wrap(err, "transmitting data error")
|
||||
}
|
||||
remoteName := filepath.Join(remoteBase, rel)
|
||||
return nil
|
||||
|
||||
if info.IsDir() {
|
||||
log.Infof("skip dir %v, webdav will mkdir automatically", info.Name())
|
||||
}
|
||||
|
||||
// if err := w.fs.Mkdir(remoteName, 0666); err != nil {
|
||||
// return errors.Wrapf(err, "mkdir %v", remoteName)
|
||||
// }
|
||||
|
||||
} else { //is file
|
||||
if w.changeMediaHash {
|
||||
if err := utils.ChangeFileHash(path); err != nil {
|
||||
log.Errorf("change file %v hash error: %v", path, err)
|
||||
}
|
||||
}
|
||||
if f, err := os.OpenFile(path, os.O_RDONLY, 0666); err != nil {
|
||||
return errors.Wrapf(err, "read file %v", path)
|
||||
} else { //open success
|
||||
defer f.Close()
|
||||
mtype, err := mimetype.DetectFile(path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "mime type error")
|
||||
}
|
||||
|
||||
callback := func(r *http.Request) {
|
||||
r.Header.Set("Content-Type", mtype.String())
|
||||
r.ContentLength = info.Size()
|
||||
}
|
||||
|
||||
if err := w.fs.WriteStream(remoteName, f, 0666, callback); err != nil {
|
||||
return errors.Wrap(err, "transmitting data error")
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Infof("file copy complete: %v", remoteName)
|
||||
return b.Upload(filepath.Join(w.dir, remoteDir), false, true, w.changeMediaHash, uploadFunc, func(s string) error {
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "move file error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *WebdavStorage) Move(local, remoteDir string) error {
|
||||
@@ -112,3 +78,14 @@ func (w *WebdavStorage) ReadFile(name string) ([]byte, error) {
|
||||
func (w *WebdavStorage) WriteFile(name string, data []byte) error {
|
||||
return w.fs.Write(filepath.Join(w.dir, name), data, os.ModePerm)
|
||||
}
|
||||
|
||||
func (w *WebdavStorage) UploadProgress() float64 {
|
||||
if w.progresser == nil {
|
||||
return 0
|
||||
}
|
||||
return w.progresser()
|
||||
}
|
||||
|
||||
func (w *WebdavStorage) RemoveAll(path string) error {
|
||||
return w.fs.RemoveAll(filepath.Join(w.dir, path))
|
||||
}
|
||||
|
||||
@@ -85,14 +85,21 @@ func (r *Response) ToResults(indexer *db.TorznabInfo) []Result {
|
||||
// log.Warnf("converting link to magnet error, error: %v, link: %v", err, item.Link)
|
||||
// continue
|
||||
// }
|
||||
imdb := ""
|
||||
if item.GetAttr("imdbid") != "" {
|
||||
imdb = item.GetAttr("imdbid")
|
||||
} else if item.GetAttr("imdb") != "" {
|
||||
imdb = item.GetAttr("imdb")
|
||||
}
|
||||
r := Result{
|
||||
Name: item.Title,
|
||||
Description: item.Description,
|
||||
Link: item.Link,
|
||||
Size: mustAtoI(item.Size),
|
||||
Seeders: mustAtoI(item.GetAttr("seeders")),
|
||||
Peers: mustAtoI(item.GetAttr("peers")),
|
||||
Category: mustAtoI(item.GetAttr("category")),
|
||||
ImdbId: item.GetAttr("imdbid"),
|
||||
Seeders: int(mustAtoI(item.GetAttr("seeders"))),
|
||||
Peers: int(mustAtoI(item.GetAttr("peers"))),
|
||||
Category: int(mustAtoI(item.GetAttr("category"))),
|
||||
ImdbId: imdb,
|
||||
DownloadVolumeFactor: tryParseFloat(item.GetAttr("downloadvolumefactor")),
|
||||
UploadVolumeFactor: tryParseFloat(item.GetAttr("uploadvolumefactor")),
|
||||
Source: indexer.Name,
|
||||
@@ -105,8 +112,8 @@ func (r *Response) ToResults(indexer *db.TorznabInfo) []Result {
|
||||
return res
|
||||
}
|
||||
|
||||
func mustAtoI(key string) int {
|
||||
i, err := strconv.Atoi(key)
|
||||
func mustAtoI(key string) int64 {
|
||||
i, err := strconv.ParseInt(key, 10, 64)
|
||||
if err != nil {
|
||||
log.Errorf("must atoi error: %v", err)
|
||||
panic(err)
|
||||
@@ -124,7 +131,7 @@ func tryParseFloat(s string) float32 {
|
||||
}
|
||||
|
||||
func Search(indexer *db.TorznabInfo, keyWord string) ([]Result, error) {
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, indexer.URL, nil)
|
||||
@@ -174,8 +181,9 @@ func doRequest(req *http.Request) (*Response, error) {
|
||||
|
||||
type Result struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Link string `json:"link"`
|
||||
Size int `json:"size"`
|
||||
Size int64 `json:"size"`
|
||||
Seeders int `json:"seeders"`
|
||||
Peers int `json:"peers"`
|
||||
Category int `json:"category"`
|
||||
|
||||
86
pkg/uploader/uploader.go
Normal file
86
pkg/uploader/uploader.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package uploader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"polaris/pkg/utils"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type StreamWriter interface {
|
||||
WriteStream(path string, stream io.Reader, _ os.FileMode) error
|
||||
}
|
||||
|
||||
type Uploader struct {
|
||||
sw StreamWriter
|
||||
progress atomic.Int64
|
||||
dir string
|
||||
size int64
|
||||
}
|
||||
|
||||
func NewUploader(dir string, sw StreamWriter) (*Uploader, error) {
|
||||
size, err := utils.DirSize(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Uploader{sw: sw, dir: dir, size: size, progress: atomic.Int64{}}, nil
|
||||
}
|
||||
|
||||
func (u *Uploader) Upload() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ProgressReader struct {
|
||||
Reader io.Reader
|
||||
Progress atomic.Int64
|
||||
Size int64
|
||||
Name string
|
||||
Once bool
|
||||
Done atomic.Bool
|
||||
}
|
||||
|
||||
func (progressReader *ProgressReader) NewLoop() {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
var op int64
|
||||
for range ticker.C {
|
||||
p := progressReader.Progress.Load()
|
||||
KB := (p - op) / 1024
|
||||
var percent int64
|
||||
if progressReader.Size != 0 {
|
||||
percent = p * 100 / progressReader.Size
|
||||
} else {
|
||||
percent = 100
|
||||
}
|
||||
if KB < 1024 {
|
||||
fmt.Printf("%s: %dKB/s %d%%\n", progressReader.Name, KB, percent)
|
||||
} else {
|
||||
fmt.Printf("%s: %.2fMB/s %d%%\n", progressReader.Name, float64(KB)/1024, percent)
|
||||
}
|
||||
|
||||
if progressReader.Done.Load() {
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (progressReader *ProgressReader) Read(p []byte) (int, error) {
|
||||
n, err := progressReader.Reader.Read(p)
|
||||
progressReader.Progress.Add(int64(n))
|
||||
if !progressReader.Once {
|
||||
progressReader.Once = true
|
||||
go progressReader.NewLoop()
|
||||
}
|
||||
if err != nil {
|
||||
progressReader.Done.Store(true)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (progressReader *ProgressReader) Close() error {
|
||||
progressReader.Done.Store(true)
|
||||
return nil
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@@ -230,7 +231,7 @@ func Link2Magnet(link string) (string, error) {
|
||||
return http.ErrUseLastResponse //do not follow redirects
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
resp, err := client.Get(link)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "get link")
|
||||
@@ -252,7 +253,6 @@ func Link2Magnet(link string) (string, error) {
|
||||
return mg.String(), nil
|
||||
}
|
||||
|
||||
|
||||
func MagnetHash(link string) (string, error) {
|
||||
if mi, err := metainfo.ParseMagnetV2Uri(link); err != nil {
|
||||
return "", errors.Errorf("magnet link is not valid: %v", err)
|
||||
@@ -271,4 +271,18 @@ func MagnetHash(link string) (string, error) {
|
||||
}
|
||||
return hash, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func DirSize(path string) (int64, error) {
|
||||
var size int64
|
||||
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
size += info.Size()
|
||||
}
|
||||
return err
|
||||
})
|
||||
return size, err
|
||||
}
|
||||
|
||||
@@ -17,8 +17,9 @@ import (
|
||||
|
||||
type Activity struct {
|
||||
*ent.History
|
||||
Progress int `json:"progress"`
|
||||
SeedRatio float64 `json:"seed_ratio"`
|
||||
Progress int `json:"progress"`
|
||||
SeedRatio float64 `json:"seed_ratio"`
|
||||
UploadProgress float64 `json:"upload_progress"`
|
||||
}
|
||||
|
||||
func (s *Server) GetAllActivities(c *gin.Context) (interface{}, error) {
|
||||
@@ -44,6 +45,9 @@ func (s *Server) GetAllActivities(c *gin.Context) (interface{}, error) {
|
||||
} else {
|
||||
a.SeedRatio = r
|
||||
}
|
||||
if task.UploadProgresser != nil {
|
||||
a.UploadProgress = task.UploadProgresser()
|
||||
}
|
||||
}
|
||||
}
|
||||
activities = append(activities, a)
|
||||
|
||||
@@ -44,8 +44,9 @@ func (c *Client) registerCronJob(name string, cron string, f func() error) {
|
||||
}
|
||||
|
||||
func (c *Client) Init() {
|
||||
c.reloadTasks()
|
||||
go c.reloadTasks()
|
||||
c.addSysCron()
|
||||
go c.checkW500PosterOnStartup()
|
||||
}
|
||||
|
||||
func (c *Client) reloadTasks() {
|
||||
@@ -82,6 +83,7 @@ func (c *Client) reloadTasks() {
|
||||
}
|
||||
|
||||
}
|
||||
log.Infof("------ task reloading done ------")
|
||||
}
|
||||
|
||||
func (c *Client) GetDownloadClient() (pkg.Downloader, *ent.DownloadClients, error) {
|
||||
|
||||
2
server/core/fliters.go
Normal file
2
server/core/fliters.go
Normal file
@@ -0,0 +1,2 @@
|
||||
package core
|
||||
|
||||
@@ -114,8 +114,9 @@ type AddWatchlistIn struct {
|
||||
Resolution string `json:"resolution" binding:"required"`
|
||||
Folder string `json:"folder" binding:"required"`
|
||||
DownloadHistoryEpisodes bool `json:"download_history_episodes"` //for tv
|
||||
SizeMin int `json:"size_min"`
|
||||
SizeMax int `json:"size_max"`
|
||||
SizeMin int64 `json:"size_min"`
|
||||
SizeMax int64 `json:"size_max"`
|
||||
PreferSize int64 `json:"prefer_size"`
|
||||
}
|
||||
|
||||
func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
@@ -139,7 +140,7 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
}
|
||||
log.Infof("find detail for tv id %d: %+v", in.TmdbID, detail)
|
||||
|
||||
lastSeason := 0
|
||||
lastSeason := 0
|
||||
for _, season := range detail.Seasons {
|
||||
if season.SeasonNumber > lastSeason && season.EpisodeCount > 0 { //如果最新一季已经有剧集信息,则以最新一季为准
|
||||
lastSeason = season.SeasonNumber
|
||||
@@ -148,6 +149,11 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
|
||||
log.Debugf("latest season is %v", lastSeason)
|
||||
|
||||
alterTitles, err := c.getAlterTitles(in.TmdbID, media.MediaTypeTv)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get alter titles")
|
||||
}
|
||||
|
||||
var epIds []int
|
||||
for _, season := range detail.Seasons {
|
||||
seasonId := season.SeasonNumber
|
||||
@@ -194,6 +200,7 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
epIds = append(epIds, epid)
|
||||
}
|
||||
}
|
||||
|
||||
m := &ent.Media{
|
||||
TmdbID: int(detail.ID),
|
||||
ImdbID: detail.IMDbID,
|
||||
@@ -212,6 +219,7 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
OriginalLanguage: detail.OriginalLanguage,
|
||||
Genres: detail.Genres,
|
||||
},
|
||||
AlternativeTitles: alterTitles,
|
||||
}
|
||||
|
||||
r, err := c.db.AddMediaWatchlist(m, epIds)
|
||||
@@ -222,6 +230,10 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
if err := c.downloadPoster(detail.PosterPath, r.ID); err != nil {
|
||||
log.Errorf("download poster error: %v", err)
|
||||
}
|
||||
if err := c.downloadW500Poster(detail.PosterPath, r.ID); err != nil {
|
||||
log.Errorf("download w500 poster error: %v", err)
|
||||
}
|
||||
|
||||
if err := c.downloadBackdrop(detail.BackdropPath, r.ID); err != nil {
|
||||
log.Errorf("download poster error: %v", err)
|
||||
}
|
||||
@@ -235,6 +247,42 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Client) getAlterTitles(tmdbId int, mediaType media.MediaType) ([]schema.AlternativeTilte, error){
|
||||
var titles []schema.AlternativeTilte
|
||||
|
||||
if mediaType == media.MediaTypeTv {
|
||||
alterTitles, err := c.MustTMDB().GetTVAlternativeTitles(tmdbId, c.language)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tmdb")
|
||||
}
|
||||
|
||||
for _, t := range alterTitles.Results {
|
||||
titles = append(titles, schema.AlternativeTilte{
|
||||
Iso3166_1: t.Iso3166_1,
|
||||
Title: t.Title,
|
||||
Type: t.Type,
|
||||
})
|
||||
}
|
||||
|
||||
} else if mediaType == media.MediaTypeMovie {
|
||||
alterTitles, err := c.MustTMDB().GetMovieAlternativeTitles(tmdbId, c.language)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tmdb")
|
||||
}
|
||||
|
||||
for _, t := range alterTitles.Titles {
|
||||
titles = append(titles, schema.AlternativeTilte{
|
||||
Iso3166_1: t.Iso3166_1,
|
||||
Title: t.Title,
|
||||
Type: t.Type,
|
||||
})
|
||||
}
|
||||
}
|
||||
log.Debugf("get alternative titles: %+v", titles)
|
||||
|
||||
return titles, nil
|
||||
}
|
||||
|
||||
func (c *Client) AddMovie2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
log.Infof("add movie watchlist input: %+v", in)
|
||||
detailCn, err := c.MustTMDB().GetMovieDetails(in.TmdbID, db.LanguageCN)
|
||||
@@ -253,6 +301,12 @@ func (c *Client) AddMovie2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
}
|
||||
log.Infof("find detail for movie id %d: %v", in.TmdbID, detail)
|
||||
|
||||
alterTitles, err := c.getAlterTitles(in.TmdbID, media.MediaTypeMovie)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get alter titles")
|
||||
}
|
||||
|
||||
|
||||
epid, err := c.db.SaveEposideDetail(&ent.Episode{
|
||||
SeasonNumber: 1,
|
||||
EpisodeNumber: 1,
|
||||
@@ -279,6 +333,7 @@ func (c *Client) AddMovie2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
StorageID: in.StorageID,
|
||||
TargetDir: in.Folder,
|
||||
Limiter: schema.MediaLimiter{SizeMin: in.SizeMin, SizeMax: in.SizeMax},
|
||||
AlternativeTitles: alterTitles,
|
||||
}
|
||||
|
||||
extras := schema.MediaExtras{
|
||||
@@ -300,6 +355,10 @@ func (c *Client) AddMovie2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
if err := c.downloadPoster(detail.PosterPath, r.ID); err != nil {
|
||||
log.Errorf("download poster error: %v", err)
|
||||
}
|
||||
if err := c.downloadW500Poster(detail.PosterPath, r.ID); err != nil {
|
||||
log.Errorf("download w500 poster error: %v", err)
|
||||
}
|
||||
|
||||
if err := c.downloadBackdrop(detail.BackdropPath, r.ID); err != nil {
|
||||
log.Errorf("download backdrop error: %v", err)
|
||||
}
|
||||
@@ -314,7 +373,7 @@ func (c *Client) AddMovie2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
}
|
||||
|
||||
func (c *Client) checkMovieFolder(m *ent.Media) error {
|
||||
var storageImpl, err = c.getStorage(m.StorageID, media.MediaTypeMovie)
|
||||
var storageImpl, err = c.GetStorage(m.StorageID, media.MediaTypeMovie)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -327,8 +386,8 @@ func (c *Client) checkMovieFolder(m *ent.Media) error {
|
||||
return err
|
||||
}
|
||||
|
||||
for _,f := range files {
|
||||
if f.IsDir() || f.Size() < 100 * 1000 * 1000 /* 100M */{ //忽略路径和小于100M的文件
|
||||
for _, f := range files {
|
||||
if f.IsDir() || f.Size() < 100*1000*1000 /* 100M */ { //忽略路径和小于100M的文件
|
||||
continue
|
||||
}
|
||||
meta := metadata.ParseMovie(f.Name())
|
||||
@@ -371,6 +430,11 @@ func (c *Client) downloadPoster(path string, mediaID int) error {
|
||||
return c.downloadImage(url, mediaID, "poster.jpg")
|
||||
}
|
||||
|
||||
func (c *Client) downloadW500Poster(path string, mediaID int) error {
|
||||
url := "https://image.tmdb.org/t/p/w500" + path
|
||||
return c.downloadImage(url, mediaID, "poster_w500.jpg")
|
||||
}
|
||||
|
||||
func (c *Client) downloadImage(url string, mediaID int, name string) error {
|
||||
|
||||
log.Infof("try to download image: %v", url)
|
||||
@@ -396,6 +460,46 @@ func (c *Client) downloadImage(url string, mediaID int, name string) error {
|
||||
|
||||
}
|
||||
|
||||
func (c *Client) checkW500PosterOnStartup() {
|
||||
log.Infof("check all w500 posters")
|
||||
all := c.db.GetMediaWatchlist(media.MediaTypeTv)
|
||||
movies := c.db.GetMediaWatchlist(media.MediaTypeMovie)
|
||||
all = append(all, movies...)
|
||||
for _, e := range all {
|
||||
targetFile := filepath.Join(fmt.Sprintf("%v/%d", db.ImgPath, e.ID), "poster_w500.jpg")
|
||||
if _, err := os.Stat(targetFile); err != nil {
|
||||
log.Infof("poster_w500.jpg not exist for %s, will download it", e.NameEn)
|
||||
|
||||
if e.MediaType ==media.MediaTypeTv {
|
||||
detail, err := c.MustTMDB().GetTvDetails(e.TmdbID, db.LanguageCN)
|
||||
if err != nil {
|
||||
log.Warnf("get tmdb detail for %s error: %v", e.NameEn, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := c.downloadW500Poster(detail.PosterPath, e.ID); err != nil {
|
||||
log.Warnf("download w500 poster error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
} else {
|
||||
detail, err := c.MustTMDB().GetMovieDetails(e.TmdbID, db.LanguageCN)
|
||||
if err != nil {
|
||||
log.Warnf("get tmdb detail for %s error: %v", e.NameEn, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := c.downloadW500Poster(detail.PosterPath, e.ID); err != nil {
|
||||
log.Warnf("download w500 poster error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SuggestedMovieFolderName(tmdbId int) (string, error) {
|
||||
|
||||
d1, err := c.MustTMDB().GetMovieDetails(tmdbId, c.language)
|
||||
|
||||
@@ -4,13 +4,13 @@ import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"polaris/db"
|
||||
"polaris/ent/media"
|
||||
storage1 "polaris/ent/storage"
|
||||
"polaris/log"
|
||||
"polaris/pkg/alist"
|
||||
"polaris/pkg/metadata"
|
||||
"polaris/pkg/notifier"
|
||||
"polaris/pkg/storage"
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Client) writeNfoFile(historyId int) error {
|
||||
@@ -33,7 +35,7 @@ func (c *Client) writeNfoFile(historyId int) error {
|
||||
}
|
||||
|
||||
if md.MediaType == media.MediaTypeTv { //tvshow.nfo
|
||||
st, err := c.getStorage(md.StorageID, media.MediaTypeTv)
|
||||
st, err := c.GetStorage(md.StorageID, media.MediaTypeTv)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get storage")
|
||||
}
|
||||
@@ -68,7 +70,7 @@ func (c *Client) writeNfoFile(historyId int) error {
|
||||
}
|
||||
|
||||
} else if md.MediaType == media.MediaTypeMovie { //movie.nfo
|
||||
st, err := c.getStorage(md.StorageID, media.MediaTypeMovie)
|
||||
st, err := c.GetStorage(md.StorageID, media.MediaTypeMovie)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get storage")
|
||||
}
|
||||
@@ -120,7 +122,7 @@ func (c *Client) writePlexmatch(historyId int) error {
|
||||
if series.MediaType != media.MediaTypeTv { //.plexmatch only support tv series
|
||||
return nil
|
||||
}
|
||||
st, err := c.getStorage(series.StorageID, media.MediaTypeTv)
|
||||
st, err := c.GetStorage(series.StorageID, media.MediaTypeTv)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get storage")
|
||||
}
|
||||
@@ -195,17 +197,26 @@ func (c *Client) nfoSupportEnabled() bool {
|
||||
return c.db.GetSetting(db.SettingNfoSupportEnabled) == "true"
|
||||
}
|
||||
|
||||
func (c *Client) getStorage(storageId int, mediaType media.MediaType) (storage.Storage, error) {
|
||||
func (c *Client) GetStorage(storageId int, mediaType media.MediaType) (storage.Storage, error) {
|
||||
st := c.db.GetStorage(storageId)
|
||||
targetPath := st.TvPath
|
||||
if mediaType == media.MediaTypeMovie {
|
||||
targetPath = st.MoviePath
|
||||
}
|
||||
videoFormats, err := c.db.GetAcceptedVideoFormats()
|
||||
if err != nil {
|
||||
log.Warnf("get accepted video format error: %v", err)
|
||||
}
|
||||
subtitleFormats, err := c.db.GetAcceptedSubtitleFormats()
|
||||
if err != nil {
|
||||
log.Warnf("get accepted subtitle format error: %v", err)
|
||||
}
|
||||
|
||||
|
||||
switch st.Implementation {
|
||||
case storage1.ImplementationLocal:
|
||||
|
||||
storageImpl1, err := storage.NewLocalStorage(targetPath)
|
||||
storageImpl1, err := storage.NewLocalStorage(targetPath, videoFormats, subtitleFormats)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "new local")
|
||||
}
|
||||
@@ -213,11 +224,18 @@ func (c *Client) getStorage(storageId int, mediaType media.MediaType) (storage.S
|
||||
|
||||
case storage1.ImplementationWebdav:
|
||||
ws := st.ToWebDavSetting()
|
||||
storageImpl1, err := storage.NewWebdavStorage(ws.URL, ws.User, ws.Password, targetPath, ws.ChangeFileHash == "true")
|
||||
storageImpl1, err := storage.NewWebdavStorage(ws.URL, ws.User, ws.Password, targetPath, ws.ChangeFileHash == "true", videoFormats, subtitleFormats)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "new webdav")
|
||||
}
|
||||
return storageImpl1, nil
|
||||
case storage1.ImplementationAlist:
|
||||
cfg := st.ToWebDavSetting()
|
||||
storageImpl1, err := storage.NewAlist(&alist.Config{URL: cfg.URL, Username: cfg.User, Password: cfg.Password}, targetPath, videoFormats, subtitleFormats)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "alist")
|
||||
}
|
||||
return storageImpl1, nil
|
||||
}
|
||||
return nil, errors.New("no storage found")
|
||||
}
|
||||
@@ -300,9 +318,9 @@ func (c *Client) findEpisodeFilesPreMoving(historyId int) error {
|
||||
}
|
||||
|
||||
meta := metadata.ParseTv(f.Name())
|
||||
if meta.Episode > 0 {
|
||||
if meta.StartEpisode > 0 {
|
||||
//episode exists
|
||||
ep, err := c.db.GetEpisode(his.MediaID, seasonNum, meta.Episode)
|
||||
ep, err := c.db.GetEpisode(his.MediaID, seasonNum, meta.StartEpisode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"polaris/ent"
|
||||
"polaris/ent/episode"
|
||||
"polaris/ent/history"
|
||||
"polaris/log"
|
||||
"polaris/pkg/metadata"
|
||||
"polaris/pkg/notifier/message"
|
||||
"polaris/pkg/torznab"
|
||||
"polaris/pkg/utils"
|
||||
@@ -13,7 +15,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Client) DownloadEpisodeTorrent(r1 torznab.Result, seriesId, seasonNum, episodeNum int) (*string, error) {
|
||||
func (c *Client) DownloadEpisodeTorrent(r1 torznab.Result, seriesId, seasonNum int, episodeNums ...int) (*string, error) {
|
||||
trc, dlc, err := c.GetDownloadClient()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "connect transmission")
|
||||
@@ -31,39 +33,42 @@ func (c *Client) DownloadEpisodeTorrent(r1 torznab.Result, seriesId, seasonNum,
|
||||
return nil, errors.New("no enough space")
|
||||
}
|
||||
|
||||
var ep *ent.Episode
|
||||
if episodeNum > 0 {
|
||||
for _, e := range series.Episodes {
|
||||
if e.SeasonNumber == seasonNum && e.EpisodeNumber == episodeNum {
|
||||
ep = e
|
||||
}
|
||||
}
|
||||
if ep == nil {
|
||||
return nil, errors.Errorf("no episode of season %d episode %d", seasonNum, episodeNum)
|
||||
}
|
||||
} else { //season package download
|
||||
ep = &ent.Episode{}
|
||||
}
|
||||
magnet, err := utils.Link2Magnet(r1.Link)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("converting link to magnet error, link: %v, error: %v", r1.Link, err)
|
||||
}
|
||||
|
||||
torrent, err := trc.Download(magnet, downloadDir)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "downloading")
|
||||
}
|
||||
torrent.Start()
|
||||
|
||||
dir := fmt.Sprintf("%s/Season %02d/", series.TargetDir, seasonNum)
|
||||
|
||||
if len(episodeNums) > 0 {
|
||||
for _, epNum := range episodeNums {
|
||||
var ep *ent.Episode
|
||||
for _, e := range series.Episodes {
|
||||
if e.SeasonNumber == seasonNum && e.EpisodeNumber == epNum {
|
||||
ep = e
|
||||
}
|
||||
}
|
||||
if ep == nil {
|
||||
return nil, errors.Errorf("no episode of season %d episode %d", seasonNum, epNum)
|
||||
}
|
||||
|
||||
if ep.Status == episode.StatusMissing {
|
||||
c.db.SetEpisodeStatus(ep.ID, episode.StatusDownloading)
|
||||
}
|
||||
|
||||
}
|
||||
} else { //season package download
|
||||
c.db.SetSeasonAllEpisodeStatus(seriesId, seasonNum, episode.StatusDownloading)
|
||||
|
||||
}
|
||||
history, err := c.db.SaveHistoryRecord(ent.History{
|
||||
MediaID: seriesId,
|
||||
EpisodeID: ep.ID,
|
||||
SourceTitle: r1.Name,
|
||||
TargetDir: dir,
|
||||
Status: history.StatusRunning,
|
||||
Size: r1.Size,
|
||||
MediaID: seriesId,
|
||||
EpisodeNums: episodeNums,
|
||||
SeasonNum: seasonNum,
|
||||
SourceTitle: r1.Name,
|
||||
TargetDir: dir,
|
||||
Status: history.StatusRunning,
|
||||
Size: int(r1.Size),
|
||||
//Saved: torrent.Save(),
|
||||
Link: magnet,
|
||||
DownloadClientID: dlc.ID,
|
||||
@@ -72,42 +77,122 @@ func (c *Client) DownloadEpisodeTorrent(r1 torznab.Result, seriesId, seasonNum,
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "save record")
|
||||
}
|
||||
if episodeNum > 0 {
|
||||
if ep.Status == episode.StatusMissing {
|
||||
c.db.SetEpisodeStatus(ep.ID, episode.StatusDownloading)
|
||||
}
|
||||
} else {
|
||||
c.db.SetSeasonAllEpisodeStatus(seriesId, seasonNum, episode.StatusDownloading)
|
||||
|
||||
torrent, err := trc.Download(magnet, downloadDir)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "downloading")
|
||||
}
|
||||
torrent.Start()
|
||||
|
||||
c.tasks[history.ID] = &Task{Torrent: torrent}
|
||||
c.sendMsg(fmt.Sprintf(message.BeginDownload, r1.Name))
|
||||
name := r1.Name
|
||||
|
||||
if len(episodeNums) > 0 {
|
||||
buff := &bytes.Buffer{}
|
||||
for i, ep := range episodeNums {
|
||||
if i != 0 {
|
||||
buff.WriteString(",")
|
||||
|
||||
}
|
||||
buff.WriteString(fmt.Sprint(ep))
|
||||
}
|
||||
name = fmt.Sprintf("第%s集 (%s)", buff.String(), name)
|
||||
} else {
|
||||
name = fmt.Sprintf("全集 (%s)", name)
|
||||
}
|
||||
|
||||
c.sendMsg(fmt.Sprintf(message.BeginDownload, name))
|
||||
|
||||
log.Infof("success add %s to download task", r1.Name)
|
||||
return &r1.Name, nil
|
||||
|
||||
}
|
||||
func (c *Client) SearchAndDownload(seriesId, seasonNum, episodeNum int) (*string, error) {
|
||||
var episodes []int
|
||||
if episodeNum > 0 {
|
||||
episodes = append(episodes, episodeNum)
|
||||
|
||||
/*
|
||||
tmdb 校验获取的资源名,如果用资源名在tmdb搜索出来的结果能匹配上想要的资源,则认为资源有效,否则无效
|
||||
解决名称过于简单的影视会匹配过多资源的问题, 例如:梦魇绝镇 FROM
|
||||
*/
|
||||
func (c *Client) checkBtReourceWithTmdb(r *torznab.Result, seriesId int) bool {
|
||||
m := metadata.ParseTv(r.Name)
|
||||
se, err := c.MustTMDB().SearchMedia(m.NameEn, "", 1)
|
||||
if err != nil {
|
||||
log.Warnf("tmdb search error, consider this torrent ok: %v", err)
|
||||
return true
|
||||
} else {
|
||||
if len(se.Results) == 0 {
|
||||
log.Debugf("tmdb search no result, consider this torrent ok: %s", r.Name) //because tv name parse is not accurate
|
||||
return true
|
||||
}
|
||||
series := c.db.GetMediaDetails(seriesId)
|
||||
|
||||
se0 := se.Results[0]
|
||||
if se0.ID != int64(series.TmdbID) {
|
||||
log.Warnf("bt reosurce name not match tmdb id: %s", r.Name)
|
||||
return false
|
||||
} else { //resource tmdb id match
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SearchAndDownload(seriesId, seasonNum int, episodeNums ...int) ([]string, error) {
|
||||
|
||||
res, err := SearchTvSeries(c.db, &SearchParam{
|
||||
MediaId: seriesId,
|
||||
SeasonNum: seasonNum,
|
||||
Episodes: episodes,
|
||||
Episodes: episodeNums,
|
||||
CheckFileSize: true,
|
||||
CheckResolution: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r1 := res[0]
|
||||
log.Infof("found resource to download: %+v", r1)
|
||||
return c.DownloadEpisodeTorrent(r1, seriesId, seasonNum, episodeNum)
|
||||
wanted := make(map[int]bool, len(episodeNums))
|
||||
for _, ep := range episodeNums {
|
||||
wanted[ep] = true
|
||||
}
|
||||
var torrentNames []string
|
||||
lo:
|
||||
for _, r := range res {
|
||||
if !c.checkBtReourceWithTmdb(&r, seriesId) {
|
||||
continue
|
||||
}
|
||||
m := metadata.ParseTv(r.Name)
|
||||
m.ParseExtraDescription(r.Description)
|
||||
if len(episodeNums) == 0 { //want season pack
|
||||
if m.IsSeasonPack {
|
||||
name, err := c.DownloadEpisodeTorrent(r, seriesId, seasonNum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
torrentNames = append(torrentNames, *name)
|
||||
break lo
|
||||
}
|
||||
} else {
|
||||
torrentEpisodes := make([]int, 0)
|
||||
for i := m.StartEpisode; i <= m.EndEpisode; i++ {
|
||||
if !wanted[i] { //torrent has episode not wanted
|
||||
continue lo
|
||||
}
|
||||
torrentEpisodes = append(torrentEpisodes, i)
|
||||
}
|
||||
name, err := c.DownloadEpisodeTorrent(r, seriesId, seasonNum, torrentEpisodes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
torrentNames = append(torrentNames, *name)
|
||||
|
||||
for _, num := range torrentEpisodes {
|
||||
delete(wanted, num) //delete downloaded episode from wanted
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(wanted) > 0 {
|
||||
log.Warnf("still wanted but not downloaded episodes: %v", wanted)
|
||||
}
|
||||
return torrentNames, nil
|
||||
}
|
||||
|
||||
func (c *Client) DownloadMovie(m *ent.Media, link, name string, size int, indexerID int) (*string, error) {
|
||||
func (c *Client) DownloadMovie(m *ent.Media, link, name string, size int64, indexerID int) (*string, error) {
|
||||
trc, dlc, err := c.GetDownloadClient()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "connect transmission")
|
||||
@@ -129,12 +214,12 @@ func (c *Client) DownloadMovie(m *ent.Media, link, name string, size int, indexe
|
||||
go func() {
|
||||
ep, _ := c.db.GetMovieDummyEpisode(m.ID)
|
||||
history, err := c.db.SaveHistoryRecord(ent.History{
|
||||
MediaID: m.ID,
|
||||
EpisodeID: ep.ID,
|
||||
SourceTitle: name,
|
||||
TargetDir: m.TargetDir,
|
||||
Status: history.StatusRunning,
|
||||
Size: size,
|
||||
MediaID: m.ID,
|
||||
EpisodeID: ep.ID,
|
||||
SourceTitle: name,
|
||||
TargetDir: m.TargetDir,
|
||||
Status: history.StatusRunning,
|
||||
Size: int(size),
|
||||
//Saved: torrent.Save(),
|
||||
Link: magnet,
|
||||
DownloadClientID: dlc.ID,
|
||||
|
||||
@@ -2,6 +2,7 @@ package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"polaris/db"
|
||||
"polaris/ent"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"polaris/pkg"
|
||||
"polaris/pkg/notifier/message"
|
||||
"polaris/pkg/utils"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@@ -19,6 +21,10 @@ import (
|
||||
func (c *Client) addSysCron() {
|
||||
c.registerCronJob("check_running_tasks", "@every 1m", c.checkTasks)
|
||||
c.registerCronJob("check_available_medias_to_download", "0 0 * * * *", func() error {
|
||||
v := os.Getenv("POLARIS_NO_AUTO_DOWNLOAD")
|
||||
if v == "true" {
|
||||
return nil
|
||||
}
|
||||
c.downloadAllTvSeries()
|
||||
c.downloadAllMovies()
|
||||
return nil
|
||||
@@ -36,6 +42,7 @@ func (c *Client) addSysCron() {
|
||||
return true
|
||||
})
|
||||
c.cron.Start()
|
||||
log.Infof("--------- add cron jobs done --------")
|
||||
}
|
||||
|
||||
func (c *Client) mustAddCron(spec string, cmd func()) {
|
||||
@@ -113,6 +120,38 @@ func (c *Client) postTaskProcessing(id int) {
|
||||
}
|
||||
}
|
||||
|
||||
func getSeasonNum(h *ent.History) int {
|
||||
if h.SeasonNum != 0 {
|
||||
return h.SeasonNum
|
||||
}
|
||||
seasonNum, err := utils.SeasonId(h.TargetDir)
|
||||
if err != nil {
|
||||
log.Errorf("no season id: %v", h.TargetDir)
|
||||
seasonNum = -1
|
||||
}
|
||||
return seasonNum
|
||||
}
|
||||
|
||||
func (c *Client) GetEpisodeIds(r *ent.History) []int {
|
||||
var episodeIds []int
|
||||
seasonNum := getSeasonNum(r)
|
||||
|
||||
if r.EpisodeID > 0 {
|
||||
episodeIds = append(episodeIds, r.EpisodeID)
|
||||
}
|
||||
if len(r.EpisodeNums) > 0 {
|
||||
series := c.db.GetMediaDetails(r.MediaID)
|
||||
for _, epNum := range r.EpisodeNums {
|
||||
for _, ep := range series.Episodes {
|
||||
if ep.SeasonNumber == seasonNum && ep.EpisodeNumber == epNum {
|
||||
episodeIds = append(episodeIds, ep.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return episodeIds
|
||||
}
|
||||
|
||||
func (c *Client) moveCompletedTask(id int) (err1 error) {
|
||||
torrent := c.tasks[id]
|
||||
r := c.db.GetHistory(id)
|
||||
@@ -121,11 +160,7 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
|
||||
return nil
|
||||
}
|
||||
c.db.SetHistoryStatus(r.ID, history.StatusUploading)
|
||||
seasonNum, err := utils.SeasonId(r.TargetDir)
|
||||
if err != nil {
|
||||
log.Errorf("no season id: %v", r.TargetDir)
|
||||
seasonNum = -1
|
||||
}
|
||||
|
||||
downloadclient, err := c.db.GetDownloadClient(r.DownloadClientID)
|
||||
if err != nil {
|
||||
log.Errorf("get task download client error: %v, use default one", err)
|
||||
@@ -136,13 +171,19 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
|
||||
return err
|
||||
}
|
||||
|
||||
seasonNum := getSeasonNum(r)
|
||||
|
||||
episodeIds := c.GetEpisodeIds(r)
|
||||
|
||||
defer func() {
|
||||
|
||||
if err1 != nil {
|
||||
c.db.SetHistoryStatus(r.ID, history.StatusFail)
|
||||
if r.EpisodeID != 0 {
|
||||
if !c.db.IsEpisodeDownloadingOrDownloaded(r.EpisodeID) {
|
||||
c.db.SetEpisodeStatus(r.EpisodeID, episode.StatusMissing)
|
||||
if len(episodeIds) > 0 {
|
||||
for _, id := range episodeIds {
|
||||
if !c.db.IsEpisodeDownloadingOrDownloaded(id) {
|
||||
c.db.SetEpisodeStatus(id, episode.StatusMissing)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.db.SetSeasonAllEpisodeStatus(r.MediaID, seasonNum, episode.StatusMissing)
|
||||
@@ -162,7 +203,7 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
|
||||
}
|
||||
st := c.db.GetStorage(series.StorageID)
|
||||
log.Infof("move task files to target dir: %v", r.TargetDir)
|
||||
stImpl, err := c.getStorage(st.ID, series.MediaType)
|
||||
stImpl, err := c.GetStorage(st.ID, series.MediaType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -171,10 +212,13 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
|
||||
if err := stImpl.Copy(filepath.Join(c.db.GetDownloadDir(), torrentName), r.TargetDir); err != nil {
|
||||
return errors.Wrap(err, "move file")
|
||||
}
|
||||
torrent.UploadProgresser = stImpl.UploadProgress
|
||||
|
||||
c.db.SetHistoryStatus(r.ID, history.StatusSeeding)
|
||||
if r.EpisodeID != 0 {
|
||||
c.db.SetEpisodeStatus(r.EpisodeID, episode.StatusDownloaded)
|
||||
if len(episodeIds) > 0 {
|
||||
for _, id := range episodeIds {
|
||||
c.db.SetEpisodeStatus(id, episode.StatusDownloaded)
|
||||
}
|
||||
} else {
|
||||
c.db.SetSeasonAllEpisodeStatus(r.MediaID, seasonNum, episode.StatusDownloaded)
|
||||
}
|
||||
@@ -199,7 +243,7 @@ func (c *Client) CheckDownloadedSeriesFiles(m *ent.Media) error {
|
||||
}
|
||||
log.Infof("check files in directory: %s", m.TargetDir)
|
||||
|
||||
var storageImpl, err = c.getStorage(m.StorageID, media.MediaTypeTv)
|
||||
var storageImpl, err = c.GetStorage(m.StorageID, media.MediaTypeTv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -247,6 +291,7 @@ func (c *Client) CheckDownloadedSeriesFiles(m *ent.Media) error {
|
||||
type Task struct {
|
||||
//Processing bool
|
||||
pkg.Torrent
|
||||
UploadProgresser func() float64
|
||||
}
|
||||
|
||||
func (c *Client) DownloadSeriesAllEpisodes(id int) []string {
|
||||
@@ -261,40 +306,53 @@ func (c *Client) DownloadSeriesAllEpisodes(id int) []string {
|
||||
continue
|
||||
}
|
||||
wantedSeasonPack := true
|
||||
seasonEpisodesWanted := make(map[int][]int, 0)
|
||||
for _, ep := range epsides {
|
||||
if !ep.Monitored {
|
||||
wantedSeasonPack = false
|
||||
continue
|
||||
}
|
||||
if ep.Status != episode.StatusMissing {
|
||||
wantedSeasonPack = false
|
||||
continue
|
||||
}
|
||||
if ep.AirDate != "" {
|
||||
t, err := time.Parse("2006-01-02", ep.AirDate)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
/*
|
||||
-------- now ------ t -----
|
||||
t - 1day < now 要检测的剧集
|
||||
提前一天开始检测
|
||||
*/
|
||||
if time.Now().Before(t.Add(-24 * time.Hour)) { //not aired
|
||||
wantedSeasonPack = false
|
||||
continue
|
||||
}
|
||||
}
|
||||
seasonEpisodesWanted[ep.SeasonNumber] = append(seasonEpisodesWanted[ep.SeasonNumber], ep.EpisodeNumber)
|
||||
}
|
||||
if wantedSeasonPack {
|
||||
name, err := c.SearchAndDownload(id, seasonNum, -1)
|
||||
names, err := c.SearchAndDownload(id, seasonNum)
|
||||
if err == nil {
|
||||
allNames = append(allNames, *name)
|
||||
log.Infof("begin download torrent resource: %v", name)
|
||||
allNames = append(allNames, names...)
|
||||
log.Infof("begin download torrent resource: %v", names)
|
||||
} else {
|
||||
log.Warnf("finding season pack error: %v", err)
|
||||
wantedSeasonPack = false
|
||||
}
|
||||
|
||||
}
|
||||
if !wantedSeasonPack {
|
||||
for _, ep := range epsides {
|
||||
if !ep.Monitored {
|
||||
continue
|
||||
}
|
||||
if ep.Status != episode.StatusMissing {
|
||||
continue
|
||||
}
|
||||
name, err := c.SearchAndDownload(id, ep.SeasonNumber, ep.EpisodeNumber)
|
||||
|
||||
for se, eps := range seasonEpisodesWanted {
|
||||
names, err := c.SearchAndDownload(id, se, eps...)
|
||||
if err != nil {
|
||||
log.Warnf("finding resoruces of season %d episode %d error: %v", ep.SeasonNumber, ep.EpisodeNumber, err)
|
||||
log.Warnf("finding resoruces of season %d episode %v error: %v", se, eps, err)
|
||||
continue
|
||||
} else {
|
||||
allNames = append(allNames, *name)
|
||||
log.Infof("begin download torrent resource: %v", name)
|
||||
allNames = append(allNames, names...)
|
||||
log.Infof("begin download torrent resource: %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,12 +434,12 @@ func (c *Client) downloadMovieSingleEpisode(ep *ent.Episode, targetDir string) (
|
||||
torrent.Start()
|
||||
|
||||
history, err := c.db.SaveHistoryRecord(ent.History{
|
||||
MediaID: ep.MediaID,
|
||||
EpisodeID: ep.ID,
|
||||
SourceTitle: r1.Name,
|
||||
TargetDir: targetDir,
|
||||
Status: history.StatusRunning,
|
||||
Size: r1.Size,
|
||||
MediaID: ep.MediaID,
|
||||
EpisodeID: ep.ID,
|
||||
SourceTitle: r1.Name,
|
||||
TargetDir: targetDir,
|
||||
Status: history.StatusRunning,
|
||||
Size: int(r1.Size),
|
||||
//Saved: torrent.Save(),
|
||||
Link: magnet,
|
||||
DownloadClientID: dlc.ID,
|
||||
|
||||
@@ -3,9 +3,12 @@ package core
|
||||
import (
|
||||
"fmt"
|
||||
"polaris/db"
|
||||
"polaris/ent"
|
||||
"polaris/ent/media"
|
||||
"polaris/log"
|
||||
"polaris/pkg/gemini"
|
||||
"polaris/pkg/metadata"
|
||||
"polaris/pkg/prowlarr"
|
||||
"polaris/pkg/torznab"
|
||||
"slices"
|
||||
"sort"
|
||||
@@ -25,22 +28,128 @@ type SearchParam struct {
|
||||
FilterQiangban bool //for movie, 是否过滤枪版电影
|
||||
}
|
||||
|
||||
func names2Query(media *ent.Media) []string {
|
||||
var names = []string{media.NameEn}
|
||||
|
||||
if media.NameCn != "" {
|
||||
hasName := false
|
||||
for _, n := range names {
|
||||
if media.NameCn == n {
|
||||
hasName = true
|
||||
}
|
||||
}
|
||||
if !hasName {
|
||||
names = append(names, media.NameCn)
|
||||
}
|
||||
|
||||
}
|
||||
if media.OriginalName != "" {
|
||||
hasName := false
|
||||
for _, n := range names {
|
||||
if media.OriginalName == n {
|
||||
hasName = true
|
||||
}
|
||||
}
|
||||
if !hasName {
|
||||
names = append(names, media.OriginalName)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, t := range media.AlternativeTitles {
|
||||
if (t.Iso3166_1 == "CN" || t.Iso3166_1 == "US") && t.Type == "" {
|
||||
hasName := false
|
||||
for _, n := range names {
|
||||
if t.Title == n {
|
||||
hasName = true
|
||||
}
|
||||
}
|
||||
if !hasName {
|
||||
names = append(names, t.Title)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Debugf("name to query %+v", names)
|
||||
return names
|
||||
}
|
||||
|
||||
func filterBasedOnGemini(cfg db.AIConfig, res []torznab.Result, names ...string) []torznab.Result {
|
||||
|
||||
var torrentNames []string
|
||||
for _, r := range res {
|
||||
torrentNames = append(torrentNames, r.Name)
|
||||
}
|
||||
g, err := gemini.NewClient(cfg.GeminiApiKey, cfg.GeminiModelName)
|
||||
if err != nil {
|
||||
log.Warnf("create gemini client: %v", err)
|
||||
return res
|
||||
}
|
||||
resf, err := g.FilterTvOrMovies(torrentNames, names...)
|
||||
if err != nil {
|
||||
log.Warnf("filter with gemini: %v", err)
|
||||
return res
|
||||
}
|
||||
var newRes []torznab.Result
|
||||
for _, r := range res {
|
||||
if slices.Contains(resf, r.Name) {
|
||||
newRes = append(newRes, r)
|
||||
}
|
||||
}
|
||||
return newRes
|
||||
}
|
||||
|
||||
func filterBasedOnRules(res []torznab.Result, names ...string) []torznab.Result {
|
||||
var filtered []torznab.Result
|
||||
for _, r := range res {
|
||||
meta := metadata.ParseTv(r.Name)
|
||||
if meta.IsAcceptable(names...) {
|
||||
filtered = append(filtered, r)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func filterResourceNames(db1 *db.Client, res []torznab.Result, names ...string) []torznab.Result {
|
||||
n1 := len(res)
|
||||
cfg, err := db1.GetAIConfig()
|
||||
if err != nil {
|
||||
log.Warnf("get ai config: %v", err)
|
||||
}
|
||||
if cfg.Enabled {
|
||||
res = filterBasedOnGemini(cfg, res, names...)
|
||||
} else {
|
||||
res = filterBasedOnRules(res, names...)
|
||||
}
|
||||
log.Infof("resource before name filtering length is %d, after filtering length is %d", n1, len(res))
|
||||
return res
|
||||
|
||||
}
|
||||
|
||||
func SearchTvSeries(db1 *db.Client, param *SearchParam) ([]torznab.Result, error) {
|
||||
series := db1.GetMediaDetails(param.MediaId)
|
||||
if series == nil {
|
||||
return nil, fmt.Errorf("no tv series of id %v", param.MediaId)
|
||||
}
|
||||
limiter, err := db1.GetSizeLimiter("tv")
|
||||
if err != nil {
|
||||
log.Warnf("get tv size limiter: %v", err)
|
||||
limiter = &db.MediaSizeLimiter{}
|
||||
}
|
||||
log.Debugf("check tv series %s, season %d, episode %v", series.NameEn, param.SeasonNum, param.Episodes)
|
||||
|
||||
res := searchWithTorznab(db1, series.NameEn, series.NameCn, series.OriginalName)
|
||||
names := names2Query(series.Media)
|
||||
|
||||
res := searchWithTorznab(db1, prowlarr.TV, names...)
|
||||
|
||||
res = filterResourceNames(db1, res, names...)
|
||||
|
||||
var filtered []torznab.Result
|
||||
lo:
|
||||
for _, r := range res {
|
||||
//log.Infof("torrent resource: %+v", r)
|
||||
meta := metadata.ParseTv(r.Name)
|
||||
if meta == nil { //cannot parse name
|
||||
continue
|
||||
}
|
||||
meta.ParseExtraDescription(r.Description)
|
||||
|
||||
if isImdbidNotMatch(series.ImdbID, r.ImdbId) { //has imdb id and not match
|
||||
continue
|
||||
}
|
||||
@@ -60,9 +169,15 @@ func SearchTvSeries(db1 *db.Client, param *SearchParam) ([]torznab.Result, error
|
||||
continue
|
||||
}
|
||||
|
||||
if len(param.Episodes) > 0 && !slices.Contains(param.Episodes, meta.Episode) { //not season pack, but episode number not equal
|
||||
continue
|
||||
|
||||
if len(param.Episodes) > 0 { //not season pack, but episode number not equal
|
||||
if meta.StartEpisode <= 0 {
|
||||
continue lo
|
||||
}
|
||||
for i := meta.StartEpisode; i <= meta.EndEpisode; i++ {
|
||||
if !slices.Contains(param.Episodes, i) {
|
||||
continue lo
|
||||
}
|
||||
}
|
||||
} else if len(param.Episodes) == 0 && !meta.IsSeasonPack { //want season pack, but not season pack
|
||||
continue
|
||||
}
|
||||
@@ -73,7 +188,7 @@ func SearchTvSeries(db1 *db.Client, param *SearchParam) ([]torznab.Result, error
|
||||
continue
|
||||
}
|
||||
|
||||
if !torrentSizeOk(series, r.Size, param) {
|
||||
if !torrentSizeOk(series, limiter, r.Size, meta.EndEpisode+1-meta.StartEpisode, param) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -107,36 +222,47 @@ func imdbIDMatchExact(id1, id2 string) bool {
|
||||
return id1 == id2
|
||||
}
|
||||
|
||||
func torrentSizeOk(detail *db.MediaDetails, torrentSize int, param *SearchParam) bool {
|
||||
defaultMinSize := 80 * 1000 * 1000 //tv, 80M min
|
||||
if detail.MediaType == media.MediaTypeMovie {
|
||||
defaultMinSize = 200 * 1000 * 1000 // movie, 200M min
|
||||
}
|
||||
if detail.Limiter.SizeMin > 0 { //if size limiter set, use configured min size
|
||||
defaultMinSize = detail.Limiter.SizeMin
|
||||
}
|
||||
func torrentSizeOk(detail *db.MediaDetails, globalLimiter *db.MediaSizeLimiter, torrentSize int64,
|
||||
torrentEpisodeNum int, param *SearchParam) bool {
|
||||
|
||||
multiplier := 1 //大小倍数,正常为1,如果是季包则为季内集数
|
||||
if detail.MediaType == media.MediaTypeTv && len(param.Episodes) == 0 { //tv season pack
|
||||
multiplier = seasonEpisodeCount(detail, param.SeasonNum)
|
||||
multiplier := 1 //大小倍数,正常为1,如果是季包则为季内集数
|
||||
if detail.MediaType == media.MediaTypeTv {
|
||||
if len(param.Episodes) == 0 { //want tv season pack
|
||||
multiplier = seasonEpisodeCount(detail, param.SeasonNum)
|
||||
} else {
|
||||
multiplier = torrentEpisodeNum
|
||||
}
|
||||
}
|
||||
|
||||
if param.CheckFileSize { //check file size when trigger automatic download
|
||||
|
||||
if detail.Limiter.SizeMin > 0 { //min size
|
||||
sizeMin := detail.Limiter.SizeMin * multiplier
|
||||
sizeMin := detail.Limiter.SizeMin * int64(multiplier)
|
||||
if torrentSize < sizeMin { //比最小要求的大小还要小, min size not qualify
|
||||
return false
|
||||
}
|
||||
} else if globalLimiter != nil {
|
||||
resLimiter := globalLimiter.GetLimiter(detail.Resolution)
|
||||
sizeMin := resLimiter.MinSize * int64(multiplier)
|
||||
if torrentSize < sizeMin { //比最小要求的大小还要小, min size not qualify
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if detail.Limiter.SizeMax > 0 { //max size
|
||||
sizeMax := detail.Limiter.SizeMax * multiplier
|
||||
sizeMax := detail.Limiter.SizeMax * int64(multiplier)
|
||||
if torrentSize > sizeMax { //larger than max size wanted, max size not qualify
|
||||
return false
|
||||
}
|
||||
} else if globalLimiter != nil {
|
||||
resLimiter := globalLimiter.GetLimiter(detail.Resolution)
|
||||
sizeMax := resLimiter.MaxSIze * int64(multiplier)
|
||||
if torrentSize > sizeMax { //larger than max size wanted, max size not qualify
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return torrentSize > defaultMinSize * multiplier
|
||||
return true
|
||||
}
|
||||
|
||||
func seasonEpisodeCount(detail *db.MediaDetails, seasonNum int) int {
|
||||
@@ -170,9 +296,18 @@ func SearchMovie(db1 *db.Client, param *SearchParam) ([]torznab.Result, error) {
|
||||
return nil, errors.New("no media found of id")
|
||||
}
|
||||
|
||||
res := searchWithTorznab(db1, movieDetail.NameEn, movieDetail.NameCn, movieDetail.OriginalName)
|
||||
limiter, err := db1.GetSizeLimiter("movie")
|
||||
if err != nil {
|
||||
log.Warnf("get tv size limiter: %v", err)
|
||||
limiter = &db.MediaSizeLimiter{}
|
||||
}
|
||||
names := names2Query(movieDetail.Media)
|
||||
|
||||
res := searchWithTorznab(db1, prowlarr.Movie, names...)
|
||||
res = filterResourceNames(db1, res, names...)
|
||||
|
||||
if movieDetail.Extras.IsJav() {
|
||||
res1 := searchWithTorznab(db1, movieDetail.Extras.JavId)
|
||||
res1 := searchWithTorznab(db1, prowlarr.Movie, movieDetail.Extras.JavId)
|
||||
res = append(res, res1...)
|
||||
}
|
||||
|
||||
@@ -210,7 +345,7 @@ func SearchMovie(db1 *db.Client, param *SearchParam) ([]torznab.Result, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !torrentSizeOk(movieDetail, r.Size, param) {
|
||||
if !torrentSizeOk(movieDetail, limiter, r.Size, 1, param) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -226,10 +361,21 @@ func SearchMovie(db1 *db.Client, param *SearchParam) ([]torznab.Result, error) {
|
||||
|
||||
}
|
||||
|
||||
func searchWithTorznab(db *db.Client, queries ...string) []torznab.Result {
|
||||
func searchWithTorznab(db *db.Client, t prowlarr.ProwlarrSupportType, queries ...string) []torznab.Result {
|
||||
|
||||
var res []torznab.Result
|
||||
allTorznab := db.GetAllTorznabInfo()
|
||||
|
||||
p, err := db.GetProwlarrSetting()
|
||||
if err == nil && !p.Disabled { //prowlarr exists
|
||||
c := prowlarr.New(p.ApiKey, p.URL)
|
||||
all, err := c.GetIndexers(t)
|
||||
if err != nil {
|
||||
log.Warnf("get prowlarr all indexer error: %v", err)
|
||||
} else {
|
||||
allTorznab = append(allTorznab, all...)
|
||||
}
|
||||
}
|
||||
resChan := make(chan []torznab.Result)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
@@ -321,5 +467,7 @@ func torrentNameOk(detail *db.MediaDetails, tester NameTester) bool {
|
||||
if detail.Extras.IsJav() && tester.IsAcceptable(detail.Extras.JavId) {
|
||||
return true
|
||||
}
|
||||
return tester.IsAcceptable(detail.NameCn, detail.NameEn, detail.OriginalName)
|
||||
names := names2Query(detail.Media)
|
||||
|
||||
return tester.IsAcceptable(names...)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func (s *Server) searchAndDownloadSeasonPackage(seriesId, seasonNum int) (*strin
|
||||
|
||||
r1 := res[0]
|
||||
log.Infof("found resource to download: %+v", r1)
|
||||
return s.core.DownloadEpisodeTorrent(r1, seriesId, seasonNum, -1)
|
||||
return s.core.DownloadEpisodeTorrent(r1, seriesId, seasonNum)
|
||||
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ func (s *Server) SearchTvAndDownload(c *gin.Context) (interface{}, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "download")
|
||||
}
|
||||
name = *name1
|
||||
name = name1[0]
|
||||
}
|
||||
|
||||
return gin.H{
|
||||
@@ -154,7 +154,7 @@ func (s *Server) DownloadTorrent(c *gin.Context) (interface{}, error) {
|
||||
name = fmt.Sprintf("%v S%02d", m.OriginalName, in.Season)
|
||||
}
|
||||
res := torznab.Result{Name: name, Link: in.Link, Size: in.Size}
|
||||
return s.core.DownloadEpisodeTorrent(res, in.MediaID, in.Season, -1)
|
||||
return s.core.DownloadEpisodeTorrent(res, in.MediaID, in.Season)
|
||||
}
|
||||
name := in.Name
|
||||
if name == "" {
|
||||
@@ -175,6 +175,10 @@ func (s *Server) DownloadAll(c *gin.Context) (interface{}, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "convert")
|
||||
}
|
||||
return s.downloadAllEpisodes(id)
|
||||
}
|
||||
|
||||
func (s *Server) downloadAllEpisodes(id int) (interface{}, error) {
|
||||
m, err := s.db.GetMedia(id)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get media")
|
||||
@@ -186,3 +190,27 @@ func (s *Server) DownloadAll(c *gin.Context) (interface{}, error) {
|
||||
|
||||
return []string{name}, err
|
||||
}
|
||||
|
||||
func (s *Server) DownloadAllTv(c *gin.Context) (interface{}, error) {
|
||||
tvs := s.db.GetMediaWatchlist(media.MediaTypeTv)
|
||||
var allNames []string
|
||||
for _, tv := range tvs {
|
||||
names, err := s.downloadAllEpisodes(tv.ID)
|
||||
if err == nil {
|
||||
allNames = append(allNames, names.([]string)...)
|
||||
}
|
||||
}
|
||||
return allNames, nil
|
||||
}
|
||||
|
||||
func (s *Server) DownloadAllMovies(c *gin.Context) (interface{}, error) {
|
||||
movies := s.db.GetMediaWatchlist(media.MediaTypeMovie)
|
||||
var allNames []string
|
||||
for _, mv := range movies {
|
||||
names, err := s.downloadAllEpisodes(mv.ID)
|
||||
if err == nil {
|
||||
allNames = append(allNames, names.([]string)...)
|
||||
}
|
||||
}
|
||||
return allNames, nil
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import (
|
||||
"net/url"
|
||||
"polaris/db"
|
||||
"polaris/log"
|
||||
"polaris/pkg/cache"
|
||||
"polaris/pkg/tmdb"
|
||||
"polaris/server/core"
|
||||
"polaris/ui"
|
||||
"time"
|
||||
|
||||
ginzap "github.com/gin-contrib/zap"
|
||||
|
||||
@@ -22,20 +24,24 @@ import (
|
||||
func NewServer(db *db.Client) *Server {
|
||||
r := gin.Default()
|
||||
s := &Server{
|
||||
r: r,
|
||||
db: db,
|
||||
language: db.GetLanguage(),
|
||||
r: r,
|
||||
db: db,
|
||||
language: db.GetLanguage(),
|
||||
monitorNumCache: cache.NewCache[int, int](10 * time.Minute),
|
||||
downloadNumCache: cache.NewCache[int, int](10 * time.Minute),
|
||||
}
|
||||
s.core = core.NewClient(db, s.language)
|
||||
return s
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
r *gin.Engine
|
||||
db *db.Client
|
||||
core *core.Client
|
||||
language string
|
||||
jwtSerect string
|
||||
r *gin.Engine
|
||||
db *db.Client
|
||||
core *core.Client
|
||||
language string
|
||||
jwtSerect string
|
||||
monitorNumCache *cache.Cache[int, int]
|
||||
downloadNumCache *cache.Cache[int, int]
|
||||
}
|
||||
|
||||
func (s *Server) Serve() error {
|
||||
@@ -70,6 +76,12 @@ func (s *Server) Serve() error {
|
||||
setting.POST("/parse/movie", HttpHandler(s.ParseMovie))
|
||||
setting.POST("/monitoring", HttpHandler(s.ChangeEpisodeMonitoring))
|
||||
setting.POST("/cron/trigger", HttpHandler(s.TriggerCronJob))
|
||||
setting.GET("/prowlarr", HttpHandler(s.GetProwlarrSetting))
|
||||
setting.POST("/prowlarr", HttpHandler(s.SaveProwlarrSetting))
|
||||
setting.GET("/limiter", HttpHandler(s.GetSizeLimiter))
|
||||
setting.POST("/limiter", HttpHandler(s.SetSizeLimiter))
|
||||
setting.GET("/ai", HttpHandler(s.GetAIConfig))
|
||||
setting.POST("/ai", HttpHandler(s.SetAIConfig))
|
||||
}
|
||||
activity := api.Group("/activity")
|
||||
{
|
||||
@@ -94,6 +106,8 @@ func (s *Server) Serve() error {
|
||||
tv.GET("/suggest/tv/:tmdb_id", HttpHandler(s.SuggestedSeriesFolderName))
|
||||
tv.GET("/suggest/movie/:tmdb_id", HttpHandler(s.SuggestedMovieFolderName))
|
||||
tv.GET("/downloadall/:id", HttpHandler(s.DownloadAll))
|
||||
tv.GET("/download/tv", HttpHandler(s.DownloadAllTv))
|
||||
tv.GET("/download/movie", HttpHandler(s.DownloadAllMovies))
|
||||
}
|
||||
indexer := api.Group("/indexer")
|
||||
{
|
||||
@@ -128,6 +142,7 @@ func (s *Server) Serve() error {
|
||||
importlist.POST("/add", HttpHandler(s.addImportlist))
|
||||
importlist.DELETE("/delete", HttpHandler(s.deleteImportList))
|
||||
}
|
||||
log.Infof("----------- Polaris Server Successfully Started ------------")
|
||||
|
||||
return s.r.Run(":8080")
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"polaris/ent"
|
||||
"polaris/ent/downloadclients"
|
||||
"polaris/log"
|
||||
"polaris/pkg/prowlarr"
|
||||
"polaris/pkg/qbittorrent"
|
||||
"polaris/pkg/torznab"
|
||||
"polaris/pkg/transmission"
|
||||
@@ -70,7 +71,6 @@ func (s *Server) SetSetting(c *gin.Context) (interface{}, error) {
|
||||
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, "")
|
||||
@@ -303,3 +303,82 @@ func (s *Server) TriggerCronJob(c *gin.Context) (interface{}, error) {
|
||||
}
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
func (s *Server) GetProwlarrSetting(c *gin.Context) (interface{}, error) {
|
||||
se, err := s.db.GetProwlarrSetting()
|
||||
if err != nil {
|
||||
return &db.ProwlarrSetting{}, nil
|
||||
}
|
||||
return se, nil
|
||||
}
|
||||
func (s *Server) SaveProwlarrSetting(c *gin.Context) (interface{}, error) {
|
||||
var in db.ProwlarrSetting
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !in.Disabled {
|
||||
client := prowlarr.New(in.ApiKey, in.URL)
|
||||
if _, err := client.GetIndexers(prowlarr.TV); err != nil {
|
||||
return nil, errors.Wrap(err, "connect to prowlarr error")
|
||||
}
|
||||
}
|
||||
err := s.db.SaveProwlarrSetting(&in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
type ResolutionSizeLimiter struct {
|
||||
TvLimiter *db.MediaSizeLimiter `json:"tv_limiter"`
|
||||
MovieLimiter *db.MediaSizeLimiter `json:"movie_limiter"`
|
||||
}
|
||||
|
||||
func (s *Server) GetSizeLimiter(c *gin.Context) (interface{}, error) {
|
||||
tv, err := s.db.GetSizeLimiter("tv")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db")
|
||||
}
|
||||
movie, err := s.db.GetSizeLimiter("movie")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db")
|
||||
}
|
||||
r := ResolutionSizeLimiter{
|
||||
TvLimiter: tv,
|
||||
MovieLimiter: movie,
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (s *Server) SetSizeLimiter(c *gin.Context) (interface{}, error) {
|
||||
var in ResolutionSizeLimiter
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.db.SetSizeLimiter("tv", in.TvLimiter); err != nil {
|
||||
return nil, errors.Wrap(err, "db")
|
||||
}
|
||||
if err := s.db.SetSizeLimiter("movie", in.MovieLimiter); err != nil {
|
||||
return nil, errors.Wrap(err, "db")
|
||||
}
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
func (s *Server) GetAIConfig(c *gin.Context) (interface{}, error) {
|
||||
aiConfig, err := s.db.GetAIConfig()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db")
|
||||
}
|
||||
return aiConfig, nil
|
||||
}
|
||||
|
||||
func (s *Server) SetAIConfig(c *gin.Context) (interface{}, error) {
|
||||
var in db.AIConfig
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.db.SetAIConfig(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "db")
|
||||
}
|
||||
return "success", nil
|
||||
}
|
||||
@@ -2,9 +2,11 @@ package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"polaris/db"
|
||||
|
||||
"polaris/log"
|
||||
"polaris/pkg/alist"
|
||||
"polaris/pkg/storage"
|
||||
"polaris/pkg/utils"
|
||||
"strconv"
|
||||
@@ -28,7 +30,7 @@ func (s *Server) AddStorage(c *gin.Context) (interface{}, error) {
|
||||
if in.Implementation == "webdav" {
|
||||
//test webdav
|
||||
wd := in.ToWebDavSetting()
|
||||
st, err := storage.NewWebdavStorage(wd.URL, wd.User, wd.Password, in.TvPath, false)
|
||||
st, err := storage.NewWebdavStorage(wd.URL, wd.User, wd.Password, in.TvPath, false, nil, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "new webdav")
|
||||
}
|
||||
@@ -39,6 +41,21 @@ func (s *Server) AddStorage(c *gin.Context) (interface{}, error) {
|
||||
for _, f := range fs {
|
||||
log.Infof("file name: %v", f.Name())
|
||||
}
|
||||
} else if in.Implementation == "alist" {
|
||||
cfg := in.ToAlistSetting()
|
||||
_, err := storage.NewAlist(&alist.Config{URL: cfg.URL, Username: cfg.User, Password: cfg.Password}, in.TvPath, nil, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "alist")
|
||||
}
|
||||
} else if in.Implementation == "local" {
|
||||
_, err := os.Stat(in.TvPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = os.Stat(in.MoviePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
log.Infof("received add storage input: %v", in)
|
||||
err := s.db.AddStorage(&in)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"polaris/log"
|
||||
"polaris/server/core"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
@@ -102,17 +103,25 @@ func (s *Server) GetTvWatchlist(c *gin.Context) (interface{}, error) {
|
||||
MonitoredNum: 0,
|
||||
DownloadedNum: 0,
|
||||
}
|
||||
|
||||
details := s.db.GetMediaDetails(item.ID)
|
||||
|
||||
for _, ep := range details.Episodes {
|
||||
if ep.Monitored {
|
||||
ms.MonitoredNum++
|
||||
if ep.Status == episode.StatusDownloaded {
|
||||
ms.DownloadedNum++
|
||||
mon, ok1 := s.monitorNumCache.Get(item.ID)
|
||||
dow, ok2 := s.downloadNumCache.Get(item.ID)
|
||||
if ok1 && ok2 {
|
||||
ms.MonitoredNum = mon
|
||||
ms.DownloadedNum = dow
|
||||
} else {
|
||||
details := s.db.GetMediaDetails(item.ID)
|
||||
for _, ep := range details.Episodes {
|
||||
if ep.Monitored {
|
||||
ms.MonitoredNum++
|
||||
if ep.Status == episode.StatusDownloaded {
|
||||
ms.DownloadedNum++
|
||||
}
|
||||
}
|
||||
}
|
||||
s.monitorNumCache.Set(item.ID, ms.MonitoredNum)
|
||||
s.downloadNumCache.Set(item.ID, ms.DownloadedNum)
|
||||
}
|
||||
|
||||
res[i] = ms
|
||||
}
|
||||
return res, nil
|
||||
@@ -162,9 +171,32 @@ func (s *Server) DeleteFromWatchlist(c *gin.Context) (interface{}, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "convert")
|
||||
}
|
||||
|
||||
deleteFiles := c.Query("delete_files")
|
||||
if strings.ToLower(deleteFiles) == "true" {
|
||||
//will delete local media file
|
||||
log.Infof("will delete local media files for %d", id)
|
||||
m, err := s.db.GetMedia(id)
|
||||
if err != nil {
|
||||
log.Warnf("get media: %v", err)
|
||||
} else {
|
||||
st, err := s.core.GetStorage(m.StorageID, m.MediaType)
|
||||
if err != nil {
|
||||
log.Warnf("get storage error: %v", err)
|
||||
} else {
|
||||
if err := st.RemoveAll(m.TargetDir); err != nil {
|
||||
log.Warnf("remove all : %v", err)
|
||||
} else {
|
||||
log.Infof("delete media files success: %v", m.TargetDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.db.DeleteMedia(id); err != nil {
|
||||
return nil, errors.Wrap(err, "delete db")
|
||||
}
|
||||
os.RemoveAll(filepath.Join(db.ImgPath, ids)) //delete image related
|
||||
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
12
ui/.metadata
12
ui/.metadata
@@ -4,7 +4,7 @@
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819"
|
||||
revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
@@ -13,11 +13,11 @@ project_type: app
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
|
||||
base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
|
||||
- platform: ios
|
||||
create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
|
||||
base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
|
||||
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||
- platform: macos
|
||||
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||
|
||||
# User provided section
|
||||
|
||||
|
||||
BIN
ui/assets/wechat.jpg
Normal file
BIN
ui/assets/wechat.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
@@ -148,7 +148,7 @@ class _ActivityPageState extends ConsumerState<ActivityPage>
|
||||
},
|
||||
));
|
||||
},
|
||||
error: (err, trace) => Text("$err"),
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator());
|
||||
})
|
||||
],
|
||||
|
||||
118
ui/lib/init_wizard.dart
Normal file
118
ui/lib/init_wizard.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:ui/settings/downloader.dart';
|
||||
import 'package:ui/settings/prowlarr.dart';
|
||||
import 'package:ui/settings/storage.dart';
|
||||
|
||||
class InitWizard extends ConsumerStatefulWidget {
|
||||
const InitWizard({super.key});
|
||||
static final String route = "/init_wizard";
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() {
|
||||
return _InitWizardState();
|
||||
}
|
||||
}
|
||||
|
||||
class _InitWizardState extends ConsumerState<InitWizard> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SelectionArea(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(50),
|
||||
child: ListView(
|
||||
children: [
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
"Polaris 影视追踪下载",
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.only(left: 10, top: 30, bottom: 30),
|
||||
child: Text(
|
||||
"设置向导",
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
),
|
||||
tmdbSetting(),
|
||||
downloaderSetting(),
|
||||
indexerSetting(),
|
||||
storageSetting(),
|
||||
],
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget tmdbSetting() {
|
||||
return ExpansionTile(
|
||||
title: Text(
|
||||
"第一步:TMDB设置",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
childrenPadding: EdgeInsets.only(left: 100, right: 20),
|
||||
initiallyExpanded: true,
|
||||
children: [
|
||||
Container(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Text("TMDB API Key 设置,用来获取各种影视的信息,API Key获取方式参考官网"),
|
||||
),
|
||||
FormBuilder(
|
||||
child: Column(
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
name: "tmdb",
|
||||
decoration: InputDecoration(labelText: "TMDB API Key"),
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: ElevatedButton(onPressed: null, child: Text("保存")),
|
||||
))
|
||||
],
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget indexerSetting() {
|
||||
return ExpansionTile(
|
||||
initiallyExpanded: true,
|
||||
childrenPadding: EdgeInsets.only(left: 100, right: 20),
|
||||
title: Text(
|
||||
"第三步:Prowlarr设置",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
children: [ProwlarrSettingPage()],
|
||||
);
|
||||
}
|
||||
|
||||
Widget downloaderSetting() {
|
||||
return ExpansionTile(
|
||||
childrenPadding: EdgeInsets.only(left: 100, right: 20),
|
||||
initiallyExpanded: true,
|
||||
title: Text("第二步:下载客户端", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
children: [
|
||||
DownloaderSettings(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget storageSetting() {
|
||||
return ExpansionTile(
|
||||
childrenPadding: EdgeInsets.only(left: 100, right: 20),
|
||||
title: Text("第四步:存储设置", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
initiallyExpanded: true,
|
||||
children: [StorageSettings()],
|
||||
);
|
||||
}
|
||||
}
|
||||
354
ui/lib/main.dart
354
ui/lib/main.dart
@@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:ui/activity.dart';
|
||||
import 'package:ui/calendar.dart';
|
||||
import 'package:ui/init_wizard.dart';
|
||||
import 'package:ui/login_page.dart';
|
||||
import 'package:ui/movie_watchlist.dart';
|
||||
import 'package:ui/providers/APIs.dart';
|
||||
@@ -49,81 +49,92 @@ class _MyAppState extends ConsumerState<MyApp> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var padding = isSmallScreen(context) ? 5.0 : 20.0;
|
||||
// GoRouter configuration
|
||||
final shellRoute = ShellRoute(
|
||||
builder: (BuildContext context, GoRouterState state, Widget child) {
|
||||
return SelectionArea(
|
||||
child: MainSkeleton(
|
||||
body: Padding(
|
||||
padding: EdgeInsets.only(left: padding, right: padding, top: 5, bottom: 5),
|
||||
child: child),
|
||||
),
|
||||
final shellRoute = StatefulShellRoute.indexedStack(
|
||||
builder: (BuildContext context, GoRouterState state,
|
||||
StatefulNavigationShell navigationShell) {
|
||||
return MainSkeleton(
|
||||
body: navigationShell,
|
||||
);
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "/",
|
||||
redirect: (context, state) => WelcomePage.routeTv,
|
||||
),
|
||||
GoRoute(
|
||||
path: WelcomePage.routeTv,
|
||||
pageBuilder: (context, state) => buildPageWithDefaultTransition(
|
||||
context: context, state: state, child: const WelcomePage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: WelcomePage.routeMoivie,
|
||||
pageBuilder: (context, state) => buildPageWithDefaultTransition(
|
||||
context: context, state: state, child: const WelcomePage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: TvDetailsPage.route,
|
||||
pageBuilder: (context, state) => buildPageWithDefaultTransition(
|
||||
context: context,
|
||||
state: state,
|
||||
child: TvDetailsPage(seriesId: state.pathParameters['id']!)),
|
||||
),
|
||||
GoRoute(
|
||||
path: MovieDetailsPage.route,
|
||||
pageBuilder: (context, state) => buildPageWithDefaultTransition(
|
||||
context: context,
|
||||
state: state,
|
||||
child: MovieDetailsPage(id: state.pathParameters['id']!)),
|
||||
),
|
||||
GoRoute(
|
||||
path: SearchPage.route,
|
||||
pageBuilder: (context, state) => buildPageWithDefaultTransition(
|
||||
context: context,
|
||||
state: state,
|
||||
child: SearchPage(query: state.uri.queryParameters["query"])),
|
||||
),
|
||||
GoRoute(
|
||||
path: SystemSettingsPage.route,
|
||||
pageBuilder: (context, state) => buildPageWithDefaultTransition(
|
||||
context: context,
|
||||
state: state,
|
||||
child: const SystemSettingsPage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: ActivityPage.route,
|
||||
pageBuilder: (context, state) => buildPageWithDefaultTransition(
|
||||
context: context, state: state, child: const ActivityPage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: SystemPage.route,
|
||||
pageBuilder: (context, state) => buildPageWithDefaultTransition(
|
||||
context: context, state: state, child: const SystemPage()),
|
||||
)
|
||||
branches: [
|
||||
StatefulShellBranch(initialLocation: WelcomePage.routeTv, routes: [
|
||||
GoRoute(
|
||||
path: WelcomePage.routeTv,
|
||||
pageBuilder: (context, state) => buildPageWithDefaultTransition(
|
||||
context: context, state: state, child: const WelcomePage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: TvDetailsPage.route,
|
||||
pageBuilder: (context, state) => buildPageWithDefaultTransition(
|
||||
context: context,
|
||||
state: state,
|
||||
child: TvDetailsPage(seriesId: state.pathParameters['id']!)),
|
||||
),
|
||||
GoRoute(
|
||||
path: SearchPage.route,
|
||||
pageBuilder: (context, state) => buildPageWithDefaultTransition(
|
||||
context: context,
|
||||
state: state,
|
||||
child: SearchPage(query: state.uri.queryParameters["query"])),
|
||||
),
|
||||
]),
|
||||
StatefulShellBranch(initialLocation: WelcomePage.routeMoivie, routes: [
|
||||
GoRoute(
|
||||
path: WelcomePage.routeMoivie,
|
||||
pageBuilder: (context, state) => buildPageWithDefaultTransition(
|
||||
context: context, state: state, child: const WelcomePage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: MovieDetailsPage.route,
|
||||
pageBuilder: (context, state) => buildPageWithDefaultTransition(
|
||||
context: context,
|
||||
state: state,
|
||||
child: MovieDetailsPage(id: state.pathParameters['id']!)),
|
||||
),
|
||||
]),
|
||||
StatefulShellBranch(routes: [
|
||||
GoRoute(
|
||||
path: ActivityPage.route,
|
||||
pageBuilder: (context, state) => buildPageWithDefaultTransition(
|
||||
context: context, state: state, child: const ActivityPage()),
|
||||
),
|
||||
]),
|
||||
StatefulShellBranch(routes: [
|
||||
GoRoute(
|
||||
path: SystemSettingsPage.route,
|
||||
pageBuilder: (context, state) => buildPageWithDefaultTransition(
|
||||
context: context,
|
||||
state: state,
|
||||
child: const SystemSettingsPage()),
|
||||
),
|
||||
]),
|
||||
StatefulShellBranch(routes: [
|
||||
GoRoute(
|
||||
path: SystemPage.route,
|
||||
pageBuilder: (context, state) => buildPageWithDefaultTransition(
|
||||
context: context, state: state, child: const SystemPage()),
|
||||
)
|
||||
]),
|
||||
],
|
||||
);
|
||||
|
||||
final router = GoRouter(
|
||||
navigatorKey: APIs.navigatorKey,
|
||||
initialLocation: WelcomePage.routeTv,
|
||||
routes: [
|
||||
shellRoute,
|
||||
GoRoute(
|
||||
path: "/",
|
||||
redirect: (context, state) => WelcomePage.routeTv,
|
||||
),
|
||||
GoRoute(
|
||||
path: LoginScreen.route,
|
||||
builder: (context, state) => const LoginScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: InitWizard.route,
|
||||
builder: (context, state) => const InitWizard(),
|
||||
)
|
||||
],
|
||||
);
|
||||
@@ -153,7 +164,7 @@ class _MyAppState extends ConsumerState<MyApp> {
|
||||
}
|
||||
|
||||
class MainSkeleton extends StatefulWidget {
|
||||
final Widget body;
|
||||
final StatefulNavigationShell body;
|
||||
const MainSkeleton({super.key, required this.body});
|
||||
|
||||
@override
|
||||
@@ -163,111 +174,17 @@ class MainSkeleton extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MainSkeletonState extends State<MainSkeleton> {
|
||||
var _selectedTab;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var uri = GoRouterState.of(context).uri.toString();
|
||||
if (uri.contains(WelcomePage.routeTv)) {
|
||||
_selectedTab = 0;
|
||||
} else if (uri.contains(WelcomePage.routeMoivie)) {
|
||||
_selectedTab = 1;
|
||||
} else if (uri.contains(ActivityPage.route)) {
|
||||
_selectedTab = 2;
|
||||
} else if (uri.contains(SystemSettingsPage.route)) {
|
||||
_selectedTab = 3;
|
||||
} else if (uri.contains(SystemPage.route)) {
|
||||
_selectedTab = 4;
|
||||
}
|
||||
|
||||
var padding = isSmallScreen(context) ? 5.0 : 20.0;
|
||||
return AdaptiveScaffold(
|
||||
appBarBreakpoint: Breakpoints.standard,
|
||||
appBar: AppBar(
|
||||
// TRY THIS: Try changing the color here to a specific color (to
|
||||
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
|
||||
// change color while the other colors stay the same.
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: TextButton(
|
||||
onPressed: () => context.go(WelcomePage.routeTv),
|
||||
child: const Text(
|
||||
"Polaris",
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(fontSize: 28),
|
||||
),
|
||||
),
|
||||
|
||||
actions: [
|
||||
SearchAnchor(
|
||||
builder: (BuildContext context, SearchController controller) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxWidth: 250, maxHeight: 40),
|
||||
child: Opacity(
|
||||
opacity: 0.8,
|
||||
child: SearchBar(
|
||||
hintText: "搜索...",
|
||||
leading: const Icon(Icons.search),
|
||||
controller: controller,
|
||||
shadowColor: WidgetStateColor.transparent,
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
Theme.of(context).colorScheme.primaryContainer),
|
||||
onSubmitted: (value) => context.go(Uri(
|
||||
path: SearchPage.route,
|
||||
queryParameters: {'query': value}).toString()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}, suggestionsBuilder:
|
||||
(BuildContext context, SearchController controller) {
|
||||
return [Text("dadada")];
|
||||
}),
|
||||
IconButton(
|
||||
onPressed: () => showCalendar(context),
|
||||
icon: Icon(Icons.calendar_month)),
|
||||
MenuAnchor(
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.exit_to_app),
|
||||
child: const Text("登出"),
|
||||
onPressed: () async {
|
||||
await APIs.logout();
|
||||
},
|
||||
),
|
||||
],
|
||||
builder: (context, controller, child) {
|
||||
return TextButton(
|
||||
onPressed: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.account_circle),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
appBar: appBar(),
|
||||
useDrawer: false,
|
||||
selectedIndex: _selectedTab,
|
||||
onSelectedIndexChange: (int index) {
|
||||
setState(() {
|
||||
_selectedTab = index;
|
||||
});
|
||||
if (index == 0) {
|
||||
context.go(WelcomePage.routeTv);
|
||||
} else if (index == 1) {
|
||||
context.go(WelcomePage.routeMoivie);
|
||||
} else if (index == 2) {
|
||||
context.go(ActivityPage.route);
|
||||
} else if (index == 3) {
|
||||
context.go(SystemSettingsPage.route);
|
||||
} else if (index == 4) {
|
||||
context.go(SystemPage.route);
|
||||
}
|
||||
},
|
||||
selectedIndex: widget.body.currentIndex,
|
||||
onSelectedIndexChange: (p0) => widget.body
|
||||
.goBranch(p0, initialLocation: p0 == widget.body.currentIndex),
|
||||
|
||||
destinations: const <NavigationDestination>[
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.live_tv_outlined),
|
||||
@@ -295,11 +212,118 @@ class _MainSkeletonState extends State<MainSkeleton> {
|
||||
label: '系统',
|
||||
),
|
||||
],
|
||||
body: (context) => SafeArea(child: widget.body),
|
||||
body: (context) => SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: padding, right: padding, top: 5, bottom: 5),
|
||||
child: widget.body)),
|
||||
// Define a default secondaryBody.
|
||||
// Override the default secondaryBody during the smallBreakpoint to be
|
||||
// empty. Must use AdaptiveScaffold.emptyBuilder to ensure it is properly
|
||||
// overridden.
|
||||
);
|
||||
}
|
||||
|
||||
showDonate(BuildContext context) {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text("项目开发不易,给开发者加个鸡腿:"),
|
||||
content: SizedBox(
|
||||
width: 350,
|
||||
height: 400,
|
||||
child: Ink.image(
|
||||
fit: BoxFit.fitWidth,
|
||||
image: AssetImage("assets/wechat.jpg"))),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
AppBar appBar() {
|
||||
return AppBar(
|
||||
// TRY THIS: Try changing the color here to a specific color (to
|
||||
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
|
||||
// change color while the other colors stay the same.
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
leading: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TextButton(
|
||||
onPressed: () => context.go(WelcomePage.routeTv),
|
||||
child: const Text(
|
||||
"Polaris",
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(fontSize: 28),
|
||||
),
|
||||
),
|
||||
),
|
||||
leadingWidth: isSmallScreen(context) ? 0 : 190,
|
||||
title: Container(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: SearchAnchor(
|
||||
builder: (BuildContext context, SearchController controller) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxWidth: 250, maxHeight: 40),
|
||||
child: Opacity(
|
||||
opacity: 0.8,
|
||||
child: SearchBar(
|
||||
hintText: "在此搜索...",
|
||||
leading: const Icon(Icons.search),
|
||||
controller: controller,
|
||||
shadowColor: WidgetStateColor.transparent,
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
Theme.of(context).colorScheme.primaryContainer),
|
||||
onSubmitted: (value) => context.go(Uri(
|
||||
path: SearchPage.route,
|
||||
queryParameters: {'query': value}).toString()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}, suggestionsBuilder:
|
||||
(BuildContext context, SearchController controller) {
|
||||
return [Text("dadada")];
|
||||
}),
|
||||
),
|
||||
|
||||
actions: [
|
||||
// IconButton(
|
||||
// onPressed: () => showCalendar(context),
|
||||
// icon: Icon(Icons.calendar_month)),
|
||||
IconButton(
|
||||
onPressed: () => showDonate(context),
|
||||
icon: Icon(
|
||||
Icons.favorite_rounded,
|
||||
color: Colors.red,
|
||||
)),
|
||||
|
||||
MenuAnchor(
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.exit_to_app),
|
||||
child: const Text("登出"),
|
||||
onPressed: () async {
|
||||
await APIs.logout();
|
||||
},
|
||||
),
|
||||
],
|
||||
builder: (context, controller, child) {
|
||||
return TextButton(
|
||||
onPressed: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.account_circle),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,21 +28,22 @@ class _MovieDetailsPageState extends ConsumerState<MovieDetailsPage> {
|
||||
Widget build(BuildContext context) {
|
||||
var seriesDetails = ref.watch(mediaDetailsProvider(widget.id));
|
||||
|
||||
return seriesDetails.when(
|
||||
data: (details) {
|
||||
return ListView(
|
||||
children: [
|
||||
DetailCard(details: details),
|
||||
NestedTabBar(
|
||||
id: widget.id,
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
error: (err, trace) {
|
||||
return Text("$err");
|
||||
},
|
||||
loading: () => const MyProgressIndicator());
|
||||
return SelectionArea(
|
||||
child: seriesDetails.when(
|
||||
data: (details) {
|
||||
return ListView(
|
||||
children: [
|
||||
DetailCard(details: details),
|
||||
NestedTabBar(
|
||||
id: widget.id,
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
error: (err, trace) {
|
||||
return Text("$err");
|
||||
},
|
||||
loading: () => const MyProgressIndicator()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -8,9 +10,12 @@ class APIs {
|
||||
static final _baseUrl = baseUrl();
|
||||
static final searchUrl = "$_baseUrl/api/v1/media/search";
|
||||
static final editMediaUrl = "$_baseUrl/api/v1/media/edit";
|
||||
static final downloadAllUrl = "$_baseUrl/api/v1/media/downloadall/";
|
||||
static final downloadAllEpisodesUrl = "$_baseUrl/api/v1/media/downloadall/";
|
||||
static final downloadAllTvUrl = "$_baseUrl/api/v1/media/download/tv";
|
||||
static final downloadAllMovieUrl = "$_baseUrl/api/v1/media/download/movie";
|
||||
static final settingsUrl = "$_baseUrl/api/v1/setting/do";
|
||||
static final settingsGeneralUrl = "$_baseUrl/api/v1/setting/general";
|
||||
//static final singleSettingUrl = "$_baseUrl/api/v1/setting/";
|
||||
static final watchlistTvUrl = "$_baseUrl/api/v1/media/tv/watchlist";
|
||||
static final watchlistMovieUrl = "$_baseUrl/api/v1/media/movie/watchlist";
|
||||
static final availableTorrentsUrl = "$_baseUrl/api/v1/media/torrents/";
|
||||
@@ -40,6 +45,8 @@ class APIs {
|
||||
static final addImportlistUrl = "$_baseUrl/api/v1/importlist/add";
|
||||
static final deleteImportlistUrl = "$_baseUrl/api/v1/importlist/delete";
|
||||
static final getAllImportlists = "$_baseUrl/api/v1/importlist/";
|
||||
static final prowlarrUrl = "$_baseUrl/api/v1/setting/prowlarr";
|
||||
static final aiConfigUrl = "$_baseUrl/api/v1/setting/ai";
|
||||
|
||||
static final notifierAllUrl = "$_baseUrl/api/v1/notifier/all";
|
||||
static final notifierDeleteUrl = "$_baseUrl/api/v1/notifier/id/";
|
||||
@@ -49,6 +56,11 @@ class APIs {
|
||||
|
||||
static final cronJobUrl = "$_baseUrl/api/v1/setting/cron/trigger";
|
||||
|
||||
static final tvParseUrl = "$_baseUrl/api/v1/setting/parse/tv";
|
||||
static final movieParseUrl = "$_baseUrl/api/v1/setting/parse/movie";
|
||||
|
||||
static final mediaSizeLimiterUrl = "$_baseUrl/api/v1/setting/limiter";
|
||||
|
||||
static const tmdbApiKey = "tmdb_api_key";
|
||||
static const downloadDirKey = "download_dir";
|
||||
|
||||
@@ -113,4 +125,50 @@ class APIs {
|
||||
throw sp.message;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<String>> downloadAllTv() async {
|
||||
var resp = await getDio().get(APIs.downloadAllTvUrl);
|
||||
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
return sp.data == null ? [] : sp.data as List<String>;
|
||||
}
|
||||
|
||||
static Future<List<String>> downloadAllMovies() async {
|
||||
var resp = await getDio().get(APIs.downloadAllMovieUrl);
|
||||
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
return sp.data == null ? [] : sp.data as List<String>;
|
||||
}
|
||||
|
||||
static Future<String> parseTvName(String s) async {
|
||||
var resp = await getDio().post(APIs.tvParseUrl, data: {"s": s});
|
||||
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
JsonEncoder encoder = new JsonEncoder.withIndent(' ');
|
||||
return encoder.convert(sp.data);
|
||||
}
|
||||
|
||||
static Future<String> parseMovieName(String s) async {
|
||||
var resp = await getDio().post(APIs.movieParseUrl, data: {"s": s});
|
||||
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
JsonEncoder encoder = new JsonEncoder.withIndent(' ');
|
||||
return encoder.convert(sp.data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,15 +25,15 @@ var mediaHistoryDataProvider = FutureProvider.autoDispose.family(
|
||||
|
||||
class ActivityData
|
||||
extends AutoDisposeFamilyAsyncNotifier<List<Activity>, String> {
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
FutureOr<List<Activity>> build(String arg) async {
|
||||
if (arg == "active") {
|
||||
//refresh active downloads
|
||||
Timer(const Duration(seconds: 5),
|
||||
ref.invalidateSelf); //Periodically Refresh
|
||||
if (_timer != null) {
|
||||
_timer!.cancel();
|
||||
}
|
||||
|
||||
final dio = await APIs.getDio();
|
||||
final dio = APIs.getDio();
|
||||
var resp =
|
||||
await dio.get(APIs.activityUrl, queryParameters: {"status": arg});
|
||||
final sp = ServerResponse.fromJson(resp.data);
|
||||
@@ -44,6 +44,12 @@ class ActivityData
|
||||
for (final a in sp.data as List) {
|
||||
activities.add(Activity.fromJson(a));
|
||||
}
|
||||
|
||||
if (arg == "active") {
|
||||
//refresh active downloads
|
||||
_timer = Timer(const Duration(seconds: 5),
|
||||
() => ref.invalidateSelf()); //Periodically Refresh
|
||||
}
|
||||
return activities;
|
||||
}
|
||||
|
||||
@@ -73,7 +79,8 @@ class Activity {
|
||||
required this.saved,
|
||||
required this.progress,
|
||||
required this.size,
|
||||
required this.seedRatio});
|
||||
required this.seedRatio,
|
||||
required this.uploadProgress});
|
||||
|
||||
final int? id;
|
||||
final int? mediaId;
|
||||
@@ -86,6 +93,7 @@ class Activity {
|
||||
final int? progress;
|
||||
final int? size;
|
||||
final double seedRatio;
|
||||
final double uploadProgress;
|
||||
|
||||
factory Activity.fromJson(Map<String, dynamic> json) {
|
||||
return Activity(
|
||||
@@ -99,6 +107,7 @@ class Activity {
|
||||
saved: json["saved"],
|
||||
progress: json["progress"],
|
||||
seedRatio: json["seed_ratio"],
|
||||
size: json["size"]);
|
||||
size: json["size"],
|
||||
uploadProgress: json["upload_progress"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ class SeriesDetailData
|
||||
return SeriesDetails.fromJson(rsp.data);
|
||||
}
|
||||
|
||||
Future<void> delete() async {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.delete("${APIs.seriesDetailUrl}$id");
|
||||
Future<void> delete(bool removeFiles ) async {
|
||||
final dio = APIs.getDio();
|
||||
var resp = await dio.delete("${APIs.seriesDetailUrl}$id", queryParameters: {"delete_files": removeFiles});
|
||||
var rsp = ServerResponse.fromJson(resp.data);
|
||||
if (rsp.code != 0) {
|
||||
throw rsp.message;
|
||||
@@ -84,7 +84,7 @@ class SeriesDetailData
|
||||
|
||||
Future<dynamic> downloadall() async {
|
||||
final dio = APIs.getDio();
|
||||
var resp = await dio.get(APIs.downloadAllUrl + id!);
|
||||
var resp = await dio.get(APIs.downloadAllEpisodesUrl + id!);
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
|
||||
@@ -25,10 +25,17 @@ var importlistProvider =
|
||||
AsyncNotifierProvider.autoDispose<ImportListData, List<ImportList>>(
|
||||
ImportListData.new);
|
||||
|
||||
var prowlarrSettingDataProvider =
|
||||
AsyncNotifierProvider.autoDispose<ProwlarrSettingData, ProwlarrSetting>(
|
||||
ProwlarrSettingData.new);
|
||||
|
||||
var aiConfigDataProvider =
|
||||
AsyncNotifierProvider.autoDispose<AIConfigData, AIConfig>(AIConfigData.new);
|
||||
|
||||
class EditSettingData extends AutoDisposeAsyncNotifier<GeneralSetting> {
|
||||
@override
|
||||
FutureOr<GeneralSetting> build() async {
|
||||
final dio = await APIs.getDio();
|
||||
final dio = APIs.getDio();
|
||||
|
||||
var resp = await dio.get(APIs.settingsGeneralUrl);
|
||||
var rrr = ServerResponse.fromJson(resp.data);
|
||||
@@ -503,3 +510,91 @@ class ImportListData extends AutoDisposeAsyncNotifier<List<ImportList>> {
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
class ProwlarrSetting {
|
||||
final bool disabled;
|
||||
final String apiKey;
|
||||
final String url;
|
||||
ProwlarrSetting(
|
||||
{required this.apiKey, required this.url, required this.disabled});
|
||||
factory ProwlarrSetting.fromJson(Map<String, dynamic> json) {
|
||||
return ProwlarrSetting(
|
||||
apiKey: json["api_key"],
|
||||
url: json["url"],
|
||||
disabled: json["disabled"] ?? false);
|
||||
}
|
||||
|
||||
Map<String, dynamic> tojson() =>
|
||||
{"api_key": apiKey, "url": url, "disabled": disabled};
|
||||
}
|
||||
|
||||
class ProwlarrSettingData extends AutoDisposeAsyncNotifier<ProwlarrSetting> {
|
||||
@override
|
||||
FutureOr<ProwlarrSetting> build() async {
|
||||
final dio = APIs.getDio();
|
||||
var resp = await dio.get(APIs.prowlarrUrl);
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
var se = ProwlarrSetting.fromJson(sp.data);
|
||||
return se;
|
||||
}
|
||||
|
||||
Future<void> save(ProwlarrSetting ps) async {
|
||||
final dio = APIs.getDio();
|
||||
var resp = await dio.post(APIs.prowlarrUrl, data: ps.tojson());
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
class AIConfigData extends AutoDisposeAsyncNotifier<AIConfig> {
|
||||
@override
|
||||
FutureOr<AIConfig> build() async {
|
||||
final dio = APIs.getDio();
|
||||
var resp = await dio.get(APIs.aiConfigUrl);
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
return AIConfig.fromJson(sp.data);
|
||||
}
|
||||
|
||||
Future<void> save(AIConfig ai) async {
|
||||
final dio = APIs.getDio();
|
||||
var resp = await dio.post(APIs.aiConfigUrl, data: ai.toJson());
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
class AIConfig {
|
||||
final bool enabled;
|
||||
final String geminiApiKey;
|
||||
final String geminiModelName;
|
||||
|
||||
AIConfig(
|
||||
{required this.enabled,
|
||||
required this.geminiApiKey,
|
||||
required this.geminiModelName});
|
||||
|
||||
factory AIConfig.fromJson(Map<String, dynamic> json) {
|
||||
return AIConfig(
|
||||
enabled: json["enabled"],
|
||||
geminiApiKey: json["gemini_api_key"],
|
||||
geminiModelName: json["gemini_model_name"]);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"enabled": enabled,
|
||||
"gemini_api_key": geminiApiKey,
|
||||
"gemini_model_name": geminiModelName
|
||||
};
|
||||
}
|
||||
|
||||
109
ui/lib/providers/size_limiter.dart
Normal file
109
ui/lib/providers/size_limiter.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:ui/providers/APIs.dart';
|
||||
import 'package:ui/providers/server_response.dart';
|
||||
|
||||
var mediaSizeLimiterDataProvider =
|
||||
AsyncNotifierProvider.autoDispose<MediaSizeLimiterData, MediaSizeLimiter>(
|
||||
MediaSizeLimiterData.new);
|
||||
|
||||
class MediaSizeLimiterData extends AutoDisposeAsyncNotifier<MediaSizeLimiter> {
|
||||
@override
|
||||
FutureOr<MediaSizeLimiter> build() async {
|
||||
final dio = APIs.getDio();
|
||||
var resp = await dio.get(APIs.mediaSizeLimiterUrl);
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
return MediaSizeLimiter.fromJson(sp.data);
|
||||
}
|
||||
|
||||
Future<void> submit(MediaSizeLimiter limiter) async {
|
||||
final dio = APIs.getDio();
|
||||
var resp = await dio.post(APIs.mediaSizeLimiterUrl, data: limiter.toJson());
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
class MediaSizeLimiter {
|
||||
SizeLimiter? tvLimiter;
|
||||
SizeLimiter? movieLimiter;
|
||||
|
||||
MediaSizeLimiter({this.tvLimiter, this.movieLimiter});
|
||||
|
||||
MediaSizeLimiter.fromJson(Map<String, dynamic> json) {
|
||||
tvLimiter = json['tv_limiter'] != null
|
||||
? SizeLimiter.fromJson(json['tv_limiter'])
|
||||
: null;
|
||||
movieLimiter = json['movie_limiter'] != null
|
||||
? SizeLimiter.fromJson(json['movie_limiter'])
|
||||
: null;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
if (tvLimiter != null) {
|
||||
data['tv_limiter'] = tvLimiter!.toJson();
|
||||
}
|
||||
if (movieLimiter != null) {
|
||||
data['movie_limiter'] = movieLimiter!.toJson();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class SizeLimiter {
|
||||
ResLimiter? p720p;
|
||||
ResLimiter? p1080p;
|
||||
ResLimiter? p2160p;
|
||||
|
||||
SizeLimiter({this.p720p, this.p1080p, this.p2160p});
|
||||
|
||||
SizeLimiter.fromJson(Map<String, dynamic> json) {
|
||||
p720p = json['720p'] != null ? ResLimiter.fromJson(json['720p']) : null;
|
||||
p1080p = json['1080p'] != null ? ResLimiter.fromJson(json['1080p']) : null;
|
||||
p2160p = json['2160p'] != null ? ResLimiter.fromJson(json['2160p']) : null;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
if (p720p != null) {
|
||||
data['720p'] = p720p!.toJson();
|
||||
}
|
||||
if (p1080p != null) {
|
||||
data['1080p'] = p1080p!.toJson();
|
||||
}
|
||||
if (p2160p != null) {
|
||||
data['2160p'] = p2160p!.toJson();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class ResLimiter {
|
||||
int? maxSize;
|
||||
int? minSize;
|
||||
int? preferSize;
|
||||
|
||||
ResLimiter({this.maxSize, this.minSize, this.preferSize});
|
||||
|
||||
ResLimiter.fromJson(Map<String, dynamic> json) {
|
||||
maxSize = json['max_size'];
|
||||
minSize = json['min_size'];
|
||||
preferSize = json['prefer_size'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['max_size'] = maxSize;
|
||||
data['min_size'] = minSize;
|
||||
data['prefer_size'] = preferSize;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -108,14 +108,14 @@ class SearchPageData
|
||||
"resolution": resolution,
|
||||
"folder": folder,
|
||||
"download_history_episodes": downloadHistoryEpisodes,
|
||||
"size_min": (limiter.start * 1000*1000).toInt(),
|
||||
"size_max": (limiter.end * 1000*1000).toInt(),
|
||||
"size_min": (limiter.start * 1000 * 1000).toInt(),
|
||||
"size_max": (limiter.end * 1000 * 1000).toInt(),
|
||||
});
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
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,
|
||||
@@ -129,6 +129,7 @@ class SearchPageData
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
ref.invalidate(movieWatchlistDataProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:ui/providers/APIs.dart';
|
||||
import 'package:ui/providers/welcome_data.dart';
|
||||
import 'package:ui/search_page/submit_dialog.dart';
|
||||
import 'package:ui/widgets/progress_indicator.dart';
|
||||
import 'package:ui/widgets/widgets.dart';
|
||||
|
||||
class SearchPage extends ConsumerStatefulWidget {
|
||||
const SearchPage({super.key, this.query});
|
||||
@@ -110,7 +111,7 @@ class _SearchPageState extends ConsumerState<SearchPage> {
|
||||
}
|
||||
return cards;
|
||||
},
|
||||
error: (err, trace) => [Text("$err")],
|
||||
error: (err, trace) => [PoError(msg: "网络错误,请确认TMDB Key正确配置,并且能够正常连接到TMDB网站", err: err)],
|
||||
loading: () => [const MyProgressIndicator()]);
|
||||
|
||||
var f = NotificationListener(
|
||||
|
||||
@@ -94,7 +94,7 @@ class _AuthState extends ConsumerState<AuthSettings> {
|
||||
],
|
||||
));
|
||||
},
|
||||
error: (err, trace) => Text("$err"),
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:quiver/strings.dart';
|
||||
import 'package:ui/providers/settings.dart';
|
||||
import 'package:ui/providers/size_limiter.dart';
|
||||
import 'package:ui/settings/dialog.dart';
|
||||
import 'package:ui/widgets/progress_indicator.dart';
|
||||
import 'package:ui/widgets/widgets.dart';
|
||||
@@ -22,27 +23,33 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var downloadClients = ref.watch(dwonloadClientsProvider);
|
||||
return downloadClients.when(
|
||||
data: (value) => Wrap(
|
||||
children: List.generate(value.length + 1, (i) {
|
||||
if (i < value.length) {
|
||||
var client = value[i];
|
||||
return SettingsCard(
|
||||
onTap: () => showDownloadClientDetails(client),
|
||||
child: Text(client.name ?? ""));
|
||||
}
|
||||
return SettingsCard(
|
||||
onTap: () => showDownloadClientDetails(DownloadClient()),
|
||||
child: const Icon(Icons.add));
|
||||
})),
|
||||
error: (err, trace) => Text("$err"),
|
||||
loading: () => const MyProgressIndicator());
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
downloadClients.when(
|
||||
data: (value) => Wrap(
|
||||
children: List.generate(value.length + 1, (i) {
|
||||
if (i < value.length) {
|
||||
var client = value[i];
|
||||
return SettingsCard(
|
||||
onTap: () => showDownloadClientDetails(client),
|
||||
child: Text(client.name ?? ""));
|
||||
}
|
||||
return SettingsCard(
|
||||
onTap: () => showSelections(),
|
||||
child: const Icon(Icons.add));
|
||||
})),
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator()),
|
||||
Divider(),
|
||||
getSizeLimiterWidget()
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showDownloadClientDetails(DownloadClient client) {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
var _enableAuth = isNotBlank(client.user);
|
||||
String selectImpl = "transmission";
|
||||
|
||||
final body =
|
||||
StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
@@ -53,29 +60,12 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
|
||||
"url": client.url,
|
||||
"user": client.user,
|
||||
"password": client.password,
|
||||
"impl": client.implementation,
|
||||
"remove_completed_downloads": client.removeCompletedDownloads,
|
||||
"remove_failed_downloads": client.removeFailedDownloads,
|
||||
"priority": client.priority.toString(),
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
FormBuilderDropdown<String>(
|
||||
name: "impl",
|
||||
decoration: const InputDecoration(labelText: "类型"),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectImpl = value!;
|
||||
});
|
||||
},
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: "transmission", child: Text("Transmission")),
|
||||
DropdownMenuItem(
|
||||
value: "qbittorrent", child: Text("qBittorrent")),
|
||||
],
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: "name",
|
||||
decoration: const InputDecoration(labelText: "名称"),
|
||||
@@ -90,7 +80,8 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: "priority",
|
||||
decoration: const InputDecoration(labelText: "优先级", helperText: "1-50, 1最高优先级,50最低优先级"),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "优先级", helperText: "1-50, 1最高优先级,50最低优先级"),
|
||||
validator: FormBuilderValidators.integer(),
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction),
|
||||
FormBuilderSwitch(
|
||||
@@ -151,7 +142,7 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
|
||||
return ref.read(dwonloadClientsProvider.notifier).addDownloadClients(
|
||||
DownloadClient(
|
||||
name: values["name"],
|
||||
implementation: values["impl"],
|
||||
implementation: client.implementation,
|
||||
url: values["url"],
|
||||
user: _enableAuth ? values["user"] : null,
|
||||
password: _enableAuth ? values["password"] : null,
|
||||
@@ -163,7 +154,214 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
|
||||
}
|
||||
}
|
||||
|
||||
var title = "下载器";
|
||||
if (client.implementation == "transmission") {
|
||||
title = "Transmission";
|
||||
} else if (client.implementation == "qbittorrent") {
|
||||
title = "qBittorrent";
|
||||
}
|
||||
|
||||
return showSettingDialog(
|
||||
context, "下载器", client.id != null, body, onSubmit, onDelete);
|
||||
context, title, client.id != null, body, onSubmit, onDelete);
|
||||
}
|
||||
|
||||
Future<void> showSelections() {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
content: SizedBox(
|
||||
height: 500,
|
||||
width: 500,
|
||||
child: Wrap(
|
||||
children: [
|
||||
SettingsCard(
|
||||
child: InkWell(
|
||||
child: const Center(
|
||||
child: Text("Transmission"),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
showDownloadClientDetails(DownloadClient(
|
||||
implementation: "transmission",
|
||||
name: "Transmission"));
|
||||
},
|
||||
),
|
||||
),
|
||||
SettingsCard(
|
||||
child: InkWell(
|
||||
child: const Center(
|
||||
child: Text("qBittorrent"),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
showDownloadClientDetails(DownloadClient(
|
||||
implementation: "qbittorrent",
|
||||
name: "qBittorrent"));
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget getSizeLimiterWidget() {
|
||||
var data = ref.watch(mediaSizeLimiterDataProvider);
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, top: 20),
|
||||
child: data.when(
|
||||
data: (value) {
|
||||
return FormBuilder(
|
||||
key: _formKey,
|
||||
initialValue: {
|
||||
"tv_720p_min": toMbString(value.tvLimiter!.p720p!.minSize!),
|
||||
"tv_720p_max": toMbString(value.tvLimiter!.p720p!.maxSize!),
|
||||
"tv_1080p_min": toMbString(value.tvLimiter!.p1080p!.minSize!),
|
||||
"tv_1080p_max": toMbString(value.tvLimiter!.p1080p!.maxSize!),
|
||||
"tv_2160p_min": toMbString(value.tvLimiter!.p2160p!.minSize!),
|
||||
"tv_2160p_max": toMbString(value.tvLimiter!.p2160p!.maxSize!),
|
||||
"movie_720p_min":
|
||||
toMbString(value.movieLimiter!.p720p!.minSize!),
|
||||
"movie_720p_max":
|
||||
toMbString(value.movieLimiter!.p720p!.maxSize!),
|
||||
"movie_1080p_min":
|
||||
toMbString(value.movieLimiter!.p1080p!.minSize!),
|
||||
"movie_1080p_max":
|
||||
toMbString(value.movieLimiter!.p1080p!.maxSize!),
|
||||
"movie_2160p_min":
|
||||
toMbString(value.movieLimiter!.p2160p!.minSize!),
|
||||
"movie_2160p_max":
|
||||
toMbString(value.movieLimiter!.p2160p!.maxSize!),
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"剧集大小限制",
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
Divider(),
|
||||
minMaxRow(" 720p", "tv_720p_min", "tv_720p_max"),
|
||||
minMaxRow("1080p", "tv_1080p_min", "tv_1080p_max"),
|
||||
minMaxRow("2160p", "tv_2160p_min", "tv_2160p_max"),
|
||||
Text(
|
||||
"电影大小限制",
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
Divider(),
|
||||
minMaxRow(" 720p", "movie_720p_min", "movie_720p_max"),
|
||||
minMaxRow("1080p", "movie_1080p_min", "movie_1080p_max"),
|
||||
minMaxRow("2160p", "movie_2160p_min", "movie_2160p_max"),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: LoadingElevatedButton(
|
||||
onPressed: () async {
|
||||
if (_formKey.currentState!.saveAndValidate()) {
|
||||
var values = _formKey.currentState!.value;
|
||||
|
||||
return ref
|
||||
.read(mediaSizeLimiterDataProvider.notifier)
|
||||
.submit(MediaSizeLimiter(
|
||||
tvLimiter: SizeLimiter(
|
||||
p720p: ResLimiter(
|
||||
minSize:
|
||||
toByteInt(values["tv_720p_min"]),
|
||||
maxSize:
|
||||
toByteInt(values["tv_720p_max"])),
|
||||
p1080p: ResLimiter(
|
||||
minSize:
|
||||
toByteInt(values["tv_1080p_min"]),
|
||||
maxSize: toByteInt(
|
||||
values["tv_1080p_max"])),
|
||||
p2160p: ResLimiter(
|
||||
minSize:
|
||||
toByteInt(values["tv_2160p_min"]),
|
||||
maxSize: toByteInt(
|
||||
values["tv_2160p_max"])),
|
||||
),
|
||||
movieLimiter: SizeLimiter(
|
||||
p720p: ResLimiter(
|
||||
minSize: toByteInt(
|
||||
values["movie_720p_min"]),
|
||||
maxSize: toByteInt(
|
||||
values["movie_720p_max"])),
|
||||
p1080p: ResLimiter(
|
||||
minSize: toByteInt(
|
||||
values["movie_1080p_min"]),
|
||||
maxSize: toByteInt(
|
||||
values["movie_1080p_max"])),
|
||||
p2160p: ResLimiter(
|
||||
minSize: toByteInt(
|
||||
values["movie_2160p_min"]),
|
||||
maxSize: toByteInt(
|
||||
values["movie_2160p_max"])),
|
||||
)));
|
||||
} else {
|
||||
throw "validation_error";
|
||||
}
|
||||
},
|
||||
label: Text("保存"),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (err, trace) => Container(),
|
||||
loading: () => const MyProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
Widget minMaxRow(String title, String nameMin, String nameMax) {
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(flex: 2, child: Container()),
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(fontSize: 16),
|
||||
)),
|
||||
Flexible(flex: 1, child: Container()),
|
||||
Flexible(
|
||||
flex: 6,
|
||||
child: FormBuilderTextField(
|
||||
name: nameMin,
|
||||
decoration: InputDecoration(suffixText: "MB", labelText: "最小"),
|
||||
validator: FormBuilderValidators.compose([
|
||||
FormBuilderValidators.required(),
|
||||
FormBuilderValidators.numeric()
|
||||
]),
|
||||
)),
|
||||
Flexible(flex: 1, child: Text(" - ")),
|
||||
Flexible(
|
||||
flex: 6,
|
||||
child: FormBuilderTextField(
|
||||
name: nameMax,
|
||||
decoration: InputDecoration(suffixText: "MB", labelText: "最大"),
|
||||
validator: FormBuilderValidators.compose([
|
||||
FormBuilderValidators.required(),
|
||||
FormBuilderValidators.numeric()
|
||||
]),
|
||||
)),
|
||||
Flexible(flex: 2, child: Container()),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String toMbString(int size) {
|
||||
return (size / 1000 / 1000).toString();
|
||||
}
|
||||
|
||||
int toByteInt(String s) {
|
||||
return int.parse(s) * 1000 * 1000;
|
||||
}
|
||||
|
||||
@@ -23,154 +23,185 @@ class _GeneralState extends ConsumerState<GeneralSettings> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var settings = ref.watch(settingProvider);
|
||||
|
||||
return settings.when(
|
||||
data: (v) {
|
||||
return FormBuilder(
|
||||
key: _formKey, //设置globalKey,用于后面获取FormState
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
initialValue: {
|
||||
"tmdb_api": v.tmdbApiKey,
|
||||
"download_dir": v.downloadDIr,
|
||||
"log_level": v.logLevel,
|
||||
"proxy": v.proxy,
|
||||
"enable_plexmatch": v.enablePlexmatch,
|
||||
"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,
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
name: "tmdb_api",
|
||||
decoration: Commons.requiredTextFieldStyle(
|
||||
text: "TMDB Api Key", icon: const Icon(Icons.key)),
|
||||
//
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: "download_dir",
|
||||
decoration: Commons.requiredTextFieldStyle(
|
||||
text: "下载路径",
|
||||
icon: const Icon(Icons.folder),
|
||||
helperText: "媒体文件临时下载路径,非最终存储路径"),
|
||||
//
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: "proxy",
|
||||
decoration: const InputDecoration(
|
||||
labelText: "代理地址",
|
||||
icon: Icon(Icons.web),
|
||||
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(
|
||||
name: "log_level",
|
||||
decoration: const InputDecoration(
|
||||
labelText: "日志级别",
|
||||
icon: Icon(Icons.file_present_rounded),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: "debug", child: Text("DEBUG")),
|
||||
DropdownMenuItem(value: "info", child: Text("INFO")),
|
||||
DropdownMenuItem(value: "warn", child: Text("WARN")),
|
||||
DropdownMenuItem(value: "error", child: Text("ERROR")),
|
||||
],
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 300,
|
||||
child: FormBuilderSwitch(
|
||||
decoration:
|
||||
const InputDecoration(icon: Icon(Icons.back_hand)),
|
||||
name: "enable_adult",
|
||||
title: const Text("是否显示成人内容")),
|
||||
),
|
||||
SizedBox(
|
||||
width: 300,
|
||||
child: FormBuilderSwitch(
|
||||
decoration:
|
||||
const InputDecoration(icon: Icon(Icons.token)),
|
||||
name: "enable_plexmatch",
|
||||
title: const Text("Plex 刮削支持")),
|
||||
),
|
||||
SizedBox(
|
||||
width: 300,
|
||||
child: FormBuilderSwitch(
|
||||
decoration: const InputDecoration(
|
||||
icon: Icon(Icons.library_books),
|
||||
helperText: "emby/kodi等软件刮削需要"),
|
||||
name: "enable_nfo",
|
||||
title: const Text("nfo 文件支持")),
|
||||
),
|
||||
SizedBox(
|
||||
width: 300,
|
||||
child: FormBuilderSwitch(
|
||||
decoration: const InputDecoration(
|
||||
icon: Icon(Icons.remove_circle)),
|
||||
name: "allow_qiangban",
|
||||
title: const Text("是否下载枪版资源")),
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 28.0),
|
||||
child: ElevatedButton(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Text("保存"),
|
||||
var aiConfig = ref.watch(aiConfigDataProvider);
|
||||
return aiConfig.when(
|
||||
data: (ai) {
|
||||
return settings.when(
|
||||
data: (v) {
|
||||
return FormBuilder(
|
||||
key: _formKey, //设置globalKey,用于后面获取FormState
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
initialValue: {
|
||||
"tmdb_api": v.tmdbApiKey,
|
||||
"download_dir": v.downloadDIr,
|
||||
"log_level": v.logLevel,
|
||||
"proxy": v.proxy,
|
||||
"enable_plexmatch": v.enablePlexmatch,
|
||||
"allow_qiangban": v.allowQiangban,
|
||||
"enable_nfo": v.enableNfo,
|
||||
"enable_adult": v.enableAdult,
|
||||
"tv_naming_format": v.tvNamingFormat,
|
||||
"movie_naming_format": v.movieNamingFormat,
|
||||
"ai_enabled": ai.enabled,
|
||||
"ai_api_key": ai.geminiApiKey,
|
||||
"ai_model_name": ai.geminiModelName
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
name: "tmdb_api",
|
||||
decoration: Commons.requiredTextFieldStyle(
|
||||
text: "TMDB Api Key", icon: const Icon(Icons.key)),
|
||||
//
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: "download_dir",
|
||||
decoration: Commons.requiredTextFieldStyle(
|
||||
text: "下载路径",
|
||||
icon: const Icon(Icons.folder),
|
||||
helperText: "媒体文件临时下载路径,非最终存储路径"),
|
||||
//
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: "proxy",
|
||||
decoration: const InputDecoration(
|
||||
labelText: "代理地址",
|
||||
icon: Icon(Icons.web),
|
||||
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(
|
||||
name: "log_level",
|
||||
decoration: const InputDecoration(
|
||||
labelText: "日志级别",
|
||||
icon: Icon(Icons.file_present_rounded),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: "debug", child: Text("DEBUG")),
|
||||
DropdownMenuItem(
|
||||
value: "info", child: Text("INFO")),
|
||||
DropdownMenuItem(
|
||||
value: "warn", child: Text("WARN")),
|
||||
DropdownMenuItem(
|
||||
value: "error", child: Text("ERROR")),
|
||||
],
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
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"],
|
||||
logLevel: values["log_level"],
|
||||
proxy: values["proxy"],
|
||||
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("更新成功"));
|
||||
showLoadingWithFuture(f);
|
||||
}
|
||||
}),
|
||||
),
|
||||
SizedBox(
|
||||
width: 300,
|
||||
child: FormBuilderSwitch(
|
||||
decoration: const InputDecoration(
|
||||
icon: Icon(Icons.back_hand)),
|
||||
name: "enable_adult",
|
||||
title: const Text("是否显示成人内容")),
|
||||
),
|
||||
SizedBox(
|
||||
width: 300,
|
||||
child: FormBuilderSwitch(
|
||||
decoration:
|
||||
const InputDecoration(icon: Icon(Icons.token)),
|
||||
name: "enable_plexmatch",
|
||||
title: const Text("Plex 刮削支持")),
|
||||
),
|
||||
SizedBox(
|
||||
width: 300,
|
||||
child: FormBuilderSwitch(
|
||||
decoration: const InputDecoration(
|
||||
icon: Icon(Icons.library_books),
|
||||
helperText: "emby/kodi等软件刮削需要"),
|
||||
name: "enable_nfo",
|
||||
title: const Text("nfo 文件支持")),
|
||||
),
|
||||
SizedBox(
|
||||
width: 300,
|
||||
child: FormBuilderSwitch(
|
||||
decoration: const InputDecoration(
|
||||
icon: Icon(Icons.remove_circle)),
|
||||
name: "allow_qiangban",
|
||||
title: const Text("是否下载枪版资源")),
|
||||
),
|
||||
Divider(),
|
||||
FormBuilderSwitch(
|
||||
name: "ai_enabled", title: Text("开启Gemini AI集成")),
|
||||
FormBuilderTextField(
|
||||
name: "ai_model_name",
|
||||
decoration: InputDecoration(labelText: "Gemini 模型名称"),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: "ai_api_key",
|
||||
decoration:
|
||||
InputDecoration(labelText: "Gemini Api Key"),
|
||||
),
|
||||
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!.saveAndValidate()) {
|
||||
var values = _formKey.currentState!.value;
|
||||
ref.read(aiConfigDataProvider.notifier).save(
|
||||
AIConfig(
|
||||
enabled: values["ai_enabled"],
|
||||
geminiApiKey: values["ai_api_key"],
|
||||
geminiModelName: values["ai_model_name"]));
|
||||
var f = ref
|
||||
.read(settingProvider.notifier)
|
||||
.updateSettings(GeneralSetting(
|
||||
tmdbApiKey: values["tmdb_api"],
|
||||
downloadDIr: values["download_dir"],
|
||||
logLevel: values["log_level"],
|
||||
proxy: values["proxy"],
|
||||
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("更新成功"));
|
||||
showLoadingWithFuture(f);
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
);
|
||||
},
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator());
|
||||
},
|
||||
error: (err, trace) => Text("$err"),
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:ui/providers/settings.dart';
|
||||
import 'package:ui/settings/dialog.dart';
|
||||
import 'package:ui/widgets/progress_indicator.dart';
|
||||
import 'package:ui/widgets/widgets.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class Importlist extends ConsumerStatefulWidget {
|
||||
const Importlist({super.key});
|
||||
@@ -31,11 +32,11 @@ class _ImportlistState extends ConsumerState<Importlist> {
|
||||
child: Text(indexer.name ?? ""));
|
||||
}
|
||||
return SettingsCard(
|
||||
onTap: () => showImportlistDetails(ImportList()),
|
||||
onTap: () => showSelections(),
|
||||
child: const Icon(Icons.add));
|
||||
}),
|
||||
),
|
||||
error: (err, trace) => Text("$err"),
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator());
|
||||
}
|
||||
|
||||
@@ -55,24 +56,16 @@ class _ImportlistState extends ConsumerState<Importlist> {
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
FormBuilderDropdown(
|
||||
name: "type",
|
||||
decoration: const InputDecoration(
|
||||
labelText: "类型",
|
||||
hintText:
|
||||
"Plex Watchlist: https://support.plex.tv/articles/universal-watchlist/"),
|
||||
items: const [
|
||||
DropdownMenuItem(value: "plex", child: Text("Plex Watchlist")),
|
||||
],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedType = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
_selectedType == "plex"
|
||||
? const Text(
|
||||
"Plex Watchlist: https://support.plex.tv/articles/universal-watchlist/")
|
||||
list.type == "plex"
|
||||
? Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: InkWell(
|
||||
onTap: () => launchUrl(Uri.parse(
|
||||
"https://support.plex.tv/articles/universal-watchlist/")),
|
||||
child: const Text(
|
||||
"https://support.plex.tv/articles/universal-watchlist/"),
|
||||
),
|
||||
)
|
||||
: const Text(""),
|
||||
FormBuilderTextField(
|
||||
name: "name",
|
||||
@@ -141,7 +134,42 @@ class _ImportlistState extends ConsumerState<Importlist> {
|
||||
}
|
||||
}
|
||||
|
||||
var title = "监控列表";
|
||||
if (list.type == "plex") {
|
||||
title = "Plex Watchlist";
|
||||
}
|
||||
|
||||
return showSettingDialog(
|
||||
context, "监控列表", list.id != null, body, onSubmit, onDelete);
|
||||
context, title, list.id != null, body, onSubmit, onDelete);
|
||||
}
|
||||
|
||||
Future<void> showSelections() {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
content: SizedBox(
|
||||
height: 500,
|
||||
width: 500,
|
||||
child: Wrap(
|
||||
children: [
|
||||
SettingsCard(
|
||||
child: InkWell(
|
||||
child: const Center(
|
||||
child: Text("Plex Watchlist"),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
showImportlistDetails(
|
||||
ImportList(type: "plex", name: "PlexWatchlist1"));
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class _IndexerState extends ConsumerState<IndexerSettings> {
|
||||
child: const Icon(Icons.add));
|
||||
}),
|
||||
),
|
||||
error: (err, trace) => Text("$err"),
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator());
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class _NotifierState extends ConsumerState<NotifierSettings> {
|
||||
child: const Icon(Icons.add));
|
||||
}),
|
||||
),
|
||||
error: (err, trace) => Text("$err"),
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator());
|
||||
}
|
||||
|
||||
|
||||
86
ui/lib/settings/prowlarr.dart
Normal file
86
ui/lib/settings/prowlarr.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:ui/providers/settings.dart';
|
||||
import 'package:ui/widgets/progress_indicator.dart';
|
||||
import 'package:ui/widgets/utils.dart';
|
||||
import 'package:ui/widgets/widgets.dart';
|
||||
|
||||
class ProwlarrSettingPage extends ConsumerStatefulWidget {
|
||||
const ProwlarrSettingPage({super.key});
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() {
|
||||
return ProwlarrSettingState();
|
||||
}
|
||||
}
|
||||
|
||||
class ProwlarrSettingState extends ConsumerState<ProwlarrSettingPage> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var ps = ref.watch(prowlarrSettingDataProvider);
|
||||
return ps.when(
|
||||
data: (v) => FormBuilder(
|
||||
key: _formKey, //设置globalKey,用于后面获取FormState
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
initialValue: {
|
||||
"api_key": v.apiKey,
|
||||
"url": v.url,
|
||||
"disabled": v.disabled
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
name: "url",
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Prowlarr URL",
|
||||
icon: Icon(Icons.web),
|
||||
hintText: "http://10.0.0.8:9696"),
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: "api_key",
|
||||
decoration: const InputDecoration(
|
||||
labelText: "API Key",
|
||||
icon: Icon(Icons.key),
|
||||
helperText: "Prowlarr 设置 -> 通用 -> API 密钥"),
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
FormBuilderSwitch(
|
||||
name: "disabled",
|
||||
title: const Text("禁用 Prowlarr"),
|
||||
decoration:
|
||||
InputDecoration(icon: Icon(Icons.do_not_disturb)),
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.saveAndValidate()) {
|
||||
var values = _formKey.currentState!.value;
|
||||
var f = ref
|
||||
.read(prowlarrSettingDataProvider.notifier)
|
||||
.save(ProwlarrSetting(
|
||||
apiKey: values["api_key"],
|
||||
url: values["url"],
|
||||
disabled: values["disabled"]))
|
||||
.then((v) => showSnakeBar("更新成功"));
|
||||
showLoadingWithFuture(f);
|
||||
}
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Text("保存"),
|
||||
)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator());
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:ui/settings/general.dart';
|
||||
import 'package:ui/settings/importlist.dart';
|
||||
import 'package:ui/settings/indexer.dart';
|
||||
import 'package:ui/settings/notifier.dart';
|
||||
import 'package:ui/settings/prowlarr.dart';
|
||||
import 'package:ui/settings/storage.dart';
|
||||
|
||||
class SystemSettingsPage extends ConsumerStatefulWidget {
|
||||
@@ -21,17 +22,19 @@ class SystemSettingsPage extends ConsumerStatefulWidget {
|
||||
class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
return SelectionArea(
|
||||
child: ListView(
|
||||
children: [
|
||||
getExpansionTile("常规", const GeneralSettings()),
|
||||
getExpansionTile("索引器", const IndexerSettings()),
|
||||
getExpansionTile("Prowlarr 设置", const ProwlarrSettingPage()),
|
||||
getExpansionTile("下载器", const DownloaderSettings()),
|
||||
getExpansionTile("存储", const StorageSettings()),
|
||||
getExpansionTile("通知客户端", const NotifierSettings()),
|
||||
getExpansionTile("监控列表", const Importlist()),
|
||||
getExpansionTile("认证", const AuthSettings())
|
||||
],
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
Widget getExpansionTile(String name, Widget body) {
|
||||
|
||||
@@ -31,18 +31,17 @@ class _StorageState extends ConsumerState<StorageSettings> {
|
||||
child: Text(storage.name ?? ""));
|
||||
}
|
||||
return SettingsCard(
|
||||
onTap: () => showStorageDetails(Storage()),
|
||||
onTap: () => showSelections(),
|
||||
child: const Icon(Icons.add));
|
||||
}),
|
||||
),
|
||||
error: (err, trace) => Text("$err"),
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator());
|
||||
}
|
||||
|
||||
Future<void> showStorageDetails(Storage s) {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
|
||||
String selectImpl = s.implementation == null ? "local" : s.implementation!;
|
||||
final widgets =
|
||||
StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
return FormBuilder(
|
||||
@@ -50,7 +49,6 @@ class _StorageState extends ConsumerState<StorageSettings> {
|
||||
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.tvPath,
|
||||
@@ -65,27 +63,6 @@ class _StorageState extends ConsumerState<StorageSettings> {
|
||||
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,
|
||||
@@ -93,15 +70,16 @@ class _StorageState extends ConsumerState<StorageSettings> {
|
||||
decoration: const InputDecoration(labelText: "名称"),
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
selectImpl != "local"
|
||||
s.implementation == "webdav" || s.implementation == "alist"
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
name: "url",
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
decoration:
|
||||
const InputDecoration(labelText: "Webdav地址"),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "网址",
|
||||
hintText: "https://abc.somewebsite.com/"),
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
@@ -115,13 +93,15 @@ class _StorageState extends ConsumerState<StorageSettings> {
|
||||
decoration: const InputDecoration(labelText: "密码"),
|
||||
obscureText: true,
|
||||
),
|
||||
FormBuilderCheckbox(
|
||||
name: "change_file_hash",
|
||||
title: const Text(
|
||||
"上传时更改文件哈希",
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
s.implementation == "webdav"
|
||||
? FormBuilderCheckbox(
|
||||
name: "change_file_hash",
|
||||
title: const Text(
|
||||
"上传时更改文件哈希",
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
],
|
||||
)
|
||||
: Container(),
|
||||
@@ -145,7 +125,7 @@ class _StorageState extends ConsumerState<StorageSettings> {
|
||||
final values = _formKey.currentState!.value;
|
||||
return ref.read(storageSettingProvider.notifier).addStorage(Storage(
|
||||
name: values["name"],
|
||||
implementation: selectImpl,
|
||||
implementation: s.implementation,
|
||||
tvPath: values["tv_path"],
|
||||
moviePath: values["movie_path"],
|
||||
settings: {
|
||||
@@ -167,7 +147,70 @@ class _StorageState extends ConsumerState<StorageSettings> {
|
||||
return ref.read(storageSettingProvider.notifier).deleteStorage(s.id!);
|
||||
}
|
||||
|
||||
var title = "存储";
|
||||
if (s.implementation == "local") {
|
||||
title = "本地存储";
|
||||
} else if (s.implementation == "webdav") {
|
||||
title = "webdav 存储";
|
||||
} else if (s.implementation == "alist") {
|
||||
title = "Alist 存储";
|
||||
}
|
||||
|
||||
return showSettingDialog(
|
||||
context, '存储', s.id != null, widgets, onSubmit, onDelete);
|
||||
context, title, s.id != null, widgets, onSubmit, onDelete);
|
||||
}
|
||||
|
||||
Future<void> showSelections() {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
content: SizedBox(
|
||||
height: 500,
|
||||
width: 500,
|
||||
child: Wrap(
|
||||
children: [
|
||||
SettingsCard(
|
||||
child: InkWell(
|
||||
child: const Center(
|
||||
child: Text("本地存储"),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
showStorageDetails(
|
||||
Storage(implementation: "local", name: "本地存储1"));
|
||||
},
|
||||
),
|
||||
),
|
||||
SettingsCard(
|
||||
child: InkWell(
|
||||
child: const Center(
|
||||
child: Text("webdav"),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
showStorageDetails(
|
||||
Storage(implementation: "webdav", name: "webdav1"));
|
||||
},
|
||||
),
|
||||
),
|
||||
SettingsCard(
|
||||
child: InkWell(
|
||||
child: const Center(
|
||||
child: Text("Alist"),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
showStorageDetails(
|
||||
Storage(implementation: "alist", name: "Alist1"));
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ class _SystemPageState extends ConsumerState<SystemPage> {
|
||||
Widget build(BuildContext context) {
|
||||
final logs = ref.watch(logFileDataProvider);
|
||||
final about = ref.watch(aboutDataProvider);
|
||||
return SingleChildScrollView(
|
||||
return SelectionArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
ExpansionTile(
|
||||
@@ -55,7 +56,7 @@ class _SystemPageState extends ConsumerState<SystemPage> {
|
||||
]);
|
||||
}));
|
||||
},
|
||||
error: (err, trace) => Text("$err"),
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator())
|
||||
],
|
||||
),
|
||||
@@ -110,7 +111,7 @@ class _SystemPageState extends ConsumerState<SystemPage> {
|
||||
]),
|
||||
]);
|
||||
},
|
||||
error: (err, trace) => Text("$err"),
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator())
|
||||
],
|
||||
),
|
||||
@@ -184,12 +185,12 @@ class _SystemPageState extends ConsumerState<SystemPage> {
|
||||
],
|
||||
);
|
||||
},
|
||||
error: (err, trace) => Text("$err"),
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator())
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,156 +34,157 @@ class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
var seriesDetails = ref.watch(mediaDetailsProvider(widget.seriesId));
|
||||
return seriesDetails.when(
|
||||
data: (details) {
|
||||
Map<int, List<DataRow>> m = {};
|
||||
for (final ep in details.episodes!) {
|
||||
var row = DataRow(cells: [
|
||||
DataCell(Text("${ep.episodeNumber}")),
|
||||
DataCell(Text("${ep.title}")),
|
||||
DataCell(Opacity(
|
||||
opacity: 0.5,
|
||||
child: Text(ep.airDate ?? "-"),
|
||||
)),
|
||||
DataCell(
|
||||
Opacity(
|
||||
opacity: 0.7,
|
||||
child: ep.status == "downloading"
|
||||
? const IconButton(
|
||||
tooltip: "下载中",
|
||||
onPressed: null,
|
||||
icon: Icon(Icons.downloading))
|
||||
: (ep.status == "downloaded"
|
||||
return SelectionArea(
|
||||
child: seriesDetails.when(
|
||||
data: (details) {
|
||||
Map<int, List<DataRow>> m = {};
|
||||
for (final ep in details.episodes!) {
|
||||
var row = DataRow(cells: [
|
||||
DataCell(Text("${ep.episodeNumber}")),
|
||||
DataCell(Text("${ep.title}")),
|
||||
DataCell(Opacity(
|
||||
opacity: 0.5,
|
||||
child: Text(ep.airDate ?? "-"),
|
||||
)),
|
||||
DataCell(
|
||||
Opacity(
|
||||
opacity: 0.7,
|
||||
child: ep.status == "downloading"
|
||||
? const IconButton(
|
||||
tooltip: "已下载",
|
||||
tooltip: "下载中",
|
||||
onPressed: null,
|
||||
icon: Icon(Icons.download_done))
|
||||
: (ep.monitored == true
|
||||
? IconButton(
|
||||
tooltip: "监控中",
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(mediaDetailsProvider(
|
||||
widget.seriesId)
|
||||
.notifier)
|
||||
.changeMonitoringStatus(
|
||||
ep.id!, false);
|
||||
},
|
||||
icon: const Icon(Icons.alarm))
|
||||
: Opacity(
|
||||
opacity: 0.7,
|
||||
child: IconButton(
|
||||
tooltip: "未监控",
|
||||
icon: Icon(Icons.downloading))
|
||||
: (ep.status == "downloaded"
|
||||
? const IconButton(
|
||||
tooltip: "已下载",
|
||||
onPressed: null,
|
||||
icon: Icon(Icons.download_done))
|
||||
: (ep.monitored == true
|
||||
? IconButton(
|
||||
tooltip: "监控中",
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(mediaDetailsProvider(
|
||||
widget.seriesId)
|
||||
.notifier)
|
||||
.changeMonitoringStatus(
|
||||
ep.id!, true);
|
||||
ep.id!, false);
|
||||
},
|
||||
icon: const Icon(Icons.alarm_off)),
|
||||
)))),
|
||||
),
|
||||
DataCell(Row(
|
||||
children: [
|
||||
LoadingIconButton(
|
||||
tooltip: "搜索下载对应剧集",
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(
|
||||
mediaDetailsProvider(widget.seriesId).notifier)
|
||||
.searchAndDownload(widget.seriesId,
|
||||
ep.seasonNumber!, ep.episodeNumber!)
|
||||
.then((v) => showSnakeBar("开始下载: $v"));
|
||||
},
|
||||
icon: Icons.download),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
icon: const Icon(Icons.alarm))
|
||||
: Opacity(
|
||||
opacity: 0.7,
|
||||
child: IconButton(
|
||||
tooltip: "未监控",
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(mediaDetailsProvider(
|
||||
widget.seriesId)
|
||||
.notifier)
|
||||
.changeMonitoringStatus(
|
||||
ep.id!, true);
|
||||
},
|
||||
icon: const Icon(Icons.alarm_off)),
|
||||
)))),
|
||||
),
|
||||
Tooltip(
|
||||
message: "查看可用资源",
|
||||
child: IconButton(
|
||||
onPressed: () => showAvailableTorrents(widget.seriesId,
|
||||
ep.seasonNumber ?? 0, ep.episodeNumber ?? 0),
|
||||
icon: const Icon(Icons.manage_search)),
|
||||
)
|
||||
],
|
||||
))
|
||||
]);
|
||||
|
||||
if (m[ep.seasonNumber] == null) {
|
||||
m[ep.seasonNumber!] = List.empty(growable: true);
|
||||
}
|
||||
m[ep.seasonNumber!]!.add(row);
|
||||
}
|
||||
List<ExpansionTile> list = List.empty(growable: true);
|
||||
for (final k in m.keys.toList().reversed) {
|
||||
final seasonEpisodes = DataTable(columns: [
|
||||
const DataColumn(label: Text("#")),
|
||||
const DataColumn(
|
||||
label: Text("标题"),
|
||||
),
|
||||
const DataColumn(label: Text("播出时间")),
|
||||
const DataColumn(label: Text("状态")),
|
||||
DataColumn(
|
||||
label: Row(
|
||||
children: [
|
||||
LoadingIconButton(
|
||||
tooltip: "搜索下载全部剧集",
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(
|
||||
mediaDetailsProvider(widget.seriesId).notifier)
|
||||
.searchAndDownload(widget.seriesId, k, 0)
|
||||
.then((v) => showSnakeBar("开始下载: $v"));
|
||||
//showLoadingWithFuture(f);
|
||||
},
|
||||
icon: Icons.download),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Tooltip(
|
||||
message: "查看可用资源",
|
||||
child: IconButton(
|
||||
onPressed: () =>
|
||||
showAvailableTorrents(widget.seriesId, k, 0),
|
||||
icon: const Icon(Icons.manage_search)),
|
||||
)
|
||||
],
|
||||
))
|
||||
], rows: m[k]!);
|
||||
|
||||
var seasonList = ExpansionTile(
|
||||
tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
|
||||
//childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
|
||||
initiallyExpanded: false,
|
||||
title: k == 0 ? const Text("特别篇") : Text("第 $k 季"),
|
||||
expandedCrossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
screenWidth < 600
|
||||
? SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: seasonEpisodes,
|
||||
DataCell(Row(
|
||||
children: [
|
||||
LoadingIconButton(
|
||||
tooltip: "搜索下载对应剧集",
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(mediaDetailsProvider(widget.seriesId)
|
||||
.notifier)
|
||||
.searchAndDownload(widget.seriesId,
|
||||
ep.seasonNumber!, ep.episodeNumber!)
|
||||
.then((v) => showSnakeBar("开始下载: $v"));
|
||||
},
|
||||
icon: Icons.download),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Tooltip(
|
||||
message: "查看可用资源",
|
||||
child: IconButton(
|
||||
onPressed: () => showAvailableTorrents(
|
||||
widget.seriesId,
|
||||
ep.seasonNumber ?? 0,
|
||||
ep.episodeNumber ?? 0),
|
||||
icon: const Icon(Icons.manage_search)),
|
||||
)
|
||||
: seasonEpisodes
|
||||
],
|
||||
);
|
||||
list.add(seasonList);
|
||||
}
|
||||
return ListView(
|
||||
children: [
|
||||
DetailCard(details: details),
|
||||
Column(
|
||||
children: list,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
error: (err, trace) {
|
||||
return Text("$err");
|
||||
},
|
||||
loading: () => const MyProgressIndicator());
|
||||
],
|
||||
))
|
||||
]);
|
||||
|
||||
if (m[ep.seasonNumber] == null) {
|
||||
m[ep.seasonNumber!] = List.empty(growable: true);
|
||||
}
|
||||
m[ep.seasonNumber!]!.add(row);
|
||||
}
|
||||
List<ExpansionTile> list = List.empty(growable: true);
|
||||
for (final k in m.keys.toList().reversed) {
|
||||
final seasonEpisodes = DataTable(columns: [
|
||||
const DataColumn(label: Text("#")),
|
||||
const DataColumn(
|
||||
label: Text("标题"),
|
||||
),
|
||||
const DataColumn(label: Text("播出时间")),
|
||||
const DataColumn(label: Text("状态")),
|
||||
DataColumn(
|
||||
label: Row(
|
||||
children: [
|
||||
LoadingIconButton(
|
||||
tooltip: "搜索下载全部剧集",
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(mediaDetailsProvider(widget.seriesId)
|
||||
.notifier)
|
||||
.searchAndDownload(widget.seriesId, k, 0)
|
||||
.then((v) => showSnakeBar("开始下载: $v"));
|
||||
//showLoadingWithFuture(f);
|
||||
},
|
||||
icon: Icons.download),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Tooltip(
|
||||
message: "查看可用资源",
|
||||
child: IconButton(
|
||||
onPressed: () =>
|
||||
showAvailableTorrents(widget.seriesId, k, 0),
|
||||
icon: const Icon(Icons.manage_search)),
|
||||
)
|
||||
],
|
||||
))
|
||||
], rows: m[k]!);
|
||||
|
||||
var seasonList = ExpansionTile(
|
||||
tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
|
||||
//childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
|
||||
initiallyExpanded: false,
|
||||
title: k == 0 ? const Text("特别篇") : Text("第 $k 季"),
|
||||
expandedCrossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
screenWidth < 600
|
||||
? SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: seasonEpisodes,
|
||||
)
|
||||
: seasonEpisodes
|
||||
],
|
||||
);
|
||||
list.add(seasonList);
|
||||
}
|
||||
return ListView(
|
||||
children: [
|
||||
DetailCard(details: details),
|
||||
Column(
|
||||
children: list,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator()));
|
||||
}
|
||||
|
||||
Future<void> showAvailableTorrents(String id, int season, int episode) {
|
||||
|
||||
@@ -1,65 +1,246 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:ui/movie_watchlist.dart';
|
||||
import 'package:ui/providers/APIs.dart';
|
||||
import 'package:ui/providers/welcome_data.dart';
|
||||
import 'package:ui/tv_details.dart';
|
||||
import 'package:ui/widgets/progress_indicator.dart';
|
||||
import 'package:ui/widgets/utils.dart';
|
||||
import 'package:ui/widgets/widgets.dart';
|
||||
|
||||
class WelcomePage extends ConsumerWidget {
|
||||
class WelcomePage extends ConsumerStatefulWidget {
|
||||
const WelcomePage({super.key});
|
||||
static const routeTv = "/series";
|
||||
static const routeMoivie = "/movies";
|
||||
|
||||
const WelcomePage({super.key});
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() {
|
||||
return WelcomePageState();
|
||||
}
|
||||
}
|
||||
|
||||
class WelcomePageState extends ConsumerState<WelcomePage> {
|
||||
//WelcomePageState({super.key});
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
bool onlyShowUnfinished = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
var uri = GoRouterState.of(context).uri.toString();
|
||||
|
||||
AsyncValue<List<MediaDetail>> data;
|
||||
if (uri == routeMoivie) {
|
||||
if (uri == WelcomePage.routeMoivie) {
|
||||
data = ref.watch(movieWatchlistDataProvider);
|
||||
} else {
|
||||
data = ref.watch(tvWatchlistDataProvider);
|
||||
}
|
||||
|
||||
return switch (data) {
|
||||
AsyncData(:final value) => SingleChildScrollView(
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
spacing: isSmallScreen(context) ? 0 : 10,
|
||||
runSpacing: isSmallScreen(context) ? 10 : 20,
|
||||
children: value.isEmpty
|
||||
? [
|
||||
Container(
|
||||
height: MediaQuery.of(context).size.height * 0.6,
|
||||
return SelectionArea(
|
||||
child: Stack(
|
||||
//alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
() {
|
||||
return data.when(
|
||||
data: (value) {
|
||||
if (value.isEmpty) {
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height * 0.7,
|
||||
alignment: Alignment.center,
|
||||
child: const Text(
|
||||
"啥都没有...",
|
||||
style: TextStyle(fontSize: 16),
|
||||
))
|
||||
]
|
||||
: List.generate(value.length, (i) {
|
||||
final item = value[i];
|
||||
return MediaCard(item: item);
|
||||
}),
|
||||
),
|
||||
),
|
||||
_ => const MyProgressIndicator(),
|
||||
};
|
||||
));
|
||||
}
|
||||
|
||||
if (onlyShowUnfinished) {
|
||||
value = value
|
||||
.where((v) => v.downloadedNum != v.monitoredNum)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: cardWidth(context) ,
|
||||
childAspectRatio: 0.55,
|
||||
mainAxisSpacing: isSmallScreen(context) ? 10 : 20,
|
||||
crossAxisSpacing: isSmallScreen(context) ? 0 : 10),
|
||||
itemCount: value.length,
|
||||
itemBuilder: (context, index) =>
|
||||
MediaCard(item: value[index]),
|
||||
);
|
||||
},
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator());
|
||||
}(),
|
||||
getMoreButtonAndActions(uri)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget getMoreButtonAndActions(String uri) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(child: Container()),
|
||||
Column(
|
||||
children: [
|
||||
Expanded(child: Container()),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: MenuAnchor(
|
||||
style: MenuStyle(
|
||||
alignment: Alignment.topLeft,
|
||||
backgroundColor: WidgetStatePropertyAll(Theme.of(context)
|
||||
.colorScheme
|
||||
.inversePrimary
|
||||
.withOpacity(0.7)),
|
||||
),
|
||||
menuChildren: [parseName(), onlyUnfinished(), refreshAll(uri)],
|
||||
builder: (context, controller, child) {
|
||||
return Opacity(
|
||||
opacity: 0.7,
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.more_horiz),
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget onlyUnfinished() {
|
||||
return CheckboxListTile(
|
||||
value: onlyShowUnfinished,
|
||||
onChanged: (b) {
|
||||
setState(() {
|
||||
onlyShowUnfinished = b!;
|
||||
});
|
||||
},
|
||||
title: const Text(
|
||||
"未完成",
|
||||
style: TextStyle(fontSize: 16),
|
||||
softWrap: false,
|
||||
),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
);
|
||||
}
|
||||
|
||||
Widget refreshAll(String uri) {
|
||||
return LoadingListTile(
|
||||
icon: Icons.refresh,
|
||||
text: "全部更新",
|
||||
onPressed: () async {
|
||||
if (uri == WelcomePage.routeMoivie) {
|
||||
await APIs.downloadAllMovies().then((v) {
|
||||
if (v.isNotEmpty) {
|
||||
showSnakeBar("开始下载电影:$v");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await APIs.downloadAllTv().then((v) {
|
||||
if (v.isNotEmpty) {
|
||||
showSnakeBar("开始下载剧集:$v");
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget parseName() {
|
||||
return ListTile(
|
||||
leading: Icon(Icons.calculate),
|
||||
title: Text("测试解析"),
|
||||
onTap: () => _showNameParsingDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
bool isSmallScreen(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
return screenWidth < 600;
|
||||
}
|
||||
Future<void> _showNameParsingDialog() async {
|
||||
final resultController = TextEditingController();
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('测试名称解析'),
|
||||
content: SizedBox(
|
||||
width: 500,
|
||||
height: 400,
|
||||
child: FormBuilder(
|
||||
key: _formKey,
|
||||
initialValue: {"name": "", "type": "tv"},
|
||||
child: Column(
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
name: "name",
|
||||
decoration: InputDecoration(labelText: "要解析的名字"),
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
FormBuilderDropdown(
|
||||
name: "type",
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: "tv",
|
||||
child: const Text("电视剧"),
|
||||
),
|
||||
DropdownMenuItem(value: "movie", child: const Text("电影"))
|
||||
],
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: LoadingTextButton(
|
||||
onPressed: () async {
|
||||
if (_formKey.currentState!.saveAndValidate()) {
|
||||
final values = _formKey.currentState!.value;
|
||||
//print(values);
|
||||
if (values["type"] == "tv") {
|
||||
var s = await APIs.parseTvName(values["name"]);
|
||||
resultController.text = s;
|
||||
} else {
|
||||
var s =
|
||||
await APIs.parseMovieName(values["name"]);
|
||||
resultController.text = s;
|
||||
}
|
||||
}
|
||||
return;
|
||||
},
|
||||
label: Text("解析")),
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
maxLines: 8,
|
||||
controller: resultController,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MediaCard extends StatelessWidget {
|
||||
final MediaDetail item;
|
||||
static const double smallWidth = 110;
|
||||
static const double largeWidth = 140;
|
||||
|
||||
const MediaCard({super.key, required this.item});
|
||||
@override
|
||||
@@ -80,19 +261,18 @@ class MediaCard extends StatelessWidget {
|
||||
context.go(TvDetailsPage.toRoute(item.id!));
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
child: LayoutBuilder(builder: (context, constraints) => Wrap(
|
||||
direction: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
Ink.image(
|
||||
width: constraints.maxWidth,
|
||||
height: constraints.maxWidth / 2 * 3,
|
||||
fit: BoxFit.cover,
|
||||
image: NetworkImage(
|
||||
"${APIs.imagesUrl}/${item.id}/poster_w500.jpg",
|
||||
)),
|
||||
SizedBox(
|
||||
width: cardWidth(context),
|
||||
height: cardWidth(context) / 2 * 3,
|
||||
child: Ink.image(
|
||||
fit: BoxFit.cover,
|
||||
image: NetworkImage(
|
||||
"${APIs.imagesUrl}/${item.id}/poster.jpg",
|
||||
)),
|
||||
),
|
||||
SizedBox(
|
||||
width: cardWidth(context),
|
||||
width: constraints.maxWidth,
|
||||
child: Column(
|
||||
children: [
|
||||
LinearProgressIndicator(
|
||||
@@ -112,15 +292,18 @@ class MediaCard extends StatelessWidget {
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
double cardWidth(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
if (screenWidth < 600) {
|
||||
return smallWidth;
|
||||
}
|
||||
return largeWidth;
|
||||
}
|
||||
}
|
||||
|
||||
double cardWidth(BuildContext context) {
|
||||
const double smallWidth = 110;
|
||||
const double largeWidth = 140;
|
||||
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
if (screenWidth < 600) {
|
||||
return smallWidth;
|
||||
}
|
||||
return largeWidth;
|
||||
}
|
||||
|
||||
@@ -193,13 +193,25 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
}
|
||||
|
||||
Future<void> showConfirmDialog(BuildContext oriContext) {
|
||||
var deleteFiles = false;
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text("确认删除:"),
|
||||
content: Text("${widget.details.name}"),
|
||||
title: const Text("确认删除"),
|
||||
content: StatefulBuilder(builder: (context, setState) {
|
||||
return CheckboxListTile(
|
||||
value: deleteFiles,
|
||||
title: Text("删除媒体文件"),
|
||||
onChanged: (v) {
|
||||
setState(
|
||||
() {
|
||||
deleteFiles = v!;
|
||||
},
|
||||
);
|
||||
});
|
||||
}),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
@@ -209,7 +221,7 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
ref
|
||||
.read(mediaDetailsProvider(widget.details.id.toString())
|
||||
.notifier)
|
||||
.delete()
|
||||
.delete(deleteFiles)
|
||||
.then((v) {
|
||||
if (oriContext.mounted) {
|
||||
oriContext.go(widget.details.mediaType == "tv"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:ui/providers/APIs.dart';
|
||||
import 'dart:io' show Platform;
|
||||
@@ -60,5 +61,5 @@ bool isDesktop() {
|
||||
|
||||
bool isSmallScreen(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
return screenWidth < 600;
|
||||
return screenWidth < Breakpoints.small.endWidth!.toDouble();
|
||||
}
|
||||
|
||||
@@ -141,8 +141,58 @@ class _MySliderState extends State<MyRangeSlider> {
|
||||
}
|
||||
}
|
||||
|
||||
class LoadingListTile extends StatefulWidget {
|
||||
const LoadingListTile(
|
||||
{super.key,
|
||||
required this.onPressed,
|
||||
required this.icon,
|
||||
required this.text});
|
||||
final Future<void> Function() onPressed;
|
||||
final IconData icon;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return LoadingListTileState();
|
||||
}
|
||||
}
|
||||
|
||||
class LoadingListTileState extends State<LoadingListTile> {
|
||||
bool loading = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
onTap: loading
|
||||
? null
|
||||
: () async {
|
||||
setState(() => loading = true);
|
||||
try {
|
||||
await widget.onPressed();
|
||||
} catch (e) {
|
||||
showSnakeBar("操作失败:$e");
|
||||
} finally {
|
||||
setState(() => loading = false);
|
||||
}
|
||||
},
|
||||
title: Text(widget.text),
|
||||
leading: loading
|
||||
? Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: const CircularProgressIndicator(
|
||||
color: Colors.grey,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
)
|
||||
: Icon(widget.icon));
|
||||
}
|
||||
}
|
||||
|
||||
class LoadingIconButton extends StatefulWidget {
|
||||
const LoadingIconButton({super.key, required this.onPressed, required this.icon, this.tooltip});
|
||||
const LoadingIconButton(
|
||||
{super.key, required this.onPressed, required this.icon, this.tooltip});
|
||||
final Future<void> Function() onPressed;
|
||||
final IconData icon;
|
||||
final String? tooltip;
|
||||
@@ -159,7 +209,7 @@ class _LoadingIconButtonState extends State<LoadingIconButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
tooltip: widget.tooltip,
|
||||
tooltip: widget.tooltip,
|
||||
onPressed: loading
|
||||
? null
|
||||
: () async {
|
||||
@@ -187,7 +237,8 @@ class _LoadingIconButtonState extends State<LoadingIconButton> {
|
||||
}
|
||||
|
||||
class LoadingTextButton extends StatefulWidget {
|
||||
const LoadingTextButton({super.key, required this.onPressed, required this.label});
|
||||
const LoadingTextButton(
|
||||
{super.key, required this.onPressed, required this.label});
|
||||
final Future<void> Function() onPressed;
|
||||
final Widget label;
|
||||
|
||||
@@ -230,3 +281,105 @@ class _LoadingTextButtonState extends State<LoadingTextButton> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LoadingElevatedButton extends StatefulWidget {
|
||||
const LoadingElevatedButton(
|
||||
{super.key, required this.onPressed, required this.label});
|
||||
final Future<void> Function() onPressed;
|
||||
final Widget label;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _LoadingElevatedButtonState();
|
||||
}
|
||||
}
|
||||
|
||||
class _LoadingElevatedButtonState extends State<LoadingElevatedButton> {
|
||||
bool loading = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ElevatedButton.icon(
|
||||
onPressed: loading
|
||||
? null
|
||||
: () async {
|
||||
setState(() => loading = true);
|
||||
try {
|
||||
await widget.onPressed();
|
||||
} catch (e) {
|
||||
showSnakeBar("操作失败:$e");
|
||||
} finally {
|
||||
setState(() => loading = false);
|
||||
}
|
||||
},
|
||||
icon: loading
|
||||
? Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: const CircularProgressIndicator(
|
||||
color: Colors.grey,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
)
|
||||
: Text(""),
|
||||
label: widget.label,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PoError extends StatelessWidget {
|
||||
const PoError({super.key, required this.msg, required this.err});
|
||||
final String msg;
|
||||
final dynamic err;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"$msg ",
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
Tooltip(
|
||||
message: "$err",
|
||||
child: Icon(
|
||||
Icons.info,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PoNetworkError extends StatelessWidget {
|
||||
const PoNetworkError({super.key, required this.err});
|
||||
final dynamic err;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PoError(msg: "网络错误,请检查网络链接", err: err);
|
||||
}
|
||||
}
|
||||
|
||||
class PoProgressIndicator extends StatelessWidget {
|
||||
const PoProgressIndicator({super.key, this.backgroundColor, this.value, this.icon});
|
||||
final double? value;
|
||||
final Color? backgroundColor;
|
||||
final IconData? icon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
backgroundColor: backgroundColor,
|
||||
value: value,
|
||||
),
|
||||
icon != null ? Opacity(opacity: 0.7, child: Icon(icon, color: Theme.of(context).colorScheme.primary,),):Container()
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
7
ui/macos/.gitignore
vendored
Normal file
7
ui/macos/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Flutter-related
|
||||
**/Flutter/ephemeral/
|
||||
**/Pods/
|
||||
|
||||
# Xcode-related
|
||||
**/dgph
|
||||
**/xcuserdata/
|
||||
2
ui/macos/Flutter/Flutter-Debug.xcconfig
Normal file
2
ui/macos/Flutter/Flutter-Debug.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||
2
ui/macos/Flutter/Flutter-Release.xcconfig
Normal file
2
ui/macos/Flutter/Flutter-Release.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||
12
ui/macos/Flutter/GeneratedPluginRegistrant.swift
Normal file
12
ui/macos/Flutter/GeneratedPluginRegistrant.swift
Normal file
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
43
ui/macos/Podfile
Normal file
43
ui/macos/Podfile
Normal file
@@ -0,0 +1,43 @@
|
||||
platform :osx, '10.14'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_macos_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
use_modular_headers!
|
||||
|
||||
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
|
||||
target 'RunnerTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_macos_build_settings(target)
|
||||
end
|
||||
end
|
||||
22
ui/macos/Podfile.lock
Normal file
22
ui/macos/Podfile.lock
Normal file
@@ -0,0 +1,22 @@
|
||||
PODS:
|
||||
- FlutterMacOS (1.0.0)
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
FlutterMacOS:
|
||||
:path: Flutter/ephemeral
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
|
||||
PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
801
ui/macos/Runner.xcodeproj/project.pbxproj
Normal file
801
ui/macos/Runner.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,801 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXAggregateTarget section */
|
||||
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
|
||||
isa = PBXAggregateTarget;
|
||||
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
|
||||
buildPhases = (
|
||||
33CC111E2044C6BF0003C045 /* ShellScript */,
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "Flutter Assemble";
|
||||
productName = FLX;
|
||||
};
|
||||
/* End PBXAggregateTarget section */
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1923000523C26F70A19C5B17 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAAFECE3C7921C6460954E3D /* Pods_RunnerTests.framework */; };
|
||||
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
|
||||
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
|
||||
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
|
||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||
465BCE0EA17751098AAEB5E0 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 338C847C60E19B48128A7DBB /* Pods_Runner.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 33CC10EC2044A3C60003C045;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
|
||||
remoteInfo = FLX;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
33CC110E2044A8840003C045 /* Bundle Framework */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Bundle Framework";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||
338C847C60E19B48128A7DBB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10ED2044A3C60003C045 /* ui.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ui.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
|
||||
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
|
||||
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
|
||||
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
|
||||
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
|
||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||
479C447F60C752B08C7F2A48 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
49BC4AE8CB23ABB9DF839C61 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
||||
864A824B0E9687E6B7C26662 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
||||
AE69DDBE6A2D973F2DDE4773 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
EAAFECE3C7921C6460954E3D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
EC6DB94E6F2EE2EBF7D7A8D3 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
F04941AF3F4D22EC07E9EC57 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
331C80D2294CF70F00263BE5 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1923000523C26F70A19C5B17 /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
33CC10EA2044A3C60003C045 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
465BCE0EA17751098AAEB5E0 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
331C80D6294CF71000263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33BA886A226E78AF003329D5 /* Configs */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
|
||||
);
|
||||
path = Configs;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC10E42044A3C60003C045 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33FAB671232836740065AC1E /* Runner */,
|
||||
33CEB47122A05771004F2AC0 /* Flutter */,
|
||||
331C80D6294CF71000263BE5 /* RunnerTests */,
|
||||
33CC10EE2044A3C60003C045 /* Products */,
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
||||
8E8421BAD07F4A7F642628C0 /* Pods */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC10EE2044A3C60003C045 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10ED2044A3C60003C045 /* ui.app */,
|
||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC11242044D66E0003C045 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */,
|
||||
33CC10F42044A3C60003C045 /* MainMenu.xib */,
|
||||
33CC10F72044A3C60003C045 /* Info.plist */,
|
||||
);
|
||||
name = Resources;
|
||||
path = ..;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CEB47122A05771004F2AC0 /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
|
||||
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
|
||||
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
|
||||
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
|
||||
);
|
||||
path = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33FAB671232836740065AC1E /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
|
||||
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
|
||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
|
||||
33E51914231749380026EE4D /* Release.entitlements */,
|
||||
33CC11242044D66E0003C045 /* Resources */,
|
||||
33BA886A226E78AF003329D5 /* Configs */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8E8421BAD07F4A7F642628C0 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC6DB94E6F2EE2EBF7D7A8D3 /* Pods-Runner.debug.xcconfig */,
|
||||
F04941AF3F4D22EC07E9EC57 /* Pods-Runner.release.xcconfig */,
|
||||
864A824B0E9687E6B7C26662 /* Pods-Runner.profile.xcconfig */,
|
||||
479C447F60C752B08C7F2A48 /* Pods-RunnerTests.debug.xcconfig */,
|
||||
49BC4AE8CB23ABB9DF839C61 /* Pods-RunnerTests.release.xcconfig */,
|
||||
AE69DDBE6A2D973F2DDE4773 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
338C847C60E19B48128A7DBB /* Pods_Runner.framework */,
|
||||
EAAFECE3C7921C6460954E3D /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C80D4294CF70F00263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
46368FA552F764B76BC73E54 /* [CP] Check Pods Manifest.lock */,
|
||||
331C80D1294CF70F00263BE5 /* Sources */,
|
||||
331C80D2294CF70F00263BE5 /* Frameworks */,
|
||||
331C80D3294CF70F00263BE5 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
331C80DA294CF71000263BE5 /* PBXTargetDependency */,
|
||||
);
|
||||
name = RunnerTests;
|
||||
productName = RunnerTests;
|
||||
productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
33CC10EC2044A3C60003C045 /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
EC8714E8A2401D3E05CE5A6C /* [CP] Check Pods Manifest.lock */,
|
||||
33CC10E92044A3C60003C045 /* Sources */,
|
||||
33CC10EA2044A3C60003C045 /* Frameworks */,
|
||||
33CC10EB2044A3C60003C045 /* Resources */,
|
||||
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||
C724001738DB1B5869C33FBB /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
33CC11202044C79F0003C045 /* PBXTargetDependency */,
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 33CC10ED2044A3C60003C045 /* ui.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
33CC10E52044A3C60003C045 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 0920;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
331C80D4294CF70F00263BE5 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 33CC10EC2044A3C60003C045;
|
||||
};
|
||||
33CC10EC2044A3C60003C045 = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
LastSwiftMigration = 1100;
|
||||
ProvisioningStyle = Automatic;
|
||||
SystemCapabilities = {
|
||||
com.apple.Sandbox = {
|
||||
enabled = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
33CC111A2044C6BA0003C045 = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
ProvisioningStyle = Manual;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 33CC10E42044A3C60003C045;
|
||||
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
33CC10EC2044A3C60003C045 /* Runner */,
|
||||
331C80D4294CF70F00263BE5 /* RunnerTests */,
|
||||
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
331C80D3294CF70F00263BE5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
33CC10EB2044A3C60003C045 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
|
||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
|
||||
};
|
||||
33CC111E2044C6BF0003C045 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
Flutter/ephemeral/FlutterInputs.xcfilelist,
|
||||
);
|
||||
inputPaths = (
|
||||
Flutter/ephemeral/tripwire,
|
||||
);
|
||||
outputFileListPaths = (
|
||||
Flutter/ephemeral/FlutterOutputs.xcfilelist,
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
||||
};
|
||||
46368FA552F764B76BC73E54 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
C724001738DB1B5869C33FBB /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
EC8714E8A2401D3E05CE5A6C /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
331C80D1294CF70F00263BE5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
33CC10E92044A3C60003C045 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
|
||||
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
|
||||
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 33CC10EC2044A3C60003C045 /* Runner */;
|
||||
targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
|
||||
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
33CC10F52044A3C60003C045 /* Base */,
|
||||
);
|
||||
name = MainMenu.xib;
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
331C80DB294CF71000263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 479C447F60C752B08C7F2A48 /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.ui.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ui";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
331C80DC294CF71000263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 49BC4AE8CB23ABB9DF839C61 /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.ui.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ui";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
331C80DD294CF71000263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AE69DDBE6A2D973F2DDE4773 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.ui.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ui";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
338D0CE9231458BD00FA5F75 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
338D0CEA231458BD00FA5F75 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
338D0CEB231458BD00FA5F75 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
33CC10F92044A3C60003C045 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
33CC10FA2044A3C60003C045 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
33CC10FC2044A3C60003C045 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
33CC10FD2044A3C60003C045 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
33CC111C2044C6BA0003C045 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
33CC111D2044C6BA0003C045 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
331C80DB294CF71000263BE5 /* Debug */,
|
||||
331C80DC294CF71000263BE5 /* Release */,
|
||||
331C80DD294CF71000263BE5 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
33CC10F92044A3C60003C045 /* Debug */,
|
||||
33CC10FA2044A3C60003C045 /* Release */,
|
||||
338D0CE9231458BD00FA5F75 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
33CC10FC2044A3C60003C045 /* Debug */,
|
||||
33CC10FD2044A3C60003C045 /* Release */,
|
||||
338D0CEA231458BD00FA5F75 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
33CC111C2044C6BA0003C045 /* Debug */,
|
||||
33CC111D2044C6BA0003C045 /* Release */,
|
||||
338D0CEB231458BD00FA5F75 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,98 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "ui.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "ui.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "331C80D4294CF70F00263BE5"
|
||||
BuildableName = "RunnerTests.xctest"
|
||||
BlueprintName = "RunnerTests"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "ui.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "ui.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
10
ui/macos/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
10
ui/macos/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user