Compare commits

...

No commits in common. "main" and "gradient-2" have entirely different histories.

118 changed files with 3395 additions and 2550 deletions

View file

@ -1,9 +0,0 @@
root = "."
tmp_dir = "tmp"
[build]
cmd = "go build -o ./tmp/main ."
bin = "./tmp/main"
delay = 1000 # ms
exclude_dir = ["assets", "tmp", "vendor"]
include_ext = ["go", "tpl", "tmpl", "html"]
exclude_regex = ["_test\\.go"]

5
.gitignore vendored
View file

@ -1,3 +1,4 @@
/tmp
dev.sh
/pixivfe
tmp
.dir-locals.el
.sass-cache/

View file

@ -1,11 +0,0 @@
steps:
build:
image: golang
commands:
- go get
- go mod download
- CGO_ENABLED=0 GOOS=linux go build -mod=readonly -o pixivfe
- ./pixivfe &
- sleep 3
- go test -v -bench=. -count 5
secrets: [pixivfe_token]

View file

@ -1,4 +1,4 @@
FROM docker.io/golang:1.21.0 as builder
FROM docker.io/golang:1.21 as builder
WORKDIR /app
COPY go.* ./
RUN go mod download
@ -7,7 +7,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -v -o pixivfe
FROM docker.io/alpine:3
COPY --from=builder /app/pixivfe /pixivfe
COPY --from=builder /app/template /template
COPY --from=builder /app/views /views
EXPOSE 8282
ENTRYPOINT ["/pixivfe"]

44
LICENSE
View file

@ -618,45 +618,5 @@ copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
PixivFE: a privacy respecting frontend for Pixiv
Copyright (C) 2023-2024 VnPower

View file

