Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions firetv/emulator-test.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
#!/bin/bash
# Boots an Android TV emulator (API 30, the base of Fire OS 8), installs the APK,
# drives it with remote-control keys and saves 1920x1080 screenshots to store/shots.
#
# THIS IS A FUNCTIONAL TEST, NOT THE SOURCE OF STORE ART. It searches the live
# directory, so 04-results, 05-results-nav, 05b-results-right and 08-menu-search
# are full of other people's cover art and show names. Uploading those is what
# got 1.0.0 rejected on 2026-11-04 under Amazon's IP and Copyright policy.
# Store screenshots come from store/capture.sh (/tv-preview), never from here.
# Runs inside gradle:8.7-jdk17 with --device /dev/kvm and the SDK cache at /sdk:
# docker run --rm --device /dev/kvm -v ~/scratch/android-sdk:/sdk -v "$PWD/firetv:/w" -w /w \
# gradle:8.7-jdk17 bash emulator-test.sh
Expand Down
35 changes: 35 additions & 0 deletions firetv/store/capture.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/sh
# Store screenshots for the Amazon Appstore listing, 1920x1080.
#
# These must carry NO third-party cover art and NO real show names. Amazon
# rejected 1.0.0 on 2026-11-04 because the screenshots were live captures of
# /search: real artwork and real titles (Mighty Oaks Daily Devotional, The
# Harry Sisson Show) were in the listing, and we hold no licence to any of it.
#
# So the grid comes from /tv-preview, which renders the real card grid with
# invented shows and no image URLs — <Art> then draws its own letter tile for
# every square. The app is a WebView of exactly this site at exactly this size,
# so a headless capture is what the TV shows, without needing the Android SDK.
#
# Usage (defaults to production):
# sh firetv/store/capture.sh [BASE_URL]
set -eu
BASE=${1:-https://p0dcasters.com}
OUT=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/shots
mkdir -p "$OUT"

shot() { # shot <name> <path>
docker run --rm --network host -v "$OUT:/out" -w /out \
--entrypoint chromium-browser zenika/alpine-chrome:124 \
--headless --no-sandbox --disable-gpu --hide-scrollbars \
--window-size=1920,1080 --virtual-time-budget=12000 \
--screenshot="/out/$1.png" "$BASE$2" >/dev/null 2>&1
echo "$1.png <- $BASE$2"
}

shot 01-home /
shot 02-grid /tv-preview

echo
echo "Check both by eye before uploading: no cover art that is not ours, no real show titles."
ls -l "$OUT"/01-home.png "$OUT"/02-grid.png
20 changes: 18 additions & 2 deletions firetv/store/listing.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ Every podcast with a feed, plus live radio and TV. Free, no account.

podcasters puts p0dcasters.com on your TV: every podcast with a public feed, on any host, plus live radio and TV channels.

Over 390,000 live shows read straight from their RSS feeds: shows on their own domain, shows on Anchor, Buzzsprout, Libsyn and every other host, and the big libraries like TWiT, NPR and the BBC. Free, no account, nothing to sign up for.
Over 390,000 live shows read straight from their RSS feeds: shows on their own domain, shows on every hosting platform, and the big network libraries. Free, no account, nothing to sign up for.

podcasters is a podcast reader. It lists publicly published RSS feeds and plays them from the publisher's own server. It does not host, rehost or alter anyone's audio, and every show remains the property of whoever publishes it. Publishers can have a feed removed at any time from the contact page.

Made for the remote:
- Press Menu to search, and type with the Fire TV keyboard or your voice.
Expand Down Expand Up @@ -47,4 +49,18 @@ podcasts, podcast player, radio, live tv, p0dcasters, podcasters, rss, free podc
| Icon 512x512 / 114x114 | out/icon-512.png, out/icon-114.png |
| Fire TV icon 1280x720 | out/firetv-icon-1280x720.png |
| Fire TV background 1920x1080 | out/firetv-background-1920x1080.png |
| Screenshots 1920x1080 | shots/01-home.png, 05-results-nav.png, 04-results.png, 08-menu-search.png |
| Screenshots 1920x1080 | shots/01-home.png, 02-grid.png, 03-keyboard.png |

### Screenshots must not carry anyone else's artwork

Amazon rejected version 1.0.0 on 2026-11-04 under the Intellectual Property
and Copyright policy. The screenshots were live captures of `/search`, so real
cover art and real show names — Mighty Oaks Daily Devotional, The Harry Sisson
Show and others — were sitting in the store listing. We have no licence to
anyone's cover art; a feed being public does not grant one.

Capture the grid from **`/tv-preview`**, never from `/search`, `/podcasts` or
the home page's "deep catalogues" row. That page renders the real card grid
with invented shows and no image URLs, so `<Art>` draws its own typographic
tile for every square. The search screenshot must be taken with the keyboard
open and **no results behind it**.
Binary file removed firetv/store/shots/01-home.png
Binary file not shown.
Binary file removed firetv/store/shots/03-keyboard.png
Binary file not shown.
Binary file removed firetv/store/shots/04-results.png
Binary file not shown.
Binary file removed firetv/store/shots/05-results-nav.png
Binary file not shown.
Binary file removed firetv/store/shots/05b-results-right.png
Binary file not shown.
Binary file removed firetv/store/shots/06-back.png
Binary file not shown.
Binary file removed firetv/store/shots/07-menu-search.png
Binary file not shown.
Binary file removed firetv/store/shots/08-menu-search.png
Binary file not shown.
2 changes: 1 addition & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const metadata: Metadata = {
// 146 characters. Kept short: a wording of 176 once lost its last clause to
// truncation in exactly the snippet it was written for.
description:
"A free directory of every live podcast with an RSS feed — on any host, plus networks like TWiT, NPR and the BBC. Listen in the browser.",
"A free directory of every live podcast with an RSS feed — on any host, plus the big network libraries. Listen in the browser.",
openGraph: {
type: "website",
siteName: "p0dcasters",
Expand Down
2 changes: 1 addition & 1 deletion src/app/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export default async function Image() {
marginTop: 22,
}}
>
Self-hosted shows, every hosting platform, and networks like TWiT, NPR and the BBC.
Self-hosted shows, every hosting platform, and the big network libraries.
Listen in the browser and follow your favourites.
</div>
</div>
Expand Down
7 changes: 3 additions & 4 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,9 @@ export default async function Home() {
<h1>Every podcast with a feed — on any host.</h1>
<p className="lede">
<span className="count">{total.toLocaleString()}</span> live shows, read from
their RSS feeds: shows on their own domain, shows on Anchor, Buzzsprout, Libsyn
and every other host, and the big libraries — TWiT, NPR, the BBC and the rest of
the <Link href="/networks">networks</Link>. Free, no account, listen in the
browser. Only the shows on their own domain?{" "}
their RSS feeds: shows on their own domain, shows on every hosting platform,
and the big library <Link href="/networks">networks</Link>. Free, no account,
listen in the browser. Only the shows on their own domain?{" "}
<Link href="/self-hosted">Self-hosted</Link>. Missing one?{" "}
<Link href="/submit">Add it</Link>.
</p>
Expand Down
79 changes: 79 additions & 0 deletions src/app/tv-preview/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { Metadata } from "next";
import type { Podcast } from "@/lib/db";
import Card from "@/components/Card";

/**
* Store-capture page. Renders the real grid with invented shows and no
* artwork, so a screenshot carries none of the cover art or show names that
* belong to the people who publish the feeds we list.
*
* It exists because the Amazon Appstore rejected com.p0dcasters.tv on
* 2026-11-04: the screenshots were live captures of /search, so real third-party
* cover art and titles sat in the store listing, which is exactly what their IP
* policy forbids. Capture Fire TV screenshots HERE, never from /search.
*
* `image_url: ""` is the point — <Art> falls back to its own typographic tile,
* so every square is drawn by us. Keep the names invented, and keep them
* obviously generic so nobody reads them as a real show.
*/
export const metadata: Metadata = {
title: "Preview",
robots: { index: false, follow: false },
};

const SAMPLE: Array<Pick<Podcast, "title" | "host" | "episode_count"> & { days: number }> = [
{ title: "The Sample Show", host: "example.com", episode_count: 412, days: 1 },
{ title: "Morning Build Notes", host: "example.org", episode_count: 196, days: 2 },
{ title: "Field Recordings Weekly", host: "example.net", episode_count: 88, days: 1 },
{ title: "Two Founders Talking", host: "example.com", episode_count: 523, days: 3 },
{ title: "The Long Commute", host: "example.org", episode_count: 274, days: 1 },
{ title: "Kitchen Table History", host: "example.net", episode_count: 331, days: 4 },
{ title: "Nightshift Radio Hour", host: "example.com", episode_count: 605, days: 2 },
{ title: "Questions for Later", host: "example.org", episode_count: 147, days: 1 },
{ title: "A Walk and a Microphone", host: "example.net", episode_count: 239, days: 5 },
{ title: "Slow News Sunday", host: "example.com", episode_count: 164, days: 2 },
{ title: "The Back Catalogue", host: "example.org", episode_count: 718, days: 1 },
{ title: "Notes from the Workshop", host: "example.net", episode_count: 205, days: 3 },
];

const DAY = 86_400;

export default function TvPreviewPage() {
const now = Math.floor(Date.now() / 1000);
const shows = SAMPLE.map((s, i) => ({
id: i + 1,
slug: `sample-${i + 1}`,
guid: null,
feed_url: "",
title: s.title,
description: "",
// Empty on purpose: <Art> draws our own letter tile instead.
image_url: "",
link: null,
host: s.host,
author: null,
owner: null,
explicit: 0,
language: "en",
lang_base: "en",
category: null,
categories: null,
episode_count: s.episode_count,
newest_pubdate: now - s.days * DAY,
oldest_pubdate: null,
created_on: null,
})) as unknown as Podcast[];

return (
<main className="wrap">
<section>
<h2 className="sec">Browse every show</h2>
<div className="grid">
{shows.map((p) => (
<Card key={p.id} p={p} />
))}
</div>
</section>
</main>
);
}
Loading