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
5 changes: 5 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[build]
# The manual FJS publisher runs Cargo from this workspace with an external
# manifest path. Normal client builds only compile the tiny Rust resource.
rustc-wrapper = "kache"
incremental = false
166 changes: 166 additions & 0 deletions .github/workflows/client.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
name: Client

on:
push:
branches: [master]
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: client-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
env:
RUSTUP_TOOLCHAIN: 1.98.1-${{ matrix.rust_host }}
KACHE_CONFIG: ${{ github.workspace }}/.kache.toml
strategy:
fail-fast: false
matrix:
include:
- target: macos-arm64
runner: macos-15
cli: listenbox
rust_host: aarch64-apple-darwin
- target: macos-x64
runner: macos-15-intel
cli: listenbox
rust_host: x86_64-apple-darwin
- target: windows-x64
runner: windows-2025
cli: listenbox.exe
rust_host: x86_64-pc-windows-msvc
- target: windows-arm64
runner: windows-11-arm
cli: listenbox.exe
rust_host: aarch64-pc-windows-msvc
- target: linux-x64
runner: ubuntu-24.04
cli: listenbox
rust_host: x86_64-unknown-linux-gnu
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
- uses: subosito/flutter-action@v2
if: matrix.target != 'windows-arm64'
with:
channel: stable
flutter-version: '3.47.5'
cache: true
# The Flutter 3.47.5 Windows archive contains only x64 Dart. A source
# checkout bootstraps the pinned native ARM64 Dart and engine artifacts.
- name: Set up native Windows ARM64 Flutter SDK
if: matrix.target == 'windows-arm64'
shell: pwsh
run: |
$flutterDir = Join-Path $env:RUNNER_TEMP 'flutter-arm64'
git clone --depth 1 --branch 3.47.5 https://github.com/flutter/flutter.git $flutterDir
if ($LASTEXITCODE -ne 0) { throw 'Flutter checkout failed.' }
& (Join-Path $flutterDir 'bin/flutter.bat') --version
if ($LASTEXITCODE -ne 0) { throw 'Flutter bootstrap failed.' }
$dartVersion = & (Join-Path $flutterDir 'bin/dart.bat') --version 2>&1 | Out-String
if ($LASTEXITCODE -ne 0 -or $dartVersion -notmatch 'windows_arm64') {
throw "Expected native Windows ARM64 Dart: $dartVersion"
}
(Join-Path $flutterDir 'bin') >> $env:GITHUB_PATH
(Join-Path $flutterDir 'bin/cache/dart-sdk/bin') >> $env:GITHUB_PATH
- uses: moonrepo/setup-toolchain@v0
if: matrix.target != 'windows-arm64'
with:
moon-version: '2.5.5'
# Moon 2.5.5 has no Windows ARM64 binary. Only the task runner is emulated;
# Flutter and Dart above remain native ARM64.
- name: Set up Moon on Windows ARM64
if: matrix.target == 'windows-arm64'
shell: pwsh
run: |
$tools = Join-Path $env:RUNNER_TEMP 'moon-x64'
New-Item -ItemType Directory -Force $tools | Out-Null
$archive = Join-Path $tools 'moon.zip'
Invoke-WebRequest 'https://github.com/moonrepo/moon/releases/download/v2.5.5/moon_cli-x86_64-pc-windows-msvc.zip' -OutFile $archive
$expected = '36d4d88284f3444d3e5759f69b7f181eb807a3bcd20ce0bf475099132b41eb74'
if ((Get-FileHash $archive -Algorithm SHA256).Hash.ToLowerInvariant() -ne $expected) {
throw 'Moon archive checksum mismatch.'
}
Expand-Archive $archive -DestinationPath $tools
$tools >> $env:GITHUB_PATH
- name: Install native Rust 1.98.1 for YouTube.js resource asset
run: |
rustup toolchain install 1.98.1-${{ matrix.rust_host }} --profile minimal
rustup default 1.98.1-${{ matrix.rust_host }}
- name: Verify native Rust host
shell: bash
run: |
rustc -vV | grep -F 'host: ${{ matrix.rust_host }}'
- name: Restore shared Rust compiler cache
uses: actions/cache@v6
with:
path: |
~/.cargo/registry
~/.cargo/git
${{ runner.temp }}/kache
key: client2-rust-kache-v1-0.27.0-${{ matrix.target }}-${{ hashFiles('.kache.toml', '.cargo/config.toml', 'rust-toolchain.toml') }}-${{ github.workflow }}-${{ github.sha }}
restore-keys: |
client2-rust-kache-v1-0.27.0-${{ matrix.target }}-${{ hashFiles('.kache.toml', '.cargo/config.toml', 'rust-toolchain.toml') }}-
- name: Install Kache 0.27.0 for the Rust resource
uses: kunobi-ninja/kache-action@ad7317540dcdd71c904f7a60a87b620a0c0e67ed # v1
with:
version: '0.27.0'
cache-dir: ${{ runner.temp }}/kache
cache-executables: true
github-cache: false
pr-comment: false
- name: Install Linux desktop, tray, and audio libraries
if: matrix.target == 'linux-x64'
run: |
sudo apt-get update
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libstdc++-12-dev libasound2t64 libayatana-appindicator3-dev
- name: Resolve frozen Dart workspace
run: flutter pub get --enforce-lockfile
- name: Analyze and format check
if: matrix.target == 'linux-x64'
run: moon run client:lint
- name: Build CLI bundle
run: moon run cli:build
- name: Smoke CLI bundle
shell: bash
run: ./packages/cli/dist/release/bundle/bin/${{ matrix.cli }} --help
- name: Build desktop release bundle
run: moon run desktop:build
- name: Run headless desktop integration test
run: moon run desktop:test-e2e
# Actions artifacts do not preserve Unix executable bits or app-bundle
# symlinks; archive each complete bundle before uploading it.
- name: Archive Unix bundles
if: runner.os != 'Windows'
shell: bash
run: |
mkdir -p release-artifacts
tar -C packages/cli/dist/release -czf release-artifacts/listenbox-cli-${{ matrix.target }}.tar.gz bundle
tar -C packages/desktop/dist -czf release-artifacts/listenbox-desktop-${{ matrix.target }}.tar.gz release
- name: Archive Windows bundles
if: runner.os == 'Windows'
shell: pwsh
run: |
New-Item -ItemType Directory -Force release-artifacts | Out-Null
Compress-Archive -Path packages/cli/dist/release/bundle -DestinationPath release-artifacts/listenbox-cli-${{ matrix.target }}.zip
Compress-Archive -Path packages/desktop/dist/release -DestinationPath release-artifacts/listenbox-desktop-${{ matrix.target }}.zip
- name: Upload CLI bundle
uses: actions/upload-artifact@v7
with:
name: listenbox-cli-${{ matrix.target }}
path: release-artifacts/listenbox-cli-${{ matrix.target }}.*
if-no-files-found: error
- name: Upload desktop bundle
uses: actions/upload-artifact@v7
with:
name: listenbox-desktop-${{ matrix.target }}
path: release-artifacts/listenbox-desktop-${{ matrix.target }}.*
if-no-files-found: error
6 changes: 3 additions & 3 deletions .github/workflows/publish-ffmpeg.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ jobs:
if: matrix.target == 'linux-x64'
run: |
sudo apt-get update
sudo apt-get install -y clang cmake ninja-build pkg-config libasound2-dev zip
sudo apt-get install -y clang cmake ninja-build pkg-config libasound2-dev zip xz-utils
- uses: msys2/setup-msys2@v2
if: matrix.target == 'windows-x64'
with:
msystem: CLANG64
update: true
install: >-
git curl make tar zip python
git curl make tar zip python diffutils
mingw-w64-clang-x86_64-toolchain
mingw-w64-clang-x86_64-cmake
mingw-w64-clang-x86_64-ninja
Expand All @@ -51,7 +51,7 @@ jobs:
msystem: CLANGARM64
update: true
install: >-
git curl make tar zip python
git curl make tar zip python diffutils
mingw-w64-clang-aarch64-toolchain
mingw-w64-clang-aarch64-cmake
mingw-w64-clang-aarch64-ninja
Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/publish-quickjs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jobs:
runs-on: ${{ matrix.runner }}
env:
RUSTUP_TOOLCHAIN: 1.98.1-${{ matrix.rust_host }}
KACHE_CONFIG: ${{ github.workspace }}/.kache.toml
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -63,6 +64,24 @@ jobs:
shell: bash
run: |
rustc -vV | grep -F 'host: ${{ matrix.rust_host }}'
- name: Restore shared Rust compiler cache
uses: actions/cache@v6
with:
path: |
~/.cargo/registry
~/.cargo/git
${{ runner.temp }}/kache
key: client2-rust-kache-v1-0.27.0-${{ matrix.target }}-${{ hashFiles('.kache.toml', '.cargo/config.toml', 'rust-toolchain.toml') }}-${{ github.workflow }}-${{ github.sha }}
restore-keys: |
client2-rust-kache-v1-0.27.0-${{ matrix.target }}-${{ hashFiles('.kache.toml', '.cargo/config.toml', 'rust-toolchain.toml') }}-
- name: Install Kache 0.27.0 for the FJS source publisher
uses: kunobi-ninja/kache-action@ad7317540dcdd71c904f7a60a87b620a0c0e67ed # v1
with:
version: '0.27.0'
cache-dir: ${{ runner.temp }}/kache
cache-executables: true
github-cache: false
pr-comment: false
- name: Resolve frozen Dart workspace
run: flutter pub get --enforce-lockfile
# This explicit release job is the only caller of the native source
Expand Down
6 changes: 6 additions & 0 deletions .kache.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[cache]
# One OS-level store is shared across checkouts; do not put it in a build tree.
# The hook's generated YouTube.js resource is a cdylib, so opt it into caching.
cache_executables = true
adaptive_incremental = false
preserve_incremental = false
17 changes: 17 additions & 0 deletions .moon/workspace.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defaultProject: client
projects:
client: .
cli: packages/cli
desktop: packages/desktop
client-engine: packages/sync-engine
client-youtubei: packages/youtubei
versionConstraint: ">=2.5.4 <3.0.0"
vcs:
client: git
defaultBranch: master
pipeline:
installDependencies: false
syncProjects: false
syncWorkspace: false
hasher:
walkStrategy: glob
24 changes: 24 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Listenbox desktop",
"type": "dart",
"request": "launch",
"cwd": "${workspaceFolder}/packages/desktop",
"program": "lib/main.dart",
"env": {
"LISTENBOX_CONFIG": "${workspaceFolder}/config/dev.yaml"
},
"flutterMode": "debug"
},
{
"name": "Listenbox CLI help",
"type": "dart",
"request": "launch",
"cwd": "${workspaceFolder}/packages/cli",
"program": "bin/listenbox.dart",
"args": ["--help"]
}
]
}
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"dart.projectSearchDepth": 3,
"dart.flutterHotReloadOnSave": "allIfDirty",
"[dart]": {
"editor.defaultFormatter": "Dart-Code.dart-code",
"editor.formatOnSave": true
}
}
32 changes: 32 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Dart and Flutter client

