Add the picked providers and a Must include group
This commit is contained in:
+10
@@ -40,6 +40,16 @@ mobile draft at 390px.
|
|||||||
- [x] C11 preview caching: Discord-hosted image/GIF previews (WebP 320px, hash-addressed, 5 MiB
|
- [x] C11 preview caching: Discord-hosted image/GIF previews (WebP 320px, hash-addressed, 5 MiB
|
||||||
cap, one attempt) run after every walk in both modes. Live run `...-20260916090722`:
|
cap, one attempt) run after every walk in both modes. Live run `...-20260916090722`:
|
||||||
`previewed: 2`; thumbs served over `/api/media/<scrape>/<item>/thumb` (200).
|
`previewed: 2`; thumbs served over `/api/media/<scrape>/<item>/thumb` (200).
|
||||||
|
- [x] C48 Expanded providers and the Must include mirror. The list grew to 19 entries from the
|
||||||
|
user's picks: reddit, twitter, tiktok, instagram, streamable, facebook, gyazo, a merged
|
||||||
|
`giphy,tenor` (one checkbox, two substrings), 2ch.hk (stand-in for 4chan; the CDN hosts stay
|
||||||
|
behind the default URL exclusion), funnyjunk, and internet archive. The drawer gained a
|
||||||
|
**Must include** block mirroring **Do not include**: provider/source-kind checkboxes
|
||||||
|
(`includeProviders`, `includeSources`), file-type chips, and the size/length bounds (moved here
|
||||||
|
since they are inclusions, not exclusions). Store, `/api/media`, `/api/media/stats`, and MCP
|
||||||
|
all carry the two include params. Live-verified: `includeProviders=youtube` returns only
|
||||||
|
YouTube rows, `giphy,tenor` includes/excludes the same rows complementarily; smoke checks both
|
||||||
|
groups against the API list (22 = 19 + 3 kinds).
|
||||||
- [x] C47 Provider list from the dataset. Aggregated the `provider` column over all 11 scraper
|
- [x] C47 Provider list from the dataset. Aggregated the `provider` column over all 11 scraper
|
||||||
databases (distinct URLs): discord 13631 (folding cdn.discordapp.com and media.discordapp.net),
|
databases (distinct URLs): discord 13631 (folding cdn.discordapp.com and media.discordapp.net),
|
||||||
youtube 4840, tenor 352, twitch 196, twitter 111, steam 83, medal 59, sketchfab 31, gfycat 30,
|
youtube 4840, tenor 352, twitch 196, twitter 111, steam 83, medal 59, sketchfab 31, gfycat 30,
|
||||||
|
|||||||
+3
-3
@@ -149,9 +149,9 @@ that deletion is irreversible. The delete button is disabled for a 5 second coun
|
|||||||
and the multi-select People popover. The **Advanced search** drawer has aspect ratio, resolution,
|
and the multi-select People popover. The **Advanced search** drawer has aspect ratio, resolution,
|
||||||
source channel, posted-date range, file-type chips, cached-only, a default-on hide for 4chan
|
source channel, posted-date range, file-type chips, cached-only, a default-on hide for 4chan
|
||||||
links (they 404 almost always), a sort select (newest, oldest, user, size, length), and a **Do not
|
links (they 404 almost always), a sort select (newest, oldest, user, size, length), and a **Do not
|
||||||
include** group with nested collapsibles for sources (the ten most common providers plus embeds,
|
include** and **Must include** groups with nested collapsibles for sources (the common providers
|
||||||
attachments, and stickers), file types, size (with B/KB/MB/GB units), and length (seconds), with
|
in the archive plus embeds, attachments, and stickers), file types, size (with B/KB/MB/GB units),
|
||||||
**Apply filters** / **Reset** in the bar. Size and length ranges drop
|
and length (seconds), with **Apply filters** / **Reset** in the bar. Size and length ranges drop
|
||||||
rows with no known value. The bar and drawer show only on the Media tab.
|
rows with no known value. The bar and drawer show only on the Media tab.
|
||||||
Nothing renders until **Load media** is pressed. A **Metadata** dropdown toggles which fields
|
Nothing renders until **Load media** is pressed. A **Metadata** dropdown toggles which fields
|
||||||
show in the small banner under each tile (User, Type, Date, Source, Resolution, Length). Video
|
show in the small banner under each tile (User, Type, Date, Source, Resolution, Length). Video
|
||||||
|
|||||||
+10
-7
@@ -131,12 +131,15 @@ box live in Chat, not Call, and the connection stats are not chips yet.
|
|||||||
popover. The **Advanced search** drawer holds aspect ratio, resolution, source channel,
|
popover. The **Advanced search** drawer holds aspect ratio, resolution, source channel,
|
||||||
posted-date range, file-type chips (video mp4/webm/mov/mkv, image png/jpg/gif/webp/avif, audio),
|
posted-date range, file-type chips (video mp4/webm/mov/mkv, image png/jpg/gif/webp/avif, audio),
|
||||||
**Show only cached previews**, a default-on **Hide 4chan links (usually dead)**, and a
|
**Show only cached previews**, a default-on **Hide 4chan links (usually dead)**, and a
|
||||||
**Do not include** group with nested collapsibles for sources (the ten most common providers in
|
**Do not include** and **Must include** groups, each with nested collapsibles. **Do not include**
|
||||||
the archive — Discord, YouTube, Tenor, Twitch, Twitter, Steam, Medal, Sketchfab, Gfycat, Imgur —
|
hides sources (the common providers in the archive — Discord, YouTube, Giphy/Tenor, Twitch,
|
||||||
plus embeds, attachments, stickers), file types, size (less/greater than, with B/KB/MB/GB units),
|
Twitter, Reddit, Streamable, Instagram, Facebook, Steam, Gyazo, TikTok, Medal, Sketchfab, Gfycat,
|
||||||
and length (less/greater than, seconds). A sort select orders the grid by newest,
|
2ch.hk, Imgur, FunnyJunk, Internet Archive — plus embeds, attachments, stickers) and file types.
|
||||||
oldest, user, size, or length. Size and length ranges drop rows with no known value. The filter
|
**Must include** requires them (same provider list and kinds, file-type chips, and size/length
|
||||||
bar and the drawer are hidden on the Bot tab. Filters do not
|
bounds with B/KB/MB/GB units or seconds). A provider entry may carry several substrings, shown as
|
||||||
|
`Giphy/Tenor`. A sort select orders the grid by newest, oldest, user, size, or length. Size and
|
||||||
|
length bounds drop rows with no known value. The filter bar and the drawer are hidden on the Bot
|
||||||
|
tab. Filters do not
|
||||||
apply until **Apply filters** is pressed; **Reset** clears every filter and the zoom, unloads the
|
apply until **Apply filters** is pressed; **Reset** clears every filter and the zoom, unloads the
|
||||||
grid, and brings back the initial Load media prompt.
|
grid, and brings back the initial Load media prompt.
|
||||||
- A **Card size** slider scales tiles from two-thirds of the old size up to one image per row; the
|
- A **Card size** slider scales tiles from two-thirds of the old size up to one image per row; the
|
||||||
@@ -286,7 +289,7 @@ replaceable.
|
|||||||
| POST | `/api/audio/stop` | none | Stop speech and clear the queue |
|
| POST | `/api/audio/stop` | none | Stop speech and clear the queue |
|
||||||
| POST | `/api/audio/capture` | `start` | Start or stop capture |
|
| POST | `/api/audio/capture` | `start` | Start or stop capture |
|
||||||
| GET | `/api/transcript` | `limit?`, `sessionId?` | Sessions and captured segments |
|
| GET | `/api/transcript` | `limit?`, `sessionId?` | Sessions and captured segments |
|
||||||
| GET | `/api/media` | `kind?`, `source?`, `channelId?`, `authorId?` (comma list), `origin?` (`guild`/`dm`), `cachedOnly?`, `ext?` (comma list), `excludeHosts?` (comma list), `excludeExt?`, `excludeProviders?`, `excludeSources?` (comma lists), `sizeLt?`/`sizeGt?` (bytes), `lenLt?`/`lenGt?` (seconds), `sort?` (`newest` default, `oldest`, `user`, `size`, `length`), `aspect?`, `resolution?`, `after?`, `before?`, `limit?`, `offset?` | Media items across every scrape, newest first. `source` is `attachment`, `sticker`, or `embed`; `aspect` and `resolution` are the preset keys; `after`/`before` are ISO times on `posted_at`. Items carry `authorName`, `postedAt`, `width`, `height`, `aspectRatio`, `contentType`, `durationSecs`, `previewError`, `animUrl`; `localUrl`/`thumbUrl` when a file or preview exists |
|
| GET | `/api/media` | `kind?`, `source?`, `channelId?`, `authorId?` (comma list), `origin?` (`guild`/`dm`), `cachedOnly?`, `ext?` (comma list), `excludeHosts?` (comma list), `excludeExt?`, `excludeProviders?`, `excludeSources?`, `includeProviders?`, `includeSources?` (comma lists), `sizeLt?`/`sizeGt?` (bytes), `lenLt?`/`lenGt?` (seconds), `sort?` (`newest` default, `oldest`, `user`, `size`, `length`), `aspect?`, `resolution?`, `after?`, `before?`, `limit?`, `offset?` | Media items across every scrape, newest first. `source` is `attachment`, `sticker`, or `embed`; `aspect` and `resolution` are the preset keys; `after`/`before` are ISO times on `posted_at`. Items carry `authorName`, `postedAt`, `width`, `height`, `aspectRatio`, `contentType`, `durationSecs`, `previewError`, `animUrl`; `localUrl`/`thumbUrl` when a file or preview exists |
|
||||||
| GET | `/api/media/:scrapeId/:itemId/file` | none | The downloaded file, content-typed from its extension |
|
| GET | `/api/media/:scrapeId/:itemId/file` | none | The downloaded file, content-typed from its extension |
|
||||||
| GET | `/api/media/:scrapeId/:itemId/thumb` | none | The cached preview (type from `preview_ext`) |
|
| GET | `/api/media/:scrapeId/:itemId/thumb` | none | The cached preview (type from `preview_ext`) |
|
||||||
| GET | `/api/media/:scrapeId/:itemId/stream` | none | Playback/full-view source: the local file, or the remote URL refreshed from the source message. Supports `Range`. Non-media upstream responses (provider pages) return `415` |
|
| GET | `/api/media/:scrapeId/:itemId/stream` | none | Playback/full-view source: the local file, or the remote URL refreshed from the source message. Supports `Range`. Non-media upstream responses (provider pages) return `415` |
|
||||||
|
|||||||
@@ -50,21 +50,32 @@ export function resolutionCondition(preset: string): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The providers that dominate the archive, for the "Do not include" shortcuts.
|
* The providers that dominate the archive, for the "Do not include" and "Must include" shortcuts.
|
||||||
* Aggregated 2026-09-17 over media-scrape/db (`provider` column, distinct URLs; discord folds in
|
* Aggregated 2026-09-17 over media-scrape/db (`provider` column, distinct URLs; discord folds in
|
||||||
* cdn.discordapp.com and media.discordapp.net). Matching is a case-insensitive substring on
|
* cdn.discordapp.com, media.discordapp.net, and images-ext-*.discordapp.net). Matching is a
|
||||||
* `provider`, so "twitter" also covers fxtwitter.com. The 4chan hosts stay out: the default URL
|
* case-insensitive substring on `provider`, so "twitter" also covers fxtwitter.com, vxtwitter.com,
|
||||||
* host exclusion already hides them.
|
* and twitfix; "reddit" covers www.reddit.com and vxreddit. Entries with several substrings are
|
||||||
|
* joined with a comma and the UI shows "Giphy/Tenor": checking it applies both. 4chan's CDN hosts
|
||||||
|
* are hidden by the default URL host exclusion, so 2ch.hk stands in for the provider box.
|
||||||
*/
|
*/
|
||||||
export const PROVIDER_FILTERS = [
|
export const PROVIDER_FILTERS = [
|
||||||
"discord",
|
"discord",
|
||||||
"youtube",
|
"youtube",
|
||||||
"tenor",
|
"giphy,tenor",
|
||||||
"twitch",
|
"twitch",
|
||||||
"twitter",
|
"twitter",
|
||||||
|
"reddit",
|
||||||
|
"streamable",
|
||||||
|
"instagram",
|
||||||
|
"facebook",
|
||||||
"steam",
|
"steam",
|
||||||
|
"gyazo",
|
||||||
|
"tiktok",
|
||||||
"medal",
|
"medal",
|
||||||
"sketchfab",
|
"sketchfab",
|
||||||
"gfycat",
|
"gfycat",
|
||||||
"imgur"
|
"2ch.hk",
|
||||||
|
"imgur",
|
||||||
|
"funnyjunk",
|
||||||
|
"internet archive"
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -119,6 +119,10 @@ export interface MediaFilterOptions {
|
|||||||
excludeProviders?: string[];
|
excludeProviders?: string[];
|
||||||
/** Row sources to hide (attachment, sticker, embed). */
|
/** Row sources to hide (attachment, sticker, embed). */
|
||||||
excludeSources?: string[];
|
excludeSources?: string[];
|
||||||
|
/** Keep only providers matching one of these substrings; empty keeps all. */
|
||||||
|
includeProviders?: string[];
|
||||||
|
/** Keep only these row sources; empty keeps all. */
|
||||||
|
includeSources?: string[];
|
||||||
/** Bytes; rows with no known size drop out while this is set. */
|
/** Bytes; rows with no known size drop out while this is set. */
|
||||||
sizeLessThan?: number;
|
sizeLessThan?: number;
|
||||||
sizeGreaterThan?: number;
|
sizeGreaterThan?: number;
|
||||||
@@ -609,6 +613,17 @@ export class ScrapeStore {
|
|||||||
conditions.push(`COALESCE(source, '') NOT IN (${options.excludeSources.map(() => "?").join(", ")})`);
|
conditions.push(`COALESCE(source, '') NOT IN (${options.excludeSources.map(() => "?").join(", ")})`);
|
||||||
values.push(...options.excludeSources);
|
values.push(...options.excludeSources);
|
||||||
}
|
}
|
||||||
|
if (options.includeProviders?.length) {
|
||||||
|
const clauses = options.includeProviders.map(() => "lower(COALESCE(provider, '')) LIKE ?");
|
||||||
|
conditions.push(`(${clauses.join(" OR ")})`);
|
||||||
|
for (const provider of options.includeProviders) {
|
||||||
|
values.push(`%${provider.toLowerCase()}%`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options.includeSources?.length) {
|
||||||
|
conditions.push(`COALESCE(source, '') IN (${options.includeSources.map(() => "?").join(", ")})`);
|
||||||
|
values.push(...options.includeSources);
|
||||||
|
}
|
||||||
if (options.sizeLessThan !== undefined) {
|
if (options.sizeLessThan !== undefined) {
|
||||||
conditions.push("COALESCE(file_bytes, size) < ?");
|
conditions.push("COALESCE(file_bytes, size) < ?");
|
||||||
values.push(options.sizeLessThan);
|
values.push(options.sizeLessThan);
|
||||||
|
|||||||
@@ -1348,20 +1348,34 @@ void (async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await test("provider filters are the curated archive list", () => {
|
await test("provider filters are the curated archive list", () => {
|
||||||
assert.ok(
|
assert.ok(PROVIDER_FILTERS.length >= 12, `provider list has ${PROVIDER_FILTERS.length} entries`);
|
||||||
PROVIDER_FILTERS.length >= 8 && PROVIDER_FILTERS.length <= 12,
|
|
||||||
`provider list has ${PROVIDER_FILTERS.length} entries`
|
|
||||||
);
|
|
||||||
assert.equal(new Set(PROVIDER_FILTERS).size, PROVIDER_FILTERS.length);
|
assert.equal(new Set(PROVIDER_FILTERS).size, PROVIDER_FILTERS.length);
|
||||||
|
|
||||||
for (const value of PROVIDER_FILTERS) {
|
for (const entry of PROVIDER_FILTERS) {
|
||||||
assert.equal(value, value.toLowerCase());
|
const values = entry.split(",");
|
||||||
assert.ok(!value.includes(" ") && !value.includes("."), `host-like entry: ${value}`);
|
assert.ok(values.length >= 1 && values.every(value => value.length > 0), entry);
|
||||||
|
for (const value of values) assert.equal(value, value.toLowerCase(), entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const required of ["discord", "youtube", "twitch", "tenor"]) {
|
for (const required of [
|
||||||
|
"discord",
|
||||||
|
"youtube",
|
||||||
|
"twitch",
|
||||||
|
"twitter",
|
||||||
|
"reddit",
|
||||||
|
"instagram",
|
||||||
|
"tiktok",
|
||||||
|
"streamable",
|
||||||
|
"facebook",
|
||||||
|
"gyazo",
|
||||||
|
"funnyjunk",
|
||||||
|
"internet archive",
|
||||||
|
"2ch.hk"
|
||||||
|
]) {
|
||||||
assert.ok((PROVIDER_FILTERS as readonly string[]).includes(required), required);
|
assert.ok((PROVIDER_FILTERS as readonly string[]).includes(required), required);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert.ok((PROVIDER_FILTERS as readonly string[]).includes("giphy,tenor"), "giphy/tenor merged");
|
||||||
});
|
});
|
||||||
|
|
||||||
await test("store exclusion, size, and length filters", () => {
|
await test("store exclusion, size, and length filters", () => {
|
||||||
@@ -1417,6 +1431,10 @@ void (async () => {
|
|||||||
assert.deepEqual(ids({ lengthGreaterThan: 10 }), [5]);
|
assert.deepEqual(ids({ lengthGreaterThan: 10 }), [5]);
|
||||||
// Unknown lengths drop out while a length range is set.
|
// Unknown lengths drop out while a length range is set.
|
||||||
assert.deepEqual(ids({ lengthLessThan: 10 }), []);
|
assert.deepEqual(ids({ lengthLessThan: 10 }), []);
|
||||||
|
assert.deepEqual(ids({ includeProviders: ["youtube"] }), [3]);
|
||||||
|
assert.deepEqual(ids({ includeProviders: ["youtube", "twitch"] }), [3, 4]);
|
||||||
|
assert.deepEqual(ids({ includeSources: ["embed"] }), [3, 4]);
|
||||||
|
assert.deepEqual(ids({ includeSources: ["attachment"] }), [1, 2, 5]);
|
||||||
store.close();
|
store.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+105
-85
@@ -280,6 +280,12 @@
|
|||||||
const excludeExts = state.filters.excludeExts.length
|
const excludeExts = state.filters.excludeExts.length
|
||||||
? `excludeExt=${state.filters.excludeExts.join(",")}&`
|
? `excludeExt=${state.filters.excludeExts.join(",")}&`
|
||||||
: "";
|
: "";
|
||||||
|
const includeProviders = state.filters.includeProviders.length
|
||||||
|
? `includeProviders=${state.filters.includeProviders.join(",")}&`
|
||||||
|
: "";
|
||||||
|
const includeSources = state.filters.includeSources.length
|
||||||
|
? `includeSources=${state.filters.includeSources.join(",")}&`
|
||||||
|
: "";
|
||||||
const amount = (value, unit) => {
|
const amount = (value, unit) => {
|
||||||
const number = Number(value);
|
const number = Number(value);
|
||||||
if (value === "" || !Number.isFinite(number) || number < 0) return "";
|
if (value === "" || !Number.isFinite(number) || number < 0) return "";
|
||||||
@@ -295,7 +301,8 @@
|
|||||||
const before = state.filters.before ? `before=${state.filters.before}T23:59:59.999Z&` : "";
|
const before = state.filters.before ? `before=${state.filters.before}T23:59:59.999Z&` : "";
|
||||||
return (
|
return (
|
||||||
`${kind}${source}${channel}${authors}${origin}${cached}${exts}${excludeHosts}` +
|
`${kind}${source}${channel}${authors}${origin}${cached}${exts}${excludeHosts}` +
|
||||||
`${sort}${excludeProviders}${excludeSources}${excludeExts}${sizeLt}${sizeGt}${lenLt}${lenGt}` +
|
`${sort}${excludeProviders}${excludeSources}${excludeExts}` +
|
||||||
|
`${includeProviders}${includeSources}${sizeLt}${sizeGt}${lenLt}${lenGt}` +
|
||||||
`${aspect}${resolution}${after}${before}`
|
`${aspect}${resolution}${after}${before}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1169,6 +1176,8 @@
|
|||||||
sort: "newest",
|
sort: "newest",
|
||||||
excludeProviders: [],
|
excludeProviders: [],
|
||||||
excludeSources: [],
|
excludeSources: [],
|
||||||
|
includeProviders: [],
|
||||||
|
includeSources: [],
|
||||||
excludeExts: [],
|
excludeExts: [],
|
||||||
sizeLt: "",
|
sizeLt: "",
|
||||||
sizeGt: "",
|
sizeGt: "",
|
||||||
@@ -1193,6 +1202,8 @@
|
|||||||
excludeHosts: [...state.filters.excludeHosts],
|
excludeHosts: [...state.filters.excludeHosts],
|
||||||
excludeProviders: [...state.filters.excludeProviders],
|
excludeProviders: [...state.filters.excludeProviders],
|
||||||
excludeSources: [...state.filters.excludeSources],
|
excludeSources: [...state.filters.excludeSources],
|
||||||
|
includeProviders: [...state.filters.includeProviders],
|
||||||
|
includeSources: [...state.filters.includeSources],
|
||||||
excludeExts: [...state.filters.excludeExts]
|
excludeExts: [...state.filters.excludeExts]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1211,6 +1222,8 @@
|
|||||||
sort: state.draft.sort,
|
sort: state.draft.sort,
|
||||||
excludeProviders: [...state.draft.excludeProviders],
|
excludeProviders: [...state.draft.excludeProviders],
|
||||||
excludeSources: [...state.draft.excludeSources],
|
excludeSources: [...state.draft.excludeSources],
|
||||||
|
includeProviders: [...state.draft.includeProviders],
|
||||||
|
includeSources: [...state.draft.includeSources],
|
||||||
excludeExts: [...state.draft.excludeExts],
|
excludeExts: [...state.draft.excludeExts],
|
||||||
sizeLt: state.draft.sizeLt,
|
sizeLt: state.draft.sizeLt,
|
||||||
sizeGt: state.draft.sizeGt,
|
sizeGt: state.draft.sizeGt,
|
||||||
@@ -1434,34 +1447,6 @@
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const typeRow = (label, extensions) => {
|
|
||||||
const row = el("div", "vk-media-toolbar");
|
|
||||||
row.append(el("span", "vk-media-status", label));
|
|
||||||
for (const extension of extensions) {
|
|
||||||
const chip = el(
|
|
||||||
"button",
|
|
||||||
`vk-chip ${draft.exts.includes(extension) ? "vk-chip-on" : "vk-chip-off"}`,
|
|
||||||
extension
|
|
||||||
);
|
|
||||||
chip.type = "button";
|
|
||||||
chip.addEventListener("click", () => {
|
|
||||||
draft.exts = draft.exts.includes(extension)
|
|
||||||
? draft.exts.filter(value => value !== extension)
|
|
||||||
: [...draft.exts, extension];
|
|
||||||
renderFilterDrawer();
|
|
||||||
});
|
|
||||||
row.append(chip);
|
|
||||||
}
|
|
||||||
return row;
|
|
||||||
};
|
|
||||||
|
|
||||||
filters.append(
|
|
||||||
el("div", "vk-media-status", "File types"),
|
|
||||||
typeRow("Video", ["mp4", "webm", "mov", "mkv"]),
|
|
||||||
typeRow("Image", ["png", "jpg", "gif", "webp", "avif"]),
|
|
||||||
typeRow("Audio", ["mp3", "ogg", "wav", "m4a", "flac"])
|
|
||||||
);
|
|
||||||
|
|
||||||
const cachedRow = el("label", "vk-media-check");
|
const cachedRow = el("label", "vk-media-check");
|
||||||
const cachedBox = document.createElement("input");
|
const cachedBox = document.createElement("input");
|
||||||
cachedBox.type = "checkbox";
|
cachedBox.type = "checkbox";
|
||||||
@@ -1482,15 +1467,41 @@
|
|||||||
deadRow.append(deadBox, document.createTextNode(" Hide 4chan links (usually dead)"));
|
deadRow.append(deadBox, document.createTextNode(" Hide 4chan links (usually dead)"));
|
||||||
filters.append(deadRow);
|
filters.append(deadRow);
|
||||||
|
|
||||||
// "Do not include": nested collapsibles, so exclusions stay out of the way.
|
// "Do not include" and "Must include": mirrored collapsible groups.
|
||||||
const exclude = document.createElement("details");
|
const EXTS = [
|
||||||
exclude.className = "vk-media-exclude";
|
"mp4",
|
||||||
exclude.append(el("summary", "vk-media-exclude-title", "Do not include"));
|
"webm",
|
||||||
|
"mov",
|
||||||
|
"mkv",
|
||||||
|
"png",
|
||||||
|
"jpg",
|
||||||
|
"gif",
|
||||||
|
"webp",
|
||||||
|
"avif",
|
||||||
|
"mp3",
|
||||||
|
"ogg",
|
||||||
|
"wav",
|
||||||
|
"m4a",
|
||||||
|
"flac"
|
||||||
|
];
|
||||||
|
const SOURCE_KINDS = [
|
||||||
|
["embed", "Embeds"],
|
||||||
|
["attachment", "Attachments"],
|
||||||
|
["sticker", "Stickers"]
|
||||||
|
];
|
||||||
|
|
||||||
const group = title => {
|
const groupBlock = title => {
|
||||||
|
const detailsNode = document.createElement("details");
|
||||||
|
detailsNode.className = "vk-media-exclude";
|
||||||
|
detailsNode.append(el("summary", "vk-media-exclude-title", title));
|
||||||
|
return detailsNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const group = (parent, title) => {
|
||||||
const detailsNode = document.createElement("details");
|
const detailsNode = document.createElement("details");
|
||||||
detailsNode.className = "vk-media-exclude-group";
|
detailsNode.className = "vk-media-exclude-group";
|
||||||
detailsNode.append(el("summary", "vk-media-status", title));
|
detailsNode.append(el("summary", "vk-media-status", title));
|
||||||
|
parent.append(detailsNode);
|
||||||
return detailsNode;
|
return detailsNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1510,63 +1521,65 @@
|
|||||||
const toggle = (list, value, on) =>
|
const toggle = (list, value, on) =>
|
||||||
on ? [...new Set([...list, value])] : list.filter(entry => entry !== value);
|
on ? [...new Set([...list, value])] : list.filter(entry => entry !== value);
|
||||||
|
|
||||||
const sourceGroup = group("Sources");
|
const toggleMany = (list, values, on) =>
|
||||||
for (const value of state.status?.providerFilters ?? []) {
|
on ? [...new Set([...list, ...values])] : list.filter(entry => !values.includes(entry));
|
||||||
const label = value.charAt(0).toUpperCase() + value.slice(1);
|
|
||||||
sourceGroup.append(
|
|
||||||
checkRow(label, draft.excludeProviders.includes(value), on => {
|
|
||||||
draft.excludeProviders = toggle(draft.excludeProviders, value, on);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (const [value, label] of [
|
|
||||||
["embed", "Embeds"],
|
|
||||||
["attachment", "Attachments"],
|
|
||||||
["sticker", "Stickers"]
|
|
||||||
]) {
|
|
||||||
sourceGroup.append(
|
|
||||||
checkRow(label, draft.excludeSources.includes(value), on => {
|
|
||||||
draft.excludeSources = toggle(draft.excludeSources, value, on);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
exclude.append(sourceGroup);
|
|
||||||
|
|
||||||
const extGroup = group("File types");
|
/** Provider boxes; a list entry may carry several substrings ("giphy,tenor"). */
|
||||||
const extRow = el("div", "vk-media-toolbar");
|
const providerChecks = (parent, key) => {
|
||||||
for (const extension of [
|
const node = group(parent, "Sources");
|
||||||
"mp4",
|
for (const entry of state.status?.providerFilters ?? []) {
|
||||||
"webm",
|
const values = entry.split(",");
|
||||||
"mov",
|
const label = values.map(value => value.charAt(0).toUpperCase() + value.slice(1)).join("/");
|
||||||
"mkv",
|
node.append(
|
||||||
"png",
|
checkRow(
|
||||||
"jpg",
|
label,
|
||||||
"gif",
|
values.every(value => draft[key].includes(value)),
|
||||||
"webp",
|
on => {
|
||||||
"avif",
|
draft[key] = toggleMany(draft[key], values, on);
|
||||||
"mp3",
|
}
|
||||||
"ogg",
|
)
|
||||||
"wav",
|
);
|
||||||
"m4a",
|
}
|
||||||
"flac"
|
for (const [value, label] of SOURCE_KINDS) {
|
||||||
]) {
|
node.append(
|
||||||
|
checkRow(label, draft[key === "excludeProviders" ? "excludeSources" : "includeSources"].includes(value), on => {
|
||||||
|
const listKey = key === "excludeProviders" ? "excludeSources" : "includeSources";
|
||||||
|
draft[listKey] = toggle(draft[listKey], value, on);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extChips = (parent, key, mode) => {
|
||||||
|
const node = group(parent, "File types");
|
||||||
|
const row = el("div", "vk-media-toolbar");
|
||||||
|
for (const extension of EXTS) {
|
||||||
|
const active = draft[key].includes(extension);
|
||||||
const chip = el(
|
const chip = el(
|
||||||
"button",
|
"button",
|
||||||
`vk-chip ${draft.excludeExts.includes(extension) ? "vk-chip-off vk-chip-strike" : "vk-chip-on"}`,
|
mode === "exclude"
|
||||||
|
? `vk-chip ${active ? "vk-chip-off vk-chip-strike" : "vk-chip-on"}`
|
||||||
|
: `vk-chip ${active ? "vk-chip-on" : "vk-chip-off"}`,
|
||||||
extension
|
extension
|
||||||
);
|
);
|
||||||
chip.type = "button";
|
chip.type = "button";
|
||||||
chip.title = draft.excludeExts.includes(extension)
|
chip.title = active
|
||||||
|
? mode === "exclude"
|
||||||
? `Excluding ${extension}`
|
? `Excluding ${extension}`
|
||||||
: `${extension} included`;
|
: `Only ${extension}`
|
||||||
|
: mode === "exclude"
|
||||||
|
? `${extension} included`
|
||||||
|
: `${extension} not required`;
|
||||||
chip.addEventListener("click", () => {
|
chip.addEventListener("click", () => {
|
||||||
draft.excludeExts = toggle(draft.excludeExts, extension, !draft.excludeExts.includes(extension));
|
draft[key] = toggle(draft[key], extension, !active);
|
||||||
renderFilterDrawer();
|
renderFilterDrawer();
|
||||||
});
|
});
|
||||||
extRow.append(chip);
|
row.append(chip);
|
||||||
}
|
}
|
||||||
extGroup.append(extRow);
|
node.append(row);
|
||||||
exclude.append(extGroup);
|
return node;
|
||||||
|
};
|
||||||
|
|
||||||
const amountRow = (label, key, unitKey) => {
|
const amountRow = (label, key, unitKey) => {
|
||||||
const row = el("div", "vk-media-toolbar");
|
const row = el("div", "vk-media-toolbar");
|
||||||
@@ -1604,18 +1617,25 @@
|
|||||||
return row;
|
return row;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeGroup = group("Size");
|
const exclude = groupBlock("Do not include");
|
||||||
|
providerChecks(exclude, "excludeProviders");
|
||||||
|
extChips(exclude, "excludeExts", "exclude");
|
||||||
|
filters.append(exclude);
|
||||||
|
|
||||||
|
const include = groupBlock("Must include");
|
||||||
|
providerChecks(include, "includeProviders");
|
||||||
|
extChips(include, "exts", "include");
|
||||||
|
|
||||||
|
const sizeGroup = group(include, "Size");
|
||||||
sizeGroup.append(
|
sizeGroup.append(
|
||||||
amountRow("Less than", "sizeLt", "sizeLtUnit"),
|
amountRow("Less than", "sizeLt", "sizeLtUnit"),
|
||||||
amountRow("Greater than", "sizeGt", "sizeGtUnit")
|
amountRow("Greater than", "sizeGt", "sizeGtUnit")
|
||||||
);
|
);
|
||||||
exclude.append(sizeGroup);
|
|
||||||
|
|
||||||
const lengthGroup = group("Length");
|
const lengthGroup = group(include, "Length");
|
||||||
lengthGroup.append(amountRow("Less than", "lenLt"), amountRow("Greater than", "lenGt"));
|
lengthGroup.append(amountRow("Less than", "lenLt"), amountRow("Greater than", "lenGt"));
|
||||||
exclude.append(lengthGroup);
|
|
||||||
|
|
||||||
filters.append(exclude);
|
filters.append(include);
|
||||||
filterDrawer.append(filters);
|
filterDrawer.append(filters);
|
||||||
|
|
||||||
// Zoom applies live; it is not part of the filter draft.
|
// Zoom applies live; it is not part of the filter draft.
|
||||||
|
|||||||
@@ -896,6 +896,14 @@ function buildServer(): McpServer {
|
|||||||
.array(z.string())
|
.array(z.string())
|
||||||
.optional()
|
.optional()
|
||||||
.describe("Hide these row sources (attachment, sticker, embed)"),
|
.describe("Hide these row sources (attachment, sticker, embed)"),
|
||||||
|
includeProviders: z
|
||||||
|
.array(z.string())
|
||||||
|
.optional()
|
||||||
|
.describe("Keep only providers matching these substrings"),
|
||||||
|
includeSources: z
|
||||||
|
.array(z.string())
|
||||||
|
.optional()
|
||||||
|
.describe("Keep only these row sources (attachment, sticker, embed)"),
|
||||||
sizeLessThan: z.number().optional().describe("Only items smaller than this many bytes"),
|
sizeLessThan: z.number().optional().describe("Only items smaller than this many bytes"),
|
||||||
sizeGreaterThan: z.number().optional().describe("Only items larger than this many bytes"),
|
sizeGreaterThan: z.number().optional().describe("Only items larger than this many bytes"),
|
||||||
lengthLessThan: z.number().optional().describe("Only items shorter than this many seconds"),
|
lengthLessThan: z.number().optional().describe("Only items shorter than this many seconds"),
|
||||||
@@ -931,6 +939,8 @@ function buildServer(): McpServer {
|
|||||||
excludeExts,
|
excludeExts,
|
||||||
excludeProviders,
|
excludeProviders,
|
||||||
excludeSources,
|
excludeSources,
|
||||||
|
includeProviders,
|
||||||
|
includeSources,
|
||||||
sizeLessThan,
|
sizeLessThan,
|
||||||
sizeGreaterThan,
|
sizeGreaterThan,
|
||||||
lengthLessThan,
|
lengthLessThan,
|
||||||
@@ -957,6 +967,8 @@ function buildServer(): McpServer {
|
|||||||
excludeExts,
|
excludeExts,
|
||||||
excludeProviders,
|
excludeProviders,
|
||||||
excludeSources,
|
excludeSources,
|
||||||
|
includeProviders,
|
||||||
|
includeSources,
|
||||||
sizeLessThan,
|
sizeLessThan,
|
||||||
sizeGreaterThan,
|
sizeGreaterThan,
|
||||||
lengthLessThan,
|
lengthLessThan,
|
||||||
|
|||||||
@@ -717,6 +717,8 @@ export class MediaService {
|
|||||||
excludeExts?: string[];
|
excludeExts?: string[];
|
||||||
excludeProviders?: string[];
|
excludeProviders?: string[];
|
||||||
excludeSources?: string[];
|
excludeSources?: string[];
|
||||||
|
includeProviders?: string[];
|
||||||
|
includeSources?: string[];
|
||||||
sizeLessThan?: number;
|
sizeLessThan?: number;
|
||||||
sizeGreaterThan?: number;
|
sizeGreaterThan?: number;
|
||||||
lengthLessThan?: number;
|
lengthLessThan?: number;
|
||||||
@@ -846,6 +848,8 @@ export class MediaService {
|
|||||||
excludeExts?: string[];
|
excludeExts?: string[];
|
||||||
excludeProviders?: string[];
|
excludeProviders?: string[];
|
||||||
excludeSources?: string[];
|
excludeSources?: string[];
|
||||||
|
includeProviders?: string[];
|
||||||
|
includeSources?: string[];
|
||||||
sizeLessThan?: number;
|
sizeLessThan?: number;
|
||||||
sizeGreaterThan?: number;
|
sizeGreaterThan?: number;
|
||||||
lengthLessThan?: number;
|
lengthLessThan?: number;
|
||||||
@@ -881,6 +885,8 @@ export class MediaService {
|
|||||||
excludeExts: options.excludeExts,
|
excludeExts: options.excludeExts,
|
||||||
excludeProviders: options.excludeProviders,
|
excludeProviders: options.excludeProviders,
|
||||||
excludeSources: options.excludeSources,
|
excludeSources: options.excludeSources,
|
||||||
|
includeProviders: options.includeProviders,
|
||||||
|
includeSources: options.includeSources,
|
||||||
sizeLessThan: options.sizeLessThan,
|
sizeLessThan: options.sizeLessThan,
|
||||||
sizeGreaterThan: options.sizeGreaterThan,
|
sizeGreaterThan: options.sizeGreaterThan,
|
||||||
lengthLessThan: options.lengthLessThan,
|
lengthLessThan: options.lengthLessThan,
|
||||||
@@ -941,6 +947,8 @@ export class MediaService {
|
|||||||
excludeExts?: string[];
|
excludeExts?: string[];
|
||||||
excludeProviders?: string[];
|
excludeProviders?: string[];
|
||||||
excludeSources?: string[];
|
excludeSources?: string[];
|
||||||
|
includeProviders?: string[];
|
||||||
|
includeSources?: string[];
|
||||||
sizeLessThan?: number;
|
sizeLessThan?: number;
|
||||||
sizeGreaterThan?: number;
|
sizeGreaterThan?: number;
|
||||||
lengthLessThan?: number;
|
lengthLessThan?: number;
|
||||||
|
|||||||
@@ -211,6 +211,8 @@ function parseMediaFilters(query: Record<string, unknown>) {
|
|||||||
excludeExts: list(query.excludeExt),
|
excludeExts: list(query.excludeExt),
|
||||||
excludeProviders: list(query.excludeProviders),
|
excludeProviders: list(query.excludeProviders),
|
||||||
excludeSources: list(query.excludeSources),
|
excludeSources: list(query.excludeSources),
|
||||||
|
includeProviders: list(query.includeProviders),
|
||||||
|
includeSources: list(query.includeSources),
|
||||||
sizeLessThan: amount(query.sizeLt),
|
sizeLessThan: amount(query.sizeLt),
|
||||||
sizeGreaterThan: amount(query.sizeGt),
|
sizeGreaterThan: amount(query.sizeGt),
|
||||||
lengthLessThan: amount(query.lenLt),
|
lengthLessThan: amount(query.lenLt),
|
||||||
|
|||||||
@@ -136,6 +136,17 @@ try {
|
|||||||
sourceBoxes === providerFilters.length + 3,
|
sourceBoxes === providerFilters.length + 3,
|
||||||
{ sourceBoxes, providers: providerFilters.length }
|
{ sourceBoxes, providers: providerFilters.length }
|
||||||
);
|
);
|
||||||
|
const includeSources = await page
|
||||||
|
.locator(".vk-media-exclude-group")
|
||||||
|
.nth(2)
|
||||||
|
.locator(".vk-media-check")
|
||||||
|
.count();
|
||||||
|
step("must-include source checkboxes", includeSources);
|
||||||
|
check(
|
||||||
|
"must-include mirrors the provider list",
|
||||||
|
includeSources === providerFilters.length + 3,
|
||||||
|
{ includeSources, providers: providerFilters.length }
|
||||||
|
);
|
||||||
await page.locator(".vk-media-check", { hasText: "Show only cached previews" }).locator("input").check();
|
await page.locator(".vk-media-check", { hasText: "Show only cached previews" }).locator("input").check();
|
||||||
await page.getByRole("button", { name: "Apply filters" }).click();
|
await page.getByRole("button", { name: "Apply filters" }).click();
|
||||||
await page.waitForTimeout(2500);
|
await page.waitForTimeout(2500);
|
||||||
|
|||||||
Reference in New Issue
Block a user