Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ jobs:
- uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
filter: blob:none
- uses: jdx/aube-action@v1
with:
version: '2.5.1'
node-version: '26.5.0'
run-install: false
- uses: pkgxdev/setup@v5
- run: sudo apt-get update && sudo apt-get install -y libasound2-dev libfontconfig1-dev libwayland-dev libxkbcommon-dev libxkbcommon-x11-dev libxcb1-dev libx11-xcb-dev libx11-dev libegl1-mesa-dev libvulkan-dev libssl-dev
- uses: moonrepo/setup-toolchain@v0
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ jobs:
- uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
- uses: jdx/aube-action@v1
with:
version: '2.5.1'
node-version: '26.5.0'
run-install: false
- uses: pkgxdev/setup@v5
- uses: moonrepo/setup-toolchain@v0
with:
Expand Down Expand Up @@ -103,6 +109,12 @@ jobs:
- uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
- uses: jdx/aube-action@v1
with:
version: '2.5.1'
node-version: '26.5.0'
run-install: false
- name: Install host-native LLVM
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down Expand Up @@ -150,6 +162,8 @@ jobs:
Expand-Archive (Join-Path $tools $archive) $tools
(Get-ChildItem $tools -Recurse -Filter moon.exe | Select-Object -First 1).DirectoryName >> $env:GITHUB_PATH
- run: rustup toolchain install 1.98.1 --profile minimal --component clippy,rustfmt --target ${{ matrix.target }}
- name: Bundle pinned YouTube.js source
run: moon exec --ignore-ci-checks youtubejs:bundle
- name: Resolve desktop release version
id: version
shell: pwsh
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
/dist/
/.moon/cache/
/node_modules/
/vendor/bundle/
/vendor/node_modules/

/crates/*/dist/

Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "vendor/youtubejs"]
path = vendor/youtubejs
url = https://github.com/listenbox/YouTube.js.git
1 change: 1 addition & 0 deletions .moon/workspace.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defaultProject: client
projects:
youtubejs: vendor
client: .
cli: crates/cli
client-engine: crates/sync-engine
Expand Down
57 changes: 30 additions & 27 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 36 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,21 @@ A Rust monorepo for the Listenbox desktop app, CLI and shared synchronization en

## Build

Install Rust via rustup, [Moon](https://moonrepo.dev), [pkgx](https://pkgx.sh), [kache 0.27.0](https://github.com/kunobi-ninja/kache/releases/tag/v0.27.0), a C compiler, make and tar. Rust is pinned in `rust-toolchain.toml`. No JavaScript runtime or package manager is needed in this repository.
Install Rust via rustup, [Moon](https://moonrepo.dev), [pkgx](https://pkgx.sh), [kache 0.27.0](https://github.com/kunobi-ninja/kache/releases/tag/v0.27.0), a C compiler, make and tar. Rust is pinned in `rust-toolchain.toml`. Source builds also use Node.js 26 and [Aube](https://aube.sh); shipped applications need neither.

```sh
# Install once per machine, outside a checkout (or use the prebuilt release).
cargo install --locked kache --version 0.27.0

git clone https://github.com/listenbox/client.git
git clone --recurse-submodules https://github.com/listenbox/client.git
cd client
moon run client:build
moon ci
```

The debug app is `crates/desktop/dist/listenbox-desktop`; the terminal executable is `crates/cli/dist/listenbox`. `moon run desktop:build-release` builds the production desktop executable for the host architecture at `crates/desktop/dist/release/listenbox-desktop`. `moon run desktop:dmg` packages its Apple Silicon build as a DMG. `moon run client:package` packages release CLI and desktop binaries with notices.

FFmpeg 9.0.2 is built from verified source by `tools/native-ffmpeg.rs`, linked with `ffmpeg-the-third`, and never run as a subprocess. The physical `youtubei` crate embeds a verified upstream bundle in QuickJS. Both applications are self-contained. See `THIRD-PARTY-NOTICES.txt` and the packaged FFmpeg source/license notice.
FFmpeg 9.0.2 is built from verified source by `tools/native-ffmpeg.rs`, linked with `ffmpeg-the-third`, and never run as a subprocess. The `youtubei` crate embeds our pinned `vendor/youtubejs` source build in QuickJS. Moon installs locked JavaScript build dependencies and creates the bundle before Cargo; Cargo never downloads a prebuilt YouTube.js bundle. Both applications are self-contained. See `THIRD-PARTY-NOTICES.txt` and the packaged FFmpeg source/license notice.

### Moon and Cargo

Expand Down Expand Up @@ -204,6 +204,39 @@ listenbox shows order --show field-notes --episode ep_0123456789abcdef --episode

Supplied episodes move to the front in sequence; other episodes retain their relative order. Sync restores playlist order for imported podcasts.

## YouTube sign-in checks

When YouTube asks you to sign in, open **Settings → YouTube** in the desktop app
(⌘, on macOS or Ctrl+, on Windows). Paste a **Netscape cookies.txt** export and
choose **Save cookies**, then return to the podcast and choose **Sync now**.
The CLI accepts the same format, without requiring a Listenbox login to save it:

```sh
listenbox youtube-cookies import /path/to/cookies.txt
listenbox shows sync youtube --show field-notes
listenbox youtube-cookies remove
```

Follow [the private-session export guide](https://listenbox.app/guides/import-youtube-as-a-podcast/#when-youtube-asks-you-to-sign-in).
Cookies stay in the shared profile's `youtube-cookies/jar.json`. The app never
shows the saved contents. A new import replaces the session; removal returns
future requests to anonymous access. Only YouTube receives these credentials;
media hosts and the Listenbox API do not.

The sync engine reads immutable snapshots without a reader lock. Writers take
an OS file lock, compare the request's generation and per-cookie revisions,
merge unrelated updates, then sync and atomically replace the snapshot. Late
responses cannot overwrite a newer rotation, resurrect a deleted cookie, or
undo a user replacement/removal. Interrupted writes leave the prior complete
snapshot readable. Response cookies are committed before consuming the body.
Unix directories are private (0700) and snapshots are 0600; Windows uses the
user profile's inherited access controls.

Tests use synthetic cookie exports and local YouTube fixtures. The optional
`verify_youtube` example is a manual live probe: it copies an explicitly supplied
export into a temporary profile, checks playlist extraction and media ranges,
and deletes that profile on exit. It never publishes and is not run by CI.

## Interrupted work

Both interfaces use the engine's persistent sync path. It opens `~/.config/listenbox/sync.sqlite` only for sync work and keeps media under `~/.config/listenbox/transfers`. Refinery applies embedded SQL migrations with strict history validation. Neither interface accesses SQLite directly.
Expand Down
26 changes: 25 additions & 1 deletion crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ enum Command {
slug: Option<String>,
source_url: String,
},
/// Import or remove the YouTube session shared with Listenbox desktop
YoutubeCookies {
#[command(subcommand)]
command: CookieCommand,
},
/// Manage shows by slug
Shows {
#[command(subcommand)]
Expand All @@ -50,6 +55,14 @@ enum Command {
},
}

#[derive(Subcommand)]
enum CookieCommand {
/// Replace saved cookies with a Netscape cookies.txt export
Import { file: std::path::PathBuf },
/// Remove the saved session and return to anonymous access
Remove,
}

#[derive(Subcommand)]
enum AuthCommand {
Status,
Expand Down Expand Up @@ -272,6 +285,16 @@ async fn run(cli: Cli, cancel: CancellationToken) -> Result<()> {
let config = config::Config::load(cli.config.as_deref())?;
let mut api = api::Api::new(config, cancel)?;
match cli.command {
Command::YoutubeCookies { command } => {
let jar = listenbox_sync_engine::cookies::CookieJar::new(&api.config.directory);
tokio::task::spawn_blocking(move || match command {
CookieCommand::Import { file } => jar.import_file(&file),
CookieCommand::Remove => jar.remove(),
})
.await??;
println!("YouTube cookies updated. Sync your podcast to continue.");
Ok(())
}
Command::Login => auth::login(&mut api).await,
Command::Auth {
command: AuthCommand::Logout,
Expand Down Expand Up @@ -301,7 +324,8 @@ async fn run(cli: Cli, cancel: CancellationToken) -> Result<()> {
Command::Shows { command } => commands::shows(&api, command).await,
Command::Episodes { command } => episodes::run(&api, command).await,
Command::Members { command } => commands::members(&api, command).await,
Command::Login
Command::YoutubeCookies { .. }
| Command::Login
| Command::Auth {
command: AuthCommand::Logout,
} => unreachable!(),
Expand Down
17 changes: 17 additions & 0 deletions crates/desktop/examples/preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ mod workspace {
view.catalog.shows[0].image_url = Some("https://artwork.example.test/missing.png".into());
}

pub fn show_settings(view: &mut Workspace, saved: bool) {
view.settings_open = true;
view.cookie_saved = saved;
}
pub fn show_cookie_recovery(view: &mut Workspace) {
view.error = Some(ErrorNotice::youtube_sign_in());
}

pub fn show_import(view: &mut Workspace) {
view.import_open = true;
}
Expand Down Expand Up @@ -177,6 +185,9 @@ fn main() -> anyhow::Result<()> {
("payment-dark", ThemeMode::Dark, 840., 600., true, false),
("quota-light", ThemeMode::Light, 1080., 760., true, false),
("quota-dark", ThemeMode::Dark, 840., 600., true, false),
("settings-light", ThemeMode::Light, 1080., 760., true, false),
("settings-dark", ThemeMode::Dark, 840., 600., true, false),
("cookie-recovery", ThemeMode::Light, 840., 600., true, false),
("quit-light", ThemeMode::Light, 1080., 760., false, true),
("quit-dark", ThemeMode::Dark, 840., 600., true, true),
] {
Expand Down Expand Up @@ -211,6 +222,12 @@ fn main() -> anyhow::Result<()> {
if name == "workspace-dark" {
workspace::show_selected_team(&mut view);
}
if name.starts_with("settings-") {
workspace::show_settings(&mut view, name == "settings-dark");
}
if name == "cookie-recovery" {
workspace::show_cookie_recovery(&mut view);
}
if quitting {
workspace::show_quit_notice(&mut view, cx);
}
Expand Down
4 changes: 2 additions & 2 deletions crates/desktop/moon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ tasks:
runInCI: false
build-release-windows-x64:
description: Build and package a Windows x64 release, also runnable through Windows 11 ARM emulation.
deps: [client:codegen, client:native-ffmpeg-windows-x64]
deps: [client:codegen, youtubejs:bundle, client:native-ffmpeg-windows-x64]
command: powershell.exe -NoProfile -ExecutionPolicy Bypass -File ../../tools/build-windows.ps1 -Architecture x64 -Stage Release
inputs: ['@group(sources)', 'project://client?group=sources', 'project://client?group=notices']
outputs: [dist/windows-x64/**/*, dist/listenbox-desktop-windows-x64.exe, dist/listenbox-desktop-windows-x64.zip]
Expand All @@ -119,7 +119,7 @@ tasks:
cache: false
build-release-windows-arm64:
description: Build and package a native Windows ARM64 release on Windows ARM64.
deps: [client:codegen, client:native-ffmpeg-windows-arm64]
deps: [client:codegen, youtubejs:bundle, client:native-ffmpeg-windows-arm64]
command: powershell.exe -NoProfile -ExecutionPolicy Bypass -File ../../tools/build-windows.ps1 -Architecture arm64 -Stage Release
inputs: ['@group(sources)', 'project://client?group=sources', 'project://client?group=notices']
outputs: [dist/windows-arm64/**/*, dist/listenbox-desktop-windows-arm64.exe, dist/listenbox-desktop-windows-arm64.zip]
Expand Down
13 changes: 12 additions & 1 deletion crates/desktop/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use gpui_kit::{App, Entity, Menu, MenuItem, Window, actions};
#[path = "platform/windows.rs"]
mod windows;

actions!(listenbox, [Logout, Quit]);
actions!(listenbox, [Logout, Quit, OpenSettings]);

/// Query the actual shortcut key while its quit attempt is active. macOS can
/// consume Command-key releases before they reach GPUI's focused view.
Expand Down Expand Up @@ -48,7 +48,18 @@ pub fn install_actions(view: &Entity<Workspace>, cx: &mut App) {
});
let quit = view.clone();
cx.on_action(move |_: &Quit, cx| quit.update(cx, |view, cx| view.shutdown(Shutdown::Quit, cx)));
cx.bind_keys([gpui_kit::KeyBinding::new(
if cfg!(target_os = "macos") {
"cmd-,"
} else {
"ctrl-,"
},
OpenSettings,
None,
)]);
cx.set_menus(vec![Menu::new("Listenbox").items([
MenuItem::action("Settings…", OpenSettings),
MenuItem::separator(),
MenuItem::action("Log out", Logout),
MenuItem::separator(),
MenuItem::action("Quit Listenbox", Quit),
Expand Down
Loading
Loading