- The four application modules are `packages/youtubei`, `packages/sync-engine`,
`packages/cli`, and `packages/desktop`.
- All application logic is Dart. Desktop is Flutter; CLI is standalone Dart.
- One Dart isolate owns each process's application state. Use async operations
and explicit admission queues; native execution details stay inside libraries.
- Shared HTTP, profiles, credentials, cookies, persistence, download management,
media preparation, and synchronization belong in sync-engine.
- Embed the pinned YouTube.js submodule through youtubei. Never invoke a Node
subprocess or the superseded Rust client at runtime.
- Import the pinned upstream `package:fjs/fjs.dart`; do not copy its Dart
bindings or fork its runtime. Accept the QuickJS version shipped with FJS
and upgrade the Dart package and matching native artifacts together.
- CLI and desktop must independently bundle pinned SQLite, QuickJS, and FFmpeg
libraries. Normal builds consume verified prebuilt artifacts, never silently
fall back to compiling native dependencies or loading system versions.
- The youtubei hook may compile a tiny generated Rust resource library that only
exposes the pinned YouTube.js bundle bytes to Dart 3.13/Flutter 3.47 native
assets. It must not compile the FJS/QuickJS runtime or application logic.
Dart and Flutter data assets are not supported on the stable SDK in use.
- A YouTube.js source or native artifact change requires a full Flutter
restart; ordinary Dart UI changes keep the hot-reload session and must not
rebuild the YouTube.js resource library.
- Preserve resumable work and acknowledgement-loss safety. Cancellation must
settle owned work before logout or exit; UI progress is never durable truth.
- Use the parent repository's integrated API E2E suites for the real Dart CLI.
Keep integrated Flutter UI coverage headless in `packages/desktop/test` with
the real shared client and local HTTP fixtures. Tests must not open or focus
a native window. The API E2E suite must not launch or build the desktop app.
- Desktop development uses Flutter's ordinary run/hot-reload session.
- Do not add unit tests or compatibility paths for the superseded Rust client.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Listenbox client

