Compare commits

...

46 Commits

Author SHA1 Message Date
Simon Ding
acb627d011 feat: add arm v7 2024-11-26 18:50:40 +08:00
Simon Ding
7c64d964e8 ui: cancel timer before calling 2024-11-21 10:01:52 +08:00
Simon Ding
bec3b04705 ui: add macos client 2024-11-21 09:45:44 +08:00
Simon Ding
990da92b75 chore: change env name 2024-11-20 19:21:25 +08:00
Simon Ding
ee14cc63b8 chore: updates 2024-11-20 19:20:14 +08:00
Simon Ding
8df7b8665b chore: add sub ext 2024-11-20 16:25:35 +08:00
Simon Ding
ea90e014b1 feat: remove default internal size limiter 2024-11-20 15:34:57 +08:00
Simon Ding
6372c5c6e6 chore: update error msg 2024-11-20 15:06:26 +08:00
Simon Ding
7b6dba1afe feat: only accept video files and subtitles of known formats 2024-11-20 12:03:56 +08:00
Simon Ding
c833f6fab6 feat: complete size limiter feature 2024-11-19 23:54:27 +08:00
Simon Ding
b4c2002ad1 feat: apply global size limiter 2024-11-19 19:51:06 +08:00
Simon Ding
b2a9f1f83b refactor: size limiter 2024-11-19 19:24:43 +08:00
Simon Ding
b69881d26b WIP: size limiter 2024-11-19 18:22:40 +08:00
Simon Ding
be07e457d0 chore: update doc 2024-11-18 10:45:33 +08:00
Simon Ding
2cdd6e3740 doc: update 2024-11-18 00:28:10 +08:00
Simon Ding
fa2968f01a chore: remove log 2024-11-18 00:10:16 +08:00
Simon Ding
36f24a7e04 feat: alist upload as task 2024-11-17 23:46:31 +08:00
Simon Ding
ecc7465028 feat: use path escape 2024-11-17 23:44:46 +08:00
Simon Ding
3af4ac795e fix: alist upload 2024-11-17 23:40:31 +08:00
Simon Ding
af2a30405c fix 2024-11-17 22:10:24 +08:00
Simon Ding
ba3f6de852 feat: add upload progress and fix panic 2024-11-17 21:57:14 +08:00
Simon Ding
7d5ce8ba97 feat: support alist as a storage 2024-11-17 21:21:21 +08:00
Simon Ding
b136b9167f ui: change ui on add settings 2024-11-17 14:00:02 +08:00
Simon Ding
f0f3281428 feat: improve name parsing 2024-11-16 14:15:45 +08:00
Simon Ding
196ba6635f feat: start download after save to db 2024-11-16 10:27:49 +08:00
Simon Ding
b61b7f082e fix: season pack download 2024-11-16 10:19:46 +08:00
Simon Ding
105b296ba2 ui: submit search will refresh data 2024-11-15 20:15:12 +08:00
Simon Ding
c4d153f15b fix 2024-11-15 17:58:24 +08:00
Simon Ding
d2619120da fix: season pack download more than once 2024-11-15 15:59:32 +08:00
Simon Ding
fbfee65a50 feat: support for torrent with multi episodes 2024-11-15 15:43:07 +08:00
Simon Ding
c433ccaa0e fix: episode match 2024-11-15 13:12:20 +08:00
Simon Ding
58428405b0 fix: episode match 2024-11-15 12:48:12 +08:00
Simon Ding
45cd94f65b feat: parse multi episode like S01E01-S01E21 2024-11-15 12:38:10 +08:00
Simon Ding
53cbca3101 fix: name empty 2024-11-15 12:05:47 +08:00
Simon Ding
576956e271 fix: season position 2024-11-15 11:57:57 +08:00
Simon Ding
31d20b4f36 fix 2024-11-15 11:56:09 +08:00
Simon Ding
d026dc4eec feat: ability to parse multi episode 2024-11-15 11:44:19 +08:00
Simon Ding
e472d67c79 ui: update desc 2024-11-10 20:20:26 +08:00
Simon Ding
2165a8c533 WIP: init wizard 2024-11-10 15:09:16 +08:00
Simon Ding
0c3b5a6907 feat: create timer only after success 2024-11-10 13:47:50 +08:00
Simon Ding
aaa006a322 WIP: init wizard 2024-11-10 13:28:21 +08:00
Simon Ding
a83f860624 feat: no result consider ok 2024-11-09 20:31:14 +08:00
Simon Ding
b0c325bc4b feat: match reource name using tmdb api 2024-11-09 20:12:28 +08:00
Simon Ding
a0431df1ee feat: not query unaired episodes 2024-11-05 19:17:46 +08:00
Simon Ding
7b02eeac51 WIP: upload with progress 2024-11-05 18:54:40 +08:00
Simon Ding
66a307f202 feat: change timeout 2024-11-05 18:44:32 +08:00
85 changed files with 4550 additions and 789 deletions

View File

