mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat(website): add the features hub and five feature landing pages
Publish /features/ with one landing page per feature people search for: M3U playlist player, Xtream Codes player, Stalker portal player, TV guide (EPG) and phone remote control. Each page composes a feature hero (download and setup-guide calls to action) with the download-page sections, carries SoftwareApplication (featureList) / FAQPage / BreadcrumbList structured data, links to the other feature pages and the matching guides, and uses only mock-backed screenshots. A registry in src/lib/features.ts drives the hub, the per-page switcher, the homepage feature cards (now links) and the header Features entry, so a new page is one registry entry and one .astro file. tools/testing/website-feature-pages.test.mjs checks canonical URLs, schema, cross-links, hub coverage and sitemap entries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
52b33fe5a3
commit
ad81fbfc45
17 files changed
+1403
-9
No files matched your search
@@ -330,7 +330,7 @@ This is an Nx monorepo with the following structure:
|
||||
- **apps/electron-backend-e2e** - Playwright E2E tests against the Electron app
|
||||
- **apps/stalker-mock-server** - Mock Stalker/Ministra portal for dev and E2E
|
||||
- **apps/xtream-mock-server** - Mock Xtream Codes API for dev and E2E
|
||||
- **apps/website** - Astro + Tailwind landing page, blog, and per-OS download landing pages (`/download/`, `/download/{windows,macos,linux}/`); direct asset links are resolved at build time from the GitHub Releases API with a `package.json` fallback (`src/lib/downloads.ts`, see `apps/website/README.md`)
|
||||
- **apps/website** - Astro + Tailwind landing page, blog (guides carry `faq:` frontmatter → FAQPage JSON-LD), per-OS download landing pages (`/download/`, `/download/{windows,macos,linux}/`) and feature landing pages (`/features/`, registry in `src/lib/features.ts`); direct asset links are resolved at build time from the GitHub Releases API with a `package.json` fallback (`src/lib/downloads.ts`, see `apps/website/README.md`)
|
||||
- **libs/** - Shared libraries:
|
||||
- **epg/data-access** - EPG services, runtime bridge, program normalization
|
||||
- **m3u-state** - NgRx state management for M3U playlists
|
||||
|
||||
@@ -82,3 +82,17 @@ Two conventions set them apart:
|
||||
`tools/testing/website-guides.test.mjs` (part of `pnpm nx test website`) checks
|
||||
each guide for the FAQPage schema, a link to the download hub and the presence
|
||||
of every referenced screenshot in the build output.
|
||||
|
||||
## Feature Pages
|
||||
|
||||
`/features/` plus one page per feature (`m3u-player`, `xtream-codes-player`,
|
||||
`stalker-portal-player`, `epg`, `remote-control`) live in
|
||||
`apps/website/src/pages/features/`. They target "<feature> player" style
|
||||
searches, reuse the download-page sections, and each carries
|
||||
`SoftwareApplication` (with `featureList`) / `FAQPage` / `BreadcrumbList`
|
||||
structured data. The registry in `src/lib/features.ts` drives the hub, the
|
||||
per-page switcher, the homepage feature cards and
|
||||
`tools/testing/website-feature-pages.test.mjs`; adding a page means adding one
|
||||
registry entry and one `.astro` file. Screenshots come only from the
|
||||
mock-backed guide and release captures, never from the older homepage
|
||||
screenshots that show real channel names.
|
||||
@@ -18,7 +18,7 @@
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": ["build"],
|
||||
"options": {
|
||||
"command": "node --test tools/testing/website-giscus-comments.test.mjs tools/testing/website-download-pages.test.mjs tools/testing/website-guides.test.mjs"
|
||||
"command": "node --test tools/testing/website-giscus-comments.test.mjs tools/testing/website-download-pages.test.mjs tools/testing/website-guides.test.mjs tools/testing/website-feature-pages.test.mjs"
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
- Guide, Xtream Codes setup: https://4gray.github.io/iptvnator/blog/xtream-codes-setup-guide/
|
||||
- Guide, Stalker/Ministra portal setup: https://4gray.github.io/iptvnator/blog/stalker-portal-setup-guide/
|
||||
- Guide, M3U playlist and EPG setup: https://4gray.github.io/iptvnator/blog/m3u-playlist-epg-setup-guide/
|
||||
- Features overview: https://4gray.github.io/iptvnator/features/
|
||||
- Feature, M3U playlist player: https://4gray.github.io/iptvnator/features/m3u-player/
|
||||
- Feature, Xtream Codes player: https://4gray.github.io/iptvnator/features/xtream-codes-player/
|
||||
- Feature, Stalker portal player: https://4gray.github.io/iptvnator/features/stalker-portal-player/
|
||||
- Feature, TV guide (EPG): https://4gray.github.io/iptvnator/features/epg/
|
||||
- Feature, phone remote control: https://4gray.github.io/iptvnator/features/remote-control/
|
||||
- Release Notes (v0.18): https://4gray.github.io/iptvnator/blog/v0-18-release-notes/
|
||||
- Troubleshooting (macOS app damaged fix): https://4gray.github.io/iptvnator/blog/macos-arm-app-damaged-fix/
|
||||
- Source Code: https://github.com/4gray/iptvnator
|
||||
|
||||
@@ -1,32 +1,39 @@
|
||||
---
|
||||
const heroFeatures = [
|
||||
type FeatureCard = { icon: string; title: string; description: string; href?: string };
|
||||
|
||||
const heroFeatures: FeatureCard[] = [
|
||||
{
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />',
|
||||
title: 'M3U / M3U8 Playlists',
|
||||
description: 'Import playlists from files or URLs with full M3U and M3U8 support.',
|
||||
href: '/iptvnator/features/m3u-player/',
|
||||
},
|
||||
{
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />',
|
||||
title: 'Xtream Codes API',
|
||||
description: 'Connect to Xtream Codes providers with live TV, VOD, and series.',
|
||||
href: '/iptvnator/features/xtream-codes-player/',
|
||||
},
|
||||
{
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />',
|
||||
title: 'Electronic Program Guide',
|
||||
description: 'XMLTV-based EPG with current and upcoming program information.',
|
||||
href: '/iptvnator/features/epg/',
|
||||
},
|
||||
{
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /><path stroke-linecap="round" stroke-linejoin="round" d="M12 18v3" />',
|
||||
title: 'Cross-Platform',
|
||||
description: 'Runs on macOS, Windows, Linux as a desktop app or PWA in any browser.',
|
||||
href: '/iptvnator/download/',
|
||||
},
|
||||
];
|
||||
|
||||
const carouselFeatures = [
|
||||
const carouselFeatures: FeatureCard[] = [
|
||||
{
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.858 15.355-5.858 21.213 0" />',
|
||||
title: 'Stalker Portals',
|
||||
description: 'Browse and stream content from Stalker/Ministra portal providers.',
|
||||
href: '/iptvnator/features/stalker-portal-player/',
|
||||
},
|
||||
{
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />',
|
||||
@@ -57,6 +64,7 @@ const carouselFeatures = [
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />',
|
||||
title: 'Remote Control',
|
||||
description: 'Control the player from your phone with the built-in remote.',
|
||||
href: '/iptvnator/features/remote-control/',
|
||||
},
|
||||
{
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />',
|
||||
@@ -87,7 +95,7 @@ const carouselFeatures = [
|
||||
<div class="stagger-children mt-16 grid grid-cols-1 gap-5 sm:grid-cols-2" data-stagger>
|
||||
{
|
||||
heroFeatures.map((feature, i) => (
|
||||
<div class="feature-card group relative overflow-hidden rounded-lg border border-dashed border-surface-600/60 bg-gradient-to-br from-surface-900/80 via-surface-950/60 to-surface-900/40 p-7 transition-all duration-500 hover:border-accent-500/40 hover:shadow-[0_0_30px_rgba(32,168,168,0.08)]">
|
||||
<a href={feature.href} class="feature-card group relative block overflow-hidden rounded-lg border border-dashed border-surface-600/60 bg-gradient-to-br from-surface-900/80 via-surface-950/60 to-surface-900/40 p-7 transition-all duration-500 hover:border-accent-500/40 hover:shadow-[0_0_30px_rgba(32,168,168,0.08)]">
|
||||
|
||||
<!-- Background watermark icon — large, rotated, partially clipped -->
|
||||
<div class={`feature-watermark pointer-events-none absolute -bottom-6 -right-4 opacity-[0.04] transition-all duration-700 ease-out group-hover:opacity-[0.1] ${i % 2 === 0 ? 'watermark-left' : 'watermark-right'}`}>
|
||||
@@ -116,7 +124,7 @@ const carouselFeatures = [
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
@@ -140,7 +148,7 @@ const carouselFeatures = [
|
||||
<!-- First set of cards -->
|
||||
{
|
||||
carouselFeatures.map((feature, i) => (
|
||||
<div class="group relative w-64 shrink-0 overflow-hidden rounded-lg border border-dashed border-surface-600/60 bg-gradient-to-b from-surface-900/60 to-surface-950/40 p-5 transition-all duration-500 hover:border-accent-500/40 hover:shadow-[0_0_24px_rgba(32,168,168,0.06)]">
|
||||
<a href={feature.href ?? '/iptvnator/features/'} class="group relative block w-64 shrink-0 overflow-hidden rounded-lg border border-dashed border-surface-600/60 bg-gradient-to-b from-surface-900/60 to-surface-950/40 p-5 transition-all duration-500 hover:border-accent-500/40 hover:shadow-[0_0_24px_rgba(32,168,168,0.06)]">
|
||||
<div class="relative mb-3.5 inline-block">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-accent-500/[0.08] ring-1 ring-accent-500/20 transition-all duration-500 group-hover:bg-accent-500/[0.14] group-hover:ring-accent-500/35 group-hover:shadow-[0_0_14px_rgba(32,168,168,0.12)]">
|
||||
<svg class="h-5 w-5 text-accent-400 transition-all duration-500 group-hover:text-accent-300 group-hover:drop-shadow-[0_0_4px_rgba(32,168,168,0.4)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
@@ -153,7 +161,7 @@ const carouselFeatures = [
|
||||
<p class="mt-1.5 text-xs leading-relaxed text-surface-500 transition-colors duration-300 group-hover:text-surface-400">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
<!-- Duplicate set for seamless loop -->
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
const navLinks = [
|
||||
{ label: 'Features', href: '/iptvnator/#features' },
|
||||
{ label: 'Features', href: '/iptvnator/features/' },
|
||||
{ label: 'Screenshots', href: '/iptvnator/#screenshots' },
|
||||
{ label: 'Download', href: '/iptvnator/download/' },
|
||||
{ label: 'Blog', href: '/iptvnator/blog/' },
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
import type { FeatureEntry } from '../../lib/features';
|
||||
import { FEATURES_HUB_HREF } from '../../lib/features';
|
||||
|
||||
interface Screenshot {
|
||||
src: string;
|
||||
alt: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
feature: FeatureEntry;
|
||||
titleLead: string;
|
||||
titleAccent: string;
|
||||
lede: string;
|
||||
facts: string[];
|
||||
screenshot: Screenshot;
|
||||
}
|
||||
|
||||
const { feature, titleLead, titleAccent, lede, facts, screenshot } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="relative overflow-hidden pt-36 pb-20 sm:pt-40">
|
||||
<div class="pointer-events-none absolute top-0 left-1/2 h-[520px] w-[900px] -translate-x-1/2 -translate-y-1/3 rounded-full bg-accent-500/[0.06] blur-[120px]"></div>
|
||||
|
||||
<div class="relative mx-auto max-w-6xl px-6">
|
||||
<nav aria-label="Breadcrumb" class="font-mono text-[11px] uppercase tracking-[0.15em] text-surface-500">
|
||||
<ol class="flex flex-wrap items-center gap-2">
|
||||
<li><a href="/iptvnator/" class="transition-colors hover:text-accent-400">IPTVnator</a></li>
|
||||
<li aria-hidden="true">/</li>
|
||||
<li><a href={FEATURES_HUB_HREF} class="transition-colors hover:text-accent-400">Features</a></li>
|
||||
<li aria-hidden="true">/</li>
|
||||
<li class="text-surface-300" aria-current="page">{feature.label}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="mt-10 grid items-center gap-14 lg:grid-cols-[minmax(0,1.05fr)_minmax(0,0.95fr)]">
|
||||
<div>
|
||||
<div class="inline-flex items-center gap-3">
|
||||
<span class="flex h-9 w-9 items-center justify-center rounded-lg bg-accent-500/[0.08] ring-1 ring-accent-500/20">
|
||||
<svg class="h-5 w-5 text-accent-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d={feature.icon} />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">{feature.eyebrow}</span>
|
||||
</div>
|
||||
|
||||
<h1 class="mt-6 font-display text-4xl font-bold tracking-tight text-surface-50 sm:text-5xl lg:text-6xl">
|
||||
{titleLead}{' '}
|
||||
<em class="font-medium text-accent-400" style="font-style: italic;">{titleAccent}</em>
|
||||
</h1>
|
||||
|
||||
<p class="mt-6 max-w-xl text-lg leading-relaxed text-surface-400">{lede}</p>
|
||||
|
||||
<div class="mt-10 flex flex-wrap gap-4">
|
||||
<a
|
||||
href="/iptvnator/download/"
|
||||
class="inline-flex items-center gap-3 rounded-xl bg-accent-500 px-6 py-4 text-base font-semibold text-surface-950 shadow-[0_0_30px_rgba(32,168,168,0.25)] transition-all duration-300 hover:-translate-y-0.5 hover:bg-accent-400 hover:shadow-[0_0_44px_rgba(32,168,168,0.4)]"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
Download IPTVnator
|
||||
</a>
|
||||
<a
|
||||
href={feature.guide.href}
|
||||
class="inline-flex items-center gap-3 rounded-xl border border-dashed border-surface-600/60 bg-surface-900/50 px-6 py-4 text-base font-semibold text-surface-100 transition-all duration-300 hover:border-accent-500/40 hover:bg-accent-500/[0.05]"
|
||||
>
|
||||
<svg class="h-5 w-5 text-accent-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.7" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.253v13m0-13C10.832 5.483 9.246 5 7.5 5 5.205 5 3.182 5.836 2 7.117v11.41C3.182 17.247 5.205 16.41 7.5 16.41c1.746 0 3.332.484 4.5 1.254m0-11.41C13.168 5.483 14.754 5 16.5 5c2.295 0 4.318.836 5.5 2.117v11.41C20.818 17.247 18.795 16.41 16.5 16.41c-1.746 0-3.332.484-4.5 1.254" />
|
||||
</svg>
|
||||
{feature.guide.label}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ul class="mt-8 flex flex-wrap gap-2" aria-label="Key facts">
|
||||
{facts.map((fact) => (
|
||||
<li class="rounded-full border border-dashed border-surface-700/70 px-3 py-1 font-mono text-[10px] uppercase tracking-[0.12em] text-surface-400">{fact}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<figure class="relative">
|
||||
<div class="pointer-events-none absolute -inset-6 rounded-3xl bg-accent-500/[0.05] blur-2xl"></div>
|
||||
<div class="relative overflow-hidden rounded-xl border border-dashed border-surface-600/60 bg-surface-900/60 p-2 shadow-[0_30px_80px_rgba(0,0,0,0.45)]">
|
||||
<img
|
||||
src={screenshot.src}
|
||||
alt={screenshot.alt}
|
||||
width={screenshot.width}
|
||||
height={screenshot.height}
|
||||
class="h-auto w-full rounded-lg"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<figcaption class="mt-3 text-center font-mono text-[10px] uppercase tracking-[0.15em] text-surface-600">{screenshot.alt}</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
import { FEATURES, FEATURES_HUB_HREF } from '../../lib/features';
|
||||
import type { FeatureSlug } from '../../lib/features';
|
||||
|
||||
interface Props {
|
||||
current: FeatureSlug;
|
||||
}
|
||||
|
||||
const { current } = Astro.props;
|
||||
const others = FEATURES.filter((feature) => feature.slug !== current);
|
||||
---
|
||||
|
||||
<section class="relative py-20">
|
||||
<hr class="section-divider mb-20" />
|
||||
|
||||
<div class="mx-auto max-w-6xl px-6">
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||
<div>
|
||||
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">More features</span>
|
||||
<h2 class="mt-3 font-display text-2xl font-bold tracking-tight text-surface-50 sm:text-3xl">
|
||||
The rest of{' '}
|
||||
<em class="font-medium text-accent-400" style="font-style: italic;">IPTVnator</em>
|
||||
</h2>
|
||||
</div>
|
||||
<a href={FEATURES_HUB_HREF} class="font-mono text-xs uppercase tracking-[0.1em] text-accent-500 transition-colors hover:text-accent-400">All features</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{others.map((feature) => (
|
||||
<a href={feature.href} class="card-dotted group flex flex-col gap-3 p-5">
|
||||
<span class="flex h-9 w-9 items-center justify-center rounded-lg bg-accent-500/[0.08] ring-1 ring-accent-500/20 transition-all duration-300 group-hover:bg-accent-500/[0.14] group-hover:ring-accent-500/35">
|
||||
<svg class="h-4.5 w-4.5 text-accent-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d={feature.icon} />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="font-display text-base font-semibold text-surface-100">{feature.label}</span>
|
||||
<span class="text-xs leading-relaxed text-surface-500">{feature.summary}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { FaqEntry } from './download-schema';
|
||||
import { REPO_URL } from './downloads';
|
||||
import type { FeatureEntry } from './features';
|
||||
import { FEATURES_HUB_HREF } from './features';
|
||||
import { absoluteUrl } from './site';
|
||||
|
||||
interface FeaturePageSchemaInput {
|
||||
feature: FeatureEntry;
|
||||
pageUrl: string;
|
||||
description: string;
|
||||
/** Plain-language feature statements for schema.org `featureList`. */
|
||||
featureList: string[];
|
||||
faq: FaqEntry[];
|
||||
}
|
||||
|
||||
/** SoftwareApplication (with featureList) + FAQPage + BreadcrumbList for one feature page. */
|
||||
export function buildFeaturePageSchema(input: FeaturePageSchemaInput): Array<Record<string, unknown>> {
|
||||
const { feature, pageUrl, description, featureList, faq } = input;
|
||||
|
||||
return [
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'IPTVnator',
|
||||
applicationCategory: 'MultimediaApplication',
|
||||
applicationSubCategory: 'IPTV player',
|
||||
operatingSystem: 'Windows, macOS, Linux',
|
||||
url: pageUrl,
|
||||
downloadUrl: absoluteUrl('/download/'),
|
||||
featureList,
|
||||
isAccessibleForFree: true,
|
||||
license: `${REPO_URL}/blob/master/LICENSE`,
|
||||
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
|
||||
author: { '@type': 'Person', name: '4gray', url: 'https://github.com/4gray' },
|
||||
sameAs: [REPO_URL, 'https://t.me/iptvnator'],
|
||||
description,
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faq.map((entry) => ({
|
||||
'@type': 'Question',
|
||||
name: entry.q,
|
||||
acceptedAnswer: { '@type': 'Answer', text: entry.a },
|
||||
})),
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'IPTVnator', item: absoluteUrl('/') },
|
||||
{ '@type': 'ListItem', position: 2, name: 'Features', item: absoluteUrl(FEATURES_HUB_HREF.replace('/iptvnator', '')) },
|
||||
{ '@type': 'ListItem', position: 3, name: feature.label, item: pageUrl },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Registry of the feature landing pages under `/features/`. The hub, the
|
||||
* per-page switcher, the homepage cards and the build test all read from
|
||||
* here, so adding a page means adding one entry and one `.astro` file.
|
||||
*/
|
||||
|
||||
export type FeatureSlug =
|
||||
| 'm3u-player'
|
||||
| 'xtream-codes-player'
|
||||
| 'stalker-portal-player'
|
||||
| 'epg'
|
||||
| 'remote-control';
|
||||
|
||||
export interface FeatureGuide {
|
||||
href: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface FeatureEntry {
|
||||
slug: FeatureSlug;
|
||||
/** Short name used in breadcrumbs, cards and the switcher. */
|
||||
label: string;
|
||||
/** Mono eyebrow above the page title. */
|
||||
eyebrow: string;
|
||||
/** One sentence for cards. */
|
||||
summary: string;
|
||||
/** Site-relative href including the GitHub Pages base. */
|
||||
href: string;
|
||||
/** 24×24 stroke icon path (Heroicons outline style). */
|
||||
icon: string;
|
||||
guide: FeatureGuide;
|
||||
}
|
||||
|
||||
export const FEATURES: readonly FeatureEntry[] = [
|
||||
{
|
||||
slug: 'm3u-player',
|
||||
label: 'M3U playlist player',
|
||||
eyebrow: 'Feature · M3U playlists',
|
||||
summary:
|
||||
'Load M3U and M3U8 playlists from a link, a file or text, and browse tens of thousands of channels with groups, favorites and search.',
|
||||
href: '/iptvnator/features/m3u-player/',
|
||||
icon: 'M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
|
||||
guide: { href: '/iptvnator/blog/m3u-playlist-epg-setup-guide/', label: 'M3U and EPG setup guide' },
|
||||
},
|
||||
{
|
||||
slug: 'xtream-codes-player',
|
||||
label: 'Xtream Codes player',
|
||||
eyebrow: 'Feature · Xtream Codes API',
|
||||
summary:
|
||||
'Sign in with server URL, username and password for live TV, movies and series with posters, details, catch-up and offline downloads.',
|
||||
href: '/iptvnator/features/xtream-codes-player/',
|
||||
icon: 'M13 10V3L4 14h7v7l9-11h-7z',
|
||||
guide: { href: '/iptvnator/blog/xtream-codes-setup-guide/', label: 'Xtream Codes setup guide' },
|
||||
},
|
||||
{
|
||||
slug: 'stalker-portal-player',
|
||||
label: 'Stalker portal player',
|
||||
eyebrow: 'Feature · Stalker / Ministra portals',
|
||||
summary:
|
||||
'Connect Stalker and Ministra portals by MAC address, the way a MAG box does, with live TV, radio, movies, series and an account page.',
|
||||
href: '/iptvnator/features/stalker-portal-player/',
|
||||
icon: 'M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.858 15.355-5.858 21.213 0',
|
||||
guide: { href: '/iptvnator/blog/stalker-portal-setup-guide/', label: 'Stalker portal setup guide' },
|
||||
},
|
||||
{
|
||||
slug: 'epg',
|
||||
label: 'TV guide (EPG)',
|
||||
eyebrow: 'Feature · Program guide',
|
||||
summary:
|
||||
'XMLTV files and portal schedules become a program guide: now and next in every list, a timeline under the player, and catch-up for archived programs.',
|
||||
href: '/iptvnator/features/epg/',
|
||||
icon: 'M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z',
|
||||
guide: { href: '/iptvnator/blog/m3u-playlist-epg-setup-guide/', label: 'M3U and EPG setup guide' },
|
||||
},
|
||||
{
|
||||
slug: 'remote-control',
|
||||
label: 'Phone remote control',
|
||||
eyebrow: 'Feature · Remote control',
|
||||
summary:
|
||||
'The desktop app serves a web remote to any phone on your network: channel up and down, direct numbers, volume, and a now-playing panel.',
|
||||
href: '/iptvnator/features/remote-control/',
|
||||
icon: 'M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z',
|
||||
guide: { href: '/iptvnator/blog/m3u-playlist-epg-setup-guide/', label: 'M3U and EPG setup guide' },
|
||||
},
|
||||
];
|
||||
|
||||
export const FEATURES_HUB_HREF = '/iptvnator/features/';
|
||||
|
||||
export function featureBySlug(slug: FeatureSlug): FeatureEntry {
|
||||
const entry = FEATURES.find((feature) => feature.slug === slug);
|
||||
if (!entry) {
|
||||
throw new Error(`Unknown feature slug: ${slug}`);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import FaqAccordion from '../../components/blog/FaqAccordion.astro';
|
||||
import DownloadSection from '../../components/download/DownloadSection.astro';
|
||||
import FeatureList from '../../components/download/FeatureList.astro';
|
||||
import OfficialSourcesNote from '../../components/download/OfficialSourcesNote.astro';
|
||||
import RelatedPosts from '../../components/download/RelatedPosts.astro';
|
||||
import SpecList from '../../components/download/SpecList.astro';
|
||||
import FeatureHero from '../../components/feature/FeatureHero.astro';
|
||||
import FeatureSwitcher from '../../components/feature/FeatureSwitcher.astro';
|
||||
import type { FaqEntry } from '../../lib/download-schema';
|
||||
import { buildFeaturePageSchema } from '../../lib/feature-schema';
|
||||
import { featureBySlug } from '../../lib/features';
|
||||
import { absoluteUrl } from '../../lib/site';
|
||||
|
||||
const feature = featureBySlug('epg');
|
||||
const pageUrl = absoluteUrl('/features/epg/');
|
||||
const title = 'TV Guide (EPG) for IPTV – XMLTV, Timeline and Catch-Up | IPTVnator';
|
||||
const description =
|
||||
'IPTVnator turns XMLTV files and portal schedules into a program guide: what is on now in every channel list, a timeline under the player, a grid across all channels, catch-up for archived programs, and manual mapping when a channel does not match. Free and open source for Windows, macOS and Linux.';
|
||||
|
||||
const guide = [
|
||||
{
|
||||
title: 'Now and next in every list',
|
||||
description:
|
||||
'Each channel row shows the current program with a progress bar, in the playlist views, the portal lists and the favorites and recent collections.',
|
||||
},
|
||||
{
|
||||
title: 'A timeline under the player',
|
||||
description:
|
||||
'The schedule of the selected channel runs as a ribbon with day navigation, zoom levels, a jump to now, and a program search. A vertical single-day list is the alternative view.',
|
||||
},
|
||||
{
|
||||
title: 'A grid across channels',
|
||||
description:
|
||||
'The multi-channel view lays the schedules of all channels side by side on one time axis, with channel filtering and program search.',
|
||||
},
|
||||
{
|
||||
title: 'Program details',
|
||||
description:
|
||||
'Title, category, description, episode, language, rating and duration open in a dialog from any program block.',
|
||||
},
|
||||
{
|
||||
title: 'Catch-up',
|
||||
description:
|
||||
'Where the provider keeps an archive, past programs start straight from the timeline, on M3U playlists with catch-up attributes and on Xtream and Stalker portals.',
|
||||
},
|
||||
{
|
||||
title: 'Manual channel mapping',
|
||||
description:
|
||||
'A channel the guide cannot match gets a right-click action to pick its XMLTV entry by hand. Mappings are stored on your device and survive refreshes.',
|
||||
},
|
||||
];
|
||||
|
||||
const sources = [
|
||||
{ term: 'M3U playlists', value: 'XMLTV sources from Settings › EPG or declared by the playlist through url-tvg. Channels match by tvg-id, then tvg-name, then name.' },
|
||||
{ term: 'Xtream Codes', value: 'The portal schedule is used first; your XMLTV sources fill missing channels or take precedence with one setting.' },
|
||||
{ term: 'Stalker portals', value: 'A seven-day schedule is loaded from the portal once per session; single-channel requests fill any gaps.' },
|
||||
{ term: 'Formats', value: 'XMLTV as .xml or compressed .xml.gz, parsed in a background worker and stored in a local database.' },
|
||||
{ term: 'Sources', value: 'Any number of global sources plus per-playlist sources, each with a status chip, a refresh button and one refresh-all action.' },
|
||||
{ term: 'Availability', value: 'The XMLTV guide is a desktop feature. Portal schedules from Xtream and Stalker also show in the browser version.' },
|
||||
];
|
||||
|
||||
const faq: FaqEntry[] = [
|
||||
{
|
||||
q: 'Where does the program guide come from?',
|
||||
a: 'From XMLTV files you add or your playlist declares, and from the schedules Xtream and Stalker portals publish. IPTVnator merges them per channel: portal data first, your XMLTV sources as a fallback or, with one setting, as the preferred source.',
|
||||
},
|
||||
{
|
||||
q: 'Why does a channel show no program?',
|
||||
a: 'It is not matched to a guide entry. Matching uses tvg-id, then tvg-name, then the channel name. Check the ids in the playlist, or map the channel by hand with a right-click and "Map EPG channel".',
|
||||
},
|
||||
{
|
||||
q: 'Can I watch a program that already aired?',
|
||||
a: 'Yes, when the provider keeps an archive. M3U playlists declare it with catchup attributes, Xtream and Stalker portals flag archived channels. Past programs then start from the timeline; without an archive the timeline is a schedule only.',
|
||||
},
|
||||
{
|
||||
q: 'How big can the XMLTV file be?',
|
||||
a: 'Large guides are fine. Parsing runs in a background worker and the result is stored in a local database, so the app stays responsive and the data is there on the next start.',
|
||||
},
|
||||
{
|
||||
q: 'Does the guide work in the browser version?',
|
||||
a: 'Portal schedules from Xtream and Stalker sources do. XMLTV downloads and the local guide database are desktop features.',
|
||||
},
|
||||
{
|
||||
q: 'Can I see several channels at once?',
|
||||
a: 'Yes. The multi-channel view lays all schedules on one time axis with zoom, channel filtering and program search, in addition to the timeline of the selected channel.',
|
||||
},
|
||||
];
|
||||
|
||||
const jsonLd = buildFeaturePageSchema({
|
||||
feature,
|
||||
pageUrl,
|
||||
description,
|
||||
featureList: [
|
||||
'XMLTV program guide from .xml and .xml.gz sources',
|
||||
'Current program and progress in every channel list',
|
||||
'Timeline and list views under the player',
|
||||
'Multi-channel schedule grid with search',
|
||||
'Catch-up playback of archived programs',
|
||||
'Manual EPG channel mapping',
|
||||
'Portal schedules from Xtream Codes and Stalker sources',
|
||||
],
|
||||
faq,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={title}
|
||||
description={description}
|
||||
keywords={['IPTV EPG', 'XMLTV EPG player', 'TV guide IPTV player', 'EPG timeline IPTV', 'IPTV catch-up player', 'IPTV player with EPG Windows', 'open-source IPTV player']}
|
||||
jsonLd={jsonLd}
|
||||
>
|
||||
<FeatureHero
|
||||
feature={feature}
|
||||
titleLead="A TV guide that"
|
||||
titleAccent="knows what is on"
|
||||
lede="IPTVnator turns XMLTV files and portal schedules into a program guide: what is on now in every channel list, a timeline under the player, a grid across all channels, catch-up for archived programs, and manual mapping when a channel does not match."
|
||||
facts={['XMLTV .xml & .xml.gz', 'Timeline or list', 'Catch-up', 'Manual channel mapping']}
|
||||
screenshot={{
|
||||
src: '/iptvnator/blog/guides/screenshots/guide-xtream-live-dark.png',
|
||||
alt: 'Channel list with the current program and progress bar for every channel',
|
||||
width: 2560,
|
||||
height: 1440,
|
||||
}}
|
||||
/>
|
||||
|
||||
<DownloadSection id="guide" eyebrow="What you get" title="The guide," accent="everywhere" lead="One schedule, shown wherever a channel appears.">
|
||||
<FeatureList items={guide} />
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="sources" eyebrow="Sources" title="Where the guide" accent="comes from">
|
||||
<div class="grid gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] lg:items-start">
|
||||
<SpecList items={sources} />
|
||||
<figure class="overflow-hidden rounded-xl border border-dashed border-surface-600/60 bg-surface-900/60 p-2">
|
||||
<img
|
||||
src="/iptvnator/blog/guides/screenshots/guide-epg-settings-dark.png"
|
||||
alt="Settings with the EPG section open, a source URL entered and its status chip"
|
||||
width={2560}
|
||||
height={1440}
|
||||
class="h-auto w-full rounded-lg"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</figure>
|
||||
</div>
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="faq" eyebrow="FAQ" title="Questions about" accent="the program guide">
|
||||
<div class="max-w-3xl">
|
||||
<FaqAccordion items={faq} />
|
||||
</div>
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="guides" eyebrow="Keep reading" title="Guides and" accent="background">
|
||||
<RelatedPosts slugs={['m3u-playlist-epg-setup-guide', 'epg-guide', 'xtream-codes-setup-guide']} />
|
||||
<div class="mt-10">
|
||||
<OfficialSourcesNote />
|
||||
</div>
|
||||
</DownloadSection>
|
||||
|
||||
<FeatureSwitcher current="epg" />
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,126 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { FEATURES } from '../../lib/features';
|
||||
import { absoluteUrl } from '../../lib/site';
|
||||
import { REPO_URL } from '../../lib/downloads';
|
||||
|
||||
const pageUrl = absoluteUrl('/features/');
|
||||
const title = 'IPTVnator Features – M3U, Xtream Codes, Stalker Portals, EPG and Remote';
|
||||
const description =
|
||||
'What IPTVnator does: M3U and M3U8 playlists, Xtream Codes and Stalker portals, an XMLTV program guide with catch-up, a phone remote, offline downloads, TMDB metadata and MPV or VLC playback. Free and open source for Windows, macOS and Linux.';
|
||||
|
||||
const more = [
|
||||
{ title: 'Offline downloads', text: 'Movies, episodes and whole seasons from Xtream and Stalker sources, with resume on interrupted transfers.' },
|
||||
{ title: 'Movie and series metadata', text: 'Optional TMDB enrichment with posters, cast, ratings, trailers, actor pages and a Discover view.' },
|
||||
{ title: 'MPV, VLC and embedded MPV', text: 'Hand streams to MPV or VLC, or render mpv inside the window with the experimental embedded engine.' },
|
||||
{ title: 'Global collections', text: 'Favorites, recent items and search that span every source you added.' },
|
||||
{ title: 'Dashboard', text: 'Continue watching, favorites rails, recently added content and source cards with expiry chips.' },
|
||||
{ title: 'Privacy first', text: 'No account, no analytics. Playlists and credentials stay on your device.' },
|
||||
{ title: 'Nineteen languages', text: 'Community-contributed translations, light and dark themes.' },
|
||||
{ title: 'Self-hosted browser version', text: 'A Docker image serves the web app and its backend for devices that cannot run the desktop app.' },
|
||||
];
|
||||
|
||||
const jsonLd = [
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'IPTVnator',
|
||||
applicationCategory: 'MultimediaApplication',
|
||||
applicationSubCategory: 'IPTV player',
|
||||
operatingSystem: 'Windows, macOS, Linux',
|
||||
url: pageUrl,
|
||||
downloadUrl: absoluteUrl('/download/'),
|
||||
featureList: FEATURES.map((feature) => feature.label),
|
||||
isAccessibleForFree: true,
|
||||
license: `${REPO_URL}/blob/master/LICENSE`,
|
||||
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
|
||||
author: { '@type': 'Person', name: '4gray', url: 'https://github.com/4gray' },
|
||||
description,
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'IPTVnator', item: absoluteUrl('/') },
|
||||
{ '@type': 'ListItem', position: 2, name: 'Features', item: pageUrl },
|
||||
],
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={title}
|
||||
description={description}
|
||||
keywords={['IPTVnator features', 'IPTV player features', 'M3U player', 'Xtream Codes player', 'Stalker portal player', 'IPTV EPG', 'open-source IPTV player']}
|
||||
jsonLd={jsonLd}
|
||||
>
|
||||
<section class="relative overflow-hidden pt-36 pb-16 sm:pt-40">
|
||||
<div class="pointer-events-none absolute top-0 left-1/2 h-[520px] w-[900px] -translate-x-1/2 -translate-y-1/3 rounded-full bg-accent-500/[0.06] blur-[120px]"></div>
|
||||
|
||||
<div class="relative mx-auto max-w-6xl px-6">
|
||||
<div class="mx-auto max-w-2xl text-center">
|
||||
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">Features</span>
|
||||
<h1 class="mt-4 font-display text-4xl font-bold tracking-tight text-surface-50 sm:text-5xl lg:text-6xl">
|
||||
Everything <em class="font-medium text-accent-400" style="font-style: italic;">IPTVnator</em> does
|
||||
</h1>
|
||||
<p class="mt-6 text-lg leading-relaxed text-surface-400">
|
||||
One player for the three ways providers hand out access, with a program guide, a phone remote and the
|
||||
comforts of a desktop app. Each page below goes into detail and links to the matching setup guide.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-14 grid gap-5 md:grid-cols-2 lg:grid-cols-3">
|
||||
{FEATURES.map((feature) => (
|
||||
<a href={feature.href} class="card-dotted group flex flex-col p-7">
|
||||
<span class="flex h-10 w-10 items-center justify-center rounded-lg bg-accent-500/[0.08] ring-1 ring-accent-500/20 transition-all duration-300 group-hover:bg-accent-500/[0.14] group-hover:ring-accent-500/35">
|
||||
<svg class="h-5 w-5 text-accent-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d={feature.icon} />
|
||||
</svg>
|
||||
</span>
|
||||
<h2 class="mt-5 font-display text-xl font-bold tracking-tight text-surface-50">{feature.label}</h2>
|
||||
<p class="mt-3 flex-1 text-sm leading-relaxed text-surface-400">{feature.summary}</p>
|
||||
<span class="mt-5 inline-flex items-center gap-1.5 font-mono text-xs uppercase tracking-[0.1em] text-accent-500 transition-colors group-hover:text-accent-400">
|
||||
Learn more
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
<a href="/iptvnator/download/" class="group flex flex-col justify-between rounded-lg border border-dashed border-accent-500/40 bg-accent-500/[0.06] p-7 transition-all duration-300 hover:bg-accent-500/[0.1]">
|
||||
<div>
|
||||
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">Get started</span>
|
||||
<h2 class="mt-4 font-display text-xl font-bold tracking-tight text-surface-50">Download for Windows, macOS or Linux</h2>
|
||||
<p class="mt-3 text-sm leading-relaxed text-surface-400">Direct installers, Homebrew, Snap, AUR and a self-hosted browser version.</p>
|
||||
</div>
|
||||
<span class="mt-5 inline-flex items-center gap-1.5 font-mono text-xs uppercase tracking-[0.1em] text-accent-400">
|
||||
Download
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="relative py-20">
|
||||
<hr class="section-divider mb-20" />
|
||||
<div class="mx-auto max-w-6xl px-6">
|
||||
<div class="max-w-2xl">
|
||||
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">And more</span>
|
||||
<h2 class="mt-4 font-display text-3xl font-bold tracking-tight text-surface-50 sm:text-4xl">
|
||||
The rest of the <em class="font-medium text-accent-400" style="font-style: italic;">desktop app</em>
|
||||
</h2>
|
||||
</div>
|
||||
<dl class="mt-12 grid gap-px overflow-hidden rounded-lg border border-dashed border-surface-600/60 bg-surface-700/40 sm:grid-cols-2">
|
||||
{more.map((item) => (
|
||||
<div class="bg-surface-950/95 px-5 py-4">
|
||||
<dt class="font-display text-base font-semibold text-surface-100">{item.title}</dt>
|
||||
<dd class="mt-1.5 text-sm leading-relaxed text-surface-400">{item.text}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,177 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import FaqAccordion from '../../components/blog/FaqAccordion.astro';
|
||||
import DownloadSection from '../../components/download/DownloadSection.astro';
|
||||
import FeatureList from '../../components/download/FeatureList.astro';
|
||||
import OfficialSourcesNote from '../../components/download/OfficialSourcesNote.astro';
|
||||
import RelatedPosts from '../../components/download/RelatedPosts.astro';
|
||||
import SpecList from '../../components/download/SpecList.astro';
|
||||
import FeatureHero from '../../components/feature/FeatureHero.astro';
|
||||
import FeatureSwitcher from '../../components/feature/FeatureSwitcher.astro';
|
||||
import type { FaqEntry } from '../../lib/download-schema';
|
||||
import { buildFeaturePageSchema } from '../../lib/feature-schema';
|
||||
import { featureBySlug } from '../../lib/features';
|
||||
import { absoluteUrl } from '../../lib/site';
|
||||
|
||||
const feature = featureBySlug('m3u-player');
|
||||
const pageUrl = absoluteUrl('/features/m3u-player/');
|
||||
const title = 'M3U Playlist Player for Windows, macOS and Linux – IPTVnator';
|
||||
const description =
|
||||
'Play M3U and M3U8 IPTV playlists on your desktop with IPTVnator: import from a URL, a file or text, browse channel groups and favorites, search tens of thousands of channels, attach an XMLTV guide with catch-up, and hand hard streams to MPV or VLC. Free and open source.';
|
||||
|
||||
const library = [
|
||||
{
|
||||
title: 'Lists that stay fast',
|
||||
description:
|
||||
'The channel list is virtualized, so a playlist with tens of thousands of entries scrolls and searches without lag. Search filters as you type.',
|
||||
},
|
||||
{
|
||||
title: 'Groups from the playlist',
|
||||
description:
|
||||
'Every group-title becomes an expandable section. Hide the groups you never watch; the hidden set survives refreshes.',
|
||||
},
|
||||
{
|
||||
title: 'Favorites and history',
|
||||
description:
|
||||
'Star channels into a favorites list you can reorder by drag and drop, and jump back to what you watched last from Recent.',
|
||||
},
|
||||
{
|
||||
title: 'Refresh and auto-update',
|
||||
description:
|
||||
'Re-download a playlist on request, or let the desktop app refresh it on every start. Favorites, hidden groups and settings are kept.',
|
||||
},
|
||||
{
|
||||
title: 'Import any way you like',
|
||||
description:
|
||||
'Paste a link, pick a file, drop it onto the window, open it from the file manager, pass it on the command line, or paste the raw text.',
|
||||
},
|
||||
{
|
||||
title: 'Global favorites and search',
|
||||
description:
|
||||
'With several playlists, the dashboard combines favorites, recent items and search across all of them.',
|
||||
},
|
||||
];
|
||||
|
||||
const declared = [
|
||||
{ term: 'Logos and names', value: 'tvg-logo, tvg-name and the display name are used for the list; XMLTV icons fill in missing logos.' },
|
||||
{ term: 'Program guide', value: 'tvg-id links a channel to the guide. url-tvg or x-tvg-url in the header imports the EPG source automatically.' },
|
||||
{ term: 'Catch-up', value: 'catchup, catchup-source and catchup-days, or the older tvg-rec and timeshift, make archived programs playable from the timeline.' },
|
||||
{ term: 'Radio', value: 'Entries with radio="true" open in a dedicated audio layout with artwork, transport controls and volume.' },
|
||||
{ term: 'DASH with ClearKey', value: '#KODIPROP lines with inputstream.adaptive keys play in the built-in player through Shaka.' },
|
||||
{ term: 'Movie files', value: 'Entries that look like a movie file open in a movie detail view with TMDB metadata when enrichment is enabled.' },
|
||||
];
|
||||
|
||||
const playback = [
|
||||
{
|
||||
title: 'Three built-in engines',
|
||||
description:
|
||||
'HTML5 with hls.js, Video.js and ArtPlayer cover HLS, MPEG-TS and DASH streams with one shared set of controls, subtitles, quality selection and picture-in-picture.',
|
||||
},
|
||||
{
|
||||
title: 'MPV and VLC handoff',
|
||||
description:
|
||||
'Streams the browser engines cannot decode go to MPV or VLC on your machine while IPTVnator keeps the list, the guide and the remote control.',
|
||||
},
|
||||
{
|
||||
title: 'Embedded MPV',
|
||||
description:
|
||||
'An experimental engine renders mpv inside the app window on Windows, macOS and Linux, including live-TV recording where supported.',
|
||||
},
|
||||
{
|
||||
title: 'Custom User-Agent',
|
||||
description:
|
||||
'Providers that only answer known player signatures get the User-Agent you set in the playlist details, for both playlist download and playback.',
|
||||
},
|
||||
];
|
||||
|
||||
const faq: FaqEntry[] = [
|
||||
{
|
||||
q: 'Does IPTVnator play both M3U and M3U8 playlists?',
|
||||
a: 'Yes. Both extensions are the same text format; M3U8 only marks the file as UTF-8. Playlists can come from a link, a local file or pasted text, and the same attributes are read from all of them.',
|
||||
},
|
||||
{
|
||||
q: 'How many channels can a playlist have?',
|
||||
a: 'The list is built for very large playlists and stays responsive with tens of thousands of channels. Groups, favorites and search all work on the full list, not on a page of it.',
|
||||
},
|
||||
{
|
||||
q: 'Does the playlist update when my provider changes it?',
|
||||
a: 'Refresh it on request from the playlist details, or enable Auto-update to re-download it every time the desktop app starts. Your favorites, hidden groups and settings are kept across refreshes.',
|
||||
},
|
||||
{
|
||||
q: 'Can I get a program guide for an M3U playlist?',
|
||||
a: 'Yes, on the desktop app. Add an XMLTV source in Settings › EPG, or let the playlist declare it through url-tvg in its header. Channels are matched by tvg-id, and you can map the rest by hand.',
|
||||
},
|
||||
{
|
||||
q: 'What if a channel does not play?',
|
||||
a: 'Open it in MPV or VLC via Settings › Playback, or enable the experimental embedded MPV engine. Providers that require a specific User-Agent get it from the playlist details.',
|
||||
},
|
||||
{
|
||||
q: 'Does IPTVnator provide playlists?',
|
||||
a: 'No. IPTVnator is a player. You bring the playlist you already have from your provider, and nothing in it is sent anywhere except to the stream hosts it points at.',
|
||||
},
|
||||
];
|
||||
|
||||
const jsonLd = buildFeaturePageSchema({
|
||||
feature,
|
||||
pageUrl,
|
||||
description,
|
||||
featureList: [
|
||||
'M3U and M3U8 playlist import from URL, file or text',
|
||||
'Virtualized channel list for tens of thousands of channels',
|
||||
'Channel groups, favorites and recent history',
|
||||
'XMLTV program guide with catch-up',
|
||||
'Radio entries in a dedicated audio layout',
|
||||
'DASH playback with ClearKey keys',
|
||||
'MPV and VLC external player handoff',
|
||||
],
|
||||
faq,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={title}
|
||||
description={description}
|
||||
keywords={['M3U player', 'M3U8 player', 'M3U player Windows', 'M3U player Mac', 'M3U player Linux', 'IPTV playlist player', 'open-source IPTV player']}
|
||||
jsonLd={jsonLd}
|
||||
>
|
||||
<FeatureHero
|
||||
feature={feature}
|
||||
titleLead="An M3U player that"
|
||||
titleAccent="keeps up"
|
||||
lede="Load an M3U or M3U8 playlist from a link, a file or pasted text and get a desktop TV app: grouped channels, favorites, instant search, a program guide with catch-up, and playback engines that handle what browsers cannot."
|
||||
facts={['M3U & M3U8', 'URL, file or text', 'Tens of thousands of channels', 'XMLTV EPG']}
|
||||
screenshot={{
|
||||
src: '/iptvnator/blog/guides/screenshots/guide-m3u-live-groups-dark.png',
|
||||
alt: 'Groups view of an M3U playlist with a group and its channel list',
|
||||
width: 2560,
|
||||
height: 1440,
|
||||
}}
|
||||
/>
|
||||
|
||||
<DownloadSection id="library" eyebrow="The library" title="Built for" accent="big playlists" lead="Provider playlists are long. The list, the groups and the search are designed around that.">
|
||||
<FeatureList items={library} />
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="attributes" eyebrow="Playlist attributes" title="Everything the playlist" accent="declares" lead="IPTVnator reads the extended M3U attributes providers use, so logos, guide ids, catch-up windows and radio flags all mean something.">
|
||||
<SpecList items={declared} />
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="playback" eyebrow="Playback" title="Engines for" accent="every stream" lead="Most streams play in the built-in players. The rest go to MPV or VLC without leaving the app.">
|
||||
<FeatureList items={playback} />
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="faq" eyebrow="FAQ" title="Questions about" accent="M3U playlists">
|
||||
<div class="max-w-3xl">
|
||||
<FaqAccordion items={faq} />
|
||||
</div>
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="guides" eyebrow="Keep reading" title="Guides and" accent="background">
|
||||
<RelatedPosts slugs={['m3u-playlist-epg-setup-guide', 'epg-guide', 'why-external-players-help']} />
|
||||
<div class="mt-10">
|
||||
<OfficialSourcesNote />
|
||||
</div>
|
||||
</DownloadSection>
|
||||
|
||||
<FeatureSwitcher current="m3u-player" />
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import FaqAccordion from '../../components/blog/FaqAccordion.astro';
|
||||
import StepRail from '../../components/blog/StepRail.astro';
|
||||
import DownloadSection from '../../components/download/DownloadSection.astro';
|
||||
import FeatureList from '../../components/download/FeatureList.astro';
|
||||
import OfficialSourcesNote from '../../components/download/OfficialSourcesNote.astro';
|
||||
import RelatedPosts from '../../components/download/RelatedPosts.astro';
|
||||
import SpecList from '../../components/download/SpecList.astro';
|
||||
import FeatureHero from '../../components/feature/FeatureHero.astro';
|
||||
import FeatureSwitcher from '../../components/feature/FeatureSwitcher.astro';
|
||||
import type { FaqEntry } from '../../lib/download-schema';
|
||||
import { buildFeaturePageSchema } from '../../lib/feature-schema';
|
||||
import { featureBySlug } from '../../lib/features';
|
||||
import { absoluteUrl } from '../../lib/site';
|
||||
|
||||
const feature = featureBySlug('remote-control');
|
||||
const pageUrl = absoluteUrl('/features/remote-control/');
|
||||
const title = 'Phone Remote Control for IPTVnator – Change Channels from the Couch';
|
||||
const description =
|
||||
'The IPTVnator desktop app serves a web remote to any phone or tablet on your network: channel up and down, direct channel numbers, volume for the built-in player and a now-playing panel with the current program. No app to install, no account, free and open source.';
|
||||
|
||||
const setup = [
|
||||
'Open Settings › Remote control on the desktop app and enable the remote. The default port is 8765; change it if something else uses it.',
|
||||
'Scan the QR code shown in the settings with your phone, or type the displayed address into its browser. Phone and computer must be on the same network.',
|
||||
'Keep the page open. It shows what is playing and offers channel up and down, a number pad and, for the built-in player, volume.',
|
||||
];
|
||||
|
||||
const remote = [
|
||||
{
|
||||
title: 'Channel up and down',
|
||||
description: 'Step through the list you are looking at: the whole playlist in the M3U player, the current category in a portal, the filtered collection in favorites and recent.',
|
||||
},
|
||||
{
|
||||
title: 'Direct channel numbers',
|
||||
description: 'Type a number on the pad to jump to that position in the list, the way a set-top box remote does.',
|
||||
},
|
||||
{
|
||||
title: 'Now playing',
|
||||
description: 'The remote shows the source, the channel name and number and the current program, refreshed every two seconds.',
|
||||
},
|
||||
{
|
||||
title: 'Volume',
|
||||
description: 'Volume up, down and mute for the built-in players in the M3U player; external MPV or VLC keep their own volume.',
|
||||
},
|
||||
{
|
||||
title: 'Works with every source',
|
||||
description: 'M3U playlists, Xtream live TV, Stalker live TV and radio, and the favorites, recent and global collections all publish their status to the remote.',
|
||||
},
|
||||
{
|
||||
title: 'Nothing to install',
|
||||
description: 'The remote is a small web page served by the desktop app itself. Any phone or tablet browser works, and there is no account.',
|
||||
},
|
||||
];
|
||||
|
||||
const details = [
|
||||
{ term: 'Where it runs', value: 'Desktop app only. The app hosts the remote page and the small API it uses.' },
|
||||
{ term: 'Address', value: 'Your computer\'s local IP address and the configured port, shown as text and as a QR code in the settings.' },
|
||||
{ term: 'Default port', value: '8765, configurable in Settings › Remote control.' },
|
||||
{ term: 'Network', value: 'Local network only. The remote has no authentication or encryption, so keep it off networks you do not trust.' },
|
||||
{ term: 'Numbers', value: 'A number selects the channel at that position in the active list, not a channel number from the guide.' },
|
||||
{ term: 'Updates', value: 'The remote polls the app every two seconds; a channel change on the desktop appears on the phone right after.' },
|
||||
];
|
||||
|
||||
const faq: FaqEntry[] = [
|
||||
{
|
||||
q: 'Do I need to install an app on my phone?',
|
||||
a: 'No. The remote is a web page served by the IPTVnator desktop app. Open the address or scan the QR code from the settings in any phone or tablet browser.',
|
||||
},
|
||||
{
|
||||
q: 'Does the remote work over the internet?',
|
||||
a: 'No. It is meant for the local network, where your phone and computer share a Wi-Fi, and it has no authentication or encryption. Do not expose the port to the internet.',
|
||||
},
|
||||
{
|
||||
q: 'Which players support volume from the remote?',
|
||||
a: 'The built-in web players in the M3U player. MPV, VLC and the embedded MPV engine keep their own volume, and portal views currently pass only channel commands.',
|
||||
},
|
||||
{
|
||||
q: 'What does a channel number mean on the remote?',
|
||||
a: 'The position in the list you are currently looking at, counted from one, like the numbering in the sidebar. It is not the channel number a provider may print in its guide.',
|
||||
},
|
||||
{
|
||||
q: 'Does the remote work with the browser version of IPTVnator?',
|
||||
a: 'No. Serving the remote page requires the desktop app, which hosts the small web server behind it.',
|
||||
},
|
||||
];
|
||||
|
||||
const jsonLd = buildFeaturePageSchema({
|
||||
feature,
|
||||
pageUrl,
|
||||
description,
|
||||
featureList: [
|
||||
'Web remote served by the desktop app to phones on the local network',
|
||||
'Channel up, channel down and direct channel numbers',
|
||||
'Now-playing status with source, channel and current program',
|
||||
'Volume control for the built-in players',
|
||||
'QR code and address shown in the settings',
|
||||
],
|
||||
faq,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={title}
|
||||
description={description}
|
||||
keywords={['IPTV player remote control', 'IPTVnator remote control', 'control IPTV player from phone', 'IPTV player with remote app', 'open-source IPTV player']}
|
||||
jsonLd={jsonLd}
|
||||
>
|
||||
<FeatureHero
|
||||
feature={feature}
|
||||
titleLead="Your phone as the"
|
||||
titleAccent="remote"
|
||||
lede="The desktop app can serve a small web remote to any phone or tablet on your network: channel up and down, direct channel numbers, volume for the built-in player, and a now-playing panel with the current program. No app to install, no account."
|
||||
facts={['Desktop app feature', 'Any phone browser', 'Same Wi-Fi network', 'QR code setup']}
|
||||
screenshot={{
|
||||
src: '/iptvnator/blog/v0-19-remote-control.png',
|
||||
alt: 'The web remote with a now-playing panel, channel navigation and a number pad',
|
||||
width: 2284,
|
||||
height: 1772,
|
||||
}}
|
||||
/>
|
||||
|
||||
<DownloadSection id="setup" eyebrow="Setup" title="Ready in" accent="three steps" lead="Enable it once in the settings; the address and QR code stay there for every device you want to use.">
|
||||
<div class="max-w-3xl">
|
||||
<StepRail title="On the desktop app" steps={setup} />
|
||||
</div>
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="remote" eyebrow="What you get" title="A remote that" accent="knows what is on">
|
||||
<FeatureList items={remote} />
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="details" eyebrow="Under the hood" title="The" accent="details">
|
||||
<SpecList items={details} />
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="faq" eyebrow="FAQ" title="Questions about" accent="the remote">
|
||||
<div class="max-w-3xl">
|
||||
<FaqAccordion items={faq} />
|
||||
</div>
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="guides" eyebrow="Keep reading" title="Set up a" accent="source first">
|
||||
<RelatedPosts slugs={['m3u-playlist-epg-setup-guide', 'xtream-codes-setup-guide', 'stalker-portal-setup-guide']} />
|
||||
<div class="mt-10">
|
||||
<OfficialSourcesNote />
|
||||
</div>
|
||||
</DownloadSection>
|
||||
|
||||
<FeatureSwitcher current="remote-control" />
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,192 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import FaqAccordion from '../../components/blog/FaqAccordion.astro';
|
||||
import DownloadSection from '../../components/download/DownloadSection.astro';
|
||||
import FeatureList from '../../components/download/FeatureList.astro';
|
||||
import OfficialSourcesNote from '../../components/download/OfficialSourcesNote.astro';
|
||||
import RelatedPosts from '../../components/download/RelatedPosts.astro';
|
||||
import SpecList from '../../components/download/SpecList.astro';
|
||||
import FeatureHero from '../../components/feature/FeatureHero.astro';
|
||||
import FeatureSwitcher from '../../components/feature/FeatureSwitcher.astro';
|
||||
import type { FaqEntry } from '../../lib/download-schema';
|
||||
import { buildFeaturePageSchema } from '../../lib/feature-schema';
|
||||
import { featureBySlug } from '../../lib/features';
|
||||
import { absoluteUrl } from '../../lib/site';
|
||||
|
||||
const feature = featureBySlug('stalker-portal-player');
|
||||
const pageUrl = absoluteUrl('/features/stalker-portal-player/');
|
||||
const title = 'Stalker Portal Player for Windows, macOS and Linux – IPTVnator';
|
||||
const description =
|
||||
'IPTVnator is a free, open-source Stalker and Ministra portal player for the desktop: MAC address identity like a MAG box, automatic endpoint and portal-type detection, live TV with a program guide, radio, movies and series in every portal shape, and an account page with your expiry date.';
|
||||
|
||||
const capabilities = [
|
||||
{
|
||||
title: 'Behaves like a MAG box',
|
||||
description:
|
||||
'Handshake, profile and token refresh with the MAC address as identity, the same headers a set-top box sends, and a watchdog that keeps full Ministra portals happy.',
|
||||
},
|
||||
{
|
||||
title: 'Finds the right endpoint',
|
||||
description:
|
||||
'Enter a bare host, /c, portal.php or server/load.php. IPTVnator probes the usual endpoints, classifies the portal by how it behaves, and repairs the stored connection if the provider migrates.',
|
||||
},
|
||||
{
|
||||
title: 'Live TV and radio',
|
||||
description:
|
||||
'Categories, channels with the current program and a progress bar, the complete channel list cached for search, and a dedicated audio layout for radio stations.',
|
||||
},
|
||||
{
|
||||
title: 'Program guide and catch-up',
|
||||
description:
|
||||
'The portal schedule is loaded for a week and shown in the list and in the timeline under the player. Archived channels play past programs from the timeline.',
|
||||
},
|
||||
{
|
||||
title: 'Movies and series, all three shapes',
|
||||
description:
|
||||
'Regular series, movies with embedded episodes and Ministra lazily loaded seasons all render as seasons and episodes with resume positions and watched markers.',
|
||||
},
|
||||
{
|
||||
title: 'Account info',
|
||||
description:
|
||||
'Status, login, tariff plan, expiry with a days-left counter and portal details, shown instantly from the import snapshot and refreshed from the portal.',
|
||||
},
|
||||
{
|
||||
title: 'Device identity, on your terms',
|
||||
description:
|
||||
'Optional serial number, device IDs and signatures are sent only when you enter them. Device IDs can be derived the StbEmu way at import, and stored IDs are protected from accidental changes.',
|
||||
},
|
||||
{
|
||||
title: 'Fresh links, never stored',
|
||||
description:
|
||||
'Temporary stream links are requested from the portal for every playback and never persisted, so favorites and history keep working when links expire.',
|
||||
},
|
||||
{
|
||||
title: 'Favorites, recent, search, downloads',
|
||||
description:
|
||||
'Per-portal collections plus global favorites, recent items and search across all sources. The desktop app downloads movies and episodes for offline viewing.',
|
||||
},
|
||||
];
|
||||
|
||||
const details = [
|
||||
{ term: 'Portal URL', value: 'Any http or https address: bare host, /c, portal.php or server/load.php. The proven endpoint is stored with the source.' },
|
||||
{ term: 'MAC address', value: 'Normalized to 00:1A:79:XX:XX:XX; dashes, dots and lowercase are accepted. Addresses outside the Infomir range get a hint, not a refusal.' },
|
||||
{ term: 'Login and password', value: 'Only when the portal demands them; IPTVnator says so and retries once you fill them in.' },
|
||||
{ term: 'Portal messages', value: 'Refusals are shown with the portal\'s own words: login required, login rejected, device conflict, blocked.' },
|
||||
{ term: 'Program guide', value: 'Portal schedule first; XMLTV sources from the desktop settings as a fallback, plus manual channel mapping.' },
|
||||
{ term: 'Browser version', value: 'Works in the self-hosted web version through its backend proxy; downloads are a desktop feature.' },
|
||||
];
|
||||
|
||||
const faq: FaqEntry[] = [
|
||||
{
|
||||
q: 'What is a Stalker portal player?',
|
||||
a: 'A player that speaks the Stalker or Ministra middleware protocol used by MAG set-top boxes. It authenticates with a MAC address instead of a username, fetches channel lists and movie catalogs on demand, and resolves a temporary link for every stream. IPTVnator does all of that on the desktop.',
|
||||
},
|
||||
{
|
||||
q: 'Which portal URL do I need?',
|
||||
a: 'Whatever your provider gave you, as long as it starts with http or https. IPTVnator tries the usual endpoints behind that address and keeps the one that answers, so a bare host, /c, portal.php or server/load.php all work.',
|
||||
},
|
||||
{
|
||||
q: 'Do I need a real MAG box or a serial number?',
|
||||
a: 'No. The MAC address the provider registered is enough for most portals. Serial number, device IDs and signatures are optional and only needed when the provider registered a specific box and told you the values.',
|
||||
},
|
||||
{
|
||||
q: 'Does IPTVnator support Ministra portals with a login?',
|
||||
a: 'Yes. When a portal asks for a login and password on top of the MAC address, IPTVnator reports it, and the optional username and password fields in the Add playlist dialog complete the authentication.',
|
||||
},
|
||||
{
|
||||
q: 'Does the program guide and catch-up work with portals?',
|
||||
a: 'Yes. The portal schedule is loaded for a week and shown in the channel list and the timeline under the player. Channels the portal marks as archived can be played from the timeline.',
|
||||
},
|
||||
{
|
||||
q: 'Does IPTVnator provide portal accounts?',
|
||||
a: 'No. IPTVnator is a player. The portal URL and MAC address come from the provider you already have a contract with, and they are only sent to that portal.',
|
||||
},
|
||||
];
|
||||
|
||||
const jsonLd = buildFeaturePageSchema({
|
||||
feature,
|
||||
pageUrl,
|
||||
description,
|
||||
featureList: [
|
||||
'Stalker and Ministra portal login by MAC address',
|
||||
'Automatic endpoint discovery and portal-type detection',
|
||||
'Live TV with portal program guide and catch-up',
|
||||
'Radio stations in a dedicated audio layout',
|
||||
'Movies and series in regular, embedded and lazy-season shapes',
|
||||
'Account status, tariff plan and expiry information',
|
||||
'Optional device identity fields with derivation from the MAC',
|
||||
],
|
||||
faq,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={title}
|
||||
description={description}
|
||||
keywords={['Stalker portal player', 'Stalker portal player Windows', 'Stalker portal player Mac', 'Stalker portal player Linux', 'Ministra portal player', 'MAC address IPTV player', 'MAG portal player PC', 'open-source IPTV player']}
|
||||
jsonLd={jsonLd}
|
||||
>
|
||||
<FeatureHero
|
||||
feature={feature}
|
||||
titleLead="A Stalker portal player"
|
||||
titleAccent="without the set-top box"
|
||||
lede="IPTVnator talks to Stalker and Ministra middleware the way a MAG box does: MAC address identity, handshake and profile, live TV with a program guide, radio, movies and series in all three portal shapes, and an account page with your expiry date."
|
||||
facts={['MAC address identity', 'Ministra & reseller panels', 'ITV, radio, movies, series', 'Endpoint discovery']}
|
||||
screenshot={{
|
||||
src: '/iptvnator/blog/guides/screenshots/guide-stalker-live-dark.png',
|
||||
alt: 'Live TV of a Stalker portal with categories, channels and the current program',
|
||||
width: 2560,
|
||||
height: 1440,
|
||||
}}
|
||||
/>
|
||||
|
||||
<DownloadSection id="capabilities" eyebrow="What you get" title="Everything the portal" accent="serves" lead="The full portal experience of a set-top box, with the search, collections and downloads of a desktop app on top.">
|
||||
<FeatureList items={capabilities} />
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="import" eyebrow="Setup" title="Two fields," accent="and it works">
|
||||
<div class="grid gap-8 lg:grid-cols-2 lg:items-center">
|
||||
<div class="space-y-4 text-base leading-relaxed text-surface-400">
|
||||
<p>
|
||||
The Add playlist dialog asks for a title, the portal URL and the MAC address. Everything else is optional and
|
||||
stays empty unless your provider gave you values. When you click Add, IPTVnator probes the portal, authenticates
|
||||
with your MAC address, saves the endpoint and mode that worked, and opens the source.
|
||||
</p>
|
||||
<p>
|
||||
Providers that migrate their portal do not break the source: on the first failure IPTVnator re-probes once and
|
||||
repairs the stored connection.
|
||||
</p>
|
||||
</div>
|
||||
<figure class="overflow-hidden rounded-xl border border-dashed border-surface-600/60 bg-surface-900/60 p-2">
|
||||
<img
|
||||
src="/iptvnator/blog/guides/screenshots/guide-stalker-add-playlist-dark.png"
|
||||
alt="Add playlist dialog with the Stalker portal method, a portal URL and a MAC address"
|
||||
width={2560}
|
||||
height={1440}
|
||||
class="h-auto w-full rounded-lg"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</figure>
|
||||
</div>
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="details" eyebrow="Under the hood" title="Connection" accent="details">
|
||||
<SpecList items={details} />
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="faq" eyebrow="FAQ" title="Questions about" accent="Stalker portals">
|
||||
<div class="max-w-3xl">
|
||||
<FaqAccordion items={faq} />
|
||||
</div>
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="guides" eyebrow="Keep reading" title="Guides and" accent="background">
|
||||
<RelatedPosts slugs={['stalker-portal-setup-guide', 'xtream-codes-setup-guide', 'why-external-players-help']} />
|
||||
<div class="mt-10">
|
||||
<OfficialSourcesNote />
|
||||
</div>
|
||||
</DownloadSection>
|
||||
|
||||
<FeatureSwitcher current="stalker-portal-player" />
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,193 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import FaqAccordion from '../../components/blog/FaqAccordion.astro';
|
||||
import DownloadSection from '../../components/download/DownloadSection.astro';
|
||||
import FeatureList from '../../components/download/FeatureList.astro';
|
||||
import OfficialSourcesNote from '../../components/download/OfficialSourcesNote.astro';
|
||||
import RelatedPosts from '../../components/download/RelatedPosts.astro';
|
||||
import SpecList from '../../components/download/SpecList.astro';
|
||||
import FeatureHero from '../../components/feature/FeatureHero.astro';
|
||||
import FeatureSwitcher from '../../components/feature/FeatureSwitcher.astro';
|
||||
import type { FaqEntry } from '../../lib/download-schema';
|
||||
import { buildFeaturePageSchema } from '../../lib/feature-schema';
|
||||
import { featureBySlug } from '../../lib/features';
|
||||
import { absoluteUrl } from '../../lib/site';
|
||||
|
||||
const feature = featureBySlug('xtream-codes-player');
|
||||
const pageUrl = absoluteUrl('/features/xtream-codes-player/');
|
||||
const title = 'Xtream Codes Player for Desktop – Live TV, Movies and Series | IPTVnator';
|
||||
const description =
|
||||
'IPTVnator is a free, open-source Xtream Codes player for Windows, macOS and Linux: live channels with the portal program guide and catch-up, movies and series with posters and details, offline downloads, a local catalog that opens instantly, and an account page with your expiry date.';
|
||||
|
||||
const unlocked = [
|
||||
{
|
||||
title: 'Live TV with a guide',
|
||||
description:
|
||||
'Categories, channels with the current program and a timeline under the player, fed by the portal schedule. Archived channels carry a catch-up badge and play from the timeline.',
|
||||
},
|
||||
{
|
||||
title: 'Movies and series',
|
||||
description:
|
||||
'Poster grids per category and a detail page with description, cast, trailer and similar titles. Seasons are tabs, episodes remember where you stopped, and whole seasons can be marked watched.',
|
||||
},
|
||||
{
|
||||
title: 'A local catalog',
|
||||
description:
|
||||
'The desktop app stores the categories and content in a local library, so a source opens instantly and only changed data is downloaded on the next sync.',
|
||||
},
|
||||
{
|
||||
title: 'Offline downloads',
|
||||
description:
|
||||
'Save movies and episodes to disk from the detail page. The download manager resumes interrupted transfers and keeps a library of finished files.',
|
||||
},
|
||||
{
|
||||
title: 'Search, favorites, recent',
|
||||
description:
|
||||
'Per-source search, favorites and history, plus a Recently added section for new content and global collections across all your sources.',
|
||||
},
|
||||
{
|
||||
title: 'Metadata and discovery',
|
||||
description:
|
||||
'Optional TMDB enrichment adds artwork, ratings and trailers, links actors to their filmography, and opens a Discover page by year, genre and country.',
|
||||
},
|
||||
{
|
||||
title: 'Account info',
|
||||
description:
|
||||
'Status, expiry date, active and maximum connections, trial flag and allowed formats in one dialog; the dashboard shows an expiry chip during the last week.',
|
||||
},
|
||||
{
|
||||
title: 'Refresh without losing data',
|
||||
description:
|
||||
'Refresh a source from the provider while favorites, watch history, hidden categories and playback positions stay in place.',
|
||||
},
|
||||
{
|
||||
title: 'The same movie, several sources',
|
||||
description:
|
||||
'With more than one Xtream source, a movie found in several of them shows a Sources chip, and playback can switch to another copy or fail over automatically.',
|
||||
},
|
||||
];
|
||||
|
||||
const connection = [
|
||||
{ term: 'What you enter', value: 'Server URL, username and password. A pasted get.php or player_api.php link fills the fields for you.' },
|
||||
{ term: 'Connection test', value: 'Before adding, IPTVnator asks the portal for the account status and reports active, expired, inactive or unreachable.' },
|
||||
{ term: 'Stream formats', value: 'Live streams use HLS when the portal allows it and fall back to MPEG-TS; a manual format setting exists under Settings › Playback.' },
|
||||
{ term: 'Program guide', value: 'The portal schedule is used first. On the desktop app, your own XMLTV sources fill missing channels or take precedence, as you prefer.' },
|
||||
{ term: 'User-Agent', value: 'A provider-compatible IPTV player signature by default; a custom one per playlist on the desktop app.' },
|
||||
{ term: 'Browser version', value: 'Works in the self-hosted web version through its backend proxy; the local catalog and downloads are desktop features.' },
|
||||
];
|
||||
|
||||
const faq: FaqEntry[] = [
|
||||
{
|
||||
q: 'What is an Xtream Codes player?',
|
||||
a: 'A player that talks to the Xtream Codes API most IPTV panels expose: it signs in with a server URL, username and password and receives categories, channels, movies, series, the program guide and catch-up from the provider. IPTVnator is one, free and open source.',
|
||||
},
|
||||
{
|
||||
q: 'Does IPTVnator work with any Xtream Codes provider?',
|
||||
a: 'It works with panels that implement the Xtream Codes API, including the common variants in account-info actions, catch-up URL shapes and stream formats. The connection test in the Add playlist dialog tells you immediately whether the portal answers.',
|
||||
},
|
||||
{
|
||||
q: 'Can I watch movies and series, not only live TV?',
|
||||
a: 'Yes. Movies and series are first-class: poster grids, detail pages with descriptions and cast, seasons and episodes with resume positions, and on the desktop app a download button for offline viewing.',
|
||||
},
|
||||
{
|
||||
q: 'Does the program guide and catch-up work?',
|
||||
a: 'Yes. The portal schedule is shown in the channel list and in a timeline under the player, and channels the provider archives can be played from the timeline. The timeline shows how many days of archive a channel has.',
|
||||
},
|
||||
{
|
||||
q: 'Can I add more than one Xtream account?',
|
||||
a: 'Yes. Every account is its own source with separate favorites, history and cached catalog. The dashboard, global favorites, recent items and search combine them.',
|
||||
},
|
||||
{
|
||||
q: 'Does IPTVnator sell Xtream Codes subscriptions?',
|
||||
a: 'No. IPTVnator is a player. The server URL, username and password come from the provider you already have a contract with, and they are only sent to that server.',
|
||||
},
|
||||
];
|
||||
|
||||
const jsonLd = buildFeaturePageSchema({
|
||||
feature,
|
||||
pageUrl,
|
||||
description,
|
||||
featureList: [
|
||||
'Xtream Codes API login with connection test',
|
||||
'Live TV with portal program guide and catch-up',
|
||||
'Movies and series with posters, details and resume',
|
||||
'Local catalog cache for instant source switching',
|
||||
'Offline downloads of movies and episodes',
|
||||
'Account status and expiry information',
|
||||
'Optional TMDB metadata and Discover pages',
|
||||
],
|
||||
faq,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={title}
|
||||
description={description}
|
||||
keywords={['Xtream Codes player', 'Xtream Codes player Windows', 'Xtream Codes player Mac', 'Xtream Codes player Linux', 'Xtream Codes API player', 'IPTV player desktop', 'open-source IPTV player']}
|
||||
jsonLd={jsonLd}
|
||||
>
|
||||
<FeatureHero
|
||||
feature={feature}
|
||||
titleLead="The Xtream Codes player"
|
||||
titleAccent="for your desktop"
|
||||
lede="Sign in with server URL, username and password and get the whole provider library: live channels with a program guide and catch-up, movies and series with posters and details, downloads for offline viewing, and a local catalog that opens instantly."
|
||||
facts={['Live, movies & series', 'Portal EPG & catch-up', 'Offline downloads', 'Local catalog']}
|
||||
screenshot={{
|
||||
src: '/iptvnator/blog/v0-23/screenshots/xtream-vod-details-dark.png',
|
||||
alt: 'Movie details from an Xtream source with poster, description and playback actions',
|
||||
width: 2560,
|
||||
height: 1440,
|
||||
}}
|
||||
/>
|
||||
|
||||
<DownloadSection id="unlocked" eyebrow="What you get" title="One login," accent="the whole library" lead="Everything the provider exposes through the API, organized the way a desktop app should organize it.">
|
||||
<FeatureList items={unlocked} />
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="series" eyebrow="Series" title="Seasons, episodes," accent="progress">
|
||||
<div class="grid gap-8 lg:grid-cols-2 lg:items-center">
|
||||
<div class="space-y-4 text-base leading-relaxed text-surface-400">
|
||||
<p>
|
||||
A series opens with its seasons as tabs and the episodes of the selected season below, with stills and
|
||||
descriptions from the provider or from TMDB when enrichment is on. IPTVnator remembers the position in every
|
||||
episode, offers Resume and Play next episode, and shows an Up Next rail beside the player while an episode is
|
||||
playing.
|
||||
</p>
|
||||
<p>
|
||||
Seasons and whole series can be marked watched in one action, and on the desktop app a season can be downloaded
|
||||
episode by episode for offline viewing.
|
||||
</p>
|
||||
</div>
|
||||
<figure class="overflow-hidden rounded-xl border border-dashed border-surface-600/60 bg-surface-900/60 p-2">
|
||||
<img
|
||||
src="/iptvnator/blog/v0-23/screenshots/xtream-series-season-open-dark.png"
|
||||
alt="Series details with season tabs and the episode list of a season"
|
||||
width={2560}
|
||||
height={1440}
|
||||
class="h-auto w-full rounded-lg"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</figure>
|
||||
</div>
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="connection" eyebrow="Under the hood" title="Connection" accent="details">
|
||||
<SpecList items={connection} />
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="faq" eyebrow="FAQ" title="Questions about" accent="Xtream Codes">
|
||||
<div class="max-w-3xl">
|
||||
<FaqAccordion items={faq} />
|
||||
</div>
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="guides" eyebrow="Keep reading" title="Guides and" accent="background">
|
||||
<RelatedPosts slugs={['xtream-codes-setup-guide', 'stalker-portal-setup-guide', 'why-external-players-help']} />
|
||||
<div class="mt-10">
|
||||
<OfficialSourcesNote />
|
||||
</div>
|
||||
</DownloadSection>
|
||||
|
||||
<FeatureSwitcher current="xtream-codes-player" />
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,69 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
/**
|
||||
* Structural checks for the feature landing pages: canonical URLs, the
|
||||
* SoftwareApplication + FAQPage + BreadcrumbList structured data, links to
|
||||
* the download hub and a setup guide, the hub's coverage, and sitemap entries.
|
||||
*/
|
||||
|
||||
const distRoot = new URL('../../dist/apps/website/', import.meta.url);
|
||||
const SITE = 'https://4gray.github.io/iptvnator';
|
||||
const FEATURES = ['m3u-player', 'xtream-codes-player', 'stalker-portal-player', 'epg', 'remote-control'];
|
||||
|
||||
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 slug of FEATURES) {
|
||||
test(`feature page ${slug}: canonical, schema and links`, async () => {
|
||||
const html = await readDist(`features/${slug}/index.html`);
|
||||
assert.match(html, new RegExp(`<link rel="canonical" href="${SITE}/features/${slug}/"`));
|
||||
assert.match(html, /<meta name="robots" content="index, follow/);
|
||||
|
||||
const schema = extractJsonLd(html);
|
||||
const app = schema.find((entry) => entry['@type'] === 'SoftwareApplication');
|
||||
assert.ok(app, 'Expected a SoftwareApplication entry.');
|
||||
assert.ok(Array.isArray(app.featureList) && app.featureList.length >= 4, 'Expected a featureList.');
|
||||
const faq = schema.find((entry) => entry['@type'] === 'FAQPage');
|
||||
assert.ok(faq && faq.mainEntity.length >= 5, 'Expected a FAQPage with at least five questions.');
|
||||
const crumbs = schema.find((entry) => entry['@type'] === 'BreadcrumbList');
|
||||
assert.equal(crumbs.itemListElement.at(-1).item, `${SITE}/features/${slug}/`);
|
||||
|
||||
assert.match(html, /href="\/iptvnator\/download\/"/);
|
||||
assert.match(html, /href="\/iptvnator\/blog\/[a-z0-9-]+-setup-guide\/"/);
|
||||
assert.match(html, /href="\/iptvnator\/features\/"/);
|
||||
for (const other of FEATURES.filter((candidate) => candidate !== slug)) {
|
||||
assert.match(html, new RegExp(`href="/iptvnator/features/${other}/"`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('features hub links to every feature page and the download hub', async () => {
|
||||
const html = await readDist('features/index.html');
|
||||
assert.match(html, new RegExp(`<link rel="canonical" href="${SITE}/features/"`));
|
||||
for (const slug of FEATURES) {
|
||||
assert.match(html, new RegExp(`href="/iptvnator/features/${slug}/"`));
|
||||
}
|
||||
assert.match(html, /href="\/iptvnator\/download\/"/);
|
||||
});
|
||||
|
||||
test('homepage feature cards and the header link into the feature pages', async () => {
|
||||
const html = await readDist('index.html');
|
||||
assert.match(html, /href="\/iptvnator\/features\/"/);
|
||||
for (const slug of ['m3u-player', 'xtream-codes-player', 'epg', 'stalker-portal-player', 'remote-control']) {
|
||||
assert.match(html, new RegExp(`href="/iptvnator/features/${slug}/"`));
|
||||
}
|
||||
});
|
||||
|
||||
test('sitemap lists the feature pages', async () => {
|
||||
const sitemap = await readDist('sitemap-0.xml');
|
||||
for (const path of ['features/', ...FEATURES.map((slug) => `features/${slug}/`)]) {
|
||||
assert.match(sitemap, new RegExp(`<loc>${SITE}/${path}</loc>`));
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user