mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 14:27:27 +00:00
bbs.profullstack.com launch kit: provisioner, custom domains, email verify, ascii-live
- setup.sh: idempotent one-shot droplet provisioner — agentbbs on :22 (admin OpenSSH moved to :2202), rootless podman, Caddy front end for https://bbs.profullstack.com with tilde-style /~user homepages - internal/sites + domain@ SSH route: self-service custom domains (ssh domain@host add example.com) backed by a symlink farm and an on-demand-TLS ask endpoint so Caddy only issues certs for mapped hosts - internal/mail + join@ email verification: optional email at signup, confirmation link served by a loopback /verify endpoint behind Caddy - internal/source + cmd/ascii-live: live video → terminal ASCII groundwork (docs/ascii-live.md) - store: additive sqlite migrations (email/verify columns, domains table) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9b0f465946
commit
f3f8e70996
14 changed files with 2005 additions and 27 deletions
107
cmd/ascii-live/main.go
Normal file
107
cmd/ascii-live/main.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// Command ascii-live previews a live video source as truecolor ASCII in the
|
||||
// local terminal — the Phase 0 deliverable of docs/ascii-live.md. It shares
|
||||
// the source adapter (internal/source) and renderer (internal/ascii) with the
|
||||
// AgentBBS SSH route, so what you see here is what tv-<slug>@ will serve.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// ascii-live watch <url> [--fps N] [--width COLS]
|
||||
//
|
||||
// ascii-live watch "https://youtube.com/live/<id>"
|
||||
// ascii-live watch "https://example.com/stream.m3u8"
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/profullstack/agentbbs/internal/ascii"
|
||||
"github.com/profullstack/agentbbs/internal/source"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 || os.Args[1] != "watch" {
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
fs := flag.NewFlagSet("watch", flag.ExitOnError)
|
||||
fps := fs.Int("fps", 10, "frames per second")
|
||||
width := fs.Int("width", 120, "max width in terminal columns")
|
||||
_ = fs.Parse(os.Args[2:])
|
||||
|
||||
if fs.NArg() < 1 {
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
url := fs.Arg(0)
|
||||
|
||||
if err := run(url, *fps, *width); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "ascii-live: "+err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, "usage: ascii-live watch <url> [--fps N] [--width COLS]")
|
||||
}
|
||||
|
||||
func run(url string, fps, maxWidth int) error {
|
||||
// Lock geometry at start (the decoder output width is fixed for the run,
|
||||
// matching how internal/calls locks pw at join time).
|
||||
cols, rows := terminalSize()
|
||||
if maxWidth > 0 && maxWidth < cols {
|
||||
cols = maxWidth
|
||||
}
|
||||
pw, ph := ascii.FitEven(cols, rows)
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
w, err := source.Start(ctx, source.Options{
|
||||
URL: url, FPS: fps, PW: pw, PH: ph, AllowHLS: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
// Alt screen + hidden cursor; always restored on exit.
|
||||
fmt.Print("\x1b[?1049h\x1b[?25l")
|
||||
defer fmt.Print("\x1b[?25h\x1b[?1049l")
|
||||
|
||||
status := fmt.Sprintf("starting %s…", w.Kind)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case s, ok := <-w.Status:
|
||||
if ok {
|
||||
status = s
|
||||
}
|
||||
case buf, ok := <-w.Frames:
|
||||
if !ok {
|
||||
return nil // stream ended
|
||||
}
|
||||
frameH := (len(buf) / 3) / pw
|
||||
// Home the cursor and repaint in place.
|
||||
fmt.Print("\x1b[H" + ascii.FrameRGB(buf, pw, frameH) +
|
||||
"\r\n\x1b[2K " + url + " · " + w.Kind.String() + " · " + status + " · ctrl-c to quit")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// terminalSize returns the stdout terminal dimensions, or a sane default when
|
||||
// stdout is not a TTY (e.g. piped output).
|
||||
func terminalSize() (cols, rows int) {
|
||||
if c, r, err := term.GetSize(int(os.Stdout.Fd())); err == nil && c > 0 && r > 0 {
|
||||
return c, r
|
||||
}
|
||||
return 120, 40
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue