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
- Executive Summary
- Architecture Overview
- Repository Structure
- Technology Stack
- State Management & Data Flow
- The Plugin System (BYOMM)
- Audio Pipeline & Local Server
- Persistence Layer
- Configuration Reference
- Build & Release
- Platform Integration
- Security Analysis
- Internationalization
- Operations & Troubleshooting
- Code Quality & Conventions
- Potential Improvements
- 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 |
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:
- Handle the special
web_view_title_barargument (desktop webview helper) and return early if matched. - Parse CLI args via
startCLI()(lib/services/cli/cli.dart) — handles--verbose/-v,--version,--help/-h. - Initialize logging (
AppLogger.initialize) and run everything inside a guarded zone (AppLogger.runZoned). - Install a
BadCertificateAllowlistOverridesHttpOverridesglobal. - Initialize timezones, native splash,
MediaKit. - Run macOS sandbox→no-sandbox migration.
- Platform tweaks: high refresh rate (Android),
NewPipeExtractor.init()(Android/desktop),MetadataGod(non-web). - Initialize key-value stores (
KVStoreService,EncryptedKvStoreService), the DriftAppDatabase. - Desktop: prevent-close, set yt-dlp binary path, Discord RPC init, local notifier, window manager tools.
- Windows:
SMTCWindows.initialize(). iOS: set home-widget app group. runAppinside aProviderScope(overridingdatabaseProvider, attachingAppLoggerProviderObserver) rendering the rootSpotubewidget.
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 DartCommandRunnerused by maintainers to build and manage the project. It is excluded from the app build (build.yamlexcludesbin/*.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 patchedflutter_secure_storage_linux). Reproducible builds require network access to these repos at the pinned refs. Auditdependency_overridesinpubspec.yamlbefore packaging.
5. State Management & Data Flow
Spotube uses Riverpod (hooks variant) as its single source of runtime truth. Widgets are HookConsumerWidgets 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
- UI calls
audioPlayerNotifier.load([track], autoPlay: true). - For each non-local track,
sourcedTrackProvider(track)runsSourcedTrack.fetchFromTrack(...), which uses the audio-source plugin to match and resolve a stream (using the selected YouTube engine and quality presets). - The track is enqueued into
media_kitas aSpotubeMedia, whose URI points at the local shelf server (/stream/<trackId>). media_kitrequests that URL; the server'sServerPlaybackRoutes.streamTrackproxies 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 aSpotube Metadata Pluginfile type).
Two plugins ship bundled as assets (assets/plugins/..., declared in pubspec.yaml):
spotube-plugin-musicbrainz-listenbrainz/plugin.smplugspotube-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/audioSourcePluginAuthenticatedProviderwatch the plugin'sauth.authStateStreamand exposeisAuthenticated(). - Scrobble (
core/scrobble.dart): when the default metadata plugin supportsscrobbling, plays are pushed toplugin.core.scrobble({...})with ISRC, duration, and timestamps. - User (
core/user.dart):metadataPluginUserProviderreturns the logged-inSpotubeUserObjectwhen 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.anyIPv4when Connect is enabled, otherwiseInternetAddress.loopbackIPv4(localhost-only). - Port:
connectPortpreference.-1means choose a random port in[5000, 22500)once and store it onSpotubeMedia.serverPort; otherwise the configured port. - Pipeline:
pipelineProvideraddslogRequests()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
cacheMusicis on and a cache file exists, serves the cached bytes with propercontent-range/accept-rangesheaders (range support for seeking). - If the upstream returns an HLS playlist (
application/vnd.apple.mpegurl), responds with a301redirect to let the player handle HLS range requests itself. - Otherwise streams the body through while simultaneously writing a
.partcache file; on completion it renames.part→final and writes ID3/metadata tags viaMetadataGod(skippingweba). - 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:portorigin (cached in_allowedConnections). Declined connections receive aWebSocketErrorEventand 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
youtubeClientEnginehas a platform-dependent default: NewPipe everywhere except iOS, where NewPipe is unavailable so it defaults to YouTubeExplode. Engine availability is gated byYoutubeClientEngine.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
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
✓ Built build/app/outputs/flutter-apk/app-release.apk
Important
Run
build_runnerafter any change to Drift tables, routes,@freezed/json_serializablemodels, orenviedenv 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 |
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).enviedis used for compile-time env (e.g.Env.discordAppId, release channel) — verify no secrets are baked into public builds. HttpOverrides.globaldisables some certificate strictness via an allowlist. ReviewBadCertificateAllowlistOverridesbefore 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 byl10n.yaml(arb-dir: lib/l10n, templateapp_en.arb, generated intolib/l10n/generated). - Coverage: 30 locale ARB files (
lib/l10n/app_*.arb). - Workflow:
cli untranslatedemits missing strings (with the English source) for machine/human translation;cli translatedmerges atm.jsonof translations back into the ARB files. Untranslated keys are tracked inuntranslated_messages.json. - Runtime:
L10n.alllists supported locales; the language picker (settings/sections/language_region.dart) offers "System default" plus every locale, displayed with native names viaLanguageLocals.
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.
- 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.
- Switch engines.
youtubeClientEngine(NewPipe / YouTubeExplode / yt-dlp). NewPipe needs no binary; yt-dlp needs the binary path set (KVStoreService.getYoutubeEnginePath). - 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. - 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
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, withfile_namesandavoid_renaming_method_parametersdisabled;invalid_annotation_targetignored. Generated files andtest/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
@RoutePageclass with a staticname; routes are code-generated byauto_route. - Codegen-heavy: Drift, auto_route, json_serializable (
any_map: true,explicit_to_json: true), freezed, envied, flutter_gen —dart run build_runner buildis mandatory after model/route/schema edits. - Platform flags:
lib/utils/platform.dartexposeskIsDesktop,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
Durationboundary (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.
amoledDarkThemeandalbumColorSynctoggles exist in the preferences schema but their UI controls are commented out inappearance.dart— either wire them up or remove to avoid dead config.sleepTimerProvidercallsexit(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
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 |