The Listenbox desktop app and command-line client are a Dart workspace. The
desktop app uses Flutter; the CLI runs as a standalone Dart executable. Both use
the same synchronization engine and an embedded, pinned YouTube.js runtime.
Each process keeps application state in one Dart isolate; the small Rust asset
only carries bundled JavaScript bytes.

| Package | Responsibility |
| --- | --- |
| `packages/youtubei` | Embedded YouTube.js and QuickJS host |
| `packages/sync-engine` | Profiles, API access, SQLite journal, downloads, media, and synchronization |
| `packages/cli` | Terminal commands and protocol output |
| `packages/desktop` | Flutter desktop interface |

## Get started

Install Flutter **3.47.5** (Dart **3.13.4**), Rust **1.98.1**, Moon
**2.5.5**, and the [prebuilt Kache **0.27.0** executable](https://github.com/kunobi-ninja/kache/releases/tag/v0.27.0)
for your host, then clone with submodules:

```sh
git clone --recurse-submodules https://github.com/listenbox/client2.git
cd client2
dart tool/flutter.dart pub get --enforce-lockfile
moon run cli:build
moon run desktop:build
```

The CLI bundle is `packages/cli/dist/release/bundle/`; launch its `bin/listenbox`
executable (`bin/listenbox.exe` on Windows) from that complete bundle. The
desktop release bundle is `packages/desktop/dist/release/`. Keep each complete
bundle together so its verified native libraries remain available.

For development, run Flutter directly from `packages/desktop`:

```sh
cd packages/desktop
flutter run -d macos
```

Use `-d windows` or `-d linux` on those hosts. Debug sessions load
`config/dev.yaml` for the parent repository's local API and dashboard.
`moon run desktop:dev` and the included VS Code launch use that same config.
Save a Dart file in that VS Code session to hot reload; in a terminal Flutter
session, press `r` to reload or `R` to restart. See
[the development guide](docs/DEVELOPMENT.md) for setup and build details.

Build hooks verify and bundle pinned SQLite, QuickJS, and FFmpeg libraries.
The youtubei hook bundles pinned `vendor/youtubejs` source and compiles a small
Rust resource containing its bytes. Normal builds need neither Node.js nor
Aube, and never compile the larger native libraries from source. Kache caches
the resource compilation in a shared OS-level store across checkouts.
6 changes: 6 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
analyzer:
exclude:
- '**/build/**'
- '**/dist/**'
- '**/ephemeral/**'
- '**/.dart_tool/**'
2 changes: 2 additions & 0 deletions config/dev.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
api_origin: http://localhost:8080
dashboard_origin: http://localhost:5174
Loading
Loading