mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
New guide at /blog/tmdb-metadata-guide/: a sent/not-sent table for the opt-in enrichment (title, year, app language and the build's API key leave the machine; nothing about the user, the provider or the playlist does), how to switch it on, when to use your own free key, what the feature unlocks, and what the local cache holds. Eight FAQ entries, and a section for readers who would rather leave it off. The post states plainly that TMDB is a metadata database and not a content source, and carries TMDB's required attribution through a reusable TmdbAttribution component (their logo plus "This product uses the TMDB API but is not endorsed or certified by TMDB."), the same wording the app shows in Settings and About. Its screenshot is the settings section itself: the capture's G5 guard keeps enrichment disabled for the whole run, so no licensed poster or still can reach a published frame. The new action stages the switch in the form only, which reveals the key field, the cache panel and the attribution while the stored setting stays off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
106 lines
3.7 KiB
JavaScript
106 lines
3.7 KiB
JavaScript
import { access, readFile } from 'node:fs/promises';
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
/**
|
|
* Structural checks for guide posts: they must carry FAQPage structured data
|
|
* next to the BlogPosting entry, link to the download hub, and reference only
|
|
* screenshots that the build actually shipped.
|
|
*/
|
|
|
|
const distRoot = new URL('../../dist/apps/website/', import.meta.url);
|
|
const SITE = 'https://4gray.github.io/iptvnator';
|
|
|
|
const GUIDES = [
|
|
{
|
|
slug: 'xtream-codes-setup-guide',
|
|
screenshots: [
|
|
'blog/guides/screenshots/guide-xtream-add-playlist-dark.png',
|
|
'blog/guides/screenshots/guide-xtream-auto-detect-dark.png',
|
|
'blog/guides/screenshots/guide-xtream-live-dark.png',
|
|
],
|
|
},
|
|
{
|
|
slug: 'stalker-portal-setup-guide',
|
|
screenshots: [
|
|
'blog/guides/screenshots/guide-stalker-add-playlist-dark.png',
|
|
'blog/guides/screenshots/guide-stalker-live-dark.png',
|
|
],
|
|
},
|
|
{
|
|
slug: 'm3u-playlist-epg-setup-guide',
|
|
screenshots: [
|
|
'blog/guides/screenshots/guide-m3u-add-playlist-dark.png',
|
|
'blog/guides/screenshots/guide-m3u-live-groups-dark.png',
|
|
'blog/guides/screenshots/guide-epg-settings-dark.png',
|
|
],
|
|
},
|
|
{
|
|
slug: 'offline-downloads-guide',
|
|
screenshots: [
|
|
'blog/guides/screenshots/guide-downloads-movie-detail-dark.png',
|
|
'blog/guides/screenshots/guide-downloads-manager-dark.png',
|
|
'blog/guides/screenshots/guide-downloads-offline-movie-dark.png',
|
|
],
|
|
},
|
|
{
|
|
slug: 'alternative-sources-guide',
|
|
screenshots: [
|
|
'blog/guides/screenshots/guide-sources-chip-dark.png',
|
|
'blog/guides/screenshots/guide-sources-menu-dark.png',
|
|
],
|
|
},
|
|
{
|
|
slug: 'remote-control-guide',
|
|
screenshots: [
|
|
'blog/guides/screenshots/guide-remote-settings-dark.png',
|
|
'blog/guides/screenshots/guide-remote-phone-dark.png',
|
|
],
|
|
},
|
|
{
|
|
slug: 'epg-wrong-program-fix',
|
|
screenshots: [
|
|
'blog/guides/screenshots/guide-epg-map-menu-dark.png',
|
|
'blog/guides/screenshots/guide-epg-map-dialog-dark.png',
|
|
'blog/guides/screenshots/guide-epg-offset-dark.png',
|
|
],
|
|
},
|
|
{
|
|
slug: 'tmdb-metadata-guide',
|
|
screenshots: ['blog/guides/screenshots/guide-tmdb-settings-dark.png'],
|
|
},
|
|
];
|
|
|
|
const readDist = (relativePath) => readFile(new URL(relativePath, distRoot), 'utf8');
|
|
|
|
function extractJsonLd(html) {
|
|
const blocks = [...html.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)];
|
|
assert.ok(blocks.length > 0, 'Expected at least one JSON-LD script block.');
|
|
return blocks.flatMap((match) => JSON.parse(match[1]));
|
|
}
|
|
|
|
for (const guide of GUIDES) {
|
|
test(`${guide.slug}: BlogPosting and FAQPage structured data`, async () => {
|
|
const html = await readDist(`blog/${guide.slug}/index.html`);
|
|
const schema = extractJsonLd(html);
|
|
|
|
assert.ok(schema.some((entry) => entry['@type'] === 'BlogPosting'), 'Expected a BlogPosting entry.');
|
|
|
|
const faq = schema.find((entry) => entry['@type'] === 'FAQPage');
|
|
assert.ok(faq, 'Expected a FAQPage entry.');
|
|
assert.ok(faq.mainEntity.length >= 5, 'Expected at least five FAQ questions.');
|
|
assert.match(html, /Frequently asked questions/);
|
|
assert.match(html, new RegExp(`<link rel="canonical" href="${SITE}/blog/${guide.slug}/"`));
|
|
});
|
|
|
|
test(`${guide.slug}: links to the download hub and ships its screenshots`, async () => {
|
|
const html = await readDist(`blog/${guide.slug}/index.html`);
|
|
assert.match(html, /href="\/iptvnator\/download\/"/);
|
|
|
|
for (const screenshot of guide.screenshots) {
|
|
assert.match(html, new RegExp(`src="/iptvnator/${screenshot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`));
|
|
await access(new URL(screenshot, distRoot));
|
|
}
|
|
});
|
|
}
|