@ -1,6 +1,3 @@
### Note
A backend rewrite is ongoing. Check out branch [v2](https://codeberg.org/VnPower/pixivfe/src/branch/v2).
# PixivFE
A privacy-respecting alternative front-end for Pixiv that doesn't suck
@ -14,11 +11,11 @@ A privacy-respecting alternative front-end for Pixiv that doesn't suck
![CI badge](https://ci.codeberg.org/api/badges/12556/status.svg)
[![Go Report Card](https://goreportcard.com/badge/codeberg.org/vnpower/pixivfe)](https://goreportcard.com/report/codeberg.org/vnpower/pixivfe)
Questions? Feedbacks? You can [PM me](https://matrix.to/#/@vnpower:exozy.me) on
Questions? Feedbacks? You can [PM me](https://matrix.to/#/@vnpower:eientei.org) on
Matrix!
You can keep track of this project's development
[here](https://codeberg.org/VnPower/pixivfe/projects/3481).
[here](https://codeberg.org/VnPower/PixivFE/wiki/Things-to-do).
## Features
@ -29,9 +26,24 @@ You can keep track of this project's development
## Hosting
You can use PixivFE for personal use! Assuming that you use an operating system that can run POSIX shell scripts, install `go`, clone this repository, modify the `run.sh` file, and profit!
I recommend self-hosting your own instance for personal use, instead of relying entirely on official instances.
Check out [this page](https://codeberg.org/VnPower/pixivfe/wiki/Hosting). We
currently have guides for Docker and Caddy.
## Development
```
# watch and compile styles with node-sass
pnpm i -g sass
sass --watch views/css
# run in development mode (auto reload templates)
PIXIVFE_DEV=1 ... go run .
```
## Instances
| Name | Cloudflare? | URL |

View file

@ -1,14 +0,0 @@
# This is required for the API. See https://github.com/Nandaka/PixivUtil2/wiki#pixiv-login-using-cookie
# Please keep this carefully, there is a risk of account theft if somebody got your cookie.
# It is better to use a decoy account, instead of using your main account's cookie.
PHPSESSID: 75921176_zsZa7sNLX8zj4N9UKf526ZV0wdWOwN79
Port: "8080"
UserAgent: Mozilla/5.0
# Number of items in each list page
PageItems: 30
# Default image proxy server. Recommended since Pixiv doesn't like you accessing images on their server.
ImageProxyServer: px2.rainchan.win

View file

@ -1,49 +0,0 @@
package configs
import (
"errors"
"os"
"time"
)
var Token, BaseURL, Port, UserAgent, ProxyServer, StartingTime, Version, AcceptLanguage string
func parseEnv(key string) (string, error) {
value, ok := os.LookupEnv(key)
if !ok {
return value, errors.New("Failed to get environment variable" + key)
}
return value, nil
}
func parseEnvWithDefault(key string, defaultValue string) string {
value, ok := os.LookupEnv(key)
if !ok {
return defaultValue
}
return value
}
func ParseConfig() error {
var err error
Token, err = parseEnv("PIXIVFE_TOKEN")
if err != nil {
return err
}
BaseURL = parseEnvWithDefault("PIXIVFE_BASEURL", "localhost")
Port = parseEnvWithDefault("PIXIVFE_PORT", "8282")
UserAgent = parseEnvWithDefault("PIXIVFE_USERAGENT", "Mozilla/5.0")
ProxyServer = parseEnvWithDefault("PIXIVFE_IMAGEPROXY", "pximg.cocomi.cf")
AcceptLanguage = parseEnvWithDefault("PIXIVFE_ACCEPTLANGUAGE", "en-US,en;q=0.5")
StartingTime = time.Now().UTC().Format("2006-01-02 15:04")
Version = "v1.0.5"
return nil
}

View file

@ -1,16 +0,0 @@
package configs
import (
"time"
"github.com/gofiber/fiber/v2/middleware/session"
)
var Store *session.Store
func SetupStorage() {
Store = session.New(session.Config{
Expiration: time.Hour * 24 * 30,
})
Store.RegisterType("")
}

150
core/config/config.go Normal file
View file

@ -0,0 +1,150 @@
package core
import (
"errors"
"log"
"os"
"strconv"
"strings"
"time"
)
var GlobalServerConfig ServerConfig
type ServerConfig struct {
// Required
Token []string
ProxyServer string // authority part of the URL; no '/'
// can be left empty
Host string
// One of two is required
Port string
UnixSocket string
UserAgent string
AcceptLanguage string
RequestLimit int
StartingTime string
Version string
InDevelopment bool
}
func (s *ServerConfig) InitializeConfig() error {
_, hasDev := os.LookupEnv("PIXIVFE_DEV")
s.InDevelopment = hasDev
if s.InDevelopment {
log.Printf("Set server to development mode\n")
}
token, hasToken := os.LookupEnv("PIXIVFE_TOKEN")
if !hasToken {
log.Fatalln("PIXIVFE_TOKEN is required, but was not set.")
return errors.New("PIXIVFE_TOKEN is required, but was not set.\n")
}
s.SetToken(token)
proxyServer, hasProxyServer := os.LookupEnv("PIXIVFE_IMAGEPROXY")
if !hasProxyServer {
log.Fatalln("PIXIVFE_IMAGEPROXY is required, but was not set.")
return errors.New("PIXIVFE_IMAGEPROXY is required, but was not set.\n")
}
s.SetProxyServer(proxyServer)
hostname, hasHostname := os.LookupEnv("PIXIVFE_HOST")
if hasHostname {
log.Printf("Set TCP hostname to: %s\n", hostname)
s.Host = hostname
}
port, hasPort := os.LookupEnv("PIXIVFE_PORT")
if hasPort {
s.SetPort(port)
}
socket, hasSocket := os.LookupEnv("PIXIVFE_UNIXSOCKET")
if hasSocket {
s.SetUnixSocket(socket)
}
if !hasPort && !hasSocket {
log.Fatalln("Either PIXIVFE_PORT or PIXIVFE_UNIXSOCKET has to be set.")
return errors.New("Either PIXIVFE_PORT or PIXIVFE_UNIXSOCKET has to be set.")
}
userAgent, hasUserAgent := os.LookupEnv("PIXIVFE_USERAGENT")
if !hasUserAgent {
userAgent = "Mozilla/5.0"
}
s.SetUserAgent(userAgent)
acceptLanguage, hasAcceptLanguage := os.LookupEnv("PIXIVFE_ACCEPTLANGUAGE")
if !hasAcceptLanguage {
acceptLanguage = "en-US,en;q=0.5"
}
s.SetAcceptLanguage(acceptLanguage)
requestLimit, hasRequestLimit := os.LookupEnv("PIXIVFE_REQUESTLIMIT")
if hasRequestLimit {
s.SetRequestLimit(requestLimit)
} else {
s.RequestLimit = 15
}
s.setStartingTime()
s.setVersion()
return nil
}
func (s *ServerConfig) SetToken(v string) {
// TODO Maybe add some testing?
s.Token = strings.Split(v, ",")
log.Printf("Set token to: %s\n", v)
}
func (s *ServerConfig) SetProxyServer(v string) {
s.ProxyServer = v
log.Printf("Set image proxy server to: %s\n", v)
}
func (s *ServerConfig) SetPort(v string) {
s.Port = v
log.Printf("Set TCP port to: %s\n", v)
}
func (s *ServerConfig) SetUnixSocket(v string) {
s.UnixSocket = v
log.Printf("Set UNIX socket path to: %s\n", v)
}
func (s *ServerConfig) SetUserAgent(v string) {
s.UserAgent = v
log.Printf("Set user agent to: %s\n", v)
}
func (s *ServerConfig) SetAcceptLanguage(v string) {
s.AcceptLanguage = v
log.Printf("Set Accept-Language header to: %s\n", v)
}
func (s *ServerConfig) SetRequestLimit(v string) {
t, err := strconv.Atoi(v)
if err != nil {
panic(err)
}
s.RequestLimit = t
log.Printf("Set request limit to %s requests per 30 seconds\n", v)
}
func (s *ServerConfig) setStartingTime() {
s.StartingTime = time.Now().UTC().Format("2006-01-02 15:04")
log.Printf("Set starting time to: %s\n", s.StartingTime)
}
func (s *ServerConfig) setVersion() {
s.Version = "v2.3"
log.Printf("Set server version to: %s\n", s.Version)
}

131
core/config/session.go Normal file
View file

@ -0,0 +1,131 @@
package core
import (
"log"
"math/rand"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/session"
)
var Store *session.Store
func SetupStorage() {
Store = session.New(session.Config{
Expiration: time.Hour * 24 * 30,
})
}
func saveSession(sess *session.Session) error {
if err := sess.Save(); err != nil {
return err
}
return nil
}
func ProxyImageUrl(c *fiber.Ctx, s string) string {
proxy := GetImageProxy(c)
s = strings.ReplaceAll(s, `https:\/\/i.pximg.net`, "https://"+proxy)
// s = strings.ReplaceAll(s, `https:\/\/i.pximg.net`, "/proxy/i.pximg.net")
s = strings.ReplaceAll(s, `https:\/\/s.pximg.net`, "/proxy/s.pximg.net")
return s
}
func GetImageProxy(c *fiber.Ctx) string {
sess, err := Store.Get(c)
if err != nil {
log.Fatalln("Failed to get current session and its values! Falling back to server default!")
return GlobalServerConfig.ProxyServer
}
value := sess.Get("ImageProxy")
if value != nil {
return value.(string)
}
return GlobalServerConfig.ProxyServer
}
func GetRandomDefaultToken() string {
defaultToken := GlobalServerConfig.Token[rand.Intn(len(GlobalServerConfig.Token))]
return defaultToken
}
func GetToken(c *fiber.Ctx) string {
defaultToken := GlobalServerConfig.Token[rand.Intn(len(GlobalServerConfig.Token))]
sess, err := Store.Get(c)
if err != nil {
log.Fatalln("Failed to get current session and its values! Falling back to server default!")
return defaultToken
}
value := sess.Get("Token")
if value != nil {
return value.(string)
}
return defaultToken
}
func CheckToken(c *fiber.Ctx) string {
sess, err := Store.Get(c)
if err != nil {
log.Fatalln("Failed to get current session and its values!")
return ""
}
value := sess.Get("Token")
if value != nil {
return value.(string)
}
return ""
}
func GetCSRFToken(c *fiber.Ctx) string {
sess, err := Store.Get(c)
if err != nil {
log.Fatalln("Failed to get current session and its values!")
return ""
}
value := sess.Get("CSRF")
if value != nil {
return value.(string)
}
return ""
}
func SetSessionValue(c *fiber.Ctx, name, value string) error {
sess, err := Store.Get(c)
if err != nil {
return err
}
sess.Set(name, value)
if err = saveSession(sess); err != nil {
log.Fatalln("Failed to save session storage!")
return err
}
return nil
}
func RemoveSessionValue(c *fiber.Ctx, name string) error {
sess, err := Store.Get(c)
if err != nil {
return err
}
sess.Delete(name)
if err = saveSession(sess); err != nil {
log.Fatalln("Failed to save session storage!")
return err
}
return nil
}

87
core/http/request.go Normal file
View file

@ -0,0 +1,87 @@
package core
import (
"errors"
"fmt"
"io"
"net/http"
core "codeberg.org/vnpower/pixivfe/v2/core/config"
"github.com/tidwall/gjson"
)
type HttpResponse struct {
Ok bool
StatusCode int
Body string
Message string
}
func WebAPIRequest(URL, token string) HttpResponse {
req, _ := http.NewRequest("GET", URL, nil)
req.Header.Add("User-Agent", core.GlobalServerConfig.UserAgent)
req.Header.Add("Accept-Language", core.GlobalServerConfig.AcceptLanguage)
if token == "" {
req.AddCookie(&http.Cookie{
Name: "PHPSESSID",
Value: core.GetRandomDefaultToken(),
})
} else {
req.AddCookie(&http.Cookie{
Name: "PHPSESSID",
Value: token,
})
}
// Make the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return HttpResponse{
Ok: false,
StatusCode: 0,
Body: "",
Message: fmt.Sprintf("Failed to create a request to %s\n.", URL),
}
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return HttpResponse{
Ok: false,
StatusCode: 0,
Body: "",
Message: fmt.Sprintln("Failed to parse request data."),
}
}
return HttpResponse{
Ok: true,
StatusCode: resp.StatusCode,
Body: string(body),
Message: "",
}
}
func UnwrapWebAPIRequest(URL, token string) (string, error) {
resp := WebAPIRequest(URL, token)
if !resp.Ok {
return "", errors.New(resp.Message)
}
err := gjson.Get(resp.Body, "error")
if !err.Exists() {
return "", errors.New("Incompatible request body.")
}
if err.Bool() {
return "", errors.New(gjson.Get(resp.Body, "message").String())
}
return gjson.Get(resp.Body, "body").String(), nil
}

115
core/http/url.go Normal file
View file

@ -0,0 +1,115 @@
package core
import "fmt"
func GetNewestArtworksURL(worktype, r18, lastID string) string {
base := "https://www.pixiv.net/ajax/illust/new?limit=30&type=%s&r18=%s&lastId=%s"
return fmt.Sprintf(base, worktype, r18, lastID)
}
func GetDiscoveryURL(mode string, limit int) string {
base := "https://www.pixiv.net/ajax/discovery/artworks?mode=%s&limit=%d"
return fmt.Sprintf(base, mode, limit)
}
func GetRankingURL(mode, content, date, page string) string {
base := "https://www.pixiv.net/ranking.php?format=json&mode=%s&content=%s&date=%s&p=%s"
baseNoDate := "https://www.pixiv.net/ranking.php?format=json&mode=%s&content=%s&p=%s"
if date != "" {
return fmt.Sprintf(base, mode, content, date, page)
}
return fmt.Sprintf(baseNoDate, mode, content, page)
}
func GetRankingCalendarURL(mode string, year, month int) string {
base := "https://www.pixiv.net/ranking_log.php?mode=%s&date=%d%02d"
return fmt.Sprintf(base, mode, year, month)
}
func GetUserInformationURL(id string) string {
base := "https://www.pixiv.net/ajax/user/%s?full=1"
return fmt.Sprintf(base, id)
}
func GetUserArtworksURL(id string) string {
base := "https://www.pixiv.net/ajax/user/%s/profile/all"
return fmt.Sprintf(base, id)
}
func GetUserFullArtworkURL(id, ids string) string {
base := "https://www.pixiv.net/ajax/user/%s/profile/illusts?work_category=illustManga&is_first_page=0&lang=en%s"
return fmt.Sprintf(base, id, ids)
}
func GetUserBookmarksURL(id, mode string, page int) string {
base := "https://www.pixiv.net/ajax/user/%s/illusts/bookmarks?tag=&offset=%d&limit=48&rest=%s"
return fmt.Sprintf(base, id, page*48, mode)
}
func GetFrequentTagsURL(ids string) string {
base := "https://www.pixiv.net/ajax/tags/frequent/illust?%s"
return fmt.Sprintf(base, ids)
}
func GetNewestFromFollowingURL(mode, page string) string {
base := "https://www.pixiv.net/ajax/follow_latest/%s?mode=%s&p=%s"
// TODO: Recheck this URL
return fmt.Sprintf(base, "illust", mode, page)
}
func GetArtworkInformationURL(id string) string {
base := "https://www.pixiv.net/ajax/illust/%s"
return fmt.Sprintf(base, id)
}
func GetArtworkImagesURL(id string) string {
base := "https://www.pixiv.net/ajax/illust/%s/pages"
return fmt.Sprintf(base, id)
}
func GetArtworkRelatedURL(id string, limit int) string {
base := "https://www.pixiv.net/ajax/illust/%s/recommend/init?limit=%d"
return fmt.Sprintf(base, id, limit)
}
func GetArtworkCommentsURL(id string) string {
base := "https://www.pixiv.net/ajax/illusts/comments/roots?illust_id=%s&limit=100"
return fmt.Sprintf(base, id)
}
func GetTagDetailURL(id string) string {
base := "https://www.pixiv.net/ajax/search/tags/%s"
return fmt.Sprintf(base, id)
}
func GetSearchArtworksURL(artworkType, name, order, age_settings, page string) string {
base := "https://www.pixiv.net/ajax/search/%s/%s?order=%s&mode=%s&p=%s"
return fmt.Sprintf(base, artworkType, name, order, age_settings, page)
}
func GetLandingURL(mode string) string {
base := "https://www.pixiv.net/ajax/top/illust?mode=%s"
return fmt.Sprintf(base, mode)
}
func GetNovelURL(id string) string {
base := "https://www.pixiv.net/ajax/novel/%s"
return fmt.Sprintf(base, id)
}

369
core/webapi/artwork.go Normal file
View file

@ -0,0 +1,369 @@
package core
import (
"errors"
"fmt"
"html/template"
"sort"
"strconv"
"strings"
"sync"
"time"
session "codeberg.org/vnpower/pixivfe/v2/core/config"
http "codeberg.org/vnpower/pixivfe/v2/core/http"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v2"
)
// Pixiv returns 0, 1, 2 to filter SFW and/or NSFW artworks.
// Those values are saved in `xRestrict`
// 0: Safe
// 1: R18
// 2: R18G
type xRestrict int
const (
Safe xRestrict = 0
R18 xRestrict = 1
R18G xRestrict = 2
)
var xRestrictModel = map[xRestrict]string{
Safe: "",
R18: "R18",
R18G: "R18G",
}
// Pixiv returns 0, 1, 2 to filter SFW and/or NSFW artworks.
// Those values are saved in `aiType`
// 0: Not rated / Unknown
// 1: Not AI-generated
// 2: AI-generated
type aiType int
const (
Unrated aiType = 0
NotAI aiType = 1
AI aiType = 2
)
var aiTypeModel = map[aiType]string{
Unrated: "Unrated",
NotAI: "Not AI",
AI: "AI",
}
type ImageResponse struct {
Urls map[string]string `json:"urls"`
}
type Image struct {
Small string `json:"thumb_mini"`
Medium string `json:"small"`
Large string `json:"regular"`
Original string `json:"original"`
}
type Tag struct {
Name string `json:"tag"`
TranslatedName string `json:"translation"`
}
type Comment struct {
AuthorID string `json:"userId"`
AuthorName string `json:"userName"`
Avatar string `json:"img"`
Context string `json:"comment"`
Stamp string `json:"stampId"`
Date string `json:"commentDate"`
}
type UserBrief struct {
ID string `json:"userId"`
Name string `json:"name"`
Avatar string `json:"imageBig"`
}
type Illust struct {
ID string `json:"id"`
Title string `json:"title"`
Description template.HTML `json:"description"`
UserID string `json:"userId"`
UserName string `json:"userName"`
UserAccount string `json:"userAccount"`
Date time.Time `json:"uploadDate"`
Images []Image `json:"images"`
Tags []Tag `json:"tags"`
Pages int `json:"pageCount"`
Bookmarks int `json:"bookmarkCount"`
Likes int `json:"likeCount"`
Comments int `json:"commentCount"`
Views int `json:"viewCount"`
CommentDisabled int `json:"commentOff"`
SanityLevel int `json:"sl"`
XRestrict xRestrict `json:"xRestrict"`
AiType aiType `json:"aiType"`
Bookmarked any `json:"bookmarkData"`
Liked any `json:"json:"likeData"`
User UserBrief
RecentWorks []ArtworkBrief
RelatedWorks []ArtworkBrief
CommentsList []Comment
IsUgoira bool
}
func GetUserBasicInformation(c *fiber.Ctx, id string) (UserBrief, error) {
var user UserBrief
URL := http.GetUserInformationURL(id)
response, err := http.UnwrapWebAPIRequest(URL, "")
if err != nil {
return user, err
}
response = session.ProxyImageUrl(c, response)
err = json.Unmarshal([]byte(response), &user)
if err != nil {
return user, err
}
return user, nil
}
func GetArtworkImages(c *fiber.Ctx, id string) ([]Image, error) {
var resp []ImageResponse
var images []Image
URL := http.GetArtworkImagesURL(id)
response, err := http.UnwrapWebAPIRequest(URL, "")
if err != nil {
return nil, err
}
response = session.ProxyImageUrl(c, response)
err = json.Unmarshal([]byte(response), &resp)
if err != nil {
return images, err
}
// Extract and proxy every images
for _, imageRaw := range resp {
var image Image
image.Small = imageRaw.Urls["thumb_mini"]
image.Medium = imageRaw.Urls["small"]
image.Large = imageRaw.Urls["regular"]
image.Original = imageRaw.Urls["original"]
images = append(images, image)
}
return images, nil
}
func GetArtworkComments(c *fiber.Ctx, id string) ([]Comment, error) {
var body struct {
Comments []Comment `json:"comments"`
}
URL := http.GetArtworkCommentsURL(id)
response, err := http.UnwrapWebAPIRequest(URL, "")
if err != nil {
return nil, err
}
response = session.ProxyImageUrl(c, response)
err = json.Unmarshal([]byte(response), &body)
if err != nil {
return nil, err
}
return body.Comments, nil
}
func GetRelatedArtworks(c *fiber.Ctx, id string) ([]ArtworkBrief, error) {
var body struct {
Illusts []ArtworkBrief `json:"illusts"`
}
// TODO: keep the hard-coded limit?
URL := http.GetArtworkRelatedURL(id, 96)
response, err := http.UnwrapWebAPIRequest(URL, "")
if err != nil {
return nil, err
}
response = session.ProxyImageUrl(c, response)
err = json.Unmarshal([]byte(response), &body)
if err != nil {
return nil, err
}
return body.Illusts, nil
}
func GetArtworkByID(c *fiber.Ctx, id string, full bool) (*Illust, error) {
URL := http.GetArtworkInformationURL(id)
response, err := http.UnwrapWebAPIRequest(URL, "")
if err != nil {
return nil, err
}
var illust struct {
*Illust
Recent map[int]any `json:"userIllusts"`
RawTags json.RawMessage `json:"tags"`
}
// Parse basic illust information
err = json.Unmarshal([]byte(response), &illust)
if err != nil {
return nil, err
}
// Begin testing here
wg := sync.WaitGroup{}
cerr := make(chan error, 6)
wg.Add(3)
go func() {
// Get illust images
defer wg.Done()
images, err := GetArtworkImages(c, id)
if err != nil {
cerr <- err
return
}
illust.Images = images
}()
go func() {
// Get basic user information (the URL above does not contain avatars)
defer wg.Done()
var err error
userInfo, err := GetUserBasicInformation(c, illust.UserID)
if err != nil {
cerr <- err
return
}
illust.User = userInfo
}()
go func() {
defer wg.Done()
var err error
// Extract tags
var tags struct {
Tags []struct {
Tag string `json:"tag"`
Translation map[string]string `json:"translation"`
} `json:"tags"`
}
err = json.Unmarshal(illust.RawTags, &tags)
if err != nil {
cerr <- err
return
}
var tagsList []Tag
for _, tag := range tags.Tags {
var newTag Tag
newTag.Name = tag.Tag
newTag.TranslatedName = tag.Translation["en"]
tagsList = append(tagsList, newTag)
}
illust.Tags = tagsList
}()
if full {
wg.Add(3)
go func() {
defer wg.Done()
var err error
// Get recent artworks
ids := make([]int, 0)
for k := range illust.Recent {
ids = append(ids, k)
}
sort.Sort(sort.Reverse(sort.IntSlice(ids)))
idsString := ""
count := min(len(ids), 20)
for i := 0; i < count; i++ {
idsString += fmt.Sprintf("&ids[]=%d", ids[i])
}
recent, err := GetUserArtworks(c, illust.UserID, idsString)
if err != nil {
cerr <- err
return
}
sort.Slice(recent[:], func(i, j int) bool {
left, _ := strconv.Atoi(recent[i].ID)
right, _ := strconv.Atoi(recent[j].ID)
return left > right
})
illust.RecentWorks = recent
}()
go func() {
defer wg.Done()
var err error
related, err := GetRelatedArtworks(c, id)
if err != nil {
cerr <- err
return
}
illust.RelatedWorks = related
}()
go func() {
defer wg.Done()
if illust.CommentDisabled == 1 {
return
}
var err error
comments, err := GetArtworkComments(c, id)
if err != nil {
println("here")
cerr <- err
return
}
illust.CommentsList = comments
}()
}
wg.Wait()
close(cerr)
all_errors := []error{}
for suberr := range cerr {
all_errors = append(all_errors, suberr)
}
err_summary := errors.Join(all_errors...)
if err_summary != nil {
return nil, err_summary
}
// If this artwork is an ugoira
illust.IsUgoira = strings.Contains(illust.Images[0].Original, "ugoira")
return illust.Illust, nil
}

31
core/webapi/discovery.go Normal file
View file

@ -0,0 +1,31 @@
package core
import (
session "codeberg.org/vnpower/pixivfe/v2/core/config"
http "codeberg.org/vnpower/pixivfe/v2/core/http"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v2"
"github.com/tidwall/gjson"
)
func GetDiscoveryArtwork(c *fiber.Ctx, mode string) ([]ArtworkBrief, error) {
token := session.GetToken(c)
URL := http.GetDiscoveryURL(mode, 100)
var artworks []ArtworkBrief
resp, err := http.UnwrapWebAPIRequest(URL, token)
if err != nil {
return nil, err
}
resp = session.ProxyImageUrl(c, resp)
data := gjson.Get(resp, "thumbnails.illust").String()
err = json.Unmarshal([]byte(data), &artworks)
if err != nil {
return nil, err
}
return artworks, nil
}

114
core/webapi/index.go Normal file
View file

@ -0,0 +1,114 @@
package core
import (
"fmt"
session "codeberg.org/vnpower/pixivfe/v2/core/config"
http "codeberg.org/vnpower/pixivfe/v2/core/http"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v2"
"github.com/tidwall/gjson"
)
type Pixivision struct {
ID string `json:"id"`
Title string `json:"title"`
Thumbnail string `json:"thumbnailUrl"`
URL string `json:"url"`
}
type RecommendedTags struct {
Name string `json:"tag"`
Artworks []ArtworkBrief
}
type LandingArtworks struct {
Commissions []ArtworkBrief
Following []ArtworkBrief
Recommended []ArtworkBrief
Newest []ArtworkBrief
Rankings []ArtworkBrief
Users []ArtworkBrief
Pixivision []Pixivision
RecommendByTags []RecommendedTags
}
func GetLanding(c *fiber.Ctx, mode string) (LandingArtworks, error) {
var pages struct {
Pixivision []Pixivision `json:"pixivision"`
Follow []int `json:"follow"`
Recommended struct {
IDs []string `json:"ids"`
} `json:"recommend"`
// EditorRecommended []any `json:"editorRecommend"`
// UserRecommended []any `json:"recommendUser"`
// Commission []any `json:"completeRequestIds"`
RecommendedByTags []struct {
Name string `json:"tag"`
IDs []string `json:"ids"`
} `json:"recommendByTag"`
}
URL := http.GetLandingURL(mode)
var landing LandingArtworks
resp, err := http.UnwrapWebAPIRequest(URL, "")
if err != nil {
return landing, err
}
resp = session.ProxyImageUrl(c, resp)
artworks := map[string]ArtworkBrief{}
// Get thumbnails and save it into a map, since they were kept
// separately and need to the index quickly.
//
// Since there are no duplicates in this object, we are unable
// to rely to ranges (ex. one artwork in two separate sections)
stuff := gjson.Get(resp, "thumbnails.illust")
stuff.ForEach(func(key, value gjson.Result) bool {
var artwork ArtworkBrief
err = json.Unmarshal([]byte(value.String()), &artwork)
if err != nil {
return false
}
if artwork.ID != "" {
artworks[artwork.ID] = artwork
}
return true // keep iterating
})
pagesStr := gjson.Get(resp, "page").String()
err = json.Unmarshal([]byte(pagesStr), &pages)
if err != nil {
return landing, err
}
// Parse everything
landing.Pixivision = pages.Pixivision
landing.Following = make([]ArtworkBrief, len(pages.Follow))
for _, i := range pages.Follow {
landing.Following = append(landing.Following, artworks[fmt.Sprint(i)])
}
for _, i := range pages.RecommendedByTags {
temp := make([]ArtworkBrief, 0)
for _, j := range i.IDs {
temp = append(temp, artworks[j])
}
landing.RecommendByTags = append(landing.RecommendByTags, RecommendedTags{Name: i.Name, Artworks: temp})
}
landing.Recommended = make([]ArtworkBrief, 0)
for _, i := range pages.Recommended.IDs {
landing.Recommended = append(landing.Recommended, artworks[i])
}
return landing, nil
}

44
core/webapi/newest.go Normal file
View file

@ -0,0 +1,44 @@
package core
import (
session "codeberg.org/vnpower/pixivfe/v2/core/config"
http "codeberg.org/vnpower/pixivfe/v2/core/http"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v2"
)
type ArtworkBrief struct {
ID string `json:"id"`
Title string `json:"title"`
ArtistID string `json:"userId"`
ArtistName string `json:"userName"`
ArtistAvatar string `json:"profileImageUrl"`
Thumbnail string `json:"url"`
Pages int `json:"pageCount"`
XRestrict int `json:"xRestrict"`
AiType int `json:"aiType"`
Bookmarked any `json:"bookmarkData"`
}
func GetNewestArtworks(c *fiber.Ctx, worktype string, r18 string) ([]ArtworkBrief, error) {
token := session.GetToken(c)
URL := http.GetNewestArtworksURL(worktype, r18, "0")
var body struct {
Artworks []ArtworkBrief `json:"illusts"`
// LastId string
}
resp, err := http.UnwrapWebAPIRequest(URL, token)
if err != nil {
return nil, err
}
resp = session.ProxyImageUrl(c, resp)
err = json.Unmarshal([]byte(resp), &body)
if err != nil {
return nil, err
}
return body.Artworks, nil
}

57
core/webapi/novel.go Normal file
View file

@ -0,0 +1,57 @@
package core
import "time"
type Novel struct {
BookmarkCount int `json:"bookmarkCount"`
CommentCount int `json:"commentCount"`
MarkerCount int `json:"markerCount"`
CreateDate time.Time `json:"createDate"`
UploadDate time.Time `json:"uploadDate"`
Description string `json:"description"`
ID string `json:"id"`
Title string `json:"title"`
LikeCount int `json:"likeCount"`
PageCount int `json:"pageCount"`
UserID string `json:"userId"`
UserName string `json:"userName"`
ViewCount int `json:"viewCount"`
IsOriginal bool `json:"isOriginal"`
IsBungei bool `json:"isBungei"`
XRestrict int `json:"xRestrict"`
Restrict int `json:"restrict"`
Content string `json:"content"`
CoverURL string `json:"coverUrl"`
IsBookmarkable bool `json:"isBookmarkable"`
BookmarkData interface{} `json:"bookmarkData"`
LikeData bool `json:"likeData"`
PollData interface{} `json:"pollData"`
Marker interface{} `json:"marker"`
Tags struct {
AuthorID string `json:"authorId"`
IsLocked bool `json:"isLocked"`
Tags []struct {
Tag string `json:"tag"`
Locked bool `json:"locked"`
Deletable bool `json:"deletable"`
UserID string `json:"userId"`
UserName string `json:"userName"`
} `json:"tags"`
Writable bool `json:"writable"`
} `json:"tags"`
SeriesNavData interface{} `json:"seriesNavData"`
HasGlossary bool `json:"hasGlossary"`
IsUnlisted bool `json:"isUnlisted"`
Language string `json:"language"`
CommentOff int `json:"commentOff"`
CharacterCount int `json:"characterCount"`
WordCount int `json:"wordCount"`
UseWordCount bool `json:"useWordCount"`
ReadingTime int `json:"readingTime"`
AiType int `json:"aiType"`
Genre string `json:"genre"`
}
func GetNovelByID(id string) {
}

38
core/webapi/personal.go Normal file
View file

@ -0,0 +1,38 @@
package core
import (
session "codeberg.org/vnpower/pixivfe/v2/core/config"
http "codeberg.org/vnpower/pixivfe/v2/core/http"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v2"
)
func GetNewestFromFollowing(c *fiber.Ctx, mode, page string) ([]ArtworkBrief, error) {
token := session.GetToken(c)
URL := http.GetNewestFromFollowingURL(mode, page)
var body struct {
Thumbnails json.RawMessage `json:"thumbnails"`
}
var artworks struct {
Artworks []ArtworkBrief `json:"illust"`
}
resp, err := http.UnwrapWebAPIRequest(URL, token)
if err != nil {
return nil, err
}
resp = session.ProxyImageUrl(c, resp)
err = json.Unmarshal([]byte(resp), &body)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(body.Thumbnails), &artworks)
if err != nil {
return nil, err
}
return artworks.Artworks, nil
}

56
core/webapi/ranking.go Normal file
View file

@ -0,0 +1,56 @@
package core
import (
"errors"
"strings"
session "codeberg.org/vnpower/pixivfe/v2/core/config"
http "codeberg.org/vnpower/pixivfe/v2/core/http"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v2"
)
type Ranking struct {
Contents []struct {
Title string `json:"title"`
Image string `json:"url"`
Pages string `json:"illust_page_count"`
ArtistName string `json:"user_name"`
ArtistAvatar string `json:"profile_img"`
ID int `json:"illust_id"`
ArtistID int `json:"user_id"`
Rank int `json:"rank"`
} `json:"contents"`
Mode string `json:"mode"`
Content string `json:"content"`
Page int `json:"page"`
RankTotal int `json:"rank_total"`
CurrentDate string `json:"date"`
PrevDateRaw json.RawMessage `json:"prev_date"`
NextDateRaw json.RawMessage `json:"next_date"`
PrevDate string
NextDate string
}
func GetRanking(c *fiber.Ctx, mode, content, date, page string) (Ranking, error) {
URL := http.GetRankingURL(mode, content, date, page)
var ranking Ranking
resp := http.WebAPIRequest(URL, "")
if !resp.Ok {
return ranking, errors.New(resp.Message)
}
proxiedResp := session.ProxyImageUrl(c, resp.Body)
err := json.Unmarshal([]byte(proxiedResp), &ranking)
if err != nil {
return ranking, err
}
ranking.PrevDate = strings.ReplaceAll(string(ranking.PrevDateRaw[:]), "\"", "")
ranking.NextDate = strings.ReplaceAll(string(ranking.NextDateRaw[:]), "\"", "")
return ranking, nil
}

View file

@ -0,0 +1,98 @@
package core
import (
"fmt"
"html/template"
"net/http"
"time"
session "codeberg.org/vnpower/pixivfe/v2/core/config"
url "codeberg.org/vnpower/pixivfe/v2/core/http"
"github.com/gofiber/fiber/v2"
"golang.org/x/net/html"
)
func get_weekday(n time.Weekday) int {
switch n {
case time.Sunday:
return 1
case time.Monday:
return 2
case time.Tuesday:
return 3
case time.Wednesday:
return 4
case time.Thursday:
return 5
case time.Friday:
return 6
case time.Saturday:
return 7
}
return 0
}
func GetRankingCalendar(c *fiber.Ctx, mode string, year, month int) (template.HTML, error) {
token := session.GetToken(c)
URL := url.GetRankingCalendarURL(mode, year, month)
req, _ := http.NewRequest("GET", URL, nil)
req.Header.Add("User-Agent", "Mozilla/5.0")
req.Header.Add("Cookie", "PHPSESSID="+token)
// req.AddCookie(&http.Cookie{
// Name: "PHPSESSID",
// Value: token,
// })
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
// Use the html package to parse the response body from the request
doc, err := html.Parse(resp.Body)
if err != nil {
return "", err
}
// Find and print all links on the web page
var links []string
var link func(*html.Node)
link = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "img" {
for _, a := range n.Attr {
if a.Key == "data-src" {
// adds a new link entry when the attribute matches
links = append(links, session.ProxyImageUrl(c, a.Val))
}
}
}
// traverses the HTML of the webpage from the first child node
for c := n.FirstChild; c != nil; c = c.NextSibling {
link(c)
}
}
link(doc)
// now := time.Now()
// yearNow := now.Year()
// monthNow := now.Month()
lastMonth := time.Date(year, time.Month(month), 0, 0, 0, 0, 0, time.UTC)
thisMonth := time.Date(year, time.Month(month+1), 0, 0, 0, 0, 0, time.UTC)
renderString := ""
for i := 0; i < get_weekday(lastMonth.Weekday()); i++ {
renderString += "<div class=\"calendar-node calendar-node-empty\"></div>"
}
for i := 0; i < thisMonth.Day(); i++ {
date := fmt.Sprintf("%d%02d%02d", year, month, i+1)
if len(links) > i {
renderString += fmt.Sprintf(`<a href="/ranking?mode=%s&date=%s"><div class="calendar-node" style="background-image: url(%s)"><span>%d</span></div></a>`, mode, date, links[i], i+1)
} else {
renderString += fmt.Sprintf(`<div class="calendar-node"><span>%d</span></div>`, i+1)
}
}
return template.HTML(renderString), nil
}

94
core/webapi/tag.go Normal file
View file

@ -0,0 +1,94 @@
package core
import (
"strings"
session "codeberg.org/vnpower/pixivfe/v2/core/config"
http "codeberg.org/vnpower/pixivfe/v2/core/http"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v2"
)
type TagDetail struct {
Name string `json:"tag"`
AlternativeName string `json:"word"`
Metadata struct {
Detail string `json:"abstract"`
Image string `json:"image"`
Name string `json:"tag"`
ID json.Number `json:"id"`
} `json:"pixpedia"`
}
type SearchArtworks struct {
Artworks []ArtworkBrief `json:"data"`
Total int `json:"total"`
}
type SearchResult struct {
Artworks SearchArtworks
Popular struct {
Permanent []ArtworkBrief `json:"permanent"`
Recent []ArtworkBrief `json:"recent"`
} `json:"popular"`
RelatedTags []string `json:"relatedTags"`
}
func GetTagData(c *fiber.Ctx, name string) (TagDetail, error) {
var tag TagDetail
URL := http.GetTagDetailURL(name)
response, err := http.UnwrapWebAPIRequest(URL, "")
if err != nil {
return tag, err
}
response = session.ProxyImageUrl(c, response)
err = json.Unmarshal([]byte(response), &tag)
if err != nil {
return tag, err
}
return tag, nil
}
func GetSearch(c *fiber.Ctx, artworkType, name, order, age_settings, page string) (*SearchResult, error) {
URL := http.GetSearchArtworksURL(artworkType, name, order, age_settings, page)
response, err := http.UnwrapWebAPIRequest(URL, "")
if err != nil {
return nil, err
}
response = session.ProxyImageUrl(c, response)
// IDK how to do better than this lol
temp := strings.ReplaceAll(string(response), `"illust"`, `"works"`)
temp = strings.ReplaceAll(temp, `"manga"`, `"works"`)
temp = strings.ReplaceAll(temp, `"illustManga"`, `"works"`)
var resultRaw struct {
*SearchResult
ArtworksRaw json.RawMessage `json:"works"`
}
var artworks SearchArtworks
var result *SearchResult
err = json.Unmarshal([]byte(temp), &resultRaw)
if err != nil {
return nil, err
}
result = resultRaw.SearchResult
err = json.Unmarshal([]byte(resultRaw.ArtworksRaw), &artworks)
if err != nil {
return nil, err
}
result.Artworks = artworks
return result, nil
}

283
core/webapi/user.go Normal file
View file

@ -0,0 +1,283 @@
package core
import (
"errors"
"fmt"
"html/template"
"math"
"sort"
"strconv"
session "codeberg.org/vnpower/pixivfe/v2/core/config"
http "codeberg.org/vnpower/pixivfe/v2/core/http"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v2"
)
type FrequentTag struct {
Name string `json:"tag"`
TranslatedName string `json:"tag_translation"`
}
type User struct {
ID string `json:"userId"`
Name string `json:"name"`
Avatar string `json:"imageBig"`
Following int `json:"following"`
MyPixiv int `json:"mypixivCount"`
Comment template.HTML `json:"commentHtml"`
Webpage string `json:"webpage"`
SocialRaw json.RawMessage `json:"social"`
Artworks []ArtworkBrief `json:"artworks"`
Background map[string]interface{} `json:"background"`
ArtworksCount int
FrequentTags []FrequentTag
Social map[string]map[string]string
BackgroundImage string
}
func (s *User) ParseSocial() {
if string(s.SocialRaw[:]) == "[]" {
// Fuck Pixiv
return
}
_ = json.Unmarshal(s.SocialRaw, &s.Social)
}
func GetFrequentTags(c *fiber.Ctx, ids string) ([]FrequentTag, error) {
var tags []FrequentTag
URL := http.GetFrequentTagsURL(ids)
response, err := http.UnwrapWebAPIRequest(URL, "")
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(response), &tags)
if err != nil {
return nil, err
}
return tags, nil
}
func GetUserArtworks(c *fiber.Ctx, id, ids string) ([]ArtworkBrief, error) {
var works []ArtworkBrief
URL := http.GetUserFullArtworkURL(id, ids)
resp, err := http.UnwrapWebAPIRequest(URL, "")
if err != nil {
return nil, err
}
resp = session.ProxyImageUrl(c, resp)
var body struct {
Illusts map[int]json.RawMessage `json:"works"`
}
err = json.Unmarshal([]byte(resp), &body)
if err != nil {
return nil, err
}
for _, v := range body.Illusts {
var illust ArtworkBrief
err = json.Unmarshal(v, &illust)
if err != nil {
return nil, err
}
works = append(works, illust)
}
return works, nil
}
func GetUserArtworksID(c *fiber.Ctx, id, category string, page int) (string, int, error) {
URL := http.GetUserArtworksURL(id)
resp, err := http.UnwrapWebAPIRequest(URL, "")
if err != nil {
return "", -1, err
}
var body struct {
Illusts json.RawMessage `json:"illusts"`
Mangas json.RawMessage `json:"manga"`
}
err = json.Unmarshal([]byte(resp), &body)
if err != nil {
return "", -1, err
}
var ids []int
var idsString string
err = json.Unmarshal([]byte(resp), &body)
if err != nil {
return "", -1, err
}
var illusts map[int]string
var mangas map[int]string
count := 0
if err = json.Unmarshal(body.Illusts, &illusts); err != nil {
illusts = make(map[int]string)
}
if err = json.Unmarshal(body.Mangas, &mangas); err != nil {
mangas = make(map[int]string)
}
// Get the keys, because Pixiv only returns IDs (very evil)
if category == "illustrations" || category == "artworks" {
for k := range illusts {
ids = append(ids, k)
count++
}
}
if category == "manga" || category == "artworks" {
for k := range mangas {
ids = append(ids, k)
count++
}
}
// Reverse sort the ids
sort.Sort(sort.Reverse(sort.IntSlice(ids)))
worksNumber := float64(count)
worksPerPage := 30.0
if page < 1 || float64(page) > math.Ceil(worksNumber/worksPerPage)+1.0 {
return "", -1, errors.New("No page available.")
}
start := (page - 1) * int(worksPerPage)
end := int(min(float64(page)*worksPerPage, worksNumber)) // no overflow
for _, k := range ids[start:end] {
idsString += fmt.Sprintf("&ids[]=%d", k)
}
return idsString, count, nil
}
func GetUserArtwork(c *fiber.Ctx, id, category string, page int) (User, error) {
var user User
token := session.GetToken(c)
URL := http.GetUserInformationURL(id)
resp, err := http.UnwrapWebAPIRequest(URL, token)
if err != nil {
return user, err
}
resp = session.ProxyImageUrl(c, resp)
err = json.Unmarshal([]byte(resp), &user)
if err != nil {
return user, err
}
if category != "bookmarks" {
ids, count, err := GetUserArtworksID(c, id, category, page)
if err != nil {
return user, err
}
if count > 0 {
// Check if the user has artworks available or not
works, err := GetUserArtworks(c, id, ids)
if err != nil {
return user, err
}
// IDK but the order got shuffled even though Pixiv sorted the IDs in the response
sort.Slice(works[:], func(i, j int) bool {
left, _ := strconv.Atoi(works[i].ID)
right, _ := strconv.Atoi(works[j].ID)
return left > right
})
user.Artworks = works
user.FrequentTags, err = GetFrequentTags(c, ids)
if err != nil {
return user, err
}
}
// Artworks count
user.ArtworksCount = count
} else {
// Bookmarks
works, count, err := GetUserBookmarks(c, id, "show", page)
if err != nil {
return user, err
}
user.Artworks = works
// Public bookmarks count
user.ArtworksCount = count
}
user.ParseSocial()
if user.Background != nil {
user.BackgroundImage = user.Background["url"].(string)
}
return user, nil
}
func GetUserBookmarks(c *fiber.Ctx, id, mode string, page int) ([]ArtworkBrief, int, error) {
page--
URL := http.GetUserBookmarksURL(id, mode, page)
resp, err := http.UnwrapWebAPIRequest(URL, "")
if err != nil {
return nil, -1, err
}
resp = session.ProxyImageUrl(c, resp)
var body struct {
Artworks []json.RawMessage `json:"works"`
Total int `json:"total"`
}
err = json.Unmarshal([]byte(resp), &body)
if err != nil {
return nil, -1, err
}
artworks := make([]ArtworkBrief, len(body.Artworks))
for index, value := range body.Artworks {
var artwork ArtworkBrief
err = json.Unmarshal([]byte(value), &artwork)
if err != nil {
artworks[index] = ArtworkBrief{
ID: "#",
Title: "Deleted or Private",
Thumbnail: "https://s.pximg.net/common/images/limit_unknown_360.png",
}
continue
}
artworks[index] = artwork
}
return artworks, body.Total, nil
}

View file

@ -17,4 +17,7 @@ services:
ports:
- "8282:8282"
environment:
# Visit https://codeberg.org/VnPower/PixivFE/wiki/Environment-variables for more details
- PIXIVFE_TOKEN=changethis
- PIXIVFE_PORT=8282
- PIXIVFE_IMAGEPROXY=pximg.cocomi.cf

29
go.mod
View file

@ -1,32 +1,33 @@
module codeberg.org/vnpower/pixivfe
module codeberg.org/vnpower/pixivfe/v2
go 1.21
require (
github.com/goccy/go-json v0.10.2
github.com/gofiber/fiber/v2 v2.47.0
github.com/gofiber/template/jet/v2 v2.1.3
github.com/gofiber/fiber/v2 v2.52.0
github.com/gofiber/template/jet/v2 v2.1.6
github.com/tidwall/gjson v1.17.0
golang.org/x/net v0.17.0
)
require (
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
github.com/CloudyKit/jet/v6 v6.2.0 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/gofiber/template v1.8.2 // indirect
github.com/gofiber/utils v1.1.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tinylib/msgp v1.1.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.47.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/sys v0.15.0 // indirect
)

75
go.sum
View file

@ -2,74 +2,64 @@ github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4s
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME=
github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofiber/fiber/v2 v2.46.0 h1:wkkWotblsGVlLjXj2dpgKQAYHtXumsK/HyFugQM68Ns=
github.com/gofiber/fiber/v2 v2.46.0/go.mod h1:DNl0/c37WLe0g92U6lx1VMQuxGUQY5V7EIaVoEsUffc=
github.com/gofiber/fiber/v2 v2.47.0 h1:EN5lHVCc+Pyqh5OEsk8fzRiifgwpbrP0rulQ4iNf3fs=
github.com/gofiber/fiber/v2 v2.47.0/go.mod h1:mbFMVN1lQuzziTkkakgtKKdjfsXSw9BKR5lmcNksUoU=
github.com/gofiber/fiber/v2 v2.52.0 h1:S+qXi7y+/Pgvqq4DrSmREGiFwtB7Bu6+QFLuIHYw/UE=
github.com/gofiber/fiber/v2 v2.52.0/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofiber/template v1.8.2 h1:PIv9s/7Uq6m+Fm2MDNd20pAFFKt5wWs7ZBd8iV9pWwk=
github.com/gofiber/template v1.8.2/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/jet/v2 v2.1.3 h1:l/mDuBrJAG1z2sPNQ8/Fn8PRX+6ywhhNCtEqUHEPpAE=
github.com/gofiber/template/jet/v2 v2.1.3/go.mod h1:eWS6P1s/VloKrzOIHLkYFOfjI7KAdFb9ZEaLU6Gtca8=
github.com/gofiber/template/jet/v2 v2.1.6 h1:PdNovaGkkMhygGW3NFHPgiq3PtMQxllvGlcUX+MsTok=
github.com/gofiber/template/jet/v2 v2.1.6/go.mod h1:zxeIxxUeD/TMVGlvuxTSyZpSNsT2zGMfOS4SuJ6Z0Q0=
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c=
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -77,12 +67,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
@ -92,9 +78,6 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -1,211 +0,0 @@
package handler
import (
"fmt"
"sort"
"strconv"
"strings"
"codeberg.org/vnpower/pixivfe/models"
"github.com/goccy/go-json"
)
func (p *PixivClient) GetArtworkImages(id string) ([]models.Image, error) {
var resp []models.ImageResponse
var images []models.Image
URL := fmt.Sprintf(ArtworkImagesURL, id)
response, err := p.PixivRequest(URL)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(response), &resp)
if err != nil {
return images, err
}
// Extract and proxy every images
for _, imageRaw := range resp {
var image models.Image
image.Small = imageRaw.Urls["thumb_mini"]
image.Medium = imageRaw.Urls["small"]
image.Large = imageRaw.Urls["regular"]
image.Original = imageRaw.Urls["original"]
images = append(images, image)
}
return images, nil
}
func (p *PixivClient) GetArtworkByID(id string) (*models.Illust, error) {
var images []models.Image
URL := fmt.Sprintf(ArtworkInformationURL, id)
response, err := p.PixivRequest(URL)
if err != nil {
return nil, err
}
var illust struct {
*models.Illust
Recent map[int]any `json:"userIllusts"`
RawTags json.RawMessage `json:"tags"`
}
// Parse basic illust information
err = json.Unmarshal([]byte(response), &illust)
if err != nil {
return nil, err
}
// Begin testing here
c1 := make(chan []models.Image)
c2 := make(chan []models.IllustShort)
c3 := make(chan models.UserShort)
c4 := make(chan []models.Tag)
c5 := make(chan []models.IllustShort)
c6 := make(chan []models.Comment)
go func() {
// Get illust images
images, err = p.GetArtworkImages(id)
if err != nil {
c1 <- nil
}
c1 <- images
}()
go func() {
// Get recent artworks
ids := make([]int, 0)
for k := range illust.Recent {
ids = append(ids, k)
}
sort.Sort(sort.Reverse(sort.IntSlice(ids)))
idsString := ""
count := min(len(ids), 20)
for i := 0; i < count; i++ {
idsString += fmt.Sprintf("&ids[]=%d", ids[i])
}
recent, err := p.GetUserArtworks(illust.UserID, idsString)
if err != nil {
c2 <- nil
}
sort.Slice(recent[:], func(i, j int) bool {
left, _ := strconv.Atoi(recent[i].ID)
right, _ := strconv.Atoi(recent[j].ID)
return left > right
})
c2 <- recent
}()
go func() {
// Get basic user information (the URL above does not contain avatars)
userInfo, err := p.GetUserBasicInformation(illust.UserID)
if err != nil {
//
}
c3 <- userInfo
}()
go func() {
var tagsList []models.Tag
// Extract tags
var tags struct {
Tags []struct {
Tag string `json:"tag"`
Translation map[string]string `json:"translation"`
} `json:"tags"`
}
err = json.Unmarshal(illust.RawTags, &tags)
if err != nil {
c4 <- nil
}
for _, tag := range tags.Tags {
var newTag models.Tag
newTag.Name = tag.Tag
newTag.TranslatedName = tag.Translation["en"]
tagsList = append(tagsList, newTag)
}
c4 <- tagsList
}()
go func() {
related, _ := p.GetRelatedArtworks(id)
// Error handling...
c5 <- related
}()
go func() {
comments, _ := p.GetArtworkComments(id)
// Error handling...
c6 <- comments
}()
illust.Images = <-c1
illust.RecentWorks = <-c2
illust.User = <-c3
illust.Tags = <-c4
illust.RelatedWorks = <-c5
illust.CommentsList = <-c6
// If this artwork is an ugoira
illust.IsUgoira = strings.Contains(illust.Images[0].Original, "ugoira")
return illust.Illust, nil
}
func (p *PixivClient) GetArtworkComments(id string) ([]models.Comment, error) {
var body struct {
Comments []models.Comment `json:"comments"`
}
URL := fmt.Sprintf(ArtworkCommentsURL, id)
response, err := p.PixivRequest(URL)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(response), &body)
if err != nil {
return nil, err
}
return body.Comments, nil
}
func (p *PixivClient) GetRelatedArtworks(id string) ([]models.IllustShort, error) {
var body struct {
Illusts []models.IllustShort `json:"illusts"`
}
URL := fmt.Sprintf(ArtworkRelatedURL, id, 96)
response, err := p.PixivRequest(URL)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(response), &body)
if err != nil {
return nil, err
}
return body.Illusts, nil
}

View file

@ -1,125 +0,0 @@
package handler
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"codeberg.org/vnpower/pixivfe/models"
)
type PixivClient struct {
Client *http.Client
Cookie map[string]string
Header map[string]string
Lang string
}
func (p *PixivClient) SetHeader(header map[string]string) {
p.Header = header
}
func (p *PixivClient) AddHeader(key, value string) {
p.Header[key] = value
}
func (p *PixivClient) SetUserAgent(value string) {
p.AddHeader("User-Agent", value)
}
func (p *PixivClient) SetCookie(cookie map[string]string) {
p.Cookie = cookie
}
func (p *PixivClient) AddCookie(key, value string) {
p.Cookie[key] = value
}
func (p *PixivClient) SetSessionID(value string) {
p.Cookie["PHPSESSID"] = value
}
func (p *PixivClient) SetLang(lang string) {
p.Lang = lang
}
func (p *PixivClient) Request(URL string, token ...string) (*http.Response, error) {
req, _ := http.NewRequest("GET", URL, nil)
// Add headers
for k, v := range p.Header {
req.Header.Add(k, v)
}
for k, v := range p.Cookie {
req.AddCookie(&http.Cookie{Name: k, Value: v})
}
if token != nil {
req.AddCookie(&http.Cookie{Name: "PHPSESSID", Value: token[0]})
}
// Make a request
resp, err := p.Client.Do(req)
if err != nil {
return resp, err
}
if resp.StatusCode != 200 {
return resp, errors.New(fmt.Sprintf("Pixiv returned code: %d for request %s", resp.StatusCode, URL))
}
return resp, nil
}
func (p *PixivClient) TextRequest(URL string, tokens ...string) (string, error) {
var token string
if len(token) > 0 {
token = tokens[0]
}
/// Make a request to a URL and return the response's string body
resp, err := p.Request(URL, token)
if err != nil {
return "", err
}
// Extract the bytes from server's response
body, err := io.ReadAll(resp.Body)
if err != nil {
return string(body), err
}
return string(body), nil
}
func (p *PixivClient) PixivRequest(URL string, tokens ...string) (json.RawMessage, error) {
/// Make a request to a Pixiv API URL with a standard response, handle errors and return the raw JSON response
var response models.PixivResponse
var token string
if len(token) > 0 {
token = tokens[0]
}
body, err := p.TextRequest(URL, token)
// body = strings.ReplaceAll(body, "i.pximg.net", configs.ProxyServer)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(body), &response)
if err != nil {
return nil, err
}
if response.Error {
// Pixiv returned an error
return nil, errors.New("Pixiv responded: " + response.Message)
}
return response.Body, nil
}

