Compare commits
67 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 |
13
.github/workflows/go.yml
vendored
@@ -29,6 +29,19 @@ jobs:
|
|||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GHCR_TOKEN }}
|
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
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
|
|||||||
17
.github/workflows/release.yml
vendored
@@ -37,12 +37,25 @@ jobs:
|
|||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GHCR_TOKEN }}
|
password: ${{ secrets.GHCR_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
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
|
- name: Build and push
|
||||||
id: push
|
id: push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
@@ -50,9 +63,7 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
push: true
|
push: true
|
||||||
platforms: |
|
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/s390x,linux/ppc64le
|
||||||
linux/amd64
|
|
||||||
linux/arm64
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
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
|
FROM golang:1.23 as builder
|
||||||
|
|
||||||
# 启用go module
|
# 启用go module
|
||||||
@@ -20,7 +12,6 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
COPY --from=flutter /app/build/web ./ui/build/web/
|
|
||||||
# 指定OS等,并go build
|
# 指定OS等,并go build
|
||||||
RUN CGO_ENABLED=0 go build -o polaris -ldflags="-X polaris/db.Version=$(git describe --tags --long)" ./cmd/
|
RUN CGO_ENABLED=0 go build -o polaris -ldflags="-X polaris/db.Version=$(git describe --tags --long)" ./cmd/
|
||||||
|
|
||||||
|
|||||||
18
README.md
@@ -12,7 +12,7 @@
|
|||||||

|

|
||||||
|
|
||||||
|
|
||||||
**Polaris 是一个电视剧和电影的追踪下载软件。对动漫日剧美剧都有良好的匹配,支持webdav或者本地存储。**
|
**Polaris 是一个电视剧和电影的追踪下载软件。对美剧动漫日剧都有良好的匹配,支持多种存储方式(webdav、alist、本地存储)。**
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -27,21 +27,31 @@
|
|||||||
|
|
||||||
- [x] 电视剧自动追踪下载
|
- [x] 电视剧自动追踪下载
|
||||||
- [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] 事件通知推送,目前支持 Pushover和 Bark,还在扩充中
|
||||||
- [x] 后台代理支持
|
- [x] TMDB 代理支持
|
||||||
- [x] 用户认证
|
- [x] 用户认证
|
||||||
- [x] plex 刮削支持
|
- [x] plex 刮削支持
|
||||||
- [x] NFO 刮削文件支持
|
- [x] NFO 刮削文件支持
|
||||||
- [x] BT/PT 支持
|
- [x] BT/PT 支持
|
||||||
- [x] qbittorrent/transmission客户端支持
|
- [x] qbittorrent/transmission客户端支持
|
||||||
|
- [x] 支持导入plex watchlist,plex里标记,自动导入polaris
|
||||||
- [x] and more...
|
- [x] and more...
|
||||||
|
|
||||||
|
## 支持的平台
|
||||||
|
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
|
- linux/arm/v7
|
||||||
|
- linux/386
|
||||||
|
- linux/s390x
|
||||||
|
- linux/ppc64le
|
||||||
|
|
||||||
## Todos
|
## Todos
|
||||||
|
|
||||||
|
|
||||||
- [ ] 更多通知客户端支持
|
- [ ] 更多通知客户端支持
|
||||||
- [ ] 第三方watchlist导入支持
|
- [ ] 更多第三方watchlist导入支持
|
||||||
- [ ] 手机客户端
|
- [ ] 手机客户端
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
14
cmd/main.go
@@ -3,9 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"polaris/db"
|
"polaris/db"
|
||||||
"polaris/log"
|
"polaris/log"
|
||||||
"polaris/pkg/utils"
|
|
||||||
"polaris/server"
|
"polaris/server"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -18,13 +16,13 @@ func main() {
|
|||||||
log.Panicf("init db error: %v", err)
|
log.Panicf("init db error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
// go func() {
|
||||||
time.Sleep(2 * time.Second)
|
// time.Sleep(2 * time.Second)
|
||||||
if err := utils.OpenURL("http://127.0.0.1:8080"); err != nil {
|
// if err := utils.OpenURL("http://127.0.0.1:8080"); err != nil {
|
||||||
log.Errorf("open url error: %v", err)
|
// log.Errorf("open url error: %v", err)
|
||||||
}
|
// }
|
||||||
|
|
||||||
}()
|
// }()
|
||||||
s := server.NewServer(dbClient)
|
s := server.NewServer(dbClient)
|
||||||
if err := s.Serve(); err != nil {
|
if err := s.Serve(); err != nil {
|
||||||
log.Errorf("server start error: %v", err)
|
log.Errorf("server start error: %v", err)
|
||||||
|
|||||||
54
db/const.go
@@ -1,5 +1,7 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
|
import "polaris/ent/media"
|
||||||
|
|
||||||
var Version = "undefined"
|
var Version = "undefined"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -14,10 +16,16 @@ const (
|
|||||||
SettingNfoSupportEnabled = "nfo_support_enabled"
|
SettingNfoSupportEnabled = "nfo_support_enabled"
|
||||||
SettingAllowQiangban = "filter_qiangban"
|
SettingAllowQiangban = "filter_qiangban"
|
||||||
SettingEnableTmdbAdultContent = "tmdb_adult_content"
|
SettingEnableTmdbAdultContent = "tmdb_adult_content"
|
||||||
SetttingSizeLimiter = "size_limiter"
|
|
||||||
SettingTvNamingFormat = "tv_naming_format"
|
SettingTvNamingFormat = "tv_naming_format"
|
||||||
SettingMovieNamingFormat = "movie_naming_format"
|
SettingMovieNamingFormat = "movie_naming_format"
|
||||||
SettingProwlarrInfo = "prowlarr_info"
|
SettingProwlarrInfo = "prowlarr_info"
|
||||||
|
|
||||||
|
SettingTvSizeLimiter = "tv_size_limiter"
|
||||||
|
SettingMovieSizeLimiter = "movie_size_limiter"
|
||||||
|
SettingAcceptedVideoFormats = "accepted_video_formats"
|
||||||
|
SettingAcceptedSubtitleFormats = "accepted_subtitle_formats"
|
||||||
|
|
||||||
|
SettingAIConfig = "ai_config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -40,6 +48,18 @@ const (
|
|||||||
|
|
||||||
const DefaultNamingFormat = "{{.NameCN}} {{.NameEN}} {{if .Year}} ({{.Year}}) {{end}}"
|
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 {
|
type NamingInfo struct {
|
||||||
NameCN string
|
NameCN string
|
||||||
NameEN string
|
NameEN string
|
||||||
@@ -51,15 +71,27 @@ type ResolutionType string
|
|||||||
|
|
||||||
const JwtSerectKey = "jwt_secrect_key"
|
const JwtSerectKey = "jwt_secrect_key"
|
||||||
|
|
||||||
type SizeLimiter struct {
|
type MediaSizeLimiter struct {
|
||||||
R720p Limiter `json:"720p"`
|
P720p SizeLimiter `json:"720p"`
|
||||||
R1080p Limiter `json:"1080p"`
|
P1080 SizeLimiter `json:"1080p"`
|
||||||
R2160p Limiter `json:"2160p"`
|
P2160 SizeLimiter `json:"2160p"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Limiter struct {
|
func (m *MediaSizeLimiter) GetLimiter(r media.Resolution) SizeLimiter {
|
||||||
Max int `json:"max"`
|
if r == media.Resolution1080p {
|
||||||
Min int `json:"min"`
|
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 {
|
type ProwlarrSetting struct {
|
||||||
@@ -67,3 +99,9 @@ type ProwlarrSetting struct {
|
|||||||
ApiKey string `json:"api_key"`
|
ApiKey string `json:"api_key"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AIConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
GeminiApiKey string `json:"gemini_api_key"`
|
||||||
|
GeminiModelName string `json:"gemini_model_name"`
|
||||||
|
}
|
||||||
|
|||||||
117
db/db.go
@@ -157,6 +157,7 @@ func (c *Client) AddMediaWatchlist(m *ent.Media, episodes []int) (*ent.Media, er
|
|||||||
SetDownloadHistoryEpisodes(m.DownloadHistoryEpisodes).
|
SetDownloadHistoryEpisodes(m.DownloadHistoryEpisodes).
|
||||||
SetLimiter(m.Limiter).
|
SetLimiter(m.Limiter).
|
||||||
SetExtras(m.Extras).
|
SetExtras(m.Extras).
|
||||||
|
SetAlternativeTitles(m.AlternativeTitles).
|
||||||
AddEpisodeIDs(episodes...).
|
AddEpisodeIDs(episodes...).
|
||||||
Save(context.TODO())
|
Save(context.TODO())
|
||||||
return r, err
|
return r, err
|
||||||
@@ -251,6 +252,7 @@ func (c *Client) SaveEposideDetail2(d *ent.Episode) (int, error) {
|
|||||||
SetMediaID(d.MediaID).
|
SetMediaID(d.MediaID).
|
||||||
SetStatus(d.Status).
|
SetStatus(d.Status).
|
||||||
SetOverview(d.Overview).
|
SetOverview(d.Overview).
|
||||||
|
SetMonitored(d.Monitored).
|
||||||
SetTitle(d.Title).Save(context.TODO())
|
SetTitle(d.Title).Save(context.TODO())
|
||||||
|
|
||||||
return ep.ID, err
|
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 {
|
type WebdavSetting struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
User string `json:"user"`
|
User string `json:"user"`
|
||||||
@@ -432,7 +443,7 @@ type Storage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) ToWebDavSetting() WebdavSetting {
|
func (s *Storage) ToWebDavSetting() WebdavSetting {
|
||||||
if s.Implementation != storage.ImplementationWebdav {
|
if s.Implementation != storage.ImplementationWebdav && s.Implementation != storage.ImplementationAlist {
|
||||||
panic("not webdav storage")
|
panic("not webdav storage")
|
||||||
}
|
}
|
||||||
var webdavSetting WebdavSetting
|
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()).
|
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).
|
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 {
|
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())
|
return c.ent.ImportList.DeleteOneID(id).Exec(context.TODO())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetSizeLimiter() (*SizeLimiter, error) {
|
func (c *Client) GetSizeLimiter(mediaType string) (*MediaSizeLimiter, error) {
|
||||||
v := c.GetSetting(SetttingSizeLimiter)
|
var v string
|
||||||
var limiter SizeLimiter
|
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)
|
err := json.Unmarshal([]byte(v), &limiter)
|
||||||
return &limiter, err
|
return &limiter, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SetSizeLimiter(limiter *SizeLimiter) error {
|
func (c *Client) SetSizeLimiter(mediaType string, limiter *MediaSizeLimiter) error {
|
||||||
data, err := json.Marshal(limiter)
|
data, err := json.Marshal(limiter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
func (c *Client) GetTvNamingFormat() string {
|
||||||
@@ -661,7 +692,6 @@ 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())
|
return c.ent.Blacklist.Create().SetType(item.Type).SetValue(item.Value).SetNotes(item.Notes).Exec(context.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (c *Client) GetProwlarrSetting() (*ProwlarrSetting, error) {
|
func (c *Client) GetProwlarrSetting() (*ProwlarrSetting, error) {
|
||||||
s := c.GetSetting(SettingProwlarrInfo)
|
s := c.GetSetting(SettingProwlarrInfo)
|
||||||
if s == "" {
|
if s == "" {
|
||||||
@@ -680,4 +710,75 @@ func (c *Client) SaveProwlarrSetting(se *ProwlarrSetting) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return c.SetSetting(SettingProwlarrInfo, string(data))
|
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
@@ -0,0 +1,13 @@
|
|||||||
|
# alist 对接
|
||||||
|
|
||||||
|
> 本程序可以把alist作为一个存储后台使用,下载完成的电影电视剧上传到alist对应的文件夹。配合阿里云、夸克云盘等实现云盘NAS功能。目前支持两种对接方式,webdav和直接对接
|
||||||
|
|
||||||
|
## webdav
|
||||||
|
|
||||||
|
使用webdav形式对接本程序,支持程序所有功能,但是不支持秒传,上传会比较慢
|
||||||
|
|
||||||
|
## alist 直接对接
|
||||||
|
|
||||||
|
存储设置里选择 alist,填入对应的信息即可。
|
||||||
|
|
||||||
|
优点:支持秒传,上传速度快。缺点:部分功能无法使用(plex和nfo文件刮削)
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
polaris:
|
polaris:
|
||||||
image: ghcr.io/simon-ding/polaris:latest
|
image: ghcr.io/simon-ding/polaris:latest
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
- PUID=99 #程序运行的用户UID
|
- PUID=99 #程序运行的用户UID
|
||||||
@@ -23,6 +23,8 @@ services:
|
|||||||
- 8080:8080 #端口映射,冒号前的端口可自行改为需要的
|
- 8080:8080 #端口映射,冒号前的端口可自行改为需要的
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> latest为发布版本,如果你追求新功能,且能接受bug,可以使用main tag
|
||||||
|
|
||||||
### 1.2 Docker 方式安装
|
### 1.2 Docker 方式安装
|
||||||
|
|
||||||
也可以通过原始 docker 命令的方式安装 Polaris:
|
也可以通过原始 docker 命令的方式安装 Polaris:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
package ent
|
package ent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"polaris/ent/history"
|
"polaris/ent/history"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -19,8 +20,12 @@ type History struct {
|
|||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
// MediaID holds the value of the "media_id" field.
|
// MediaID holds the value of the "media_id" field.
|
||||||
MediaID int `json:"media_id,omitempty"`
|
MediaID int `json:"media_id,omitempty"`
|
||||||
// EpisodeID holds the value of the "episode_id" field.
|
// deprecated
|
||||||
EpisodeID int `json:"episode_id,omitempty"`
|
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 holds the value of the "source_title" field.
|
||||||
SourceTitle string `json:"source_title,omitempty"`
|
SourceTitle string `json:"source_title,omitempty"`
|
||||||
// Date holds the value of the "date" field.
|
// Date holds the value of the "date" field.
|
||||||
@@ -47,7 +52,9 @@ func (*History) scanValues(columns []string) ([]any, error) {
|
|||||||
values := make([]any, len(columns))
|
values := make([]any, len(columns))
|
||||||
for i := range columns {
|
for i := range columns {
|
||||||
switch columns[i] {
|
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)
|
values[i] = new(sql.NullInt64)
|
||||||
case history.FieldSourceTitle, history.FieldTargetDir, history.FieldLink, history.FieldStatus, history.FieldSaved:
|
case history.FieldSourceTitle, history.FieldTargetDir, history.FieldLink, history.FieldStatus, history.FieldSaved:
|
||||||
values[i] = new(sql.NullString)
|
values[i] = new(sql.NullString)
|
||||||
@@ -86,6 +93,20 @@ func (h *History) assignValues(columns []string, values []any) error {
|
|||||||
} else if value.Valid {
|
} else if value.Valid {
|
||||||
h.EpisodeID = int(value.Int64)
|
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:
|
case history.FieldSourceTitle:
|
||||||
if value, ok := values[i].(*sql.NullString); !ok {
|
if value, ok := values[i].(*sql.NullString); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field source_title", values[i])
|
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("episode_id=")
|
||||||
builder.WriteString(fmt.Sprintf("%v", h.EpisodeID))
|
builder.WriteString(fmt.Sprintf("%v", h.EpisodeID))
|
||||||
builder.WriteString(", ")
|
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("source_title=")
|
||||||
builder.WriteString(h.SourceTitle)
|
builder.WriteString(h.SourceTitle)
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ const (
|
|||||||
FieldMediaID = "media_id"
|
FieldMediaID = "media_id"
|
||||||
// FieldEpisodeID holds the string denoting the episode_id field in the database.
|
// FieldEpisodeID holds the string denoting the episode_id field in the database.
|
||||||
FieldEpisodeID = "episode_id"
|
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 holds the string denoting the source_title field in the database.
|
||||||
FieldSourceTitle = "source_title"
|
FieldSourceTitle = "source_title"
|
||||||
// FieldDate holds the string denoting the date field in the database.
|
// FieldDate holds the string denoting the date field in the database.
|
||||||
@@ -44,6 +48,8 @@ var Columns = []string{
|
|||||||
FieldID,
|
FieldID,
|
||||||
FieldMediaID,
|
FieldMediaID,
|
||||||
FieldEpisodeID,
|
FieldEpisodeID,
|
||||||
|
FieldEpisodeNums,
|
||||||
|
FieldSeasonNum,
|
||||||
FieldSourceTitle,
|
FieldSourceTitle,
|
||||||
FieldDate,
|
FieldDate,
|
||||||
FieldTargetDir,
|
FieldTargetDir,
|
||||||
@@ -114,6 +120,11 @@ func ByEpisodeID(opts ...sql.OrderTermOption) OrderOption {
|
|||||||
return sql.OrderByField(FieldEpisodeID, opts...).ToFunc()
|
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.
|
// BySourceTitle orders the results by the source_title field.
|
||||||
func BySourceTitle(opts ...sql.OrderTermOption) OrderOption {
|
func BySourceTitle(opts ...sql.OrderTermOption) OrderOption {
|
||||||
return sql.OrderByField(FieldSourceTitle, opts...).ToFunc()
|
return sql.OrderByField(FieldSourceTitle, opts...).ToFunc()
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ func EpisodeID(v int) predicate.History {
|
|||||||
return predicate.History(sql.FieldEQ(FieldEpisodeID, v))
|
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.
|
// SourceTitle applies equality check predicate on the "source_title" field. It's identical to SourceTitleEQ.
|
||||||
func SourceTitle(v string) predicate.History {
|
func SourceTitle(v string) predicate.History {
|
||||||
return predicate.History(sql.FieldEQ(FieldSourceTitle, v))
|
return predicate.History(sql.FieldEQ(FieldSourceTitle, v))
|
||||||
@@ -194,6 +199,66 @@ func EpisodeIDNotNil() predicate.History {
|
|||||||
return predicate.History(sql.FieldNotNull(FieldEpisodeID))
|
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.
|
// SourceTitleEQ applies the EQ predicate on the "source_title" field.
|
||||||
func SourceTitleEQ(v string) predicate.History {
|
func SourceTitleEQ(v string) predicate.History {
|
||||||
return predicate.History(sql.FieldEQ(FieldSourceTitle, v))
|
return predicate.History(sql.FieldEQ(FieldSourceTitle, v))
|
||||||
|
|||||||
@@ -40,6 +40,26 @@ func (hc *HistoryCreate) SetNillableEpisodeID(i *int) *HistoryCreate {
|
|||||||
return hc
|
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.
|
// SetSourceTitle sets the "source_title" field.
|
||||||
func (hc *HistoryCreate) SetSourceTitle(s string) *HistoryCreate {
|
func (hc *HistoryCreate) SetSourceTitle(s string) *HistoryCreate {
|
||||||
hc.mutation.SetSourceTitle(s)
|
hc.mutation.SetSourceTitle(s)
|
||||||
@@ -234,6 +254,14 @@ func (hc *HistoryCreate) createSpec() (*History, *sqlgraph.CreateSpec) {
|
|||||||
_spec.SetField(history.FieldEpisodeID, field.TypeInt, value)
|
_spec.SetField(history.FieldEpisodeID, field.TypeInt, value)
|
||||||
_node.EpisodeID = 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 {
|
if value, ok := hc.mutation.SourceTitle(); ok {
|
||||||
_spec.SetField(history.FieldSourceTitle, field.TypeString, value)
|
_spec.SetField(history.FieldSourceTitle, field.TypeString, value)
|
||||||
_node.SourceTitle = value
|
_node.SourceTitle = value
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"entgo.io/ent/dialect/sql"
|
"entgo.io/ent/dialect/sql"
|
||||||
"entgo.io/ent/dialect/sql/sqlgraph"
|
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||||
|
"entgo.io/ent/dialect/sql/sqljson"
|
||||||
"entgo.io/ent/schema/field"
|
"entgo.io/ent/schema/field"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -76,6 +77,51 @@ func (hu *HistoryUpdate) ClearEpisodeID() *HistoryUpdate {
|
|||||||
return hu
|
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.
|
// SetSourceTitle sets the "source_title" field.
|
||||||
func (hu *HistoryUpdate) SetSourceTitle(s string) *HistoryUpdate {
|
func (hu *HistoryUpdate) SetSourceTitle(s string) *HistoryUpdate {
|
||||||
hu.mutation.SetSourceTitle(s)
|
hu.mutation.SetSourceTitle(s)
|
||||||
@@ -316,6 +362,26 @@ func (hu *HistoryUpdate) sqlSave(ctx context.Context) (n int, err error) {
|
|||||||
if hu.mutation.EpisodeIDCleared() {
|
if hu.mutation.EpisodeIDCleared() {
|
||||||
_spec.ClearField(history.FieldEpisodeID, field.TypeInt)
|
_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 {
|
if value, ok := hu.mutation.SourceTitle(); ok {
|
||||||
_spec.SetField(history.FieldSourceTitle, field.TypeString, value)
|
_spec.SetField(history.FieldSourceTitle, field.TypeString, value)
|
||||||
}
|
}
|
||||||
@@ -432,6 +498,51 @@ func (huo *HistoryUpdateOne) ClearEpisodeID() *HistoryUpdateOne {
|
|||||||
return huo
|
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.
|
// SetSourceTitle sets the "source_title" field.
|
||||||
func (huo *HistoryUpdateOne) SetSourceTitle(s string) *HistoryUpdateOne {
|
func (huo *HistoryUpdateOne) SetSourceTitle(s string) *HistoryUpdateOne {
|
||||||
huo.mutation.SetSourceTitle(s)
|
huo.mutation.SetSourceTitle(s)
|
||||||
@@ -702,6 +813,26 @@ func (huo *HistoryUpdateOne) sqlSave(ctx context.Context) (_node *History, err e
|
|||||||
if huo.mutation.EpisodeIDCleared() {
|
if huo.mutation.EpisodeIDCleared() {
|
||||||
_spec.ClearField(history.FieldEpisodeID, field.TypeInt)
|
_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 {
|
if value, ok := huo.mutation.SourceTitle(); ok {
|
||||||
_spec.SetField(history.FieldSourceTitle, field.TypeString, value)
|
_spec.SetField(history.FieldSourceTitle, field.TypeString, value)
|
||||||
}
|
}
|
||||||
|
|||||||
15
ent/media.go
@@ -49,6 +49,8 @@ type Media struct {
|
|||||||
Limiter schema.MediaLimiter `json:"limiter,omitempty"`
|
Limiter schema.MediaLimiter `json:"limiter,omitempty"`
|
||||||
// Extras holds the value of the "extras" field.
|
// Extras holds the value of the "extras" field.
|
||||||
Extras schema.MediaExtras `json:"extras,omitempty"`
|
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.
|
// Edges holds the relations/edges for other nodes in the graph.
|
||||||
// The values are being populated by the MediaQuery when eager-loading is set.
|
// The values are being populated by the MediaQuery when eager-loading is set.
|
||||||
Edges MediaEdges `json:"edges"`
|
Edges MediaEdges `json:"edges"`
|
||||||
@@ -78,7 +80,7 @@ func (*Media) scanValues(columns []string) ([]any, error) {
|
|||||||
values := make([]any, len(columns))
|
values := make([]any, len(columns))
|
||||||
for i := range columns {
|
for i := range columns {
|
||||||
switch columns[i] {
|
switch columns[i] {
|
||||||
case media.FieldLimiter, media.FieldExtras:
|
case media.FieldLimiter, media.FieldExtras, media.FieldAlternativeTitles:
|
||||||
values[i] = new([]byte)
|
values[i] = new([]byte)
|
||||||
case media.FieldDownloadHistoryEpisodes:
|
case media.FieldDownloadHistoryEpisodes:
|
||||||
values[i] = new(sql.NullBool)
|
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)
|
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:
|
default:
|
||||||
m.selectValues.Set(columns[i], values[i])
|
m.selectValues.Set(columns[i], values[i])
|
||||||
}
|
}
|
||||||
@@ -288,6 +298,9 @@ func (m *Media) String() string {
|
|||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
builder.WriteString("extras=")
|
builder.WriteString("extras=")
|
||||||
builder.WriteString(fmt.Sprintf("%v", m.Extras))
|
builder.WriteString(fmt.Sprintf("%v", m.Extras))
|
||||||
|
builder.WriteString(", ")
|
||||||
|
builder.WriteString("alternative_titles=")
|
||||||
|
builder.WriteString(fmt.Sprintf("%v", m.AlternativeTitles))
|
||||||
builder.WriteByte(')')
|
builder.WriteByte(')')
|
||||||
return builder.String()
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ const (
|
|||||||
FieldLimiter = "limiter"
|
FieldLimiter = "limiter"
|
||||||
// FieldExtras holds the string denoting the extras field in the database.
|
// FieldExtras holds the string denoting the extras field in the database.
|
||||||
FieldExtras = "extras"
|
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 holds the string denoting the episodes edge name in mutations.
|
||||||
EdgeEpisodes = "episodes"
|
EdgeEpisodes = "episodes"
|
||||||
// Table holds the table name of the media in the database.
|
// Table holds the table name of the media in the database.
|
||||||
@@ -76,6 +78,7 @@ var Columns = []string{
|
|||||||
FieldDownloadHistoryEpisodes,
|
FieldDownloadHistoryEpisodes,
|
||||||
FieldLimiter,
|
FieldLimiter,
|
||||||
FieldExtras,
|
FieldExtras,
|
||||||
|
FieldAlternativeTitles,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidColumn reports if the column name is valid (part of the table columns).
|
// 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))
|
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.
|
// HasEpisodes applies the HasEdge predicate on the "episodes" edge.
|
||||||
func HasEpisodes() predicate.Media {
|
func HasEpisodes() predicate.Media {
|
||||||
return predicate.Media(func(s *sql.Selector) {
|
return predicate.Media(func(s *sql.Selector) {
|
||||||
|
|||||||
@@ -184,6 +184,12 @@ func (mc *MediaCreate) SetNillableExtras(se *schema.MediaExtras) *MediaCreate {
|
|||||||
return mc
|
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.
|
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by IDs.
|
||||||
func (mc *MediaCreate) AddEpisodeIDs(ids ...int) *MediaCreate {
|
func (mc *MediaCreate) AddEpisodeIDs(ids ...int) *MediaCreate {
|
||||||
mc.mutation.AddEpisodeIDs(ids...)
|
mc.mutation.AddEpisodeIDs(ids...)
|
||||||
@@ -377,6 +383,10 @@ func (mc *MediaCreate) createSpec() (*Media, *sqlgraph.CreateSpec) {
|
|||||||
_spec.SetField(media.FieldExtras, field.TypeJSON, value)
|
_spec.SetField(media.FieldExtras, field.TypeJSON, value)
|
||||||
_node.Extras = 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 {
|
if nodes := mc.mutation.EpisodesIDs(); len(nodes) > 0 {
|
||||||
edge := &sqlgraph.EdgeSpec{
|
edge := &sqlgraph.EdgeSpec{
|
||||||
Rel: sqlgraph.O2M,
|
Rel: sqlgraph.O2M,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"entgo.io/ent/dialect/sql"
|
"entgo.io/ent/dialect/sql"
|
||||||
"entgo.io/ent/dialect/sql/sqlgraph"
|
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||||
|
"entgo.io/ent/dialect/sql/sqljson"
|
||||||
"entgo.io/ent/schema/field"
|
"entgo.io/ent/schema/field"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -290,6 +291,24 @@ func (mu *MediaUpdate) ClearExtras() *MediaUpdate {
|
|||||||
return mu
|
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.
|
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by IDs.
|
||||||
func (mu *MediaUpdate) AddEpisodeIDs(ids ...int) *MediaUpdate {
|
func (mu *MediaUpdate) AddEpisodeIDs(ids ...int) *MediaUpdate {
|
||||||
mu.mutation.AddEpisodeIDs(ids...)
|
mu.mutation.AddEpisodeIDs(ids...)
|
||||||
@@ -454,6 +473,17 @@ func (mu *MediaUpdate) sqlSave(ctx context.Context) (n int, err error) {
|
|||||||
if mu.mutation.ExtrasCleared() {
|
if mu.mutation.ExtrasCleared() {
|
||||||
_spec.ClearField(media.FieldExtras, field.TypeJSON)
|
_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() {
|
if mu.mutation.EpisodesCleared() {
|
||||||
edge := &sqlgraph.EdgeSpec{
|
edge := &sqlgraph.EdgeSpec{
|
||||||
Rel: sqlgraph.O2M,
|
Rel: sqlgraph.O2M,
|
||||||
@@ -779,6 +809,24 @@ func (muo *MediaUpdateOne) ClearExtras() *MediaUpdateOne {
|
|||||||
return muo
|
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.
|
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by IDs.
|
||||||
func (muo *MediaUpdateOne) AddEpisodeIDs(ids ...int) *MediaUpdateOne {
|
func (muo *MediaUpdateOne) AddEpisodeIDs(ids ...int) *MediaUpdateOne {
|
||||||
muo.mutation.AddEpisodeIDs(ids...)
|
muo.mutation.AddEpisodeIDs(ids...)
|
||||||
@@ -973,6 +1021,17 @@ func (muo *MediaUpdateOne) sqlSave(ctx context.Context) (_node *Media, err error
|
|||||||
if muo.mutation.ExtrasCleared() {
|
if muo.mutation.ExtrasCleared() {
|
||||||
_spec.ClearField(media.FieldExtras, field.TypeJSON)
|
_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() {
|
if muo.mutation.EpisodesCleared() {
|
||||||
edge := &sqlgraph.EdgeSpec{
|
edge := &sqlgraph.EdgeSpec{
|
||||||
Rel: sqlgraph.O2M,
|
Rel: sqlgraph.O2M,
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ var (
|
|||||||
{Name: "id", Type: field.TypeInt, Increment: true},
|
{Name: "id", Type: field.TypeInt, Increment: true},
|
||||||
{Name: "media_id", Type: field.TypeInt},
|
{Name: "media_id", Type: field.TypeInt},
|
||||||
{Name: "episode_id", Type: field.TypeInt, Nullable: true},
|
{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: "source_title", Type: field.TypeString},
|
||||||
{Name: "date", Type: field.TypeTime},
|
{Name: "date", Type: field.TypeTime},
|
||||||
{Name: "target_dir", Type: field.TypeString},
|
{Name: "target_dir", Type: field.TypeString},
|
||||||
@@ -141,6 +143,7 @@ var (
|
|||||||
{Name: "download_history_episodes", Type: field.TypeBool, Nullable: true, Default: false},
|
{Name: "download_history_episodes", Type: field.TypeBool, Nullable: true, Default: false},
|
||||||
{Name: "limiter", Type: field.TypeJSON, Nullable: true},
|
{Name: "limiter", Type: field.TypeJSON, Nullable: true},
|
||||||
{Name: "extras", 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 holds the schema information for the "media" table.
|
||||||
MediaTable = &schema.Table{
|
MediaTable = &schema.Table{
|
||||||
@@ -178,7 +181,7 @@ var (
|
|||||||
StoragesColumns = []*schema.Column{
|
StoragesColumns = []*schema.Column{
|
||||||
{Name: "id", Type: field.TypeInt, Increment: true},
|
{Name: "id", Type: field.TypeInt, Increment: true},
|
||||||
{Name: "name", Type: field.TypeString, Unique: 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: "tv_path", Type: field.TypeString, Nullable: true},
|
||||||
{Name: "movie_path", Type: field.TypeString, Nullable: true},
|
{Name: "movie_path", Type: field.TypeString, Nullable: true},
|
||||||
{Name: "settings", Type: field.TypeString, Nullable: true},
|
{Name: "settings", Type: field.TypeString, Nullable: true},
|
||||||
|
|||||||
291
ent/mutation.go
@@ -2336,6 +2336,10 @@ type HistoryMutation struct {
|
|||||||
addmedia_id *int
|
addmedia_id *int
|
||||||
episode_id *int
|
episode_id *int
|
||||||
addepisode_id *int
|
addepisode_id *int
|
||||||
|
episode_nums *[]int
|
||||||
|
appendepisode_nums []int
|
||||||
|
season_num *int
|
||||||
|
addseason_num *int
|
||||||
source_title *string
|
source_title *string
|
||||||
date *time.Time
|
date *time.Time
|
||||||
target_dir *string
|
target_dir *string
|
||||||
@@ -2578,6 +2582,141 @@ func (m *HistoryMutation) ResetEpisodeID() {
|
|||||||
delete(m.clearedFields, history.FieldEpisodeID)
|
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.
|
// SetSourceTitle sets the "source_title" field.
|
||||||
func (m *HistoryMutation) SetSourceTitle(s string) {
|
func (m *HistoryMutation) SetSourceTitle(s string) {
|
||||||
m.source_title = &s
|
m.source_title = &s
|
||||||
@@ -3050,13 +3189,19 @@ func (m *HistoryMutation) Type() string {
|
|||||||
// order to get all numeric fields that were incremented/decremented, call
|
// order to get all numeric fields that were incremented/decremented, call
|
||||||
// AddedFields().
|
// AddedFields().
|
||||||
func (m *HistoryMutation) Fields() []string {
|
func (m *HistoryMutation) Fields() []string {
|
||||||
fields := make([]string, 0, 11)
|
fields := make([]string, 0, 13)
|
||||||
if m.media_id != nil {
|
if m.media_id != nil {
|
||||||
fields = append(fields, history.FieldMediaID)
|
fields = append(fields, history.FieldMediaID)
|
||||||
}
|
}
|
||||||
if m.episode_id != nil {
|
if m.episode_id != nil {
|
||||||
fields = append(fields, history.FieldEpisodeID)
|
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 {
|
if m.source_title != nil {
|
||||||
fields = append(fields, history.FieldSourceTitle)
|
fields = append(fields, history.FieldSourceTitle)
|
||||||
}
|
}
|
||||||
@@ -3096,6 +3241,10 @@ func (m *HistoryMutation) Field(name string) (ent.Value, bool) {
|
|||||||
return m.MediaID()
|
return m.MediaID()
|
||||||
case history.FieldEpisodeID:
|
case history.FieldEpisodeID:
|
||||||
return m.EpisodeID()
|
return m.EpisodeID()
|
||||||
|
case history.FieldEpisodeNums:
|
||||||
|
return m.EpisodeNums()
|
||||||
|
case history.FieldSeasonNum:
|
||||||
|
return m.SeasonNum()
|
||||||
case history.FieldSourceTitle:
|
case history.FieldSourceTitle:
|
||||||
return m.SourceTitle()
|
return m.SourceTitle()
|
||||||
case history.FieldDate:
|
case history.FieldDate:
|
||||||
@@ -3127,6 +3276,10 @@ func (m *HistoryMutation) OldField(ctx context.Context, name string) (ent.Value,
|
|||||||
return m.OldMediaID(ctx)
|
return m.OldMediaID(ctx)
|
||||||
case history.FieldEpisodeID:
|
case history.FieldEpisodeID:
|
||||||
return m.OldEpisodeID(ctx)
|
return m.OldEpisodeID(ctx)
|
||||||
|
case history.FieldEpisodeNums:
|
||||||
|
return m.OldEpisodeNums(ctx)
|
||||||
|
case history.FieldSeasonNum:
|
||||||
|
return m.OldSeasonNum(ctx)
|
||||||
case history.FieldSourceTitle:
|
case history.FieldSourceTitle:
|
||||||
return m.OldSourceTitle(ctx)
|
return m.OldSourceTitle(ctx)
|
||||||
case history.FieldDate:
|
case history.FieldDate:
|
||||||
@@ -3168,6 +3321,20 @@ func (m *HistoryMutation) SetField(name string, value ent.Value) error {
|
|||||||
}
|
}
|
||||||
m.SetEpisodeID(v)
|
m.SetEpisodeID(v)
|
||||||
return nil
|
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:
|
case history.FieldSourceTitle:
|
||||||
v, ok := value.(string)
|
v, ok := value.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -3245,6 +3412,9 @@ func (m *HistoryMutation) AddedFields() []string {
|
|||||||
if m.addepisode_id != nil {
|
if m.addepisode_id != nil {
|
||||||
fields = append(fields, history.FieldEpisodeID)
|
fields = append(fields, history.FieldEpisodeID)
|
||||||
}
|
}
|
||||||
|
if m.addseason_num != nil {
|
||||||
|
fields = append(fields, history.FieldSeasonNum)
|
||||||
|
}
|
||||||
if m.addsize != nil {
|
if m.addsize != nil {
|
||||||
fields = append(fields, history.FieldSize)
|
fields = append(fields, history.FieldSize)
|
||||||
}
|
}
|
||||||
@@ -3266,6 +3436,8 @@ func (m *HistoryMutation) AddedField(name string) (ent.Value, bool) {
|
|||||||
return m.AddedMediaID()
|
return m.AddedMediaID()
|
||||||
case history.FieldEpisodeID:
|
case history.FieldEpisodeID:
|
||||||
return m.AddedEpisodeID()
|
return m.AddedEpisodeID()
|
||||||
|
case history.FieldSeasonNum:
|
||||||
|
return m.AddedSeasonNum()
|
||||||
case history.FieldSize:
|
case history.FieldSize:
|
||||||
return m.AddedSize()
|
return m.AddedSize()
|
||||||
case history.FieldDownloadClientID:
|
case history.FieldDownloadClientID:
|
||||||
@@ -3295,6 +3467,13 @@ func (m *HistoryMutation) AddField(name string, value ent.Value) error {
|
|||||||
}
|
}
|
||||||
m.AddEpisodeID(v)
|
m.AddEpisodeID(v)
|
||||||
return nil
|
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:
|
case history.FieldSize:
|
||||||
v, ok := value.(int)
|
v, ok := value.(int)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -3327,6 +3506,12 @@ func (m *HistoryMutation) ClearedFields() []string {
|
|||||||
if m.FieldCleared(history.FieldEpisodeID) {
|
if m.FieldCleared(history.FieldEpisodeID) {
|
||||||
fields = append(fields, 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) {
|
if m.FieldCleared(history.FieldDownloadClientID) {
|
||||||
fields = append(fields, history.FieldDownloadClientID)
|
fields = append(fields, history.FieldDownloadClientID)
|
||||||
}
|
}
|
||||||
@@ -3356,6 +3541,12 @@ func (m *HistoryMutation) ClearField(name string) error {
|
|||||||
case history.FieldEpisodeID:
|
case history.FieldEpisodeID:
|
||||||
m.ClearEpisodeID()
|
m.ClearEpisodeID()
|
||||||
return nil
|
return nil
|
||||||
|
case history.FieldEpisodeNums:
|
||||||
|
m.ClearEpisodeNums()
|
||||||
|
return nil
|
||||||
|
case history.FieldSeasonNum:
|
||||||
|
m.ClearSeasonNum()
|
||||||
|
return nil
|
||||||
case history.FieldDownloadClientID:
|
case history.FieldDownloadClientID:
|
||||||
m.ClearDownloadClientID()
|
m.ClearDownloadClientID()
|
||||||
return nil
|
return nil
|
||||||
@@ -3382,6 +3573,12 @@ func (m *HistoryMutation) ResetField(name string) error {
|
|||||||
case history.FieldEpisodeID:
|
case history.FieldEpisodeID:
|
||||||
m.ResetEpisodeID()
|
m.ResetEpisodeID()
|
||||||
return nil
|
return nil
|
||||||
|
case history.FieldEpisodeNums:
|
||||||
|
m.ResetEpisodeNums()
|
||||||
|
return nil
|
||||||
|
case history.FieldSeasonNum:
|
||||||
|
m.ResetSeasonNum()
|
||||||
|
return nil
|
||||||
case history.FieldSourceTitle:
|
case history.FieldSourceTitle:
|
||||||
m.ResetSourceTitle()
|
m.ResetSourceTitle()
|
||||||
return nil
|
return nil
|
||||||
@@ -4918,6 +5115,8 @@ type MediaMutation struct {
|
|||||||
download_history_episodes *bool
|
download_history_episodes *bool
|
||||||
limiter *schema.MediaLimiter
|
limiter *schema.MediaLimiter
|
||||||
extras *schema.MediaExtras
|
extras *schema.MediaExtras
|
||||||
|
alternative_titles *[]schema.AlternativeTilte
|
||||||
|
appendalternative_titles []schema.AlternativeTilte
|
||||||
clearedFields map[string]struct{}
|
clearedFields map[string]struct{}
|
||||||
episodes map[int]struct{}
|
episodes map[int]struct{}
|
||||||
removedepisodes map[int]struct{}
|
removedepisodes map[int]struct{}
|
||||||
@@ -5684,6 +5883,71 @@ func (m *MediaMutation) ResetExtras() {
|
|||||||
delete(m.clearedFields, media.FieldExtras)
|
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.
|
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by ids.
|
||||||
func (m *MediaMutation) AddEpisodeIDs(ids ...int) {
|
func (m *MediaMutation) AddEpisodeIDs(ids ...int) {
|
||||||
if m.episodes == nil {
|
if m.episodes == nil {
|
||||||
@@ -5772,7 +6036,7 @@ func (m *MediaMutation) Type() string {
|
|||||||
// order to get all numeric fields that were incremented/decremented, call
|
// order to get all numeric fields that were incremented/decremented, call
|
||||||
// AddedFields().
|
// AddedFields().
|
||||||
func (m *MediaMutation) Fields() []string {
|
func (m *MediaMutation) Fields() []string {
|
||||||
fields := make([]string, 0, 15)
|
fields := make([]string, 0, 16)
|
||||||
if m.tmdb_id != nil {
|
if m.tmdb_id != nil {
|
||||||
fields = append(fields, media.FieldTmdbID)
|
fields = append(fields, media.FieldTmdbID)
|
||||||
}
|
}
|
||||||
@@ -5818,6 +6082,9 @@ func (m *MediaMutation) Fields() []string {
|
|||||||
if m.extras != nil {
|
if m.extras != nil {
|
||||||
fields = append(fields, media.FieldExtras)
|
fields = append(fields, media.FieldExtras)
|
||||||
}
|
}
|
||||||
|
if m.alternative_titles != nil {
|
||||||
|
fields = append(fields, media.FieldAlternativeTitles)
|
||||||
|
}
|
||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5856,6 +6123,8 @@ func (m *MediaMutation) Field(name string) (ent.Value, bool) {
|
|||||||
return m.Limiter()
|
return m.Limiter()
|
||||||
case media.FieldExtras:
|
case media.FieldExtras:
|
||||||
return m.Extras()
|
return m.Extras()
|
||||||
|
case media.FieldAlternativeTitles:
|
||||||
|
return m.AlternativeTitles()
|
||||||
}
|
}
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
@@ -5895,6 +6164,8 @@ func (m *MediaMutation) OldField(ctx context.Context, name string) (ent.Value, e
|
|||||||
return m.OldLimiter(ctx)
|
return m.OldLimiter(ctx)
|
||||||
case media.FieldExtras:
|
case media.FieldExtras:
|
||||||
return m.OldExtras(ctx)
|
return m.OldExtras(ctx)
|
||||||
|
case media.FieldAlternativeTitles:
|
||||||
|
return m.OldAlternativeTitles(ctx)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("unknown Media field %s", name)
|
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)
|
m.SetExtras(v)
|
||||||
return nil
|
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)
|
return fmt.Errorf("unknown Media field %s", name)
|
||||||
}
|
}
|
||||||
@@ -6084,6 +6362,9 @@ func (m *MediaMutation) ClearedFields() []string {
|
|||||||
if m.FieldCleared(media.FieldExtras) {
|
if m.FieldCleared(media.FieldExtras) {
|
||||||
fields = append(fields, media.FieldExtras)
|
fields = append(fields, media.FieldExtras)
|
||||||
}
|
}
|
||||||
|
if m.FieldCleared(media.FieldAlternativeTitles) {
|
||||||
|
fields = append(fields, media.FieldAlternativeTitles)
|
||||||
|
}
|
||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6116,6 +6397,9 @@ func (m *MediaMutation) ClearField(name string) error {
|
|||||||
case media.FieldExtras:
|
case media.FieldExtras:
|
||||||
m.ClearExtras()
|
m.ClearExtras()
|
||||||
return nil
|
return nil
|
||||||
|
case media.FieldAlternativeTitles:
|
||||||
|
m.ClearAlternativeTitles()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("unknown Media nullable field %s", name)
|
return fmt.Errorf("unknown Media nullable field %s", name)
|
||||||
}
|
}
|
||||||
@@ -6169,6 +6453,9 @@ func (m *MediaMutation) ResetField(name string) error {
|
|||||||
case media.FieldExtras:
|
case media.FieldExtras:
|
||||||
m.ResetExtras()
|
m.ResetExtras()
|
||||||
return nil
|
return nil
|
||||||
|
case media.FieldAlternativeTitles:
|
||||||
|
m.ResetAlternativeTitles()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("unknown Media field %s", name)
|
return fmt.Errorf("unknown Media field %s", name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func init() {
|
|||||||
historyFields := schema.History{}.Fields()
|
historyFields := schema.History{}.Fields()
|
||||||
_ = historyFields
|
_ = historyFields
|
||||||
// historyDescSize is the schema descriptor for size field.
|
// 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 holds the default value on creation for the size field.
|
||||||
history.DefaultSize = historyDescSize.Default.(int)
|
history.DefaultSize = historyDescSize.Default.(int)
|
||||||
indexersFields := schema.Indexers{}.Fields()
|
indexersFields := schema.Indexers{}.Fields()
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ type History struct {
|
|||||||
func (History) Fields() []ent.Field {
|
func (History) Fields() []ent.Field {
|
||||||
return []ent.Field{
|
return []ent.Field{
|
||||||
field.Int("media_id"),
|
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.String("source_title"),
|
||||||
field.Time("date"),
|
field.Time("date"),
|
||||||
field.String("target_dir"),
|
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.Bool("download_history_episodes").Optional().Default(false).Comment("tv series only"),
|
||||||
field.JSON("limiter", MediaLimiter{}).Optional(),
|
field.JSON("limiter", MediaLimiter{}).Optional(),
|
||||||
field.JSON("extras", MediaExtras{}).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 {
|
type MediaLimiter struct {
|
||||||
SizeMin int `json:"size_min"` //in B
|
SizeMin int64 `json:"size_min"` //in B
|
||||||
SizeMax int `json:"size_max"` //in B
|
SizeMax int64 `json:"size_max"` //in B
|
||||||
|
PreferSize int64 `json:"prefer_max"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MediaExtras struct {
|
type MediaExtras struct {
|
||||||
@@ -51,7 +59,7 @@ type MediaExtras struct {
|
|||||||
JavId string `json:"javid"`
|
JavId string `json:"javid"`
|
||||||
//OriginCountry []string `json:"origin_country"`
|
//OriginCountry []string `json:"origin_country"`
|
||||||
OriginalLanguage string `json:"original_language"`
|
OriginalLanguage string `json:"original_language"`
|
||||||
Genres []struct {
|
Genres []struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
} `json:"genres"`
|
} `json:"genres"`
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type Storage struct {
|
|||||||
func (Storage) Fields() []ent.Field {
|
func (Storage) Fields() []ent.Field {
|
||||||
return []ent.Field{
|
return []ent.Field{
|
||||||
field.String("name").Unique(),
|
field.String("name").Unique(),
|
||||||
field.Enum("implementation").Values("webdav", "local"),
|
field.Enum("implementation").Values("webdav", "local", "alist"),
|
||||||
field.String("tv_path").Optional(),
|
field.String("tv_path").Optional(),
|
||||||
field.String("movie_path").Optional(),
|
field.String("movie_path").Optional(),
|
||||||
field.String("settings").Optional(),
|
field.String("settings").Optional(),
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ type Implementation string
|
|||||||
const (
|
const (
|
||||||
ImplementationWebdav Implementation = "webdav"
|
ImplementationWebdav Implementation = "webdav"
|
||||||
ImplementationLocal Implementation = "local"
|
ImplementationLocal Implementation = "local"
|
||||||
|
ImplementationAlist Implementation = "alist"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (i Implementation) String() string {
|
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.
|
// ImplementationValidator is a validator for the "implementation" field enum values. It is called by the builders before save.
|
||||||
func ImplementationValidator(i Implementation) error {
|
func ImplementationValidator(i Implementation) error {
|
||||||
switch i {
|
switch i {
|
||||||
case ImplementationWebdav, ImplementationLocal:
|
case ImplementationWebdav, ImplementationLocal, ImplementationAlist:
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("storage: invalid enum value for implementation field: %q", i)
|
return fmt.Errorf("storage: invalid enum value for implementation field: %q", i)
|
||||||
|
|||||||
37
go.mod
@@ -10,7 +10,7 @@ require (
|
|||||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
github.com/robfig/cron v1.2.0
|
github.com/robfig/cron v1.2.0
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
golang.org/x/net v0.27.0
|
golang.org/x/net v0.33.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -25,6 +25,12 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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/BurntSushi/toml v1.4.0 // indirect
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect
|
github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect
|
||||||
github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca // indirect
|
github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca // indirect
|
||||||
@@ -33,8 +39,17 @@ require (
|
|||||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||||
github.com/blinkbean/dingtalk v1.1.3 // indirect
|
github.com/blinkbean/dingtalk v1.1.3 // indirect
|
||||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // 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-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
|
||||||
github.com/go-test/deep v1.0.4 // 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/gregdel/pushover v1.3.1 // indirect
|
||||||
github.com/huandu/xstrings v1.3.2 // indirect
|
github.com/huandu/xstrings v1.3.2 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||||
@@ -47,7 +62,19 @@ require (
|
|||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||||
github.com/tetratelabs/wazero v1.8.0 // 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/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
lukechampine.com/blake3 v1.1.6 // indirect
|
lukechampine.com/blake3 v1.1.6 // indirect
|
||||||
@@ -95,11 +122,11 @@ require (
|
|||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
github.com/zclconf/go-cty v1.8.0 // indirect
|
github.com/zclconf/go-cty v1.8.0 // indirect
|
||||||
golang.org/x/arch v0.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/exp v0.0.0-20240823005443-9b4947da3948
|
||||||
golang.org/x/mod v0.20.0 // indirect
|
golang.org/x/mod v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.25.0
|
golang.org/x/sys v0.28.0
|
||||||
golang.org/x/text v0.18.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
106
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=
|
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.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.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/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk=
|
||||||
crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
|
crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
|
||||||
entgo.io/ent v0.13.1 h1:uD8QwN1h6SNphdCCzmkMN3feSUzNnVvV/WIkHKMbzOE=
|
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 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 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
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/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
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 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
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 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
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 h1:TKK9h+uuwiDOaFlsVispG1KxqhsSM5Y4ZELnUF3GlqU=
|
||||||
github.com/cyruzin/golang-tmdb v1.6.3/go.mod h1:ZSryJLCcY+9TiKU+LbouXKns++YBrM8Tizannr05c+I=
|
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=
|
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-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/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/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 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
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=
|
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-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.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
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 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
||||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
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=
|
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/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-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-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/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.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.1/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.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-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.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.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/golang/snappy v0.0.1/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 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/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.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.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.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.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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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-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-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
@@ -271,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-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-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-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/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.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
@@ -350,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.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||||
go.opencensus.io v0.20.2/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.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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
@@ -361,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/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-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-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.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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
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-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 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
|
||||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||||
@@ -383,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-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-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-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-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-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-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-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-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.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.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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
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-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.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-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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -403,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-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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
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-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-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-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-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-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-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-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-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-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-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-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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -423,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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.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.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
@@ -436,14 +499,17 @@ 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.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.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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
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-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-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-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-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-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-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.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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
@@ -452,20 +518,39 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
|||||||
golift.io/starr v1.0.0 h1:IDSaSL+ZYxdLT/Lg//dg/iwZ39LHO3D5CmbLCOgSXbI=
|
golift.io/starr v1.0.0 h1:IDSaSL+ZYxdLT/Lg//dg/iwZ39LHO3D5CmbLCOgSXbI=
|
||||||
golift.io/starr v1.0.0/go.mod h1:xnUwp4vK62bDvozW9QHUYc08m6kjwaZnGw3Db65fQHw=
|
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.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.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.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
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-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-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-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.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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
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-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-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 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.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.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 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
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=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
@@ -491,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=
|
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-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-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 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c=
|
||||||
lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
|
lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
|||||||
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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,8 @@
|
|||||||
|
package gemini
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_any1(t *testing.T) {
|
||||||
|
}
|
||||||
@@ -7,242 +7,104 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Metadata struct {
|
type Info struct {
|
||||||
NameEn string
|
NameEn string
|
||||||
NameCn string
|
NameCn string
|
||||||
|
Year int
|
||||||
Season int
|
Season int
|
||||||
Episode int
|
StartEpisode int
|
||||||
|
EndEpisode int
|
||||||
Resolution string
|
Resolution string
|
||||||
IsSeasonPack bool
|
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 {
|
for _, name := range names {
|
||||||
re := regexp.MustCompile(`[^\p{L}\w\s]`)
|
|
||||||
name = re.ReplaceAllString(strings.ToLower(name), " ")
|
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), " ")
|
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
|
if utils.IsASCII(name) { //ascii name should match words
|
||||||
re := regexp.MustCompile(`\b` + name + `\b`)
|
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 true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseTv(name string) *Info {
|
||||||
func ParseTv(name string) *Metadata {
|
|
||||||
name = strings.ToLower(name)
|
name = strings.ToLower(name)
|
||||||
name = strings.ReplaceAll(name, "\u200b", "") //remove unicode hidden character
|
name = strings.ReplaceAll(name, "\u200b", "") //remove unicode hidden character
|
||||||
if utils.ContainsChineseChar(name) {
|
|
||||||
return parseChineseName(name)
|
return parseName(name)
|
||||||
}
|
|
||||||
return parseEnglishName(name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseEnglishName(name string) *Metadata {
|
func adjacentNumber(s string, start int) (n1 int, l int) {
|
||||||
re := regexp.MustCompile(`[^\p{L}\w\s]`)
|
runes := []rune(s)
|
||||||
name = re.ReplaceAllString(strings.ToLower(name), " ")
|
if start > len(runes)-1 { //out of bound
|
||||||
newSplits := strings.Split(strings.TrimSpace(name), " ")
|
return -1, -1
|
||||||
|
}
|
||||||
seasonRe := regexp.MustCompile(`^s\d{1,2}`)
|
var n []rune
|
||||||
resRe := regexp.MustCompile(`^\d{3,4}p`)
|
for i := start; i < len(runes); i++ {
|
||||||
episodeRe := regexp.MustCompile(`e\d{1,3}`)
|
k := runes[i]
|
||||||
|
if (k < '0' || k > '9') && !chineseNum[k] { //not digit anymore
|
||||||
var seasonIndex = -1
|
break
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
n = append(n, k)
|
||||||
}
|
}
|
||||||
|
if len(n) == 0 {
|
||||||
meta := &Metadata{
|
return -1, -1
|
||||||
Season: -1,
|
|
||||||
Episode: -1,
|
|
||||||
}
|
}
|
||||||
if seasonIndex != -1 {
|
m, err := strconv.Atoi(string(n))
|
||||||
//season exists
|
if err != nil {
|
||||||
ss := seasonRe.FindAllString(newSplits[seasonIndex], -1)
|
return chinese2Num[string(n)], len(n)
|
||||||
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 {
|
return m, len(n)
|
||||||
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 parseChineseName(name string) *Metadata {
|
func findSeason(s string) (n int, p int) {
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//season numner
|
//season numner
|
||||||
seasonRe1 := regexp.MustCompile(`s\d{1,2}`)
|
seasonRe1 := regexp.MustCompile(`s\d{1,2}`)
|
||||||
seasonMatches := seasonRe1.FindAllString(name, -1)
|
seasonMatches := seasonRe1.FindAllString(s, -1)
|
||||||
if len(seasonMatches) > 0 {
|
if len(seasonMatches) > 0 {
|
||||||
seNum := seasonMatches[0][1:]
|
seNum := seasonMatches[0][1:]
|
||||||
n, err := strconv.Atoi(seNum)
|
n, err := strconv.Atoi(seNum)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Sprintf("convert %s error: %v", seNum, err))
|
panic(fmt.Sprintf("convert %s error: %v", seNum, err))
|
||||||
}
|
}
|
||||||
meta.Season = n
|
|
||||||
|
return n, strings.Index(s, seNum)
|
||||||
} else {
|
} else {
|
||||||
seasonRe1 := regexp.MustCompile(`season \d{1,2}`)
|
seasonRe1 := regexp.MustCompile(`season \d{1,2}`)
|
||||||
seasonMatches := seasonRe1.FindAllString(name, -1)
|
seasonMatches := seasonRe1.FindAllString(s, -1)
|
||||||
if len(seasonMatches) > 0 {
|
if len(seasonMatches) > 0 {
|
||||||
re3 := regexp.MustCompile(`\d{1,2}`)
|
re3 := regexp.MustCompile(`\d{1,2}`)
|
||||||
seNum := re3.FindAllString(seasonMatches[0], -1)[0]
|
seNum := re3.FindAllString(seasonMatches[0], -1)[0]
|
||||||
@@ -250,10 +112,10 @@ func parseChineseName(name string) *Metadata {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Sprintf("convert %s error: %v", seNum, err))
|
panic(fmt.Sprintf("convert %s error: %v", seNum, err))
|
||||||
}
|
}
|
||||||
meta.Season = n
|
return n, strings.Index(s, seasonMatches[0])
|
||||||
} else {
|
} else {
|
||||||
seasonRe1 := regexp.MustCompile(`第.{1,2}季`)
|
seasonRe1 := regexp.MustCompile(`第.{1,2}季`)
|
||||||
seasonMatches := seasonRe1.FindAllString(name, -1)
|
seasonMatches := seasonRe1.FindAllString(s, -1)
|
||||||
if len(seasonMatches) > 0 {
|
if len(seasonMatches) > 0 {
|
||||||
m1 := []rune(seasonMatches[0])
|
m1 := []rune(seasonMatches[0])
|
||||||
seNum := m1[1 : len(m1)-1]
|
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)
|
log.Warnf("parse season number %v error: %v, try to parse using chinese", seNum, err)
|
||||||
n = chinese2Num[string(seNum)]
|
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 {
|
return -1, -1
|
||||||
meta.Season = meta.Episode
|
}
|
||||||
meta.Episode = -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
|
//tv name
|
||||||
|
if utils.IsASCII(name) && p < len(name) && p-1 > 0 {
|
||||||
fields := strings.FieldsFunc(name, func(r rune) bool {
|
meta.NameEn = strings.TrimSpace(name[:p-1])
|
||||||
return r == '[' || r == ']' || r == '【' || r == '】'
|
meta.NameCn = meta.NameEn
|
||||||
})
|
} else {
|
||||||
titleCn := ""
|
fields := strings.FieldsFunc(name, func(r rune) bool {
|
||||||
title := ""
|
return r == '[' || r == ']' || r == '【' || r == '】'
|
||||||
for _, p := range fields { //寻找匹配的最长的字符串,最有可能是名字
|
})
|
||||||
if utils.ContainsChineseChar(p) && len([]rune(p)) > len([]rune(titleCn)) { //最长含中文字符串
|
titleCn := ""
|
||||||
titleCn = p
|
title := ""
|
||||||
}
|
for _, p := range fields { //寻找匹配的最长的字符串,最有可能是名字
|
||||||
if len([]rune(p)) > len([]rune(title)) { //最长字符串
|
if utils.ContainsChineseChar(p) && len([]rune(p)) > len([]rune(titleCn)) { //最长含中文字符串
|
||||||
title = p
|
titleCn = p
|
||||||
}
|
}
|
||||||
}
|
if len([]rune(p)) > len([]rune(title)) { //最长字符串
|
||||||
re := regexp.MustCompile(`[^\p{L}\w\s]`)
|
title = p
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
re := regexp.MustCompile(`[^\p{L}\w\s]`)
|
||||||
|
title = re.ReplaceAllString(strings.TrimSpace(strings.ToLower(title)), "") //去除标点符号
|
||||||
|
titleCn = re.ReplaceAllString(strings.TrimSpace(strings.ToLower(titleCn)), "")
|
||||||
|
|
||||||
//匹配title中最长拉丁字符串
|
meta.NameCn = titleCn
|
||||||
enRe := regexp.MustCompile(`[[:ascii:]]*`)
|
cnRe := regexp.MustCompile(`\p{Han}.*\p{Han}`)
|
||||||
enM := enRe.FindAllString(title, -1)
|
cnmatches := cnRe.FindAllString(titleCn, -1)
|
||||||
if len(enM) > 0 {
|
|
||||||
for _, t := range enM {
|
//titleCn中最长的中文字符
|
||||||
if len(t) > len(meta.NameEn) {
|
if len(cnmatches) > 0 {
|
||||||
meta.NameEn = strings.TrimSpace(strings.ToLower(t))
|
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
|
return meta
|
||||||
@@ -330,3 +498,15 @@ var chinese2Num = map[string]int{
|
|||||||
"八": 8,
|
"八": 8,
|
||||||
"九": 9,
|
"九": 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)
|
m := ParseTv(s1)
|
||||||
log.Infof("results: %+v", m)
|
log.Infof("results: %+v", m)
|
||||||
assert.Equal(t, m.Season, 1)
|
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.IsSeasonPack, false)
|
||||||
assert.Equal(t, m.Resolution, "1080p")
|
assert.Equal(t, m.Resolution, "1080p")
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ func Test_ParseTV3(t *testing.T) {
|
|||||||
m := ParseTv(s1)
|
m := ParseTv(s1)
|
||||||
log.Infof("results: %+v", m)
|
log.Infof("results: %+v", m)
|
||||||
assert.Equal(t, m.Season, 37)
|
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.IsSeasonPack, false)
|
||||||
//assert.Equal(t, m.Resolution, "1080p")
|
//assert.Equal(t, m.Resolution, "1080p")
|
||||||
}
|
}
|
||||||
@@ -41,8 +41,8 @@ func Test_ParseTV4(t *testing.T) {
|
|||||||
m := ParseTv(s1)
|
m := ParseTv(s1)
|
||||||
log.Infof("results: %+v", m)
|
log.Infof("results: %+v", m)
|
||||||
assert.Equal(t, m.Season, 2)
|
assert.Equal(t, m.Season, 2)
|
||||||
//assert.Equal(t, m.Episode, 219)
|
assert.Equal(t, m.StartEpisode, 12)
|
||||||
assert.Equal(t, m.IsSeasonPack, true)
|
assert.Equal(t, m.IsSeasonPack, false)
|
||||||
//assert.Equal(t, m.Resolution, "1080p")
|
//assert.Equal(t, m.Resolution, "1080p")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ func Test_ParseTV6(t *testing.T) {
|
|||||||
m := ParseTv(s1)
|
m := ParseTv(s1)
|
||||||
log.Infof("results: %+v", m)
|
log.Infof("results: %+v", m)
|
||||||
assert.Equal(t, m.Season, 1)
|
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.IsSeasonPack, false)
|
||||||
assert.Equal(t, m.Resolution, "1080p")
|
assert.Equal(t, m.Resolution, "1080p")
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ func Test_ParseTV7(t *testing.T) {
|
|||||||
m := ParseTv(s1)
|
m := ParseTv(s1)
|
||||||
log.Infof("results: %+v", m)
|
log.Infof("results: %+v", m)
|
||||||
assert.Equal(t, m.Season, 1)
|
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.IsSeasonPack, false)
|
||||||
assert.Equal(t, m.Resolution, "1080p")
|
assert.Equal(t, m.Resolution, "1080p")
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ func Test_ParseTV8(t *testing.T) {
|
|||||||
m := ParseTv(s1)
|
m := ParseTv(s1)
|
||||||
log.Infof("results: %+v", m)
|
log.Infof("results: %+v", m)
|
||||||
assert.Equal(t, m.Season, 1)
|
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.IsSeasonPack, false)
|
||||||
assert.Equal(t, m.Resolution, "1080p")
|
assert.Equal(t, m.Resolution, "1080p")
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ func Test_ParseTV9(t *testing.T) {
|
|||||||
m := ParseTv(s1)
|
m := ParseTv(s1)
|
||||||
log.Infof("results: %+v", m)
|
log.Infof("results: %+v", m)
|
||||||
assert.Equal(t, m.Season, 1)
|
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.IsSeasonPack, false)
|
||||||
assert.Equal(t, m.Resolution, "1080p")
|
assert.Equal(t, m.Resolution, "1080p")
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ func Test_ParseTV11(t *testing.T) {
|
|||||||
m := ParseTv(s1)
|
m := ParseTv(s1)
|
||||||
log.Infof("results: %+v", m)
|
log.Infof("results: %+v", m)
|
||||||
assert.Equal(t, 2, m.Season)
|
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, false, m.IsSeasonPack)
|
||||||
assert.Equal(t, "1080p", m.Resolution)
|
assert.Equal(t, "1080p", m.Resolution)
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ func Test_ParseTV12(t *testing.T) {
|
|||||||
m := ParseTv(s1)
|
m := ParseTv(s1)
|
||||||
log.Infof("results: %+v", m)
|
log.Infof("results: %+v", m)
|
||||||
assert.Equal(t, 2, m.Season)
|
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, false, m.IsSeasonPack)
|
||||||
assert.Equal(t, "1080p", m.Resolution)
|
assert.Equal(t, "1080p", m.Resolution)
|
||||||
}
|
}
|
||||||
@@ -131,7 +131,7 @@ func Test_ParseTV13(t *testing.T) {
|
|||||||
m := ParseTv(s1)
|
m := ParseTv(s1)
|
||||||
log.Infof("results: %+v", m)
|
log.Infof("results: %+v", m)
|
||||||
assert.Equal(t, 2, m.Season)
|
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, false, m.IsSeasonPack)
|
||||||
assert.Equal(t, "1080p", m.Resolution)
|
assert.Equal(t, "1080p", m.Resolution)
|
||||||
}
|
}
|
||||||
@@ -141,10 +141,11 @@ func Test_ParseTV14(t *testing.T) {
|
|||||||
m := ParseTv(s1)
|
m := ParseTv(s1)
|
||||||
log.Infof("results: %+v", m)
|
log.Infof("results: %+v", m)
|
||||||
assert.Equal(t, 5, m.Season)
|
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, false, m.IsSeasonPack)
|
||||||
//assert.Equal(t, "720p", m.Resolution)
|
//assert.Equal(t, "720p", m.Resolution)
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
func Test_ParseTV15(t *testing.T) {
|
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, 113, m.Episode)
|
||||||
//assert.Equal(t, false, m.IsSeasonPack)
|
//assert.Equal(t, false, m.IsSeasonPack)
|
||||||
//assert.Equal(t, "720p", m.Resolution)
|
//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)
|
||||||
|
}
|
||||||
|
|||||||
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
@@ -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"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"polaris/log"
|
|
||||||
|
|
||||||
|
"github.com/gabriel-vasile/mimetype"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Storage interface {
|
func NewLocalStorage(dir string, videoFormats []string, subtitleFormats []string) (*LocalStorage, error) {
|
||||||
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) {
|
|
||||||
os.MkdirAll(dir, 0655)
|
os.MkdirAll(dir, 0655)
|
||||||
|
return &LocalStorage{dir: dir, videoFormats: videoFormats, subtitleFormats: subtitleFormats}, nil
|
||||||
return &LocalStorage{dir: dir}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocalStorage struct {
|
type LocalStorage struct {
|
||||||
dir string
|
dir string
|
||||||
|
videoFormats []string
|
||||||
|
subtitleFormats []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LocalStorage) Copy(src, destDir string) error {
|
func (l *LocalStorage) Copy(src, destDir string) error {
|
||||||
os.MkdirAll(filepath.Join(l.dir, destDir), os.ModePerm)
|
b, err := NewBase(src, l.videoFormats, l.subtitleFormats)
|
||||||
|
|
||||||
targetBase := filepath.Join(l.dir, destDir, filepath.Base(src)) //文件的场景,要加上文件名, move filename ./dir/
|
|
||||||
info, err := os.Stat(src)
|
|
||||||
if err != nil {
|
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
|
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 {
|
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)
|
os.MkdirAll(filepath.Dir(path), os.ModePerm)
|
||||||
return os.WriteFile(path, data, 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
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"polaris/log"
|
|
||||||
"polaris/pkg/gowebdav"
|
"polaris/pkg/gowebdav"
|
||||||
"polaris/pkg/utils"
|
|
||||||
|
|
||||||
"github.com/gabriel-vasile/mimetype"
|
"github.com/gabriel-vasile/mimetype"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -17,9 +16,12 @@ type WebdavStorage struct {
|
|||||||
fs *gowebdav.Client
|
fs *gowebdav.Client
|
||||||
dir string
|
dir string
|
||||||
changeMediaHash bool
|
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)
|
c := gowebdav.NewClient(url, user, password)
|
||||||
if err := c.Connect(); err != nil {
|
if err := c.Connect(); err != nil {
|
||||||
return nil, errors.Wrap(err, "connect webdav")
|
return nil, errors.Wrap(err, "connect webdav")
|
||||||
@@ -27,71 +29,35 @@ func NewWebdavStorage(url, user, password, path string, changeMediaHash bool) (*
|
|||||||
return &WebdavStorage{
|
return &WebdavStorage{
|
||||||
fs: c,
|
fs: c,
|
||||||
dir: path,
|
dir: path,
|
||||||
|
videoFormats: videoFormats,
|
||||||
|
subtitleFormats: subtitleFormats,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebdavStorage) Copy(local, remoteDir string) error {
|
func (w *WebdavStorage) Copy(local, remoteDir string) error {
|
||||||
remoteBase := filepath.Join(w.dir, remoteDir, filepath.Base(local))
|
b, err := NewBase(local, w.videoFormats, w.subtitleFormats)
|
||||||
info, err := os.Stat(local)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "read source dir")
|
return err
|
||||||
}
|
|
||||||
if info.IsDir() { //如果是路径,则只移动路径里面的文件,不管当前路径, 行为类似 move dirname/* target_dir/
|
|
||||||
remoteBase = filepath.Join(w.dir, remoteDir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//log.Infof("remove all content in %s", remoteBase)
|
w.progresser = b.Progress
|
||||||
//w.fs.RemoveAll(remoteBase)
|
|
||||||
err = filepath.Walk(local, func(path string, info fs.FileInfo, err error) error {
|
uploadFunc := func(destPath string, destInfo fs.FileInfo, srcReader io.Reader, mtype *mimetype.MIME) error {
|
||||||
if err != nil {
|
callback := func(r *http.Request) {
|
||||||
return errors.Wrapf(err, "read file %v", path)
|
r.Header.Set("Content-Type", mtype.String())
|
||||||
|
r.ContentLength = destInfo.Size()
|
||||||
}
|
}
|
||||||
|
|
||||||
rel, err := filepath.Rel(local, path)
|
if err := w.fs.WriteStream(destPath, srcReader, 0666, callback); err != nil {
|
||||||
if err != nil {
|
return errors.Wrap(err, "transmitting data error")
|
||||||
return errors.Wrap(err, "path relation")
|
|
||||||
}
|
}
|
||||||
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 b.Upload(filepath.Join(w.dir, remoteDir), false, true, w.changeMediaHash, uploadFunc, func(s string) error {
|
||||||
// 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 nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "move file error")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebdavStorage) Move(local, remoteDir string) error {
|
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 {
|
func (w *WebdavStorage) WriteFile(name string, data []byte) error {
|
||||||
return w.fs.Write(filepath.Join(w.dir, name), data, os.ModePerm)
|
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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -93,11 +93,12 @@ func (r *Response) ToResults(indexer *db.TorznabInfo) []Result {
|
|||||||
}
|
}
|
||||||
r := Result{
|
r := Result{
|
||||||
Name: item.Title,
|
Name: item.Title,
|
||||||
|
Description: item.Description,
|
||||||
Link: item.Link,
|
Link: item.Link,
|
||||||
Size: mustAtoI(item.Size),
|
Size: mustAtoI(item.Size),
|
||||||
Seeders: mustAtoI(item.GetAttr("seeders")),
|
Seeders: int(mustAtoI(item.GetAttr("seeders"))),
|
||||||
Peers: mustAtoI(item.GetAttr("peers")),
|
Peers: int(mustAtoI(item.GetAttr("peers"))),
|
||||||
Category: mustAtoI(item.GetAttr("category")),
|
Category: int(mustAtoI(item.GetAttr("category"))),
|
||||||
ImdbId: imdb,
|
ImdbId: imdb,
|
||||||
DownloadVolumeFactor: tryParseFloat(item.GetAttr("downloadvolumefactor")),
|
DownloadVolumeFactor: tryParseFloat(item.GetAttr("downloadvolumefactor")),
|
||||||
UploadVolumeFactor: tryParseFloat(item.GetAttr("uploadvolumefactor")),
|
UploadVolumeFactor: tryParseFloat(item.GetAttr("uploadvolumefactor")),
|
||||||
@@ -111,8 +112,8 @@ func (r *Response) ToResults(indexer *db.TorznabInfo) []Result {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustAtoI(key string) int {
|
func mustAtoI(key string) int64 {
|
||||||
i, err := strconv.Atoi(key)
|
i, err := strconv.ParseInt(key, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("must atoi error: %v", err)
|
log.Errorf("must atoi error: %v", err)
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -130,7 +131,7 @@ func tryParseFloat(s string) float32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Search(indexer *db.TorznabInfo, keyWord string) ([]Result, error) {
|
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()
|
defer cancel()
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, indexer.URL, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, indexer.URL, nil)
|
||||||
@@ -180,8 +181,9 @@ func doRequest(req *http.Request) (*Response, error) {
|
|||||||
|
|
||||||
type Result struct {
|
type Result struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
Link string `json:"link"`
|
Link string `json:"link"`
|
||||||
Size int `json:"size"`
|
Size int64 `json:"size"`
|
||||||
Seeders int `json:"seeders"`
|
Seeders int `json:"seeders"`
|
||||||
Peers int `json:"peers"`
|
Peers int `json:"peers"`
|
||||||
Category int `json:"category"`
|
Category int `json:"category"`
|
||||||
|
|||||||
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"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -230,7 +231,7 @@ func Link2Magnet(link string) (string, error) {
|
|||||||
return http.ErrUseLastResponse //do not follow redirects
|
return http.ErrUseLastResponse //do not follow redirects
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.Get(link)
|
resp, err := client.Get(link)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "get link")
|
return "", errors.Wrap(err, "get link")
|
||||||
@@ -252,7 +253,6 @@ func Link2Magnet(link string) (string, error) {
|
|||||||
return mg.String(), nil
|
return mg.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func MagnetHash(link string) (string, error) {
|
func MagnetHash(link string) (string, error) {
|
||||||
if mi, err := metainfo.ParseMagnetV2Uri(link); err != nil {
|
if mi, err := metainfo.ParseMagnetV2Uri(link); err != nil {
|
||||||
return "", errors.Errorf("magnet link is not valid: %v", err)
|
return "", errors.Errorf("magnet link is not valid: %v", err)
|
||||||
@@ -271,4 +271,18 @@ func MagnetHash(link string) (string, error) {
|
|||||||
}
|
}
|
||||||
return hash, nil
|
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 {
|
type Activity struct {
|
||||||
*ent.History
|
*ent.History
|
||||||
Progress int `json:"progress"`
|
Progress int `json:"progress"`
|
||||||
SeedRatio float64 `json:"seed_ratio"`
|
SeedRatio float64 `json:"seed_ratio"`
|
||||||
|
UploadProgress float64 `json:"upload_progress"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) GetAllActivities(c *gin.Context) (interface{}, error) {
|
func (s *Server) GetAllActivities(c *gin.Context) (interface{}, error) {
|
||||||
@@ -44,6 +45,9 @@ func (s *Server) GetAllActivities(c *gin.Context) (interface{}, error) {
|
|||||||
} else {
|
} else {
|
||||||
a.SeedRatio = r
|
a.SeedRatio = r
|
||||||
}
|
}
|
||||||
|
if task.UploadProgresser != nil {
|
||||||
|
a.UploadProgress = task.UploadProgresser()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
activities = append(activities, a)
|
activities = append(activities, a)
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ func (c *Client) registerCronJob(name string, cron string, f func() error) {
|
|||||||
func (c *Client) Init() {
|
func (c *Client) Init() {
|
||||||
go c.reloadTasks()
|
go c.reloadTasks()
|
||||||
c.addSysCron()
|
c.addSysCron()
|
||||||
|
go c.checkW500PosterOnStartup()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) reloadTasks() {
|
func (c *Client) reloadTasks() {
|
||||||
|
|||||||
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"`
|
Resolution string `json:"resolution" binding:"required"`
|
||||||
Folder string `json:"folder" binding:"required"`
|
Folder string `json:"folder" binding:"required"`
|
||||||
DownloadHistoryEpisodes bool `json:"download_history_episodes"` //for tv
|
DownloadHistoryEpisodes bool `json:"download_history_episodes"` //for tv
|
||||||
SizeMin int `json:"size_min"`
|
SizeMin int64 `json:"size_min"`
|
||||||
SizeMax int `json:"size_max"`
|
SizeMax int64 `json:"size_max"`
|
||||||
|
PreferSize int64 `json:"prefer_size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
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)
|
log.Infof("find detail for tv id %d: %+v", in.TmdbID, detail)
|
||||||
|
|
||||||
lastSeason := 0
|
lastSeason := 0
|
||||||
for _, season := range detail.Seasons {
|
for _, season := range detail.Seasons {
|
||||||
if season.SeasonNumber > lastSeason && season.EpisodeCount > 0 { //如果最新一季已经有剧集信息,则以最新一季为准
|
if season.SeasonNumber > lastSeason && season.EpisodeCount > 0 { //如果最新一季已经有剧集信息,则以最新一季为准
|
||||||
lastSeason = season.SeasonNumber
|
lastSeason = season.SeasonNumber
|
||||||
@@ -148,6 +149,11 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
|||||||
|
|
||||||
log.Debugf("latest season is %v", lastSeason)
|
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
|
var epIds []int
|
||||||
for _, season := range detail.Seasons {
|
for _, season := range detail.Seasons {
|
||||||
seasonId := season.SeasonNumber
|
seasonId := season.SeasonNumber
|
||||||
@@ -194,6 +200,7 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
|||||||
epIds = append(epIds, epid)
|
epIds = append(epIds, epid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m := &ent.Media{
|
m := &ent.Media{
|
||||||
TmdbID: int(detail.ID),
|
TmdbID: int(detail.ID),
|
||||||
ImdbID: detail.IMDbID,
|
ImdbID: detail.IMDbID,
|
||||||
@@ -212,6 +219,7 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
|||||||
OriginalLanguage: detail.OriginalLanguage,
|
OriginalLanguage: detail.OriginalLanguage,
|
||||||
Genres: detail.Genres,
|
Genres: detail.Genres,
|
||||||
},
|
},
|
||||||
|
AlternativeTitles: alterTitles,
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := c.db.AddMediaWatchlist(m, epIds)
|
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 {
|
if err := c.downloadPoster(detail.PosterPath, r.ID); err != nil {
|
||||||
log.Errorf("download poster error: %v", err)
|
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 {
|
if err := c.downloadBackdrop(detail.BackdropPath, r.ID); err != nil {
|
||||||
log.Errorf("download poster error: %v", err)
|
log.Errorf("download poster error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -235,6 +247,42 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
|||||||
return nil, nil
|
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) {
|
func (c *Client) AddMovie2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||||
log.Infof("add movie watchlist input: %+v", in)
|
log.Infof("add movie watchlist input: %+v", in)
|
||||||
detailCn, err := c.MustTMDB().GetMovieDetails(in.TmdbID, db.LanguageCN)
|
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)
|
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{
|
epid, err := c.db.SaveEposideDetail(&ent.Episode{
|
||||||
SeasonNumber: 1,
|
SeasonNumber: 1,
|
||||||
EpisodeNumber: 1,
|
EpisodeNumber: 1,
|
||||||
@@ -279,6 +333,7 @@ func (c *Client) AddMovie2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
|||||||
StorageID: in.StorageID,
|
StorageID: in.StorageID,
|
||||||
TargetDir: in.Folder,
|
TargetDir: in.Folder,
|
||||||
Limiter: schema.MediaLimiter{SizeMin: in.SizeMin, SizeMax: in.SizeMax},
|
Limiter: schema.MediaLimiter{SizeMin: in.SizeMin, SizeMax: in.SizeMax},
|
||||||
|
AlternativeTitles: alterTitles,
|
||||||
}
|
}
|
||||||
|
|
||||||
extras := schema.MediaExtras{
|
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 {
|
if err := c.downloadPoster(detail.PosterPath, r.ID); err != nil {
|
||||||
log.Errorf("download poster error: %v", err)
|
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 {
|
if err := c.downloadBackdrop(detail.BackdropPath, r.ID); err != nil {
|
||||||
log.Errorf("download backdrop error: %v", err)
|
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 {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -327,8 +386,8 @@ func (c *Client) checkMovieFolder(m *ent.Media) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _,f := range files {
|
for _, f := range files {
|
||||||
if f.IsDir() || f.Size() < 100 * 1000 * 1000 /* 100M */{ //忽略路径和小于100M的文件
|
if f.IsDir() || f.Size() < 100*1000*1000 /* 100M */ { //忽略路径和小于100M的文件
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
meta := metadata.ParseMovie(f.Name())
|
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")
|
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 {
|
func (c *Client) downloadImage(url string, mediaID int, name string) error {
|
||||||
|
|
||||||
log.Infof("try to download image: %v", url)
|
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) {
|
func (c *Client) SuggestedMovieFolderName(tmdbId int) (string, error) {
|
||||||
|
|
||||||
d1, err := c.MustTMDB().GetMovieDetails(tmdbId, c.language)
|
d1, err := c.MustTMDB().GetMovieDetails(tmdbId, c.language)
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"polaris/db"
|
"polaris/db"
|
||||||
"polaris/ent/media"
|
"polaris/ent/media"
|
||||||
storage1 "polaris/ent/storage"
|
storage1 "polaris/ent/storage"
|
||||||
"polaris/log"
|
"polaris/log"
|
||||||
|
"polaris/pkg/alist"
|
||||||
"polaris/pkg/metadata"
|
"polaris/pkg/metadata"
|
||||||
"polaris/pkg/notifier"
|
"polaris/pkg/notifier"
|
||||||
"polaris/pkg/storage"
|
"polaris/pkg/storage"
|
||||||
@@ -18,6 +18,8 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) writeNfoFile(historyId int) error {
|
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
|
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 {
|
if err != nil {
|
||||||
return errors.Wrap(err, "get storage")
|
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
|
} 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 {
|
if err != nil {
|
||||||
return errors.Wrap(err, "get storage")
|
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
|
if series.MediaType != media.MediaTypeTv { //.plexmatch only support tv series
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
st, err := c.getStorage(series.StorageID, media.MediaTypeTv)
|
st, err := c.GetStorage(series.StorageID, media.MediaTypeTv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "get storage")
|
return errors.Wrap(err, "get storage")
|
||||||
}
|
}
|
||||||
@@ -195,17 +197,26 @@ func (c *Client) nfoSupportEnabled() bool {
|
|||||||
return c.db.GetSetting(db.SettingNfoSupportEnabled) == "true"
|
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)
|
st := c.db.GetStorage(storageId)
|
||||||
targetPath := st.TvPath
|
targetPath := st.TvPath
|
||||||
if mediaType == media.MediaTypeMovie {
|
if mediaType == media.MediaTypeMovie {
|
||||||
targetPath = st.MoviePath
|
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 {
|
switch st.Implementation {
|
||||||
case storage1.ImplementationLocal:
|
case storage1.ImplementationLocal:
|
||||||
|
|
||||||
storageImpl1, err := storage.NewLocalStorage(targetPath)
|
storageImpl1, err := storage.NewLocalStorage(targetPath, videoFormats, subtitleFormats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "new local")
|
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:
|
case storage1.ImplementationWebdav:
|
||||||
ws := st.ToWebDavSetting()
|
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 {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "new webdav")
|
return nil, errors.Wrap(err, "new webdav")
|
||||||
}
|
}
|
||||||
return storageImpl1, nil
|
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")
|
return nil, errors.New("no storage found")
|
||||||
}
|
}
|
||||||
@@ -300,9 +318,9 @@ func (c *Client) findEpisodeFilesPreMoving(historyId int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
meta := metadata.ParseTv(f.Name())
|
meta := metadata.ParseTv(f.Name())
|
||||||
if meta.Episode > 0 {
|
if meta.StartEpisode > 0 {
|
||||||
//episode exists
|
//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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"polaris/ent"
|
"polaris/ent"
|
||||||
"polaris/ent/episode"
|
"polaris/ent/episode"
|
||||||
"polaris/ent/history"
|
"polaris/ent/history"
|
||||||
"polaris/log"
|
"polaris/log"
|
||||||
|
"polaris/pkg/metadata"
|
||||||
"polaris/pkg/notifier/message"
|
"polaris/pkg/notifier/message"
|
||||||
"polaris/pkg/torznab"
|
"polaris/pkg/torznab"
|
||||||
"polaris/pkg/utils"
|
"polaris/pkg/utils"
|
||||||
@@ -13,7 +15,7 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"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()
|
trc, dlc, err := c.GetDownloadClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "connect transmission")
|
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")
|
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)
|
magnet, err := utils.Link2Magnet(r1.Link)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Errorf("converting link to magnet error, link: %v, error: %v", r1.Link, err)
|
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)
|
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{
|
history, err := c.db.SaveHistoryRecord(ent.History{
|
||||||
MediaID: seriesId,
|
MediaID: seriesId,
|
||||||
EpisodeID: ep.ID,
|
EpisodeNums: episodeNums,
|
||||||
SourceTitle: r1.Name,
|
SeasonNum: seasonNum,
|
||||||
TargetDir: dir,
|
SourceTitle: r1.Name,
|
||||||
Status: history.StatusRunning,
|
TargetDir: dir,
|
||||||
Size: r1.Size,
|
Status: history.StatusRunning,
|
||||||
|
Size: int(r1.Size),
|
||||||
//Saved: torrent.Save(),
|
//Saved: torrent.Save(),
|
||||||
Link: magnet,
|
Link: magnet,
|
||||||
DownloadClientID: dlc.ID,
|
DownloadClientID: dlc.ID,
|
||||||
@@ -72,42 +77,122 @@ func (c *Client) DownloadEpisodeTorrent(r1 torznab.Result, seriesId, seasonNum,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "save record")
|
return nil, errors.Wrap(err, "save record")
|
||||||
}
|
}
|
||||||
if episodeNum > 0 {
|
|
||||||
if ep.Status == episode.StatusMissing {
|
torrent, err := trc.Download(magnet, downloadDir)
|
||||||
c.db.SetEpisodeStatus(ep.ID, episode.StatusDownloading)
|
if err != nil {
|
||||||
}
|
return nil, errors.Wrap(err, "downloading")
|
||||||
} else {
|
|
||||||
c.db.SetSeasonAllEpisodeStatus(seriesId, seasonNum, episode.StatusDownloading)
|
|
||||||
}
|
}
|
||||||
|
torrent.Start()
|
||||||
|
|
||||||
c.tasks[history.ID] = &Task{Torrent: torrent}
|
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)
|
log.Infof("success add %s to download task", r1.Name)
|
||||||
return &r1.Name, nil
|
return &r1.Name, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
func (c *Client) SearchAndDownload(seriesId, seasonNum, episodeNum int) (*string, error) {
|
|
||||||
var episodes []int
|
/*
|
||||||
if episodeNum > 0 {
|
tmdb 校验获取的资源名,如果用资源名在tmdb搜索出来的结果能匹配上想要的资源,则认为资源有效,否则无效
|
||||||
episodes = append(episodes, episodeNum)
|
解决名称过于简单的影视会匹配过多资源的问题, 例如:梦魇绝镇 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{
|
res, err := SearchTvSeries(c.db, &SearchParam{
|
||||||
MediaId: seriesId,
|
MediaId: seriesId,
|
||||||
SeasonNum: seasonNum,
|
SeasonNum: seasonNum,
|
||||||
Episodes: episodes,
|
Episodes: episodeNums,
|
||||||
CheckFileSize: true,
|
CheckFileSize: true,
|
||||||
CheckResolution: true,
|
CheckResolution: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
r1 := res[0]
|
wanted := make(map[int]bool, len(episodeNums))
|
||||||
log.Infof("found resource to download: %+v", r1)
|
for _, ep := range episodeNums {
|
||||||
return c.DownloadEpisodeTorrent(r1, seriesId, seasonNum, episodeNum)
|
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()
|
trc, dlc, err := c.GetDownloadClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "connect transmission")
|
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() {
|
go func() {
|
||||||
ep, _ := c.db.GetMovieDummyEpisode(m.ID)
|
ep, _ := c.db.GetMovieDummyEpisode(m.ID)
|
||||||
history, err := c.db.SaveHistoryRecord(ent.History{
|
history, err := c.db.SaveHistoryRecord(ent.History{
|
||||||
MediaID: m.ID,
|
MediaID: m.ID,
|
||||||
EpisodeID: ep.ID,
|
EpisodeID: ep.ID,
|
||||||
SourceTitle: name,
|
SourceTitle: name,
|
||||||
TargetDir: m.TargetDir,
|
TargetDir: m.TargetDir,
|
||||||
Status: history.StatusRunning,
|
Status: history.StatusRunning,
|
||||||
Size: size,
|
Size: int(size),
|
||||||
//Saved: torrent.Save(),
|
//Saved: torrent.Save(),
|
||||||
Link: magnet,
|
Link: magnet,
|
||||||
DownloadClientID: dlc.ID,
|
DownloadClientID: dlc.ID,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"polaris/db"
|
"polaris/db"
|
||||||
"polaris/ent"
|
"polaris/ent"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
"polaris/pkg"
|
"polaris/pkg"
|
||||||
"polaris/pkg/notifier/message"
|
"polaris/pkg/notifier/message"
|
||||||
"polaris/pkg/utils"
|
"polaris/pkg/utils"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@@ -19,6 +21,10 @@ import (
|
|||||||
func (c *Client) addSysCron() {
|
func (c *Client) addSysCron() {
|
||||||
c.registerCronJob("check_running_tasks", "@every 1m", c.checkTasks)
|
c.registerCronJob("check_running_tasks", "@every 1m", c.checkTasks)
|
||||||
c.registerCronJob("check_available_medias_to_download", "0 0 * * * *", func() error {
|
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.downloadAllTvSeries()
|
||||||
c.downloadAllMovies()
|
c.downloadAllMovies()
|
||||||
return nil
|
return nil
|
||||||
@@ -114,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) {
|
func (c *Client) moveCompletedTask(id int) (err1 error) {
|
||||||
torrent := c.tasks[id]
|
torrent := c.tasks[id]
|
||||||
r := c.db.GetHistory(id)
|
r := c.db.GetHistory(id)
|
||||||
@@ -122,11 +160,7 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
c.db.SetHistoryStatus(r.ID, history.StatusUploading)
|
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)
|
downloadclient, err := c.db.GetDownloadClient(r.DownloadClientID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("get task download client error: %v, use default one", err)
|
log.Errorf("get task download client error: %v, use default one", err)
|
||||||
@@ -137,13 +171,19 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
seasonNum := getSeasonNum(r)
|
||||||
|
|
||||||
|
episodeIds := c.GetEpisodeIds(r)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
||||||
if err1 != nil {
|
if err1 != nil {
|
||||||
c.db.SetHistoryStatus(r.ID, history.StatusFail)
|
c.db.SetHistoryStatus(r.ID, history.StatusFail)
|
||||||
if r.EpisodeID != 0 {
|
if len(episodeIds) > 0 {
|
||||||
if !c.db.IsEpisodeDownloadingOrDownloaded(r.EpisodeID) {
|
for _, id := range episodeIds {
|
||||||
c.db.SetEpisodeStatus(r.EpisodeID, episode.StatusMissing)
|
if !c.db.IsEpisodeDownloadingOrDownloaded(id) {
|
||||||
|
c.db.SetEpisodeStatus(id, episode.StatusMissing)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
c.db.SetSeasonAllEpisodeStatus(r.MediaID, seasonNum, episode.StatusMissing)
|
c.db.SetSeasonAllEpisodeStatus(r.MediaID, seasonNum, episode.StatusMissing)
|
||||||
@@ -163,7 +203,7 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
|
|||||||
}
|
}
|
||||||
st := c.db.GetStorage(series.StorageID)
|
st := c.db.GetStorage(series.StorageID)
|
||||||
log.Infof("move task files to target dir: %v", r.TargetDir)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -172,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 {
|
if err := stImpl.Copy(filepath.Join(c.db.GetDownloadDir(), torrentName), r.TargetDir); err != nil {
|
||||||
return errors.Wrap(err, "move file")
|
return errors.Wrap(err, "move file")
|
||||||
}
|
}
|
||||||
|
torrent.UploadProgresser = stImpl.UploadProgress
|
||||||
|
|
||||||
c.db.SetHistoryStatus(r.ID, history.StatusSeeding)
|
c.db.SetHistoryStatus(r.ID, history.StatusSeeding)
|
||||||
if r.EpisodeID != 0 {
|
if len(episodeIds) > 0 {
|
||||||
c.db.SetEpisodeStatus(r.EpisodeID, episode.StatusDownloaded)
|
for _, id := range episodeIds {
|
||||||
|
c.db.SetEpisodeStatus(id, episode.StatusDownloaded)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
c.db.SetSeasonAllEpisodeStatus(r.MediaID, seasonNum, episode.StatusDownloaded)
|
c.db.SetSeasonAllEpisodeStatus(r.MediaID, seasonNum, episode.StatusDownloaded)
|
||||||
}
|
}
|
||||||
@@ -200,7 +243,7 @@ func (c *Client) CheckDownloadedSeriesFiles(m *ent.Media) error {
|
|||||||
}
|
}
|
||||||
log.Infof("check files in directory: %s", m.TargetDir)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -248,6 +291,7 @@ func (c *Client) CheckDownloadedSeriesFiles(m *ent.Media) error {
|
|||||||
type Task struct {
|
type Task struct {
|
||||||
//Processing bool
|
//Processing bool
|
||||||
pkg.Torrent
|
pkg.Torrent
|
||||||
|
UploadProgresser func() float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) DownloadSeriesAllEpisodes(id int) []string {
|
func (c *Client) DownloadSeriesAllEpisodes(id int) []string {
|
||||||
@@ -262,40 +306,53 @@ func (c *Client) DownloadSeriesAllEpisodes(id int) []string {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
wantedSeasonPack := true
|
wantedSeasonPack := true
|
||||||
|
seasonEpisodesWanted := make(map[int][]int, 0)
|
||||||
for _, ep := range epsides {
|
for _, ep := range epsides {
|
||||||
if !ep.Monitored {
|
if !ep.Monitored {
|
||||||
wantedSeasonPack = false
|
wantedSeasonPack = false
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if ep.Status != episode.StatusMissing {
|
if ep.Status != episode.StatusMissing {
|
||||||
wantedSeasonPack = false
|
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 {
|
if wantedSeasonPack {
|
||||||
name, err := c.SearchAndDownload(id, seasonNum, -1)
|
names, err := c.SearchAndDownload(id, seasonNum)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
allNames = append(allNames, *name)
|
allNames = append(allNames, names...)
|
||||||
log.Infof("begin download torrent resource: %v", name)
|
log.Infof("begin download torrent resource: %v", names)
|
||||||
} else {
|
} else {
|
||||||
log.Warnf("finding season pack error: %v", err)
|
log.Warnf("finding season pack error: %v", err)
|
||||||
wantedSeasonPack = false
|
wantedSeasonPack = false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if !wantedSeasonPack {
|
if !wantedSeasonPack {
|
||||||
for _, ep := range epsides {
|
|
||||||
if !ep.Monitored {
|
for se, eps := range seasonEpisodesWanted {
|
||||||
continue
|
names, err := c.SearchAndDownload(id, se, eps...)
|
||||||
}
|
|
||||||
if ep.Status != episode.StatusMissing {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name, err := c.SearchAndDownload(id, ep.SeasonNumber, ep.EpisodeNumber)
|
|
||||||
if err != nil {
|
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
|
continue
|
||||||
} else {
|
} else {
|
||||||
allNames = append(allNames, *name)
|
allNames = append(allNames, names...)
|
||||||
log.Infof("begin download torrent resource: %v", name)
|
log.Infof("begin download torrent resource: %v", names)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,12 +434,12 @@ func (c *Client) downloadMovieSingleEpisode(ep *ent.Episode, targetDir string) (
|
|||||||
torrent.Start()
|
torrent.Start()
|
||||||
|
|
||||||
history, err := c.db.SaveHistoryRecord(ent.History{
|
history, err := c.db.SaveHistoryRecord(ent.History{
|
||||||
MediaID: ep.MediaID,
|
MediaID: ep.MediaID,
|
||||||
EpisodeID: ep.ID,
|
EpisodeID: ep.ID,
|
||||||
SourceTitle: r1.Name,
|
SourceTitle: r1.Name,
|
||||||
TargetDir: targetDir,
|
TargetDir: targetDir,
|
||||||
Status: history.StatusRunning,
|
Status: history.StatusRunning,
|
||||||
Size: r1.Size,
|
Size: int(r1.Size),
|
||||||
//Saved: torrent.Save(),
|
//Saved: torrent.Save(),
|
||||||
Link: magnet,
|
Link: magnet,
|
||||||
DownloadClientID: dlc.ID,
|
DownloadClientID: dlc.ID,
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package core
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"polaris/db"
|
"polaris/db"
|
||||||
|
"polaris/ent"
|
||||||
"polaris/ent/media"
|
"polaris/ent/media"
|
||||||
"polaris/log"
|
"polaris/log"
|
||||||
|
"polaris/pkg/gemini"
|
||||||
"polaris/pkg/metadata"
|
"polaris/pkg/metadata"
|
||||||
"polaris/pkg/prowlarr"
|
"polaris/pkg/prowlarr"
|
||||||
"polaris/pkg/torznab"
|
"polaris/pkg/torznab"
|
||||||
@@ -26,22 +28,128 @@ type SearchParam struct {
|
|||||||
FilterQiangban bool //for movie, 是否过滤枪版电影
|
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) {
|
func SearchTvSeries(db1 *db.Client, param *SearchParam) ([]torznab.Result, error) {
|
||||||
series := db1.GetMediaDetails(param.MediaId)
|
series := db1.GetMediaDetails(param.MediaId)
|
||||||
if series == nil {
|
if series == nil {
|
||||||
return nil, fmt.Errorf("no tv series of id %v", param.MediaId)
|
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)
|
log.Debugf("check tv series %s, season %d, episode %v", series.NameEn, param.SeasonNum, param.Episodes)
|
||||||
|
|
||||||
res := searchWithTorznab(db1, prowlarr.TV, 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
|
var filtered []torznab.Result
|
||||||
|
lo:
|
||||||
for _, r := range res {
|
for _, r := range res {
|
||||||
//log.Infof("torrent resource: %+v", r)
|
//log.Infof("torrent resource: %+v", r)
|
||||||
meta := metadata.ParseTv(r.Name)
|
meta := metadata.ParseTv(r.Name)
|
||||||
if meta == nil { //cannot parse name
|
meta.ParseExtraDescription(r.Description)
|
||||||
continue
|
|
||||||
}
|
|
||||||
if isImdbidNotMatch(series.ImdbID, r.ImdbId) { //has imdb id and not match
|
if isImdbidNotMatch(series.ImdbID, r.ImdbId) { //has imdb id and not match
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -61,9 +169,15 @@ func SearchTvSeries(db1 *db.Client, param *SearchParam) ([]torznab.Result, error
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(param.Episodes) > 0 && !slices.Contains(param.Episodes, meta.Episode) { //not season pack, but episode number not equal
|
if len(param.Episodes) > 0 { //not season pack, but episode number not equal
|
||||||
continue
|
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
|
} else if len(param.Episodes) == 0 && !meta.IsSeasonPack { //want season pack, but not season pack
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -74,7 +188,7 @@ func SearchTvSeries(db1 *db.Client, param *SearchParam) ([]torznab.Result, error
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !torrentSizeOk(series, r.Size, param) {
|
if !torrentSizeOk(series, limiter, r.Size, meta.EndEpisode+1-meta.StartEpisode, param) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,36 +222,47 @@ func imdbIDMatchExact(id1, id2 string) bool {
|
|||||||
return id1 == id2
|
return id1 == id2
|
||||||
}
|
}
|
||||||
|
|
||||||
func torrentSizeOk(detail *db.MediaDetails, torrentSize int, param *SearchParam) bool {
|
func torrentSizeOk(detail *db.MediaDetails, globalLimiter *db.MediaSizeLimiter, torrentSize int64,
|
||||||
defaultMinSize := 80 * 1000 * 1000 //tv, 80M min
|
torrentEpisodeNum int, param *SearchParam) bool {
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
multiplier := 1 //大小倍数,正常为1,如果是季包则为季内集数
|
multiplier := 1 //大小倍数,正常为1,如果是季包则为季内集数
|
||||||
if detail.MediaType == media.MediaTypeTv && len(param.Episodes) == 0 { //tv season pack
|
if detail.MediaType == media.MediaTypeTv {
|
||||||
multiplier = seasonEpisodeCount(detail, param.SeasonNum)
|
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 param.CheckFileSize { //check file size when trigger automatic download
|
||||||
|
|
||||||
if detail.Limiter.SizeMin > 0 { //min size
|
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
|
if torrentSize < sizeMin { //比最小要求的大小还要小, min size not qualify
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if detail.Limiter.SizeMax > 0 { //max size
|
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
|
if torrentSize > sizeMax { //larger than max size wanted, max size not qualify
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return torrentSize > defaultMinSize*multiplier
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func seasonEpisodeCount(detail *db.MediaDetails, seasonNum int) int {
|
func seasonEpisodeCount(detail *db.MediaDetails, seasonNum int) int {
|
||||||
@@ -171,7 +296,16 @@ func SearchMovie(db1 *db.Client, param *SearchParam) ([]torznab.Result, error) {
|
|||||||
return nil, errors.New("no media found of id")
|
return nil, errors.New("no media found of id")
|
||||||
}
|
}
|
||||||
|
|
||||||
res := searchWithTorznab(db1, prowlarr.Movie, 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() {
|
if movieDetail.Extras.IsJav() {
|
||||||
res1 := searchWithTorznab(db1, prowlarr.Movie, movieDetail.Extras.JavId)
|
res1 := searchWithTorznab(db1, prowlarr.Movie, movieDetail.Extras.JavId)
|
||||||
res = append(res, res1...)
|
res = append(res, res1...)
|
||||||
@@ -211,7 +345,7 @@ func SearchMovie(db1 *db.Client, param *SearchParam) ([]torznab.Result, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !torrentSizeOk(movieDetail, r.Size, param) {
|
if !torrentSizeOk(movieDetail, limiter, r.Size, 1, param) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,5 +467,7 @@ func torrentNameOk(detail *db.MediaDetails, tester NameTester) bool {
|
|||||||
if detail.Extras.IsJav() && tester.IsAcceptable(detail.Extras.JavId) {
|
if detail.Extras.IsJav() && tester.IsAcceptable(detail.Extras.JavId) {
|
||||||
return true
|
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]
|
r1 := res[0]
|
||||||
log.Infof("found resource to download: %+v", r1)
|
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 {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "download")
|
return nil, errors.Wrap(err, "download")
|
||||||
}
|
}
|
||||||
name = *name1
|
name = name1[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
return gin.H{
|
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)
|
name = fmt.Sprintf("%v S%02d", m.OriginalName, in.Season)
|
||||||
}
|
}
|
||||||
res := torznab.Result{Name: name, Link: in.Link, Size: in.Size}
|
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
|
name := in.Name
|
||||||
if name == "" {
|
if name == "" {
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"polaris/db"
|
"polaris/db"
|
||||||
"polaris/log"
|
"polaris/log"
|
||||||
|
"polaris/pkg/cache"
|
||||||
"polaris/pkg/tmdb"
|
"polaris/pkg/tmdb"
|
||||||
"polaris/server/core"
|
"polaris/server/core"
|
||||||
"polaris/ui"
|
"polaris/ui"
|
||||||
|
"time"
|
||||||
|
|
||||||
ginzap "github.com/gin-contrib/zap"
|
ginzap "github.com/gin-contrib/zap"
|
||||||
|
|
||||||
@@ -22,20 +24,24 @@ import (
|
|||||||
func NewServer(db *db.Client) *Server {
|
func NewServer(db *db.Client) *Server {
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
s := &Server{
|
s := &Server{
|
||||||
r: r,
|
r: r,
|
||||||
db: db,
|
db: db,
|
||||||
language: db.GetLanguage(),
|
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)
|
s.core = core.NewClient(db, s.language)
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
r *gin.Engine
|
r *gin.Engine
|
||||||
db *db.Client
|
db *db.Client
|
||||||
core *core.Client
|
core *core.Client
|
||||||
language string
|
language string
|
||||||
jwtSerect string
|
jwtSerect string
|
||||||
|
monitorNumCache *cache.Cache[int, int]
|
||||||
|
downloadNumCache *cache.Cache[int, int]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Serve() error {
|
func (s *Server) Serve() error {
|
||||||
@@ -72,6 +78,10 @@ func (s *Server) Serve() error {
|
|||||||
setting.POST("/cron/trigger", HttpHandler(s.TriggerCronJob))
|
setting.POST("/cron/trigger", HttpHandler(s.TriggerCronJob))
|
||||||
setting.GET("/prowlarr", HttpHandler(s.GetProwlarrSetting))
|
setting.GET("/prowlarr", HttpHandler(s.GetProwlarrSetting))
|
||||||
setting.POST("/prowlarr", HttpHandler(s.SaveProwlarrSetting))
|
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")
|
activity := api.Group("/activity")
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ func (s *Server) SetSetting(c *gin.Context) (interface{}, error) {
|
|||||||
if _, err := template.New("test").Parse(in.MovieNamingFormat); err != nil {
|
if _, err := template.New("test").Parse(in.MovieNamingFormat); err != nil {
|
||||||
return nil, errors.Wrap(err, "movie format")
|
return nil, errors.Wrap(err, "movie format")
|
||||||
}
|
}
|
||||||
|
|
||||||
s.db.SetSetting(db.SettingMovieNamingFormat, in.MovieNamingFormat)
|
s.db.SetSetting(db.SettingMovieNamingFormat, in.MovieNamingFormat)
|
||||||
} else {
|
} else {
|
||||||
s.db.SetSetting(db.SettingMovieNamingFormat, "")
|
s.db.SetSetting(db.SettingMovieNamingFormat, "")
|
||||||
@@ -105,7 +104,6 @@ func (s *Server) SetSetting(c *gin.Context) (interface{}, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (s *Server) GetSetting(c *gin.Context) (interface{}, error) {
|
func (s *Server) GetSetting(c *gin.Context) (interface{}, error) {
|
||||||
tmdb := s.db.GetSetting(db.SettingTmdbApiKey)
|
tmdb := s.db.GetSetting(db.SettingTmdbApiKey)
|
||||||
downloadDir := s.db.GetSetting(db.SettingDownloadDir)
|
downloadDir := s.db.GetSetting(db.SettingDownloadDir)
|
||||||
@@ -307,7 +305,7 @@ func (s *Server) TriggerCronJob(c *gin.Context) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) GetProwlarrSetting(c *gin.Context) (interface{}, error) {
|
func (s *Server) GetProwlarrSetting(c *gin.Context) (interface{}, error) {
|
||||||
se, err :=s.db.GetProwlarrSetting()
|
se, err := s.db.GetProwlarrSetting()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &db.ProwlarrSetting{}, nil
|
return &db.ProwlarrSetting{}, nil
|
||||||
}
|
}
|
||||||
@@ -322,7 +320,7 @@ func (s *Server) SaveProwlarrSetting(c *gin.Context) (interface{}, error) {
|
|||||||
client := prowlarr.New(in.ApiKey, in.URL)
|
client := prowlarr.New(in.ApiKey, in.URL)
|
||||||
if _, err := client.GetIndexers(prowlarr.TV); err != nil {
|
if _, err := client.GetIndexers(prowlarr.TV); err != nil {
|
||||||
return nil, errors.Wrap(err, "connect to prowlarr error")
|
return nil, errors.Wrap(err, "connect to prowlarr error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err := s.db.SaveProwlarrSetting(&in)
|
err := s.db.SaveProwlarrSetting(&in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -330,3 +328,57 @@ func (s *Server) SaveProwlarrSetting(c *gin.Context) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
return "success", nil
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"polaris/db"
|
"polaris/db"
|
||||||
|
|
||||||
"polaris/log"
|
"polaris/log"
|
||||||
|
"polaris/pkg/alist"
|
||||||
"polaris/pkg/storage"
|
"polaris/pkg/storage"
|
||||||
"polaris/pkg/utils"
|
"polaris/pkg/utils"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -28,7 +30,7 @@ func (s *Server) AddStorage(c *gin.Context) (interface{}, error) {
|
|||||||
if in.Implementation == "webdav" {
|
if in.Implementation == "webdav" {
|
||||||
//test webdav
|
//test webdav
|
||||||
wd := in.ToWebDavSetting()
|
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 {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "new webdav")
|
return nil, errors.Wrap(err, "new webdav")
|
||||||
}
|
}
|
||||||
@@ -39,6 +41,21 @@ func (s *Server) AddStorage(c *gin.Context) (interface{}, error) {
|
|||||||
for _, f := range fs {
|
for _, f := range fs {
|
||||||
log.Infof("file name: %v", f.Name())
|
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)
|
log.Infof("received add storage input: %v", in)
|
||||||
err := s.db.AddStorage(&in)
|
err := s.db.AddStorage(&in)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"polaris/log"
|
"polaris/log"
|
||||||
"polaris/server/core"
|
"polaris/server/core"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -102,17 +103,25 @@ func (s *Server) GetTvWatchlist(c *gin.Context) (interface{}, error) {
|
|||||||
MonitoredNum: 0,
|
MonitoredNum: 0,
|
||||||
DownloadedNum: 0,
|
DownloadedNum: 0,
|
||||||
}
|
}
|
||||||
|
mon, ok1 := s.monitorNumCache.Get(item.ID)
|
||||||
details := s.db.GetMediaDetails(item.ID)
|
dow, ok2 := s.downloadNumCache.Get(item.ID)
|
||||||
|
if ok1 && ok2 {
|
||||||
for _, ep := range details.Episodes {
|
ms.MonitoredNum = mon
|
||||||
if ep.Monitored {
|
ms.DownloadedNum = dow
|
||||||
ms.MonitoredNum++
|
} else {
|
||||||
if ep.Status == episode.StatusDownloaded {
|
details := s.db.GetMediaDetails(item.ID)
|
||||||
ms.DownloadedNum++
|
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
|
res[i] = ms
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
@@ -162,9 +171,32 @@ func (s *Server) DeleteFromWatchlist(c *gin.Context) (interface{}, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "convert")
|
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 {
|
if err := s.db.DeleteMedia(id); err != nil {
|
||||||
return nil, errors.Wrap(err, "delete db")
|
return nil, errors.Wrap(err, "delete db")
|
||||||
}
|
}
|
||||||
os.RemoveAll(filepath.Join(db.ImgPath, ids)) //delete image related
|
os.RemoveAll(filepath.Join(db.ImgPath, ids)) //delete image related
|
||||||
|
|
||||||
return "success", nil
|
return "success", nil
|
||||||
}
|
}
|
||||||
|
|||||||
12
ui/.metadata
@@ -4,7 +4,7 @@
|
|||||||
# This file should be version controlled and should not be manually edited.
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
version:
|
version:
|
||||||
revision: "80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819"
|
revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668"
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
|
|
||||||
project_type: app
|
project_type: app
|
||||||
@@ -13,11 +13,11 @@ project_type: app
|
|||||||
migration:
|
migration:
|
||||||
platforms:
|
platforms:
|
||||||
- platform: root
|
- platform: root
|
||||||
create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
|
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||||
base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
|
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||||
- platform: ios
|
- platform: macos
|
||||||
create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
|
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||||
base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
|
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||||
|
|
||||||
# User provided section
|
# User provided section
|
||||||
|
|
||||||
|
|||||||
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()],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
178
ui/lib/main.dart
@@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:intl/date_symbol_data_local.dart';
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
import 'package:ui/activity.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/login_page.dart';
|
||||||
import 'package:ui/movie_watchlist.dart';
|
import 'package:ui/movie_watchlist.dart';
|
||||||
import 'package:ui/providers/APIs.dart';
|
import 'package:ui/providers/APIs.dart';
|
||||||
@@ -124,9 +124,17 @@ class _MyAppState extends ConsumerState<MyApp> {
|
|||||||
initialLocation: WelcomePage.routeTv,
|
initialLocation: WelcomePage.routeTv,
|
||||||
routes: [
|
routes: [
|
||||||
shellRoute,
|
shellRoute,
|
||||||
|
GoRoute(
|
||||||
|
path: "/",
|
||||||
|
redirect: (context, state) => WelcomePage.routeTv,
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: LoginScreen.route,
|
path: LoginScreen.route,
|
||||||
builder: (context, state) => const LoginScreen(),
|
builder: (context, state) => const LoginScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: InitWizard.route,
|
||||||
|
builder: (context, state) => const InitWizard(),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -171,88 +179,7 @@ class _MainSkeletonState extends State<MainSkeleton> {
|
|||||||
var padding = isSmallScreen(context) ? 5.0 : 20.0;
|
var padding = isSmallScreen(context) ? 5.0 : 20.0;
|
||||||
return AdaptiveScaffold(
|
return AdaptiveScaffold(
|
||||||
appBarBreakpoint: Breakpoints.standard,
|
appBarBreakpoint: Breakpoints.standard,
|
||||||
appBar: AppBar(
|
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.
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
useDrawer: false,
|
useDrawer: false,
|
||||||
selectedIndex: widget.body.currentIndex,
|
selectedIndex: widget.body.currentIndex,
|
||||||
onSelectedIndexChange: (p0) => widget.body
|
onSelectedIndexChange: (p0) => widget.body
|
||||||
@@ -314,4 +241,89 @@ class _MainSkeletonState extends State<MainSkeleton> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class APIs {
|
|||||||
static final deleteImportlistUrl = "$_baseUrl/api/v1/importlist/delete";
|
static final deleteImportlistUrl = "$_baseUrl/api/v1/importlist/delete";
|
||||||
static final getAllImportlists = "$_baseUrl/api/v1/importlist/";
|
static final getAllImportlists = "$_baseUrl/api/v1/importlist/";
|
||||||
static final prowlarrUrl = "$_baseUrl/api/v1/setting/prowlarr";
|
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 notifierAllUrl = "$_baseUrl/api/v1/notifier/all";
|
||||||
static final notifierDeleteUrl = "$_baseUrl/api/v1/notifier/id/";
|
static final notifierDeleteUrl = "$_baseUrl/api/v1/notifier/id/";
|
||||||
@@ -58,6 +59,8 @@ class APIs {
|
|||||||
static final tvParseUrl = "$_baseUrl/api/v1/setting/parse/tv";
|
static final tvParseUrl = "$_baseUrl/api/v1/setting/parse/tv";
|
||||||
static final movieParseUrl = "$_baseUrl/api/v1/setting/parse/movie";
|
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 tmdbApiKey = "tmdb_api_key";
|
||||||
static const downloadDirKey = "download_dir";
|
static const downloadDirKey = "download_dir";
|
||||||
|
|
||||||
@@ -131,7 +134,7 @@ class APIs {
|
|||||||
if (sp.code != 0) {
|
if (sp.code != 0) {
|
||||||
throw sp.message;
|
throw sp.message;
|
||||||
}
|
}
|
||||||
return sp.data==null? []:sp.data as List<String>;
|
return sp.data == null ? [] : sp.data as List<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<String>> downloadAllMovies() async {
|
static Future<List<String>> downloadAllMovies() async {
|
||||||
@@ -142,7 +145,7 @@ class APIs {
|
|||||||
if (sp.code != 0) {
|
if (sp.code != 0) {
|
||||||
throw sp.message;
|
throw sp.message;
|
||||||
}
|
}
|
||||||
return sp.data==null? []:sp.data as List<String>;
|
return sp.data == null ? [] : sp.data as List<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<String> parseTvName(String s) async {
|
static Future<String> parseTvName(String s) async {
|
||||||
|
|||||||
@@ -25,15 +25,15 @@ var mediaHistoryDataProvider = FutureProvider.autoDispose.family(
|
|||||||
|
|
||||||
class ActivityData
|
class ActivityData
|
||||||
extends AutoDisposeFamilyAsyncNotifier<List<Activity>, String> {
|
extends AutoDisposeFamilyAsyncNotifier<List<Activity>, String> {
|
||||||
|
Timer? _timer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<List<Activity>> build(String arg) async {
|
FutureOr<List<Activity>> build(String arg) async {
|
||||||
if (arg == "active") {
|
if (_timer != null) {
|
||||||
//refresh active downloads
|
_timer!.cancel();
|
||||||
Timer(const Duration(seconds: 5),
|
|
||||||
ref.invalidateSelf); //Periodically Refresh
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final dio = await APIs.getDio();
|
final dio = APIs.getDio();
|
||||||
var resp =
|
var resp =
|
||||||
await dio.get(APIs.activityUrl, queryParameters: {"status": arg});
|
await dio.get(APIs.activityUrl, queryParameters: {"status": arg});
|
||||||
final sp = ServerResponse.fromJson(resp.data);
|
final sp = ServerResponse.fromJson(resp.data);
|
||||||
@@ -44,6 +44,12 @@ class ActivityData
|
|||||||
for (final a in sp.data as List) {
|
for (final a in sp.data as List) {
|
||||||
activities.add(Activity.fromJson(a));
|
activities.add(Activity.fromJson(a));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (arg == "active") {
|
||||||
|
//refresh active downloads
|
||||||
|
_timer = Timer(const Duration(seconds: 5),
|
||||||
|
() => ref.invalidateSelf()); //Periodically Refresh
|
||||||
|
}
|
||||||
return activities;
|
return activities;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +79,8 @@ class Activity {
|
|||||||
required this.saved,
|
required this.saved,
|
||||||
required this.progress,
|
required this.progress,
|
||||||
required this.size,
|
required this.size,
|
||||||
required this.seedRatio});
|
required this.seedRatio,
|
||||||
|
required this.uploadProgress});
|
||||||
|
|
||||||
final int? id;
|
final int? id;
|
||||||
final int? mediaId;
|
final int? mediaId;
|
||||||
@@ -86,6 +93,7 @@ class Activity {
|
|||||||
final int? progress;
|
final int? progress;
|
||||||
final int? size;
|
final int? size;
|
||||||
final double seedRatio;
|
final double seedRatio;
|
||||||
|
final double uploadProgress;
|
||||||
|
|
||||||
factory Activity.fromJson(Map<String, dynamic> json) {
|
factory Activity.fromJson(Map<String, dynamic> json) {
|
||||||
return Activity(
|
return Activity(
|
||||||
@@ -99,6 +107,7 @@ class Activity {
|
|||||||
saved: json["saved"],
|
saved: json["saved"],
|
||||||
progress: json["progress"],
|
progress: json["progress"],
|
||||||
seedRatio: json["seed_ratio"],
|
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);
|
return SeriesDetails.fromJson(rsp.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> delete() async {
|
Future<void> delete(bool removeFiles ) async {
|
||||||
final dio = await APIs.getDio();
|
final dio = APIs.getDio();
|
||||||
var resp = await dio.delete("${APIs.seriesDetailUrl}$id");
|
var resp = await dio.delete("${APIs.seriesDetailUrl}$id", queryParameters: {"delete_files": removeFiles});
|
||||||
var rsp = ServerResponse.fromJson(resp.data);
|
var rsp = ServerResponse.fromJson(resp.data);
|
||||||
if (rsp.code != 0) {
|
if (rsp.code != 0) {
|
||||||
throw rsp.message;
|
throw rsp.message;
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ var prowlarrSettingDataProvider =
|
|||||||
AsyncNotifierProvider.autoDispose<ProwlarrSettingData, ProwlarrSetting>(
|
AsyncNotifierProvider.autoDispose<ProwlarrSettingData, ProwlarrSetting>(
|
||||||
ProwlarrSettingData.new);
|
ProwlarrSettingData.new);
|
||||||
|
|
||||||
|
var aiConfigDataProvider =
|
||||||
|
AsyncNotifierProvider.autoDispose<AIConfigData, AIConfig>(AIConfigData.new);
|
||||||
|
|
||||||
class EditSettingData extends AutoDisposeAsyncNotifier<GeneralSetting> {
|
class EditSettingData extends AutoDisposeAsyncNotifier<GeneralSetting> {
|
||||||
@override
|
@override
|
||||||
FutureOr<GeneralSetting> build() async {
|
FutureOr<GeneralSetting> build() async {
|
||||||
@@ -548,3 +551,50 @@ class ProwlarrSettingData extends AutoDisposeAsyncNotifier<ProwlarrSetting> {
|
|||||||
ref.invalidateSelf();
|
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
@@ -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,
|
"resolution": resolution,
|
||||||
"folder": folder,
|
"folder": folder,
|
||||||
"download_history_episodes": downloadHistoryEpisodes,
|
"download_history_episodes": downloadHistoryEpisodes,
|
||||||
"size_min": (limiter.start * 1000*1000).toInt(),
|
"size_min": (limiter.start * 1000 * 1000).toInt(),
|
||||||
"size_max": (limiter.end * 1000*1000).toInt(),
|
"size_max": (limiter.end * 1000 * 1000).toInt(),
|
||||||
});
|
});
|
||||||
var sp = ServerResponse.fromJson(resp.data);
|
var sp = ServerResponse.fromJson(resp.data);
|
||||||
if (sp.code != 0) {
|
if (sp.code != 0) {
|
||||||
throw sp.message;
|
throw sp.message;
|
||||||
}
|
}
|
||||||
//ref.invalidate(tvWatchlistDataProvider);
|
ref.invalidate(tvWatchlistDataProvider);
|
||||||
} else {
|
} else {
|
||||||
var resp = await dio.post(APIs.watchlistMovieUrl, data: {
|
var resp = await dio.post(APIs.watchlistMovieUrl, data: {
|
||||||
"tmdb_id": tmdbId,
|
"tmdb_id": tmdbId,
|
||||||
@@ -129,6 +129,7 @@ class SearchPageData
|
|||||||
if (sp.code != 0) {
|
if (sp.code != 0) {
|
||||||
throw sp.message;
|
throw sp.message;
|
||||||
}
|
}
|
||||||
|
ref.invalidate(movieWatchlistDataProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class _SearchPageState extends ConsumerState<SearchPage> {
|
|||||||
}
|
}
|
||||||
return cards;
|
return cards;
|
||||||
},
|
},
|
||||||
error: (err, trace) => [PoError(msg: "网络错误,请确认TMDB Key正确配置,并且服务端能够正常连接TMDB网站", err: err)],
|
error: (err, trace) => [PoError(msg: "网络错误,请确认TMDB Key正确配置,并且能够正常连接到TMDB网站", err: err)],
|
||||||
loading: () => [const MyProgressIndicator()]);
|
loading: () => [const MyProgressIndicator()]);
|
||||||
|
|
||||||
var f = NotificationListener(
|
var f = NotificationListener(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||||
import 'package:quiver/strings.dart';
|
import 'package:quiver/strings.dart';
|
||||||
import 'package:ui/providers/settings.dart';
|
import 'package:ui/providers/settings.dart';
|
||||||
|
import 'package:ui/providers/size_limiter.dart';
|
||||||
import 'package:ui/settings/dialog.dart';
|
import 'package:ui/settings/dialog.dart';
|
||||||
import 'package:ui/widgets/progress_indicator.dart';
|
import 'package:ui/widgets/progress_indicator.dart';
|
||||||
import 'package:ui/widgets/widgets.dart';
|
import 'package:ui/widgets/widgets.dart';
|
||||||
@@ -22,27 +23,33 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var downloadClients = ref.watch(dwonloadClientsProvider);
|
var downloadClients = ref.watch(dwonloadClientsProvider);
|
||||||
return downloadClients.when(
|
return Column(
|
||||||
data: (value) => Wrap(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: List.generate(value.length + 1, (i) {
|
children: [
|
||||||
if (i < value.length) {
|
downloadClients.when(
|
||||||
var client = value[i];
|
data: (value) => Wrap(
|
||||||
return SettingsCard(
|
children: List.generate(value.length + 1, (i) {
|
||||||
onTap: () => showDownloadClientDetails(client),
|
if (i < value.length) {
|
||||||
child: Text(client.name ?? ""));
|
var client = value[i];
|
||||||
}
|
return SettingsCard(
|
||||||
return SettingsCard(
|
onTap: () => showDownloadClientDetails(client),
|
||||||
onTap: () => showDownloadClientDetails(DownloadClient()),
|
child: Text(client.name ?? ""));
|
||||||
child: const Icon(Icons.add));
|
}
|
||||||
})),
|
return SettingsCard(
|
||||||
error: (err, trace) => PoNetworkError(err: err),
|
onTap: () => showSelections(),
|
||||||
loading: () => const MyProgressIndicator());
|
child: const Icon(Icons.add));
|
||||||
|
})),
|
||||||
|
error: (err, trace) => PoNetworkError(err: err),
|
||||||
|
loading: () => const MyProgressIndicator()),
|
||||||
|
Divider(),
|
||||||
|
getSizeLimiterWidget()
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showDownloadClientDetails(DownloadClient client) {
|
Future<void> showDownloadClientDetails(DownloadClient client) {
|
||||||
final _formKey = GlobalKey<FormBuilderState>();
|
final _formKey = GlobalKey<FormBuilderState>();
|
||||||
var _enableAuth = isNotBlank(client.user);
|
var _enableAuth = isNotBlank(client.user);
|
||||||
String selectImpl = "transmission";
|
|
||||||
|
|
||||||
final body =
|
final body =
|
||||||
StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||||
@@ -53,29 +60,12 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
|
|||||||
"url": client.url,
|
"url": client.url,
|
||||||
"user": client.user,
|
"user": client.user,
|
||||||
"password": client.password,
|
"password": client.password,
|
||||||
"impl": client.implementation,
|
|
||||||
"remove_completed_downloads": client.removeCompletedDownloads,
|
"remove_completed_downloads": client.removeCompletedDownloads,
|
||||||
"remove_failed_downloads": client.removeFailedDownloads,
|
"remove_failed_downloads": client.removeFailedDownloads,
|
||||||
"priority": client.priority.toString(),
|
"priority": client.priority.toString(),
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
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(
|
FormBuilderTextField(
|
||||||
name: "name",
|
name: "name",
|
||||||
decoration: const InputDecoration(labelText: "名称"),
|
decoration: const InputDecoration(labelText: "名称"),
|
||||||
@@ -90,7 +80,8 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
|
|||||||
),
|
),
|
||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
name: "priority",
|
name: "priority",
|
||||||
decoration: const InputDecoration(labelText: "优先级", helperText: "1-50, 1最高优先级,50最低优先级"),
|
decoration: const InputDecoration(
|
||||||
|
labelText: "优先级", helperText: "1-50, 1最高优先级,50最低优先级"),
|
||||||
validator: FormBuilderValidators.integer(),
|
validator: FormBuilderValidators.integer(),
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction),
|
autovalidateMode: AutovalidateMode.onUserInteraction),
|
||||||
FormBuilderSwitch(
|
FormBuilderSwitch(
|
||||||
@@ -151,7 +142,7 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
|
|||||||
return ref.read(dwonloadClientsProvider.notifier).addDownloadClients(
|
return ref.read(dwonloadClientsProvider.notifier).addDownloadClients(
|
||||||
DownloadClient(
|
DownloadClient(
|
||||||
name: values["name"],
|
name: values["name"],
|
||||||
implementation: values["impl"],
|
implementation: client.implementation,
|
||||||
url: values["url"],
|
url: values["url"],
|
||||||
user: _enableAuth ? values["user"] : null,
|
user: _enableAuth ? values["user"] : null,
|
||||||
password: _enableAuth ? values["password"] : 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(
|
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,152 +23,183 @@ class _GeneralState extends ConsumerState<GeneralSettings> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var settings = ref.watch(settingProvider);
|
var settings = ref.watch(settingProvider);
|
||||||
|
var aiConfig = ref.watch(aiConfigDataProvider);
|
||||||
return settings.when(
|
return aiConfig.when(
|
||||||
data: (v) {
|
data: (ai) {
|
||||||
return FormBuilder(
|
return settings.when(
|
||||||
key: _formKey, //设置globalKey,用于后面获取FormState
|
data: (v) {
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
return FormBuilder(
|
||||||
initialValue: {
|
key: _formKey, //设置globalKey,用于后面获取FormState
|
||||||
"tmdb_api": v.tmdbApiKey,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
"download_dir": v.downloadDIr,
|
initialValue: {
|
||||||
"log_level": v.logLevel,
|
"tmdb_api": v.tmdbApiKey,
|
||||||
"proxy": v.proxy,
|
"download_dir": v.downloadDIr,
|
||||||
"enable_plexmatch": v.enablePlexmatch,
|
"log_level": v.logLevel,
|
||||||
"allow_qiangban": v.allowQiangban,
|
"proxy": v.proxy,
|
||||||
"enable_nfo": v.enableNfo,
|
"enable_plexmatch": v.enablePlexmatch,
|
||||||
"enable_adult": v.enableAdult,
|
"allow_qiangban": v.allowQiangban,
|
||||||
"tv_naming_format": v.tvNamingFormat,
|
"enable_nfo": v.enableNfo,
|
||||||
"movie_naming_format": v.movieNamingFormat,
|
"enable_adult": v.enableAdult,
|
||||||
},
|
"tv_naming_format": v.tvNamingFormat,
|
||||||
child: Column(
|
"movie_naming_format": v.movieNamingFormat,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
"ai_enabled": ai.enabled,
|
||||||
children: [
|
"ai_api_key": ai.geminiApiKey,
|
||||||
FormBuilderTextField(
|
"ai_model_name": ai.geminiModelName
|
||||||
name: "tmdb_api",
|
},
|
||||||
decoration: Commons.requiredTextFieldStyle(
|
child: Column(
|
||||||
text: "TMDB Api Key", icon: const Icon(Icons.key)),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
//
|
children: [
|
||||||
validator: FormBuilderValidators.required(),
|
FormBuilderTextField(
|
||||||
),
|
name: "tmdb_api",
|
||||||
FormBuilderTextField(
|
decoration: Commons.requiredTextFieldStyle(
|
||||||
name: "download_dir",
|
text: "TMDB Api Key", icon: const Icon(Icons.key)),
|
||||||
decoration: Commons.requiredTextFieldStyle(
|
//
|
||||||
text: "下载路径",
|
validator: FormBuilderValidators.required(),
|
||||||
icon: const Icon(Icons.folder),
|
),
|
||||||
helperText: "媒体文件临时下载路径,非最终存储路径"),
|
FormBuilderTextField(
|
||||||
//
|
name: "download_dir",
|
||||||
validator: FormBuilderValidators.required(),
|
decoration: Commons.requiredTextFieldStyle(
|
||||||
),
|
text: "下载路径",
|
||||||
FormBuilderTextField(
|
icon: const Icon(Icons.folder),
|
||||||
name: "proxy",
|
helperText: "媒体文件临时下载路径,非最终存储路径"),
|
||||||
decoration: const InputDecoration(
|
//
|
||||||
labelText: "代理地址",
|
validator: FormBuilderValidators.required(),
|
||||||
icon: Icon(Icons.web),
|
),
|
||||||
hintText: "http://10.0.0.1:1080",
|
FormBuilderTextField(
|
||||||
helperText: "后台联网代理地址,留空表示不启用代理"),
|
name: "proxy",
|
||||||
),
|
decoration: const InputDecoration(
|
||||||
FormBuilderTextField(
|
labelText: "代理地址",
|
||||||
decoration: const InputDecoration(
|
icon: Icon(Icons.web),
|
||||||
icon: Icon(Icons.folder),
|
hintText: "http://10.0.0.1:1080",
|
||||||
labelText: "电视剧路径命名规则",
|
helperText: "后台联网代理地址,留空表示不启用代理"),
|
||||||
helperText:
|
),
|
||||||
"go template语法,可用的变量为:.NameCN, .NameEN, .Year, .TmdbID"),
|
FormBuilderTextField(
|
||||||
name: "tv_naming_format",
|
decoration: const InputDecoration(
|
||||||
),
|
icon: Icon(Icons.folder),
|
||||||
FormBuilderTextField(
|
labelText: "电视剧路径命名规则",
|
||||||
decoration: const InputDecoration(
|
helperText:
|
||||||
icon: Icon(Icons.folder),
|
"go template语法,可用的变量为:.NameCN, .NameEN, .Year, .TmdbID"),
|
||||||
labelText: "电影路径命名规则",
|
name: "tv_naming_format",
|
||||||
helperText:
|
),
|
||||||
"go template语法,可用的变量为:.NameCN, .NameEN, .Year, .TmdbID"),
|
FormBuilderTextField(
|
||||||
name: "movie_naming_format",
|
decoration: const InputDecoration(
|
||||||
),
|
icon: Icon(Icons.folder),
|
||||||
SizedBox(
|
labelText: "电影路径命名规则",
|
||||||
width: 300,
|
helperText:
|
||||||
child: FormBuilderDropdown(
|
"go template语法,可用的变量为:.NameCN, .NameEN, .Year, .TmdbID"),
|
||||||
name: "log_level",
|
name: "movie_naming_format",
|
||||||
decoration: const InputDecoration(
|
),
|
||||||
labelText: "日志级别",
|
SizedBox(
|
||||||
icon: Icon(Icons.file_present_rounded),
|
width: 300,
|
||||||
),
|
child: FormBuilderDropdown(
|
||||||
items: const [
|
name: "log_level",
|
||||||
DropdownMenuItem(value: "debug", child: Text("DEBUG")),
|
decoration: const InputDecoration(
|
||||||
DropdownMenuItem(value: "info", child: Text("INFO")),
|
labelText: "日志级别",
|
||||||
DropdownMenuItem(value: "warn", child: Text("WARN")),
|
icon: Icon(Icons.file_present_rounded),
|
||||||
DropdownMenuItem(value: "error", child: Text("ERROR")),
|
),
|
||||||
],
|
items: const [
|
||||||
validator: FormBuilderValidators.required(),
|
DropdownMenuItem(
|
||||||
),
|
value: "debug", child: Text("DEBUG")),
|
||||||
),
|
DropdownMenuItem(
|
||||||
SizedBox(
|
value: "info", child: Text("INFO")),
|
||||||
width: 300,
|
DropdownMenuItem(
|
||||||
child: FormBuilderSwitch(
|
value: "warn", child: Text("WARN")),
|
||||||
decoration:
|
DropdownMenuItem(
|
||||||
const InputDecoration(icon: Icon(Icons.back_hand)),
|
value: "error", child: Text("ERROR")),
|
||||||
name: "enable_adult",
|
],
|
||||||
title: const Text("是否显示成人内容")),
|
validator: FormBuilderValidators.required(),
|
||||||
),
|
|
||||||
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("保存"),
|
|
||||||
),
|
),
|
||||||
onPressed: () {
|
),
|
||||||
if (_formKey.currentState!.saveAndValidate()) {
|
SizedBox(
|
||||||
var values = _formKey.currentState!.value;
|
width: 300,
|
||||||
var f = ref
|
child: FormBuilderSwitch(
|
||||||
.read(settingProvider.notifier)
|
decoration: const InputDecoration(
|
||||||
.updateSettings(GeneralSetting(
|
icon: Icon(Icons.back_hand)),
|
||||||
tmdbApiKey: values["tmdb_api"],
|
name: "enable_adult",
|
||||||
downloadDIr: values["download_dir"],
|
title: const Text("是否显示成人内容")),
|
||||||
logLevel: values["log_level"],
|
),
|
||||||
proxy: values["proxy"],
|
SizedBox(
|
||||||
allowQiangban: values["allow_qiangban"],
|
width: 300,
|
||||||
enableAdult: values["enable_adult"],
|
child: FormBuilderSwitch(
|
||||||
enableNfo: values["enable_nfo"],
|
decoration:
|
||||||
tvNamingFormat: values["tv_naming_format"],
|
const InputDecoration(icon: Icon(Icons.token)),
|
||||||
movieNamingFormat:
|
name: "enable_plexmatch",
|
||||||
values["movie_naming_format"],
|
title: const Text("Plex 刮削支持")),
|
||||||
enablePlexmatch:
|
),
|
||||||
values["enable_plexmatch"]))
|
SizedBox(
|
||||||
.then((v) => showSnakeBar("更新成功"));
|
width: 300,
|
||||||
showLoadingWithFuture(f);
|
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) => PoNetworkError(err: err),
|
error: (err, trace) => PoNetworkError(err: err),
|
||||||
loading: () => const MyProgressIndicator());
|
loading: () => const MyProgressIndicator());
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:ui/providers/settings.dart';
|
|||||||
import 'package:ui/settings/dialog.dart';
|
import 'package:ui/settings/dialog.dart';
|
||||||
import 'package:ui/widgets/progress_indicator.dart';
|
import 'package:ui/widgets/progress_indicator.dart';
|
||||||
import 'package:ui/widgets/widgets.dart';
|
import 'package:ui/widgets/widgets.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class Importlist extends ConsumerStatefulWidget {
|
class Importlist extends ConsumerStatefulWidget {
|
||||||
const Importlist({super.key});
|
const Importlist({super.key});
|
||||||
@@ -31,7 +32,7 @@ class _ImportlistState extends ConsumerState<Importlist> {
|
|||||||
child: Text(indexer.name ?? ""));
|
child: Text(indexer.name ?? ""));
|
||||||
}
|
}
|
||||||
return SettingsCard(
|
return SettingsCard(
|
||||||
onTap: () => showImportlistDetails(ImportList()),
|
onTap: () => showSelections(),
|
||||||
child: const Icon(Icons.add));
|
child: const Icon(Icons.add));
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -55,24 +56,16 @@ class _ImportlistState extends ConsumerState<Importlist> {
|
|||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
FormBuilderDropdown(
|
list.type == "plex"
|
||||||
name: "type",
|
? Container(
|
||||||
decoration: const InputDecoration(
|
alignment: Alignment.centerLeft,
|
||||||
labelText: "类型",
|
child: InkWell(
|
||||||
hintText:
|
onTap: () => launchUrl(Uri.parse(
|
||||||
"Plex Watchlist: https://support.plex.tv/articles/universal-watchlist/"),
|
"https://support.plex.tv/articles/universal-watchlist/")),
|
||||||
items: const [
|
child: const Text(
|
||||||
DropdownMenuItem(value: "plex", child: Text("Plex Watchlist")),
|
"https://support.plex.tv/articles/universal-watchlist/"),
|
||||||
],
|
),
|
||||||
onChanged: (value) {
|
)
|
||||||
setState(() {
|
|
||||||
_selectedType = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_selectedType == "plex"
|
|
||||||
? const Text(
|
|
||||||
"Plex Watchlist: https://support.plex.tv/articles/universal-watchlist/")
|
|
||||||
: const Text(""),
|
: const Text(""),
|
||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
name: "name",
|
name: "name",
|
||||||
@@ -141,7 +134,42 @@ class _ImportlistState extends ConsumerState<Importlist> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var title = "监控列表";
|
||||||
|
if (list.type == "plex") {
|
||||||
|
title = "Plex Watchlist";
|
||||||
|
}
|
||||||
|
|
||||||
return showSettingDialog(
|
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"));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ class ProwlarrSettingState extends ConsumerState<ProwlarrSettingPage> {
|
|||||||
FormBuilderSwitch(
|
FormBuilderSwitch(
|
||||||
name: "disabled",
|
name: "disabled",
|
||||||
title: const Text("禁用 Prowlarr"),
|
title: const Text("禁用 Prowlarr"),
|
||||||
decoration: InputDecoration(icon: Icon(Icons.do_not_disturb)),
|
decoration:
|
||||||
|
InputDecoration(icon: Icon(Icons.do_not_disturb)),
|
||||||
),
|
),
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class _StorageState extends ConsumerState<StorageSettings> {
|
|||||||
child: Text(storage.name ?? ""));
|
child: Text(storage.name ?? ""));
|
||||||
}
|
}
|
||||||
return SettingsCard(
|
return SettingsCard(
|
||||||
onTap: () => showStorageDetails(Storage()),
|
onTap: () => showSelections(),
|
||||||
child: const Icon(Icons.add));
|
child: const Icon(Icons.add));
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -42,7 +42,6 @@ class _StorageState extends ConsumerState<StorageSettings> {
|
|||||||
Future<void> showStorageDetails(Storage s) {
|
Future<void> showStorageDetails(Storage s) {
|
||||||
final _formKey = GlobalKey<FormBuilderState>();
|
final _formKey = GlobalKey<FormBuilderState>();
|
||||||
|
|
||||||
String selectImpl = s.implementation == null ? "local" : s.implementation!;
|
|
||||||
final widgets =
|
final widgets =
|
||||||
StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||||
return FormBuilder(
|
return FormBuilder(
|
||||||
@@ -50,7 +49,6 @@ class _StorageState extends ConsumerState<StorageSettings> {
|
|||||||
autovalidateMode: AutovalidateMode.disabled,
|
autovalidateMode: AutovalidateMode.disabled,
|
||||||
initialValue: {
|
initialValue: {
|
||||||
"name": s.name,
|
"name": s.name,
|
||||||
"impl": s.implementation == null ? "local" : s.implementation!,
|
|
||||||
"user": s.settings != null ? s.settings!["user"] ?? "" : "",
|
"user": s.settings != null ? s.settings!["user"] ?? "" : "",
|
||||||
"password": s.settings != null ? s.settings!["password"] ?? "" : "",
|
"password": s.settings != null ? s.settings!["password"] ?? "" : "",
|
||||||
"tv_path": s.tvPath,
|
"tv_path": s.tvPath,
|
||||||
@@ -65,27 +63,6 @@ class _StorageState extends ConsumerState<StorageSettings> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
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(
|
FormBuilderTextField(
|
||||||
name: "name",
|
name: "name",
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
@@ -93,15 +70,16 @@ class _StorageState extends ConsumerState<StorageSettings> {
|
|||||||
decoration: const InputDecoration(labelText: "名称"),
|
decoration: const InputDecoration(labelText: "名称"),
|
||||||
validator: FormBuilderValidators.required(),
|
validator: FormBuilderValidators.required(),
|
||||||
),
|
),
|
||||||
selectImpl != "local"
|
s.implementation == "webdav" || s.implementation == "alist"
|
||||||
? Column(
|
? Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
name: "url",
|
name: "url",
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
decoration:
|
decoration: const InputDecoration(
|
||||||
const InputDecoration(labelText: "Webdav地址"),
|
labelText: "网址",
|
||||||
|
hintText: "https://abc.somewebsite.com/"),
|
||||||
validator: FormBuilderValidators.required(),
|
validator: FormBuilderValidators.required(),
|
||||||
),
|
),
|
||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
@@ -115,13 +93,15 @@ class _StorageState extends ConsumerState<StorageSettings> {
|
|||||||
decoration: const InputDecoration(labelText: "密码"),
|
decoration: const InputDecoration(labelText: "密码"),
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
),
|
),
|
||||||
FormBuilderCheckbox(
|
s.implementation == "webdav"
|
||||||
name: "change_file_hash",
|
? FormBuilderCheckbox(
|
||||||
title: const Text(
|
name: "change_file_hash",
|
||||||
"上传时更改文件哈希",
|
title: const Text(
|
||||||
style: TextStyle(fontSize: 14),
|
"上传时更改文件哈希",
|
||||||
),
|
style: TextStyle(fontSize: 14),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: Container(),
|
: Container(),
|
||||||
@@ -145,7 +125,7 @@ class _StorageState extends ConsumerState<StorageSettings> {
|
|||||||
final values = _formKey.currentState!.value;
|
final values = _formKey.currentState!.value;
|
||||||
return ref.read(storageSettingProvider.notifier).addStorage(Storage(
|
return ref.read(storageSettingProvider.notifier).addStorage(Storage(
|
||||||
name: values["name"],
|
name: values["name"],
|
||||||
implementation: selectImpl,
|
implementation: s.implementation,
|
||||||
tvPath: values["tv_path"],
|
tvPath: values["tv_path"],
|
||||||
moviePath: values["movie_path"],
|
moviePath: values["movie_path"],
|
||||||
settings: {
|
settings: {
|
||||||
@@ -167,7 +147,70 @@ class _StorageState extends ConsumerState<StorageSettings> {
|
|||||||
return ref.read(storageSettingProvider.notifier).deleteStorage(s.id!);
|
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(
|
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"));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,14 +44,34 @@ class WelcomePageState extends ConsumerState<WelcomePage> {
|
|||||||
children: [
|
children: [
|
||||||
() {
|
() {
|
||||||
return data.when(
|
return data.when(
|
||||||
data: (value) => SingleChildScrollView(
|
data: (value) {
|
||||||
child: Wrap(
|
if (value.isEmpty) {
|
||||||
alignment: WrapAlignment.start,
|
return Container(
|
||||||
spacing: isSmallScreen(context) ? 0 : 10,
|
height: MediaQuery.of(context).size.height * 0.7,
|
||||||
runSpacing: isSmallScreen(context) ? 10 : 20,
|
alignment: Alignment.center,
|
||||||
children: getMediaAll(value),
|
child: const Text(
|
||||||
),
|
"啥都没有...",
|
||||||
),
|
style: TextStyle(fontSize: 16),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
error: (err, trace) => PoNetworkError(err: err),
|
||||||
loading: () => const MyProgressIndicator());
|
loading: () => const MyProgressIndicator());
|
||||||
}(),
|
}(),
|
||||||
@@ -152,28 +172,6 @@ class WelcomePageState extends ConsumerState<WelcomePage> {
|
|||||||
final screenWidth = MediaQuery.of(context).size.width;
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
return screenWidth < 600;
|
return screenWidth < 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> getMediaAll(List<MediaDetail> list) {
|
|
||||||
if (list.isEmpty) {
|
|
||||||
return [
|
|
||||||
Container(
|
|
||||||
height: MediaQuery.of(context).size.height * 0.6,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: const Text(
|
|
||||||
"啥都没有...",
|
|
||||||
style: TextStyle(fontSize: 16),
|
|
||||||
))
|
|
||||||
];
|
|
||||||
}
|
|
||||||
if (onlyShowUnfinished) {
|
|
||||||
list = list.where((v) => v.downloadedNum != v.monitoredNum).toList();
|
|
||||||
}
|
|
||||||
return List.generate(list.length, (i) {
|
|
||||||
final item = list[i];
|
|
||||||
return MediaCard(item: item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showNameParsingDialog() async {
|
Future<void> _showNameParsingDialog() async {
|
||||||
final resultController = TextEditingController();
|
final resultController = TextEditingController();
|
||||||
return showDialog<void>(
|
return showDialog<void>(
|
||||||
@@ -243,8 +241,6 @@ class WelcomePageState extends ConsumerState<WelcomePage> {
|
|||||||
|
|
||||||
class MediaCard extends StatelessWidget {
|
class MediaCard extends StatelessWidget {
|
||||||
final MediaDetail item;
|
final MediaDetail item;
|
||||||
static const double smallWidth = 110;
|
|
||||||
static const double largeWidth = 140;
|
|
||||||
|
|
||||||
const MediaCard({super.key, required this.item});
|
const MediaCard({super.key, required this.item});
|
||||||
@override
|
@override
|
||||||
@@ -265,19 +261,18 @@ class MediaCard extends StatelessWidget {
|
|||||||
context.go(TvDetailsPage.toRoute(item.id!));
|
context.go(TvDetailsPage.toRoute(item.id!));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Column(
|
child: LayoutBuilder(builder: (context, constraints) => Wrap(
|
||||||
|
direction: Axis.horizontal,
|
||||||
children: <Widget>[
|
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(
|
SizedBox(
|
||||||
width: cardWidth(context),
|
width: constraints.maxWidth,
|
||||||
height: cardWidth(context) / 2 * 3,
|
|
||||||
child: Ink.image(
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
image: NetworkImage(
|
|
||||||
"${APIs.imagesUrl}/${item.id}/poster.jpg",
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: cardWidth(context),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
@@ -297,15 +292,18 @@ class MediaCard extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
),
|
)),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
double cardWidth(BuildContext context) {
|
|
||||||
final screenWidth = MediaQuery.of(context).size.width;
|
double cardWidth(BuildContext context) {
|
||||||
if (screenWidth < 600) {
|
const double smallWidth = 110;
|
||||||
return smallWidth;
|
const double largeWidth = 140;
|
||||||
}
|
|
||||||
return largeWidth;
|
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) {
|
Future<void> showConfirmDialog(BuildContext oriContext) {
|
||||||
|
var deleteFiles = false;
|
||||||
return showDialog<void>(
|
return showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: true,
|
barrierDismissible: true,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text("确认删除:"),
|
title: const Text("确认删除"),
|
||||||
content: Text("${widget.details.name}"),
|
content: StatefulBuilder(builder: (context, setState) {
|
||||||
|
return CheckboxListTile(
|
||||||
|
value: deleteFiles,
|
||||||
|
title: Text("删除媒体文件"),
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(
|
||||||
|
() {
|
||||||
|
deleteFiles = v!;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
@@ -209,7 +221,7 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
|||||||
ref
|
ref
|
||||||
.read(mediaDetailsProvider(widget.details.id.toString())
|
.read(mediaDetailsProvider(widget.details.id.toString())
|
||||||
.notifier)
|
.notifier)
|
||||||
.delete()
|
.delete(deleteFiles)
|
||||||
.then((v) {
|
.then((v) {
|
||||||
if (oriContext.mounted) {
|
if (oriContext.mounted) {
|
||||||
oriContext.go(widget.details.mediaType == "tv"
|
oriContext.go(widget.details.mediaType == "tv"
|
||||||
|
|||||||
@@ -282,6 +282,52 @@ 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 {
|
class PoError extends StatelessWidget {
|
||||||
const PoError({super.key, required this.msg, required this.err});
|
const PoError({super.key, required this.msg, required this.err});
|
||||||
final String msg;
|
final String msg;
|
||||||
@@ -292,10 +338,16 @@ class PoError extends StatelessWidget {
|
|||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text("$msg ", style: TextStyle(color:Theme.of(context).colorScheme.error),),
|
Text(
|
||||||
|
"$msg ",
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||||
|
),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: "$err",
|
message: "$err",
|
||||||
child: Icon(Icons.info,color: Theme.of(context).colorScheme.error,),
|
child: Icon(
|
||||||
|
Icons.info,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -304,9 +356,30 @@ class PoError extends StatelessWidget {
|
|||||||
|
|
||||||
class PoNetworkError extends StatelessWidget {
|
class PoNetworkError extends StatelessWidget {
|
||||||
const PoNetworkError({super.key, required this.err});
|
const PoNetworkError({super.key, required this.err});
|
||||||
final dynamic err;
|
final dynamic err;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PoError(msg: "网络错误,请检查网络链接", err: err);
|
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
@@ -0,0 +1,7 @@
|
|||||||
|
# Flutter-related
|
||||||
|
**/Flutter/ephemeral/
|
||||||
|
**/Pods/
|
||||||
|
|
||||||
|
# Xcode-related
|
||||||
|
**/dgph
|
||||||
|
**/xcuserdata/
|
||||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||||
@@ -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>
|
||||||
9
ui/macos/Runner/AppDelegate.swift
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import Cocoa
|
||||||
|
import FlutterMacOS
|
||||||
|
|
||||||
|
@main
|
||||||
|
class AppDelegate: FlutterAppDelegate {
|
||||||
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"size" : "16x16",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_16.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "16x16",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_32.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "32x32",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_32.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "32x32",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_64.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "128x128",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_128.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "128x128",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_256.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "256x256",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_256.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "256x256",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_512.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "512x512",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_512.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "512x512",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_1024.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 520 B |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
343
ui/macos/Runner/Base.lproj/MainMenu.xib
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="macosx"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
|
||||||
|
</connections>
|
||||||
|
</customObject>
|
||||||
|
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||||
|
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||||
|
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
|
||||||
|
<connections>
|
||||||
|
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
|
||||||
|
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
|
||||||
|
</connections>
|
||||||
|
</customObject>
|
||||||
|
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||||
|
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||||
|
<items>
|
||||||
|
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
|
||||||
|
<items>
|
||||||
|
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||||
|
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
|
||||||
|
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||||
|
<menuItem title="Services" id="NMo-om-nkz">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||||
|
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
|
||||||
|
<connections>
|
||||||
|
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||||
|
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
|
||||||
|
<connections>
|
||||||
|
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Edit" id="5QF-Oa-p0T">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
|
||||||
|
<connections>
|
||||||
|
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
|
||||||
|
<connections>
|
||||||
|
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
|
||||||
|
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
|
||||||
|
<connections>
|
||||||
|
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
|
||||||
|
<connections>
|
||||||
|
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
|
||||||
|
<connections>
|
||||||
|
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Delete" id="pa3-QI-u2k">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
|
||||||
|
<connections>
|
||||||
|
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
|
||||||
|
<menuItem title="Find" id="4EN-yA-p0u">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Find" id="1b7-l0-nxx">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
|
||||||
|
<connections>
|
||||||
|
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
|
||||||
|
<connections>
|
||||||
|
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
|
||||||
|
<connections>
|
||||||
|
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
|
||||||
|
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Substitutions" id="9ic-FL-obx">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
|
||||||
|
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Smart Links" id="cwL-P1-jid">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Data Detectors" id="tRr-pd-1PS">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Transformations" id="2oI-Rn-ZJC">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Speech" id="xrE-MZ-jX0">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="View" id="H8h-7b-M4v">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="View" id="HyV-fh-RgO">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Window" id="aUF-d1-5bR">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
|
||||||
|
<connections>
|
||||||
|
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Zoom" id="R4o-n2-Eq4">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
|
||||||
|
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Help" id="EPT-qC-fAb">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
<point key="canvasLocation" x="142" y="-258"/>
|
||||||
|
</menu>
|
||||||
|
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
|
||||||
|
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||||
|
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
|
||||||
|
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
|
||||||
|
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
</view>
|
||||||
|
</window>
|
||||||
|
</objects>
|
||||||
|
</document>
|
||||||
14
ui/macos/Runner/Configs/AppInfo.xcconfig
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// Application-level settings for the Runner target.
|
||||||
|
//
|
||||||
|
// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
|
||||||
|
// future. If not, the values below would default to using the project name when this becomes a
|
||||||
|
// 'flutter create' template.
|
||||||
|
|
||||||
|
// The application's name. By default this is also the title of the Flutter window.
|
||||||
|
PRODUCT_NAME = ui
|
||||||
|
|
||||||
|
// The application's bundle identifier
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.ui
|
||||||
|
|
||||||
|
// The copyright displayed in application information
|
||||||
|
PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved.
|
||||||
2
ui/macos/Runner/Configs/Debug.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include "../../Flutter/Flutter-Debug.xcconfig"
|
||||||
|
#include "Warnings.xcconfig"
|
||||||
2
ui/macos/Runner/Configs/Release.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include "../../Flutter/Flutter-Release.xcconfig"
|
||||||
|
#include "Warnings.xcconfig"
|
||||||
13
ui/macos/Runner/Configs/Warnings.xcconfig
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES
|
||||||
|
CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
|
||||||
|
CLANG_WARN_PRAGMA_PACK = YES
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES
|
||||||
|
CLANG_WARN_COMMA = YES
|
||||||
|
GCC_WARN_STRICT_SELECTOR_MATCH = YES
|
||||||
|
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
|
||||||
|
GCC_WARN_SHADOW = YES
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES
|
||||||
14
ui/macos/Runner/DebugProfile.entitlements
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?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>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.server</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
32
ui/macos/Runner/Info.plist
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?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>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string></string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>$(PRODUCT_COPYRIGHT)</string>
|
||||||
|
<key>NSMainNibFile</key>
|
||||||
|
<string>MainMenu</string>
|
||||||
|
<key>NSPrincipalClass</key>
|
||||||
|
<string>NSApplication</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||