темплейты в бинарнике и улучшенная система кеша

This commit is contained in:
lost+skunk 2024-08-13 15:59:52 +03:00
parent 4db018fb7f
commit 1537da9b16
29 changed files with 555 additions and 303 deletions

View File

@ -1,15 +1,15 @@
<img src="https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/misc/logo.png" alt="SkunkyArt" title="SkunkyArt Logo" width="20%" loading="lazy"/>
<img src="/skunky/SkunkyArt/raw/branch/master/misc/logo.png" alt="SkunkyArt" title="SkunkyArt Logo" width="20%" loading="lazy"/>
[![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.md`](https://git.macaw.me/skunky/SkunkyArt/src/branch/master/INSTANCES.md)
Instances: [`INSTANCES.md`](/skunky/SkunkyArt/src/branch/master/INSTANCES.md)
# EN 🇺🇸
## Description
SkunkyArt 🦨 — alternative frontend for DevianArt, which works without JS.
## Setup
The sample config is in the `config.example.json` file. For custom config, use `--config` option.
See the [`SETUP.md`](https://git.macaw.me/skunky/SkunkyArt/src/branch/master/SETUP.md) file for more info about directives.
See the [`SETUP.md`](/skunky/SkunkyArt/src/branch/master/SETUP.md) file for more info about directives.
## Adding instance to the list
To do this, you must either make a PR by adding your instance to the `instances.json` and `INSTANCES.md` files (you can use `--add-instance` cli-argument to automatically add the instance to these files), or create an Issue, or report it to the room in Matrix. Keep in mind that your instance must comply with the following rules:
1. the Instance must not use Cloudflare.
@ -23,7 +23,7 @@ To do this, you must either make a PR by adding your instance to the `instances.
SkunkyArt 🦨 — альтернативный фронтенд к DeviantArt, который полностью работает без JS (JavaScript).
## Настройка
Пример конфига находится в файле `config.example.json`. Чтобы указать свой конфиг, используйте cli-аргумент `--config`.
См. [`SETUP-RU.md`](https://git.macaw.me/skunky/SkunkyArt/src/branch/master/SETUP-RU.md) для информации о настройки фронтенда.
См. [`SETUP-RU.md`](/skunky/SkunkyArt/src/branch/master/SETUP-RU.md) для информации о настройки фронтенда.
## Добавление инстанса в список
Чтобы это сделать, вы должны либо сделать PR, добавив в файлы `instances.json` и `INSTANCES.md` свой инстанс (можете воспользоваться cli-аргументом `--add-instance`, который автоматически это сделает), либо создать Issue, или сообщить о нём в комнате в Matrix. Учтите, что ваш инстанс должен соблюсти следущие правила:
1. Инстанс не должен использовать Cloudflare итп.

View File

@ -1,4 +1,4 @@
[English version 🇬🇧](https://git.macaw.me/skunky/SkunkyArt/src/branch/master/SETUP.md)
[English version 🇬🇧](/skunky/SkunkyArt/src/branch/master/SETUP.md)
# Единицы измерения
Размер файла в кеше измеряется в мегабайтах.<br>

View File

@ -1,4 +1,4 @@
[Версия на русском языке 🇷🇺](https://git.macaw.me/skunky/SkunkyArt/src/branch/master/SETUP-RU.md)
[Версия на русском языке 🇷🇺](/skunky/SkunkyArt/src/branch/master/SETUP-RU.md)
# Units
Maximum file size in megabytes, requires numeric value.<br>

18
TODO.md
View File

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

132
app/cache.go Normal file
View File

@ -0,0 +1,132 @@
// TODO: реализовать кеширование JSON и почистить код
package app
import (
"crypto/sha1"
"encoding/hex"
"io"
"os"
"strings"
"sync"
"syscall"
"time"
)
type file struct {
Score int
Content []byte
}
var tempFS = make(map[[20]byte]*file)
var mx = &sync.RWMutex{}
func (s skunkyart) DownloadAndSendMedia(subdomain, path string) {
var url strings.Builder
url.WriteString("https://images-wixmp-")
url.WriteString(subdomain)
url.WriteString(".wixmp.com/")
url.WriteString(path)
url.WriteString("?token=")
url.WriteString(s.Args.Get("token"))
var response []byte
switch {
case CFG.Cache.Enabled:
fileName := sha1.Sum([]byte(subdomain + path))
filePath := CFG.Cache.Path + "/" + hex.EncodeToString(fileName[:])
mx.Lock()
if tempFS[fileName] == nil {
tempFS[fileName] = &file{}
}
f := *tempFS[fileName]
mx.Unlock()
if f.Content != nil {
f.Score += 2
} else {
file, err := os.Open(filePath)
if err != nil {
if dwnld := Download(url.String()); dwnld.Status == 200 && dwnld.Headers["Content-Type"][0][:5] == "image" {
f.Content = dwnld.Body
try(os.WriteFile(filePath, f.Content, 0700))
} else {
s.ReturnHTTPError(dwnld.Status)
return
}
} else {
file, e := io.ReadAll(file)
try(e)
f.Content = file
}
go func() {
defer restore()
for {
time.Sleep(1 * time.Minute)
mx.Lock()
if tempFS[fileName].Score <= 0 {
delete(tempFS, fileName)
mx.Unlock()
return
}
tempFS[fileName].Score--
mx.Unlock()
}
}()
}
mx.Lock()
tempFS[fileName] = &f
mx.Unlock()
response = f.Content
case CFG.Proxy:
dwnld := Download(url.String())
if dwnld.Status != 200 {
s.ReturnHTTPError(dwnld.Status)
return
}
response = dwnld.Body
default:
s.Writer.WriteHeader(403)
response = []byte("Sorry, butt proxy on this instance are disabled.")
}
s.Writer.Write(response)
}
func InitCacheSystem() {
c := &CFG.Cache
os.Mkdir(c.Path, 0700)
for {
dir, e := os.Open(c.Path)
try(e)
stat, e := dir.Stat()
try(e)
dirnames, e := dir.Readdirnames(-1)
try(e)
for _, a := range dirnames {
a = c.Path + "/" + a
if c.Lifetime != "" {
now := time.Now().UnixMilli()
f, _ := os.Stat(a)
stat := f.Sys().(*syscall.Stat_t)
time := statTime(stat)
if time+lifetimeParsed <= now {
try(os.RemoveAll(a))
}
}
if c.MaxSize != 0 && stat.Size() > c.MaxSize {
try(os.RemoveAll(a))
}
}
dir.Close()
time.Sleep(time.Second * time.Duration(c.UpdateInterval))
}
}

View File

@ -78,16 +78,16 @@ func addInstance() {
try(err)
defer instancesJson.Close()
instances, err := os.OpenFile("INSTANCES.md", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
instancesFile, err := os.OpenFile("INSTANCES.md", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
try(err)
defer instances.Close()
defer instancesFile.Close()
for {
if Templates["instances.json"] == "" {
if string(instances) == "" {
print("\rDownloading instance list...")
} else {
println("\r\033[2KDownloaded!")
try(json.Unmarshal([]byte(Templates["instances.json"]), &settingsVar))
try(json.Unmarshal(instances, &settingsVar))
settingsVar.Instances = append(settingsVar.Instances, settings{
Title: prompt("Title", true),
@ -113,51 +113,46 @@ func addInstance() {
settingsVar := &settingsVar.Instances[len(settingsVar.Instances)-1]
var mdstr bytes.Buffer
mdstr.WriteString("\n|")
if settingsVar.Urls.Clearnet != "" {
mdbuilder := func(yes bool, link string, title string) {
switch {
case yes && (title != "" && link != ""):
mdstr.WriteString("[")
mdstr.WriteString(settingsVar.Title)
mdstr.WriteString(title)
mdstr.WriteString("](")
mdstr.WriteString(settingsVar.Urls.Clearnet)
mdstr.WriteString(link)
mdstr.WriteString(")")
} else {
mdstr.WriteString(settingsVar.Title)
case yes && link != "":
mdstr.WriteString("[Yes](")
mdstr.WriteString(link)
mdstr.WriteString(")")
case yes:
mdstr.WriteString("Yes")
default:
mdstr.WriteString("No")
}
mdstr.WriteString("|")
}
mdstr.WriteString("\n|")
mdbuilder(settingsVar.Urls.Clearnet != "", settingsVar.Urls.Clearnet, settingsVar.Title)
urls := []string{settingsVar.Urls.Ygg, settingsVar.Urls.I2P, settingsVar.Urls.Tor}
for i, l := 0, len(urls); i < l; i++ {
url := urls[i]
if url != "" {
mdstr.WriteString("[Yes](")
mdstr.WriteString(url)
mdstr.WriteString(")|")
} else {
mdstr.WriteString("No|")
}
mdbuilder(url != "", url, "")
}
settings := []bool{settingsVar.Settings.Nsfw, settingsVar.Settings.Proxy}
for i, l := 0, len(settings); i < l; i++ {
if settings[i] {
mdstr.WriteString("Yes|")
} else {
mdstr.WriteString("No|")
}
mdbuilder(settings[i], "", "")
}
if settingsVar.ModifiedSrc != "" {
mdstr.WriteString("[Yes](")
mdstr.WriteString(settingsVar.ModifiedSrc)
mdstr.WriteString(")|")
} else {
mdstr.WriteString("No|")
}
mdbuilder(settingsVar.ModifiedSrc != "", settingsVar.ModifiedSrc, "")
mdstr.WriteString(settingsVar.Country)
mdstr.WriteString("|")
instances.Write(mdstr.Bytes())
instancesFile.Write(mdstr.Bytes())
break
}
time.Sleep(500 * time.Millisecond)

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"os"
"regexp"
"skunkyart/static"
"strconv"
"time"
@ -26,7 +27,7 @@ type config struct {
Proxy, Nsfw bool
UserAgent string `json:"user-agent"`
DownloadProxy string `json:"download-proxy"`
Dirs []string `json:"dirs-to-memory"`
StaticPath string `json:"static-path"`
}
var CFG = config{
@ -38,7 +39,7 @@ var CFG = config{
Path: "cache",
UpdateInterval: 1,
},
Dirs: []string{"html", "css", "misc"},
StaticPath: "static",
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,
Nsfw: true,
@ -56,6 +57,7 @@ func ExecuteConfig() {
exit("Incompatible settings detected: cannot use caching media content without proxy", 1)
}
static.StaticPath = CFG.StaticPath
if CFG.Cache.Enabled {
if CFG.Cache.Lifetime != "" {
var duration int64

View File

@ -41,7 +41,9 @@ func (s skunkyart) ParseComments(c devianter.Comments) string {
cmmts.WriteString("</b></a> ")
if x.Parent > 0 {
cmmts.WriteString(` In reply to <a href="#`)
cmmts.WriteString(` In reply to <a href="`)
cmmts.WriteString(Path)
cmmts.WriteString("#")
cmmts.WriteString(strconv.Itoa(x.Parent))
cmmts.WriteString(`">`)
if replied[x.Parent] == "" {
@ -80,7 +82,7 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con
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 preview, fullview := ParseMedia(data.Media, data.Title, 320), ParseMedia(data.Media, data.Title); !(data.NSFW && !CFG.Nsfw) {
if allowAtom && s.Atom {
id := strconv.Itoa(data.ID)
listContent.WriteString(`<entry><author><name>`)
@ -284,7 +286,7 @@ func ParseDescription(dscr devianter.Text) string {
parsedDescription.WriteString(`<a href="`)
parsedDescription.WriteString(ConvertDeviantArtUrlToSkunkyArt(d.Url))
parsedDescription.WriteString(`"><img width="50%" src="`)
parsedDescription.WriteString(ParseMedia(d.Media))
parsedDescription.WriteString(ParseMedia(d.Media, d.Title))
parsedDescription.WriteString(`" title="`)
parsedDescription.WriteString(d.Author.Username)
parsedDescription.WriteString(" - ")

View File

@ -1,13 +1,15 @@
package app
import (
"io"
"net/http"
u "net/url"
"skunkyart/static"
"strconv"
"strings"
)
var Host string
var Host, Path string
func Router() {
parsepath := func(path string) map[int]string {
@ -41,6 +43,15 @@ func Router() {
return
}
open := func(name string) []byte {
file, err := static.Templates.Open(name)
try(err)
fileReaded, err := io.ReadAll(file)
try(err)
return fileReaded
}
// функция, что управляет всем
handle := func(w http.ResponseWriter, r *http.Request) {
if h := r.Header["X-Forwarded-Proto"]; len(h) != 0 && h[0] == "https" {
@ -49,7 +60,8 @@ func Router() {
Host = "http://" + r.Host
}
path := parsepath(r.URL.Path)
Path = r.URL.Path
path := parsepath(Path)
// структура с функциями
var skunky skunkyart
skunky.Writer = w
@ -70,12 +82,14 @@ func Router() {
skunky.Atom = true
}
skunky.Endpoint = path[1]
// пути
switch path[1] {
switch skunky.Endpoint {
default:
skunky.ReturnHTTPError(404)
case "":
skunky.ExecuteTemplate("index.htm", &CFG.URI)
w.Write(open("html/index.htm"))
case "post":
skunky.Deviation(path[2], path[3])
case "search":
@ -88,6 +102,13 @@ func Router() {
case "media":
switch path[2] {
case "file":
if a := arg("filename"); a != "" {
var filename strings.Builder
filename.WriteString(`filename="`)
filename.WriteString(a)
filename.WriteString(`"`)
w.Header().Add("Content-Disposition", filename.String())
}
skunky.DownloadAndSendMedia(path[3], next(path, 4))
case "emojitar":
skunky.Emojitar(path[3])
@ -96,9 +117,9 @@ func Router() {
skunky.About()
case "stylesheet":
w.Header().Add("content-type", "text/css")
wr(w, Templates["skunky.css"])
w.Write(open("css/skunky.css"))
case "favicon.ico":
wr(w, Templates["logo.png"])
w.Write(open("images/logo.png"))
}
}

13
app/stat-freebsd.go Normal file
View File

@ -0,0 +1,13 @@
//go:build freebsd
// +build freebsd
package app
import (
"syscall"
"time"
)
func statTime(stat *syscall.Stat_t) int64 {
return time.Unix(stat.Ctimespec.Unix()).UnixMilli()
}

13
app/stat.go Normal file
View File

@ -0,0 +1,13 @@
//go:build !freebsd
// +build !freebsd
package app
import (
"syscall"
"time"
)
func statTime(stat *syscall.Stat_t) int64 {
return time.Unix(stat.Ctim.Unix()).UnixMilli()
}

View File

@ -1,14 +1,13 @@
package app
import (
"encoding/base64"
"io"
"net/http"
u "net/url"
"net/url"
"os"
"skunkyart/static"
"strconv"
"strings"
"syscall"
"text/template"
"time"
@ -32,26 +31,34 @@ func tryWithExitStatus(err error, code int) {
}
}
func RefreshInstances() {
for {
func() {
defer func() {
func restore() {
if r := recover(); r != nil {
recover()
}
}()
Templates["instances.json"] = string(Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body)
}
var instances []byte
func RefreshInstances() {
for {
func() {
defer restore()
instances = Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body
}()
time.Sleep(1 * time.Hour)
}
}
// some crap for frontend
func (s skunkyart) ExecuteTemplate(file string, data any) {
func (s skunkyart) ExecuteTemplate(file, dir string, data any) {
var buf strings.Builder
tmp := template.New(file)
tmp, e := tmp.Parse(Templates[file])
try(e)
tmp, err := tmp.ParseFS(static.Templates, dir+"/*")
if err != nil {
s.Writer.WriteHeader(500)
wr(s.Writer, err.Error())
return
}
try(tmp.Execute(&buf, &data))
wr(s.Writer, buf.String())
}
@ -91,15 +98,15 @@ type Downloaded struct {
Body []byte
}
func Download(url string) (d Downloaded) {
func Download(urlString string) (d Downloaded) {
cli := &http.Client{}
if CFG.DownloadProxy != "" {
u, e := u.Parse(CFG.DownloadProxy)
u, e := url.Parse(CFG.DownloadProxy)
try(e)
cli.Transport = &http.Transport{Proxy: http.ProxyURL(u)}
}
req, e := http.NewRequest("GET", url, nil)
req, e := http.NewRequest("GET", urlString, nil)
try(e)
req.Header.Set("User-Agent", CFG.UserAgent)
@ -115,97 +122,16 @@ func Download(url string) (d Downloaded) {
return
}
// caching
func (s skunkyart) DownloadAndSendMedia(subdomain, path string) {
var url strings.Builder
url.WriteString("https://images-wixmp-")
url.WriteString(subdomain)
url.WriteString(".wixmp.com/")
url.WriteString(path)
url.WriteString("?token=")
url.WriteString(s.Args.Get("token"))
if CFG.Cache.Enabled {
fname := CFG.Cache.Path + "/" + base64.StdEncoding.EncodeToString([]byte(subdomain+path))
file, e := os.Open(fname)
if e != nil {
dwnld := Download(url.String())
if dwnld.Status == 200 && dwnld.Headers["Content-Type"][0][:5] == "image" {
try(os.WriteFile(fname, dwnld.Body, 0700))
s.Writer.Write(dwnld.Body)
}
} else {
file, e := io.ReadAll(file)
try(e)
s.Writer.Write(file)
}
} else if CFG.Proxy {
dwnld := Download(url.String())
s.Writer.Write(dwnld.Body)
} else {
s.Writer.WriteHeader(403)
s.Writer.Write([]byte("Sorry, butt proxy on this instance are disabled."))
}
}
func InitCacheSystem() {
c := &CFG.Cache
os.Mkdir(CFG.Cache.Path, 0700)
for {
dir, e := os.Open(c.Path)
try(e)
stat, e := dir.Stat()
try(e)
dirnames, e := dir.Readdirnames(-1)
try(e)
for _, a := range dirnames {
a = c.Path + "/" + a
if c.Lifetime != "" {
now := time.Now().UnixMilli()
f, _ := os.Stat(a)
stat := f.Sys().(*syscall.Stat_t)
time := time.Unix(stat.Ctim.Unix()).UnixMilli()
if time+lifetimeParsed <= now {
try(os.RemoveAll(a))
}
}
if c.MaxSize != 0 && stat.Size() > c.MaxSize {
try(os.RemoveAll(a))
}
}
dir.Close()
time.Sleep(time.Second * time.Duration(CFG.Cache.UpdateInterval))
}
}
func CopyTemplatesToMemory() {
for _, dirname := range CFG.Dirs {
dir, e := os.ReadDir(dirname)
tryWithExitStatus(e, 1)
for _, x := range dir {
file, e := os.ReadFile(dirname + "/" + x.Name())
tryWithExitStatus(e, 1)
Templates[x.Name()] = string(file)
}
}
}
/* PARSING HELPERS */
func ParseMedia(media devianter.Media, thumb ...int) string {
url := devianter.UrlFromMedia(media, thumb...)
if len(url) != 0 && CFG.Proxy {
url = url[21:]
dot := strings.Index(url, ".")
func ParseMedia(media devianter.Media, filename string, thumb ...int) string {
mediaUrl := devianter.UrlFromMedia(media, thumb...)
if len(mediaUrl) != 0 && CFG.Proxy {
mediaUrl = mediaUrl[21:]
dot := strings.Index(mediaUrl, ".")
return UrlBuilder("media", "file", url[:dot], url[dot+11:])
return UrlBuilder("media", "file", mediaUrl[:dot], mediaUrl[dot+11:], "&filename=", url.QueryEscape(filename))
}
return url
return mediaUrl
}
func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) {
@ -255,7 +181,9 @@ func (s skunkyart) NavBase(c DeviationList) string {
list.WriteString("<br>")
prevrev := func(msg string, page int, onpage bool) {
if !onpage {
list.WriteString(`<a href="?p=`)
list.WriteString(`<a href="`)
list.WriteString(Path)
list.WriteString(`?p=`)
list.WriteString(strconv.Itoa(page))
if s.Type != 0 {
list.WriteString("&type=")

View File

@ -15,18 +15,18 @@ import (
)
var wr = io.WriteString
var Templates = make(map[string]string)
type skunkyart struct {
Writer http.ResponseWriter
Args url.Values
BasePath string
Type rune
Query, QueryRaw string
Page int
Type rune
Atom bool
BasePath, Endpoint string
Query, QueryRaw string
Templates struct {
About struct {
Proxy bool
@ -136,7 +136,7 @@ func (s skunkyart) GRUser() {
case "cover_deviation":
group.About.BGMeta = x.ModuleData.CoverDeviation.Deviation
group.About.BGMeta.Url = ConvertDeviantArtUrlToSkunkyArt(group.About.BGMeta.Url)
group.About.BG = ParseMedia(group.About.BGMeta.Media)
group.About.BG = ParseMedia(group.About.BGMeta.Media, group.About.BGMeta.Title)
case "group_admins":
var htm strings.Builder
for _, z := range x.ModuleData.GroupAdmins.Results {
@ -171,7 +171,7 @@ func (s skunkyart) GRUser() {
folders.WriteString(`<a href="`)
folders.WriteString(ConvertDeviantArtUrlToSkunkyArt(x.Thumb.Url))
folders.WriteString(`"><img loading="lazy" src="`)
folders.WriteString(ParseMedia(x.Thumb.Media))
folders.WriteString(ParseMedia(x.Thumb.Media, x.Thumb.Title))
folders.WriteString(`" title="`)
folders.WriteString(x.Thumb.Title)
folders.WriteString(`"></a>`)
@ -180,7 +180,7 @@ func (s skunkyart) GRUser() {
}
folders.WriteString("<br>")
folders.WriteString(`<a href="?folder=`)
folders.WriteString(`<a href="group_user?folder=`)
folders.WriteString(strconv.Itoa(x.FolderId))
folders.WriteString("&q=")
folders.WriteString(s.Query)
@ -209,7 +209,7 @@ func (s skunkyart) GRUser() {
}
if !s.Atom {
s.ExecuteTemplate("gruser.htm", &s)
s.ExecuteTemplate("gruser.htm", "html", &s)
}
}
@ -233,7 +233,7 @@ func (s skunkyart) Deviation(author, postname string) {
}
// время публикации
post.StringTime = post.Post.Deviation.PublishedTime.UTC().String()
post.Post.IMG = ParseMedia(post.Post.Deviation.Media)
post.Post.IMG = ParseMedia(post.Post.Deviation.Media, post.Post.Deviation.Title)
for _, x := range post.Post.Deviation.Extended.RelatedContent {
if len(x.Deviations) != 0 {
post.Related += s.DeviationList(x.Deviations, false)
@ -258,7 +258,7 @@ func (s skunkyart) Deviation(author, postname string) {
post.Comments = s.ParseComments(devianter.GetComments(id, post.Post.Comments.Cursor, s.Page, 1))
s.ExecuteTemplate("deviantion.htm", &s)
s.ExecuteTemplate("deviantion.htm", "html", &s)
}
func (s skunkyart) DD() {
@ -281,11 +281,16 @@ func (s skunkyart) DD() {
More: dd.HasMore,
})
if !s.Atom {
s.ExecuteTemplate("daily.htm", &s)
s.ExecuteTemplate("daily.htm", "html", &s)
}
}
func (s skunkyart) Search() {
if s.Query == "" {
s.ReturnHTTPError(400)
return
}
var err error
ss := &s.Templates.Search
switch s.Type {
@ -338,6 +343,7 @@ func (s skunkyart) Search() {
}
default:
s.ReturnHTTPError(400)
return
}
try(err)
@ -348,7 +354,7 @@ func (s skunkyart) Search() {
})
}
s.ExecuteTemplate("search.htm", &s)
s.ExecuteTemplate("search.htm", "html", &s)
}
func (s skunkyart) Emojitar(name string) {
@ -367,6 +373,6 @@ func (s skunkyart) Emojitar(name string) {
func (s skunkyart) About() {
s.Templates.About.Nsfw = CFG.Nsfw
s.Templates.About.Proxy = CFG.Proxy
try(json.Unmarshal([]byte(Templates["instances.json"]), &s.Templates.About))
s.ExecuteTemplate("about.htm", &s)
try(json.Unmarshal(instances, &s.Templates.About))
s.ExecuteTemplate("about.htm", "html", &s)
}

View File

@ -8,11 +8,7 @@
"max-size": 1024,
"update-interval": 5
},
"dirs-to-memory": [
"html",
"css",
"misc"
],
"static-path": "static",
"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,

View File

@ -1,28 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>SkunkyArt | Daily Deviations</title>
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
<link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
</head>
<main>
<header>
<h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a> | <a href="?atom=true">RSS</a></h1>
<form method="get" action="{{.BasePath}}search">
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
<select name="type">
<option value="all">All</option>
<option value="tag">Tag</option>
<option value="r">Groups</option>
</select>
<button type="submit">Search!</button>
</form>
</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}}
</main>
</html>

View File

@ -1,30 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>SkunkyArt | Search "{{.QueryRaw}}"</title>
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
<link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
</head>
<main>
<header>
<h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a></h1>
<form method="get" action="search">
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
<select name="type">
<option value="all">All</option>
<option value="tag">Tag</option>
<option value="r">Groups</option>
</select>
<button type="submit">Search!</button>
</form>
</header>
{{if ne .Templates.Search.List ""}}
{{if ne .Templates.Search.Content.Total 0}}
<h1>Results by request '{{.QueryRaw}}': {{.Templates.Search.Content.Total}}</h1>
{{end}}
{{.Templates.Search.List}}
{{else}}
<p>No results :(</p>
{{end}}
</main>
</html>

View File

@ -2,6 +2,7 @@ package main
import (
"skunkyart/app"
"skunkyart/static"
"time"
"git.macaw.me/skunky/devianter"
@ -12,7 +13,7 @@ func main() {
app.ExecuteCommandLineArguments()
app.ExecuteConfig()
app.CopyTemplatesToMemory()
static.CopyTemplatesToMemory()
go func() {
for {

View File

@ -22,12 +22,19 @@ header form {
header {
display: flex;
}
form {
font-size: 0;
border: solid #164e3e 1px;
max-width: fit-content;
}
form input, button, select {
background-color: #134134;
padding: 5px;
color: whitesmoke;
border: 0px;
border-radius: 1px;
border: 0;
}
input:focus {
outline: none;
}
/* BLOCKS */
@ -146,6 +153,8 @@ form input, button, select {
header form {
font-size: 60%;
max-width: unset;
border: 0;
}
header, center {
text-align: center;

View File

@ -1,23 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<title>SkunkyArt</title>
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
<link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
</head>
{{template "head" .}}
<main>
<header>
<h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a></h1>
<form method="get" action="{{.BasePath}}search">
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
<select name="type">
<option value="all">All</option>
<option value="tag">Tag</option>
<option value="r">Groups</option>
</select>
<button type="submit">Search!</button>
</form>
</header>
{{template "header" .}}
<p>
SkunkyArt is an alternative frontend for deviantart.com, written in Go.
</p>

13
static/html/daily.htm Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
{{template "head" . }}
<main>
{{template "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}}
</main>
</html>

View File

@ -1,24 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<title>SkunkyArt | {{.Templates.Deviation.Post.Deviation.Author.Username}} - {{.Templates.Deviation.Post.Deviation.Title}}</title>
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
<meta name="referrer" content="no-referrer" />
<link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
</head>
{{template "head" . }}
<main>
<header>
<h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a></h1>
<form method="get" action="{{.BasePath}}search">
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
<select name="type">
<option value="all">All</option>
<option value="tag">Tag</option>
<option value="r">Groups</option>
</select>
<button type="submit">Search!</button>
</form>
</header>
{{template "header" . }}
<figure>
<img src="{{.BasePath}}media/emojitar/{{.Templates.Deviation.Post.Deviation.Author.Username}}?type=a" width="30px">
<span><strong><a href="{{.BasePath}}group_user?type=about&q={{.Templates.Deviation.Post.Deviation.Author.Username}}">{{.Templates.Deviation.Post.Deviation.Author.Username}}</a></strong> — {{if (.Templates.Deviation.Post.Deviation.DD)}}

View File

@ -1,21 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>SkunkyArt |
{{if eq .Type 'a'}}
{{.Templates.GroupUser.GR.Owner.Username}}
{{else}}
gallery of {{.Templates.GroupUser.GR.Owner.Username}}
{{end}}
</title>
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
<link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
</head>
{{template "head" . }}
<main>
<header>
<h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a>
| <a href="?q={{.Templates.GroupUser.GR.Owner.Username}}&type={{if eq .Type 'a'}}gallery">Gallery{{else}}about">About{{end}}</a>
| <a href="?q={{.Templates.GroupUser.GR.Owner.Username}}&type=gallery&atom=true">RSS</a></h1>
| <a href="group_user?q={{.Templates.GroupUser.GR.Owner.Username}}&type={{if eq .Type 'a'}}gallery">Gallery{{else}}about">About{{end}}</a>
| <a href="group_user?q={{.Templates.GroupUser.GR.Owner.Username}}&type=gallery&atom=true">RSS</a></h1>
<form method="get" action="{{.BasePath}}search">
<input type="gallery" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
<input type="hidden" name="usr" value="{{.Templates.GroupUser.GR.Owner.Username}}">

24
static/html/head.htm Normal file
View File

@ -0,0 +1,24 @@
{{define "head"}}
<head>
<title>SkunkyArt |
{{if eq .Endpoint "search"}}
"{{.QueryRaw}}"
{{else if eq .Endpoint "post"}}
{{.Templates.Deviation.Post.Deviation.Author.Username}} — {{.Templates.Deviation.Post.Deviation.Title}}
{{else if eq .Type 'a'}}
{{if .Templates.GroupUser.GR.Owner.Username}}
{{.Templates.GroupUser.GR.Owner.Username}}
{{else}}
gallery of {{.Templates.GroupUser.GR.Owner.Username}}
{{end}}
{{else}}
{{.Endpoint}}
{{end}}
</title>
<base href="{{.BasePath}}">
<meta name="referrer" content="no-referrer" />
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
<link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
</head>
{{end}}

14
static/html/header.htm Normal file
View File

@ -0,0 +1,14 @@
{{define "header"}}
<header>
<h1><a href="">HOME</a> | <a href="dd">DD</a> {{if eq .Endpoint "dd"}}| <a href="{{.Endpoint}}?atom=true">RSS</a>{{end}}</h1>
<form method="get" action="search">
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false" value="{{.QueryRaw}}">
<select name="type">
<option value="all">All</option>
<option value="tag">Tag</option>
<option value="r">Groups</option>
</select>
<button type="submit">Search!</button>
</form>
</header>
{{end}}

View File

@ -2,12 +2,12 @@
<html>
<head>
<title>SkunkyArt</title>
<link rel="stylesheet" href="{{.}}stylesheet"/>
<link rel="icon" type="image/x-icon" href="{{.}}favicon.ico">
<link rel="stylesheet" href="stylesheet"/>
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<main>
<center>
<form method="get" action="{{.}}search">
<form method="get" action="search">
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
<select name="type">
<option value="all">All</option>
@ -16,7 +16,7 @@
</select>
<button type="submit">Search!</button>
</form>
<h1><a href="{{.}}dd">Daily Deviations</a> | <a href="{{.}}about">About</a> | <a href="https://git.macaw.me/skunky/SkunkyArt" target="_blank">Source Code</a></h1>
<h1><a href="dd">Daily Deviations</a> | <a href="about">About</a> | <a href="https://git.macaw.me/skunky/SkunkyArt" target="_blank">Source Code</a></h1>
</center>
</main>
</html>

16
static/html/search.htm Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
{{template "head" . }}
<main>
{{template "header" . }}
{{if ne .Templates.Search.List ""}}
{{if ne .Templates.Search.Content.Total 0}}
<h1>Results by request '{{.QueryRaw}}': {{.Templates.Search.Content.Total}}</h1>
{{end}}
{{.Templates.Search.List}}
{{else}}
<p>No results :(</p>
{{end}}
</main>
</html>

View File

Before

Width:  |  Height:  |  Size: 398 KiB

After

Width:  |  Height:  |  Size: 398 KiB

148
static/templates-noembed.go Normal file
View File

@ -0,0 +1,148 @@
//go:build !embed
// +build !embed
package static
import (
"bytes"
"io/fs"
"os"
"strings"
"time"
)
var Templates FS
type file struct {
path string
name string
content []byte
}
var templateNames = []string{}
var templates = make(map[string][]file)
var StaticPath string
func CopyTemplatesToMemory() {
baseDir, err := os.ReadDir(StaticPath)
try(err)
for _, c := range baseDir {
if c.IsDir() {
templateNames = append(templateNames, c.Name())
var filePath strings.Builder
filePath.WriteString(StaticPath)
filePath.WriteString("/")
filePath.WriteString(c.Name())
dir, err := os.ReadDir(filePath.String())
try(err)
filePath.WriteString("/")
for _, cd := range dir {
f, err := os.ReadFile(filePath.String() + cd.Name())
try(err)
templates[c.Name()] = append(templates[c.Name()], file{
content: f,
name: cd.Name(),
path: c.Name() + "/" + cd.Name(),
})
}
}
}
}
type FS struct{}
func (FS) Open(name string) (fs.File, error) {
for i, l := 0, len(templateNames); i < l; i++ {
for _, x := range templates[templateNames[i]] {
if x.content != nil && name == x.path {
return &File{
name: x.path,
content: bytes.NewBuffer(x.content),
}, nil
}
}
}
return nil, &fs.PathError{}
}
func (FS) Glob(pattern string) ([]string, error) {
trimmed := strings.Split(pattern, "/")
var matches = []string{}
for x, s := range templates {
for i, l := 0, len(s); i < l && trimmed[0] == x; i++ {
s := s[i]
matches = append(matches, s.path)
}
}
if len(matches) != 0 {
return matches, nil
}
return nil, &fs.PathError{}
}
func try(err error) {
if err != nil {
println(err.Error())
os.Exit(1)
}
}
/* сделано на основе https://github.com/psanford/memfs; требуется для корректной работы templates.ParseFS */
type fileInfo struct {
name string
}
func (fi fileInfo) Name() string {
return fi.name
}
func (fi fileInfo) Size() int64 {
return 4096
}
func (fileInfo) Mode() fs.FileMode {
return 0
}
func (fileInfo) ModTime() time.Time {
return time.Time{}
}
func (fileInfo) IsDir() bool {
return false
}
func (fileInfo) Sys() interface{} {
return nil
}
type File struct {
name string
content *bytes.Buffer
closed bool
}
func (f *File) Stat() (fs.FileInfo, error) {
return fileInfo{
name: f.name,
}, nil
}
func (f *File) Read(b []byte) (int, error) {
if f.closed {
return 0, fs.ErrClosed
}
return f.content.Read(b)
}
func (f *File) Close() error {
if f.closed {
return fs.ErrClosed
}
f.closed = true
return nil
}

16
static/templates.go Normal file
View File

@ -0,0 +1,16 @@
//go:build embed
// +build embed
package static
import "embed"
//go:embed *
var Templates embed.FS
var Enabled bool = true
var StaticPath string
func CopyTemplatesToMemory() {
_ = StaticPath
}