@@ -53,6 +53,7 @@ jobs:
platforms: |
linux/amd64
linux/arm64
linux/arm/v7
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -12,7 +12,7 @@
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/simon-ding/polaris)
**Polaris 是一个电视剧和电影的追踪下载软件。对动漫日剧美剧都有良好的匹配,支持webdav或者本地存储。**
**Polaris 是一个电视剧和电影的追踪下载软件。对美剧动漫日剧都有良好的匹配,支持多种存储方式webdav、alist、本地存储**
</div>
@@ -27,21 +27,22 @@
- [x] 电视剧自动追踪下载
- [x] 电影自动追踪下载
- [x] webdav 存储支持,配合 [alist](https://github.com/alist-org/alist) 或阿里云等实现更多功能
- [x] 本地、webdav [alist](https://github.com/alist-org/alist) 存储支持,使用 alist 存储支持秒传功能
- [x] 事件通知推送,目前支持 Pushover和 Bark还在扩充中
- [x] 后台代理支持
- [x] TMDB 代理支持
- [x] 用户认证
- [x] plex 刮削支持
- [x] NFO 刮削文件支持
- [x] BT/PT 支持
- [x] qbittorrent/transmission客户端支持
- [x] 支持导入plex watchlistplex里标记自动导入polaris
- [x] and more...
## Todos
- [ ] 更多通知客户端支持
- [ ] 第三方watchlist导入支持
- [ ] 更多第三方watchlist导入支持
- [ ] 手机客户端

View File

@@ -1,5 +1,7 @@
package db
import "polaris/ent/media"
var Version = "undefined"
const (
@@ -14,10 +16,14 @@ const (
SettingNfoSupportEnabled = "nfo_support_enabled"
SettingAllowQiangban = "filter_qiangban"
SettingEnableTmdbAdultContent = "tmdb_adult_content"
SetttingSizeLimiter = "size_limiter"
SettingTvNamingFormat = "tv_naming_format"
SettingMovieNamingFormat = "movie_naming_format"
SettingProwlarrInfo = "prowlarr_info"
SettingTvSizeLimiter = "tv_size_limiter"
SettingMovieSizeLimiter = "movie_size_limiter"
SettingAcceptedVideoFormats = "accepted_video_formats"
SettingAcceptedSubtitleFormats = "accepted_subtitle_formats"
)
const (
@@ -40,6 +46,17 @@ const (
const DefaultNamingFormat = "{{.NameCN}} {{.NameEN}} {{if .Year}} ({{.Year}}) {{end}}"
//https://en.wikipedia.org/wiki/Video_file_format
var defaultAcceptedVideoFormats = []string{
".webm", ".mkv", ".flv", ".vob", ".ogv", ".ogg", ".drc", ".mng", ".avi", ".mts", ".m2ts",".ts",
".mov", ".qt", ".wmv", ".yuv", ".rm", ".rmvb", ".viv", ".amv", ".mp4", ".m4p", ".m4v",
".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".m2v", ".m4v",
".svi", ".3gp", ".3g2", ".nsv",
}
var defaultAcceptedSubtitleFormats = []string{
".ass", ".srt",".vtt", ".webvtt", ".sub", ".idx",
}
type NamingInfo struct {
NameCN string
NameEN string
@@ -51,15 +68,27 @@ type ResolutionType string
const JwtSerectKey = "jwt_secrect_key"
type SizeLimiter struct {
R720p Limiter `json:"720p"`
R1080p Limiter `json:"1080p"`
R2160p Limiter `json:"2160p"`
type MediaSizeLimiter struct {
P720p SizeLimiter `json:"720p"`
P1080 SizeLimiter `json:"1080p"`
P2160 SizeLimiter `json:"2160p"`
}
type Limiter struct {
Max int `json:"max"`
Min int `json:"min"`
func (m *MediaSizeLimiter) GetLimiter(r media.Resolution) SizeLimiter {
if r == media.Resolution1080p {
return m.P1080
} else if r == media.Resolution720p {
return m.P720p
} else if r == media.Resolution2160p {
return m.P2160
}
return SizeLimiter{}
}
type SizeLimiter struct {
MaxSIze int64 `json:"max_size"`
MinSize int64 `json:"min_size"`
PreferSIze int64 `json:"prefer_size"`
}
type ProwlarrSetting struct {

View File

@@ -374,6 +374,15 @@ func (s *StorageInfo) ToWebDavSetting() WebdavSetting {
}
}
func (s *StorageInfo) ToAlistSetting() WebdavSetting {
return WebdavSetting{
URL: s.Settings["url"],
User: s.Settings["user"],
Password: s.Settings["password"],
ChangeFileHash: s.Settings["change_file_hash"],
}
}
type WebdavSetting struct {
URL string `json:"url"`
User string `json:"user"`
@@ -432,7 +441,7 @@ type Storage struct {
}
func (s *Storage) ToWebDavSetting() WebdavSetting {
if s.Implementation != storage.ImplementationWebdav {
if s.Implementation != storage.ImplementationWebdav && s.Implementation != storage.ImplementationAlist {
panic("not webdav storage")
}
var webdavSetting WebdavSetting
@@ -483,7 +492,8 @@ func (c *Client) SaveHistoryRecord(h ent.History) (*ent.History, error) {
}
return c.ent.History.Create().SetMediaID(h.MediaID).SetEpisodeID(h.EpisodeID).SetDate(time.Now()).
SetStatus(h.Status).SetTargetDir(h.TargetDir).SetSourceTitle(h.SourceTitle).SetIndexerID(h.IndexerID).
SetDownloadClientID(h.DownloadClientID).SetSize(h.Size).SetSaved(h.Saved).SetLink(h.Link).Save(context.TODO())
SetDownloadClientID(h.DownloadClientID).SetSize(h.Size).SetSaved(h.Saved).SetSeasonNum(h.SeasonNum).
SetEpisodeNums(h.EpisodeNums).SetLink(h.Link).Save(context.TODO())
}
func (c *Client) SetHistoryStatus(id int, status history.Status) error {
@@ -621,19 +631,38 @@ func (c *Client) DeleteImportlist(id int) error {
return c.ent.ImportList.DeleteOneID(id).Exec(context.TODO())
}
func (c *Client) GetSizeLimiter() (*SizeLimiter, error) {
v := c.GetSetting(SetttingSizeLimiter)
var limiter SizeLimiter
func (c *Client) GetSizeLimiter(mediaType string) (*MediaSizeLimiter, error) {
var v string
if mediaType == "tv" {
v = c.GetSetting(SettingTvSizeLimiter)
} else if mediaType == "movie" {
v = c.GetSetting(SettingMovieSizeLimiter)
} else {
return nil, errors.Errorf("media type not supported: %v", mediaType)
}
var limiter MediaSizeLimiter
if v == "" {
return &limiter, nil
}
err := json.Unmarshal([]byte(v), &limiter)
return &limiter, err
}
func (c *Client) SetSizeLimiter(limiter *SizeLimiter) error {
func (c *Client) SetSizeLimiter(mediaType string, limiter *MediaSizeLimiter) error {
data, err := json.Marshal(limiter)
if err != nil {
return err
}
return c.SetSetting(SetttingSizeLimiter, string(data))
if mediaType == "tv" {
return c.SetSetting(SettingTvSizeLimiter, string(data))
} else if mediaType == "movie" {
return c.SetSetting(SettingMovieSizeLimiter, string(data))
} else {
return errors.Errorf("media type not supported: %v", mediaType)
}
}
func (c *Client) GetTvNamingFormat() string {
@@ -661,7 +690,6 @@ func (c *Client) AddBlacklistItem(item *ent.Blacklist) error {
return c.ent.Blacklist.Create().SetType(item.Type).SetValue(item.Value).SetNotes(item.Notes).Exec(context.Background())
}
func (c *Client) GetProwlarrSetting() (*ProwlarrSetting, error) {
s := c.GetSetting(SettingProwlarrInfo)
if s == "" {
@@ -680,4 +708,54 @@ func (c *Client) SaveProwlarrSetting(se *ProwlarrSetting) error {
return err
}
return c.SetSetting(SettingProwlarrInfo, string(data))
}
func (c *Client) getAcceptedFormats(key string) ([]string, error) {
v := c.GetSetting(key)
if v == "" {
return nil, nil
}
var res []string
err := json.Unmarshal([]byte(v), &res)
return res, err
}
func (c *Client) setAcceptedFormats(key string, v []string) error {
data, err := json.Marshal(v)
if err != nil {
return err
}
return c.SetSetting(key, string(data))
}
func (c *Client) GetAcceptedVideoFormats() ([]string, error) {
res, err := c.getAcceptedFormats(SettingAcceptedVideoFormats)
if err != nil {
return nil, err
}
if res == nil {
return defaultAcceptedVideoFormats, nil
}
return res, nil
}
func (c *Client) SetAcceptedVideoFormats(key string, v []string) error {
return c.setAcceptedFormats(SettingAcceptedVideoFormats, v)
}
func (c *Client) GetAcceptedSubtitleFormats() ([]string, error) {
res, err := c.getAcceptedFormats(SettingAcceptedSubtitleFormats)
if err != nil {
return nil, err
}
if res== nil {
return defaultAcceptedSubtitleFormats, nil
}
return res, nil
}
func (c *Client) SetAcceptedSubtitleFormats(key string, v []string) error {
return c.setAcceptedFormats(SettingAcceptedSubtitleFormats, v)
}

13
doc/alist.md Normal file
View File

@@ -0,0 +1,13 @@
# alist 对接
> 本程序可以把alist作为一个存储后台使用下载完成的电影电视剧上传到alist对应的文件夹。配合阿里云、夸克云盘等实现云盘NAS功能。目前支持两种对接方式webdav和直接对接
## webdav
使用webdav形式对接本程序支持程序所有功能但是不支持秒传上传会比较慢
## alist 直接对接
存储设置里选择 alist填入对应的信息即可。
优点支持秒传上传速度快。缺点部分功能无法使用plex和nfo文件刮削

View File

@@ -9,7 +9,7 @@
```yaml
services:
polaris:
image: ghcr.io/simon-ding/polaris:latest
image: ghcr.io/simon-ding/polaris:latest
restart: always
environment:
- PUID=99 #程序运行的用户UID
@@ -23,6 +23,8 @@ services:
- 8080:8080 #端口映射,冒号前的端口可自行改为需要的
```
> latest为发布版本如果你追求新功能且能接受bug可以使用main tag
### 1.2 Docker 方式安装
也可以通过原始 docker 命令的方式安装 Polaris

View File

@@ -3,6 +3,7 @@
package ent
import (
"encoding/json"
"fmt"
"polaris/ent/history"
"strings"
@@ -19,8 +20,12 @@ type History struct {
ID int `json:"id,omitempty"`
// MediaID holds the value of the "media_id" field.
MediaID int `json:"media_id,omitempty"`
// EpisodeID holds the value of the "episode_id" field.
// deprecated
EpisodeID int `json:"episode_id,omitempty"`
// EpisodeNums holds the value of the "episode_nums" field.
EpisodeNums []int `json:"episode_nums,omitempty"`
// SeasonNum holds the value of the "season_num" field.
SeasonNum int `json:"season_num,omitempty"`
// SourceTitle holds the value of the "source_title" field.
SourceTitle string `json:"source_title,omitempty"`
// Date holds the value of the "date" field.
@@ -47,7 +52,9 @@ func (*History) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case history.FieldID, history.FieldMediaID, history.FieldEpisodeID, history.FieldSize, history.FieldDownloadClientID, history.FieldIndexerID:
case history.FieldEpisodeNums:
values[i] = new([]byte)
case history.FieldID, history.FieldMediaID, history.FieldEpisodeID, history.FieldSeasonNum, history.FieldSize, history.FieldDownloadClientID, history.FieldIndexerID:
values[i] = new(sql.NullInt64)
case history.FieldSourceTitle, history.FieldTargetDir, history.FieldLink, history.FieldStatus, history.FieldSaved:
values[i] = new(sql.NullString)
@@ -86,6 +93,20 @@ func (h *History) assignValues(columns []string, values []any) error {
} else if value.Valid {
h.EpisodeID = int(value.Int64)
}
case history.FieldEpisodeNums:
if value, ok := values[i].(*[]byte); !ok {
return fmt.Errorf("unexpected type %T for field episode_nums", values[i])
} else if value != nil && len(*value) > 0 {
if err := json.Unmarshal(*value, &h.EpisodeNums); err != nil {
return fmt.Errorf("unmarshal field episode_nums: %w", err)
}
}
case history.FieldSeasonNum:
if value, ok := values[i].(*sql.NullInt64); !ok {
return fmt.Errorf("unexpected type %T for field season_num", values[i])
} else if value.Valid {
h.SeasonNum = int(value.Int64)
}
case history.FieldSourceTitle:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field source_title", values[i])
@@ -182,6 +203,12 @@ func (h *History) String() string {
builder.WriteString("episode_id=")
builder.WriteString(fmt.Sprintf("%v", h.EpisodeID))
builder.WriteString(", ")
builder.WriteString("episode_nums=")
builder.WriteString(fmt.Sprintf("%v", h.EpisodeNums))
builder.WriteString(", ")
builder.WriteString("season_num=")
builder.WriteString(fmt.Sprintf("%v", h.SeasonNum))
builder.WriteString(", ")
builder.WriteString("source_title=")
builder.WriteString(h.SourceTitle)
builder.WriteString(", ")

View File

@@ -17,6 +17,10 @@ const (
FieldMediaID = "media_id"
// FieldEpisodeID holds the string denoting the episode_id field in the database.
FieldEpisodeID = "episode_id"
// FieldEpisodeNums holds the string denoting the episode_nums field in the database.
FieldEpisodeNums = "episode_nums"
// FieldSeasonNum holds the string denoting the season_num field in the database.
FieldSeasonNum = "season_num"
// FieldSourceTitle holds the string denoting the source_title field in the database.
FieldSourceTitle = "source_title"
// FieldDate holds the string denoting the date field in the database.
@@ -44,6 +48,8 @@ var Columns = []string{
FieldID,
FieldMediaID,
FieldEpisodeID,
FieldEpisodeNums,
FieldSeasonNum,
FieldSourceTitle,
FieldDate,
FieldTargetDir,
@@ -114,6 +120,11 @@ func ByEpisodeID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldEpisodeID, opts...).ToFunc()
}
// BySeasonNum orders the results by the season_num field.
func BySeasonNum(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldSeasonNum, opts...).ToFunc()
}
// BySourceTitle orders the results by the source_title field.
func BySourceTitle(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldSourceTitle, opts...).ToFunc()

View File

@@ -64,6 +64,11 @@ func EpisodeID(v int) predicate.History {
return predicate.History(sql.FieldEQ(FieldEpisodeID, v))
}
// SeasonNum applies equality check predicate on the "season_num" field. It's identical to SeasonNumEQ.
func SeasonNum(v int) predicate.History {
return predicate.History(sql.FieldEQ(FieldSeasonNum, v))
}
// SourceTitle applies equality check predicate on the "source_title" field. It's identical to SourceTitleEQ.
func SourceTitle(v string) predicate.History {
return predicate.History(sql.FieldEQ(FieldSourceTitle, v))
@@ -194,6 +199,66 @@ func EpisodeIDNotNil() predicate.History {
return predicate.History(sql.FieldNotNull(FieldEpisodeID))
}
// EpisodeNumsIsNil applies the IsNil predicate on the "episode_nums" field.
func EpisodeNumsIsNil() predicate.History {
return predicate.History(sql.FieldIsNull(FieldEpisodeNums))
}
// EpisodeNumsNotNil applies the NotNil predicate on the "episode_nums" field.
func EpisodeNumsNotNil() predicate.History {
return predicate.History(sql.FieldNotNull(FieldEpisodeNums))
}
// SeasonNumEQ applies the EQ predicate on the "season_num" field.
func SeasonNumEQ(v int) predicate.History {
return predicate.History(sql.FieldEQ(FieldSeasonNum, v))
}
// SeasonNumNEQ applies the NEQ predicate on the "season_num" field.
func SeasonNumNEQ(v int) predicate.History {
return predicate.History(sql.FieldNEQ(FieldSeasonNum, v))
}
// SeasonNumIn applies the In predicate on the "season_num" field.
func SeasonNumIn(vs ...int) predicate.History {
return predicate.History(sql.FieldIn(FieldSeasonNum, vs...))
}
// SeasonNumNotIn applies the NotIn predicate on the "season_num" field.
func SeasonNumNotIn(vs ...int) predicate.History {
return predicate.History(sql.FieldNotIn(FieldSeasonNum, vs...))
}
// SeasonNumGT applies the GT predicate on the "season_num" field.
func SeasonNumGT(v int) predicate.History {
return predicate.History(sql.FieldGT(FieldSeasonNum, v))
}
// SeasonNumGTE applies the GTE predicate on the "season_num" field.
func SeasonNumGTE(v int) predicate.History {
return predicate.History(sql.FieldGTE(FieldSeasonNum, v))
}
// SeasonNumLT applies the LT predicate on the "season_num" field.
func SeasonNumLT(v int) predicate.History {
return predicate.History(sql.FieldLT(FieldSeasonNum, v))
}
// SeasonNumLTE applies the LTE predicate on the "season_num" field.
func SeasonNumLTE(v int) predicate.History {
return predicate.History(sql.FieldLTE(FieldSeasonNum, v))
}
// SeasonNumIsNil applies the IsNil predicate on the "season_num" field.
func SeasonNumIsNil() predicate.History {
return predicate.History(sql.FieldIsNull(FieldSeasonNum))
}
// SeasonNumNotNil applies the NotNil predicate on the "season_num" field.
func SeasonNumNotNil() predicate.History {
return predicate.History(sql.FieldNotNull(FieldSeasonNum))
}
// SourceTitleEQ applies the EQ predicate on the "source_title" field.
func SourceTitleEQ(v string) predicate.History {
return predicate.History(sql.FieldEQ(FieldSourceTitle, v))

View File

@@ -40,6 +40,26 @@ func (hc *HistoryCreate) SetNillableEpisodeID(i *int) *HistoryCreate {
return hc
}
// SetEpisodeNums sets the "episode_nums" field.
func (hc *HistoryCreate) SetEpisodeNums(i []int) *HistoryCreate {
hc.mutation.SetEpisodeNums(i)
return hc
}
// SetSeasonNum sets the "season_num" field.
func (hc *HistoryCreate) SetSeasonNum(i int) *HistoryCreate {
hc.mutation.SetSeasonNum(i)
return hc
}
// SetNillableSeasonNum sets the "season_num" field if the given value is not nil.
func (hc *HistoryCreate) SetNillableSeasonNum(i *int) *HistoryCreate {
if i != nil {
hc.SetSeasonNum(*i)
}
return hc
}
// SetSourceTitle sets the "source_title" field.
func (hc *HistoryCreate) SetSourceTitle(s string) *HistoryCreate {
hc.mutation.SetSourceTitle(s)
@@ -234,6 +254,14 @@ func (hc *HistoryCreate) createSpec() (*History, *sqlgraph.CreateSpec) {
_spec.SetField(history.FieldEpisodeID, field.TypeInt, value)
_node.EpisodeID = value
}
if value, ok := hc.mutation.EpisodeNums(); ok {
_spec.SetField(history.FieldEpisodeNums, field.TypeJSON, value)
_node.EpisodeNums = value
}
if value, ok := hc.mutation.SeasonNum(); ok {
_spec.SetField(history.FieldSeasonNum, field.TypeInt, value)
_node.SeasonNum = value
}
if value, ok := hc.mutation.SourceTitle(); ok {
_spec.SetField(history.FieldSourceTitle, field.TypeString, value)
_node.SourceTitle = value

View File

@@ -12,6 +12,7 @@ import (
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/dialect/sql/sqljson"
"entgo.io/ent/schema/field"
)
@@ -76,6 +77,51 @@ func (hu *HistoryUpdate) ClearEpisodeID() *HistoryUpdate {
return hu
}
// SetEpisodeNums sets the "episode_nums" field.
func (hu *HistoryUpdate) SetEpisodeNums(i []int) *HistoryUpdate {
hu.mutation.SetEpisodeNums(i)
return hu
}
// AppendEpisodeNums appends i to the "episode_nums" field.
func (hu *HistoryUpdate) AppendEpisodeNums(i []int) *HistoryUpdate {
hu.mutation.AppendEpisodeNums(i)
return hu
}
// ClearEpisodeNums clears the value of the "episode_nums" field.
func (hu *HistoryUpdate) ClearEpisodeNums() *HistoryUpdate {
hu.mutation.ClearEpisodeNums()
return hu
}
// SetSeasonNum sets the "season_num" field.
func (hu *HistoryUpdate) SetSeasonNum(i int) *HistoryUpdate {
hu.mutation.ResetSeasonNum()
hu.mutation.SetSeasonNum(i)
return hu
}
// SetNillableSeasonNum sets the "season_num" field if the given value is not nil.
func (hu *HistoryUpdate) SetNillableSeasonNum(i *int) *HistoryUpdate {
if i != nil {
hu.SetSeasonNum(*i)
}
return hu
}
// AddSeasonNum adds i to the "season_num" field.
func (hu *HistoryUpdate) AddSeasonNum(i int) *HistoryUpdate {
hu.mutation.AddSeasonNum(i)
return hu
}
// ClearSeasonNum clears the value of the "season_num" field.
func (hu *HistoryUpdate) ClearSeasonNum() *HistoryUpdate {
hu.mutation.ClearSeasonNum()
return hu
}
// SetSourceTitle sets the "source_title" field.
func (hu *HistoryUpdate) SetSourceTitle(s string) *HistoryUpdate {
hu.mutation.SetSourceTitle(s)
@@ -316,6 +362,26 @@ func (hu *HistoryUpdate) sqlSave(ctx context.Context) (n int, err error) {
if hu.mutation.EpisodeIDCleared() {
_spec.ClearField(history.FieldEpisodeID, field.TypeInt)
}
if value, ok := hu.mutation.EpisodeNums(); ok {
_spec.SetField(history.FieldEpisodeNums, field.TypeJSON, value)
}
if value, ok := hu.mutation.AppendedEpisodeNums(); ok {
_spec.AddModifier(func(u *sql.UpdateBuilder) {
sqljson.Append(u, history.FieldEpisodeNums, value)
})
}
if hu.mutation.EpisodeNumsCleared() {
_spec.ClearField(history.FieldEpisodeNums, field.TypeJSON)
}
if value, ok := hu.mutation.SeasonNum(); ok {
_spec.SetField(history.FieldSeasonNum, field.TypeInt, value)
}
if value, ok := hu.mutation.AddedSeasonNum(); ok {
_spec.AddField(history.FieldSeasonNum, field.TypeInt, value)
}
if hu.mutation.SeasonNumCleared() {
_spec.ClearField(history.FieldSeasonNum, field.TypeInt)
}
if value, ok := hu.mutation.SourceTitle(); ok {
_spec.SetField(history.FieldSourceTitle, field.TypeString, value)
}
@@ -432,6 +498,51 @@ func (huo *HistoryUpdateOne) ClearEpisodeID() *HistoryUpdateOne {
return huo
}
// SetEpisodeNums sets the "episode_nums" field.
func (huo *HistoryUpdateOne) SetEpisodeNums(i []int) *HistoryUpdateOne {
huo.mutation.SetEpisodeNums(i)
return huo
}
// AppendEpisodeNums appends i to the "episode_nums" field.
func (huo *HistoryUpdateOne) AppendEpisodeNums(i []int) *HistoryUpdateOne {
huo.mutation.AppendEpisodeNums(i)
return huo
}
// ClearEpisodeNums clears the value of the "episode_nums" field.
func (huo *HistoryUpdateOne) ClearEpisodeNums() *HistoryUpdateOne {
huo.mutation.ClearEpisodeNums()
return huo
}
// SetSeasonNum sets the "season_num" field.
func (huo *HistoryUpdateOne) SetSeasonNum(i int) *HistoryUpdateOne {
huo.mutation.ResetSeasonNum()
huo.mutation.SetSeasonNum(i)
return huo
}
// SetNillableSeasonNum sets the "season_num" field if the given value is not nil.
func (huo *HistoryUpdateOne) SetNillableSeasonNum(i *int) *HistoryUpdateOne {
if i != nil {
huo.SetSeasonNum(*i)
}
return huo
}
// AddSeasonNum adds i to the "season_num" field.
func (huo *HistoryUpdateOne) AddSeasonNum(i int) *HistoryUpdateOne {
huo.mutation.AddSeasonNum(i)
return huo
}
// ClearSeasonNum clears the value of the "season_num" field.
func (huo *HistoryUpdateOne) ClearSeasonNum() *HistoryUpdateOne {
huo.mutation.ClearSeasonNum()
return huo
}
// SetSourceTitle sets the "source_title" field.
func (huo *HistoryUpdateOne) SetSourceTitle(s string) *HistoryUpdateOne {
huo.mutation.SetSourceTitle(s)
@@ -702,6 +813,26 @@ func (huo *HistoryUpdateOne) sqlSave(ctx context.Context) (_node *History, err e
if huo.mutation.EpisodeIDCleared() {
_spec.ClearField(history.FieldEpisodeID, field.TypeInt)
}
if value, ok := huo.mutation.EpisodeNums(); ok {
_spec.SetField(history.FieldEpisodeNums, field.TypeJSON, value)
}
if value, ok := huo.mutation.AppendedEpisodeNums(); ok {
_spec.AddModifier(func(u *sql.UpdateBuilder) {
sqljson.Append(u, history.FieldEpisodeNums, value)
})
}
if huo.mutation.EpisodeNumsCleared() {
_spec.ClearField(history.FieldEpisodeNums, field.TypeJSON)
}
if value, ok := huo.mutation.SeasonNum(); ok {
_spec.SetField(history.FieldSeasonNum, field.TypeInt, value)
}
if value, ok := huo.mutation.AddedSeasonNum(); ok {
_spec.AddField(history.FieldSeasonNum, field.TypeInt, value)
}
if huo.mutation.SeasonNumCleared() {
_spec.ClearField(history.FieldSeasonNum, field.TypeInt)
}
if value, ok := huo.mutation.SourceTitle(); ok {
_spec.SetField(history.FieldSourceTitle, field.TypeString, value)
}

View File

@@ -74,6 +74,8 @@ var (
{Name: "id", Type: field.TypeInt, Increment: true},
{Name: "media_id", Type: field.TypeInt},
{Name: "episode_id", Type: field.TypeInt, Nullable: true},
{Name: "episode_nums", Type: field.TypeJSON, Nullable: true},
{Name: "season_num", Type: field.TypeInt, Nullable: true},
{Name: "source_title", Type: field.TypeString},
{Name: "date", Type: field.TypeTime},
{Name: "target_dir", Type: field.TypeString},
@@ -178,7 +180,7 @@ var (
StoragesColumns = []*schema.Column{
{Name: "id", Type: field.TypeInt, Increment: true},
{Name: "name", Type: field.TypeString, Unique: true},
{Name: "implementation", Type: field.TypeEnum, Enums: []string{"webdav", "local"}},
{Name: "implementation", Type: field.TypeEnum, Enums: []string{"webdav", "local", "alist"}},
{Name: "tv_path", Type: field.TypeString, Nullable: true},
{Name: "movie_path", Type: field.TypeString, Nullable: true},
{Name: "settings", Type: field.TypeString, Nullable: true},

View File

@@ -2336,6 +2336,10 @@ type HistoryMutation struct {
addmedia_id *int
episode_id *int
addepisode_id *int
episode_nums *[]int
appendepisode_nums []int
season_num *int
addseason_num *int
source_title *string
date *time.Time
target_dir *string
@@ -2578,6 +2582,141 @@ func (m *HistoryMutation) ResetEpisodeID() {
delete(m.clearedFields, history.FieldEpisodeID)
}
// SetEpisodeNums sets the "episode_nums" field.
func (m *HistoryMutation) SetEpisodeNums(i []int) {
m.episode_nums = &i
m.appendepisode_nums = nil
}
// EpisodeNums returns the value of the "episode_nums" field in the mutation.
func (m *HistoryMutation) EpisodeNums() (r []int, exists bool) {
v := m.episode_nums
if v == nil {
return
}
return *v, true
}
// OldEpisodeNums returns the old "episode_nums" field's value of the History entity.
// If the History object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *HistoryMutation) OldEpisodeNums(ctx context.Context) (v []int, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldEpisodeNums is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldEpisodeNums requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldEpisodeNums: %w", err)
}
return oldValue.EpisodeNums, nil
}
// AppendEpisodeNums adds i to the "episode_nums" field.
func (m *HistoryMutation) AppendEpisodeNums(i []int) {
m.appendepisode_nums = append(m.appendepisode_nums, i...)
}
// AppendedEpisodeNums returns the list of values that were appended to the "episode_nums" field in this mutation.
func (m *HistoryMutation) AppendedEpisodeNums() ([]int, bool) {
if len(m.appendepisode_nums) == 0 {
return nil, false
}
return m.appendepisode_nums, true
}
// ClearEpisodeNums clears the value of the "episode_nums" field.
func (m *HistoryMutation) ClearEpisodeNums() {
m.episode_nums = nil
m.appendepisode_nums = nil
m.clearedFields[history.FieldEpisodeNums] = struct{}{}
}
// EpisodeNumsCleared returns if the "episode_nums" field was cleared in this mutation.
func (m *HistoryMutation) EpisodeNumsCleared() bool {
_, ok := m.clearedFields[history.FieldEpisodeNums]
return ok
}
// ResetEpisodeNums resets all changes to the "episode_nums" field.
func (m *HistoryMutation) ResetEpisodeNums() {
m.episode_nums = nil
m.appendepisode_nums = nil
delete(m.clearedFields, history.FieldEpisodeNums)
}
// SetSeasonNum sets the "season_num" field.
func (m *HistoryMutation) SetSeasonNum(i int) {
m.season_num = &i
m.addseason_num = nil
}
// SeasonNum returns the value of the "season_num" field in the mutation.
func (m *HistoryMutation) SeasonNum() (r int, exists bool) {
v := m.season_num
if v == nil {
return
}
return *v, true
}
// OldSeasonNum returns the old "season_num" field's value of the History entity.
// If the History object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *HistoryMutation) OldSeasonNum(ctx context.Context) (v int, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldSeasonNum is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldSeasonNum requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldSeasonNum: %w", err)
}
return oldValue.SeasonNum, nil
}
// AddSeasonNum adds i to the "season_num" field.
func (m *HistoryMutation) AddSeasonNum(i int) {
if m.addseason_num != nil {
*m.addseason_num += i
} else {
m.addseason_num = &i
}
}
// AddedSeasonNum returns the value that was added to the "season_num" field in this mutation.
func (m *HistoryMutation) AddedSeasonNum() (r int, exists bool) {
v := m.addseason_num
if v == nil {
return
}
return *v, true
}
// ClearSeasonNum clears the value of the "season_num" field.
func (m *HistoryMutation) ClearSeasonNum() {
m.season_num = nil
m.addseason_num = nil
m.clearedFields[history.FieldSeasonNum] = struct{}{}
}
// SeasonNumCleared returns if the "season_num" field was cleared in this mutation.
func (m *HistoryMutation) SeasonNumCleared() bool {
_, ok := m.clearedFields[history.FieldSeasonNum]
return ok
}
// ResetSeasonNum resets all changes to the "season_num" field.
func (m *HistoryMutation) ResetSeasonNum() {
m.season_num = nil
m.addseason_num = nil
delete(m.clearedFields, history.FieldSeasonNum)
}
// SetSourceTitle sets the "source_title" field.
func (m *HistoryMutation) SetSourceTitle(s string) {
m.source_title = &s
@@ -3050,13 +3189,19 @@ func (m *HistoryMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *HistoryMutation) Fields() []string {
fields := make([]string, 0, 11)
fields := make([]string, 0, 13)
if m.media_id != nil {
fields = append(fields, history.FieldMediaID)
}
if m.episode_id != nil {
fields = append(fields, history.FieldEpisodeID)
}
if m.episode_nums != nil {
fields = append(fields, history.FieldEpisodeNums)
}
if m.season_num != nil {
fields = append(fields, history.FieldSeasonNum)
}
if m.source_title != nil {
fields = append(fields, history.FieldSourceTitle)
}
@@ -3096,6 +3241,10 @@ func (m *HistoryMutation) Field(name string) (ent.Value, bool) {
return m.MediaID()
case history.FieldEpisodeID:
return m.EpisodeID()
case history.FieldEpisodeNums:
return m.EpisodeNums()
case history.FieldSeasonNum:
return m.SeasonNum()
case history.FieldSourceTitle:
return m.SourceTitle()
case history.FieldDate:
@@ -3127,6 +3276,10 @@ func (m *HistoryMutation) OldField(ctx context.Context, name string) (ent.Value,
return m.OldMediaID(ctx)
case history.FieldEpisodeID:
return m.OldEpisodeID(ctx)
case history.FieldEpisodeNums:
return m.OldEpisodeNums(ctx)
case history.FieldSeasonNum:
return m.OldSeasonNum(ctx)
case history.FieldSourceTitle:
return m.OldSourceTitle(ctx)
case history.FieldDate:
@@ -3168,6 +3321,20 @@ func (m *HistoryMutation) SetField(name string, value ent.Value) error {
}
m.SetEpisodeID(v)
return nil
case history.FieldEpisodeNums:
v, ok := value.([]int)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetEpisodeNums(v)
return nil
case history.FieldSeasonNum:
v, ok := value.(int)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetSeasonNum(v)
return nil
case history.FieldSourceTitle:
v, ok := value.(string)
if !ok {
@@ -3245,6 +3412,9 @@ func (m *HistoryMutation) AddedFields() []string {
if m.addepisode_id != nil {
fields = append(fields, history.FieldEpisodeID)
}
if m.addseason_num != nil {
fields = append(fields, history.FieldSeasonNum)
}
if m.addsize != nil {
fields = append(fields, history.FieldSize)
}
@@ -3266,6 +3436,8 @@ func (m *HistoryMutation) AddedField(name string) (ent.Value, bool) {
return m.AddedMediaID()
case history.FieldEpisodeID:
return m.AddedEpisodeID()
case history.FieldSeasonNum:
return m.AddedSeasonNum()
case history.FieldSize:
return m.AddedSize()
case history.FieldDownloadClientID:
@@ -3295,6 +3467,13 @@ func (m *HistoryMutation) AddField(name string, value ent.Value) error {
}
m.AddEpisodeID(v)
return nil
case history.FieldSeasonNum:
v, ok := value.(int)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.AddSeasonNum(v)
return nil
case history.FieldSize:
v, ok := value.(int)
if !ok {
@@ -3327,6 +3506,12 @@ func (m *HistoryMutation) ClearedFields() []string {
if m.FieldCleared(history.FieldEpisodeID) {
fields = append(fields, history.FieldEpisodeID)
}
if m.FieldCleared(history.FieldEpisodeNums) {
fields = append(fields, history.FieldEpisodeNums)
}
if m.FieldCleared(history.FieldSeasonNum) {
fields = append(fields, history.FieldSeasonNum)
}
if m.FieldCleared(history.FieldDownloadClientID) {
fields = append(fields, history.FieldDownloadClientID)
}
@@ -3356,6 +3541,12 @@ func (m *HistoryMutation) ClearField(name string) error {
case history.FieldEpisodeID:
m.ClearEpisodeID()
return nil
case history.FieldEpisodeNums:
m.ClearEpisodeNums()
return nil
case history.FieldSeasonNum:
m.ClearSeasonNum()
return nil
case history.FieldDownloadClientID:
m.ClearDownloadClientID()
return nil
@@ -3382,6 +3573,12 @@ func (m *HistoryMutation) ResetField(name string) error {
case history.FieldEpisodeID:
m.ResetEpisodeID()
return nil
case history.FieldEpisodeNums:
m.ResetEpisodeNums()
return nil
case history.FieldSeasonNum:
m.ResetSeasonNum()
return nil
case history.FieldSourceTitle:
m.ResetSourceTitle()
return nil

View File

@@ -66,7 +66,7 @@ func init() {
historyFields := schema.History{}.Fields()
_ = historyFields
// historyDescSize is the schema descriptor for size field.
historyDescSize := historyFields[5].Descriptor()
historyDescSize := historyFields[7].Descriptor()
// history.DefaultSize holds the default value on creation for the size field.
history.DefaultSize = historyDescSize.Default.(int)
indexersFields := schema.Indexers{}.Fields()

View File

@@ -14,7 +14,9 @@ type History struct {
func (History) Fields() []ent.Field {
return []ent.Field{
field.Int("media_id"),
field.Int("episode_id").Optional(),
field.Int("episode_id").Optional().Comment("deprecated"),
field.Ints("episode_nums").Optional(),
field.Int("season_num").Optional(),
field.String("source_title"),
field.Time("date"),
field.String("target_dir"),

View File

@@ -42,8 +42,9 @@ func (Media) Edges() []ent.Edge {
}
type MediaLimiter struct {
SizeMin int `json:"size_min"` //in B
SizeMax int `json:"size_max"` //in B
SizeMin int64 `json:"size_min"` //in B
SizeMax int64 `json:"size_max"` //in B
PreferSize int64 `json:"prefer_max"`
}
type MediaExtras struct {
@@ -51,7 +52,7 @@ type MediaExtras struct {
JavId string `json:"javid"`
//OriginCountry []string `json:"origin_country"`
OriginalLanguage string `json:"original_language"`
Genres []struct {
Genres []struct {
ID int64 `json:"id"`
Name string `json:"name"`
} `json:"genres"`

View File

@@ -14,7 +14,7 @@ type Storage struct {
func (Storage) Fields() []ent.Field {
return []ent.Field{
field.String("name").Unique(),
field.Enum("implementation").Values("webdav", "local"),
field.Enum("implementation").Values("webdav", "local", "alist"),
field.String("tv_path").Optional(),
field.String("movie_path").Optional(),
field.String("settings").Optional(),

View File

@@ -67,6 +67,7 @@ type Implementation string
const (
ImplementationWebdav Implementation = "webdav"
ImplementationLocal Implementation = "local"
ImplementationAlist Implementation = "alist"
)
func (i Implementation) String() string {
@@ -76,7 +77,7 @@ func (i Implementation) String() string {
// ImplementationValidator is a validator for the "implementation" field enum values. It is called by the builders before save.
func ImplementationValidator(i Implementation) error {
switch i {
case ImplementationWebdav, ImplementationLocal:
case ImplementationWebdav, ImplementationLocal, ImplementationAlist:
return nil
default:
return fmt.Errorf("storage: invalid enum value for implementation field: %q", i)

196
pkg/alist/alist.go Normal file
View File

@@ -0,0 +1,196 @@
package alist
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/pkg/errors"
)
type Resposne[T any] struct {
Code int `json:"code"`
Message string `json:"message"`
Data T `json:"data"`
}
type Config struct {
Username string
Password string
URL string
}
func New(cfg *Config) *Client {
cfg.URL = strings.Trim(cfg.URL, "/")
return &Client{
cfg: cfg,
http: http.DefaultClient,
}
}
type Client struct {
cfg *Config
http *http.Client
token string
}
func (c *Client) Login() (string, error) {
p := map[string]string{
"username": c.cfg.Username,
"password": c.cfg.Password,
}
data, _ := json.Marshal(p)
resp, err := c.http.Post(c.cfg.URL+loginUrl, "application/json", bytes.NewBuffer(data))
if err != nil {
return "", errors.Wrap(err, "login")
}
defer resp.Body.Close()
d1, err := io.ReadAll(resp.Body)
if err != nil {
return "", errors.Wrap(err, "read body")
}
var rp Resposne[map[string]string]
err = json.Unmarshal(d1, &rp)
if err != nil {
return "", errors.Wrap(err, "json")
}
if rp.Code != 200 {
return "", errors.Errorf("alist error: code %d, %s", rp.Code, rp.Message)
}
c.token = rp.Data["token"]
return c.token, nil
}
type LsInfo struct {
Content []struct {
Name string `json:"name"`
Size int `json:"size"`
IsDir bool `json:"is_dir"`
Modified time.Time `json:"modified"`
Created time.Time `json:"created"`
Sign string `json:"sign"`
Thumb string `json:"thumb"`
Type int `json:"type"`
Hashinfo string `json:"hashinfo"`
HashInfo any `json:"hash_info"`
} `json:"content"`
Total int `json:"total"`
Readme string `json:"readme"`
Header string `json:"header"`
Write bool `json:"write"`
Provider string `json:"provider"`
}
func (c *Client) Ls(dir string) (*LsInfo, error) {
in := map[string]string{
"path": dir,
}
resp, err := c.post(c.cfg.URL+lsUrl, in)
if err != nil {
return nil, errors.Wrap(err, "http")
}
var out Resposne[LsInfo]
err = json.Unmarshal(resp, &out)
if err != nil {
return nil, err
}
if out.Code != 200 {
return nil, errors.Errorf("alist error: code %d, %s", out.Code, out.Message)
}
return &out.Data, nil
}
func (c *Client) Mkdir(dir string) error {
in := map[string]string{
"path": dir,
}
resp, err := c.post(c.cfg.URL+mkdirUrl, in)
if err != nil {
return errors.Wrap(err, "http")
}
var out Resposne[any]
err = json.Unmarshal(resp, &out)
if err != nil {
return err
}
if out.Code != 200 {
return errors.Errorf("alist error: code %d, %s", out.Code, out.Message)
}
return nil
}
func (c *Client) post(url string, body interface{}) ([]byte, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data))
if err != nil {
return nil, errors.Wrap(err, "new request")
}
req.Header.Add("Authorization", c.token)
req.Header.Set("Content-Type", "application/json")
resp, err := c.http.Do(req)
if err != nil {
return nil, errors.Wrap(err, "http")
}
defer resp.Body.Close()
d1, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "read body")
}
return d1, nil
}
type UploadStreamResponse struct {
Task struct {
ID string `json:"id"`
Name string `json:"name"`
State int `json:"state"`
Status string `json:"status"`
Progress int `json:"progress"`
Error string `json:"error"`
} `json:"task"`
}
func (c *Client) UploadStream(reader io.Reader, size int64, toDir string) (*UploadStreamResponse, error) {
req, err := http.NewRequest(http.MethodPut, c.cfg.URL+streamUploadUrl, reader)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", c.token)
req.Header.Add("File-Path", url.PathEscape(toDir))
req.Header.Add("As-Task", "true")
req.Header.Add("Content-Type", "application/octet-stream")
req.ContentLength = size
res, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
d1, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
var out Resposne[UploadStreamResponse]
err = json.Unmarshal(d1, &out)
if err != nil {
return nil, err
}
if out.Code != 200 {
return nil, errors.Errorf("alist error: code %d, %s", out.Code, out.Message)
}
return &out.Data, nil
}

46
pkg/alist/alist_test.go Normal file
View File

@@ -0,0 +1,46 @@
package alist
import (
"os"
"polaris/log"
"testing"
)
func TestLogin(t *testing.T) {
c := New(&Config{
URL: "http://10.0.0.8:5244/",
Username: "",
Password: "",
})
cre, err := c.Login()
if err != nil {
log.Errorf("login fail: %v", err)
t.Fail()
} else {
log.Errorf("login success: %s", cre)
}
info, err := c.Ls("/aliyun")
if err != nil {
log.Errorf("ls fail: %v", err)
t.Fail()
} else {
log.Infof("ls results: %+v", info)
}
f, err := os.Open("/Users/simonding/Downloads/Steam Link_1.3.9_APKPure.apk")
if err != nil {
log.Errorf("openfile: %v", err)
t.Fail()
} else {
defer f.Close()
ss, _ := f.Stat()
log.Infof("upload file size %d", ss.Size())
info, err := c.UploadStream(f, ss.Size(), "/aliyun/Steam Link_1.3.9_APKPure.apk")
if err != nil {
log.Errorf("upload error: %v", err)
t.Fail()
} else {
log.Infof("upload success: %+v", info)
}
}
}

8
pkg/alist/url.go Normal file
View File

@@ -0,0 +1,8 @@
package alist
const (
loginUrl = "/api/auth/login"
lsUrl = "/api/fs/list"
mkdirUrl = "/api/fs/mkdir"
streamUploadUrl = "/api/fs/put"
)

View File

@@ -7,242 +7,104 @@ import (
"regexp"
"strconv"
"strings"
"time"
)
type Metadata struct {
type Info struct {
NameEn string
NameCn string
Year int
Season int
Episode int
StartEpisode int
EndEpisode int
Resolution string
IsSeasonPack bool
}
func (m *Metadata) IsAcceptable(names... string) bool {
func (m *Info) ParseExtraDescription(desc string) {
if m.IsSeasonPack { //try to parse episode number with description
mm := ParseTv(desc)
if mm.StartEpisode > 0 { //sometimes they put episode info in desc text
m.IsSeasonPack = false
m.StartEpisode = mm.StartEpisode
m.EndEpisode = mm.EndEpisode
}
}
}
func (m *Info) IsAcceptable(names ...string) bool {
re := regexp.MustCompile(`[^\p{L}\w\s]`)
nameCN := re.ReplaceAllString(strings.ToLower(m.NameCn), " ")
nameEN := re.ReplaceAllString(strings.ToLower(m.NameEn), " ")
nameCN = strings.Join(strings.Fields(nameCN), " ")
nameEN = strings.Join(strings.Fields(nameEN), " ")
for _, name := range names {
re := regexp.MustCompile(`[^\p{L}\w\s]`)
name = re.ReplaceAllString(strings.ToLower(name), " ")
nameCN := re.ReplaceAllString(strings.ToLower(m.NameCn), " ")
nameEN := re.ReplaceAllString(strings.ToLower(m.NameEn), " ")
name = strings.Join(strings.Fields(name), " ")
nameCN = strings.Join(strings.Fields(nameCN), " ")
nameEN = strings.Join(strings.Fields(nameEN), " ")
if utils.IsASCII(name) { //ascii name should match words
re := regexp.MustCompile(`\b` + name + `\b`)
return re.MatchString(nameCN) || re.MatchString(nameEN)
if re.MatchString(nameCN) || re.MatchString(nameEN) {
return true
} else {
continue
}
}
if strings.Contains(nameCN, name) || strings.Contains(nameEN, name) {
if strings.Contains(nameCN, name) || strings.Contains(nameEN, name) {
return true
}
}
return false
}
func ParseTv(name string) *Metadata {
func ParseTv(name string) *Info {
name = strings.ToLower(name)
name = strings.ReplaceAll(name, "\u200b", "") //remove unicode hidden character
if utils.ContainsChineseChar(name) {
return parseChineseName(name)
}
return parseEnglishName(name)
return parseName(name)
}
func parseEnglishName(name string) *Metadata {
re := regexp.MustCompile(`[^\p{L}\w\s]`)
name = re.ReplaceAllString(strings.ToLower(name), " ")
newSplits := strings.Split(strings.TrimSpace(name), " ")
seasonRe := regexp.MustCompile(`^s\d{1,2}`)
resRe := regexp.MustCompile(`^\d{3,4}p`)
episodeRe := regexp.MustCompile(`e\d{1,3}`)
var seasonIndex = -1
var episodeIndex = -1
var resIndex = -1
for i, p := range newSplits {
p = strings.TrimSpace(p)
if p == "" {
continue
}
if seasonRe.MatchString(p) {
//season part
seasonIndex = i
} else if resRe.MatchString(p) {
resIndex = i
}
if i >= seasonIndex && episodeRe.MatchString(p) {
episodeIndex = i
func adjacentNumber(s string, start int) (n1 int, l int) {
runes := []rune(s)
if start > len(runes)-1 { //out of bound
return -1, -1
}
var n []rune
for i := start; i < len(runes); i++ {
k := runes[i]
if (k < '0' || k > '9') && !chineseNum[k] { //not digit anymore
break
}
n = append(n, k)
}
meta := &Metadata{
Season: -1,
Episode: -1,
if len(n) == 0 {
return -1, -1
}
if seasonIndex != -1 {
//season exists
ss := seasonRe.FindAllString(newSplits[seasonIndex], -1)
if len(ss) != 0 {
//season info
ssNum := strings.TrimLeft(ss[0], "s")
n, err := strconv.Atoi(ssNum)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", ssNum, err))
}
meta.Season = n
}
} else { //maybe like Season 1?
seasonRe := regexp.MustCompile(`season \d{1,2}`)
matches := seasonRe.FindAllString(name, -1)
if len(matches) > 0 {
for i, s := range newSplits {
if s == "season" {
seasonIndex = i
}
}
numRe := regexp.MustCompile(`\d{1,2}`)
seNum := numRe.FindAllString(matches[0], -1)[0]
n, err := strconv.Atoi(seNum)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", seNum, err))
}
meta.Season = n
}
m, err := strconv.Atoi(string(n))
if err != nil {
return chinese2Num[string(n)], len(n)
}
if episodeIndex != -1 {
ep := episodeRe.FindAllString(newSplits[episodeIndex], -1)
if len(ep) > 0 {
//episode info exists
epNum := strings.TrimLeft(ep[0], "e")
n, err := strconv.Atoi(epNum)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
}
meta.Episode = n
}
} else { //no episode, maybe like One Punch Man S2 - 08 [1080p].mkv
// numRe := regexp.MustCompile(`^\d{1,2}$`)
// for i, p := range newSplits {
// if numRe.MatchString(p) {
// if i > 0 && strings.Contains(newSplits[i-1], "season") { //last word cannot be season
// continue
// }
// if i < seasonIndex {
// //episode number most likely should comes alfter season number
// continue
// }
// //episodeIndex = i
// n, err := strconv.Atoi(p)
// if err != nil {
// panic(fmt.Sprintf("convert %s error: %v", p, err))
// }
// meta.Episode = n
// }
// }
}
if resIndex != -1 {
//resolution exists
meta.Resolution = newSplits[resIndex]
}
if meta.Episode == -1 {
meta.Episode = -1
meta.IsSeasonPack = true
}
if seasonIndex > 0 {
//name exists
names := newSplits[0:seasonIndex]
meta.NameEn = strings.TrimSpace(strings.Join(names, " "))
} else {
meta.NameEn = name
}
return meta
return m, len(n)
}
func parseChineseName(name string) *Metadata {
var meta = parseEnglishName(name)
if meta.Season != -1 && (meta.Episode != -1 || meta.IsSeasonPack) {
return meta
}
meta = &Metadata{Season: 1}
//season pack
packRe := regexp.MustCompile(`(\d{1,2}-\d{1,2})|(全集)`)
if packRe.MatchString(name) {
meta.IsSeasonPack = true
}
//resolution
resRe := regexp.MustCompile(`\d{3,4}p`)
resMatches := resRe.FindAllString(name, -1)
if len(resMatches) != 0 {
meta.Resolution = resMatches[0]
} else {
if strings.Contains(name, "720") {
meta.Resolution = "720p"
} else if strings.Contains(name, "1080") {
meta.Resolution = "1080p"
}
}
//episode number
re1 := regexp.MustCompile(`\[\d{1,3}\]`)
episodeMatches1 := re1.FindAllString(name, -1)
if len(episodeMatches1) > 0 { //[11] [1080p]
epNum := strings.TrimRight(strings.TrimLeft(episodeMatches1[0], "["), "]")
n, err := strconv.Atoi(epNum)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
}
meta.Episode = n
} else { //【第09話】
re2 := regexp.MustCompile(`第\d{1,4}(话|話|集)`)
episodeMatches1 := re2.FindAllString(name, -1)
if len(episodeMatches1) > 0 {
re := regexp.MustCompile(`\d{1,4}`)
epNum := re.FindAllString(episodeMatches1[0], -1)[0]
n, err := strconv.Atoi(epNum)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
}
meta.Episode = n
} else { //SHY 靦腆英雄 / Shy -05 ( CR 1920x1080 AVC AAC MKV)
re3 := regexp.MustCompile(`[^\d\w]\d{1,2}[^\d\w]`)
epNums := re3.FindAllString(name, -1)
if len(epNums) > 0 {
re3 := regexp.MustCompile(`\d{1,2}`)
epNum := re3.FindAllString(epNums[0], -1)[0]
n, err := strconv.Atoi(epNum)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
}
meta.Episode = n
}
}
}
func findSeason(s string) (n int, p int) {
//season numner
seasonRe1 := regexp.MustCompile(`s\d{1,2}`)
seasonMatches := seasonRe1.FindAllString(name, -1)
seasonMatches := seasonRe1.FindAllString(s, -1)
if len(seasonMatches) > 0 {
seNum := seasonMatches[0][1:]
n, err := strconv.Atoi(seNum)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", seNum, err))
}
meta.Season = n
return n, strings.Index(s, seNum)
} else {
seasonRe1 := regexp.MustCompile(`season \d{1,2}`)
seasonMatches := seasonRe1.FindAllString(name, -1)
seasonMatches := seasonRe1.FindAllString(s, -1)
if len(seasonMatches) > 0 {
re3 := regexp.MustCompile(`\d{1,2}`)
seNum := re3.FindAllString(seasonMatches[0], -1)[0]
@@ -250,10 +112,10 @@ func parseChineseName(name string) *Metadata {
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", seNum, err))
}
meta.Season = n
return n, strings.Index(s, seasonMatches[0])
} else {
seasonRe1 := regexp.MustCompile(`第.{1,2}季`)
seasonMatches := seasonRe1.FindAllString(name, -1)
seasonMatches := seasonRe1.FindAllString(s, -1)
if len(seasonMatches) > 0 {
m1 := []rune(seasonMatches[0])
seNum := m1[1 : len(m1)-1]
@@ -262,58 +124,364 @@ func parseChineseName(name string) *Metadata {
log.Warnf("parse season number %v error: %v, try to parse using chinese", seNum, err)
n = chinese2Num[string(seNum)]
}
meta.Season = n
return n, strings.Index(s, seasonMatches[0])
}
}
}
return -1, -1
}
func findEpisodes(s string) (start int, end int) {
var episodeCn = map[rune]bool{
'话': true,
'話': true,
'集': true,
}
rr := []rune(s)
for i := 0; i < len(rr); i++ {
r := rr[i]
if r == 'e' {
n, l := adjacentNumber(s, i+1)
if n > 0 {
foundDash := false
for j := i + l + 1; j < len(rr); j++ {
r1 := rr[j]
if r1 == '-' {
foundDash = true
continue
}
if r1 == ' ' || r1 == 'e' {
continue
}
if foundDash {
if r1 == 's' {
s1, l1 := adjacentNumber(s, j+1)
if s1 > 0 { //S01E01-S01E21
n1, _ := adjacentNumber(s, j+l1+2)
if n1 > 0 {
return n, n1
}
}
}
n1, _ := adjacentNumber(s, j)
if n1 > 0 {
return n, n1
}
} else {
break
}
}
return n, n
}
} else if r == '第' {
n, l := adjacentNumber(s, i+1)
if len(rr) > i+l+1 && episodeCn[rr[i+l+1]] {
return n, n
} else if len(rr) > i+l+1 {
if rr[i+l+1] == '-' {
n1, l1 := adjacentNumber(s, i+l+2)
if episodeCn[rr[i+l+2+l1]] {
return n, n1
}
}
}
}
}
//episode number
re1 := regexp.MustCompile(`\[\d{1,4}\]`)
episodeMatches1 := re1.FindAllString(s, -1)
if len(episodeMatches1) > 0 { //[11] [1080p], [2022][113][HEVC][GB][4K]
for _, m := range episodeMatches1 {
epNum := strings.TrimRight(strings.TrimLeft(m, "["), "]")
n, err := strconv.Atoi(epNum)
if err != nil {
log.Debugf("convert %s error: %v", epNum, err)
continue
}
nowYear := time.Now().Year()
if n > nowYear-50 { //high possibility is year number
continue
}
return n, n
}
} else { //【第09話】
re2 := regexp.MustCompile(`第\d{1,4}([话話集])`)
episodeMatches1 := re2.FindAllString(s, -1)
if len(episodeMatches1) > 0 {
re := regexp.MustCompile(`\d{1,4}`)
epNum := re.FindAllString(episodeMatches1[0], -1)[0]
n, err := strconv.Atoi(epNum)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
}
return n, n
} else { //The Road Season 2 Episode 12 XviD-AFG
re3 := regexp.MustCompile(`episode \d{1,4}`)
epNums := re3.FindAllString(s, -1)
if len(epNums) > 0 {
re3 := regexp.MustCompile(`\d{1,4}`)
epNum := re3.FindAllString(epNums[0], -1)[0]
n, err := strconv.Atoi(epNum)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
}
return n, n
} else { //SHY 靦腆英雄 / Shy -05 ( CR 1920x1080 AVC AAC MKV)
if maybeSeasonPack(s) { //avoid miss match, season pack not use this rule
return -1, -1
}
re3 := regexp.MustCompile(`[^(season)][^\d\w]\d{1,2}[^\d\w]`)
epNums := re3.FindAllString(s, -1)
if len(epNums) > 0 {
re3 := regexp.MustCompile(`\d{1,2}`)
epNum := re3.FindAllString(epNums[0], -1)[0]
n, err := strconv.Atoi(epNum)
if err != nil {
panic(fmt.Sprintf("convert %s error: %v", epNum, err))
}
return n, n
}
}
}
}
if meta.IsSeasonPack && meta.Episode != 0 {
meta.Season = meta.Episode
meta.Episode = -1
return -1, -1
}
func matchResolution(s string) string {
//resolution
resRe := regexp.MustCompile(`\d{3,4}p`)
resMatches := resRe.FindAllString(s, -1)
if len(resMatches) != 0 {
return resMatches[0]
} else {
if strings.Contains(s, "720") {
return "720p"
} else if strings.Contains(s, "1080") {
return "1080p"
}
}
return ""
}
func maybeSeasonPack(s string) bool {
//season pack
packRe := regexp.MustCompile(`((\d{1,2}-\d{1,2}))|(complete)|(全集)`)
if packRe.MatchString(s) {
return true
}
return false
}
//func parseEnglishName(name string) *Info {
// meta := &Info{
// //Season: -1,
// Episode: -1,
// }
//
// start, end := findEpisodes(name)
// if start > 0 && end > 0 {
// meta.Episode = start
// }
//
// re := regexp.MustCompile(`[^\p{L}\w\s]`)
// name = re.ReplaceAllString(strings.ToLower(name), " ")
// newSplits := strings.Split(strings.TrimSpace(name), " ")
//
// seasonRe := regexp.MustCompile(`^s\d{1,2}`)
// resRe := regexp.MustCompile(`^\d{3,4}p`)
// episodeRe := regexp.MustCompile(`e\d{1,3}`)
//
// var seasonIndex = -1
// var episodeIndex = -1
// var resIndex = -1
// for i, p := range newSplits {
// p = strings.TrimSpace(p)
// if p == "" {
// continue
// }
// if seasonRe.MatchString(p) {
// //season part
// seasonIndex = i
// } else if resRe.MatchString(p) {
// resIndex = i
// }
// if i >= seasonIndex && episodeRe.MatchString(p) {
// episodeIndex = i
// }
// }
//
// if seasonIndex != -1 {
// //season exists
// ss := seasonRe.FindAllString(newSplits[seasonIndex], -1)
// if len(ss) != 0 {
// //season info
//
// ssNum := strings.TrimLeft(ss[0], "s")
// n, err := strconv.Atoi(ssNum)
// if err != nil {
// panic(fmt.Sprintf("convert %s error: %v", ssNum, err))
// }
// meta.Season = n
// }
// } else { //maybe like Season 1?
// seasonRe := regexp.MustCompile(`season \d{1,2}`)
// matches := seasonRe.FindAllString(name, -1)
// if len(matches) > 0 {
// for i, s := range newSplits {
// if s == "season" {
// seasonIndex = i
// }
// }
// numRe := regexp.MustCompile(`\d{1,2}`)
// seNum := numRe.FindAllString(matches[0], -1)[0]
// n, err := strconv.Atoi(seNum)
// if err != nil {
// panic(fmt.Sprintf("convert %s error: %v", seNum, err))
// }
// meta.Season = n
//
// }
// }
//
// if episodeIndex != -1 {
// // ep := episodeRe.FindAllString(newSplits[episodeIndex], -1)
// //if len(ep) > 0 {
// // //episode info exists
// // epNum := strings.TrimLeft(ep[0], "e")
// // n, err := strconv.Atoi(epNum)
// // if err != nil {
// // panic(fmt.Sprintf("convert %s error: %v", epNum, err))
// // }
// // meta.Episode = n
// //}
// } else { //no episode, maybe like One Punch Man S2 - 08 [1080p].mkv
//
// // numRe := regexp.MustCompile(`^\d{1,2}$`)
// // for i, p := range newSplits {
// // if numRe.MatchString(p) {
// // if i > 0 && strings.Contains(newSplits[i-1], "season") { //last word cannot be season
// // continue
// // }
// // if i < seasonIndex {
// // //episode number most likely should comes alfter season number
// // continue
// // }
// // //episodeIndex = i
// // n, err := strconv.Atoi(p)
// // if err != nil {
// // panic(fmt.Sprintf("convert %s error: %v", p, err))
// // }
// // meta.Episode = n
//
// // }
// // }
//
// }
// if resIndex != -1 {
// //resolution exists
// meta.Resolution = newSplits[resIndex]
// }
// if meta.Episode == -1 {
// meta.Episode = -1
// meta.IsSeasonPack = true
// }
//
// if seasonIndex > 0 {
// //name exists
// names := newSplits[0:seasonIndex]
// meta.NameEn = strings.TrimSpace(strings.Join(names, " "))
// } else {
// meta.NameEn = name
// }
//
// return meta
//}
func parseName(name string) *Info {
meta := &Info{Season: 1}
if strings.TrimSpace(name) == "" {
return meta
}
season, p := findSeason(name)
if season == -1 {
log.Debugf("not find season info: %s", name)
if !utils.IsASCII(name) {
season = 1
}
p = len(name) - 1
}
meta.Season = season
start, end := findEpisodes(name)
if start > 0 && end > 0 {
meta.StartEpisode = start
meta.EndEpisode = end
} else {
meta.IsSeasonPack = true
}
meta.Resolution = matchResolution(name)
//if meta.IsSeasonPack && meta.Episode != 0 {
// meta.Season = meta.Episode
// meta.Episode = -1
//}
//tv name
fields := strings.FieldsFunc(name, func(r rune) bool {
return r == '[' || r == ']' || r == '【' || r == '】'
})
titleCn := ""
title := ""
for _, p := range fields { //寻找匹配的最长的字符串,最有可能是名字
if utils.ContainsChineseChar(p) && len([]rune(p)) > len([]rune(titleCn)) { //最长含中文字符串
titleCn = p
}
if len([]rune(p)) > len([]rune(title)) { //最长字符串
title = p
}
}
re := regexp.MustCompile(`[^\p{L}\w\s]`)
title = re.ReplaceAllString(strings.TrimSpace(strings.ToLower(title)), "") //去除标点符号
titleCn = re.ReplaceAllString(strings.TrimSpace(strings.ToLower(titleCn)), "")
meta.NameCn = titleCn
cnRe := regexp.MustCompile(`\p{Han}.*\p{Han}`)
cnmatches := cnRe.FindAllString(titleCn, -1)
//titleCn中最长的中文字符
if len(cnmatches) > 0 {
for _, t := range cnmatches {
if len([]rune(t)) > len([]rune(meta.NameCn)) {
meta.NameCn = strings.ToLower(t)
if utils.IsASCII(name) && p < len(name) && p-1 > 0 {
meta.NameEn = strings.TrimSpace(name[:p-1])
meta.NameCn = meta.NameEn
} else {
fields := strings.FieldsFunc(name, func(r rune) bool {
return r == '[' || r == ']' || r == '【' || r == '】'
})
titleCn := ""
title := ""
for _, p := range fields { //寻找匹配的最长的字符串,最有可能是名字
if utils.ContainsChineseChar(p) && len([]rune(p)) > len([]rune(titleCn)) { //最长含中文字符串
titleCn = p
}
if len([]rune(p)) > len([]rune(title)) { //最长字符串
title = p
}
}
}
re := regexp.MustCompile(`[^\p{L}\w\s]`)
title = re.ReplaceAllString(strings.TrimSpace(strings.ToLower(title)), "") //去除标点符号
titleCn = re.ReplaceAllString(strings.TrimSpace(strings.ToLower(titleCn)), "")
//匹配title中最长拉丁字符串
enRe := regexp.MustCompile(`[[:ascii:]]*`)
enM := enRe.FindAllString(title, -1)
if len(enM) > 0 {
for _, t := range enM {
if len(t) > len(meta.NameEn) {
meta.NameEn = strings.TrimSpace(strings.ToLower(t))
meta.NameCn = titleCn
cnRe := regexp.MustCompile(`\p{Han}.*\p{Han}`)
cnmatches := cnRe.FindAllString(titleCn, -1)
//titleCn中最长的中文字符
if len(cnmatches) > 0 {
for _, t := range cnmatches {
if len([]rune(t)) > len([]rune(meta.NameCn)) {
meta.NameCn = strings.ToLower(t)
}
}
}
meta.NameEn = title
////匹配title中最长拉丁字符串
//enRe := regexp.MustCompile(`[[:ascii:]]*`)
//enM := enRe.FindAllString(title, -1)
//if len(enM) > 0 {
// for _, t := range enM {
// if len(t) > len(meta.NameEn) {
// meta.NameEn = strings.TrimSpace(strings.ToLower(t))
// }
// }
//}
}
return meta
@@ -330,3 +498,15 @@ var chinese2Num = map[string]int{
"八": 8,
"九": 9,
}
var chineseNum = map[rune]bool{
'一': true,
'二': true,
'三': true,
'四': true,
'五': true,
'六': true,
'七': true,
'八': true,
'九': true,
}

View File

@@ -21,7 +21,7 @@ func Test_ParseTV2(t *testing.T) {
m := ParseTv(s1)
log.Infof("results: %+v", m)
assert.Equal(t, m.Season, 1)
assert.Equal(t, m.Episode, 4)
assert.Equal(t, m.StartEpisode, 4)
assert.Equal(t, m.IsSeasonPack, false)
assert.Equal(t, m.Resolution, "1080p")
}
@@ -31,7 +31,7 @@ func Test_ParseTV3(t *testing.T) {
m := ParseTv(s1)
log.Infof("results: %+v", m)
assert.Equal(t, m.Season, 37)
assert.Equal(t, m.Episode, 219)
assert.Equal(t, m.StartEpisode, 219)
assert.Equal(t, m.IsSeasonPack, false)
//assert.Equal(t, m.Resolution, "1080p")
}
@@ -41,8 +41,8 @@ func Test_ParseTV4(t *testing.T) {
m := ParseTv(s1)
log.Infof("results: %+v", m)
assert.Equal(t, m.Season, 2)
//assert.Equal(t, m.Episode, 219)
assert.Equal(t, m.IsSeasonPack, true)
assert.Equal(t, m.StartEpisode, 12)
assert.Equal(t, m.IsSeasonPack, false)
//assert.Equal(t, m.Resolution, "1080p")
}
@@ -61,7 +61,7 @@ func Test_ParseTV6(t *testing.T) {
m := ParseTv(s1)
log.Infof("results: %+v", m)
assert.Equal(t, m.Season, 1)
assert.Equal(t, m.Episode, 3)
assert.Equal(t, m.StartEpisode, 3)
assert.Equal(t, m.IsSeasonPack, false)
assert.Equal(t, m.Resolution, "1080p")
}
@@ -71,7 +71,7 @@ func Test_ParseTV7(t *testing.T) {
m := ParseTv(s1)
log.Infof("results: %+v", m)
assert.Equal(t, m.Season, 1)
assert.Equal(t, m.Episode, 1113)
assert.Equal(t, m.StartEpisode, 1113)
assert.Equal(t, m.IsSeasonPack, false)
assert.Equal(t, m.Resolution, "1080p")
}
@@ -81,7 +81,7 @@ func Test_ParseTV8(t *testing.T) {
m := ParseTv(s1)
log.Infof("results: %+v", m)
assert.Equal(t, m.Season, 1)
assert.Equal(t, m.Episode, 4)
assert.Equal(t, m.StartEpisode, 4)
assert.Equal(t, m.IsSeasonPack, false)
assert.Equal(t, m.Resolution, "1080p")
}
@@ -91,7 +91,7 @@ func Test_ParseTV9(t *testing.T) {
m := ParseTv(s1)
log.Infof("results: %+v", m)
assert.Equal(t, m.Season, 1)
assert.Equal(t, m.Episode, 16)
assert.Equal(t, m.StartEpisode, 16)
assert.Equal(t, m.IsSeasonPack, false)
assert.Equal(t, m.Resolution, "1080p")
}
@@ -111,7 +111,7 @@ func Test_ParseTV11(t *testing.T) {
m := ParseTv(s1)
log.Infof("results: %+v", m)
assert.Equal(t, 2, m.Season)
assert.Equal(t, 4, m.Episode)
assert.Equal(t, 4, m.StartEpisode)
assert.Equal(t, false, m.IsSeasonPack)
assert.Equal(t, "1080p", m.Resolution)
}
@@ -121,7 +121,7 @@ func Test_ParseTV12(t *testing.T) {
m := ParseTv(s1)
log.Infof("results: %+v", m)
assert.Equal(t, 2, m.Season)
assert.Equal(t, 4, m.Episode)
assert.Equal(t, 4, m.StartEpisode)
assert.Equal(t, false, m.IsSeasonPack)
assert.Equal(t, "1080p", m.Resolution)
}
@@ -131,7 +131,7 @@ func Test_ParseTV13(t *testing.T) {
m := ParseTv(s1)
log.Infof("results: %+v", m)
assert.Equal(t, 2, m.Season)
assert.Equal(t, 8, m.Episode)
assert.Equal(t, 8, m.StartEpisode)
assert.Equal(t, false, m.IsSeasonPack)
assert.Equal(t, "1080p", m.Resolution)
}
@@ -141,10 +141,11 @@ func Test_ParseTV14(t *testing.T) {
m := ParseTv(s1)
log.Infof("results: %+v", m)
assert.Equal(t, 5, m.Season)
assert.Equal(t, 113, m.Episode)
assert.Equal(t, 113, m.StartEpisode)
assert.Equal(t, false, m.IsSeasonPack)
//assert.Equal(t, "720p", m.Resolution)
}
//
func Test_ParseTV15(t *testing.T) {
@@ -157,4 +158,58 @@ func Test_ParseTV15(t *testing.T) {
//assert.Equal(t, 113, m.Episode)
//assert.Equal(t, false, m.IsSeasonPack)
//assert.Equal(t, "720p", m.Resolution)
}
}
func Test_ParseTV16(t *testing.T) {
s1 := "Romance in the Alley 2024 S01 E24-E25 1080p WEB-DL H.264 AAC-PTerWEB"
m := ParseTv(s1)
log.Infof("results: %+v", m)
assert.Equal(t, 1, m.Season)
assert.Equal(t, 24, m.StartEpisode)
assert.Equal(t, 25, m.EndEpisode)
//assert.Equal(t, false, m.IsSeasonPack)
//assert.Equal(t, "720p", m.Resolution)
}
func Test_ParseTV17(t *testing.T) {
s1 := "小巷人家/Romance in the Alley 第24-25集 "
m := ParseTv(s1)
log.Infof("results: %+v", m)
assert.Equal(t, 1, m.Season)
assert.Equal(t, 24, m.StartEpisode)
assert.Equal(t, 25, m.EndEpisode)
//assert.Equal(t, false, m.IsSeasonPack)
//assert.Equal(t, "720p", m.Resolution)
}
// Romance in the Alley 2024 S01E01-S01E21 2160p WEB-DL HEVC AAC-UBWEB
func Test_ParseTV18(t *testing.T) {
s1 := "Romance in the Alley 2024 S01E01-S01E21 2160p WEB-DL HEVC AAC-UBWEB "
m := ParseTv(s1)
log.Infof("results: %+v", m)
assert.Equal(t, 1, m.Season)
assert.Equal(t, 1, m.StartEpisode)
assert.Equal(t, 21, m.EndEpisode)
//assert.Equal(t, false, m.IsSeasonPack)
//assert.Equal(t, "720p", m.Resolution)
}
// The Day of the Jackal (Season 1) WEB-DL 1080p
func Test_ParseTV19(t *testing.T) {
s1 := "The Day of the Jackal (Season 1) WEB-DL 1080p "
m := ParseTv(s1)
log.Infof("results: %+v", m)
assert.Equal(t, 1, m.Season)
assert.Equal(t, true, m.IsSeasonPack)
//assert.Equal(t, "720p", m.Resolution)
}
func Test_Name(t *testing.T) {
m := Info{NameEn: "word кибердеревня новый год 2023 webrip 1080p"}
b := m.IsAcceptable("word")
assert.True(t, b)
}

74
pkg/storage/alist.go Normal file
View File

@@ -0,0 +1,74 @@
package storage
import (
"io"
"io/fs"
"os"
"path/filepath"
"polaris/pkg/alist"
"github.com/gabriel-vasile/mimetype"
)
func NewAlist(cfg *alist.Config, dir string, videoFormats []string, subtitleFormats []string) (*Alist, error) {
cl := alist.New(cfg)
_, err := cl.Login()
if err != nil {
return nil, err
}
return &Alist{baseDir: dir, cfg: cfg, client: cl, videoFormats: videoFormats, subtitleFormats: subtitleFormats}, nil
}
type Alist struct {
baseDir string
cfg *alist.Config
client *alist.Client
progresser func() float64
videoFormats []string
subtitleFormats []string
}
func (a *Alist) Move(src, dest string) error {
if err := a.Copy(src, dest); err != nil {
return err
}
return os.RemoveAll(src)
}
func (a *Alist) Copy(src, dest string) error {
b, err := NewBase(src, a.videoFormats, a.subtitleFormats)
if err != nil {
return err
}
a.progresser = b.Progress
uploadFunc := func(destPath string, destInfo fs.FileInfo, srcReader io.Reader, mimeType *mimetype.MIME) error {
_, err := a.client.UploadStream(srcReader, destInfo.Size(), destPath)
return err
}
mkdirFunc := func(dir string) error {
return a.client.Mkdir(dir)
}
baseDest := filepath.Join(a.baseDir, dest)
return b.Upload(baseDest, false, false, false, uploadFunc, mkdirFunc)
}
func (a *Alist) ReadDir(dir string) ([]fs.FileInfo, error) {
return nil, nil
}
func (a *Alist) ReadFile(s string) ([]byte, error) {
return nil, nil
}
func (a *Alist) WriteFile(s string, bytes []byte) error {
return nil
}
func (a *Alist) UploadProgress() float64 {
if a.progresser == nil {
return 0
}
return a.progresser()
}

181
pkg/storage/base.go Normal file
View File

@@ -0,0 +1,181 @@
package storage
import (
"io"
"io/fs"
"os"
"path/filepath"
"polaris/log"
"polaris/pkg/utils"
"strings"
"github.com/gabriel-vasile/mimetype"
"github.com/pkg/errors"
)
type Storage interface {
Move(src, dest string) error
Copy(src, dest string) error
ReadDir(dir string) ([]fs.FileInfo, error)
ReadFile(string) ([]byte, error)
WriteFile(string, []byte) error
UploadProgress() float64
}
type uploadFunc func(destPath string, destInfo fs.FileInfo, srcReader io.Reader, mimeType *mimetype.MIME) error
type Base struct {
src string
videoFormats []string
subtitleFormats []string
totalSize int64
uploadedSize int64
}
func NewBase(src string, videoFormats []string, subtitleFormats []string) (*Base, error) {
b := &Base{src: src, videoFormats: videoFormats, subtitleFormats: subtitleFormats}
err := b.calculateSize()
return b, err
}
func (b *Base) checkVideoFilesExist() bool {
if len(b.videoFormats) == 0 { // do not check
return true
}
hasVideo := false
filepath.Walk(b.src, func(path string, info fs.FileInfo, err error) error {
ext := filepath.Ext(strings.ToLower(info.Name()))
for _, f := range b.videoFormats {
if f == ext {
hasVideo = true
}
}
return nil
})
return hasVideo
}
func (b *Base) isFileNeeded(name string) bool {
ext := filepath.Ext(strings.ToLower(name))
if len(b.videoFormats) == 0 {
return true
} else {
for _, f := range b.videoFormats {
if f == ext {
return true
}
}
}
if len(b.subtitleFormats) > 0 {
for _, f := range b.subtitleFormats {
if f == ext {
return true
}
}
}
return false
}
func (b *Base) Upload(destDir string, tryLink, detectMime, changeMediaHash bool, upload uploadFunc, mkdir func(string) error) error {
if !b.checkVideoFilesExist() {
return errors.Errorf("torrent has no video file(s)")
}
os.MkdirAll(destDir, os.ModePerm)
targetBase := filepath.Join(destDir, filepath.Base(b.src)) //文件的场景,要加上文件名, move filename ./dir/
info, err := os.Stat(b.src)
if err != nil {
return errors.Wrap(err, "read source dir")
}
if info.IsDir() { //如果是路径,则只移动路径里面的文件,不管当前路径, 行为类似 move dirname/* target_dir/
targetBase = destDir
}
log.Debugf("local storage target base dir is: %v", targetBase)
err = filepath.Walk(b.src, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(b.src, path)
if err != nil {
return errors.Wrapf(err, "relation between %s and %s", b.src, path)
}
destName := filepath.Join(targetBase, rel)
if info.IsDir() {
mkdir(destName)
} else { //is file
if !b.isFileNeeded(info.Name()) {
log.Debugf("file is not needed, skip: %s", info.Name())
return nil
}
if tryLink {
if err := os.Link(path, destName); err == nil {
return nil //link success
}
log.Warnf("hard link file error: %v, will try copy file, source: %s, dest: %s", err, path, destName)
}
if changeMediaHash {
if err := utils.ChangeFileHash(path); err != nil {
log.Errorf("change file %v hash error: %v", path, err)
}
}
if f, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm); err != nil {
return errors.Wrapf(err, "read file %v", path)
} else { //open success
defer f.Close()
var mtype *mimetype.MIME
if detectMime {
mtype, err = mimetype.DetectFile(path)
if err != nil {
return errors.Wrap(err, "mime type error")
}
}
return upload(destName, info, &progressReader{R: f, Add: func(i int) {
b.uploadedSize += int64(i)
}}, mtype)
}
}
log.Infof("file copy complete: %v", destName)
return nil
})
if err != nil {
return errors.Wrap(err, "move file error")
}
return nil
}
func (b *Base) calculateSize() error {
var size int64
err := filepath.Walk(b.src, func(_ string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
size += info.Size()
}
return err
})
b.totalSize = size
return err
}
func (b *Base) Progress() float64 {
return float64(b.uploadedSize) / float64(b.totalSize)
}
type progressReader struct {
R io.Reader
Add func(int)
}
func (pr *progressReader) Read(p []byte) (int, error) {
n, err := pr.R.Read(p)
pr.Add(n)
return n, err
}

View File

@@ -6,81 +6,45 @@ import (
"io/ioutil"
"os"
"path/filepath"
"polaris/log"
"github.com/gabriel-vasile/mimetype"
"github.com/pkg/errors"
)
type Storage interface {
Move(src, dest string) error
Copy(src, dest string) error
ReadDir(dir string) ([]fs.FileInfo, error)
ReadFile(string) ([]byte, error)
WriteFile(string, []byte) error
}
func NewLocalStorage(dir string) (*LocalStorage, error) {
func NewLocalStorage(dir string, videoFormats []string, subtitleFormats []string) (*LocalStorage, error) {
os.MkdirAll(dir, 0655)
return &LocalStorage{dir: dir}, nil
return &LocalStorage{dir: dir, videoFormats: videoFormats, subtitleFormats: subtitleFormats}, nil
}
type LocalStorage struct {
dir string
dir string
videoFormats []string
subtitleFormats []string
}
func (l *LocalStorage) Copy(src, destDir string) error {
os.MkdirAll(filepath.Join(l.dir, destDir), os.ModePerm)
targetBase := filepath.Join(l.dir, destDir, filepath.Base(src)) //文件的场景,要加上文件名, move filename ./dir/
info, err := os.Stat(src)
b, err := NewBase(src, l.videoFormats, l.subtitleFormats)
if err != nil {
return errors.Wrap(err, "read source dir")
return err
}
if info.IsDir() { //如果是路径,则只移动路径里面的文件,不管当前路径, 行为类似 move dirname/* target_dir/
targetBase = filepath.Join(l.dir, destDir)
}
log.Debugf("local storage target base dir is: %v", targetBase)
err = filepath.Walk(src, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(src, path)
if err != nil {
return errors.Wrapf(err, "relation between %s and %s", src, path)
}
destName := filepath.Join(targetBase, rel)
if info.IsDir() {
os.Mkdir(destName, os.ModePerm)
} else { //is file
if err := os.Link(path, destName); err != nil {
log.Warnf("hard link file error: %v, will try copy file, source: %s, dest: %s", err, path, destName)
if writer, err := os.OpenFile(destName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.ModePerm); err != nil {
return errors.Wrapf(err, "create file %s", destName)
} else {
defer writer.Close()
if f, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm); err != nil {
return errors.Wrapf(err, "read file %v", path)
} else { //open success
defer f.Close()
_, err := io.Copy(writer, f)
if err != nil {
return errors.Wrap(err, "transmitting data error")
}
}
}
baseDest := filepath.Join(l.dir, destDir)
uploadFunc := func(destPath string, destInfo fs.FileInfo, srcReader io.Reader, mimeType *mimetype.MIME) error {
if writer, err := os.OpenFile(destPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.ModePerm); err != nil {
return errors.Wrapf(err, "create file %s", destPath)
} else {
defer writer.Close()
_, err := io.Copy(writer, srcReader)
if err != nil {
return errors.Wrap(err, "transmitting data error")
}
}
log.Infof("file copy complete: %v", destName)
return nil
})
if err != nil {
return errors.Wrap(err, "move file error")
}
return nil
return b.Upload(baseDest, true, false, false, uploadFunc, func(s string) error {
return os.Mkdir(s, os.ModePerm)
})
}
func (l *LocalStorage) Move(src, destDir string) error {
@@ -103,3 +67,7 @@ func (l *LocalStorage) WriteFile(name string, data []byte) error {
os.MkdirAll(filepath.Dir(path), os.ModePerm)
return os.WriteFile(path, data, os.ModePerm)
}
func (l *LocalStorage) UploadProgress() float64 {
return 0
}

View File

@@ -1,13 +1,12 @@
package storage
import (
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"polaris/log"
"polaris/pkg/gowebdav"
"polaris/pkg/utils"
"github.com/gabriel-vasile/mimetype"
"github.com/pkg/errors"
@@ -17,9 +16,12 @@ type WebdavStorage struct {
fs *gowebdav.Client
dir string
changeMediaHash bool
progresser func() float64
videoFormats []string
subtitleFormats []string
}
func NewWebdavStorage(url, user, password, path string, changeMediaHash bool) (*WebdavStorage, error) {
func NewWebdavStorage(url, user, password, path string, changeMediaHash bool, videoFormats []string, subtitleFormats []string) (*WebdavStorage, error) {
c := gowebdav.NewClient(url, user, password)
if err := c.Connect(); err != nil {
return nil, errors.Wrap(err, "connect webdav")
@@ -27,71 +29,35 @@ func NewWebdavStorage(url, user, password, path string, changeMediaHash bool) (*
return &WebdavStorage{
fs: c,
dir: path,
videoFormats: videoFormats,
subtitleFormats: subtitleFormats,
}, nil
}
func (w *WebdavStorage) Copy(local, remoteDir string) error {
remoteBase := filepath.Join(w.dir, remoteDir, filepath.Base(local))
info, err := os.Stat(local)
b, err := NewBase(local, w.videoFormats, w.subtitleFormats)
if err != nil {
return errors.Wrap(err, "read source dir")
}
if info.IsDir() { //如果是路径,则只移动路径里面的文件,不管当前路径, 行为类似 move dirname/* target_dir/
remoteBase = filepath.Join(w.dir, remoteDir)
return err
}
//log.Infof("remove all content in %s", remoteBase)
//w.fs.RemoveAll(remoteBase)
err = filepath.Walk(local, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return errors.Wrapf(err, "read file %v", path)
w.progresser = b.Progress
uploadFunc := func(destPath string, destInfo fs.FileInfo, srcReader io.Reader, mtype *mimetype.MIME) error {
callback := func(r *http.Request) {
r.Header.Set("Content-Type", mtype.String())
r.ContentLength = destInfo.Size()
}
rel, err := filepath.Rel(local, path)
if err != nil {
return errors.Wrap(err, "path relation")
if err := w.fs.WriteStream(destPath, srcReader, 0666, callback); err != nil {
return errors.Wrap(err, "transmitting data error")
}
remoteName := filepath.Join(remoteBase, rel)
return nil
if info.IsDir() {
log.Infof("skip dir %v, webdav will mkdir automatically", info.Name())
}
// if err := w.fs.Mkdir(remoteName, 0666); err != nil {
// return errors.Wrapf(err, "mkdir %v", remoteName)
// }
} else { //is file
if w.changeMediaHash {
if err := utils.ChangeFileHash(path); err != nil {
log.Errorf("change file %v hash error: %v", path, err)
}
}
if f, err := os.OpenFile(path, os.O_RDONLY, 0666); err != nil {
return errors.Wrapf(err, "read file %v", path)
} else { //open success
defer f.Close()
mtype, err := mimetype.DetectFile(path)
if err != nil {
return errors.Wrap(err, "mime type error")
}
callback := func(r *http.Request) {
r.Header.Set("Content-Type", mtype.String())
r.ContentLength = info.Size()
}
if err := w.fs.WriteStream(remoteName, f, 0666, callback); err != nil {
return errors.Wrap(err, "transmitting data error")
}
}
}
log.Infof("file copy complete: %v", remoteName)
return b.Upload(filepath.Join(w.dir, remoteDir), false, true, w.changeMediaHash, uploadFunc, func(s string) error {
return nil
})
if err != nil {
return errors.Wrap(err, "move file error")
}
return nil
}
func (w *WebdavStorage) Move(local, remoteDir string) error {
@@ -112,3 +78,10 @@ func (w *WebdavStorage) ReadFile(name string) ([]byte, error) {
func (w *WebdavStorage) WriteFile(name string, data []byte) error {
return w.fs.Write(filepath.Join(w.dir, name), data, os.ModePerm)
}
func (w *WebdavStorage) UploadProgress() float64 {
if w.progresser == nil {
return 0
}
return w.progresser()
}

View File

@@ -93,11 +93,12 @@ func (r *Response) ToResults(indexer *db.TorznabInfo) []Result {
}
r := Result{
Name: item.Title,
Description: item.Description,
Link: item.Link,
Size: mustAtoI(item.Size),
Seeders: mustAtoI(item.GetAttr("seeders")),
Peers: mustAtoI(item.GetAttr("peers")),
Category: mustAtoI(item.GetAttr("category")),
Seeders: int(mustAtoI(item.GetAttr("seeders"))),
Peers: int(mustAtoI(item.GetAttr("peers"))),
Category: int(mustAtoI(item.GetAttr("category"))),
ImdbId: imdb,
DownloadVolumeFactor: tryParseFloat(item.GetAttr("downloadvolumefactor")),
UploadVolumeFactor: tryParseFloat(item.GetAttr("uploadvolumefactor")),
@@ -111,8 +112,8 @@ func (r *Response) ToResults(indexer *db.TorznabInfo) []Result {
return res
}
func mustAtoI(key string) int {
i, err := strconv.Atoi(key)
func mustAtoI(key string) int64 {
i, err := strconv.ParseInt(key, 10, 64)
if err != nil {
log.Errorf("must atoi error: %v", err)
panic(err)
@@ -130,7 +131,7 @@ func tryParseFloat(s string) float32 {
}
func Search(indexer *db.TorznabInfo, keyWord string) ([]Result, error) {
ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second)
ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, indexer.URL, nil)
@@ -180,8 +181,9 @@ func doRequest(req *http.Request) (*Response, error) {
type Result struct {
Name string `json:"name"`
Description string `json:"description"`
Link string `json:"link"`
Size int `json:"size"`
Size int64 `json:"size"`
Seeders int `json:"seeders"`
Peers int `json:"peers"`
Category int `json:"category"`

86
pkg/uploader/uploader.go Normal file
View File

@@ -0,0 +1,86 @@
package uploader
import (
"fmt"
"io"
"os"
"polaris/pkg/utils"
"sync/atomic"
"time"
)
type StreamWriter interface {
WriteStream(path string, stream io.Reader, _ os.FileMode) error
}
type Uploader struct {
sw StreamWriter
progress atomic.Int64
dir string
size int64
}
func NewUploader(dir string, sw StreamWriter) (*Uploader, error) {
size, err := utils.DirSize(dir)
if err != nil {
return nil, err
}
return &Uploader{sw: sw, dir: dir, size: size, progress: atomic.Int64{}}, nil
}
func (u *Uploader) Upload() error {
return nil
}
type ProgressReader struct {
Reader io.Reader
Progress atomic.Int64
Size int64
Name string
Once bool
Done atomic.Bool
}
func (progressReader *ProgressReader) NewLoop() {
ticker := time.NewTicker(time.Second)
var op int64
for range ticker.C {
p := progressReader.Progress.Load()
KB := (p - op) / 1024
var percent int64
if progressReader.Size != 0 {
percent = p * 100 / progressReader.Size
} else {
percent = 100
}
if KB < 1024 {
fmt.Printf("%s: %dKB/s %d%%\n", progressReader.Name, KB, percent)
} else {
fmt.Printf("%s: %.2fMB/s %d%%\n", progressReader.Name, float64(KB)/1024, percent)
}
if progressReader.Done.Load() {
ticker.Stop()
return
}
}
}
func (progressReader *ProgressReader) Read(p []byte) (int, error) {
n, err := progressReader.Reader.Read(p)
progressReader.Progress.Add(int64(n))
if !progressReader.Once {
progressReader.Once = true
go progressReader.NewLoop()
}
if err != nil {
progressReader.Done.Store(true)
}
return n, err
}
func (progressReader *ProgressReader) Close() error {
progressReader.Done.Store(true)
return nil
}

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strconv"
@@ -230,7 +231,7 @@ func Link2Magnet(link string) (string, error) {
return http.ErrUseLastResponse //do not follow redirects
},
}
resp, err := client.Get(link)
if err != nil {
return "", errors.Wrap(err, "get link")
@@ -252,7 +253,6 @@ func Link2Magnet(link string) (string, error) {
return mg.String(), nil
}
func MagnetHash(link string) (string, error) {
if mi, err := metainfo.ParseMagnetV2Uri(link); err != nil {
return "", errors.Errorf("magnet link is not valid: %v", err)
@@ -271,4 +271,18 @@ func MagnetHash(link string) (string, error) {
}
return hash, nil
}
}
}
func DirSize(path string) (int64, error) {
var size int64
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
size += info.Size()
}
return err
})
return size, err
}

View File

@@ -17,8 +17,9 @@ import (
type Activity struct {
*ent.History
Progress int `json:"progress"`
SeedRatio float64 `json:"seed_ratio"`
Progress int `json:"progress"`
SeedRatio float64 `json:"seed_ratio"`
UploadProgress float64 `json:"upload_progress"`
}
func (s *Server) GetAllActivities(c *gin.Context) (interface{}, error) {
@@ -44,6 +45,9 @@ func (s *Server) GetAllActivities(c *gin.Context) (interface{}, error) {
} else {
a.SeedRatio = r
}
if task.UploadProgresser != nil {
a.UploadProgress = task.UploadProgresser()
}
}
}
activities = append(activities, a)

2
server/core/fliters.go Normal file
View File

@@ -0,0 +1,2 @@
package core

View File

@@ -114,8 +114,9 @@ type AddWatchlistIn struct {
Resolution string `json:"resolution" binding:"required"`
Folder string `json:"folder" binding:"required"`
DownloadHistoryEpisodes bool `json:"download_history_episodes"` //for tv
SizeMin int `json:"size_min"`
SizeMax int `json:"size_max"`
SizeMin int64 `json:"size_min"`
SizeMax int64 `json:"size_max"`
PreferSize int64 `json:"prefer_size"`
}
func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
@@ -139,7 +140,7 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
}
log.Infof("find detail for tv id %d: %+v", in.TmdbID, detail)
lastSeason := 0
lastSeason := 0
for _, season := range detail.Seasons {
if season.SeasonNumber > lastSeason && season.EpisodeCount > 0 { //如果最新一季已经有剧集信息,则以最新一季为准
lastSeason = season.SeasonNumber
@@ -327,8 +328,8 @@ func (c *Client) checkMovieFolder(m *ent.Media) error {
return err
}
for _,f := range files {
if f.IsDir() || f.Size() < 100 * 1000 * 1000 /* 100M */{ //忽略路径和小于100M的文件
for _, f := range files {
if f.IsDir() || f.Size() < 100*1000*1000 /* 100M */ { //忽略路径和小于100M的文件
continue
}
meta := metadata.ParseMovie(f.Name())

View File

@@ -4,13 +4,13 @@ import (
"bytes"
"encoding/xml"
"fmt"
"github.com/pkg/errors"
"os"
"path/filepath"
"polaris/db"
"polaris/ent/media"
storage1 "polaris/ent/storage"
"polaris/log"
"polaris/pkg/alist"
"polaris/pkg/metadata"
"polaris/pkg/notifier"
"polaris/pkg/storage"
@@ -18,6 +18,8 @@ import (
"slices"
"strconv"
"strings"
"github.com/pkg/errors"
)
func (c *Client) writeNfoFile(historyId int) error {
@@ -201,11 +203,20 @@ func (c *Client) getStorage(storageId int, mediaType media.MediaType) (storage.S
if mediaType == media.MediaTypeMovie {
targetPath = st.MoviePath
}
videoFormats, err := c.db.GetAcceptedVideoFormats()
if err != nil {
log.Warnf("get accepted video format error: %v", err)
}
subtitleFormats, err := c.db.GetAcceptedSubtitleFormats()
if err != nil {
log.Warnf("get accepted subtitle format error: %v", err)
}
switch st.Implementation {
case storage1.ImplementationLocal:
storageImpl1, err := storage.NewLocalStorage(targetPath)
storageImpl1, err := storage.NewLocalStorage(targetPath, videoFormats, subtitleFormats)
if err != nil {
return nil, errors.Wrap(err, "new local")
}
@@ -213,11 +224,18 @@ func (c *Client) getStorage(storageId int, mediaType media.MediaType) (storage.S
case storage1.ImplementationWebdav:
ws := st.ToWebDavSetting()
storageImpl1, err := storage.NewWebdavStorage(ws.URL, ws.User, ws.Password, targetPath, ws.ChangeFileHash == "true")
storageImpl1, err := storage.NewWebdavStorage(ws.URL, ws.User, ws.Password, targetPath, ws.ChangeFileHash == "true", videoFormats, subtitleFormats)
if err != nil {
return nil, errors.Wrap(err, "new webdav")
}
return storageImpl1, nil
case storage1.ImplementationAlist:
cfg := st.ToWebDavSetting()
storageImpl1, err := storage.NewAlist(&alist.Config{URL: cfg.URL, Username: cfg.User, Password: cfg.Password}, targetPath, videoFormats, subtitleFormats)
if err != nil {
return nil, errors.Wrap(err, "alist")
}
return storageImpl1, nil
}
return nil, errors.New("no storage found")
}
@@ -300,9 +318,9 @@ func (c *Client) findEpisodeFilesPreMoving(historyId int) error {
}
meta := metadata.ParseTv(f.Name())
if meta.Episode > 0 {
if meta.StartEpisode > 0 {
//episode exists
ep, err := c.db.GetEpisode(his.MediaID, seasonNum, meta.Episode)
ep, err := c.db.GetEpisode(his.MediaID, seasonNum, meta.StartEpisode)
if err != nil {
return err
}

View File

@@ -1,11 +1,13 @@
package core
import (
"bytes"
"fmt"
"polaris/ent"
"polaris/ent/episode"
"polaris/ent/history"
"polaris/log"
"polaris/pkg/metadata"
"polaris/pkg/notifier/message"
"polaris/pkg/torznab"
"polaris/pkg/utils"
@@ -13,7 +15,7 @@ import (
"github.com/pkg/errors"
)
func (c *Client) DownloadEpisodeTorrent(r1 torznab.Result, seriesId, seasonNum, episodeNum int) (*string, error) {
func (c *Client) DownloadEpisodeTorrent(r1 torznab.Result, seriesId, seasonNum int, episodeNums ...int) (*string, error) {
trc, dlc, err := c.GetDownloadClient()
if err != nil {
return nil, errors.Wrap(err, "connect transmission")
@@ -31,39 +33,42 @@ func (c *Client) DownloadEpisodeTorrent(r1 torznab.Result, seriesId, seasonNum,
return nil, errors.New("no enough space")
}
var ep *ent.Episode
if episodeNum > 0 {
for _, e := range series.Episodes {
if e.SeasonNumber == seasonNum && e.EpisodeNumber == episodeNum {
ep = e
}
}
if ep == nil {
return nil, errors.Errorf("no episode of season %d episode %d", seasonNum, episodeNum)
}
} else { //season package download
ep = &ent.Episode{}
}
magnet, err := utils.Link2Magnet(r1.Link)
if err != nil {
return nil, errors.Errorf("converting link to magnet error, link: %v, error: %v", r1.Link, err)
}
torrent, err := trc.Download(magnet, downloadDir)
if err != nil {
return nil, errors.Wrap(err, "downloading")
}
torrent.Start()
dir := fmt.Sprintf("%s/Season %02d/", series.TargetDir, seasonNum)
if len(episodeNums) > 0 {
for _, epNum := range episodeNums {
var ep *ent.Episode
for _, e := range series.Episodes {
if e.SeasonNumber == seasonNum && e.EpisodeNumber == epNum {
ep = e
}
}
if ep == nil {
return nil, errors.Errorf("no episode of season %d episode %d", seasonNum, epNum)
}
if ep.Status == episode.StatusMissing {
c.db.SetEpisodeStatus(ep.ID, episode.StatusDownloading)
}
}
} else { //season package download
c.db.SetSeasonAllEpisodeStatus(seriesId, seasonNum, episode.StatusDownloading)
}
history, err := c.db.SaveHistoryRecord(ent.History{
MediaID: seriesId,
EpisodeID: ep.ID,
SourceTitle: r1.Name,
TargetDir: dir,
Status: history.StatusRunning,
Size: r1.Size,
MediaID: seriesId,
EpisodeNums: episodeNums,
SeasonNum: seasonNum,
SourceTitle: r1.Name,
TargetDir: dir,
Status: history.StatusRunning,
Size: int(r1.Size),
//Saved: torrent.Save(),
Link: magnet,
DownloadClientID: dlc.ID,
@@ -72,42 +77,122 @@ func (c *Client) DownloadEpisodeTorrent(r1 torznab.Result, seriesId, seasonNum,
if err != nil {
return nil, errors.Wrap(err, "save record")
}
if episodeNum > 0 {
if ep.Status == episode.StatusMissing {
c.db.SetEpisodeStatus(ep.ID, episode.StatusDownloading)
}
} else {
c.db.SetSeasonAllEpisodeStatus(seriesId, seasonNum, episode.StatusDownloading)
torrent, err := trc.Download(magnet, downloadDir)
if err != nil {
return nil, errors.Wrap(err, "downloading")
}
torrent.Start()
c.tasks[history.ID] = &Task{Torrent: torrent}
c.sendMsg(fmt.Sprintf(message.BeginDownload, r1.Name))
name := r1.Name
if len(episodeNums) > 0 {
buff := &bytes.Buffer{}
for i, ep := range episodeNums {
if i != 0 {
buff.WriteString(",")
}
buff.WriteString(fmt.Sprint(ep))
}
name = fmt.Sprintf("第%s集 (%s)", buff.String(), name)
} else {
name = fmt.Sprintf("全集 (%s)", name)
}
c.sendMsg(fmt.Sprintf(message.BeginDownload, name))
log.Infof("success add %s to download task", r1.Name)
return &r1.Name, nil
}
func (c *Client) SearchAndDownload(seriesId, seasonNum, episodeNum int) (*string, error) {
var episodes []int
if episodeNum > 0 {
episodes = append(episodes, episodeNum)
/*
tmdb 校验获取的资源名如果用资源名在tmdb搜索出来的结果能匹配上想要的资源则认为资源有效否则无效
解决名称过于简单的影视会匹配过多资源的问题, 例如:梦魇绝镇 FROM
*/
func (c *Client) checkBtReourceWithTmdb(r *torznab.Result, seriesId int) bool {
m := metadata.ParseTv(r.Name)
se, err := c.MustTMDB().SearchMedia(m.NameEn, "", 1)
if err != nil {
log.Warnf("tmdb search error, consider this torrent ok: %v", err)
return true
} else {
if len(se.Results) == 0 {
log.Debugf("tmdb search no result, consider this torrent ok: %s", r.Name) //because tv name parse is not accurate
return true
}
series := c.db.GetMediaDetails(seriesId)
se0 := se.Results[0]
if se0.ID != int64(series.TmdbID) {
log.Warnf("bt reosurce name not match tmdb id: %s", r.Name)
return false
} else { //resource tmdb id match
return true
}
}
}
func (c *Client) SearchAndDownload(seriesId, seasonNum int, episodeNums ...int) ([]string, error) {
res, err := SearchTvSeries(c.db, &SearchParam{
MediaId: seriesId,
SeasonNum: seasonNum,
Episodes: episodes,
Episodes: episodeNums,
CheckFileSize: true,
CheckResolution: true,
})
if err != nil {
return nil, err
}
r1 := res[0]
log.Infof("found resource to download: %+v", r1)
return c.DownloadEpisodeTorrent(r1, seriesId, seasonNum, episodeNum)
wanted := make(map[int]bool, len(episodeNums))
for _, ep := range episodeNums {
wanted[ep] = true
}
var torrentNames []string
lo:
for _, r := range res {
if !c.checkBtReourceWithTmdb(&r, seriesId) {
continue
}
m := metadata.ParseTv(r.Name)
m.ParseExtraDescription(r.Description)
if len(episodeNums) == 0 { //want season pack
if m.IsSeasonPack {
name, err := c.DownloadEpisodeTorrent(r, seriesId, seasonNum)
if err != nil {
return nil, err
}
torrentNames = append(torrentNames, *name)
break lo
}
} else {
torrentEpisodes := make([]int, 0)
for i := m.StartEpisode; i <= m.EndEpisode; i++ {
if !wanted[i] { //torrent has episode not wanted
continue lo
}
torrentEpisodes = append(torrentEpisodes, i)
}
name, err := c.DownloadEpisodeTorrent(r, seriesId, seasonNum, torrentEpisodes...)
if err != nil {
return nil, err
}
torrentNames = append(torrentNames, *name)
for _, num := range torrentEpisodes {
delete(wanted, num) //delete downloaded episode from wanted
}
}
}
if len(wanted) > 0 {
log.Warnf("still wanted but not downloaded episodes: %v", wanted)
}
return torrentNames, nil
}
func (c *Client) DownloadMovie(m *ent.Media, link, name string, size int, indexerID int) (*string, error) {
func (c *Client) DownloadMovie(m *ent.Media, link, name string, size int64, indexerID int) (*string, error) {
trc, dlc, err := c.GetDownloadClient()
if err != nil {
return nil, errors.Wrap(err, "connect transmission")
@@ -129,12 +214,12 @@ func (c *Client) DownloadMovie(m *ent.Media, link, name string, size int, indexe
go func() {
ep, _ := c.db.GetMovieDummyEpisode(m.ID)
history, err := c.db.SaveHistoryRecord(ent.History{
MediaID: m.ID,
EpisodeID: ep.ID,
SourceTitle: name,
TargetDir: m.TargetDir,
Status: history.StatusRunning,
Size: size,
MediaID: m.ID,
EpisodeID: ep.ID,
SourceTitle: name,
TargetDir: m.TargetDir,
Status: history.StatusRunning,
Size: int(size),
//Saved: torrent.Save(),
Link: magnet,
DownloadClientID: dlc.ID,

View File

@@ -2,6 +2,7 @@ package core
import (
"fmt"
"os"
"path/filepath"
"polaris/db"
"polaris/ent"
@@ -12,6 +13,7 @@ import (
"polaris/pkg"
"polaris/pkg/notifier/message"
"polaris/pkg/utils"
"time"
"github.com/pkg/errors"
)
@@ -19,6 +21,10 @@ import (
func (c *Client) addSysCron() {
c.registerCronJob("check_running_tasks", "@every 1m", c.checkTasks)
c.registerCronJob("check_available_medias_to_download", "0 0 * * * *", func() error {
v := os.Getenv("POLARIS_NO_AUTO_DOWNLOAD")
if v == "true" {
return nil
}
c.downloadAllTvSeries()
c.downloadAllMovies()
return nil
@@ -114,6 +120,38 @@ func (c *Client) postTaskProcessing(id int) {
}
}
func getSeasonNum(h *ent.History) int {
if h.SeasonNum != 0 {
return h.SeasonNum
}
seasonNum, err := utils.SeasonId(h.TargetDir)
if err != nil {
log.Errorf("no season id: %v", h.TargetDir)
seasonNum = -1
}
return seasonNum
}
func (c *Client) GetEpisodeIds(r *ent.History) []int {
var episodeIds []int
seasonNum := getSeasonNum(r)
if r.EpisodeID > 0 {
episodeIds = append(episodeIds, r.EpisodeID)
}
if len(r.EpisodeNums) > 0 {
series := c.db.GetMediaDetails(r.MediaID)
for _, epNum := range r.EpisodeNums {
for _, ep := range series.Episodes {
if ep.SeasonNumber == seasonNum && ep.EpisodeNumber == epNum {
episodeIds = append(episodeIds, ep.ID)
}
}
}
}
return episodeIds
}
func (c *Client) moveCompletedTask(id int) (err1 error) {
torrent := c.tasks[id]
r := c.db.GetHistory(id)
@@ -122,11 +160,7 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
return nil
}
c.db.SetHistoryStatus(r.ID, history.StatusUploading)
seasonNum, err := utils.SeasonId(r.TargetDir)
if err != nil {
log.Errorf("no season id: %v", r.TargetDir)
seasonNum = -1
}
downloadclient, err := c.db.GetDownloadClient(r.DownloadClientID)
if err != nil {
log.Errorf("get task download client error: %v, use default one", err)
@@ -137,13 +171,19 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
return err
}
seasonNum := getSeasonNum(r)
episodeIds := c.GetEpisodeIds(r)
defer func() {
if err1 != nil {
c.db.SetHistoryStatus(r.ID, history.StatusFail)
if r.EpisodeID != 0 {
if !c.db.IsEpisodeDownloadingOrDownloaded(r.EpisodeID) {
c.db.SetEpisodeStatus(r.EpisodeID, episode.StatusMissing)
if len(episodeIds) > 0 {
for _, id := range episodeIds {
if !c.db.IsEpisodeDownloadingOrDownloaded(id) {
c.db.SetEpisodeStatus(id, episode.StatusMissing)
}
}
} else {
c.db.SetSeasonAllEpisodeStatus(r.MediaID, seasonNum, episode.StatusMissing)
@@ -172,10 +212,13 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
if err := stImpl.Copy(filepath.Join(c.db.GetDownloadDir(), torrentName), r.TargetDir); err != nil {
return errors.Wrap(err, "move file")
}
torrent.UploadProgresser = stImpl.UploadProgress
c.db.SetHistoryStatus(r.ID, history.StatusSeeding)
if r.EpisodeID != 0 {
c.db.SetEpisodeStatus(r.EpisodeID, episode.StatusDownloaded)
if len(episodeIds) > 0 {
for _, id := range episodeIds {
c.db.SetEpisodeStatus(id, episode.StatusDownloaded)
}
} else {
c.db.SetSeasonAllEpisodeStatus(r.MediaID, seasonNum, episode.StatusDownloaded)
}
@@ -248,6 +291,7 @@ func (c *Client) CheckDownloadedSeriesFiles(m *ent.Media) error {
type Task struct {
//Processing bool
pkg.Torrent
UploadProgresser func() float64
}
func (c *Client) DownloadSeriesAllEpisodes(id int) []string {
@@ -262,40 +306,53 @@ func (c *Client) DownloadSeriesAllEpisodes(id int) []string {
continue
}
wantedSeasonPack := true
seasonEpisodesWanted := make(map[int][]int, 0)
for _, ep := range epsides {
if !ep.Monitored {
wantedSeasonPack = false
continue
}
if ep.Status != episode.StatusMissing {
wantedSeasonPack = false
continue
}
if ep.AirDate != "" {
t, err := time.Parse("2006-01-02", ep.AirDate)
if err != nil {
continue
}
/*
-------- now ------ t -----
t - 1day < now 要检测的剧集
提前一天开始检测
*/
if time.Now().Before(t.Add(-24 * time.Hour)) { //not aired
wantedSeasonPack = false
continue
}
}
seasonEpisodesWanted[ep.SeasonNumber] = append(seasonEpisodesWanted[ep.SeasonNumber], ep.EpisodeNumber)
}
if wantedSeasonPack {
name, err := c.SearchAndDownload(id, seasonNum, -1)
names, err := c.SearchAndDownload(id, seasonNum)
if err == nil {
allNames = append(allNames, *name)
log.Infof("begin download torrent resource: %v", name)
allNames = append(allNames, names...)
log.Infof("begin download torrent resource: %v", names)
} else {
log.Warnf("finding season pack error: %v", err)
wantedSeasonPack = false
}
}
if !wantedSeasonPack {
for _, ep := range epsides {
if !ep.Monitored {
continue
}
if ep.Status != episode.StatusMissing {
continue
}
name, err := c.SearchAndDownload(id, ep.SeasonNumber, ep.EpisodeNumber)
for se, eps := range seasonEpisodesWanted {
names, err := c.SearchAndDownload(id, se, eps...)
if err != nil {
log.Warnf("finding resoruces of season %d episode %d error: %v", ep.SeasonNumber, ep.EpisodeNumber, err)
log.Warnf("finding resoruces of season %d episode %v error: %v", se, eps, err)
continue
} else {
allNames = append(allNames, *name)
log.Infof("begin download torrent resource: %v", name)
allNames = append(allNames, names...)
log.Infof("begin download torrent resource: %v", names)
}
}
@@ -377,12 +434,12 @@ func (c *Client) downloadMovieSingleEpisode(ep *ent.Episode, targetDir string) (
torrent.Start()
history, err := c.db.SaveHistoryRecord(ent.History{
MediaID: ep.MediaID,
EpisodeID: ep.ID,
SourceTitle: r1.Name,
TargetDir: targetDir,
Status: history.StatusRunning,
Size: r1.Size,
MediaID: ep.MediaID,
EpisodeID: ep.ID,
SourceTitle: r1.Name,
TargetDir: targetDir,
Status: history.StatusRunning,
Size: int(r1.Size),
//Saved: torrent.Save(),
Link: magnet,
DownloadClientID: dlc.ID,

View File

@@ -31,17 +31,22 @@ func SearchTvSeries(db1 *db.Client, param *SearchParam) ([]torznab.Result, error
if series == nil {
return nil, fmt.Errorf("no tv series of id %v", param.MediaId)
}
limiter, err := db1.GetSizeLimiter("tv")
if err != nil {
log.Warnf("get tv size limiter: %v", err)
limiter = &db.MediaSizeLimiter{}
}
log.Debugf("check tv series %s, season %d, episode %v", series.NameEn, param.SeasonNum, param.Episodes)
res := searchWithTorznab(db1, prowlarr.TV, series.NameEn, series.NameCn, series.OriginalName)
var filtered []torznab.Result
lo:
for _, r := range res {
//log.Infof("torrent resource: %+v", r)
meta := metadata.ParseTv(r.Name)
if meta == nil { //cannot parse name
continue
}
meta.ParseExtraDescription(r.Description)
if isImdbidNotMatch(series.ImdbID, r.ImdbId) { //has imdb id and not match
continue
}
@@ -61,9 +66,15 @@ func SearchTvSeries(db1 *db.Client, param *SearchParam) ([]torznab.Result, error
continue
}
if len(param.Episodes) > 0 && !slices.Contains(param.Episodes, meta.Episode) { //not season pack, but episode number not equal
continue
if len(param.Episodes) > 0 { //not season pack, but episode number not equal
if meta.StartEpisode <= 0 {
continue lo
}
for i := meta.StartEpisode; i <= meta.EndEpisode; i++ {
if !slices.Contains(param.Episodes, i) {
continue lo
}
}
} else if len(param.Episodes) == 0 && !meta.IsSeasonPack { //want season pack, but not season pack
continue
}
@@ -74,7 +85,7 @@ func SearchTvSeries(db1 *db.Client, param *SearchParam) ([]torznab.Result, error
continue
}
if !torrentSizeOk(series, r.Size, param) {
if !torrentSizeOk(series, limiter, r.Size, meta.EndEpisode+1-meta.StartEpisode, param) {
continue
}
@@ -108,36 +119,47 @@ func imdbIDMatchExact(id1, id2 string) bool {
return id1 == id2
}
func torrentSizeOk(detail *db.MediaDetails, torrentSize int, param *SearchParam) bool {
defaultMinSize := 80 * 1000 * 1000 //tv, 80M min
if detail.MediaType == media.MediaTypeMovie {
defaultMinSize = 200 * 1000 * 1000 // movie, 200M min
}
if detail.Limiter.SizeMin > 0 { //if size limiter set, use configured min size
defaultMinSize = detail.Limiter.SizeMin
}
func torrentSizeOk(detail *db.MediaDetails, globalLimiter *db.MediaSizeLimiter, torrentSize int64,
torrentEpisodeNum int, param *SearchParam) bool {
multiplier := 1 //大小倍数正常为1如果是季包则为季内集数
if detail.MediaType == media.MediaTypeTv && len(param.Episodes) == 0 { //tv season pack
multiplier = seasonEpisodeCount(detail, param.SeasonNum)
multiplier := 1 //大小倍数正常为1如果是季包则为季内集数
if detail.MediaType == media.MediaTypeTv {
if len(param.Episodes) == 0 { //want tv season pack
multiplier = seasonEpisodeCount(detail, param.SeasonNum)
} else {
multiplier = torrentEpisodeNum
}
}
if param.CheckFileSize { //check file size when trigger automatic download
if detail.Limiter.SizeMin > 0 { //min size
sizeMin := detail.Limiter.SizeMin * multiplier
sizeMin := detail.Limiter.SizeMin * int64(multiplier)
if torrentSize < sizeMin { //比最小要求的大小还要小, min size not qualify
return false
}
} else if globalLimiter != nil {
resLimiter := globalLimiter.GetLimiter(detail.Resolution)
sizeMin := resLimiter.MinSize * int64(multiplier)
if torrentSize < sizeMin { //比最小要求的大小还要小, min size not qualify
return false
}
}
if detail.Limiter.SizeMax > 0 { //max size
sizeMax := detail.Limiter.SizeMax * multiplier
sizeMax := detail.Limiter.SizeMax * int64(multiplier)
if torrentSize > sizeMax { //larger than max size wanted, max size not qualify
return false
}
} else if globalLimiter != nil {
resLimiter := globalLimiter.GetLimiter(detail.Resolution)
sizeMax := resLimiter.MaxSIze * int64(multiplier)
if torrentSize > sizeMax { //larger than max size wanted, max size not qualify
return false
}
}
}
return torrentSize > defaultMinSize*multiplier
return true
}
func seasonEpisodeCount(detail *db.MediaDetails, seasonNum int) int {
@@ -171,6 +193,12 @@ func SearchMovie(db1 *db.Client, param *SearchParam) ([]torznab.Result, error) {
return nil, errors.New("no media found of id")
}
limiter, err := db1.GetSizeLimiter("movie")
if err != nil {
log.Warnf("get tv size limiter: %v", err)
limiter = &db.MediaSizeLimiter{}
}
res := searchWithTorznab(db1, prowlarr.Movie, movieDetail.NameEn, movieDetail.NameCn, movieDetail.OriginalName)
if movieDetail.Extras.IsJav() {
res1 := searchWithTorznab(db1, prowlarr.Movie, movieDetail.Extras.JavId)
@@ -211,7 +239,7 @@ func SearchMovie(db1 *db.Client, param *SearchParam) ([]torznab.Result, error) {
continue
}
if !torrentSizeOk(movieDetail, r.Size, param) {
if !torrentSizeOk(movieDetail, limiter, r.Size, 1, param) {
continue
}

View File

@@ -28,7 +28,7 @@ func (s *Server) searchAndDownloadSeasonPackage(seriesId, seasonNum int) (*strin
r1 := res[0]
log.Infof("found resource to download: %+v", r1)
return s.core.DownloadEpisodeTorrent(r1, seriesId, seasonNum, -1)
return s.core.DownloadEpisodeTorrent(r1, seriesId, seasonNum)
}
@@ -120,7 +120,7 @@ func (s *Server) SearchTvAndDownload(c *gin.Context) (interface{}, error) {
if err != nil {
return nil, errors.Wrap(err, "download")
}
name = *name1
name = name1[0]
}
return gin.H{
@@ -154,7 +154,7 @@ func (s *Server) DownloadTorrent(c *gin.Context) (interface{}, error) {
name = fmt.Sprintf("%v S%02d", m.OriginalName, in.Season)
}
res := torznab.Result{Name: name, Link: in.Link, Size: in.Size}
return s.core.DownloadEpisodeTorrent(res, in.MediaID, in.Season, -1)
return s.core.DownloadEpisodeTorrent(res, in.MediaID, in.Season)
}
name := in.Name
if name == "" {

View File

@@ -72,6 +72,8 @@ func (s *Server) Serve() error {
setting.POST("/cron/trigger", HttpHandler(s.TriggerCronJob))
setting.GET("/prowlarr", HttpHandler(s.GetProwlarrSetting))
setting.POST("/prowlarr", HttpHandler(s.SaveProwlarrSetting))
setting.GET("/limiter", HttpHandler(s.GetSizeLimiter))
setting.POST("/limiter", HttpHandler(s.SetSizeLimiter))
}
activity := api.Group("/activity")
{

View File

@@ -71,7 +71,6 @@ func (s *Server) SetSetting(c *gin.Context) (interface{}, error) {
if _, err := template.New("test").Parse(in.MovieNamingFormat); err != nil {
return nil, errors.Wrap(err, "movie format")
}
s.db.SetSetting(db.SettingMovieNamingFormat, in.MovieNamingFormat)
} else {
s.db.SetSetting(db.SettingMovieNamingFormat, "")
@@ -105,7 +104,6 @@ func (s *Server) SetSetting(c *gin.Context) (interface{}, error) {
return nil, nil
}
func (s *Server) GetSetting(c *gin.Context) (interface{}, error) {
tmdb := s.db.GetSetting(db.SettingTmdbApiKey)
downloadDir := s.db.GetSetting(db.SettingDownloadDir)
@@ -307,7 +305,7 @@ func (s *Server) TriggerCronJob(c *gin.Context) (interface{}, error) {
}
func (s *Server) GetProwlarrSetting(c *gin.Context) (interface{}, error) {
se, err :=s.db.GetProwlarrSetting()
se, err := s.db.GetProwlarrSetting()
if err != nil {
return &db.ProwlarrSetting{}, nil
}
@@ -322,7 +320,7 @@ func (s *Server) SaveProwlarrSetting(c *gin.Context) (interface{}, error) {
client := prowlarr.New(in.ApiKey, in.URL)
if _, err := client.GetIndexers(prowlarr.TV); err != nil {
return nil, errors.Wrap(err, "connect to prowlarr error")
}
}
}
err := s.db.SaveProwlarrSetting(&in)
if err != nil {
@@ -330,3 +328,38 @@ func (s *Server) SaveProwlarrSetting(c *gin.Context) (interface{}, error) {
}
return "success", nil
}
type ResolutionSizeLimiter struct {
TvLimiter *db.MediaSizeLimiter `json:"tv_limiter"`
MovieLimiter *db.MediaSizeLimiter `json:"movie_limiter"`
}
func (s *Server) GetSizeLimiter(c *gin.Context) (interface{}, error) {
tv, err := s.db.GetSizeLimiter("tv")
if err != nil {
return nil, errors.Wrap(err, "db")
}
movie, err := s.db.GetSizeLimiter("movie")
if err != nil {
return nil, errors.Wrap(err, "db")
}
r := ResolutionSizeLimiter{
TvLimiter: tv,
MovieLimiter: movie,
}
return r, nil
}
func (s *Server) SetSizeLimiter(c *gin.Context) (interface{}, error) {
var in ResolutionSizeLimiter
if err := c.ShouldBindJSON(&in); err != nil {
return nil, err
}
if err := s.db.SetSizeLimiter("tv", in.TvLimiter); err != nil {
return nil, errors.Wrap(err, "db")
}
if err := s.db.SetSizeLimiter("movie", in.MovieLimiter); err != nil {
return nil, errors.Wrap(err, "db")
}
return "success", nil
}

View File

@@ -2,9 +2,11 @@ package server
import (
"fmt"
"os"
"polaris/db"
"polaris/log"
"polaris/pkg/alist"
"polaris/pkg/storage"
"polaris/pkg/utils"
"strconv"
@@ -28,7 +30,7 @@ func (s *Server) AddStorage(c *gin.Context) (interface{}, error) {
if in.Implementation == "webdav" {
//test webdav
wd := in.ToWebDavSetting()
st, err := storage.NewWebdavStorage(wd.URL, wd.User, wd.Password, in.TvPath, false)
st, err := storage.NewWebdavStorage(wd.URL, wd.User, wd.Password, in.TvPath, false, nil, nil)
if err != nil {
return nil, errors.Wrap(err, "new webdav")
}
@@ -39,6 +41,21 @@ func (s *Server) AddStorage(c *gin.Context) (interface{}, error) {
for _, f := range fs {
log.Infof("file name: %v", f.Name())
}
} else if in.Implementation == "alist" {
cfg := in.ToAlistSetting()
_, err := storage.NewAlist(&alist.Config{URL: cfg.URL, Username: cfg.User, Password: cfg.Password}, in.TvPath, nil, nil)
if err != nil {
return nil, errors.Wrap(err, "alist")
}
} else if in.Implementation == "local" {
_, err := os.Stat(in.TvPath)
if err != nil {
return nil, err
}
_, err = os.Stat(in.MoviePath)
if err != nil {
return nil, err
}
}
log.Infof("received add storage input: %v", in)
err := s.db.AddStorage(&in)

View File

@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819"
revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668"
channel: "stable"
project_type: app
@@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
- platform: ios
create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: macos
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
# User provided section

118
ui/lib/init_wizard.dart Normal file
View File

@@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ui/settings/downloader.dart';
import 'package:ui/settings/prowlarr.dart';
import 'package:ui/settings/storage.dart';
class InitWizard extends ConsumerStatefulWidget {
const InitWizard({super.key});
static final String route = "/init_wizard";
@override
ConsumerState<ConsumerStatefulWidget> createState() {
return _InitWizardState();
}
}
class _InitWizardState extends ConsumerState<InitWizard> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SelectionArea(
child: Container(
padding: EdgeInsets.all(50),
child: ListView(
children: [
Container(
alignment: Alignment.center,
child: Text(
"Polaris 影视追踪下载",
style: TextStyle(
fontSize: 30,
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold),
),
),
Container(
padding: EdgeInsets.only(left: 10, top: 30, bottom: 30),
child: Text(
"设置向导",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary),
),
),
tmdbSetting(),
downloaderSetting(),
indexerSetting(),
storageSetting(),
],
),
)),
);
}
Widget tmdbSetting() {
return ExpansionTile(
title: Text(
"第一步TMDB设置",
style: TextStyle(fontWeight: FontWeight.bold),
),
childrenPadding: EdgeInsets.only(left: 100, right: 20),
initiallyExpanded: true,
children: [
Container(
alignment: Alignment.topLeft,
child: Text("TMDB API Key 设置用来获取各种影视的信息API Key获取方式参考官网"),
),
FormBuilder(
child: Column(
children: [
FormBuilderTextField(
name: "tmdb",
decoration: InputDecoration(labelText: "TMDB API Key"),
),
Center(
child: Padding(
padding: EdgeInsets.all(10),
child: ElevatedButton(onPressed: null, child: Text("保存")),
))
],
))
],
);
}
Widget indexerSetting() {
return ExpansionTile(
initiallyExpanded: true,
childrenPadding: EdgeInsets.only(left: 100, right: 20),
title: Text(
"第三步Prowlarr设置",
style: TextStyle(fontWeight: FontWeight.bold),
),
children: [ProwlarrSettingPage()],
);
}
Widget downloaderSetting() {
return ExpansionTile(
childrenPadding: EdgeInsets.only(left: 100, right: 20),
initiallyExpanded: true,
title: Text("第二步:下载客户端", style: TextStyle(fontWeight: FontWeight.bold)),
children: [
DownloaderSettings(),
],
);
}
Widget storageSetting() {
return ExpansionTile(
childrenPadding: EdgeInsets.only(left: 100, right: 20),
title: Text("第四步:存储设置", style: TextStyle(fontWeight: FontWeight.bold)),
initiallyExpanded: true,
children: [StorageSettings()],
);
}
}

View File

@@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:ui/activity.dart';
import 'package:ui/calendar.dart';
import 'package:ui/init_wizard.dart';
import 'package:ui/login_page.dart';
import 'package:ui/movie_watchlist.dart';
import 'package:ui/providers/APIs.dart';
@@ -124,9 +125,17 @@ class _MyAppState extends ConsumerState<MyApp> {
initialLocation: WelcomePage.routeTv,
routes: [
shellRoute,
GoRoute(
path: "/",
redirect: (context, state) => WelcomePage.routeTv,
),
GoRoute(
path: LoginScreen.route,
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: InitWizard.route,
builder: (context, state) => const InitWizard(),
)
],
);
@@ -171,88 +180,7 @@ class _MainSkeletonState extends State<MainSkeleton> {
var padding = isSmallScreen(context) ? 5.0 : 20.0;
return AdaptiveScaffold(
appBarBreakpoint: Breakpoints.standard,
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
leading: Container(
alignment: Alignment.centerLeft,
child: TextButton(
onPressed: () => context.go(WelcomePage.routeTv),
child: const Text(
"Polaris",
overflow: TextOverflow.clip,
style: TextStyle(fontSize: 28),
),
),
),
leadingWidth: isSmallScreen(context) ? 0 : 190,
title: Container(
alignment: Alignment.bottomLeft,
child: SearchAnchor(
builder: (BuildContext context, SearchController controller) {
return Container(
constraints: const BoxConstraints(maxWidth: 250, maxHeight: 40),
child: Opacity(
opacity: 0.8,
child: SearchBar(
hintText: "在此搜索...",
leading: const Icon(Icons.search),
controller: controller,
shadowColor: WidgetStateColor.transparent,
backgroundColor: WidgetStatePropertyAll(
Theme.of(context).colorScheme.primaryContainer),
onSubmitted: (value) => context.go(Uri(
path: SearchPage.route,
queryParameters: {'query': value}).toString()),
),
),
);
}, suggestionsBuilder:
(BuildContext context, SearchController controller) {
return [Text("dadada")];
}),
),
actions: [
// IconButton(
// onPressed: () => showCalendar(context),
// icon: Icon(Icons.calendar_month)),
IconButton(
onPressed: () => showDonate(context),
icon: Icon(
Icons.favorite_rounded,
color: Colors.red,
)),
MenuAnchor(
menuChildren: [
MenuItemButton(
leadingIcon: const Icon(Icons.exit_to_app),
child: const Text("登出"),
onPressed: () async {
await APIs.logout();
},
),
],
builder: (context, controller, child) {
return TextButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Icon(Icons.account_circle),
);
},
),
],
),
appBar: appBar(),
useDrawer: false,
selectedIndex: widget.body.currentIndex,
onSelectedIndexChange: (p0) => widget.body
@@ -314,4 +242,89 @@ class _MainSkeletonState extends State<MainSkeleton> {
},
);
}
AppBar appBar() {
return AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
leading: Container(
alignment: Alignment.centerLeft,
child: TextButton(
onPressed: () => context.go(WelcomePage.routeTv),
child: const Text(
"Polaris",
overflow: TextOverflow.clip,
style: TextStyle(fontSize: 28),
),
),
),
leadingWidth: isSmallScreen(context) ? 0 : 190,
title: Container(
alignment: Alignment.bottomLeft,
child: SearchAnchor(
builder: (BuildContext context, SearchController controller) {
return Container(
constraints: const BoxConstraints(maxWidth: 250, maxHeight: 40),
child: Opacity(
opacity: 0.8,
child: SearchBar(
hintText: "在此搜索...",
leading: const Icon(Icons.search),
controller: controller,
shadowColor: WidgetStateColor.transparent,
backgroundColor: WidgetStatePropertyAll(
Theme.of(context).colorScheme.primaryContainer),
onSubmitted: (value) => context.go(Uri(
path: SearchPage.route,
queryParameters: {'query': value}).toString()),
),
),
);
}, suggestionsBuilder:
(BuildContext context, SearchController controller) {
return [Text("dadada")];
}),
),
actions: [
// IconButton(
// onPressed: () => showCalendar(context),
// icon: Icon(Icons.calendar_month)),
IconButton(
onPressed: () => showDonate(context),
icon: Icon(
Icons.favorite_rounded,
color: Colors.red,
)),
MenuAnchor(
menuChildren: [
MenuItemButton(
leadingIcon: const Icon(Icons.exit_to_app),
child: const Text("登出"),
onPressed: () async {
await APIs.logout();
},
),
],
builder: (context, controller, child) {
return TextButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Icon(Icons.account_circle),
);
},
),
],
);
}
}

View File

@@ -58,6 +58,8 @@ class APIs {
static final tvParseUrl = "$_baseUrl/api/v1/setting/parse/tv";
static final movieParseUrl = "$_baseUrl/api/v1/setting/parse/movie";
static final mediaSizeLimiterUrl = "$_baseUrl/api/v1/setting/limiter";
static const tmdbApiKey = "tmdb_api_key";
static const downloadDirKey = "download_dir";
@@ -131,7 +133,7 @@ class APIs {
if (sp.code != 0) {
throw sp.message;
}
return sp.data==null? []:sp.data as List<String>;
return sp.data == null ? [] : sp.data as List<String>;
}
static Future<List<String>> downloadAllMovies() async {
@@ -142,7 +144,7 @@ class APIs {
if (sp.code != 0) {
throw sp.message;
}
return sp.data==null? []:sp.data as List<String>;
return sp.data == null ? [] : sp.data as List<String>;
}
static Future<String> parseTvName(String s) async {

View File

@@ -25,15 +25,15 @@ var mediaHistoryDataProvider = FutureProvider.autoDispose.family(
class ActivityData
extends AutoDisposeFamilyAsyncNotifier<List<Activity>, String> {
Timer? _timer;
@override
FutureOr<List<Activity>> build(String arg) async {
if (arg == "active") {
//refresh active downloads
Timer(const Duration(seconds: 5),
ref.invalidateSelf); //Periodically Refresh
if (_timer != null) {
_timer!.cancel();
}
final dio = await APIs.getDio();
final dio = APIs.getDio();
var resp =
await dio.get(APIs.activityUrl, queryParameters: {"status": arg});
final sp = ServerResponse.fromJson(resp.data);
@@ -44,6 +44,12 @@ class ActivityData
for (final a in sp.data as List) {
activities.add(Activity.fromJson(a));
}
if (arg == "active") {
//refresh active downloads
_timer = Timer(const Duration(seconds: 5),
() => ref.invalidateSelf()); //Periodically Refresh
}
return activities;
}
@@ -73,7 +79,8 @@ class Activity {
required this.saved,
required this.progress,
required this.size,
required this.seedRatio});
required this.seedRatio,
required this.uploadProgress});
final int? id;
final int? mediaId;
@@ -86,6 +93,7 @@ class Activity {
final int? progress;
final int? size;
final double seedRatio;
final double uploadProgress;
factory Activity.fromJson(Map<String, dynamic> json) {
return Activity(
@@ -99,6 +107,7 @@ class Activity {
saved: json["saved"],
progress: json["progress"],
seedRatio: json["seed_ratio"],
size: json["size"]);
size: json["size"],
uploadProgress: json["upload_progress"]);
}
}

View File

@@ -0,0 +1,109 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ui/providers/APIs.dart';
import 'package:ui/providers/server_response.dart';
var mediaSizeLimiterDataProvider =
AsyncNotifierProvider.autoDispose<MediaSizeLimiterData, MediaSizeLimiter>(
MediaSizeLimiterData.new);
class MediaSizeLimiterData extends AutoDisposeAsyncNotifier<MediaSizeLimiter> {
@override
FutureOr<MediaSizeLimiter> build() async {
final dio = APIs.getDio();
var resp = await dio.get(APIs.mediaSizeLimiterUrl);
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
return MediaSizeLimiter.fromJson(sp.data);
}
Future<void> submit(MediaSizeLimiter limiter) async {
final dio = APIs.getDio();
var resp = await dio.post(APIs.mediaSizeLimiterUrl, data: limiter.toJson());
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
ref.invalidateSelf();
}
}
class MediaSizeLimiter {
SizeLimiter? tvLimiter;
SizeLimiter? movieLimiter;
MediaSizeLimiter({this.tvLimiter, this.movieLimiter});
MediaSizeLimiter.fromJson(Map<String, dynamic> json) {
tvLimiter = json['tv_limiter'] != null
? SizeLimiter.fromJson(json['tv_limiter'])
: null;
movieLimiter = json['movie_limiter'] != null
? SizeLimiter.fromJson(json['movie_limiter'])
: null;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (tvLimiter != null) {
data['tv_limiter'] = tvLimiter!.toJson();
}
if (movieLimiter != null) {
data['movie_limiter'] = movieLimiter!.toJson();
}
return data;
}
}
class SizeLimiter {
ResLimiter? p720p;
ResLimiter? p1080p;
ResLimiter? p2160p;
SizeLimiter({this.p720p, this.p1080p, this.p2160p});
SizeLimiter.fromJson(Map<String, dynamic> json) {
p720p = json['720p'] != null ? ResLimiter.fromJson(json['720p']) : null;
p1080p = json['1080p'] != null ? ResLimiter.fromJson(json['1080p']) : null;
p2160p = json['2160p'] != null ? ResLimiter.fromJson(json['2160p']) : null;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (p720p != null) {
data['720p'] = p720p!.toJson();
}
if (p1080p != null) {
data['1080p'] = p1080p!.toJson();
}
if (p2160p != null) {
data['2160p'] = p2160p!.toJson();
}
return data;
}
}
class ResLimiter {
int? maxSize;
int? minSize;
int? preferSize;
ResLimiter({this.maxSize, this.minSize, this.preferSize});
ResLimiter.fromJson(Map<String, dynamic> json) {
maxSize = json['max_size'];
minSize = json['min_size'];
preferSize = json['prefer_size'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['max_size'] = maxSize;
data['min_size'] = minSize;
data['prefer_size'] = preferSize;
return data;
}
}

View File

@@ -115,7 +115,7 @@ class SearchPageData
if (sp.code != 0) {
throw sp.message;
}
//ref.invalidate(tvWatchlistDataProvider);
ref.invalidate(tvWatchlistDataProvider);
} else {
var resp = await dio.post(APIs.watchlistMovieUrl, data: {
"tmdb_id": tmdbId,
@@ -129,6 +129,7 @@ class SearchPageData
if (sp.code != 0) {
throw sp.message;
}
ref.invalidate(movieWatchlistDataProvider);
}
}
}

View File

@@ -111,7 +111,7 @@ class _SearchPageState extends ConsumerState<SearchPage> {
}
return cards;
},
error: (err, trace) => [PoError(msg: "网络错误请确认TMDB Key正确配置并且服务端能够正常连接TMDB网站", err: err)],
error: (err, trace) => [PoError(msg: "网络错误请确认TMDB Key正确配置并且能够正常连接TMDB网站", err: err)],
loading: () => [const MyProgressIndicator()]);
var f = NotificationListener(

View File

@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:quiver/strings.dart';
import 'package:ui/providers/settings.dart';
import 'package:ui/providers/size_limiter.dart';
import 'package:ui/settings/dialog.dart';
import 'package:ui/widgets/progress_indicator.dart';
import 'package:ui/widgets/widgets.dart';
@@ -22,27 +23,33 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
@override
Widget build(BuildContext context) {
var downloadClients = ref.watch(dwonloadClientsProvider);
return downloadClients.when(
data: (value) => Wrap(
children: List.generate(value.length + 1, (i) {
if (i < value.length) {
var client = value[i];
return SettingsCard(
onTap: () => showDownloadClientDetails(client),
child: Text(client.name ?? ""));
}
return SettingsCard(
onTap: () => showDownloadClientDetails(DownloadClient()),
child: const Icon(Icons.add));
})),
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator());
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
downloadClients.when(
data: (value) => Wrap(
children: List.generate(value.length + 1, (i) {
if (i < value.length) {
var client = value[i];
return SettingsCard(
onTap: () => showDownloadClientDetails(client),
child: Text(client.name ?? ""));
}
return SettingsCard(
onTap: () => showSelections(),
child: const Icon(Icons.add));
})),
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator()),
Divider(),
getSizeLimiterWidget()
],
);
}
Future<void> showDownloadClientDetails(DownloadClient client) {
final _formKey = GlobalKey<FormBuilderState>();
var _enableAuth = isNotBlank(client.user);
String selectImpl = "transmission";
final body =
StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
@@ -53,29 +60,12 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
"url": client.url,
"user": client.user,
"password": client.password,
"impl": client.implementation,
"remove_completed_downloads": client.removeCompletedDownloads,
"remove_failed_downloads": client.removeFailedDownloads,
"priority": client.priority.toString(),
},
child: Column(
children: [
FormBuilderDropdown<String>(
name: "impl",
decoration: const InputDecoration(labelText: "类型"),
onChanged: (value) {
setState(() {
selectImpl = value!;
});
},
items: const [
DropdownMenuItem(
value: "transmission", child: Text("Transmission")),
DropdownMenuItem(
value: "qbittorrent", child: Text("qBittorrent")),
],
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "name",
decoration: const InputDecoration(labelText: "名称"),
@@ -90,7 +80,8 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
),
FormBuilderTextField(
name: "priority",
decoration: const InputDecoration(labelText: "优先级", helperText: "1-50, 1最高优先级50最低优先级"),
decoration: const InputDecoration(
labelText: "优先级", helperText: "1-50, 1最高优先级50最低优先级"),
validator: FormBuilderValidators.integer(),
autovalidateMode: AutovalidateMode.onUserInteraction),
FormBuilderSwitch(
@@ -151,7 +142,7 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
return ref.read(dwonloadClientsProvider.notifier).addDownloadClients(
DownloadClient(
name: values["name"],
implementation: values["impl"],
implementation: client.implementation,
url: values["url"],
user: _enableAuth ? values["user"] : null,
password: _enableAuth ? values["password"] : null,
@@ -163,7 +154,214 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
}
}
var title = "下载器";
if (client.implementation == "transmission") {
title = "Transmission";
} else if (client.implementation == "qbittorrent") {
title = "qBittorrent";
}
return showSettingDialog(
context, "下载器", client.id != null, body, onSubmit, onDelete);
context, title, client.id != null, body, onSubmit, onDelete);
}
Future<void> showSelections() {
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
content: SizedBox(
height: 500,
width: 500,
child: Wrap(
children: [
SettingsCard(
child: InkWell(
child: const Center(
child: Text("Transmission"),
),
onTap: () {
Navigator.of(context).pop();
showDownloadClientDetails(DownloadClient(
implementation: "transmission",
name: "Transmission"));
},
),
),
SettingsCard(
child: InkWell(
child: const Center(
child: Text("qBittorrent"),
),
onTap: () {
Navigator.of(context).pop();
showDownloadClientDetails(DownloadClient(
implementation: "qbittorrent",
name: "qBittorrent"));
},
),
)
],
),
),
);
});
}
Widget getSizeLimiterWidget() {
var data = ref.watch(mediaSizeLimiterDataProvider);
final _formKey = GlobalKey<FormBuilderState>();
return Container(
padding: EdgeInsets.only(left: 20, right: 20, top: 20),
child: data.when(
data: (value) {
return FormBuilder(
key: _formKey,
initialValue: {
"tv_720p_min": toMbString(value.tvLimiter!.p720p!.minSize!),
"tv_720p_max": toMbString(value.tvLimiter!.p720p!.maxSize!),
"tv_1080p_min": toMbString(value.tvLimiter!.p1080p!.minSize!),
"tv_1080p_max": toMbString(value.tvLimiter!.p1080p!.maxSize!),
"tv_2160p_min": toMbString(value.tvLimiter!.p2160p!.minSize!),
"tv_2160p_max": toMbString(value.tvLimiter!.p2160p!.maxSize!),
"movie_720p_min":
toMbString(value.movieLimiter!.p720p!.minSize!),
"movie_720p_max":
toMbString(value.movieLimiter!.p720p!.maxSize!),
"movie_1080p_min":
toMbString(value.movieLimiter!.p1080p!.minSize!),
"movie_1080p_max":
toMbString(value.movieLimiter!.p1080p!.maxSize!),
"movie_2160p_min":
toMbString(value.movieLimiter!.p2160p!.minSize!),
"movie_2160p_max":
toMbString(value.movieLimiter!.p2160p!.maxSize!),
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"剧集大小限制",
style: TextStyle(fontSize: 18),
),
Divider(),
minMaxRow(" 720p", "tv_720p_min", "tv_720p_max"),
minMaxRow("1080p", "tv_1080p_min", "tv_1080p_max"),
minMaxRow("2160p", "tv_2160p_min", "tv_2160p_max"),
Text(
"电影大小限制",
style: TextStyle(fontSize: 18),
),
Divider(),
minMaxRow(" 720p", "movie_720p_min", "movie_720p_max"),
minMaxRow("1080p", "movie_1080p_min", "movie_1080p_max"),
minMaxRow("2160p", "movie_2160p_min", "movie_2160p_max"),
Center(
child: Padding(
padding: EdgeInsets.all(20),
child: LoadingElevatedButton(
onPressed: () async {
if (_formKey.currentState!.saveAndValidate()) {
var values = _formKey.currentState!.value;
return ref
.read(mediaSizeLimiterDataProvider.notifier)
.submit(MediaSizeLimiter(
tvLimiter: SizeLimiter(
p720p: ResLimiter(
minSize:
toByteInt(values["tv_720p_min"]),
maxSize:
toByteInt(values["tv_720p_max"])),
p1080p: ResLimiter(
minSize:
toByteInt(values["tv_1080p_min"]),
maxSize: toByteInt(
values["tv_1080p_max"])),
p2160p: ResLimiter(
minSize:
toByteInt(values["tv_2160p_min"]),
maxSize: toByteInt(
values["tv_2160p_max"])),
),
movieLimiter: SizeLimiter(
p720p: ResLimiter(
minSize: toByteInt(
values["movie_720p_min"]),
maxSize: toByteInt(
values["movie_720p_max"])),
p1080p: ResLimiter(
minSize: toByteInt(
values["movie_1080p_min"]),
maxSize: toByteInt(
values["movie_1080p_max"])),
p2160p: ResLimiter(
minSize: toByteInt(
values["movie_2160p_min"]),
maxSize: toByteInt(
values["movie_2160p_max"])),
)));
} else {
throw "validation_error";
}
},
label: Text("保存"),
),
),
)
],
),
);
},
error: (err, trace) => Container(),
loading: () => const MyProgressIndicator()),
);
}
Widget minMaxRow(String title, String nameMin, String nameMax) {
return Row(
children: [
Flexible(flex: 2, child: Container()),
Flexible(
flex: 2,
child: Text(
title,
style: TextStyle(fontSize: 16),
)),
Flexible(flex: 1, child: Container()),
Flexible(
flex: 6,
child: FormBuilderTextField(
name: nameMin,
decoration: InputDecoration(suffixText: "MB", labelText: "最小"),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.numeric()
]),
)),
Flexible(flex: 1, child: Text(" - ")),
Flexible(
flex: 6,
child: FormBuilderTextField(
name: nameMax,
decoration: InputDecoration(suffixText: "MB", labelText: "最大"),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.numeric()
]),
)),
Flexible(flex: 2, child: Container()),
],
);
}
}
String toMbString(int size) {
return (size / 1000 / 1000).toString();
}
int toByteInt(String s) {
return int.parse(s) * 1000 * 1000;
}

View File

@@ -6,6 +6,7 @@ import 'package:ui/providers/settings.dart';
import 'package:ui/settings/dialog.dart';
import 'package:ui/widgets/progress_indicator.dart';
import 'package:ui/widgets/widgets.dart';
import 'package:url_launcher/url_launcher.dart';
class Importlist extends ConsumerStatefulWidget {
const Importlist({super.key});
@@ -31,7 +32,7 @@ class _ImportlistState extends ConsumerState<Importlist> {
child: Text(indexer.name ?? ""));
}
return SettingsCard(
onTap: () => showImportlistDetails(ImportList()),
onTap: () => showSelections(),
child: const Icon(Icons.add));
}),
),
@@ -55,24 +56,16 @@ class _ImportlistState extends ConsumerState<Importlist> {
},
child: Column(
children: [
FormBuilderDropdown(
name: "type",
decoration: const InputDecoration(
labelText: "类型",
hintText:
"Plex Watchlist: https://support.plex.tv/articles/universal-watchlist/"),
items: const [
DropdownMenuItem(value: "plex", child: Text("Plex Watchlist")),
],
onChanged: (value) {
setState(() {
_selectedType = value;
});
},
),
_selectedType == "plex"
? const Text(
"Plex Watchlist: https://support.plex.tv/articles/universal-watchlist/")
list.type == "plex"
? Container(
alignment: Alignment.centerLeft,
child: InkWell(
onTap: () => launchUrl(Uri.parse(
"https://support.plex.tv/articles/universal-watchlist/")),
child: const Text(
"https://support.plex.tv/articles/universal-watchlist/"),
),
)
: const Text(""),
FormBuilderTextField(
name: "name",
@@ -141,7 +134,42 @@ class _ImportlistState extends ConsumerState<Importlist> {
}
}
var title = "监控列表";
if (list.type == "plex") {
title = "Plex Watchlist";
}
return showSettingDialog(
context, "监控列表", list.id != null, body, onSubmit, onDelete);
context, title, list.id != null, body, onSubmit, onDelete);
}
Future<void> showSelections() {
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
content: SizedBox(
height: 500,
width: 500,
child: Wrap(
children: [
SettingsCard(
child: InkWell(
child: const Center(
child: Text("Plex Watchlist"),
),
onTap: () {
Navigator.of(context).pop();
showImportlistDetails(
ImportList(type: "plex", name: "PlexWatchlist1"));
},
),
),
],
),
),
);
});
}
}

View File

@@ -51,7 +51,8 @@ class ProwlarrSettingState extends ConsumerState<ProwlarrSettingPage> {
FormBuilderSwitch(
name: "disabled",
title: const Text("禁用 Prowlarr"),
decoration: InputDecoration(icon: Icon(Icons.do_not_disturb)),
decoration:
InputDecoration(icon: Icon(Icons.do_not_disturb)),
),
Center(
child: Padding(

View File

@@ -31,7 +31,7 @@ class _StorageState extends ConsumerState<StorageSettings> {
child: Text(storage.name ?? ""));
}
return SettingsCard(
onTap: () => showStorageDetails(Storage()),
onTap: () => showSelections(),
child: const Icon(Icons.add));
}),
),
@@ -42,7 +42,6 @@ class _StorageState extends ConsumerState<StorageSettings> {
Future<void> showStorageDetails(Storage s) {
final _formKey = GlobalKey<FormBuilderState>();
String selectImpl = s.implementation == null ? "local" : s.implementation!;
final widgets =
StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return FormBuilder(
@@ -50,7 +49,6 @@ class _StorageState extends ConsumerState<StorageSettings> {
autovalidateMode: AutovalidateMode.disabled,
initialValue: {
"name": s.name,
"impl": s.implementation == null ? "local" : s.implementation!,
"user": s.settings != null ? s.settings!["user"] ?? "" : "",
"password": s.settings != null ? s.settings!["password"] ?? "" : "",
"tv_path": s.tvPath,
@@ -65,27 +63,6 @@ class _StorageState extends ConsumerState<StorageSettings> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
FormBuilderDropdown<String>(
name: "impl",
autovalidateMode: AutovalidateMode.onUserInteraction,
decoration: const InputDecoration(labelText: "类型"),
onChanged: (value) {
setState(() {
selectImpl = value!;
});
},
items: const [
DropdownMenuItem(
value: "local",
child: Text("本地存储"),
),
DropdownMenuItem(
value: "webdav",
child: Text("webdav"),
)
],
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "name",
autovalidateMode: AutovalidateMode.onUserInteraction,
@@ -93,15 +70,16 @@ class _StorageState extends ConsumerState<StorageSettings> {
decoration: const InputDecoration(labelText: "名称"),
validator: FormBuilderValidators.required(),
),
selectImpl != "local"
s.implementation == "webdav" || s.implementation == "alist"
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FormBuilderTextField(
name: "url",
autovalidateMode: AutovalidateMode.onUserInteraction,
decoration:
const InputDecoration(labelText: "Webdav地"),
decoration: const InputDecoration(
labelText: "",
hintText: "https://abc.somewebsite.com/"),
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
@@ -115,13 +93,15 @@ class _StorageState extends ConsumerState<StorageSettings> {
decoration: const InputDecoration(labelText: "密码"),
obscureText: true,
),
FormBuilderCheckbox(
name: "change_file_hash",
title: const Text(
"上传时更改文件哈希",
style: TextStyle(fontSize: 14),
),
),
s.implementation == "webdav"
? FormBuilderCheckbox(
name: "change_file_hash",
title: const Text(
"上传时更改文件哈希",
style: TextStyle(fontSize: 14),
),
)
: Container(),
],
)
: Container(),
@@ -145,7 +125,7 @@ class _StorageState extends ConsumerState<StorageSettings> {
final values = _formKey.currentState!.value;
return ref.read(storageSettingProvider.notifier).addStorage(Storage(
name: values["name"],
implementation: selectImpl,
implementation: s.implementation,
tvPath: values["tv_path"],
moviePath: values["movie_path"],
settings: {
@@ -167,7 +147,70 @@ class _StorageState extends ConsumerState<StorageSettings> {
return ref.read(storageSettingProvider.notifier).deleteStorage(s.id!);
}
var title = "存储";
if (s.implementation == "local") {
title = "本地存储";
} else if (s.implementation == "webdav") {
title = "webdav 存储";
} else if (s.implementation == "alist") {
title = "Alist 存储";
}
return showSettingDialog(
context, '存储', s.id != null, widgets, onSubmit, onDelete);
context, title, s.id != null, widgets, onSubmit, onDelete);
}
Future<void> showSelections() {
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
content: SizedBox(
height: 500,
width: 500,
child: Wrap(
children: [
SettingsCard(
child: InkWell(
child: const Center(
child: Text("本地存储"),
),
onTap: () {
Navigator.of(context).pop();
showStorageDetails(
Storage(implementation: "local", name: "本地存储1"));
},
),
),
SettingsCard(
child: InkWell(
child: const Center(
child: Text("webdav"),
),
onTap: () {
Navigator.of(context).pop();
showStorageDetails(
Storage(implementation: "webdav", name: "webdav1"));
},
),
),
SettingsCard(
child: InkWell(
child: const Center(
child: Text("Alist"),
),
onTap: () {
Navigator.of(context).pop();
showStorageDetails(
Storage(implementation: "alist", name: "Alist1"));
},
),
)
],
),
),
);
});
}
}

View File

@@ -282,6 +282,52 @@ class _LoadingTextButtonState extends State<LoadingTextButton> {
}
}
class LoadingElevatedButton extends StatefulWidget {
const LoadingElevatedButton(
{super.key, required this.onPressed, required this.label});
final Future<void> Function() onPressed;
final Widget label;
@override
State<StatefulWidget> createState() {
return _LoadingElevatedButtonState();
}
}
class _LoadingElevatedButtonState extends State<LoadingElevatedButton> {
bool loading = false;
@override
Widget build(BuildContext context) {
return ElevatedButton.icon(
onPressed: loading
? null
: () async {
setState(() => loading = true);
try {
await widget.onPressed();
} catch (e) {
showSnakeBar("操作失败:$e");
} finally {
setState(() => loading = false);
}
},
icon: loading
? Container(
width: 24,
height: 24,
padding: const EdgeInsets.all(2.0),
child: const CircularProgressIndicator(
color: Colors.grey,
strokeWidth: 3,
),
)
: Text(""),
label: widget.label,
);
}
}
class PoError extends StatelessWidget {
const PoError({super.key, required this.msg, required this.err});
final String msg;
@@ -292,10 +338,16 @@ class PoError extends StatelessWidget {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("$msg ", style: TextStyle(color:Theme.of(context).colorScheme.error),),
Text(
"$msg ",
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
Tooltip(
message: "$err",
child: Icon(Icons.info,color: Theme.of(context).colorScheme.error,),
child: Icon(
Icons.info,
color: Theme.of(context).colorScheme.error,
),
)
],
);
@@ -304,9 +356,30 @@ class PoError extends StatelessWidget {
class PoNetworkError extends StatelessWidget {
const PoNetworkError({super.key, required this.err});
final dynamic err;
final dynamic err;
@override
Widget build(BuildContext context) {
return PoError(msg: "网络错误,请检查网络链接", err: err);
}
}
class PoProgressIndicator extends StatelessWidget {
const PoProgressIndicator({super.key, this.backgroundColor, this.value, this.icon});
final double? value;
final Color? backgroundColor;
final IconData? icon;
@override
Widget build(BuildContext context) {
return Stack(
alignment: AlignmentDirectional.center,
children: [
CircularProgressIndicator(
backgroundColor: backgroundColor,
value: value,
),
icon != null ? Opacity(opacity: 0.7, child: Icon(icon, color: Theme.of(context).colorScheme.primary,),):Container()
],
);
}
}

7
ui/macos/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -0,0 +1,12 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

43
ui/macos/Podfile Normal file
View File

@@ -0,0 +1,43 @@
platform :osx, '10.14'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
end
end

22
ui/macos/Podfile.lock Normal file
View File

@@ -0,0 +1,22 @@
PODS:
- FlutterMacOS (1.0.0)
- url_launcher_macos (0.0.1):
- FlutterMacOS
DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
EXTERNAL SOURCES:
FlutterMacOS:
:path: Flutter/ephemeral
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS:
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367
COCOAPODS: 1.15.2

View File

@@ -0,0 +1,801 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXAggregateTarget section */
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
isa = PBXAggregateTarget;
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
buildPhases = (
33CC111E2044C6BF0003C045 /* ShellScript */,
);
dependencies = (
);
name = "Flutter Assemble";
productName = FLX;
};
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
1923000523C26F70A19C5B17 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAAFECE3C7921C6460954E3D /* Pods_RunnerTests.framework */; };
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
465BCE0EA17751098AAEB5E0 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 338C847C60E19B48128A7DBB /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC10EC2044A3C60003C045;
remoteInfo = Runner;
};
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
remoteInfo = FLX;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
33CC110E2044A8840003C045 /* Bundle Framework */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Bundle Framework";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
338C847C60E19B48128A7DBB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10ED2044A3C60003C045 /* ui.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ui.app; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
479C447F60C752B08C7F2A48 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
49BC4AE8CB23ABB9DF839C61 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
864A824B0E9687E6B7C26662 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
AE69DDBE6A2D973F2DDE4773 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
EAAFECE3C7921C6460954E3D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
EC6DB94E6F2EE2EBF7D7A8D3 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
F04941AF3F4D22EC07E9EC57 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
331C80D2294CF70F00263BE5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
1923000523C26F70A19C5B17 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EA2044A3C60003C045 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
465BCE0EA17751098AAEB5E0 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C80D6294CF71000263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C80D7294CF71000263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
33BA886A226E78AF003329D5 /* Configs */ = {
isa = PBXGroup;
children = (
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
);
path = Configs;
sourceTree = "<group>";
};
33CC10E42044A3C60003C045 = {
isa = PBXGroup;
children = (
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
8E8421BAD07F4A7F642628C0 /* Pods */,
);
sourceTree = "<group>";
};
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* ui.app */,
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
);
name = Resources;
path = ..;
sourceTree = "<group>";
};
33CEB47122A05771004F2AC0 /* Flutter */ = {
isa = PBXGroup;
children = (
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
);
path = Flutter;
sourceTree = "<group>";
};
33FAB671232836740065AC1E /* Runner */ = {
isa = PBXGroup;
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */,
33CC11242044D66E0003C045 /* Resources */,
33BA886A226E78AF003329D5 /* Configs */,
);
path = Runner;
sourceTree = "<group>";
};
8E8421BAD07F4A7F642628C0 /* Pods */ = {
isa = PBXGroup;
children = (
EC6DB94E6F2EE2EBF7D7A8D3 /* Pods-Runner.debug.xcconfig */,
F04941AF3F4D22EC07E9EC57 /* Pods-Runner.release.xcconfig */,
864A824B0E9687E6B7C26662 /* Pods-Runner.profile.xcconfig */,
479C447F60C752B08C7F2A48 /* Pods-RunnerTests.debug.xcconfig */,
49BC4AE8CB23ABB9DF839C61 /* Pods-RunnerTests.release.xcconfig */,
AE69DDBE6A2D973F2DDE4773 /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
338C847C60E19B48128A7DBB /* Pods_Runner.framework */,
EAAFECE3C7921C6460954E3D /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C80D4294CF70F00263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
46368FA552F764B76BC73E54 /* [CP] Check Pods Manifest.lock */,
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C80DA294CF71000263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
33CC10EC2044A3C60003C045 /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
EC8714E8A2401D3E05CE5A6C /* [CP] Check Pods Manifest.lock */,
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
C724001738DB1B5869C33FBB /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
33CC11202044C79F0003C045 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* ui.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
33CC10E52044A3C60003C045 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C80D4294CF70F00263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 33CC10EC2044A3C60003C045;
};
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
};
};
};
33CC111A2044C6BA0003C045 = {
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Manual;
};
};
};
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 33CC10E42044A3C60003C045;
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
33CC10EC2044A3C60003C045 /* Runner */,
331C80D4294CF70F00263BE5 /* RunnerTests */,
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C80D3294CF70F00263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EB2044A3C60003C045 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
};
33CC111E2044C6BF0003C045 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
Flutter/ephemeral/FlutterInputs.xcfilelist,
);
inputPaths = (
Flutter/ephemeral/tripwire,
);
outputFileListPaths = (
Flutter/ephemeral/FlutterOutputs.xcfilelist,
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
46368FA552F764B76BC73E54 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
C724001738DB1B5869C33FBB /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
EC8714E8A2401D3E05CE5A6C /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C80D1294CF70F00263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10E92044A3C60003C045 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC10EC2044A3C60003C045 /* Runner */;
targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
};
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
isa = PBXVariantGroup;
children = (
33CC10F52044A3C60003C045 /* Base */,
);
name = MainMenu.xib;
path = Runner;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
331C80DB294CF71000263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 479C447F60C752B08C7F2A48 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.ui.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ui";
};
name = Debug;
};
331C80DC294CF71000263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 49BC4AE8CB23ABB9DF839C61 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.ui.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ui";
};
name = Release;
};
331C80DD294CF71000263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AE69DDBE6A2D973F2DDE4773 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.ui.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ui";
};
name = Profile;
};
338D0CE9231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Profile;
};
338D0CEA231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Profile;
};
338D0CEB231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Profile;
};
33CC10F92044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
33CC10FA2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
33CC10FC2044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
33CC10FD2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
33CC111C2044C6BA0003C045 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
33CC111D2044C6BA0003C045 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C80DB294CF71000263BE5 /* Debug */,
331C80DC294CF71000263BE5 /* Release */,
331C80DD294CF71000263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10F92044A3C60003C045 /* Debug */,
33CC10FA2044A3C60003C045 /* Release */,
338D0CE9231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10FC2044A3C60003C045 /* Debug */,
33CC10FD2044A3C60003C045 /* Release */,
338D0CEA231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC111C2044C6BA0003C045 /* Debug */,
33CC111D2044C6BA0003C045 /* Release */,
338D0CEB231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "ui.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "ui.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C80D4294CF70F00263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "ui.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "ui.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,9 @@
import Cocoa
import FlutterMacOS
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}

View File

@@ -0,0 +1,68 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_16.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_64.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_1024.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,343 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
<connections>
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="5QF-Oa-p0T">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
<items>
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
<connections>
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
<connections>
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
<connections>
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
<connections>
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
<connections>
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
</connections>
</menuItem>
<menuItem title="Delete" id="pa3-QI-u2k">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
<connections>
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
<menuItem title="Find" id="4EN-yA-p0u">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="1b7-l0-nxx">
<items>
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
<connections>
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
</connections>
</menuItem>
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
<connections>
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
</connections>
</menuItem>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
<connections>
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
</connections>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
<connections>
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
</connections>
</menuItem>
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
<connections>
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
<items>
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
<connections>
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
</connections>
</menuItem>
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
<connections>
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
</connections>
</menuItem>
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="9ic-FL-obx">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
<items>
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
</connections>
</menuItem>
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
</connections>
</menuItem>
<menuItem title="Smart Links" id="cwL-P1-jid">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
</connections>
</menuItem>
<menuItem title="Data Detectors" id="tRr-pd-1PS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
</connections>
</menuItem>
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Transformations" id="2oI-Rn-ZJC">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
<items>
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
</connections>
</menuItem>
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
</connections>
</menuItem>
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="xrE-MZ-jX0">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
<items>
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="View" id="H8h-7b-M4v">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="View" id="HyV-fh-RgO">
<items>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="EPT-qC-fAb">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/>
</menuItem>
</items>
<point key="canvasLocation" x="142" y="-258"/>
</menu>
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</window>
</objects>
</document>

View File

@@ -0,0 +1,14 @@
// Application-level settings for the Runner target.
//
// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
// future. If not, the values below would default to using the project name when this becomes a
// 'flutter create' template.
// The application's name. By default this is also the title of the Flutter window.
PRODUCT_NAME = ui
// The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = com.example.ui
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved.

View File

@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Debug.xcconfig"
#include "Warnings.xcconfig"

View File

@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Release.xcconfig"
#include "Warnings.xcconfig"

View File

@@ -0,0 +1,13 @@
WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
GCC_WARN_UNDECLARED_SELECTOR = YES
CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
CLANG_WARN_PRAGMA_PACK = YES
CLANG_WARN_STRICT_PROTOTYPES = YES
CLANG_WARN_COMMA = YES
GCC_WARN_STRICT_SELECTOR_MATCH = YES
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
GCC_WARN_SHADOW = YES
CLANG_WARN_UNREACHABLE_CODE = YES

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

View File

@@ -0,0 +1,15 @@
import Cocoa
import FlutterMacOS
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
let flutterViewController = FlutterViewController()
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib()
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,12 @@
import Cocoa
import FlutterMacOS
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View File

@@ -6,7 +6,7 @@ packages:
description:
name: another_flushbar
sha256: "19bf9520230ec40b300aaf9dd2a8fefcb277b25ecd1c4838f530566965befc2a"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.12.30"
another_transformer_page_view:
@@ -14,7 +14,7 @@ packages:
description:
name: another_transformer_page_view
sha256: a7cd46ede62d621c5abe7e58c7cb2745abe67b3bfec64f59b8889c93d7be7a8e
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.1"
async:
@@ -22,7 +22,7 @@ packages:
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.11.0"
boolean_selector:
@@ -30,7 +30,7 @@ packages:
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
characters:
@@ -38,7 +38,7 @@ packages:
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
clock:
@@ -46,7 +46,7 @@ packages:
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
collection:
@@ -54,7 +54,7 @@ packages:
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.18.0"
cupertino_icons:
@@ -62,7 +62,7 @@ packages:
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.8"
dio:
@@ -70,7 +70,7 @@ packages:
description:
name: dio
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.7.0"
dio_web_adapter:
@@ -78,7 +78,7 @@ packages:
description:
name: dio_web_adapter
sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.0"
equatable:
@@ -86,7 +86,7 @@ packages:
description:
name: equatable
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.5"
fake_async:
@@ -94,7 +94,7 @@ packages:
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.1"
flutter:
@@ -107,7 +107,7 @@ packages:
description:
name: flutter_adaptive_scaffold
sha256: "8c515a2cb8abb3a567f8e77f10b33f47bb6fcadfe31f62364e0aca36280cdf93"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.1"
flutter_form_builder:
@@ -115,7 +115,7 @@ packages:
description:
name: flutter_form_builder
sha256: c278ef69b08957d484f83413f0e77b656a39b7a7bb4eb8a295da3a820ecc6545
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "9.5.0"
flutter_lints:
@@ -123,7 +123,7 @@ packages:
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.0"
flutter_localizations:
@@ -136,7 +136,7 @@ packages:
description:
name: flutter_login
sha256: "1f7c46d0d76081cf4c5180e3a265b1f5b1d7e48c81859f58f03a8dcd27338b85"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.0"
flutter_riverpod:
@@ -144,7 +144,7 @@ packages:
description:
name: flutter_riverpod
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.6.1"
flutter_test:
@@ -161,32 +161,32 @@ packages:
dependency: transitive
description:
name: font_awesome_flutter
sha256: "275ff26905134bcb59417cf60ad979136f1f8257f2f449914b2c3e05bbb4cd6f"
url: "https://pub.dev"
sha256: d3a89184101baec7f4600d58840a764d2ef760fe1c5a20ef9e6b0e9b24a07a3a
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.7.0"
version: "10.8.0"
form_builder_validators:
dependency: "direct main"
description:
name: form_builder_validators
sha256: c61ed7b1deecf0e1ebe49e2fa79e3283937c5a21c7e48e3ed9856a4a14e1191a
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "11.0.0"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: "6f1b756f6e863259a99135ff3c95026c3cdca17d10ebef2bba2261a25ddc8bbc"
url: "https://pub.dev"
sha256: "8ae664a70174163b9f65ea68dd8673e29db8f9095de7b5cd00e167c621f4fef5"
url: "https://pub.flutter-io.cn"
source: hosted
version: "14.3.0"
version: "14.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.2"
intl:
@@ -194,7 +194,7 @@ packages:
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.19.0"
intl_phone_number_input:
@@ -202,7 +202,7 @@ packages:
description:
name: intl_phone_number_input
sha256: "1c4328713a9503ab26a1fdbb6b00b4cada68c18aac922b35bedbc72eff1297c3"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.4"
js:
@@ -210,7 +210,7 @@ packages:
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.6.7"
leak_tracker:
@@ -218,7 +218,7 @@ packages:
description:
name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.0.5"
leak_tracker_flutter_testing:
@@ -226,7 +226,7 @@ packages:
description:
name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.5"
leak_tracker_testing:
@@ -234,7 +234,7 @@ packages:
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.1"
libphonenumber_platform_interface:
@@ -242,7 +242,7 @@ packages:
description:
name: libphonenumber_platform_interface
sha256: f801f6c65523f56504b83f0890e6dad584ab3a7507dca65fec0eed640afea40f
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.4.2"
libphonenumber_plugin:
@@ -250,7 +250,7 @@ packages:
description:
name: libphonenumber_plugin
sha256: c615021d9816fbda2b2587881019ed595ecdf54d999652d7e4cce0e1f026368c
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.3"
libphonenumber_web:
@@ -258,7 +258,7 @@ packages:
description:
name: libphonenumber_web
sha256: "8186f420dbe97c3132283e52819daff1e55d60d6db46f7ea5ac42f42a28cc2ef"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.2"
lints:
@@ -266,7 +266,7 @@ packages:
description:
name: lints
sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.0"
logging:
@@ -274,7 +274,7 @@ packages:
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
matcher:
@@ -282,7 +282,7 @@ packages:
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.16+1"
material_color_utilities:
@@ -290,7 +290,7 @@ packages:
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.11.1"
meta:
@@ -298,7 +298,7 @@ packages:
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.15.0"
nested:
@@ -306,7 +306,7 @@ packages:
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.0"
path:
@@ -314,7 +314,7 @@ packages:
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.0"
phone_numbers_parser:
@@ -322,7 +322,7 @@ packages:
description:
name: phone_numbers_parser
sha256: "62451b689d842791ed1fd5dc9eacf36ffa8bad23a78ad6cde732dc2fb222fae2"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.3.0"
plugin_platform_interface:
@@ -330,7 +330,7 @@ packages:
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.8"
provider:
@@ -338,7 +338,7 @@ packages:
description:
name: provider
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.2"
quiver:
@@ -346,7 +346,7 @@ packages:
description:
name: quiver
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.2"
riverpod:
@@ -354,7 +354,7 @@ packages:
description:
name: riverpod
sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.6.1"
sign_in_button:
@@ -362,7 +362,7 @@ packages:
description:
name: sign_in_button
sha256: "977b9b0415d2f3909e642275dfabba7919ba8e111324641b76cae6d1acbd183e"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.0"
simple_gesture_detector:
@@ -370,7 +370,7 @@ packages:
description:
name: simple_gesture_detector
sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.1"
sky_engine:
@@ -383,7 +383,7 @@ packages:
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.0"
stack_trace:
@@ -391,7 +391,7 @@ packages:
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.11.1"
state_notifier:
@@ -399,7 +399,7 @@ packages:
description:
name: state_notifier
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.0"
stream_channel:
@@ -407,7 +407,7 @@ packages:
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
string_scanner:
@@ -415,7 +415,7 @@ packages:
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.0"
table_calendar:
@@ -423,7 +423,7 @@ packages:
description:
name: table_calendar
sha256: "4ca32b2fc919452c9974abd4c6ea611a63e33b9e4f0b8c38dba3ac1f4a6549d1"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.2"
term_glyph:
@@ -431,7 +431,7 @@ packages:
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.1"
test_api:
@@ -439,7 +439,7 @@ packages:
description:
name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.2"
timeago:
@@ -447,7 +447,7 @@ packages:
description:
name: timeago
sha256: "054cedf68706bb142839ba0ae6b135f6b68039f0b8301cbe8784ae653d5ff8de"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.7.0"
typed_data:
@@ -455,7 +455,7 @@ packages:
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.0"
url_launcher:
@@ -463,7 +463,7 @@ packages:
description:
name: url_launcher
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.3.1"
url_launcher_android:
@@ -471,7 +471,7 @@ packages:
description:
name: url_launcher_android
sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.3.14"
url_launcher_ios:
@@ -479,23 +479,23 @@ packages:
description:
name: url_launcher_ios
sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.3.1"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af
url: "https://pub.dev"
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.0"
version: "3.2.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.1"
url_launcher_platform_interface:
@@ -503,7 +503,7 @@ packages:
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.2"
url_launcher_web:
@@ -511,7 +511,7 @@ packages:
description:
name: url_launcher_web
sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.3"
url_launcher_windows:
@@ -519,7 +519,7 @@ packages:
description:
name: url_launcher_windows
sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.3"
vector_math:
@@ -527,7 +527,7 @@ packages:
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.4"
vm_service:
@@ -535,7 +535,7 @@ packages:
description:
name: vm_service
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "14.2.5"
web:
@@ -543,7 +543,7 @@ packages:
description:
name: web
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
sdks: