Release v1.0.0: server switching, Walker launcher, curl install, user guide
- Switch servers from the browser (s) with auto-discovery; auto-fallback to discovery when a saved server is unreachable, so you're never locked to a rebuilt box. cliamp's Jellyfin provider is re-synced on every (re)login. - Ignore late LAN-discovery results once off the login screen (footer/input clobber fix). - install.sh: curl|bash one-liner that downloads the per-arch release binary, still builds from a clone when Go is present; adds a floating Walker entry (TUI.float app-id) + icon on Omarchy desktops. - Docs: USER_GUIDE.md plain-English walkthrough; README install one-liner, accurate keybindings, features section. - Bump version to 1.0.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3c64f13b92
commit
b3bf49df05
8 changed files with 435 additions and 54 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,4 +1,4 @@
|
|||
/dist/
|
||||
/tuiplay
|
||||
/tsplay
|
||||
build.log
|
||||
*.sock
|
||||
|
|
|
|||
67
README.md
67
README.md
|
|
@ -5,8 +5,13 @@ the terminal; the selected title plays in an **mpv window** (hardware
|
|||
accelerated) while the TUI acts as the remote — pause, seek, and a live progress
|
||||
bar, with resume points reported back to the server.
|
||||
|
||||
It's the watch-side companion to [TUISTREAM](../TUISTREAM) (which sets up and
|
||||
manages the Jellyfin *server*). tsplay only *plays*.
|
||||
It's the watch-side companion to
|
||||
[TUISTREAM](https://github.com/28allday/TUISTREAM) (which sets up and manages the
|
||||
Jellyfin *server*). tsplay only *plays*.
|
||||
|
||||
> **New here?** The [User Guide](USER_GUIDE.md) is a plain-English, step-by-step
|
||||
> walkthrough of installing, signing in, and watching. This README is the
|
||||
> technical overview.
|
||||
|
||||
## How it works
|
||||
|
||||
|
|
@ -45,12 +50,28 @@ with software decoding (higher CPU use).
|
|||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
./install.sh
|
||||
One line, nothing to clone:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://raw.githubusercontent.com/28allday/tsplay/main/install.sh | bash
|
||||
```
|
||||
|
||||
Installs missing deps (Arch), builds `~/.local/bin/tsplay`, then just run
|
||||
`tsplay`.
|
||||
This installs `mpv` if it's missing (Arch), downloads the right binary for your
|
||||
architecture into `~/.local/bin`, then you just run `tsplay`. Pin a version with
|
||||
`TSPLAY_VERSION=v1.0.0`, or change the prefix with `PREFIX=/opt`.
|
||||
|
||||
From a clone it builds from source instead (if Go is present), otherwise it falls
|
||||
back to downloading the released binary:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/28allday/tsplay.git
|
||||
cd tsplay && ./install.sh
|
||||
```
|
||||
|
||||
On an **Omarchy desktop** the installer also adds a Walker entry (with icon) that
|
||||
launches tsplay as a floating terminal window — search **`tsplay`** in Walker —
|
||||
using the stock `TUI.float` app-id, so no Hyprland config is needed. On headless
|
||||
boxes this step is skipped and tsplay stays a plain shell TUI.
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
@ -63,6 +84,14 @@ subsequent runs go straight to the library.
|
|||
> Discovery probes both broadcast **and** a unicast sweep of your subnet,
|
||||
> because many home networks (especially WiFi) silently drop broadcast packets.
|
||||
|
||||
**Switching servers.** Once signed in, tsplay locks onto that server and goes
|
||||
straight to the library on every later run — it won't ask again. If you move to
|
||||
a different Jellyfin box, press **`s`** in the browser to drop back to discovery,
|
||||
pick the new server, and sign in; the new one is then saved and locked in. If a
|
||||
saved server has gone away (e.g. you rebuilt it elsewhere), tsplay notices the
|
||||
failed connection on startup and falls back to discovery automatically so you're
|
||||
never stuck pointing at a dead server.
|
||||
|
||||
### Login
|
||||
|
||||
| Key | Action |
|
||||
|
|
@ -71,7 +100,7 @@ subsequent runs go straight to the library.
|
|||
| `ctrl+n` | next discovered server (if more than one)|
|
||||
| `ctrl+r` | rescan the network |
|
||||
| `enter` | sign in |
|
||||
| `esc` | quit |
|
||||
| `esc` | cancel — back to the library, or quit if not yet signed in |
|
||||
|
||||
### Browse
|
||||
|
||||
|
|
@ -81,6 +110,7 @@ subsequent runs go straight to the library.
|
|||
| `enter` / `l` / `→`| open folder · play item |
|
||||
| `esc` / `←` / `h` | back |
|
||||
| `c` | jump to Continue Watching |
|
||||
| `s` | switch server (back to discovery)|
|
||||
| `/` | filter the current list |
|
||||
| `q` | quit |
|
||||
|
||||
|
|
@ -91,21 +121,24 @@ subsequent runs go straight to the library.
|
|||
| `space` / `k` | play / pause |
|
||||
| `←` / `→` | seek ∓10s |
|
||||
| `shift`+`←`/`→`| seek ∓60s |
|
||||
| `n` / `p` | next / previous track (music) |
|
||||
| `↑` / `↓` | volume ±5 |
|
||||
| `m` | mute / unmute |
|
||||
| `q` / `esc` | stop, back to list|
|
||||
|
||||
## Status
|
||||
## Features
|
||||
|
||||
v0.1 — LAN auto-discovery, movies/shows (season/episode drill-down)/music browse
|
||||
+ direct-play, resume on start, progress/stop reporting, volume control with
|
||||
remembered level, and **audio auto-advance** (music plays through the album/
|
||||
folder, then returns to the browser). The UI uses a shared header/footer frame
|
||||
and a compact one-line list (media-type icon, title, and a right-aligned year ·
|
||||
duration · watched column).
|
||||
|
||||
Roadmap: search, subtitle/audio-track selection, quick-connect login, video
|
||||
"play next episode".
|
||||
- **LAN auto-discovery** of Jellyfin servers, with one-key **server switching**
|
||||
and automatic fallback if a saved server goes away.
|
||||
- Browse **movies, shows** (season/episode drill-down) **and music**, with a
|
||||
compact one-line list (media-type icon, title, and a right-aligned year ·
|
||||
duration · watched column) and `/` to filter.
|
||||
- **Direct-play** to mpv (no server-side transcoding), **resume from your last
|
||||
position**, and progress/stop reporting so **Continue Watching** stays in sync
|
||||
across all your devices.
|
||||
- **Audio auto-advance** — music plays through the album/folder and returns to
|
||||
the browser when it's done — with volume remembered across tracks and runs.
|
||||
- Colours follow the active **Omarchy** theme, with an ANSI fallback over SSH.
|
||||
|
||||
## Notes
|
||||
|
||||
|
|
|
|||
209
USER_GUIDE.md
Normal file
209
USER_GUIDE.md
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
# tsplay — User Guide
|
||||
|
||||
tsplay lets you watch everything on your Jellyfin server straight from the
|
||||
terminal. You browse your libraries in a tidy text interface; when you pick
|
||||
something, it opens in a proper **mpv** video window (hardware-accelerated, full
|
||||
quality) while the terminal becomes the remote control — pause, seek, volume,
|
||||
and a live progress bar. Where you stop is remembered on the server, so you can
|
||||
pick up later from any device.
|
||||
|
||||
This guide is for everyday use. If you want the technical details, see the
|
||||
[README](README.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. Installing
|
||||
|
||||
On an **Omarchy / Arch** machine, one line does everything:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/28allday/tsplay/main/install.sh | bash
|
||||
```
|
||||
|
||||
The installer:
|
||||
|
||||
- Installs the only thing it needs — **mpv** — if it isn't already present.
|
||||
- Downloads the `tsplay` program and puts it in `~/.local/bin`.
|
||||
- On an **Omarchy desktop**, it also adds tsplay to the **Walker** launcher
|
||||
(with an icon), so you can start it like any other app.
|
||||
|
||||
That's it — nothing else to set up.
|
||||
|
||||
> Prefer to build it yourself? Clone the repo and run the same script — with Go
|
||||
> installed it builds from source:
|
||||
>
|
||||
> ```bash
|
||||
> git clone https://github.com/28allday/tsplay.git
|
||||
> cd tsplay && ./install.sh
|
||||
> ```
|
||||
|
||||
> **On a non-Arch system?** The installer won't install packages for you; it
|
||||
> will just tell you to install `mpv` first, then re-run it.
|
||||
|
||||
---
|
||||
|
||||
## 2. Starting tsplay
|
||||
|
||||
Three ways, pick whichever suits you:
|
||||
|
||||
- **Walker (Omarchy desktop):** press your launcher key (usually `SUPER`), type
|
||||
**`tsplay`**, and hit enter. It opens in a floating terminal window.
|
||||
- **Terminal:** just type `tsplay`.
|
||||
- **Over SSH** (e.g. on a headless box): type `tsplay`. The video window then
|
||||
appears wherever mpv can open one; on a pure server with no screen, tsplay is
|
||||
mainly used for music.
|
||||
|
||||
---
|
||||
|
||||
## 3. Signing in (first run)
|
||||
|
||||
The first time you run tsplay you'll see a short login form.
|
||||
|
||||
tsplay **automatically finds Jellyfin servers on your network**, so the server
|
||||
address is usually filled in for you. You normally only need to type your
|
||||
**username** and **password**, then press `enter`.
|
||||
|
||||
| Key | What it does |
|
||||
|----------|------------------------------------------------|
|
||||
| `tab` | Move between the boxes (server / username / password) |
|
||||
| `ctrl+n` | Pick the next server, if more than one was found |
|
||||
| `ctrl+r` | Scan the network again |
|
||||
| `enter` | Sign in |
|
||||
| `esc` | Quit |
|
||||
|
||||
A few notes:
|
||||
|
||||
- If nothing is found automatically, just type the address yourself, e.g.
|
||||
`https://media.example.com` or `http://192.168.1.50:8096`.
|
||||
- Your login is **saved securely** on your own machine, so you won't be asked
|
||||
again — every later run goes straight to your library.
|
||||
- If you don't see your server, make sure tsplay and the server are on the **same
|
||||
network**, then press `ctrl+r` to rescan.
|
||||
|
||||
---
|
||||
|
||||
## 4. Finding and playing something
|
||||
|
||||
Once you're in, you'll see your libraries (Movies, Shows, Music, …). Move around
|
||||
with the arrow keys and open things with `enter`.
|
||||
|
||||
| Key | What it does |
|
||||
|-------------------------|---------------------------------------|
|
||||
| `↑` / `↓` (or `j` / `k`)| Move up and down the list |
|
||||
| `enter` (or `→`) | Open a folder, **or play** a title |
|
||||
| `esc` (or `←`) | Go back one level |
|
||||
| `/` | Search/filter the list you're looking at |
|
||||
| `c` | Jump to **Continue Watching** |
|
||||
| `s` | Switch to a different server |
|
||||
| `q` | Quit tsplay |
|
||||
|
||||
- Drill down naturally: **Shows → a series → a season → an episode**, then
|
||||
`enter` to play.
|
||||
- Press `/` to filter a long list — start typing and the list narrows; press
|
||||
`esc` to clear the filter.
|
||||
|
||||
### Continue Watching
|
||||
|
||||
Press **`c`** anywhere in the browser to see everything you started but didn't
|
||||
finish, newest first. Pick one and it **resumes from where you left off** — no
|
||||
need to scrub to the right spot. This is the same "Continue Watching" you see in
|
||||
the Jellyfin app, kept in sync with your phone, TV, and browser.
|
||||
|
||||
---
|
||||
|
||||
## 5. While something is playing
|
||||
|
||||
The video plays in its own mpv window. Keep the **terminal focused** to use these
|
||||
controls (you'll also see a live progress bar and the time):
|
||||
|
||||
| Key | What it does |
|
||||
|-----------------|-------------------------|
|
||||
| `space` (or `k`)| Play / pause |
|
||||
| `←` / `→` | Skip back / forward 10s |
|
||||
| `shift`+`←`/`→` | Skip back / forward 60s |
|
||||
| `↑` / `↓` | Volume up / down |
|
||||
| `m` | Mute / unmute |
|
||||
| `n` / `p` | Next / previous track (music only) |
|
||||
| `q` (or `esc`) | Stop and return to the library |
|
||||
|
||||
Your position is reported back to the server as you watch, so it shows up in
|
||||
**Continue Watching** next time — and disappears once you finish.
|
||||
|
||||
---
|
||||
|
||||
## 6. Music
|
||||
|
||||
Music works just like video, but with no black window — the audio simply plays in
|
||||
the background while the terminal shows what's on.
|
||||
|
||||
- Play an album or a track and it keeps going: when one track ends, tsplay
|
||||
**automatically moves to the next** one in the album/folder, then returns to
|
||||
the browser when the album finishes.
|
||||
- Use `n` and `p` to jump between tracks.
|
||||
- Your last volume is remembered between tracks and between runs.
|
||||
|
||||
---
|
||||
|
||||
## 7. Switching to a different server
|
||||
|
||||
If you set up a new Jellyfin box (or moved house and rebuilt it elsewhere), you
|
||||
don't need to reinstall or wipe anything:
|
||||
|
||||
- Press **`s`** while browsing. tsplay drops back to the network scan, finds the
|
||||
new server, and lets you sign in. From then on it locks onto the new one and
|
||||
goes straight there on every run.
|
||||
- If your old server has simply **gone away**, tsplay notices it can't connect
|
||||
when it starts and **takes you to the scan automatically** — so you're never
|
||||
stuck staring at a server that no longer exists.
|
||||
|
||||
(If you have the **cliamp** music player installed, switching servers updates its
|
||||
Jellyfin settings too, so your music stays in sync.)
|
||||
|
||||
---
|
||||
|
||||
## 8. Troubleshooting
|
||||
|
||||
**"No servers found" on the login screen.**
|
||||
Make sure your computer and the Jellyfin server are on the same Wi-Fi/network,
|
||||
then press `ctrl+r` to scan again. You can always type the address by hand —
|
||||
e.g. `http://192.168.1.50:8096`.
|
||||
|
||||
**"Login failed".**
|
||||
Double-check the username and password. If the server address is wrong, press
|
||||
`tab` back up to it and correct it (or `ctrl+r` to rediscover).
|
||||
|
||||
**It can't connect / the library won't load.**
|
||||
tsplay will usually send you back to the server scan on its own. If you're typing
|
||||
the address, prefer a plain `http://…:8096` address on your home network, or a
|
||||
proper `https://` address with a real certificate. Self-signed certificates are
|
||||
**not** trusted.
|
||||
|
||||
**Video plays but is choppy / uses lots of CPU.**
|
||||
That's the graphics drivers, not tsplay. On a normal Omarchy desktop this is
|
||||
already sorted. On a brand-new, minimal install you may need your GPU's
|
||||
video-decode drivers (the [README](README.md#gpu-drivers-hardware-accelerated-playback)
|
||||
lists them per graphics card).
|
||||
|
||||
**I want to start over / log in as someone else.**
|
||||
Delete the saved settings and relaunch:
|
||||
|
||||
```bash
|
||||
rm ~/.config/tsplay/config.json
|
||||
tsplay
|
||||
```
|
||||
|
||||
You'll get the fresh login screen again.
|
||||
|
||||
---
|
||||
|
||||
## 9. Quick reference
|
||||
|
||||
```
|
||||
Browsing ↑/↓ move · enter open/play · esc back · / search
|
||||
c continue watching · s switch server · q quit
|
||||
|
||||
Playing space pause · ←/→ ±10s · shift+←/→ ±60s
|
||||
↑/↓ volume · m mute · n/p track (music) · q stop
|
||||
```
|
||||
|
||||
Happy watching.
|
||||
150
install.sh
150
install.sh
|
|
@ -1,56 +1,146 @@
|
|||
#!/usr/bin/env bash
|
||||
# tsplay installer — builds the terminal Jellyfin player from source and drops
|
||||
# the binary in ~/.local/bin. On Arch/Omarchy it installs the dependencies it
|
||||
# needs (mpv at run-time, go at build-time) via pacman; on other distros it
|
||||
# falls back to telling you what to install.
|
||||
#
|
||||
# install.sh — install tsplay, a terminal Jellyfin player for Omarchy / Arch.
|
||||
#
|
||||
# Quick install (nothing to clone):
|
||||
#
|
||||
# curl -fsSL https://raw.githubusercontent.com/28allday/tsplay/main/install.sh | bash
|
||||
#
|
||||
# When run from a git clone it builds from source instead (if Go is present),
|
||||
# otherwise it downloads the latest released binary for your architecture.
|
||||
#
|
||||
# ./install.sh
|
||||
#
|
||||
# Environment overrides:
|
||||
# PREFIX=/somewhere install prefix (default ~/.local → ~/.local/bin)
|
||||
# TSPLAY_VERSION=v1.0.0 pin a release (default: latest)
|
||||
#
|
||||
# tsplay needs mpv at run time; the installer fetches it via pacman on Arch if
|
||||
# it's missing. On an Omarchy desktop it also adds a floating Walker entry.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BIN_DIR="${HOME}/.local/bin"
|
||||
BIN="${BIN_DIR}/tsplay"
|
||||
REPO="28allday/tsplay"
|
||||
NAME="tsplay"
|
||||
PREFIX="${PREFIX:-$HOME/.local}"
|
||||
BIN_DIR="$PREFIX/bin"
|
||||
BIN="$BIN_DIR/$NAME"
|
||||
APP_DIR="$HOME/.local/share/applications"
|
||||
VERSION="${TSPLAY_VERSION:-latest}"
|
||||
|
||||
bold() { printf '\033[1m%s\033[0m\n' "$1"; }
|
||||
warn() { printf '\033[33m%s\033[0m\n' "$1" >&2; }
|
||||
err() { printf '\033[31m%s\033[0m\n' "$1" >&2; }
|
||||
|
||||
# pkg_for maps a required command to its Arch package name (same here, but kept
|
||||
# explicit so it's obvious what gets installed).
|
||||
pkg_for() {
|
||||
case "$1" in
|
||||
go) echo "go" ;;
|
||||
mpv) echo "mpv" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
|
||||
|
||||
# ensure_dep makes sure a command exists, installing it via pacman on Arch if
|
||||
# missing. $2 = "build" or "run" just tweaks the wording.
|
||||
# missing. mpv is tsplay's only run-time dependency.
|
||||
ensure_dep() {
|
||||
local cmd="$1" kind="$2" pkg
|
||||
local cmd="$1" pkg="$2"
|
||||
command -v "$cmd" >/dev/null 2>&1 && return 0
|
||||
pkg="$(pkg_for "$cmd")"
|
||||
if command -v pacman >/dev/null 2>&1; then
|
||||
bold "Installing missing ${kind} dependency: ${pkg}"
|
||||
bold "Installing missing dependency: ${pkg}"
|
||||
sudo pacman -S --needed --noconfirm "$pkg"
|
||||
else
|
||||
err "Missing ${kind} dependency '${cmd}'. Install the '${pkg}' package and re-run."
|
||||
err "Missing dependency '${cmd}'. Install the '${pkg}' package and re-run."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Build-time and run-time deps. mpv is the only thing tsplay needs at run time;
|
||||
# everything else is compiled into the Go binary.
|
||||
ensure_dep go build
|
||||
ensure_dep mpv run
|
||||
# mpv is required however we obtain the binary.
|
||||
ensure_dep mpv mpv
|
||||
|
||||
bold "Building tsplay…"
|
||||
cd "$REPO_DIR"
|
||||
mkdir -p "$BIN_DIR"
|
||||
go build -o "$BIN" ./cmd/tsplay
|
||||
|
||||
# If the script lives next to the source tree, we're in a clone.
|
||||
SCRIPT_DIR=""
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ] && [ -f "${BASH_SOURCE[0]}" ]; then
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
fi
|
||||
|
||||
# Detect OS/arch for prebuilt asset names.
|
||||
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
case "$(uname -m)" in
|
||||
x86_64 | amd64) arch=amd64 ;;
|
||||
aarch64 | arm64) arch=arm64 ;;
|
||||
*) arch="" ;;
|
||||
esac
|
||||
|
||||
# ---- obtain the binary ----------------------------------------------------
|
||||
TMPBIN=""
|
||||
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/go.mod" ] && command -v go >/dev/null 2>&1; then
|
||||
bold "Building $NAME from source…"
|
||||
TMPBIN="$(mktemp)"
|
||||
trap 'rm -f "$TMPBIN"' EXIT
|
||||
( cd "$SCRIPT_DIR" && CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' \
|
||||
-o "$TMPBIN" "./cmd/$NAME" )
|
||||
elif [ -n "$SCRIPT_DIR" ] && [ -n "$arch" ] && [ -x "$SCRIPT_DIR/dist/${NAME}-${os}-${arch}" ]; then
|
||||
log "Using prebuilt binary from dist/"
|
||||
TMPBIN="$SCRIPT_DIR/dist/${NAME}-${os}-${arch}"
|
||||
else
|
||||
# Download the released binary for this OS/arch (curl-style install).
|
||||
[ "$os" = "linux" ] || err "tsplay ships Linux binaries only (detected: $os). Clone the repo and build with Go."
|
||||
[ "$os" = "linux" ] || exit 1
|
||||
[ -n "$arch" ] || { err "unsupported architecture: $(uname -m)"; exit 1; }
|
||||
asset="${NAME}-${os}-${arch}"
|
||||
if [ "$VERSION" = "latest" ]; then
|
||||
url="https://github.com/$REPO/releases/latest/download/$asset"
|
||||
else
|
||||
url="https://github.com/$REPO/releases/download/$VERSION/$asset"
|
||||
fi
|
||||
log "Downloading $asset ($VERSION)…"
|
||||
TMPBIN="$(mktemp)"
|
||||
trap 'rm -f "$TMPBIN"' EXIT
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fSL --proto '=https' --tlsv1.2 -o "$TMPBIN" "$url"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -qO "$TMPBIN" "$url"
|
||||
else
|
||||
err "need curl or wget to download the binary."; exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- install --------------------------------------------------------------
|
||||
install -Dm755 "$TMPBIN" "$BIN"
|
||||
bold "Installed: $BIN"
|
||||
|
||||
# ---- Omarchy desktop integration -----------------------------------------
|
||||
# Only on Omarchy: add a Walker entry that opens tsplay in a floating window.
|
||||
# The TUI.float app-id is matched by Omarchy's stock floating-window rule, so
|
||||
# no Hyprland configuration is required (same approach as omarchy-send).
|
||||
if command -v omarchy-launch-tui >/dev/null 2>&1 || [ -d "$HOME/.local/share/omarchy" ]; then
|
||||
mkdir -p "$APP_DIR"
|
||||
|
||||
# Install the bundled icon into the user's hicolor theme — a play triangle
|
||||
# over a progress bar, in terminal-accent blue.
|
||||
icon_dir="$HOME/.local/share/icons/hicolor/scalable/apps"
|
||||
mkdir -p "$icon_dir"
|
||||
cat > "$icon_dir/tsplay.svg" <<'SVG'
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
|
||||
<rect width="256" height="256" rx="56" fill="#16161e"/>
|
||||
<path d="M104 78 L104 158 L172 118 Z" fill="#7aa2f7"/>
|
||||
<rect x="64" y="186" width="128" height="10" rx="5" fill="#2a2e42"/>
|
||||
<rect x="64" y="186" width="80" height="10" rx="5" fill="#7aa2f7"/>
|
||||
</svg>
|
||||
SVG
|
||||
gtk-update-icon-cache -q -t -f "$HOME/.local/share/icons/hicolor" 2>/dev/null || true
|
||||
|
||||
cat > "$APP_DIR/tsplay.desktop" <<EOF
|
||||
[Desktop Entry]
|
||||
Name=tsplay
|
||||
Comment=Terminal Jellyfin player — browse your server and play in mpv
|
||||
Exec=xdg-terminal-exec --app-id=TUI.float -e $BIN
|
||||
Icon=tsplay
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=AudioVideo;Player;Video;Network;
|
||||
Keywords=jellyfin;media;player;stream;mpv;movies;tv;music;
|
||||
EOF
|
||||
bold "Omarchy detected — added floating Walker entry (with icon)."
|
||||
echo " Launch it from Walker by searching 'tsplay'."
|
||||
fi
|
||||
|
||||
case ":$PATH:" in
|
||||
*":$BIN_DIR:"*) ;;
|
||||
*":$BIN_DIR:"*) : ;;
|
||||
*) warn "Note: $BIN_DIR is not on your PATH. Add it to use 'tsplay' directly." ;;
|
||||
esac
|
||||
echo "Run 'tsplay' to sign in to your Jellyfin server (it auto-detects LAN servers)."
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import (
|
|||
)
|
||||
|
||||
// Version is stamped into the Authorization header and User-Agent.
|
||||
const Version = "0.1.0"
|
||||
const Version = "1.0.0"
|
||||
|
||||
// Client talks to one Jellyfin server as one user.
|
||||
type Client struct {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ func (m Model) updateBrowse(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m, nil
|
||||
case "c":
|
||||
return m, m.loadResume()
|
||||
case "s":
|
||||
// Switch to a different Jellyfin server: back to discovery/login.
|
||||
return m.toLogin("")
|
||||
case "enter", "l", "right":
|
||||
sel, ok := cur.SelectedItem().(listItem)
|
||||
if !ok {
|
||||
|
|
@ -62,7 +65,7 @@ func (m Model) viewBrowse() string {
|
|||
m.headerBar(), "", m.st.muted.Render(" Loading libraries…"))
|
||||
}
|
||||
cur := m.stack[len(m.stack)-1]
|
||||
help := "enter play/open · c continue · / filter · esc back · q quit"
|
||||
help := "enter play/open · c continue · / filter · s server · esc back · q quit"
|
||||
return lipgloss.JoinVertical(lipgloss.Left,
|
||||
m.headerBar(),
|
||||
"",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,18 @@ import (
|
|||
func (m Model) updateLogin(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyMsg); ok {
|
||||
switch key.String() {
|
||||
case "ctrl+c", "esc":
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
// If we reached login from an active session (switch server) and the
|
||||
// old browse view is still around, esc cancels and returns to it. On
|
||||
// genuine first-run, or after a failed-server fallback (empty stack),
|
||||
// there's nothing to go back to, so esc quits.
|
||||
if m.client != nil && len(m.stack) > 0 {
|
||||
m.screen = screenBrowse
|
||||
m.status = ""
|
||||
return m, nil
|
||||
}
|
||||
return m, tea.Quit
|
||||
case "ctrl+n":
|
||||
// Cycle through discovered servers, filling the URL field.
|
||||
|
|
|
|||
|
|
@ -79,12 +79,30 @@ func New(cfg *config.Config, t theme.Theme) Model {
|
|||
m.screen = screenBrowse
|
||||
} else {
|
||||
m.screen = screenLogin
|
||||
m.initLogin()
|
||||
m.initLogin(true)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Model) initLogin() {
|
||||
// toLogin returns to the login/discovery screen to (re)connect to a server and
|
||||
// kicks off a fresh LAN scan. The URL field is left blank so discovery can
|
||||
// pre-fill the *new* server; the saved username is kept. status seeds the
|
||||
// footer (empty → the default "scanning…" line). The old browse stack is left
|
||||
// intact so esc can return to it; a successful login replaces it.
|
||||
//
|
||||
// Used both for an explicit "switch server" from the browser and as the
|
||||
// fallback when the saved server can't be reached on startup (e.g. the Jellyfin
|
||||
// box was rebuilt), so the user is never permanently locked to a dead server.
|
||||
func (m Model) toLogin(status string) (Model, tea.Cmd) {
|
||||
m.screen = screenLogin
|
||||
m.initLogin(false)
|
||||
if status != "" {
|
||||
m.status = status
|
||||
}
|
||||
return m, tea.Batch(textinput.Blink, discoverCmd())
|
||||
}
|
||||
|
||||
func (m *Model) initLogin(prefillURL bool) {
|
||||
labels := []string{"Server URL (https://media.example.com)", "Username", "Password"}
|
||||
m.inputs = make([]textinput.Model, 3)
|
||||
for i := range m.inputs {
|
||||
|
|
@ -97,7 +115,7 @@ func (m *Model) initLogin() {
|
|||
}
|
||||
m.inputs[i] = ti
|
||||
}
|
||||
if m.cfg.ServerURL != "" {
|
||||
if prefillURL && m.cfg.ServerURL != "" {
|
||||
m.inputs[0].SetValue(m.cfg.ServerURL)
|
||||
}
|
||||
if m.cfg.Username != "" {
|
||||
|
|
@ -268,6 +286,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m, nil
|
||||
|
||||
case discoveredMsg:
|
||||
// Ignore late scan results that arrive after we've left the login screen
|
||||
// (e.g. the user signed in before the 2s scan finished). The discovered
|
||||
// list and URL pre-fill are only meaningful on the login form, and
|
||||
// applying them on the browser would clobber its footer/inputs.
|
||||
if m.screen != screenLogin {
|
||||
return m, nil
|
||||
}
|
||||
m.discovered = msg.servers
|
||||
m.discIdx = 0
|
||||
if len(msg.servers) == 0 {
|
||||
|
|
@ -296,6 +321,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.cfg.UserID = msg.res.User.ID
|
||||
_ = m.cfg.Save()
|
||||
m.client = api.New(m.cfg.ServerURL, m.cfg.Token, m.cfg.UserID, m.cfg.DeviceID)
|
||||
// Drop any browse stack from a previous server so the new server's
|
||||
// libraries replace it instead of stacking on top.
|
||||
m.stack = nil
|
||||
m.screen = screenBrowse
|
||||
m.status = ""
|
||||
// Best-effort: mirror the Jellyfin credentials into cliamp's config so
|
||||
|
|
@ -314,6 +342,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
|
||||
case levelMsg:
|
||||
if msg.err != nil {
|
||||
// A failed *initial* load (empty stack) means the saved server is
|
||||
// unreachable or our token expired — typically the Jellyfin box was
|
||||
// rebuilt elsewhere. Drop back to discovery so the user can reconnect,
|
||||
// rather than dead-ending on an error footer.
|
||||
if len(m.stack) == 0 {
|
||||
return m.toLogin("Couldn't reach " + m.cfg.ServerURL + " — pick a server to reconnect.")
|
||||
}
|
||||
m.status = "Load failed: " + msg.err.Error()
|
||||
return m, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue