diff --git a/firetv/emulator-test.sh b/firetv/emulator-test.sh index 50ee5c1..8e76e6f 100644 --- a/firetv/emulator-test.sh +++ b/firetv/emulator-test.sh @@ -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 diff --git a/firetv/store/capture.sh b/firetv/store/capture.sh new file mode 100755 index 0000000..27e8e75 --- /dev/null +++ b/firetv/store/capture.sh @@ -0,0 +1,51 @@ +#!/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. +# +# Both shots therefore come from /tv-preview, which renders the real card grid +# with invented shows and no image URLs, so draws its own typographic +# tile for every square. The app is a WebView of this site at this size, so a +# headless capture is what the TV shows, without needing the Android SDK. +# +# Three things that are not obvious, each found the hard way: +# +# * NOT the home page. Below its hero it shows the "deep catalogues" row, +# which is real cover art - and two of those shows are the ones Amazon +# named. /tv-preview?hero=1 gives the same hero with our own grid instead. +# * NOT /search either, even with no query: it carries a sponsored ad and +# names a real third-party domain in its help text. +# * Point BASE at the app container, not at the public hostname. p0dcasters +# answers a headless browser with its own x402 paywall ("You have used up +# the free allowance") after 20 requests an hour, and you will screenshot +# that instead. A browser user agent is still needed: the default +# HeadlessChrome string is treated as a crawler. +# +# Usage: +# sh firetv/store/capture.sh # the dev2 app container +# sh firetv/store/capture.sh http://127.0.0.1:PORT +set -eu +BASE=${1:-http://127.0.0.1:3235} +OUT=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/shots +UA="Mozilla/5.0 (X11; CrKey armv7l 1.5.16041) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" +mkdir -p "$OUT" + +shot() { # shot + docker run --rm --memory 3g --shm-size 1g --network host \ + -v "$OUT:/out" -w /out --entrypoint chromium-browser \ + zenika/alpine-chrome:124 \ + --headless --no-sandbox --disable-gpu --disable-dev-shm-usage \ + --hide-scrollbars --window-size=1920,1080 --virtual-time-budget=15000 \ + --user-agent="$UA" --screenshot="/out/$1.png" "$BASE$2" + echo "$1.png <- $BASE$2" +} + +shot 01-hero "/tv-preview?hero=1" +shot 02-grid /tv-preview + +echo +echo "Look at both before uploading: no cover art that is not ours, no real show titles." +ls -l "$OUT" diff --git a/firetv/store/listing.md b/firetv/store/listing.md index b6cce52..e82f43d 100644 --- a/firetv/store/listing.md +++ b/firetv/store/listing.md @@ -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. @@ -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-hero.png, shots/02-grid.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 `` draws its own typographic +tile for every square. The search screenshot must be taken with the keyboard +open and **no results behind it**. diff --git a/firetv/store/shots/01-hero.png b/firetv/store/shots/01-hero.png new file mode 100644 index 0000000..0149508 Binary files /dev/null and b/firetv/store/shots/01-hero.png differ diff --git a/firetv/store/shots/01-home.png b/firetv/store/shots/01-home.png deleted file mode 100644 index 2387b39..0000000 Binary files a/firetv/store/shots/01-home.png and /dev/null differ diff --git a/firetv/store/shots/02-grid.png b/firetv/store/shots/02-grid.png new file mode 100644 index 0000000..8697b60 Binary files /dev/null and b/firetv/store/shots/02-grid.png differ diff --git a/firetv/store/shots/03-keyboard.png b/firetv/store/shots/03-keyboard.png deleted file mode 100644 index 2387b39..0000000 Binary files a/firetv/store/shots/03-keyboard.png and /dev/null differ diff --git a/firetv/store/shots/04-results.png b/firetv/store/shots/04-results.png deleted file mode 100644 index ecc1e4b..0000000 Binary files a/firetv/store/shots/04-results.png and /dev/null differ diff --git a/firetv/store/shots/05-results-nav.png b/firetv/store/shots/05-results-nav.png deleted file mode 100644 index 3cd286d..0000000 Binary files a/firetv/store/shots/05-results-nav.png and /dev/null differ diff --git a/firetv/store/shots/05b-results-right.png b/firetv/store/shots/05b-results-right.png deleted file mode 100644 index 5453af9..0000000 Binary files a/firetv/store/shots/05b-results-right.png and /dev/null differ diff --git a/firetv/store/shots/06-back.png b/firetv/store/shots/06-back.png deleted file mode 100644 index 4b8ea4f..0000000 Binary files a/firetv/store/shots/06-back.png and /dev/null differ diff --git a/firetv/store/shots/07-menu-search.png b/firetv/store/shots/07-menu-search.png deleted file mode 100644 index 86ac169..0000000 Binary files a/firetv/store/shots/07-menu-search.png and /dev/null differ diff --git a/firetv/store/shots/08-menu-search.png b/firetv/store/shots/08-menu-search.png deleted file mode 100644 index 5dc70b8..0000000 Binary files a/firetv/store/shots/08-menu-search.png and /dev/null differ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index daf1a70..1182edb 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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", diff --git a/src/app/opengraph-image.tsx b/src/app/opengraph-image.tsx index a907135..4599d3a 100644 --- a/src/app/opengraph-image.tsx +++ b/src/app/opengraph-image.tsx @@ -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. diff --git a/src/app/page.tsx b/src/app/page.tsx index 2c38771..0c9f740 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -45,10 +45,9 @@ export default async function Home() {

Every podcast with a feed — on any host.

{total.toLocaleString()} 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 networks. 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 networks. Free, no account, + listen in the browser. Only the shows on their own domain?{" "} Self-hosted. Missing one?{" "} Add it.

diff --git a/src/app/tv-preview/page.tsx b/src/app/tv-preview/page.tsx new file mode 100644 index 0000000..bbeeaad --- /dev/null +++ b/src/app/tv-preview/page.tsx @@ -0,0 +1,106 @@ +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 — 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 & { 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 async function TvPreviewPage({ + searchParams, +}: { + searchParams: Promise<{ hero?: string }>; +}) { + // ?hero=1 adds the real hero above the grid, for the first store screenshot. + // The home page cannot be used for one: below its hero it shows the "deep + // catalogues" row, which is real cover art, and two of those shows are the + // ones Amazon named in the rejection. + const hero = (await searchParams).hero === "1"; + 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: 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 ( + <> + {hero && ( +
+
+

Every podcast with a feed — on any host.

+

+ 390,000 live shows, read from their RSS feeds: shows on their own domain, + shows on every hosting platform, and the big library networks. Free, no + account, listen on your TV. +

+
+ + +
+
+
+ )} +
+
+

Browse every show

+
+ {shows.map((p) => ( + + ))} +
+
+
+ + ); +}