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

This commit is contained in:
lost+skunk 2024-07-30 01:06:40 +03:00
parent e02174cb71
commit 2dfeaae772
17 changed files with 357 additions and 218 deletions

View File

@ -1,11 +1,14 @@
<img src="https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/misc/logo.png" alt="SkunkyArt" title="SkunkyArt Logo" width="20%"/>
[![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) [![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)
# Instances # Instances
|Инстанс|Yggdrasil|I2P|Tor|NSFW|Proxifying|Country| |Instance|Yggdrasil|I2P|Tor|NSFW|Proxifying|Country|
|:-----:|:-------:|:-:|:-:|:--:|:--------:|:-----:| |:------:|:-------:|:-:|:-:|:--:|:--------:|:-----:|
|[skunky.ebloid.ru](https://skunky.ebloid.ru/art)|[Yes](http://[201:eba5:d1fc:bf7b:cfcb:a811:4b8b:7ea3]/art)|No|No| No | No | Russia | |[skunky.ebloid.ru](https://skunky.ebloid.ru/art)|[Yes](http://[201:eba5:d1fc:bf7b:cfcb:a811:4b8b:7ea3]/art)|No|No| No | No | Russia |
|[clovius.club](https://skunky.clovius.club)|No|No|No| Yes | Yes | Sweden | |[clovius.club](https://skunky.clovius.club)|No|No|No| Yes | Yes | Sweden |
|[bloat.cat](https://skunky.bloat.cat)|No|No|No| Yes | Yes | Romania | |[bloat.cat](https://skunky.bloat.cat)|No|No|No| Yes | Yes | Romania |
|[frontendfriendly.xyz](https://skunkyart.frontendfriendly.xyz)|No|No|No| Yes | Yes | Finland | |[frontendfriendly.xyz](https://skunkyart.frontendfriendly.xyz)|No|No|No| Yes | Yes | Finland |
|[lumaeris.com](https://skunkyart.lumaeris.com)|No|No|No| Yes | Yes | US |
# EN 🇺🇸 # EN 🇺🇸
## Description ## Description
@ -13,12 +16,12 @@ SkunkyArt 🦨 -- alternative frontend to DeviantArt, which will work without pr
## Config ## Config
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`. 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`.
* `listen` -- the address and port on which SkunkyArt will listen * `listen` -- the address and port on which SkunkyArt will listen
* `base-path` -- the path to the instance. Example: "`base-path`:"/art/" -> https://skunky.ebloid.ru/art/ * `base-path` -- the path to the instance. Example: `"base-path":"/art/"` -> https://skunky.ebloid.ru/art/
* `cache` -- caching system; default is off. * `cache` -- caching system; default is off.
* * `path` -- the path to the cache * `path` -- the path to the cache
* * `lifetime` -- cache file lifetime; measured in Unix milliseconds. * `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.
* * `max-size` -- maximum file size in bytes. * `max-size` -- maximum file size in megabytes.
* `dirs-to-memory` -- this setting determines which directories will be copied to RAM when SkunkyArt is started. Required * `dirs-to-memory` -- this setting determines which directories will be copied to RAM when SkunkyArt is started. Mandatory
* `download-proxy` -- proxy address for downloading files. * `download-proxy` -- proxy address for downloading files.
## Examples of reverse proxies ## Examples of reverse proxies
Nginx: Nginx:
@ -27,17 +30,18 @@ server {
listen 443 ssl; listen 443 ssl;
server_name skunky.example.com; server_name skunky.example.com;
location ((BASE URL)) { # if you have a separate subdomain for the frontend, insert '/' instead of '((BASE URL))'. location ((BASE URL)) { # if you have a separate subdomain for the frontend, insert '/' instead of '((BASE URL)))'.
proxy_set_header Scheme $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_pass http://((IP)):((PORT)); proxy_pass http://((IP)):((PORT));
} }
} }
``` ```
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.
## How do I add my instance to the list? ## How do I add my instance to the list?
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: 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:
1. the instance must not use Cloudflare. 1. the Instance must not use Cloudflare.
2. If your instance has modified source code, you need to publish it to any free platform. For example, Github and Gitlab are not. 2. If your instance has modified source code, you need to publish it to any free platform. For example, Github and Gitlab are not.
## Acknowledgements ## Acknowledgements
* [Лис⚛](https://go.kde.org/matrix/#/@fox:matrix.org) -- helped me understand Go and gave me a lot of useful advice on this language. * [Лис⚛](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,
## Конфиг ## Конфиг
Пример конфига находится в файле `config.example.json`. Чтобы указать свой путь до конфига, используйте CLI-аргумент `-c` или `--config`. Пример конфига находится в файле `config.example.json`. Чтобы указать свой путь до конфига, используйте CLI-аргумент `-c` или `--config`.
* `listen` -- адрес и порт, на котором будет слушать SkunkyArt * `listen` -- адрес и порт, на котором будет слушать SkunkyArt
* `base-path` -- путь к инстансу. Пример: "base-path": "/art/" -> https://skunky.ebloid.ru/art/ * `base-path` -- путь к инстансу. Пример: `"base-path": "/art/"` -> https://skunky.ebloid.ru/art/
* `cache` -- система кеширования; по умолчанию - выкл. * `cache` -- система кеширования; по умолчанию - выкл.
* * `path` -- путь до кеша * `path` -- путь до кеша
* * `lifetime` -- время жизни файла в кеше; измеряется в Unix-миллисекундах * `lifetime` -- время жизни файла в кеше. Единицы измерения: i, h, d, w, m, y. I -- минута, всё остальные единицы измерения, я считаю понятными и без объяснения.
* * `max-size` -- максимальный размер файла в байтах * `max-size` -- максимальный размер файла в мегабайтах
* `dirs-to-memory` -- данная настройка определяет какие каталоги будут скопированы в ОЗУ при запуске SkunkyArt. Обязательна * `dirs-to-memory` -- данная настройка определяет какие каталоги будут скопированы в ОЗУ при запуске SkunkyArt. Обязательна
* `download-proxy` -- адрес прокси для загрузки файлов * `download-proxy` -- адрес прокси для загрузки файлов
## Примеры reverse-прокси ## Примеры reverse-прокси
@ -63,15 +67,16 @@ server {
server_name skunky.example.com; server_name skunky.example.com;
location ((BASE URL)) { # если у вас отдельный поддомен для фронтенда, вместо '((BASE URL))' вставляйте '/' location ((BASE URL)) { # если у вас отдельный поддомен для фронтенда, вместо '((BASE URL))' вставляйте '/'
proxy_set_header Scheme $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_pass http://((IP)):((PORT)); proxy_pass http://((IP)):((PORT));
} }
} }
``` ```
В целом, всё как обычно, за исключением настройки заголовка [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto).
## Как добавить свой инстанс в список? ## Как добавить свой инстанс в список?
Чтобы это сделать, вы должны либо сделать PR, добавив в файл `instances.json` свой инстанс, либо сообщить о нём в комнате в Matrix. Думаю, он не нуждается в описании. Однако учтите, у этого списка есть пара правил: Чтобы это сделать, вы должны либо сделать PR, добавив в файлы `instances.json` и `README.md` свой инстанс, либо создать Issue, или сообщить о нём в комнате в Matrix. Думаю, он не нуждается в описании. Однако учтите, у этого списка есть пара правил:
1. Инстанс не должен использовать Cloudflare. 1. Инстанс не должен использовать Cloudflare.
2. Если ваш инстанс имеет модифицированный исходный код, то вам нужно опубликовать его на любую свободную площадку. Например, Github и Gitlab таковыми не являются. 2. Если ваш инстанс имеет модифицированный исходный код, то вам нужно опубликовать его на любую свободную площадку. Например, Github и Gitlab таковыми не являются.
## Благодарности ## Благодарности

15
TODO.md
View File

@ -1,7 +1,16 @@
# v1.3.x # v1.3.x
* Доделать парсинг описания * Написать Makefile
* Реализовать миниатюры и оптимизировать CSS под маленькие экраны * Почистить говнокод
* **Доделать парсинг описания**
* Избавиться от хардкода под Linux
* ~~Реализовать стрипы в ежедневных артах~~
* ~~Исправить баг с навигацией по страницам~~
* ~~Сделать единицы в конфиге более понятными~~
* Добавить возможность включить темплейты в бинарник
* ~~Реализовать миниатюры и оптимизировать CSS под маленькие экраны~~
* **Реализовать отображение контента, отличного от картинок (видео, аудио, etc)**
* Улучшить систему кеширования: добавить рейтинг для удаления и копирование изображений в ОЗУ
# v1.4 # v1.4
* Реализовать API
* Реализовать темы * Реализовать темы
* Реализовать многоязычный интерфейс * Реализовать многоязычный интерфейс
* Реализовать API

View File

@ -3,14 +3,18 @@ package app
import ( import (
"encoding/json" "encoding/json"
"os" "os"
"regexp"
"strconv"
"time" "time"
"git.macaw.me/skunky/devianter"
) )
type cache_config struct { type cache_config struct {
Enabled bool Enabled bool
Path string Path string
MaxSize int64 `json:"max-size"` MaxSize int64 `json:"max-size"`
Lifetime int64 Lifetime string
UpdateInterval int64 `json:"update-interval"` UpdateInterval int64 `json:"update-interval"`
} }
@ -20,6 +24,7 @@ type config struct {
BasePath string `json:"base-path"` BasePath string `json:"base-path"`
Cache cache_config Cache cache_config
Proxy, Nsfw bool Proxy, Nsfw bool
UserAgent string `json:"user-agent"`
DownloadProxy string `json:"download-proxy"` DownloadProxy string `json:"download-proxy"`
Dirs []string `json:"dirs-to-memory"` Dirs []string `json:"dirs-to-memory"`
} }
@ -34,30 +39,35 @@ var CFG = config{
UpdateInterval: 1, UpdateInterval: 1,
}, },
Dirs: []string{"html", "css"}, Dirs: []string{"html", "css"},
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
Proxy: true, Proxy: true,
Nsfw: true, Nsfw: true,
} }
var lifetimeParsed int64
func ExecuteConfig() { func ExecuteConfig() {
go func() { go func() {
for {
func() {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
recover() recover()
} }
}() }()
for {
Templates["instances.json"] = string(Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body) Templates["instances.json"] = string(Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body)
time.Sleep(1 * time.Second) }()
time.Sleep(1 * time.Hour)
} }
}() }()
const helpmsg = `SkunkyArt v1.3 [refactoring] const helpmsg = `SkunkyArt v1.3.1 [CSS improvements for mobile and strips on Daily Deviations]
Usage: Usage:
- [-c|--config] - path to config - [-c|--config] - path to config
- [-h|--help] - returns this message - [-h|--help] - returns this message
Example: Example:
./skunkyart -c config.json ./skunkyart -c config.json
Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3` Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3.1`
a := os.Args a := os.Args
for n, x := range a { for n, x := range a {
@ -75,15 +85,42 @@ Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3`
if CFG.cfg != "" { if CFG.cfg != "" {
f, err := os.ReadFile(CFG.cfg) f, err := os.ReadFile(CFG.cfg)
try_with_exitstatus(err, 1) tryWithExitStatus(err, 1)
try_with_exitstatus(json.Unmarshal(f, &CFG), 1) tryWithExitStatus(json.Unmarshal(f, &CFG), 1)
if CFG.Cache.Enabled && !CFG.Proxy { if CFG.Cache.Enabled && !CFG.Proxy {
exit("Incompatible settings detected: cannot use caching media content without proxy", 1) exit("Incompatible settings detected: cannot use caching media content without proxy", 1)
} }
if CFG.Cache.MaxSize != 0 || CFG.Cache.Lifetime != 0 { if CFG.Cache.Enabled {
if CFG.Cache.Lifetime != "" {
var duration int64
day := 24 * time.Hour.Milliseconds()
numstr := regexp.MustCompile("[0-9]+").FindAllString(CFG.Cache.Lifetime, -1)
num, _ := strconv.Atoi(numstr[len(numstr)-1])
switch unit := CFG.Cache.Lifetime[len(CFG.Cache.Lifetime)-1:]; unit {
case "i":
duration = time.Minute.Milliseconds()
case "h":
duration = time.Hour.Milliseconds()
case "d":
duration = day
case "w":
duration = day * 7
case "m":
duration = day * 30
case "y":
duration = day * 360
default:
exit("Invalid unit specified: "+unit, 1)
}
lifetimeParsed = duration * int64(num)
}
CFG.Cache.MaxSize /= 1024 ^ 2
go InitCacheSystem() go InitCacheSystem()
} }
devianter.UserAgent = CFG.UserAgent
} }
} }

View File

@ -70,17 +70,81 @@ func (s skunkyart) ParseComments(c devianter.Comments) string {
return cmmts.String() return cmmts.String()
} }
func (s skunkyart) DeviationList(devs []devianter.Deviation, content ...DeviationList) string { func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, content ...DeviationList) string {
var list strings.Builder
if s.Atom && s.Page > 1 { if s.Atom && s.Page > 1 {
s.ReturnHTTPError(400) s.ReturnHTTPError(400)
return "" return ""
} else if s.Atom { }
var list, listContent strings.Builder
for i, l := 0, len(devs); i < l; i++ {
data := &devs[i]
if preview, fullview := ParseMedia(data.Media, 320), ParseMedia(data.Media); !(data.NSFW && !CFG.Nsfw) {
if allowAtom && s.Atom {
id := strconv.Itoa(data.ID)
listContent.WriteString(`<entry><author><name>`)
listContent.WriteString(data.Author.Username)
listContent.WriteString(`</name></author><title>`)
listContent.WriteString(data.Title)
listContent.WriteString(`</title><link rel="alternate" type="text/html" href="`)
listContent.WriteString(UrlBuilder("post", data.Author.Username, "atom-"+id))
listContent.WriteString(`"/><id>`)
listContent.WriteString(id)
listContent.WriteString(`</id><published>`)
listContent.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700"))
listContent.WriteString(`</published>`)
listContent.WriteString(`<media:group><media:title>`)
listContent.WriteString(data.Title)
listContent.WriteString(`</media:title><media:thumbinal url="`)
listContent.WriteString(preview)
listContent.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`)
listContent.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url))
listContent.WriteString(`"><img src="`)
listContent.WriteString(fullview)
listContent.WriteString(`"/></a><p>`)
listContent.WriteString(ParseDescription(data.TextContent))
listContent.WriteString(`</p></div></content></entry>`)
} else {
listContent.WriteString(`<div class="block">`)
if fullview != "" && preview != "" {
listContent.WriteString(`<a title="open/download" href="`)
listContent.WriteString(fullview)
listContent.WriteString(`"><img loading="lazy" src="`)
listContent.WriteString(preview)
listContent.WriteString(`" width="15%"></a>`)
} else {
listContent.WriteString(`<h1>[ TEXT ]</h1>`)
}
listContent.WriteString(`<br><a href="`)
listContent.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url))
listContent.WriteString(`">`)
listContent.WriteString(data.Author.Username)
listContent.WriteString(" - ")
listContent.WriteString(data.Title)
if data.NSFW {
listContent.WriteString(` [<span class="nsfw">NSFW</span>]`)
}
if data.AI {
listContent.WriteString(" [🤖]")
}
if data.DD {
listContent.WriteString(` [<span class="dd">DD</span>]`)
}
listContent.WriteString("</a></div>")
}
}
}
if allowAtom && s.Atom {
list.WriteString(`<?xml version="1.0" encoding="UTF-8"?><feed xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom">`) list.WriteString(`<?xml version="1.0" encoding="UTF-8"?><feed xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom">`)
list.WriteString(`<title>`) list.WriteString(`<title>`)
if s.Type == 0 { if s.Type == 0 {
list.WriteString("Daily Deviations") list.WriteString("Daily Deviations")
} else if len(devs) != 0 { } else if s.Type == 'g' && len(devs) != 0 {
list.WriteString(devs[0].Author.Username) list.WriteString(devs[0].Author.Username)
} else { } else {
list.WriteString("SkunkyArt") list.WriteString("SkunkyArt")
@ -90,75 +154,16 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, content ...Deviatio
list.WriteString(`<link rel="alternate" href="`) list.WriteString(`<link rel="alternate" href="`)
list.WriteString(Host) list.WriteString(Host)
list.WriteString(`"/>`) list.WriteString(`"/>`)
list.WriteString(listContent.String())
list.WriteString("</feed>")
wr(s.Writer, list.String())
} else { } else {
list.WriteString(`<div class="content">`) list.WriteString(`<div class="content">`)
}
for _, data := range devs {
if !(data.NSFW && !CFG.Nsfw) {
url := ParseMedia(data.Media)
if s.Atom {
id := strconv.Itoa(data.ID)
list.WriteString(`<entry><author><name>`)
list.WriteString(data.Author.Username)
list.WriteString(`</name></author><title>`)
list.WriteString(data.Title)
list.WriteString(`</title><link rel="alternate" type="text/html" href="`)
list.WriteString(UrlBuilder("post", data.Author.Username, "atom-"+id))
list.WriteString(`"/><id>`)
list.WriteString(id)
list.WriteString(`</id><published>`)
list.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700"))
list.WriteString(`</published>`)
list.WriteString(`<media:group><media:title>`)
list.WriteString(data.Title)
list.WriteString(`</media:title><media:thumbinal url="`)
list.WriteString(url)
list.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`)
list.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url))
list.WriteString(`"><img src="`)
list.WriteString(url)
list.WriteString(`"/></a><p>`)
list.WriteString(ParseDescription(data.TextContent))
list.WriteString(`</p></div></content></entry>`)
} else {
list.WriteString(`<div class="block">`)
if url != "" {
list.WriteString(`<a title="open/download" href="`)
list.WriteString(url)
list.WriteString(`"><img loading="lazy" src="`)
list.WriteString(url)
list.WriteString(`" width="15%"></a>`)
} else {
list.WriteString(`<h1>[ TEXT ]</h1>`)
}
list.WriteString(`<br><a href="`)
list.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url))
list.WriteString(`">`)
list.WriteString(data.Author.Username)
list.WriteString(" - ")
list.WriteString(data.Title)
// шильдики нсфв, аи и ежедневного поста list.WriteString(listContent.String())
if data.NSFW {
list.WriteString(` [<span class="nsfw">NSFW</span>]`)
}
if data.AI {
list.WriteString(" [🤖]")
}
if data.DD {
list.WriteString(` [<span class="dd">DD</span>]`)
}
list.WriteString("</a></div>")
}
}
}
if s.Atom {
list.WriteString("</feed>")
s.Writer.Write([]byte(list.String()))
return ""
} else {
list.WriteString("</div>") list.WriteString("</div>")
if content != nil { if content != nil {
list.WriteString(s.NavBase(content[0])) list.WriteString(s.NavBase(content[0]))
@ -177,7 +182,7 @@ type text struct {
} }
func ParseDescription(dscr devianter.Text) string { func ParseDescription(dscr devianter.Text) string {
var parseddescription strings.Builder var parsedDescription strings.Builder
TagBuilder := func(content string, tags ...string) string { TagBuilder := func(content string, tags ...string) string {
l := len(tags) l := len(tags)
for x := 0; x < l; x++ { for x := 0; x < l; x++ {
@ -274,16 +279,18 @@ func ParseDescription(dscr devianter.Text) string {
switch x.Type { switch x.Type {
case "atomic": case "atomic":
if len(x.EntityRanges) != 0 {
d := entities[x.EntityRanges[0].Key] d := entities[x.EntityRanges[0].Key]
parseddescription.WriteString(`<a href="`) parsedDescription.WriteString(`<a href="`)
parseddescription.WriteString(ConvertDeviantArtUrlToSkunkyArt(d.Url)) parsedDescription.WriteString(ConvertDeviantArtUrlToSkunkyArt(d.Url))
parseddescription.WriteString(`"><img width="50%" src="`) parsedDescription.WriteString(`"><img width="50%" src="`)
parseddescription.WriteString(ParseMedia(d.Media)) parsedDescription.WriteString(ParseMedia(d.Media))
parseddescription.WriteString(`" title="`) parsedDescription.WriteString(`" title="`)
parseddescription.WriteString(d.Author.Username) parsedDescription.WriteString(d.Author.Username)
parseddescription.WriteString(" - ") parsedDescription.WriteString(" - ")
parseddescription.WriteString(d.Title) parsedDescription.WriteString(d.Title)
parseddescription.WriteString(`"></a>`) parsedDescription.WriteString(`"></a>`)
}
case "unstyled": case "unstyled":
if l := len(Styles); l != 0 { if l := len(Styles); l != 0 {
for n, r := range Styles { for n, r := range Styles {
@ -292,31 +299,31 @@ func ParseDescription(dscr devianter.Text) string {
tag = "h2" tag = "h2"
} }
parseddescription.WriteString(x.Text[:r.From]) parsedDescription.WriteString(x.Text[:r.From])
if len(urls) != 0 && len(x.EntityRanges) != 0 { if len(urls) != 0 && len(x.EntityRanges) != 0 {
ra := &x.EntityRanges[0] ra := &x.EntityRanges[0]
parseddescription.WriteString(`<a target="_blank" href="`) parsedDescription.WriteString(`<a target="_blank" href="`)
parseddescription.WriteString(urls[ra.Key]) parsedDescription.WriteString(urls[ra.Key])
parseddescription.WriteString(`">`) parsedDescription.WriteString(`">`)
parseddescription.WriteString(r.TXT) parsedDescription.WriteString(r.TXT)
parseddescription.WriteString(`</a>`) parsedDescription.WriteString(`</a>`)
} else if l > n+1 { } else if l > n+1 {
parseddescription.WriteString(r.TXT) parsedDescription.WriteString(r.TXT)
} }
parseddescription.WriteString(TagBuilder(tag, x.Text[r.To:])) parsedDescription.WriteString(TagBuilder(tag, x.Text[r.To:]))
} }
} else { } else {
parseddescription.WriteString(x.Text) parsedDescription.WriteString(x.Text)
} }
} }
parseddescription.WriteString("<br>") parsedDescription.WriteString("<br>")
} }
} else if dl != 0 { } else if dl != 0 {
for tt := html.NewTokenizer(strings.NewReader(dscr.Html.Markup)); ; { for tt := html.NewTokenizer(strings.NewReader(dscr.Html.Markup)); ; {
switch tt.Next() { switch tt.Next() {
case html.ErrorToken: case html.ErrorToken:
return parseddescription.String() return parsedDescription.String()
case html.StartTagToken, html.EndTagToken, html.SelfClosingTagToken: case html.StartTagToken, html.EndTagToken, html.SelfClosingTagToken:
token := tt.Token() token := tt.Token()
switch token.Data { switch token.Data {
@ -324,11 +331,11 @@ func ParseDescription(dscr devianter.Text) string {
for _, a := range token.Attr { for _, a := range token.Attr {
if a.Key == "href" { if a.Key == "href" {
url := DeleteTrackingFromUrl(a.Val) url := DeleteTrackingFromUrl(a.Val)
parseddescription.WriteString(`<a target="_blank" href="`) parsedDescription.WriteString(`<a target="_blank" href="`)
parseddescription.WriteString(url) parsedDescription.WriteString(url)
parseddescription.WriteString(`">`) parsedDescription.WriteString(`">`)
parseddescription.WriteString(GetValueOfTag(tt)) parsedDescription.WriteString(GetValueOfTag(tt))
parseddescription.WriteString("</a> ") parsedDescription.WriteString("</a> ")
} }
} }
case "img": case "img":
@ -344,24 +351,24 @@ func ParseDescription(dscr devianter.Text) string {
} }
if title != "" { if title != "" {
for x := -1; x < b; x++ { for x := -1; x < b; x++ {
parseddescription.WriteString(`<img src="`) parsedDescription.WriteString(`<img src="`)
parseddescription.WriteString(uri) parsedDescription.WriteString(uri)
parseddescription.WriteString(`" title="`) parsedDescription.WriteString(`" title="`)
parseddescription.WriteString(title) parsedDescription.WriteString(title)
parseddescription.WriteString(`">`) parsedDescription.WriteString(`">`)
} }
} }
} }
case "br", "li", "ul", "p", "b": case "br", "li", "ul", "p", "b":
parseddescription.WriteString(token.String()) parsedDescription.WriteString(token.String())
case "div": case "div":
parseddescription.WriteString("<p> ") parsedDescription.WriteString("<p> ")
} }
case html.TextToken: case html.TextToken:
parseddescription.Write(tt.Text()) parsedDescription.Write(tt.Text())
} }
} }
} }
return parseddescription.String() return parsedDescription.String()
} }

View File

@ -1,7 +1,6 @@
package app package app
import ( import (
"io"
"net/http" "net/http"
u "net/url" u "net/url"
"strconv" "strconv"
@ -44,7 +43,7 @@ func Router() {
// функция, что управляет всем // функция, что управляет всем
handle := func(w http.ResponseWriter, r *http.Request) { handle := func(w http.ResponseWriter, r *http.Request) {
if h := r.Header["Scheme"]; len(h) != 0 && h[0] == "https" { if h := r.Header["X-Forwarded-Proto"]; len(h) != 0 && h[0] == "https" {
Host = h[0] + "://" + r.Host Host = h[0] + "://" + r.Host
} else { } else {
Host = "http://" + r.Host Host = "http://" + r.Host
@ -97,10 +96,14 @@ func Router() {
skunky.About() skunky.About()
case "stylesheet": case "stylesheet":
w.Header().Add("content-type", "text/css") w.Header().Add("content-type", "text/css")
io.WriteString(w, Templates["skunky.css"]) wr(w, Templates["skunky.css"])
case "favicon.ico":
wr(w, Templates["logo.png"])
} }
} }
http.HandleFunc("/", handle) http.HandleFunc("/", handle)
try_with_exitstatus(http.ListenAndServe(CFG.Listen, nil), 1) println("SkunkyArt is listening on", CFG.Listen)
tryWithExitStatus(http.ListenAndServe(CFG.Listen, nil), 1)
} }

View File

@ -26,7 +26,7 @@ func try(e error) {
println(e.Error()) println(e.Error())
} }
} }
func try_with_exitstatus(err error, code int) { func tryWithExitStatus(err error, code int) {
if err != nil { if err != nil {
exit(err.Error(), code) exit(err.Error(), code)
} }
@ -87,7 +87,7 @@ func Download(url string) (d Downloaded) {
req, e := http.NewRequest("GET", url, nil) req, e := http.NewRequest("GET", url, nil)
try(e) try(e)
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0.0") req.Header.Set("User-Agent", CFG.UserAgent)
resp, e := cli.Do(req) resp, e := cli.Do(req)
try(e) try(e)
@ -148,14 +148,14 @@ func InitCacheSystem() {
try(e) try(e)
for _, a := range dirnames { for _, a := range dirnames {
a = c.Path + "/" + a a = c.Path + "/" + a
if c.Lifetime != 0 { if c.Lifetime != "" {
now := time.Now().UnixMilli() now := time.Now().UnixMilli()
f, _ := os.Stat(a) f, _ := os.Stat(a)
stat := f.Sys().(*syscall.Stat_t) stat := f.Sys().(*syscall.Stat_t)
time := time.Unix(stat.Ctim.Unix()).UnixMilli() time := time.Unix(stat.Ctim.Unix()).UnixMilli()
if time+c.Lifetime <= now { if time+lifetimeParsed <= now {
try(os.RemoveAll(a)) try(os.RemoveAll(a))
} }
} }
@ -172,19 +172,19 @@ func InitCacheSystem() {
func CopyTemplatesToMemory() { func CopyTemplatesToMemory() {
for _, dirname := range CFG.Dirs { for _, dirname := range CFG.Dirs {
dir, e := os.ReadDir(dirname) dir, e := os.ReadDir(dirname)
try_with_exitstatus(e, 1) tryWithExitStatus(e, 1)
for _, x := range dir { for _, x := range dir {
file, e := os.ReadFile(dirname + "/" + x.Name()) file, e := os.ReadFile(dirname + "/" + x.Name())
try_with_exitstatus(e, 1) tryWithExitStatus(e, 1)
Templates[x.Name()] = string(file) Templates[x.Name()] = string(file)
} }
} }
} }
/* PARSING HELPERS */ /* PARSING HELPERS */
func ParseMedia(media devianter.Media) string { func ParseMedia(media devianter.Media, thumb ...int) string {
url := devianter.UrlFromMedia(media) url := devianter.UrlFromMedia(media, thumb...)
if len(url) != 0 && CFG.Proxy { if len(url) != 0 && CFG.Proxy {
url = url[21:] url = url[21:]
dot := strings.Index(url, ".") dot := strings.Index(url, ".")
@ -197,9 +197,10 @@ func ParseMedia(media devianter.Media) string {
func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) { func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) {
if len(url) > 32 && url[27:32] != "stash" { if len(url) > 32 && url[27:32] != "stash" {
url = url[27:] url = url[27:]
toart := strings.Index(url, "/art/") firstshash := strings.Index(url, "/")
if toart != -1 { lastshash := firstshash + strings.Index(url[firstshash+1:], "/")
output = UrlBuilder("post", url[:toart], url[toart+5:]) if lastshash != -1 {
output = UrlBuilder("post", url[:firstshash], url[lastshash+2:])
} }
} }
return return
@ -236,13 +237,9 @@ type DeviationList struct {
// FIXME: на некоротрых артах первая страница может вызывать полное отсутствие панели навигации. // FIXME: на некоротрых артах первая страница может вызывать полное отсутствие панели навигации.
func (s skunkyart) NavBase(c DeviationList) string { func (s skunkyart) NavBase(c DeviationList) string {
// TODO: сделать понятнее
// навигация по страницам
var list strings.Builder var list strings.Builder
list.WriteString("<br>")
p := s.Page
// функция для генерации ссылок list.WriteString("<br>")
prevrev := func(msg string, page int, onpage bool) { prevrev := func(msg string, page int, onpage bool) {
if !onpage { if !onpage {
list.WriteString(`<a href="?p=`) list.WriteString(`<a href="?p=`)
@ -268,33 +265,26 @@ func (s skunkyart) NavBase(c DeviationList) string {
} }
} }
// вперёд-назад p := s.Page
if p > 1 { if p > 1 {
prevrev("<= Prev |", p-1, false) prevrev("<= Prev |", p-1, false)
} else { } else {
p = 1 p = 1
} }
if c.Pages > 0 { for i, x := p-6, 0; (i <= c.Pages && i <= p+6) && x < 12; i++ {
// назад if i > 0 {
for x := p - 6; x < p && x > 0; x++ { var onPage bool
prevrev(strconv.Itoa(x), x, false) if i == p {
onPage = true
} }
// вперёд prevrev(strconv.Itoa(i), i, onPage)
for x := p; x <= p+6 && c.Pages > p+6; x++ {
if x == p {
prevrev("", x, true)
x++ x++
} }
if x > p {
prevrev(strconv.Itoa(x), x, false)
}
}
} }
// вперёд-назад
if c.More { if c.More {
prevrev("| Next =>", p+1, false) prevrev("| Next =>", p+1, false)
} }

View File

@ -48,6 +48,7 @@ type skunkyart struct {
} }
SomeList string SomeList string
DDStrips string
Deviation struct { Deviation struct {
Post devianter.Post Post devianter.Post
Related string Related string
@ -93,7 +94,10 @@ func (s skunkyart) GRUser() {
var g devianter.Group var g devianter.Group
g.Name = s.Query g.Name = s.Query
s.Templates.GroupUser.GR = g.GroupFunc() var err error
s.Templates.GroupUser.GR, err = g.GetGroup()
try(err)
group := &s.Templates.GroupUser group := &s.Templates.GroupUser
switch s.Type { switch s.Type {
@ -135,7 +139,7 @@ func (s skunkyart) GRUser() {
group.About.Interests += interest.String() group.About.Interests += interest.String()
} }
} }
group.About.Comments = s.ParseComments(devianter.CommentsFunc( group.About.Comments = s.ParseComments(devianter.GetComments(
strconv.Itoa(group.GR.Gruser.ID), strconv.Itoa(group.GR.Gruser.ID),
"", "",
s.Page, s.Page,
@ -161,9 +165,11 @@ func (s skunkyart) GRUser() {
s.Page++ s.Page++
} }
gallery := g.Gallery(s.Page, folderid) gallery, err := g.GetGallery(s.Page, folderid)
try(err)
if folderid > 0 { if folderid > 0 {
group.Gallery.List = s.DeviationList(gallery.Content.Results, DeviationList{ group.Gallery.List = s.DeviationList(gallery.Content.Results, true, DeviationList{
More: gallery.Content.HasMore, More: gallery.Content.HasMore,
}) })
} else { } else {
@ -204,7 +210,7 @@ func (s skunkyart) GRUser() {
} }
if x.Name == "folder_deviations" { if x.Name == "folder_deviations" {
group.Gallery.List = s.DeviationList(x.ModuleData.Folder.Deviations, DeviationList{ group.Gallery.List = s.DeviationList(x.ModuleData.Folder.Deviations, true, DeviationList{
Pages: x.ModuleData.Folder.Pages, Pages: x.ModuleData.Folder.Pages,
More: x.ModuleData.Folder.HasMore, More: x.ModuleData.Folder.HasMore,
}) })
@ -227,7 +233,7 @@ func (s skunkyart) Deviation(author, postname string) {
post := &s.Templates.Deviation post := &s.Templates.Deviation
id := id_search[len(id_search)-1] id := id_search[len(id_search)-1]
post.Post = devianter.DeviationFunc(id, author) post.Post = devianter.GetDeviation(id, author)
if post.Post.Deviation.TextContent.Excerpt != "" { if post.Post.Deviation.TextContent.Excerpt != "" {
post.Post.Description = ParseDescription(post.Post.Deviation.TextContent) post.Post.Description = ParseDescription(post.Post.Deviation.TextContent)
@ -239,7 +245,7 @@ func (s skunkyart) Deviation(author, postname string) {
post.Post.IMG = ParseMedia(post.Post.Deviation.Media) post.Post.IMG = ParseMedia(post.Post.Deviation.Media)
for _, x := range post.Post.Deviation.Extended.RelatedContent { for _, x := range post.Post.Deviation.Extended.RelatedContent {
if len(x.Deviations) != 0 { if len(x.Deviations) != 0 {
post.Related += s.DeviationList(x.Deviations) post.Related += s.DeviationList(x.Deviations, false)
} }
} }
@ -259,7 +265,7 @@ func (s skunkyart) Deviation(author, postname string) {
post.Post.Comments.Cursor = "" post.Post.Comments.Cursor = ""
} }
post.Comments = s.ParseComments(devianter.CommentsFunc(id, post.Post.Comments.Cursor, s.Page, 1)) post.Comments = s.ParseComments(devianter.GetComments(id, post.Post.Comments.Cursor, s.Page, 1))
s.ExecuteTemplate("deviantion.htm", &s) s.ExecuteTemplate("deviantion.htm", &s)
} else { } else {
@ -268,25 +274,38 @@ func (s skunkyart) Deviation(author, postname string) {
} }
func (s skunkyart) DD() { func (s skunkyart) DD() {
dd := devianter.DailyDeviationsFunc(s.Page) dd := devianter.GetDailyDeviations(s.Page)
s.Templates.SomeList = s.DeviationList(dd.Deviations, DeviationList{ var strips strings.Builder
for _, x := range dd.Strips {
strips.WriteString(`<h3 class="`)
strips.WriteString(x.Codename)
strips.WriteString(`"> <a href="#`)
strips.WriteString(x.Codename)
strips.WriteString(`"># </a>`)
strips.WriteString(x.Title)
strips.WriteString(`</h3>`)
strips.WriteString(s.DeviationList(x.Deviations, false))
}
s.Templates.DDStrips = strips.String()
s.Templates.SomeList = s.DeviationList(dd.Deviations, true, DeviationList{
Pages: 0, Pages: 0,
More: dd.HasMore, More: dd.HasMore,
}) })
if !s.Atom { if !s.Atom {
s.ExecuteTemplate("list.htm", &s) s.ExecuteTemplate("daily.htm", &s)
} }
} }
func (s skunkyart) Search() { func (s skunkyart) Search() {
s.Atom = false s.Atom = false
var e error var err error
ss := &s.Templates.Search ss := &s.Templates.Search
switch s.Type { switch s.Type {
case 'a', 't': case 'a', 't':
ss.Content, e = devianter.SearchFunc(s.Query, s.Page, s.Type) ss.Content, err = devianter.PerformSearch(s.Query, s.Page, s.Type)
case 'g': case 'g':
ss.Content, e = devianter.SearchFunc(s.Query, s.Page, s.Type, s.Args.Get("usr")) ss.Content, err = devianter.PerformSearch(s.Query, s.Page, s.Type, s.Args.Get("usr"))
case 'r': // скраппер, поскольку девиантартовцы зажопили гостевое API для поиска групп case 'r': // скраппер, поскольку девиантартовцы зажопили гостевое API для поиска групп
var ( var (
usernames = make(map[int]string) usernames = make(map[int]string)
@ -333,10 +352,10 @@ func (s skunkyart) Search() {
default: default:
s.ReturnHTTPError(400) s.ReturnHTTPError(400)
} }
try(e) try(err)
if s.Type != 'r' { if s.Type != 'r' {
ss.List = s.DeviationList(ss.Content.Results, DeviationList{ ss.List = s.DeviationList(ss.Content.Results, false, DeviationList{
Pages: ss.Content.Pages, Pages: ss.Content.Pages,
More: ss.Content.HasMore, More: ss.Content.HasMore,
}) })

View File

@ -4,15 +4,17 @@
"cache": { "cache": {
"enabled": true, "enabled": true,
"path": "/home/skunk/projects/skunkyart/cache", "path": "/home/skunk/projects/skunkyart/cache",
"lifetime": null, "lifetime": "1w",
"max-size": 100000, "max-size": 1024,
"update-interval": 5 "update-interval": 5
}, },
"dirs-to-memory": [ "dirs-to-memory": [
"/home/skunk/projects/skunkyart/html", "/home/skunk/projects/skunkyart/html",
"/home/skunk/projects/skunkyart/css" "/home/skunk/projects/skunkyart/css",
"/home/skunk/projects/skunkyart/misc"
], ],
"download-proxy": null, "download-proxy": "http://127.0.0.1:8080",
"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",
"proxy": true, "proxy": true,
"nsfw": false "nsfw": false
} }

View File

@ -131,28 +131,64 @@ form input, button, select {
} }
/* SCREEN OPTIMISATIONS */ /* SCREEN OPTIMISATIONS */
@media screen and (orientation: portrait) { @media (orientation: portrait) {
header { * {
scale: 155%; font-size: 120%
justify-content: center;
} }
ul {
font-size: 80%
}
center form {
font-size: 60%
}
header form {
font-size: 60%;
}
header, center {
text-align: center;
display: block;
clear: both;
font-size: 200%;
}
.content { .content {
margin: auto; margin: auto;
display: inherit; display: inherit;
scale: 100%; scale: 100%;
} }
.block { .block {
max-width: 60%; margin-top: 10%;
max-width: 200%;
}
.folder-item {
width: 25%
}
.folders {
display: flexbox;
justify-content: center
}
figure img {
width: 10%
}
figure a img {
width: 100%
}
.msg {
font-size: 60%;
max-width: 80%
} }
} }
@media screen and (max-width: 1462px) { @media (max-width: 1462px) and (orientation: landscape) {
.block { .block {
max-width: 30%; max-width: 30%;
} }
} }
@media screen and (min-width: 788px) and (max-width: 884px) { @media (min-width: 788px) and (max-width: 884px) {
.block { .block {
max-width: 35%; max-width: 35%;
} }

2
go.mod
View File

@ -2,6 +2,8 @@ module skunkyart
go 1.22.3 go 1.22.3
replace git.macaw.me/skunky/devianter v0.2.0 => /home/skunk/projects/devianter
require ( require (
git.macaw.me/skunky/devianter v0.2.0 git.macaw.me/skunky/devianter v0.2.0
golang.org/x/net v0.27.0 golang.org/x/net v0.27.0

View File

@ -58,6 +58,6 @@
</li> </li>
{{end}} {{end}}
</ul> </ul>
<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> <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>
</main> </main>
</html> </html>

View File

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>SkunkyArt</title> <title>SkunkyArt | Daily Deviations</title>
<link rel="stylesheet" href="{{.BasePath}}stylesheet"> <link rel="stylesheet" href="{{.BasePath}}stylesheet">
</head> </head>
<main> <main>
@ -17,6 +17,11 @@
<button type="submit">Search!</button> <button type="submit">Search!</button>
</form> </form>
</header> </header>
{{if ne .Templates.DDStrips ""}}
<h2 id="strips"><a href="#strips">#</a> Strips</h2>
{{.Templates.DDStrips}}
{{end}}
<h2 id="content"><a href="#content">#</a> Content</h2>
{{.Templates.SomeList}} {{.Templates.SomeList}}
</main> </main>
</html> </html>

View File

@ -44,6 +44,17 @@
"nsfw": true, "nsfw": true,
"proxy": true "proxy": true
} }
},
{
"title": "lumaeris.com",
"country": "US",
"urls": [{
"clearnet": "https://skunkyart.lumaeris.com"
}],
"settings": {
"nsfw": true,
"proxy": true
}
} }
] ]
} }

BIN
misc/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

View File

@ -0,0 +1,13 @@
#!/sbin/openrc-run
directory=<path_to_dir_with_skunkyart>
# command_args="-c $directory/config.json" # if SA wasn't start, try to uncomment this line
command=$directory/skunkyart
name="SkunkyArt"
description="Privacy frontend for deviantart.com"
supervisor=supervise-daemon
depend() {
need net
}

View File

@ -0,0 +1,11 @@
# Note: i didn't use systemd, so it can be not works :)
[Unit]
Description=Privacy frontend for deviantart.com
[Service]
Directory=<path-to-dir-with-skunkyart>
ExecStart=<path-to-dir-skunkyart>
[Install]
WantedBy=multi-user.target

View File

@ -1,11 +0,0 @@
#!/sbin/openrc-run
name="SkunkyArt"
description="Privacy frontend for deviantart.com"
supervisor=supervise-daemon
command=<path_to_skunkyart>
command_args="-c <path_to_config>"
directory="<path_to_dir_with_skunkyart>"
depend() {
need net
}