Jesse A. Green / notebook

Spotube Admin & Developer Guide

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.

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
  2. Architecture Overview
  3. Repository Structure
  4. Technology Stack
  5. State Management & Data Flow
  6. The Plugin System (BYOMM)
  7. Audio Pipeline & Local Server
  8. Persistence Layer
  9. Configuration Reference
  10. Build & Release
  11. Platform Integration
  12. Security Analysis
  13. Internationalization
  14. Operations & Troubleshooting
  15. Code Quality & Conventions
  16. Potential Improvements
  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

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 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

  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

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_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
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

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

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