Update Mastodon.ts

Added video support.
master
Nes370 2026-10-11 09:39:09 -07:00
parent f9014fff03
commit 568660c6a9
1 changed files with 56 additions and 32 deletions

View File

@ -7,26 +7,34 @@ import {
} from "../ScrapeEngine.js"; } from "../ScrapeEngine.js";
interface Attachment { interface Attachment {
url: string; // original URL, ?sensitive=true stripped url: string; // original media URL, ?sensitive=true stripped
previewUrl?: string; // local cached preview (for video posters) previewUrl?: string; // poster / cached preview (for video posters)
contentType: ContentType; contentType: ContentType;
} }
/** /**
* Handles Mastodon-compatible instances, including forks such as baraag.net. * Handles Mastodon-compatible instances, including forks such as baraag.net.
* *
* On a Mastodon post page, media is rendered as: * Mastodon renders media in three different shapes:
* <div class="media-gallery__item">
* <a class="media-gallery__item-thumbnail" href=".../original/....png">
* <img src=".../small/....png">
* </a>
* </div>
* *
* The anchor href is the original (often federated) file. The <img> is a * 1. Images:
* scaled-down local cache. We read the anchor href, never the img src. * <a class="media-gallery__item-thumbnail" href=".../original/....png">
* <img src=".../small/....png">
* </a>
* → read the anchor href.
* *
* Multi-image posts repeat the same structure; we return one ScrapedPost * 2. GIFVs:
* per anchor, matching how Pixiv/Twitter engines behave. * <div class="media-gallery__gifv autoplay">
* <video class="media-gallery__item-gifv-thumbnail"
* src=".../original/....mp4" autoplay loop></video>
* </div>
* → read the <video> src (already the original .mp4).
*
* 3. Videos:
* <div role="menuitem" class="video-player">
* <video src=".../original/....mp4" poster=".../small/....png"></video>
* </div>
* → read the <video> src and use the poster attribute for extraContentUrl.
*/ */
export default class Mastodon extends ScrapeEngineBase { export default class Mastodon extends ScrapeEngineBase {
name = "mastodon"; name = "mastodon";
@ -35,8 +43,6 @@ export default class Mastodon extends ScrapeEngineBase {
"Rating is assumed to be safe." "Rating is assumed to be safe."
]; ];
// Explicit hosts we claim. Other Mastodon instances are picked up by the
// URL-shape check in canImport().
supportedHosts = ["baraag.net"]; supportedHosts = ["baraag.net"];
canImport(url: Location): boolean { canImport(url: Location): boolean {
@ -59,9 +65,7 @@ export default class Mastodon extends ScrapeEngineBase {
post.contentUrl = att.url; post.contentUrl = att.url;
post.contentType = att.contentType; post.contentType = att.contentType;
post.referrer = document.location.origin; post.referrer = document.location.origin;
// post.rating = "safe";
// Cached preview as a poster frame for videos.
if (att.contentType === "video" && att.previewUrl) { if (att.contentType === "video" && att.previewUrl) {
post.extraContentUrl = att.previewUrl; post.extraContentUrl = att.previewUrl;
} }
@ -78,20 +82,23 @@ export default class Mastodon extends ScrapeEngineBase {
private findAttachments(document: Document): Attachment[] { private findAttachments(document: Document): Attachment[] {
const out: Attachment[] = []; const out: Attachment[] = [];
const seen = new Set<string>();
// Primary selector, confirmed against baraag's rendered HTML for both const add = (att: Attachment) => {
// single- and multi-image posts. if (seen.has(att.url)) return;
seen.add(att.url);
out.push(att);
};
// 1. Images: anchor href points at the original.
let anchors = Array.from( let anchors = Array.from(
document.querySelectorAll<HTMLAnchorElement>("a.media-gallery__item-thumbnail[href]") document.querySelectorAll<HTMLAnchorElement>("a.media-gallery__item-thumbnail[href]")
); );
// Looser fallback for themes that drop the class.
if (anchors.length === 0) { if (anchors.length === 0) {
anchors = Array.from( anchors = Array.from(
document.querySelectorAll<HTMLAnchorElement>("a[href*='/media_attachments/']") document.querySelectorAll<HTMLAnchorElement>("a[href*='/media_attachments/']")
); );
} }
for (const a of anchors) { for (const a of anchors) {
const rawHref = a.getAttribute("href"); const rawHref = a.getAttribute("href");
if (!rawHref) continue; if (!rawHref) continue;
@ -99,14 +106,32 @@ export default class Mastodon extends ScrapeEngineBase {
const img = a.querySelector("img"); const img = a.querySelector("img");
const preview = img?.currentSrc || img?.src; const preview = img?.currentSrc || img?.src;
const url = this.normalizeUrl(rawHref); const url = this.normalizeUrl(rawHref) || this.largestFromSrcset(img);
const finalUrl = url || this.largestFromSrcset(img); if (!url) continue;
if (!finalUrl) continue;
out.push({ add({
url: finalUrl, url,
previewUrl: preview && preview !== finalUrl ? preview : undefined, previewUrl: preview && preview !== url ? preview : undefined,
contentType: this.contentTypeFor(finalUrl), contentType: this.contentTypeFor(url),
});
}
// 2 & 3. Videos and GIFVs: <video> elements carry the original .mp4 in src.
const videos = Array.from(document.querySelectorAll<HTMLVideoElement>("video[src]"));
for (const v of videos) {
const rawSrc = v.getAttribute("src");
if (!rawSrc) continue;
const url = this.normalizeUrl(rawSrc);
if (!url) continue;
// Use the poster as a thumbnail when available.
const poster = v.getAttribute("poster") || undefined;
add({
url,
previewUrl: poster && poster !== url ? poster : undefined,
contentType: "video",
}); });
} }
@ -114,9 +139,9 @@ export default class Mastodon extends ScrapeEngineBase {
} }
/** /**
* Strip Mastodon's ?sensitive=true marker and report whether it was present. * Strip Mastodon's ?sensitive=true marker. The marker is metadata, not
* The marker is metadata, not part of the media URL, and leaving it in can * part of the media URL, and leaving it in can confuse booru deduplication.
* confuse booru deduplication and content-type sniffing. * Returns an empty string if the URL is unusable.
*/ */
private normalizeUrl(href: string): string { private normalizeUrl(href: string): string {
try { try {
@ -128,7 +153,6 @@ export default class Mastodon extends ScrapeEngineBase {
} }
} }
/** Pick the highest-resolution candidate out of an <img srcset>. */
private largestFromSrcset(img: HTMLImageElement | null): string | undefined { private largestFromSrcset(img: HTMLImageElement | null): string | undefined {
if (!img) return undefined; if (!img) return undefined;
const srcset = img.getAttribute("srcset"); const srcset = img.getAttribute("srcset");