Feature: Shitpost Mode -> upload multiple files at once
This commit is contained in:
@@ -61,7 +61,13 @@
|
||||
"uploading": "Wird hochgeladen...",
|
||||
"pending_approval_patient": "Upload wartet auf Freigabe, bitte haben Sie etwas Geduld",
|
||||
"remove_file": "Datei entfernen",
|
||||
"cancel_upload": "Upload abbrechen"
|
||||
"cancel_upload": "Upload abbrechen",
|
||||
"shitpost_success": "{n} Beiträge erfolgreich gepostet!",
|
||||
"shitposting_status": "Wird gepostet",
|
||||
"item_comment_placeholder": "Kommentar (optional)...",
|
||||
"item_tags_placeholder": "Tags...",
|
||||
"btn_add_urls": "URL(s) hinzufügen",
|
||||
"tags_required_shitpost": "Alle Beiträge benötigen Tags"
|
||||
},
|
||||
"auth": {
|
||||
"registering": "Wird registriert...",
|
||||
@@ -305,6 +311,7 @@
|
||||
"enter_url": "URL eingeben",
|
||||
"tags_required": "{n} Tag(s) noch erforderlich",
|
||||
"select_rating": "SFW oder NSFW auswählen",
|
||||
"select_rating_nsfl": "SFW, NSFW oder NSFL auswählen",
|
||||
"embed_youtube": "YouTube-Video einbetten",
|
||||
"upload_from_url": "Von URL hochladen",
|
||||
"upload": "Hochladen"
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"url_tab_yt": "URL / YouTube",
|
||||
"url_placeholder": "Paste a URL to download...",
|
||||
"url_placeholder_yt": "Paste a URL or YouTube link...",
|
||||
"url_placeholder_shitpost": "Paste multiple URLs here (one per line)...",
|
||||
"drop_here": "Drop your file here",
|
||||
"admin_boost": "Admin Boost",
|
||||
"custom_thumbnail": "Custom Thumbnail",
|
||||
@@ -61,7 +62,13 @@
|
||||
"uploading": "Uploading...",
|
||||
"pending_approval_patient": "Upload awaits approval, please be patient",
|
||||
"remove_file": "Remove File",
|
||||
"cancel_upload": "Cancel Upload"
|
||||
"cancel_upload": "Cancel Upload",
|
||||
"shitpost_success": "Successfully shitposted {n} items!",
|
||||
"shitposting_status": "Shitposting",
|
||||
"item_comment_placeholder": "Comment (optional)...",
|
||||
"item_tags_placeholder": "Tags...",
|
||||
"btn_add_urls": "Add URL(s)",
|
||||
"tags_required_shitpost": "All items need tags"
|
||||
},
|
||||
"auth": {
|
||||
"registering": "Registering...",
|
||||
@@ -305,6 +312,7 @@
|
||||
"enter_url": "Enter a URL",
|
||||
"tags_required": "{n} more tag{s} required",
|
||||
"select_rating": "Select SFW or NSFW",
|
||||
"select_rating_nsfl": "Select SFW, NSFW or NSFL",
|
||||
"embed_youtube": "Embed YouTube Video",
|
||||
"upload_from_url": "Upload from URL",
|
||||
"upload": "Upload"
|
||||
|
||||
@@ -61,7 +61,13 @@
|
||||
"uploading": "Uploaden...",
|
||||
"pending_approval_patient": "Upload wacht op goedkeuring, even geduld alstublieft",
|
||||
"remove_file": "Bestand Verwijderen",
|
||||
"cancel_upload": "Upload Annuleren"
|
||||
"cancel_upload": "Upload Annuleren",
|
||||
"shitpost_success": "{n} items succesvol gepost!",
|
||||
"shitposting_status": "Lekker shitposten",
|
||||
"item_comment_placeholder": "Opmerking (optioneel)...",
|
||||
"item_tags_placeholder": "Etiketten...",
|
||||
"btn_add_urls": "URL(s) toevoegen",
|
||||
"tags_required_shitpost": "Alle items hebben tags nodig"
|
||||
},
|
||||
"auth": {
|
||||
"registering": "Registreren...",
|
||||
@@ -305,6 +311,7 @@
|
||||
"enter_url": "Voer een URL in",
|
||||
"tags_required": "{n} extra tag{s} vereist",
|
||||
"select_rating": "Selecteer SFW of NSFW",
|
||||
"select_rating_nsfl": "Selecteer SFW, NSFW of NSFL",
|
||||
"embed_youtube": "YouTube-video Insluiten",
|
||||
"upload_from_url": "Uploaden via URL",
|
||||
"upload": "Uploaden"
|
||||
|
||||
@@ -61,7 +61,11 @@
|
||||
"uploading": "Wird aufladiert...",
|
||||
"pending_approval_patient": "Die Ladung harrt der Absegnung, bitte haben Sie Geduld",
|
||||
"remove_file": "Datei entfernen",
|
||||
"cancel_upload": "Aufladierung abbrechen"
|
||||
"cancel_upload": "Aufladierung abbrechen",
|
||||
"shitpost_success": "{n} Fetzen erfolgreich gepfeffert!",
|
||||
"shitposting_status": "Wird gepfeffert",
|
||||
"item_comment_placeholder": "Senf dazugeben (optional)...",
|
||||
"item_tags_placeholder": "Etiketten..."
|
||||
},
|
||||
"auth": {
|
||||
"registering": "Registrierung wird in die Wege geleitet...",
|
||||
|
||||
@@ -253,6 +253,16 @@ export default new class queue {
|
||||
const tmpFile = path.join(os.tmpdir(), itemid + '.png');
|
||||
const tmpJpg = path.join(os.tmpdir(), itemid + '.jpg');
|
||||
|
||||
// Resolve real path if it's a symlink (important for reposts)
|
||||
let sourcePath = path.join(bDir, filename);
|
||||
try {
|
||||
const lstat = await fs.promises.lstat(sourcePath);
|
||||
if (lstat.isSymbolicLink()) {
|
||||
sourcePath = await fs.promises.realpath(sourcePath);
|
||||
console.log(`[QUEUE] Resolved symlink for thumbnailing: ${filename} -> ${sourcePath}`);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
if (mime === 'video/youtube') {
|
||||
const videoId = filename.startsWith('yt:') ? filename.split(':')[1] : null;
|
||||
if (videoId) {
|
||||
@@ -271,7 +281,7 @@ export default new class queue {
|
||||
else if (mime.startsWith('video/') || mime == 'image/gif') {
|
||||
const seeks = ['20%', '40%', '60%', '80%'];
|
||||
for (const seek of seeks) {
|
||||
await this.spawn('ffmpegthumbnailer', ['-i', path.join(bDir, filename), '-s', '1024', '-t', seek, '-o', tmpFile]);
|
||||
await this.spawn('ffmpegthumbnailer', ['-i', sourcePath, '-s', '1024', '-t', seek, '-o', tmpFile]);
|
||||
try {
|
||||
const { stdout } = await this.spawn('magick', [tmpFile, '-colorspace', 'Gray', '-format', '%[fx:mean]', 'info:']);
|
||||
if (parseFloat(stdout.trim()) > 0.05) break;
|
||||
@@ -279,9 +289,10 @@ export default new class queue {
|
||||
}
|
||||
}
|
||||
else if (mime.startsWith('image/') && mime != 'image/gif')
|
||||
await this.spawn('magick', [path.join(bDir, filename) + '[0]', tmpFile]);
|
||||
await this.spawn('magick', [sourcePath + '[0]', tmpFile]);
|
||||
else if (mime.startsWith('audio/')) {
|
||||
let coverExtracted = false;
|
||||
this._lastCoverExtracted = false; // Reset state for this call
|
||||
if (link.match(/soundcloud/)) {
|
||||
const proxyArgs = (cfg.main.socks && cfg.main.socks !== 'undefined' && cfg.main.socks !== '') ? ['--proxy', cfg.main.socks.includes('://') ? cfg.main.socks : `socks5h://${cfg.main.socks}`] : [];
|
||||
let cover = (await this.spawn('yt-dlp', [...proxyArgs, '-f', 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w', '--get-thumbnail', link])).stdout.trim().split('\n').map(l => l.trim()).filter(l => l.length > 0).pop();
|
||||
@@ -301,7 +312,7 @@ export default new class queue {
|
||||
}
|
||||
if (!coverExtracted) {
|
||||
try {
|
||||
await this.spawn('ffmpeg', ['-i', path.join(bDir, filename), '-an', '-vcodec', 'copy', '-frames:v', '1', '-update', '1', tmpJpg]);
|
||||
await this.spawn('ffmpeg', ['-i', sourcePath, '-an', '-vcodec', 'copy', '-frames:v', '1', '-update', '1', tmpJpg]);
|
||||
const size = (await fs.promises.stat(tmpJpg)).size;
|
||||
if (size > 0) {
|
||||
await this.spawn('magick', [tmpJpg, tmpFile]);
|
||||
@@ -313,7 +324,7 @@ export default new class queue {
|
||||
} else {
|
||||
// Try extracting embedded cover art (video stream in audio file)
|
||||
try {
|
||||
await this.spawn('ffmpeg', ['-i', path.join(bDir, filename), '-an', '-vcodec', 'copy', '-frames:v', '1', '-update', '1', tmpJpg]);
|
||||
await this.spawn('ffmpeg', ['-i', sourcePath, '-an', '-vcodec', 'copy', '-frames:v', '1', '-update', '1', tmpJpg]);
|
||||
const size = (await fs.promises.stat(tmpJpg)).size;
|
||||
if (size > 0) {
|
||||
await this.spawn('magick', [tmpJpg, tmpFile]);
|
||||
@@ -367,7 +378,7 @@ export default new class queue {
|
||||
'-dTextAlphaBits=4', '-dGraphicsAlphaBits=4',
|
||||
'-dLastPage=1',
|
||||
'-sOutputFile=' + tmpFile,
|
||||
path.join(bDir, filename)
|
||||
sourcePath
|
||||
]);
|
||||
} catch (err) {
|
||||
console.warn(`[QUEUE] PDF extraction failed for ${itemid}, using fallback icon.`);
|
||||
|
||||
@@ -12,7 +12,7 @@ import cfg from "../config.mjs";
|
||||
import security from "../security.mjs";
|
||||
import crypto from "crypto";
|
||||
import path from "path";
|
||||
import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getEnablePdf, setEnablePdf, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate } from "../settings.mjs";
|
||||
import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getEnablePdf, setEnablePdf, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate, getShitpostMode } from "../settings.mjs";
|
||||
|
||||
export default (router, tpl) => {
|
||||
router.get(/^\/login(\/)?$/, async (req, res) => {
|
||||
@@ -285,6 +285,7 @@ export default (router, tpl) => {
|
||||
log_user_ips: getLogUserIps(),
|
||||
hash_user_ips: getHashUserIps(),
|
||||
enable_cleanup: getEnableCleanup(),
|
||||
shitpost_mode: getShitpostMode(),
|
||||
enable_cleanup_config: cfg.websrv.enable_cleanup !== false,
|
||||
tmp: null
|
||||
}, req)
|
||||
@@ -625,6 +626,7 @@ export default (router, tpl) => {
|
||||
setRegistrationOpen(registration_open === 'true');
|
||||
}
|
||||
|
||||
|
||||
await db`INSERT INTO site_settings (key, value) VALUES ('min_tags', ${min_tags.toString()}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`;
|
||||
await db`INSERT INTO site_settings (key, value) VALUES ('trusted_uploads', ${trusted_uploads.toString()}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`;
|
||||
|
||||
|
||||
@@ -145,6 +145,57 @@ export default router => {
|
||||
return [...new Set(tags)];
|
||||
};
|
||||
|
||||
group.get(/\/meta\/extract-url$/, lib.loggedin, async (req, res) => {
|
||||
const url = req.url.qs?.url;
|
||||
if (!url) return res.json({ success: false, msg: 'URL required' }, 400);
|
||||
|
||||
try {
|
||||
const results = [];
|
||||
const seen = new Set();
|
||||
const addResult = (val) => {
|
||||
if (!val) return;
|
||||
const clean = String(val).replace(/<[^>]*>/g, '').replace(/[\x00-\x1F\x7F]/g, '').trim();
|
||||
if (clean && clean.length > 1 && clean.length <= 255 && !seen.has(clean.toLowerCase())) {
|
||||
seen.add(clean.toLowerCase());
|
||||
results.push(clean);
|
||||
}
|
||||
};
|
||||
|
||||
// Add domain and auto-tags
|
||||
const auto = autoTagsFromUrl(url);
|
||||
auto.forEach(t => addResult(t));
|
||||
|
||||
// Try to get title via yt-dlp for supported sites
|
||||
try {
|
||||
const proxyArgs = (cfg.main.socks && cfg.main.socks !== 'undefined') ? ['--proxy', cfg.main.socks] : [];
|
||||
const { stdout } = await queue.spawn('yt-dlp', [
|
||||
...proxyArgs,
|
||||
'--get-title',
|
||||
'--get-description',
|
||||
'--no-playlist',
|
||||
'--skip-download',
|
||||
url
|
||||
], { quiet: true, timeout: 5000 });
|
||||
|
||||
if (stdout) {
|
||||
const lines = stdout.split('\n').map(l => l.trim()).filter(l => l.length > 0);
|
||||
if (lines[0]) addResult(lines[0]); // Title
|
||||
if (lines[1]) {
|
||||
// Description often has garbage, take only first line or short snippet
|
||||
const desc = lines[1].split(/[.\n]/)[0].trim();
|
||||
if (desc.length > 3) addResult(desc);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback or ignore
|
||||
}
|
||||
|
||||
return res.json({ success: true, fields: results });
|
||||
} catch (err) {
|
||||
return res.json({ success: false, msg: err.message }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
group.post(/\/upload-url$/, lib.loggedin, async (req, res) => {
|
||||
try {
|
||||
if (!cfg.websrv.web_url_upload) {
|
||||
@@ -515,7 +566,7 @@ export default router => {
|
||||
if (rating === 'nsfw' || rating === 'nsfl') await queue.genBlurredThumbnail(itemid, isApprovalRequired);
|
||||
} catch (err) {
|
||||
const tDir = isApprovalRequired ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
|
||||
await queue.spawn('magick', ['./mugge.png', path.join(tDir, `${itemid}.webp`)]).catch(() => {});
|
||||
await queue.spawn('magick', ['-size', '128x128', 'xc:#1a1a1a', path.join(tDir, `${itemid}.webp`)]).catch(() => {});
|
||||
}
|
||||
|
||||
const ratingTagId = rating === 'sfw' ? 1 : (rating === 'nsfw' ? 2 : (cfg.nsfl_tag_id || 3));
|
||||
|
||||
@@ -12,6 +12,8 @@ let enable_pdf = false;
|
||||
let enable_cleanup = false;
|
||||
let cleanup_start_date = '';
|
||||
let cleanup_end_date = '';
|
||||
export const getShitpostMode = () => !!cfg.websrv.shitpost_mode;
|
||||
export const setShitpostMode = (val) => {}; // No-op, strictly config-based
|
||||
|
||||
export const getEnableCleanup = () => {
|
||||
if (cfg.websrv.enable_cleanup === false) return false;
|
||||
|
||||
@@ -16,7 +16,7 @@ import { handleEmojiUpload } from "./emoji_upload_handler.mjs";
|
||||
import { handleHallImageUpload, handleHallImageDelete, handleHallDelete, handleHallUpdate, handleHallCreate } from "./hall_image_handler.mjs";
|
||||
import { handleMetaExtract } from "./meta_extract_handler.mjs";
|
||||
import { handleMetaStrip } from "./meta_strip_handler.mjs";
|
||||
import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getBypassDuplicateCheck, setBypassDuplicateCheck, getProtectFiles, setProtectFiles, getPrivateMessages, setPrivateMessages, getDefaultLayout, setDefaultLayout, getEnablePdf, setEnablePdf, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps } from "./inc/settings.mjs";
|
||||
import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getBypassDuplicateCheck, setBypassDuplicateCheck, getProtectFiles, setProtectFiles, getPrivateMessages, setPrivateMessages, getDefaultLayout, setDefaultLayout, getEnablePdf, setEnablePdf, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps, getShitpostMode, setShitpostMode } from "./inc/settings.mjs";
|
||||
import { updateHallsCache, getHalls } from "./inc/halls_cache.mjs";
|
||||
import { createI18n } from "./inc/i18n.mjs";
|
||||
import security from "./inc/security.mjs";
|
||||
@@ -660,6 +660,7 @@ process.on('uncaughtException', err => {
|
||||
console.warn(`[BOOT] Trusted Uploads fetch failed:`, e.message);
|
||||
}
|
||||
|
||||
|
||||
// Set enable_pdf from config (pure config setting)
|
||||
setEnablePdf(!!cfg.enable_pdf);
|
||||
console.log(`[BOOT] Enable PDF setting: ${getEnablePdf()}`);
|
||||
@@ -766,6 +767,7 @@ process.on('uncaughtException', err => {
|
||||
get registration_open() { return getRegistrationOpen(); },
|
||||
registration_web_toggle_enabled: cfg.websrv.open_registration_web_toggle !== false,
|
||||
get trusted_uploads() { return getTrustedUploads(); },
|
||||
get shitpost_mode() { return getShitpostMode(); },
|
||||
get about_text() { return getAboutText(); },
|
||||
get rules_text() { return getRulesText(); },
|
||||
get terms_text() { return getTermsText(); },
|
||||
|
||||
@@ -79,21 +79,27 @@ export const handleUpload = async (req, res, self) => {
|
||||
|
||||
const is_oc = (parts.is_oc === 'true' || parts.is_oc === '1');
|
||||
|
||||
const is_shitpost = (parts.is_shitpost === 'true' || parts.is_shitpost === '1');
|
||||
|
||||
if (!file || !file.data) {
|
||||
return sendJson(res, { success: false, msg: 'No file provided' }, 400);
|
||||
}
|
||||
|
||||
if (!rating || !['sfw', 'nsfw', 'nsfl'].includes(rating)) {
|
||||
// In shitpost mode, rating is optional — null means no rating tag is assigned (truly untagged)
|
||||
const effectiveRating = (rating && ['sfw', 'nsfw', 'nsfl'].includes(rating)) ? rating : (is_shitpost ? null : null);
|
||||
|
||||
if (!is_shitpost && !effectiveRating) {
|
||||
return sendJson(res, { success: false, msg: 'Rating (sfw/nsfw/nsfl) is required' }, 400);
|
||||
}
|
||||
|
||||
if (rating === 'nsfl' && !cfg.enable_nsfl) {
|
||||
if (effectiveRating === 'nsfl' && !cfg.enable_nsfl) {
|
||||
return sendJson(res, { success: false, msg: 'NSFL mode is currently disabled' }, 400);
|
||||
}
|
||||
|
||||
const tags = tagsRaw ? tagsRaw.split(',').map(t => t.trim()).filter(t => t.length > 0 && !['sfw', 'nsfw', 'nsfl'].includes(t.toLowerCase())) : [];
|
||||
const minTags = getMinTags();
|
||||
if (tags.length < minTags) {
|
||||
// In shitpost mode, tags are optional — items without tags enter as untagged
|
||||
if (!is_shitpost && tags.length < minTags) {
|
||||
return sendJson(res, { success: false, msg: `At least ${minTags} tags are required` }, 400);
|
||||
}
|
||||
|
||||
@@ -363,7 +369,7 @@ export const handleUpload = async (req, res, self) => {
|
||||
}
|
||||
|
||||
// Generate blurred thumbnail for NSFW/NSFL
|
||||
if (rating === 'nsfw' || rating === 'nsfl') {
|
||||
if (effectiveRating === 'nsfw' || effectiveRating === 'nsfl') {
|
||||
await queue.genBlurredThumbnail(itemid, isPending);
|
||||
}
|
||||
|
||||
@@ -382,11 +388,13 @@ export const handleUpload = async (req, res, self) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Tags
|
||||
const ratingTagId = rating === 'sfw' ? 1 : (rating === 'nsfw' ? 2 : (cfg.nsfl_tag_id || 3));
|
||||
await db`
|
||||
insert into tags_assign ${db({ item_id: itemid, tag_id: ratingTagId, user_id: req.session.id })}
|
||||
`;
|
||||
// Tags — rating tag only assigned if a rating was selected
|
||||
if (effectiveRating) {
|
||||
const ratingTagId = effectiveRating === 'sfw' ? 1 : (effectiveRating === 'nsfw' ? 2 : (cfg.nsfl_tag_id || 3));
|
||||
await db`
|
||||
insert into tags_assign ${db({ item_id: itemid, tag_id: ratingTagId, user_id: req.session.id })}
|
||||
`;
|
||||
}
|
||||
|
||||
for (const tagName of tags) {
|
||||
let tagRow = await db`
|
||||
@@ -476,7 +484,7 @@ export const handleUpload = async (req, res, self) => {
|
||||
if (actualMime.startsWith('audio')) {
|
||||
await moveSafe(path.join(cfg.paths.pending, 'ca', `${itemid}.webp`), coverDest);
|
||||
}
|
||||
if (rating === 'nsfw' || rating === 'nsfl') {
|
||||
if (effectiveRating === 'nsfw' || effectiveRating === 'nsfl') {
|
||||
await moveSafe(path.join(cfg.paths.pending, 't', `${itemid}_blur.webp`), blurDest);
|
||||
}
|
||||
}
|
||||
@@ -508,7 +516,7 @@ export const handleUpload = async (req, res, self) => {
|
||||
}
|
||||
|
||||
// Ensure blurred thumbnail exists if needed
|
||||
if (rating === 'nsfw' || rating === 'nsfl') {
|
||||
if (effectiveRating === 'nsfw' || effectiveRating === 'nsfl') {
|
||||
const tDir = isPending ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
|
||||
const blurPath = path.join(tDir, `${itemid}_blur.webp`);
|
||||
const blurExists = await fs.access(blurPath).then(() => true).catch(() => false);
|
||||
@@ -555,7 +563,7 @@ export const handleUpload = async (req, res, self) => {
|
||||
mime: actualMime,
|
||||
username: req.session.user,
|
||||
display_name: req.session.display_name || null,
|
||||
tag_id: rating === 'sfw' ? 1 : (rating === 'nsfw' ? 2 : (cfg.nsfl_tag_id || 3)),
|
||||
tag_id: effectiveRating ? (effectiveRating === 'sfw' ? 1 : (effectiveRating === 'nsfw' ? 2 : (cfg.nsfl_tag_id || 3))) : 0,
|
||||
is_oc: !!is_oc
|
||||
})})`;
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user