Compare commits

...

102 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
Simon Ding
58e65b21fb feat: check connect before add download client 2024-07-25 14:19:03 +08:00
Simon Ding
520933085d fix: remove url check 2024-07-25 14:09:58 +08:00
Simon Ding
5cc88986d2 feat: better form 2024-07-25 14:02:47 +08:00
Simon Ding
d63a923589 feat: not allow empty fields 2024-07-25 11:16:31 +08:00
Simon Ding
bca68befb1 fix: name not exist 2024-07-25 10:58:58 +08:00
Simon Ding
1be44bff9e fix: add storage 2024-07-25 10:38:37 +08:00
Simon Ding
3998270cbd feat: check new season every 12h 2024-07-25 09:44:02 +08:00
Simon Ding
73e76c2185 feat: change logic 2024-07-25 08:43:49 +08:00
Simon Ding
c72a460509 update 2024-07-25 01:09:44 +08:00
Simon Ding
912293d8e8 feat: match anime that only rely on episode number 2024-07-25 01:07:24 +08:00
Simon Ding
7f2e84ad52 feat: no need auto dispose 2024-07-25 00:36:55 +08:00
Simon Ding
e52ad612c1 fix: extra spaces 2024-07-25 00:11:15 +08:00
Simon Ding
45a212fec5 feat: add msg 2024-07-24 23:51:20 +08:00
Simon Ding
39bfda4cda feat: remove unicode hindden char 2024-07-24 23:11:45 +08:00
Simon Ding
24a4d3152d fix: condition mismatch 2024-07-24 22:47:25 +08:00
Simon Ding
6c6670a8c0 fix name match 2024-07-24 22:35:50 +08:00
Simon Ding
63fc4f277b feat: better episode matching 2024-07-24 22:26:25 +08:00
Simon Ding
45d2a4fb79 feat: better name parser 2024-07-24 22:15:59 +08:00
Simon Ding
5e337871c9 fix: finding season pack 2024-07-24 21:57:50 +08:00
Simon Ding
803dcfeacd fix: replace strings 2024-07-24 21:48:14 +08:00
Simon Ding
c26e61bbee fix: incase name not submitted 2024-07-24 21:35:59 +08:00
Simon Ding
e334acba32 feat: submit all torrent info to server 2024-07-24 21:24:36 +08:00
Simon Ding
1359df599b feat: follow jackett redirect to get real link 2024-07-24 19:30:24 +08:00
Simon Ding
16ca00d19c fix: name cannot parsed 2024-07-24 18:35:01 +08:00
Simon Ding
f4b8d03cfc add log 2024-07-24 18:18:48 +08:00
Simon Ding
8811b89889 fix local storage 2024-07-24 18:12:11 +08:00
Simon Ding
daff2cfcfc feat: telegram chat 2024-07-24 17:13:23 +08:00
Simon Ding
79ec63bfdb feat: test webdav 2024-07-24 16:43:20 +08:00
Simon Ding
bd0ada5897 chore: remove useless logs 2024-07-24 15:57:39 +08:00
Simon Ding
a7dfa2d0f0 feat: fetch torznab results cocurrently 2024-07-24 15:55:33 +08:00
Simon Ding
33f0a5b53f fix: revert main 2024-07-24 15:23:27 +08:00
Simon Ding
1878d6b679 feat: japan anime support 2024-07-24 15:21:48 +08:00
Simon Ding
627f838ab9 feat: null operation 2024-07-23 22:34:45 +08:00
Simon Ding
215511fab0 remove log 2024-07-23 22:25:46 +08:00
Simon Ding
730db5c94a feat: seperate active and archived activities 2024-07-23 22:22:53 +08:00
Simon Ding
55f5ce329c feat: do not return error when no resource 2024-07-23 21:27:06 +08:00
Simon Ding
5b2d86d301 fix: cn name 2024-07-23 21:17:54 +08:00
Simon Ding
95708a4c0c fix: chinese name 2024-07-23 21:15:54 +08:00
Simon Ding
c41b3026df fix: airdate is null 2024-07-23 20:56:04 +08:00
Simon Ding
fa84f881a4 feat: do not download episode aired more than 24h 2024-07-23 20:52:07 +08:00
Simon Ding
90ac4cddff fix: no overview 2024-07-23 20:35:46 +08:00
Simon Ding
2c5e4d0530 feat: better readable text when no resources 2024-07-23 20:24:14 +08:00
Simon Ding
fb638dff8b fix: download dir 2024-07-23 19:50:50 +08:00
Simon Ding
11f7b51eb5 add movie download history 2024-07-23 19:01:24 +08:00
Simon Ding
d2439480c8 update redame 2024-07-23 15:49:48 +08:00
108 changed files with 6556 additions and 1867 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,10 @@
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
## 功能
@@ -11,87 +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
jackett: #资源提供者,也可以不安装使用已有的
image: lscr.io/linuxserver/jackett:latest
environment:
- PUID=1000
- PGID=1000
- TZ=Asia/Shanghai
- AUTO_UPDATE=false
volumes:
- ./config/jackett:/config
ports:
- 9117:9117
restart: always
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
@@ -162,6 +172,10 @@ func (c *Client) UpdateEpiode(episodeId int, name, overview string) error {
return c.ent.Episode.Update().Where(episode.ID(episodeId)).SetTitle(name).SetOverview(overview).Exec(context.TODO())
}
func (c *Client) UpdateEpiode2(episodeId int, name, overview, airdate string) error {
return c.ent.Episode.Update().Where(episode.ID(episodeId)).SetTitle(name).SetOverview(overview).SetAirDate(airdate).Exec(context.TODO())
}
type MediaDetails struct {
*ent.Media
Episodes []*ent.Episode `json:"episodes"`
@@ -187,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 {
@@ -203,6 +221,21 @@ func (c *Client) SaveEposideDetail(d *ent.Episode) (int, error) {
SetEpisodeNumber(d.EpisodeNumber).
SetOverview(d.Overview).
SetTitle(d.Title).Save(context.TODO())
if err != nil {
return 0, errors.Wrap(err, "save episode")
}
return ep.ID, nil
}
func (c *Client) SaveEposideDetail2(d *ent.Episode) (int, error) {
ep, err := c.ent.Episode.Create().
SetAirDate(d.AirDate).
SetSeasonNumber(d.SeasonNumber).
SetEpisodeNumber(d.EpisodeNumber).
SetMediaID(d.MediaID).
SetStatus(d.Status).
SetOverview(d.Overview).
SetTitle(d.Title).Save(context.TODO())
return ep.ID, err
}
@@ -299,12 +332,26 @@ func (c *Client) DeleteDownloadCLient(id int) {
// Storage is the model entity for the Storage schema.
type StorageInfo struct {
Name string `json:"name"`
Implementation string `json:"implementation"`
Settings map[string]string `json:"settings"`
Name string `json:"name" binding:"required"`
Implementation string `json:"implementation" binding:"required"`
Settings map[string]string `json:"settings" binding:"required"`
Default bool `json:"default"`
}
func (s *StorageInfo) ToWebDavSetting() WebdavSetting {
if s.Implementation != storage.ImplementationWebdav.String() {
panic("not webdav storage")
}
return WebdavSetting{
URL: s.Settings["url"],
TvPath: s.Settings["tv_path"],
MoviePath: s.Settings["movie_path"],
User: s.Settings["user"],
Password: s.Settings["password"],
ChangeFileHash: s.Settings["change_file_hash"],
}
}
type LocalDirSetting struct {
TvPath string `json:"tv_path"`
MoviePath string `json:"movie_path"`
@@ -465,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 {
@@ -485,3 +532,19 @@ func (c *Client) SetSeasonAllEpisodeStatus(mediaID, seasonNum int, status episod
func (c *Client) TmdbIdInWatchlist(tmdb_id int) bool {
return c.ent.Media.Query().Where(media.TmdbID(tmdb_id)).CountX(context.TODO()) > 0
}
func (c *Client) GetDownloadHistory(mediaID int) ([]*ent.History, error) {
return c.ent.History.Query().Where(history.MediaID(mediaID)).All(context.TODO())
}
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 // indirect
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 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sys v0.21.0 // indirect
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
)

57
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=
@@ -104,6 +114,10 @@ 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=
@@ -138,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=
@@ -149,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=
@@ -159,48 +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.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
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{}) {

44
pkg/metadata/movie.go Normal file
View File

@@ -0,0 +1,44 @@
package metadata
import (
"fmt"
"regexp"
"strconv"
"strings"
)
type MovieMetadata struct {
NameEn string
NameCN string
Year int
Resolution string
}
func ParseMovie(name string) *MovieMetadata {
name = strings.Join(strings.Fields(name), " ") //remove unnessary spaces
name = strings.ToLower(strings.TrimSpace(name))
var meta = &MovieMetadata{}
yearRe := regexp.MustCompile(`\(\d{4}\)`)
yearMatches := yearRe.FindAllString(name, -1)
var yearIndex = -1
if len(yearMatches) > 0 {
yearIndex = strings.Index(name, yearMatches[0])
y := yearMatches[0][1 : len(yearMatches[0])-1]
n, err := strconv.Atoi(y)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", y, err))
}
meta.Year = n
}
if yearIndex != -1 {
meta.NameEn = name[:yearIndex]
} else {
meta.NameEn = name
}
resRe := regexp.MustCompile(`\d{3,4}p`)
resMatches := resRe.FindAllString(name, -1)
if len(resMatches) > 0 {
meta.Resolution = resMatches[0]
}
return meta
}

304
pkg/metadata/tv.go Normal file
View File

@@ -0,0 +1,304 @@
package metadata
import (
"fmt"
"polaris/pkg/utils"
"regexp"
"strconv"
"strings"
)
type Metadata struct {
NameEn string
NameCn string
Season int
Episode int
Resolution string
IsSeasonPack bool
}
func ParseTv(name string) *Metadata {
name = strings.ToLower(name)
name = strings.ReplaceAll(name, "\u200b", "") //remove unicode hidden character
if utils.ContainsChineseChar(name) {
return parseChineseName(name)
}
return parseEnglishName(name)
}
func parseEnglishName(name string) *Metadata {
re := regexp.MustCompile(`[^\p{L}\w\s]`)
name = re.ReplaceAllString(strings.ToLower(name), " ")
splits := strings.Split(strings.TrimSpace(name), " ")
var newSplits []string
for _, p := range splits {
p = strings.TrimSpace(p)
if p == "" {
continue
}
newSplits = append(newSplits, p)
}
seasonRe := regexp.MustCompile(`^s\d{1,2}`)
resRe := regexp.MustCompile(`^\d{3,4}p`)
episodeRe := regexp.MustCompile(`e\d{1,2}`)
var seasonIndex = -1
var episodeIndex = -1
var resIndex = -1
for i, p := range newSplits {
p = strings.TrimSpace(p)
if p == "" {
continue
}
if seasonRe.MatchString(p) {
//season part
seasonIndex = i
} else if resRe.MatchString(p) {
resIndex = i
}
if episodeRe.MatchString(p) {
episodeIndex = i
}
}
meta := &Metadata{
Season: -1,
Episode: -1,
}
if seasonIndex != -1 {
//season exists
ss := seasonRe.FindAllString(newSplits[seasonIndex], -1)
if len(ss) != 0 {
//season info
ssNum := strings.TrimLeft(ss[0], "s")
n, err := strconv.Atoi(ssNum)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", ssNum, err))
}
meta.Season = n
}
} else { //maybe like Season 1?
seasonRe := regexp.MustCompile(`season \d{1,2}`)
matches := seasonRe.FindAllString(name, -1)
if len(matches) > 0 {
for i, s := range newSplits {
if s == "season" {
seasonIndex = i
}
}
numRe := regexp.MustCompile(`\d{1,2}`)
seNum := numRe.FindAllString(matches[0], -1)[0]
n, err := strconv.Atoi(seNum)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", seNum, err))
}
meta.Season = n
}
}
if episodeIndex != -1 {
ep := episodeRe.FindAllString(newSplits[episodeIndex], -1)
if len(ep) > 0 {
//episode info exists
epNum := strings.TrimLeft(ep[0], "e")
n, err := strconv.Atoi(epNum)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
}
meta.Episode = n
}
} else { //no episode, maybe like One Punch Man S2 - 08 [1080p].mkv
// numRe := regexp.MustCompile(`^\d{1,2}$`)
// for i, p := range newSplits {
// if numRe.MatchString(p) {
// if i > 0 && strings.Contains(newSplits[i-1], "season") { //last word cannot be season
// continue
// }
// if i < seasonIndex {
// //episode number most likely should comes alfter season number
// continue
// }
// //episodeIndex = i
// n, err := strconv.Atoi(p)
// if err != nil {
// panic(fmt.Sprintf("convert %s error: %v", p, err))
// }
// meta.Episode = n
// }
// }
}
if resIndex != -1 {
//resolution exists
meta.Resolution = newSplits[resIndex]
}
if meta.Episode == -1 || strings.Contains(name, "complete") {
meta.Episode = -1
meta.IsSeasonPack = true
}
if seasonIndex > 0 {
//name exists
names := newSplits[0:seasonIndex]
meta.NameEn = strings.TrimSpace(strings.Join(names, " "))
} else {
meta.NameEn = name
}
return meta
}
func parseChineseName(name string) *Metadata {
var meta = &Metadata{
Season: 1,
}
//season pack
packRe := regexp.MustCompile(`(\d{1,2}-\d{1,2})|(全集)`)
if packRe.MatchString(name) {
meta.IsSeasonPack = true
}
//resolution
resRe := regexp.MustCompile(`\d{3,4}p`)
resMatches := resRe.FindAllString(name, -1)
if len(resMatches) != 0 {
meta.Resolution = resMatches[0]
} else {
if strings.Contains(name, "720") {
meta.Resolution = "720p"
} else if strings.Contains(name, "1080") {
meta.Resolution = "1080p"
}
}
//episode number
re1 := regexp.MustCompile(`\[\d{1,2}\]`)
episodeMatches1 := re1.FindAllString(name, -1)
if len(episodeMatches1) > 0 { //[11] [1080p]
epNum := strings.TrimRight(strings.TrimLeft(episodeMatches1[0], "["), "]")
n, err := strconv.Atoi(epNum)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
}
meta.Episode = n
} else { //【第09話】
re2 := regexp.MustCompile(`第\d{1,4}(话|話|集)`)
episodeMatches1 := re2.FindAllString(name, -1)
if len(episodeMatches1) > 0 {
re := regexp.MustCompile(`\d{1,4}`)
epNum := re.FindAllString(episodeMatches1[0], -1)[0]
n, err := strconv.Atoi(epNum)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
}
meta.Episode = n
} else { //SHY 靦腆英雄 / Shy -05 ( CR 1920x1080 AVC AAC MKV)
re3 := regexp.MustCompile(`[^\d\w]\d{1,2}[^\d\w]`)
epNums := re3.FindAllString(name, -1)
if len(epNums) > 0 {
re3 := regexp.MustCompile(`\d{1,2}`)
epNum := re3.FindAllString(epNums[0], -1)[0]
n, err := strconv.Atoi(epNum)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
}
meta.Episode = n
}
}
}
//season numner
seasonRe1 := regexp.MustCompile(`s\d{1,2}`)
seasonMatches := seasonRe1.FindAllString(name, -1)
if len(seasonMatches) > 0 {
seNum := seasonMatches[0][1:]
n, err := strconv.Atoi(seNum)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", seNum, err))
}
meta.Season = n
} else {
seasonRe1 := regexp.MustCompile(`season \d{1,2}`)
seasonMatches := seasonRe1.FindAllString(name, -1)
if len(seasonMatches) > 0 {
re3 := regexp.MustCompile(`\d{1,2}`)
seNum := re3.FindAllString(seasonMatches[0], -1)[0]
n, err := strconv.Atoi(seNum)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", seNum, err))
}
meta.Season = n
} else {
seasonRe1 := regexp.MustCompile(`第.{1}季`)
seasonMatches := seasonRe1.FindAllString(name, -1)
if len(seasonMatches) > 0 {
se := []rune(seasonMatches[0])
seNum := se[1]
meta.Season = chinese2Num[string(seNum)]
}
}
}
if meta.IsSeasonPack && meta.Episode != 0 {
meta.Season = meta.Episode
meta.Episode = -1
}
//tv name
title := name
fields := strings.FieldsFunc(title, func(r rune) bool {
return r == '[' || r == ']' || r == '【' || r == '】'
})
title = ""
for _, p := range fields { //寻找匹配的最长的字符串,最有可能是名字
if len([]rune(p)) > len([]rune(title)) {
title = p
}
}
re := regexp.MustCompile(`[^\p{L}\w\s]`)
title = re.ReplaceAllString(strings.TrimSpace(strings.ToLower(title)), "")
meta.NameCn = title
cnRe := regexp.MustCompile(`\p{Han}.*\p{Han}`)
cnmatches := cnRe.FindAllString(title, -1)
if len(cnmatches) > 0 {
for _, t := range cnmatches {
if len([]rune(t)) > len([]rune(meta.NameCn)) {
meta.NameCn = strings.ToLower(t)
}
}
}
enRe := regexp.MustCompile(`[[:ascii:]]*`)
enM := enRe.FindAllString(title, -1)
if len(enM) > 0 {
for _, t := range enM {
if len(t) > len(meta.NameEn) {
meta.NameEn = strings.ToLower(t)
}
}
}
return meta
}
var chinese2Num = map[string]int{
"一": 1,
"二": 2,
"三": 3,
"四": 4,
"五": 5,
"六": 6,
"七": 7,
"八": 8,
"九": 9,
}

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(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

@@ -28,6 +28,9 @@ func NewClient(apiKey string) (*Client, error) {
func (c *Client) GetTvDetails(id int, language string) (*tmdb.TVDetails, error) {
d, err := c.tmdbClient.GetTVDetails(id, withLangOption(language))
if err != nil {
return nil, errors.Wrap(err, "get tv detail")
}
log.Infof("tv id %d, language %s", id, language)
if !episodeNameUseful(d.LastEpisodeToAir.Name) {
@@ -38,13 +41,12 @@ func (c *Client) GetTvDetails(id int, language string) (*tmdb.TVDetails, error)
if err != nil {
return d, nil
}
}
if episodeNameUseful(detailEN.LastEpisodeToAir.Name) {
d.LastEpisodeToAir.Name = detailEN.LastEpisodeToAir.Name
d.LastEpisodeToAir.Overview = detailEN.LastEpisodeToAir.Overview
d.NextEpisodeToAir.Name = detailEN.NextEpisodeToAir.Name
d.NextEpisodeToAir.Overview = detailEN.NextEpisodeToAir.Overview
if episodeNameUseful(detailEN.LastEpisodeToAir.Name) {
d.LastEpisodeToAir.Name = detailEN.LastEpisodeToAir.Name
d.LastEpisodeToAir.Overview = detailEN.LastEpisodeToAir.Overview
d.NextEpisodeToAir.Name = detailEN.NextEpisodeToAir.Name
d.NextEpisodeToAir.Overview = detailEN.NextEpisodeToAir.Overview
}
}
}
@@ -170,7 +172,7 @@ func (c *Client) GetSeasonDetails(id, seasonNumber int, language string) (*tmdb.
}
for i, ep := range detailCN.Episodes {
if episodeNameUseful(ep.Name){
if !episodeNameUseful(ep.Name) && episodeNameUseful(detailEN.Episodes[i].Name){
detailCN.Episodes[i].Name = detailEN.Episodes[i].Name
detailCN.Episodes[i].Overview = detailEN.Episodes[i].Overview
}

View File

@@ -1,12 +1,14 @@
package torznab
import (
"context"
"encoding/xml"
"io"
"net/http"
"net/url"
"polaris/log"
"strconv"
"time"
"github.com/pkg/errors"
)
@@ -59,7 +61,6 @@ type Item struct {
Name string `xml:"name,attr"`
Value string `xml:"value,attr"`
} `xml:"attr"`
}
func (i *Item) GetAttr(key string) string {
@@ -75,12 +76,12 @@ func (r *Response) ToResults() []Result {
for _, item := range r.Channel.Item {
r := Result{
Name: item.Title,
Magnet: item.Link,
Size: mustAtoI(item.Size),
Seeders: mustAtoI(item.GetAttr("seeders")),
Peers: mustAtoI(item.GetAttr("peers")),
Link: item.Link,
Size: mustAtoI(item.Size),
Seeders: mustAtoI(item.GetAttr("seeders")),
Peers: mustAtoI(item.GetAttr("peers")),
Category: mustAtoI(item.GetAttr("category")),
Source: r.Channel.Title,
Source: r.Channel.Title,
}
res = append(res, r)
}
@@ -96,7 +97,10 @@ func mustAtoI(key string) int {
return i
}
func Search(torznabUrl, api, keyWord string) ([]Result, error) {
req, err := http.NewRequest(http.MethodGet, torznabUrl, nil)
ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, torznabUrl, nil)
if err != nil {
return nil, errors.Wrap(err, "new request")
}
@@ -125,10 +129,10 @@ func Search(torznabUrl, api, keyWord string) ([]Result, error) {
type Result struct {
Name string
Magnet string
Link string
Size int
Seeders int
Peers int
Category int
Source string
Source string
}

View File

@@ -3,8 +3,11 @@ package transmission
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"polaris/log"
"strings"
"github.com/hekmon/transmissionrpc/v3"
"github.com/pkg/errors"
@@ -25,6 +28,10 @@ func NewClient(c Config) (*Client, error) {
if err != nil {
return nil, errors.Wrap(err, "connect transmission")
}
_, err = tbt.TorrentGetAll(context.TODO())
if err != nil {
return nil, errors.Wrap(err, "transmission cannot connect")
}
return &Client{c: tbt, cfg: c}, nil
}
@@ -34,20 +41,58 @@ type Config struct {
Password string `json:"password"`
}
type Client struct {
c *transmissionrpc.Client
c *transmissionrpc.Client
cfg Config
}
func (c *Client) Download(magnet, dir string) (*Torrent, error) {
func (c *Client) 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{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err:=client.Get(link)
if err == nil {
if resp.StatusCode == http.StatusFound {
loc, err := resp.Location()
if err == nil {
link = loc.String()
log.Warnf("transimision redirect to url: %v", link)
}
}
}
}
t, err := c.c.TorrentAdd(context.TODO(), transmissionrpc.TorrentAddPayload{
Filename: &magnet,
Filename: &link,
DownloadDir: &dir,
})
log.Infof("get torrent info: %+v", t)
if t.ID == nil {
return nil, fmt.Errorf("download torrent error: %v", link)
}
return &Torrent{
ID: *t.ID,
c: c.c,
ID: *t.ID,
c: c.c,
Config: c.cfg,
}, err
}
@@ -95,7 +140,7 @@ func (t *Torrent) Progress() int {
if t.getTorrent().PercentComplete != nil && *t.getTorrent().PercentComplete >= 1 {
return 100
}
if t.getTorrent().PercentComplete != nil {
p := int(*t.getTorrent().PercentComplete * 100)
if p == 100 {
@@ -143,4 +188,4 @@ func ReloadTorrent(s string) (*Torrent, error) {
return nil, errors.Wrap(err, "reload client")
}
return &torrent, nil
}
}

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

@@ -15,7 +15,7 @@ import (
"golang.org/x/sys/unix"
)
func isASCII(s string) bool {
func IsASCII(s string) bool {
for _, c := range s {
if c > unicode.MaxASCII {
return false
@@ -36,7 +36,7 @@ func VerifyPassword(password, hash string) bool {
return err == nil
}
func IsChineseChar(str string) bool {
func ContainsChineseChar(str string) bool {
for _, r := range str {
if unicode.Is(unicode.Han, r) || (regexp.MustCompile("[\u3002\uff1b\uff0c\uff1a\u201c\u201d\uff08\uff09\u3001\uff1f\u300a\u300b]").MatchString(string(r))) {
@@ -57,11 +57,15 @@ func RandString(n int) string {
}
func IsNameAcceptable(name1, name2 string) bool {
re := regexp.MustCompile(`[^\p{L}\w\s]`)
name1 = re.ReplaceAllString(strings.ToLower(name1), "")
name2 = re.ReplaceAllString(strings.ToLower(name2), "")
return strutil.Similarity(name1, name2, metrics.NewHamming()) > 0.1
name1 = re.ReplaceAllString(strings.ToLower(name1), " ")
name2 = re.ReplaceAllString(strings.ToLower(name2), " ")
name1 = strings.Join(strings.Fields(name1), " ")
name2 = strings.Join(strings.Fields(name2), " ")
if strings.Contains(name1, name2) || strings.Contains(name2, name1) {
return true
}
return strutil.Similarity(name1, name2, metrics.NewHamming()) > 0.4
}
func FindSeasonEpisodeNum(name string) (se int, ep int, err error) {

View File

@@ -1,8 +1,10 @@
package server
import (
"fmt"
"polaris/ent"
"polaris/ent/episode"
"polaris/ent/history"
"polaris/log"
"polaris/pkg/utils"
"strconv"
@@ -17,9 +19,16 @@ type Activity struct {
}
func (s *Server) GetAllActivities(c *gin.Context) (interface{}, error) {
q := c.Query("status")
his := s.db.GetHistories()
var activities = make([]Activity, 0, len(his))
for _, h := range his {
if q == "active" && (h.Status != history.StatusRunning && h.Status != history.StatusUploading) {
continue //active downloads
} else if q == "archive" && (h.Status == history.StatusRunning || h.Status == history.StatusUploading) {
continue //archived downloads
}
a := Activity{
History: h,
}
@@ -72,3 +81,45 @@ func (s *Server) RemoveActivity(c *gin.Context) (interface{}, error) {
log.Infof("history record successful deleted: %v", his.SourceTitle)
return nil, nil
}
func (s *Server) GetMediaDownloadHistory(c *gin.Context) (interface{}, error) {
var ids = c.Param("id")
id, err := strconv.Atoi(ids)
if err != nil {
return nil, fmt.Errorf("id is not correct: %v", ids)
}
his, err := s.db.GetDownloadHistory(id)
if err != nil {
return nil, errors.Wrap(err, "db")
}
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

@@ -3,46 +3,35 @@ package core
import (
"fmt"
"polaris/db"
"polaris/ent"
"polaris/ent/media"
"polaris/log"
"polaris/pkg/metadata"
"polaris/pkg/torznab"
"polaris/pkg/utils"
"sort"
"strconv"
"strings"
"sync"
"github.com/pkg/errors"
)
func SearchSeasonPackage(db1 *db.Client, seriesId, seasonNum int, checkResolution bool) ([]torznab.Result, error) {
series := db1.GetMediaDetails(seriesId)
if series == nil {
return nil, fmt.Errorf("no tv series of id %v", seriesId)
}
q := fmt.Sprintf("%s S%02d", series.NameEn, seasonNum)
return SearchEpisode(db1, seriesId, seasonNum, -1, checkResolution)
}
res := searchWithTorznab(db1, q)
if len(res) == 0 {
return nil, fmt.Errorf("no resource found")
}
var filtered []torznab.Result
for _, r := range res {
if !isNameAcceptable(r.Name, series.Media, seasonNum, -1) {
continue
}
if checkResolution && !IsWantedResolution(r.Name, series.Resolution) {
continue
}
filtered = append(filtered, r)
}
func isNumberedSeries(detail *db.MediaDetails) bool {
hasSeason2 := false
season2HasEpisode1 := false
for _, ep := range detail.Episodes {
if ep.SeasonNumber == 2 {
hasSeason2 = true
if ep.EpisodeNumber == 1 {
season2HasEpisode1 = true
}
if len(filtered) == 0 {
return nil, errors.New("no resource found")
}
}
return filtered, nil
return hasSeason2 && !season2HasEpisode1 //only one 1st episode
}
func SearchEpisode(db1 *db.Client, seriesId, seasonNum, episodeNum int, checkResolution bool) ([]torznab.Result, error) {
@@ -51,23 +40,43 @@ func SearchEpisode(db1 *db.Client, seriesId, seasonNum, episodeNum int, checkRes
return nil, fmt.Errorf("no tv series of id %v", seriesId)
}
q := fmt.Sprintf("%s S%02dE%02d", series.NameEn, seasonNum, episodeNum)
res := searchWithTorznab(db1, q)
if len(res) == 0 {
return nil, fmt.Errorf("no resource found")
}
res := searchWithTorznab(db1, series.NameEn)
resCn := searchWithTorznab(db1, series.NameCn)
res = append(res, resCn...)
var filtered []torznab.Result
for _, r := range res {
if !isNameAcceptable(r.Name, series.Media, seasonNum, episodeNum) {
//log.Infof("torrent resource: %+v", r)
meta := metadata.ParseTv(r.Name)
if meta == nil { //cannot parse name
continue
}
if checkResolution && !IsWantedResolution(r.Name, series.Resolution) {
if !isNumberedSeries(series) { //do not check season on series that only rely on episode number
if meta.Season != seasonNum {
continue
}
}
if isNumberedSeries(series) && episodeNum == -1 {
//should not want season
continue
}
if episodeNum != -1 && meta.Episode != episodeNum { //not season pack, episode number equals
continue
}else if episodeNum == -1 && !meta.IsSeasonPack { //want season pack, but not season pack
continue
}
if checkResolution && meta.Resolution != series.Resolution.String() {
continue
}
if !utils.IsNameAcceptable(meta.NameEn, series.NameEn) && !utils.IsNameAcceptable(meta.NameCn, series.NameCn) {
continue
}
filtered = append(filtered, r)
}
if len(filtered) == 0 {
return nil, errors.New("no resource found")
}
return filtered, nil
@@ -89,10 +98,16 @@ func SearchMovie(db1 *db.Client, movieId int, checkResolution bool) ([]torznab.R
}
var filtered []torznab.Result
for _, r := range res {
if !isNameAcceptable(r.Name, movieDetail.Media, -1, -1) {
meta := metadata.ParseMovie(r.Name)
if !utils.IsNameAcceptable(meta.NameEn, movieDetail.NameEn) {
continue
}
if checkResolution && !IsWantedResolution(r.Name, movieDetail.Resolution) {
if checkResolution && meta.Resolution != movieDetail.Resolution.String() {
continue
}
ss := strings.Split(movieDetail.AirDate, "-")[0]
year, _ := strconv.Atoi(ss)
if meta.Year != year && meta.Year != year-1 && meta.Year != year+1 { //year not match
continue
}
@@ -111,14 +126,32 @@ func searchWithTorznab(db *db.Client, q string) []torznab.Result {
var res []torznab.Result
allTorznab := db.GetAllTorznabInfo()
resChan := make(chan []torznab.Result)
var wg sync.WaitGroup
for _, tor := range allTorznab {
resp, err := torznab.Search(tor.URL, tor.ApiKey, q)
if err != nil {
log.Errorf("search %s error: %v", tor.Name, err)
continue
}
res = append(res, resp...)
wg.Add(1)
go func() {
log.Debugf("search torznab %v with %v", tor.Name, q)
defer wg.Done()
resp, err := torznab.Search(tor.URL, tor.ApiKey, q)
if err != nil {
log.Errorf("search %s error: %v", tor.Name, err)
return
}
resChan <- resp
}()
}
go func() {
wg.Wait()
close(resChan) // 在所有的worker完成后关闭Channel
}()
for result := range resChan {
res = append(res, result...)
}
sort.Slice(res, func(i, j int) bool {
var s1 = res[i]
var s2 = res[j]
@@ -127,53 +160,3 @@ func searchWithTorznab(db *db.Client, q string) []torznab.Result {
return res
}
func isNameAcceptable(torrentName string, m *ent.Media, seasonNum, episodeNum int) bool {
if !utils.IsNameAcceptable(torrentName, m.NameCn) && !utils.IsNameAcceptable(torrentName, m.NameEn) && !utils.IsNameAcceptable(torrentName, m.OriginalName){
return false //name not match
}
ss := strings.Split(m.AirDate, "-")[0]
year, _ := strconv.Atoi(ss)
if m.MediaType == media.MediaTypeMovie {
if !strings.Contains(torrentName, strconv.Itoa(year)) && !strings.Contains(torrentName, strconv.Itoa(year+1)) && !strings.Contains(torrentName, strconv.Itoa(year-1)) {
return false //not the same movie, if year is not correct
}
}
if m.MediaType == media.MediaTypeTv {
if episodeNum != -1 {
se := fmt.Sprintf("S%02dE%02d", seasonNum, episodeNum)
if !utils.ContainsIgnoreCase(torrentName, se) {
return false
}
} else {
//season package
if !utils.IsSeasonPackageName(torrentName) {
return false
}
seNum, err := utils.FindSeasonPackageInfo(torrentName)
if err != nil {
return false
}
if seNum != seasonNum {
return false
}
}
}
return true
}
func IsWantedResolution(name string, res media.Resolution) bool {
switch res {
case media.Resolution720p:
return utils.ContainsIgnoreCase(name, "720p")
case media.Resolution1080p:
return utils.ContainsIgnoreCase(name, "1080p")
case media.Resolution4k:
return utils.ContainsIgnoreCase(name, "4k") || utils.ContainsIgnoreCase(name, "2160p")
}
return false
}

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

@@ -2,77 +2,21 @@ package server
import (
"fmt"
"polaris/db"
"polaris/ent"
"polaris/ent/episode"
"polaris/ent/history"
"polaris/ent/media"
"polaris/log"
"polaris/pkg/transmission"
"polaris/pkg/notifier/message"
"polaris/pkg/torznab"
"polaris/pkg/utils"
"polaris/server/core"
"strconv"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)
type addTorznabIn struct {
Name string `json:"name"`
URL string `json:"url"`
ApiKey string `json:"api_key"`
}
func (s *Server) AddTorznabInfo(c *gin.Context) (interface{}, error) {
var in addTorznabIn
if err := c.ShouldBindJSON(&in); err != nil {
return nil, errors.Wrap(err, "bind json")
}
err := s.db.SaveTorznabInfo(in.Name, db.TorznabSetting{
URL: in.URL,
ApiKey: in.ApiKey,
})
if err != nil {
return nil, errors.Wrap(err, "add ")
}
return nil, nil
}
func (s *Server) DeleteTorznabInfo(c *gin.Context) (interface{}, error) {
var ids = c.Param("id")
id, err := strconv.Atoi(ids)
if err != nil {
return nil, fmt.Errorf("id is not correct: %v", ids)
}
s.db.DeleteTorznab(id)
return "success", nil
}
func (s *Server) GetAllIndexers(c *gin.Context) (interface{}, error) {
indexers := s.db.GetAllTorznabInfo()
if len(indexers) == 0 {
return nil, nil
}
return indexers, nil
}
func (s *Server) getDownloadClient() (*transmission.Client, error) {
tr := s.db.GetTransmission()
trc, err := transmission.NewClient(transmission.Config{
URL: tr.URL,
User: tr.User,
Password: tr.Password,
})
if err != nil {
return nil, errors.Wrap(err, "connect transmission")
}
return trc, nil
}
func (s *Server) searchAndDownloadSeasonPackage(seriesId, seasonNum int) (*string, error) {
trc, err := s.getDownloadClient()
if err != nil {
return nil, errors.Wrap(err, "connect transmission")
}
res, err := core.SearchSeasonPackage(s.db, seriesId, seasonNum, true)
if err != nil {
@@ -80,8 +24,16 @@ func (s *Server) searchAndDownloadSeasonPackage(seriesId, seasonNum int) (*strin
}
r1 := res[0]
log.Infof("found resource to download: %v", r1)
log.Infof("found resource to download: %+v", r1)
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) {
@@ -89,7 +41,7 @@ func (s *Server) searchAndDownloadSeasonPackage(seriesId, seasonNum int) (*strin
return nil, errors.New("no enough space")
}
torrent, err := trc.Download(r1.Magnet, s.db.GetDownloadDir())
torrent, err := trc.Download(r1.Link, s.db.GetDownloadDir())
if err != nil {
return nil, errors.Wrap(err, "downloading")
}
@@ -99,7 +51,7 @@ func (s *Server) searchAndDownloadSeasonPackage(seriesId, seasonNum int) (*strin
if series == nil {
return nil, fmt.Errorf("no tv series of id %v", seriesId)
}
dir := fmt.Sprintf("%s/Season %02d", series.TargetDir, seasonNum)
dir := fmt.Sprintf("%s/Season %02d/", series.TargetDir, seasonNum)
history, err := s.db.SaveHistoryRecord(ent.History{
MediaID: seriesId,
@@ -116,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")
@@ -137,20 +92,13 @@ 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.Magnet, s.db.GetDownloadDir())
torrent, err := trc.Download(r1.Link, s.db.GetDownloadDir())
if err != nil {
return nil, errors.Wrap(err, "downloading")
}
torrent.Start()
dir := fmt.Sprintf("%s/Season %02d", series.TargetDir, seasonNum)
dir := fmt.Sprintf("%s/Season %02d/", series.TargetDir, seasonNum)
history, err := s.db.SaveHistoryRecord(ent.History{
MediaID: ep.MediaID,
@@ -167,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 {
@@ -178,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 {
@@ -195,12 +186,9 @@ func (s *Server) SearchAvailableEpisodeResource(c *gin.Context) (interface{}, er
Size: r.Size,
Seeders: r.Seeders,
Peers: r.Peers,
Link: r.Magnet,
Link: r.Link,
})
}
if len(searchResults) == 0 {
return nil, errors.New("no resource found")
}
return searchResults, nil
}
@@ -239,74 +227,63 @@ 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 {
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.Magnet,
})
}
if len(searchResults) == 0 {
return nil, errors.New("no resource found")
}
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 {
return nil, errors.Wrap(err, "downloading")
}
torrent.Start()
name := in.Name
if name == "" {
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: media.NameCn,
SourceTitle: name,
TargetDir: "./",
Status: history.StatusRunning,
Size: in.Size,
@@ -320,45 +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
}
type downloadClientIn struct {
Name string `json:"name"`
URL string `json:"url"`
User string `json:"user"`
Password string `json:"password"`
Implementation string `json:"implementation"`
}
func (s *Server) AddDownloadClient(c *gin.Context) (interface{}, error) {
var in downloadClientIn
if err := c.ShouldBindJSON(&in); err != nil {
return nil, errors.Wrap(err, "bind json")
}
if err := s.db.SaveTransmission(in.Name, in.URL, in.User, in.Password); err != nil {
return nil, errors.Wrap(err, "save transmission")
}
return nil, nil
}
func (s *Server) GetAllDonloadClients(c *gin.Context) (interface{}, error) {
res := s.db.GetAllDonloadClients()
if len(res) == 0 {
return nil, nil
}
return res, nil
}
func (s *Server) DeleteDownloadCLient(c *gin.Context) (interface{}, error) {
var ids = c.Param("id")
id, err := strconv.Atoi(ids)
if err != nil {
return nil, fmt.Errorf("id is not correct: %v", ids)
}
s.db.DeleteDownloadCLient(id)
return "success", nil
}

View File

@@ -1,6 +1,7 @@
package server
import (
"fmt"
"path/filepath"
"polaris/ent"
"polaris/ent/episode"
@@ -9,19 +10,22 @@ import (
storage1 "polaris/ent/storage"
"polaris/log"
"polaris/pkg"
"polaris/pkg/notifier/message"
"polaris/pkg/storage"
"polaris/pkg/utils"
"polaris/server/core"
"time"
"github.com/pkg/errors"
)
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("0 0 */12 * * *", s.checkAllSeriesNewSeason)
s.cron.Start()
}
@@ -33,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)
@@ -42,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)
@@ -74,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)
@@ -83,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()
}
@@ -190,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
@@ -211,35 +223,29 @@ 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
}
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.Error("air date not known, skip: %v", ep.Title)
continue
}
if series.CreatedAt.Sub(t) > 24*time.Hour { //剧集在加入watchlist之前不去下载
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)
}
if lastEpisode.Status == episode.StatusMissing {
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)
}
}
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)
}
}
}
@@ -250,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
@@ -279,7 +285,7 @@ func (s *Server) downloadMovieSingleEpisode(ep *ent.Episode) error {
}
r1 := res[0]
log.Infof("begin download torrent resource: %v", r1.Name)
torrent, err := trc.Download(r1.Magnet, s.db.GetDownloadDir())
torrent, err := trc.Download(r1.Link, s.db.GetDownloadDir())
if err != nil {
return errors.Wrap(err, "downloading")
}
@@ -303,3 +309,51 @@ func (s *Server) downloadMovieSingleEpisode(ep *ent.Episode) error {
s.db.SetEpisodeStatus(ep.ID, episode.StatusDownloading)
return nil
}
func (s *Server) checkAllSeriesNewSeason() {
log.Infof("begin checking series all new season")
allSeries := s.db.GetMediaWatchlist(media.MediaTypeTv)
for _, series := range allSeries {
err := s.checkSeiesNewSeason(series)
if err != nil {
log.Errorf("check series new season error: series name %v, error: %v", series.NameEn, err)
}
}
}
func (s *Server) checkSeiesNewSeason(media *ent.Media) error {
d, err := s.MustTMDB().GetTvDetails(media.TmdbID, s.language)
if err != nil {
return errors.Wrap(err, "tmdb")
}
lastsSason := d.NumberOfSeasons
seasonDetail, err := s.MustTMDB().GetSeasonDetails(media.TmdbID, lastsSason, s.language)
if err != nil {
return errors.Wrap(err, "tmdb season")
}
for _, ep := range seasonDetail.Episodes {
epDb, err := s.db.GetEpisode(media.ID, ep.SeasonNumber, ep.EpisodeNumber)
if err != nil {
if ent.IsNotFound(err) {
log.Infof("add new episode: %+v", ep)
episode := &ent.Episode{
MediaID: media.ID,
SeasonNumber: ep.SeasonNumber,
EpisodeNumber: ep.EpisodeNumber,
Title: ep.Name,
Overview: ep.Overview,
AirDate: ep.AirDate,
Status: episode.StatusMissing,
}
s.db.SaveEposideDetail2(episode)
}
} else { //update episode
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,28 +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")
@@ -69,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))
@@ -99,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")
@@ -145,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

@@ -1,40 +1,179 @@
package server
import (
"fmt"
"net/http"
"net/url"
"polaris/db"
"polaris/log"
"polaris/pkg/transmission"
"strconv"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)
type GeneralSettings struct {
TmdbApiKey string `json:"tmdb_api_key"`
TmdbApiKey string `json:"tmdb_api_key"`
DownloadDir string `json:"download_dir"`
LogLevel string `json:"log_level"`
Proxy string `json:"proxy"`
}
func (s *Server) SetSetting(c *gin.Context) (interface{}, error) {
var in GeneralSettings
if err := c.ShouldBindJSON(&in); err != nil {
return nil, errors.Wrap(err, "bind json")
}
log.Infof("set setting input: %+v", in)
if in.TmdbApiKey != "" {
if err := s.db.SetSetting(db.SettingTmdbApiKey, in.TmdbApiKey); err != nil {
return nil, errors.Wrap(err, "save tmdb api")
}
}
if in.DownloadDir == "" {
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)
return &GeneralSettings{
TmdbApiKey: tmdb,
logLevel := s.db.GetSetting(db.SettingLogLevel)
return &GeneralSettings{
TmdbApiKey: tmdb,
DownloadDir: downloadDir,
LogLevel: logLevel,
Proxy: s.db.GetSetting(db.SettingProxy),
}, nil
}
type addTorznabIn struct {
Name string `json:"name" binding:"required"`
URL string `json:"url" binding:"required"`
ApiKey string `json:"api_key" binding:"required"`
}
func (s *Server) AddTorznabInfo(c *gin.Context) (interface{}, error) {
var in addTorznabIn
if err := c.ShouldBindJSON(&in); err != nil {
return nil, errors.Wrap(err, "bind json")
}
err := s.db.SaveTorznabInfo(in.Name, db.TorznabSetting{
URL: in.URL,
ApiKey: in.ApiKey,
})
if err != nil {
return nil, errors.Wrap(err, "add ")
}
return nil, nil
}
func (s *Server) DeleteTorznabInfo(c *gin.Context) (interface{}, error) {
var ids = c.Param("id")
id, err := strconv.Atoi(ids)
if err != nil {
return nil, fmt.Errorf("id is not correct: %v", ids)
}
s.db.DeleteTorznab(id)
return "success", nil
}
func (s *Server) GetAllIndexers(c *gin.Context) (interface{}, error) {
indexers := s.db.GetAllTorznabInfo()
if len(indexers) == 0 {
return nil, nil
}
return indexers, nil
}
func (s *Server) getDownloadClient() (*transmission.Client, error) {
tr := s.db.GetTransmission()
trc, err := transmission.NewClient(transmission.Config{
URL: tr.URL,
User: tr.User,
Password: tr.Password,
})
if err != nil {
return nil, errors.Wrap(err, "connect transmission")
}
return trc, nil
}
type downloadClientIn struct {
Name string `json:"name" binding:"required"`
URL string `json:"url" binding:"required"`
User string `json:"user"`
Password string `json:"password"`
Implementation string `json:"implementation" binding:"required"`
}
func (s *Server) AddDownloadClient(c *gin.Context) (interface{}, error) {
var in downloadClientIn
if err := c.ShouldBindJSON(&in); err != nil {
return nil, errors.Wrap(err, "bind json")
}
//test connection
_, err := transmission.NewClient(transmission.Config{
URL: in.URL,
User: in.User,
Password: in.Password,
})
if err != nil {
return nil, errors.Wrap(err, "tranmission setting")
}
if err := s.db.SaveTransmission(in.Name, in.URL, in.User, in.Password); err != nil {
return nil, errors.Wrap(err, "save transmission")
}
return nil, nil
}
func (s *Server) GetAllDonloadClients(c *gin.Context) (interface{}, error) {
res := s.db.GetAllDonloadClients()
if len(res) == 0 {
return nil, nil
}
return res, nil
}
func (s *Server) DeleteDownloadCLient(c *gin.Context) (interface{}, error) {
var ids = c.Param("id")
id, err := strconv.Atoi(ids)
if err != nil {
return nil, fmt.Errorf("id is not correct: %v", ids)
}
s.db.DeleteDownloadCLient(id)
return "success", nil
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"polaris/db"
"polaris/log"
"polaris/pkg/storage"
"polaris/pkg/utils"
"strconv"
"strings"
@@ -23,6 +24,21 @@ func (s *Server) AddStorage(c *gin.Context) (interface{}, error) {
return nil, errors.Wrap(err, "bind json")
}
if in.Implementation == "webdav" {
//test webdav
wd := in.ToWebDavSetting()
st, err := storage.NewWebdavStorage(wd.URL, wd.User, wd.Password, wd.TvPath, false)
if err != nil {
return nil, errors.Wrap(err, "new webdav")
}
fs, err := st.ReadDir(".")
if err != nil {
return nil, errors.Wrap(err, "test read")
}
for _, f := range fs {
log.Infof("file name: %v", f.Name())
}
}
log.Infof("received add storage input: %v", in)
err := s.db.AddStorage(&in)
return nil, err
@@ -62,11 +78,12 @@ func (s *Server) SuggestedSeriesFolderName(c *gin.Context) (interface{}, error)
}
name = fmt.Sprintf("%s %s", name, originalName)
if !utils.IsChineseChar(name) {
if !utils.ContainsChineseChar(name) {
name = originalName
}
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,44 +2,93 @@ 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 ConsumerWidget {
class ActivityPage extends ConsumerStatefulWidget {
const ActivityPage({super.key});
static const route = "/activities";
const ActivityPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
var activitiesWatcher = ref.watch(activitiesDataProvider);
_ActivityPageState createState() => _ActivityPageState();
}
return activitiesWatcher.when(
data: (activities) {
return SingleChildScrollView(
child: PaginatedDataTable(
rowsPerPage: 10,
columns: const [
DataColumn(label: Text("#"), numeric: true),
DataColumn(label: Text("名称")),
DataColumn(label: Text("开始时间")),
DataColumn(label: Text("状态")),
DataColumn(label: Text("操作"))
],
source: ActivityDataSource(
activities: activities, onDelete: onDelete(ref)),
),
);
},
error: (err, trace) => Text("$err"),
loading: () => const MyProgressIndicator());
class _ActivityPageState extends ConsumerState<ActivityPage>
with TickerProviderStateMixin {
late TabController _nestedTabController;
@override
void initState() {
super.initState();
_nestedTabController = new TabController(length: 2, vsync: this);
}
Function(int) onDelete(WidgetRef ref) {
@override
void dispose() {
super.dispose();
_nestedTabController.dispose();
}
int selectedTab = 0;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TabBar(
controller: _nestedTabController,
isScrollable: true,
onTap: (value) {
setState(() {
selectedTab = value;
});
},
tabs: const <Widget>[
Tab(
text: "下载中",
),
Tab(
text: "历史记录",
),
],
),
Builder(builder: (context) {
var activitiesWatcher = ref.watch(activitiesDataProvider("active"));
if (selectedTab == 1) {
activitiesWatcher = ref.watch(activitiesDataProvider("archive"));
}
return activitiesWatcher.when(
data: (activities) {
return SingleChildScrollView(
child: PaginatedDataTable(
rowsPerPage: 10,
columns: const [
DataColumn(label: Text("#"), numeric: true),
DataColumn(label: Text("名称")),
DataColumn(label: Text("开始时间")),
DataColumn(label: Text("状态")),
DataColumn(label: Text("操作"))
],
source: ActivityDataSource(
activities: activities,
onDelete: selectedTab == 0 ? onDelete() : null),
),
);
},
error: (err, trace) => Text("$err"),
loading: () => const MyProgressIndicator());
})
],
);
}
Function(int) onDelete() {
return (id) {
ref
.read(activitiesDataProvider.notifier)
.read(activitiesDataProvider("active").notifier)
.deleteActivity(id)
.whenComplete(() => Utils.showSnakeBar("删除成功"))
.then((v) => Utils.showSnakeBar("删除成功"))
.onError((error, trace) => Utils.showSnakeBar("删除失败:$error"));
};
}
@@ -47,8 +96,8 @@ class ActivityPage extends ConsumerWidget {
class ActivityDataSource extends DataTableSource {
List<Activity> activities;
Function(int) onDelete;
ActivityDataSource({required this.activities, required this.onDelete});
Function(int)? onDelete;
ActivityDataSource({required this.activities, this.onDelete});
@override
int get rowCount => activities.length;
@@ -96,11 +145,13 @@ class ActivityDataSource extends DataTableSource {
progressColor: Colors.green,
);
}()),
DataCell(Tooltip(
message: "删除任务",
child: IconButton(
onPressed: () => onDelete(activity.id!),
icon: const Icon(Icons.delete))))
onDelete != null
? DataCell(Tooltip(
message: "删除任务",
child: IconButton(
onPressed: () => onDelete!(activity.id!),
icon: const Icon(Icons.delete))))
: const DataCell(Text("-"))
]);
}

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

@@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
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';
@@ -30,7 +30,6 @@ class _MovieDetailsPageState extends ConsumerState<MovieDetailsPage> {
@override
Widget build(BuildContext context) {
var seriesDetails = ref.watch(mediaDetailsProvider(widget.id));
var torrents = ref.watch(movieTorrentsDataProvider(widget.id));
var storage = ref.watch(storageSettingProvider);
return seriesDetails.when(
@@ -40,122 +39,99 @@ class _MovieDetailsPageState extends ConsumerState<MovieDetailsPage> {
Card(
margin: const EdgeInsets.all(4),
clipBehavior: Clip.hardEdge,
child: Padding(
padding: const EdgeInsets.all(10),
child: Row(
children: <Widget>[
Flexible(
flex: 1,
child: Padding(
padding: const EdgeInsets.all(10),
child: Image.network(
"${APIs.imagesUrl}/${details.id}/poster.jpg",
fit: BoxFit.contain,
headers: APIs.authHeaders,
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
opacity: 0.5,
image: NetworkImage(
"${APIs.imagesUrl}/${details.id}/backdrop.jpg",
))),
child: Padding(
padding: const EdgeInsets.all(10),
child: Row(
children: <Widget>[
Flexible(
flex: 1,
child: Padding(
padding: const EdgeInsets.all(10),
child: Image.network(
"${APIs.imagesUrl}/${details.id}/poster.jpg",
fit: BoxFit.contain,
),
),
),
),
),
Expanded(
flex: 6,
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
Expanded(
flex: 6,
child: Row(
children: [
Row(
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("${details.resolution}"),
const SizedBox(
width: 30,
Row(
children: [
Text("${details.resolution}"),
const SizedBox(
width: 30,
),
storage.when(
data: (value) {
for (final s in value) {
if (s.id == details.storageId) {
return Text(
"${s.name}(${s.implementation})");
}
}
return const Text("未知存储");
},
error: (error, stackTrace) =>
Text("$error"),
loading: () =>
const MyProgressIndicator()),
],
),
const Divider(thickness: 1, height: 1),
Text(
"${details.name} (${details.airDate!.split("-")[0]})",
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold),
),
const Text(""),
Text(
details.overview!,
),
storage.when(
data: (value) {
for (final s in value) {
if (s.id == details.storageId) {
return Text(
"${s.name}(${s.implementation})");
}
}
return const Text("未知存储");
},
error: (error, stackTrace) =>
Text("$error"),
loading: () =>
const MyProgressIndicator()),
],
),
const Divider(thickness: 1, height: 1),
Text(
"${details.name} (${details.airDate!.split("-")[0]})",
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold),
),
const Text(""),
Text(
details.overview!,
),
)),
Column(
children: [
IconButton(
onPressed: () {
ref
.read(mediaDetailsProvider(
widget.id)
.notifier)
.delete()
.then((v) => context
.go(WelcomePage.routeMoivie))
.onError((error, trace) =>
Utils.showSnakeBar(
"删除失败:$error"));
},
icon: const Icon(Icons.delete))
],
)
],
)),
Column(
children: [
IconButton(
onPressed: () {
ref
.read(mediaDetailsProvider(widget.id)
.notifier)
.delete()
.whenComplete(() => context
.go(WelcomePage.routeMoivie))
.onError((error, trace) =>
Utils.showSnakeBar(
"删除失败:$error"));
},
icon: const Icon(Icons.delete))
],
)
],
),
),
),
],
),
],
),
),
)),
),
torrents.when(
data: (v) {
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.link!)
.whenComplete(() => Utils.showSnakeBar(
"开始下载:${torrent.name}")).onError((error, trace) => Utils.showSnakeBar("操作失败: $error"));
},
))
]);
}),
);
},
error: (error, trace) => Text("$error"),
loading: () => const MyProgressIndicator()),
NestedTabBar(
id: widget.id,
)
],
);
},
@@ -165,3 +141,140 @@ class _MovieDetailsPageState extends ConsumerState<MovieDetailsPage> {
loading: () => const MyProgressIndicator());
}
}
class NestedTabBar extends ConsumerStatefulWidget {
final String id;
const NestedTabBar({super.key, required this.id});
@override
_NestedTabBarState createState() => _NestedTabBarState();
}
class _NestedTabBarState extends ConsumerState<NestedTabBar>
with TickerProviderStateMixin {
late TabController _nestedTabController;
@override
void initState() {
super.initState();
_nestedTabController = new TabController(length: 2, vsync: this);
}
@override
void dispose() {
super.dispose();
_nestedTabController.dispose();
}
int selectedTab = 0;
@override
Widget build(BuildContext context) {
var histories = ref.watch(mediaHistoryDataProvider(widget.id));
return Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TabBar(
controller: _nestedTabController,
isScrollable: true,
onTap: (value) {
setState(() {
selectedTab = value;
});
},
tabs: const <Widget>[
Tab(
text: "下载记录",
),
Tab(
text: "资源",
),
],
),
Builder(builder: (context) {
if (selectedTab == 0) {
return histories.when(
data: (v) {
if (v.isEmpty) {
return const Center(
child: Text("无下载记录"),
);
}
return DataTable(
columns: const [
DataColumn(label: Text("#"), numeric: true),
DataColumn(label: Text("名称")),
DataColumn(label: Text("下载时间")),
],
rows: List.generate(v.length, (i) {
final activity = v[i];
return DataRow(cells: [
DataCell(Text("${activity.id}")),
DataCell(Text("${activity.sourceTitle}")),
DataCell(Text("${activity.date!.toLocal()}")),
]);
}));
},
error: (error, trace) => Text("$error"),
loading: () => const MyProgressIndicator());
} else {
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(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,9 +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";
@@ -44,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;
}
@@ -103,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

@@ -5,16 +5,37 @@ import 'package:ui/providers/APIs.dart';
import 'package:ui/providers/server_response.dart';
var activitiesDataProvider =
AsyncNotifierProvider.autoDispose<ActivityData, List<Activity>>(
AsyncNotifierProvider.family<ActivityData, List<Activity>, String>(
ActivityData.new);
class ActivityData extends AutoDisposeAsyncNotifier<List<Activity>> {
var mediaHistoryDataProvider = FutureProvider.autoDispose.family(
(ref, arg) async {
final dio = await APIs.getDio();
var resp = await dio.get("${APIs.activityMediaUrl}$arg");
final sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
List<Activity> activities = List.empty(growable: true);
for (final a in sp.data as List) {
activities.add(Activity.fromJson(a));
}
return activities;
},
);
class ActivityData extends FamilyAsyncNotifier<List<Activity>, String> {
@override
FutureOr<List<Activity>> build() async {
Timer(const Duration(seconds: 5), ref.invalidateSelf);//Periodically Refresh
FutureOr<List<Activity>> build(String arg) async {
if (arg == "active") {
//refresh active downloads
Timer(const Duration(seconds: 5),
ref.invalidateSelf); //Periodically Refresh
}
final dio = await APIs.getDio();
var resp = await dio.get(APIs.activityUrl);
var resp =
await dio.get(APIs.activityUrl, queryParameters: {"status": arg});
final sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;

View File

@@ -41,10 +41,11 @@ class AuthSettingData extends AutoDisposeAsyncNotifier<AuthSetting> {
class AuthSetting {
bool enable;
String user;
String password;
AuthSetting({required this.enable, required this.user});
AuthSetting({required this.enable, required this.user, required this.password});
factory AuthSetting.fromJson(Map<String, dynamic> json) {
return AuthSetting(enable: json["enable"], user: json["user"]);
return AuthSetting(enable: json["enable"], user: json["user"], password: json["password"]);
}
}

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,28 +176,28 @@ class MediaDetail {
resolution = json["resolution"];
storageId = json["storage_id"];
airDate = json["air_date"];
status = json["status"];
}
}
class SearchResult {
SearchResult({
required this.backdropPath,
required this.id,
required this.name,
required this.originalName,
required this.overview,
required this.posterPath,
required this.mediaType,
required this.adult,
required this.originalLanguage,
required this.genreIds,
required this.popularity,
required this.firstAirDate,
required this.voteAverage,
required this.voteCount,
required this.originCountry,
this.inWatchlist
});
SearchResult(
{required this.backdropPath,
required this.id,
required this.name,
required this.originalName,
required this.overview,
required this.posterPath,
required this.mediaType,
required this.adult,
required this.originalLanguage,
required this.genreIds,
required this.popularity,
required this.firstAirDate,
required this.voteAverage,
required this.voteCount,
required this.originCountry,
this.inWatchlist});
final String? backdropPath;
final int? id;
@@ -243,47 +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(String link) async {
final dio = await APIs.getDio();
var resp = await dio.post(APIs.availableMoviesUrl,
data: {"media_id": int.parse(mediaId!), "link": link});
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"]);
}
}

View File

@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:ui/providers/APIs.dart';
import 'package:ui/providers/settings.dart';
import 'package:ui/providers/welcome_data.dart';
import 'package:ui/widgets/utils.dart';
import 'package:ui/widgets/progress_indicator.dart';
class SearchPage extends ConsumerStatefulWidget {
@@ -28,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(
@@ -49,7 +59,6 @@ class _SearchPageState extends ConsumerState<SearchPage> {
child: Image.network(
"${APIs.tmdbImgBaseUrl}${item.posterPath}",
fit: BoxFit.contain,
headers: APIs.authHeaders,
),
),
),
@@ -145,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(
@@ -229,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(),
],
);
});
@@ -253,8 +277,15 @@ 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(
@@ -262,8 +293,17 @@ class _SearchPageState extends ConsumerState<SearchPage> {
storageSelected,
resSelected,
item.mediaType!,
pathController.text);
Navigator.of(context).pop();
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())
],
)
],
),
);
}
}

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