View file

@ -1,22 +0,0 @@
package handler
const (
ArtworkInformationURL = "https://www.pixiv.net/ajax/illust/%s"
ArtworkImagesURL = "https://www.pixiv.net/ajax/illust/%s/pages"
ArtworkRelatedURL = "https://www.pixiv.net/ajax/illust/%s/recommend/init?limit=%d"
ArtworkCommentsURL = "https://www.pixiv.net/ajax/illusts/comments/roots?illust_id=%s&limit=100"
ArtworkNewestURL = "https://www.pixiv.net/ajax/illust/new?limit=30&type=%s&r18=%s&lastId=%s"
ArtworkRankingURL = "https://www.pixiv.net/ranking.php?format=json&mode=%s&content=%s%s&p=%s"
ArtworkDiscoveryURL = "https://www.pixiv.net/ajax/discovery/artworks?mode=%s&limit=%d"
SearchTagURL = "https://www.pixiv.net/ajax/search/tags/%s"
SearchArtworksURL = "https://www.pixiv.net/ajax/search/%s/%s?order=%s&mode=%s&p=%s"
SearchTopURL = "https://www.pixiv.net/ajax/search/top/%s"
UserInformationURL = "https://www.pixiv.net/ajax/user/%s?full=1"
UserBasicInformationURL = "https://www.pixiv.net/ajax/user/%s"
UserArtworksURL = "https://www.pixiv.net/ajax/user/%s/profile/all"
UserArtworksFullURL = "https://www.pixiv.net/ajax/user/%s/profile/illusts?work_category=illustManga&is_first_page=0&lang=en%s"
UserBookmarksURL = "https://www.pixiv.net/ajax/user/%s/illusts/bookmarks?tag=&offset=%d&limit=48&rest=%s"
FrequentTagsURL = "https://www.pixiv.net/ajax/tags/frequent/illust?%s"
LandingPageURL = "https://www.pixiv.net/ajax/top/illust?mode=%s"
NewestFromFollowURL = "https://www.pixiv.net/ajax/follow_latest/%s?mode=%s&p=%s"
)

View file

