Changeset 1d6f9a2 in Klonkt


Ignore:
Timestamp:
06/30/2026 05:29:01 PM (2 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
b5a09ce2
Parents:
ecbfa41
Message:

feat(video-cover): backend — convert animated WebP covers to a muted loop MP4 on upload

  • DB: posts.cover_video_url (the muted loop MP4 for an animated cover)
  • /posts/upload-image: an animated WebP → VideoCoverService.animatedWebpToVideo → returns {video} next to the still {url}; create + save persist cover_video_url (req.body.cover_video_url)

Co-Authored-By: Claude <noreply@…>

Location:
src
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    recbfa41 r1d6f9a2  
    8484  ensureColumn('posts', 'fan_only', 'INTEGER DEFAULT 0');  // fan-only preview (premium #3)
    8585  ensureColumn('posts', 'nsfw',     'INTEGER DEFAULT 0');  // sensitive content → blur + click-to-reveal; fediverse sensitive
     86  ensureColumn('posts', 'cover_video_url', 'TEXT');        // muted loop MP4 for an animated cover (Safari-smooth)
    8687  ensureColumn('posts', 'content_warning', 'TEXT');        // custom CW label (empty = default "Gevoelige inhoud")
    8788  ensureColumn('posts', 'type',    "TEXT DEFAULT 'post'");  // post | foto | video | audio
  • src/routes/posts.js

    recbfa41 r1d6f9a2  
    1818import { audioUrl } from '../services/AudioStreamService.js';
    1919import { toWebp } from '../services/ImageWebpService.js';
     20import VideoCoverService from '../services/VideoCoverService.js';
    2021import ActivityPubService from '../services/ActivityPubService.js';
    2122import MusicMeta from '../services/MusicMeta.js';
     
    7273// insert a markdown ![](url) into content.
    7374router.post('/posts/upload-image', requireAuth, (req, res) => {
    74   imageUpload.single('image')(req, res, (err) => {
     75  imageUpload.single('image')(req, res, async (err) => {
    7576    if (err) return res.status(400).json({ error: err.message });
    7677    if (!req.file) return res.status(400).json({ error: 'No file' });
    77     const url = '/media/post-images/' + toWebp(req.file);
    78     res.json({ url, size: req.file.size, mime: req.file.mimetype });
     78    const name = toWebp(req.file);
     79    const url = '/media/post-images/' + name;
     80    // An animated WebP cover → also make a muted loop MP4 (Safari plays it smoothly where the
     81    // animated WebP is janky on iOS). Best-effort; on failure we just return the still image.
     82    // The editor stores `video` in the hidden cover_video_url field for the cover.
     83    let video = null;
     84    try {
     85      const src = path.join(POST_IMAGES_DIR, name);
     86      if (VideoCoverService.isAnimatedWebp(src)) {
     87        const r = await VideoCoverService.animatedWebpToVideo(src, POST_IMAGES_DIR, path.basename(name, path.extname(name)) + '-v');
     88        if (r) video = '/media/post-images/' + path.basename(r.videoPath);
     89      }
     90    } catch { /* keep the still image */ }
     91    res.json({ url, video, size: req.file.size, mime: req.file.mimetype });
    7992  });
    8093});
     
    245258    INSERT INTO posts (
    246259      id, site_id, slug, author_id, title, content, excerpt,
    247       status, cover_image_url, pinned, tags, type, noindex, fan_only, nsfw, content_warning, publish_at,
     260      status, cover_image_url, cover_video_url, pinned, tags, type, noindex, fan_only, nsfw, content_warning, publish_at,
    248261      created_at, updated_at, published_at
    249     ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
     262    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    250263  `).run(
    251264    postId, site.id, finalSlug, req.session.user.id,
    252265    title || finalSlug, cleanContent, excerpt || '',
    253     finalStatus, cover_image_url || null, parsePinnedRank(pinned),
     266    finalStatus, cover_image_url || null, (req.body.cover_video_url || null), parsePinnedRank(pinned),
    254267    JSON.stringify((tags || '').split(',').map(t => t.trim()).filter(Boolean)),
    255268    finalType, noindex ? 1 : 0, fanOnly, nsfw, cw, publishAt,
     
    372385    UPDATE posts SET
    373386      title = ?, content = ?, excerpt = ?, status = ?,
    374       cover_image_url = ?, pinned = ?, tags = ?,
     387      cover_image_url = ?, cover_video_url = ?, pinned = ?, tags = ?,
    375388      type = ?, noindex = ?, fan_only = ?, nsfw = ?, content_warning = ?, publish_at = ?,
    376389      slug = ?, published_at = ?, updated_at = ?
     
    378391  `).run(
    379392    title, cleanContent, excerpt, finalStatus,
    380     cover_image_url || null, parsePinnedRank(pinned),
     393    cover_image_url || null, (req.body.cover_video_url || null), parsePinnedRank(pinned),
    381394    JSON.stringify((tags || '').split(',').map(t => t.trim()).filter(Boolean)),
    382395    finalType, noindex ? 1 : 0, fanOnly, nsfw, cw, publishAt,
Note: See TracChangeset for help on using the changeset viewer.