<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>cat/airwaves</title>
<subtitle>a desktop tv and program guide for over-the-air broadcasts: what&#39;s on air, what your tuner gets, live tv, recordings</subtitle>
<id>https://src.rest/cat/airwaves/</id>
<link href="https://src.rest/cat/airwaves/"/>
<updated>2026-11-02T10:05:17-07:00</updated><entry>
<id>9d065f2720a3a4e979cb659e5749205ecdf300a1</id>
<title>Ignore macOS Finder files</title>
<link href="https://src.rest/cat/airwaves/commit/9d065f2720a3a4e979cb659e5749205ecdf300a1/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-11-02T10:05:17-07:00</updated>
<content type="text"></content>
</entry>
<entry>
<id>2b8cb8bf4a85460eab914d3e692ddc0d08844d43</id>
<title>Vet and test the module on your machine for src.rest</title>
<link href="https://src.rest/cat/airwaves/commit/2b8cb8bf4a85460eab914d3e692ddc0d08844d43/"/>
<author><name>cat</name></author>
<updated>2026-10-17T16:56:18-06:00</updated>
<content type="text">.forge/ci runs go vet ./... and go test ./.... forge attest runs it in a
clean worktree of each commit and signs the result into refs/notes/ci, and
forge push sends it with the branch. Go&#39;s build and module caches live
outside the worktree, so runs stay warm.</content>
</entry>
<entry>
<id>eb0d5925c0b71bad3e7a6aa5b196779b408ba286</id>
<title>Keep Podcasts still while picking an episode</title>
<link href="https://src.rest/cat/airwaves/commit/eb0d5925c0b71bad3e7a6aa5b196779b408ba286/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-13T18:31:15-06:00</updated>
<content type="text">The column the episode picked and the grid share took the width of the
picked episode&#39;s title and description, set on one line: 1,300 to 2,300
pixels in a 966-pixel area, so the grid of episodes changed its columns
with every pick and ran off the screen. It now keeps to its area, as
Recordings&#39; library does.

The episode picked also takes the same room whichever it is: room for two
lines of title (a longer one ends in &quot;...&quot;), three of description, the
actions and the hint, the words at the bottom by the actions. Its picture
is its column&#39;s width rather than the height of the words, which made it
grow past its column. On a phone the actions stay in one row, scrolling
sideways, as the guide&#39;s do.</content>
</entry>
<entry>
<id>7c7538696d7df66799a95ff8c3c2fe3ab17da0e8</id>
<title>Add a show at once, not behind the channels&#39; catalogs, and refresh Podcasts for the newest</title>
<link href="https://src.rest/cat/airwaves/commit/7c7538696d7df66799a95ff8c3c2fe3ab17da0e8/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-13T18:20:51-06:00</updated>
<content type="text">Adding a show waited for yt-dlp&#39;s one listing job at a time, which the
YouTube channels hold for minutes while they list a whole catalog for their
schedules. Its two minutes ran out while it waited, so it failed the moment
its turn came, and pressing Add again only queued more. Now adding a show
lists it at once, a few seconds&#39; work, as a search does. The background
listings and lookups still take turns, but their time starts when their
turn does, so a busy channel no longer fails them.

Podcasts has Refresh under the shows in the rail (also R), for someone who
wants the newest now rather than at the next listing, every 6 hours while
YouTube&#39;s feeds are down. The server lists every show again, a few at once,
and the app says how many episodes are new and when every show was last
checked.

In Settings, Add says it&#39;s still adding when pressed again, a channel found
says how many subscribers it has as YouTube does (3.39M), and one already a
show says Added. Names from YouTube lose the spaces around them.</content>
</entry>
<entry>
<id>6129c5cc215d1cf96465e8d95f51f0b14a43c950</id>
<title>Know a Galaxy phone as a touch screen, and take a finger&#39;s gestures in any layout</title>
<link href="https://src.rest/cat/airwaves/commit/6129c5cc215d1cf96465e8d95f51f0b14a43c950/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T20:44:00-06:00</updated>
<content type="text">A browser counted as a touch screen only if it had no fine pointer at all. A
Galaxy phone with an S Pen reports one, so it got the desktop layout, where
nothing answered a tap or a swipe on the picture. Now its main pointer
decides. Taps and swipes are also taken from a finger in any layout, as on a
laptop&#39;s touch screen.

On TV a swipe over the banner, or over the timeline, counts as one over the
picture. The browser no longer takes a drag on the banner as a scroll, which
ended the swipe.</content>
</entry>
<entry>
<id>f6cf4475353c7a4ce39af19307372557698e221a</id>
<title>Taps on a phone&#39;s TV: the picture shows above the controls, and a tap off a button puts them away</title>
<link href="https://src.rest/cat/airwaves/commit/f6cf4475353c7a4ce39af19307372557698e221a/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T20:38:35-06:00</updated>
<content type="text">On a phone held sideways the controls covered about 70% of the picture, so a
tap aimed at the picture landed on the banner: it put the controls away but
left the banner up with no timer. Now, on a short touch screen, the controls
draw tighter (what&#39;s next is left to the guide) and cover about half. A tap
anywhere off a button puts the banner away with them, as a phone&#39;s video
players do. The tap is listened for on the picture and the banner themselves,
since an iPhone sends a tap&#39;s click only to what listens for one.

A finger&#39;s stand-in mouse events no longer act a second time. Two quick taps
in a list were also a double click, playing again what the second tap had
already played. A tap&#39;s move was also a pointer brought to the screen.

The first tap unmutes a picture that started muted on the tap&#39;s click, not
when the finger lands, which browsers don&#39;t count as the user&#39;s yet.</content>
</entry>
<entry>
<id>114b5948464065e2b8c9804bfd3d2dd85de51dba</id>
<title>Install the web app on a phone, and touch: a fourth experience beside desktop, console and TV</title>
<link href="https://src.rest/cat/airwaves/commit/114b5948464065e2b8c9804bfd3d2dd85de51dba/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T16:00:32-06:00</updated>
<content type="text">The web app has a manifest, icons rendered from the launcher&#39;s art, and
a service worker that keeps nothing offline but says when the server
can&#39;t be reached, so a phone or a browser can install it from a secure
address such as a Tailscale Service.

Touch is its own platform (body.pf-touch, js/touch.js), found by a
touch screen without a mouse: a tap on TV brings up the controls,
swipes skip and change channel, the guide swipes through the hours,
a second tap watches or plays, and holding a program shows its
actions. Nothing names keys. A phone on its side gets a compact banner
and guide, and the picture-in-picture goes; held upright it asks to be
turned. The tests check that no name is declared in two of the app&#39;s
files, which share one scope.</content>
</entry>
<entry>
<id>b7fa79df4c0b287deb4035649bf617a915722eb4</id>
<title>Give podcasts&#39; episodes their captions, and stop the waiting mark at a stall that plays on</title>
<link href="https://src.rest/cat/airwaves/commit/b7fa79df4c0b287deb4035649bf617a915722eb4/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T15:42:41-06:00</updated>
<content type="text">An episode&#39;s English captions (the uploader&#39;s, else YouTube&#39;s own) are
fetched with where it streams from, ahead for the newest, and go to the
app as a WebVTT rendition timed from where the stream starts: its first
sound, since a copied picture starts at the key frame before.

WebKit calls a player stalled when it has all it needs and stops
fetching, as an on-demand stream does; the waiting mark then stayed up
over a picture that played on. It now shows only while the picture has
stopped, and goes once it moves.</content>
</entry>
<entry>
<id>fec7b36daab98507804a811045f0da73552c3040</id>
<title>Keep library cards whole however many there are</title>
<link href="https://src.rest/cat/airwaves/commit/fec7b36daab98507804a811045f0da73552c3040/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T15:33:08-06:00</updated>
<content type="text">A card clips what it holds, so a long list squeezed each to a sliver to
fit the grid; its rows are now as tall as their cards.</content>
</entry>
<entry>
<id>23c10d9f4c476a2b1d380096a2e4858274e8309c</id>
<title>Read a show&#39;s run-together channel name as words</title>
<link href="https://src.rest/cat/airwaves/commit/23c10d9f4c476a2b1d380096a2e4858274e8309c/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T15:30:33-06:00</updated>
<content type="text"></content>
</entry>
<entry>
<id>189823c3efa08791159c5061d0c08adf0bb595b2</id>
<title>Name a show for its channel when its playlist&#39;s title names none, and wait for a video&#39;s title</title>
<link href="https://src.rest/cat/airwaves/commit/189823c3efa08791159c5061d0c08adf0bb595b2/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T15:29:39-06:00</updated>
<content type="text">&quot;Every Episode&quot; says what a playlist holds, not which show; a premiere
to come is listed without a title until it airs.</content>
</entry>
<entry>
<id>758b5ecc158d5f4e94988fc02b87933acec2e10e</id>
<title>Serve the Podcasts app: its shows, a search for more, and episodes to play</title>
<link href="https://src.rest/cat/airwaves/commit/758b5ecc158d5f4e94988fc02b87933acec2e10e/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T15:26:20-06:00</updated>
<content type="text">/api/podcasts lists the shows and their episodes; shows are found with a
search for YouTube channels, added by link and taken out; an episode
plays like a recording, and how far it was watched is kept. The desktop
app and the web app have the same calls, /api/info says when the
server has the app, and airwavesd keeps the library in podcasts.json.</content>
</entry>
<entry>
<id>6ca783a947264d232cb0a06a1f2be1cb94e76683</id>
<title>Keep a library of video podcasts from YouTube, to watch on demand</title>
<link href="https://src.rest/cat/airwaves/commit/6ca783a947264d232cb0a06a1f2be1cb94e76683/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T15:26:20-06:00</updated>
<content type="text">A show is a channel&#39;s videos or past live streams, or a playlist. Its
newest 30 episodes are kept, with how far each was watched, in
podcasts.json: listed every 6 hours, its channel&#39;s feed read every 30
minutes for new uploads, new videos looked up a few at a time, and
clips, Shorts and streams to come left out. The newest unwatched episode
of each show has where it streams from found ahead.

An episode plays through the stream server from where it&#39;s asked to,
its H.264 picture copied rather than transcoded.</content>
</entry>
<entry>
<id>6af73667de0900ad072a29d7039e5074b2dd8bc2</id>
<title>Take YouTube as a source out of the channels package</title>
<link href="https://src.rest/cat/airwaves/commit/6af73667de0900ad072a29d7039e5074b2dd8bc2/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T15:26:20-06:00</updated>
<content type="text">internal/youtube has yt-dlp (kept up to date), channel and playlist
listings, feeds, search and lookups, and a video&#39;s details with where
to stream it from. The YouTube channels use it as before; the admin
page and airwavesd name it directly. Nothing changes in what they do.</content>
</entry>
<entry>
<id>d0c9c3523d980295d0f4d7e4091cf3740997dd8f</id>
<title>Put Podcasts beside Recordings in the top bar, Weather after them</title>
<link href="https://src.rest/cat/airwaves/commit/d0c9c3523d980295d0f4d7e4091cf3740997dd8f/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T15:17:43-06:00</updated>
<content type="text"></content>
</entry>
<entry>
<id>df7a6ae652ed8fcba8d23a7364f2e6e41bf57ead</id>
<title>Podcasts, an app: episodes to play on demand, and shows to add in Settings</title>
<link href="https://src.rest/cat/airwaves/commit/df7a6ae652ed8fcba8d23a7364f2e6e41bf57ead/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T15:13:14-06:00</updated>
<content type="text">Laid out as Recordings&#39; library is, with Latest, Continue and a list per
show, and played by the recordings&#39; player (now playOnDemand), which
shows positions in the whole episode and saves where it got to. Settings,
Podcasts adds a show by search or address and removes one. The view stays
hidden until the server says it has podcasts.</content>
</entry>
<entry>
<id>a440a82686881f280b7cc3bab902e6070bdf26e8</id>
<title>Split the app by area into js/, in load order</title>
<link href="https://src.rest/cat/airwaves/commit/a440a82686881f280b7cc3bab902e6070bdf26e8/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T15:09:49-06:00</updated>
<content type="text">app.js had grown to 5,000 lines. It&#39;s now ten files by area (core,
player, input, guide, reception, settings, recordings, captions,
playback, weather), loaded in their old order as plain scripts sharing
one scope: still no build step, and nothing changes for the TV, which
loads them from /tv/ as before. The embed test checks the page loads
every file there.</content>
</entry>
<entry>
<id>dc8aac0ecb5a3da68342ad58ff1a00c869ed575f</id>
<title>Lead the server&#39;s own address to the TV app</title>
<link href="https://src.rest/cat/airwaves/commit/dc8aac0ecb5a3da68342ad58ff1a00c869ed575f/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T15:06:43-06:00</updated>
<content type="text">A name for the server alone, such as a Tailscale Service, then opens it.</content>
</entry>
<entry>
<id>cbf3d1504187045f50d48d207adf5bd91480e7d6</id>
<title>Fold Reception into Settings&#39; tabs behind More, and keep desktop, console and TV apart</title>
<link href="https://src.rest/cat/airwaves/commit/cbf3d1504187045f50d48d207adf5bd91480e7d6/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T15:04:19-06:00</updated>
<content type="text">The top bar is TV, Guide, Recordings, Weather and More. More is Settings
with a tab for each area in the rail, Reception among them: Up and Down
pick a tab, OK goes in, Back comes out, as on Android TV.

The app is three experiences, chosen once (platform, body.pf-*): a
desktop&#39;s mouse and keyboard, a console&#39;s controller, a TV&#39;s remote,
with Settings, Display, Layout to change it on a device. What used to
ask whether this was the Android TV app now asks what&#39;s in hand.

While a stream is open the app says so every 30 s, and starts one the
server stopped again. A failed recording puts a count on Recordings.</content>
</entry>
<entry>
<id>09a035da3052c3bbe8805b6fa14c95c413c9cc3d</id>
<title>Say which experience the desktop app is: desktop, or console under gamescope</title>
<link href="https://src.rest/cat/airwaves/commit/09a035da3052c3bbe8805b6fa14c95c413c9cc3d/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T15:04:19-06:00</updated>
<content type="text">AIRWAVES_PLATFORM says otherwise.</content>
</entry>
<entry>
<id>71151f59be443b65522c7e2ccfd70859155e73e2</id>
<title>Let apps change only the hours of listings</title>
<link href="https://src.rest/cat/airwaves/commit/71151f59be443b65522c7e2ccfd70859155e73e2/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T14:57:23-06:00</updated>
<content type="text">PUT /api/config took a whole config, so an app could move the server&#39;s
location; the location is the server&#39;s own, from its flags.</content>
</entry>
<entry>
<id>b15ab0845c3b1b1e2e8f8a00c82e7e8c3e28ddd4</id>
<title>Keep the recordings, measurements and config through a crash or a mistake</title>
<link href="https://src.rest/cat/airwaves/commit/b15ab0845c3b1b1e2e8f8a00c82e7e8c3e28ddd4/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T14:57:23-06:00</updated>
<content type="text">One way of saving the server&#39;s records (store.WriteFile): atomically and
synced, the file replaced kept as .bak, and the first save of a day keeps
the file as it was in backups/, a week of them. A file that can&#39;t be read
is set aside as .corrupt-&lt;time&gt; and the newest readable backup read and
put back, instead of starting empty; a config with no good backup leaves
the defaults rather than a server that won&#39;t start.</content>
</entry>
<entry>
<id>05a14fcd3a6fe9ee28890acb5ad176b1d7a14a34</id>
<title>Stop streams nobody is watching, and let a paused player say it still is</title>
<link href="https://src.rest/cat/airwaves/commit/05a14fcd3a6fe9ee28890acb5ad176b1d7a14a34/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T14:57:23-06:00</updated>
<content type="text">A stream unfetched and untouched for two minutes is stopped, freeing its
tuner when an app quits or a TV is switched off without saying so. A
player holding one paused calls POST /api/heartbeat (Heartbeat in the
desktop app and web.js), which keeps it and says whether it&#39;s still
there.</content>
</entry>
<entry>
<id>83eee8152f368132ee417a80bcbf5c20cd1c7513</id>
<title>Say a repeat&#39;s year once: its first aired date has it</title>
<link href="https://src.rest/cat/airwaves/commit/83eee8152f368132ee417a80bcbf5c20cd1c7513/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T14:55:04-06:00</updated>
<content type="text"></content>
</entry>
<entry>
<id>e268f0f0c355959355ff9ca4f6dc8fff71299eea</id>
<title>Catch a late channel up by cutting the credits, not the cold open</title>
<link href="https://src.rest/cat/airwaves/commit/e268f0f0c355959355ff9ca4f6dc8fff71299eea/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T14:55:04-06:00</updated>
<content type="text">A scheduled channel that fell more than a few seconds behind joined its
next airing part way in, losing however much it was late of the start.
Up to two minutes late, the next airing now plays from its top and loses
that much of its end instead, as a broadcaster running long would.</content>
</entry>
<entry>
<id>2bfb8c61c376ae5f166359c42abb93f088bf5325</id>
<title>Give Jellyfin and other XMLTV readers everything the guide knows</title>
<link href="https://src.rest/cat/airwaves/commit/2bfb8c61c376ae5f166359c42abb93f088bf5325/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T14:38:41-06:00</updated>
<content type="text">One conversion for antenna and library channels: cast with roles, first
aired date or year, type words players file by (Movie, Series, Sports,
News, Children) ahead of genres, running time, an absolute picture for
antenna programs too, xmltv_ns, onscreen and dd_progid episode numbers
(made up for library programs, so each episode is its own), HD, repeats
with when they first aired, premieres, new, live, the rating with its
system, stars and a series ID.</content>
</entry>
<entry>
<id>a2588d9a50700d8c33a35bbe8356f87f04d6af32</id>
<title>Be a better Schedules Direct client, and take a subscription&#39;s pictures</title>
<link href="https://src.rest/cat/airwaves/commit/a2588d9a50700d8c33a35bbe8356f87f04d6af32/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T14:26:45-06:00</updated>
<content type="text">From a review against what got other clients banned: a 4003 on anything
but a login is an old token (as Schedules Direct sometimes says), so it
gets one new login rather than stopping the account; pictures are
fetched from where they redirect without the token going along to S3;
programs are asked for 2000 at a time rather than the 5000 allowed; and
up to 2000 pictures an update, of a subscriber&#39;s 5000 a day. Saving the
account again lifts a picture pause, as a subscription lifts a trial&#39;s
cap.</content>
</entry>
<entry>
<id>9633ca2e7b6a5c1e87223a037d485fc6a0029978</id>
<title>Record opens to this one, every episode or new only</title>
<link href="https://src.rest/cat/airwaves/commit/9633ca2e7b6a5c1e87223a037d485fc6a0029978/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T14:25:00-06:00</updated>
<content type="text">The guide&#39;s actions had three record buttons side by side. One Record
now opens, in its place, to the three choices, the first focused; Esc
or another program folds them back. A program without series
information keeps the plain Record. R, Shift R and N are as before.</content>
</entry>
<entry>
<id>b1832427144bb0a1813f8fc378b73fa1c9b6e867</id>
<title>Describe Jellyfin channels&#39; programs as fully as the antenna&#39;s</title>
<link href="https://src.rest/cat/airwaves/commit/b1832427144bb0a1813f8fc378b73fa1c9b6e867/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T14:15:39-06:00</updated>
<content type="text">The Jellyfin client now asks for each video&#39;s people, content rating and
genres too, so the guide shows a Jellyfin channel&#39;s programs as it does
Schedules Direct&#39;s: the cast with their characters, the rating (R,
TV-14), genres, when it first aired, and up to 1000 characters of the
overview rather than 300. The community rating is left out: it&#39;s on a
10-point scale, not the guide&#39;s 4 stars.</content>
</entry>
<entry>
<id>8e08183a288c2d63c3b0c35dd9f26e3675166dde</id>
<title>Show Schedules Direct&#39;s pictures, and the guide&#39;s picture whenever there&#39;s room</title>
<link href="https://src.rest/cat/airwaves/commit/8e08183a288c2d63c3b0c35dd9f26e3675166dde/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-10-12T14:12:16-06:00</updated>
<content type="text">Schedules Direct&#39;s pictures are behind the token and capped daily, so
the server keeps them: for each series or movie aired, one landscape
picture, without the title written on it where there&#39;s one, near 720
wide, downloaded once into the listings&#39; folder and served at
/guide-art/ (on the API and the emulated HDHomeRun, whose XMLTV now
gives absolute URLs). Up to 500 a day, the shows on soonest first; the
daily cap pauses them until midnight UTC, a picture Schedules Direct
hasn&#39;t got is never asked for again, and a key without any is looked up
again after a week. Listings kept before pictures were fetch them at the
next start.

The guide hid the program&#39;s picture whenever the interface size made
its top tight, on a wide window as much as a TV; it now goes only when
the window is too narrow for it.</content>
</entry>
</feed>
