Compare commits

...

57 Commits

Author SHA1 Message Date
Simon Ding
cff093ca98 fix: use root user, for now 2024-07-29 22:12:05 +08:00
Simon Ding
8f677b29a7 chore: change cron 2024-07-29 19:48:05 +08:00
Simon Ding
e08c126af2 feat: add bark push notification 2024-07-29 18:06:32 +08:00
Simon Ding
eed72c5eb9 refactor: settings page 2024-07-29 16:34:32 +08:00
Simon Ding
fcff47041a feat: support push notification clients 2024-07-29 15:56:29 +08:00
Simon Ding
f9d4f851eb feat: no animation 2024-07-29 10:45:31 +08:00
Simon Ding
262baf769f feat: fade route transition and fixes 2024-07-29 09:21:11 +08:00
Simon Ding
6ef2e5b347 feat: offline support 2024-07-28 21:28:29 +08:00
Simon Ding
e0e11b70bb fix: add movie download 2024-07-28 19:30:06 +08:00
Simon Ding
3de2f89107 feat: use cookie to store jwt token, better performance 2024-07-28 18:07:24 +08:00
Simon Ding
b024b5f6dc feat: add default download client 2024-07-28 17:19:05 +08:00
Simon Ding
961d762f35 feat: add text to empty screen 2024-07-28 16:43:59 +08:00
Simon Ding
7f025a6246 fix: bugs 2024-07-28 15:54:51 +08:00
Simon Ding
fc86a441f4 feat: option to download all episodes 2024-07-28 13:22:12 +08:00
Simon Ding
34fa05e7dd fix: var 2024-07-28 12:31:22 +08:00
Simon Ding
9c3757a1bf feat: season package list 2024-07-28 11:50:58 +08:00
Simon Ding
e63a899df5 chore: remove unused code 2024-07-28 11:13:26 +08:00
Simon Ding
3a4e303d9d feat: log to console in dev 2024-07-28 11:08:17 +08:00
Simon Ding
ef9e4487c6 fix: class to dart3 record 2024-07-28 11:04:51 +08:00
Simon Ding
02f6cfb5b7 fix 2024-07-27 23:42:28 +08:00
Simon Ding
e73ae86801 add parse tv & movie api 2024-07-27 23:42:06 +08:00
Simon Ding
b19938f2df remove 2024-07-27 22:42:29 +08:00
Simon Ding
bb3c4551af fix: api call 2024-07-27 22:35:49 +08:00
Simon Ding
eae35ce862 feat: show episode resource 2024-07-27 22:22:06 +08:00
Simon Ding
feecc9f983 feat: app proxy 2024-07-27 17:18:38 +08:00
Simon Ding
5175e651ee fix: donot use git shallow copy 2024-07-27 16:04:01 +08:00
Simon Ding
f065abfbf9 fix: install git 2024-07-27 15:55:08 +08:00
Simon Ding
cd4d600f5e feat: add app version ui 2024-07-27 15:44:49 +08:00
Simon Ding
741a4024fd feat: add app version 2024-07-27 15:34:58 +08:00
Simon Ding
0433cc7b0a chore: update doc 2024-07-27 11:24:22 +08:00
Simon Ding
accc02c74c chore: update doc 2024-07-27 11:21:34 +08:00
Simon Ding
87b6c99f1f feat: better doc 2024-07-27 11:17:16 +08:00
Simon Ding
b2a092c64e feat: add get all torrents api 2024-07-27 09:26:51 +08:00
Simon Ding
51fc5c3c74 chore: fix typo 2024-07-27 00:08:46 +08:00
Simon Ding
5e6a17f86c change padding 2024-07-27 00:06:18 +08:00
Simon Ding
2fedfd6c76 fix: not display error 2024-07-26 23:56:29 +08:00
Simon Ding
61bc9b72bd feat: disable webgl2 on safari 2024-07-26 23:26:14 +08:00
Simon Ding
a997726a5f feat: size readable 2024-07-26 17:13:43 +08:00
Simon Ding
7a2c67af04 main page 2024-07-26 17:07:36 +08:00
Simon Ding
3698170d0b chore: update main page 2024-07-26 17:03:08 +08:00
Simon Ding
6c38db5248 chore: upper case log level 2024-07-26 17:00:43 +08:00
Simon Ding
b597edab8a feat: add system page 2024-07-26 16:59:33 +08:00
Simon Ding
2e3b67dfce fix: db init 2024-07-26 14:28:58 +08:00
Simon Ding
1dd61ccbca feat: add log level setting 2024-07-26 14:22:08 +08:00
Simon Ding
f5f8434832 feat: support log rotation 2024-07-26 13:59:10 +08:00
Simon Ding
2cb6a15c0b feat: movie ignore sound tracks 2024-07-26 13:45:25 +08:00
Simon Ding
317f5655b8 chore: add space vertical 2024-07-26 13:33:24 +08:00
Simon Ding
00506df5a1 change image width 2024-07-26 13:12:40 +08:00
Simon Ding
57de442eb9 feat: simple tv & movie status 2024-07-26 13:07:54 +08:00
Simon Ding
690ce272c2 chore: change ui 2024-07-26 12:54:37 +08:00
Simon Ding
6a9f63fff6 feat: add movie status to ui 2024-07-26 12:25:23 +08:00
Simon Ding
7b9b619de6 add log 2024-07-25 20:44:04 +08:00
Simon Ding
8bc9076d90 fix: debug level default 2024-07-25 20:34:43 +08:00
Simon Ding
891be34504 feat: log to file 2024-07-25 20:28:13 +08:00
Simon Ding
04df9adfdf feat: change task scheduler 2024-07-25 15:35:44 +08:00
Simon Ding
3c47eba618 feat: add default timezone 2024-07-25 14:52:34 +08:00
Simon Ding
e85bd231c9 feat: add source field 2024-07-25 14:43:45 +08:00
102 changed files with 5542 additions and 1650 deletions

View File

@@ -25,6 +25,8 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Login to image repository
uses: docker/login-action@v2

View File

@@ -22,6 +22,8 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set Up QEMU
uses: docker/setup-qemu-action@v3

View File

@@ -3,7 +3,7 @@ WORKDIR /app
COPY ./ui/pubspec.yaml ./ui/pubspec.lock ./
RUN flutter pub get
COPY ./ui/ ./
RUN flutter build web
RUN flutter build web --no-web-resources-cdn
# 打包依赖阶段使用golang作为基础镜像
FROM golang:1.22 as builder
@@ -22,9 +22,10 @@ COPY . .
COPY --from=flutter /app/build/web ./ui/build/web/
# 指定OS等并go build
RUN CGO_ENABLED=1 go build -o polaris ./cmd/
RUN CGO_ENABLED=1 go build -o polaris -ldflags="-X polaris/db.Version=$(git describe --tags --long)" ./cmd/
FROM debian:12
FROM debian:stable-slim
ENV TZ="Asia/Shanghai" GIN_MODE=release
WORKDIR /app
RUN apt-get update && apt-get -y install ca-certificates
@@ -34,4 +35,6 @@ COPY --from=builder /app/polaris .
EXPOSE 8080
#USER 1000:1000
ENTRYPOINT ["./polaris"]

View File

