Commit 2f8c35ba32
Unsigned
Layout: unified · split
INSTANCES.md +1 −1
| @@ -2,6 +2,6 @@ | |||
| 2 | |:------:|:-------:|:-:|:-:|:--:|:--------:|:--------------:|:-----:| | 2 | |:------:|:-------:|:-:|:-:|:--:|:--------:|:--------------:|:-----:| |
| 3 | |[skunky.ebloid.ru](https://skunky.ebloid.ru/art)|[Yes](http://[201:eba5:d1fc:bf7b:cfcb:a811:4b8b:7ea3]/art)|No|No| No | No | No | Russia | | 3 | |[skunky.ebloid.ru](https://skunky.ebloid.ru/art)|[Yes](http://[201:eba5:d1fc:bf7b:cfcb:a811:4b8b:7ea3]/art)|No|No| No | No | No | Russia | |
| 4 | |[clovius.club](https://skunky.clovius.club)|No|No|No| Yes | Yes | No | Sweden | | 4 | |[clovius.club](https://skunky.clovius.club)|No|No|No| Yes | Yes | No | Sweden | |
| 5 | |[bloat.cat](https://skunky.bloat.cat)|No|No|No| Yes | Yes | No | Romania | | 5 | |[bloat.cat](https://skunky.bloat.cat)|No|No|No| Yes | Yes | No | Germany | |
| 6 | |[frontendfriendly.xyz](https://skunkyart.frontendfriendly.xyz)|No|No|No| Yes | Yes | No | Finland | | 6 | |[frontendfriendly.xyz](https://skunkyart.frontendfriendly.xyz)|No|No|No| Yes | Yes | No | Finland | |
| 7 | |[lumaeris.com](https://skunkyart.lumaeris.com)|No|No|No| Yes | Yes | No | US |\ No newline at end of file | 7 | |[lumaeris.com](https://skunkyart.lumaeris.com)|No|No|No| Yes | Yes | No | US |\ No newline at end of file |
TODO.md +1 −1
| @@ -6,7 +6,7 @@ | |||
| 6 | * ~~Исправить баг с навигацией по страницам~~ ✔️ | 6 | * ~~Исправить баг с навигацией по страницам~~ ✔️ |
| 7 | * Сделать нормальное отображение ошибок | 7 | * Сделать нормальное отображение ошибок |
| 8 | * ~~Сделать единицы в конфиге более понятными~~ ✔️ | 8 | * ~~Сделать единицы в конфиге более понятными~~ ✔️ |
| 9 | * Добавить просмотр понравившихся артов пользователю | 9 | * ~~Добавить просмотр понравившихся артов пользователю~~ ✔️ |
| 10 | * Добавить возможность включить темплейты в бинарник [P] | 10 | * Добавить возможность включить темплейты в бинарник [P] |
| 11 | * ~~Реализовать миниатюры и оптимизировать CSS под маленькие экраны~~ ✔️ | 11 | * ~~Реализовать миниатюры и оптимизировать CSS под маленькие экраны~~ ✔️ |
| 12 | * Написать Makefile и скрипт для автоматического развёртывания инстанса | 12 | * Написать Makefile и скрипт для автоматического развёртывания инстанса |
app/cache.go +18 −16
| @@ -99,34 +99,36 @@ func (s skunkyart) DownloadAndSendMedia(subdomain, path string) { | |||
| 99 | 99 | ||
| 100 | func InitCacheSystem() { | 100 | func InitCacheSystem() { |
| 101 | c := &CFG.Cache | 101 | c := &CFG.Cache |
| 102 | os.Mkdir(c.Path, 0700) | ||
| 103 | for { | 102 | for { |
| 104 | dir, e := os.Open(c.Path) | 103 | dir, err := os.ReadDir(c.Path) |
| 105 | try(e) | 104 | if err != nil { |
| 106 | stat, e := dir.Stat() | 105 | if os.IsNotExist(err) { |
| 107 | try(e) | 106 | os.Mkdir(c.Path, 0700) |
| 108 | 107 | continue | |
| 109 | dirnames, e := dir.Readdirnames(-1) | 108 | } |
| 110 | try(e) | 109 | println(err.Error()) |
| 111 | for _, a := range dirnames { | 110 | } |
| 112 | a = c.Path + "/" + a | 111 | |
| 112 | for _, file := range dir { | ||
| 113 | fileName := c.Path + "/" + file.Name() | ||
| 114 | fileInfo, err := file.Info() | ||
| 115 | try(err) | ||
| 116 | |||
| 113 | if c.Lifetime != "" { | 117 | if c.Lifetime != "" { |
| 114 | now := time.Now().UnixMilli() | 118 | now := time.Now().UnixMilli() |
| 115 | 119 | ||
| 116 | f, _ := os.Stat(a) | 120 | stat := fileInfo.Sys().(*syscall.Stat_t) |
| 117 | stat := f.Sys().(*syscall.Stat_t) | ||
| 118 | time := statTime(stat) | 121 | time := statTime(stat) |
| 119 | 122 | ||
| 120 | if time+lifetimeParsed <= now { | 123 | if time+lifetimeParsed <= now { |
| 121 | try(os.RemoveAll(a)) | 124 | try(os.RemoveAll(fileName)) |
| 122 | } | 125 | } |
| 123 | } | 126 | } |
| 124 | if c.MaxSize != 0 && stat.Size() > c.MaxSize { | 127 | if c.MaxSize != 0 && fileInfo.Size() > c.MaxSize { |
| 125 | try(os.RemoveAll(a)) | 128 | try(os.RemoveAll(fileName)) |
| 126 | } | 129 | } |
| 127 | } | 130 | } |
| 128 | 131 | ||
| 129 | dir.Close() | ||
| 130 | time.Sleep(time.Second * time.Duration(c.UpdateInterval)) | 132 | time.Sleep(time.Second * time.Duration(c.UpdateInterval)) |
| 131 | } | 133 | } |
| 132 | } | 134 | } |
app/util.go +59
| @@ -16,6 +16,8 @@ import ( | |||
| 16 | ) | 16 | ) |
| 17 | 17 | ||
| 18 | /* INTERNAL */ | 18 | /* INTERNAL */ |
| 19 | var wr = io.WriteString | ||
| 20 | |||
| 19 | func exit(msg string, code int) { | 21 | func exit(msg string, code int) { |
| 20 | println(msg) | 22 | println(msg) |
| 21 | os.Exit(code) | 23 | os.Exit(code) |
| @@ -50,6 +52,63 @@ func RefreshInstances() { | |||
| 50 | } | 52 | } |
| 51 | 53 | ||
| 52 | // some crap for frontend | 54 | // some crap for frontend |
| 55 | type skunkyart struct { | ||
| 56 | Writer http.ResponseWriter | ||
| 57 | |||
| 58 | Args url.Values | ||
| 59 | Page int | ||
| 60 | Type rune | ||
| 61 | Atom bool | ||
| 62 | |||
| 63 | BasePath, Endpoint string | ||
| 64 | Query, QueryRaw string | ||
| 65 | |||
| 66 | Templates struct { | ||
| 67 | About struct { | ||
| 68 | Proxy bool | ||
| 69 | Nsfw bool | ||
| 70 | Instances []settings | ||
| 71 | } | ||
| 72 | |||
| 73 | SomeList string | ||
| 74 | DDStrips string | ||
| 75 | Deviation struct { | ||
| 76 | Post devianter.Post | ||
| 77 | Related string | ||
| 78 | StringTime string | ||
| 79 | Tags string | ||
| 80 | Comments string | ||
| 81 | } | ||
| 82 | |||
| 83 | GroupUser struct { | ||
| 84 | GR devianter.GRuser | ||
| 85 | Admins string | ||
| 86 | Group bool | ||
| 87 | CreationDate string | ||
| 88 | |||
| 89 | About struct { | ||
| 90 | A devianter.About | ||
| 91 | |||
| 92 | DescriptionFormatted string | ||
| 93 | Interests, Social string | ||
| 94 | Comments string | ||
| 95 | BG string | ||
| 96 | BGMeta devianter.Deviation | ||
| 97 | } | ||
| 98 | |||
| 99 | Gallery struct { | ||
| 100 | Folders string | ||
| 101 | Pages int | ||
| 102 | List string | ||
| 103 | } | ||
| 104 | } | ||
| 105 | Search struct { | ||
| 106 | Content devianter.Search | ||
| 107 | List string | ||
| 108 | } | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 53 | func (s skunkyart) ExecuteTemplate(file, dir string, data any) { | 112 | func (s skunkyart) ExecuteTemplate(file, dir string, data any) { |
| 54 | var buf strings.Builder | 113 | var buf strings.Builder |
| 55 | tmp := template.New(file) | 114 | tmp := template.New(file) |
app/wrapper.go +48 −96
| @@ -2,9 +2,6 @@ package app | |||
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "encoding/json" | 4 | "encoding/json" |
| 5 | "io" | ||
| 6 | "net/http" | ||
| 7 | "net/url" | ||
| 8 | "regexp" | 5 | "regexp" |
| 9 | "strconv" | 6 | "strconv" |
| 10 | "strings" | 7 | "strings" |
| @@ -14,65 +11,6 @@ import ( | |||
| 14 | "golang.org/x/net/html" | 11 | "golang.org/x/net/html" |
| 15 | ) | 12 | ) |
| 16 | 13 | ||
| 17 | var wr = io.WriteString | ||
| 18 | |||
| 19 | type skunkyart struct { | ||
| 20 | Writer http.ResponseWriter | ||
| 21 | |||
| 22 | Args url.Values | ||
| 23 | Page int | ||
| 24 | Type rune | ||
| 25 | Atom bool | ||
| 26 | |||
| 27 | BasePath, Endpoint string | ||
| 28 | Query, QueryRaw string | ||
| 29 | |||
| 30 | Templates struct { | ||
| 31 | About struct { | ||
| 32 | Proxy bool | ||
| 33 | Nsfw bool | ||
| 34 | Instances []settings | ||
| 35 | } | ||
| 36 | |||
| 37 | SomeList string | ||
| 38 | DDStrips string | ||
| 39 | Deviation struct { | ||
| 40 | Post devianter.Post | ||
| 41 | Related string | ||
| 42 | StringTime string | ||
| 43 | Tags string | ||
| 44 | Comments string | ||
| 45 | } | ||
| 46 | |||
| 47 | GroupUser struct { | ||
| 48 | GR devianter.GRuser | ||
| 49 | Admins string | ||
| 50 | Group bool | ||
| 51 | CreationDate string | ||
| 52 | |||
| 53 | About struct { | ||
| 54 | A devianter.About | ||
| 55 | |||
| 56 | DescriptionFormatted string | ||
| 57 | Interests, Social string | ||
| 58 | Comments string | ||
| 59 | BG string | ||
| 60 | BGMeta devianter.Deviation | ||
| 61 | } | ||
| 62 | |||
| 63 | Gallery struct { | ||
| 64 | Folders string | ||
| 65 | Pages int | ||
| 66 | List string | ||
| 67 | } | ||
| 68 | } | ||
| 69 | Search struct { | ||
| 70 | Content devianter.Search | ||
| 71 | List string | ||
| 72 | } | ||
| 73 | } | ||
| 74 | } | ||
| 75 | |||
| 76 | func (s skunkyart) GRUser() { | 14 | func (s skunkyart) GRUser() { |
| 77 | if len(s.Query) < 1 { | 15 | if len(s.Query) < 1 { |
| 78 | s.ReturnHTTPError(400) | 16 | s.ReturnHTTPError(400) |
| @@ -82,7 +20,7 @@ func (s skunkyart) GRUser() { | |||
| 82 | var g devianter.Group | 20 | var g devianter.Group |
| 83 | g.Name = s.Query | 21 | g.Name = s.Query |
| 84 | var err error | 22 | var err error |
| 85 | s.Templates.GroupUser.GR, err = g.GetGroup() | 23 | s.Templates.GroupUser.GR, err = g.Get() |
| 86 | try(err) | 24 | try(err) |
| 87 | 25 | ||
| 88 | group := &s.Templates.GroupUser | 26 | group := &s.Templates.GroupUser |
| @@ -146,51 +84,65 @@ func (s skunkyart) GRUser() { | |||
| 146 | } | 84 | } |
| 147 | 85 | ||
| 148 | } | 86 | } |
| 149 | case 'g': | 87 | case 'g', 'f': |
| 88 | var all bool | ||
| 89 | var content devianter.Group | ||
| 90 | |||
| 150 | folderid, _ := strconv.Atoi(s.Args.Get("folder")) | 91 | folderid, _ := strconv.Atoi(s.Args.Get("folder")) |
| 92 | |||
| 93 | if a := s.Args.Get("all"); a == "true" { | ||
| 94 | all = true | ||
| 95 | } | ||
| 96 | |||
| 151 | if s.Page == 0 { | 97 | if s.Page == 0 { |
| 152 | s.Page++ | 98 | s.Page++ |
| 153 | } | 99 | } |
| 154 | 100 | ||
| 155 | gallery, err := g.GetGallery(s.Page, folderid) | 101 | if s.Type == 'f' { |
| 156 | try(err) | 102 | content = g.Favourites(s.Page, all, folderid) |
| 103 | } else { | ||
| 104 | content, err = g.Gallery(s.Page, folderid) | ||
| 105 | try(err) | ||
| 106 | } | ||
| 157 | 107 | ||
| 158 | if folderid > 0 { | 108 | if folderid > 0 || (s.Type == 'f' && all) { |
| 159 | group.Gallery.List = s.DeviationList(gallery.Content.Results, true, DeviationList{ | 109 | group.Gallery.List = s.DeviationList(content.Content.Results, true, DeviationList{ |
| 160 | More: gallery.Content.HasMore, | 110 | More: content.Content.HasMore, |
| 161 | }) | 111 | }) |
| 162 | } else { | 112 | } else { |
| 163 | for _, x := range gallery.Content.Gruser.Page.Modules { | 113 | for _, x := range content.Content.Gruser.Page.Modules { |
| 164 | if l := len(x.ModuleData.Folders.Results); l != 0 { | 114 | if l := len(x.ModuleData.Folders.Results); l != 0 { |
| 165 | var folders strings.Builder | 115 | var folders strings.Builder |
| 166 | folders.WriteString(`<h1 id="folders"><a href="#folder">#</a> Folders</h1><div class="folders"><br>`) | 116 | folders.WriteString(`<h1 id="folders"><a href="#folder">#</a> Folders</h1><div class="folders"><br>`) |
| 167 | for _, x := range x.ModuleData.Folders.Results { | 117 | for _, x := range x.ModuleData.Folders.Results { |
| 168 | folders.WriteString(`<div class="block folder-item">`) | 118 | if x.FolderId != -1 && x.Size != 0 { |
| 169 | 119 | folders.WriteString(`<div class="block folder-item">`) | |
| 170 | if !(x.Thumb.NSFW && !CFG.Nsfw) { | 120 | |
| 171 | folders.WriteString(`<a href="`) | 121 | if !(x.Thumb.NSFW && !CFG.Nsfw) { |
| 172 | folders.WriteString(ConvertDeviantArtUrlToSkunkyArt(x.Thumb.Url)) | 122 | folders.WriteString(`<a href="`) |
| 173 | folders.WriteString(`"><img loading="lazy" src="`) | 123 | folders.WriteString(ConvertDeviantArtUrlToSkunkyArt(x.Thumb.Url)) |
| 174 | folders.WriteString(ParseMedia(x.Thumb.Media, x.Thumb.Title)) | 124 | folders.WriteString(`"><img loading="lazy" src="`) |
| 175 | folders.WriteString(`" title="`) | 125 | folders.WriteString(ParseMedia(x.Thumb.Media, x.Thumb.Title)) |
| 176 | folders.WriteString(x.Thumb.Title) | 126 | folders.WriteString(`" title="`) |
| 177 | folders.WriteString(`"></a>`) | 127 | folders.WriteString(x.Thumb.Title) |
| 178 | } else { | 128 | folders.WriteString(`"></a>`) |
| 179 | folders.WriteString(`<h1>[ <span class="nsfw">NSFW</span> ]</h1>`) | 129 | } else { |
| 130 | folders.WriteString(`<h1>[ <span class="nsfw">NSFW</span> ]</h1>`) | ||
| 131 | } | ||
| 132 | folders.WriteString("<br>") | ||
| 133 | |||
| 134 | folders.WriteString(`<a href="group_user?folder=`) | ||
| 135 | folders.WriteString(strconv.Itoa(x.FolderId)) | ||
| 136 | folders.WriteString("&q=") | ||
| 137 | folders.WriteString(s.Query) | ||
| 138 | folders.WriteString("&type=") | ||
| 139 | folders.WriteString(string(s.Type)) | ||
| 140 | folders.WriteString(`">`) | ||
| 141 | folders.WriteString(x.Name) | ||
| 142 | folders.WriteString(`</a>`) | ||
| 143 | |||
| 144 | folders.WriteString("</div>") | ||
| 180 | } | 145 | } |
| 181 | folders.WriteString("<br>") | ||
| 182 | |||
| 183 | folders.WriteString(`<a href="group_user?folder=`) | ||
| 184 | folders.WriteString(strconv.Itoa(x.FolderId)) | ||
| 185 | folders.WriteString("&q=") | ||
| 186 | folders.WriteString(s.Query) | ||
| 187 | folders.WriteString("&type=") | ||
| 188 | folders.WriteString(string(s.Type)) | ||
| 189 | folders.WriteString(`">`) | ||
| 190 | folders.WriteString(x.Name) | ||
| 191 | folders.WriteString(`</a>`) | ||
| 192 | |||
| 193 | folders.WriteString("</div>") | ||
| 194 | } | 146 | } |
| 195 | folders.WriteString(`</div><h1 id="content"><a href="#content">#</a> Content</h1>`) | 147 | folders.WriteString(`</div><h1 id="content"><a href="#content">#</a> Content</h1>`) |
| 196 | group.Gallery.Folders = folders.String() | 148 | group.Gallery.Folders = folders.String() |
| @@ -296,7 +248,7 @@ func (s skunkyart) Search() { | |||
| 296 | switch s.Type { | 248 | switch s.Type { |
| 297 | case 'a', 't': | 249 | case 'a', 't': |
| 298 | ss.Content, err = devianter.PerformSearch(s.Query, s.Page, s.Type) | 250 | ss.Content, err = devianter.PerformSearch(s.Query, s.Page, s.Type) |
| 299 | case 'g': | 251 | case 'g', 'f': |
| 300 | ss.Content, err = devianter.PerformSearch(s.Query, s.Page, s.Type, s.Args.Get("usr")) | 252 | ss.Content, err = devianter.PerformSearch(s.Query, s.Page, s.Type, s.Args.Get("usr")) |
| 301 | case 'r': // скраппер, поскольку девиантартовцы зажопили гостевое API для поиска групп | 253 | case 'r': // скраппер, поскольку девиантартовцы зажопили гостевое API для поиска групп |
| 302 | var ( | 254 | var ( |
go.mod +1
| @@ -2,6 +2,7 @@ module skunkyart | |||
| 2 | 2 | ||
| 3 | go 1.22.3 | 3 | go 1.22.3 |
| 4 | 4 | ||
| 5 | replace git.macaw.me/skunky/devianter v0.2.5 => /home/skunk/projects/devianter | ||
| 5 | require ( | 6 | require ( |
| 6 | git.macaw.me/skunky/devianter v0.2.5 | 7 | git.macaw.me/skunky/devianter v0.2.5 |
| 7 | golang.org/x/net v0.27.0 | 8 | golang.org/x/net v0.27.0 |
instances.json +1 −1
| @@ -25,7 +25,7 @@ | |||
| 25 | }, | 25 | }, |
| 26 | { | 26 | { |
| 27 | "title": "bloat.cat", | 27 | "title": "bloat.cat", |
| 28 | "country": "Romania", | 28 | "country": "Germany", |
| 29 | "urls": { | 29 | "urls": { |
| 30 | "clearnet": "https://skunky.bloat.cat" | 30 | "clearnet": "https://skunky.bloat.cat" |
| 31 | }, | 31 | }, |
static/html/gruser.htm +14 −3
| @@ -3,14 +3,25 @@ | |||
| 3 | {{template "head" . }} | 3 | {{template "head" . }} |
| 4 | <main> | 4 | <main> |
| 5 | <header> | 5 | <header> |
| 6 | <h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a> | 6 | <h1> |
| 7 | | <a href="group_user?q={{.Templates.GroupUser.GR.Owner.Username}}&type={{if eq .Type 'a'}}gallery">Gallery{{else}}about">About{{end}}</a> | 7 | <a href="{{.BasePath}}">HOME</a> |
| 8 | | <a href="group_user?q={{.Templates.GroupUser.GR.Owner.Username}}&type=gallery&atom=true">RSS</a></h1> | 8 | | <a href="{{.BasePath}}dd">DD</a> |
| 9 | {{if ne .Type 'f'}} | ||
| 10 | | <a href="group_user?q={{.Templates.GroupUser.GR.Owner.Username}}&type={{if eq .Type 'a'}}gallery">Gallery{{else}}about">About{{end}}</a> | ||
| 11 | | <a href="group_user?q={{.Templates.GroupUser.GR.Owner.Username}}&type=favourites">Favourites</a> | ||
| 12 | {{else}} | ||
| 13 | | <a href="group_user?q={{.Templates.GroupUser.GR.Owner.Username}}&type=about">About</a> | ||
| 14 | | <a href="group_user?q={{.Templates.GroupUser.GR.Owner.Username}}&type=gallery">Gallery</a> | ||
| 15 | | <a href="group_user?q={{.Templates.GroupUser.GR.Owner.Username}}&type=favourites">Favourites</a> | ||
| 16 | {{end}} | ||
| 17 | | <a href="group_user?q={{.Templates.GroupUser.GR.Owner.Username}}&type=gallery&atom=true">RSS</a> | ||
| 18 | </h1> | ||
| 9 | <form method="get" action="{{.BasePath}}search"> | 19 | <form method="get" action="{{.BasePath}}search"> |
| 10 | <input type="gallery" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false"> | 20 | <input type="gallery" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false"> |
| 11 | <input type="hidden" name="usr" value="{{.Templates.GroupUser.GR.Owner.Username}}"> | 21 | <input type="hidden" name="usr" value="{{.Templates.GroupUser.GR.Owner.Username}}"> |
| 12 | <select name="type"> | 22 | <select name="type"> |
| 13 | <option value="gallery">Gallery</option> | 23 | <option value="gallery">Gallery</option> |
| 24 | <option value="folders">Folders</option> | ||
| 14 | <option value="all">All</option> | 25 | <option value="all">All</option> |
| 15 | <option value="tag">Tag</option> | 26 | <option value="tag">Tag</option> |
| 16 | <option value="r">Groups</option> | 27 | <option value="r">Groups</option> |
static/html/head.htm +6 −5
| @@ -5,12 +5,13 @@ | |||
| 5 | "{{.QueryRaw}}" | 5 | "{{.QueryRaw}}" |
| 6 | {{else if eq .Endpoint "post"}} | 6 | {{else if eq .Endpoint "post"}} |
| 7 | {{.Templates.Deviation.Post.Deviation.Author.Username}} — {{.Templates.Deviation.Post.Deviation.Title}} | 7 | {{.Templates.Deviation.Post.Deviation.Author.Username}} — {{.Templates.Deviation.Post.Deviation.Title}} |
| 8 | {{else if eq .Type 'a'}} | 8 | {{else if eq .Endpoint "group_user"}} |
| 9 | {{if .Templates.GroupUser.GR.Owner.Username}} | 9 | {{if eq .Type 'g'}} |
| 10 | {{.Templates.GroupUser.GR.Owner.Username}} | 10 | gallery of |
| 11 | {{else}} | 11 | {{else if eq .Type 'f'}} |
| 12 | gallery of {{.Templates.GroupUser.GR.Owner.Username}} | 12 | favourites of |
| 13 | {{end}} | 13 | {{end}} |
| 14 | {{.Templates.GroupUser.GR.Owner.Username}} | ||
| 14 | {{else}} | 15 | {{else}} |
| 15 | {{.Endpoint}} | 16 | {{.Endpoint}} |
| 16 | {{end}} | 17 | {{end}} |