@ -1,219 +0,0 @@
package handler
import (
"fmt"
"html/template"
"net/http"
"strings"
"time"
"codeberg.org/vnpower/pixivfe/models"
"github.com/goccy/go-json"
"golang.org/x/net/html"
)
func get_weekday(n time.Weekday) int {
switch n {
case time.Sunday:
return 1
case time.Monday:
return 2
case time.Tuesday:
return 3
case time.Wednesday:
return 4
case time.Thursday:
return 5
case time.Friday:
return 6
case time.Saturday:
return 7
}
return 0
}
func (p *PixivClient) GetNewestArtworks(worktype string, r18 string) ([]models.IllustShort, error) {
var newWorks []models.IllustShort
lastID := "0"
for i := 0; i < 10; i++ {
URL := fmt.Sprintf(ArtworkNewestURL, worktype, r18, lastID)
response, err := p.PixivRequest(URL)
if err != nil {
return nil, err
}
var body struct {
Illusts []models.IllustShort `json:"illusts"`
LastID string `json:"lastId"`
}
err = json.Unmarshal([]byte(response), &body)
if err != nil {
return nil, err
}
newWorks = append(newWorks, body.Illusts...)
lastID = body.LastID
}
return newWorks, nil
}
func (p *PixivClient) GetRanking(mode string, content string, date string, page string) (models.RankingResponse, error) {
// Ranking data is formatted differently
var pr models.RankingResponse
if len(date) > 0 {
date = "&date=" + date
}
url := fmt.Sprintf(ArtworkRankingURL, mode, content, date, page)
s, err := p.TextRequest(url)
if err != nil {
return pr, err
}
err = json.Unmarshal([]byte(s), &pr)
if err != nil {
return pr, err
}
pr.PrevDate = strings.ReplaceAll(string(pr.PrevDateRaw[:]), "\"", "")
pr.NextDate = strings.ReplaceAll(string(pr.NextDateRaw[:]), "\"", "")
return pr, nil
}
func (p *PixivClient) GetSearch(artworkType string, name string, order string, age_settings string, page string) (*models.SearchResult, error) {
URL := fmt.Sprintf(SearchArtworksURL, artworkType, name, order, age_settings, page)
response, err := p.PixivRequest(URL)
if err != nil {
return nil, err
}
// IDK how to do better than this lol
temp := strings.ReplaceAll(string(response), `"illust"`, `"works"`)
temp = strings.ReplaceAll(temp, `"manga"`, `"works"`)
temp = strings.ReplaceAll(temp, `"illustManga"`, `"works"`)
var resultRaw struct {
*models.SearchResult
ArtworksRaw json.RawMessage `json:"works"`
}
var artworks models.SearchArtworks
var result *models.SearchResult
err = json.Unmarshal([]byte(temp), &resultRaw)
if err != nil {
return nil, err
}
result = resultRaw.SearchResult
err = json.Unmarshal([]byte(resultRaw.ArtworksRaw), &artworks)
if err != nil {
return nil, err
}
result.Artworks = artworks
return result, nil
}
func (p *PixivClient) GetDiscoveryArtwork(mode string, count int) ([]models.IllustShort, error) {
var artworks []models.IllustShort
for count > 0 {
itemsForRequest := min(100, count)
count -= itemsForRequest
URL := fmt.Sprintf(ArtworkDiscoveryURL, mode, itemsForRequest)
response, err := p.PixivRequest(URL)
if err != nil {
return nil, err
}
var thumbnail struct {
Data json.RawMessage `json:"thumbnails"`
}
err = json.Unmarshal([]byte(response), &thumbnail)
if err != nil {
return nil, err
}
var body struct {
Artworks []models.IllustShort `json:"illust"`
}
err = json.Unmarshal([]byte(thumbnail.Data), &body)
if err != nil {
return nil, err
}
artworks = append(artworks, body.Artworks...)
}
return artworks, nil
}
func (p *PixivClient) GetRankingLog(mode string, year, month int, image_proxy string) (template.HTML, error) {
url := fmt.Sprintf("https://www.pixiv.net/ranking_log.php?mode=%s&date=%d%02d", mode, year, month)
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
// Use the html package to parse the response body from the request
doc, err := html.Parse(resp.Body)
if err != nil {
return "", err
}
// Find and print all links on the web page
var links []string
var link func(*html.Node)
link = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "img" {
for _, a := range n.Attr {
if a.Key == "data-src" {
// adds a new link entry when the attribute matches
links = append(links, models.ProxyImage(a.Val, image_proxy))
}
}
}
// traverses the HTML of the webpage from the first child node
for c := n.FirstChild; c != nil; c = c.NextSibling {
link(c)
}
}
link(doc)
// now := time.Now()
// yearNow := now.Year()
// monthNow := now.Month()
lastMonth := time.Date(year, time.Month(month), 0, 0, 0, 0, 0, time.UTC)
thisMonth := time.Date(year, time.Month(month+1), 0, 0, 0, 0, 0, time.UTC)
renderString := ""
for i := 0; i < get_weekday(lastMonth.Weekday()); i++ {
renderString += "<div class=\"calendar-node calendar-node-empty\"></div>"
}
for i := 0; i < thisMonth.Day(); i++ {
date := fmt.Sprintf("%d%02d%02d", year, month, i+1)
if len(links) > i {
renderString += fmt.Sprintf(`<a href="/ranking?mode=%s&date=%s"><div class="calendar-node" style="background-image: url(%s)"><span>%d</span></div></a>`, mode, date, links[i], i+1)
} else {
renderString += fmt.Sprintf(`<div class="calendar-node"><span>%d</span></div>`, i+1)
}
}
return template.HTML(renderString), nil
}

View file

@ -1,64 +0,0 @@
package handler
import (
"fmt"
"codeberg.org/vnpower/pixivfe/models"
"github.com/goccy/go-json"
)
func (p *PixivClient) GetNewestFromFollowing(mode, page, token string) ([]models.IllustShort, error) {
URL := fmt.Sprintf(NewestFromFollowURL, "illust", mode, page)
var body struct {
Thumbnails json.RawMessage `json:"thumbnails"`
}
var artworks struct {
Artworks []models.IllustShort `json:"illust"`
}
response, err := p.PixivRequest(URL, token)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(response), &body)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(body.Thumbnails), &artworks)
if err != nil {
return nil, err
}
return artworks.Artworks, nil
}
// func (p *PixivClient) FollowUser(id string) error {
// formData := url.Values{}
// formData.Add("mode", "add")
// formData.Add("type", "user")
// formData.Add("user_id", id)
// formData.Add("tag", "")
// formData.Add("restrict", "0")
// formData.Add("format", "json")
// init, err := p.GetCSRF()
// println(init)
// if err != nil {
// return err
// }
// pattern := regexp.MustCompile(`.*pixiv.context.token = "([a-z0-9]{32})"?.*`)
// quotesPattern := regexp.MustCompile(`([a-z0-9]{32})`)
// token := quotesPattern.FindString(pattern.FindString(init))
// println(token)
// _, err = p.RequestWithFormData(FollowUserURL, formData, token)
// if err != nil {
// return err
// }
// return nil
// }

View file

@ -1,44 +0,0 @@
package handler
import (
"fmt"
"codeberg.org/vnpower/pixivfe/models"
"github.com/goccy/go-json"
)
func (p *PixivClient) GetTagData(name string) (models.TagDetail, error) {
var tag models.TagDetail
URL := fmt.Sprintf(SearchTagURL, name)
response, err := p.PixivRequest(URL)
if err != nil {
return tag, err
}
err = json.Unmarshal([]byte(response), &tag)
if err != nil {
return tag, err
}
return tag, nil
}
func (p *PixivClient) GetFrequentTags(ids string) ([]models.FrequentTag, error) {
var tags []models.FrequentTag
URL := fmt.Sprintf(FrequentTagsURL, ids)
response, err := p.PixivRequest(URL)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(response), &tags)
if err != nil {
return nil, err
}
return tags, nil
}

View file

@ -1,95 +0,0 @@
package handler
import (
"fmt"
"codeberg.org/vnpower/pixivfe/models"
"github.com/goccy/go-json"
)
func (p *PixivClient) GetLandingPage(mode string) (models.LandingArtworks, error) {
var context models.LandingArtworks
URL := fmt.Sprintf(LandingPageURL, mode)
response, err := p.PixivRequest(URL)
if err != nil {
return context, err
}
type IDS struct {
Ids []any `json:"ids"`
}
var pages struct {
Pixivision []models.Pixivision `json:"pixivision"`
Follow []any `json:"follow"`
Commission []any `json:"completeRequestIds"`
Newest []any `json:"newPost"`
Recommended IDS `json:"recommend"`
EditorRecommended []any `json:"editorRecommend"`
UserRecommended []any `json:"recommendUser"`
RecommendedByTags []struct {
models.LandingRecommendByTags
Ids []any `json:"ids"`
} `json:"recommendByTag"`
}
var body struct {
Thumbnails json.RawMessage `json:"thumbnails"`
Page json.RawMessage `json:"page"`
}
var artworks struct {
Artworks []models.IllustShort `json:"illust"`
}
err = json.Unmarshal([]byte(response), &body)
if err != nil {
return context, err
}
err = json.Unmarshal([]byte(body.Thumbnails), &artworks)
if err != nil {
return context, err
}
err = json.Unmarshal([]byte(body.Page), &pages)
if err != nil {
return context, err
}
context.Pixivision = pages.Pixivision
// Keep track
count := len(pages.Commission)
context.Commissions = artworks.Artworks[:count]
context.Following = artworks.Artworks[count : count+len(pages.Follow)]
count += len(pages.Follow)
context.Recommended = artworks.Artworks[count : count+len(pages.Recommended.Ids)]
count += len(pages.Recommended.Ids)
context.Newest = artworks.Artworks[count : count+len(pages.Newest)]
count += len(pages.Newest)
// For rankings, we just take 100 anyway
context.Rankings = artworks.Artworks[count : count+100]
count += 100
// IDK what this is
count += len(pages.EditorRecommended)
context.Users = artworks.Artworks[count : count+len(pages.UserRecommended)*3]
count += len(pages.UserRecommended) * 3
for i := 0; i < len(pages.RecommendedByTags); i++ {
temp := pages.RecommendedByTags[i]
temp.Artworks = artworks.Artworks[count : count+min(len(temp.Ids), 18)]
context.RecommendByTags = append(context.RecommendByTags, temp.LandingRecommendByTags)
count += len(temp.Ids)
}
return context, nil
}

View file

@ -1,240 +0,0 @@
package handler
import (
"errors"
"fmt"
"math"
"sort"
"strconv"
"codeberg.org/vnpower/pixivfe/models"
"github.com/goccy/go-json"
)
func (p *PixivClient) GetUserArtworksID(id string, category string, page int) (string, int, error) {
URL := fmt.Sprintf(UserArtworksURL, id)
response, err := p.PixivRequest(URL)
if err != nil {
return "", -1, err
}
var ids []int
var idsString string
var body struct {
Illusts json.RawMessage `json:"illusts"`
Mangas json.RawMessage `json:"manga"`
}
err = json.Unmarshal(response, &body)
if err != nil {
return "", -1, err
}
var illusts map[int]string
var mangas map[int]string
count := 0
if err = json.Unmarshal(body.Illusts, &illusts); err != nil {
illusts = make(map[int]string)
}
if err = json.Unmarshal(body.Mangas, &mangas); err != nil {
mangas = make(map[int]string)
}
// Get the keys, because Pixiv only returns IDs (very evil)
if category == "illustrations" || category == "artworks" {
for k := range illusts {
ids = append(ids, k)
count++
}
}
if category == "manga" || category == "artworks" {
for k := range mangas {
ids = append(ids, k)
count++
}
}
// Reverse sort the ids
sort.Sort(sort.Reverse(sort.IntSlice(ids)))
worksNumber := float64(count)
worksPerPage := 30.0
if page < 1 || float64(page) > math.Ceil(worksNumber/worksPerPage)+1.0 {
return "", -1, errors.New("Page overflow")
}
start := (page - 1) * int(worksPerPage)
end := int(min(float64(page)*worksPerPage, worksNumber)) // no overflow
for _, k := range ids[start:end] {
idsString += fmt.Sprintf("&ids[]=%d", k)
}
return idsString, count, nil
}
func (p *PixivClient) GetUserArtworks(id string, ids string) ([]models.IllustShort, error) {
var works []models.IllustShort
URL := fmt.Sprintf(UserArtworksFullURL, id, ids)
response, err := p.PixivRequest(URL)
if err != nil {
return nil, err
}
var body struct {
Illusts map[int]json.RawMessage `json:"works"`
}
err = json.Unmarshal(response, &body)
if err != nil {
return nil, err
}
for _, v := range body.Illusts {
var illust models.IllustShort
err = json.Unmarshal(v, &illust)
if err != nil {
return nil, err
}
works = append(works, illust)
}
return works, nil
}
func (p *PixivClient) GetUserBasicInformation(id string) (models.UserShort, error) {
var user models.UserShort
URL := fmt.Sprintf(UserBasicInformationURL, id)
response, err := p.PixivRequest(URL)
if err != nil {
return user, err
}
err = json.Unmarshal([]byte(response), &user)
if err != nil {
return user, err
}
return user, nil
}
func (p *PixivClient) GetUserInformation(id string, category string, page int) (*models.User, error) {
var user *models.User
URL := fmt.Sprintf(UserInformationURL, id)
response, err := p.PixivRequest(URL)
if err != nil {
return user, err
}
var body struct {
*models.User
Background map[string]interface{} `json:"background"`
}
// Basic user information
err = json.Unmarshal([]byte(response), &body)
if err != nil {
return nil, err
}
user = body.User
if category != "bookmarks" {
// Artworks
ids, count, err := p.GetUserArtworksID(id, category, page)
if err != nil {
return nil, err
}
works, _ := p.GetUserArtworks(id, ids)
// IDK but the order got shuffled even though Pixiv sorted the IDs in the response
sort.Slice(works[:], func(i, j int) bool {
left, _ := strconv.Atoi(works[i].ID)
right, _ := strconv.Atoi(works[j].ID)
return left > right
})
user.Artworks = works
// Artworks count
user.ArtworksCount = count
// Frequent tags
user.FrequentTags, err = p.GetFrequentTags(ids)
if err != nil {
return nil, err
}
} else {
// Bookmarks
works, count, err := p.GetUserBookmarks(id, "show", page)
if err != nil {
return nil, err
}
user.Artworks = works
// Public bookmarks count
user.ArtworksCount = count
// Parse social medias
}
user.ParseSocial()
// Background image
if body.Background != nil {
user.BackgroundImage = body.Background["url"].(string)
}
return user, nil
}
func (p *PixivClient) GetUserBookmarks(id string, mode string, page int) ([]models.IllustShort, int, error) {
page--
URL := fmt.Sprintf(UserBookmarksURL, id, page*48, mode)
response, err := p.PixivRequest(URL)
if err != nil {
return nil, -1, err
}
var body struct {
Artworks []json.RawMessage `json:"works"`
Total int `json:"total"`
}
err = json.Unmarshal([]byte(response), &body)
if err != nil {
return nil, -1, err
}
artworks := make([]models.IllustShort, len(body.Artworks))
for index, value := range body.Artworks {
var artwork models.IllustShort
err = json.Unmarshal([]byte(value), &artwork)
if err != nil {
artworks[index] = models.IllustShort{
ID: "#",
Title: "Deleted or Private",
Thumbnail: "https://s.pximg.net/common/images/limit_unknown_360.png",
}
continue
}
artworks[index] = artwork
}
return artworks, body.Total, nil
}

188
main.go
View file