@@ -2,8 +2,8 @@
Polaris 是一个电视剧和电影的追踪软件。配置好了之后当剧集或者电影播出后会第一时间下载对应的资源。支持本地存储或者webdav。
![main_page](assets/main_page.png)
![detail_page](assets/detail_page.png)
![main_page](./doc/assets/main_page.png)
![detail_page](./doc/assets/detail_page.png)
交流群: https://t.me/+8R2nzrlSs2JhMDgx
@@ -13,75 +13,21 @@ Polaris 是一个电视剧和电影的追踪软件。配置好了之后,当剧
- [x] 电影自动追踪下载
- [x] webdav 存储支持,配合 [alist](https://github.com/alist-org/alist) 或阿里云等实现更多功能
## 使用
使用此程序参考 [【快速开始】](./doc/quick_start.md)
## 对比 sonarr/radarr
* 更好的中文支持
* 对于动漫、日剧的良好支持,配合国内站点基本能匹配上对应资源
* 支持 webdav 后端存储,可以配合 alist 或者阿里云来实现下载后实时传到云上的功能。这样外出就可以不依靠家里的宽带来看电影了,或者实现个轻 NAS 功能,下载功能放在本地,数据放在云盘
* golang 实现后端,相比于 .NET 更节省资源
* 一个程序同时实现了电影、电视剧功能,不需要装两个程序
* 当然 sonarr/radarr 也是非常优秀的开源项目,目前 Polaris 功能还没有 sonarr/radarr 丰富
## 快速开始
最简单部署 Polaris 的方式是使用 docker compose
```yaml
services:
polaris:
image: ghcr.io/simon-ding/polaris:latest
restart: always
volumes:
- ./config/polaris:/app/data #程序配置文件路径
- /downloads:/downloads #下载路径,需要和下载客户端配置一致
- /data:/data #媒体数据存储路径也可以启动自己配置webdav存储
ports:
- 8080:8080
transmission: #下载客户端,也可以不安装使用已有的
image: lscr.io/linuxserver/transmission:latest
container_name: transmission
environment:
- PUID=1000
- PGID=1000
- TZ=Asia/Shanghai
volumes:
- ./config/transmission:/config
- /downloads:/downloads #此路径要与polaris下载路径保持一致
ports:
- 9091:9091
- 51413:51413
- 51413:51413/udp
```
拉起之后访问 http://< ip >:8080 的形式访问
## 配置
要正确使用此程序,需要配置好以下设置:
### TMDB设置
因为此程序需要使用到 TMDB 的数据,使用此程序首先要申请一个 TMDB 的 Api Key
### 索引器
索引器是资源提供者,目前支持 torznab 协议,意味着 polarr 或者 jackett 都可以支持。请自行部署相关程序。
推荐使用 linuxserver 的镜像https://docs.linuxserver.io/images/docker-jackett/
### 下载器
资源由谁下载,目前可支持 tansmission需要配置好对应下载器
### 存储设置
程序默认所有剧集和电影存储在 /data 路径下如果想修改路径或者webdav存储需要在存储配置下修改
## 开始使用
配置完了这些,开始享受使用此程序吧!可以搜索几部自己想看的电影或者电视机,加入想看列表。当剧集有更新或者电影有资源是就会自动下载对应资源了。
-------------
## 请我喝杯咖啡
<img src="assets/wechat.JPG" width=40% height=40%>
<img src="./doc/assets/wechat.JPG" width=40% height=40%>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

View File

@@ -7,6 +7,7 @@ import (
)
func main() {
log.Infof("------------------- Starting Polaris ---------------------")
dbClient, err := db.Open()
if err != nil {
log.Panicf("init db error: %v", err)

View File

@@ -1,11 +1,15 @@
package db
var Version = "undefined"
const (
SettingTmdbApiKey = "tmdb_api_key"
SettingLanguage = "language"
SettingJacketUrl = "jacket_url"
SettingJacketApiKey = "jacket_api_key"
SettingDownloadDir = "download_dir"
SettingLogLevel = "log_level"
SettingProxy = "proxy"
)
const (
@@ -18,6 +22,7 @@ const (
IndexerTorznabImpl = "torznab"
DataPath = "./data"
ImgPath = DataPath + "/img"
LogPath = DataPath + "/logs"
)
const (

View File

@@ -42,6 +42,7 @@ func Open() (*Client, error) {
c := &Client{
ent: client,
}
c.init()
return c, nil
}
@@ -55,7 +56,16 @@ func (c *Client) init() {
downloadDir := c.GetSetting(SettingDownloadDir)
if downloadDir == "" {
log.Infof("set default download dir")
c.SetSetting(downloadDir, "/downloads")
c.SetSetting(SettingDownloadDir, "/downloads")
}
logLevel := c.GetSetting(SettingLogLevel)
if logLevel == "" {
log.Infof("set default log level")
c.SetSetting(SettingLogLevel, "info")
}
if tr := c.GetTransmission(); tr == nil {
log.Warnf("no download client, set default download client")
c.SaveTransmission("transmission", "http://transmission:9091", "", "")
}
}
@@ -88,7 +98,7 @@ func (c *Client) generateDefaultLocalStorage() error {
func (c *Client) GetSetting(key string) string {
v, err := c.ent.Settings.Query().Where(settings.Key(key)).Only(context.TODO())
if err != nil {
log.Errorf("get setting by key: %s error: %v", key, err)
log.Warnf("get setting by key: %s error: %v", key, err)
return ""
}
return v.Value
@@ -191,6 +201,10 @@ func (c *Client) GetMediaDetails(id int) *MediaDetails {
return md
}
func (c *Client) GetMedia(id int) (*ent.Media, error) {
return c.ent.Media.Query().Where(media.ID(id)).First(context.TODO())
}
func (c *Client) DeleteMedia(id int) error {
_, err := c.ent.Episode.Delete().Where(episode.MediaID(id)).Exec(context.TODO())
if err != nil {
@@ -207,8 +221,10 @@ func (c *Client) SaveEposideDetail(d *ent.Episode) (int, error) {
SetEpisodeNumber(d.EpisodeNumber).
SetOverview(d.Overview).
SetTitle(d.Title).Save(context.TODO())
return ep.ID, err
if err != nil {
return 0, errors.Wrap(err, "save episode")
}
return ep.ID, nil
}
func (c *Client) SaveEposideDetail2(d *ent.Episode) (int, error) {
@@ -496,13 +512,13 @@ func (c *Client) GetDownloadDir() string {
return r.Value
}
func (c *Client) UpdateEpisodeFile(mediaID int, seasonNum, episodeNum int, file string) error {
func (c *Client) UpdateEpisodeStatus(mediaID int, seasonNum, episodeNum int) error {
ep, err := c.ent.Episode.Query().Where(episode.MediaID(mediaID)).Where(episode.EpisodeNumber(episodeNum)).
Where(episode.SeasonNumber(seasonNum)).First(context.TODO())
if err != nil {
return errors.Wrap(err, "finding episode")
}
return ep.Update().SetFileInStorage(file).SetStatus(episode.StatusDownloaded).Exec(context.TODO())
return ep.Update().SetStatus(episode.StatusDownloaded).Exec(context.TODO())
}
func (c *Client) SetEpisodeStatus(id int, status episode.Status) error {
@@ -520,3 +536,15 @@ func (c *Client) TmdbIdInWatchlist(tmdb_id int) bool {
func (c *Client) GetDownloadHistory(mediaID int) ([]*ent.History, error) {
return c.ent.History.Query().Where(history.MediaID(mediaID)).All(context.TODO())
}
func (c *Client) GetMovieDummyEpisode(movieId int) (*ent.Episode, error) {
_, err := c.ent.Media.Query().Where(media.ID(movieId), media.MediaTypeEQ(media.MediaTypeMovie)).First(context.TODO())
if err != nil {
return nil, errors.Wrap(err, "get movie")
}
ep, err := c.ent.Episode.Query().Where(episode.MediaID(movieId)).First(context.TODO())
if err != nil {
return nil, errors.Wrap(err, "query episode")
}
return ep, nil
}

97
db/notification.go Normal file
View File

@@ -0,0 +1,97 @@
package db
import (
"context"
"encoding/json"
"polaris/ent"
"polaris/ent/notificationclient"
"polaris/pkg/notifier"
"strings"
"github.com/pkg/errors"
)
func (c *Client) GetAllNotificationClients2() ([]*ent.NotificationClient, error) {
return c.ent.NotificationClient.Query().All(context.TODO())
}
func (c *Client) GetAllNotificationClients() ([]*NotificationClient, error) {
all, err := c.ent.NotificationClient.Query().All(context.TODO())
if err != nil {
return nil, errors.Wrap(err, "query db")
}
var all1 []*NotificationClient
for _, item := range all {
cl, err := toNotificationClient(item)
if err != nil {
return nil, errors.Wrap(err, "convert")
}
all1 = append(all1, cl)
}
return all1, nil
}
func (c *Client) AddNotificationClient(name, service string, setting string, enabled bool) error {
// data, err := json.Marshal(setting)
// if err != nil {
// return errors.Wrap(err, "json")
// }
service = strings.ToLower(service)
count, err := c.ent.NotificationClient.Query().Where(notificationclient.Name(name)).Count(context.Background())
if err == nil && count > 0 {
//update exist one
return c.ent.NotificationClient.Update().Where(notificationclient.Name(name)).SetService(service).
SetSettings(setting).SetEnabled(enabled).Exec(context.Background())
}
return c.ent.NotificationClient.Create().SetName(name).SetService(service).
SetSettings(setting).SetEnabled(enabled).Exec(context.Background())
}
func (c *Client) DeleteNotificationClient(id int) error {
_, err := c.ent.NotificationClient.Delete().Where(notificationclient.ID(id)).Exec(context.Background())
return err
}
func (c *Client) GetNotificationClient(id int) (*NotificationClient, error) {
noti, err := c.ent.NotificationClient.Query().Where(notificationclient.ID(id)).First(context.Background())
if err != nil {
return nil, errors.Wrap(err, "query")
}
return toNotificationClient(noti)
}
func toNotificationClient(cl *ent.NotificationClient) (*NotificationClient, error) {
var settings interface{}
switch cl.Service {
case "pushover":
settings = notifier.PushoverConfig{}
case "dingtalk":
settings = notifier.DingTalkConfig{}
case "telegram":
settings = notifier.TelegramConfig{}
case "bark":
settings = notifier.BarkConfig{}
}
err := json.Unmarshal([]byte(cl.Settings), &settings)
if err != nil {
return nil, errors.Wrap(err, "json")
}
return &NotificationClient{
ID: cl.ID,
Name: cl.Name,
Service: cl.Service,
Enabled: cl.Enabled,
Settings: settings,
}, nil
}
type NotificationClient struct {
ID int `json:"id"`
Name string `json:"name"`
Service string `json:"service"`
Enabled bool `json:"enabled"`
Settings interface{} `json:"settings"`
}

BIN
doc/assets/add_indexer.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
doc/assets/add_series.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 KiB

BIN
doc/assets/copy_feed.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
doc/assets/downloader.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
doc/assets/main_page.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
doc/assets/search_add.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

54
doc/configuration.md Normal file
View File

@@ -0,0 +1,54 @@
# 配置
要正确使用此程序,需要配置好以下设置:
### TMDB设置
1. 因为此程序需要使用到 TMDB 的数据,使用此程序首先要申请一个 TMDB 的 Api Key. 申请教程请 google [tmdb api key申请](https://www.google.com/search?q=tmdb+api+key%E7%94%B3%E8%AF%B7)
2. 拿到 TMDB Api Key之后请填到 *设置 -> 常规设置 -> TMDB Api Key里*
### 索引器
索引器是资源提供者,目前支持 torznab 协议,意味着 polarr 或者 jackett 都可以支持。请自行部署相关程序,或者使用的 docker compose 配置一起拉起
推荐使用 linuxserver 的镜像https://docs.linuxserver.io/images/docker-jackett/
#### 索引器配置
索引器配置这里以 jackett 为例。使用默认 docker compose 配置拉起后以 http://< ip >:9117 可访问 jackett 的主页。
1. 打开 jackett 主页后,点击页面上面的 Add indexer会出现 BT/PT 站点列表,选择你需要的站点点击+号添加。如果是PT请自行配置好相关配置
![add indexer](./assets/add_indexer.png)
![search add](./assets/search_add.png)
2. 添加后主页即会显示相应的BT/PT站点点击 *Copy Torznab Feed* 即得到了我们需要的地址
![copy feed](./assets/copy_feed.png)
3. 回到我们的主程序 Polaris 当中,点击 *设置 -> 索引器设置* -> 点击+号增加新的索引器输入一个名称拷贝我们第2步得到的地址到地址栏
![polaris add indexer](./assets/polaris_add_indexer.png)
4. 选相框中我们可以看到,还需要一个 API Key我们回到 Jackett 中,在页面右上角,复制我们需要的 API Key
![api key](./assets/jackett_api_key.png)
5. 恭喜!你已经成功完成了索引器配置。如需要更多的站点,请重复相同的操作完成配置
### 下载器
资源下载器,目前可支持 tansmission请配置好对应配置
![transmission](./assets/downloader.png)
### 存储设置
默认配置了名为 local 的本地存储,如果你不知道怎么配置。请使用默认配置
![local_storage](./assets/local_storage.png)
类型里选择 webdav 可以使用 webdav 存储,配合 alist/clouddrive 等,可以实现存储到云盘里的功能。
![webdav](./assets/webdav_storage.png)

69
doc/quick_start.md Normal file
View File

@@ -0,0 +1,69 @@
## 快速开始
最简单部署 Polaris 的方式是使用 docker composePolaris要完整运行另外需要一个索引客户端和一个下载客户端。索引客户端支持 polarr 或 jackett下载客户端目前只支持 transmission。
下面是一个示例 docker-compose 配置,为了简单起见,一起拉起了 transmission 和 jackett你也可选择单独安装
**注意:** transmission 的下载路径映射要和 polaris 保持一致,如果您不知道怎么做,请保持默认设置。
```yaml
services:
polaris:
image: ghcr.io/simon-ding/polaris:latest
restart: always
volumes:
- ./config/polaris:/app/data #程序配置文件路径
- /downloads:/downloads #下载路径,需要和下载客户端配置一致
- /data:/data #媒体数据存储路径也可以启动自己配置webdav存储
ports:
- 8080:8080
transmission: #下载客户端,也可以不安装使用已有的
image: lscr.io/linuxserver/transmission:latest
environment:
- PUID=1000
- PGID=1000
- TZ=Asia/Shanghai
volumes:
- ./config/transmission:/config
- /downloads:/downloads #此路径要与polaris下载路径保持一致
ports:
- 9091:9091
- 51413:51413
- 51413:51413/udp
jackett: #索引客户端,也可以不安装使用已有的
image: lscr.io/linuxserver/jackett:latest
environment:
- PUID=1000
- PGID=1000
- TZ=Asia/Shanghai
volumes:
- ./config/jackett:/config
ports:
- 9117:9117
restart: unless-stopped
```
拉起之后访问 http://< ip >:8080 的形式访问
![](./assets/main_page.png)
## 配置
详细配置请看 [配置篇](./configuration.md)
## 开始使用
1. 完成配置之后,我们就可以在右上角的搜索按钮里输入我们想看的电影、电视剧。
![search](./assets/search_series.png)
2. 找到对应电影电视剧后,点击加入想看列表
![add](./assets/add_series.png)
3. 当电影有资源、或者电视剧有更新时,程序就会自动下载对应资源到指定的存储。对于剧集,您也可以进入剧集的详细页面,点击搜索按钮来自己搜索对应集的资源。
到此,您已经基本掌握了此程序的使用方式,请尽情体验吧!

View File

@@ -16,6 +16,7 @@ import (
"polaris/ent/history"
"polaris/ent/indexers"
"polaris/ent/media"
"polaris/ent/notificationclient"
"polaris/ent/settings"
"polaris/ent/storage"
@@ -40,6 +41,8 @@ type Client struct {
Indexers *IndexersClient
// Media is the client for interacting with the Media builders.
Media *MediaClient
// NotificationClient is the client for interacting with the NotificationClient builders.
NotificationClient *NotificationClientClient
// Settings is the client for interacting with the Settings builders.
Settings *SettingsClient
// Storage is the client for interacting with the Storage builders.
@@ -60,6 +63,7 @@ func (c *Client) init() {
c.History = NewHistoryClient(c.config)
c.Indexers = NewIndexersClient(c.config)
c.Media = NewMediaClient(c.config)
c.NotificationClient = NewNotificationClientClient(c.config)
c.Settings = NewSettingsClient(c.config)
c.Storage = NewStorageClient(c.config)
}
@@ -152,15 +156,16 @@ func (c *Client) Tx(ctx context.Context) (*Tx, error) {
cfg := c.config
cfg.driver = tx
return &Tx{
ctx: ctx,
config: cfg,
DownloadClients: NewDownloadClientsClient(cfg),
Episode: NewEpisodeClient(cfg),
History: NewHistoryClient(cfg),
Indexers: NewIndexersClient(cfg),
Media: NewMediaClient(cfg),
Settings: NewSettingsClient(cfg),
Storage: NewStorageClient(cfg),
ctx: ctx,
config: cfg,
DownloadClients: NewDownloadClientsClient(cfg),
Episode: NewEpisodeClient(cfg),
History: NewHistoryClient(cfg),
Indexers: NewIndexersClient(cfg),
Media: NewMediaClient(cfg),
NotificationClient: NewNotificationClientClient(cfg),
Settings: NewSettingsClient(cfg),
Storage: NewStorageClient(cfg),
}, nil
}
@@ -178,15 +183,16 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)
cfg := c.config
cfg.driver = &txDriver{tx: tx, drv: c.driver}
return &Tx{
ctx: ctx,
config: cfg,
DownloadClients: NewDownloadClientsClient(cfg),
Episode: NewEpisodeClient(cfg),
History: NewHistoryClient(cfg),
Indexers: NewIndexersClient(cfg),
Media: NewMediaClient(cfg),
Settings: NewSettingsClient(cfg),
Storage: NewStorageClient(cfg),
ctx: ctx,
config: cfg,
DownloadClients: NewDownloadClientsClient(cfg),
Episode: NewEpisodeClient(cfg),
History: NewHistoryClient(cfg),
Indexers: NewIndexersClient(cfg),
Media: NewMediaClient(cfg),
NotificationClient: NewNotificationClientClient(cfg),
Settings: NewSettingsClient(cfg),
Storage: NewStorageClient(cfg),
}, nil
}
@@ -216,8 +222,8 @@ func (c *Client) Close() error {
// In order to add hooks to a specific client, call: `client.Node.Use(...)`.
func (c *Client) Use(hooks ...Hook) {
for _, n := range []interface{ Use(...Hook) }{
c.DownloadClients, c.Episode, c.History, c.Indexers, c.Media, c.Settings,
c.Storage,
c.DownloadClients, c.Episode, c.History, c.Indexers, c.Media,
c.NotificationClient, c.Settings, c.Storage,
} {
n.Use(hooks...)
}
@@ -227,8 +233,8 @@ func (c *Client) Use(hooks ...Hook) {
// In order to add interceptors to a specific client, call: `client.Node.Intercept(...)`.
func (c *Client) Intercept(interceptors ...Interceptor) {
for _, n := range []interface{ Intercept(...Interceptor) }{
c.DownloadClients, c.Episode, c.History, c.Indexers, c.Media, c.Settings,
c.Storage,
c.DownloadClients, c.Episode, c.History, c.Indexers, c.Media,
c.NotificationClient, c.Settings, c.Storage,
} {
n.Intercept(interceptors...)
}
@@ -247,6 +253,8 @@ func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) {
return c.Indexers.mutate(ctx, m)
case *MediaMutation:
return c.Media.mutate(ctx, m)
case *NotificationClientMutation:
return c.NotificationClient.mutate(ctx, m)
case *SettingsMutation:
return c.Settings.mutate(ctx, m)
case *StorageMutation:
@@ -953,6 +961,139 @@ func (c *MediaClient) mutate(ctx context.Context, m *MediaMutation) (Value, erro
}
}
// NotificationClientClient is a client for the NotificationClient schema.
type NotificationClientClient struct {
config
}
// NewNotificationClientClient returns a client for the NotificationClient from the given config.
func NewNotificationClientClient(c config) *NotificationClientClient {
return &NotificationClientClient{config: c}
}
// Use adds a list of mutation hooks to the hooks stack.
// A call to `Use(f, g, h)` equals to `notificationclient.Hooks(f(g(h())))`.
func (c *NotificationClientClient) Use(hooks ...Hook) {
c.hooks.NotificationClient = append(c.hooks.NotificationClient, hooks...)
}
// Intercept adds a list of query interceptors to the interceptors stack.
// A call to `Intercept(f, g, h)` equals to `notificationclient.Intercept(f(g(h())))`.
func (c *NotificationClientClient) Intercept(interceptors ...Interceptor) {
c.inters.NotificationClient = append(c.inters.NotificationClient, interceptors...)
}
// Create returns a builder for creating a NotificationClient entity.
func (c *NotificationClientClient) Create() *NotificationClientCreate {
mutation := newNotificationClientMutation(c.config, OpCreate)
return &NotificationClientCreate{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// CreateBulk returns a builder for creating a bulk of NotificationClient entities.
func (c *NotificationClientClient) CreateBulk(builders ...*NotificationClientCreate) *NotificationClientCreateBulk {
return &NotificationClientCreateBulk{config: c.config, builders: builders}
}
// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates
// a builder and applies setFunc on it.
func (c *NotificationClientClient) MapCreateBulk(slice any, setFunc func(*NotificationClientCreate, int)) *NotificationClientCreateBulk {
rv := reflect.ValueOf(slice)
if rv.Kind() != reflect.Slice {
return &NotificationClientCreateBulk{err: fmt.Errorf("calling to NotificationClientClient.MapCreateBulk with wrong type %T, need slice", slice)}
}
builders := make([]*NotificationClientCreate, rv.Len())
for i := 0; i < rv.Len(); i++ {
builders[i] = c.Create()
setFunc(builders[i], i)
}
return &NotificationClientCreateBulk{config: c.config, builders: builders}
}
// Update returns an update builder for NotificationClient.
func (c *NotificationClientClient) Update() *NotificationClientUpdate {
mutation := newNotificationClientMutation(c.config, OpUpdate)
return &NotificationClientUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// UpdateOne returns an update builder for the given entity.
func (c *NotificationClientClient) UpdateOne(nc *NotificationClient) *NotificationClientUpdateOne {
mutation := newNotificationClientMutation(c.config, OpUpdateOne, withNotificationClient(nc))
return &NotificationClientUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// UpdateOneID returns an update builder for the given id.
func (c *NotificationClientClient) UpdateOneID(id int) *NotificationClientUpdateOne {
mutation := newNotificationClientMutation(c.config, OpUpdateOne, withNotificationClientID(id))
return &NotificationClientUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// Delete returns a delete builder for NotificationClient.
func (c *NotificationClientClient) Delete() *NotificationClientDelete {
mutation := newNotificationClientMutation(c.config, OpDelete)
return &NotificationClientDelete{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// DeleteOne returns a builder for deleting the given entity.
func (c *NotificationClientClient) DeleteOne(nc *NotificationClient) *NotificationClientDeleteOne {
return c.DeleteOneID(nc.ID)
}
// DeleteOneID returns a builder for deleting the given entity by its id.
func (c *NotificationClientClient) DeleteOneID(id int) *NotificationClientDeleteOne {
builder := c.Delete().Where(notificationclient.ID(id))
builder.mutation.id = &id
builder.mutation.op = OpDeleteOne
return &NotificationClientDeleteOne{builder}
}
// Query returns a query builder for NotificationClient.
func (c *NotificationClientClient) Query() *NotificationClientQuery {
return &NotificationClientQuery{
config: c.config,
ctx: &QueryContext{Type: TypeNotificationClient},
inters: c.Interceptors(),
}
}
// Get returns a NotificationClient entity by its id.
func (c *NotificationClientClient) Get(ctx context.Context, id int) (*NotificationClient, error) {
return c.Query().Where(notificationclient.ID(id)).Only(ctx)
}
// GetX is like Get, but panics if an error occurs.
func (c *NotificationClientClient) GetX(ctx context.Context, id int) *NotificationClient {
obj, err := c.Get(ctx, id)
if err != nil {
panic(err)
}
return obj
}
// Hooks returns the client hooks.
func (c *NotificationClientClient) Hooks() []Hook {
return c.hooks.NotificationClient
}
// Interceptors returns the client interceptors.
func (c *NotificationClientClient) Interceptors() []Interceptor {
return c.inters.NotificationClient
}
func (c *NotificationClientClient) mutate(ctx context.Context, m *NotificationClientMutation) (Value, error) {
switch m.Op() {
case OpCreate:
return (&NotificationClientCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpUpdate:
return (&NotificationClientUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpUpdateOne:
return (&NotificationClientUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpDelete, OpDeleteOne:
return (&NotificationClientDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx)
default:
return nil, fmt.Errorf("ent: unknown NotificationClient mutation op: %q", m.Op())
}
}
// SettingsClient is a client for the Settings schema.
type SettingsClient struct {
config
@@ -1222,10 +1363,11 @@ func (c *StorageClient) mutate(ctx context.Context, m *StorageMutation) (Value,
// hooks and interceptors per client, for fast access.
type (
hooks struct {
DownloadClients, Episode, History, Indexers, Media, Settings, Storage []ent.Hook
DownloadClients, Episode, History, Indexers, Media, NotificationClient,
Settings, Storage []ent.Hook
}
inters struct {
DownloadClients, Episode, History, Indexers, Media, Settings,
Storage []ent.Interceptor
DownloadClients, Episode, History, Indexers, Media, NotificationClient,
Settings, Storage []ent.Interceptor
}
)

View File

@@ -11,6 +11,7 @@ import (
"polaris/ent/history"
"polaris/ent/indexers"
"polaris/ent/media"
"polaris/ent/notificationclient"
"polaris/ent/settings"
"polaris/ent/storage"
"reflect"
@@ -79,13 +80,14 @@ var (
func checkColumn(table, column string) error {
initCheck.Do(func() {
columnCheck = sql.NewColumnCheck(map[string]func(string) bool{
downloadclients.Table: downloadclients.ValidColumn,
episode.Table: episode.ValidColumn,
history.Table: history.ValidColumn,
indexers.Table: indexers.ValidColumn,
media.Table: media.ValidColumn,
settings.Table: settings.ValidColumn,
storage.Table: storage.ValidColumn,
downloadclients.Table: downloadclients.ValidColumn,
episode.Table: episode.ValidColumn,
history.Table: history.ValidColumn,
indexers.Table: indexers.ValidColumn,
media.Table: media.ValidColumn,
notificationclient.Table: notificationclient.ValidColumn,
settings.Table: settings.ValidColumn,
storage.Table: storage.ValidColumn,
})
})
return columnCheck(table, column)

View File

@@ -31,8 +31,6 @@ type Episode struct {
AirDate string `json:"air_date,omitempty"`
// Status holds the value of the "status" field.
Status episode.Status `json:"status,omitempty"`
// FileInStorage holds the value of the "file_in_storage" field.
FileInStorage string `json:"file_in_storage,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the EpisodeQuery when eager-loading is set.
Edges EpisodeEdges `json:"edges"`
@@ -66,7 +64,7 @@ func (*Episode) scanValues(columns []string) ([]any, error) {
switch columns[i] {
case episode.FieldID, episode.FieldMediaID, episode.FieldSeasonNumber, episode.FieldEpisodeNumber:
values[i] = new(sql.NullInt64)
case episode.FieldTitle, episode.FieldOverview, episode.FieldAirDate, episode.FieldStatus, episode.FieldFileInStorage:
case episode.FieldTitle, episode.FieldOverview, episode.FieldAirDate, episode.FieldStatus:
values[i] = new(sql.NullString)
default:
values[i] = new(sql.UnknownType)
@@ -131,12 +129,6 @@ func (e *Episode) assignValues(columns []string, values []any) error {
} else if value.Valid {
e.Status = episode.Status(value.String)
}
case episode.FieldFileInStorage:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field file_in_storage", values[i])
} else if value.Valid {
e.FileInStorage = value.String
}
default:
e.selectValues.Set(columns[i], values[i])
}
@@ -198,9 +190,6 @@ func (e *Episode) String() string {
builder.WriteString(", ")
builder.WriteString("status=")
builder.WriteString(fmt.Sprintf("%v", e.Status))
builder.WriteString(", ")
builder.WriteString("file_in_storage=")
builder.WriteString(e.FileInStorage)
builder.WriteByte(')')
return builder.String()
}

View File

@@ -28,8 +28,6 @@ const (
FieldAirDate = "air_date"
// FieldStatus holds the string denoting the status field in the database.
FieldStatus = "status"
// FieldFileInStorage holds the string denoting the file_in_storage field in the database.
FieldFileInStorage = "file_in_storage"
// EdgeMedia holds the string denoting the media edge name in mutations.
EdgeMedia = "media"
// Table holds the table name of the episode in the database.
@@ -53,7 +51,6 @@ var Columns = []string{
FieldOverview,
FieldAirDate,
FieldStatus,
FieldFileInStorage,
}
// ValidColumn reports if the column name is valid (part of the table columns).
@@ -136,11 +133,6 @@ func ByStatus(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldStatus, opts...).ToFunc()
}
// ByFileInStorage orders the results by the file_in_storage field.
func ByFileInStorage(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldFileInStorage, opts...).ToFunc()
}
// ByMediaField orders the results by media field.
func ByMediaField(field string, opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {

View File

@@ -84,11 +84,6 @@ func AirDate(v string) predicate.Episode {
return predicate.Episode(sql.FieldEQ(FieldAirDate, v))
}
// FileInStorage applies equality check predicate on the "file_in_storage" field. It's identical to FileInStorageEQ.
func FileInStorage(v string) predicate.Episode {
return predicate.Episode(sql.FieldEQ(FieldFileInStorage, v))
}
// MediaIDEQ applies the EQ predicate on the "media_id" field.
func MediaIDEQ(v int) predicate.Episode {
return predicate.Episode(sql.FieldEQ(FieldMediaID, v))
@@ -414,81 +409,6 @@ func StatusNotIn(vs ...Status) predicate.Episode {
return predicate.Episode(sql.FieldNotIn(FieldStatus, vs...))
}
// FileInStorageEQ applies the EQ predicate on the "file_in_storage" field.
func FileInStorageEQ(v string) predicate.Episode {
return predicate.Episode(sql.FieldEQ(FieldFileInStorage, v))
}
// FileInStorageNEQ applies the NEQ predicate on the "file_in_storage" field.
func FileInStorageNEQ(v string) predicate.Episode {
return predicate.Episode(sql.FieldNEQ(FieldFileInStorage, v))
}
// FileInStorageIn applies the In predicate on the "file_in_storage" field.
func FileInStorageIn(vs ...string) predicate.Episode {
return predicate.Episode(sql.FieldIn(FieldFileInStorage, vs...))
}
// FileInStorageNotIn applies the NotIn predicate on the "file_in_storage" field.
func FileInStorageNotIn(vs ...string) predicate.Episode {
return predicate.Episode(sql.FieldNotIn(FieldFileInStorage, vs...))
}
// FileInStorageGT applies the GT predicate on the "file_in_storage" field.
func FileInStorageGT(v string) predicate.Episode {
return predicate.Episode(sql.FieldGT(FieldFileInStorage, v))
}
// FileInStorageGTE applies the GTE predicate on the "file_in_storage" field.
func FileInStorageGTE(v string) predicate.Episode {
return predicate.Episode(sql.FieldGTE(FieldFileInStorage, v))
}
// FileInStorageLT applies the LT predicate on the "file_in_storage" field.
func FileInStorageLT(v string) predicate.Episode {
return predicate.Episode(sql.FieldLT(FieldFileInStorage, v))
}
// FileInStorageLTE applies the LTE predicate on the "file_in_storage" field.
func FileInStorageLTE(v string) predicate.Episode {
return predicate.Episode(sql.FieldLTE(FieldFileInStorage, v))
}
// FileInStorageContains applies the Contains predicate on the "file_in_storage" field.
func FileInStorageContains(v string) predicate.Episode {
return predicate.Episode(sql.FieldContains(FieldFileInStorage, v))
}
// FileInStorageHasPrefix applies the HasPrefix predicate on the "file_in_storage" field.
func FileInStorageHasPrefix(v string) predicate.Episode {
return predicate.Episode(sql.FieldHasPrefix(FieldFileInStorage, v))
}
// FileInStorageHasSuffix applies the HasSuffix predicate on the "file_in_storage" field.
func FileInStorageHasSuffix(v string) predicate.Episode {
return predicate.Episode(sql.FieldHasSuffix(FieldFileInStorage, v))
}
// FileInStorageIsNil applies the IsNil predicate on the "file_in_storage" field.
func FileInStorageIsNil() predicate.Episode {
return predicate.Episode(sql.FieldIsNull(FieldFileInStorage))
}
// FileInStorageNotNil applies the NotNil predicate on the "file_in_storage" field.
func FileInStorageNotNil() predicate.Episode {
return predicate.Episode(sql.FieldNotNull(FieldFileInStorage))
}
// FileInStorageEqualFold applies the EqualFold predicate on the "file_in_storage" field.
func FileInStorageEqualFold(v string) predicate.Episode {
return predicate.Episode(sql.FieldEqualFold(FieldFileInStorage, v))
}
// FileInStorageContainsFold applies the ContainsFold predicate on the "file_in_storage" field.
func FileInStorageContainsFold(v string) predicate.Episode {
return predicate.Episode(sql.FieldContainsFold(FieldFileInStorage, v))
}
// HasMedia applies the HasEdge predicate on the "media" edge.
func HasMedia() predicate.Episode {
return predicate.Episode(func(s *sql.Selector) {

View File

@@ -78,20 +78,6 @@ func (ec *EpisodeCreate) SetNillableStatus(e *episode.Status) *EpisodeCreate {
return ec
}
// SetFileInStorage sets the "file_in_storage" field.
func (ec *EpisodeCreate) SetFileInStorage(s string) *EpisodeCreate {
ec.mutation.SetFileInStorage(s)
return ec
}
// SetNillableFileInStorage sets the "file_in_storage" field if the given value is not nil.
func (ec *EpisodeCreate) SetNillableFileInStorage(s *string) *EpisodeCreate {
if s != nil {
ec.SetFileInStorage(*s)
}
return ec
}
// SetMedia sets the "media" edge to the Media entity.
func (ec *EpisodeCreate) SetMedia(m *Media) *EpisodeCreate {
return ec.SetMediaID(m.ID)
@@ -213,10 +199,6 @@ func (ec *EpisodeCreate) createSpec() (*Episode, *sqlgraph.CreateSpec) {
_spec.SetField(episode.FieldStatus, field.TypeEnum, value)
_node.Status = value
}
if value, ok := ec.mutation.FileInStorage(); ok {
_spec.SetField(episode.FieldFileInStorage, field.TypeString, value)
_node.FileInStorage = value
}
if nodes := ec.mutation.MediaIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,

View File

@@ -146,26 +146,6 @@ func (eu *EpisodeUpdate) SetNillableStatus(e *episode.Status) *EpisodeUpdate {
return eu
}
// SetFileInStorage sets the "file_in_storage" field.
func (eu *EpisodeUpdate) SetFileInStorage(s string) *EpisodeUpdate {
eu.mutation.SetFileInStorage(s)
return eu
}
// SetNillableFileInStorage sets the "file_in_storage" field if the given value is not nil.
func (eu *EpisodeUpdate) SetNillableFileInStorage(s *string) *EpisodeUpdate {
if s != nil {
eu.SetFileInStorage(*s)
}
return eu
}
// ClearFileInStorage clears the value of the "file_in_storage" field.
func (eu *EpisodeUpdate) ClearFileInStorage() *EpisodeUpdate {
eu.mutation.ClearFileInStorage()
return eu
}
// SetMedia sets the "media" edge to the Media entity.
func (eu *EpisodeUpdate) SetMedia(m *Media) *EpisodeUpdate {
return eu.SetMediaID(m.ID)
@@ -255,12 +235,6 @@ func (eu *EpisodeUpdate) sqlSave(ctx context.Context) (n int, err error) {
if value, ok := eu.mutation.Status(); ok {
_spec.SetField(episode.FieldStatus, field.TypeEnum, value)
}
if value, ok := eu.mutation.FileInStorage(); ok {
_spec.SetField(episode.FieldFileInStorage, field.TypeString, value)
}
if eu.mutation.FileInStorageCleared() {
_spec.ClearField(episode.FieldFileInStorage, field.TypeString)
}
if eu.mutation.MediaCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
@@ -428,26 +402,6 @@ func (euo *EpisodeUpdateOne) SetNillableStatus(e *episode.Status) *EpisodeUpdate
return euo
}
// SetFileInStorage sets the "file_in_storage" field.
func (euo *EpisodeUpdateOne) SetFileInStorage(s string) *EpisodeUpdateOne {
euo.mutation.SetFileInStorage(s)
return euo
}
// SetNillableFileInStorage sets the "file_in_storage" field if the given value is not nil.
func (euo *EpisodeUpdateOne) SetNillableFileInStorage(s *string) *EpisodeUpdateOne {
if s != nil {
euo.SetFileInStorage(*s)
}
return euo
}
// ClearFileInStorage clears the value of the "file_in_storage" field.
func (euo *EpisodeUpdateOne) ClearFileInStorage() *EpisodeUpdateOne {
euo.mutation.ClearFileInStorage()
return euo
}
// SetMedia sets the "media" edge to the Media entity.
func (euo *EpisodeUpdateOne) SetMedia(m *Media) *EpisodeUpdateOne {
return euo.SetMediaID(m.ID)
@@ -567,12 +521,6 @@ func (euo *EpisodeUpdateOne) sqlSave(ctx context.Context) (_node *Episode, err e
if value, ok := euo.mutation.Status(); ok {
_spec.SetField(episode.FieldStatus, field.TypeEnum, value)
}
if value, ok := euo.mutation.FileInStorage(); ok {
_spec.SetField(episode.FieldFileInStorage, field.TypeString, value)
}
if euo.mutation.FileInStorageCleared() {
_spec.ClearField(episode.FieldFileInStorage, field.TypeString)
}
if euo.mutation.MediaCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,

View File

@@ -68,6 +68,18 @@ func (f MediaFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error
return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.MediaMutation", m)
}
// The NotificationClientFunc type is an adapter to allow the use of ordinary
// function as NotificationClient mutator.
type NotificationClientFunc func(context.Context, *ent.NotificationClientMutation) (ent.Value, error)
// Mutate calls f(ctx, m).
func (f NotificationClientFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) {
if mv, ok := m.(*ent.NotificationClientMutation); ok {
return f(ctx, mv)
}
return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.NotificationClientMutation", m)
}
// The SettingsFunc type is an adapter to allow the use of ordinary
// function as Settings mutator.
type SettingsFunc func(context.Context, *ent.SettingsMutation) (ent.Value, error)

View File

@@ -41,6 +41,8 @@ type Media struct {
StorageID int `json:"storage_id,omitempty"`
// TargetDir holds the value of the "target_dir" field.
TargetDir string `json:"target_dir,omitempty"`
// tv series only
DownloadHistoryEpisodes bool `json:"download_history_episodes,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the MediaQuery when eager-loading is set.
Edges MediaEdges `json:"edges"`
@@ -70,6 +72,8 @@ func (*Media) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case media.FieldDownloadHistoryEpisodes:
values[i] = new(sql.NullBool)
case media.FieldID, media.FieldTmdbID, media.FieldStorageID:
values[i] = new(sql.NullInt64)
case media.FieldImdbID, media.FieldMediaType, media.FieldNameCn, media.FieldNameEn, media.FieldOriginalName, media.FieldOverview, media.FieldAirDate, media.FieldResolution, media.FieldTargetDir:
@@ -169,6 +173,12 @@ func (m *Media) assignValues(columns []string, values []any) error {
} else if value.Valid {
m.TargetDir = value.String
}
case media.FieldDownloadHistoryEpisodes:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field download_history_episodes", values[i])
} else if value.Valid {
m.DownloadHistoryEpisodes = value.Bool
}
default:
m.selectValues.Set(columns[i], values[i])
}
@@ -245,6 +255,9 @@ func (m *Media) String() string {
builder.WriteString(", ")
builder.WriteString("target_dir=")
builder.WriteString(m.TargetDir)
builder.WriteString(", ")
builder.WriteString("download_history_episodes=")
builder.WriteString(fmt.Sprintf("%v", m.DownloadHistoryEpisodes))
builder.WriteByte(')')
return builder.String()
}

View File

@@ -39,6 +39,8 @@ const (
FieldStorageID = "storage_id"
// FieldTargetDir holds the string denoting the target_dir field in the database.
FieldTargetDir = "target_dir"
// FieldDownloadHistoryEpisodes holds the string denoting the download_history_episodes field in the database.
FieldDownloadHistoryEpisodes = "download_history_episodes"
// EdgeEpisodes holds the string denoting the episodes edge name in mutations.
EdgeEpisodes = "episodes"
// Table holds the table name of the media in the database.
@@ -67,6 +69,7 @@ var Columns = []string{
FieldResolution,
FieldStorageID,
FieldTargetDir,
FieldDownloadHistoryEpisodes,
}
// ValidColumn reports if the column name is valid (part of the table columns).
@@ -84,6 +87,8 @@ var (
DefaultCreatedAt time.Time
// DefaultAirDate holds the default value on creation for the "air_date" field.
DefaultAirDate string
// DefaultDownloadHistoryEpisodes holds the default value on creation for the "download_history_episodes" field.
DefaultDownloadHistoryEpisodes bool
)
// MediaType defines the type for the "media_type" enum field.
@@ -204,6 +209,11 @@ func ByTargetDir(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldTargetDir, opts...).ToFunc()
}
// ByDownloadHistoryEpisodes orders the results by the download_history_episodes field.
func ByDownloadHistoryEpisodes(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldDownloadHistoryEpisodes, opts...).ToFunc()
}
// ByEpisodesCount orders the results by episodes count.
func ByEpisodesCount(opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {

View File

@@ -105,6 +105,11 @@ func TargetDir(v string) predicate.Media {
return predicate.Media(sql.FieldEQ(FieldTargetDir, v))
}
// DownloadHistoryEpisodes applies equality check predicate on the "download_history_episodes" field. It's identical to DownloadHistoryEpisodesEQ.
func DownloadHistoryEpisodes(v bool) predicate.Media {
return predicate.Media(sql.FieldEQ(FieldDownloadHistoryEpisodes, v))
}
// TmdbIDEQ applies the EQ predicate on the "tmdb_id" field.
func TmdbIDEQ(v int) predicate.Media {
return predicate.Media(sql.FieldEQ(FieldTmdbID, v))
@@ -750,6 +755,26 @@ func TargetDirContainsFold(v string) predicate.Media {
return predicate.Media(sql.FieldContainsFold(FieldTargetDir, v))
}
// DownloadHistoryEpisodesEQ applies the EQ predicate on the "download_history_episodes" field.
func DownloadHistoryEpisodesEQ(v bool) predicate.Media {
return predicate.Media(sql.FieldEQ(FieldDownloadHistoryEpisodes, v))
}
// DownloadHistoryEpisodesNEQ applies the NEQ predicate on the "download_history_episodes" field.
func DownloadHistoryEpisodesNEQ(v bool) predicate.Media {
return predicate.Media(sql.FieldNEQ(FieldDownloadHistoryEpisodes, v))
}
// DownloadHistoryEpisodesIsNil applies the IsNil predicate on the "download_history_episodes" field.
func DownloadHistoryEpisodesIsNil() predicate.Media {
return predicate.Media(sql.FieldIsNull(FieldDownloadHistoryEpisodes))
}
// DownloadHistoryEpisodesNotNil applies the NotNil predicate on the "download_history_episodes" field.
func DownloadHistoryEpisodesNotNil() predicate.Media {
return predicate.Media(sql.FieldNotNull(FieldDownloadHistoryEpisodes))
}
// HasEpisodes applies the HasEdge predicate on the "episodes" edge.
func HasEpisodes() predicate.Media {
return predicate.Media(func(s *sql.Selector) {

View File

@@ -141,6 +141,20 @@ func (mc *MediaCreate) SetNillableTargetDir(s *string) *MediaCreate {
return mc
}
// SetDownloadHistoryEpisodes sets the "download_history_episodes" field.
func (mc *MediaCreate) SetDownloadHistoryEpisodes(b bool) *MediaCreate {
mc.mutation.SetDownloadHistoryEpisodes(b)
return mc
}
// SetNillableDownloadHistoryEpisodes sets the "download_history_episodes" field if the given value is not nil.
func (mc *MediaCreate) SetNillableDownloadHistoryEpisodes(b *bool) *MediaCreate {
if b != nil {
mc.SetDownloadHistoryEpisodes(*b)
}
return mc
}
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by IDs.
func (mc *MediaCreate) AddEpisodeIDs(ids ...int) *MediaCreate {
mc.mutation.AddEpisodeIDs(ids...)
@@ -203,6 +217,10 @@ func (mc *MediaCreate) defaults() {
v := media.DefaultResolution
mc.mutation.SetResolution(v)
}
if _, ok := mc.mutation.DownloadHistoryEpisodes(); !ok {
v := media.DefaultDownloadHistoryEpisodes
mc.mutation.SetDownloadHistoryEpisodes(v)
}
}
// check runs all checks and user-defined validators on the builder.
@@ -318,6 +336,10 @@ func (mc *MediaCreate) createSpec() (*Media, *sqlgraph.CreateSpec) {
_spec.SetField(media.FieldTargetDir, field.TypeString, value)
_node.TargetDir = value
}
if value, ok := mc.mutation.DownloadHistoryEpisodes(); ok {
_spec.SetField(media.FieldDownloadHistoryEpisodes, field.TypeBool, value)
_node.DownloadHistoryEpisodes = value
}
if nodes := mc.mutation.EpisodesIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,

View File

@@ -229,6 +229,26 @@ func (mu *MediaUpdate) ClearTargetDir() *MediaUpdate {
return mu
}
// SetDownloadHistoryEpisodes sets the "download_history_episodes" field.
func (mu *MediaUpdate) SetDownloadHistoryEpisodes(b bool) *MediaUpdate {
mu.mutation.SetDownloadHistoryEpisodes(b)
return mu
}
// SetNillableDownloadHistoryEpisodes sets the "download_history_episodes" field if the given value is not nil.
func (mu *MediaUpdate) SetNillableDownloadHistoryEpisodes(b *bool) *MediaUpdate {
if b != nil {
mu.SetDownloadHistoryEpisodes(*b)
}
return mu
}
// ClearDownloadHistoryEpisodes clears the value of the "download_history_episodes" field.
func (mu *MediaUpdate) ClearDownloadHistoryEpisodes() *MediaUpdate {
mu.mutation.ClearDownloadHistoryEpisodes()
return mu
}
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by IDs.
func (mu *MediaUpdate) AddEpisodeIDs(ids ...int) *MediaUpdate {
mu.mutation.AddEpisodeIDs(ids...)
@@ -375,6 +395,12 @@ func (mu *MediaUpdate) sqlSave(ctx context.Context) (n int, err error) {
if mu.mutation.TargetDirCleared() {
_spec.ClearField(media.FieldTargetDir, field.TypeString)
}
if value, ok := mu.mutation.DownloadHistoryEpisodes(); ok {
_spec.SetField(media.FieldDownloadHistoryEpisodes, field.TypeBool, value)
}
if mu.mutation.DownloadHistoryEpisodesCleared() {
_spec.ClearField(media.FieldDownloadHistoryEpisodes, field.TypeBool)
}
if mu.mutation.EpisodesCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
@@ -640,6 +666,26 @@ func (muo *MediaUpdateOne) ClearTargetDir() *MediaUpdateOne {
return muo
}
// SetDownloadHistoryEpisodes sets the "download_history_episodes" field.
func (muo *MediaUpdateOne) SetDownloadHistoryEpisodes(b bool) *MediaUpdateOne {
muo.mutation.SetDownloadHistoryEpisodes(b)
return muo
}
// SetNillableDownloadHistoryEpisodes sets the "download_history_episodes" field if the given value is not nil.
func (muo *MediaUpdateOne) SetNillableDownloadHistoryEpisodes(b *bool) *MediaUpdateOne {
if b != nil {
muo.SetDownloadHistoryEpisodes(*b)
}
return muo
}
// ClearDownloadHistoryEpisodes clears the value of the "download_history_episodes" field.
func (muo *MediaUpdateOne) ClearDownloadHistoryEpisodes() *MediaUpdateOne {
muo.mutation.ClearDownloadHistoryEpisodes()
return muo
}
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by IDs.
func (muo *MediaUpdateOne) AddEpisodeIDs(ids ...int) *MediaUpdateOne {
muo.mutation.AddEpisodeIDs(ids...)
@@ -816,6 +862,12 @@ func (muo *MediaUpdateOne) sqlSave(ctx context.Context) (_node *Media, err error
if muo.mutation.TargetDirCleared() {
_spec.ClearField(media.FieldTargetDir, field.TypeString)
}
if value, ok := muo.mutation.DownloadHistoryEpisodes(); ok {
_spec.SetField(media.FieldDownloadHistoryEpisodes, field.TypeBool, value)
}
if muo.mutation.DownloadHistoryEpisodesCleared() {
_spec.ClearField(media.FieldDownloadHistoryEpisodes, field.TypeBool)
}
if muo.mutation.EpisodesCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,

View File

@@ -38,7 +38,6 @@ var (
{Name: "overview", Type: field.TypeString},
{Name: "air_date", Type: field.TypeString},
{Name: "status", Type: field.TypeEnum, Enums: []string{"missing", "downloading", "downloaded"}, Default: "missing"},
{Name: "file_in_storage", Type: field.TypeString, Nullable: true},
{Name: "media_id", Type: field.TypeInt, Nullable: true},
}
// EpisodesTable holds the schema information for the "episodes" table.
@@ -49,7 +48,7 @@ var (
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "episodes_media_episodes",
Columns: []*schema.Column{EpisodesColumns[8]},
Columns: []*schema.Column{EpisodesColumns[7]},
RefColumns: []*schema.Column{MediaColumns[0]},
OnDelete: schema.SetNull,
},
@@ -103,6 +102,7 @@ var (
{Name: "resolution", Type: field.TypeEnum, Enums: []string{"720p", "1080p", "4k"}, Default: "1080p"},
{Name: "storage_id", Type: field.TypeInt, Nullable: true},
{Name: "target_dir", Type: field.TypeString, Nullable: true},
{Name: "download_history_episodes", Type: field.TypeBool, Nullable: true, Default: false},
}
// MediaTable holds the schema information for the "media" table.
MediaTable = &schema.Table{
@@ -110,6 +110,20 @@ var (
Columns: MediaColumns,
PrimaryKey: []*schema.Column{MediaColumns[0]},
}
// NotificationClientsColumns holds the columns for the "notification_clients" table.
NotificationClientsColumns = []*schema.Column{
{Name: "id", Type: field.TypeInt, Increment: true},
{Name: "name", Type: field.TypeString},
{Name: "service", Type: field.TypeString},
{Name: "settings", Type: field.TypeString},
{Name: "enabled", Type: field.TypeBool, Default: true},
}
// NotificationClientsTable holds the schema information for the "notification_clients" table.
NotificationClientsTable = &schema.Table{
Name: "notification_clients",
Columns: NotificationClientsColumns,
PrimaryKey: []*schema.Column{NotificationClientsColumns[0]},
}
// SettingsColumns holds the columns for the "settings" table.
SettingsColumns = []*schema.Column{
{Name: "id", Type: field.TypeInt, Increment: true},
@@ -144,6 +158,7 @@ var (
HistoriesTable,
IndexersTable,
MediaTable,
NotificationClientsTable,
SettingsTable,
StoragesTable,
}

View File

@@ -11,6 +11,7 @@ import (
"polaris/ent/history"
"polaris/ent/indexers"
"polaris/ent/media"
"polaris/ent/notificationclient"
"polaris/ent/predicate"
"polaris/ent/settings"
"polaris/ent/storage"
@@ -30,13 +31,14 @@ const (
OpUpdateOne = ent.OpUpdateOne
// Node types.
TypeDownloadClients = "DownloadClients"
TypeEpisode = "Episode"
TypeHistory = "History"
TypeIndexers = "Indexers"
TypeMedia = "Media"
TypeSettings = "Settings"
TypeStorage = "Storage"
TypeDownloadClients = "DownloadClients"
TypeEpisode = "Episode"
TypeHistory = "History"
TypeIndexers = "Indexers"
TypeMedia = "Media"
TypeNotificationClient = "NotificationClient"
TypeSettings = "Settings"
TypeStorage = "Storage"
)
// DownloadClientsMutation represents an operation that mutates the DownloadClients nodes in the graph.
@@ -919,7 +921,6 @@ type EpisodeMutation struct {
overview *string
air_date *string
status *episode.Status
file_in_storage *string
clearedFields map[string]struct{}
media *int
clearedmedia bool
@@ -1331,55 +1332,6 @@ func (m *EpisodeMutation) ResetStatus() {
m.status = nil
}
// SetFileInStorage sets the "file_in_storage" field.
func (m *EpisodeMutation) SetFileInStorage(s string) {
m.file_in_storage = &s
}
// FileInStorage returns the value of the "file_in_storage" field in the mutation.
func (m *EpisodeMutation) FileInStorage() (r string, exists bool) {
v := m.file_in_storage
if v == nil {
return
}
return *v, true
}
// OldFileInStorage returns the old "file_in_storage" field's value of the Episode entity.
// If the Episode object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *EpisodeMutation) OldFileInStorage(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldFileInStorage is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldFileInStorage requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldFileInStorage: %w", err)
}
return oldValue.FileInStorage, nil
}
// ClearFileInStorage clears the value of the "file_in_storage" field.
func (m *EpisodeMutation) ClearFileInStorage() {
m.file_in_storage = nil
m.clearedFields[episode.FieldFileInStorage] = struct{}{}
}
// FileInStorageCleared returns if the "file_in_storage" field was cleared in this mutation.
func (m *EpisodeMutation) FileInStorageCleared() bool {
_, ok := m.clearedFields[episode.FieldFileInStorage]
return ok
}
// ResetFileInStorage resets all changes to the "file_in_storage" field.
func (m *EpisodeMutation) ResetFileInStorage() {
m.file_in_storage = nil
delete(m.clearedFields, episode.FieldFileInStorage)
}
// ClearMedia clears the "media" edge to the Media entity.
func (m *EpisodeMutation) ClearMedia() {
m.clearedmedia = true
@@ -1441,7 +1393,7 @@ func (m *EpisodeMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *EpisodeMutation) Fields() []string {
fields := make([]string, 0, 8)
fields := make([]string, 0, 7)
if m.media != nil {
fields = append(fields, episode.FieldMediaID)
}
@@ -1463,9 +1415,6 @@ func (m *EpisodeMutation) Fields() []string {
if m.status != nil {
fields = append(fields, episode.FieldStatus)
}
if m.file_in_storage != nil {
fields = append(fields, episode.FieldFileInStorage)
}
return fields
}
@@ -1488,8 +1437,6 @@ func (m *EpisodeMutation) Field(name string) (ent.Value, bool) {
return m.AirDate()
case episode.FieldStatus:
return m.Status()
case episode.FieldFileInStorage:
return m.FileInStorage()
}
return nil, false
}
@@ -1513,8 +1460,6 @@ func (m *EpisodeMutation) OldField(ctx context.Context, name string) (ent.Value,
return m.OldAirDate(ctx)
case episode.FieldStatus:
return m.OldStatus(ctx)
case episode.FieldFileInStorage:
return m.OldFileInStorage(ctx)
}
return nil, fmt.Errorf("unknown Episode field %s", name)
}
@@ -1573,13 +1518,6 @@ func (m *EpisodeMutation) SetField(name string, value ent.Value) error {
}
m.SetStatus(v)
return nil
case episode.FieldFileInStorage:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetFileInStorage(v)
return nil
}
return fmt.Errorf("unknown Episode field %s", name)
}
@@ -1640,9 +1578,6 @@ func (m *EpisodeMutation) ClearedFields() []string {
if m.FieldCleared(episode.FieldMediaID) {
fields = append(fields, episode.FieldMediaID)
}
if m.FieldCleared(episode.FieldFileInStorage) {
fields = append(fields, episode.FieldFileInStorage)
}
return fields
}
@@ -1660,9 +1595,6 @@ func (m *EpisodeMutation) ClearField(name string) error {
case episode.FieldMediaID:
m.ClearMediaID()
return nil
case episode.FieldFileInStorage:
m.ClearFileInStorage()
return nil
}
return fmt.Errorf("unknown Episode nullable field %s", name)
}
@@ -1692,9 +1624,6 @@ func (m *EpisodeMutation) ResetField(name string) error {
case episode.FieldStatus:
m.ResetStatus()
return nil
case episode.FieldFileInStorage:
m.ResetFileInStorage()
return nil
}
return fmt.Errorf("unknown Episode field %s", name)
}
@@ -3202,30 +3131,31 @@ func (m *IndexersMutation) ResetEdge(name string) error {
// MediaMutation represents an operation that mutates the Media nodes in the graph.
type MediaMutation struct {
config
op Op
typ string
id *int
tmdb_id *int
addtmdb_id *int
imdb_id *string
media_type *media.MediaType
name_cn *string
name_en *string
original_name *string
overview *string
created_at *time.Time
air_date *string
resolution *media.Resolution
storage_id *int
addstorage_id *int
target_dir *string
clearedFields map[string]struct{}
episodes map[int]struct{}
removedepisodes map[int]struct{}
clearedepisodes bool
done bool
oldValue func(context.Context) (*Media, error)
predicates []predicate.Media
op Op
typ string
id *int
tmdb_id *int
addtmdb_id *int
imdb_id *string
media_type *media.MediaType
name_cn *string
name_en *string
original_name *string
overview *string
created_at *time.Time
air_date *string
resolution *media.Resolution
storage_id *int
addstorage_id *int
target_dir *string
download_history_episodes *bool
clearedFields map[string]struct{}
episodes map[int]struct{}
removedepisodes map[int]struct{}
clearedepisodes bool
done bool
oldValue func(context.Context) (*Media, error)
predicates []predicate.Media
}
var _ ent.Mutation = (*MediaMutation)(nil)
@@ -3838,6 +3768,55 @@ func (m *MediaMutation) ResetTargetDir() {
delete(m.clearedFields, media.FieldTargetDir)
}
// SetDownloadHistoryEpisodes sets the "download_history_episodes" field.
func (m *MediaMutation) SetDownloadHistoryEpisodes(b bool) {
m.download_history_episodes = &b
}
// DownloadHistoryEpisodes returns the value of the "download_history_episodes" field in the mutation.
func (m *MediaMutation) DownloadHistoryEpisodes() (r bool, exists bool) {
v := m.download_history_episodes
if v == nil {
return
}
return *v, true
}
// OldDownloadHistoryEpisodes returns the old "download_history_episodes" 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) OldDownloadHistoryEpisodes(ctx context.Context) (v bool, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldDownloadHistoryEpisodes is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldDownloadHistoryEpisodes requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldDownloadHistoryEpisodes: %w", err)
}
return oldValue.DownloadHistoryEpisodes, nil
}
// ClearDownloadHistoryEpisodes clears the value of the "download_history_episodes" field.
func (m *MediaMutation) ClearDownloadHistoryEpisodes() {
m.download_history_episodes = nil
m.clearedFields[media.FieldDownloadHistoryEpisodes] = struct{}{}
}
// DownloadHistoryEpisodesCleared returns if the "download_history_episodes" field was cleared in this mutation.
func (m *MediaMutation) DownloadHistoryEpisodesCleared() bool {
_, ok := m.clearedFields[media.FieldDownloadHistoryEpisodes]
return ok
}
// ResetDownloadHistoryEpisodes resets all changes to the "download_history_episodes" field.
func (m *MediaMutation) ResetDownloadHistoryEpisodes() {
m.download_history_episodes = nil
delete(m.clearedFields, media.FieldDownloadHistoryEpisodes)
}
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by ids.
func (m *MediaMutation) AddEpisodeIDs(ids ...int) {
if m.episodes == nil {
@@ -3926,7 +3905,7 @@ func (m *MediaMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *MediaMutation) Fields() []string {
fields := make([]string, 0, 12)
fields := make([]string, 0, 13)
if m.tmdb_id != nil {
fields = append(fields, media.FieldTmdbID)
}
@@ -3963,6 +3942,9 @@ func (m *MediaMutation) Fields() []string {
if m.target_dir != nil {
fields = append(fields, media.FieldTargetDir)
}
if m.download_history_episodes != nil {
fields = append(fields, media.FieldDownloadHistoryEpisodes)
}
return fields
}
@@ -3995,6 +3977,8 @@ func (m *MediaMutation) Field(name string) (ent.Value, bool) {
return m.StorageID()
case media.FieldTargetDir:
return m.TargetDir()
case media.FieldDownloadHistoryEpisodes:
return m.DownloadHistoryEpisodes()
}
return nil, false
}
@@ -4028,6 +4012,8 @@ func (m *MediaMutation) OldField(ctx context.Context, name string) (ent.Value, e
return m.OldStorageID(ctx)
case media.FieldTargetDir:
return m.OldTargetDir(ctx)
case media.FieldDownloadHistoryEpisodes:
return m.OldDownloadHistoryEpisodes(ctx)
}
return nil, fmt.Errorf("unknown Media field %s", name)
}
@@ -4121,6 +4107,13 @@ func (m *MediaMutation) SetField(name string, value ent.Value) error {
}
m.SetTargetDir(v)
return nil
case media.FieldDownloadHistoryEpisodes:
v, ok := value.(bool)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetDownloadHistoryEpisodes(v)
return nil
}
return fmt.Errorf("unknown Media field %s", name)
}
@@ -4187,6 +4180,9 @@ func (m *MediaMutation) ClearedFields() []string {
if m.FieldCleared(media.FieldTargetDir) {
fields = append(fields, media.FieldTargetDir)
}
if m.FieldCleared(media.FieldDownloadHistoryEpisodes) {
fields = append(fields, media.FieldDownloadHistoryEpisodes)
}
return fields
}
@@ -4210,6 +4206,9 @@ func (m *MediaMutation) ClearField(name string) error {
case media.FieldTargetDir:
m.ClearTargetDir()
return nil
case media.FieldDownloadHistoryEpisodes:
m.ClearDownloadHistoryEpisodes()
return nil
}
return fmt.Errorf("unknown Media nullable field %s", name)
}
@@ -4254,6 +4253,9 @@ func (m *MediaMutation) ResetField(name string) error {
case media.FieldTargetDir:
m.ResetTargetDir()
return nil
case media.FieldDownloadHistoryEpisodes:
m.ResetDownloadHistoryEpisodes()
return nil
}
return fmt.Errorf("unknown Media field %s", name)
}
@@ -4342,6 +4344,494 @@ func (m *MediaMutation) ResetEdge(name string) error {
return fmt.Errorf("unknown Media edge %s", name)
}
// NotificationClientMutation represents an operation that mutates the NotificationClient nodes in the graph.
type NotificationClientMutation struct {
config
op Op
typ string
id *int
name *string
service *string
settings *string
enabled *bool
clearedFields map[string]struct{}
done bool
oldValue func(context.Context) (*NotificationClient, error)
predicates []predicate.NotificationClient
}
var _ ent.Mutation = (*NotificationClientMutation)(nil)
// notificationclientOption allows management of the mutation configuration using functional options.
type notificationclientOption func(*NotificationClientMutation)
// newNotificationClientMutation creates new mutation for the NotificationClient entity.
func newNotificationClientMutation(c config, op Op, opts ...notificationclientOption) *NotificationClientMutation {
m := &NotificationClientMutation{
config: c,
op: op,
typ: TypeNotificationClient,
clearedFields: make(map[string]struct{}),
}
for _, opt := range opts {
opt(m)
}
return m
}
// withNotificationClientID sets the ID field of the mutation.
func withNotificationClientID(id int) notificationclientOption {
return func(m *NotificationClientMutation) {
var (
err error
once sync.Once
value *NotificationClient
)
m.oldValue = func(ctx context.Context) (*NotificationClient, error) {
once.Do(func() {
if m.done {
err = errors.New("querying old values post mutation is not allowed")
} else {
value, err = m.Client().NotificationClient.Get(ctx, id)
}
})
return value, err
}
m.id = &id
}
}
// withNotificationClient sets the old NotificationClient of the mutation.
func withNotificationClient(node *NotificationClient) notificationclientOption {
return func(m *NotificationClientMutation) {
m.oldValue = func(context.Context) (*NotificationClient, error) {
return node, nil
}
m.id = &node.ID
}
}
// Client returns a new `ent.Client` from the mutation. If the mutation was
// executed in a transaction (ent.Tx), a transactional client is returned.
func (m NotificationClientMutation) Client() *Client {
client := &Client{config: m.config}
client.init()
return client
}
// Tx returns an `ent.Tx` for mutations that were executed in transactions;
// it returns an error otherwise.
func (m NotificationClientMutation) Tx() (*Tx, error) {
if _, ok := m.driver.(*txDriver); !ok {
return nil, errors.New("ent: mutation is not running in a transaction")
}
tx := &Tx{config: m.config}
tx.init()
return tx, nil
}
// ID returns the ID value in the mutation. Note that the ID is only available
// if it was provided to the builder or after it was returned from the database.
func (m *NotificationClientMutation) ID() (id int, exists bool) {
if m.id == nil {
return
}
return *m.id, true
}
// IDs queries the database and returns the entity ids that match the mutation's predicate.
// That means, if the mutation is applied within a transaction with an isolation level such
// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated
// or updated by the mutation.
func (m *NotificationClientMutation) IDs(ctx context.Context) ([]int, error) {
switch {
case m.op.Is(OpUpdateOne | OpDeleteOne):
id, exists := m.ID()
if exists {
return []int{id}, nil
}
fallthrough
case m.op.Is(OpUpdate | OpDelete):
return m.Client().NotificationClient.Query().Where(m.predicates...).IDs(ctx)
default:
return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op)
}
}
// SetName sets the "name" field.
func (m *NotificationClientMutation) SetName(s string) {
m.name = &s
}
// Name returns the value of the "name" field in the mutation.
func (m *NotificationClientMutation) Name() (r string, exists bool) {
v := m.name
if v == nil {
return
}
return *v, true
}
// OldName returns the old "name" field's value of the NotificationClient entity.
// If the NotificationClient 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 *NotificationClientMutation) OldName(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldName is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldName requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldName: %w", err)
}
return oldValue.Name, nil
}
// ResetName resets all changes to the "name" field.
func (m *NotificationClientMutation) ResetName() {
m.name = nil
}
// SetService sets the "service" field.
func (m *NotificationClientMutation) SetService(s string) {
m.service = &s
}
// Service returns the value of the "service" field in the mutation.
func (m *NotificationClientMutation) Service() (r string, exists bool) {
v := m.service
if v == nil {
return
}
return *v, true
}
// OldService returns the old "service" field's value of the NotificationClient entity.
// If the NotificationClient 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 *NotificationClientMutation) OldService(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldService is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldService requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldService: %w", err)
}
return oldValue.Service, nil
}
// ResetService resets all changes to the "service" field.
func (m *NotificationClientMutation) ResetService() {
m.service = nil
}
// SetSettings sets the "settings" field.
func (m *NotificationClientMutation) SetSettings(s string) {
m.settings = &s
}
// Settings returns the value of the "settings" field in the mutation.
func (m *NotificationClientMutation) Settings() (r string, exists bool) {
v := m.settings
if v == nil {
return
}
return *v, true
}
// OldSettings returns the old "settings" field's value of the NotificationClient entity.
// If the NotificationClient 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 *NotificationClientMutation) OldSettings(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldSettings is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldSettings requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldSettings: %w", err)
}
return oldValue.Settings, nil
}
// ResetSettings resets all changes to the "settings" field.
func (m *NotificationClientMutation) ResetSettings() {
m.settings = nil
}
// SetEnabled sets the "enabled" field.
func (m *NotificationClientMutation) SetEnabled(b bool) {
m.enabled = &b
}
// Enabled returns the value of the "enabled" field in the mutation.
func (m *NotificationClientMutation) Enabled() (r bool, exists bool) {
v := m.enabled
if v == nil {
return
}
return *v, true
}
// OldEnabled returns the old "enabled" field's value of the NotificationClient entity.
// If the NotificationClient 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 *NotificationClientMutation) OldEnabled(ctx context.Context) (v bool, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldEnabled is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldEnabled requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldEnabled: %w", err)
}
return oldValue.Enabled, nil
}
// ResetEnabled resets all changes to the "enabled" field.
func (m *NotificationClientMutation) ResetEnabled() {
m.enabled = nil
}
// Where appends a list predicates to the NotificationClientMutation builder.
func (m *NotificationClientMutation) Where(ps ...predicate.NotificationClient) {
m.predicates = append(m.predicates, ps...)
}
// WhereP appends storage-level predicates to the NotificationClientMutation builder. Using this method,
// users can use type-assertion to append predicates that do not depend on any generated package.
func (m *NotificationClientMutation) WhereP(ps ...func(*sql.Selector)) {
p := make([]predicate.NotificationClient, len(ps))
for i := range ps {
p[i] = ps[i]
}
m.Where(p...)
}
// Op returns the operation name.
func (m *NotificationClientMutation) Op() Op {
return m.op
}
// SetOp allows setting the mutation operation.
func (m *NotificationClientMutation) SetOp(op Op) {
m.op = op
}
// Type returns the node type of this mutation (NotificationClient).
func (m *NotificationClientMutation) Type() string {
return m.typ
}
// Fields returns all fields that were changed during this mutation. Note that in
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *NotificationClientMutation) Fields() []string {
fields := make([]string, 0, 4)
if m.name != nil {
fields = append(fields, notificationclient.FieldName)
}
if m.service != nil {
fields = append(fields, notificationclient.FieldService)
}
if m.settings != nil {
fields = append(fields, notificationclient.FieldSettings)
}
if m.enabled != nil {
fields = append(fields, notificationclient.FieldEnabled)
}
return fields
}
// Field returns the value of a field with the given name. The second boolean
// return value indicates that this field was not set, or was not defined in the
// schema.
func (m *NotificationClientMutation) Field(name string) (ent.Value, bool) {
switch name {
case notificationclient.FieldName:
return m.Name()
case notificationclient.FieldService:
return m.Service()
case notificationclient.FieldSettings:
return m.Settings()
case notificationclient.FieldEnabled:
return m.Enabled()
}
return nil, false
}
// OldField returns the old value of the field from the database. An error is
// returned if the mutation operation is not UpdateOne, or the query to the
// database failed.
func (m *NotificationClientMutation) OldField(ctx context.Context, name string) (ent.Value, error) {
switch name {
case notificationclient.FieldName:
return m.OldName(ctx)
case notificationclient.FieldService:
return m.OldService(ctx)
case notificationclient.FieldSettings:
return m.OldSettings(ctx)
case notificationclient.FieldEnabled:
return m.OldEnabled(ctx)
}
return nil, fmt.Errorf("unknown NotificationClient field %s", name)
}
// SetField sets the value of a field with the given name. It returns an error if
// the field is not defined in the schema, or if the type mismatched the field
// type.
func (m *NotificationClientMutation) SetField(name string, value ent.Value) error {
switch name {
case notificationclient.FieldName:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetName(v)
return nil
case notificationclient.FieldService:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetService(v)
return nil
case notificationclient.FieldSettings:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetSettings(v)
return nil
case notificationclient.FieldEnabled:
v, ok := value.(bool)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetEnabled(v)
return nil
}
return fmt.Errorf("unknown NotificationClient field %s", name)
}
// AddedFields returns all numeric fields that were incremented/decremented during
// this mutation.
func (m *NotificationClientMutation) AddedFields() []string {
return nil
}
// AddedField returns the numeric value that was incremented/decremented on a field
// with the given name. The second boolean return value indicates that this field
// was not set, or was not defined in the schema.
func (m *NotificationClientMutation) AddedField(name string) (ent.Value, bool) {
return nil, false
}
// AddField adds the value to the field with the given name. It returns an error if
// the field is not defined in the schema, or if the type mismatched the field
// type.
func (m *NotificationClientMutation) AddField(name string, value ent.Value) error {
switch name {
}
return fmt.Errorf("unknown NotificationClient numeric field %s", name)
}
// ClearedFields returns all nullable fields that were cleared during this
// mutation.
func (m *NotificationClientMutation) ClearedFields() []string {
return nil
}
// FieldCleared returns a boolean indicating if a field with the given name was
// cleared in this mutation.
func (m *NotificationClientMutation) FieldCleared(name string) bool {
_, ok := m.clearedFields[name]
return ok
}
// ClearField clears the value of the field with the given name. It returns an
// error if the field is not defined in the schema.
func (m *NotificationClientMutation) ClearField(name string) error {
return fmt.Errorf("unknown NotificationClient nullable field %s", name)
}
// ResetField resets all changes in the mutation for the field with the given name.
// It returns an error if the field is not defined in the schema.
func (m *NotificationClientMutation) ResetField(name string) error {
switch name {
case notificationclient.FieldName:
m.ResetName()
return nil
case notificationclient.FieldService:
m.ResetService()
return nil
case notificationclient.FieldSettings:
m.ResetSettings()
return nil
case notificationclient.FieldEnabled:
m.ResetEnabled()
return nil
}
return fmt.Errorf("unknown NotificationClient field %s", name)
}
// AddedEdges returns all edge names that were set/added in this mutation.
func (m *NotificationClientMutation) AddedEdges() []string {
edges := make([]string, 0, 0)
return edges
}
// AddedIDs returns all IDs (to other nodes) that were added for the given edge
// name in this mutation.
func (m *NotificationClientMutation) AddedIDs(name string) []ent.Value {
return nil
}
// RemovedEdges returns all edge names that were removed in this mutation.
func (m *NotificationClientMutation) RemovedEdges() []string {
edges := make([]string, 0, 0)
return edges
}
// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with
// the given name in this mutation.
func (m *NotificationClientMutation) RemovedIDs(name string) []ent.Value {
return nil
}
// ClearedEdges returns all edge names that were cleared in this mutation.
func (m *NotificationClientMutation) ClearedEdges() []string {
edges := make([]string, 0, 0)
return edges
}
// EdgeCleared returns a boolean which indicates if the edge with the given name
// was cleared in this mutation.
func (m *NotificationClientMutation) EdgeCleared(name string) bool {
return false
}
// ClearEdge clears the value of the edge with the given name. It returns an error
// if that edge is not defined in the schema.
func (m *NotificationClientMutation) ClearEdge(name string) error {
return fmt.Errorf("unknown NotificationClient unique edge %s", name)
}
// ResetEdge resets all changes to the edge with the given name in this mutation.
// It returns an error if the edge is not defined in the schema.
func (m *NotificationClientMutation) ResetEdge(name string) error {
return fmt.Errorf("unknown NotificationClient edge %s", name)
}
// SettingsMutation represents an operation that mutates the Settings nodes in the graph.
type SettingsMutation struct {
config

138
ent/notificationclient.go Normal file
View File

@@ -0,0 +1,138 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"fmt"
"polaris/ent/notificationclient"
"strings"
"entgo.io/ent"
"entgo.io/ent/dialect/sql"
)
// NotificationClient is the model entity for the NotificationClient schema.
type NotificationClient struct {
config `json:"-"`
// ID of the ent.
ID int `json:"id,omitempty"`
// Name holds the value of the "name" field.
Name string `json:"name,omitempty"`
// Service holds the value of the "service" field.
Service string `json:"service,omitempty"`
// Settings holds the value of the "settings" field.
Settings string `json:"settings,omitempty"`
// Enabled holds the value of the "enabled" field.
Enabled bool `json:"enabled,omitempty"`
selectValues sql.SelectValues
}
// scanValues returns the types for scanning values from sql.Rows.
func (*NotificationClient) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case notificationclient.FieldEnabled:
values[i] = new(sql.NullBool)
case notificationclient.FieldID:
values[i] = new(sql.NullInt64)
case notificationclient.FieldName, notificationclient.FieldService, notificationclient.FieldSettings:
values[i] = new(sql.NullString)
default:
values[i] = new(sql.UnknownType)
}
}
return values, nil
}
// assignValues assigns the values that were returned from sql.Rows (after scanning)
// to the NotificationClient fields.
func (nc *NotificationClient) assignValues(columns []string, values []any) error {
if m, n := len(values), len(columns); m < n {
return fmt.Errorf("mismatch number of scan values: %d != %d", m, n)
}
for i := range columns {
switch columns[i] {
case notificationclient.FieldID:
value, ok := values[i].(*sql.NullInt64)
if !ok {
return fmt.Errorf("unexpected type %T for field id", value)
}
nc.ID = int(value.Int64)
case notificationclient.FieldName:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field name", values[i])
} else if value.Valid {
nc.Name = value.String
}
case notificationclient.FieldService:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field service", values[i])
} else if value.Valid {
nc.Service = value.String
}
case notificationclient.FieldSettings:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field settings", values[i])
} else if value.Valid {
nc.Settings = value.String
}
case notificationclient.FieldEnabled:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field enabled", values[i])
} else if value.Valid {
nc.Enabled = value.Bool
}
default:
nc.selectValues.Set(columns[i], values[i])
}
}
return nil
}
// Value returns the ent.Value that was dynamically selected and assigned to the NotificationClient.
// This includes values selected through modifiers, order, etc.
func (nc *NotificationClient) Value(name string) (ent.Value, error) {
return nc.selectValues.Get(name)
}
// Update returns a builder for updating this NotificationClient.
// Note that you need to call NotificationClient.Unwrap() before calling this method if this NotificationClient
// was returned from a transaction, and the transaction was committed or rolled back.
func (nc *NotificationClient) Update() *NotificationClientUpdateOne {
return NewNotificationClientClient(nc.config).UpdateOne(nc)
}
// Unwrap unwraps the NotificationClient entity that was returned from a transaction after it was closed,
// so that all future queries will be executed through the driver which created the transaction.
func (nc *NotificationClient) Unwrap() *NotificationClient {
_tx, ok := nc.config.driver.(*txDriver)
if !ok {
panic("ent: NotificationClient is not a transactional entity")
}
nc.config.driver = _tx.drv
return nc
}
// String implements the fmt.Stringer.
func (nc *NotificationClient) String() string {
var builder strings.Builder
builder.WriteString("NotificationClient(")
builder.WriteString(fmt.Sprintf("id=%v, ", nc.ID))
builder.WriteString("name=")
builder.WriteString(nc.Name)
builder.WriteString(", ")
builder.WriteString("service=")
builder.WriteString(nc.Service)
builder.WriteString(", ")
builder.WriteString("settings=")
builder.WriteString(nc.Settings)
builder.WriteString(", ")
builder.WriteString("enabled=")
builder.WriteString(fmt.Sprintf("%v", nc.Enabled))
builder.WriteByte(')')
return builder.String()
}
// NotificationClients is a parsable slice of NotificationClient.
type NotificationClients []*NotificationClient

View File

@@ -0,0 +1,76 @@
// Code generated by ent, DO NOT EDIT.
package notificationclient
import (
"entgo.io/ent/dialect/sql"
)
const (
// Label holds the string label denoting the notificationclient type in the database.
Label = "notification_client"
// FieldID holds the string denoting the id field in the database.
FieldID = "id"
// FieldName holds the string denoting the name field in the database.
FieldName = "name"
// FieldService holds the string denoting the service field in the database.
FieldService = "service"
// FieldSettings holds the string denoting the settings field in the database.
FieldSettings = "settings"
// FieldEnabled holds the string denoting the enabled field in the database.
FieldEnabled = "enabled"
// Table holds the table name of the notificationclient in the database.
Table = "notification_clients"
)
// Columns holds all SQL columns for notificationclient fields.
var Columns = []string{
FieldID,
FieldName,
FieldService,
FieldSettings,
FieldEnabled,
}
// ValidColumn reports if the column name is valid (part of the table columns).
func ValidColumn(column string) bool {
for i := range Columns {
if column == Columns[i] {
return true
}
}
return false
}
var (
// DefaultEnabled holds the default value on creation for the "enabled" field.
DefaultEnabled bool
)
// OrderOption defines the ordering options for the NotificationClient queries.
type OrderOption func(*sql.Selector)
// ByID orders the results by the id field.
func ByID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldID, opts...).ToFunc()
}
// ByName orders the results by the name field.
func ByName(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldName, opts...).ToFunc()
}
// ByService orders the results by the service field.
func ByService(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldService, opts...).ToFunc()
}
// BySettings orders the results by the settings field.
func BySettings(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldSettings, opts...).ToFunc()
}
// ByEnabled orders the results by the enabled field.
func ByEnabled(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldEnabled, opts...).ToFunc()
}

View File

@@ -0,0 +1,294 @@
// Code generated by ent, DO NOT EDIT.
package notificationclient
import (
"polaris/ent/predicate"
"entgo.io/ent/dialect/sql"
)
// ID filters vertices based on their ID field.
func ID(id int) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldEQ(FieldID, id))
}
// IDEQ applies the EQ predicate on the ID field.
func IDEQ(id int) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldEQ(FieldID, id))
}
// IDNEQ applies the NEQ predicate on the ID field.
func IDNEQ(id int) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldNEQ(FieldID, id))
}
// IDIn applies the In predicate on the ID field.
func IDIn(ids ...int) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldIn(FieldID, ids...))
}
// IDNotIn applies the NotIn predicate on the ID field.
func IDNotIn(ids ...int) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldNotIn(FieldID, ids...))
}
// IDGT applies the GT predicate on the ID field.
func IDGT(id int) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldGT(FieldID, id))
}
// IDGTE applies the GTE predicate on the ID field.
func IDGTE(id int) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldGTE(FieldID, id))
}
// IDLT applies the LT predicate on the ID field.
func IDLT(id int) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldLT(FieldID, id))
}
// IDLTE applies the LTE predicate on the ID field.
func IDLTE(id int) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldLTE(FieldID, id))
}
// Name applies equality check predicate on the "name" field. It's identical to NameEQ.
func Name(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldEQ(FieldName, v))
}
// Service applies equality check predicate on the "service" field. It's identical to ServiceEQ.
func Service(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldEQ(FieldService, v))
}
// Settings applies equality check predicate on the "settings" field. It's identical to SettingsEQ.
func Settings(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldEQ(FieldSettings, v))
}
// Enabled applies equality check predicate on the "enabled" field. It's identical to EnabledEQ.
func Enabled(v bool) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldEQ(FieldEnabled, v))
}
// NameEQ applies the EQ predicate on the "name" field.
func NameEQ(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldEQ(FieldName, v))
}
// NameNEQ applies the NEQ predicate on the "name" field.
func NameNEQ(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldNEQ(FieldName, v))
}
// NameIn applies the In predicate on the "name" field.
func NameIn(vs ...string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldIn(FieldName, vs...))
}
// NameNotIn applies the NotIn predicate on the "name" field.
func NameNotIn(vs ...string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldNotIn(FieldName, vs...))
}
// NameGT applies the GT predicate on the "name" field.
func NameGT(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldGT(FieldName, v))
}
// NameGTE applies the GTE predicate on the "name" field.
func NameGTE(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldGTE(FieldName, v))
}
// NameLT applies the LT predicate on the "name" field.
func NameLT(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldLT(FieldName, v))
}
// NameLTE applies the LTE predicate on the "name" field.
func NameLTE(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldLTE(FieldName, v))
}
// NameContains applies the Contains predicate on the "name" field.
func NameContains(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldContains(FieldName, v))
}
// NameHasPrefix applies the HasPrefix predicate on the "name" field.
func NameHasPrefix(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldHasPrefix(FieldName, v))
}
// NameHasSuffix applies the HasSuffix predicate on the "name" field.
func NameHasSuffix(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldHasSuffix(FieldName, v))
}
// NameEqualFold applies the EqualFold predicate on the "name" field.
func NameEqualFold(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldEqualFold(FieldName, v))
}
// NameContainsFold applies the ContainsFold predicate on the "name" field.
func NameContainsFold(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldContainsFold(FieldName, v))
}
// ServiceEQ applies the EQ predicate on the "service" field.
func ServiceEQ(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldEQ(FieldService, v))
}
// ServiceNEQ applies the NEQ predicate on the "service" field.
func ServiceNEQ(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldNEQ(FieldService, v))
}
// ServiceIn applies the In predicate on the "service" field.
func ServiceIn(vs ...string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldIn(FieldService, vs...))
}
// ServiceNotIn applies the NotIn predicate on the "service" field.
func ServiceNotIn(vs ...string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldNotIn(FieldService, vs...))
}
// ServiceGT applies the GT predicate on the "service" field.
func ServiceGT(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldGT(FieldService, v))
}
// ServiceGTE applies the GTE predicate on the "service" field.
func ServiceGTE(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldGTE(FieldService, v))
}
// ServiceLT applies the LT predicate on the "service" field.
func ServiceLT(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldLT(FieldService, v))
}
// ServiceLTE applies the LTE predicate on the "service" field.
func ServiceLTE(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldLTE(FieldService, v))
}
// ServiceContains applies the Contains predicate on the "service" field.
func ServiceContains(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldContains(FieldService, v))
}
// ServiceHasPrefix applies the HasPrefix predicate on the "service" field.
func ServiceHasPrefix(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldHasPrefix(FieldService, v))
}
// ServiceHasSuffix applies the HasSuffix predicate on the "service" field.
func ServiceHasSuffix(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldHasSuffix(FieldService, v))
}
// ServiceEqualFold applies the EqualFold predicate on the "service" field.
func ServiceEqualFold(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldEqualFold(FieldService, v))
}
// ServiceContainsFold applies the ContainsFold predicate on the "service" field.
func ServiceContainsFold(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldContainsFold(FieldService, v))
}
// SettingsEQ applies the EQ predicate on the "settings" field.
func SettingsEQ(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldEQ(FieldSettings, v))
}
// SettingsNEQ applies the NEQ predicate on the "settings" field.
func SettingsNEQ(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldNEQ(FieldSettings, v))
}
// SettingsIn applies the In predicate on the "settings" field.
func SettingsIn(vs ...string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldIn(FieldSettings, vs...))
}
// SettingsNotIn applies the NotIn predicate on the "settings" field.
func SettingsNotIn(vs ...string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldNotIn(FieldSettings, vs...))
}
// SettingsGT applies the GT predicate on the "settings" field.
func SettingsGT(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldGT(FieldSettings, v))
}
// SettingsGTE applies the GTE predicate on the "settings" field.
func SettingsGTE(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldGTE(FieldSettings, v))
}
// SettingsLT applies the LT predicate on the "settings" field.
func SettingsLT(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldLT(FieldSettings, v))
}
// SettingsLTE applies the LTE predicate on the "settings" field.
func SettingsLTE(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldLTE(FieldSettings, v))
}
// SettingsContains applies the Contains predicate on the "settings" field.
func SettingsContains(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldContains(FieldSettings, v))
}
// SettingsHasPrefix applies the HasPrefix predicate on the "settings" field.
func SettingsHasPrefix(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldHasPrefix(FieldSettings, v))
}
// SettingsHasSuffix applies the HasSuffix predicate on the "settings" field.
func SettingsHasSuffix(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldHasSuffix(FieldSettings, v))
}
// SettingsEqualFold applies the EqualFold predicate on the "settings" field.
func SettingsEqualFold(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldEqualFold(FieldSettings, v))
}
// SettingsContainsFold applies the ContainsFold predicate on the "settings" field.
func SettingsContainsFold(v string) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldContainsFold(FieldSettings, v))
}
// EnabledEQ applies the EQ predicate on the "enabled" field.
func EnabledEQ(v bool) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldEQ(FieldEnabled, v))
}
// EnabledNEQ applies the NEQ predicate on the "enabled" field.
func EnabledNEQ(v bool) predicate.NotificationClient {
return predicate.NotificationClient(sql.FieldNEQ(FieldEnabled, v))
}
// And groups predicates with the AND operator between them.
func And(predicates ...predicate.NotificationClient) predicate.NotificationClient {
return predicate.NotificationClient(sql.AndPredicates(predicates...))
}
// Or groups predicates with the OR operator between them.
func Or(predicates ...predicate.NotificationClient) predicate.NotificationClient {
return predicate.NotificationClient(sql.OrPredicates(predicates...))
}
// Not applies the not operator on the given predicate.
func Not(p predicate.NotificationClient) predicate.NotificationClient {
return predicate.NotificationClient(sql.NotPredicates(p))
}

