Commit 2dfeaae772
Unsigned
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 | |||
| 1 | [](https://go.kde.org/matrix/#/#skunkyart:ebloid.ru) | 3 | [](https://go.kde.org/matrix/#/#skunkyart:ebloid.ru) |
| 2 | # Instances | 4 | # Instances |
| 3 | |Инстанс|Yggdrasil|I2P|Tor|NSFW|Proxifying|Country| | 5 | |Instance|Yggdrasil|I2P|Tor|NSFW|Proxifying|Country| |
| 4 | |:-----:|:-------:|:-:|:-:|:--:|:--------:|:-----:| | 6 | |:------:|:-------:|:-:|:-:|:--:|:--------:|:-----:| |
| 5 | |[skunky.ebloid.ru](https://skunky.ebloid.ru/art)|[Yes](http://[201:eba5:d1fc:bf7b:cfcb:a811:4b8b:7ea3]/art)|No|No| No | No | Russia | | 7 | |[skunky.ebloid.ru](https://skunky.ebloid.ru/art)|[Yes](http://[201:eba5:d1fc:bf7b:cfcb:a811:4b8b:7ea3]/art)|No|No| No | No | Russia | |
| 6 | |[clovius.club](https://skunky.clovius.club)|No|No|No| Yes | Yes | Sweden | | 8 | |[clovius.club](https://skunky.clovius.club)|No|No|No| Yes | Yes | Sweden | |
| 7 | |[bloat.cat](https://skunky.bloat.cat)|No|No|No| Yes | Yes | Romania | | 9 | |[bloat.cat](https://skunky.bloat.cat)|No|No|No| Yes | Yes | Romania | |
| 8 | |[frontendfriendly.xyz](https://skunkyart.frontendfriendly.xyz)|No|No|No| Yes | Yes | Finland | | 10 | |[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 | | ||
| 9 | 12 | ||
| 10 | # EN 🇺🇸 | 13 | # EN 🇺🇸 |
| 11 | ## Description | 14 | ## Description |
| @@ -13,12 +16,12 @@ SkunkyArt 🦨 -- alternative frontend to DeviantArt, which will work without pr | |||
| 13 | ## Config | 16 | ## Config |
| 14 | The sample config is in the `config.example.json` file. To specify your own path to the config, use the CLI argument `-c` or `--config`. | 17 | The sample config is in the `config.example.json` file. To specify your own path to the config, use the CLI argument `-c` or `--config`. |
| 15 | * `listen` -- the address and port on which SkunkyArt will listen | 18 | * `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/ |
| 17 | * `cache` -- caching system; default is off. | 20 | * `cache` -- caching system; default is off. |
| 18 | * * `path` -- the path to the cache | 21 | * `path` -- the path to the cache |
| 19 | * * `lifetime` -- cache file lifetime; measured in Unix milliseconds. | 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. |
| 20 | * * `max-size` -- maximum file size in bytes. | 23 | * `max-size` -- maximum file size in megabytes. |
| 21 | * `dirs-to-memory` -- this setting determines which directories will be copied to RAM when SkunkyArt is started. Required | 24 | * `dirs-to-memory` -- this setting determines which directories will be copied to RAM when SkunkyArt is started. Mandatory |
| 22 | * `download-proxy` -- proxy address for downloading files. | 25 | * `download-proxy` -- proxy address for downloading files. |
| 23 | ## Examples of reverse proxies | 26 | ## Examples of reverse proxies |
| 24 | Nginx: | 27 | Nginx: |
| @@ -27,17 +30,18 @@ server { | |||
| 27 | listen 443 ssl; | 30 | listen 443 ssl; |
| 28 | server_name skunky.example.com; | 31 | server_name skunky.example.com; |
| 29 | 32 | ||
| 30 | location ((BASE URL)) { # if you have a separate subdomain for the frontend, insert '/' instead of '((BASE URL))'. | 33 | location ((BASE URL)) { # if you have a separate subdomain for the frontend, insert '/' instead of '((BASE URL)))'. |
| 31 | proxy_set_header Scheme $scheme; | 34 | proxy_set_header X-Forwarded-Proto $scheme; |
| 32 | proxy_set_header Host $host; | 35 | proxy_set_header Host $host; |
| 33 | proxy_http_version 1.1; | 36 | proxy_http_version 1.1; |
| 34 | proxy_pass http://((IP)):((PORT)); | 37 | proxy_pass http://((IP)):((PORT)); |
| 35 | } | 38 | } |
| 36 | } | 39 | } |
| 37 | ``` | 40 | ``` |
| 41 | Pretty 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. | ||
| 38 | ## How do I add my instance to the list? | 42 | ## How do I add my instance to the list? |
| 39 | To 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: | 43 | To 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: |
| 40 | 1. the instance must not use Cloudflare. | 44 | 1. the Instance must not use Cloudflare. |
| 41 | 2. If your instance has modified source code, you need to publish it to any free platform. For example, Github and Gitlab are not. | 45 | 2. If your instance has modified source code, you need to publish it to any free platform. For example, Github and Gitlab are not. |
| 42 | ## Acknowledgements | 46 | ## Acknowledgements |
| 43 | * [Лис⚛](https://go.kde.org/matrix/#/@fox:matrix.org) -- helped me understand Go and gave me a lot of useful advice on this language. | 47 | * [Лис⚛](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, | |||
| 48 | ## Конфиг | 52 | ## Конфиг |
| 49 | Пример конфига находится в файле `config.example.json`. Чтобы указать свой путь до конфига, используйте CLI-аргумент `-c` или `--config`. | 53 | Пример конфига находится в файле `config.example.json`. Чтобы указать свой путь до конфига, используйте CLI-аргумент `-c` или `--config`. |
| 50 | * `listen` -- адрес и порт, на котором будет слушать SkunkyArt | 54 | * `listen` -- адрес и порт, на котором будет слушать SkunkyArt |
| 51 | * `base-path` -- путь к инстансу. Пример: "base-path": "/art/" -> https://skunky.ebloid.ru/art/ | 55 | * `base-path` -- путь к инстансу. Пример: `"base-path": "/art/"` -> https://skunky.ebloid.ru/art/ |
| 52 | * `cache` -- система кеширования; по умолчанию - выкл. | 56 | * `cache` -- система кеширования; по умолчанию - выкл. |
| 53 | * * `path` -- путь до кеша | 57 | * `path` -- путь до кеша |
| 54 | * * `lifetime` -- время жизни файла в кеше; измеряется в Unix-миллисекундах | 58 | * `lifetime` -- время жизни файла в кеше. Единицы измерения: i, h, d, w, m, y. I -- минута, всё остальные единицы измерения, я считаю понятными и без объяснения. |
| 55 | * * `max-size` -- максимальный размер файла в байтах | 59 | * `max-size` -- максимальный размер файла в мегабайтах |
| 56 | * `dirs-to-memory` -- данная настройка определяет какие каталоги будут скопированы в ОЗУ при запуске SkunkyArt. Обязательна | 60 | * `dirs-to-memory` -- данная настройка определяет какие каталоги будут скопированы в ОЗУ при запуске SkunkyArt. Обязательна |
| 57 | * `download-proxy` -- адрес прокси для загрузки файлов | 61 | * `download-proxy` -- адрес прокси для загрузки файлов |
| 58 | ## Примеры reverse-прокси | 62 | ## Примеры reverse-прокси |
| @@ -63,15 +67,16 @@ server { | |||
| 63 | server_name skunky.example.com; | 67 | server_name skunky.example.com; |
| 64 | 68 | ||
| 65 | location ((BASE URL)) { # если у вас отдельный поддомен для фронтенда, вместо '((BASE URL))' вставляйте '/' | 69 | location ((BASE URL)) { # если у вас отдельный поддомен для фронтенда, вместо '((BASE URL))' вставляйте '/' |
| 66 | proxy_set_header Scheme $scheme; | 70 | proxy_set_header X-Forwarded-Proto $scheme; |
| 67 | proxy_set_header Host $host; | 71 | proxy_set_header Host $host; |
| 68 | proxy_http_version 1.1; | 72 | proxy_http_version 1.1; |
| 69 | proxy_pass http://((IP)):((PORT)); | 73 | proxy_pass http://((IP)):((PORT)); |
| 70 | } | 74 | } |
| 71 | } | 75 | } |
| 72 | ``` | 76 | ``` |
| 77 | В целом, всё как обычно, за исключением настройки заголовка [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto). | ||
| 73 | ## Как добавить свой инстанс в список? | 78 | ## Как добавить свой инстанс в список? |
| 74 | Чтобы это сделать, вы должны либо сделать PR, добавив в файл `instances.json` свой инстанс, либо сообщить о нём в комнате в Matrix. Думаю, он не нуждается в описании. Однако учтите, у этого списка есть пара правил: | 79 | Чтобы это сделать, вы должны либо сделать PR, добавив в файлы `instances.json` и `README.md` свой инстанс, либо создать Issue, или сообщить о нём в комнате в Matrix. Думаю, он не нуждается в описании. Однако учтите, у этого списка есть пара правил: |
| 75 | 1. Инстанс не должен использовать Cloudflare. | 80 | 1. Инстанс не должен использовать Cloudflare. |
| 76 | 2. Если ваш инстанс имеет модифицированный исходный код, то вам нужно опубликовать его на любую свободную площадку. Например, Github и Gitlab таковыми не являются. | 81 | 2. Если ваш инстанс имеет модифицированный исходный код, то вам нужно опубликовать его на любую свободную площадку. Например, Github и Gitlab таковыми не являются. |
| 77 | ## Благодарности | 82 | ## Благодарности |
TODO.md +13 −4
| @@ -1,7 +1,16 @@ | |||
| 1 | # v1.3.x | 1 | # v1.3.x |
| 2 | * Доделать парсинг описания | 2 | * Написать Makefile |
| 3 | * Реализовать миниатюры и оптимизировать CSS под маленькие экраны | 3 | * Почистить говнокод |
| 4 | * **Доделать парсинг описания** | ||
| 5 | * Избавиться от хардкода под Linux | ||
| 6 | * ~~Реализовать стрипы в ежедневных артах~~ | ||
| 7 | * ~~Исправить баг с навигацией по страницам~~ | ||
| 8 | * ~~Сделать единицы в конфиге более понятными~~ | ||
| 9 | * Добавить возможность включить темплейты в бинарник | ||
| 10 | * ~~Реализовать миниатюры и оптимизировать CSS под маленькие экраны~~ | ||
| 11 | * **Реализовать отображение контента, отличного от картинок (видео, аудио, etc)** | ||
| 12 | * Улучшить систему кеширования: добавить рейтинг для удаления и копирование изображений в ОЗУ | ||
| 4 | # v1.4 | 13 | # v1.4 |
| 14 | * Реализовать API | ||
| 5 | * Реализовать темы | 15 | * Реализовать темы |
| 6 | * Реализовать многоязычный интерфейс | 16 | * Реализовать многоязычный интерфейс\ No newline at end of file |
| 7 | * Реализовать API\ No newline at end of file | ||
app/config.go +53 −16
| @@ -3,14 +3,18 @@ package app | |||
| 3 | import ( | 3 | import ( |
| 4 | "encoding/json" | 4 | "encoding/json" |
| 5 | "os" | 5 | "os" |
| 6 | "regexp" | ||
| 7 | "strconv" | ||
| 6 | "time" | 8 | "time" |
| 9 | |||
| 10 | "git.macaw.me/skunky/devianter" | ||
| 7 | ) | 11 | ) |
| 8 | 12 | ||
| 9 | type cache_config struct { | 13 | type cache_config struct { |
| 10 | Enabled bool | 14 | Enabled bool |
| 11 | Path string | 15 | Path string |
| 12 | MaxSize int64 `json:"max-size"` | 16 | MaxSize int64 `json:"max-size"` |
| 13 | Lifetime int64 | 17 | Lifetime string |
| 14 | UpdateInterval int64 `json:"update-interval"` | 18 | UpdateInterval int64 `json:"update-interval"` |
| 15 | } | 19 | } |
| 16 | 20 | ||
| @@ -20,6 +24,7 @@ type config struct { | |||
| 20 | BasePath string `json:"base-path"` | 24 | BasePath string `json:"base-path"` |
| 21 | Cache cache_config | 25 | Cache cache_config |
| 22 | Proxy, Nsfw bool | 26 | Proxy, Nsfw bool |
| 27 | UserAgent string `json:"user-agent"` | ||
| 23 | DownloadProxy string `json:"download-proxy"` | 28 | DownloadProxy string `json:"download-proxy"` |
| 24 | Dirs []string `json:"dirs-to-memory"` | 29 | Dirs []string `json:"dirs-to-memory"` |
| 25 | } | 30 | } |
| @@ -33,31 +38,36 @@ var CFG = config{ | |||
| 33 | Path: "cache", | 38 | Path: "cache", |
| 34 | UpdateInterval: 1, | 39 | UpdateInterval: 1, |
| 35 | }, | 40 | }, |
| 36 | Dirs: []string{"html", "css"}, | 41 | Dirs: []string{"html", "css"}, |
| 37 | Proxy: true, | 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", |
| 38 | Nsfw: true, | 43 | Proxy: true, |
| 44 | Nsfw: true, | ||
| 39 | } | 45 | } |
| 40 | 46 | ||
| 47 | var lifetimeParsed int64 | ||
| 48 | |||
| 41 | func ExecuteConfig() { | 49 | func ExecuteConfig() { |
| 42 | go func() { | 50 | go func() { |
| 43 | defer func() { | ||
| 44 | if r := recover(); r != nil { | ||
| 45 | recover() | ||
| 46 | } | ||
| 47 | }() | ||
| 48 | for { | 51 | for { |
| 49 | Templates["instances.json"] = string(Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body) | 52 | func() { |
| 50 | time.Sleep(1 * time.Second) | 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) | ||
| 51 | } | 61 | } |
| 52 | }() | 62 | }() |
| 53 | 63 | ||
| 54 | const helpmsg = `SkunkyArt v1.3 [refactoring] | 64 | const helpmsg = `SkunkyArt v1.3.1 [CSS improvements for mobile and strips on Daily Deviations] |
| 55 | Usage: | 65 | Usage: |
| 56 | - [-c|--config] - path to config | 66 | - [-c|--config] - path to config |
| 57 | - [-h|--help] - returns this message | 67 | - [-h|--help] - returns this message |
| 58 | Example: | 68 | Example: |
| 59 | ./skunkyart -c config.json | 69 | ./skunkyart -c config.json |
| 60 | Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3` | 70 | Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3.1` |
| 61 | 71 | ||
| 62 | a := os.Args | 72 | a := os.Args |
| 63 | for n, x := range a { | 73 | for n, x := range a { |
| @@ -75,15 +85,42 @@ Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3` | |||
| 75 | 85 | ||
| 76 | if CFG.cfg != "" { | 86 | if CFG.cfg != "" { |
| 77 | f, err := os.ReadFile(CFG.cfg) | 87 | f, err := os.ReadFile(CFG.cfg) |
| 78 | try_with_exitstatus(err, 1) | 88 | tryWithExitStatus(err, 1) |
| 79 | 89 | ||
| 80 | try_with_exitstatus(json.Unmarshal(f, &CFG), 1) | 90 | tryWithExitStatus(json.Unmarshal(f, &CFG), 1) |
| 81 | if CFG.Cache.Enabled && !CFG.Proxy { | 91 | if CFG.Cache.Enabled && !CFG.Proxy { |
| 82 | exit("Incompatible settings detected: cannot use caching media content without proxy", 1) | 92 | exit("Incompatible settings detected: cannot use caching media content without proxy", 1) |
| 83 | } | 93 | } |
| 84 | 94 | ||
| 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 | ||
| 86 | go InitCacheSystem() | 122 | go InitCacheSystem() |
| 87 | } | 123 | } |
| 124 | devianter.UserAgent = CFG.UserAgent | ||
| 88 | } | 125 | } |
| 89 | } | 126 | } |
app/parsers.go +110 −103
| @@ -70,95 +70,100 @@ func (s skunkyart) ParseComments(c devianter.Comments) string { | |||
| 70 | return cmmts.String() | 70 | return cmmts.String() |
| 71 | } | 71 | } |
| 72 | 72 | ||
| 73 | func (s skunkyart) DeviationList(devs []devianter.Deviation, content ...DeviationList) string { | 73 | func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, content ...DeviationList) string { |
| 74 | var list strings.Builder | ||
| 75 | if s.Atom && s.Page > 1 { | 74 | if s.Atom && s.Page > 1 { |
| 76 | s.ReturnHTTPError(400) | 75 | s.ReturnHTTPError(400) |
| 77 | return "" | 76 | 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">`) | ||
| 95 | } | 77 | } |
| 96 | for _, data := range devs { | 78 | |
| 97 | if !(data.NSFW && !CFG.Nsfw) { | 79 | var list, listContent strings.Builder |
| 98 | url := ParseMedia(data.Media) | 80 | |
| 99 | if s.Atom { | 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 { | ||
| 100 | id := strconv.Itoa(data.ID) | 85 | id := strconv.Itoa(data.ID) |
| 101 | list.WriteString(`<entry><author><name>`) | 86 | listContent.WriteString(`<entry><author><name>`) |
| 102 | list.WriteString(data.Author.Username) | 87 | listContent.WriteString(data.Author.Username) |
| 103 | list.WriteString(`</name></author><title>`) | 88 | listContent.WriteString(`</name></author><title>`) |
| 104 | list.WriteString(data.Title) | 89 | listContent.WriteString(data.Title) |
| 105 | list.WriteString(`</title><link rel="alternate" type="text/html" href="`) | 90 | listContent.WriteString(`</title><link rel="alternate" type="text/html" href="`) |
| 106 | list.WriteString(UrlBuilder("post", data.Author.Username, "atom-"+id)) | 91 | listContent.WriteString(UrlBuilder("post", data.Author.Username, "atom-"+id)) |
| 107 | list.WriteString(`"/><id>`) | 92 | listContent.WriteString(`"/><id>`) |
| 108 | list.WriteString(id) | 93 | listContent.WriteString(id) |
| 109 | list.WriteString(`</id><published>`) | 94 | listContent.WriteString(`</id><published>`) |
| 110 | list.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700")) | 95 | listContent.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700")) |
| 111 | list.WriteString(`</published>`) | 96 | listContent.WriteString(`</published>`) |
| 112 | list.WriteString(`<media:group><media:title>`) | 97 | listContent.WriteString(`<media:group><media:title>`) |
| 113 | list.WriteString(data.Title) | 98 | listContent.WriteString(data.Title) |
| 114 | list.WriteString(`</media:title><media:thumbinal url="`) | 99 | listContent.WriteString(`</media:title><media:thumbinal url="`) |
| 115 | list.WriteString(url) | 100 | listContent.WriteString(preview) |
| 116 | list.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`) | 101 | listContent.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`) |
| 117 | list.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url)) | 102 | listContent.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url)) |
| 118 | list.WriteString(`"><img src="`) | 103 | listContent.WriteString(`"><img src="`) |
| 119 | list.WriteString(url) | 104 | listContent.WriteString(fullview) |
| 120 | list.WriteString(`"/></a><p>`) | 105 | listContent.WriteString(`"/></a><p>`) |
| 121 | list.WriteString(ParseDescription(data.TextContent)) | 106 | listContent.WriteString(ParseDescription(data.TextContent)) |
| 122 | list.WriteString(`</p></div></content></entry>`) | 107 | listContent.WriteString(`</p></div></content></entry>`) |
| 123 | } else { | 108 | } else { |
| 124 | list.WriteString(`<div class="block">`) | 109 | listContent.WriteString(`<div class="block">`) |
| 125 | if url != "" { | 110 | if fullview != "" && preview != "" { |
| 126 | list.WriteString(`<a title="open/download" href="`) | 111 | listContent.WriteString(`<a title="open/download" href="`) |
| 127 | list.WriteString(url) | 112 | listContent.WriteString(fullview) |
| 128 | list.WriteString(`"><img loading="lazy" src="`) | 113 | listContent.WriteString(`"><img loading="lazy" src="`) |
| 129 | list.WriteString(url) | 114 | listContent.WriteString(preview) |
| 130 | list.WriteString(`" width="15%"></a>`) | 115 | listContent.WriteString(`" width="15%"></a>`) |
| 131 | } else { | 116 | } else { |
| 132 | list.WriteString(`<h1>[ TEXT ]</h1>`) | 117 | listContent.WriteString(`<h1>[ TEXT ]</h1>`) |
| 133 | } | 118 | } |
| 134 | list.WriteString(`<br><a href="`) | 119 | listContent.WriteString(`<br><a href="`) |
| 135 | list.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url)) | 120 | listContent.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url)) |
| 136 | list.WriteString(`">`) | 121 | listContent.WriteString(`">`) |
| 137 | list.WriteString(data.Author.Username) | 122 | listContent.WriteString(data.Author.Username) |
| 138 | list.WriteString(" - ") | 123 | listContent.WriteString(" - ") |
| 139 | list.WriteString(data.Title) | 124 | listContent.WriteString(data.Title) |
| 140 | 125 | ||
| 141 | // шильдики нсфв, аи и ежедневного поста | ||
| 142 | if data.NSFW { | 126 | if data.NSFW { |
| 143 | list.WriteString(` [<span class="nsfw">NSFW</span>]`) | 127 | listContent.WriteString(` [<span class="nsfw">NSFW</span>]`) |
| 144 | } | 128 | } |
| 145 | if data.AI { | 129 | if data.AI { |
| 146 | list.WriteString(" [🤖]") | 130 | listContent.WriteString(" [🤖]") |
| 147 | } | 131 | } |
| 148 | if data.DD { | 132 | if data.DD { |
| 149 | list.WriteString(` [<span class="dd">DD</span>]`) | 133 | listContent.WriteString(` [<span class="dd">DD</span>]`) |
| 150 | } | 134 | } |
| 151 | 135 | ||
| 152 | list.WriteString("</a></div>") | 136 | listContent.WriteString("</a></div>") |
| 153 | } | 137 | } |
| 154 | } | 138 | } |
| 155 | } | 139 | } |
| 156 | 140 | ||
| 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 | |||
| 158 | list.WriteString("</feed>") | 160 | list.WriteString("</feed>") |
| 159 | s.Writer.Write([]byte(list.String())) | 161 | wr(s.Writer, list.String()) |
| 160 | return "" | ||
| 161 | } else { | 162 | } else { |
| 163 | list.WriteString(`<div class="content">`) | ||
| 164 | |||
| 165 | list.WriteString(listContent.String()) | ||
| 166 | |||
| 162 | list.WriteString("</div>") | 167 | list.WriteString("</div>") |
| 163 | if content != nil { | 168 | if content != nil { |
| 164 | list.WriteString(s.NavBase(content[0])) | 169 | list.WriteString(s.NavBase(content[0])) |
| @@ -177,7 +182,7 @@ type text struct { | |||
| 177 | } | 182 | } |
| 178 | 183 | ||
| 179 | func ParseDescription(dscr devianter.Text) string { | 184 | func ParseDescription(dscr devianter.Text) string { |
| 180 | var parseddescription strings.Builder | 185 | var parsedDescription strings.Builder |
| 181 | TagBuilder := func(content string, tags ...string) string { | 186 | TagBuilder := func(content string, tags ...string) string { |
| 182 | l := len(tags) | 187 | l := len(tags) |
| 183 | for x := 0; x < l; x++ { | 188 | for x := 0; x < l; x++ { |
| @@ -274,16 +279,18 @@ func ParseDescription(dscr devianter.Text) string { | |||
| 274 | 279 | ||
| 275 | switch x.Type { | 280 | switch x.Type { |
| 276 | case "atomic": | 281 | case "atomic": |
| 277 | d := entities[x.EntityRanges[0].Key] | 282 | if len(x.EntityRanges) != 0 { |
| 278 | parseddescription.WriteString(`<a href="`) | 283 | d := entities[x.EntityRanges[0].Key] |
| 279 | parseddescription.WriteString(ConvertDeviantArtUrlToSkunkyArt(d.Url)) | 284 | parsedDescription.WriteString(`<a href="`) |
| 280 | parseddescription.WriteString(`"><img width="50%" src="`) | 285 | parsedDescription.WriteString(ConvertDeviantArtUrlToSkunkyArt(d.Url)) |
| 281 | parseddescription.WriteString(ParseMedia(d.Media)) | 286 | parsedDescription.WriteString(`"><img width="50%" src="`) |
| 282 | parseddescription.WriteString(`" title="`) | 287 | parsedDescription.WriteString(ParseMedia(d.Media)) |
| 283 | parseddescription.WriteString(d.Author.Username) | 288 | parsedDescription.WriteString(`" title="`) |
| 284 | parseddescription.WriteString(" - ") | 289 | parsedDescription.WriteString(d.Author.Username) |
| 285 | parseddescription.WriteString(d.Title) | 290 | parsedDescription.WriteString(" - ") |
| 286 | parseddescription.WriteString(`"></a>`) | 291 | parsedDescription.WriteString(d.Title) |
| 292 | parsedDescription.WriteString(`"></a>`) | ||
| 293 | } | ||
| 287 | case "unstyled": | 294 | case "unstyled": |
| 288 | if l := len(Styles); l != 0 { | 295 | if l := len(Styles); l != 0 { |
| 289 | for n, r := range Styles { | 296 | for n, r := range Styles { |
| @@ -292,31 +299,31 @@ func ParseDescription(dscr devianter.Text) string { | |||
| 292 | tag = "h2" | 299 | tag = "h2" |
| 293 | } | 300 | } |
| 294 | 301 | ||
| 295 | parseddescription.WriteString(x.Text[:r.From]) | 302 | parsedDescription.WriteString(x.Text[:r.From]) |
| 296 | if len(urls) != 0 && len(x.EntityRanges) != 0 { | 303 | if len(urls) != 0 && len(x.EntityRanges) != 0 { |
| 297 | ra := &x.EntityRanges[0] | 304 | ra := &x.EntityRanges[0] |
| 298 | 305 | ||
| 299 | parseddescription.WriteString(`<a target="_blank" href="`) | 306 | parsedDescription.WriteString(`<a target="_blank" href="`) |
| 300 | parseddescription.WriteString(urls[ra.Key]) | 307 | parsedDescription.WriteString(urls[ra.Key]) |
| 301 | parseddescription.WriteString(`">`) | 308 | parsedDescription.WriteString(`">`) |
| 302 | parseddescription.WriteString(r.TXT) | 309 | parsedDescription.WriteString(r.TXT) |
| 303 | parseddescription.WriteString(`</a>`) | 310 | parsedDescription.WriteString(`</a>`) |
| 304 | } else if l > n+1 { | 311 | } else if l > n+1 { |
| 305 | parseddescription.WriteString(r.TXT) | 312 | parsedDescription.WriteString(r.TXT) |
| 306 | } | 313 | } |
| 307 | parseddescription.WriteString(TagBuilder(tag, x.Text[r.To:])) | 314 | parsedDescription.WriteString(TagBuilder(tag, x.Text[r.To:])) |
| 308 | } | 315 | } |
| 309 | } else { | 316 | } else { |
| 310 | parseddescription.WriteString(x.Text) | 317 | parsedDescription.WriteString(x.Text) |
| 311 | } | 318 | } |
| 312 | } | 319 | } |
| 313 | parseddescription.WriteString("<br>") | 320 | parsedDescription.WriteString("<br>") |
| 314 | } | 321 | } |
| 315 | } else if dl != 0 { | 322 | } else if dl != 0 { |
| 316 | for tt := html.NewTokenizer(strings.NewReader(dscr.Html.Markup)); ; { | 323 | for tt := html.NewTokenizer(strings.NewReader(dscr.Html.Markup)); ; { |
| 317 | switch tt.Next() { | 324 | switch tt.Next() { |
| 318 | case html.ErrorToken: | 325 | case html.ErrorToken: |
| 319 | return parseddescription.String() | 326 | return parsedDescription.String() |
| 320 | case html.StartTagToken, html.EndTagToken, html.SelfClosingTagToken: | 327 | case html.StartTagToken, html.EndTagToken, html.SelfClosingTagToken: |
| 321 | token := tt.Token() | 328 | token := tt.Token() |
| 322 | switch token.Data { | 329 | switch token.Data { |
| @@ -324,11 +331,11 @@ func ParseDescription(dscr devianter.Text) string { | |||
| 324 | for _, a := range token.Attr { | 331 | for _, a := range token.Attr { |
| 325 | if a.Key == "href" { | 332 | if a.Key == "href" { |
| 326 | url := DeleteTrackingFromUrl(a.Val) | 333 | url := DeleteTrackingFromUrl(a.Val) |
| 327 | parseddescription.WriteString(`<a target="_blank" href="`) | 334 | parsedDescription.WriteString(`<a target="_blank" href="`) |
| 328 | parseddescription.WriteString(url) | 335 | parsedDescription.WriteString(url) |
| 329 | parseddescription.WriteString(`">`) | 336 | parsedDescription.WriteString(`">`) |
| 330 | parseddescription.WriteString(GetValueOfTag(tt)) | 337 | parsedDescription.WriteString(GetValueOfTag(tt)) |
| 331 | parseddescription.WriteString("</a> ") | 338 | parsedDescription.WriteString("</a> ") |
| 332 | } | 339 | } |
| 333 | } | 340 | } |
| 334 | case "img": | 341 | case "img": |
| @@ -344,24 +351,24 @@ func ParseDescription(dscr devianter.Text) string { | |||
| 344 | } | 351 | } |
| 345 | if title != "" { | 352 | if title != "" { |
| 346 | for x := -1; x < b; x++ { | 353 | for x := -1; x < b; x++ { |
| 347 | parseddescription.WriteString(`<img src="`) | 354 | parsedDescription.WriteString(`<img src="`) |
| 348 | parseddescription.WriteString(uri) | 355 | parsedDescription.WriteString(uri) |
| 349 | parseddescription.WriteString(`" title="`) | 356 | parsedDescription.WriteString(`" title="`) |
| 350 | parseddescription.WriteString(title) | 357 | parsedDescription.WriteString(title) |
| 351 | parseddescription.WriteString(`">`) | 358 | parsedDescription.WriteString(`">`) |
| 352 | } | 359 | } |
| 353 | } | 360 | } |
| 354 | } | 361 | } |
| 355 | case "br", "li", "ul", "p", "b": | 362 | case "br", "li", "ul", "p", "b": |
| 356 | parseddescription.WriteString(token.String()) | 363 | parsedDescription.WriteString(token.String()) |
| 357 | case "div": | 364 | case "div": |
| 358 | parseddescription.WriteString("<p> ") | 365 | parsedDescription.WriteString("<p> ") |
| 359 | } | 366 | } |
| 360 | case html.TextToken: | 367 | case html.TextToken: |
| 361 | parseddescription.Write(tt.Text()) | 368 | parsedDescription.Write(tt.Text()) |
| 362 | } | 369 | } |
| 363 | } | 370 | } |
| 364 | } | 371 | } |
| 365 | 372 | ||
| 366 | return parseddescription.String() | 373 | return parsedDescription.String() |
| 367 | } | 374 | } |
app/router.go +7 −4
| @@ -1,7 +1,6 @@ | |||
| 1 | package app | 1 | package app |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "io" | ||
| 5 | "net/http" | 4 | "net/http" |
| 6 | u "net/url" | 5 | u "net/url" |
| 7 | "strconv" | 6 | "strconv" |
| @@ -44,7 +43,7 @@ func Router() { | |||
| 44 | 43 | ||
| 45 | // функция, что управляет всем | 44 | // функция, что управляет всем |
| 46 | handle := func(w http.ResponseWriter, r *http.Request) { | 45 | 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" { |
| 48 | Host = h[0] + "://" + r.Host | 47 | Host = h[0] + "://" + r.Host |
| 49 | } else { | 48 | } else { |
| 50 | Host = "http://" + r.Host | 49 | Host = "http://" + r.Host |
| @@ -97,10 +96,14 @@ func Router() { | |||
| 97 | skunky.About() | 96 | skunky.About() |
| 98 | case "stylesheet": | 97 | case "stylesheet": |
| 99 | w.Header().Add("content-type", "text/css") | 98 | 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"]) | ||
| 101 | } | 102 | } |
| 102 | } | 103 | } |
| 103 | 104 | ||
| 104 | http.HandleFunc("/", handle) | 105 | 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) | ||
| 106 | } | 109 | } |
app/util.go +22 −32
| @@ -26,7 +26,7 @@ func try(e error) { | |||
| 26 | println(e.Error()) | 26 | println(e.Error()) |
| 27 | } | 27 | } |
| 28 | } | 28 | } |
| 29 | func try_with_exitstatus(err error, code int) { | 29 | func tryWithExitStatus(err error, code int) { |
| 30 | if err != nil { | 30 | if err != nil { |
| 31 | exit(err.Error(), code) | 31 | exit(err.Error(), code) |
| 32 | } | 32 | } |
| @@ -87,7 +87,7 @@ func Download(url string) (d Downloaded) { | |||
| 87 | 87 | ||
| 88 | req, e := http.NewRequest("GET", url, nil) | 88 | req, e := http.NewRequest("GET", url, nil) |
| 89 | try(e) | 89 | 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) |
| 91 | 91 | ||
| 92 | resp, e := cli.Do(req) | 92 | resp, e := cli.Do(req) |
| 93 | try(e) | 93 | try(e) |
| @@ -148,14 +148,14 @@ func InitCacheSystem() { | |||
| 148 | try(e) | 148 | try(e) |
| 149 | for _, a := range dirnames { | 149 | for _, a := range dirnames { |
| 150 | a = c.Path + "/" + a | 150 | a = c.Path + "/" + a |
| 151 | if c.Lifetime != 0 { | 151 | if c.Lifetime != "" { |
| 152 | now := time.Now().UnixMilli() | 152 | now := time.Now().UnixMilli() |
| 153 | 153 | ||
| 154 | f, _ := os.Stat(a) | 154 | f, _ := os.Stat(a) |
| 155 | stat := f.Sys().(*syscall.Stat_t) | 155 | stat := f.Sys().(*syscall.Stat_t) |
| 156 | time := time.Unix(stat.Ctim.Unix()).UnixMilli() | 156 | time := time.Unix(stat.Ctim.Unix()).UnixMilli() |
| 157 | 157 | ||
| 158 | if time+c.Lifetime <= now { | 158 | if time+lifetimeParsed <= now { |
| 159 | try(os.RemoveAll(a)) | 159 | try(os.RemoveAll(a)) |
| 160 | } | 160 | } |
| 161 | } | 161 | } |
| @@ -172,19 +172,19 @@ func InitCacheSystem() { | |||
| 172 | func CopyTemplatesToMemory() { | 172 | func CopyTemplatesToMemory() { |
| 173 | for _, dirname := range CFG.Dirs { | 173 | for _, dirname := range CFG.Dirs { |
| 174 | dir, e := os.ReadDir(dirname) | 174 | dir, e := os.ReadDir(dirname) |
| 175 | try_with_exitstatus(e, 1) | 175 | tryWithExitStatus(e, 1) |
| 176 | 176 | ||
| 177 | for _, x := range dir { | 177 | for _, x := range dir { |
| 178 | file, e := os.ReadFile(dirname + "/" + x.Name()) | 178 | file, e := os.ReadFile(dirname + "/" + x.Name()) |
| 179 | try_with_exitstatus(e, 1) | 179 | tryWithExitStatus(e, 1) |
| 180 | Templates[x.Name()] = string(file) | 180 | Templates[x.Name()] = string(file) |
| 181 | } | 181 | } |
| 182 | } | 182 | } |
| 183 | } | 183 | } |
| 184 | 184 | ||
| 185 | /* PARSING HELPERS */ | 185 | /* PARSING HELPERS */ |
| 186 | func ParseMedia(media devianter.Media) string { | 186 | func ParseMedia(media devianter.Media, thumb ...int) string { |
| 187 | url := devianter.UrlFromMedia(media) | 187 | url := devianter.UrlFromMedia(media, thumb...) |
| 188 | if len(url) != 0 && CFG.Proxy { | 188 | if len(url) != 0 && CFG.Proxy { |
| 189 | url = url[21:] | 189 | url = url[21:] |
| 190 | dot := strings.Index(url, ".") | 190 | dot := strings.Index(url, ".") |
| @@ -197,9 +197,10 @@ func ParseMedia(media devianter.Media) string { | |||
| 197 | func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) { | 197 | func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) { |
| 198 | if len(url) > 32 && url[27:32] != "stash" { | 198 | if len(url) > 32 && url[27:32] != "stash" { |
| 199 | url = url[27:] | 199 | url = url[27:] |
| 200 | toart := strings.Index(url, "/art/") | 200 | firstshash := strings.Index(url, "/") |
| 201 | if toart != -1 { | 201 | lastshash := firstshash + strings.Index(url[firstshash+1:], "/") |
| 202 | output = UrlBuilder("post", url[:toart], url[toart+5:]) | 202 | if lastshash != -1 { |
| 203 | output = UrlBuilder("post", url[:firstshash], url[lastshash+2:]) | ||
| 203 | } | 204 | } |
| 204 | } | 205 | } |
| 205 | return | 206 | return |
| @@ -236,13 +237,9 @@ type DeviationList struct { | |||
| 236 | 237 | ||
| 237 | // FIXME: на некоротрых артах первая страница может вызывать полное отсутствие панели навигации. | 238 | // FIXME: на некоротрых артах первая страница может вызывать полное отсутствие панели навигации. |
| 238 | func (s skunkyart) NavBase(c DeviationList) string { | 239 | func (s skunkyart) NavBase(c DeviationList) string { |
| 239 | // TODO: сделать понятнее | ||
| 240 | // навигация по страницам | ||
| 241 | var list strings.Builder | 240 | var list strings.Builder |
| 242 | list.WriteString("<br>") | ||
| 243 | p := s.Page | ||
| 244 | 241 | ||
| 245 | // функция для генерации ссылок | 242 | list.WriteString("<br>") |
| 246 | prevrev := func(msg string, page int, onpage bool) { | 243 | prevrev := func(msg string, page int, onpage bool) { |
| 247 | if !onpage { | 244 | if !onpage { |
| 248 | list.WriteString(`<a href="?p=`) | 245 | list.WriteString(`<a href="?p=`) |
| @@ -268,33 +265,26 @@ func (s skunkyart) NavBase(c DeviationList) string { | |||
| 268 | } | 265 | } |
| 269 | } | 266 | } |
| 270 | 267 | ||
| 271 | // вперёд-назад | 268 | p := s.Page |
| 269 | |||
| 272 | if p > 1 { | 270 | if p > 1 { |
| 273 | prevrev("<= Prev |", p-1, false) | 271 | prevrev("<= Prev |", p-1, false) |
| 274 | } else { | 272 | } else { |
| 275 | p = 1 | 273 | p = 1 |
| 276 | } | 274 | } |
| 277 | 275 | ||
| 278 | if c.Pages > 0 { | 276 | for i, x := p-6, 0; (i <= c.Pages && i <= p+6) && x < 12; i++ { |
| 279 | // назад | 277 | if i > 0 { |
| 280 | for x := p - 6; x < p && x > 0; x++ { | 278 | var onPage bool |
| 281 | prevrev(strconv.Itoa(x), x, false) | 279 | if i == p { |
| 282 | } | 280 | onPage = true |
| 283 | |||
| 284 | // вперёд | ||
| 285 | for x := p; x <= p+6 && c.Pages > p+6; x++ { | ||
| 286 | if x == p { | ||
| 287 | prevrev("", x, true) | ||
| 288 | x++ | ||
| 289 | } | 281 | } |
| 290 | 282 | ||
| 291 | if x > p { | 283 | prevrev(strconv.Itoa(i), i, onPage) |
| 292 | prevrev(strconv.Itoa(x), x, false) | 284 | x++ |
| 293 | } | ||
| 294 | } | 285 | } |
| 295 | } | 286 | } |
| 296 | 287 | ||
| 297 | // вперёд-назад | ||
| 298 | if c.More { | 288 | if c.More { |
| 299 | prevrev("| Next =>", p+1, false) | 289 | prevrev("| Next =>", p+1, false) |
| 300 | } | 290 | } |
app/wrapper.go +35 −16
| @@ -48,6 +48,7 @@ type skunkyart struct { | |||
| 48 | } | 48 | } |
| 49 | 49 | ||
| 50 | SomeList string | 50 | SomeList string |
| 51 | DDStrips string | ||
| 51 | Deviation struct { | 52 | Deviation struct { |
| 52 | Post devianter.Post | 53 | Post devianter.Post |
| 53 | Related string | 54 | Related string |
| @@ -93,7 +94,10 @@ func (s skunkyart) GRUser() { | |||
| 93 | 94 | ||
| 94 | var g devianter.Group | 95 | var g devianter.Group |
| 95 | g.Name = s.Query | 96 | 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 | |||
| 97 | group := &s.Templates.GroupUser | 101 | group := &s.Templates.GroupUser |
| 98 | 102 | ||
| 99 | switch s.Type { | 103 | switch s.Type { |
| @@ -135,7 +139,7 @@ func (s skunkyart) GRUser() { | |||
| 135 | group.About.Interests += interest.String() | 139 | group.About.Interests += interest.String() |
| 136 | } | 140 | } |
| 137 | } | 141 | } |
| 138 | group.About.Comments = s.ParseComments(devianter.CommentsFunc( | 142 | group.About.Comments = s.ParseComments(devianter.GetComments( |
| 139 | strconv.Itoa(group.GR.Gruser.ID), | 143 | strconv.Itoa(group.GR.Gruser.ID), |
| 140 | "", | 144 | "", |
| 141 | s.Page, | 145 | s.Page, |
| @@ -161,9 +165,11 @@ func (s skunkyart) GRUser() { | |||
| 161 | s.Page++ | 165 | s.Page++ |
| 162 | } | 166 | } |
| 163 | 167 | ||
| 164 | gallery := g.Gallery(s.Page, folderid) | 168 | gallery, err := g.GetGallery(s.Page, folderid) |
| 169 | try(err) | ||
| 170 | |||
| 165 | if folderid > 0 { | 171 | if folderid > 0 { |
| 166 | group.Gallery.List = s.DeviationList(gallery.Content.Results, DeviationList{ | 172 | group.Gallery.List = s.DeviationList(gallery.Content.Results, true, DeviationList{ |
| 167 | More: gallery.Content.HasMore, | 173 | More: gallery.Content.HasMore, |
| 168 | }) | 174 | }) |
| 169 | } else { | 175 | } else { |
| @@ -204,7 +210,7 @@ func (s skunkyart) GRUser() { | |||
| 204 | } | 210 | } |
| 205 | 211 | ||
| 206 | if x.Name == "folder_deviations" { | 212 | 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{ |
| 208 | Pages: x.ModuleData.Folder.Pages, | 214 | Pages: x.ModuleData.Folder.Pages, |
| 209 | More: x.ModuleData.Folder.HasMore, | 215 | More: x.ModuleData.Folder.HasMore, |
| 210 | }) | 216 | }) |
| @@ -227,7 +233,7 @@ func (s skunkyart) Deviation(author, postname string) { | |||
| 227 | post := &s.Templates.Deviation | 233 | post := &s.Templates.Deviation |
| 228 | 234 | ||
| 229 | id := id_search[len(id_search)-1] | 235 | id := id_search[len(id_search)-1] |
| 230 | post.Post = devianter.DeviationFunc(id, author) | 236 | post.Post = devianter.GetDeviation(id, author) |
| 231 | 237 | ||
| 232 | if post.Post.Deviation.TextContent.Excerpt != "" { | 238 | if post.Post.Deviation.TextContent.Excerpt != "" { |
| 233 | post.Post.Description = ParseDescription(post.Post.Deviation.TextContent) | 239 | post.Post.Description = ParseDescription(post.Post.Deviation.TextContent) |
| @@ -239,7 +245,7 @@ func (s skunkyart) Deviation(author, postname string) { | |||
| 239 | post.Post.IMG = ParseMedia(post.Post.Deviation.Media) | 245 | post.Post.IMG = ParseMedia(post.Post.Deviation.Media) |
| 240 | for _, x := range post.Post.Deviation.Extended.RelatedContent { | 246 | for _, x := range post.Post.Deviation.Extended.RelatedContent { |
| 241 | if len(x.Deviations) != 0 { | 247 | if len(x.Deviations) != 0 { |
| 242 | post.Related += s.DeviationList(x.Deviations) | 248 | post.Related += s.DeviationList(x.Deviations, false) |
| 243 | } | 249 | } |
| 244 | } | 250 | } |
| 245 | 251 | ||
| @@ -259,7 +265,7 @@ func (s skunkyart) Deviation(author, postname string) { | |||
| 259 | post.Post.Comments.Cursor = "" | 265 | post.Post.Comments.Cursor = "" |
| 260 | } | 266 | } |
| 261 | 267 | ||
| 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)) |
| 263 | 269 | ||
| 264 | s.ExecuteTemplate("deviantion.htm", &s) | 270 | s.ExecuteTemplate("deviantion.htm", &s) |
| 265 | } else { | 271 | } else { |
| @@ -268,25 +274,38 @@ func (s skunkyart) Deviation(author, postname string) { | |||
| 268 | } | 274 | } |
| 269 | 275 | ||
| 270 | func (s skunkyart) DD() { | 276 | func (s skunkyart) DD() { |
| 271 | dd := devianter.DailyDeviationsFunc(s.Page) | 277 | dd := devianter.GetDailyDeviations(s.Page) |
| 272 | s.Templates.SomeList = s.DeviationList(dd.Deviations, DeviationList{ | 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{ | ||
| 273 | Pages: 0, | 292 | Pages: 0, |
| 274 | More: dd.HasMore, | 293 | More: dd.HasMore, |
| 275 | }) | 294 | }) |
| 276 | if !s.Atom { | 295 | if !s.Atom { |
| 277 | s.ExecuteTemplate("list.htm", &s) | 296 | s.ExecuteTemplate("daily.htm", &s) |
| 278 | } | 297 | } |
| 279 | } | 298 | } |
| 280 | 299 | ||
| 281 | func (s skunkyart) Search() { | 300 | func (s skunkyart) Search() { |
| 282 | s.Atom = false | 301 | s.Atom = false |
| 283 | var e error | 302 | var err error |
| 284 | ss := &s.Templates.Search | 303 | ss := &s.Templates.Search |
| 285 | switch s.Type { | 304 | switch s.Type { |
| 286 | case 'a', 't': | 305 | 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) |
| 288 | case 'g': | 307 | 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")) |
| 290 | case 'r': // скраппер, поскольку девиантартовцы зажопили гостевое API для поиска групп | 309 | case 'r': // скраппер, поскольку девиантартовцы зажопили гостевое API для поиска групп |
| 291 | var ( | 310 | var ( |
| 292 | usernames = make(map[int]string) | 311 | usernames = make(map[int]string) |
| @@ -333,10 +352,10 @@ func (s skunkyart) Search() { | |||
| 333 | default: | 352 | default: |
| 334 | s.ReturnHTTPError(400) | 353 | s.ReturnHTTPError(400) |
| 335 | } | 354 | } |
| 336 | try(e) | 355 | try(err) |
| 337 | 356 | ||
| 338 | if s.Type != 'r' { | 357 | if s.Type != 'r' { |
| 339 | ss.List = s.DeviationList(ss.Content.Results, DeviationList{ | 358 | ss.List = s.DeviationList(ss.Content.Results, false, DeviationList{ |
| 340 | Pages: ss.Content.Pages, | 359 | Pages: ss.Content.Pages, |
| 341 | More: ss.Content.HasMore, | 360 | More: ss.Content.HasMore, |
| 342 | }) | 361 | }) |
config.example.json +6 −4
| @@ -4,15 +4,17 @@ | |||
| 4 | "cache": { | 4 | "cache": { |
| 5 | "enabled": true, | 5 | "enabled": true, |
| 6 | "path": "/home/skunk/projects/skunkyart/cache", | 6 | "path": "/home/skunk/projects/skunkyart/cache", |
| 7 | "lifetime": null, | 7 | "lifetime": "1w", |
| 8 | "max-size": 100000, | 8 | "max-size": 1024, |
| 9 | "update-interval": 5 | 9 | "update-interval": 5 |
| 10 | }, | 10 | }, |
| 11 | "dirs-to-memory": [ | 11 | "dirs-to-memory": [ |
| 12 | "/home/skunk/projects/skunkyart/html", | 12 | "/home/skunk/projects/skunkyart/html", |
| 13 | "/home/skunk/projects/skunkyart/css" | 13 | "/home/skunk/projects/skunkyart/css", |
| 14 | "/home/skunk/projects/skunkyart/misc" | ||
| 14 | ], | 15 | ], |
| 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", | ||
| 16 | "proxy": true, | 18 | "proxy": true, |
| 17 | "nsfw": false | 19 | "nsfw": false |
| 18 | } | 20 | } |
css/skunky.css +43 −7
| @@ -131,28 +131,64 @@ form input, button, select { | |||
| 131 | } | 131 | } |
| 132 | 132 | ||
| 133 | /* SCREEN OPTIMISATIONS */ | 133 | /* SCREEN OPTIMISATIONS */ |
| 134 | @media screen and (orientation: portrait) { | 134 | @media (orientation: portrait) { |
| 135 | header { | 135 | * { |
| 136 | scale: 155%; | 136 | font-size: 120% |
| 137 | justify-content: center; | ||
| 138 | } | 137 | } |
| 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 | |||
| 139 | .content { | 157 | .content { |
| 140 | margin: auto; | 158 | margin: auto; |
| 141 | display: inherit; | 159 | display: inherit; |
| 142 | scale: 100%; | 160 | scale: 100%; |
| 143 | } | 161 | } |
| 144 | .block { | 162 | .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% | ||
| 146 | } | 182 | } |
| 147 | } | 183 | } |
| 148 | 184 | ||
| 149 | @media screen and (max-width: 1462px) { | 185 | @media (max-width: 1462px) and (orientation: landscape) { |
| 150 | .block { | 186 | .block { |
| 151 | max-width: 30%; | 187 | max-width: 30%; |
| 152 | } | 188 | } |
| 153 | } | 189 | } |
| 154 | 190 | ||
| 155 | @media screen and (min-width: 788px) and (max-width: 884px) { | 191 | @media (min-width: 788px) and (max-width: 884px) { |
| 156 | .block { | 192 | .block { |
| 157 | max-width: 35%; | 193 | max-width: 35%; |
| 158 | } | 194 | } |
go.mod +2
| @@ -2,6 +2,8 @@ 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.0 => /home/skunk/projects/devianter | ||
| 6 | |||
| 5 | require ( | 7 | require ( |
| 6 | git.macaw.me/skunky/devianter v0.2.0 | 8 | git.macaw.me/skunky/devianter v0.2.0 |
| 7 | golang.org/x/net v0.27.0 | 9 | golang.org/x/net v0.27.0 |
html/about.htm +1 −1
| @@ -58,6 +58,6 @@ | |||
| 58 | </li> | 58 | </li> |
| 59 | {{end}} | 59 | {{end}} |
| 60 | </ul> | 60 | </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> |
| 62 | </main> | 62 | </main> |
| 63 | </html>\ No newline at end of file | 63 | </html>\ No newline at end of file |
html/list.htm → html/daily.htm renamed +6 −1
| @@ -1,7 +1,7 @@ | |||
| 1 | <!DOCTYPE html> | 1 | <!DOCTYPE html> |
| 2 | <html> | 2 | <html> |
| 3 | <head> | 3 | <head> |
| 4 | <title>SkunkyArt</title> | 4 | <title>SkunkyArt | Daily Deviations</title> |
| 5 | <link rel="stylesheet" href="{{.BasePath}}stylesheet"> | 5 | <link rel="stylesheet" href="{{.BasePath}}stylesheet"> |
| 6 | </head> | 6 | </head> |
| 7 | <main> | 7 | <main> |
| @@ -17,6 +17,11 @@ | |||
| 17 | <button type="submit">Search!</button> | 17 | <button type="submit">Search!</button> |
| 18 | </form> | 18 | </form> |
| 19 | </header> | 19 | </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> | ||
| 20 | {{.Templates.SomeList}} | 25 | {{.Templates.SomeList}} |
| 21 | </main> | 26 | </main> |
| 22 | </html>\ No newline at end of file | 27 | </html>\ No newline at end of file |
instances.json +11
| @@ -44,6 +44,17 @@ | |||
| 44 | "nsfw": true, | 44 | "nsfw": true, |
| 45 | "proxy": true | 45 | "proxy": true |
| 46 | } | 46 | } |
| 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 | } | ||
| 47 | } | 58 | } |
| 48 | ] | 59 | ] |
| 49 | }\ No newline at end of file | 60 | }\ 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 | |||
| 3 | directory=<path_to_dir_with_skunkyart> | ||
| 4 | # command_args="-c $directory/config.json" # if SA wasn't start, try to uncomment this line | ||
| 5 | command=$directory/skunkyart | ||
| 6 | |||
| 7 | name="SkunkyArt" | ||
| 8 | description="Privacy frontend for deviantart.com" | ||
| 9 | supervisor=supervise-daemon | ||
| 10 | |||
| 11 | depend() { | ||
| 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] | ||
| 4 | Description=Privacy frontend for deviantart.com | ||
| 5 | |||
| 6 | [Service] | ||
| 7 | Directory=<path-to-dir-with-skunkyart> | ||
| 8 | ExecStart=<path-to-dir-skunkyart> | ||
| 9 | |||
| 10 | [Install] | ||
| 11 | WantedBy=multi-user.target | ||
skunkyart.example.openrc deleted −11
| @@ -1,11 +0,0 @@ | |||
| 1 | #!/sbin/openrc-run | ||
| 2 | name="SkunkyArt" | ||
| 3 | description="Privacy frontend for deviantart.com" | ||
| 4 | supervisor=supervise-daemon | ||
| 5 | command=<path_to_skunkyart> | ||
| 6 | command_args="-c <path_to_config>" | ||
| 7 | directory="<path_to_dir_with_skunkyart>" | ||
| 8 | |||
| 9 | depend() { | ||
| 10 | need net | ||
| 11 | }\ No newline at end of file | ||