@ -1,28 +1,50 @@
package main
import (
"errors"
"log"
"net"
"net/http"
"os"
"strings"
"time"
"codeberg.org/vnpower/pixivfe/configs"
"codeberg.org/vnpower/pixivfe/handler"
"codeberg.org/vnpower/pixivfe/views"
config "codeberg.org/vnpower/pixivfe/v2/core/config"
"codeberg.org/vnpower/pixivfe/v2/pages"
"codeberg.org/vnpower/pixivfe/v2/serve"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cache"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/fiber/v2/utils"
"github.com/gofiber/template/jet/v2"
)
func setup_router() *fiber.App {
// HTML templates, automatically loaded
engine := jet.New("./template", ".jet.html")
func CanRequestSkipLimiter(c *fiber.Ctx) bool {
path := c.Path()
return strings.HasPrefix(path, "/assets/") ||
strings.HasPrefix(path, "/css/") ||
strings.HasPrefix(path, "/js/") ||
strings.HasPrefix(path, "/proxy/s.pximg.net/")
}
engine.AddFuncMap(handler.GetTemplateFunctions())
func main() {
config.SetupStorage()
config.GlobalServerConfig.InitializeConfig()
engine := jet.New("./views", ".jet.html")
engine.AddFuncMap(serve.GetTemplateFunctions())
if config.GlobalServerConfig.InDevelopment {
engine.Reload(true)
}
// // no error even if the templates are invalid???
// err := engine.Load()
// if err != nil {
// panic(err)
// }
server := fiber.New(fiber.Config{
AppName: "PixivFE",
@ -57,37 +79,52 @@ func setup_router() *fiber.App {
},
})
server.Use(logger.New())
server.Use(cache.New(
cache.Config{
Next: func(c *fiber.Ctx) bool {
resp_code := c.Response().StatusCode()
if resp_code < 200 || resp_code >= 300 {
return true
}
if c.Path() == "/" {
return true
}
// Disable cache for settings page
if strings.Contains(c.Path(), "/settings") {
return true
}
return false
},
Expiration: 5 * time.Minute,
CacheControl: true,
KeyGenerator: func(c *fiber.Ctx) string {
return utils.CopyString(c.OriginalURL())
},
server.Use(logger.New(
logger.Config{
Format: "${time} ${ip} | ${path}\n",
Next: CanRequestSkipLimiter,
},
))
server.Use(recover.New())
if !config.GlobalServerConfig.InDevelopment {
server.Use(cache.New(
cache.Config{
Next: func(c *fiber.Ctx) bool {
resp_code := c.Response().StatusCode()
if resp_code < 200 || resp_code >= 300 {
return true
}
// Disable cache for settings page
return strings.Contains(c.Path(), "/settings") || c.Path() == "/"
},
Expiration: 5 * time.Minute,
CacheControl: true,
KeyGenerator: func(c *fiber.Ctx) string {
return utils.CopyString(c.OriginalURL())
},
},
))
}
server.Use(recover.New(recover.Config{EnableStackTrace: config.GlobalServerConfig.InDevelopment}))
server.Use(compress.New(compress.Config{
Level: compress.LevelBestSpeed, // 1
}))
server.Use(limiter.New(limiter.Config{
Next: CanRequestSkipLimiter,
Expiration: 30 * time.Second,
Max: config.GlobalServerConfig.RequestLimit,
LimiterMiddleware: limiter.SlidingWindow{},
LimitReached: func(c *fiber.Ctx) error {
log.Println("Limit Reached!")
return errors.New("Woah! You are going too fast! I'll have to keep an eye on you.")
},
}))
// Global headers (from GotHub)
server.Use(func(c *fiber.Ctx) error {
c.Set("X-Frame-Options", "SAMEORIGIN")
@ -100,46 +137,77 @@ func setup_router() *fiber.App {
})
server.Use(func(c *fiber.Ctx) error {
var baseURL string
if configs.BaseURL != "localhost" {
baseURL = "https://" + configs.BaseURL
}
c.Bind(fiber.Map{"FullURL": baseURL + c.OriginalURL(), "BaseURL": baseURL})
baseURL := c.BaseURL() + c.OriginalURL()
c.Bind(fiber.Map{"BaseURL": baseURL})
return c.Next()
})
// Static files
server.Static("/favicon.ico", "./template/favicon.ico")
server.Static("css/", "./template/css")
server.Static("assets/", "./template/assets")
server.Static("/robots.txt", "./template/robots.txt")
server.Static("/favicon.ico", "./views/assets/favicon.ico")
server.Static("/robots.txt", "./views/assets/robots.txt")
server.Static("/assets/", "./views/assets")
server.Static("/css/", "./views/css")
server.Static("/js/", "./views/js")
// Routes/Views
views.SetupRoutes(server)
// Routes
// Disable trusted proxies since we do not use any for now
// server.SetTrustedProxies(nil)
server.Get("/", pages.IndexPage)
server.Get("/about", pages.AboutPage)
server.Get("/newest", pages.NewestPage)
server.Get("/discovery", pages.DiscoveryPage)
server.Get("/ranking", pages.RankingPage)
server.Get("/rankingCalendar", pages.RankingCalendarPage)
server.Post("/rankingCalendar", pages.RankingCalendarPicker)
server.Get("/users/:id/:category?", pages.UserPage)
server.Get("/artworks/:id/", pages.ArtworkPage).Name("artworks")
server.Get("/artworks-multi/:ids/", pages.ArtworkMultiPage)
return server
}
// Settings group
settings := server.Group("/settings")
settings.Get("/", pages.SettingsPage)
settings.Post("/:type", pages.SettingsPost)
func main() {
err := configs.ParseConfig()
configs.SetupStorage()
// Personal group
self := server.Group("/self")
self.Get("/", pages.LoginUserPage)
self.Get("/followingWorks", pages.FollowingWorksPage)
self.Get("/bookmarks", pages.LoginBookmarkPage)
self.Post("/addBookmark/:id", pages.AddBookmarkRoute)
self.Post("/deleteBookmark/:id", pages.DeleteBookmarkRoute)
self.Post("/like/:id", pages.LikeRoute)
if err != nil {
panic(err)
}
server.Get("/tags/:name", pages.TagPage)
server.Post("/tags",
func(c *fiber.Ctx) error {
name := c.FormValue("name")
r := setup_router()
return c.Redirect("/tags/"+name, http.StatusFound)
})
if strings.Contains(configs.Port, "/") {
ln, err := net.Listen("unix", configs.Port)
// Legacy illust URL
server.Get("/member_illust.php", func(c *fiber.Ctx) error {
return c.Redirect("/artworks/" + c.Query("illust_id"))
})
// Proxy routes
proxy := server.Group("/proxy")
proxy.Get("/i.pximg.net/*", pages.IPximgProxy)
proxy.Get("/s.pximg.net/*", pages.SPximgProxy)
proxy.Get("/ugoira.com/*", pages.UgoiraProxy)
// Listen
if config.GlobalServerConfig.UnixSocket != "" {
ln, err := net.Listen("unix", config.GlobalServerConfig.UnixSocket)
if err != nil {
panic("Failed to listen to " + configs.Port)
log.Fatalf("Failed to run on Unix socket. %s", err)
os.Exit(1)
}
r.Listener(ln)
log.Printf("PixivFE is running on %v\n", config.GlobalServerConfig.UnixSocket)
server.Listener(ln)
} else {
addr := config.GlobalServerConfig.Host + ":" + config.GlobalServerConfig.Port
log.Printf("PixivFE is running on %v\n", addr)
// note: string concatenation is very flaky
server.Listen(addr)
}
println("PixivFE is up and running on port " + configs.Port + "!")
r.Listen(":" + configs.Port)
}

View file

@ -1,56 +0,0 @@
package models
import (
"regexp"
"strings"
)
func ProxyImage(URL string, target string) string {
if strings.Contains(URL, "s.pximg.net") {
// This subdomain didn't get proxied
return URL
}
regex := regexp.MustCompile(`.*?pximg\.net`)
proxy := "https://" + target
return regex.ReplaceAllString(URL, proxy)
}
func ProxyShortArtworkSlice(artworks []IllustShort, proxy string) []IllustShort {
for i := range artworks {
artworks[i].Thumbnail = ProxyImage(artworks[i].Thumbnail, proxy)
artworks[i].ArtistAvatar = ProxyImage(artworks[i].ArtistAvatar, proxy)
}
return artworks
}
func ProxyRecommendedByTagsSlice(artworks []LandingRecommendByTags, proxy string) []LandingRecommendByTags {
for i := range artworks {
artworks[i].Artworks = ProxyShortArtworkSlice(artworks[i].Artworks, proxy)
}
return artworks
}
func ProxyRankedArtworkSlice(artworks []RankedArtwork, proxy string) []RankedArtwork {
for i := range artworks {
artworks[i].Image = ProxyImage(artworks[i].Image, proxy)
artworks[i].ArtistAvatar = ProxyImage(artworks[i].ArtistAvatar, proxy)
}
return artworks
}
func ProxyCommentsSlice(comments []Comment, proxy string) []Comment {
for i := range comments {
comments[i].Avatar = ProxyImage(comments[i].Avatar, proxy)
}
return comments
}
func ProxyPixivisionSlice(articles []Pixivision, proxy string) []Pixivision {
for i := range articles {
articles[i].Thumbnail = ProxyImage(articles[i].Thumbnail, proxy)
}
return articles
}

View file

@ -1,268 +0,0 @@
package models
import (
"html/template"
"time"
"encoding/json"
)
type PaginationData struct {
PreviousPage int
CurrentPage int
NextPage int
}
type PixivResponse struct {
Error bool
Message string
Body json.RawMessage
}
type RankingResponse struct {
Artworks []RankedArtwork `json:"contents"`
Mode string `json:"mode"`
Content string `json:"content"`
CurrentDate string `json:"date"`
PrevDateRaw json.RawMessage `json:"prev_date"`
NextDateRaw json.RawMessage `json:"next_date"`
PrevDate string
NextDate string
}
func (s *RankingResponse) ProxyImages(proxy string) {
s.Artworks = ProxyRankedArtworkSlice(s.Artworks, proxy)
}
type ImageResponse struct {
Urls map[string]string `json:"urls"`
}
type TagResponse struct {
AuthorID string `json:"authorId"`
RawTags json.RawMessage `json:"tags"`
}
// Pixiv returns 0, 1, 2 to filter SFW and/or NSFW artworks.
// Those values are saved in `xRestrict`
// 0: Safe
// 1: R18
// 2: R18G
type xRestrict int
const (
Safe xRestrict = 0
R18 xRestrict = 1
R18G xRestrict = 2
)
var xRestrictModel = map[xRestrict]string{
Safe: "",
R18: "R18",
R18G: "R18G",
}
// Pixiv returns 0, 1, 2 to filter SFW and/or NSFW artworks.
// Those values are saved in `aiType`
// 0: Not rated / Unknown
// 1: Not AI-generated
// 2: AI-generated
type aiType int
const (
Unrated aiType = 0
NotAI aiType = 1
AI aiType = 2
)
var aiTypeModel = map[aiType]string{
Unrated: "Unrated",
NotAI: "Not AI",
AI: "AI",
}
// Pixiv gives us 5 types of an image. I don't need the mini one tho.
// PS: Where tf is my 360x360 image, Pixiv?
type Image struct {
Small string `json:"thumb_mini"`
Medium string `json:"small"`
Large string `json:"regular"`
Original string `json:"original"`
}
type Tag struct {
Name string `json:"tag"`
TranslatedName string `json:"translation"`
}
type FrequentTag struct {
Name string `json:"tag"`
TranslatedName string `json:"tag_translation"`
}
type Illust struct {
ID string `json:"id"`
Title string `json:"title"`
Description template.HTML `json:"description"`
UserID string `json:"userId"`
UserName string `json:"userName"`
UserAccount string `json:"userAccount"`
Date time.Time `json:"uploadDate"`
Images []Image `json:"images"`
Tags []Tag `json:"tags"`
Pages int `json:"pageCount"`
Bookmarks int `json:"bookmarkCount"`
Likes int `json:"likeCount"`
Comments int `json:"commentCount"`
Views int `json:"viewCount"`
CommentDisabled int `json:"commentOff"`
SanityLevel int `json:"sl"`
XRestrict xRestrict `json:"xRestrict"`
AiType aiType `json:"aiType"`
User UserShort
RecentWorks []IllustShort
RelatedWorks []IllustShort
CommentsList []Comment
IsUgoira bool
}
func (s *Illust) ProxyImages(proxy string) {
for i := range s.Images {
s.Images[i].Small = ProxyImage(s.Images[i].Small, proxy)
s.Images[i].Medium = ProxyImage(s.Images[i].Medium, proxy)
s.Images[i].Large = ProxyImage(s.Images[i].Large, proxy)
s.Images[i].Original = ProxyImage(s.Images[i].Original, proxy)
}
for i := range s.RecentWorks {
s.RecentWorks[i].Thumbnail = ProxyImage(s.RecentWorks[i].Thumbnail, proxy)
}
s.RelatedWorks = ProxyShortArtworkSlice(s.RelatedWorks, proxy)
s.CommentsList = ProxyCommentsSlice(s.CommentsList, proxy)
s.User.Avatar = ProxyImage(s.User.Avatar, proxy)
}
type IllustShort struct {
ID string `json:"id"`
Title string `json:"title"`
Description template.HTML `json:"description"`
ArtistID string `json:"userId"`
ArtistName string `json:"userName"`
ArtistAvatar string `json:"profileImageUrl"`
Date time.Time `json:"uploadDate"`
Thumbnail string `json:"url"`
Pages int `json:"pageCount"`
XRestrict xRestrict `json:"xRestrict"`
AiType aiType `json:"aiType"`
}
type Comment struct {
AuthorID string `json:"userId"`
AuthorName string `json:"userName"`
Avatar string `json:"img"`
Context string `json:"comment"`
Stamp string `json:"stampId"`
Date string `json:"commentDate"`
}
type User struct {
ID string `json:"userId"`
Name string `json:"name"`
Avatar string `json:"imageBig"`
BackgroundImage string `json:"background"`
Following int `json:"following"`
MyPixiv int `json:"mypixivCount"`
Comment template.HTML `json:"commentHtml"`
Webpage string `json:"webpage"`
SocialRaw json.RawMessage `json:"social"`
Artworks []IllustShort `json:"artworks"`
ArtworksCount int
FrequentTags []FrequentTag
Social map[string]map[string]string
}
func (s *User) ProxyImages(proxy string) {
s.Avatar = ProxyImage(s.Avatar, proxy)
s.BackgroundImage = ProxyImage(s.BackgroundImage, proxy)
s.Artworks = ProxyShortArtworkSlice(s.Artworks, proxy)
}
func (s *User) ParseSocial() {
if string(s.SocialRaw[:]) == "[]" {
// Fuck Pixiv
return
}
_ = json.Unmarshal(s.SocialRaw, &s.Social)
}
type UserShort struct {
ID string `json:"userId"`
Name string `json:"name"`
Avatar string `json:"imageBig"`
}
type RankedArtwork struct {
ID int `json:"illust_id"`
Title string `json:"title"`
Rank int `json:"rank"`
Pages string `json:"illust_page_count"`
Image string `json:"url"`
ArtistID int `json:"user_id"`
ArtistName string `json:"user_name"`
ArtistAvatar string `json:"profile_img"`
}
type TagDetail struct {
Name string `json:"tag"`
AlternativeName string `json:"word"`
Metadata struct {
Detail string `json:"abstract"`
Image string `json:"image"`
Name string `json:"tag"`
ID json.Number `json:"id"`
} `json:"pixpedia"`
}
type SearchArtworks struct {
Artworks []IllustShort `json:"data"`
Total int `json:"total"`
}
type SearchResult struct {
Artworks SearchArtworks
Popular struct {
Permanent []IllustShort `json:"permanent"`
Recent []IllustShort `json:"recent"`
} `json:"popular"`
RelatedTags []string `json:"relatedTags"`
}
func (s *SearchResult) ProxyImages(proxy string) {
s.Artworks.Artworks = ProxyShortArtworkSlice(s.Artworks.Artworks, proxy)
s.Popular.Permanent = ProxyShortArtworkSlice(s.Popular.Permanent, proxy)
s.Popular.Recent = ProxyShortArtworkSlice(s.Popular.Recent, proxy)
}
type Pixivision struct {
ID string `json:"id"`
Title string `json:"title"`
Thumbnail string `json:"thumbnailUrl"`
URL string `json:"url"`
}
type LandingRecommendByTags struct {
Name string `json:"tag"`
Artworks []IllustShort
}
type LandingArtworks struct {
Commissions []IllustShort
Following []IllustShort
Recommended []IllustShort
Newest []IllustShort
Rankings []IllustShort
Users []IllustShort
Pixivision []Pixivision
RecommendByTags []LandingRecommendByTags
}

View file

@ -1,32 +0,0 @@
server {
server_name changethis;
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
ssl_certificate /etc/letsencrypt/live/changethis/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/changethis/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
resolver 1.1.1.1;
ssl_trusted_certificate /etc/letsencrypt/live/changethis/chain.pem;
ssl_stapling on;
ssl_stapling_verify on;
access_log /dev/null;
error_log /dev/null;
location / {
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://localhost:8282;
}
}
server {
listen 80;
listen [::]:80;
server_name changethis;
return 301 https://changethis$request_uri;
}

16
pages/about.go Normal file
View file

@ -0,0 +1,16 @@
package pages
import (
"codeberg.org/vnpower/pixivfe/v2/core/config"
"github.com/gofiber/fiber/v2"
)
func AboutPage(c *fiber.Ctx) error {
info := fiber.Map{
"Time": core.GlobalServerConfig.StartingTime,
"Version": core.GlobalServerConfig.Version,
"ImageProxy": core.GlobalServerConfig.ProxyServer,
"AcceptLanguage": core.GlobalServerConfig.AcceptLanguage,
}
return c.Render("pages/about", info)
}

122
pages/actions.go Normal file
View file

@ -0,0 +1,122 @@
package pages
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
session "codeberg.org/vnpower/pixivfe/v2/core/config"
"github.com/gofiber/fiber/v2"
"github.com/tidwall/gjson"
)
func pixivPostRequest(url, payload, token, csrf string) error {
requestBody := []byte(payload)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(requestBody))
req.Header.Add("User-Agent", "Mozilla/5.0")
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json; charset=utf-8")
req.Header.Add("Cookie", "PHPSESSID="+token)
req.Header.Add("x-csrf-token", csrf)
// req.AddCookie(&http.Cookie{
// Name: "PHPSESSID",
// Value: token,
// })
resp, err := http.DefaultClient.Do(req)
if err != nil {
return errors.New("Failed to do this action.")
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return errors.New("Cannot parse the response from Pixiv. Please report this issue.")
}
errr := gjson.Get(string(body), "error")
if !errr.Exists() {
return errors.New("Incompatible request body.")
}
if errr.Bool() {
return errors.New("Pixiv: Invalid request.")
}
return nil
}
func AddBookmarkRoute(c *fiber.Ctx) error {
token := session.CheckToken(c)
csrf := session.GetCSRFToken(c)
if token == "" || csrf == "" {
return c.Redirect("/login")
}
id := c.Params("id")
if id == "" {
return errors.New("No ID provided.")
}
URL := "https://www.pixiv.net/ajax/illusts/bookmarks/add"
payload := fmt.Sprintf(`{
"illust_id": "%s",
"restrict": 0,
"comment": "",
"tags": []
}`, id)
if err := pixivPostRequest(URL, payload, token, csrf); err != nil {
return err
}
return c.SendString("Success")
}
func DeleteBookmarkRoute(c *fiber.Ctx) error {
token := session.CheckToken(c)
csrf := session.GetCSRFToken(c)
if token == "" || csrf == "" {
return c.Redirect("/login")
}
id := c.Params("id")
if id == "" {
return errors.New("No ID provided.")
}
// You can't unlike
URL := "https://www.pixiv.net/ajax/illusts/bookmarks/delete"
payload := fmt.Sprintf(`bookmark_id=%s`, id)
if err := pixivPostRequest(URL, payload, token, csrf); err != nil {
return err
}
return c.SendString("Success")
}
func LikeRoute(c *fiber.Ctx) error {
token := session.CheckToken(c)
csrf := session.GetCSRFToken(c)
if token == "" || csrf == "" {
return c.Redirect("/login")
}
id := c.Params("id")
if id == "" {
return errors.New("No ID provided.")
}
URL := "https://www.pixiv.net/ajax/illusts/like"
payload := fmt.Sprintf(`{"illust_id": "%s"}`, id)
if err := pixivPostRequest(URL, payload, token, csrf); err != nil {
return err
}
return c.SendString("Success")
}

56
pages/artwork-multi.go Normal file
View file

@ -0,0 +1,56 @@
package pages
import (
"errors"
"strconv"
"strings"
"sync"
core "codeberg.org/vnpower/pixivfe/v2/core/webapi"
"github.com/gofiber/fiber/v2"
)
func ArtworkMultiPage(c *fiber.Ctx) error {
param_ids := c.Params("ids")
ids := strings.Split(param_ids, ",")
artworks := make([]ArtWorkData, len(ids))
wg := sync.WaitGroup{}
wg.Add(len(ids))
for i, id := range ids {
if _, err := strconv.Atoi(id); err != nil {
return errors.New("invalid id")
}
go func(i int, id string) {
defer wg.Done()
illust, err := core.GetArtworkByID(c, id, false)
if err != nil {
artworks[i] = ArtWorkData{
Title: err.Error(),
}
return
}
metaDescription := ""
for _, i := range illust.Tags {
metaDescription += "#" + i.Name + ", "
}
artworks[i] = ArtWorkData{
Illust: illust,
Title: illust.Title,
PageType: "artwork",
MetaDescription: metaDescription,
MetaImage: illust.Images[0].Original,
}
}(i, id)
}
wg.Wait()
return c.Render("pages/artwork-multi", fiber.Map{
"Artworks": artworks,
})
}

43
pages/artwork.go Normal file
View file

@ -0,0 +1,43 @@
package pages
import (
"errors"
"strconv"
core "codeberg.org/vnpower/pixivfe/v2/core/webapi"
"github.com/gofiber/fiber/v2"
)
type ArtWorkData struct {
Illust *core.Illust
Title string
PageType string
MetaDescription string
MetaImage string
}
func ArtworkPage(c *fiber.Ctx) error {
param_id := c.Params("id")
if _, err := strconv.Atoi(param_id); err != nil {
return errors.New("invalid id")
}
illust, err := core.GetArtworkByID(c, param_id, true)
if err != nil {
return err
}
metaDescription := ""
for _, i := range illust.Tags {
metaDescription += "#" + i.Name + ", "
}
// todo: passing ArtWorkData{} here will not work. maybe lowercase?
return c.Render("pages/artwork", fiber.Map{
"Illust": illust,
"Title": illust.Title,
"PageType": "artwork",
"MetaDescription": metaDescription,
"MetaImage": illust.Images[0].Original,
})
}

20
pages/discovery.go Normal file
View file

@ -0,0 +1,20 @@
package pages
import (
core "codeberg.org/vnpower/pixivfe/v2/core/webapi"
"github.com/gofiber/fiber/v2"
)
func DiscoveryPage(c *fiber.Ctx) error {
mode := c.Query("mode", "safe")
works, err := core.GetDiscoveryArtwork(c, mode)
if err != nil {
return err
}
return c.Render("pages/discovery", fiber.Map{
"Artworks": works,
"Title": "Discovery",
})
}

34
pages/index.go Normal file
View file

@ -0,0 +1,34 @@
package pages
import (
session "codeberg.org/vnpower/pixivfe/v2/core/config"
core "codeberg.org/vnpower/pixivfe/v2/core/webapi"
"github.com/gofiber/fiber/v2"
)
func IndexPage(c *fiber.Ctx) error {
// If token is set, do the landing request...
if token := session.CheckToken(c); token != "" {
mode := c.Query("mode", "all")
works, err := core.GetLanding(c, mode)
if err != nil {
return err
}
return c.Render("pages/index", fiber.Map{
"Title": "Landing", "Data": works,
})
}
// ...otherwise, default to today's illustration ranking
works, err := core.GetRanking(c, "daily", "illust", "", "1")
if err != nil {
return err
}
return c.Render("pages/index", fiber.Map{
"Title": "Landing", "NoTokenData": works,
})
}

22
pages/newest.go Normal file
View file

@ -0,0 +1,22 @@
package pages
import (
core "codeberg.org/vnpower/pixivfe/v2/core/webapi"
"github.com/gofiber/fiber/v2"
)
func NewestPage(c *fiber.Ctx) error {
worktype := c.Query("type", "illust")
r18 := c.Query("r18", "false")
works, err := core.GetNewestArtworks(c, worktype, r18)
if err != nil {
return err
}
return c.Render("pages/newest", fiber.Map{
"Items": works,
"Title": "Newest works",
})
}

61
pages/personal.go Normal file
View file

@ -0,0 +1,61 @@
package pages
import (
"strconv"
"strings"
session "codeberg.org/vnpower/pixivfe/v2/core/config"
core "codeberg.org/vnpower/pixivfe/v2/core/webapi"
"github.com/gofiber/fiber/v2"
)
func LoginUserPage(c *fiber.Ctx) error {
token := session.CheckToken(c)
if token == "" {
return c.Redirect("/settings")
}
// The left part of the token is the member ID
userId := strings.Split(token, "_")
c.Redirect("/users/" + userId[0])
return nil
}
func LoginBookmarkPage(c *fiber.Ctx) error {
token := session.CheckToken(c)
if token == "" {
return c.Redirect("/settings")
}
// The left part of the token is the member ID
userId := strings.Split(token, "_")
c.Redirect("/users/" + userId[0] + "/bookmarks#checkpoint")
return nil
}
func FollowingWorksPage(c *fiber.Ctx) error {
if token := session.CheckToken(c); token == "" {
return c.Redirect("/settings")
}
mode := c.Query("mode", "all")
page := c.Query("page", "1")
pageInt, _ := strconv.Atoi(page)
works, err := core.GetNewestFromFollowing(c, mode, page)
if err != nil {
return err
}
return c.Render("pages/following", fiber.Map{
"Title": "Following works",
"Mode": mode,
"Artworks": works,
"CurPage": page,
"Page": pageInt,
})
}

74
pages/proxy.go Normal file
View file

@ -0,0 +1,74 @@
package pages
import (
"fmt"
"io"
"net/http"
config "codeberg.org/vnpower/pixivfe/v2/core/config"
"github.com/gofiber/fiber/v2"
)
func SPximgProxy(c *fiber.Ctx) error {
URL := fmt.Sprintf("https://s.pximg.net/%s", c.Params("*"))
req, _ := http.NewRequest("GET", URL, nil)
// Make the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
c.Set("Content-Type", resp.Header.Get("Content-Type"))
return c.Send([]byte(body))
}
func IPximgProxy(c *fiber.Ctx) error {
proxy_authority := config.GetImageProxy(c)
URL := fmt.Sprintf("https://%s/%s", proxy_authority, c.Params("*"))
req, _ := http.NewRequest("GET", URL, nil)
// Make the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
c.Set("Content-Type", resp.Header.Get("Content-Type"))
return c.Send([]byte(body))
}
func UgoiraProxy(c *fiber.Ctx) error {
URL := fmt.Sprintf("https://ugoira.com/api/mp4/%s", c.Params("*"))
req, _ := http.NewRequest("GET", URL, nil)
// Make the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
c.Set("Content-Type", resp.Header.Get("Content-Type"))
return c.Send([]byte(body))
}

35
pages/ranking.go Normal file
View file

@ -0,0 +1,35 @@
package pages
import (
"strconv"
core "codeberg.org/vnpower/pixivfe/v2/core/webapi"
"github.com/gofiber/fiber/v2"
)
func RankingPage(c *fiber.Ctx) error {
mode := c.Query("mode", "daily")
content := c.Query("content", "all")
date := c.Query("date", "")
page := c.Query("page", "1")
pageInt, err := strconv.Atoi(page)
if err != nil {
panic(err)
}
works, err := core.GetRanking(c, mode, content, date, page)
if err != nil {
return err
}
return c.Render("pages/rank", fiber.Map{
"Title": "Ranking",
"Page": pageInt,
"PageLimit": 10, // hard-coded by pixiv
"Mode": mode,
"Content": content,
"Date": date,
"Data": works,
})
}

90
pages/rankingCalendar.go Normal file
View file

@ -0,0 +1,90 @@
package pages
import (
"fmt"
"strconv"
"time"
core "codeberg.org/vnpower/pixivfe/v2/core/webapi"
"github.com/gofiber/fiber/v2"
)
type DateWrap struct {
Link string
Year int
Month int
MonthPadded string
MonthLiteral string
}
func parseDate(t time.Time) DateWrap {
var d DateWrap
year := t.Year()
month := t.Month()
monthPadded := fmt.Sprintf("%02d", month)
d.Link = fmt.Sprintf("%d-%s-01", year, monthPadded)
d.Year = year
d.Month = int(month)
d.MonthPadded = monthPadded
d.MonthLiteral = month.String()
return d
}
func RankingCalendarPicker(c *fiber.Ctx) error {
mode := c.FormValue("mode", "daily")
date := c.FormValue("date", "")
return c.RedirectToRoute("/rankingCalendar", fiber.Map{
"queries": map[string]string{
"mode": mode,
"date": date,
},
})
}
func RankingCalendarPage(c *fiber.Ctx) error {
mode := c.Query("mode", "daily")
date := c.Query("date", "")
var year int
var month int
// If the user supplied a date
if len(date) == 10 {
var err error
year, err = strconv.Atoi(date[:4])
if err != nil {
return err
}
month, err = strconv.Atoi(date[5:7])
if err != nil {
return err
}
} else {
now := time.Now()
year = now.Year()
month = int(now.Month())
}
realDate := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
monthBefore := realDate.AddDate(0, -1, 0)
monthAfter := realDate.AddDate(0, 1, 0)
render, err := core.GetRankingCalendar(c, mode, year, month)
if err != nil {
return err
}
return c.Render("pages/rankingCalendar", fiber.Map{
"Title": "Ranking calendar",
"Render": render,
"Mode": mode,
"Year": year,
"MonthBefore": parseDate(monthBefore),
"MonthAfter": parseDate(monthAfter),
"ThisMonth": parseDate(realDate),
})
}

34
pages/search.go Normal file
View file

@ -0,0 +1,34 @@
package pages
// import (
// session "codeberg.org/vnpower/pixivfe/v2/core/config"
// core "codeberg.org/vnpower/pixivfe/v2/core/webapi"
// "github.com/gofiber/fiber/v2"
// )
// func IndexPage(c *fiber.Ctx) error {
// // If token is set, do the landing request...
// if token := session.CheckToken(c); token != "" {
// mode := c.Query("mode", "all")
// works, err := core.GetLanding(c, mode)
// if err != nil {
// return err
// }
// return c.Render("pages/index", fiber.Map{
// "Title": "Landing", "Data": works,
// })
// }
// // ...otherwise, default to today's illustration ranking
// works, err := core.GetRanking(c, "daily", "illust", "", "1")
// if err != nil {
// return err
// }
// return c.Render("pages/index", fiber.Map{
// "Title": "Landing", "NoTokenData": works,
// })
// }

107
pages/settings.go Normal file
View file

@ -0,0 +1,107 @@
package pages
import (
"errors"
"io"
"net/http"
"regexp"
session "codeberg.org/vnpower/pixivfe/v2/core/config"
httpc "codeberg.org/vnpower/pixivfe/v2/core/http"
"github.com/gofiber/fiber/v2"
)
func setToken(c *fiber.Ctx) error {
// Parse the value from the form
token := c.FormValue("token")
if token != "" {
URL := httpc.GetNewestFromFollowingURL("all", "1")
_, err := httpc.UnwrapWebAPIRequest(URL, token)
if err != nil {
return errors.New("Cannot authorize with supplied token.")
}
// Make a test request to verify the token.
// THE TEST URL IS NSFW!
req, _ := http.NewRequest("GET", "https://www.pixiv.net/en/artworks/115365120", nil)
req.Header.Add("User-Agent", "Mozilla/5.0")
req.AddCookie(&http.Cookie{
Name: "PHPSESSID",
Value: token,
})
resp, err := http.DefaultClient.Do(req)
if err != nil {
return errors.New("Cannot authorize with supplied token.")
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return errors.New("Cannot parse the response from Pixiv. Please report this issue.")
}
// CSRF token
r := regexp.MustCompile(`"token":"([0-9a-f]+)"`)
csrf := r.FindStringSubmatch(string(body))[1]
if csrf == "" {
return errors.New("Cannot authorize with supplied token.")
}
// Set the tokens
if err := session.SetSessionValue(c, "Token", token); err != nil {
return err
}
if err := session.SetSessionValue(c, "CSRF", csrf); err != nil {
return err
}
return nil
}
return errors.New("You submitted an empty/invalid form.")
}
func setImageServer(c *fiber.Ctx) error {
// Parse the value from the form
token := c.FormValue("image-proxy")
if token != "" {
if err := session.SetSessionValue(c, "ImageProxy", token); err != nil {
return err
}
return nil
}
return errors.New("You submitted an empty/invalid form.")
}
func setLogout(c *fiber.Ctx) error {
session.RemoveSessionValue(c, "Token")
return nil
}
func SettingsPage(c *fiber.Ctx) error {
return c.Render("pages/settings", fiber.Map{})
}
func SettingsPost(c *fiber.Ctx) error {
t := c.Params("type")
var err error
switch t {
case "image_server":
err = setImageServer(c)
case "token":
err = setToken(c)
case "logout":
err = setLogout(c)
default:
err = errors.New("No such methods available.")
}
if err != nil {
return err
}
c.Redirect("/")
return nil
}

31
pages/tag.go Normal file
View file

@ -0,0 +1,31 @@
package pages
import (
"strconv"
core "codeberg.org/vnpower/pixivfe/v2/core/webapi"
"github.com/gofiber/fiber/v2"
)
func TagPage(c *fiber.Ctx) error {
queries := make(map[string]string, 3)
queries["Mode"] = c.Query("mode", "safe")
queries["Category"] = c.Query("category", "artworks")
queries["Order"] = c.Query("order", "date_d")
name := c.Params("name")
page := c.Query("page", "1")
pageInt, _ := strconv.Atoi(page)
tag, err := core.GetTagData(c, name)
if err != nil {
return err
}
result, err := core.GetSearch(c, queries["Category"], name, queries["Order"], queries["Mode"], page)
if err != nil {
return err
}
return c.Render("pages/tag", fiber.Map{"Title": "Results for " + tag.Name, "Tag": tag, "Data": result, "Queries": queries, "Page": pageInt})
}

50
pages/user.go Normal file
View file

@ -0,0 +1,50 @@
package pages
import (
"errors"
"math"
"strconv"
core "codeberg.org/vnpower/pixivfe/v2/core/webapi"
"github.com/gofiber/fiber/v2"
)
func UserPage(c *fiber.Ctx) error {
id := c.Params("id")
if _, err := strconv.Atoi(id); err != nil {
return err
}
category := c.Params("category", "artworks")
if !(category == "artworks" || category == "illustrations" || category == "manga" || category == "bookmarks") {
return errors.New("Invalid work category: only illustrations, manga, artworks and bookmarks are available")
}
page := c.Query("page", "1")
pageInt, _ := strconv.Atoi(page)
user, err := core.GetUserArtwork(c, id, category, pageInt)
if err != nil {
return err
}
var worksCount int
var worksPerPage float64
if category == "bookmarks" {
worksPerPage = 48.0
} else {
worksPerPage = 30.0
}
worksCount = user.ArtworksCount
pageLimit := math.Ceil(float64(worksCount) / worksPerPage)
return c.Render("pages/user", fiber.Map{
"Title": user.Name,
"User": user,
"Category": category,
"PageLimit": int(pageLimit),
"Page": pageInt,
"MetaImage": user.BackgroundImage,
})
}

View file

@ -1,76 +0,0 @@
package main
import (
"net/http"
"testing"
)
func Benchmark_Main(t *testing.B) {
r, err := http.Get("http://localhost:8282/")
if r.StatusCode != 200 {
t.Errorf("Status code not 200: was %d", r.StatusCode)
}
if err != nil {
t.Error(err)
}
}
func Benchmark_Ranking(t *testing.B) {
r, err := http.Get("http://localhost:8282/ranking")
if r.StatusCode != 200 {
t.Errorf("Status code not 200: was %d", r.StatusCode)
}
if err != nil {
t.Error(err)
}
}
func Benchmark_Ranking_Complex(t *testing.B) {
r, err := http.Get("http://localhost:8282/ranking?content=all&mode=daily_r18&page=1&date=20230826")
if r.StatusCode != 200 {
t.Errorf("Status code not 200: was %d", r.StatusCode)
}
if err != nil {
t.Error(err)
}
}
func Benchmark_Artwork(t *testing.B) {
r, err := http.Get("http://localhost:8282/artworks/111157207")
if r.StatusCode != 200 {
t.Errorf("Status code not 200: was %d", r.StatusCode)
}
if err != nil {
t.Error(err)
}
}
func Benchmark_Artwork_R18(t *testing.B) {
r, err := http.Get("http://localhost:8282/artworks/111130033")
if r.StatusCode != 200 {
t.Errorf("Status code not 200: was %d", r.StatusCode)
}
if err != nil {
t.Error(err)
}
}
func Benchmark_User_NoSocial(t *testing.B) {
r, err := http.Get("http://localhost:8282/users/1035047")
if r.StatusCode != 200 {
t.Errorf("Status code not 200: was %d", r.StatusCode)
}
if err != nil {
t.Error(err)
}
}
func Benchmark_User_WithSocial(t *testing.B) {
r, err := http.Get("http://localhost:8282/users/59336265")
if r.StatusCode != 200 {
t.Errorf("Status code not 200: was %d", r.StatusCode)
}
if err != nil {
t.Error(err)
}
}

16
run.sh Normal file
View file

@ -0,0 +1,16 @@
#!/bin/sh
# Update the program every time you run?
# git pull
# Visit https://codeberg.org/VnPower/PixivFE/wiki/Environment-variables for more details
export PIXIVFE_TOKEN=token_123456
export PIXIVFE_IMAGEPROXY=pximg.cocomi.cf
# export PIXIVFE_UNIXSOCKET=/srv/http/pages/pixivfe
export PIXIVFE_PORT=8282
go mod download
go get codeberg.org/vnpower/pixivfe/v2/...
CGO_ENABLED=0 GOOS=linux go build -mod=readonly -o pixivfe
./pixivfe

26
semgrep.yml Normal file
View file

@ -0,0 +1,26 @@
# Usage: semgrep scan -f semgrep.yml
rules:
- id: rule-0
message: "find http requests made not with *fiber.Ctx available"
languages: [go]
severity: WARNING
patterns:
- pattern-either:
- pattern: |
http.UnwrapWebAPIRequest(...)
- pattern: |
http.WebAPIRequest(...)
- pattern-not-inside: |
func $FUNC(c *fiber.Ctx, ...) $RET {
...
}
- id: rule-1
message: "find http requests made (limiter should be installed at all places)"
languages: [go]
severity: INFO
patterns:
- pattern-either:
- pattern: |
http.UnwrapWebAPIRequest(...)
- pattern: |
http.WebAPIRequest(...)

View file

@ -1,4 +1,4 @@
package handler
package serve
import (
"fmt"
@ -7,7 +7,10 @@ import (
"net/url"
"regexp"
"strconv"
"strings"
"time"
core "codeberg.org/vnpower/pixivfe/v2/core/webapi"
)
func GetRandomColor() string {
@ -87,7 +90,7 @@ func ParseEmojis(s string) template.HTML {
s = s[1 : len(s)-1] // Get the string inside
id := emojiList[s]
return fmt.Sprintf(`<img src="https://s.pximg.net/common/images/emoji/%s.png" alt="(%s)" class="emoji" />`, id, s)
return fmt.Sprintf(`<img src="/proxy/s.pximg.net/common/images/emoji/%s.png" alt="(%s)" class="emoji" />`, id, s)
})
return template.HTML(parsedString)
}
@ -116,36 +119,75 @@ func ParseTime(date time.Time) string {
}
func CreatePaginator(base, ending string, current_page, max_page int) template.HTML {
peek := 2
limit := peek*peek + 1
pageUrl := func(page int) string {
return fmt.Sprintf(`%s%d%s`, base, page, ending)
}
const (
peek = 5 // this can be changed freely
limit = peek*2 + 1 // tied to the algorithm below, do not change
)
hasMaxPage := max_page != -1
count := 0
pages := ""
pages += fmt.Sprintf(`<a href="%s1%s" class="pagination-button">&laquo;</a>`, base, ending)
pages += fmt.Sprintf(`<a href="%s%d%s" class="pagination-button">&lsaquo;</a>`, base, max(1, current_page-1), ending)
for i := current_page - peek; (i <= max_page || max_page == -1) && count < limit; i++ {
if i < 1 {
continue
pages += `<div class="pagination-buttons">`
{ // "jump to page" <form>
hidden_section := ""
urlParsed, err := url.Parse(base)
if err != nil {
panic(err)
}
for k, vs := range urlParsed.Query() {
if k == "page" {
continue
}
for _, v := range vs {
hidden_section += fmt.Sprintf(`<input type="hidden" name="%s" value="%s"/>`, k, v)
}
}
if i == current_page {
pages += fmt.Sprintf(`<a href="%s%d%s" class="pagination-button" id="highlight">%d</a>`, base, i, ending, i)
max_section := ""
if hasMaxPage {
max_section = fmt.Sprintf(`max="%d"`, max_page)
}
pages += fmt.Sprintf(`<form action="%s">%s<input name="page" type="number" required value="%d" min="%d" %s placeholder="Page№" title="Jump To Page Number"/></form>`, pageUrl(current_page), hidden_section, current_page, 1, max_section)
pages += "<br />"
}
{
// previous,first (two buttons)
pages += `<span>`
{
pages += fmt.Sprintf(`<a href="%s" class="pagination-button">&laquo;</a>`, pageUrl(1))
pages += fmt.Sprintf(`<a href="%s" class="pagination-button">&lsaquo;</a>`, pageUrl(max(1, current_page-1)))
}
pages += `</span>`
for i := current_page - peek; (i <= max_page || max_page == -1) && count < limit; i++ {
if i < 1 {
continue
}
if i == current_page {
pages += fmt.Sprintf(`<a href="%s" class="pagination-button" id="highlight">%d</a>`, pageUrl(i), i)
} else {
pages += fmt.Sprintf(`<a href="%s" class="pagination-button">%d</a>`, pageUrl(i), i)
}
count++
}
// next,last (two buttons)
pages += `<span>`
if hasMaxPage {
pages += fmt.Sprintf(`<a href="%s" class="pagination-button">&rsaquo;</a>`, pageUrl(min(max_page, current_page+1)))
pages += fmt.Sprintf(`<a href="%s" class="pagination-button">&raquo;</a>`, pageUrl(max_page))
} else {
pages += fmt.Sprintf(`<a href="%s%d%s" class="pagination-button">%d</a>`, base, i, ending, i)
pages += fmt.Sprintf(`<a href="%s" class="pagination-button">&rsaquo;</a>`, pageUrl(current_page+1))
pages += fmt.Sprintf(`<a href="%s" class="pagination-button" class="disabled">&raquo;</a>`, pageUrl(max_page))
}
count++
}
if max_page == -1 {
pages += fmt.Sprintf(`<a href="%s%d%s" class="pagination-button">&rsaquo;</a>`, base, current_page+1, ending)
pages += fmt.Sprintf(`<a href="%s%d%s" class="pagination-button" id="disabled">&raquo;</a>`, base, max_page, ending)
} else {
pages += fmt.Sprintf(`<a href="%s%d%s" class="pagination-button">&rsaquo;</a>`, base, min(max_page, current_page+1), ending)
pages += fmt.Sprintf(`<a href="%s%d%s" class="pagination-button">&raquo;</a>`, base, max_page, ending)
pages += `</span>`
}
pages += `</div>`
return template.HTML(pages)
}
@ -197,5 +239,12 @@ func GetTemplateFunctions() template.FuncMap {
"createPaginator": func(base, ending string, current_page, max_page int) template.HTML {
return CreatePaginator(base, ending, current_page, max_page)
},
"joinArtworkIds": func(artworks []core.ArtworkBrief) string {
ids := []string{}
for _, art := range artworks {
ids = append(ids, art.ID)
}
return strings.Join(ids, ",")
},
}
}

View file

@ -1 +0,0 @@
{"version":3,"sourceRoot":"","sources":["style.scss"],"names":[],"mappings":"AAgBA;EACE;EACA;;;AAGF;EACE;EACA;EAEA,kBAzBG;EA0BH,OAxBG;EA0BH;EACA,aAjBY;EAmBZ;EACA;EACA;;;AAGF;EACE,OAjCK;EAkCL;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;AAAA;AAAA;AAAA;EAIE;EACA;EACA;;;AAGF;AAAA;EAEE;;;AAGF;AAAA;EAEE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;AAAA;AAAA;EAGE;EACA,eA9Ec;EA+Ed;EACA;;;AAGF;AAAA;EAEE;EACA,kBA3Fa;EA4Fb,OA1Fa;;;AA6Ff;AAAA;EAEE,kBAjGa;;;AAoGf;EACE;EACA;;;AAGF;EACE;EACA,eAtGc;EAuGd;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAQE,eA3Hc;EA4Hd;EACA;EACA;EACA,aA9HY;EA+HZ;EACA;;;AAGF;AAAA;AAAA;AAAA;EAIE,aAlIY;;;AAqId;AAAA;AAAA;AAAA;EAIE;;;AAGF;AAAA;AAAA;AAAA;EAIE;EACA;;;AAGF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME,kBAlKK;EAmKL;EACA,OAtKG;;;AAyKL;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;EACA,kBAhMW;EAiMX;;;AAIJ;EACE;EACA;EACA;EACA;;AACA;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAEA;EACE;EACA;EACA,OAtNH;;AA2NC;EACE;EACA;;AAKF;EACE;;AAIJ;EACE;;AAEA;EACE;;AAIJ;EACE;EACA;;AAGF;EACE,kBAvPD;EAwPC;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EAEA;EACA;;AAEA;EACE;EACA;EACA;EACA,OA7QL;EA8QK;EACA;EACA;;AAEA;EACE,kBApRG;;AAuRL;EACE;EACA;EACA;EACA;;;AAoBZ;EAEI;IACE;;;AAKN;EACE;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EAIA;EACA;;;AAGF;EACE;EACA;EACA;;;AAKA;AAAA;EACE;EACA,OA7VC;;AAiWD;AAAA;EACE,OAtVgB;EAuVhB,QAvVgB;;AA0VlB;AAAA;EACE,WA3VgB;;AA8VlB;AAAA;EACE,WA/VgB;;AAgWhB;AAAA;EACE,WAlWc;;AAwWlB;AAAA;EACE,OAzWgB;EA0WhB,QA1WgB;;AA6WlB;AAAA;EACE,WA9WgB;;AAiXlB;AAAA;EACE,WAlXgB;;AAoXhB;AAAA;EACE,WArXc;;AA0XpB;AAAA;EACE;EACA;EACA;;AAEA;AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEF;AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA,kBAraI;EAsaJ,OAzaH;;AA4aC;AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA,OApbH;EAqbG;EACA;EACA;EACA;EACA;EACA;;AAEA;AAAA;EACE;;AAKN;AAAA;EACE;EACA;EACA,eAjcU;;AAocZ;AAAA;EACE;EACA;;AAEA;AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAIJ;AAAA;EACE;EACA;;AAEA;AAAA;EACE;EACA;;AAEA;AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;AAAA;EACE;EACA;EACA;EACA;EACA;;;AAOV;EACE,kBAvfa;EAwfb,eAnfc;EAofd;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;;AAEA;EACE;;AAEF;EACE;;AACA;EACE,OA5hBK;;AAkiBb;EACE;;AAGF;EACE;;AAEA;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAIJ;EACE;;AAEA;EACE,OAxjBI;;AA2jBN;EACE,OA7jBD;EA8jBC;EACA;;AAKN;EACE;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAKF;EACE;EACA;EACA;EACA,OAzmBD;;AA2mBC;EACE;EACA;EACA;EACA;EACA;;;AAMR;EACE;EAOA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EACE;;AAIJ;EACE;;AAEA;EACE;;;AAKN;EACE;EACA;;;AAGF;AAAA;EAEE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA,kBAlsBa;EAmsBb;;AAEA;EACE;EACA,OAtsBC;EAusBD,kBAxsBW;EAysBX;EACA;EACA;EACA;EACA;EACA;EACA;;;AAIJ;EACE;IACE;;EAGF;AAAA;IAEE;;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;EACA;EAEA;EACA;;AAIA;EACE;;;AAKN;EACE,kBAnvBa;EAovBb;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAEF;EACE;EACA","file":"style.css"}

View file

@ -1,6 +0,0 @@
{{ range . }} {{ if .ID }}
<div class="artwork-thumbnail-small artwork-thumbnail">
{{ include "thumbnail-dt" . }} {{ include "thumbnail-tt" . }} {{ include
"thumbnail-at" . }}
</div>
{{ end }} {{ end }}

View file

@ -1,3 +0,0 @@
<div class="artwork-title">
<a href="/artworks/{{ .ID }}"> {{ .Title }} </a>
</div>

View file

@ -1,36 +0,0 @@
<div class="container">
<h2>Ranking calendar ({{ Month }} {{ Year }})</h2>
<div class="switcher">
{{ url := "/ranking_log?date=" + ThisMonth + "&mode=" }}
<span class="switch-title">Modes</span>
<a href="{{ url }}daily" class="switch-button">Daily</a>
<a href="{{ url }}weekly" class="switch-button">Weekly</a>
<a href="{{ url }}monthly" class="switch-button">Monthly</a>
<a href="{{ url }}rookie" class="switch-button">Rookie</a>
<span class="switch-seperator"></span>
<a href="{{ url }}daily_r18" class="switch-button">Daily (R-18)</a>
<a href="{{ url }}weekly_r18" class="switch-button">Weekly (R-18)</a>
</div>
<a href="https://codeberg.org/VnPower/pixivfe/wiki/Questions-you-may-ask"><small>R18?</small></a>
<br>
<div id="calendar">
<div class="calendar-weeks">
<div>Sun</div>
<div>Mon</div>
<div>Tue</div>
<div>Wed</div>
<div>Thu</div>
<div>Fri</div>
<div>Sat</div>
</div>
<div class="calendar-board">
{{ raw: Render }}
</div>
</div>
<div class="pagination">
{{ url := "/ranking_log?mode=" + Mode + "&date=" }}
<a href="{{ url }}{{ MonthBefore }}" class="pagination-button ">Last month</a>
<a href="{{ url }}{{ MonthAfter }}" class="pagination-button ">Next month</a>
</div>
</div>

View file

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

Before

Width:  |  Height:  |  Size: 303 B

After

Width:  |  Height:  |  Size: 303 B

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

Before

Width:  |  Height:  |  Size: 383 B

After

Width:  |  Height:  |  Size: 383 B

View file

Before

Width:  |  Height:  |  Size: 830 B

After

Width:  |  Height:  |  Size: 830 B

View file

Before

Width:  |  Height:  |  Size: 732 B

After

Width:  |  Height:  |  Size: 732 B

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

Before

Width:  |  Height:  |  Size: 577 B

After

Width:  |  Height:  |  Size: 577 B

View file

Before

Width:  |  Height:  |  Size: 569 B

After

Width:  |  Height:  |  Size: 569 B

View file

Before

Width:  |  Height:  |  Size: 949 B

After

Width:  |  Height:  |  Size: 949 B

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 466 B

After

Width:  |  Height:  |  Size: 466 B

View file

Before

Width:  |  Height:  |  Size: 125 B

After

Width:  |  Height:  |  Size: 125 B

View file

Before

Width:  |  Height:  |  Size: 159 B

After

Width:  |  Height:  |  Size: 159 B

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

View file

Before

Width:  |  Height:  |  Size: 776 B

After

Width:  |  Height:  |  Size: 776 B

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 903 B

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

View file

Before

Width:  |  Height:  |  Size: 897 B

After

Width:  |  Height:  |  Size: 897 B

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -10,8 +10,12 @@ body {
font-size: 1.8rem;
font-family: "Roboto", "Open Sans", "Noto Sans", sans-serif, "Noto Sans CJK JP";
margin-bottom: 10px;
margin-left: 5px;
margin-right: 5px;
}
@media (min-width: 440px) {
main {
margin-inline: 5px;
}
}
a {
@ -22,7 +26,6 @@ a {
/* Scrollbars */
* {
scrollbar-width: thin;
scrollbar-color: #118bee auto;
}
*::-webkit-scrollbar {
@ -180,18 +183,51 @@ input[type=submit][hidden] {
.pagination {
text-align: center;
}
.pagination form {
display: inline-block;
}
.pagination .pagination-buttons {
text-align: center;
}
.pagination .pagination-buttons input {
width: 5em;
}
.pagination .pagination-button {
margin-right: 5px;
}
.pagination #highlight {
filter: brightness(1.2);
}
.pagination #disabled {
.pagination .disabled {
pointer-events: none;
background-color: #222;
filter: brightness(1);
}
#loading-indicator {
z-index: 2;
isolation: isolate;
position: sticky;
top: 0;
height: 4px;
margin-bottom: -4px;
animation: rolling-something 3s linear infinite;
animation-play-state: paused;
background-size: 60px auto;
}
#loading-indicator.htmx-request {
background-image: repeating-linear-gradient(90deg, #118bee 0px 30px, transparent 30px 60px);
animation-play-state: running;
}
@keyframes rolling-something {
0% {
background-position-x: -20px;
}
100% {
background-position-x: 40px;
}
}
nav {
margin-top: 15px;
margin-bottom: 15px;
@ -231,6 +267,8 @@ nav .navigation-wrapper .sidebar-label {
cursor: pointer;
}
nav .navigation-wrapper .sidebar {
z-index: 1;
isolation: isolate;
background-color: #131516;
position: absolute;
padding-top: 6px;
@ -238,7 +276,6 @@ nav .navigation-wrapper .sidebar {
width: 220px;
transform: translateX(-220px);
transition: transform 250ms cubic-bezier(0.23, 1, 0.32, 1);
z-index: 999;
}
nav .navigation-wrapper .sidebar br {
align-self: stretch;
@ -285,10 +322,20 @@ nav .navigation-wrapper .sidebar .sidebar-list .sidebar-item img {
.container {
max-width: 1200px;
margin-left: auto;
margin-right: auto;
padding-left: 10px;
padding-right: 10px;
margin-inline: auto;
padding-inline: 4px;
}
@media (min-width: 440px) {
.container {
padding-inline: 10px;
}
}
.artwork-container-header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
}
.artwork-container {
@ -549,13 +596,13 @@ nav .navigation-wrapper .sidebar .sidebar-list .sidebar-item img {
margin-left: 3px;
margin-right: 3px;
}
.illust .illust-other-works > a {
.illust .illust-other-works a.illust-other-works-author {
display: inline-flex;
align-items: center;
text-decoration: none;
color: #fff;
}
.illust .illust-other-works > a > img {
.illust .illust-other-works a.illust-other-works-author > img {
aspect-ratio: 1/1;
width: 50px;
height: 50px;
@ -591,8 +638,6 @@ nav .navigation-wrapper .sidebar .sidebar-list .sidebar-item img {
aspect-ratio: 1/1;
width: 170px;
height: 170px;
}
.user .user-avatar img {
border-radius: 50%;
}
.user .user-social {
@ -610,6 +655,20 @@ nav .navigation-wrapper .sidebar .sidebar-list .sidebar-item img {
margin: 0;
}
.user-tags {
display: flex;
flex-wrap: wrap;
row-gap: 8px;
column-gap: 1em;
margin-block: 4px 20px;
}
.user-tags > a {
line-height: 1;
}
.user-tags > a:hover {
text-decoration: underline;
}
#calendar {
width: 100%;
height: auto;

1
views/css/style.css.map Normal file
View file

@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["style.scss"],"names":[],"mappings":"AAiBA;EACE;;;AAGF;EACE;EACA;EAEA,kBAzBG;EA0BH,OAxBG;EA0BH;EACA,aAjBY;EAmBZ;;;AAIA;EADF;IAEI;;;;AAIJ;EACE,OArCK;EAsCL;;;AAGF;AACA;EACI;;;AAKJ;EACI;EACA;;;AAGJ;EACI;;;AAGJ;EACI,kBA1DG;EA2DH;;;AAGJ;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;AAAA;AAAA;AAAA;EAIE;EACA;EACA;;;AAGF;AAAA;EAEE;;;AAGF;AAAA;EAEE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;AAAA;AAAA;EAGE;EACA,eAvGc;EAwGd;EACA;;;AAGF;AAAA;EAEE;EACA,kBApHa;EAqHb,OAnHa;;;AAsHf;AAAA;EAEE,kBA1Ha;;;AA6Hf;EACE;EACA;;;AAGF;EACE;EACA,eA/Hc;EAgId;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAQE,eApJc;EAqJd;EACA;EACA;EACA,aAvJY;EAwJZ;EACA;;;AAGF;AAAA;AAAA;AAAA;EAIE,aA3JY;;;AA8Jd;AAAA;AAAA;AAAA;EAIE;;;AAGF;AAAA;AAAA;AAAA;EAIE;EACA;;;AAGF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME,kBA3LK;EA4LL;EACA,OA/LG;;;AAkML;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;AAEA;EACE;;AAGF;EACE;;AAEA;EACD;;AAID;EACE;;AAGF;EACE;;AAGF;EACE;EACA,kBArOW;EAsOX;;;AAKJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;;AACA;EACE;EACA;;AAEF;EACE;IACE;;EAEF;IACE;;;;AAKN;EACE;EACA;EACA;EACA;;AACA;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAEA;EACE;EACA;EACA,OAtRH;;AA2RC;EACE;EACA;;AAKF;EACE;;AAIJ;EACE;;AAEA;EACE;;AAIJ;EACE;EACA;;AAGF;EACE;EACA;EACA,kBAzTD;EA0TC;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EAEA;EACA;;AAEA;EACE;EACA;EACA;EACA,OA9UL;EA+UK;EACA;EACA;;AAEA;EACE,kBArVG;;AAwVL;EACE;EACA;EACA;EACA;;;AAoBZ;EAEI;IACE;;;AAKN;EACE;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;EACE;EACA;EACA;;AACA;EAJF;IAKI;;;;AAIJ;EACE;EACA;EACA;EACA;;;AAGF;EACE;EAEA;EAIA;EACA;;;AAGF;EACE;EACA;EACA;;;AAKA;AAAA;EACE;EACA,OAvaC;;AA2aD;AAAA;EACE,OAhagB;EAiahB,QAjagB;;AAoalB;AAAA;EACE,WAragB;;AAwalB;AAAA;EACE,WAzagB;;AA0ahB;AAAA;EACE,WA5ac;;AAkblB;AAAA;EACE,OAnbgB;EAobhB,QApbgB;;AAublB;AAAA;EACE,WAxbgB;;AA2blB;AAAA;EACE,WA5bgB;;AA8bhB;AAAA;EACE,WA/bc;;AAocpB;AAAA;EACE;EACA;EACA;;AAEA;AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEF;AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA,kBA/eI;EAgfJ,OAnfH;;AAsfC;AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA,OA9fH;EA+fG;EACA;EACA;EACA;EACA;EACA;;AAEA;AAAA;EACE;;AAKN;AAAA;EACE;EACA;EACA,eA3gBU;;AA8gBZ;AAAA;EACE;EACA;;AAEA;AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAIJ;AAAA;EACE;EACA;;AAEA;AAAA;EACE;EACA;;AAEA;AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;AAAA;EACE;EACA;EACA;EACA;EACA;;;AAOV;EACE,kBAjkBa;EAkkBb,eA7jBc;EA8jBd;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;;AAEA;EACE;;AAEF;EACE;;AACA;EACE,OAtmBK;;AA4mBb;EACE;;AAGF;EACE;;AAEA;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAIJ;EACE;;AAEA;EACE,OAloBI;;AAqoBN;EACE,OAvoBD;EAwoBC;EACA;;AAKN;EACE;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAKF;EACE;EACA;EACA;EACA,OAnrBD;;AAqrBC;EACE;EACA;EACA;EACA;EACA;;;AAMR;EACE;EAOA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAEA;EACE;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;;;AAKN;EACE;EACA;EACA;EACA;EACA;;AACA;EACE;;AACA;EACE;;;AAKN;EACE;EACA;;;AAGF;AAAA;EAEE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA,kBAjyBa;EAkyBb;;AAEA;EACE;EACA,OAryBC;EAsyBD,kBAvyBW;EAwyBX;EACA;EACA;EACA;EACA;EACA;EACA;;;AAIJ;EACE;IACE;;EAGF;AAAA;IAEE;;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;EACA;EAEA;EACA;;AAIA;EACE;;;AAKN;EACE,kBAl1Ba;EAm1Bb;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAEF;EACE;EACA","file":"style.css"}

View file

@ -13,6 +13,7 @@ $color-shadow: #bbbbbb20;
$font-family: "Roboto", "Open Sans", "Noto Sans", sans-serif, "Noto Sans CJK JP";
$small-artwork-width: 184px;
$large-artwork-width: 288px;
$small-breakpoint: 440px;
html {
font-size: 62.5%;
@ -29,8 +30,12 @@ body {
font-family: $font-family;
margin-bottom: 10px;
margin-left: 5px;
margin-right: 5px;
}
main {
@media (min-width: $small-breakpoint) {
margin-inline: 5px;
}
}
a {
@ -41,7 +46,8 @@ a {
/* Scrollbars */
* {
scrollbar-width: thin;
scrollbar-color: $link auto;
// invalid line
// scrollbar-color: $link auto;
}
*::-webkit-scrollbar {
@ -200,6 +206,18 @@ input[type="submit"][hidden] {
.pagination {
text-align: center;
form {
display: inline-block;
}
.pagination-buttons {
text-align: center;
input {
width: 5em;
}
}
.pagination-button {
margin-right: 5px;
}
@ -208,13 +226,40 @@ input[type="submit"][hidden] {
filter: brightness($hover-brightness);
}
#disabled {
.disabled {
pointer-events: none;
background-color: $bg-secondary;
filter: brightness(1);
}
}
// https://stackoverflow.com/questions/63787241/css-how-to-create-an-infinitely-moving-repeating-linear-gradient/63787567#63787567
#loading-indicator {
z-index: 2;
isolation: isolate;
position: sticky;
top: 0;
height: 4px;
margin-bottom: -4px;
animation: rolling-something 3s linear infinite;
animation-play-state: paused;
$segment-size: 60px;
background-size: $segment-size auto;
&.htmx-request {
background-image: repeating-linear-gradient(90deg, $link 0px 30px, transparent 30px 60px);
animation-play-state: running;
}
@keyframes rolling-something {
0% {
background-position-x: -20px
}
100% {
background-position-x: $segment-size - 20px
}
}
}
nav {
margin-top: 15px;
margin-bottom: 15px;
@ -264,6 +309,8 @@ nav {
}
.sidebar {
z-index: 1;
isolation: isolate;
background-color: $bg;
position: absolute;
padding-top: 6px;
@ -271,7 +318,6 @@ nav {
width: 220px;
transform: translateX(-220px);
transition: transform 250ms cubic-bezier(0.23, 1, 0.32, 1);
z-index: 999;
br {
align-self: stretch;
@ -342,14 +388,23 @@ nav {
.container {
max-width: 1200px;
margin-left: auto;
margin-right: auto;
padding-left: 10px;
padding-right: 10px;
margin-inline: auto;
padding-inline: 4px;
@media (min-width: $small-breakpoint) {
padding-inline: 10px;
}
}
.artwork-container-header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
}
.artwork-container {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(calc($small-artwork-width + 15px), 1fr)
@ -632,7 +687,7 @@ nav {
}
.illust-other-works {
& > a {
a.illust-other-works-author {
display: inline-flex;
align-items: center;
text-decoration: none;
@ -684,10 +739,7 @@ nav {
aspect-ratio: 1/1;
width: 170px;
height: 170px;
img {
border-radius: 50%;
}
border-radius: 50%;
}
.user-social {
@ -709,6 +761,20 @@ nav {
}
}
.user-tags {
display: flex;
flex-wrap: wrap;
row-gap: 8px;
column-gap: 1em;
margin-block: 4px 20px;
&>a {
line-height: 1;
&:hover {
text-decoration: underline;
}
}
}
#calendar {
width: 100%;
height: auto;

1
views/js/htmx@1.9.10.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -7,47 +7,43 @@
<meta charset="UTF-8" />
<meta name="description" content="View this page on PixivFE." />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self'" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'" />
<meta name="referrer" content="no-referrer, same-origin" />
<link href="/css/style.css" rel="stylesheet" />
{{ if BaseURL }}
<meta property="og:site_name" content="PixivFE" />
<meta property="og:title" content="{{ title }}" />
<meta property="og:type" content="website" />
<meta property="og:url" content="{{ FullURL }}" />
<meta property="twitter:url" content="{{ FullURL }}">
<meta name="twitter:title" content="{{ title }}" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content="{{ BaseURL }}">
<script src="/js/htmx@1.9.10.min.js" integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"></script>
{{ if isset(PageType) }} {{ if PageType == "artwork" }} <meta
property="og:description"
content="View this artwork by {{ Illust.User.Name }} on PixivFE."
/>
<meta name="twitter:description" content="View this artwork by {{ Illust.User.Name }} on PixivFE.">
<meta property="og:image" content="{{ Illust.Images[0].Large }}" />
<meta name="twitter:image" content="{{ Illust.Images[0].Large }}">
{{ else if PageType == "user" }}
{{ if BaseURL }}
<meta property="og:title" content="{{ title }}" />
<meta property="og:url" content="{{ BaseURL }}" />
<meta property="og:site_name" content="PixivFE" />
<meta property="og:type" content="article" />
<meta content="summary_large_image" name="twitter:card" />
{{ if isset(MetaDescription) }}
<meta
property="og:description"
content="View this user's profile on PixivFE."
content="{{MetaDescription}}"
/>
<meta name="twitter:description" content="View this user's profile on PixivFE.">
{{ if User.BackgroundImage }}
<meta property="og:image" content="{{ User.BackgroundImage }}" />
<meta name="twitter:image" content="{{ User.BackgroundImage }}">
{{ else }}
<meta property="og:image" content="{{ User.Avatar }}" />
<meta name="twitter:image" content="{{ User.Avatar }}">
{{ end }} {{ end }} {{ end }} {{ end }}
{{ end }}
{{ if isset(MetaImage) }}
<meta property="og:image" content="{{ MetaImage }}" />
{{ end }}
{{ end }}
</head>
<body>
<body hx-boost="true" hx-target="main" hx-select="main" hx-indicator="#loading-indicator">
<script>
document.body.addEventListener('htmx:beforeOnLoad', function (evt) {
evt.detail.shouldSwap = true;
evt.detail.isError = false;
});
</script>
<div id="loading-indicator"></div>
<nav>
<div class="navigation-wrapper">
<span>
<input type="checkbox" class="sidebar-toggler" id="sidebar-toggler" checked />
<input type="checkbox" class="sidebar-toggler" id="sidebar-toggler" />
<label for="sidebar-toggler" class="sidebar-label">
<img
src="/assets/menu-thin.png"
@ -65,11 +61,14 @@
<a class="sidebar-item" href="/ranking">
<img src="/assets/crown.png" alt="icon" />Ranking</a
>
<a class="sidebar-item" href="/rankingCalendar">
<img src="/assets/calendar.png" alt="icon" />Ranking history</a
>
<a class="sidebar-item" href="/newest">
<img src="/assets/sparkling.png" alt="icon" />Newest</a
>
<br />
<a class="sidebar-item" href="/self/following_works">
<a class="sidebar-item" href="/self/followingWorks">
<img src="/assets/users.png" alt="icon" />Latest by followed</a
>
<a class="sidebar-item" href="/self/bookmarks">
@ -95,7 +94,7 @@
</ul>
</div>
<span class="navbar-brand">
<img src="https://pixivfe.exozy.me/favicon.ico" alt="Icon" />
<img src="/assets/favicon.ico" alt="Icon" />
<a href="/">
<span>PixivFE</span>
</a>
@ -131,6 +130,8 @@
</div>
<div class="navbar-shadow"></div>
</nav>
{{ embed() }}
<main>
{{ embed() }}
</main>
</body>
</html>

View file

@ -1,372 +0,0 @@
package views
import (
"errors"
"fmt"
"math"
"net/http"
"strconv"
"strings"
"time"
"codeberg.org/vnpower/pixivfe/configs"
"codeberg.org/vnpower/pixivfe/models"
"github.com/gofiber/fiber/v2"
)
func get_session_value(c *fiber.Ctx, key string) *string {
sess, err := configs.Store.Get(c)
if err != nil {
panic(err)
}
value := sess.Get(key)
if value != nil {
placeholder := value.(string)
return &placeholder
}
return nil
}
func artwork_page(c *fiber.Ctx) error {
image_proxy := get_session_value(c, "image-proxy")
if image_proxy == nil {
image_proxy = &configs.ProxyServer
}
id := c.Params("id")
if _, err := strconv.Atoi(id); err != nil {
return errors.New("Bad id")
}
illust, err := PC.GetArtworkByID(id)
if err != nil {
return err
}
illust.ProxyImages(*image_proxy)
// Optimize this
return c.Render("pages/artwork", fiber.Map{
"Illust": illust,
"Title": illust.Title,
"PageType": "artwork",
})
}
func index_page(c *fiber.Ctx) error {
had_token := true
image_proxy := get_session_value(c, "image-proxy")
if image_proxy == nil {
image_proxy = &configs.ProxyServer
}
token := get_session_value(c, "token")
if token == nil {
had_token = false
token = &configs.Token
}
PC := NewPixivClient(5000)
PC.SetSessionID(*token)
PC.SetUserAgent(configs.UserAgent)
PC.AddHeader("Accept-Language", configs.AcceptLanguage)
mode := c.Query("mode", "all")
artworks, err := PC.GetLandingPage(mode)
if err != nil {
return err
}
if had_token {
artworks.Following = models.ProxyShortArtworkSlice(artworks.Following, *image_proxy)
artworks.Commissions = models.ProxyShortArtworkSlice(artworks.Commissions, *image_proxy)
artworks.Recommended = models.ProxyShortArtworkSlice(artworks.Recommended, *image_proxy)
artworks.Newest = models.ProxyShortArtworkSlice(artworks.Newest, *image_proxy)
artworks.Users = models.ProxyShortArtworkSlice(artworks.Users, *image_proxy)
artworks.RecommendByTags = models.ProxyRecommendedByTagsSlice(artworks.RecommendByTags, *image_proxy)
}
artworks.Rankings = models.ProxyShortArtworkSlice(artworks.Rankings, *image_proxy)
artworks.Pixivision = models.ProxyPixivisionSlice(artworks.Pixivision, *image_proxy)
return c.Render("pages/index", fiber.Map{"Title": "Landing", "Artworks": artworks, "Token": had_token})
}
func user_page(c *fiber.Ctx) error {
image_proxy := get_session_value(c, "image-proxy")
if image_proxy == nil {
image_proxy = &configs.ProxyServer
}
id := c.Params("id")
if _, err := strconv.Atoi(id); err != nil {
return err
}
category := c.Params("category", "artworks")
if !(category == "artworks" || category == "illustrations" || category == "manga" || category == "bookmarks") {
return errors.New("Invalid work category: only illustrations, manga, artworks and bookmarks are available")
}
page := c.Query("page", "1")
pageInt, _ := strconv.Atoi(page)
user, err := PC.GetUserInformation(id, category, pageInt)
if err != nil {
return err
}
user.ProxyImages(*image_proxy)
var worksCount int
worksCount = user.ArtworksCount
pageLimit := math.Ceil(float64(worksCount) / 30.0)
return c.Render("pages/user", fiber.Map{"Title": user.Name, "User": user, "Category": category, "PageLimit": int(pageLimit), "Page": pageInt})
}
func ranking_page(c *fiber.Ctx) error {
image_proxy := get_session_value(c, "image-proxy")
if image_proxy == nil {
image_proxy = &configs.ProxyServer
}
queries := make(map[string]string, 4)
queries["Mode"] = c.Query("mode", "daily")
queries["Content"] = c.Query("content", "all")
queries["Date"] = c.Query("date", "")
page := c.Query("page", "1")
pageInt, _ := strconv.Atoi(page)
response, err := PC.GetRanking(queries["Mode"], queries["Content"], queries["Date"], page)
if err != nil {
return err
}
response.ProxyImages(*image_proxy)
return c.Render("pages/rank", fiber.Map{
"Title": "Ranking",
"Data": response,
"Queries": queries,
"Page": pageInt,
})
}
func newest_artworks_page(c *fiber.Ctx) error {
image_proxy := get_session_value(c, "image-proxy")
if image_proxy == nil {
image_proxy = &configs.ProxyServer
}
worktype := c.Query("type", "illust")
r18 := c.Query("r18", "false")
works, err := PC.GetNewestArtworks(worktype, r18)
if err != nil {
return err
}
works = models.ProxyShortArtworkSlice(works, *image_proxy)
return c.Render("pages/newest", fiber.Map{
"Items": works,
"Title": "Newest works",
})
}
func search_page(c *fiber.Ctx) error {
image_proxy := get_session_value(c, "image-proxy")
if image_proxy == nil {
image_proxy = &configs.ProxyServer
}
queries := make(map[string]string, 3)
queries["Mode"] = c.Query("mode", "safe")
queries["Category"] = c.Query("category", "artworks")
queries["Order"] = c.Query("order", "date_d")
name := c.Params("name")
page := c.Query("page", "1")
pageInt, _ := strconv.Atoi(page)
tag, err := PC.GetTagData(name)
if err != nil {
return err
}
if len(tag.Metadata.Image) > 0 {
tag.Metadata.Image = models.ProxyImage(tag.Metadata.Image, *image_proxy)
}
result, err := PC.GetSearch(queries["Category"], name, queries["Order"], queries["Mode"], page)
if err != nil {
return err
}
result.ProxyImages(*image_proxy)
return c.Render("pages/tag", fiber.Map{"Title": "Results for " + tag.Name, "Tag": tag, "Data": result, "Queries": queries, "Page": pageInt})
}
func search(c *fiber.Ctx) error {
name := c.FormValue("name")
return c.Redirect("/tags/"+name, http.StatusFound)
}
func discovery_page(c *fiber.Ctx) error {
image_proxy := get_session_value(c, "image-proxy")
if image_proxy == nil {
image_proxy = &configs.ProxyServer
}
mode := c.Query("mode", "safe")
artworks, err := PC.GetDiscoveryArtwork(mode, 100)
if err != nil {
return err
}
artworks = models.ProxyShortArtworkSlice(artworks, *image_proxy)
return c.Render("pages/discovery", fiber.Map{"Title": "Discovery", "Artworks": artworks})
}
func ranking_log_page(c *fiber.Ctx) error {
image_proxy := get_session_value(c, "image-proxy")
if image_proxy == nil {
image_proxy = &configs.ProxyServer
}
mode := c.Query("mode", "daily")
date := c.Query("date", "")
var year int
var month int
var monthLit string
// If the user supplied a date
if len(date) == 6 {
var err error
year, err = strconv.Atoi(date[:4])
if err != nil {
return err
}
month, err = strconv.Atoi(date[4:])
if err != nil {
return err
}
} else {
now := time.Now()
year = now.Year()
month = int(now.Month())
}
realDate := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
monthLit = realDate.Month().String()
monthBefore := realDate.AddDate(0, -1, 0)
monthAfter := realDate.AddDate(0, 1, 0)
thisMonthLink := fmt.Sprintf("%d%02d", realDate.Year(), realDate.Month())
monthBeforeLink := fmt.Sprintf("%d%02d", monthBefore.Year(), monthBefore.Month())
monthAfterLink := fmt.Sprintf("%d%02d", monthAfter.Year(), monthAfter.Month())
render, err := PC.GetRankingLog(mode, year, month, *image_proxy)
if err != nil {
return err
}
return c.Render("pages/ranking_log", fiber.Map{"Title": "Ranking calendar", "Render": render, "Mode": mode, "Month": monthLit, "Year": year, "MonthBefore": monthBeforeLink, "MonthAfter": monthAfterLink, "ThisMonth": thisMonthLink})
}
func following_works_page(c *fiber.Ctx) error {
image_proxy := get_session_value(c, "image-proxy")
if image_proxy == nil {
image_proxy = &configs.ProxyServer
}
token := get_session_value(c, "token")
if token == nil {
return c.Redirect("/login")
}
queries := make(map[string]string, 2)
queries["Mode"] = c.Query("mode", "all")
queries["Page"] = c.Query("page", "1")
pageInt, _ := strconv.Atoi(queries["Page"])
artworks, err := PC.GetNewestFromFollowing(queries["Mode"], queries["Page"], *token)
if err != nil {
return err
}
artworks = models.ProxyShortArtworkSlice(artworks, *image_proxy)
return c.Render("pages/following", fiber.Map{"Title": "Following works", "Queries": queries, "Artworks": artworks, "Page": pageInt})
}
func your_bookmark_page(c *fiber.Ctx) error {
token := get_session_value(c, "token")
if token == nil {
return c.Redirect("/login")
}
// The left part of the token is the member ID
userId := strings.Split(*token, "_")
c.Redirect("/users/" + userId[0] + "/bookmarks#checkpoint")
return nil
}
func login_page(c *fiber.Ctx) error {
return c.Render("pages/login", fiber.Map{})
}
func settings_page(c *fiber.Ctx) error {
return c.Render("pages/settings", fiber.Map{})
}
func settings_post(c *fiber.Ctx) error {
t := c.Params("type")
error := ""
switch t {
case "image_server":
error = set_image_server(c)
case "token":
error = set_token(c)
case "logout":
error = set_logout(c)
default:
error = "No method available"
}
if error != "" {
return errors.New(error)
}
c.Redirect("/settings")
return nil
}
func get_logged_in_user(c *fiber.Ctx) error {
token := get_session_value(c, "token")
if token == nil {
return c.Redirect("/login")
}
// The left part of the token is the member ID
userId := strings.Split(*token, "_")
c.Redirect("/users/" + userId[0])
return nil
}
func about_page(c *fiber.Ctx) error {
info := fiber.Map{
"Time": configs.StartingTime,
"BaseURL": configs.BaseURL,
"Version": configs.Version,
"ImageProxy": configs.ProxyServer,
"AcceptLanguage": configs.AcceptLanguage,
}
return c.Render("pages/about", info)
}

View file

@ -9,12 +9,6 @@
<b>Started PixivFE (UTC)</b>: {{ Time }} <br />The date when PixivFE
started on this server
</li>
<li>
<b>Base URL</b>: {{ if isset(BaseURL) and !BaseURL == "localhost" }} {{
BaseURL }} <br />
Meta tags are available for embeds {{ else }} Not set <br />
Meta tags are not available, embeds may not work {{ end }}
</li>
<li>
<b>Default image proxy server</b>: {{ ImageProxy }}<br />The default image
proxy server that was set on this server, used to proxy images from

View file

@ -0,0 +1,3 @@
{{ range Artworks }}
{{ include "components/artwork" . }}
{{ end }}

View file

@ -1,4 +1,4 @@
<div class="container illust">
<div class="container illust" id="checkpoint">
{{ if !Illust.IsUgoira }}
<div class="illust-images">
{{ range index := Illust.Images }}
@ -9,20 +9,23 @@
</div>
{{ else }}
<div class="illust-images">
<a href="https://ugoira.com/i/{{ Illust.ID }}" target="_blank">
<video
autoplay
loop
muted
disablepictureinpicture
playsinline
controls
poster="{{ Illust.Images[0].Large }}"
src="https://ugoira.com/api/mp4/{{ Illust.ID }}"
src="/proxy/ugoira.com/{{ Illust.ID }}"
>
Unable to load ugoira.
</video>
</a>
</div>
<a href="/proxy/ugoira.com/{{ Illust.ID }}"
>Download</a
>
<br />
<a href="https://ugoira.com/i/{{ Illust.ID }}"
>Go to ugoira.com for more options</a
>
@ -51,6 +54,20 @@
<br />
</div>
<div class="illust-tags">
<!--
To know if this artwork is bookmarked:
```
{{ if Illust.Bookmarked }}
<button>Unbookmark</button>
//...
{{ else }}
<button>Bookmarked</button>
//...
{{ end }}
```
Same with Illust.Liked
-->
<span class="illust-tag-attr">
<img src="/assets/eye.png" alt="Views" />
{{ Illust.Views }}
@ -92,11 +109,14 @@
</div>
<br />
<div class="illust-other-works">
<a href="/users/{{ Illust.User.ID }}"
><img src="{{ Illust.User.Avatar }}" alt="{{ Illust.User.Name }}" /> Other
works by {{ Illust.User.Name }}
<span class="special-symbol">&raquo;</span>
</a>
<div class="artwork-container-header">
<a class="illust-other-works-author" href="/users/{{ Illust.User.ID }}"
><img src="{{ Illust.User.Avatar }}" alt="{{ Illust.User.Name }}" /> Other works by {{ Illust.User.Name }}
<span class="special-symbol">&raquo;</span>
</a>
{{ combinedUrl := "/artworks-multi/" + joinArtworkIds(Illust.RecentWorks) }}
<div class="artwork-actions"><a href="{{combinedUrl}}">View all</a></div>
</div>
<div class="artwork-container-scroll">
{{ range Illust.RecentWorks }}
<div class="artwork-small artwork">
@ -120,8 +140,8 @@
{{ if .Stamp }}
<img
class="stamp"
src="https://s.pximg.net/common/images/stamp/generated-stamps/{{ .Stamp }}_s.jpg"
alt="https://s.pximg.net/common/images/stamp/generated-stamps/{{ .Stamp }}_s.jpg"
src="/proxy/s.pximg.net/common/images/stamp/generated-stamps/{{ .Stamp }}_s.jpg"
alt="/proxy/s.pximg.net/common/images/stamp/generated-stamps/{{ .Stamp }}_s.jpg"
/>
{{ else }} {{ raw: parseEmojis(.Context) }} {{ end }}
</p>

View file

@ -0,0 +1,111 @@
<div class="container illust">
{{ if !.Illust.IsUgoira }}
<div class="illust-images">
{{ range index := .Illust.Images }}
<a href="{{ .Original }}" target="_blank">
<img src="{{ .Large }}" alt="Page {{ index }}" />
</a>
{{ end }}
</div>
{{ else }}
<div class="illust-images">
<video
autoplay
loop
muted
disablepictureinpicture
playsinline
controls
poster="{{ .Illust.Images[0].Large }}"
src="/proxy/ugoira.com/{{ .Illust.ID }}"
>
Unable to load ugoira.
</video>
</div>
<a href="/proxy/ugoira.com/{{ .Illust.ID }}"
>Download</a
>
<br />
<a href="https://ugoira.com/i/{{ .Illust.ID }}"
>Go to ugoira.com for more options</a
>
{{ end }}
<div class="illust-attr">
<a href="/users/{{ .Illust.User.ID }}"
><img
src="{{ .Illust.User.Avatar }}"
alt="{{ .Illust.User.Name }}"
class="illust-avatar"
/>
</a>
<div class="attr-wrap">
<div class="illust-title">{{ .Illust.Title }}</div>
<div class="illust-author">
<a href="/users/{{ .Illust.User.ID }}">{{ .Illust.User.Name }}</a>
</div>
</div>
</div>
<div>
<a href="https://pixiv.net/i/{{ .Illust.ID }}"
>pixiv.net/i/{{ .Illust.ID }}</a
>
<br />
</div>
<div class="illust-tags">
<!--
To know if this artwork is bookmarked:
```
{{ if .Illust.Bookmarked }}
<button>Unbookmark</button>
//...
{{ else }}
<button>Bookmarked</button>
//...
{{ end }}
```
Same with .Illust.Liked
-->
<span class="illust-tag-attr">
<img src="/assets/eye.png" alt="Views" />
{{ .Illust.Views }}
</span>
<span class="illust-tag-attr">
<img src="/assets/heart-solid.png" alt="Bookmarks" />
{{ .Illust.Bookmarks }}
</span>
<span class="illust-tag-attr">
<img src="/assets/like.png" alt="Likes" />
{{ .Illust.Likes }}
</span>
<span class="illust-tag-attr">
<img src="/assets/calendar.png" alt="Date" />
{{ parseTime: .Illust.Date }}
</span>
</div>
<div class="illust-tags">
{{ if .Illust.AiType == 2 }}
<span class="illust-tag">
<span class="illust-tag-name" id="highlight">AI-generated</span>
</span>
{{ end }} {{ range .Illust.Tags }} {{ if isEmphasize(.Name) }}
<span class="illust-tag">
<span class="illust-tag-name" id="highlight">{{ .Name }}</span>
</span>
{{ else }}
<span class="illust-tag">
<span class="illust-tag-name"
><a href="/tags/{{ escapeString(.Name) }}">#{{ .Name }}</a></span
><span class="illust-tag-translation">{{ .TranslatedName }}</span>
</span>
{{ end }} {{ end }}
</div>
<br />
<div class="illust-description">
{{ raw: parsePixivRedirect(.Illust.Description) }}
</div>
<br />
</div>

View file

@ -0,0 +1,23 @@
{{ range . }}
<div class="artwork-small artwork">
<div class="artwork-additional">
<div class="artwork-position">{{ .Rank }}</div>
{{ if toInt(.Pages) > 1 }}
<div class="artwork-page-count"><span>&boxbox; {{ .Pages }}</span></div>
{{ end }}
</div>
<a href="/artworks/{{ .ID }}#checkpoint">
<img src="{{ .Image }}" alt="{{ .Title }}" />
</a>
<div class="artwork-title">
<a href="/artworks/{{ .ID }}#checkpoint"> {{ .Title }} </a>
</div>
<div class="artwork-author">
<a href="/users/{{ .ArtistID }}"
><img src="{{ .ArtistAvatar }}" alt="{{ .ArtistName }}" />
<span>{{ .ArtistName }}</span></a
>
</div>
</div>
{{ end }}

Some files were not shown because too many files have changed in this diff Show more