rc-1
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"gifuu/tools"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func DELETE_Art_ID(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
query := r.URL.Query()
|
||||
|
||||
paramToken := query.Get("token")
|
||||
paramID := tools.ParseSnowflake(r.PathValue("id"))
|
||||
if paramToken == "" || paramID == 0 {
|
||||
tools.SendClientError(w, r, tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete Art
|
||||
tag, err := tools.Database.Exec(ctx,
|
||||
`DELETE FROM gifuu.upload WHERE id = $1 AND upload_token_hash = $2`,
|
||||
paramID,
|
||||
tools.RequestHash(paramToken),
|
||||
)
|
||||
if err != nil {
|
||||
tools.SendServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
tools.SendClientError(w, r, tools.ERROR_GENERIC_UNAUTHORIZED)
|
||||
return
|
||||
}
|
||||
|
||||
go RemoveFilesForArt(paramID)
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func RemoveFilesForArt(artID int64) {
|
||||
normal := strconv.FormatInt(artID, 10)
|
||||
target := path.Join(tools.STORAGE_DISK_PUBLIC, normal)
|
||||
if err := os.RemoveAll(target); err != nil {
|
||||
tools.LoggerStorage.Log(tools.WARN, "Failed to delete directory '%s': %s", target, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"gifuu/tools"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func DELETE_Moderation_Art_ID(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
paramID := tools.ParseSnowflake(r.PathValue("id"))
|
||||
if paramID == 0 {
|
||||
tools.SendClientError(w, r, tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete Art
|
||||
tag, err := tools.Database.Exec(ctx,
|
||||
`DELETE FROM gifuu.upload WHERE id = $1`,
|
||||
paramID,
|
||||
)
|
||||
if err != nil {
|
||||
tools.SendServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
tools.SendClientError(w, r, tools.ERROR_GENERIC_UNAUTHORIZED)
|
||||
return
|
||||
}
|
||||
|
||||
go RemoveFilesForArt(paramID)
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"gifuu/tools"
|
||||
"net/http"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func GET_Art_ID(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
paramID := tools.ParseSnowflake(r.PathValue("id"))
|
||||
if paramID == 0 {
|
||||
tools.SendClientError(w, r, tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
}
|
||||
|
||||
var Results []byte
|
||||
err := tools.Database.QueryRow(r.Context(),
|
||||
`SELECT jsonb_build_object(
|
||||
'id', u.id::text,
|
||||
'created', u.created::timestamptz,
|
||||
'sticker', u.flag_sticker,
|
||||
'audio', u.flag_audio,
|
||||
'framerate', u.encode_fps,
|
||||
'width', u.encode_width,
|
||||
'height', u.encode_height,
|
||||
'rating', u.meta_rating,
|
||||
'title', u.meta_title,
|
||||
'tags', COALESCE(
|
||||
jsonb_agg(
|
||||
CASE WHEN t.id IS NOT NULL THEN
|
||||
jsonb_build_object(
|
||||
'id', t.id::text,
|
||||
'label', t.label,
|
||||
'usage', t.usage
|
||||
)
|
||||
END
|
||||
) FILTER (WHERE t.id IS NOT NULL),
|
||||
'[]'
|
||||
)
|
||||
)
|
||||
FROM gifuu.upload u
|
||||
LEFT JOIN gifuu.upload_tag ut ON ut.gif_id = u.id
|
||||
LEFT JOIN gifuu.tag t ON t.id = ut.tag_id
|
||||
WHERE u.id = $1
|
||||
GROUP BY u.id`,
|
||||
paramID,
|
||||
).Scan(&Results)
|
||||
|
||||
if err == pgx.ErrNoRows {
|
||||
tools.SendClientError(w, r, tools.ERROR_UNKNOWN_ANIMATION)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
tools.SendServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
tools.SendJSON(w, r, http.StatusOK, Results)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"gifuu/tools"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func GET_Art_Latest(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
query := r.URL.Query()
|
||||
|
||||
paramLimit := tools.ParseLimit(query.Get("limit"))
|
||||
paramAfter := tools.ParseSnowflake(query.Get("after"))
|
||||
|
||||
var Results []byte
|
||||
err := tools.Database.QueryRow(ctx,
|
||||
`SELECT COALESCE(jsonb_agg(t.row), '[]') FROM (
|
||||
SELECT jsonb_build_object(
|
||||
'id', u.id::text,
|
||||
'created', u.created::timestamptz,
|
||||
'sticker', u.flag_sticker,
|
||||
'audio', u.flag_audio,
|
||||
'framerate', u.encode_fps,
|
||||
'width', u.encode_width,
|
||||
'height', u.encode_height,
|
||||
'rating', u.meta_rating,
|
||||
'title', u.meta_title,
|
||||
'tags', COALESCE(
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'id', t.id::text,
|
||||
'label', t.label,
|
||||
'usage', t.usage
|
||||
)
|
||||
) FILTER (WHERE t.id IS NOT NULL),
|
||||
'[]'
|
||||
)
|
||||
) AS row
|
||||
FROM gifuu.upload u
|
||||
LEFT JOIN gifuu.upload_tag ut ON ut.gif_id = u.id
|
||||
LEFT JOIN gifuu.tag t ON t.id = ut.tag_id
|
||||
WHERE ($1::bigint = 0 OR u.id < $1::bigint) AND u.meta_rating < $3
|
||||
GROUP BY u.id
|
||||
ORDER BY u.id DESC
|
||||
LIMIT $2
|
||||
) t`,
|
||||
paramAfter,
|
||||
paramLimit,
|
||||
tools.MODEL_THRESHOLD_HIDE,
|
||||
).Scan(&Results)
|
||||
if err != nil {
|
||||
tools.SendServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
tools.SendJSON(w, r, http.StatusOK, Results)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"gifuu/tools"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func GET_Art_Search(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
query := r.URL.Query()
|
||||
|
||||
paramLimit := tools.ParseLimit(query.Get("limit"))
|
||||
paramAfter := tools.ParseSnowflake(query.Get("after"))
|
||||
var paramTags []int64
|
||||
|
||||
if params, ok := query["tag"]; ok {
|
||||
paramTags = make([]int64, 0, len(params))
|
||||
indexTags := make(map[int64]struct{}, len(params))
|
||||
for _, raw := range params {
|
||||
id := tools.ParseSnowflake(raw)
|
||||
if _, exists := indexTags[id]; exists || id == 0 {
|
||||
tools.SendClientError(w, r, tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
}
|
||||
indexTags[id] = struct{}{}
|
||||
paramTags = append(paramTags, id)
|
||||
}
|
||||
}
|
||||
if len(paramTags) == 0 {
|
||||
tools.SendClientError(w, r, tools.ERROR_BODY_EMPTY)
|
||||
return
|
||||
}
|
||||
|
||||
var Results []byte
|
||||
err := tools.Database.QueryRow(ctx,
|
||||
`SELECT COALESCE(jsonb_agg(t.obj), '[]') FROM (
|
||||
SELECT jsonb_build_object(
|
||||
'id', u.id::text,
|
||||
'created', u.created::timestamptz,
|
||||
'sticker', u.flag_sticker,
|
||||
'audio', u.flag_audio,
|
||||
'framerate', u.encode_fps,
|
||||
'width', u.encode_width,
|
||||
'height', u.encode_height,
|
||||
'rating', u.meta_rating,
|
||||
'title', u.meta_title,
|
||||
'tags', COALESCE(
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'id', t.id::text,
|
||||
'label', t.label,
|
||||
'usage', t.usage
|
||||
)
|
||||
ORDER BY t.usage DESC
|
||||
) FILTER (WHERE t.id IS NOT NULL),
|
||||
'[]'
|
||||
)
|
||||
) AS obj
|
||||
FROM gifuu.upload u
|
||||
LEFT JOIN gifuu.upload_tag ut ON ut.gif_id = u.id
|
||||
LEFT JOIN gifuu.tag t ON t.id = ut.tag_id
|
||||
WHERE ut.tag_id = ANY($1::bigint[])
|
||||
AND ($2::bigint = 0 OR u.id < $2::bigint)
|
||||
AND u.meta_rating < $4
|
||||
GROUP BY u.id
|
||||
HAVING COUNT(DISTINCT ut.tag_id) = cardinality($1::bigint[])
|
||||
ORDER BY u.id DESC
|
||||
LIMIT $3::int
|
||||
) t`,
|
||||
paramTags,
|
||||
paramAfter,
|
||||
paramLimit,
|
||||
tools.MODEL_THRESHOLD_HIDE,
|
||||
).Scan(&Results)
|
||||
if err != nil {
|
||||
tools.SendServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
tools.SendJSON(w, r, http.StatusOK, Results)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"gifuu/tools"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GET_Challenge(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
|
||||
paramDifficulty, _ := strconv.Atoi(query.Get("difficulty"))
|
||||
if paramDifficulty < 18 {
|
||||
tools.SendClientError(w, r, tools.ERROR_CHALLENGE_TOO_EASY)
|
||||
return
|
||||
}
|
||||
|
||||
// Create Session
|
||||
nonce := make([]byte, 16)
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
tools.SendServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
normal := hex.EncodeToString(nonce)
|
||||
expiry := time.Now().Add(5 * time.Minute).Unix()
|
||||
|
||||
// Store Session
|
||||
tools.ChallengeAtomic.Lock()
|
||||
tools.ChallengeSession[normal] = tools.ChallengeSessionData{
|
||||
Expires: expiry,
|
||||
Difficulty: paramDifficulty,
|
||||
}
|
||||
tools.ChallengeAtomic.Unlock()
|
||||
|
||||
tools.SendJSON(w, r, http.StatusOK, map[string]any{
|
||||
"nonce": normal,
|
||||
"difficulty": paramDifficulty,
|
||||
"expires": expiry,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"gifuu/tools"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
REPORT_REASON_EXPLICIT = 0 // Sexual, Nudity, or Fetish Content
|
||||
REPORT_REASON_HARASSMENT = 1 // Targeted Harassment or Hate Speech
|
||||
REPORT_REASON_VIOLENCE = 2 // Violence, Gore, or Abuse
|
||||
REPORT_REASON_SPAM = 3 // Spam, Advertising, or Solicitation
|
||||
REPORT_REASON_HARMFUL = 4 // Seizure-Inducing, Self-Harm, or Dangerous Content
|
||||
REPORT_REASON_ILLEGAL = 5 // Illegal Content (CSAM, Threats, etc.)
|
||||
)
|
||||
|
||||
type normalizerItem struct {
|
||||
Match string `json:"match"`
|
||||
Replace string `json:"replace"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
var cachedLimitsJSON []byte
|
||||
var cachedLimitsGZIP []byte
|
||||
|
||||
func init() {
|
||||
json, gzip, err := tools.PrepareStaticJSON(map[string]any{
|
||||
"upload": map[string]any{
|
||||
"input_width_min": MEDIA_MIN_WIDTH,
|
||||
"input_height_min": MEDIA_MIN_HEIGHT,
|
||||
"video_width_max": VIDEO_MAX_WIDTH,
|
||||
"video_height_max": VIDEO_MAX_HEIGHT,
|
||||
"image_width_max": IMAGE_MAX_WIDTH,
|
||||
"image_height_max": IMAGE_MAX_HEIGHT,
|
||||
"duration": MEDIA_MAX_DURATION,
|
||||
"filesize": tools.LIMIT_FILE,
|
||||
"mime_types": tools.LIMIT_MIME_TYPE,
|
||||
},
|
||||
"title": map[string]any{
|
||||
"normalizers": []normalizerItem{
|
||||
{ /**/ `^\s+` /*---*/, `` /**/, "Trim Left"},
|
||||
{ /**/ `\s+$` /*---*/, `` /**/, "Trim Right"},
|
||||
{ /**/ `\s{2,}` /**/, ` ` /**/, "Regulate Excessive Spaces"},
|
||||
},
|
||||
"matcher": `^[\S\s]{1,80}$`,
|
||||
"max_length": 80,
|
||||
"min_length": 1,
|
||||
},
|
||||
"tag": map[string]any{
|
||||
"normalizers": []normalizerItem{
|
||||
{ /**/ `^_+` /*-*/, `` /*-*/, "Trim Left Underscores"},
|
||||
{ /**/ `_+$` /*-*/, `` /*-*/, "Trim Right Underscores"},
|
||||
{ /**/ `_+` /*--*/, `_` /**/, "Regulate Excessive Underscores"},
|
||||
},
|
||||
"matcher": `^[\p{L}\p{N}_]{1,32}$`,
|
||||
"max_length": 32,
|
||||
"min_length": 1,
|
||||
},
|
||||
"comment": map[string]any{
|
||||
"normalizers": []normalizerItem{
|
||||
{ /**/ `^\s+` /*--*/, `` /*--*/, "Trim Left Spaces"},
|
||||
{ /**/ `\s+$` /*--*/, `` /*--*/, "Trim Right Spaces"},
|
||||
{ /**/ `\n{2,}` /**/, `\n` /**/, "Regulate Excessive Newlines"},
|
||||
},
|
||||
"matcher": `^[\S\s]{10,240}$`,
|
||||
"max_length": 240,
|
||||
"min_length": 10,
|
||||
},
|
||||
"report": map[string]any{
|
||||
"values": []any{
|
||||
map[string]any{
|
||||
"id": REPORT_REASON_EXPLICIT,
|
||||
"title": "EXPLICIT",
|
||||
"description": "Sexual, Nudity, or Fetish Content"},
|
||||
map[string]any{
|
||||
"id": REPORT_REASON_HARASSMENT,
|
||||
"title": "HARASSMENT",
|
||||
"description": "Targeted Harassment or Hate Speech"},
|
||||
map[string]any{
|
||||
"id": REPORT_REASON_VIOLENCE,
|
||||
"title": "VIOLENCE",
|
||||
"description": "Violence, Gore, or Abuse"},
|
||||
map[string]any{
|
||||
"id": REPORT_REASON_SPAM,
|
||||
"title": "SPAM",
|
||||
"description": "Spam, Advertising, or Solicitation"},
|
||||
map[string]any{
|
||||
"id": REPORT_REASON_HARMFUL,
|
||||
"title": "HARMFUL",
|
||||
"description": "Seizure-Inducing, Self-Harm, or Dangerous Content"},
|
||||
map[string]any{
|
||||
"id": REPORT_REASON_ILLEGAL,
|
||||
"title": "ILLEGAL",
|
||||
"description": "Illegal Content (CSAM, Threats, etc.)"},
|
||||
},
|
||||
"normalizers": []normalizerItem{
|
||||
{ /**/ `^\s+` /*--*/, `` /*--*/, "Trim Left Spaces"},
|
||||
{ /**/ `\s+$` /*--*/, `` /*--*/, "Trim Right Spaces"},
|
||||
{ /**/ `\n{2,}` /**/, `\n` /**/, "Regulate Excessive Newlines"},
|
||||
},
|
||||
"matcher": `^[\S\s]{10,240}$`,
|
||||
"max_length": 240,
|
||||
"min_length": 10,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cachedLimitsJSON = json
|
||||
cachedLimitsGZIP = gzip
|
||||
}
|
||||
|
||||
func GET_Limits(w http.ResponseWriter, r *http.Request) {
|
||||
tools.SendStaticJSON(w, r, http.StatusOK, cachedLimitsJSON, cachedLimitsGZIP)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gifuu/include"
|
||||
"gifuu/tools"
|
||||
"html"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
var animTemplate = template.Must(template.New("").Parse(include.TEMPLATE_ANIMATION_METADATA))
|
||||
|
||||
func GET_Metadata_ID(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
paramID := tools.ParseSnowflake(r.PathValue("id"))
|
||||
if paramID == 0 {
|
||||
tools.SendClientError(w, r, tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
}
|
||||
|
||||
var animationData struct {
|
||||
Created string
|
||||
Width int
|
||||
Height int
|
||||
Rating float64
|
||||
Sticker bool
|
||||
Title string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
// Fetch Animation Metadata
|
||||
err := tools.Database.QueryRow(r.Context(),
|
||||
`SELECT
|
||||
u.encode_width,
|
||||
u.encode_height,
|
||||
u.meta_rating,
|
||||
u.flag_sticker,
|
||||
u.meta_title,
|
||||
COALESCE(array_agg(t.label ORDER BY t.usage) FILTER (WHERE t.id IS NOT NULL), '{}')
|
||||
FROM gifuu.upload u
|
||||
LEFT JOIN gifuu.upload_tag ut ON ut.gif_id = u.id
|
||||
LEFT JOIN gifuu.tag t ON t.id = ut.tag_id
|
||||
WHERE u.id = $1
|
||||
GROUP BY u.id`,
|
||||
paramID,
|
||||
).Scan(
|
||||
&animationData.Width,
|
||||
&animationData.Height,
|
||||
&animationData.Rating,
|
||||
&animationData.Sticker,
|
||||
&animationData.Title,
|
||||
&animationData.Tags,
|
||||
)
|
||||
if err == pgx.ErrNoRows {
|
||||
tools.SendClientError(w, r, tools.ERROR_UNKNOWN_ANIMATION)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
tools.SendServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Render Webpage
|
||||
w.Header().Add("Content-Type", "text/html")
|
||||
err = animTemplate.Execute(w, map[string]any{
|
||||
"width": animationData.Width,
|
||||
"height": animationData.Height,
|
||||
"title": html.EscapeString(animationData.Title),
|
||||
"tags": html.EscapeString(strings.Join(animationData.Tags, ", ")),
|
||||
"uri_embed": fmt.Sprintf("%s/embed.html?id=%d&quality=standard", tools.TEMPLATE_BASE_WEB, paramID),
|
||||
"uri_image": fmt.Sprintf("%s/%d/standard.avif", tools.TEMPLATE_BASE_CDN, paramID),
|
||||
"uri_site": fmt.Sprintf("%s/art/%d", tools.TEMPLATE_BASE_WEB, paramID),
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("Render Error:", r.URL.Path, err)
|
||||
http.Error(w, "Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"gifuu/tools"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func GET_Tags_Autocomplete(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
query := r.URL.Query()
|
||||
|
||||
paramLimit := tools.ParseLimit(query.Get("limit"))
|
||||
paramQuery := query.Get("query")
|
||||
|
||||
if str, ok := tools.NormalizeTag(paramQuery); !ok {
|
||||
tools.SendClientError(w, r, tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
} else {
|
||||
paramQuery = str
|
||||
}
|
||||
|
||||
var Results []byte
|
||||
err := tools.Database.QueryRow(ctx,
|
||||
`SELECT COALESCE(jsonb_agg(t), '[]') FROM (
|
||||
SELECT id::text AS id, label, usage
|
||||
FROM gifuu.tag
|
||||
ORDER BY word_similarity(label, $1) DESC, usage DESC
|
||||
LIMIT $2
|
||||
) t`,
|
||||
paramQuery,
|
||||
paramLimit,
|
||||
).Scan(&Results)
|
||||
|
||||
if err != nil {
|
||||
tools.SendServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
tools.SendJSON(w, r, http.StatusOK, Results)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"gifuu/tools"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func GET_Tags_Popular(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
query := r.URL.Query()
|
||||
|
||||
paramLimit := tools.ParseLimit(query.Get("limit"))
|
||||
|
||||
var Results []byte
|
||||
err := tools.Database.QueryRow(ctx,
|
||||
`SELECT COALESCE(jsonb_agg(t), '[]') FROM (
|
||||
SELECT id::text AS id, label, usage
|
||||
FROM gifuu.tag
|
||||
ORDER BY usage DESC
|
||||
LIMIT $1
|
||||
) t`,
|
||||
paramLimit,
|
||||
).Scan(&Results)
|
||||
|
||||
if err != nil {
|
||||
tools.SendServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
tools.SendJSON(w, r, http.StatusOK, Results)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"gifuu/tools"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func POST_Art_ID_Reports(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var Body struct {
|
||||
ReasonType int `json:"type"`
|
||||
ReasonText string `json:"reason"`
|
||||
}
|
||||
|
||||
paramID := tools.ParseSnowflake(r.PathValue("id"))
|
||||
if paramID == 0 {
|
||||
tools.SendClientError(w, r, tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
}
|
||||
|
||||
if err := tools.ParseJSON(r.Body, &Body); err != nil {
|
||||
tools.SendClientError(w, r, tools.ERROR_BODY_INVALID_DATA)
|
||||
return
|
||||
}
|
||||
|
||||
if Body.ReasonType < REPORT_REASON_EXPLICIT || Body.ReasonType > REPORT_REASON_ILLEGAL {
|
||||
tools.SendClientError(w, r, tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
}
|
||||
|
||||
if str, ok := tools.NormalizeComment(Body.ReasonText); !ok {
|
||||
tools.SendClientError(w, r, tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
} else {
|
||||
Body.ReasonText = str
|
||||
}
|
||||
|
||||
// Attempt to store the users report. It may be discarded if another moderator
|
||||
// has previously review this item and deemed it didn't violate any rules.
|
||||
|
||||
if _, err := tools.Database.Exec(ctx,
|
||||
`INSERT INTO gifuu.mod_report (upload_id, reason_type, reason_text, report_address_hash)
|
||||
SELECT $1, $2, $3, $4 FROM gifuu.upload WHERE id = $1 AND flag_bypass = FALSE`,
|
||||
paramID,
|
||||
Body.ReasonType,
|
||||
Body.ReasonText,
|
||||
tools.RequestAddressHash(r),
|
||||
); err != nil {
|
||||
tools.SendServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,751 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gifuu/tools"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
VIDEO_MAX_WIDTH = 3840
|
||||
VIDEO_MAX_HEIGHT = 2160
|
||||
IMAGE_MAX_WIDTH = 7680
|
||||
IMAGE_MAX_HEIGHT = 4320
|
||||
MEDIA_MIN_WIDTH = 64
|
||||
MEDIA_MIN_HEIGHT = 64
|
||||
MEDIA_MAX_DURATION = 62
|
||||
MEDIA_COLOR_SPACE = "yuv420p"
|
||||
MEDIA_COLOR_RANGE = "tv"
|
||||
MEDIA_FILENAME_PREVIEW = "preview.avif"
|
||||
MEDIA_FILENAME_STANDARD = "standard.avif"
|
||||
MEDIA_FILENAME_ALPHA = "alpha.webm"
|
||||
MEDIA_FILENAME_AUDIO = "standard.ogg"
|
||||
MEDIA_BACKGROUND = "#ffffff"
|
||||
VIDEO_ENCODE_CODEC = "libsvtav1"
|
||||
VIDEO_ENCODE_EFFORT = "7"
|
||||
VIDEO_ENCODE_PARAMS = "lp=2"
|
||||
VIDEO_FILTERS = ""
|
||||
VIDEO_KEYFRAME_INTERVAL = 6
|
||||
VIDEO_PREVIEW_MAX_DURATION = 8
|
||||
VIDEO_PREVIEW_FPS = 16
|
||||
VIDEO_PREVIEW_SIZE = 240
|
||||
VIDEO_PREVIEW_QUALITY = "55"
|
||||
VIDEO_STANDARD_FPS = 60
|
||||
VIDEO_STANDARD_SIZE = 720
|
||||
VIDEO_STANDARD_QUALITY = "50"
|
||||
IMAGE_ENCODE_CODEC = "libsvtav1"
|
||||
IMAGE_ENCODE_EFFORT = "7"
|
||||
IMAGE_ENCODE_QUALITY = "45"
|
||||
IMAGE_ENCODE_PARAMS = "lp=2"
|
||||
IMAGE_FILTERS = "tpad=stop_mode=clone:stop_duration=1,"
|
||||
IMAGE_LARGE_SIZE = 2160
|
||||
IMAGE_PREVIEW_SIZE = 240
|
||||
)
|
||||
|
||||
func ternary[T any](cond bool, a, b T) T {
|
||||
if cond {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func POST_Uploads(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// --- Update Request Lifetime ---
|
||||
sse, err := tools.NewEventHelper(ctx, w, r)
|
||||
if err != nil {
|
||||
tools.SendServerError(w, r, err)
|
||||
}
|
||||
|
||||
rc := http.NewResponseController(w)
|
||||
if err := rc.SetWriteDeadline(time.Now().Add(10 * time.Minute)); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// --- [ Prevent Abuse ] ----------------------------------------------------------
|
||||
// --------------------------------------------------------------------------------
|
||||
var (
|
||||
ClientAddress = tools.RequestAddressHash(r)
|
||||
ClientToken = tools.RequestToken()
|
||||
ClientLength = max(0, r.ContentLength)
|
||||
)
|
||||
{
|
||||
// --- Request Upload Capacity ---
|
||||
if ClientLength == 0 {
|
||||
sse.SendClientError(tools.ERROR_BODY_EMPTY)
|
||||
return
|
||||
}
|
||||
if ClientLength > int64(tools.LIMIT_FILE+tools.LIMIT_JSON) {
|
||||
sse.SendClientError(tools.ERROR_BODY_TOO_LARGE)
|
||||
return
|
||||
}
|
||||
|
||||
if !tools.SEMA_UPLOADS.TryAcquire(ClientLength) {
|
||||
sse.SendClientError(tools.ERROR_SERVER_RESOURCES_EXHAUSTED)
|
||||
return
|
||||
}
|
||||
defer tools.SEMA_UPLOADS.Release(ClientLength)
|
||||
|
||||
// --- Check Ban List ---
|
||||
var SubjectCount int
|
||||
err := tools.Database.
|
||||
QueryRow(ctx, "SELECT COUNT(*) FROM gifuu.mod_banned WHERE address_hash = $1", ClientAddress).
|
||||
Scan(&SubjectCount)
|
||||
if err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
if SubjectCount > 0 {
|
||||
sse.SendClientError(tools.ERROR_GENERIC_FORBIDDEN)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// --- [ Parse Incoming Request ] -------------------------------------------------
|
||||
// --------------------------------------------------------------------------------
|
||||
var (
|
||||
TempID = tools.RequestSnowflake()
|
||||
TempIDString = strconv.FormatInt(TempID, 10)
|
||||
TempSuccess = false
|
||||
TempUpload *os.File
|
||||
TempLogger *os.File
|
||||
Body struct {
|
||||
Title string `json:"title"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
PathUpload = filepath.Join(tools.STORAGE_DISK_TEMP, TempIDString+".bin")
|
||||
PathLogger = filepath.Join(tools.STORAGE_DISK_TEMP, TempIDString+".log")
|
||||
PathAlpha = filepath.Join(tools.STORAGE_DISK_TEMP, TempIDString+"_"+MEDIA_FILENAME_ALPHA)
|
||||
PathPreview = filepath.Join(tools.STORAGE_DISK_TEMP, TempIDString+"_"+MEDIA_FILENAME_PREVIEW)
|
||||
PathStandard = filepath.Join(tools.STORAGE_DISK_TEMP, TempIDString+"_"+MEDIA_FILENAME_STANDARD)
|
||||
PathAudio = filepath.Join(tools.STORAGE_DISK_TEMP, TempIDString+"_"+MEDIA_FILENAME_AUDIO)
|
||||
)
|
||||
{
|
||||
// --- Create Temporary Files ---
|
||||
flags := os.O_CREATE | os.O_TRUNC | os.O_WRONLY
|
||||
|
||||
if f, err := os.OpenFile(PathUpload, flags, tools.FILE_PUBLIC); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
} else {
|
||||
TempUpload = f
|
||||
defer os.Remove(PathUpload)
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
if f, err := os.OpenFile(PathLogger, flags, tools.FILE_PRIVATE); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
} else {
|
||||
TempLogger = f
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
defer os.Remove(PathAlpha)
|
||||
defer os.Remove(PathPreview)
|
||||
defer os.Remove(PathStandard)
|
||||
defer os.Remove(PathAudio)
|
||||
|
||||
// --- Parse Form Body ---
|
||||
var haveFile, haveData bool
|
||||
reader, err := r.MultipartReader()
|
||||
if err != nil {
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_DATA)
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
part, err := reader.NextPart()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// --- Parse Incoming Metadata ---
|
||||
if part.FormName() == "data" {
|
||||
if haveData {
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
}
|
||||
haveData = true
|
||||
|
||||
if err := tools.ParseJSON(part, &Body); err != nil {
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_FIELD)
|
||||
break
|
||||
}
|
||||
|
||||
// Validate Struct
|
||||
if normal, ok := tools.NormalizeTitle(Body.Title); !ok {
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
} else {
|
||||
Body.Title = normal
|
||||
}
|
||||
|
||||
indexTags := make(map[string]struct{}, len(Body.Tags))
|
||||
for i, given := range Body.Tags {
|
||||
normal, ok := tools.NormalizeTag(given)
|
||||
if _, exists := indexTags[normal]; exists || !ok {
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
}
|
||||
indexTags[normal] = struct{}{}
|
||||
Body.Tags[i] = normal
|
||||
}
|
||||
|
||||
fmt.Fprintf(TempLogger, "Collected JSON : %s\n", Body)
|
||||
continue
|
||||
}
|
||||
|
||||
// --- Store Incoming Upload ---
|
||||
if part.FormName() == "file" {
|
||||
if haveFile {
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
}
|
||||
haveFile = true
|
||||
|
||||
// Check File Extension
|
||||
mediaAccept := false
|
||||
mediaType := part.Header.Get("Content-Type")
|
||||
for _, t := range tools.LIMIT_MIME_TYPE {
|
||||
if strings.EqualFold(t, mediaType) {
|
||||
mediaAccept = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !mediaAccept {
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_CONTENT_TYPE)
|
||||
return
|
||||
}
|
||||
|
||||
// Copy File
|
||||
mediaSize, err := io.Copy(TempUpload, io.LimitReader(part, ClientLength))
|
||||
if err != nil {
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_DATA)
|
||||
return
|
||||
}
|
||||
TempUpload.Close()
|
||||
|
||||
fmt.Fprintf(TempLogger,
|
||||
"Collected File : %s (Type: %s) (Size: %db)\n",
|
||||
mediaType, part.FileName(), mediaSize,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
}
|
||||
|
||||
if !haveFile || !haveData {
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
}
|
||||
|
||||
sse.SendJSON("id", TempIDString)
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// --- [ Media Validation ] -------------------------------------------------------
|
||||
// --------------------------------------------------------------------------------
|
||||
var (
|
||||
ProbeResults tools.ProbeResults // Probe Results
|
||||
ProbeVideo *tools.ProbeStream // Relevant Video Stream
|
||||
ProbeAudio *tools.ProbeStream // Relevant Audio Stream
|
||||
MediaSticker bool // Is Static?
|
||||
MediaFramerate int // Approximate Framerate
|
||||
MediaHeight int // Approximate Scaled Height
|
||||
MediaWidth int // Approximate Scaled Width
|
||||
MediaRating float32 // Worst value from Classification
|
||||
)
|
||||
{
|
||||
sse.SendJSON("step", map[string]any{"id": "PROBE_QUEUE", "message": "Queued for Probing"})
|
||||
t := time.Now()
|
||||
|
||||
// --- Acquire Probe Slot ---
|
||||
if err := tools.SEMA_PROBES.Acquire(ctx, 1); err != nil {
|
||||
return
|
||||
}
|
||||
defer tools.SEMA_PROBES.Release(1)
|
||||
|
||||
// --- Probe Media Stream ---
|
||||
sse.SendJSON("step", map[string]any{"id": "PROBE_START", "message": "Probing"})
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.CommandContext(ctx, "ffprobe",
|
||||
"-hide_banner",
|
||||
"-loglevel", "verbose",
|
||||
"-print_format", "json",
|
||||
"-show_streams",
|
||||
"-i", PathUpload,
|
||||
)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
|
||||
fmt.Fprintf(TempLogger, "\n%s\n%s\nProbing completed in %s\n\n",
|
||||
// extra newline for proper log padding
|
||||
stdout.String(),
|
||||
stderr.String(),
|
||||
time.Since(t),
|
||||
)
|
||||
|
||||
if ctx.Err() == context.Canceled {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &ProbeResults); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// --- Find Media Stream ---
|
||||
// isVideo: Only video streams (AV1, MP4, etc.)
|
||||
// isImage: Only select image streams if it's the only stream available
|
||||
for _, s := range ProbeResults.Streams {
|
||||
isVideo := (s.CodecType == "video")
|
||||
isImage := (s.CodecType == "image" && len(ProbeResults.Streams) == 1)
|
||||
isValid := true &&
|
||||
s.Width >= MEDIA_MIN_WIDTH && s.Height >= MEDIA_MIN_HEIGHT &&
|
||||
float64(s.Duration) <= float64(MEDIA_MAX_DURATION) &&
|
||||
ternary(s.NumberFrames < 2,
|
||||
s.Width < IMAGE_MAX_WIDTH && s.Height < IMAGE_MAX_HEIGHT,
|
||||
s.Width < VIDEO_MAX_WIDTH && s.Height < VIDEO_MAX_HEIGHT,
|
||||
)
|
||||
if isValid && (isVideo || isImage) && (ProbeVideo == nil || s.Duration > ProbeVideo.Duration) {
|
||||
ProbeVideo = &s
|
||||
}
|
||||
|
||||
isAudio := (s.CodecType == "audio")
|
||||
if isAudio {
|
||||
ProbeAudio = &s
|
||||
}
|
||||
}
|
||||
if ProbeVideo == nil {
|
||||
sse.SendClientError(tools.ERROR_MEDIA_INVALID)
|
||||
return
|
||||
}
|
||||
|
||||
// --- Detect Properties ---
|
||||
|
||||
// Remove Audio from Stickers
|
||||
MediaSticker = (ProbeVideo.NumberFrames < 2)
|
||||
if MediaSticker {
|
||||
ProbeAudio = nil
|
||||
}
|
||||
|
||||
// Approximate Video Properties
|
||||
d := ternary(MediaSticker, IMAGE_LARGE_SIZE, VIDEO_STANDARD_SIZE)
|
||||
s := float64(min(ProbeVideo.Height, d)) / float64(ProbeVideo.Height)
|
||||
MediaWidth = int(float64(ProbeVideo.Width)*s) &^ 1
|
||||
MediaHeight = int(float64(ProbeVideo.Height)*s) &^ 1
|
||||
MediaFramerate = ternary(MediaSticker, 1, min(int(ProbeVideo.RFrameRate), VIDEO_STANDARD_FPS))
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// --- [ Media Processing ] -------------------------------------------------------
|
||||
// --------------------------------------------------------------------------------
|
||||
{
|
||||
sse.SendJSON("step", map[string]any{"id": "ENCODE_QUEUE", "message": "Queued for Processing"})
|
||||
t := time.Now()
|
||||
|
||||
// --- Acquire Upload Slot ---
|
||||
if err := tools.SEMA_ENCODES.Acquire(ctx, 1); err != nil {
|
||||
return
|
||||
}
|
||||
defer tools.SEMA_ENCODES.Release(1)
|
||||
|
||||
// --- Prepare For Encoding ---
|
||||
sse.SendJSON("step", map[string]any{"id": "ENCODE_START", "message": "Processing"})
|
||||
|
||||
args := []string{
|
||||
"-loglevel", "verbose",
|
||||
"-hide_banner",
|
||||
"-stats",
|
||||
|
||||
"-i", PathUpload,
|
||||
"-filter_complex", fmt.Sprintf(""+
|
||||
"[0:%d]format=rgba[fg];[fg]split[fg1][fg2];[fg2]drawbox=x=0:y=0:w=iw:h=ih:color=%s:t=fill[bg];[bg][fg1]overlay[base];"+
|
||||
"[base]split=4[v1][v2][v3][v4];"+
|
||||
"[v1]%sscale=-2:%d:flags=lanczos,fps=%d[v1o];"+
|
||||
"[v2]%sscale=-2:%d:flags=lanczos,fps=%d[v2o];"+
|
||||
"[v3]%sscale=-2:%d:flags=lanczos,fps=%d[v3a];[v3a]format=rgba[v3f];[v3f]split[v3color][v3mask];[v3mask]alphaextract[v3ae];[v3color][v3ae]vstack[v3o];"+
|
||||
"[v4]%sscale=%d:%d:flags=neighbor,fps=%d[v4o];",
|
||||
|
||||
// Import
|
||||
ProbeVideo.Index,
|
||||
MEDIA_BACKGROUND,
|
||||
|
||||
// Export: Preview
|
||||
ternary(MediaSticker, IMAGE_FILTERS, VIDEO_FILTERS),
|
||||
min(ProbeVideo.Height, ternary(MediaSticker, IMAGE_PREVIEW_SIZE, VIDEO_PREVIEW_SIZE)),
|
||||
min(int(ProbeVideo.RFrameRate), ternary(MediaSticker, 1, VIDEO_PREVIEW_FPS)),
|
||||
|
||||
// Export: Standard
|
||||
ternary(MediaSticker, IMAGE_FILTERS, VIDEO_FILTERS),
|
||||
min(ProbeVideo.Height, ternary(MediaSticker, IMAGE_LARGE_SIZE, VIDEO_STANDARD_SIZE)),
|
||||
min(int(ProbeVideo.RFrameRate), ternary(MediaSticker, 1, VIDEO_STANDARD_FPS)),
|
||||
|
||||
// Export: Alpha
|
||||
ternary(MediaSticker, IMAGE_FILTERS, VIDEO_FILTERS),
|
||||
min(ProbeVideo.Height, ternary(MediaSticker, IMAGE_LARGE_SIZE, VIDEO_STANDARD_SIZE)),
|
||||
min(int(ProbeVideo.RFrameRate), ternary(MediaSticker, 1, VIDEO_STANDARD_FPS)),
|
||||
|
||||
// Export: Inference
|
||||
IMAGE_FILTERS,
|
||||
tools.MODEL_SIZE,
|
||||
tools.MODEL_SIZE,
|
||||
tools.MODEL_FRAMERATE,
|
||||
),
|
||||
|
||||
// Export Preview
|
||||
"-map", "[v1o]", "-an", "-sn",
|
||||
"-c:v" /*----------*/, ternary(MediaSticker, IMAGE_ENCODE_CODEC, VIDEO_ENCODE_CODEC),
|
||||
"-preset" /*-------*/, ternary(MediaSticker, IMAGE_ENCODE_EFFORT, VIDEO_ENCODE_EFFORT),
|
||||
"-qp" /*-----------*/, ternary(MediaSticker, IMAGE_ENCODE_QUALITY, VIDEO_PREVIEW_QUALITY),
|
||||
"-g" /*------------*/, strconv.Itoa(VIDEO_PREVIEW_FPS * VIDEO_KEYFRAME_INTERVAL),
|
||||
"-pix_fmt" /*------*/, MEDIA_COLOR_SPACE,
|
||||
"-color_range" /*--*/, MEDIA_COLOR_RANGE,
|
||||
"-svtav1-params" /**/, ternary(MediaSticker, IMAGE_ENCODE_PARAMS, VIDEO_ENCODE_PARAMS),
|
||||
"-frames:v" /*-----*/, ternary(MediaSticker, "1", strconv.Itoa(VIDEO_PREVIEW_FPS*VIDEO_PREVIEW_MAX_DURATION)),
|
||||
"-loop" /*---------*/, ternary(MediaSticker, "1", "0"),
|
||||
"-map_metadata" /*-*/, "-1",
|
||||
"-metadata", ("gifuu_machine=" + tools.MACHINE_HOSTNAME),
|
||||
"-metadata", ("gifuu_proverb=" + tools.MACHINE_PROVERB),
|
||||
PathPreview,
|
||||
|
||||
// Export: Standard
|
||||
"-map", "[v2o]", "-an", "-sn",
|
||||
"-c:v" /*----------*/, ternary(MediaSticker, IMAGE_ENCODE_CODEC, VIDEO_ENCODE_CODEC),
|
||||
"-preset" /*-------*/, ternary(MediaSticker, IMAGE_ENCODE_EFFORT, VIDEO_ENCODE_EFFORT),
|
||||
"-qp" /*-----------*/, ternary(MediaSticker, IMAGE_ENCODE_QUALITY, VIDEO_STANDARD_QUALITY),
|
||||
"-g" /*------------*/, strconv.Itoa(VIDEO_STANDARD_FPS * VIDEO_KEYFRAME_INTERVAL),
|
||||
"-pix_fmt" /*------*/, MEDIA_COLOR_SPACE,
|
||||
"-color_range" /*--*/, MEDIA_COLOR_RANGE,
|
||||
"-svtav1-params" /**/, ternary(MediaSticker, IMAGE_ENCODE_PARAMS, VIDEO_ENCODE_PARAMS),
|
||||
"-frames:v" /*-----*/, ternary(MediaSticker, "1", strconv.Itoa(VIDEO_STANDARD_FPS*MEDIA_MAX_DURATION)),
|
||||
"-loop" /*---------*/, ternary(MediaSticker, "1", "0"),
|
||||
"-map_metadata" /*-*/, "-1",
|
||||
"-metadata", ("gifuu_machine=" + tools.MACHINE_HOSTNAME),
|
||||
"-metadata", ("gifuu_proverb=" + tools.MACHINE_PROVERB),
|
||||
PathStandard,
|
||||
|
||||
// Export: Alpha
|
||||
"-map", "[v3o]", "-an", "-sn",
|
||||
"-c:v" /*----------*/, ternary(MediaSticker, IMAGE_ENCODE_CODEC, VIDEO_ENCODE_CODEC),
|
||||
"-preset" /*-------*/, ternary(MediaSticker, IMAGE_ENCODE_EFFORT, VIDEO_ENCODE_EFFORT),
|
||||
"-qp" /*-----------*/, ternary(MediaSticker, IMAGE_ENCODE_QUALITY, VIDEO_STANDARD_QUALITY),
|
||||
"-g" /*------------*/, strconv.Itoa(VIDEO_STANDARD_FPS * VIDEO_KEYFRAME_INTERVAL),
|
||||
"-pix_fmt" /*------*/, MEDIA_COLOR_SPACE,
|
||||
"-color_range" /*--*/, MEDIA_COLOR_RANGE,
|
||||
"-svtav1-params" /**/, IMAGE_ENCODE_PARAMS,
|
||||
"-frames:v" /*-----*/, ternary(MediaSticker, "1", strconv.Itoa(VIDEO_STANDARD_FPS*MEDIA_MAX_DURATION)),
|
||||
"-loop" /*---------*/, ternary(MediaSticker, "1", "0"),
|
||||
"-map_metadata" /*-*/, "-1",
|
||||
"-metadata", ("gifuu_machine=" + tools.MACHINE_HOSTNAME),
|
||||
"-metadata", ("gifuu_proverb=" + tools.MACHINE_PROVERB),
|
||||
PathAlpha,
|
||||
|
||||
// Export: Moderation
|
||||
"-map", "[v4o]", "-an", "-sn",
|
||||
"-f" /*-------*/, "rawvideo",
|
||||
"-fps_mode" /**/, "vfr",
|
||||
"-pix_fmt" /*-*/, "rgb24",
|
||||
"-",
|
||||
}
|
||||
|
||||
if ProbeAudio != nil {
|
||||
args = append(args,
|
||||
"-map", fmt.Sprintf("0:%d", ProbeAudio.Index), "-vn", "-sn",
|
||||
"-c:a", "libopus",
|
||||
"-b:a", "64k",
|
||||
"-af", "loudnorm=I=-16:TP=-2:LRA=11",
|
||||
"-ar", "48000",
|
||||
"-ac", "2",
|
||||
"-map_metadata", "-1",
|
||||
"-metadata", ("gifuu_machine=" + tools.MACHINE_HOSTNAME),
|
||||
"-metadata", ("gifuu_proverb=" + tools.MACHINE_PROVERB),
|
||||
PathAudio,
|
||||
)
|
||||
}
|
||||
|
||||
var encodeCtx, encodeCancel = context.WithCancel(ctx)
|
||||
defer encodeCancel()
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.CommandContext(encodeCtx, "ffmpeg", args...)
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// --- Auto Moderation ---
|
||||
var (
|
||||
classifyError error
|
||||
classifyPercent float32
|
||||
classifyAllowed = true
|
||||
classifyComplete = make(chan struct{}, 1)
|
||||
frameSize = (tools.MODEL_SIZE * tools.MODEL_SIZE * 3)
|
||||
tensorSize = (tools.MODEL_FRAMERATE * frameSize)
|
||||
tensorData = make([]float32, tensorSize)
|
||||
frameData = make([]byte, tensorSize)
|
||||
)
|
||||
|
||||
go func() {
|
||||
defer close(classifyComplete)
|
||||
defer io.Copy(io.Discard, stdout)
|
||||
frameIndex := 0
|
||||
for {
|
||||
// Process Raw Frames for Model
|
||||
n, err := io.ReadFull(stdout, frameData)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil && err != io.ErrUnexpectedEOF {
|
||||
encodeCancel()
|
||||
return
|
||||
}
|
||||
|
||||
frameCount := n / frameSize
|
||||
for i := 0; i < n; i++ {
|
||||
tensorData[i] = float32(frameData[i]) / 255.0
|
||||
}
|
||||
|
||||
// Classify Frames
|
||||
logits, err := tools.ModelClassifyTensorBatch(
|
||||
tensorData[:frameCount*frameSize],
|
||||
frameCount,
|
||||
)
|
||||
if err != nil {
|
||||
classifyError = err
|
||||
encodeCancel()
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate Results
|
||||
for idx, results := range logits {
|
||||
classifyPercent = (results.Hentai + results.Porn + (results.Sexy * 0.9))
|
||||
classifyAllowed = (classifyPercent < tools.MODEL_THRESHOLD_DENY)
|
||||
|
||||
fmt.Fprintf(TempLogger,
|
||||
"#%02d | D: %.2f | H: %.2f | N: %.2f | P: %.2f | S: %.2f | T: %.2f%% | OK: %t\n",
|
||||
frameIndex+idx,
|
||||
results.Drawing,
|
||||
results.Hentai,
|
||||
results.Neutral,
|
||||
results.Porn,
|
||||
results.Sexy,
|
||||
classifyPercent,
|
||||
classifyAllowed,
|
||||
)
|
||||
|
||||
if classifyPercent > MediaRating {
|
||||
MediaRating = classifyPercent
|
||||
}
|
||||
|
||||
if !classifyAllowed {
|
||||
encodeCancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
frameIndex += frameCount
|
||||
|
||||
// Calculate Progress (Approximate)
|
||||
sse.SendJSON("progress", map[string]any{
|
||||
"percent": strconv.FormatFloat(
|
||||
min(100, (float64(frameIndex)/float64(tools.MODEL_FRAMERATE))/float64(ProbeVideo.Duration)*100),
|
||||
'f', 2, 64,
|
||||
),
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
// --- Wait for Results ---
|
||||
err = cmd.Wait()
|
||||
<-classifyComplete
|
||||
|
||||
fmt.Fprintf(TempLogger, "\n%s\nProcessing completed in %s\n",
|
||||
stderr.String(),
|
||||
time.Since(t),
|
||||
)
|
||||
|
||||
if ctx.Err() == context.Canceled {
|
||||
return
|
||||
}
|
||||
if !classifyAllowed {
|
||||
sse.SendClientError(tools.ERROR_MEDIA_INAPPROPRIATE)
|
||||
return
|
||||
}
|
||||
if classifyError != nil {
|
||||
sse.SendServerError(classifyError)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sse.SendJSON("step", map[string]any{"id": "SERVER_FINALIZE", "message": "Syncing"})
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// --- [ Upload Objects ] ---------------------------------------------------------
|
||||
// --------------------------------------------------------------------------------
|
||||
{
|
||||
var (
|
||||
TargetDir = filepath.Join(tools.STORAGE_DISK_PUBLIC, TempIDString)
|
||||
TargetAlpha = filepath.Join(TargetDir, MEDIA_FILENAME_ALPHA)
|
||||
TargetPreview = filepath.Join(TargetDir, MEDIA_FILENAME_PREVIEW)
|
||||
TargetStandard = filepath.Join(TargetDir, MEDIA_FILENAME_STANDARD)
|
||||
TargetAudio = filepath.Join(TargetDir, MEDIA_FILENAME_AUDIO)
|
||||
)
|
||||
|
||||
// --- Create Directory ---
|
||||
if err := os.MkdirAll(TargetDir, tools.FILE_PUBLIC); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if !TempSuccess {
|
||||
if err := os.RemoveAll(TargetDir); err != nil {
|
||||
tools.LoggerStorage.Log(tools.WARN, "Failed to delete incomplete files: %s", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// --- Move Files ---
|
||||
for _, op := range []struct {
|
||||
ShouldCopy bool
|
||||
PathSource string
|
||||
PathTarget string
|
||||
ContentType string
|
||||
}{
|
||||
{true, PathAlpha, TargetAlpha, "video/webm"},
|
||||
{true, PathPreview, TargetPreview, "image/avif"},
|
||||
{true, PathStandard, TargetStandard, "image/avif"},
|
||||
{ProbeAudio != nil, PathAudio, TargetAudio, "audio/ogg"},
|
||||
} {
|
||||
if !op.ShouldCopy {
|
||||
continue
|
||||
}
|
||||
|
||||
s, err := os.Open(op.PathSource)
|
||||
if err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
t, err := os.OpenFile(op.PathTarget, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, tools.FILE_PUBLIC)
|
||||
if err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
defer t.Close()
|
||||
|
||||
if _, err := io.Copy(t, s); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// --- [ Upload Metadata ] --------------------------------------------------------
|
||||
// --------------------------------------------------------------------------------
|
||||
{
|
||||
// --- Begin Transaction ---
|
||||
tx, err := tools.Database.Begin(ctx)
|
||||
if err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
// --- Insert Row ---
|
||||
if _, err = tx.Exec(ctx,
|
||||
`INSERT INTO gifuu.upload (
|
||||
id,
|
||||
upload_address_hash,
|
||||
upload_token_hash,
|
||||
flag_sticker,
|
||||
flag_audio,
|
||||
encode_fps,
|
||||
encode_width,
|
||||
encode_height,
|
||||
meta_rating,
|
||||
meta_title
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||||
TempID,
|
||||
ClientAddress,
|
||||
tools.RequestHash(ClientToken),
|
||||
MediaSticker,
|
||||
ProbeAudio != nil,
|
||||
MediaFramerate,
|
||||
MediaWidth,
|
||||
MediaHeight,
|
||||
MediaRating,
|
||||
Body.Title,
|
||||
); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// --- Upsert Tags ---
|
||||
if _, err := tx.Exec(ctx,
|
||||
`WITH upserted_tags AS (
|
||||
INSERT INTO gifuu.tag (label)
|
||||
SELECT unnest($1::text[])
|
||||
ON CONFLICT (label) DO UPDATE SET label = EXCLUDED.label
|
||||
RETURNING id
|
||||
)
|
||||
INSERT INTO gifuu.upload_tag (gif_id, tag_id)
|
||||
SELECT $2, id FROM upserted_tags`,
|
||||
Body.Tags,
|
||||
TempID,
|
||||
); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// --- Commit Transaction ---
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// --- [ Return Results ] ---------------------------------------------------------
|
||||
// --------------------------------------------------------------------------------
|
||||
TempSuccess = true
|
||||
sse.SendJSON("finish", map[string]any{
|
||||
"id": strconv.FormatInt(TempID, 10),
|
||||
"edit_token": ClientToken,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user