View File

@@ -0,0 +1,240 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"errors"
"fmt"
"polaris/ent/notificationclient"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
)
// NotificationClientCreate is the builder for creating a NotificationClient entity.
type NotificationClientCreate struct {
config
mutation *NotificationClientMutation
hooks []Hook
}
// SetName sets the "name" field.
func (ncc *NotificationClientCreate) SetName(s string) *NotificationClientCreate {
ncc.mutation.SetName(s)
return ncc
}
// SetService sets the "service" field.
func (ncc *NotificationClientCreate) SetService(s string) *NotificationClientCreate {
ncc.mutation.SetService(s)
return ncc
}
// SetSettings sets the "settings" field.
func (ncc *NotificationClientCreate) SetSettings(s string) *NotificationClientCreate {
ncc.mutation.SetSettings(s)
return ncc
}
// SetEnabled sets the "enabled" field.
func (ncc *NotificationClientCreate) SetEnabled(b bool) *NotificationClientCreate {
ncc.mutation.SetEnabled(b)
return ncc
}
// SetNillableEnabled sets the "enabled" field if the given value is not nil.
func (ncc *NotificationClientCreate) SetNillableEnabled(b *bool) *NotificationClientCreate {
if b != nil {
ncc.SetEnabled(*b)
}
return ncc
}
// Mutation returns the NotificationClientMutation object of the builder.
func (ncc *NotificationClientCreate) Mutation() *NotificationClientMutation {
return ncc.mutation
}
// Save creates the NotificationClient in the database.
func (ncc *NotificationClientCreate) Save(ctx context.Context) (*NotificationClient, error) {
ncc.defaults()
return withHooks(ctx, ncc.sqlSave, ncc.mutation, ncc.hooks)
}
// SaveX calls Save and panics if Save returns an error.
func (ncc *NotificationClientCreate) SaveX(ctx context.Context) *NotificationClient {
v, err := ncc.Save(ctx)
if err != nil {
panic(err)
}
return v
}
// Exec executes the query.
func (ncc *NotificationClientCreate) Exec(ctx context.Context) error {
_, err := ncc.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
func (ncc *NotificationClientCreate) ExecX(ctx context.Context) {
if err := ncc.Exec(ctx); err != nil {
panic(err)
}
}
// defaults sets the default values of the builder before save.
func (ncc *NotificationClientCreate) defaults() {
if _, ok := ncc.mutation.Enabled(); !ok {
v := notificationclient.DefaultEnabled
ncc.mutation.SetEnabled(v)
}
}
// check runs all checks and user-defined validators on the builder.
func (ncc *NotificationClientCreate) check() error {
if _, ok := ncc.mutation.Name(); !ok {
return &ValidationError{Name: "name", err: errors.New(`ent: missing required field "NotificationClient.name"`)}
}
if _, ok := ncc.mutation.Service(); !ok {
return &ValidationError{Name: "service", err: errors.New(`ent: missing required field "NotificationClient.service"`)}
}
if _, ok := ncc.mutation.Settings(); !ok {
return &ValidationError{Name: "settings", err: errors.New(`ent: missing required field "NotificationClient.settings"`)}
}
if _, ok := ncc.mutation.Enabled(); !ok {
return &ValidationError{Name: "enabled", err: errors.New(`ent: missing required field "NotificationClient.enabled"`)}
}
return nil
}
func (ncc *NotificationClientCreate) sqlSave(ctx context.Context) (*NotificationClient, error) {
if err := ncc.check(); err != nil {
return nil, err
}
_node, _spec := ncc.createSpec()
if err := sqlgraph.CreateNode(ctx, ncc.driver, _spec); err != nil {
if sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
return nil, err
}
id := _spec.ID.Value.(int64)
_node.ID = int(id)
ncc.mutation.id = &_node.ID
ncc.mutation.done = true
return _node, nil
}
func (ncc *NotificationClientCreate) createSpec() (*NotificationClient, *sqlgraph.CreateSpec) {
var (
_node = &NotificationClient{config: ncc.config}
_spec = sqlgraph.NewCreateSpec(notificationclient.Table, sqlgraph.NewFieldSpec(notificationclient.FieldID, field.TypeInt))
)
if value, ok := ncc.mutation.Name(); ok {
_spec.SetField(notificationclient.FieldName, field.TypeString, value)
_node.Name = value
}
if value, ok := ncc.mutation.Service(); ok {
_spec.SetField(notificationclient.FieldService, field.TypeString, value)
_node.Service = value
}
if value, ok := ncc.mutation.Settings(); ok {
_spec.SetField(notificationclient.FieldSettings, field.TypeString, value)
_node.Settings = value
}
if value, ok := ncc.mutation.Enabled(); ok {
_spec.SetField(notificationclient.FieldEnabled, field.TypeBool, value)
_node.Enabled = value
}
return _node, _spec
}
// NotificationClientCreateBulk is the builder for creating many NotificationClient entities in bulk.
type NotificationClientCreateBulk struct {
config
err error
builders []*NotificationClientCreate
}
// Save creates the NotificationClient entities in the database.
func (nccb *NotificationClientCreateBulk) Save(ctx context.Context) ([]*NotificationClient, error) {
if nccb.err != nil {
return nil, nccb.err
}
specs := make([]*sqlgraph.CreateSpec, len(nccb.builders))
nodes := make([]*NotificationClient, len(nccb.builders))
mutators := make([]Mutator, len(nccb.builders))
for i := range nccb.builders {
func(i int, root context.Context) {
builder := nccb.builders[i]
builder.defaults()
var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
mutation, ok := m.(*NotificationClientMutation)
if !ok {
return nil, fmt.Errorf("unexpected mutation type %T", m)
}
if err := builder.check(); err != nil {
return nil, err
}
builder.mutation = mutation
var err error
nodes[i], specs[i] = builder.createSpec()
if i < len(mutators)-1 {
_, err = mutators[i+1].Mutate(root, nccb.builders[i+1].mutation)
} else {
spec := &sqlgraph.BatchCreateSpec{Nodes: specs}
// Invoke the actual operation on the latest mutation in the chain.
if err = sqlgraph.BatchCreate(ctx, nccb.driver, spec); err != nil {
if sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
}
}
if err != nil {
return nil, err
}
mutation.id = &nodes[i].ID
if specs[i].ID.Value != nil {
id := specs[i].ID.Value.(int64)
nodes[i].ID = int(id)
}
mutation.done = true
return nodes[i], nil
})
for i := len(builder.hooks) - 1; i >= 0; i-- {
mut = builder.hooks[i](mut)
}
mutators[i] = mut
}(i, ctx)
}
if len(mutators) > 0 {
if _, err := mutators[0].Mutate(ctx, nccb.builders[0].mutation); err != nil {
return nil, err
}
}
return nodes, nil
}
// SaveX is like Save, but panics if an error occurs.
func (nccb *NotificationClientCreateBulk) SaveX(ctx context.Context) []*NotificationClient {
v, err := nccb.Save(ctx)
if err != nil {
panic(err)
}
return v
}
// Exec executes the query.
func (nccb *NotificationClientCreateBulk) Exec(ctx context.Context) error {
_, err := nccb.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
func (nccb *NotificationClientCreateBulk) ExecX(ctx context.Context) {
if err := nccb.Exec(ctx); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,88 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"polaris/ent/notificationclient"
"polaris/ent/predicate"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
)
// NotificationClientDelete is the builder for deleting a NotificationClient entity.
type NotificationClientDelete struct {
config
hooks []Hook
mutation *NotificationClientMutation
}
// Where appends a list predicates to the NotificationClientDelete builder.
func (ncd *NotificationClientDelete) Where(ps ...predicate.NotificationClient) *NotificationClientDelete {
ncd.mutation.Where(ps...)
return ncd
}
// Exec executes the deletion query and returns how many vertices were deleted.
func (ncd *NotificationClientDelete) Exec(ctx context.Context) (int, error) {
return withHooks(ctx, ncd.sqlExec, ncd.mutation, ncd.hooks)
}
// ExecX is like Exec, but panics if an error occurs.
func (ncd *NotificationClientDelete) ExecX(ctx context.Context) int {
n, err := ncd.Exec(ctx)
if err != nil {
panic(err)
}
return n
}
func (ncd *NotificationClientDelete) sqlExec(ctx context.Context) (int, error) {
_spec := sqlgraph.NewDeleteSpec(notificationclient.Table, sqlgraph.NewFieldSpec(notificationclient.FieldID, field.TypeInt))
if ps := ncd.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
affected, err := sqlgraph.DeleteNodes(ctx, ncd.driver, _spec)
if err != nil && sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
ncd.mutation.done = true
return affected, err
}
// NotificationClientDeleteOne is the builder for deleting a single NotificationClient entity.
type NotificationClientDeleteOne struct {
ncd *NotificationClientDelete
}
// Where appends a list predicates to the NotificationClientDelete builder.
func (ncdo *NotificationClientDeleteOne) Where(ps ...predicate.NotificationClient) *NotificationClientDeleteOne {
ncdo.ncd.mutation.Where(ps...)
return ncdo
}
// Exec executes the deletion query.
func (ncdo *NotificationClientDeleteOne) Exec(ctx context.Context) error {
n, err := ncdo.ncd.Exec(ctx)
switch {
case err != nil:
return err
case n == 0:
return &NotFoundError{notificationclient.Label}
default:
return nil
}
}
// ExecX is like Exec, but panics if an error occurs.
func (ncdo *NotificationClientDeleteOne) ExecX(ctx context.Context) {
if err := ncdo.Exec(ctx); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,526 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"fmt"
"math"
"polaris/ent/notificationclient"
"polaris/ent/predicate"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
)
// NotificationClientQuery is the builder for querying NotificationClient entities.
type NotificationClientQuery struct {
config
ctx *QueryContext
order []notificationclient.OrderOption
inters []Interceptor
predicates []predicate.NotificationClient
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
}
// Where adds a new predicate for the NotificationClientQuery builder.
func (ncq *NotificationClientQuery) Where(ps ...predicate.NotificationClient) *NotificationClientQuery {
ncq.predicates = append(ncq.predicates, ps...)
return ncq
}
// Limit the number of records to be returned by this query.
func (ncq *NotificationClientQuery) Limit(limit int) *NotificationClientQuery {
ncq.ctx.Limit = &limit
return ncq
}
// Offset to start from.
func (ncq *NotificationClientQuery) Offset(offset int) *NotificationClientQuery {
ncq.ctx.Offset = &offset
return ncq
}
// Unique configures the query builder to filter duplicate records on query.
// By default, unique is set to true, and can be disabled using this method.
func (ncq *NotificationClientQuery) Unique(unique bool) *NotificationClientQuery {
ncq.ctx.Unique = &unique
return ncq
}
// Order specifies how the records should be ordered.
func (ncq *NotificationClientQuery) Order(o ...notificationclient.OrderOption) *NotificationClientQuery {
ncq.order = append(ncq.order, o...)
return ncq
}
// First returns the first NotificationClient entity from the query.
// Returns a *NotFoundError when no NotificationClient was found.
func (ncq *NotificationClientQuery) First(ctx context.Context) (*NotificationClient, error) {
nodes, err := ncq.Limit(1).All(setContextOp(ctx, ncq.ctx, "First"))
if err != nil {
return nil, err
}
if len(nodes) == 0 {
return nil, &NotFoundError{notificationclient.Label}
}
return nodes[0], nil
}
// FirstX is like First, but panics if an error occurs.
func (ncq *NotificationClientQuery) FirstX(ctx context.Context) *NotificationClient {
node, err := ncq.First(ctx)
if err != nil && !IsNotFound(err) {
panic(err)
}
return node
}
// FirstID returns the first NotificationClient ID from the query.
// Returns a *NotFoundError when no NotificationClient ID was found.
func (ncq *NotificationClientQuery) FirstID(ctx context.Context) (id int, err error) {
var ids []int
if ids, err = ncq.Limit(1).IDs(setContextOp(ctx, ncq.ctx, "FirstID")); err != nil {
return
}
if len(ids) == 0 {
err = &NotFoundError{notificationclient.Label}
return
}
return ids[0], nil
}
// FirstIDX is like FirstID, but panics if an error occurs.
func (ncq *NotificationClientQuery) FirstIDX(ctx context.Context) int {
id, err := ncq.FirstID(ctx)
if err != nil && !IsNotFound(err) {
panic(err)
}
return id
}
// Only returns a single NotificationClient entity found by the query, ensuring it only returns one.
// Returns a *NotSingularError when more than one NotificationClient entity is found.
// Returns a *NotFoundError when no NotificationClient entities are found.
func (ncq *NotificationClientQuery) Only(ctx context.Context) (*NotificationClient, error) {
nodes, err := ncq.Limit(2).All(setContextOp(ctx, ncq.ctx, "Only"))
if err != nil {
return nil, err
}
switch len(nodes) {
case 1:
return nodes[0], nil
case 0:
return nil, &NotFoundError{notificationclient.Label}
default:
return nil, &NotSingularError{notificationclient.Label}
}
}
// OnlyX is like Only, but panics if an error occurs.
func (ncq *NotificationClientQuery) OnlyX(ctx context.Context) *NotificationClient {
node, err := ncq.Only(ctx)
if err != nil {
panic(err)
}
return node
}
// OnlyID is like Only, but returns the only NotificationClient ID in the query.
// Returns a *NotSingularError when more than one NotificationClient ID is found.
// Returns a *NotFoundError when no entities are found.
func (ncq *NotificationClientQuery) OnlyID(ctx context.Context) (id int, err error) {
var ids []int
if ids, err = ncq.Limit(2).IDs(setContextOp(ctx, ncq.ctx, "OnlyID")); err != nil {
return
}
switch len(ids) {
case 1:
id = ids[0]
case 0:
err = &NotFoundError{notificationclient.Label}
default:
err = &NotSingularError{notificationclient.Label}
}
return
}
// OnlyIDX is like OnlyID, but panics if an error occurs.
func (ncq *NotificationClientQuery) OnlyIDX(ctx context.Context) int {
id, err := ncq.OnlyID(ctx)
if err != nil {
panic(err)
}
return id
}
// All executes the query and returns a list of NotificationClients.
func (ncq *NotificationClientQuery) All(ctx context.Context) ([]*NotificationClient, error) {
ctx = setContextOp(ctx, ncq.ctx, "All")
if err := ncq.prepareQuery(ctx); err != nil {
return nil, err
}
qr := querierAll[[]*NotificationClient, *NotificationClientQuery]()
return withInterceptors[[]*NotificationClient](ctx, ncq, qr, ncq.inters)
}
// AllX is like All, but panics if an error occurs.
func (ncq *NotificationClientQuery) AllX(ctx context.Context) []*NotificationClient {
nodes, err := ncq.All(ctx)
if err != nil {
panic(err)
}
return nodes
}
// IDs executes the query and returns a list of NotificationClient IDs.
func (ncq *NotificationClientQuery) IDs(ctx context.Context) (ids []int, err error) {
if ncq.ctx.Unique == nil && ncq.path != nil {
ncq.Unique(true)
}
ctx = setContextOp(ctx, ncq.ctx, "IDs")
if err = ncq.Select(notificationclient.FieldID).Scan(ctx, &ids); err != nil {
return nil, err
}
return ids, nil
}
// IDsX is like IDs, but panics if an error occurs.
func (ncq *NotificationClientQuery) IDsX(ctx context.Context) []int {
ids, err := ncq.IDs(ctx)
if err != nil {
panic(err)
}
return ids
}
// Count returns the count of the given query.
func (ncq *NotificationClientQuery) Count(ctx context.Context) (int, error) {
ctx = setContextOp(ctx, ncq.ctx, "Count")
if err := ncq.prepareQuery(ctx); err != nil {
return 0, err
}
return withInterceptors[int](ctx, ncq, querierCount[*NotificationClientQuery](), ncq.inters)
}
// CountX is like Count, but panics if an error occurs.
func (ncq *NotificationClientQuery) CountX(ctx context.Context) int {
count, err := ncq.Count(ctx)
if err != nil {
panic(err)
}
return count
}
// Exist returns true if the query has elements in the graph.
func (ncq *NotificationClientQuery) Exist(ctx context.Context) (bool, error) {
ctx = setContextOp(ctx, ncq.ctx, "Exist")
switch _, err := ncq.FirstID(ctx); {
case IsNotFound(err):
return false, nil
case err != nil:
return false, fmt.Errorf("ent: check existence: %w", err)
default:
return true, nil
}
}
// ExistX is like Exist, but panics if an error occurs.
func (ncq *NotificationClientQuery) ExistX(ctx context.Context) bool {
exist, err := ncq.Exist(ctx)
if err != nil {
panic(err)
}
return exist
}
// Clone returns a duplicate of the NotificationClientQuery builder, including all associated steps. It can be
// used to prepare common query builders and use them differently after the clone is made.
func (ncq *NotificationClientQuery) Clone() *NotificationClientQuery {
if ncq == nil {
return nil
}
return &NotificationClientQuery{
config: ncq.config,
ctx: ncq.ctx.Clone(),
order: append([]notificationclient.OrderOption{}, ncq.order...),
inters: append([]Interceptor{}, ncq.inters...),
predicates: append([]predicate.NotificationClient{}, ncq.predicates...),
// clone intermediate query.
sql: ncq.sql.Clone(),
path: ncq.path,
}
}
// GroupBy is used to group vertices by one or more fields/columns.
// It is often used with aggregate functions, like: count, max, mean, min, sum.
//
// Example:
//
// var v []struct {
// Name string `json:"name,omitempty"`
// Count int `json:"count,omitempty"`
// }
//
// client.NotificationClient.Query().
// GroupBy(notificationclient.FieldName).
// Aggregate(ent.Count()).
// Scan(ctx, &v)
func (ncq *NotificationClientQuery) GroupBy(field string, fields ...string) *NotificationClientGroupBy {
ncq.ctx.Fields = append([]string{field}, fields...)
grbuild := &NotificationClientGroupBy{build: ncq}
grbuild.flds = &ncq.ctx.Fields
grbuild.label = notificationclient.Label
grbuild.scan = grbuild.Scan
return grbuild
}
// Select allows the selection one or more fields/columns for the given query,
// instead of selecting all fields in the entity.
//
// Example:
//
// var v []struct {
// Name string `json:"name,omitempty"`
// }
//
// client.NotificationClient.Query().
// Select(notificationclient.FieldName).
// Scan(ctx, &v)
func (ncq *NotificationClientQuery) Select(fields ...string) *NotificationClientSelect {
ncq.ctx.Fields = append(ncq.ctx.Fields, fields...)
sbuild := &NotificationClientSelect{NotificationClientQuery: ncq}
sbuild.label = notificationclient.Label
sbuild.flds, sbuild.scan = &ncq.ctx.Fields, sbuild.Scan
return sbuild
}
// Aggregate returns a NotificationClientSelect configured with the given aggregations.
func (ncq *NotificationClientQuery) Aggregate(fns ...AggregateFunc) *NotificationClientSelect {
return ncq.Select().Aggregate(fns...)
}
func (ncq *NotificationClientQuery) prepareQuery(ctx context.Context) error {
for _, inter := range ncq.inters {
if inter == nil {
return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)")
}
if trv, ok := inter.(Traverser); ok {
if err := trv.Traverse(ctx, ncq); err != nil {
return err
}
}
}
for _, f := range ncq.ctx.Fields {
if !notificationclient.ValidColumn(f) {
return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
}
}
if ncq.path != nil {
prev, err := ncq.path(ctx)
if err != nil {
return err
}
ncq.sql = prev
}
return nil
}
func (ncq *NotificationClientQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*NotificationClient, error) {
var (
nodes = []*NotificationClient{}
_spec = ncq.querySpec()
)
_spec.ScanValues = func(columns []string) ([]any, error) {
return (*NotificationClient).scanValues(nil, columns)
}
_spec.Assign = func(columns []string, values []any) error {
node := &NotificationClient{config: ncq.config}
nodes = append(nodes, node)
return node.assignValues(columns, values)
}
for i := range hooks {
hooks[i](ctx, _spec)
}
if err := sqlgraph.QueryNodes(ctx, ncq.driver, _spec); err != nil {
return nil, err
}
if len(nodes) == 0 {
return nodes, nil
}
return nodes, nil
}
func (ncq *NotificationClientQuery) sqlCount(ctx context.Context) (int, error) {
_spec := ncq.querySpec()
_spec.Node.Columns = ncq.ctx.Fields
if len(ncq.ctx.Fields) > 0 {
_spec.Unique = ncq.ctx.Unique != nil && *ncq.ctx.Unique
}
return sqlgraph.CountNodes(ctx, ncq.driver, _spec)
}
func (ncq *NotificationClientQuery) querySpec() *sqlgraph.QuerySpec {
_spec := sqlgraph.NewQuerySpec(notificationclient.Table, notificationclient.Columns, sqlgraph.NewFieldSpec(notificationclient.FieldID, field.TypeInt))
_spec.From = ncq.sql
if unique := ncq.ctx.Unique; unique != nil {
_spec.Unique = *unique
} else if ncq.path != nil {
_spec.Unique = true
}
if fields := ncq.ctx.Fields; len(fields) > 0 {
_spec.Node.Columns = make([]string, 0, len(fields))
_spec.Node.Columns = append(_spec.Node.Columns, notificationclient.FieldID)
for i := range fields {
if fields[i] != notificationclient.FieldID {
_spec.Node.Columns = append(_spec.Node.Columns, fields[i])
}
}
}
if ps := ncq.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
if limit := ncq.ctx.Limit; limit != nil {
_spec.Limit = *limit
}
if offset := ncq.ctx.Offset; offset != nil {
_spec.Offset = *offset
}
if ps := ncq.order; len(ps) > 0 {
_spec.Order = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
return _spec
}
func (ncq *NotificationClientQuery) sqlQuery(ctx context.Context) *sql.Selector {
builder := sql.Dialect(ncq.driver.Dialect())
t1 := builder.Table(notificationclient.Table)
columns := ncq.ctx.Fields
if len(columns) == 0 {
columns = notificationclient.Columns
}
selector := builder.Select(t1.Columns(columns...)...).From(t1)
if ncq.sql != nil {
selector = ncq.sql
selector.Select(selector.Columns(columns...)...)
}
if ncq.ctx.Unique != nil && *ncq.ctx.Unique {
selector.Distinct()
}
for _, p := range ncq.predicates {
p(selector)
}
for _, p := range ncq.order {
p(selector)
}
if offset := ncq.ctx.Offset; offset != nil {
// limit is mandatory for offset clause. We start
// with default value, and override it below if needed.
selector.Offset(*offset).Limit(math.MaxInt32)
}
if limit := ncq.ctx.Limit; limit != nil {
selector.Limit(*limit)
}
return selector
}
// NotificationClientGroupBy is the group-by builder for NotificationClient entities.
type NotificationClientGroupBy struct {
selector
build *NotificationClientQuery
}
// Aggregate adds the given aggregation functions to the group-by query.
func (ncgb *NotificationClientGroupBy) Aggregate(fns ...AggregateFunc) *NotificationClientGroupBy {
ncgb.fns = append(ncgb.fns, fns...)
return ncgb
}
// Scan applies the selector query and scans the result into the given value.
func (ncgb *NotificationClientGroupBy) Scan(ctx context.Context, v any) error {
ctx = setContextOp(ctx, ncgb.build.ctx, "GroupBy")
if err := ncgb.build.prepareQuery(ctx); err != nil {
return err
}
return scanWithInterceptors[*NotificationClientQuery, *NotificationClientGroupBy](ctx, ncgb.build, ncgb, ncgb.build.inters, v)
}
func (ncgb *NotificationClientGroupBy) sqlScan(ctx context.Context, root *NotificationClientQuery, v any) error {
selector := root.sqlQuery(ctx).Select()
aggregation := make([]string, 0, len(ncgb.fns))
for _, fn := range ncgb.fns {
aggregation = append(aggregation, fn(selector))
}
if len(selector.SelectedColumns()) == 0 {
columns := make([]string, 0, len(*ncgb.flds)+len(ncgb.fns))
for _, f := range *ncgb.flds {
columns = append(columns, selector.C(f))
}
columns = append(columns, aggregation...)
selector.Select(columns...)
}
selector.GroupBy(selector.Columns(*ncgb.flds...)...)
if err := selector.Err(); err != nil {
return err
}
rows := &sql.Rows{}
query, args := selector.Query()
if err := ncgb.build.driver.Query(ctx, query, args, rows); err != nil {
return err
}
defer rows.Close()
return sql.ScanSlice(rows, v)
}
// NotificationClientSelect is the builder for selecting fields of NotificationClient entities.
type NotificationClientSelect struct {
*NotificationClientQuery
selector
}
// Aggregate adds the given aggregation functions to the selector query.
func (ncs *NotificationClientSelect) Aggregate(fns ...AggregateFunc) *NotificationClientSelect {
ncs.fns = append(ncs.fns, fns...)
return ncs
}
// Scan applies the selector query and scans the result into the given value.
func (ncs *NotificationClientSelect) Scan(ctx context.Context, v any) error {
ctx = setContextOp(ctx, ncs.ctx, "Select")
if err := ncs.prepareQuery(ctx); err != nil {
return err
}
return scanWithInterceptors[*NotificationClientQuery, *NotificationClientSelect](ctx, ncs.NotificationClientQuery, ncs, ncs.inters, v)
}
func (ncs *NotificationClientSelect) sqlScan(ctx context.Context, root *NotificationClientQuery, v any) error {
selector := root.sqlQuery(ctx)
aggregation := make([]string, 0, len(ncs.fns))
for _, fn := range ncs.fns {
aggregation = append(aggregation, fn(selector))
}
switch n := len(*ncs.selector.flds); {
case n == 0 && len(aggregation) > 0:
selector.Select(aggregation...)
case n != 0 && len(aggregation) > 0:
selector.AppendSelect(aggregation...)
}
rows := &sql.Rows{}
query, args := selector.Query()
if err := ncs.driver.Query(ctx, query, args, rows); err != nil {
return err
}
defer rows.Close()
return sql.ScanSlice(rows, v)
}

View File

@@ -0,0 +1,311 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"errors"
"fmt"
"polaris/ent/notificationclient"
"polaris/ent/predicate"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
)
// NotificationClientUpdate is the builder for updating NotificationClient entities.
type NotificationClientUpdate struct {
config
hooks []Hook
mutation *NotificationClientMutation
}
// Where appends a list predicates to the NotificationClientUpdate builder.
func (ncu *NotificationClientUpdate) Where(ps ...predicate.NotificationClient) *NotificationClientUpdate {
ncu.mutation.Where(ps...)
return ncu
}
// SetName sets the "name" field.
func (ncu *NotificationClientUpdate) SetName(s string) *NotificationClientUpdate {
ncu.mutation.SetName(s)
return ncu
}
// SetNillableName sets the "name" field if the given value is not nil.
func (ncu *NotificationClientUpdate) SetNillableName(s *string) *NotificationClientUpdate {
if s != nil {
ncu.SetName(*s)
}
return ncu
}
// SetService sets the "service" field.
func (ncu *NotificationClientUpdate) SetService(s string) *NotificationClientUpdate {
ncu.mutation.SetService(s)
return ncu
}
// SetNillableService sets the "service" field if the given value is not nil.
func (ncu *NotificationClientUpdate) SetNillableService(s *string) *NotificationClientUpdate {
if s != nil {
ncu.SetService(*s)
}
return ncu
}
// SetSettings sets the "settings" field.
func (ncu *NotificationClientUpdate) SetSettings(s string) *NotificationClientUpdate {
ncu.mutation.SetSettings(s)
return ncu
}
// SetNillableSettings sets the "settings" field if the given value is not nil.
func (ncu *NotificationClientUpdate) SetNillableSettings(s *string) *NotificationClientUpdate {
if s != nil {
ncu.SetSettings(*s)
}
return ncu
}
// SetEnabled sets the "enabled" field.
func (ncu *NotificationClientUpdate) SetEnabled(b bool) *NotificationClientUpdate {
ncu.mutation.SetEnabled(b)
return ncu
}
// SetNillableEnabled sets the "enabled" field if the given value is not nil.
func (ncu *NotificationClientUpdate) SetNillableEnabled(b *bool) *NotificationClientUpdate {
if b != nil {
ncu.SetEnabled(*b)
}
return ncu
}
// Mutation returns the NotificationClientMutation object of the builder.
func (ncu *NotificationClientUpdate) Mutation() *NotificationClientMutation {
return ncu.mutation
}
// Save executes the query and returns the number of nodes affected by the update operation.
func (ncu *NotificationClientUpdate) Save(ctx context.Context) (int, error) {
return withHooks(ctx, ncu.sqlSave, ncu.mutation, ncu.hooks)
}
// SaveX is like Save, but panics if an error occurs.
func (ncu *NotificationClientUpdate) SaveX(ctx context.Context) int {
affected, err := ncu.Save(ctx)
if err != nil {
panic(err)
}
return affected
}
// Exec executes the query.
func (ncu *NotificationClientUpdate) Exec(ctx context.Context) error {
_, err := ncu.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
func (ncu *NotificationClientUpdate) ExecX(ctx context.Context) {
if err := ncu.Exec(ctx); err != nil {
panic(err)
}
}
func (ncu *NotificationClientUpdate) sqlSave(ctx context.Context) (n int, err error) {
_spec := sqlgraph.NewUpdateSpec(notificationclient.Table, notificationclient.Columns, sqlgraph.NewFieldSpec(notificationclient.FieldID, field.TypeInt))
if ps := ncu.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
if value, ok := ncu.mutation.Name(); ok {
_spec.SetField(notificationclient.FieldName, field.TypeString, value)
}
if value, ok := ncu.mutation.Service(); ok {
_spec.SetField(notificationclient.FieldService, field.TypeString, value)
}
if value, ok := ncu.mutation.Settings(); ok {
_spec.SetField(notificationclient.FieldSettings, field.TypeString, value)
}
if value, ok := ncu.mutation.Enabled(); ok {
_spec.SetField(notificationclient.FieldEnabled, field.TypeBool, value)
}
if n, err = sqlgraph.UpdateNodes(ctx, ncu.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{notificationclient.Label}
} else if sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
return 0, err
}
ncu.mutation.done = true
return n, nil
}
// NotificationClientUpdateOne is the builder for updating a single NotificationClient entity.
type NotificationClientUpdateOne struct {
config
fields []string
hooks []Hook
mutation *NotificationClientMutation
}
// SetName sets the "name" field.
func (ncuo *NotificationClientUpdateOne) SetName(s string) *NotificationClientUpdateOne {
ncuo.mutation.SetName(s)
return ncuo
}
// SetNillableName sets the "name" field if the given value is not nil.
func (ncuo *NotificationClientUpdateOne) SetNillableName(s *string) *NotificationClientUpdateOne {
if s != nil {
ncuo.SetName(*s)
}
return ncuo
}
// SetService sets the "service" field.
func (ncuo *NotificationClientUpdateOne) SetService(s string) *NotificationClientUpdateOne {
ncuo.mutation.SetService(s)
return ncuo
}
// SetNillableService sets the "service" field if the given value is not nil.
func (ncuo *NotificationClientUpdateOne) SetNillableService(s *string) *NotificationClientUpdateOne {
if s != nil {
ncuo.SetService(*s)
}
return ncuo
}
// SetSettings sets the "settings" field.
func (ncuo *NotificationClientUpdateOne) SetSettings(s string) *NotificationClientUpdateOne {
ncuo.mutation.SetSettings(s)
return ncuo
}
// SetNillableSettings sets the "settings" field if the given value is not nil.
func (ncuo *NotificationClientUpdateOne) SetNillableSettings(s *string) *NotificationClientUpdateOne {
if s != nil {
ncuo.SetSettings(*s)
}
return ncuo
}
// SetEnabled sets the "enabled" field.
func (ncuo *NotificationClientUpdateOne) SetEnabled(b bool) *NotificationClientUpdateOne {
ncuo.mutation.SetEnabled(b)
return ncuo
}
// SetNillableEnabled sets the "enabled" field if the given value is not nil.
func (ncuo *NotificationClientUpdateOne) SetNillableEnabled(b *bool) *NotificationClientUpdateOne {
if b != nil {
ncuo.SetEnabled(*b)
}
return ncuo
}
// Mutation returns the NotificationClientMutation object of the builder.
func (ncuo *NotificationClientUpdateOne) Mutation() *NotificationClientMutation {
return ncuo.mutation
}
// Where appends a list predicates to the NotificationClientUpdate builder.
func (ncuo *NotificationClientUpdateOne) Where(ps ...predicate.NotificationClient) *NotificationClientUpdateOne {
ncuo.mutation.Where(ps...)
return ncuo
}
// Select allows selecting one or more fields (columns) of the returned entity.
// The default is selecting all fields defined in the entity schema.
func (ncuo *NotificationClientUpdateOne) Select(field string, fields ...string) *NotificationClientUpdateOne {
ncuo.fields = append([]string{field}, fields...)
return ncuo
}
// Save executes the query and returns the updated NotificationClient entity.
func (ncuo *NotificationClientUpdateOne) Save(ctx context.Context) (*NotificationClient, error) {
return withHooks(ctx, ncuo.sqlSave, ncuo.mutation, ncuo.hooks)
}
// SaveX is like Save, but panics if an error occurs.
func (ncuo *NotificationClientUpdateOne) SaveX(ctx context.Context) *NotificationClient {
node, err := ncuo.Save(ctx)
if err != nil {
panic(err)
}
return node
}
// Exec executes the query on the entity.
func (ncuo *NotificationClientUpdateOne) Exec(ctx context.Context) error {
_, err := ncuo.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
func (ncuo *NotificationClientUpdateOne) ExecX(ctx context.Context) {
if err := ncuo.Exec(ctx); err != nil {
panic(err)
}
}
func (ncuo *NotificationClientUpdateOne) sqlSave(ctx context.Context) (_node *NotificationClient, err error) {
_spec := sqlgraph.NewUpdateSpec(notificationclient.Table, notificationclient.Columns, sqlgraph.NewFieldSpec(notificationclient.FieldID, field.TypeInt))
id, ok := ncuo.mutation.ID()
if !ok {
return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "NotificationClient.id" for update`)}
}
_spec.Node.ID.Value = id
if fields := ncuo.fields; len(fields) > 0 {
_spec.Node.Columns = make([]string, 0, len(fields))
_spec.Node.Columns = append(_spec.Node.Columns, notificationclient.FieldID)
for _, f := range fields {
if !notificationclient.ValidColumn(f) {
return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
}
if f != notificationclient.FieldID {
_spec.Node.Columns = append(_spec.Node.Columns, f)
}
}
}
if ps := ncuo.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
if value, ok := ncuo.mutation.Name(); ok {
_spec.SetField(notificationclient.FieldName, field.TypeString, value)
}
if value, ok := ncuo.mutation.Service(); ok {
_spec.SetField(notificationclient.FieldService, field.TypeString, value)
}
if value, ok := ncuo.mutation.Settings(); ok {
_spec.SetField(notificationclient.FieldSettings, field.TypeString, value)
}
if value, ok := ncuo.mutation.Enabled(); ok {
_spec.SetField(notificationclient.FieldEnabled, field.TypeBool, value)
}
_node = &NotificationClient{config: ncuo.config}
_spec.Assign = _node.assignValues
_spec.ScanValues = _node.scanValues
if err = sqlgraph.UpdateNode(ctx, ncuo.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{notificationclient.Label}
} else if sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
return nil, err
}
ncuo.mutation.done = true
return _node, nil
}

View File

@@ -21,6 +21,9 @@ type Indexers func(*sql.Selector)
// Media is the predicate function for media builders.
type Media func(*sql.Selector)
// NotificationClient is the predicate function for notificationclient builders.
type NotificationClient func(*sql.Selector)
// Settings is the predicate function for settings builders.
type Settings func(*sql.Selector)

View File

@@ -7,6 +7,7 @@ import (
"polaris/ent/history"
"polaris/ent/indexers"
"polaris/ent/media"
"polaris/ent/notificationclient"
"polaris/ent/schema"
"polaris/ent/storage"
"time"
@@ -70,6 +71,16 @@ func init() {
mediaDescAirDate := mediaFields[8].Descriptor()
// media.DefaultAirDate holds the default value on creation for the air_date field.
media.DefaultAirDate = mediaDescAirDate.Default.(string)
// mediaDescDownloadHistoryEpisodes is the schema descriptor for download_history_episodes field.
mediaDescDownloadHistoryEpisodes := mediaFields[12].Descriptor()
// media.DefaultDownloadHistoryEpisodes holds the default value on creation for the download_history_episodes field.
media.DefaultDownloadHistoryEpisodes = mediaDescDownloadHistoryEpisodes.Default.(bool)
notificationclientFields := schema.NotificationClient{}.Fields()
_ = notificationclientFields
// notificationclientDescEnabled is the schema descriptor for enabled field.
notificationclientDescEnabled := notificationclientFields[3].Descriptor()
// notificationclient.DefaultEnabled holds the default value on creation for the enabled field.
notificationclient.DefaultEnabled = notificationclientDescEnabled.Default.(bool)
storageFields := schema.Storage{}.Fields()
_ = storageFields
// storageDescDeleted is the schema descriptor for deleted field.

View File

@@ -21,7 +21,6 @@ func (Episode) Fields() []ent.Field {
field.String("overview"),
field.String("air_date"),
field.Enum("status").Values("missing", "downloading", "downloaded").Default("missing"),
field.String("file_in_storage").Optional(),
}
}

View File

@@ -28,6 +28,7 @@ func (Media) Fields() []ent.Field {
field.Enum("resolution").Values("720p", "1080p", "4k").Default("1080p"),
field.Int("storage_id").Optional(),
field.String("target_dir").Optional(),
field.Bool("download_history_episodes").Optional().Default(false).Comment("tv series only"),
}
}

View File

@@ -0,0 +1,26 @@
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// NotificationClient holds the schema definition for the NotificationClient entity.
type NotificationClient struct {
ent.Schema
}
// Fields of the NotificationClient.
func (NotificationClient) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.String("service"),
field.String("settings"),
field.Bool("enabled").Default(true),
}
}
// Edges of the NotificationClient.
func (NotificationClient) Edges() []ent.Edge {
return nil
}

View File

@@ -22,6 +22,8 @@ type Tx struct {
Indexers *IndexersClient
// Media is the client for interacting with the Media builders.
Media *MediaClient
// NotificationClient is the client for interacting with the NotificationClient builders.
NotificationClient *NotificationClientClient
// Settings is the client for interacting with the Settings builders.
Settings *SettingsClient
// Storage is the client for interacting with the Storage builders.
@@ -162,6 +164,7 @@ func (tx *Tx) init() {
tx.History = NewHistoryClient(tx.config)
tx.Indexers = NewIndexersClient(tx.config)
tx.Media = NewMediaClient(tx.config)
tx.NotificationClient = NewNotificationClientClient(tx.config)
tx.Settings = NewSettingsClient(tx.config)
tx.Storage = NewStorageClient(tx.config)
}

38
go.mod
View File

@@ -8,10 +8,29 @@ require (
github.com/mattn/go-sqlite3 v1.14.16
github.com/robfig/cron v1.2.0
go.uber.org/zap v1.27.0
golang.org/x/net v0.25.0
golang.org/x/net v0.27.0
)
require github.com/adrg/strutil v0.3.1
require (
github.com/adrg/strutil v0.3.1
github.com/gin-contrib/zap v1.1.3
github.com/nikoksr/notify v1.0.0
)
require (
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/blinkbean/dingtalk v1.1.3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
github.com/gregdel/pushover v1.3.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
golang.org/x/sync v0.7.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
require (
ariga.io/atlas v0.19.1-0.20240203083654-5948b60a8e43 // indirect
@@ -31,7 +50,7 @@ require (
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/hcl/v2 v2.13.0 // indirect
@@ -55,12 +74,12 @@ require (
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/zclconf/go-cty v1.8.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sys v0.21.0
golang.org/x/crypto v0.25.0
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
golang.org/x/mod v0.19.0 // indirect
golang.org/x/sys v0.22.0
golang.org/x/text v0.16.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
@@ -72,7 +91,8 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/pkg/errors v0.9.1
github.com/spf13/viper v1.19.0
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
)

63
go.sum
View File

@@ -2,6 +2,8 @@ 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=
entgo.io/ent v0.13.1 h1:uD8QwN1h6SNphdCCzmkMN3feSUzNnVvV/WIkHKMbzOE=
entgo.io/ent v0.13.1/go.mod h1:qCEmo+biw3ccBn9OyL4ZK5dfpwg++l1Gxwac5B1206A=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/adrg/strutil v0.3.1 h1:OLvSS7CSJO8lBii4YmBt8jiK9QOtB9CzCzwl4Ic/Fz4=
@@ -10,6 +12,8 @@ github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tj
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/blinkbean/dingtalk v1.1.3 h1:MbidFZYom7DTFHD/YIs+eaI7kRy52kmWE/sy0xjo6E4=
github.com/blinkbean/dingtalk v1.1.3/go.mod h1:9BaLuGSBqY3vT5hstValh48DbsKO7vaHaJnG9pXwbto=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
@@ -34,6 +38,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-contrib/static v1.1.2 h1:c3kT4bFkUJn2aoRU3s6XnMjJT8J6nNWJkR0NglqmlZ4=
github.com/gin-contrib/static v1.1.2/go.mod h1:Fw90ozjHCmZBWbgrsqrDvO28YbhKEKzKp8GixhR4yLw=
github.com/gin-contrib/zap v1.1.3 h1:9e/U9fYd4/OBfmSEBs5hHZq114uACn7bpuzvCkcJySA=
github.com/gin-contrib/zap v1.1.3/go.mod h1:+BD/6NYZKJyUpqVoJEvgeq9GLz8pINEQvak9LHNOTSE=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
@@ -46,6 +52,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
@@ -58,8 +66,10 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gregdel/pushover v1.3.1 h1:4bMLITOZ15+Zpi6qqoGqOPuVHCwSUvMCgVnN5Xhilfo=
github.com/gregdel/pushover v1.3.1/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@@ -91,6 +101,8 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
@@ -102,6 +114,12 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
github.com/nikoksr/notify v1.0.0 h1:qe9/6FRsWdxBgQgWcpvQ0sv8LRGJZDpRB4TkL2uNdO8=
github.com/nikoksr/notify v1.0.0/go.mod h1:hPaaDt30d6LAA7/5nb0e48Bp/MctDfycCSs8VEgN29I=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -125,6 +143,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
@@ -132,6 +152,7 @@ github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -143,6 +164,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
@@ -153,44 +176,52 @@ github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA=
github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,18 +1,62 @@
package log
import (
"os"
"path/filepath"
"strings"
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var sugar *zap.SugaredLogger
var atom zap.AtomicLevel
const dataPath = "./data"
func init() {
config := zap.NewDevelopmentConfig()
config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
config.DisableStacktrace = true
logger, _ := config.Build(zap.AddCallerSkip(1))
atom = zap.NewAtomicLevel()
atom.SetLevel(zap.DebugLevel)
w := zapcore.Lock(os.Stdout)
if os.Getenv("GIN_MODE") == "release" {
w = zapcore.AddSync(&lumberjack.Logger{
Filename: filepath.Join(dataPath, "logs", "polaris.log"),
MaxSize: 50, // megabytes
MaxBackups: 3,
MaxAge: 30, // days
})
}
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
logger := zap.New(zapcore.NewCore(consoleEncoder, w, atom), zap.AddCallerSkip(1))
sugar = logger.Sugar()
}
func SetLogLevel(l string) {
switch strings.TrimSpace(strings.ToLower(l)) {
case "debug":
atom.SetLevel(zap.DebugLevel)
Debug("set log level to debug")
case "info":
atom.SetLevel(zap.InfoLevel)
Info("set log level to info")
case "warn", "warning":
atom.SetLevel(zap.WarnLevel)
Warn("set log level to warning")
case "error":
atom.SetLevel(zap.ErrorLevel)
Error("set log level to error")
}
}
func Logger() *zap.SugaredLogger {
return sugar
}
func Info(args ...interface{}) {

View File

@@ -112,25 +112,25 @@ func parseEnglishName(name string) *Metadata {
}
} 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
// 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 {

90
pkg/notifier/clients.go Normal file
View File

@@ -0,0 +1,90 @@
package notifier
import (
"encoding/json"
"github.com/nikoksr/notify/service/bark"
"github.com/nikoksr/notify/service/dingding"
po "github.com/nikoksr/notify/service/pushover"
"github.com/nikoksr/notify/service/telegram"
"github.com/pkg/errors"
)
type PushoverConfig struct {
UserKey string `json:"user_key"`
GroupKey string `json:"group_key"`
AppToken string `json:"app_token"`
}
func NewPushoverClient(s string) (NotificationClient, error) {
var cfg PushoverConfig
if err := json.Unmarshal([]byte(s), &cfg); err != nil {
return nil, errors.Wrap(err, "json")
}
c := po.New(cfg.AppToken)
if cfg.UserKey != "" {
c.AddReceivers(cfg.UserKey)
}
if cfg.GroupKey != "" {
c.AddReceivers(cfg.GroupKey)
}
return &Notifier{service: c}, nil
}
type DingTalkConfig struct {
Token string `json:"token"`
Secret string `json:"secret"`
}
func NewDingTalkClient(s string) (NotificationClient, error) {
var cfg DingTalkConfig
if err := json.Unmarshal([]byte(s), &cfg); err != nil {
return nil, errors.Wrap(err, "json")
}
svc := dingding.New(&dingding.Config{
Token: cfg.Token,
Secret: cfg.Secret,
})
return &Notifier{service: svc}, nil
}
type TelegramConfig struct {
Token string `json:"token"`
ChatID int64 `json:"chat_id"`
}
func NewTelegramClient(s string) (NotificationClient, error) {
var cfg TelegramConfig
if err := json.Unmarshal([]byte(s), &cfg); err != nil {
return nil, errors.Wrap(err, "json")
}
svc, err := telegram.New(cfg.Token)
if err != nil {
panic(err)
}
svc.AddReceivers(cfg.ChatID)
return &Notifier{service: svc}, nil
}
type BarkConfig struct {
DeviceKey string `json:"device_key"`
URL string `json:"url"`
}
func NewbarkClient(s string) (NotificationClient, error) {
var cfg BarkConfig
if err := json.Unmarshal([]byte(s), &cfg); err != nil {
return nil, errors.Wrap(err, "json")
}
url := cfg.URL
if url == "" {
url = bark.DefaultServerURL
}
b := bark.NewWithServers(cfg.DeviceKey, url)
return &Notifier{service: b}, nil
}

37
pkg/notifier/doc.go Normal file
View File

@@ -0,0 +1,37 @@
package notifier
import (
"context"
"polaris/pkg/utils"
"github.com/nikoksr/notify"
)
type HandlerFunc func(string) (NotificationClient, error)
type NotificationClient interface {
SendMsg(msg string) error
}
type Notifier struct {
service notify.Notifier
}
func (s *Notifier) SendMsg(msg string) error {
notifier := notify.New()
notifier.UseServices(s.service)
return notifier.Send(context.TODO(), "Polaris", msg)
}
var handler = utils.Map[string, HandlerFunc]{}
func init() {
handler.Store("pushover", NewPushoverClient)
handler.Store("dingtalk", NewDingTalkClient)
handler.Store("telegram", NewTelegramClient)
handler.Store("bark", NewbarkClient)
}
func Gethandler(name string) (HandlerFunc, bool) {
return handler.Load(name)
}

View File

@@ -0,0 +1,8 @@
package message
const (
BeginDownload = "开始下载:%v"
DownloadComplete = "下载完成:%v"
ProcessingComplete = "文件处理完成:%v"
ProcessingFailed = "文件处理失败:%v"
)

View File

@@ -28,7 +28,7 @@ type LocalStorage struct {
func (l *LocalStorage) Move(src, dest string) error {
targetDir := filepath.Join(l.dir, dest)
os.MkdirAll(filepath.Dir(targetDir), 0655)
os.MkdirAll(filepath.Dir(targetDir), os.ModePerm)
err := filepath.Walk(src, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
@@ -40,13 +40,13 @@ func (l *LocalStorage) Move(src, dest string) error {
destName := filepath.Join(targetDir, rel)
if info.IsDir() {
os.Mkdir(destName, 0655)
os.Mkdir(destName, os.ModePerm)
} else { //is file
if writer, err := os.Create(destName); err != nil {
return errors.Wrapf(err, "create file %s", destName)
} else {
defer writer.Close()
if f, err := os.OpenFile(path, os.O_RDONLY, 0666); err != nil {
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()

View File

@@ -45,6 +45,22 @@ type Client struct {
cfg Config
}
func (c *Client) GetAll() ([]*Torrent, error) {
all, err := c.c.TorrentGetAll(context.TODO())
if err != nil {
return nil, errors.Wrap(err, "get all")
}
var torrents []*Torrent
for _, t := range all {
torrents = append(torrents, &Torrent{
ID: *t.ID,
c: c.c,
Config: c.cfg,
})
}
return torrents, nil
}
func (c *Client) Download(link, dir string) (*Torrent, error) {
if strings.HasPrefix(link, "http") {
client := &http.Client{

13
pkg/uptime/uptime.go Normal file
View File

@@ -0,0 +1,13 @@
package uptime
import "time"
var startTime time.Time
func Uptime() time.Duration {
return time.Since(startTime)
}
func init() {
startTime = time.Now()
}

31
pkg/utils/map.go Normal file
View File

@@ -0,0 +1,31 @@
package utils
import "sync"
type Map[K comparable, V any] struct {
m sync.Map
}
func (m *Map[K, V]) Delete(key K) { m.m.Delete(key) }
func (m *Map[K, V]) Load(key K) (value V, ok bool) {
v, ok := m.m.Load(key)
if !ok {
return value, ok
}
return v.(V), ok
}
func (m *Map[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
v, loaded := m.m.LoadAndDelete(key)
if !loaded {
return value, loaded
}
return v.(V), loaded
}
func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
a, loaded := m.m.LoadOrStore(key, value)
return a.(V), loaded
}
func (m *Map[K, V]) Range(f func(key K, value V) bool) {
m.m.Range(func(key, value any) bool { return f(key.(K), value.(V)) })
}
func (m *Map[K, V]) Store(key K, value V) { m.m.Store(key, value) }

View File

@@ -93,3 +93,33 @@ func (s *Server) GetMediaDownloadHistory(c *gin.Context) (interface{}, error) {
}
return his, nil
}
type TorrentInfo struct {
Name string `json:"name"`
ID int64 `json:"id"`
SeedRatio float32 `json:"seed_ratio"`
Progress int `json:"progress"`
}
func (s *Server) GetAllTorrents(c *gin.Context) (interface{}, error) {
trc, err := s.getDownloadClient()
if err != nil {
return nil, errors.Wrap(err, "connect transmission")
}
all, err := trc.GetAll()
if err != nil {
return nil, errors.Wrap(err, "get all")
}
var infos []TorrentInfo
for _, t := range all {
if !t.Exists() {
continue
}
infos = append(infos, TorrentInfo{
Name: t.Name(),
ID: t.ID,
Progress: t.Progress(),
})
}
return infos, nil
}

View File

@@ -5,7 +5,6 @@ import (
"polaris/db"
"polaris/log"
"polaris/pkg/utils"
"strings"
"time"
"github.com/gin-gonic/gin"
@@ -23,16 +22,15 @@ func (s *Server) authModdleware(c *gin.Context) {
c.Next()
return
}
auth := c.GetHeader("Authorization")
if auth == "" {
log.Infof("token is not present, abort")
token, err := c.Cookie("token")
if err != nil {
log.Errorf("token error: %v", err)
c.AbortWithStatus(http.StatusForbidden)
return
}
auth = strings.TrimPrefix(auth, "Bearer ")
//log.Infof("current token: %v", auth)
token, err := jwt.ParseWithClaims(auth, &jwt.RegisteredClaims{}, func(t *jwt.Token) (interface{}, error) {
//log.Debugf("current token: %v", auth)
tokenParsed, err := jwt.ParseWithClaims(token, &jwt.RegisteredClaims{}, func(t *jwt.Token) (interface{}, error) {
return []byte(s.jwtSerect), nil
})
if err != nil {
@@ -40,15 +38,15 @@ func (s *Server) authModdleware(c *gin.Context) {
c.AbortWithStatus(http.StatusForbidden)
return
}
if !token.Valid {
log.Errorf("token is not valid: %v", auth)
if !tokenParsed.Valid {
log.Errorf("token is not valid: %v", token)
c.AbortWithStatus(http.StatusForbidden)
return
}
claim := token.Claims.(*jwt.RegisteredClaims)
claim := tokenParsed.Claims.(*jwt.RegisteredClaims)
if time.Until(claim.ExpiresAt.Time) <= 0 {
log.Infof("token is no longer valid: %s", auth)
log.Infof("token is no longer valid: %s", token)
c.AbortWithStatus(http.StatusForbidden)
return
}
@@ -61,7 +59,6 @@ type LoginIn struct {
Password string `json:"password"`
}
func (s *Server) Login(c *gin.Context) (interface{}, error) {
var in LoginIn
@@ -93,11 +90,23 @@ func (s *Server) Login(c *gin.Context) (interface{}, error) {
if err != nil {
return nil, errors.Wrap(err, "sign")
}
c.SetSameSite(http.SameSiteNoneMode)
c.SetCookie("token", sig, 0, "/", "", true, false)
return gin.H{
"token": sig,
}, nil
}
func (s *Server) Logout(c *gin.Context) (interface{}, error) {
if !s.isAuthEnabled() {
return nil, errors.New( "auth is not enabled")
}
c.SetSameSite(http.SameSiteNoneMode)
c.SetCookie("token", "", -1, "/", "", true, false)
return nil, nil
}
type EnableAuthIn struct {
Enable bool `json:"enable"`
User string `json:"user"`

View File

@@ -19,7 +19,7 @@ func HttpHandler(f func(*gin.Context) (interface{}, error)) gin.HandlerFunc {
})
return
}
//log.Infof("url %v return: %+v", ctx.Request.URL, r)
log.Debug("url %v return: %+v", ctx.Request.URL, r)
ctx.JSON(200, Response{
Code: 0,

View File

@@ -28,10 +28,10 @@ func isNumberedSeries(detail *db.MediaDetails) bool {
if ep.EpisodeNumber == 1 {
season2HasEpisode1 = true
}
}
}
return hasSeason2 && !season2HasEpisode1//only one 1st episode
return hasSeason2 && !season2HasEpisode1 //only one 1st episode
}
func SearchEpisode(db1 *db.Client, seriesId, seasonNum, episodeNum int, checkResolution bool) ([]torznab.Result, error) {
@@ -54,7 +54,7 @@ func SearchEpisode(db1 *db.Client, seriesId, seasonNum, episodeNum int, checkRes
if !isNumberedSeries(series) { //do not check season on series that only rely on episode number
if meta.Season != seasonNum {
continue
}
}
}
if isNumberedSeries(series) && episodeNum == -1 {
//should not want season
@@ -63,7 +63,7 @@ func SearchEpisode(db1 *db.Client, seriesId, seasonNum, episodeNum int, checkRes
if episodeNum != -1 && meta.Episode != episodeNum { //not season pack, episode number equals
continue
} else if seasonNum == -1 && !meta.IsSeasonPack { //want season pack, but not season pack
}else if episodeNum == -1 && !meta.IsSeasonPack { //want season pack, but not season pack
continue
}
if checkResolution && meta.Resolution != series.Resolution.String() {
@@ -131,7 +131,7 @@ func searchWithTorznab(db *db.Client, q string) []torznab.Result {
for _, tor := range allTorznab {
wg.Add(1)
go func () {
go func() {
log.Debugf("search torznab %v with %v", tor.Name, q)
defer wg.Done()
resp, err := torznab.Search(tor.URL, tor.ApiKey, q)
@@ -140,7 +140,7 @@ func searchWithTorznab(db *db.Client, q string) []torznab.Result {
return
}
resChan <- resp
}()
}
go func() {

75
server/notify_client.go Normal file
View File

@@ -0,0 +1,75 @@
package server
import (
"polaris/ent"
"polaris/log"
"polaris/pkg/notifier"
"strconv"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)
func (s *Server) GetAllNotificationClients(c *gin.Context) (interface{}, error) {
return s.db.GetAllNotificationClients()
}
func (s *Server) GetNotificationClient(c *gin.Context) (interface{}, error) {
ids := c.Param("id")
id, err := strconv.Atoi(ids)
if err != nil {
return nil, errors.Wrap(err, "convert")
}
return s.db.GetNotificationClient(id)
}
func (s *Server) DeleteNotificationClient(c *gin.Context) (interface{}, error) {
ids := c.Param("id")
id, err := strconv.Atoi(ids)
if err != nil {
return nil, errors.Wrap(err, "convert")
}
return nil, s.db.DeleteNotificationClient(id)
}
func (s *Server) AddNotificationClient(c *gin.Context) (interface{}, error) {
var in ent.NotificationClient
if err := c.ShouldBindJSON(&in); err != nil {
return nil, errors.Wrap(err, "json")
}
err := s.db.AddNotificationClient(in.Name, in.Service, in.Settings, in.Enabled)
if err != nil {
return nil, errors.Wrap(err, "save db")
}
return nil, nil
}
func (s *Server) sendMsg(msg string) {
clients, err := s.db.GetAllNotificationClients2()
if err != nil {
log.Errorf("query notification clients: %v", err)
return
}
for _, cl := range clients {
if !cl.Enabled {
continue
}
handler, ok := notifier.Gethandler(cl.Service)
if !ok {
log.Errorf("no notification implementation of service %s", cl.Service)
continue
}
noCl, err := handler(cl.Settings)
if err != nil {
log.Errorf("handle setting for name %s error: %v", cl.Name, err)
continue
}
err = noCl.SendMsg(msg)
if err != nil {
log.Errorf("send message error: %v", err)
continue
}
log.Debugf("send message to %s success, msg is %s", cl.Name, msg)
}
}

View File

@@ -5,20 +5,18 @@ import (
"polaris/ent"
"polaris/ent/episode"
"polaris/ent/history"
"polaris/ent/media"
"polaris/log"
"polaris/pkg/notifier/message"
"polaris/pkg/torznab"
"polaris/pkg/utils"
"polaris/server/core"
"strconv"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)
func (s *Server) searchAndDownloadSeasonPackage(seriesId, seasonNum int) (*string, error) {
trc, err := s.getDownloadClient()
if err != nil {
return nil, errors.Wrap(err, "connect transmission")
}
res, err := core.SearchSeasonPackage(s.db, seriesId, seasonNum, true)
if err != nil {
@@ -27,7 +25,15 @@ func (s *Server) searchAndDownloadSeasonPackage(seriesId, seasonNum int) (*strin
r1 := res[0]
log.Infof("found resource to download: %+v", r1)
return s.downloadSeasonPackage(r1, seriesId, seasonNum)
}
func (s *Server) downloadSeasonPackage(r1 torznab.Result, seriesId, seasonNum int) (*string, error) {
trc, err := s.getDownloadClient()
if err != nil {
return nil, errors.Wrap(err, "connect transmission")
}
downloadDir := s.db.GetDownloadDir()
size := utils.AvailableSpace(downloadDir)
if size < uint64(r1.Size) {
@@ -62,10 +68,13 @@ func (s *Server) searchAndDownloadSeasonPackage(seriesId, seasonNum int) (*strin
s.db.SetSeasonAllEpisodeStatus(seriesId, seasonNum, episode.StatusDownloading)
s.tasks[history.ID] = &Task{Torrent: torrent}
s.sendMsg(fmt.Sprintf(message.BeginDownload, r1.Name))
return &r1.Name, nil
}
func (s *Server) searchAndDownload(seriesId, seasonNum, episodeNum int) (*string, error) {
func (s *Server) downloadEpisodeTorrent(r1 torznab.Result, seriesId, seasonNum, episodeNum int) (*string, error) {
trc, err := s.getDownloadClient()
if err != nil {
return nil, errors.Wrap(err, "connect transmission")
@@ -83,13 +92,6 @@ func (s *Server) searchAndDownload(seriesId, seasonNum, episodeNum int) (*string
if ep == nil {
return nil, errors.Errorf("no episode of season %d episode %d", seasonNum, episodeNum)
}
res, err := core.SearchEpisode(s.db, seriesId, seasonNum, episodeNum, true)
if err != nil {
return nil, err
}
r1 := res[0]
log.Infof("found resource to download: %+v", r1)
torrent, err := trc.Download(r1.Link, s.db.GetDownloadDir())
if err != nil {
return nil, errors.Wrap(err, "downloading")
@@ -113,9 +115,21 @@ func (s *Server) searchAndDownload(seriesId, seasonNum, episodeNum int) (*string
s.db.SetEpisodeStatus(ep.ID, episode.StatusDownloading)
s.tasks[history.ID] = &Task{Torrent: torrent}
s.sendMsg(fmt.Sprintf(message.BeginDownload, r1.Name))
log.Infof("success add %s to download task", r1.Name)
return &r1.Name, nil
}
func (s *Server) searchAndDownload(seriesId, seasonNum, episodeNum int) (*string, error) {
res, err := core.SearchEpisode(s.db, seriesId, seasonNum, episodeNum, true)
if err != nil {
return nil, err
}
r1 := res[0]
log.Infof("found resource to download: %+v", r1)
return s.downloadEpisodeTorrent(r1, seriesId, seasonNum, episodeNum)
}
type searchAndDownloadIn struct {
@@ -124,15 +138,46 @@ type searchAndDownloadIn struct {
Episode int `json:"episode"`
}
func (s *Server) SearchAvailableEpisodeResource(c *gin.Context) (interface{}, error) {
func (s *Server) SearchAvailableTorrents(c *gin.Context) (interface{}, error) {
var in searchAndDownloadIn
if err := c.ShouldBindJSON(&in); err != nil {
return nil, errors.Wrap(err, "bind json")
}
log.Infof("search episode resources link: %v", in)
res, err := core.SearchEpisode(s.db, in.ID, in.Season, in.Episode, true)
m, err := s.db.GetMedia(in.ID)
if err != nil {
return nil, errors.Wrap(err, "search episode")
return nil, errors.Wrap(err, "get media")
}
log.Infof("search torrents resources link: %+v", in)
var res []torznab.Result
if m.MediaType == media.MediaTypeTv {
if in.Episode == 0 {
//search season package
log.Infof("search series season package S%02d", in.Season)
res, err = core.SearchSeasonPackage(s.db, in.ID, in.Season, false)
if err != nil {
return nil, errors.Wrap(err, "search season package")
}
} else {
log.Infof("search series episode S%02dE%02d", in.Season, in.Episode)
res, err = core.SearchEpisode(s.db, in.ID, in.Season, in.Episode, false)
if err != nil {
if err.Error() == "no resource found" {
return []TorznabSearchResult{}, nil
}
return nil, errors.Wrap(err, "search episode")
}
}
} else {
log.Info("search movie %d", in.ID)
res, err = core.SearchMovie(s.db, in.ID, false)
if err != nil {
if err.Error() == "no resource found" {
return []TorznabSearchResult{}, nil
}
return nil, err
}
}
var searchResults []TorznabSearchResult
for _, r := range res {
@@ -144,9 +189,6 @@ func (s *Server) SearchAvailableEpisodeResource(c *gin.Context) (interface{}, er
Link: r.Link,
})
}
if len(searchResults) == 0 {
return nil, errors.New("no resource found")
}
return searchResults, nil
}
@@ -185,64 +227,47 @@ type TorznabSearchResult struct {
Link string `json:"link"`
Seeders int `json:"seeders"`
Peers int `json:"peers"`
Source string `json:"source"`
}
func (s *Server) SearchAvailableMovies(c *gin.Context) (interface{}, error) {
ids := c.Param("id")
id, err := strconv.Atoi(ids)
if err != nil {
return nil, errors.Wrap(err, "convert")
}
movieDetail := s.db.GetMediaDetails(id)
if movieDetail == nil {
return nil, errors.New("no media found of id " + ids)
}
res, err := core.SearchMovie(s.db, id, false)
if err != nil {
if err.Error() == "no resource found" {
return []TorznabSearchResult{}, nil
}
return nil, err
}
var searchResults []TorznabSearchResult
for _, r := range res {
searchResults = append(searchResults, TorznabSearchResult{
Name: r.Name,
Size: r.Size,
Seeders: r.Seeders,
Peers: r.Peers,
Link: r.Link,
})
}
if len(searchResults) == 0 {
return []TorznabSearchResult{}, nil
}
return searchResults, nil
}
type downloadTorrentIn struct {
MediaID int `json:"media_id" binding:"required"`
MediaID int `json:"id" binding:"required"`
Season int `json:"season"`
Episode int `json:"episode"`
TorznabSearchResult
}
func (s *Server) DownloadMovieTorrent(c *gin.Context) (interface{}, error) {
func (s *Server) DownloadTorrent(c *gin.Context) (interface{}, error) {
var in downloadTorrentIn
if err := c.ShouldBindJSON(&in); err != nil {
return nil, errors.Wrap(err, "bind json")
}
log.Infof("download torrent input: %+v", in)
m, err := s.db.GetMedia(in.MediaID)
if err != nil {
return nil, fmt.Errorf("no tv series of id %v", in.MediaID)
}
if m.MediaType == media.MediaTypeTv {
if in.Episode == 0 {
//download season package
name := in.Name
if name == "" {
name = fmt.Sprintf("%v S%02d", m.OriginalName, in.Season)
}
res := torznab.Result{Name: name, Link: in.Link, Size: in.Size}
return s.downloadSeasonPackage(res, in.MediaID, in.Season)
}
name := in.Name
if name == "" {
name = fmt.Sprintf("%v S%02dE%02d", m.OriginalName, in.Season, in.Episode)
}
res := torznab.Result{Name: name, Link: in.Link, Size: in.Size}
return s.downloadEpisodeTorrent(res, in.MediaID, in.Season, in.Episode)
}
trc, err := s.getDownloadClient()
if err != nil {
return nil, errors.Wrap(err, "connect transmission")
}
media := s.db.GetMediaDetails(in.MediaID)
if media == nil {
return nil, fmt.Errorf("no tv series of id %v", in.MediaID)
}
torrent, err := trc.Download(in.Link, s.db.GetDownloadDir())
if err != nil {
@@ -251,12 +276,12 @@ func (s *Server) DownloadMovieTorrent(c *gin.Context) (interface{}, error) {
torrent.Start()
name := in.Name
if name == "" {
name = media.OriginalName
name = m.OriginalName
}
go func() {
ep := media.Episodes[0]
ep, _ := s.db.GetMovieDummyEpisode(m.ID)
history, err := s.db.SaveHistoryRecord(ent.History{
MediaID: media.ID,
MediaID: m.ID,
EpisodeID: ep.ID,
SourceTitle: name,
TargetDir: "./",
@@ -272,9 +297,9 @@ func (s *Server) DownloadMovieTorrent(c *gin.Context) (interface{}, error) {
s.db.SetEpisodeStatus(ep.ID, episode.StatusDownloading)
}()
log.Infof("success add %s to download task", media.NameEn)
return media.NameEn, nil
s.sendMsg(fmt.Sprintf(message.BeginDownload, in.Name))
log.Infof("success add %s to download task", in.Name)
return in.Name, nil
}

View File

@@ -1,6 +1,7 @@
package server
import (
"fmt"
"path/filepath"
"polaris/ent"
"polaris/ent/episode"
@@ -9,6 +10,7 @@ import (
storage1 "polaris/ent/storage"
"polaris/log"
"polaris/pkg"
"polaris/pkg/notifier/message"
"polaris/pkg/storage"
"polaris/pkg/utils"
"polaris/server/core"
@@ -19,11 +21,11 @@ import (
func (s *Server) scheduler() {
s.mustAddCron("@every 1m", s.checkTasks)
s.mustAddCron("@every 1h", func() {
s.mustAddCron("0 0 * * * *", func() {
s.downloadTvSeries()
s.downloadMovie()
})
s.mustAddCron("@every 12h", s.checkAllSeriesNewSeason)
s.mustAddCron("0 0 */12 * * *", s.checkAllSeriesNewSeason)
s.cron.Start()
}
@@ -35,7 +37,7 @@ func (s *Server) mustAddCron(spec string, cmd func()) {
}
func (s *Server) checkTasks() {
log.Infof("begin check tasks...")
log.Debug("begin check tasks...")
for id, t := range s.tasks {
if !t.Exists() {
log.Infof("task no longer exists: %v", id)
@@ -44,6 +46,7 @@ func (s *Server) checkTasks() {
log.Infof("task (%s) percentage done: %d%%", t.Name(), t.Progress())
if t.Progress() == 100 {
log.Infof("task is done: %v", t.Name())
s.sendMsg(fmt.Sprintf(message.DownloadComplete, t.Name()))
go func() {
if err := s.moveCompletedTask(id); err != nil {
log.Infof("post tasks for id %v fail: %v", id, err)
@@ -76,7 +79,7 @@ func (s *Server) moveCompletedTask(id int) (err1 error) {
} else {
s.db.SetSeasonAllEpisodeStatus(r.MediaID, seasonNum, episode.StatusMissing)
}
s.sendMsg(fmt.Sprintf(message.ProcessingFailed, err))
} else {
delete(s.tasks, r.ID)
s.db.SetHistoryStatus(r.ID, history.StatusSuccess)
@@ -85,6 +88,7 @@ func (s *Server) moveCompletedTask(id int) (err1 error) {
} else {
s.db.SetSeasonAllEpisodeStatus(r.MediaID, seasonNum, episode.StatusDownloaded)
}
s.sendMsg(fmt.Sprintf(message.ProcessingComplete, torrent.Name()))
torrent.Remove()
}
@@ -192,12 +196,18 @@ func (s *Server) checkDownloadedSeriesFiles(m *ent.Media) error {
log.Errorf("find season episode num error: %v", err)
continue
}
var dirname = filepath.Join(in.Name(), ep.Name())
log.Infof("found match, season num %d, episode num %d", seNum, epNum)
err = s.db.UpdateEpisodeFile(m.ID, seNum, epNum, dirname)
ep, err := s.db.GetEpisode(m.ID, seNum, epNum)
if err != nil {
log.Error("update episode: %v", err)
continue
}
err = s.db.SetEpisodeStatus(ep.ID, episode.StatusDownloaded)
if err != nil {
log.Error("update episode: %v", err)
continue
}
}
}
return nil
@@ -213,50 +223,30 @@ func (s *Server) downloadTvSeries() {
log.Infof("begin check all tv series resources")
allSeries := s.db.GetMediaWatchlist(media.MediaTypeTv)
for _, series := range allSeries {
detail, err := s.MustTMDB().GetTvDetails(series.TmdbID, s.language)
if err != nil {
log.Errorf("get tv details error: %v", err)
continue
}
lastEpisode, err := s.db.GetEpisode(series.ID, detail.LastEpisodeToAir.SeasonNumber, detail.LastEpisodeToAir.EpisodeNumber)
if err != nil {
log.Errorf("get last episode error: %v", err)
continue
}
if lastEpisode.Title != detail.LastEpisodeToAir.Name {
s.db.UpdateEpiode(lastEpisode.ID, detail.LastEpisodeToAir.Name, detail.LastEpisodeToAir.Overview)
}
nextEpisode, err := s.db.GetEpisode(series.ID, detail.NextEpisodeToAir.SeasonNumber, detail.NextEpisodeToAir.EpisodeNumber)
if err == nil {
if nextEpisode.Title != detail.NextEpisodeToAir.Name {
s.db.UpdateEpiode(nextEpisode.ID, detail.NextEpisodeToAir.Name, detail.NextEpisodeToAir.Overview)
log.Errorf("updated next episode name to %v", detail.NextEpisodeToAir.Name)
}
}
if lastEpisode.Status == episode.StatusMissing {
if lastEpisode.AirDate != "" {
t, err := time.ParseInLocation("2006-01-02", lastEpisode.AirDate, time.Local)
tvDetail := s.db.GetMediaDetails(series.ID)
for _, ep := range tvDetail.Episodes {
if !series.DownloadHistoryEpisodes { //设置不下载历史已播出剧集,只下载将来剧集
t, err := time.Parse("2006-01-02", ep.AirDate)
if err != nil {
log.Errorf("parse air date error: airdate %v, error %v",lastEpisode.AirDate, err)
} else {
if series.CreatedAt.Sub(t) > 24*time.Hour { //24h容错时间
log.Infof("episode were aired 24h before monitoring, skipping: %v", lastEpisode.Title)
return
}
log.Error("air date not known, skip: %v", ep.Title)
continue
}
if series.CreatedAt.Sub(t) > 24*time.Hour { //剧集在加入watchlist之前不去下载
continue
}
}
name, err := s.searchAndDownload(series.ID, lastEpisode.SeasonNumber, lastEpisode.EpisodeNumber)
if ep.Status != episode.StatusMissing { //已经下载的不去下载
continue
}
name, err := s.searchAndDownload(series.ID, ep.SeasonNumber, ep.EpisodeNumber)
if err != nil {
log.Infof("cannot find resource to download for %s: %v", lastEpisode.Title, err)
log.Infof("cannot find resource to download for %s: %v", ep.Title, err)
} else {
log.Infof("begin download torrent resource: %v", name)
}
}
}
}
}
@@ -266,7 +256,7 @@ func (s *Server) downloadMovie() {
allSeries := s.db.GetMediaWatchlist(media.MediaTypeMovie)
for _, series := range allSeries {
detail := s.db.GetMediaDetails(series.ID)
detail := s.db.GetMediaDetails(series.ID)
if len(detail.Episodes) == 0 {
log.Errorf("no related dummy episode: %v", detail.NameEn)
continue
@@ -331,7 +321,7 @@ func (s *Server) checkAllSeriesNewSeason() {
}
}
func (s *Server) checkSeiesNewSeason(media *ent.Media) error{
func (s *Server) checkSeiesNewSeason(media *ent.Media) error {
d, err := s.MustTMDB().GetTvDetails(media.TmdbID, s.language)
if err != nil {
return errors.Wrap(err, "tmdb")
@@ -341,27 +331,29 @@ func (s *Server) checkSeiesNewSeason(media *ent.Media) error{
if err != nil {
return errors.Wrap(err, "tmdb season")
}
for _, ep := range seasonDetail.Episodes {
epDb, err := s.db.GetEpisode(media.ID, ep.SeasonNumber, ep.EpisodeNumber)
if err != nil {
if ent.IsNotFound(err) {
log.Infof("add new episode: %+v", ep)
episode := &ent.Episode{
MediaID: media.ID,
SeasonNumber: ep.SeasonNumber,
MediaID: media.ID,
SeasonNumber: ep.SeasonNumber,
EpisodeNumber: ep.EpisodeNumber,
Title: ep.Name,
Overview: ep.Overview,
AirDate: ep.AirDate,
Status: episode.StatusMissing,
Title: ep.Name,
Overview: ep.Overview,
AirDate: ep.AirDate,
Status: episode.StatusMissing,
}
s.db.SaveEposideDetail2(episode)
}
} else {//update episode
log.Infof("update new episode: %+v", ep)
s.db.UpdateEpiode2(epDb.ID, ep.Name, ep.Overview, ep.AirDate)
} else { //update episode
if ep.Name != epDb.Title || ep.Overview != epDb.Overview || ep.AirDate != epDb.AirDate {
log.Infof("update new episode: %+v", ep)
s.db.UpdateEpiode2(epDb.ID, ep.Name, ep.Overview, ep.AirDate)
}
}
}
return nil
}
}

View File

@@ -10,6 +10,9 @@ import (
"polaris/pkg/tmdb"
"polaris/pkg/transmission"
"polaris/ui"
"time"
ginzap "github.com/gin-contrib/zap"
"github.com/gin-contrib/static"
"github.com/robfig/cron"
@@ -40,29 +43,42 @@ type Server struct {
func (s *Server) Serve() error {
s.scheduler()
s.reloadTasks()
s.restoreProxy()
s.jwtSerect = s.db.GetSetting(db.JwtSerectKey)
//st, _ := fs.Sub(ui.Web, "build/web")
s.r.Use(static.Serve("/", static.EmbedFolder(ui.Web, "build/web")))
s.r.Use(ginzap.Ginzap(log.Logger().Desugar(), time.RFC3339, false))
s.r.Use(ginzap.RecoveryWithZap(log.Logger().Desugar(), true))
log.SetLogLevel(s.db.GetSetting(db.SettingLogLevel)) //restore log level
s.r.POST("/api/login", HttpHandler(s.Login))
api := s.r.Group("/api/v1")
api.Use(s.authModdleware)
api.StaticFS("/img", http.Dir(db.ImgPath))
api.StaticFS("/logs", http.Dir(db.LogPath))
api.Any("/posters/*proxyPath", s.proxyPosters)
setting := api.Group("/setting")
{
setting.GET("/logout", HttpHandler(s.Logout))
setting.POST("/general", HttpHandler(s.SetSetting))
setting.GET("/general", HttpHandler(s.GetSetting))
setting.POST("/auth", HttpHandler(s.EnableAuth))
setting.GET("/auth", HttpHandler(s.GetAuthSetting))
setting.GET("/logfiles", HttpHandler(s.GetAllLogs))
setting.GET("/about", HttpHandler(s.About))
setting.POST("/parse/tv", HttpHandler(s.ParseTv))
setting.POST("/parse/movie", HttpHandler(s.ParseMovie))
}
activity := api.Group("/activity")
{
activity.GET("/", HttpHandler(s.GetAllActivities))
activity.DELETE("/:id", HttpHandler(s.RemoveActivity))
activity.GET("/media/:id", HttpHandler(s.GetMediaDownloadHistory))
activity.GET("/torrents", HttpHandler(s.GetAllTorrents))
}
tv := api.Group("/media")
@@ -70,11 +86,10 @@ func (s *Server) Serve() error {
tv.GET("/search", HttpHandler(s.SearchMedia))
tv.POST("/tv/watchlist", HttpHandler(s.AddTv2Watchlist))
tv.GET("/tv/watchlist", HttpHandler(s.GetTvWatchlist))
tv.POST("/tv/torrents", HttpHandler(s.SearchAvailableEpisodeResource))
tv.POST("/torrents", HttpHandler(s.SearchAvailableTorrents))
tv.POST("/torrents/download/", HttpHandler(s.DownloadTorrent))
tv.POST("/movie/watchlist", HttpHandler(s.AddMovie2Watchlist))
tv.GET("/movie/watchlist", HttpHandler(s.GetMovieWatchlist))
tv.GET("/movie/resources/:id", HttpHandler(s.SearchAvailableMovies))
tv.POST("/movie/resources/", HttpHandler(s.DownloadMovieTorrent))
tv.GET("/record/:id", HttpHandler(s.GetMediaDetails))
tv.DELETE("/record/:id", HttpHandler(s.DeleteFromWatchlist))
tv.GET("/resolutions", HttpHandler(s.GetAvailableResolutions))
@@ -100,6 +115,13 @@ func (s *Server) Serve() error {
storage.POST("/", HttpHandler(s.AddStorage))
storage.DELETE("/:id", HttpHandler(s.DeleteStorage))
}
notifier := api.Group("/notifier")
{
notifier.GET("/all", HttpHandler(s.GetAllNotificationClients))
notifier.GET("/id/:id", HttpHandler(s.GetNotificationClient))
notifier.DELETE("/id/:id", HttpHandler(s.DeleteNotificationClient))
notifier.POST("/add", HttpHandler(s.AddNotificationClient))
}
s.language = s.db.GetLanguage()
return s.r.Run(":8080")
@@ -146,7 +168,7 @@ func (s *Server) proxyPosters(c *gin.Context) {
req.Host = remote.Host
req.URL.Scheme = remote.Scheme
req.URL.Host = remote.Host
req.URL.Path = fmt.Sprintf("/t/p/w500/%v", c.Param("proxyPath"))
req.URL.Path = fmt.Sprintf("/t/p/w500/%v", c.Param("proxyPath"))
}
proxy.ServeHTTP(c.Writer, c.Request)
}

View File

@@ -2,6 +2,8 @@ package server
import (
"fmt"
"net/http"
"net/url"
"polaris/db"
"polaris/log"
"polaris/pkg/transmission"
@@ -14,6 +16,8 @@ import (
type GeneralSettings struct {
TmdbApiKey string `json:"tmdb_api_key"`
DownloadDir string `json:"download_dir"`
LogLevel string `json:"log_level"`
Proxy string `json:"proxy"`
}
func (s *Server) SetSetting(c *gin.Context) (interface{}, error) {
@@ -28,20 +32,51 @@ func (s *Server) SetSetting(c *gin.Context) (interface{}, error) {
}
}
if in.DownloadDir != "" {
log.Info("set download dir to %s", in.DownloadDir)
if err := s.db.SetSetting(db.SettingDownloadDir, in.DownloadDir); err != nil {
return nil, errors.Wrap(err, "save download dir")
}
}
if in.LogLevel != "" {
log.SetLogLevel(in.LogLevel)
if err := s.db.SetSetting(db.SettingLogLevel, in.LogLevel); err != nil {
return nil, errors.Wrap(err, "save log level")
}
}
s.setProxy(in.Proxy)
return nil, nil
}
func (s *Server) setProxy(proxy string) {
proxyUrl, err := url.Parse(proxy)
tp := http.DefaultTransport.(*http.Transport)
if proxy == "" || err != nil {
log.Warnf("proxy url not valid, disabling: %v", proxy)
tp.Proxy = nil
s.db.SetSetting(db.SettingProxy, "")
} else {
log.Infof("set proxy to %v", proxy)
tp.Proxy = http.ProxyURL(proxyUrl)
s.db.SetSetting(db.SettingProxy, proxy)
}
}
func (s *Server) restoreProxy() {
p := s.db.GetSetting(db.SettingProxy)
s.setProxy(p)
}
func (s *Server) GetSetting(c *gin.Context) (interface{}, error) {
tmdb := s.db.GetSetting(db.SettingTmdbApiKey)
downloadDir := s.db.GetSetting(db.SettingDownloadDir)
logLevel := s.db.GetSetting(db.SettingLogLevel)
return &GeneralSettings{
TmdbApiKey: tmdb,
DownloadDir: downloadDir,
LogLevel: logLevel,
Proxy: s.db.GetSetting(db.SettingProxy),
}, nil
}
@@ -97,7 +132,6 @@ func (s *Server) getDownloadClient() (*transmission.Client, error) {
return trc, nil
}
type downloadClientIn struct {
Name string `json:"name" binding:"required"`
URL string `json:"url" binding:"required"`
@@ -113,8 +147,8 @@ func (s *Server) AddDownloadClient(c *gin.Context) (interface{}, error) {
}
//test connection
_, err := transmission.NewClient(transmission.Config{
URL: in.URL,
User: in.User,
URL: in.URL,
User: in.User,
Password: in.Password,
})
if err != nil {

View File

@@ -84,5 +84,6 @@ func (s *Server) SuggestedSeriesFolderName(c *gin.Context) (interface{}, error)
if year != "" {
name = fmt.Sprintf("%s (%s)", name, year)
}
log.Infof("tv series of tmdb id %v suggestting name is %v", id, name)
return gin.H{"name": name}, nil
}

74
server/systems.go Normal file
View File

@@ -0,0 +1,74 @@
package server
import (
"os"
"polaris/db"
"polaris/log"
"polaris/pkg/metadata"
"polaris/pkg/uptime"
"runtime"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)
type LogFile struct {
Name string `json:"name"`
Size int64 `json:"size"`
}
func (s *Server) GetAllLogs(c *gin.Context) (interface{}, error) {
fs, err := os.ReadDir(db.LogPath)
if err != nil {
return nil, errors.Wrap(err, "read log dir")
}
var logs []LogFile
for _, f := range fs {
if f.IsDir() {
continue
}
info, err := f.Info()
if err != nil {
log.Warnf("get log file error: %v", err)
continue
}
l := LogFile{
Name: f.Name(),
Size: info.Size(),
}
logs = append(logs, l)
}
return logs, nil
}
func (s *Server) About(c *gin.Context) (interface{}, error) {
return gin.H{
"intro": "Polaris © Simon Ding",
"homepage": "https://github.com/simon-ding/polaris",
"uptime": uptime.Uptime(),
"chat_group": "https://t.me/+8R2nzrlSs2JhMDgx",
"go_version": runtime.Version(),
"version": db.Version,
}, nil
}
type parseIn struct {
S string `json:"s" binding:"required"`
}
func (s *Server) ParseTv(c *gin.Context) (interface{}, error) {
var in parseIn
if err := c.ShouldBindJSON(&in); err != nil {
return nil, errors.Wrap(err, "bind")
}
return metadata.ParseTv(in.S), nil
}
func (s *Server) ParseMovie(c *gin.Context) (interface{}, error) {
var in parseIn
if err := c.ShouldBindJSON(&in); err != nil {
return nil, errors.Wrap(err, "bind")
}
return metadata.ParseMovie(in.S), nil
}

View File

@@ -8,9 +8,11 @@ import (
"path/filepath"
"polaris/db"
"polaris/ent"
"polaris/ent/episode"
"polaris/ent/media"
"polaris/log"
"strconv"
"time"
tmdb "github.com/cyruzin/golang-tmdb"
"github.com/gin-gonic/gin"
@@ -55,10 +57,11 @@ func (s *Server) SearchMedia(c *gin.Context) (interface{}, error) {
}
type addWatchlistIn struct {
TmdbID int `json:"tmdb_id" binding:"required"`
StorageID int `json:"storage_id" `
Resolution string `json:"resolution" binding:"required"`
Folder string `json:"folder"`
TmdbID int `json:"tmdb_id" binding:"required"`
StorageID int `json:"storage_id" `
Resolution string `json:"resolution" binding:"required"`
Folder string `json:"folder"`
DownloadHistoryEpisodes bool `json:"download_history_episodes"` //for tv
}
func (s *Server) AddTv2Watchlist(c *gin.Context) (interface{}, error) {
@@ -66,7 +69,7 @@ func (s *Server) AddTv2Watchlist(c *gin.Context) (interface{}, error) {
if err := c.ShouldBindJSON(&in); err != nil {
return nil, errors.Wrap(err, "bind query")
}
if (in.Folder == "") {
if in.Folder == "" {
return nil, errors.New("folder should be provided")
}
detailCn, err := s.MustTMDB().GetTvDetails(in.TmdbID, db.LanguageCN)
@@ -118,7 +121,8 @@ func (s *Server) AddTv2Watchlist(c *gin.Context) (interface{}, error) {
AirDate: detail.FirstAirDate,
Resolution: media.Resolution(in.Resolution),
StorageID: in.StorageID,
TargetDir: in.Folder,
TargetDir: in.Folder,
DownloadHistoryEpisodes: in.DownloadHistoryEpisodes,
}, epIds)
if err != nil {
return nil, errors.Wrap(err, "add to list")
@@ -183,7 +187,7 @@ func (s *Server) AddMovie2Watchlist(c *gin.Context) (interface{}, error) {
AirDate: detail.ReleaseDate,
Resolution: media.Resolution(in.Resolution),
StorageID: in.StorageID,
TargetDir: "./",
TargetDir: "./",
}, []int{epid})
if err != nil {
return nil, errors.Wrap(err, "add to list")
@@ -238,14 +242,66 @@ func (s *Server) downloadImage(url string, mediaID int, name string) error {
}
type MediaWithStatus struct {
*ent.Media
Status string `json:"status"`
}
//missing: episode aired missing
//downloaded: all monitored episode downloaded
//monitoring: episode aired downloaded, but still has not aired episode
//for movie, only monitoring/downloaded
func (s *Server) GetTvWatchlist(c *gin.Context) (interface{}, error) {
list := s.db.GetMediaWatchlist(media.MediaTypeTv)
return list, nil
res := make([]MediaWithStatus, len(list))
for i, item := range list {
var ms = MediaWithStatus{
Media: item,
Status: "downloaded",
}
details := s.db.GetMediaDetails(item.ID)
for _, ep := range details.Episodes {
if ep.SeasonNumber == 0 {
continue
}
t, err := time.Parse("2006-01-02", ep.AirDate)
if err != nil { //airdate not exist
ms.Status = "monitoring"
} else {
if item.CreatedAt.Sub(t) > 24*time.Hour { //剧集在加入watchlist之前不去下载
continue
}
if ep.Status == episode.StatusMissing {
ms.Status = "monitoring"
}
}
}
res[i] = ms
}
return res, nil
}
func (s *Server) GetMovieWatchlist(c *gin.Context) (interface{}, error) {
list := s.db.GetMediaWatchlist(media.MediaTypeMovie)
return list, nil
res := make([]MediaWithStatus, len(list))
for i, item := range list {
var ms = MediaWithStatus{
Media: item,
Status: "monitoring",
}
dummyEp, err := s.db.GetMovieDummyEpisode(item.ID)
if err != nil {
log.Errorf("get dummy episode: %v", err)
} else {
if dummyEp.Status != episode.StatusMissing {
ms.Status = "downloaded"
}
}
res[i] = ms
}
return res, nil
}
func (s *Server) GetMediaDetails(c *gin.Context) (interface{}, error) {

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:percent_indicator/circular_percent_indicator.dart';
import 'package:ui/providers/activity.dart';
import 'package:ui/utils.dart';
import 'package:ui/widgets/utils.dart';
import 'package:ui/widgets/progress_indicator.dart';
class ActivityPage extends ConsumerStatefulWidget {

View File

@@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:ui/activity.dart';
import 'package:ui/login_page.dart';
import 'package:ui/movie_watchlist.dart';
import 'package:ui/providers/APIs.dart';
import 'package:ui/search.dart';
import 'package:ui/system_settings.dart';
import 'package:ui/settings.dart';
import 'package:ui/system_page.dart';
import 'package:ui/tv_details.dart';
import 'package:ui/welcome_page.dart';
@@ -25,9 +25,21 @@ class MyApp extends ConsumerStatefulWidget {
}
}
class _MyAppState extends ConsumerState<MyApp> {
CustomTransitionPage buildPageWithDefaultTransition<T>({
required BuildContext context,
required GoRouterState state,
required Widget child,
}) {
return CustomTransitionPage<T>(
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
key: state.pageKey,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) => child,
);
}
class _MyAppState extends ConsumerState<MyApp> {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
@@ -35,8 +47,9 @@ class _MyAppState extends ConsumerState<MyApp> {
final shellRoute = ShellRoute(
builder: (BuildContext context, GoRouterState state, Widget child) {
return SelectionArea(
child: MainSkeleton(body: Padding(padding: const EdgeInsets.all(20), child: child),
),
child: MainSkeleton(
body: Padding(padding: const EdgeInsets.all(20), child: child),
),
);
},
routes: [
@@ -46,34 +59,51 @@ class _MyAppState extends ConsumerState<MyApp> {
),
GoRoute(
path: WelcomePage.routeTv,
builder: (context, state) => const WelcomePage(),
),
GoRoute(
path: TvDetailsPage.route,
builder: (context, state) =>
TvDetailsPage(seriesId: state.pathParameters['id']!),
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context, state: state, child: const WelcomePage()),
),
GoRoute(
path: WelcomePage.routeMoivie,
builder: (context, state) => const WelcomePage(),
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context, state: state, child: const WelcomePage()),
),
GoRoute(
path: TvDetailsPage.route,
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context,
state: state,
child: TvDetailsPage(seriesId: state.pathParameters['id']!)),
),
GoRoute(
path: MovieDetailsPage.route,
builder: (context, state) =>
MovieDetailsPage(id: state.pathParameters['id']!),
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context,
state: state,
child: MovieDetailsPage(id: state.pathParameters['id']!)),
),
GoRoute(
path: SearchPage.route,
builder: (context, state) =>
SearchPage(query: state.uri.queryParameters["query"]),
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context,
state: state,
child: SearchPage(query: state.uri.queryParameters["query"])),
),
GoRoute(
path: SystemSettingsPage.route,
builder: (context, state) => const SystemSettingsPage(),
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context,
state: state,
child: const SystemSettingsPage()),
),
GoRoute(
path: ActivityPage.route,
builder: (context, state) => const ActivityPage(),
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context, state: state, child: const ActivityPage()),
),
GoRoute(
path: SystemPage.route,
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context, state: state, child: const SystemPage()),
)
],
);
@@ -95,7 +125,9 @@ class _MyAppState extends ConsumerState<MyApp> {
theme: ThemeData(
fontFamily: "NotoSansSC",
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blueAccent, brightness: Brightness.dark, surface: Colors.black54),
seedColor: Colors.blueAccent,
brightness: Brightness.dark,
surface: Colors.black54),
useMaterial3: true,
//scaffoldBackgroundColor: Color.fromARGB(255, 26, 24, 24)
),
@@ -103,7 +135,6 @@ class _MyAppState extends ConsumerState<MyApp> {
),
);
}
}
class MainSkeleton extends StatefulWidget {
@@ -130,85 +161,73 @@ class _MainSkeletonState extends State<MainSkeleton> {
_selectedTab = 2;
} else if (uri.contains(SystemSettingsPage.route)) {
_selectedTab = 3;
} else if (uri.contains(SystemPage.route)) {
_selectedTab = 4;
}
return AdaptiveScaffold(
appBarBreakpoint: Breakpoints.standard,
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: const Row(
children: [
Text("Polaris"),
],
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: const Row(
children: [
Text("Polaris"),
],
),
actions: [
SearchAnchor(
builder: (BuildContext context, SearchController controller) {
return Container(
constraints: const BoxConstraints(maxWidth: 300, 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()),
),
actions: [
SearchAnchor(builder:
(BuildContext context, SearchController controller) {
return Container(
constraints:
const BoxConstraints(maxWidth: 300, 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")];
}),
FutureBuilder(
future: APIs.isLoggedIn(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data == true) {
return MenuAnchor(
menuChildren: [
MenuItemButton(
leadingIcon: const Icon(Icons.exit_to_app),
child: const Text("登出"),
onPressed: () async {
final SharedPreferences prefs =
await SharedPreferences.getInstance();
await prefs.remove('token');
if (context.mounted) {
context.go(LoginScreen.route);
}
},
),
],
builder: (context, controller, child) {
return TextButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Icon(Icons.account_circle),
);
},
);
}
return Container();
})
],
),
);
}, suggestionsBuilder:
(BuildContext context, SearchController controller) {
return [Text("dadada")];
}),
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,
selectedIndex: _selectedTab,
onSelectedIndexChange: (int index) {
@@ -223,12 +242,14 @@ class _MainSkeletonState extends State<MainSkeleton> {
context.go(ActivityPage.route);
} else if (index == 3) {
context.go(SystemSettingsPage.route);
} else if (index == 4) {
context.go(SystemPage.route);
}
},
destinations: const <NavigationDestination>[
NavigationDestination(
icon: Icon(Icons.live_tv),
label: '电视',
label: '',
),
NavigationDestination(
icon: Icon(Icons.movie),
@@ -242,6 +263,10 @@ class _MainSkeletonState extends State<MainSkeleton> {
icon: Icon(Icons.settings),
label: '设置',
),
NavigationDestination(
icon: Icon(Icons.computer_rounded),
label: '系统',
),
],
body: (context) => widget.body,
// Define a default secondaryBody.

View File

@@ -5,8 +5,7 @@ import 'package:ui/providers/APIs.dart';
import 'package:ui/providers/activity.dart';
import 'package:ui/providers/series_details.dart';
import 'package:ui/providers/settings.dart';
import 'package:ui/providers/welcome_data.dart';
import 'package:ui/utils.dart';
import 'package:ui/widgets/utils.dart';
import 'package:ui/welcome_page.dart';
import 'package:ui/widgets/progress_indicator.dart';
@@ -43,11 +42,11 @@ class _MovieDetailsPageState extends ConsumerState<MovieDetailsPage> {
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.fitWidth,
fit: BoxFit.cover,
opacity: 0.5,
image: NetworkImage(
"${APIs.imagesUrl}/${details.id}/backdrop.jpg",
headers: APIs.authHeaders))),
"${APIs.imagesUrl}/${details.id}/backdrop.jpg",
))),
child: Padding(
padding: const EdgeInsets.all(10),
child: Row(
@@ -59,7 +58,6 @@ class _MovieDetailsPageState extends ConsumerState<MovieDetailsPage> {
child: Image.network(
"${APIs.imagesUrl}/${details.id}/poster.jpg",
fit: BoxFit.contain,
headers: APIs.authHeaders,
),
),
),
@@ -172,7 +170,6 @@ class _NestedTabBarState extends ConsumerState<NestedTabBar>
@override
Widget build(BuildContext context) {
var torrents = ref.watch(movieTorrentsDataProvider(widget.id));
var histories = ref.watch(mediaHistoryDataProvider(widget.id));
return Column(
@@ -223,48 +220,58 @@ class _NestedTabBarState extends ConsumerState<NestedTabBar>
error: (error, trace) => Text("$error"),
loading: () => const MyProgressIndicator());
} else {
return torrents.when(
data: (v) {
if (v.isEmpty) {
return const Center(
child: Text("无可用资源"),
);
}
return Consumer(
builder: (context, ref, child) {
var torrents = ref.watch(mediaTorrentsDataProvider(
(mediaId: widget.id, seasonNumber: 0, episodeNumber: 0)));
return torrents.when(
data: (v) {
if (v.isEmpty) {
return const Center(
child: Text("无可用资源"),
);
}
return DataTable(
columns: const [
DataColumn(label: Text("名称")),
DataColumn(label: Text("大小")),
DataColumn(label: Text("seeders")),
DataColumn(label: Text("peers")),
DataColumn(label: Text("操作"))
],
rows: List.generate(v.length, (i) {
final torrent = v[i];
return DataRow(cells: [
DataCell(Text("${torrent.name}")),
DataCell(Text("${torrent.size?.readableFileSize()}")),
DataCell(Text("${torrent.seeders}")),
DataCell(Text("${torrent.peers}")),
DataCell(IconButton(
icon: const Icon(Icons.download),
onPressed: () {
ref
.read(movieTorrentsDataProvider(widget.id)
.notifier)
.download(torrent)
.then((v) =>
Utils.showSnakeBar("开始下载:${torrent.name}"))
.onError((error, trace) =>
Utils.showSnakeBar("操作失败: $error"));
},
))
]);
}),
);
},
error: (error, trace) => Text("$error"),
loading: () => const MyProgressIndicator());
return DataTable(
columns: const [
DataColumn(label: Text("名称")),
DataColumn(label: Text("大小")),
DataColumn(label: Text("seeders")),
DataColumn(label: Text("peers")),
DataColumn(label: Text("操作"))
],
rows: List.generate(v.length, (i) {
final torrent = v[i];
return DataRow(cells: [
DataCell(Text("${torrent.name}")),
DataCell(
Text("${torrent.size?.readableFileSize()}")),
DataCell(Text("${torrent.seeders}")),
DataCell(Text("${torrent.peers}")),
DataCell(IconButton(
icon: const Icon(Icons.download),
onPressed: () {
ref
.read(mediaTorrentsDataProvider((
mediaId: widget.id,
seasonNumber: 0,
episodeNumber: 0
)).notifier)
.download(torrent)
.then((v) => Utils.showSnakeBar(
"开始下载:${torrent.name}"))
.onError((error, trace) =>
Utils.showSnakeBar("操作失败: $error"));
},
))
]);
}),
);
},
error: (error, trace) => Text("$error"),
loading: () => const MyProgressIndicator());
},
);
}
})
],

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:ui/activity.dart';
import 'package:ui/system_settings.dart';
import 'package:ui/settings.dart';
import 'package:ui/welcome_page.dart';
class NavDrawer extends StatefulWidget {

View File

@@ -2,8 +2,6 @@ import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:quiver/strings.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:ui/providers/server_response.dart';
class APIs {
@@ -13,7 +11,8 @@ class APIs {
static final settingsGeneralUrl = "$_baseUrl/api/v1/setting/general";
static final watchlistTvUrl = "$_baseUrl/api/v1/media/tv/watchlist";
static final watchlistMovieUrl = "$_baseUrl/api/v1/media/movie/watchlist";
static final availableMoviesUrl = "$_baseUrl/api/v1/media/movie/resources/";
static final availableTorrentsUrl = "$_baseUrl/api/v1/media/torrents/";
static final downloadTorrentUrl = "$_baseUrl/api/v1/media/torrents/download";
static final seriesDetailUrl = "$_baseUrl/api/v1/media/record/";
static final suggestedTvName = "$_baseUrl/api/v1/media/suggest/";
static final searchAndDownloadUrl = "$_baseUrl/api/v1/indexer/download";
@@ -25,10 +24,18 @@ class APIs {
static final delDownloadClientUrl = "$_baseUrl/api/v1/downloader/del/";
static final storageUrl = "$_baseUrl/api/v1/storage/";
static final loginUrl = "$_baseUrl/api/login";
static final logoutUrl = "$_baseUrl/api/v1/setting/logout";
static final loginSettingUrl = "$_baseUrl/api/v1/setting/auth";
static final activityUrl = "$_baseUrl/api/v1/activity/";
static final activityMediaUrl = "$_baseUrl/api/v1/activity/media/";
static final imagesUrl = "$_baseUrl/api/v1/img";
static final logsBaseUrl = "$_baseUrl/api/v1/logs/";
static final logFilesUrl = "$_baseUrl/api/v1/setting/logfiles";
static final aboutUrl = "$_baseUrl/api/v1/setting/about";
static final notifierAllUrl = "$_baseUrl/api/v1/notifier/all";
static final notifierDeleteUrl = "$_baseUrl/api/v1/notifier/id/";
static final notifierAddUrl = "$_baseUrl/api/v1/notifier/add/";
static final tmdbImgBaseUrl = "$_baseUrl/api/v1/posters";
@@ -45,53 +52,20 @@ class APIs {
return "http://127.0.0.1:8080";
}
static Dio? gDio;
static Map<String, String> authHeaders = {};
static Future<bool> isLoggedIn() async {
return isNotBlank(await getToken());
}
static Future<String> getToken() async {
var token = authHeaders["Authorization"];
if (isBlank(token)) {
final SharedPreferences prefs = await SharedPreferences.getInstance();
var t = prefs.getString("token");
if (isNotBlank(t)) {
authHeaders["Authorization"] = t!;
token = t;
}
}
return token ?? "";
}
static Future<Dio> getDio() async {
if (gDio != null) {
return gDio!;
}
var token = await getToken();
static Dio getDio() {
var dio = Dio();
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] = token;
return handler.next(options);
},
onError: (error, handler) {
if (error.response?.statusCode != null &&
error.response?.statusCode! == 403) {
final context = navigatorKey.currentContext;
if (context != null) {
context.go('/login');
gDio = null;
}
}
return handler.next(error);
},
));
if (isNotBlank(token)) {
gDio = dio;
}
return dio;
}
@@ -104,9 +78,19 @@ class APIs {
if (sp.code != 0) {
throw sp.message;
}
final SharedPreferences prefs = await SharedPreferences.getInstance();
var t = sp.data["token"];
authHeaders["Authorization"] = "Bearer $t";
prefs.setString("token", "Bearer $t");
}
static Future<void> logout() async {
var resp = await getDio().get(APIs.logoutUrl);
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
final context = navigatorKey.currentContext;
if (context != null) {
context.go('/login');
}
}
}

View File

@@ -0,0 +1,75 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ui/providers/APIs.dart';
import 'package:ui/providers/server_response.dart';
final notifiersDataProvider =
AsyncNotifierProvider.autoDispose<NotifiersData, List<NotifierData>>(
NotifiersData.new);
class NotifierData {
int? id;
String? name;
String? service;
Map<String, dynamic>? settings;
bool? enabled;
NotifierData({this.id, this.name, this.service, this.enabled, this.settings});
factory NotifierData.fromJson(Map<String, dynamic> json) {
return NotifierData(
id: json["id"],
name: json["name"],
service: json["service"],
enabled: json["enabled"] ?? true,
settings: json["settings"]);
}
Map<String, dynamic> toJson() {
final data = <String, dynamic>{};
data["name"] = name;
data["service"] = service;
data["enabled"] = enabled;
data["settings"] = jsonEncode(settings);
return data;
}
}
class NotifiersData extends AutoDisposeAsyncNotifier<List<NotifierData>> {
@override
FutureOr<List<NotifierData>> build() async {
final dio = APIs.getDio();
final resp = await dio.get(APIs.notifierAllUrl);
final sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
return sp.data == null
? List.empty()
: (sp.data as List).map((e) => NotifierData.fromJson(e)).toList();
}
Future<void> delete(int id) async {
final dio = APIs.getDio();
final resp = await dio.delete(APIs.notifierDeleteUrl + id.toString());
final sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
ref.invalidateSelf();
}
Future<void> add(NotifierData n) async {
final dio = APIs.getDio();
final resp = await dio.post(APIs.notifierAddUrl, data: n.toJson());
final sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
ref.invalidateSelf();
}
}

View File

@@ -97,7 +97,7 @@ class SeriesDetails {
class Episodes {
int? id;
int? seriesId;
int? mediaId;
int? episodeNumber;
String? title;
String? airDate;
@@ -107,7 +107,7 @@ class Episodes {
Episodes(
{this.id,
this.seriesId,
this.mediaId,
this.episodeNumber,
this.title,
this.airDate,
@@ -117,7 +117,7 @@ class Episodes {
Episodes.fromJson(Map<String, dynamic> json) {
id = json['id'];
seriesId = json['series_id'];
mediaId = json['media_id'];
episodeNumber = json['episode_number'];
title = json['title'];
airDate = json['air_date'];
@@ -126,3 +126,85 @@ class Episodes {
overview = json['overview'];
}
}
var mediaTorrentsDataProvider = AsyncNotifierProvider.autoDispose
.family<MediaTorrentResource, List<TorrentResource>, TorrentQuery>(
MediaTorrentResource.new);
// class TorrentQuery {
// final String mediaId;
// final int seasonNumber;
// final int episodeNumber;
// TorrentQuery(
// {required this.mediaId, this.seasonNumber = 0, this.episodeNumber = 0});
// Map<String, dynamic> toJson() {
// final Map<String, dynamic> data = <String, dynamic>{};
// data["id"] = int.parse(mediaId);
// data["season"] = seasonNumber;
// data["episode"] = episodeNumber;
// return data;
// }
// }
typedef TorrentQuery =({String mediaId, int seasonNumber, int episodeNumber});
class MediaTorrentResource extends AutoDisposeFamilyAsyncNotifier<
List<TorrentResource>, TorrentQuery> {
@override
FutureOr<List<TorrentResource>> build(TorrentQuery arg) async {
final dio = await APIs.getDio();
var resp = await dio.post(APIs.availableTorrentsUrl, data: {
"id": int.parse(arg.mediaId),
"season": arg.seasonNumber,
"episode": arg.episodeNumber
});
var rsp = ServerResponse.fromJson(resp.data);
if (rsp.code != 0) {
throw rsp.message;
}
return (rsp.data as List).map((v) => TorrentResource.fromJson(v)).toList();
}
Future<void> download(TorrentResource res) async {
final data = res.toJson();
data.addAll({
"id": int.parse(arg.mediaId),
"season": arg.seasonNumber,
"episode": arg.episodeNumber
});
final dio = await APIs.getDio();
var resp = await dio.post(APIs.downloadTorrentUrl, data: data);
var rsp = ServerResponse.fromJson(resp.data);
if (rsp.code != 0) {
throw rsp.message;
}
}
}
class TorrentResource {
TorrentResource({this.name, this.size, this.seeders, this.peers, this.link});
String? name;
int? size;
int? seeders;
int? peers;
String? link;
factory TorrentResource.fromJson(Map<String, dynamic> json) {
return TorrentResource(
name: json["name"],
size: json["size"],
seeders: json["seeders"],
peers: json["peers"],
link: json["link"]);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['name'] = name;
data['size'] = size;
data["link"] = link;
return data;
}
}

View File

@@ -49,18 +49,26 @@ class EditSettingData extends AutoDisposeAsyncNotifier<GeneralSetting> {
class GeneralSetting {
String? tmdbApiKey;
String? downloadDIr;
String? logLevel;
String? proxy;
GeneralSetting({this.tmdbApiKey, this.downloadDIr});
GeneralSetting(
{this.tmdbApiKey, this.downloadDIr, this.logLevel, this.proxy});
factory GeneralSetting.fromJson(Map<String, dynamic> json) {
return GeneralSetting(
tmdbApiKey: json["tmdb_api_key"], downloadDIr: json["download_dir"]);
tmdbApiKey: json["tmdb_api_key"],
downloadDIr: json["download_dir"],
logLevel: json["log_level"],
proxy: json["proxy"]);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['tmdb_api_key'] = tmdbApiKey;
data['download_dir'] = downloadDIr;
data["log_level"] = logLevel;
data["proxy"] = proxy;
return data;
}
}
@@ -257,13 +265,13 @@ class StorageSettingData extends AutoDisposeAsyncNotifier<List<Storage>> {
}
class Storage {
Storage(
{this.id,
this.name,
this.implementation,
this.settings,
this.isDefault,
});
Storage({
this.id,
this.name,
this.implementation,
this.settings,
this.isDefault,
});
final int? id;
final String? name;
@@ -288,3 +296,69 @@ class Storage {
"default": isDefault,
};
}
final logFileDataProvider = FutureProvider.autoDispose((ref) async {
final dio = await APIs.getDio();
var resp = await dio.get(APIs.logFilesUrl);
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
List<LogFile> favList = List.empty(growable: true);
for (var item in sp.data as List) {
var tv = LogFile.fromJson(item);
favList.add(tv);
}
return favList;
});
final aboutDataProvider = FutureProvider.autoDispose((ref) async {
final dio = await APIs.getDio();
var resp = await dio.get(APIs.aboutUrl);
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
return About.fromJson(sp.data);
});
class LogFile {
String? name;
int? size;
LogFile({this.name, this.size});
factory LogFile.fromJson(Map<String, dynamic> json1) {
return LogFile(name: json1["name"], size: json1["size"]);
}
}
class About {
About({
required this.chatGroup,
required this.goVersion,
required this.homepage,
required this.intro,
required this.uptime,
required this.version,
});
final String? chatGroup;
final String? goVersion;
final String? homepage;
final String? intro;
final Duration? uptime;
final String? version;
factory About.fromJson(Map<String, dynamic> json) {
return About(
chatGroup: json["chat_group"],
goVersion: json["go_version"],
homepage: json["homepage"],
intro: json["intro"],
version: json["version"],
uptime:
Duration(microseconds: (json["uptime"] / 1000.0 as double).round()),
);
}
}

View File

@@ -44,10 +44,6 @@ final movieWatchlistDataProvider = FutureProvider.autoDispose((ref) async {
var searchPageDataProvider = AsyncNotifierProvider.autoDispose
.family<SearchPageData, List<SearchResult>, String>(SearchPageData.new);
var movieTorrentsDataProvider = AsyncNotifierProvider.autoDispose
.family<MovieTorrentResource, List<TorrentResource>, String>(
MovieTorrentResource.new);
class SearchPageData
extends AutoDisposeFamilyAsyncNotifier<List<SearchResult>, String> {
List<SearchResult> list = List.empty(growable: true);
@@ -88,14 +84,15 @@ class SearchPageData
}
Future<void> submit2Watchlist(int tmdbId, int storageId, String resolution,
String mediaType, String folder) async {
String mediaType, String folder, bool downloadHistoryEpisodes) async {
final dio = await APIs.getDio();
if (mediaType == "tv") {
var resp = await dio.post(APIs.watchlistTvUrl, data: {
"tmdb_id": tmdbId,
"storage_id": storageId,
"resolution": resolution,
"folder": folder
"folder": folder,
"download_history_episodes":downloadHistoryEpisodes
});
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
@@ -150,6 +147,7 @@ class MediaDetail {
String? resolution;
int? storageId;
String? airDate;
String? status;
MediaDetail({
this.id,
@@ -163,6 +161,7 @@ class MediaDetail {
this.resolution,
this.storageId,
this.airDate,
this.status,
});
MediaDetail.fromJson(Map<String, dynamic> json) {
@@ -177,6 +176,7 @@ class MediaDetail {
resolution = json["resolution"];
storageId = json["storage_id"];
airDate = json["air_date"];
status = json["status"];
}
}
@@ -242,56 +242,3 @@ class SearchResult {
}
}
class MovieTorrentResource
extends AutoDisposeFamilyAsyncNotifier<List<TorrentResource>, String> {
String? mediaId;
@override
FutureOr<List<TorrentResource>> build(String id) async {
mediaId = id;
final dio = await APIs.getDio();
var resp = await dio.get(APIs.availableMoviesUrl + id);
var rsp = ServerResponse.fromJson(resp.data);
if (rsp.code != 0) {
throw rsp.message;
}
return (rsp.data as List).map((v) => TorrentResource.fromJson(v)).toList();
}
Future<void> download(TorrentResource res) async {
var m = res.toJson();
m["media_id"] = int.parse(mediaId!);
final dio = await APIs.getDio();
var resp = await dio.post(APIs.availableMoviesUrl, data: m);
var rsp = ServerResponse.fromJson(resp.data);
if (rsp.code != 0) {
throw rsp.message;
}
}
}
class TorrentResource {
TorrentResource({this.name, this.size, this.seeders, this.peers, this.link});
String? name;
int? size;
int? seeders;
int? peers;
String? link;
factory TorrentResource.fromJson(Map<String, dynamic> json) {
return TorrentResource(
name: json["name"],
size: json["size"],
seeders: json["seeders"],
peers: json["peers"],
link: json["link"]);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['name'] = name;
data['size'] = size;
data["link"] = link;
return data;
}
}

View File

@@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:ui/providers/APIs.dart';
import 'package:ui/providers/settings.dart';
import 'package:ui/providers/welcome_data.dart';
import 'package:ui/utils.dart';
import 'package:ui/widgets/utils.dart';
import 'package:ui/widgets/progress_indicator.dart';
class SearchPage extends ConsumerStatefulWidget {
@@ -29,6 +29,15 @@ class _SearchPageState extends ConsumerState<SearchPage> {
List<Widget> res = searchList.when(
data: (data) {
if (data.isEmpty) {
return [Container(
height: MediaQuery.of(context).size.height * 0.6,
alignment: Alignment.center,
child: const Text(
"啥都没有...",
style: TextStyle(fontSize: 16),
))];
}
var cards = List<Widget>.empty(growable: true);
for (final item in data) {
cards.add(Card(
@@ -50,7 +59,6 @@ class _SearchPageState extends ConsumerState<SearchPage> {
child: Image.network(
"${APIs.tmdbImgBaseUrl}${item.posterPath}",
fit: BoxFit.contain,
headers: APIs.authHeaders,
),
),
),
@@ -146,6 +154,8 @@ class _SearchPageState extends ConsumerState<SearchPage> {
int storageSelected = 0;
var storage = ref.watch(storageSettingProvider);
var name = ref.watch(suggestNameDataProvider(item.id!));
bool downloadHistoryEpisodes = false;
bool buttonTapped = false;
var pathController = TextEditingController();
return AlertDialog(
@@ -230,6 +240,19 @@ class _SearchPageState extends ConsumerState<SearchPage> {
),
)
: Text(""),
item.mediaType == "tv"
? SizedBox(
width: 250,
child: CheckboxListTile(
title: const Text("是否下载往期剧集"),
value: downloadHistoryEpisodes,
onChanged: (v) {
setState(() {
downloadHistoryEpisodes = v!;
});
}),
)
: const SizedBox(),
],
);
});
@@ -254,18 +277,33 @@ class _SearchPageState extends ConsumerState<SearchPage> {
textStyle: Theme.of(context).textTheme.labelLarge,
),
child: const Text('确定'),
onPressed: () {
ref
onPressed: () async {
if (buttonTapped) {
return;
}
setState(() {
buttonTapped = true;
});
await ref
.read(searchPageDataProvider(widget.query ?? "")
.notifier)
.submit2Watchlist(item.id!, storageSelected,
resSelected, item.mediaType!, pathController.text)
.submit2Watchlist(
item.id!,
storageSelected,
resSelected,
item.mediaType!,
pathController.text,
downloadHistoryEpisodes)
.then((v) {
Utils.showSnakeBar("添加成功");
Navigator.of(context).pop();
}).onError((error, trace) {
Utils.showSnakeBar("添加失败:$error");
});
setState(() {
buttonTapped = false;
});
},
),
],

69
ui/lib/settings.dart Normal file
View File

@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ui/settings/auth.dart';
import 'package:ui/settings/downloader.dart';
import 'package:ui/settings/general.dart';
import 'package:ui/settings/indexer.dart';
import 'package:ui/settings/notifier.dart';
import 'package:ui/settings/storage.dart';
class SystemSettingsPage extends ConsumerStatefulWidget {
static const route = "/settings";
const SystemSettingsPage({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() {
return _SystemSettingsPageState();
}
}
class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
@override
Widget build(BuildContext context) {
return ListView(
children: const [
ExpansionTile(
expandedAlignment: Alignment.centerLeft,
childrenPadding: EdgeInsets.fromLTRB(20, 0, 20, 0),
initiallyExpanded: true,
title: Text("常规"),
children: [GeneralSettings()],
),
ExpansionTile(
expandedAlignment: Alignment.centerLeft,
childrenPadding: EdgeInsets.fromLTRB(20, 0, 20, 0),
initiallyExpanded: false,
title: Text("索引器"),
children: [IndexerSettings()],
),
ExpansionTile(
expandedAlignment: Alignment.centerLeft,
childrenPadding: EdgeInsets.fromLTRB(20, 0, 20, 0),
initiallyExpanded: false,
title: Text("下载器"),
children: [DownloaderSettings()],
),
ExpansionTile(
expandedAlignment: Alignment.centerLeft,
childrenPadding: EdgeInsets.fromLTRB(20, 0, 50, 0),
initiallyExpanded: false,
title: Text("存储"),
children: [StorageSettings()],
),
ExpansionTile(
expandedAlignment: Alignment.centerLeft,
childrenPadding: EdgeInsets.fromLTRB(20, 0, 50, 0),
initiallyExpanded: false,
title: Text("通知客户端"),
children: [NotifierSettings()],
),
ExpansionTile(
childrenPadding: EdgeInsets.fromLTRB(20, 0, 20, 0),
initiallyExpanded: false,
title: Text("认证"),
children: [AuthSettings()],
),
],
);
}
}

102
ui/lib/settings/auth.dart Normal file
View File

@@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:ui/providers/login.dart';
import 'package:ui/widgets/progress_indicator.dart';
import 'package:ui/widgets/utils.dart';
import 'package:ui/widgets/widgets.dart';
class AuthSettings extends ConsumerStatefulWidget {
static const route = "/settings";
const AuthSettings({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() {
return _AuthState();
}
}
class _AuthState extends ConsumerState<AuthSettings> {
final _formKey2 = GlobalKey<FormBuilderState>();
bool? _enableAuth;
@override
Widget build(BuildContext context) {
var authData = ref.watch(authSettingProvider);
return authData.when(
data: (data) {
if (_enableAuth == null) {
setState(() {
_enableAuth = data.enable;
});
}
return FormBuilder(
key: _formKey2,
initialValue: {
"user": data.user,
"password": data.password,
"enable": data.enable
},
child: Column(
children: [
FormBuilderSwitch(
name: "enable",
title: const Text("开启认证"),
onChanged: (v) {
setState(() {
_enableAuth = v;
});
}),
_enableAuth!
? Column(
children: [
FormBuilderTextField(
name: "user",
autovalidateMode:
AutovalidateMode.onUserInteraction,
validator: FormBuilderValidators.required(),
decoration: Commons.requiredTextFieldStyle(
text: "用户名",
icon: const Icon(Icons.account_box),
)),
FormBuilderTextField(
name: "password",
obscureText: true,
enableSuggestions: false,
autocorrect: false,
autovalidateMode:
AutovalidateMode.onUserInteraction,
validator: FormBuilderValidators.required(),
decoration: Commons.requiredTextFieldStyle(
text: "密码",
icon: const Icon(Icons.password),
))
],
)
: const Column(),
Center(
child: ElevatedButton(
child: const Text("保存"),
onPressed: () {
if (_formKey2.currentState!.saveAndValidate()) {
var values = _formKey2.currentState!.value;
var f = ref
.read(authSettingProvider.notifier)
.updateAuthSetting(_enableAuth!,
values["user"], values["password"]);
f.then((v) {
Utils.showSnakeBar("更新成功");
}).onError((e, s) {
Utils.showSnakeBar("更新失败:$e");
});
}
}))
],
));
},
error: (err, trace) => Text("$err"),
loading: () => const MyProgressIndicator());
}
}

View File

@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:ui/widgets/utils.dart';
Future<void> showSettingDialog(BuildContext context,String title, bool showDelete, Widget body,
Future Function() onSubmit, Future Function() onDelete) {
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
title: Text(title),
content: SingleChildScrollView(
child: SizedBox(
width: 300,
child: body,
),
),
actions: <Widget>[
showDelete
? TextButton(
onPressed: () {
final f = onDelete();
f.then((v) {
Utils.showSnakeBar("删除成功");
Navigator.of(context).pop();
}).onError((e, s) {
Utils.showSnakeBar("删除失败:$e");
});
},
child: const Text(
'删除',
style: TextStyle(color: Colors.red),
))
: const Text(""),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消')),
TextButton(
child: const Text('确定'),
onPressed: () {
final f = onSubmit();
f.then((v) {
Utils.showSnakeBar("操作成功");
Navigator.of(context).pop();
}).onError((e, s) {
if (e.toString() != "validation_error") {
Utils.showSnakeBar("操作失败:$e");
}
});
},
),
],
);
});
}

View File

@@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:quiver/strings.dart';
import 'package:ui/providers/settings.dart';
import 'package:ui/settings/dialog.dart';
import 'package:ui/widgets/progress_indicator.dart';
import 'package:ui/widgets/widgets.dart';
class DownloaderSettings extends ConsumerStatefulWidget {
static const route = "/settings";
const DownloaderSettings({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() {
return _DownloaderState();
}
}
class _DownloaderState extends ConsumerState<DownloaderSettings> {
@override
Widget build(BuildContext context) {
var downloadClients = ref.watch(dwonloadClientsProvider);
return downloadClients.when(
data: (value) => Wrap(
children: List.generate(value.length + 1, (i) {
if (i < value.length) {
var client = value[i];
return SettingsCard(
onTap: () => showDownloadClientDetails(client),
child: Text(client.name ?? ""));
}
return SettingsCard(
onTap: () => showDownloadClientDetails(DownloadClient()),
child: const Icon(Icons.add));
})),
error: (err, trace) => Text("$err"),
loading: () => const MyProgressIndicator());
}
Future<void> showDownloadClientDetails(DownloadClient client) {
final _formKey = GlobalKey<FormBuilderState>();
var _enableAuth = isNotBlank(client.user);
String selectImpl = "transmission";
final body =
StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return FormBuilder(
key: _formKey,
initialValue: {
"name": client.name,
"url": client.url,
"user": client.user,
"password": client.password,
"impl": "transmission"
},
child: Column(
children: [
FormBuilderDropdown<String>(
name: "impl",
decoration: const InputDecoration(labelText: "类型"),
onChanged: (value) {
setState(() {
selectImpl = value!;
});
},
items: const [
DropdownMenuItem(
value: "transmission", child: Text("Transmission")),
],
),
FormBuilderTextField(
name: "name",
decoration: const InputDecoration(labelText: "名称"),
validator: FormBuilderValidators.required(),
autovalidateMode: AutovalidateMode.onUserInteraction),
FormBuilderTextField(
name: "url",
decoration: const InputDecoration(
labelText: "地址", hintText: "http://127.0.0.1:9091"),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: FormBuilderValidators.required(),
),
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Column(
children: [
FormBuilderSwitch(
name: "auth",
title: const Text("需要认证"),
initialValue: _enableAuth,
onChanged: (v) {
setState(() {
_enableAuth = v!;
});
}),
_enableAuth
? Column(
children: [
FormBuilderTextField(
name: "user",
decoration: Commons.requiredTextFieldStyle(
text: "用户"),
validator: FormBuilderValidators.required(),
autovalidateMode:
AutovalidateMode.onUserInteraction),
FormBuilderTextField(
name: "password",
decoration: Commons.requiredTextFieldStyle(
text: "密码"),
validator: FormBuilderValidators.required(),
obscureText: true,
autovalidateMode:
AutovalidateMode.onUserInteraction),
],
)
: Container()
],
);
})
],
));
});
onDelete() async {
return ref
.read(dwonloadClientsProvider.notifier)
.deleteDownloadClients(client.id!);
}
onSubmit() async {
if (_formKey.currentState!.saveAndValidate()) {
var values = _formKey.currentState!.value;
return ref.read(dwonloadClientsProvider.notifier).addDownloadClients(
DownloadClient(
name: values["name"],
implementation: values["impl"],
url: values["url"],
user: _enableAuth ? values["user"] : null,
password: _enableAuth ? values["password"] : null));
} else {
throw "validation_error";
}
}
return showSettingDialog(
context, "下载器", client.id != null, body, onSubmit, onDelete);
}
}

View File

@@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:ui/providers/settings.dart';
import 'package:ui/widgets/utils.dart';
import 'package:ui/widgets/progress_indicator.dart';
import 'package:ui/widgets/widgets.dart';
class GeneralSettings extends ConsumerStatefulWidget {
static const route = "/settings";
const GeneralSettings({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() {
return _GeneralState();
}
}
class _GeneralState extends ConsumerState<GeneralSettings> {
final _formKey = GlobalKey<FormBuilderState>();
@override
Widget build(BuildContext context) {
var settings = ref.watch(settingProvider);
return settings.when(
data: (v) {
return FormBuilder(
key: _formKey, //设置globalKey用于后面获取FormState
autovalidateMode: AutovalidateMode.onUserInteraction,
initialValue: {
"tmdb_api": v.tmdbApiKey,
"download_dir": v.downloadDIr,
"log_level": v.logLevel,
"proxy": v.proxy,
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FormBuilderTextField(
name: "tmdb_api",
decoration: Commons.requiredTextFieldStyle(
text: "TMDB Api Key", icon: const Icon(Icons.key)),
//
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "download_dir",
decoration: Commons.requiredTextFieldStyle(
text: "下载路径",
icon: const Icon(Icons.folder),
helperText: "媒体文件临时下载路径,非最终存储路径"),
//
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "proxy",
decoration: const InputDecoration(
labelText: "代理地址",
icon: Icon(Icons.folder),
helperText: "后台联网代理地址,留空表示不启用代理"),
),
SizedBox(
width: 300,
child: FormBuilderDropdown(
name: "log_level",
decoration: const InputDecoration(
labelText: "日志级别",
icon: Icon(Icons.file_present_rounded),
),
items: const [
DropdownMenuItem(value: "debug", child: Text("DEBUG")),
DropdownMenuItem(value: "info", child: Text("INFO")),
DropdownMenuItem(value: "warn", child: Text("WARN")),
DropdownMenuItem(value: "error", child: Text("ERROR")),
],
validator: FormBuilderValidators.required(),
),
),
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;
var f = ref
.read(settingProvider.notifier)
.updateSettings(GeneralSetting(
tmdbApiKey: values["tmdb_api"],
downloadDIr: values["download_dir"],
logLevel: values["log_level"],
proxy: values["proxy"]));
f.then((v) {
Utils.showSnakeBar("更新成功");
}).onError((e, s) {
Utils.showSnakeBar("更新失败:$e");
});
}
}),
),
)
],
),
);
},
error: (err, trace) => Text("$err"),
loading: () => const MyProgressIndicator());
}
}

View File

@@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:ui/providers/settings.dart';
import 'package:ui/settings/dialog.dart';
import 'package:ui/widgets/progress_indicator.dart';
import 'package:ui/widgets/widgets.dart';
class IndexerSettings extends ConsumerStatefulWidget {
const IndexerSettings({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() {
return _IndexerState();
}
}
class _IndexerState extends ConsumerState<IndexerSettings> {
@override
Widget build(BuildContext context) {
var indexers = ref.watch(indexersProvider);
return indexers.when(
data: (value) => Wrap(
children: List.generate(value.length + 1, (i) {
if (i < value.length) {
var indexer = value[i];
return SettingsCard(
onTap: () => showIndexerDetails(indexer),
child: Text(indexer.name ?? ""));
}
return SettingsCard(
onTap: () => showIndexerDetails(Indexer()),
child: const Icon(Icons.add));
}),
),
error: (err, trace) => Text("$err"),
loading: () => const MyProgressIndicator());
}
Future<void> showIndexerDetails(Indexer indexer) {
final _formKey = GlobalKey<FormBuilderState>();
var body = FormBuilder(
key: _formKey,
initialValue: {
"name": indexer.name,
"url": indexer.url,
"api_key": indexer.apiKey,
"impl": "torznab"
},
child: Column(
children: [
FormBuilderDropdown(
name: "impl",
decoration: const InputDecoration(labelText: "类型"),
items: const [
DropdownMenuItem(value: "torznab", child: Text("Torznab")),
],
),
FormBuilderTextField(
name: "name",
decoration: Commons.requiredTextFieldStyle(text: "名称"),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "url",
decoration: Commons.requiredTextFieldStyle(text: "地址"),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "api_key",
decoration: Commons.requiredTextFieldStyle(text: "API Key"),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: FormBuilderValidators.required(),
),
],
),
);
onDelete() async {
return ref.read(indexersProvider.notifier).deleteIndexer(indexer.id!);
}
onSubmit() async {
if (_formKey.currentState!.saveAndValidate()) {
var values = _formKey.currentState!.value;
return ref.read(indexersProvider.notifier).addIndexer(Indexer(
name: values["name"],
url: values["url"],
apiKey: values["api_key"]));
} else {
throw "validation_error";
}
}
return showSettingDialog(
context, "索引器", indexer.id != null, body, onSubmit, onDelete);
}
}

View File

@@ -0,0 +1,224 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:ui/providers/notifier.dart';
import 'package:ui/settings/dialog.dart';
import 'package:ui/widgets/progress_indicator.dart';
import 'package:ui/widgets/widgets.dart';
class NotifierSettings extends ConsumerStatefulWidget {
static const route = "/settings";
const NotifierSettings({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() {
return _NotifierState();
}
}
class _NotifierState extends ConsumerState<NotifierSettings> {
@override
Widget build(BuildContext context) {
final notifierData = ref.watch(notifiersDataProvider);
return notifierData.when(
data: (v) => Wrap(
children: List.generate(v.length + 1, (i) {
if (i < v.length) {
final client = v[i];
return SettingsCard(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
client.name!,
style: TextStyle(fontSize: 20, height: 3),
),
Opacity(
opacity: 0.5,
child: Text(client.service!),
)
],
),
onTap: () => showNotifierAccordingToService(client),
);
}
return SettingsCard(
onTap: () => showSelections(),
child: const Icon(Icons.add));
}),
),
error: (err, trace) => Text("$err"),
loading: () => const MyProgressIndicator());
}
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("Pushover"),
),
onTap: () {
Navigator.of(context).pop();
showPushoverNotifierDetails(NotifierData());
},
),
),
SettingsCard(
child: InkWell(
child: const Center(
child: Text("Bark"),
),
onTap: () {
Navigator.of(context).pop();
showBarkNotifierDetails(NotifierData());
},
),
)
],
),
),
);
});
}
Future<void> showNotifierAccordingToService(NotifierData notifier) {
switch (notifier.service) {
case "bark":
return showBarkNotifierDetails(notifier);
case "pushover":
return showPushoverNotifierDetails(notifier);
}
return Future<void>.value();
}
Future<void> showBarkNotifierDetails(NotifierData notifier) {
final _formKey = GlobalKey<FormBuilderState>();
var body = FormBuilder(
key: _formKey,
initialValue: {
"name": notifier.name,
"enabled": notifier.enabled ?? true,
"device_key":
notifier.settings != null ? notifier.settings!["device_key"] : "",
"url": notifier.settings != null ? notifier.settings!["url"] : "",
},
child: Column(
children: [
FormBuilderTextField(
name: "name",
decoration: Commons.requiredTextFieldStyle(text: "名称"),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "url",
decoration: const InputDecoration(
labelText: "服务器地址", helperText: "留空使用默认地址"),
),
FormBuilderTextField(
name: "device_key",
decoration: Commons.requiredTextFieldStyle(text: "Device Key"),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: FormBuilderValidators.required(),
),
FormBuilderSwitch(name: "enabled", title: const Text("启用"))
],
),
);
onDelete() async {
return ref.read(notifiersDataProvider.notifier).delete(notifier.id!);
}
onSubmit() async {
if (_formKey.currentState!.saveAndValidate()) {
var values = _formKey.currentState!.value;
return ref.read(notifiersDataProvider.notifier).add(NotifierData(
name: values["name"],
service: "bark",
enabled: values["enabled"],
settings: {
"device_key": values["device_key"],
"url": values["url"]
}));
} else {
throw "validation_error";
}
}
return showSettingDialog(
context, "Bark", notifier.id != null, body, onSubmit, onDelete);
}
Future<void> showPushoverNotifierDetails(NotifierData notifier) {
final _formKey = GlobalKey<FormBuilderState>();
var body = FormBuilder(
key: _formKey,
initialValue: {
"name": notifier.name,
"enabled": notifier.enabled ?? true,
"app_token":
notifier.settings != null ? notifier.settings!["app_token"] : "",
"user_key":
notifier.settings != null ? notifier.settings!["user_key"] : "",
},
child: Column(
children: [
FormBuilderTextField(
name: "name",
decoration: Commons.requiredTextFieldStyle(text: "名称"),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "app_token",
decoration: Commons.requiredTextFieldStyle(text: "APP密钥"),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "user_key",
decoration: Commons.requiredTextFieldStyle(text: "用户密钥"),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: FormBuilderValidators.required(),
),
FormBuilderSwitch(name: "enabled", title: const Text("启用"))
],
),
);
onDelete() async {
return ref.read(notifiersDataProvider.notifier).delete(notifier.id!);
}
onSubmit() async {
if (_formKey.currentState!.saveAndValidate()) {
var values = _formKey.currentState!.value;
return ref.read(notifiersDataProvider.notifier).add(NotifierData(
name: values["name"],
service: "pushover",
enabled: values["enabled"],
settings: {
"app_token": values["app_token"],
"user_key": values["user_key"]
}));
} else {
throw "validation_error";
}
}
return showSettingDialog(
context, "Pushover", notifier.id != null, body, onSubmit, onDelete);
}
}

View File

@@ -0,0 +1,174 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:ui/providers/settings.dart';
import 'package:ui/settings/dialog.dart';
import 'package:ui/widgets/progress_indicator.dart';
import 'package:ui/widgets/widgets.dart';
class StorageSettings extends ConsumerStatefulWidget {
static const route = "/settings";
const StorageSettings({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() {
return _StorageState();
}
}
class _StorageState extends ConsumerState<StorageSettings> {
@override
Widget build(BuildContext context) {
var storageSettingData = ref.watch(storageSettingProvider);
return storageSettingData.when(
data: (value) => Wrap(
children: List.generate(value.length + 1, (i) {
if (i < value.length) {
var storage = value[i];
return SettingsCard(
onTap: () => showStorageDetails(storage),
child: Text(storage.name ?? ""));
}
return SettingsCard(
onTap: () => showStorageDetails(Storage()),
child: const Icon(Icons.add));
}),
),
error: (err, trace) => Text("$err"),
loading: () => const MyProgressIndicator());
}
Future<void> showStorageDetails(Storage s) {
final _formKey = GlobalKey<FormBuilderState>();
String selectImpl = s.implementation == null ? "local" : s.implementation!;
final widgets =
StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return FormBuilder(
key: _formKey,
autovalidateMode: AutovalidateMode.disabled,
initialValue: {
"name": s.name,
"impl": s.implementation == null ? "local" : s.implementation!,
"user": s.settings != null ? s.settings!["user"] ?? "" : "",
"password": s.settings != null ? s.settings!["password"] ?? "" : "",
"tv_path": s.settings != null ? s.settings!["tv_path"] ?? "" : "",
"url": s.settings != null ? s.settings!["url"] ?? "" : "",
"movie_path":
s.settings != null ? s.settings!["movie_path"] ?? "" : "",
"change_file_hash": s.settings != null
? s.settings!["change_file_hash"] == "true"
? true
: false
: false,
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
FormBuilderDropdown<String>(
name: "impl",
autovalidateMode: AutovalidateMode.onUserInteraction,
decoration: const InputDecoration(labelText: "类型"),
onChanged: (value) {
setState(() {
selectImpl = value!;
});
},
items: const [
DropdownMenuItem(
value: "local",
child: Text("本地存储"),
),
DropdownMenuItem(
value: "webdav",
child: Text("webdav"),
)
],
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "name",
autovalidateMode: AutovalidateMode.onUserInteraction,
initialValue: s.name,
decoration: const InputDecoration(labelText: "名称"),
validator: FormBuilderValidators.required(),
),
selectImpl != "local"
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FormBuilderTextField(
name: "url",
autovalidateMode: AutovalidateMode.onUserInteraction,
decoration:
const InputDecoration(labelText: "Webdav地址"),
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "user",
autovalidateMode: AutovalidateMode.onUserInteraction,
decoration: const InputDecoration(labelText: "用户"),
),
FormBuilderTextField(
name: "password",
autovalidateMode: AutovalidateMode.onUserInteraction,
decoration: const InputDecoration(labelText: "密码"),
obscureText: true,
),
FormBuilderCheckbox(
name: "change_file_hash",
title: const Text(
"上传时更改文件哈希",
style: TextStyle(fontSize: 14),
),
),
],
)
: Container(),
FormBuilderTextField(
name: "tv_path",
autovalidateMode: AutovalidateMode.onUserInteraction,
decoration: const InputDecoration(labelText: "电视剧路径"),
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "movie_path",
autovalidateMode: AutovalidateMode.onUserInteraction,
decoration: const InputDecoration(labelText: "电影路径"),
validator: FormBuilderValidators.required(),
)
],
));
});
onSubmit() async {
if (_formKey.currentState!.saveAndValidate()) {
final values = _formKey.currentState!.value;
return ref.read(storageSettingProvider.notifier).addStorage(Storage(
name: values["name"],
implementation: selectImpl,
settings: {
"tv_path": values["tv_path"],
"movie_path": values["movie_path"],
"url": values["url"],
"user": values["user"],
"password": values["password"],
"change_file_hash":
(values["change_file_hash"] ?? false) as bool
? "true"
: "false"
},
));
} else {
throw "validation_error";
}
}
onDelete() async {
return ref.read(storageSettingProvider.notifier).deleteStorage(s.id!);
}
return showSettingDialog(context,'存储', s.id != null, widgets, onSubmit, onDelete);
}
}

139
ui/lib/system_page.dart Normal file
View File

@@ -0,0 +1,139 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ui/providers/APIs.dart';
import 'package:ui/providers/settings.dart';
import 'package:ui/widgets/utils.dart';
import 'package:ui/widgets/progress_indicator.dart';
import 'package:url_launcher/url_launcher.dart';
class SystemPage extends ConsumerStatefulWidget {
static const route = "/system";
const SystemPage({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() {
return _SystemPageState();
}
}
class _SystemPageState extends ConsumerState<SystemPage> {
@override
Widget build(BuildContext context) {
final logs = ref.watch(logFileDataProvider);
final about = ref.watch(aboutDataProvider);
return SingleChildScrollView(
child: Column(
children: [
ExpansionTile(
expandedCrossAxisAlignment: CrossAxisAlignment.stretch,
initiallyExpanded: true,
childrenPadding: EdgeInsets.all(20),
title: Text("日志"),
children: [
logs.when(
data: (list) {
return DataTable(
columns: const [
DataColumn(label: Text("日志")),
DataColumn(label: Text("大小")),
DataColumn(label: Text("下载"))
],
rows: List.generate(list.length, (i) {
final item = list[i];
final uri =
Uri.parse("${APIs.logsBaseUrl}${item.name}");
return DataRow(cells: [
DataCell(Text(item.name ?? "")),
DataCell(Text((item.size ?? 0).readableFileSize())),
DataCell(InkWell(
child: const Icon(Icons.download),
onTap: () => launchUrl(uri),
))
]);
}));
},
error: (err, trace) => Text("$err"),
loading: () => const MyProgressIndicator())
],
),
ExpansionTile(
title: Text("关于"),
expandedCrossAxisAlignment: CrossAxisAlignment.center,
initiallyExpanded: true,
children: [
about.when(
data: (v) {
final uri = Uri.parse(v.chatGroup ?? "");
final homepage = Uri.parse(v.homepage ?? "");
return Row(
children: [
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
SizedBox(
height: 20,
),
Text(
"#",
style: TextStyle(height: 2.5),
),
Text("版本", style: TextStyle(height: 2.5)),
Text("主页", style: TextStyle(height: 2.5)),
Text("讨论组", style: TextStyle(height: 2.5)),
Text("go version", style: TextStyle(height: 2.5)),
Text("uptime", style: TextStyle(height: 2.5)),
SizedBox(
height: 20,
),
],
)),
const SizedBox(
width: 20,
),
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 20,
),
Text(v.intro ?? "",
style: const TextStyle(height: 2.5)),
Text(v.version ?? "",
style: const TextStyle(height: 2.5)),
InkWell(
child: Text(v.homepage ?? "",
softWrap: false,
style: const TextStyle(height: 2.5)),
onTap: () => launchUrl(homepage),
),
InkWell(
child: const Text("Telegram",
style: TextStyle(height: 2.5)),
onTap: () => launchUrl(uri),
),
Text("${v.goVersion}",
style: const TextStyle(height: 2.5)),
Text("${v.uptime}",
style: const TextStyle(height: 2.5)),
const SizedBox(
height: 20,
),
],
)),
],
);
},
error: (err, trace) => Text("$err"),
loading: () => const MyProgressIndicator())
],
)
],
),
);
}
}

View File

@@ -1,617 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:quiver/strings.dart';
import 'package:ui/providers/login.dart';
import 'package:ui/providers/settings.dart';
import 'package:ui/utils.dart';
import 'package:ui/widgets/progress_indicator.dart';
import 'package:ui/widgets/widgets.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
class SystemSettingsPage extends ConsumerStatefulWidget {
static const route = "/settings";
const SystemSettingsPage({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() {
return _SystemSettingsPageState();
}
}
class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
final _formKey = GlobalKey<FormBuilderState>();
final _formKey2 = GlobalKey<FormBuilderState>();
bool? _enableAuth;
@override
Widget build(BuildContext context) {
var settings = ref.watch(settingProvider);
var tmdbSetting = settings.when(
data: (v) {
return Container(
padding: const EdgeInsets.fromLTRB(40, 10, 40, 0),
child: FormBuilder(
key: _formKey, //设置globalKey用于后面获取FormState
autovalidateMode: AutovalidateMode.onUserInteraction,
initialValue: {
"tmdb_api": v.tmdbApiKey,
"download_dir": v.downloadDIr
},
child: Column(
children: [
FormBuilderTextField(
name: "tmdb_api",
autofocus: true,
decoration: Commons.requiredTextFieldStyle(
text: "TMDB Api Key", icon: const Icon(Icons.key)),
//
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "download_dir",
autofocus: true,
decoration: Commons.requiredTextFieldStyle(
text: "下载路径", icon: const Icon(Icons.folder)),
//
validator: FormBuilderValidators.required(),
),
Center(
child: Padding(
padding: const EdgeInsets.only(top: 28.0),
child: ElevatedButton(
child: const Padding(
padding: EdgeInsets.all(16.0),
child: Text("保存"),
),
onPressed: () {
if (_formKey.currentState!.saveAndValidate()) {
var values = _formKey.currentState!.value;
var f = ref
.read(settingProvider.notifier)
.updateSettings(GeneralSetting(
tmdbApiKey: values["tmdb_api"],
downloadDIr: values["download_dir"]));
f.then((v) {
Utils.showSnakeBar("更新成功");
}).onError((e, s) {
Utils.showSnakeBar("更新失败:$e");
});
}
}),
),
)
],
),
));
},
error: (err, trace) => Text("$err"),
loading: () => const MyProgressIndicator());
var indexers = ref.watch(indexersProvider);
var indexerSetting = indexers.when(
data: (value) => Wrap(
children: List.generate(value.length + 1, (i) {
if (i < value.length) {
var indexer = value[i];
return SettingsCard(
onTap: () => showIndexerDetails(indexer),
child: Text(indexer.name ?? ""));
}
return SettingsCard(
onTap: () => showIndexerDetails(Indexer()),
child: const Icon(Icons.add));
}),
),
error: (err, trace) => Text("$err"),
loading: () => const MyProgressIndicator());
var downloadClients = ref.watch(dwonloadClientsProvider);
var downloadSetting = downloadClients.when(
data: (value) => Wrap(
children: List.generate(value.length + 1, (i) {
if (i < value.length) {
var client = value[i];
return SettingsCard(
onTap: () => showDownloadClientDetails(client),
child: Text(client.name ?? ""));
}
return SettingsCard(
onTap: () => showDownloadClientDetails(DownloadClient()),
child: const Icon(Icons.add));
})),
error: (err, trace) => Text("$err"),
loading: () => const MyProgressIndicator());
var storageSettingData = ref.watch(storageSettingProvider);
var storageSetting = storageSettingData.when(
data: (value) => Wrap(
children: List.generate(value.length + 1, (i) {
if (i < value.length) {
var storage = value[i];
return SettingsCard(
onTap: () => showStorageDetails(storage),
child: Text(storage.name ?? ""));
}
return SettingsCard(
onTap: () => showStorageDetails(Storage()),
child: const Icon(Icons.add));
}),
),
error: (err, trace) => Text("$err"),
loading: () => const MyProgressIndicator());
var authData = ref.watch(authSettingProvider);
TextEditingController userController = TextEditingController();
TextEditingController passController = TextEditingController();
var authSetting = authData.when(
data: (data) {
if (_enableAuth == null) {
setState(() {
_enableAuth = data.enable;
});
}
userController.text = data.user;
return FormBuilder(
key: _formKey2,
initialValue: {
"user": data.user,
"password": data.password,
"enable": data.enable
},
child: Column(
children: [
FormBuilderSwitch(
name: "enable",
title: const Text("开启认证"),
onChanged: (v) {
setState(() {
_enableAuth = v;
});
}),
_enableAuth!
? Column(
children: [
FormBuilderTextField(
name: "user",
autovalidateMode:
AutovalidateMode.onUserInteraction,
validator: FormBuilderValidators.required(),
decoration: Commons.requiredTextFieldStyle(
text: "用户名",
icon: const Icon(Icons.account_box),
)),
FormBuilderTextField(
name: "password",
obscureText: true,
enableSuggestions: false,
autocorrect: false,
autovalidateMode:
AutovalidateMode.onUserInteraction,
validator: FormBuilderValidators.required(),
decoration: Commons.requiredTextFieldStyle(
text: "密码",
icon: const Icon(Icons.password),
))
],
)
: const Column(),
Center(
child: ElevatedButton(
child: const Text("保存"),
onPressed: () {
if (_formKey2.currentState!.saveAndValidate()) {
var values = _formKey2.currentState!.value;
var f = ref
.read(authSettingProvider.notifier)
.updateAuthSetting(_enableAuth!,
values["user"], values["password"]);
f.then((v) {
Utils.showSnakeBar("更新成功");
}).onError((e, s) {
Utils.showSnakeBar("更新失败:$e");
});
}
}))
],
));
},
error: (err, trace) => Text("$err"),
loading: () => const MyProgressIndicator());
return ListView(
children: [
ExpansionTile(
expandedAlignment: Alignment.centerLeft,
tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
initiallyExpanded: true,
title: const Text("常规设置"),
children: [tmdbSetting],
),
ExpansionTile(
expandedAlignment: Alignment.centerLeft,
tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
initiallyExpanded: false,
title: const Text("索引器设置"),
children: [indexerSetting],
),
ExpansionTile(
expandedAlignment: Alignment.centerLeft,
tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
initiallyExpanded: false,
title: const Text("下载器设置"),
children: [downloadSetting],
),
ExpansionTile(
expandedAlignment: Alignment.centerLeft,
tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
initiallyExpanded: false,
title: const Text("存储设置"),
children: [storageSetting],
),
ExpansionTile(
tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
initiallyExpanded: false,
title: const Text("认证设置"),
children: [authSetting],
),
],
);
}
Future<void> showIndexerDetails(Indexer indexer) {
final _formKey = GlobalKey<FormBuilderState>();
var selectImpl = "torznab";
var body = FormBuilder(
key: _formKey,
initialValue: {
"name": indexer.name,
"url": indexer.url,
"api_key": indexer.apiKey,
"impl": "torznab"
},
child: Column(
children: [
FormBuilderDropdown(
name: "impl",
decoration: const InputDecoration(labelText: "类型"),
items: const [
DropdownMenuItem(value: "torznab", child: Text("Torznab")),
],
),
FormBuilderTextField(
name: "name",
decoration: Commons.requiredTextFieldStyle(text: "名称"),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "url",
decoration: Commons.requiredTextFieldStyle(text: "地址"),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "api_key",
decoration: Commons.requiredTextFieldStyle(text: "API Key"),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: FormBuilderValidators.required(),
),
],
),
);
onDelete() async {
return ref.read(indexersProvider.notifier).deleteIndexer(indexer.id!);
}
onSubmit() async {
if (_formKey.currentState!.saveAndValidate()) {
var values = _formKey.currentState!.value;
return ref.read(indexersProvider.notifier).addIndexer(Indexer(
name: values["name"],
url: values["url"],
apiKey: values["api_key"]));
} else {
throw "数据校验失败";
}
}
return showSettingDialog(
"索引器", indexer.id != null, body, onSubmit, onDelete);
}
Future<void> showDownloadClientDetails(DownloadClient client) {
final _formKey = GlobalKey<FormBuilderState>();
var _enableAuth = isNotBlank(client.user);
String selectImpl = "transmission";
final body =
StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return FormBuilder(
key: _formKey,
initialValue: {
"name": client.name,
"url": client.url,
"user": client.user,
"password": client.password,
"impl": "transmission"
},
child: Column(
children: [
FormBuilderDropdown<String>(
name: "impl",
decoration: const InputDecoration(labelText: "类型"),
onChanged: (value) {
setState(() {
selectImpl = value!;
});
},
items: const [
DropdownMenuItem(
value: "transmission", child: Text("Transmission")),
],
),
FormBuilderTextField(
name: "name",
decoration: const InputDecoration(labelText: "名称"),
validator: FormBuilderValidators.required(),
autovalidateMode: AutovalidateMode.onUserInteraction),
FormBuilderTextField(
name: "url",
decoration: const InputDecoration(
labelText: "地址", hintText: "http://127.0.0.1:9091"),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: FormBuilderValidators.required(),
),
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Column(
children: [
FormBuilderSwitch(
name: "auth",
title: const Text("需要认证"),
initialValue: _enableAuth,
onChanged: (v) {
setState(() {
_enableAuth = v!;
});
}),
_enableAuth
? Column(
children: [
FormBuilderTextField(
name: "user",
decoration: Commons.requiredTextFieldStyle(
text: "用户"),
validator: FormBuilderValidators.required(),
autovalidateMode:
AutovalidateMode.onUserInteraction),
FormBuilderTextField(
name: "password",
decoration: Commons.requiredTextFieldStyle(
text: "密码"),
validator: FormBuilderValidators.required(),
obscureText: true,
autovalidateMode:
AutovalidateMode.onUserInteraction),
],
)
: Container()
],
);
})
],
));
});
onDelete() async {
return ref
.read(dwonloadClientsProvider.notifier)
.deleteDownloadClients(client.id!);
}
onSubmit() async {
if (_formKey.currentState!.saveAndValidate()) {
var values = _formKey.currentState!.value;
return ref.read(dwonloadClientsProvider.notifier).addDownloadClients(
DownloadClient(
name: values["name"],
implementation: values["impl"],
url: values["url"],
user: _enableAuth ? values["user"] : null,
password: _enableAuth ? values["password"] : null));
} else {
throw "数据校验不通过";
}
}
return showSettingDialog(
"下载器", client.id != null, body, onSubmit, onDelete);
}
Future<void> showStorageDetails(Storage s) {
final _formKey = GlobalKey<FormBuilderState>();
String selectImpl = s.implementation == null ? "local" : s.implementation!;
final widgets =
StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return FormBuilder(
key: _formKey,
autovalidateMode: AutovalidateMode.disabled,
initialValue: {
"name": s.name,
"impl": s.implementation == null ? "local" : s.implementation!,
"user": s.settings != null ? s.settings!["user"] ?? "" : "",
"password": s.settings != null ? s.settings!["password"] ?? "" : "",
"tv_path": s.settings != null ? s.settings!["tv_path"] ?? "" : "",
"url": s.settings != null ? s.settings!["url"] ?? "" : "",
"movie_path":
s.settings != null ? s.settings!["movie_path"] ?? "" : "",
"change_file_hash": s.settings != null
? s.settings!["change_file_hash"] == "true"
? true
: false
: false,
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
FormBuilderDropdown<String>(
name: "impl",
autovalidateMode: AutovalidateMode.onUserInteraction,
decoration: const InputDecoration(labelText: "类型"),
onChanged: (value) {
setState(() {
selectImpl = value!;
});
},
items: const [
DropdownMenuItem(
value: "local",
child: Text("本地存储"),
),
DropdownMenuItem(
value: "webdav",
child: Text("webdav"),
)
],
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "name",
autovalidateMode: AutovalidateMode.onUserInteraction,
initialValue: s.name,
decoration: const InputDecoration(labelText: "名称"),
validator: FormBuilderValidators.required(),
),
selectImpl != "local"
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FormBuilderTextField(
name: "url",
autovalidateMode: AutovalidateMode.onUserInteraction,
decoration:
const InputDecoration(labelText: "Webdav地址"),
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "user",
autovalidateMode: AutovalidateMode.onUserInteraction,
decoration: const InputDecoration(labelText: "用户"),
),
FormBuilderTextField(
name: "password",
autovalidateMode: AutovalidateMode.onUserInteraction,
decoration: const InputDecoration(labelText: "密码"),
obscureText: true,
),
FormBuilderCheckbox(
name: "change_file_hash",
title: const Text(
"上传时更改文件哈希",
style: TextStyle(fontSize: 14),
),
),
],
)
: Container(),
FormBuilderTextField(
name: "tv_path",
autovalidateMode: AutovalidateMode.onUserInteraction,
decoration: const InputDecoration(labelText: "电视剧路径"),
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "movie_path",
autovalidateMode: AutovalidateMode.onUserInteraction,
decoration: const InputDecoration(labelText: "电影路径"),
validator: FormBuilderValidators.required(),
)
],
));
});
onSubmit() async {
if (_formKey.currentState!.saveAndValidate()) {
final values = _formKey.currentState!.value;
return ref.read(storageSettingProvider.notifier).addStorage(Storage(
name: values["name"],
implementation: selectImpl,
settings: {
"tv_path": values["tv_path"],
"movie_path": values["movie_path"],
"url": values["url"],
"user": values["user"],
"password": values["password"],
"change_file_hash":
values["change_file_hash"] as bool ? "true" : "false"
},
));
} else {
throw "数据校验位未通过";
}
}
onDelete() async {
return ref.read(storageSettingProvider.notifier).deleteStorage(s.id!);
}
return showSettingDialog('存储', s.id != null, widgets, onSubmit, onDelete);
}
Future<void> showSettingDialog(String title, bool showDelete, Widget body,
Future Function() onSubmit, Future Function() onDelete) {
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
title: Text(title),
content: SingleChildScrollView(
child: Container(
constraints: const BoxConstraints(maxWidth: 200),
child: body,
),
),
actions: <Widget>[
showDelete
? TextButton(
onPressed: () {
final f = onDelete();
f.then((v) {
Utils.showSnakeBar("删除成功");
Navigator.of(context).pop();
}).onError((e, s) {
Utils.showSnakeBar("删除失败:$e");
});
},
child: const Text(
'删除',
style: TextStyle(color: Colors.red),
))
: const Text(""),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消')),
TextButton(
child: const Text('确定'),
onPressed: () {
final f = onSubmit();
f.then((v) {
Utils.showSnakeBar("操作成功");
Navigator.of(context).pop();
}).onError((e, s) {
Utils.showSnakeBar("操作失败:$e");
});
},
),
],
);
});
}
}

View File

@@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:ui/providers/APIs.dart';
import 'package:ui/providers/series_details.dart';
import 'package:ui/providers/settings.dart';
import 'package:ui/utils.dart';
import 'package:ui/widgets/utils.dart';
import 'package:ui/welcome_page.dart';
import 'package:ui/widgets/progress_indicator.dart';
@@ -26,7 +26,6 @@ class TvDetailsPage extends ConsumerStatefulWidget {
}
class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
@override
void initState() {
super.initState();
@@ -45,7 +44,7 @@ class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
DataCell(Text("${ep.title}")),
DataCell(Opacity(
opacity: 0.5,
child: Text(ep.airDate??"-"),
child: Text(ep.airDate ?? "-"),
)),
DataCell(
Opacity(
@@ -80,13 +79,15 @@ class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
.onError((error, trace) =>
Utils.showSnakeBar("操作失败: $error"));
},
icon: const Icon(Icons.search)),
icon: const Icon(Icons.download)),
),
const SizedBox(
width: 10,
),
IconButton(
onPressed: () {}, icon: const Icon(Icons.manage_search))
onPressed: () => showAvailableTorrents(widget.seriesId,
ep.seasonNumber ?? 0, ep.episodeNumber ?? 0),
icon: const Icon(Icons.manage_search))
],
))
]);
@@ -113,19 +114,30 @@ class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
const DataColumn(label: Text("播出时间")),
const DataColumn(label: Text("状态")),
DataColumn(
label: Tooltip(
message: "搜索下载全部剧集",
child: IconButton(
onPressed: () {
ref
.read(mediaDetailsProvider(widget.seriesId)
.notifier)
.searchAndDownload(widget.seriesId, k, 0)
.then((v) => Utils.showSnakeBar("开始下载: $v"))
.onError((error, trace) =>
Utils.showSnakeBar("操作失败: $error"));
},
icon: const Icon(Icons.search)),
label: Row(
children: [
Tooltip(
message: "搜索下载全部剧集",
child: IconButton(
onPressed: () {
ref
.read(mediaDetailsProvider(widget.seriesId)
.notifier)
.searchAndDownload(widget.seriesId, k, 0)
.then((v) => Utils.showSnakeBar("开始下载: $v"))
.onError((error, trace) =>
Utils.showSnakeBar("操作失败: $error"));
},
icon: const Icon(Icons.download)),
),
const SizedBox(
width: 10,
),
IconButton(
onPressed: () =>
showAvailableTorrents(widget.seriesId, k, 0),
icon: const Icon(Icons.manage_search))
],
))
], rows: m[k]!),
],
@@ -140,11 +152,10 @@ class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.fitWidth,
fit: BoxFit.cover,
opacity: 0.5,
image: NetworkImage(
"${APIs.imagesUrl}/${details.id}/backdrop.jpg",
headers: APIs.authHeaders))),
"${APIs.imagesUrl}/${details.id}/backdrop.jpg"))),
child: Padding(
padding: const EdgeInsets.all(10),
child: Row(
@@ -155,8 +166,7 @@ class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
padding: const EdgeInsets.all(10),
child: Image.network(
"${APIs.imagesUrl}/${details.id}/poster.jpg",
fit: BoxFit.contain,
headers: APIs.authHeaders,
fit: BoxFit.contain
),
),
),
@@ -198,7 +208,7 @@ class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
),
const Text(""),
Text(
details.overview??"",
details.overview ?? "",
),
],
)),
@@ -239,4 +249,71 @@ class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
},
loading: () => const MyProgressIndicator());
}
Future<void> showAvailableTorrents(String id, int season, int episode) {
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return Consumer(builder: (context, ref, _) {
final torrents = ref.watch(mediaTorrentsDataProvider(
(mediaId: id, seasonNumber: season, episodeNumber: episode)));
return AlertDialog(
//title: Text("资源"),
content: SelectionArea(
child: SizedBox(
width: 800,
height: 400,
child: torrents.when(
data: (v) {
return SingleChildScrollView(
child: DataTable(
dataTextStyle: const TextStyle(fontSize: 12, height: 0),
columns: const [
DataColumn(label: Text("名称")),
DataColumn(label: Text("大小")),
DataColumn(label: Text("seeders")),
DataColumn(label: Text("peers")),
DataColumn(label: Text("操作"))
],
rows: List.generate(v.length, (i) {
final torrent = v[i];
return DataRow(cells: [
DataCell(Text("${torrent.name}")),
DataCell(Text(
"${torrent.size?.readableFileSize()}")),
DataCell(Text("${torrent.seeders}")),
DataCell(Text("${torrent.peers}")),
DataCell(IconButton(
icon: const Icon(Icons.download),
onPressed: () async {
await ref
.read(mediaTorrentsDataProvider((
mediaId: id,
seasonNumber: season,
episodeNumber: episode
)).notifier)
.download(torrent)
.then((v) {
Navigator.of(context).pop();
Utils.showSnakeBar(
"开始下载:${torrent.name}");
}).onError((error, trace) =>
Utils.showSnakeBar("下载失败:$error"));
},
))
]);
})));
},
error: (err, trace) {
return Text("$err");
},
loading: () => const MyProgressIndicator()),
),
));
});
},
);
}
}

View File

@@ -27,45 +27,60 @@ class WelcomePage extends ConsumerWidget {
return switch (data) {
AsyncData(:final value) => SingleChildScrollView(
child: Wrap(
spacing: 20,
children: List.generate(value.length, (i) {
var item = value[i];
return Card(
margin: const EdgeInsets.all(4),
clipBehavior: Clip.hardEdge,
child: InkWell(
//splashColor: Colors.blue.withAlpha(30),
onTap: () {
if (uri == routeMoivie) {
context.go(MovieDetailsPage.toRoute(item.id!));
} else {
context.go(TvDetailsPage.toRoute(item.id!));
}
},
child: Column(
children: <Widget>[
SizedBox(
width: 160,
height: 240,
child: ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: Image.network(
"${APIs.imagesUrl}/${item.id}/poster.jpg",
fit: BoxFit.fill,
headers: APIs.authHeaders,
spacing: 10,
runSpacing: 20,
children: value.isEmpty
? [
Container(
height: MediaQuery.of(context).size.height * 0.6,
alignment: Alignment.center,
child: const Text(
"啥都没有...",
style: TextStyle(fontSize: 16),
))
]
: List.generate(value.length, (i) {
var item = value[i];
return Card(
margin: const EdgeInsets.all(4),
clipBehavior: Clip.hardEdge,
child: InkWell(
//splashColor: Colors.blue.withAlpha(30),
onTap: () {
if (uri == routeMoivie) {
context.go(MovieDetailsPage.toRoute(item.id!));
} else {
context.go(TvDetailsPage.toRoute(item.id!));
}
},
child: Column(
children: <Widget>[
SizedBox(
width: 140,
height: 210,
child: Image.network(
"${APIs.imagesUrl}/${item.id}/poster.jpg",
fit: BoxFit.fill),
),
)),
Text(
item.name!,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
height: 2.5),
),
],
),
));
}),
SizedBox(
width: 140,
child: LinearProgressIndicator(
value: 1,
color: item.status == "downloaded"
? Colors.green
: Colors.blue,
)),
Text(
item.name!,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
height: 2.5),
),
],
),
));
}),
),
),
_ => const MyProgressIndicator(),

View File

@@ -4,8 +4,10 @@ class Commons {
static InputDecoration requiredTextFieldStyle({
required String text,
Widget? icon,
String? helperText,
}) {
return InputDecoration(
helperText: helperText,
label: Row(
children: [
Text(text),

View File

@@ -89,22 +89,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
file:
dependency: transitive
description:
name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.0"
flutter:
dependency: "direct main"
description: flutter
@@ -325,30 +309,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.0"
percent_indicator:
dependency: "direct main"
description:
@@ -365,14 +325,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.5"
plugin_platform_interface:
dependency: transitive
description:
@@ -405,62 +357,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.1"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.3"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.0"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.2"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.0"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.2"
sign_in_button:
dependency: transitive
description:
@@ -539,7 +435,7 @@ packages:
source: hosted
version: "1.3.2"
url_launcher:
dependency: transitive
dependency: "direct main"
description:
name: url_launcher
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
@@ -626,14 +522,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.5.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.4"
sdks:
dart: ">=3.4.3 <4.0.0"
flutter: ">=3.22.0"

Some files were not shown because too many files have changed in this diff Show More