large gifs are converted to vp9 instead of webp
This commit is contained in:
@@ -7432,6 +7432,10 @@ input#s_avatar {
|
|||||||
|
|
||||||
/* Comments System */
|
/* Comments System */
|
||||||
/* Primary definition moved up to line 1082 to avoid overrides */
|
/* Primary definition moved up to line 1082 to avoid overrides */
|
||||||
|
video.autoplay-gif {
|
||||||
|
background: rgba(0, 0, 0, 0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
#comments-container {
|
#comments-container {
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
font-family: var(--font);
|
font-family: var(--font);
|
||||||
@@ -7615,6 +7619,11 @@ input#s_avatar {
|
|||||||
background: #000;
|
background: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.video-embed-wrap:has(.autoplay-gif) {
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
.video-embed-wrap video {
|
.video-embed-wrap video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -504,6 +504,7 @@ class CommentSystem {
|
|||||||
|
|
||||||
contentEl.dataset.raw = this.escapeHtml(fullContent);
|
contentEl.dataset.raw = this.escapeHtml(fullContent);
|
||||||
contentEl.innerHTML = this.renderCommentContent(fullContent, commentId);
|
contentEl.innerHTML = this.renderCommentContent(fullContent, commentId);
|
||||||
|
CommentSystem.autoplayConvertedGifs(contentEl);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_f0ckDebug('[CommentSystem] _patchLiveCommentContent failed:', e);
|
_f0ckDebug('[CommentSystem] _patchLiveCommentContent failed:', e);
|
||||||
}
|
}
|
||||||
@@ -527,6 +528,7 @@ class CommentSystem {
|
|||||||
const contentEl = el.querySelector('.comment-content');
|
const contentEl = el.querySelector('.comment-content');
|
||||||
if (contentEl) {
|
if (contentEl) {
|
||||||
contentEl.innerHTML = this.renderCommentContent(data.content, data.comment_id);
|
contentEl.innerHTML = this.renderCommentContent(data.content, data.comment_id);
|
||||||
|
CommentSystem.autoplayConvertedGifs(contentEl);
|
||||||
|
|
||||||
// Flash effect to draw attention
|
// Flash effect to draw attention
|
||||||
el.classList.remove('new-item-fade');
|
el.classList.remove('new-item-fade');
|
||||||
@@ -1196,6 +1198,7 @@ class CommentSystem {
|
|||||||
this.container.innerHTML = html;
|
this.container.innerHTML = html;
|
||||||
this.restoreMediaState(mediaState);
|
this.restoreMediaState(mediaState);
|
||||||
this.syncSubscribeButton(isSubscribed);
|
this.syncSubscribeButton(isSubscribed);
|
||||||
|
CommentSystem.autoplayConvertedGifs(this.container);
|
||||||
|
|
||||||
// Attach media load listeners to re-stabilize scroll if a hash is active.
|
// Attach media load listeners to re-stabilize scroll if a hash is active.
|
||||||
// Only during the initial anchor scroll — never on subsequent renders (tab re-focus,
|
// Only during the initial anchor scroll — never on subsequent renders (tab re-focus,
|
||||||
@@ -1307,6 +1310,7 @@ class CommentSystem {
|
|||||||
if (contentEl && contentEl.dataset.raw !== incoming.content) {
|
if (contentEl && contentEl.dataset.raw !== incoming.content) {
|
||||||
_f0ckDebug(`[CommentSystem] Reconcile: Updating content for #c${id}`);
|
_f0ckDebug(`[CommentSystem] Reconcile: Updating content for #c${id}`);
|
||||||
contentEl.innerHTML = this.renderCommentContent(incoming.content, incoming.id);
|
contentEl.innerHTML = this.renderCommentContent(incoming.content, incoming.id);
|
||||||
|
CommentSystem.autoplayConvertedGifs(contentEl);
|
||||||
contentEl.dataset.raw = incoming.content;
|
contentEl.dataset.raw = incoming.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1553,7 +1557,7 @@ class CommentSystem {
|
|||||||
// Prevents concatenated URLs (url1.webpurl2.webp) being consumed as one giant src.
|
// Prevents concatenated URLs (url1.webpurl2.webp) being consumed as one giant src.
|
||||||
const safeS = `(?:(?!https?:\\/\\/)\\S)`;
|
const safeS = `(?:(?!https?:\\/\\/)\\S)`;
|
||||||
const imageRegex = new RegExp(`(?<![\\(\\[])(${domainOrRelative}(?:\\/${safeS}+\\.(?:jpg|jpeg|png|gif|webp)(?:\\?${safeS}+)?))(?![\\)\\]])`, 'gi');
|
const imageRegex = new RegExp(`(?<![\\(\\[])(${domainOrRelative}(?:\\/${safeS}+\\.(?:jpg|jpeg|png|gif|webp)(?:\\?${safeS}+)?))(?![\\)\\]])`, 'gi');
|
||||||
const rawVideoRegex = new RegExp(`(?<![\\(\\[])(${domainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]*\\.(?:mp4|webm|ogv|mov)(?:\\?[^\\s\\[\\]\\(\\)]+)?))`, 'gi');
|
const rawVideoRegex = new RegExp(`(?<![\\(\\[])(${domainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]*\\.(?:mp4|webm|ogv|mov)(?:\\?[^\\s\\[\\]\\(\\)]+)?(?:#gif)?))`, 'gi');
|
||||||
const rawAudioRegex = new RegExp(`(?<![\\(\\[])(${domainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]*\\.(?:mp3|ogg|wav|flac|aac|opus|m4a)(?:\\?[^\\s\\[\\]\\(\\)]+)?))`, 'gi');
|
const rawAudioRegex = new RegExp(`(?<![\\(\\[])(${domainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]*\\.(?:mp3|ogg|wav|flac|aac|opus|m4a)(?:\\?[^\\s\\[\\]\\(\\)]+)?))`, 'gi');
|
||||||
const mentionRegex = /(?<!\[)@([a-zA-Z0-9_\-\.]+)(?!\])|\[@([^\]]+)\]/g;
|
const mentionRegex = /(?<!\[)@([a-zA-Z0-9_\-\.]+)(?!\])|\[@([^\]]+)\]/g;
|
||||||
|
|
||||||
@@ -1666,9 +1670,14 @@ class CommentSystem {
|
|||||||
const mediaDomainOrRelative = `(?:(?:https?:\\/\\/|\\/\\/)?(?:${mediaHostsPart})|(?=\\/[a-zA-Z0-9_\\-]))`;
|
const mediaDomainOrRelative = `(?:(?:https?:\\/\\/|\\/\\/)?(?:${mediaHostsPart})|(?=\\/[a-zA-Z0-9_\\-]))`;
|
||||||
|
|
||||||
// Video embed: replace anchor links pointing to video files from allowed hosters with a video player
|
// Video embed: replace anchor links pointing to video files from allowed hosters with a video player
|
||||||
const videoEmbedRegex = new RegExp(`<a\\s[^>]*href="(${mediaDomainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]+\\.(?:mp4|webm|ogv|mov)(?:\\?[^\\s\\[\\]\\(\\)]+)?))"[^>]*>([\\s\\S]*?)<\\/a>`, 'gi');
|
const videoEmbedRegex = new RegExp(`<a\\s[^>]*href="(${mediaDomainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]+\\.(?:mp4|webm|ogv|mov)(?:\\?[^\\s\\[\\]\\(\\)]+)?(?:#gif)?))"[^>]*>([\\s\\S]*?)<\\/a>`, 'gi');
|
||||||
md = md.replace(videoEmbedRegex, (match, url) => {
|
md = md.replace(videoEmbedRegex, (match, url) => {
|
||||||
return `<span class="video-embed-wrap"><video src="${url}" controls loop muted playsinline preload="metadata"></video></span>`;
|
const isConvertedGif = url.endsWith('#gif');
|
||||||
|
const cleanUrl = url.replace(/#gif$/, '');
|
||||||
|
if (isConvertedGif) {
|
||||||
|
return `<span class="video-embed-wrap"><video src="${cleanUrl}" class="autoplay-gif" loop muted playsinline preload="auto"></video></span>`;
|
||||||
|
}
|
||||||
|
return `<span class="video-embed-wrap"><video src="${cleanUrl}" controls loop muted playsinline preload="metadata"></video></span>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Audio embed: replace anchor links pointing to audio files from allowed hosters with an audio player
|
// Audio embed: replace anchor links pointing to audio files from allowed hosters with an audio player
|
||||||
@@ -1728,6 +1737,23 @@ class CommentSystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force-play videos with autoplay attribute in a container.
|
||||||
|
* Browsers often block autoplay on dynamically inserted elements;
|
||||||
|
* calling .play() explicitly after DOM insertion resolves this.
|
||||||
|
*/
|
||||||
|
static autoplayConvertedGifs(container) {
|
||||||
|
if (!container) return;
|
||||||
|
const videos = container.querySelectorAll('video.autoplay-gif');
|
||||||
|
videos.forEach(v => {
|
||||||
|
v.autoplay = true;
|
||||||
|
v.muted = true;
|
||||||
|
v.play().catch(() => {
|
||||||
|
v.addEventListener('canplay', () => v.play().catch(() => {}), { once: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
buildBacklinkMap(comments) {
|
buildBacklinkMap(comments) {
|
||||||
this.backlinkMap = {};
|
this.backlinkMap = {};
|
||||||
const process = (c) => {
|
const process = (c) => {
|
||||||
@@ -2184,7 +2210,7 @@ class CommentSystem {
|
|||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (json.success && json.files && json.files.length > 0) {
|
if (json.success && json.files && json.files.length > 0) {
|
||||||
const fileData = json.files[0];
|
const fileData = json.files[0];
|
||||||
const url = `/c/${fileData.dest}`;
|
const url = `/c/${fileData.dest}${fileData.converted_gif ? '#gif' : ''}`;
|
||||||
textarea.value = textarea.value.replace(placeholder, url);
|
textarea.value = textarea.value.replace(placeholder, url);
|
||||||
|
|
||||||
// Update preview with actual thumbnail
|
// Update preview with actual thumbnail
|
||||||
|
|||||||
@@ -130,7 +130,7 @@
|
|||||||
const safeS = `(?:(?!https?:\\/\\/)\\S)`;
|
const safeS = `(?:(?!https?:\\/\\/)\\S)`;
|
||||||
const domainOrRelative = `(?:(?:https?:\\/\\/|\\/\\/)?(?:${hostsRegexPart})|(?<!\\S)(?=\\/[a-zA-Z0-9_\\-]))`;
|
const domainOrRelative = `(?:(?:https?:\\/\\/|\\/\\/)?(?:${hostsRegexPart})|(?<!\\S)(?=\\/[a-zA-Z0-9_\\-]))`;
|
||||||
const imageRegex = new RegExp(`(?<![\\(\\[])(${domainOrRelative}(?:\\/${safeS}+\\.(?:jpg|jpeg|png|gif|webp)(?:\\?${safeS}+)?))(?![\\)\\]])`, 'gi');
|
const imageRegex = new RegExp(`(?<![\\(\\[])(${domainOrRelative}(?:\\/${safeS}+\\.(?:jpg|jpeg|png|gif|webp)(?:\\?${safeS}+)?))(?![\\)\\]])`, 'gi');
|
||||||
const rawVideoRegex = new RegExp(`(?<![\\(\\[])(${domainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]*\\.(?:mp4|webm|ogv|mov)(?:\\?[^\\s\\[\\]\\(\\)]+)?))`, 'gi');
|
const rawVideoRegex = new RegExp(`(?<![\\(\\[])(${domainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]*\\.(?:mp4|webm|ogv|mov)(?:\\?[^\\s\\[\\]\\(\\)]+)?(?:#gif)?))`, 'gi');
|
||||||
const mentionRegex = /(?<!\[)@([a-zA-Z0-9_\-\.]+)(?!\])|\[@([^\]]+)\]/g;
|
const mentionRegex = /(?<!\[)@([a-zA-Z0-9_\-\.]+)(?!\])|\[@([^\]]+)\]/g;
|
||||||
|
|
||||||
const renderer = new marked.Renderer();
|
const renderer = new marked.Renderer();
|
||||||
@@ -303,18 +303,24 @@
|
|||||||
const mediaDomainOrRelative = `(?:(?:https?:\\/\\/|\\/\\/)?(?:${mediaHostsPart})|(?=\\/[a-zA-Z0-9_\\-]))`;
|
const mediaDomainOrRelative = `(?:(?:https?:\\/\\/|\\/\\/)?(?:${mediaHostsPart})|(?=\\/[a-zA-Z0-9_\\-]))`;
|
||||||
|
|
||||||
// Video label replacement: instead of embedding, show a link
|
// Video label replacement: instead of embedding, show a link
|
||||||
const videoEmbedRegex = new RegExp(`<a\\s[^>]*href="(${mediaDomainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]+\\.(?:mp4|webm|ogv|mov)(?:\\?[^\\s\\[\\]\\(\\)]+)?))"[^>]*>([\\s\\S]*?)<\\/a>`, 'gi');
|
const videoEmbedRegex = new RegExp(`<a\\s[^>]*href="(${mediaDomainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]+\\.(?:mp4|webm|ogv|mov)(?:\\?[^\\s\\[\\]\\(\\)]+)?(?:#gif)?))"[^>]*>([\\s\\S]*?)<\\/a>`, 'gi');
|
||||||
md = md.replace(videoEmbedRegex, (match, url) => {
|
md = md.replace(videoEmbedRegex, (match, url) => {
|
||||||
|
const isConvertedGif = url.endsWith('#gif');
|
||||||
|
const cleanUrl = url.replace(/#gif$/, '');
|
||||||
|
// Converted GIFs → inline autoplay in sidebar too
|
||||||
|
if (isConvertedGif) {
|
||||||
|
return `<span class="video-embed-wrap"><video src="${cleanUrl}" class="sidebar-comment-img autoplay-gif" loop muted playsinline preload="auto"></video></span>`;
|
||||||
|
}
|
||||||
let isSameSite = false;
|
let isSameSite = false;
|
||||||
try {
|
try {
|
||||||
const urlToParse = url.startsWith('//') ? window.location.protocol + url : url;
|
const urlToParse = cleanUrl.startsWith('//') ? window.location.protocol + cleanUrl : cleanUrl;
|
||||||
const urlObj = new URL(urlToParse, siteOrigin);
|
const urlObj = new URL(urlToParse, siteOrigin);
|
||||||
isSameSite = (urlObj.hostname === window.location.hostname);
|
isSameSite = (urlObj.hostname === window.location.hostname);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
isSameSite = url.startsWith(siteOrigin) || (url.startsWith('/') && !url.startsWith('//'));
|
isSameSite = cleanUrl.startsWith(siteOrigin) || (cleanUrl.startsWith('/') && !cleanUrl.startsWith('//'));
|
||||||
}
|
}
|
||||||
const label = isSameSite ? 'Video Link' : 'External Video Link';
|
const label = isSameSite ? 'Video Link' : 'External Video Link';
|
||||||
const targetHref = (itemId && commentId) ? `/${itemId}#c${commentId}` : (commentId ? `#sc${commentId}` : url);
|
const targetHref = (itemId && commentId) ? `/${itemId}#c${commentId}` : (commentId ? `#sc${commentId}` : cleanUrl);
|
||||||
const externalAttr = (itemId && commentId) || commentId ? '' : ' target="_blank" rel="noopener noreferrer"';
|
const externalAttr = (itemId && commentId) || commentId ? '' : ' target="_blank" rel="noopener noreferrer"';
|
||||||
return `<a href="${targetHref}"${externalAttr} class="sidebar-video-link"><i class="fa-solid fa-film"></i> ${label} »</a>`;
|
return `<a href="${targetHref}"${externalAttr} class="sidebar-video-link"><i class="fa-solid fa-film"></i> ${label} »</a>`;
|
||||||
});
|
});
|
||||||
@@ -482,6 +488,8 @@
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
|
// Auto-play converted GIF videos
|
||||||
|
container.querySelectorAll('video.autoplay-gif').forEach(v => { v.autoplay = true; v.muted = true; v.play().catch(() => { v.addEventListener('canplay', () => v.play().catch(() => {}), { once: true }); }); });
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderFromCache = () => {
|
const renderFromCache = () => {
|
||||||
@@ -500,6 +508,8 @@
|
|||||||
}
|
}
|
||||||
checkOverflow();
|
checkOverflow();
|
||||||
fetchSidebarYoutubeTitles(container);
|
fetchSidebarYoutubeTitles(container);
|
||||||
|
// Auto-play converted GIF videos
|
||||||
|
container.querySelectorAll('video.autoplay-gif').forEach(v => { v.autoplay = true; v.muted = true; v.play().catch(() => { v.addEventListener('canplay', () => v.play().catch(() => {}), { once: true }); }); });
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -604,6 +614,8 @@
|
|||||||
if (ioSentinel) container.appendChild(ioSentinel);
|
if (ioSentinel) container.appendChild(ioSentinel);
|
||||||
checkOverflow();
|
checkOverflow();
|
||||||
fetchSidebarYoutubeTitles(container);
|
fetchSidebarYoutubeTitles(container);
|
||||||
|
// Auto-play converted GIF videos
|
||||||
|
container.querySelectorAll('video.autoplay-gif').forEach(v => { v.autoplay = true; v.muted = true; v.play().catch(() => { v.addEventListener('canplay', () => v.play().catch(() => {}), { once: true }); }); });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hasMore = false;
|
hasMore = false;
|
||||||
@@ -690,6 +702,8 @@
|
|||||||
el.classList.add('new-item-fade');
|
el.classList.add('new-item-fade');
|
||||||
checkOverflow();
|
checkOverflow();
|
||||||
fetchSidebarYoutubeTitles(el);
|
fetchSidebarYoutubeTitles(el);
|
||||||
|
// Auto-play converted GIF videos
|
||||||
|
inner.querySelectorAll('video.autoplay-gif').forEach(v => { v.autoplay = true; v.muted = true; v.play().catch(() => { v.addEventListener('canplay', () => v.play().catch(() => {}), { once: true }); }); });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ db`CREATE TABLE IF NOT EXISTS public.comment_files (
|
|||||||
phash TEXT,
|
phash TEXT,
|
||||||
original_filename TEXT,
|
original_filename TEXT,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
)`.catch(() => {});
|
)`.catch(() => { });
|
||||||
db`CREATE INDEX IF NOT EXISTS idx_comment_files_comment_id ON public.comment_files(comment_id)`.catch(() => {});
|
db`CREATE INDEX IF NOT EXISTS idx_comment_files_comment_id ON public.comment_files(comment_id)`.catch(() => { });
|
||||||
db`CREATE INDEX IF NOT EXISTS idx_comment_files_checksum ON public.comment_files(checksum)`.catch(() => {});
|
db`CREATE INDEX IF NOT EXISTS idx_comment_files_checksum ON public.comment_files(checksum)`.catch(() => { });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse multipart form data supporting multiple files with the same field name.
|
* Parse multipart form data supporting multiple files with the same field name.
|
||||||
@@ -207,7 +207,7 @@ export const handleCommentUpload = async (req, res) => {
|
|||||||
// Verify actual MIME with `file` command
|
// Verify actual MIME with `file` command
|
||||||
let actualMime = (await queue.spawn('file', ['--mime-type', '-b', tmpPath])).stdout.trim();
|
let actualMime = (await queue.spawn('file', ['--mime-type', '-b', tmpPath])).stdout.trim();
|
||||||
if (!allowedMimes.includes(actualMime)) {
|
if (!allowedMimes.includes(actualMime)) {
|
||||||
await fs.unlink(tmpPath).catch(() => {});
|
await fs.unlink(tmpPath).catch(() => { });
|
||||||
return sendJson(res, {
|
return sendJson(res, {
|
||||||
success: false,
|
success: false,
|
||||||
msg: `Invalid file type detected: ${actualMime}`
|
msg: `Invalid file type detected: ${actualMime}`
|
||||||
@@ -236,21 +236,84 @@ export const handleCommentUpload = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let ext = cfg.mimes[actualMime] || 'bin';
|
let ext = cfg.mimes[actualMime] || 'bin';
|
||||||
|
// Max dimension for comment images (scale down if larger)
|
||||||
|
const COMMENT_IMG_MAX_DIM = 1280;
|
||||||
|
|
||||||
// Convert GIF → animated WebP to save disk space
|
// Convert GIF → WebP (small) or WebM (large) to save disk space
|
||||||
|
let convertedFromGif = false;
|
||||||
if (actualMime === 'image/gif') {
|
if (actualMime === 'image/gif') {
|
||||||
const webpTmpPath = tmpPath.replace(/\.tmp$/, '.webp');
|
const gifSize = file.data.length;
|
||||||
|
const GIF_WEBM_THRESHOLD = 5 * 1024 * 1024; // 8MB
|
||||||
|
let converted = false;
|
||||||
|
|
||||||
|
if (gifSize <= GIF_WEBM_THRESHOLD) {
|
||||||
|
// Small GIF → try WebP (with resize)
|
||||||
|
const webpTmpPath = tmpPath.replace(/\.tmp$/, '.webp');
|
||||||
|
try {
|
||||||
|
await queue.spawn('magick', [tmpPath, '-coalesce',
|
||||||
|
'-resize', `${COMMENT_IMG_MAX_DIM}x${COMMENT_IMG_MAX_DIM}>`,
|
||||||
|
'-quality', '80', webpTmpPath]);
|
||||||
|
const webpStat = await fs.stat(webpTmpPath);
|
||||||
|
if (webpStat.size < gifSize) {
|
||||||
|
await fs.unlink(tmpPath).catch(() => { });
|
||||||
|
await fs.rename(webpTmpPath, tmpPath);
|
||||||
|
actualMime = 'image/webp';
|
||||||
|
ext = 'webp';
|
||||||
|
converted = true;
|
||||||
|
console.log(`[COMMENT_UPLOAD] GIF → WebP (${(gifSize / 1024 / 1024).toFixed(1)}MB → ${(webpStat.size / 1024 / 1024).toFixed(1)}MB): ${file.filename}`);
|
||||||
|
} else {
|
||||||
|
console.log(`[COMMENT_UPLOAD] WebP larger than GIF, keeping original: ${file.filename}`);
|
||||||
|
await fs.unlink(webpTmpPath).catch(() => { });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[COMMENT_UPLOAD] GIF→WebP failed, keeping original:`, e.message);
|
||||||
|
await fs.unlink(webpTmpPath).catch(() => { });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Large GIF → go straight to WebM (VP9 with alpha + resize)
|
||||||
|
const webmTmpPath = tmpPath.replace(/\.tmp$/, '.webm');
|
||||||
|
try {
|
||||||
|
await queue.spawn('ffmpeg', [
|
||||||
|
'-y', '-i', tmpPath,
|
||||||
|
'-an',
|
||||||
|
'-vf', `scale='min(${COMMENT_IMG_MAX_DIM},iw)':min'(${COMMENT_IMG_MAX_DIM},ih)':force_original_aspect_ratio=decrease`,
|
||||||
|
'-c:v', 'libvpx-vp9',
|
||||||
|
'-pix_fmt', 'yuva420p',
|
||||||
|
'-auto-alt-ref', '0',
|
||||||
|
'-crf', '30', '-b:v', '0',
|
||||||
|
webmTmpPath
|
||||||
|
]);
|
||||||
|
const webmStat = await fs.stat(webmTmpPath);
|
||||||
|
if (webmStat.size < gifSize) {
|
||||||
|
await fs.unlink(tmpPath).catch(() => { });
|
||||||
|
await fs.rename(webmTmpPath, tmpPath);
|
||||||
|
actualMime = 'video/webm';
|
||||||
|
ext = 'webm';
|
||||||
|
converted = true;
|
||||||
|
convertedFromGif = true;
|
||||||
|
console.log(`[COMMENT_UPLOAD] GIF → WebM (${(gifSize / 1024 / 1024).toFixed(1)}MB → ${(webmStat.size / 1024 / 1024).toFixed(1)}MB): ${file.filename}`);
|
||||||
|
} else {
|
||||||
|
console.log(`[COMMENT_UPLOAD] WebM also larger, keeping original GIF: ${file.filename}`);
|
||||||
|
await fs.unlink(webmTmpPath).catch(() => { });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[COMMENT_UPLOAD] GIF→WebM failed, keeping original:`, e.message);
|
||||||
|
await fs.unlink(webmTmpPath).catch(() => { });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Downscale regular images (JPEG, PNG, WebP) if too large
|
||||||
|
if (actualMime.startsWith('image/') && actualMime !== 'image/gif') {
|
||||||
try {
|
try {
|
||||||
await queue.spawn('magick', [tmpPath, '-coalesce', '-quality', '80', webpTmpPath]);
|
const { stdout: dims } = await queue.spawn('magick', [tmpPath, '-format', '%wx%h', 'info:']);
|
||||||
// Replace the temp file with the converted one
|
const [w, h] = dims.trim().split('x').map(Number);
|
||||||
await fs.unlink(tmpPath).catch(() => {});
|
if (w > COMMENT_IMG_MAX_DIM || h > COMMENT_IMG_MAX_DIM) {
|
||||||
await fs.rename(webpTmpPath, tmpPath);
|
await queue.spawn('magick', [tmpPath, '-resize', `${COMMENT_IMG_MAX_DIM}x${COMMENT_IMG_MAX_DIM}>`, '-quality', '85', tmpPath]);
|
||||||
actualMime = 'image/webp';
|
console.log(`[COMMENT_UPLOAD] Resized ${w}x${h} → max ${COMMENT_IMG_MAX_DIM}px: ${file.filename}`);
|
||||||
ext = 'webp';
|
}
|
||||||
console.log(`[COMMENT_UPLOAD] Converted GIF → WebP: ${file.filename}`);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`[COMMENT_UPLOAD] GIF→WebP conversion failed, keeping original:`, e.message);
|
console.warn(`[COMMENT_UPLOAD] Resize check failed:`, e.message);
|
||||||
await fs.unlink(webpTmpPath).catch(() => {});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,7 +427,7 @@ export const handleCommentUpload = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clean up tmp
|
// Clean up tmp
|
||||||
await fs.unlink(tmpPath).catch(() => {});
|
await fs.unlink(tmpPath).catch(() => { });
|
||||||
|
|
||||||
// Generate thumbnail (same size as regular uploads = 512px)
|
// Generate thumbnail (same size as regular uploads = 512px)
|
||||||
const dynThumbSize = 512;
|
const dynThumbSize = 512;
|
||||||
@@ -376,20 +439,20 @@ export const handleCommentUpload = async (req, res) => {
|
|||||||
console.warn(`[COMMENT_UPLOAD] Thumbnail generation failed for ${filename}:`, err.message);
|
console.warn(`[COMMENT_UPLOAD] Thumbnail generation failed for ${filename}:`, err.message);
|
||||||
// Fallback to placeholder
|
// Fallback to placeholder
|
||||||
const tPath = path.join(cfg.paths.t, `cf_${uuid}.webp`);
|
const tPath = path.join(cfg.paths.t, `cf_${uuid}.webp`);
|
||||||
await queue.spawn('magick', ['-size', `${dynThumbSize}x${dynThumbSize}`, 'xc:#1a1a1a', tPath]).catch(() => {});
|
await queue.spawn('magick', ['-size', `${dynThumbSize}x${dynThumbSize}`, 'xc:#1a1a1a', tPath]).catch(() => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert into comment_files (comment_id is null; will be linked when comment is posted)
|
// Insert into comment_files (comment_id is null; will be linked when comment is posted)
|
||||||
const inserted = await db`
|
const inserted = await db`
|
||||||
INSERT INTO comment_files ${db({
|
INSERT INTO comment_files ${db({
|
||||||
user_id: req.session.id,
|
user_id: req.session.id,
|
||||||
dest: filename,
|
dest: filename,
|
||||||
mime: actualMime,
|
mime: actualMime,
|
||||||
size: file.data.length,
|
size: file.data.length,
|
||||||
checksum: checksum,
|
checksum: checksum,
|
||||||
phash: phash,
|
phash: phash,
|
||||||
original_filename: file.filename || null
|
original_filename: file.filename || null
|
||||||
}, 'user_id', 'dest', 'mime', 'size', 'checksum', 'phash', 'original_filename')}
|
}, 'user_id', 'dest', 'mime', 'size', 'checksum', 'phash', 'original_filename')}
|
||||||
RETURNING id, dest, mime
|
RETURNING id, dest, mime
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -397,7 +460,8 @@ export const handleCommentUpload = async (req, res) => {
|
|||||||
id: inserted[0].id,
|
id: inserted[0].id,
|
||||||
dest: inserted[0].dest,
|
dest: inserted[0].dest,
|
||||||
mime: inserted[0].mime,
|
mime: inserted[0].mime,
|
||||||
thumbnail: `/t/cf_${uuid}.webp`
|
thumbnail: `/t/cf_${uuid}.webp`,
|
||||||
|
converted_gif: convertedFromGif
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,7 +493,7 @@ async function generateCommentThumbnail(filename, mime, uuid, size = 512) {
|
|||||||
if (lstat.isSymbolicLink()) {
|
if (lstat.isSymbolicLink()) {
|
||||||
realSource = await fs.realpath(sourcePath);
|
realSource = await fs.realpath(sourcePath);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
|
|
||||||
if (mime.startsWith('video/') || mime === 'image/gif') {
|
if (mime.startsWith('video/') || mime === 'image/gif') {
|
||||||
const ffThumbSize = Math.max(size, 512);
|
const ffThumbSize = Math.max(size, 512);
|
||||||
@@ -452,7 +516,7 @@ async function generateCommentThumbnail(filename, mime, uuid, size = 512) {
|
|||||||
if (stat && stat.size > 0) {
|
if (stat && stat.size > 0) {
|
||||||
coverExtracted = true;
|
coverExtracted = true;
|
||||||
}
|
}
|
||||||
} catch (err) {}
|
} catch (err) { }
|
||||||
|
|
||||||
if (!coverExtracted) {
|
if (!coverExtracted) {
|
||||||
// Generate a placeholder for audio
|
// Generate a placeholder for audio
|
||||||
@@ -472,7 +536,7 @@ async function generateCommentThumbnail(filename, mime, uuid, size = 512) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup tmp
|
// Cleanup tmp
|
||||||
await fs.unlink(tmpFile).catch(() => {});
|
await fs.unlink(tmpFile).catch(() => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user