# Spotube Admin & Developer Guide

This guide is the deep reference for developers, packagers, and technically-minded users who want to understand how Spotube is built, how its internals fit together, how to compile and distribute it, and how to operate or extend it safely. Where the User Guide covers *using* Spotube, this guide covers *building, architecting, and reasoning about* it.

Spotube is a Flutter application written in Dart, targeting Android, iOS, Windows, macOS, Linux, and the web from a single codebase. Its defining architectural choice is the **Bring Your Own Music Metadata (BYOMM)** model: the app is a player shell, and all music metadata and audio sourcing is delegated to runtime-loadable plugins.

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [Architecture Overview](#2-architecture-overview)
3. [Repository Structure](#3-repository-structure)
4. [Technology Stack](#4-technology-stack)
5. [State Management & Data Flow](#5-state-management--data-flow)
6. [The Plugin System (BYOMM)](#6-the-plugin-system-byomm)
7. [Audio Pipeline & Local Server](#7-audio-pipeline--local-server)
8. [Persistence Layer](#8-persistence-layer)
9. [Configuration Reference](#9-configuration-reference)
10. [Build & Release](#10-build--release)
11. [Platform Integration](#11-platform-integration)
12. [Security Analysis](#12-security-analysis)
13. [Internationalization](#13-internationalization)
14. [Operations & Troubleshooting](#14-operations--troubleshooting)
15. [Code Quality & Conventions](#15-code-quality--conventions)
16. [Potential Improvements](#16-potential-improvements)
17. [Quick Reference](#17-quick-reference)

---

## 1. Executive Summary

| Attribute | Value |
|---|---|
| Project | Spotube |
| Version | 5.1.1+44 (`version: 5.1.1+44` in `pubspec.yaml`) |
| Description | Open source extensible music streaming platform and app, based on the BYOMM concept |
| Language | Dart (Flutter) |
| Dart SDK | `>=3.0.0 <4.0.0` |
| Flutter | `>=3.29.0` |
| Author | Kingkor Roy Tirtho |
| License | BSD-4-Clause |
| App ID | `oss.krtirtho.spotube` |
| Targets | Android, iOS, Windows, macOS, Linux, Web |
| Repository | [github.com/KRTirtho/spotube](https://github.com/KRTirtho/spotube) |

**What it is, architecturally:** a Flutter front end built on the `shadcn_flutter` design system, with Riverpod (hooks variant) for state, Drift/SQLite for persistence, `media_kit` for audio playback, and a `shelf`-based in-process HTTP/WebSocket server that proxies audio and powers remote control ("Spotube Connect"). All domain logic for *what music exists* and *where audio comes from* is externalized into Hetu-scripted plugins loaded at runtime.

**Why the design matters:** decoupling metadata from audio sourcing means Spotube is not tied to any one provider and can adapt as providers change, but it also concentrates fragility at the audio-resolution boundary (YouTube extractor breakage is the dominant source of user-reported failures).

---

## 2. Architecture Overview

```
                         ┌──────────────────────────────────────────┐
                         │              Spotube (Flutter)            │
                         │                                           │
   ┌─────────┐           │   ┌───────────────┐   ┌───────────────┐   │
   │  User   │──────────▶│   │  UI Layer     │   │  Intents /    │   │
   │  Input  │           │   │ shadcn_flutter│   │  Shortcuts    │   │
   └─────────┘           │   │  pages/       │   │  Actions      │   │
                         │   └───────┬───────┘   └───────┬───────┘   │
                         │           │ watch/read        │           │
                         │   ┌───────▼───────────────────▼───────┐   │
                         │   │     Riverpod Providers (state)     │   │
                         │   │  audioPlayer, userPreferences,     │   │
                         │   │  metadataPlugin, downloadManager,  │   │
                         │   │  server, scrobbler, blacklist...   │   │
                         │   └───┬─────────────┬──────────┬──────┘   │
                         │       │             │          │          │
         ┌───────────────┼───────▼───┐   ┌─────▼─────┐ ┌──▼───────┐  │
         │   Plugin Runtime (Hetu)   │   │  Drift /  │ │ media_kit│  │
         │  metadata + audio source  │   │  SQLite   │ │  player  │  │
         └───────┬───────────┬───────┘   └───────────┘ └────┬─────┘  │
                 │           │                              │        │
                 │           │            ┌─────────────────▼─────┐  │
                 │           │            │ shelf local server     │ │
                 │           │            │ /stream /playback /ws  │ │
                 │           │            └──────────┬─────────────┘ │
                 └───────────┼───────────────────────┼──────────────┘
                             │                        │
                    ┌────────▼────────┐      ┌────────▼─────────┐
                    │ Metadata source │      │  Audio source    │
                    │ (e.g. MusicBrainz)     │ (e.g. YouTube via │
                    │                 │      │ NewPipe/yt-dlp/   │
                    └─────────────────┘      │ youtube_explode)  │
                                             └──────────────────┘
```

### Layered breakdown

| Layer | Location | Responsibility |
|---|---|---|
| **UI** | `lib/pages/`, `lib/modules/`, `lib/components/` | Screens (`@RoutePage`), feature modules, reusable widgets |
| **Routing** | `lib/collections/routes.dart` | `auto_route` declarative router (`AppRouter`) |
| **State** | `lib/provider/` | Riverpod providers/notifiers — the application's brain |
| **Services** | `lib/services/` | Cross-cutting singletons: audio player, CLI, key-value store, logger, metadata, youtube engines |
| **Models** | `lib/models/` | Drift tables, metadata objects, connect/playback models |
| **Plugin runtime** | `lib/provider/metadata_plugin/`, `lib/modules/metadata_plugins/` | Loading, caching, and invoking Hetu-script plugins |
| **Server** | `lib/provider/server/` | In-process `shelf` HTTP/WS server (audio proxy + Connect) |
| **Native shells** | `android/`, `ios/`, `windows/`, `macos/`, `linux/`, `web/` | Platform entry points and packaging |

### Startup sequence

`lib/main.dart` orchestrates a careful initialization order:

1. Handle the special `web_view_title_bar` argument (desktop webview helper) and return early if matched.
2. Parse CLI args via `startCLI()` (`lib/services/cli/cli.dart`) — handles `--verbose/-v`, `--version`, `--help/-h`.
3. Initialize logging (`AppLogger.initialize`) and run everything inside a guarded zone (`AppLogger.runZoned`).
4. Install a `BadCertificateAllowlistOverrides` `HttpOverrides` global.
5. Initialize timezones, native splash, `MediaKit`.
6. Run macOS sandbox→no-sandbox migration.
7. Platform tweaks: high refresh rate (Android), `NewPipeExtractor.init()` (Android/desktop), `MetadataGod` (non-web).
8. Initialize key-value stores (`KVStoreService`, `EncryptedKvStoreService`), the Drift `AppDatabase`.
9. Desktop: prevent-close, set yt-dlp binary path, Discord RPC init, local notifier, window manager tools.
10. Windows: `SMTCWindows.initialize()`. iOS: set home-widget app group.
11. `runApp` inside a `ProviderScope` (overriding `databaseProvider`, attaching `AppLoggerProviderObserver`) rendering the root `Spotube` widget.

The root `Spotube` widget (`HookConsumerWidget`) wires up `ShadcnApp.router`, themes, localization delegates, global keyboard shortcuts/intents, and *listens* (to keep alive) a set of background providers: audio player streams, Bonsoir (mDNS), connect clients, the server, tray manager, metadata/audio-source plugins, and update checkers.

---

## 3. Repository Structure

```
spotube/
├── lib/                      # All Dart application code
│   ├── main.dart             # App entry point & bootstrap
│   ├── collections/          # Generated assets, routes, intents, env, icons, markets, locales
│   ├── components/           # Reusable widgets (buttons, dialogs, player, titlebar, ...)
│   ├── extensions/           # Dart extension methods (context, dio, ...)
│   ├── hooks/                # flutter_hooks: configurators, controllers, utils
│   ├── l10n/                 # ARB translation files + generated localizations
│   ├── models/               # Drift DB tables, metadata/connect/playback models
│   │   └── database/
│   │       ├── database.dart         # AppDatabase definition (drift)
│   │       └── tables/               # preferences, history, blacklist, lyrics, scrobbler, ...
│   ├── modules/              # Feature modules (settings, stats, metadata_plugins, player, ...)
│   ├── pages/                # @RoutePage screens (settings, library, player, search, stats, ...)
│   ├── provider/             # Riverpod providers (state) — the core logic
│   │   ├── audio_player/
│   │   ├── metadata_plugin/  # BYOMM plugin runtime
│   │   ├── server/           # shelf server + Connect routes
│   │   ├── user_preferences/
│   │   └── ...
│   ├── services/             # audio_player, cli, kv_store, logger, metadata, youtube_engine, ...
│   └── utils/                # platform flags, migrations, service utils
├── cli/                      # DEVELOPER build CLI (not shipped in the app)
│   ├── cli.dart              # CommandRunner entry
│   └── commands/             # build, credits, install-dependencies, translated, untranslated
├── assets/                   # images, fonts, branding, bundled plugins (.smplug)
├── drift_schemas/            # Drift schema snapshots (migration history)
├── android/ ios/ windows/    # Native platform projects
├── macos/ linux/ web/
├── website/                  # Astro + React marketing/docs site
├── integration_test/         # Flutter integration tests
├── test/                     # Unit/widget tests (incl. drift)
├── pubspec.yaml              # Dependencies & Flutter asset/font manifest
├── build.yaml                # build_runner / codegen config
├── l10n.yaml                 # Localization generation config
├── analysis_options.yaml     # Lints & analyzer config
└── distribute_options.yaml   # flutter_distributor release jobs
```

> [!NOTE]
> The `cli/` directory is a standalone Dart `CommandRunner` used by maintainers to build and manage the project. It is excluded from the app build (`build.yaml` excludes `bin/*.dart`) and is unrelated to the application's own runtime flags.

---

## 4. Technology Stack

Spotube depends on a large set of pub packages. The most architecturally significant are grouped below (versions per `pubspec.yaml`).

### UI & app framework

| Package | Role |
|---|---|
| `shadcn_flutter` | Primary design system / widget library (the app's look) |
| `flutter_hooks`, `hooks_riverpod`, `flutter_riverpod`, `riverpod` | State management (hooks + Riverpod) |
| `auto_route` (+ `auto_route_generator`) | Declarative, code-generated routing |
| `flutter_localizations`, `intl` | Localization runtime |
| `skeletonizer`, `flutter_undraw`, `cached_network_image`, `palette_generator` | Loading skeletons, illustrations, image handling, color extraction |

### Audio & media

| Package | Role |
|---|---|
| `media_kit` (+ libs) | Core audio playback engine (git pin) |
| `audio_service`, `audio_service_mpris`, `audio_session` | Background playback + OS media integration (Android/MPRIS) |
| `smtc_windows` | Windows System Media Transport Controls |
| `metadata_god` | Read/write audio file tags |
| `youtube_explode_dart`, `yt_dlp_dart`, `flutter_new_pipe_extractor` | Three YouTube stream-resolution engines |

### Data & networking

| Package | Role |
|---|---|
| `drift`, `sqlite3`, `sqlite3_flutter_libs` | SQLite ORM + engine |
| `dio`, `http`, `http_parser` | HTTP clients |
| `shelf`, `shelf_router`, `shelf_web_socket`, `web_socket_channel` | In-process server + WebSocket |
| `bonsoir` | mDNS/Bonjour service discovery (Connect) |
| `shared_preferences`, `flutter_secure_storage`, `encrypt` | Key-value + secure storage |

### Plugin runtime

| Package | Role |
|---|---|
| `hetu_script`, `hetu_std`, `hetu_otp_util`, `hetu_spotube_plugin` | Hetu scripting VM + standard library that runs BYOMM plugins |
| `archive` | Unpack `.smplug` plugin archives |

### Integrations & platform

| Package | Role |
|---|---|
| `flutter_discord_rpc` | Discord Rich Presence |
| `scrobblenaut` | Last.fm scrobbling |
| `window_manager`, `tray_manager`, `local_notifier`, `titlebar_buttons` | Desktop window/tray/notifications |
| `home_widget` | Home-screen widgets (Android Glance / iOS WidgetKit) |
| `app_links`, `flutter_sharing_intent` | Deep linking & share intents |
| `permission_handler`, `device_info_plus`, `package_info_plus`, `connectivity_plus` | Runtime permissions & device info |
| `wikipedia_api`, `lrc`, `fuzzywuzzy` | Artist bios, lyrics parsing, fuzzy matching |

> [!IMPORTANT]
> Several dependencies are **git pins or forks** (e.g. `media_kit`, `desktop_webview_window`, `flutter_broadcasts`, `bonsoir_android`, `yt_dlp_dart`, `flutter_new_pipe_extractor`, `scrobblenaut`, a patched `flutter_secure_storage_linux`). Reproducible builds require network access to these repos at the pinned refs. Audit `dependency_overrides` in `pubspec.yaml` before packaging.

---

## 5. State Management & Data Flow

Spotube uses **Riverpod** (hooks variant) as its single source of runtime truth. Widgets are `HookConsumerWidget`s that `ref.watch` providers; mutations go through notifier methods.

### Provider taxonomy

| Provider | Type | Responsibility |
|---|---|---|
| `userPreferencesProvider` | `NotifierProvider` | All persisted user settings (backed by Drift `PreferencesTable`) |
| `audioPlayerProvider` | (notifier) | Queue + playback state; wraps the `media_kit` engine |
| `downloadManagerProvider` | `NotifierProvider` | Offline download queue & per-track tasks |
| `metadataPluginProvider` / `metadataPluginsProvider` | Async | Active + installed metadata plugins |
| `audioSourcePluginProvider` | Async | Active audio-source plugin |
| `sourcedTrackProvider(track)` | `AsyncNotifierProviderFamily` | Resolves a metadata track to a streamable source |
| `serverProvider` | `FutureProvider` | The shelf playback/Connect server lifecycle |
| `bonsoirProvider` | `FutureProvider` | mDNS advertisement for Connect |
| `connectClientsProvider` | Async | Discovered/connected Connect peers |
| `scrobblerProvider` | Async | Last.fm scrobbler session |
| `blacklistProvider` | `AsyncNotifierProvider` | Blacklisted tracks/artists (Drift-backed, live-watched) |
| `volumeProvider` | `NotifierProvider` | Volume (persisted via `KVStoreService`) |
| `sleepTimerProvider` | `StateNotifierProvider` | Sleep timer (calls `exit(0)` on fire) |
| `discordProvider` | `AsyncNotifierProvider` | Discord Rich Presence sync |

### Example: how a track becomes audio

1. UI calls `audioPlayerNotifier.load([track], autoPlay: true)`.
2. For each non-local track, `sourcedTrackProvider(track)` runs `SourcedTrack.fetchFromTrack(...)`, which uses the **audio-source plugin** to match and resolve a stream (using the selected YouTube engine and quality presets).
3. The track is enqueued into `media_kit` as a `SpotubeMedia`, whose URI points at the **local shelf server** (`/stream/<trackId>`).
4. `media_kit` requests that URL; the server's `ServerPlaybackRoutes.streamTrack` proxies the upstream audio (with caching) back to the player.

This indirection is why the local server runs even for ordinary local playback.

### Reactive persistence

`UserPreferencesNotifier` (`lib/provider/user_preferences/user_preferences_provider.dart`) seeds a singleton row (`id = 0`) in `PreferencesTable` and then `watchSingle()`s it. Any write updates the row; the watch fires and the notifier re-applies side effects (desktop title-bar style, audio normalization). Settings are therefore **live** across the whole app — change one place, every watcher updates.

---

## 6. The Plugin System (BYOMM)

Plugins are the heart of Spotube. They are **Hetu-script** programs distributed as `.smplug` archives and run inside an embedded Hetu VM (`hetu_script` + `hetu_spotube_plugin`).

### Plugin abilities

A plugin declares one or more abilities (`PluginAbilities`):

| Ability | Meaning |
|---|---|
| `metadata` | Provides search, artists, albums, playlists, recommendations, user library |
| `audioSource` | Resolves a track to a playable/downloadable audio stream |
| `authentication` | Can log a user into the underlying provider |
| `scrobbling` | Can report plays back to the provider |

### Discovery

`MetadataPluginRepositoriesNotifier` (`lib/provider/metadata_plugin/core/repositories.dart`) searches two forges in parallel for the topic **`spotube-plugin`**:

- GitHub: `GET https://api.github.com/search/repositories?q=topic:spotube-plugin&sort=stars&order=desc`
- Codeberg: `GET https://codeberg.org/api/v1/repos/search?q=spotube-plugin&topic=true&sort=stars&order=desc`

Results are paginated and merged. The settings UI further filters by topic `spotube-metadata-plugin` vs `spotube-audio-source-plugin`.

### Installation paths

`MetadataPluginsNotifier` (referenced from `lib/pages/settings/metadata_plugins.dart`) exposes:

- `downloadAndCachePlugin(url)` → `addPlugin(config)` — install from a URL.
- `extractPluginArchive(bytes)` → `addPlugin(config)` — install from an uploaded `.smplug` (validated as a `Spotube Metadata Plugin` file type).

Two plugins ship bundled as assets (`assets/plugins/...`, declared in `pubspec.yaml`):

- `spotube-plugin-musicbrainz-listenbrainz/plugin.smplug`
- `spotube-plugin-youtube-audio/plugin.smplug`

### Default selection

`metadataPluginsProvider.value` carries `defaultMetadataPluginConfig` and `defaultAudioSourcePluginConfig`. The UI marks installed plugins as default per ability; the rest of the app reads "the default plugin" through `metadataPluginProvider` / `audioSourcePluginProvider`.

### Cross-cutting plugin services

- **Auth** (`core/auth.dart`): `metadataPluginAuthenticatedProvider` / `audioSourcePluginAuthenticatedProvider` watch the plugin's `auth.authStateStream` and expose `isAuthenticated()`.
- **Scrobble** (`core/scrobble.dart`): when the default metadata plugin supports `scrobbling`, plays are pushed to `plugin.core.scrobble({...})` with ISRC, duration, and timestamps.
- **User** (`core/user.dart`): `metadataPluginUserProvider` returns the logged-in `SpotubeUserObject` when authenticated.
- **Support** (`core/support.dart`): exposes plugin-provided support text.

> [!WARNING]
> Plugins execute third-party logic and make arbitrary network calls (e.g. to GitHub, Codeberg, and provider APIs). The Plugins screen surfaces a DMCA/disclaimer notice. From a threat-model perspective, an installed plugin is **trusted code running in your app**. Packagers and security reviewers should treat the plugin install surface (URL + file upload) as the primary untrusted-input boundary.

---

## 7. Audio Pipeline & Local Server

### The in-process shelf server

`serverProvider` (`lib/provider/server/server.dart`) starts a `shelf` server:

- **Bind address:** `InternetAddress.anyIPv4` when **Connect is enabled**, otherwise `InternetAddress.loopbackIPv4` (localhost-only).
- **Port:** `connectPort` preference. `-1` means choose a random port in `[5000, 22500)` once and store it on `SpotubeMedia.serverPort`; otherwise the configured port.
- **Pipeline:** `pipelineProvider` adds `logRequests()` middleware in debug mode only.

### Routes

`serverRouterProvider` (`lib/provider/server/router.dart`) wires:

| Method | Route | Handler |
|---|---|---|
| GET | `/ping` | returns `pong` |
| HEAD | `/stream/<trackId>` | `ServerPlaybackRoutes.headStreamTrackId` |
| GET | `/stream/<trackId>` | `ServerPlaybackRoutes.getStreamTrackId` |
| GET | `/playback/toggle-playback` | toggle play/pause |
| GET | `/playback/previous` | previous track |
| GET | `/playback/next` | next track |
| ALL | `/ws` | `ServerConnectRoutes.websocket` |

### Streaming & caching (`routes/playback.dart`)

`streamTrack` resolves the upstream URL from the `SourcedTrack`, requests it with `dio` (rotating a random mobile/desktop **user-agent** from a set of `YoutubeApiClient` device clients), and:

- If `cacheMusic` is on and a cache file exists, serves the cached bytes with proper `content-range`/`accept-ranges` headers (range support for seeking).
- If the upstream returns an HLS playlist (`application/vnd.apple.mpegurl`), responds with a `301` redirect to let the player handle HLS range requests itself.
- Otherwise streams the body through while **simultaneously writing a `.part` cache file**; on completion it renames `.part`→final and writes ID3/metadata tags via `MetadataGod` (skipping `weba`).
- On stream errors it transparently **refreshes the streaming URL** or **swaps to a sibling source** (`swapWithNextSibling`, `refreshStreamingUrl`) — the self-healing that keeps playback alive when a single source dies.

Cache files live under `getMusicCacheDir()`:
- Android: external cache `.../Cached Tracks`
- Other: application cache `.../cached_tracks`

### Connect WebSocket (`routes/connect.dart`)

The `/ws` endpoint is the Connect control plane:

- On connect, the **player** device shows an **Accept/Decline** dialog keyed by the controller's `host:port` origin (cached in `_allowedConnections`). Declined connections receive a `WebSocketErrorEvent` and are closed.
- The server pushes initial state (queue, playing, position, duration, shuffle, loop, volume) and then forwards live stream events.
- Inbound events drive the audio player: `load`, `pause`, `resume`, `stop`, `next`, `previous`, `jump`, `seek`, `shuffle`, `loop`, `addTrack`, `removeTrack`, `reorder`, `volume`.

### mDNS advertisement (`server/bonsoir.dart`)

When Connect is enabled and no service is already resolved, `bonsoirProvider` advertises a `_spotube._tcp` service named after the device, with `id` and `deviceId` attributes, on the server port — so peer Spotube instances can discover it.

---

## 8. Persistence Layer

### Drift / SQLite

The app database is defined in `lib/models/database/database.dart` (`AppDatabase`), generated by `drift_dev` (config in `build.yaml`: database `app_db`, SQLite dialect, `json1` module). Schema snapshots are versioned under `drift_schemas/app_db/` and `test/drift/` covers migrations.

Tables (`lib/models/database/tables/`):

| Table | Purpose |
|---|---|
| `preferences` | Singleton (`id=0`) user settings |
| `authentication` | Stored auth/session data |
| `blacklist` | Blacklisted tracks/artists |
| `history` | Playback history |
| `lyrics` | Cached lyrics |
| `metadata_plugins` | Installed plugin configs |
| `scrobbler` | Scrobbler session |
| `skip_segment` | Skip segments (non-music) |
| `source_match` | Cached track→source matches |
| `audio_player_state` | Persisted player/queue state |

Custom type converters (`SpotubeColorConverter`, `LocaleConverter`, `StringListConverter`, `textEnum<>`) serialize complex preference values into text columns.

### Key-value & secure storage

- `KVStoreService` (`shared_preferences`) — fast scalar prefs (e.g. volume, YouTube engine binary paths).
- `EncryptedKvStoreService` (`flutter_secure_storage` + `encrypt`) — sensitive values.

### Migrations

`lib/utils/migrations/sandbox.dart` runs `migrateMacOsFromSandboxToNoSandbox()` at startup, moving data when the macOS build switched away from sandboxing.

---

## 9. Configuration Reference

All user-facing configuration is the `PreferencesTable` (`lib/models/database/tables/preferences.dart`), a single row keyed `id=0`. Defaults are defined in `PreferencesTable.defaults()`.

### Complete preferences table

| Key | Type | Default | Meaning |
|---|---|---|---|
| `albumColorSync` | bool | `true` | Sync accent color to album art |
| `amoledDarkTheme` | bool | `false` | AMOLED pitch-black dark theme (UI toggle currently commented out) |
| `checkUpdate` | bool | `true` | Check for app updates |
| `normalizeAudio` | bool | `false` | Audio loudness normalization |
| `showSystemTrayIcon` | bool | `false` | Desktop system tray icon |
| `systemTitleBar` | bool | `false` | Use OS native title bar |
| `skipNonMusic` | bool | `false` | Skip non-music segments |
| `closeBehavior` | enum | `close` | Desktop close action: `close` or `minimizeToTray` |
| `accentColorScheme` | color | `Slate:0xff64748b` | Accent color (custom converter) |
| `layoutMode` | enum | `adaptive` | `compact`, `extended`, or `adaptive` |
| `locale` | locale | system/system | UI language + country |
| `market` | enum | `US` | Recommendation market/region |
| `searchMode` | enum | `youtube` | `youtube` or `youtubeMusic` |
| `downloadLocation` | text | `""` (resolved to platform default) | Download folder |
| `localLibraryLocation` | string list | `[]` | Folders scanned for local music |
| `themeMode` | enum | `system` | `system`, `light`, `dark` |
| `audioSourceId` | text? | `null` | Selected audio source id |
| `youtubeClientEngine` | enum | `newPipe` (iOS: `youtubeExplode`) | `ytDlp`, `youtubeExplode`, or `newPipe` |
| `discordPresence` | bool | `true` | Discord Rich Presence (desktop) |
| `endlessPlayback` | bool | `true` | Auto-queue related tracks |
| `enableConnect` | bool | `false` | Spotube Connect server exposure |
| `connectPort` | int | `-1` | Connect port (`-1` = random; valid `-1`..`65535`) |
| `cacheMusic` | bool | `true` | Cache streamed audio to disk |

> [!NOTE]
> `youtubeClientEngine` has a platform-dependent default: NewPipe everywhere except iOS, where NewPipe is unavailable so it defaults to YouTubeExplode. Engine availability is gated by `YoutubeClientEngine.isAvailableForPlatform()`.

### Build-time configuration

| File | Purpose |
|---|---|
| `pubspec.yaml` | Dependencies, version, assets, fonts |
| `build.yaml` | Codegen targets: `auto_route`, `json_serializable`, `drift_dev` |
| `l10n.yaml` | ARB dir, template `app_en.arb`, output dir, untranslated-messages file |
| `analysis_options.yaml` | Lints (`package:flutter_lints`), excludes generated files |
| `distribute_options.yaml` | `flutter_distributor` release jobs per platform/target |
| `flutter_launcher_icons*.yaml` | App icon generation (stable + nightly) |
| `flutter_native_splash*.yaml` | Splash screens (stable + nightly) |
| `android/app/build.gradle` | Android flavors (`stable`/`nightly`/`dev`), signing, SDKs |

### Android flavors & SDKs

`android/app/build.gradle` defines three product flavors, each with a distinct app name and application-id suffix:

| Flavor | App name | App ID suffix |
|---|---|---|
| `stable` | Spotube | (none) → `oss.krtirtho.spotube` |
| `nightly` | Spotube Nightly | `.nightly` |
| `dev` | Spotube Dev | `.dev` |

SDK targets: `minSdkVersion 24`, `targetSdkVersion 35`, `compileSdkVersion 36`, NDK `29.0.14206865`, multidex enabled, core-library desugaring on. Release signing reads `key.properties` (keystore alias/passwords) — never commit that file.

---

## 10. Build & Release

### Prerequisites

- Flutter `>=3.29.0`, Dart `>=3.0.0 <4.0.0`.
- Platform toolchains (Android SDK/NDK, Xcode, Visual Studio with C++ for Windows, GTK/mpv dev libs for Linux).
- Network access to fetch git-pinned dependencies and forks.

### Standard Flutter build

```bash
flutter pub get
dart run build_runner build --delete-conflicting-outputs   # codegen (drift, auto_route, json, freezed, envied)
flutter build apk         # or appbundle / windows / macos / linux / ipa
```

```text
✓ Built build/app/outputs/flutter-apk/app-release.apk
```

> [!IMPORTANT]
> Run `build_runner` after any change to Drift tables, routes, `@freezed`/`json_serializable` models, or `envied` env definitions. Generated files (`*.g.dart`, `*.gr.dart`, `*.freezed.dart`) are excluded from analysis and must be regenerated, not hand-edited.

### Developer build CLI

The project ships a maintainer CLI at `cli/cli.dart` (a Dart `CommandRunner` named `cli`, "Configuration CLI for Spotube"):

| Command | Description |
|---|---|
| `install-dependencies -p <platform> [-a <arch>]` | Install platform build dependencies (`-p` is mandatory: `windows`, `linux`, `linux_arm`, `macos`, `ios`, `android`; `-a` is `x86`/`arm64`/`all`, default `x86`) |
| `build [-a <arch>] <android\|ios\|linux\|macos\|windows>` | Build for a platform (`-a` arch default `x86`) |
| `credits` | Generate a markdown credits list from `pubspec.yaml` (queries pub.dev + git pubspecs) |
| `translated` | Apply translated messages from `tm.json` into the `app_<locale>.arb` files |
| `untranslated` | Emit untranslated messages (for ChatGPT-assisted translation) from `untranslated_messages.json` |

```bash
dart run cli/cli.dart install-dependencies -p linux -a x86
dart run cli/cli.dart build -a all linux
```

For example, `install-dependencies -p linux` installs the GTK/mpv/webkit/appindicator toolchain and `appimagetool`; `-p windows` installs Inno Setup via Chocolatey; `-p macos` installs `appdmg`.

### Release distribution

`distribute_options.yaml` drives `flutter_distributor` jobs that package per platform/target (zip, deb, appimage, exe, dmg) with `APP_ENV` dart-defines. The published artifact names are referenced by the website (`website/src/collections/app.ts`) and the GitHub releases:

```
Spotube-android-all-arch.apk
Spotube-windows-x86_64-setup.exe
Spotube-macos-universal.dmg
Spotube-linux-x86_64.deb        Spotube-linux-aarch64.deb
Spotube-linux-x86_64.rpm
Spotube-linux-x86_64.AppImage   Spotube-linux-aarch64.AppImage
Spotube-iOS.ipa
```

### Changelog

`cliff.toml` (git-cliff) generates conventional-commit changelogs grouped into Features / Bug Fixes / Translation, with issue links rewritten to the repo.

---

## 11. Platform Integration

| Concern | Mechanism |
|---|---|
| Background audio (Android) | `audio_service` + media notification |
| Media controls (Linux) | `audio_service_mpris` (MPRIS D-Bus) |
| Media controls (Windows) | `smtc_windows` (SMTC) |
| Rich presence | `flutter_discord_rpc` (desktop only) |
| Home-screen widgets | `home_widget` — Android **Glance** widgets (`android/.../glance/`), iOS **HomePlayerWidget** (WidgetKit, app group `group.spotube_home_player_widget`) |
| Deep links / `spotube://` | `app_links` + `use_deep_linking` hook |
| Share-to-Spotube | `flutter_sharing_intent` |
| Window management | `window_manager`, custom title bar, `DragToResizeArea` (Linux), `titlebar_buttons` |
| System tray | `tray_manager` (`trayManagerProvider`) |
| Notifications | `local_notifier` (desktop) |
| Battery optimization | `disable_battery_optimization` + `use_disable_battery_optimizations` hook (Android) |
| High refresh rate | `flutter_displaymode` (Android) |
| Secure cert handling | `BadCertificateAllowlistOverrides` global `HttpOverrides` |

The Windows native entry (`windows/runner/main.cpp`) creates a 1200×800 window titled `spotube`, attaches a console for `flutter run`/debugger, and forwards command-line args to the Dart entrypoint.

---

## 12. Security Analysis

### Trust boundaries

| Boundary | Surface | Risk | Mitigation in code |
|---|---|---|---|
| **Plugin install** | URL download + `.smplug` file upload, executed in Hetu VM | Untrusted code running in-app with the app's privileges | DMCA/disclaimer notice; user-initiated only; (no sandbox beyond the Hetu VM) |
| **Connect server** | `shelf` HTTP/WS bound to `anyIPv4` when enabled | LAN peers could control playback / read queue | Off by default; **Accept/Decline** dialog per origin; localhost-only when disabled |
| **Audio proxy** | `/stream/<trackId>` fetches arbitrary upstream URLs | SSRF-style fetch of plugin-provided URLs | URLs originate from the (trusted) audio-source plugin; status validated `<400` |
| **TLS** | `BadCertificateAllowlistOverrides` | Custom certificate acceptance logic | Allowlist-based override (review before trusting on hostile networks) |
| **Secrets** | Auth tokens / sessions | Disclosure at rest | `flutter_secure_storage` + `encrypt` for sensitive KV |

### Observations & hardening notes

> [!CAUTION]
> The Connect server binds to **all interfaces** when enabled. The per-origin Accept/Decline prompt is the only authorization gate — there is no token/password. On untrusted networks, leave Connect off. Packagers should keep the default (`enableConnect=false`).

> [!WARNING]
> Plugins are arbitrary Hetu programs with network access. There is no capability sandbox limiting which hosts a plugin may contact. Users installing third-party plugins are extending their trust boundary to the plugin author. This is the most security-relevant area of the app.

- **License keys / signing:** Android release signing uses a local `key.properties`/keystore (not in-repo). `envied` is used for compile-time env (e.g. `Env.discordAppId`, release channel) — verify no secrets are baked into public builds.
- **`HttpOverrides.global`** disables some certificate strictness via an allowlist. Review `BadCertificateAllowlistOverrides` before distributing on networks where MITM is a concern.
- **No telemetry server of its own:** the app talks to plugin providers, GitHub/Codeberg (plugin discovery), Discord (RPC), Last.fm (scrobble), and Wikipedia (artist bios). Network reviewers should expect these destinations.

---

## 13. Internationalization

- **Framework:** Flutter `intl` + ARB files, configured by `l10n.yaml` (`arb-dir: lib/l10n`, template `app_en.arb`, generated into `lib/l10n/generated`).
- **Coverage:** **30 locale ARB files** (`lib/l10n/app_*.arb`).
- **Workflow:** `cli untranslated` emits missing strings (with the English source) for machine/human translation; `cli translated` merges a `tm.json` of translations back into the ARB files. Untranslated keys are tracked in `untranslated_messages.json`.
- **Runtime:** `L10n.all` lists supported locales; the language picker (`settings/sections/language_region.dart`) offers "System default" plus every locale, displayed with native names via `LanguageLocals`.

---

## 14. Operations & Troubleshooting

This section targets packagers and power users diagnosing field issues. (End-user troubleshooting is in the User Guide.)

### Diagnosing playback failures

The dominant failure class is audio resolution. The chain is: metadata track → `sourcedTrackProvider` → audio-source plugin → YouTube engine → stream URL → shelf proxy → `media_kit`.

1. **Confirm the layer.** If local-library tracks play but online tracks don't, the metadata/UI/server layers are fine — the fault is the audio-source plugin or engine.
2. **Switch engines.** `youtubeClientEngine` (NewPipe / YouTubeExplode / yt-dlp). NewPipe needs no binary; yt-dlp needs the binary path set (`KVStoreService.getYoutubeEnginePath`).
3. **Inspect logs.** Settings → Developers → Logs (or run with `--verbose`). Logs are stored on disk (`AppLogger.getLogsPath()`); the request proxy logs HEAD/GET per track in verbose.
4. **Self-healing.** The proxy already retries via `refreshStreamingUrl`/`swapWithNextSibling`; persistent failure usually means the engine itself is broken upstream.

### Known issue patterns (from the community tracker)

| Symptom | Root cause area |
|---|---|
| "No music playing", track at 0:00 | YouTube engine/extractor breakage |
| `DioException [bad response]` browsing | Plugin HTTP request failed / rate-limited |
| Queue shows 0:00 durations | Duration unit mismatch (seconds vs microseconds) between engine and matcher |
| Plays only from local library | Audio-source plugin not resolving |
| Cannot create playlist | Requires authenticated, write-capable metadata plugin |

### Log collection

```text
Settings → Developers → Logs → (copy icon) → paste into a GitHub issue
```

Include platform, app version (Settings → About), release channel, and the selected YouTube engine.

### Data locations

| Data | Location |
|---|---|
| SQLite DB | App support/documents dir (`sqlite3_flutter_libs` default) |
| Music cache | `cached_tracks` (app cache) / `Cached Tracks` (Android external cache) |
| Downloads | `downloadLocation` preference (platform default if empty) |
| Logs | `AppLogger.getLogsPath()` |
| Secure storage | OS keychain/keystore via `flutter_secure_storage` |

---

## 15. Code Quality & Conventions

- **Lints:** `package:flutter_lints/flutter.yaml`, with `file_names` and `avoid_renaming_method_parameters` disabled; `invalid_annotation_target` ignored. Generated files and `test/` are excluded from analysis.
- **State convention:** Riverpod everywhere; UI is `HookConsumerWidget`; providers expose notifiers for mutations. Side effects (DB writes, audio engine calls) live in notifiers, not widgets.
- **Routing convention:** every screen is a `@RoutePage` class with a static `name`; routes are code-generated by `auto_route`.
- **Codegen-heavy:** Drift, auto_route, json_serializable (`any_map: true`, `explicit_to_json: true`), freezed, envied, flutter_gen — `dart run build_runner build` is mandatory after model/route/schema edits.
- **Platform flags:** `lib/utils/platform.dart` exposes `kIsDesktop`, `kIsMobile`, `kIsAndroid`, `kIsIOS`, `kIsWindows`, `kIsMacOS`, `kIsLinux`, `kIsWeb`, `kIsFlatpak` — used pervasively to branch behavior.
- **Error handling:** centralized via `AppLogger.reportError(e, stack)`; the whole app runs in a guarded zone.
- **Naming:** feature folders mirror UI structure (`pages/`, `modules/`, `provider/`, `services/`). Models suffix domain objects (`SpotubeTrackObject`, `SpotubeFullTrackObject`, `SourcedTrack`).

### Complexity hotspots

- `lib/models/database/` (largest module by source size — generated Drift code dominates).
- `lib/collections/` (generated assets, routes, large catalogs like markets/locales).
- The audio proxy `routes/playback.dart` — non-trivial streaming/caching/range/HLS logic with self-healing fallbacks; the highest-risk handwritten code for regressions.

---

## 16. Potential Improvements

> [!NOTE]
> These are observations for maintainers, derived from the source. They are suggestions, not defects.

- **Plugin sandboxing/capabilities.** The biggest security lever: constrain which network hosts/APIs a plugin may reach, or surface a permissions manifest at install time. Today an installed plugin is fully trusted.
- **Connect authorization.** The per-origin Accept/Decline gate has no persistent pairing secret or token. A shared-secret/pairing-code scheme would harden LAN exposure beyond a one-time prompt.
- **Duration-unit robustness.** The recurring "0:00 in queue" bug stems from seconds↔microseconds confusion at the engine/matcher seam; a typed `Duration` boundary (rather than raw ints) would prevent the whole class.
- **Engine health surfacing.** Because YouTube engines break independently and often, an in-app "test engine" / auto-failover across engines would cut the most common support load.
- **`amoledDarkTheme` and `albumColorSync` toggles** exist in the preferences schema but their UI controls are commented out in `appearance.dart` — either wire them up or remove to avoid dead config.
- **`sleepTimerProvider` calls `exit(0)`** to stop playback — the code itself flags this may cause iOS App Store rejection. A graceful pause-and-background would be safer.
- **Git-pinned dependencies** (many forks) make builds fragile to upstream repo availability; vendoring or publishing the forks to pub would improve reproducibility.

---

## 17. Quick Reference

### Key files

| File | Role |
|---|---|
| `lib/main.dart` | Bootstrap & root widget |
| `lib/provider/user_preferences/user_preferences_provider.dart` | Settings notifier |
| `lib/models/database/tables/preferences.dart` | Preferences schema + defaults |
| `lib/provider/server/server.dart` | shelf server lifecycle |
| `lib/provider/server/router.dart` | Server routes |
| `lib/provider/server/routes/playback.dart` | Audio proxy + caching |
| `lib/provider/server/routes/connect.dart` | Connect WebSocket |
| `lib/provider/metadata_plugin/core/repositories.dart` | Plugin discovery |
| `lib/provider/download_manager_provider.dart` | Download queue |
| `cli/cli.dart` | Developer build CLI |

### Server routes

| Method | Route | Purpose |
|---|---|---|
| GET | `/ping` | Health (`pong`) |
| HEAD/GET | `/stream/<trackId>` | Audio stream proxy |
| GET | `/playback/toggle-playback` | Toggle play/pause |
| GET | `/playback/previous` | Previous |
| GET | `/playback/next` | Next |
| ALL | `/ws` | Connect control WebSocket |

### App CLI flags

| Flag | Short | Effect |
|---|---|---|
| `--verbose` | `-v` | Verbose logging (debug default on) |
| `--version` | | Print version, exit |
| `--help` | `-h` | Print usage, exit |

### Developer CLI commands

| Command | Purpose |
|---|---|
| `install-dependencies -p <platform> [-a <arch>]` | Install build deps |
| `build [-a <arch>] <platform>` | Build platform binary |
| `credits` | Generate credits list |
| `translated` / `untranslated` | Manage translations |

### Build essentials

```bash
flutter pub get
dart run build_runner build --delete-conflicting-outputs
flutter build <apk|windows|macos|linux|ipa>
```

### Identity

| Item | Value |
|---|---|
| App ID | `oss.krtirtho.spotube` |
| Version | 5.1.1+44 |
| Min Android | API 24 (Android 7) |
| Connect service | `_spotube._tcp` (mDNS) |
| DB engine | SQLite via Drift |
| License | BSD-4-Clause |

---

_Generated June 2026 from KRTirtho/spotube at 2cc8d26. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
