Commit 2dfeaae772

2dfeaae772e630ae361a35bf4680257d081560c1

parent: e02174cb71

Unsigned

lost+skunk <skunky@ebloid.ru> · 2024-07-29 22:06 UTC

Подготовка к релизу v1.3.1

Layout: unified · split

README.md +22 −17
@@ -1,11 +1,14 @@
1<img src="https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/misc/logo.png" alt="SkunkyArt" title="SkunkyArt Logo" width="20%"/>
2
13[![Matrix room](https://img.shields.io/badge/matrix-000000?style=for-the-badge&logo=Matrix&logoColor=white)](https://go.kde.org/matrix/#/#skunkyart:ebloid.ru)
24# Instances
3|Инстанс|Yggdrasil|I2P|Tor|NSFW|Proxifying|Country|
4|:-----:|:-------:|:-:|:-:|:--:|:--------:|:-----:|
5|Instance|Yggdrasil|I2P|Tor|NSFW|Proxifying|Country|
6|:------:|:-------:|:-:|:-:|:--:|:--------:|:-----:|
57|[skunky.ebloid.ru](https://skunky.ebloid.ru/art)|[Yes](http://[201:eba5:d1fc:bf7b:cfcb:a811:4b8b:7ea3]/art)|No|No| No | No | Russia |
68|[clovius.club](https://skunky.clovius.club)|No|No|No| Yes | Yes | Sweden |
79|[bloat.cat](https://skunky.bloat.cat)|No|No|No| Yes | Yes | Romania |
810|[frontendfriendly.xyz](https://skunkyart.frontendfriendly.xyz)|No|No|No| Yes | Yes | Finland |
11|[lumaeris.com](https://skunkyart.lumaeris.com)|No|No|No| Yes | Yes | US |
912
1013# EN 🇺🇸
1114## Description
@@ -13,12 +16,12 @@ SkunkyArt 🦨 -- alternative frontend to DeviantArt, which will work without pr
1316## Config
1417The sample config is in the `config.example.json` file. To specify your own path to the config, use the CLI argument `-c` or `--config`.
1518* `listen` -- the address and port on which SkunkyArt will listen
16* `base-path` -- the path to the instance. Example: "`base-path`:"/art/" -> https://skunky.ebloid.ru/art/
19* `base-path` -- the path to the instance. Example: `"base-path":"/art/"` -> https://skunky.ebloid.ru/art/
1720* `cache` -- caching system; default is off.
18* * `path` -- the path to the cache
19* * `lifetime` -- cache file lifetime; measured in Unix milliseconds.
20* * `max-size` -- maximum file size in bytes.
21* `dirs-to-memory` -- this setting determines which directories will be copied to RAM when SkunkyArt is started. Required
21 * `path` -- the path to the cache
22 * `lifetime` -- the lifetime of the file in the cache. Units: i, h, d, w, m, y. I -- minute, all other units I think are self-explanatory.
23 * `max-size` -- maximum file size in megabytes.
24* `dirs-to-memory` -- this setting determines which directories will be copied to RAM when SkunkyArt is started. Mandatory
2225* `download-proxy` -- proxy address for downloading files.
2326## Examples of reverse proxies
2427Nginx:
@@ -27,17 +30,18 @@ server {
2730 listen 443 ssl;
2831 server_name skunky.example.com;
2932
30 location ((BASE URL)) { # if you have a separate subdomain for the frontend, insert '/' instead of '((BASE URL))'.
31 proxy_set_header Scheme $scheme;
33 location ((BASE URL)) { # if you have a separate subdomain for the frontend, insert '/' instead of '((BASE URL)))'.
34 proxy_set_header X-Forwarded-Proto $scheme;
3235 proxy_set_header Host $host;
3336 proxy_http_version 1.1;
3437 proxy_pass http://((IP)):((PORT));
3538 }
3639}
3740```
41Pretty much business as usual, except for the [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) header setting.
3842## How do I add my instance to the list?
39To do this, you must either make a PR by adding your instance to the `instances.json` file, or report it to the room in Matrix. I don't think it needs any description. However, be aware, this list has a couple rules:
401. the instance must not use Cloudflare.
43To do this, you must either make a PR by adding your instance to the `instances.json` and `README.md` files, or create an Issue, or report it to the room in Matrix. I don't think it needs any description. However, be warned, this list has a couple rules:
441. the Instance must not use Cloudflare.
41452. If your instance has modified source code, you need to publish it to any free platform. For example, Github and Gitlab are not.
4246## Acknowledgements
4347* [Лис⚛](https://go.kde.org/matrix/#/@fox:matrix.org) -- helped me understand Go and gave me a lot of useful advice on this language.
@@ -48,11 +52,11 @@ SkunkyArt 🦨 -- альтернативный фронтенд к DeviantArt,
4852## Конфиг
4953Пример конфига находится в файле `config.example.json`. Чтобы указать свой путь до конфига, используйте CLI-аргумент `-c` или `--config`.
5054* `listen` -- адрес и порт, на котором будет слушать SkunkyArt
51* `base-path` -- путь к инстансу. Пример: "base-path": "/art/" -> https://skunky.ebloid.ru/art/
55* `base-path` -- путь к инстансу. Пример: `"base-path": "/art/"` -> https://skunky.ebloid.ru/art/
5256* `cache` -- система кеширования; по умолчанию - выкл.
53* * `path` -- путь до кеша
54* * `lifetime` -- время жизни файла в кеше; измеряется в Unix-миллисекундах
55* * `max-size` -- максимальный размер файла в байтах
57 * `path` -- путь до кеша
58 * `lifetime` -- время жизни файла в кеше. Единицы измерения: i, h, d, w, m, y. I -- минута, всё остальные единицы измерения, я считаю понятными и без объяснения.
59 * `max-size` -- максимальный размер файла в мегабайтах
5660* `dirs-to-memory` -- данная настройка определяет какие каталоги будут скопированы в ОЗУ при запуске SkunkyArt. Обязательна
5761* `download-proxy` -- адрес прокси для загрузки файлов
5862## Примеры reverse-прокси
@@ -63,15 +67,16 @@ server {
6367 server_name skunky.example.com;
6468
6569 location ((BASE URL)) { # если у вас отдельный поддомен для фронтенда, вместо '((BASE URL))' вставляйте '/'
66 proxy_set_header Scheme $scheme;
70 proxy_set_header X-Forwarded-Proto $scheme;
6771 proxy_set_header Host $host;
6872 proxy_http_version 1.1;
6973 proxy_pass http://((IP)):((PORT));
7074 }
7175}
7276```
77В целом, всё как обычно, за исключением настройки заголовка [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto).
7378## Как добавить свой инстанс в список?
74Чтобы это сделать, вы должны либо сделать PR, добавив в файл `instances.json` свой инстанс, либо сообщить о нём в комнате в Matrix. Думаю, он не нуждается в описании. Однако учтите, у этого списка есть пара правил:
79Чтобы это сделать, вы должны либо сделать PR, добавив в файлы `instances.json` и `README.md` свой инстанс, либо создать Issue, или сообщить о нём в комнате в Matrix. Думаю, он не нуждается в описании. Однако учтите, у этого списка есть пара правил:
75801. Инстанс не должен использовать Cloudflare.
76812. Если ваш инстанс имеет модифицированный исходный код, то вам нужно опубликовать его на любую свободную площадку. Например, Github и Gitlab таковыми не являются.
7782## Благодарности
TODO.md +13 −4
@@ -1,7 +1,16 @@
11# v1.3.x
2* Доделать парсинг описания
3* Реализовать миниатюры и оптимизировать CSS под маленькие экраны
2* Написать Makefile
3* Почистить говнокод
4* **Доделать парсинг описания**
5* Избавиться от хардкода под Linux
6* ~~Реализовать стрипы в ежедневных артах~~
7* ~~Исправить баг с навигацией по страницам~~
8* ~~Сделать единицы в конфиге более понятными~~
9* Добавить возможность включить темплейты в бинарник
10* ~~Реализовать миниатюры и оптимизировать CSS под маленькие экраны~~
11* **Реализовать отображение контента, отличного от картинок (видео, аудио, etc)**
12* Улучшить систему кеширования: добавить рейтинг для удаления и копирование изображений в ОЗУ
413# v1.4
14* Реализовать API
515* Реализовать темы
6* Реализовать многоязычный интерфейс
7* Реализовать API
\ No newline at end of file
16* Реализовать многоязычный интерфейс
\ No newline at end of file
app/config.go +53 −16
@@ -3,14 +3,18 @@ package app
33import (
44 "encoding/json"
55 "os"
6 "regexp"
7 "strconv"
68 "time"
9
10 "git.macaw.me/skunky/devianter"
711)
812
913type cache_config struct {
1014 Enabled bool
1115 Path string
1216 MaxSize int64 `json:"max-size"`
13 Lifetime int64
17 Lifetime string
1418 UpdateInterval int64 `json:"update-interval"`
1519}
1620
@@ -20,6 +24,7 @@ type config struct {
2024 BasePath string `json:"base-path"`
2125 Cache cache_config
2226 Proxy, Nsfw bool
27 UserAgent string `json:"user-agent"`
2328 DownloadProxy string `json:"download-proxy"`
2429 Dirs []string `json:"dirs-to-memory"`
2530}
@@ -33,31 +38,36 @@ var CFG = config{
3338 Path: "cache",
3439 UpdateInterval: 1,
3540 },
36 Dirs: []string{"html", "css"},
37 Proxy: true,
38 Nsfw: true,
41 Dirs: []string{"html", "css"},
42 UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
43 Proxy: true,
44 Nsfw: true,
3945}
4046
47var lifetimeParsed int64
48
4149func ExecuteConfig() {
4250 go func() {
43 defer func() {
44 if r := recover(); r != nil {
45 recover()
46 }
47 }()
4851 for {
49 Templates["instances.json"] = string(Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body)
50 time.Sleep(1 * time.Second)
52 func() {
53 defer func() {
54 if r := recover(); r != nil {
55 recover()
56 }
57 }()
58 Templates["instances.json"] = string(Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body)
59 }()
60 time.Sleep(1 * time.Hour)
5161 }
5262 }()
5363
54 const helpmsg = `SkunkyArt v1.3 [refactoring]
64 const helpmsg = `SkunkyArt v1.3.1 [CSS improvements for mobile and strips on Daily Deviations]
5565Usage:
5666 - [-c|--config] - path to config
5767 - [-h|--help] - returns this message
5868Example:
5969 ./skunkyart -c config.json
60Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3`
70Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3.1`
6171
6272 a := os.Args
6373 for n, x := range a {
@@ -75,15 +85,42 @@ Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3`
7585
7686 if CFG.cfg != "" {
7787 f, err := os.ReadFile(CFG.cfg)
78 try_with_exitstatus(err, 1)
88 tryWithExitStatus(err, 1)
7989
80 try_with_exitstatus(json.Unmarshal(f, &CFG), 1)
90 tryWithExitStatus(json.Unmarshal(f, &CFG), 1)
8191 if CFG.Cache.Enabled && !CFG.Proxy {
8292 exit("Incompatible settings detected: cannot use caching media content without proxy", 1)
8393 }
8494
85 if CFG.Cache.MaxSize != 0 || CFG.Cache.Lifetime != 0 {
95 if CFG.Cache.Enabled {
96 if CFG.Cache.Lifetime != "" {
97 var duration int64
98 day := 24 * time.Hour.Milliseconds()
99 numstr := regexp.MustCompile("[0-9]+").FindAllString(CFG.Cache.Lifetime, -1)
100 num, _ := strconv.Atoi(numstr[len(numstr)-1])
101
102 switch unit := CFG.Cache.Lifetime[len(CFG.Cache.Lifetime)-1:]; unit {
103 case "i":
104 duration = time.Minute.Milliseconds()
105 case "h":
106 duration = time.Hour.Milliseconds()
107 case "d":
108 duration = day
109 case "w":
110 duration = day * 7
111 case "m":
112 duration = day * 30
113 case "y":
114 duration = day * 360
115 default:
116 exit("Invalid unit specified: "+unit, 1)
117 }
118
119 lifetimeParsed = duration * int64(num)
120 }
121 CFG.Cache.MaxSize /= 1024 ^ 2
86122 go InitCacheSystem()
87123 }
124 devianter.UserAgent = CFG.UserAgent
88125 }
89126}
app/parsers.go +110 −103
@@ -70,95 +70,100 @@ func (s skunkyart) ParseComments(c devianter.Comments) string {
7070 return cmmts.String()
7171}
7272
73func (s skunkyart) DeviationList(devs []devianter.Deviation, content ...DeviationList) string {
74 var list strings.Builder
73func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, content ...DeviationList) string {
7574 if s.Atom && s.Page > 1 {
7675 s.ReturnHTTPError(400)
7776 return ""
78 } else if s.Atom {
79 list.WriteString(`<?xml version="1.0" encoding="UTF-8"?><feed xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom">`)
80 list.WriteString(`<title>`)
81 if s.Type == 0 {
82 list.WriteString("Daily Deviations")
83 } else if len(devs) != 0 {
84 list.WriteString(devs[0].Author.Username)
85 } else {
86 list.WriteString("SkunkyArt")
87 }
88 list.WriteString(`</title>`)
89
90 list.WriteString(`<link rel="alternate" href="`)
91 list.WriteString(Host)
92 list.WriteString(`"/>`)
93 } else {
94 list.WriteString(`<div class="content">`)
9577 }
96 for _, data := range devs {
97 if !(data.NSFW && !CFG.Nsfw) {
98 url := ParseMedia(data.Media)
99 if s.Atom {
78
79 var list, listContent strings.Builder
80
81 for i, l := 0, len(devs); i < l; i++ {
82 data := &devs[i]
83 if preview, fullview := ParseMedia(data.Media, 320), ParseMedia(data.Media); !(data.NSFW && !CFG.Nsfw) {
84 if allowAtom && s.Atom {
10085 id := strconv.Itoa(data.ID)
101 list.WriteString(`<entry><author><name>`)
102 list.WriteString(data.Author.Username)
103 list.WriteString(`</name></author><title>`)
104 list.WriteString(data.Title)
105 list.WriteString(`</title><link rel="alternate" type="text/html" href="`)
106 list.WriteString(UrlBuilder("post", data.Author.Username, "atom-"+id))
107 list.WriteString(`"/><id>`)
108 list.WriteString(id)
109 list.WriteString(`</id><published>`)
110 list.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700"))
111 list.WriteString(`</published>`)
112 list.WriteString(`<media:group><media:title>`)
113 list.WriteString(data.Title)
114 list.WriteString(`</media:title><media:thumbinal url="`)
115 list.WriteString(url)
116 list.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`)
117 list.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url))
118 list.WriteString(`"><img src="`)
119 list.WriteString(url)
120 list.WriteString(`"/></a><p>`)
121 list.WriteString(ParseDescription(data.TextContent))
122 list.WriteString(`</p></div></content></entry>`)
86 listContent.WriteString(`<entry><author><name>`)
87 listContent.WriteString(data.Author.Username)
88 listContent.WriteString(`</name></author><title>`)
89 listContent.WriteString(data.Title)
90 listContent.WriteString(`</title><link rel="alternate" type="text/html" href="`)
91 listContent.WriteString(UrlBuilder("post", data.Author.Username, "atom-"+id))
92 listContent.WriteString(`"/><id>`)
93 listContent.WriteString(id)
94 listContent.WriteString(`</id><published>`)
95 listContent.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700"))
96 listContent.WriteString(`</published>`)
97 listContent.WriteString(`<media:group><media:title>`)
98 listContent.WriteString(data.Title)
99 listContent.WriteString(`</media:title><media:thumbinal url="`)
100 listContent.WriteString(preview)
101 listContent.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`)
102 listContent.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url))
103 listContent.WriteString(`"><img src="`)
104 listContent.WriteString(fullview)
105 listContent.WriteString(`"/></a><p>`)
106 listContent.WriteString(ParseDescription(data.TextContent))
107 listContent.WriteString(`</p></div></content></entry>`)
123108 } else {
124 list.WriteString(`<div class="block">`)
125 if url != "" {
126 list.WriteString(`<a title="open/download" href="`)
127 list.WriteString(url)
128 list.WriteString(`"><img loading="lazy" src="`)
129 list.WriteString(url)
130 list.WriteString(`" width="15%"></a>`)
109 listContent.WriteString(`<div class="block">`)
110 if fullview != "" && preview != "" {
111 listContent.WriteString(`<a title="open/download" href="`)
112 listContent.WriteString(fullview)
113 listContent.WriteString(`"><img loading="lazy" src="`)
114 listContent.WriteString(preview)
115 listContent.WriteString(`" width="15%"></a>`)
131116 } else {
132 list.WriteString(`<h1>[ TEXT ]</h1>`)
117 listContent.WriteString(`<h1>[ TEXT ]</h1>`)
133118 }
134 list.WriteString(`<br><a href="`)
135 list.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url))
136 list.WriteString(`">`)
137 list.WriteString(data.Author.Username)
138 list.WriteString(" - ")
139 list.WriteString(data.Title)
119 listContent.WriteString(`<br><a href="`)
120 listContent.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url))
121 listContent.WriteString(`">`)
122 listContent.WriteString(data.Author.Username)
123 listContent.WriteString(" - ")
124 listContent.WriteString(data.Title)
140125
141 // шильдики нсфв, аи и ежедневного поста
142126 if data.NSFW {
143 list.WriteString(` [<span class="nsfw">NSFW</span>]`)
127 listContent.WriteString(` [<span class="nsfw">NSFW</span>]`)
144128 }
145129 if data.AI {
146 list.WriteString(" [🤖]")
130 listContent.WriteString(" [🤖]")
147131 }
148132 if data.DD {
149 list.WriteString(` [<span class="dd">DD</span>]`)
133 listContent.WriteString(` [<span class="dd">DD</span>]`)
150134 }
151135
152 list.WriteString("</a></div>")
136 listContent.WriteString("</a></div>")
153137 }
154138 }
155139 }
156140
157 if s.Atom {
141 if allowAtom && s.Atom {
142 list.WriteString(`<?xml version="1.0" encoding="UTF-8"?><feed xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom">`)
143
144 list.WriteString(`<title>`)
145 if s.Type == 0 {
146 list.WriteString("Daily Deviations")
147 } else if s.Type == 'g' && len(devs) != 0 {
148 list.WriteString(devs[0].Author.Username)
149 } else {
150 list.WriteString("SkunkyArt")
151 }
152 list.WriteString(`</title>`)
153
154 list.WriteString(`<link rel="alternate" href="`)
155 list.WriteString(Host)
156 list.WriteString(`"/>`)
157
158 list.WriteString(listContent.String())
159
158160 list.WriteString("</feed>")
159 s.Writer.Write([]byte(list.String()))
160 return ""
161 wr(s.Writer, list.String())
161162 } else {
163 list.WriteString(`<div class="content">`)
164
165 list.WriteString(listContent.String())
166
162167 list.WriteString("</div>")
163168 if content != nil {
164169 list.WriteString(s.NavBase(content[0]))
@@ -177,7 +182,7 @@ type text struct {
177182}
178183
179184func ParseDescription(dscr devianter.Text) string {
180 var parseddescription strings.Builder
185 var parsedDescription strings.Builder
181186 TagBuilder := func(content string, tags ...string) string {
182187 l := len(tags)
183188 for x := 0; x < l; x++ {
@@ -274,16 +279,18 @@ func ParseDescription(dscr devianter.Text) string {
274279
275280 switch x.Type {
276281 case "atomic":
277 d := entities[x.EntityRanges[0].Key]
278 parseddescription.WriteString(`<a href="`)
279 parseddescription.WriteString(ConvertDeviantArtUrlToSkunkyArt(d.Url))
280 parseddescription.WriteString(`"><img width="50%" src="`)
281 parseddescription.WriteString(ParseMedia(d.Media))
282 parseddescription.WriteString(`" title="`)
283 parseddescription.WriteString(d.Author.Username)
284 parseddescription.WriteString(" - ")
285 parseddescription.WriteString(d.Title)
286 parseddescription.WriteString(`"></a>`)
282 if len(x.EntityRanges) != 0 {
283 d := entities[x.EntityRanges[0].Key]
284 parsedDescription.WriteString(`<a href="`)
285 parsedDescription.WriteString(ConvertDeviantArtUrlToSkunkyArt(d.Url))
286 parsedDescription.WriteString(`"><img width="50%" src="`)
287 parsedDescription.WriteString(ParseMedia(d.Media))
288 parsedDescription.WriteString(`" title="`)
289 parsedDescription.WriteString(d.Author.Username)
290 parsedDescription.WriteString(" - ")
291 parsedDescription.WriteString(d.Title)
292 parsedDescription.WriteString(`"></a>`)
293 }
287294 case "unstyled":
288295 if l := len(Styles); l != 0 {
289296 for n, r := range Styles {
@@ -292,31 +299,31 @@ func ParseDescription(dscr devianter.Text) string {
292299 tag = "h2"
293300 }
294301
295 parseddescription.WriteString(x.Text[:r.From])
302 parsedDescription.WriteString(x.Text[:r.From])
296303 if len(urls) != 0 && len(x.EntityRanges) != 0 {
297304 ra := &x.EntityRanges[0]
298305
299 parseddescription.WriteString(`<a target="_blank" href="`)
300 parseddescription.WriteString(urls[ra.Key])
301 parseddescription.WriteString(`">`)
302 parseddescription.WriteString(r.TXT)
303 parseddescription.WriteString(`</a>`)
306 parsedDescription.WriteString(`<a target="_blank" href="`)
307 parsedDescription.WriteString(urls[ra.Key])
308 parsedDescription.WriteString(`">`)
309 parsedDescription.WriteString(r.TXT)
310 parsedDescription.WriteString(`</a>`)
304311 } else if l > n+1 {
305 parseddescription.WriteString(r.TXT)
312 parsedDescription.WriteString(r.TXT)
306313 }
307 parseddescription.WriteString(TagBuilder(tag, x.Text[r.To:]))
314 parsedDescription.WriteString(TagBuilder(tag, x.Text[r.To:]))
308315 }
309316 } else {
310 parseddescription.WriteString(x.Text)
317 parsedDescription.WriteString(x.Text)
311318 }
312319 }
313 parseddescription.WriteString("<br>")
320 parsedDescription.WriteString("<br>")
314321 }
315322 } else if dl != 0 {
316323 for tt := html.NewTokenizer(strings.NewReader(dscr.Html.Markup)); ; {
317324 switch tt.Next() {
318325 case html.ErrorToken:
319 return parseddescription.String()
326 return parsedDescription.String()
320327 case html.StartTagToken, html.EndTagToken, html.SelfClosingTagToken:
321328 token := tt.Token()
322329 switch token.Data {
@@ -324,11 +331,11 @@ func ParseDescription(dscr devianter.Text) string {
324331 for _, a := range token.Attr {
325332 if a.Key == "href" {
326333 url := DeleteTrackingFromUrl(a.Val)
327 parseddescription.WriteString(`<a target="_blank" href="`)
328 parseddescription.WriteString(url)
329 parseddescription.WriteString(`">`)
330 parseddescription.WriteString(GetValueOfTag(tt))
331 parseddescription.WriteString("</a> ")
334 parsedDescription.WriteString(`<a target="_blank" href="`)
335 parsedDescription.WriteString(url)
336 parsedDescription.WriteString(`">`)
337 parsedDescription.WriteString(GetValueOfTag(tt))
338 parsedDescription.WriteString("</a> ")
332339 }
333340 }
334341 case "img":
@@ -344,24 +351,24 @@ func ParseDescription(dscr devianter.Text) string {
344351 }
345352 if title != "" {
346353 for x := -1; x < b; x++ {
347 parseddescription.WriteString(`<img src="`)
348 parseddescription.WriteString(uri)
349 parseddescription.WriteString(`" title="`)
350 parseddescription.WriteString(title)
351 parseddescription.WriteString(`">`)
354 parsedDescription.WriteString(`<img src="`)
355 parsedDescription.WriteString(uri)
356 parsedDescription.WriteString(`" title="`)
357 parsedDescription.WriteString(title)
358 parsedDescription.WriteString(`">`)
352359 }
353360 }
354361 }
355362 case "br", "li", "ul", "p", "b":
356 parseddescription.WriteString(token.String())
363 parsedDescription.WriteString(token.String())
357364 case "div":
358 parseddescription.WriteString("<p> ")
365 parsedDescription.WriteString("<p> ")
359366 }
360367 case html.TextToken:
361 parseddescription.Write(tt.Text())
368 parsedDescription.Write(tt.Text())
362369 }
363370 }
364371 }
365372
366 return parseddescription.String()
373 return parsedDescription.String()
367374}
app/router.go +7 −4
@@ -1,7 +1,6 @@
11package app
22
33import (
4 "io"
54 "net/http"
65 u "net/url"
76 "strconv"
@@ -44,7 +43,7 @@ func Router() {
4443
4544 // функция, что управляет всем
4645 handle := func(w http.ResponseWriter, r *http.Request) {
47 if h := r.Header["Scheme"]; len(h) != 0 && h[0] == "https" {
46 if h := r.Header["X-Forwarded-Proto"]; len(h) != 0 && h[0] == "https" {
4847 Host = h[0] + "://" + r.Host
4948 } else {
5049 Host = "http://" + r.Host
@@ -97,10 +96,14 @@ func Router() {
9796 skunky.About()
9897 case "stylesheet":
9998 w.Header().Add("content-type", "text/css")
100 io.WriteString(w, Templates["skunky.css"])
99 wr(w, Templates["skunky.css"])
100 case "favicon.ico":
101 wr(w, Templates["logo.png"])
101102 }
102103 }
103104
104105 http.HandleFunc("/", handle)
105 try_with_exitstatus(http.ListenAndServe(CFG.Listen, nil), 1)
106 println("SkunkyArt is listening on", CFG.Listen)
107
108 tryWithExitStatus(http.ListenAndServe(CFG.Listen, nil), 1)
106109}
app/util.go +22 −32
@@ -26,7 +26,7 @@ func try(e error) {
2626 println(e.Error())
2727 }
2828}
29func try_with_exitstatus(err error, code int) {
29func tryWithExitStatus(err error, code int) {
3030 if err != nil {
3131 exit(err.Error(), code)
3232 }
@@ -87,7 +87,7 @@ func Download(url string) (d Downloaded) {
8787
8888 req, e := http.NewRequest("GET", url, nil)
8989 try(e)
90 req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0.0")
90 req.Header.Set("User-Agent", CFG.UserAgent)
9191
9292 resp, e := cli.Do(req)
9393 try(e)
@@ -148,14 +148,14 @@ func InitCacheSystem() {
148148 try(e)
149149 for _, a := range dirnames {
150150 a = c.Path + "/" + a
151 if c.Lifetime != 0 {
151 if c.Lifetime != "" {
152152 now := time.Now().UnixMilli()
153153
154154 f, _ := os.Stat(a)
155155 stat := f.Sys().(*syscall.Stat_t)
156156 time := time.Unix(stat.Ctim.Unix()).UnixMilli()
157157
158 if time+c.Lifetime <= now {
158 if time+lifetimeParsed <= now {
159159 try(os.RemoveAll(a))
160160 }
161161 }
@@ -172,19 +172,19 @@ func InitCacheSystem() {
172172func CopyTemplatesToMemory() {
173173 for _, dirname := range CFG.Dirs {
174174 dir, e := os.ReadDir(dirname)
175 try_with_exitstatus(e, 1)
175 tryWithExitStatus(e, 1)
176176
177177 for _, x := range dir {
178178 file, e := os.ReadFile(dirname + "/" + x.Name())
179 try_with_exitstatus(e, 1)
179 tryWithExitStatus(e, 1)
180180 Templates[x.Name()] = string(file)
181181 }
182182 }
183183}
184184
185185/* PARSING HELPERS */
186func ParseMedia(media devianter.Media) string {
187 url := devianter.UrlFromMedia(media)
186func ParseMedia(media devianter.Media, thumb ...int) string {
187 url := devianter.UrlFromMedia(media, thumb...)
188188 if len(url) != 0 && CFG.Proxy {
189189 url = url[21:]
190190 dot := strings.Index(url, ".")
@@ -197,9 +197,10 @@ func ParseMedia(media devianter.Media) string {
197197func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) {
198198 if len(url) > 32 && url[27:32] != "stash" {
199199 url = url[27:]
200 toart := strings.Index(url, "/art/")
201 if toart != -1 {
202 output = UrlBuilder("post", url[:toart], url[toart+5:])
200 firstshash := strings.Index(url, "/")
201 lastshash := firstshash + strings.Index(url[firstshash+1:], "/")
202 if lastshash != -1 {
203 output = UrlBuilder("post", url[:firstshash], url[lastshash+2:])
203204 }
204205 }
205206 return
@@ -236,13 +237,9 @@ type DeviationList struct {
236237
237238// FIXME: на некоротрых артах первая страница может вызывать полное отсутствие панели навигации.
238239func (s skunkyart) NavBase(c DeviationList) string {
239 // TODO: сделать понятнее
240 // навигация по страницам
241240 var list strings.Builder
242 list.WriteString("<br>")
243 p := s.Page
244241
245 // функция для генерации ссылок
242 list.WriteString("<br>")
246243 prevrev := func(msg string, page int, onpage bool) {
247244 if !onpage {
248245 list.WriteString(`<a href="?p=`)
@@ -268,33 +265,26 @@ func (s skunkyart) NavBase(c DeviationList) string {
268265 }
269266 }
270267
271 // вперёд-назад
268 p := s.Page
269
272270 if p > 1 {
273271 prevrev("<= Prev |", p-1, false)
274272 } else {
275273 p = 1
276274 }
277275
278 if c.Pages > 0 {
279 // назад
280 for x := p - 6; x < p && x > 0; x++ {
281 prevrev(strconv.Itoa(x), x, false)
282 }
283
284 // вперёд
285 for x := p; x <= p+6 && c.Pages > p+6; x++ {
286 if x == p {
287 prevrev("", x, true)
288 x++
276 for i, x := p-6, 0; (i <= c.Pages && i <= p+6) && x < 12; i++ {
277 if i > 0 {
278 var onPage bool
279 if i == p {
280 onPage = true
289281 }
290282
291 if x > p {
292 prevrev(strconv.Itoa(x), x, false)
293 }
283 prevrev(strconv.Itoa(i), i, onPage)
284 x++
294285 }
295286 }
296287
297 // вперёд-назад
298288 if c.More {
299289 prevrev("| Next =>", p+1, false)
300290 }
app/wrapper.go +35 −16
@@ -48,6 +48,7 @@ type skunkyart struct {
4848 }
4949
5050 SomeList string
51 DDStrips string
5152 Deviation struct {
5253 Post devianter.Post
5354 Related string
@@ -93,7 +94,10 @@ func (s skunkyart) GRUser() {
9394
9495 var g devianter.Group
9596 g.Name = s.Query
96 s.Templates.GroupUser.GR = g.GroupFunc()
97 var err error
98 s.Templates.GroupUser.GR, err = g.GetGroup()
99 try(err)
100
97101 group := &s.Templates.GroupUser
98102
99103 switch s.Type {
@@ -135,7 +139,7 @@ func (s skunkyart) GRUser() {
135139 group.About.Interests += interest.String()
136140 }
137141 }
138 group.About.Comments = s.ParseComments(devianter.CommentsFunc(
142 group.About.Comments = s.ParseComments(devianter.GetComments(
139143 strconv.Itoa(group.GR.Gruser.ID),
140144 "",
141145 s.Page,
@@ -161,9 +165,11 @@ func (s skunkyart) GRUser() {
161165 s.Page++
162166 }
163167
164 gallery := g.Gallery(s.Page, folderid)
168 gallery, err := g.GetGallery(s.Page, folderid)
169 try(err)
170
165171 if folderid > 0 {
166 group.Gallery.List = s.DeviationList(gallery.Content.Results, DeviationList{
172 group.Gallery.List = s.DeviationList(gallery.Content.Results, true, DeviationList{
167173 More: gallery.Content.HasMore,
168174 })
169175 } else {
@@ -204,7 +210,7 @@ func (s skunkyart) GRUser() {
204210 }
205211
206212 if x.Name == "folder_deviations" {
207 group.Gallery.List = s.DeviationList(x.ModuleData.Folder.Deviations, DeviationList{
213 group.Gallery.List = s.DeviationList(x.ModuleData.Folder.Deviations, true, DeviationList{
208214 Pages: x.ModuleData.Folder.Pages,
209215 More: x.ModuleData.Folder.HasMore,
210216 })
@@ -227,7 +233,7 @@ func (s skunkyart) Deviation(author, postname string) {
227233 post := &s.Templates.Deviation
228234
229235 id := id_search[len(id_search)-1]
230 post.Post = devianter.DeviationFunc(id, author)
236 post.Post = devianter.GetDeviation(id, author)
231237
232238 if post.Post.Deviation.TextContent.Excerpt != "" {
233239 post.Post.Description = ParseDescription(post.Post.Deviation.TextContent)
@@ -239,7 +245,7 @@ func (s skunkyart) Deviation(author, postname string) {
239245 post.Post.IMG = ParseMedia(post.Post.Deviation.Media)
240246 for _, x := range post.Post.Deviation.Extended.RelatedContent {
241247 if len(x.Deviations) != 0 {
242 post.Related += s.DeviationList(x.Deviations)
248 post.Related += s.DeviationList(x.Deviations, false)
243249 }
244250 }
245251
@@ -259,7 +265,7 @@ func (s skunkyart) Deviation(author, postname string) {
259265 post.Post.Comments.Cursor = ""
260266 }
261267
262 post.Comments = s.ParseComments(devianter.CommentsFunc(id, post.Post.Comments.Cursor, s.Page, 1))
268 post.Comments = s.ParseComments(devianter.GetComments(id, post.Post.Comments.Cursor, s.Page, 1))
263269
264270 s.ExecuteTemplate("deviantion.htm", &s)
265271 } else {
@@ -268,25 +274,38 @@ func (s skunkyart) Deviation(author, postname string) {
268274}
269275
270276func (s skunkyart) DD() {
271 dd := devianter.DailyDeviationsFunc(s.Page)
272 s.Templates.SomeList = s.DeviationList(dd.Deviations, DeviationList{
277 dd := devianter.GetDailyDeviations(s.Page)
278 var strips strings.Builder
279 for _, x := range dd.Strips {
280 strips.WriteString(`<h3 class="`)
281 strips.WriteString(x.Codename)
282 strips.WriteString(`"> <a href="#`)
283 strips.WriteString(x.Codename)
284 strips.WriteString(`"># </a>`)
285 strips.WriteString(x.Title)
286 strips.WriteString(`</h3>`)
287
288 strips.WriteString(s.DeviationList(x.Deviations, false))
289 }
290 s.Templates.DDStrips = strips.String()
291 s.Templates.SomeList = s.DeviationList(dd.Deviations, true, DeviationList{
273292 Pages: 0,
274293 More: dd.HasMore,
275294 })
276295 if !s.Atom {
277 s.ExecuteTemplate("list.htm", &s)
296 s.ExecuteTemplate("daily.htm", &s)
278297 }
279298}
280299
281300func (s skunkyart) Search() {
282301 s.Atom = false
283 var e error
302 var err error
284303 ss := &s.Templates.Search
285304 switch s.Type {
286305 case 'a', 't':
287 ss.Content, e = devianter.SearchFunc(s.Query, s.Page, s.Type)
306 ss.Content, err = devianter.PerformSearch(s.Query, s.Page, s.Type)
288307 case 'g':
289 ss.Content, e = devianter.SearchFunc(s.Query, s.Page, s.Type, s.Args.Get("usr"))
308 ss.Content, err = devianter.PerformSearch(s.Query, s.Page, s.Type, s.Args.Get("usr"))
290309 case 'r': // скраппер, поскольку девиантартовцы зажопили гостевое API для поиска групп
291310 var (
292311 usernames = make(map[int]string)
@@ -333,10 +352,10 @@ func (s skunkyart) Search() {
333352 default:
334353 s.ReturnHTTPError(400)
335354 }
336 try(e)
355 try(err)
337356
338357 if s.Type != 'r' {
339 ss.List = s.DeviationList(ss.Content.Results, DeviationList{
358 ss.List = s.DeviationList(ss.Content.Results, false, DeviationList{
340359 Pages: ss.Content.Pages,
341360 More: ss.Content.HasMore,
342361 })
config.example.json +6 −4
@@ -4,15 +4,17 @@
44 "cache": {
55 "enabled": true,
66 "path": "/home/skunk/projects/skunkyart/cache",
7 "lifetime": null,
8 "max-size": 100000,
7 "lifetime": "1w",
8 "max-size": 1024,
99 "update-interval": 5
1010 },
1111 "dirs-to-memory": [
1212 "/home/skunk/projects/skunkyart/html",
13 "/home/skunk/projects/skunkyart/css"
13 "/home/skunk/projects/skunkyart/css",
14 "/home/skunk/projects/skunkyart/misc"
1415 ],
15 "download-proxy": null,
16 "download-proxy": "http://127.0.0.1:8080",
17 "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
1618 "proxy": true,
1719 "nsfw": false
1820}
css/skunky.css +43 −7
@@ -131,28 +131,64 @@ form input, button, select {
131131}
132132
133133/* SCREEN OPTIMISATIONS */
134@media screen and (orientation: portrait) {
135 header {
136 scale: 155%;
137 justify-content: center;
134@media (orientation: portrait) {
135 * {
136 font-size: 120%
138137 }
138
139 ul {
140 font-size: 80%
141 }
142
143 center form {
144 font-size: 60%
145 }
146
147 header form {
148 font-size: 60%;
149 }
150 header, center {
151 text-align: center;
152 display: block;
153 clear: both;
154 font-size: 200%;
155 }
156
139157 .content {
140158 margin: auto;
141159 display: inherit;
142160 scale: 100%;
143161 }
144162 .block {
145 max-width: 60%;
163 margin-top: 10%;
164 max-width: 200%;
165 }
166 .folder-item {
167 width: 25%
168 }
169 .folders {
170 display: flexbox;
171 justify-content: center
172 }
173 figure img {
174 width: 10%
175 }
176 figure a img {
177 width: 100%
178 }
179 .msg {
180 font-size: 60%;
181 max-width: 80%
146182 }
147183}
148184
149@media screen and (max-width: 1462px) {
185@media (max-width: 1462px) and (orientation: landscape) {
150186 .block {
151187 max-width: 30%;
152188 }
153189}
154190
155@media screen and (min-width: 788px) and (max-width: 884px) {
191@media (min-width: 788px) and (max-width: 884px) {
156192 .block {
157193 max-width: 35%;
158194 }
go.mod +2
@@ -2,6 +2,8 @@ module skunkyart
22
33go 1.22.3
44
5replace git.macaw.me/skunky/devianter v0.2.0 => /home/skunk/projects/devianter
6
57require (
68 git.macaw.me/skunky/devianter v0.2.0
79 golang.org/x/net v0.27.0
html/about.htm +1 −1
@@ -58,6 +58,6 @@
5858 </li>
5959 {{end}}
6060 </ul>
61 <p>Copyright <a href="https://go.kde.org/matrix/#/@softpigeones:ebloid.ru" target="_blank">lost+skunk</a>, X11. <a href="https://git.macaw.me/skunky/skunkyart/src/tag/v1.3" target="_blank">SkunkyArt v1.3</a></p>
61 <p>Copyright <a href="https://go.kde.org/matrix/#/@softpigeones:ebloid.ru" target="_blank">lost+skunk</a>, X11. <a href="https://git.macaw.me/skunky/skunkyart/src/tag/v1.3.1" target="_blank">SkunkyArt v1.3.1</a></p>
6262 </main>
6363</html>
\ No newline at end of file
html/list.htm → html/daily.htm renamed +6 −1
@@ -1,7 +1,7 @@
11<!DOCTYPE html>
22<html>
33 <head>
4 <title>SkunkyArt</title>
4 <title>SkunkyArt | Daily Deviations</title>
55 <link rel="stylesheet" href="{{.BasePath}}stylesheet">
66 </head>
77 <main>
@@ -17,6 +17,11 @@
1717 <button type="submit">Search!</button>
1818 </form>
1919 </header>
20 {{if ne .Templates.DDStrips ""}}
21 <h2 id="strips"><a href="#strips">#</a> Strips</h2>
22 {{.Templates.DDStrips}}
23 {{end}}
24 <h2 id="content"><a href="#content">#</a> Content</h2>
2025 {{.Templates.SomeList}}
2126 </main>
2227</html>
\ No newline at end of file
instances.json +11
@@ -44,6 +44,17 @@
4444 "nsfw": true,
4545 "proxy": true
4646 }
47 },
48 {
49 "title": "lumaeris.com",
50 "country": "US",
51 "urls": [{
52 "clearnet": "https://skunkyart.lumaeris.com"
53 }],
54 "settings": {
55 "nsfw": true,
56 "proxy": true
57 }
4758 }
4859 ]
4960}
\ No newline at end of file
misc/logo.png added

Binary file not shown.

services/skunkyart.example.openrc added +13
@@ -0,0 +1,13 @@
1#!/sbin/openrc-run
2
3directory=<path_to_dir_with_skunkyart>
4# command_args="-c $directory/config.json" # if SA wasn't start, try to uncomment this line
5command=$directory/skunkyart
6
7name="SkunkyArt"
8description="Privacy frontend for deviantart.com"
9supervisor=supervise-daemon
10
11depend() {
12 need net
13}
\ No newline at end of file
services/skunkyart.example.service added +11
@@ -0,0 +1,11 @@
1# Note: i didn't use systemd, so it can be not works :)
2
3[Unit]
4Description=Privacy frontend for deviantart.com
5
6[Service]
7Directory=<path-to-dir-with-skunkyart>
8ExecStart=<path-to-dir-skunkyart>
9
10[Install]
11WantedBy=multi-user.target
skunkyart.example.openrc deleted −11
@@ -1,11 +0,0 @@
1#!/sbin/openrc-run
2name="SkunkyArt"
3description="Privacy frontend for deviantart.com"
4supervisor=supervise-daemon
5command=<path_to_skunkyart>
6command_args="-c <path_to_config>"
7directory="<path_to_dir_with_skunkyart>"
8
9depend() {
10 need net
11}
\ No newline at end of file