Changeset 857a06f in Klonkt


Ignore:
Timestamp:
06/30/2026 05:43:39 PM (2 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
7ecbefc
Parents:
b5a09ce2
Message:

feat(video-cover): federate an animated cover as a Video attachment (not the WebP)

buildNote now prefers post.cover_video_url -> the muted loop MP4 ships as an AS2 Video attachment
instead of the animated WebP (unreliable on Mastodon + its iOS apps; the MP4 plays everywhere).
mediaType learns mp4/webm/mov; cover_video_url threaded through every buildNote source
(create/save hooks, Scheduler, outbox + featured + backfill SELECTs).

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

Location:
src
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    rb5a09ce2 r857a06f  
    7676  if (!site) return res.status(404).end();
    7777  const posts = db.prepare(
    78     `SELECT id, slug, title, content, cover_image_url, nsfw, content_warning, published_at, created_at
     78    `SELECT id, slug, title, content, cover_image_url, cover_video_url, nsfw, content_warning, published_at, created_at
    7979     FROM posts WHERE site_id = ? AND status = 'published' AND (fan_only IS NULL OR fan_only = 0)
    8080     ORDER BY COALESCE(published_at, created_at) DESC LIMIT 20`
     
    108108  // rank 1 last) → Mastodon flips it back to pin-rank ascending on the profile.
    109109  const posts = db.prepare(
    110     `SELECT id, slug, title, content, cover_image_url, nsfw, content_warning, published_at, created_at
     110    `SELECT id, slug, title, content, cover_image_url, cover_video_url, nsfw, content_warning, published_at, created_at
    111111     FROM posts WHERE site_id = ? AND status = 'published' AND (fan_only IS NULL OR fan_only = 0)
    112112       AND pinned IS NOT NULL AND pinned > 0
  • src/routes/posts.js

    rb5a09ce2 r857a06f  
    286286      ActivityPubService.deliverCreate(site, {
    287287        id: postId, slug: finalSlug, title: title || finalSlug,
    288         content: cleanContent, cover_image_url: cover_image_url || null,
     288        content: cleanContent, cover_image_url: cover_image_url || null, cover_video_url: req.body.cover_video_url || null,
    289289        published_at: publishedAt, created_at: now, fan_only: fanOnly, nsfw, content_warning: cw,
    290290      }).catch(() => { /* best-effort */ });
     
    417417    const apPost = {
    418418      id: post.id, slug: finalSlug, title: title || finalSlug,
    419       content: cleanContent, cover_image_url: cover_image_url || null,
     419      content: cleanContent, cover_image_url: cover_image_url || null, cover_video_url: req.body.cover_video_url || null,
    420420      published_at: publishedAt, created_at: post.created_at, fan_only: fanOnly, nsfw, content_warning: cw,
    421421    };
  • src/services/ActivityPubService.js

    rb5a09ce2 r857a06f  
    222222  const mediaType = (u) => {
    223223    const e = ((u || '').split('?')[0].match(/\.(\w+)$/) || [])[1];
    224     return ({ jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp', avif: 'image/avif' })[(e || '').toLowerCase()] || 'image/jpeg';
     224    return ({ jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp', avif: 'image/avif', mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime' })[(e || '').toLowerCase()] || 'image/jpeg';
    225225  };
    226226  const hadAudio = /\[\[(track|album|playlist):/i.test(post.content || '');
     
    256256  // link/player card are mutually exclusive on Mastodon. Link-only audio (external)
    257257  // keeps its cover (no player card to show).
    258   if (post.cover_image_url && !noImages) urls.push(abs(post.cover_image_url));
     258  // An animated cover federates as the muted loop MP4 (→ a Video attachment): animated WebP is
     259  // unreliable on Mastodon and its iOS apps; the MP4 plays everywhere. Else the still cover image.
     260  if (post.cover_video_url && !noImages) urls.push(abs(post.cover_video_url));
     261  else if (post.cover_image_url && !noImages) urls.push(abs(post.cover_image_url));
    259262  let body = post.content || '';
    260263  // Only federate inline images we can actually serve: absolute http(s) URLs, or our own
     
    976979  if (!site) return;
    977980  const recent = db.prepare(
    978     `SELECT id, slug, title, content, cover_image_url, nsfw, content_warning, published_at, created_at
     981    `SELECT id, slug, title, content, cover_image_url, cover_video_url, nsfw, content_warning, published_at, created_at
    979982     FROM posts WHERE site_id = ? AND status = 'published' AND (fan_only IS NULL OR fan_only = 0)
    980983     ORDER BY COALESCE(published_at, created_at) DESC LIMIT 20`
  • src/services/Scheduler.js

    rb5a09ce2 r857a06f  
    1515  try {
    1616    const due = db.prepare(`
    17       SELECT p.id, p.site_id, p.slug, p.title, p.content, p.cover_image_url, p.fan_only, p.nsfw, p.content_warning,
     17      SELECT p.id, p.site_id, p.slug, p.title, p.content, p.cover_image_url, p.cover_video_url, p.fan_only, p.nsfw, p.content_warning,
    1818             p.published_at, p.publish_at, p.created_at, u.username
    1919      FROM posts p JOIN users u ON u.id = p.author_id
     
    3838          ActivityPubService.deliverCreate(site, {
    3939            id: p.id, slug: p.slug, title: p.title || p.slug,
    40             content: p.content, cover_image_url: p.cover_image_url || null,
     40            content: p.content, cover_image_url: p.cover_image_url || null, cover_video_url: p.cover_video_url || null,
    4141            published_at: p.published_at || p.publish_at, created_at: p.created_at, fan_only: p.fan_only, nsfw: p.nsfw, content_warning: p.content_warning,
    4242          }).catch(() => { /* best-effort */ });
Note: See TracChangeset for help on using the changeset viewer.