parent
f9014fff03
commit
568660c6a9
|
|
@ -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");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue