Share an episode as a page, on ipx-play (#447)

Share handed over the raw file address or the item's link: no preview,
no time, nothing a person without iPX could do much with. Now, as
Overcast does, an episode shares as a short link to its own page,
play.ipxreader.app/e/<id>, which anyone can open: the show's art and
name, the episode's title, date, length and notes, a player on the
publisher's own file starting at ?t= if a time was shared, buttons to
subscribe in Apple Podcasts, Overcast, Pocket Casts or Castro, and Open
Graph tags for a preview where the link is pasted.

ipx-play is a service of its own (crates/play), built like the
directory: ipx sends it the episode with its token (POST /v1/shares)
and gets the link back. The id comes from the feed's address and the
guid, so the same episode is one link whoever shares it. Only episodes
of feeds the directory would be sent go there, by the directory's own
rule (snapshot_entries), so the two cannot disagree on what is private;
ipx-play also refuses an address that looks private. Anything else, and
any server without ipx-play set up, shares as before.

In the page, Share on an episode opens a menu once the link is back:
Share link, Share from 12:34 (the player's clock, or where you left
off), Copy link. A menu because the share sheet needs a press of its
own, and the request for the link used up the one that opened it.

The page runs no script: the start time is the media fragment #t=, and
its CSP loads nothing but its stylesheet, the art and the file.

Admin, Directory, Sharing episodes sets the address and token. The
browser suite starts its own ipx-play; it was not run here (no Caddy on
this Mac).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ray Slakinski
2026-11-13 09:24:18 -05:00
parent 171c77475a
commit 679a674fc9
32 changed files with 1595 additions and 24 deletions

View File

@@ -1,7 +1,7 @@
#!/bin/sh
# Build and push one service's image, inside limits Tower can afford.
#
# sh scripts/build-image.sh ipx|directory|web|coverage
# sh scripts/build-image.sh ipx|directory|play|web|coverage
#
# On 2026-11-04 a plain `docker buildx build` of ipx (its dependencies' cache layer invalidated
# by a Cargo.toml change) rebuilt everything in release on all twelve threads with 5 GB free and
@@ -19,10 +19,11 @@ BUILDER=ipx-capped
case "${1:-}" in
ipx) FILE=Dockerfile; IMAGE=ipx ;;
directory) FILE=Dockerfile.directory; IMAGE=ipx-directory ;;
play) FILE=Dockerfile.play; IMAGE=ipx-play ;;
web) FILE=Dockerfile.web; IMAGE=ipx-web ;;
# Not a service: what scripts/coverage.sh runs the Rust tests in (#390).
coverage) FILE=Dockerfile.coverage; IMAGE=ipx-coverage ;;
*) echo "usage: $0 ipx|directory|web|coverage" >&2; exit 2 ;;
*) echo "usage: $0 ipx|directory|play|web|coverage" >&2; exit 2 ;;
esac
cd "$(dirname "$0")/.."

View File

@@ -1,10 +1,10 @@
#!/bin/sh
# Which lines of the Rust the tests reach (#390, #394), inside the limits builds are held to.
#
# sh scripts/coverage.sh the cargo tests, all three crates, then their report
# sh scripts/coverage.sh the cargo tests, all four crates, then their report
# sh scripts/coverage.sh ipx-feed one crate's cargo tests, then the report of what has run
# sh scripts/coverage.sh api the browser suite against an instrumented ipx and
# ipx-directory: what the API and the daemon run (and the
# sh scripts/coverage.sh api the browser suite against an instrumented ipx, ipx-directory
# and ipx-play: what the API and the daemon run (and the
# page's coverage, which the same run measures)
#
# A coverage build instruments every crate, dependencies included, in a target of its own: a whole
@@ -96,7 +96,7 @@ if [ "${1:-}" = api ]; then
# Built as cargo-llvm-cov builds for a test of its own, instrumented, with no tests: the
# binaries the browser suite starts. Profiles from an earlier run are not this one's.
run cargo llvm-cov clean --workspace
for p in ipx-directory ipx; do
for p in ipx-directory ipx-play ipx; do
room
echo "== build $p"
run sh -c "eval \"\$(cargo llvm-cov show-env --export-prefix)\" && cargo build --locked -p $p"
@@ -116,7 +116,7 @@ if [ "${1:-}" = api ]; then
exit "$suite"
fi
crates=${1:-ipx-feed ipx-directory ipx}
crates=${1:-ipx-feed ipx-directory ipx-play ipx}
# A whole run starts from nothing; one crate adds to what has run.
[ $# -eq 0 ] && run cargo llvm-cov clean --workspace
for c in $crates; do

View File

@@ -1,7 +1,7 @@
#!/bin/sh
# Publish a Gitea release for one service's tag, with a tarball taken from the image that was
# deployed, so what is attached is what runs on Tower. Run after the release commit is tagged,
# pushed and its image built and pushed: sh scripts/release.sh ipx|directory|web
# pushed and its image built and pushed: sh scripts/release.sh ipx|directory|play|web
set -eu
cd "$(dirname "$0")/.."
@@ -10,9 +10,11 @@ ipx) name=ipx; ver=$(sed -n 's/^version = "\(.*\)"/\1/p' crates/
tag=v$ver; log=crates/ipx/CHANGELOG.md; from=/usr/local/bin/ipxd ;;
directory) name=ipx-directory; ver=$(sed -n 's/^version = "\(.*\)"/\1/p' crates/directory/Cargo.toml | head -1)
tag=directory-v$ver; log=crates/directory/CHANGELOG.md; from=/usr/local/bin/ipx-directory ;;
play) name=ipx-play; ver=$(sed -n 's/^version = "\(.*\)"/\1/p' crates/play/Cargo.toml | head -1)
tag=play-v$ver; log=crates/play/CHANGELOG.md; from=/usr/local/bin/ipx-play ;;
web) name=ipx-web; ver=$(node -p 'require("./package.json").version')
tag=web-v$ver; log=web/CHANGELOG.md; from=/srv ;;
*) echo "usage: $0 ipx|directory|web" >&2; exit 2 ;;
*) echo "usage: $0 ipx|directory|play|web" >&2; exit 2 ;;
esac
case "$ver" in *-dev) echo "$name is at $ver: release it first" >&2; exit 1 ;; esac