- Grant Permissions: On first launch, grant storage permissions and select your Music folder when prompted
- Configure Server: Enter your OwnTone server URL (e.g.,
http://192.168.1.100:3689) - Load Playlists: Tap "Load Playlists" to fetch available playlists from your server
- Select Playlists: Check the playlists you want to sync
- Navigate to the Sync tab
- Select playlists using checkboxes
- Optionally enable "Delete orphaned files" to remove tracks no longer in any selected playlist
- Tap Sync to start downloading
During sync, you'll see:
- Current playlist and track being processed
- Overall progress (tracks completed / total tracks)
- Cancel option (current track will finish downloading first)
- Navigate to Sync tab
- Tap Sync Schedule in the Sync Options card
- Enable automatic sync
- Configure schedule days and time
- Configure conditions:
- Only when charging: Prevent battery drain
- Only on WiFi: Avoid mobile data usage
- Save schedule
Background syncs run at the scheduled time you set (e.g., 2:00 AM daily). After each sync completes, the app automatically schedules the next occurrence.
Note on battery optimization: Android may delay or skip background syncs if battery optimization is enabled for the app. The app will prompt you to disable this when setting up scheduled syncs.
The app can track your music playback from other Android music players and sync play/skip statistics back to your OwnTone server.
To enable:
- Navigate to Sync tab, then Server Configuration
- Enable "Track Playback Events"
- Grant notification listener permission when prompted
The app uses Android's NotificationListener to detect when tracks are played or skipped in music players like PowerAmp, Auxio, Vinyl Music Player, etc. Events are synced to your OwnTone server during the next sync.
The Browse tab lets you view your synced music library by:
- Playlists
- Artists
- Albums
- Tracks
Note: This shows only the music synced from OwnTone, not all music files on your device. Other music players may have additional tracks that weren't synced by this app.
The History tab shows a log of past sync operations including:
- Sync timestamp and duration
- Number of playlists synced
- Tracks downloaded and deleted
- Success/failure status
- Error messages (if any)
Downloaded music is stored in:
/storage/emulated/0/Music/
├── tracks/ # Audio files (artist_album_trackid.ext)
└── playlists/ # M3U playlist files
After syncing, open your preferred music player app and:
- Scan for new media (most apps do this automatically)
- Navigate to playlists
- Look for your synced playlists by name
The app follows a three-layer architecture:
lib/
├── data/ # Data Layer
│ ├── models/ # Data models (Playlist, Track, SyncSchedule, etc.)
│ ├── repositories/ # Data access
│ │ ├── owntone_api_repository.dart # OwnTone API client
│ │ ├── local_database_repository.dart # SQLite operations
│ │ └── file_system_repository.dart # File I/O coordination
│ └── database/
│ └── database_helper.dart # SQLite schema
├── domain/ # Business Logic Layer
│ └── services/
│ └── permissions_service.dart # Android permissions
└── presentation/ # UI Layer
├── screens/ # Full-page views
├── widgets/ # Reusable UI components
└── providers/ # State management (Provider pattern)
Android (Kotlin) components:
android/app/src/main/kotlin/dev/educoder/owntone_sync/
├── MainActivity.kt # Main activity, handles permissions and method channels
├── BackgroundSyncWorker.kt # WorkManager worker for background sync
├── MediaNotificationListener.kt # NotificationListener for tracking playback events
├── OwnToneApiClient.kt # HTTP client for OwnTone API
├── DatabaseHelper.kt # SQLite operations (Kotlin side)
├── FileOperations.kt # SAF file operations
└── SyncProgressBroadcaster.kt # Progress notifications and EventChannel bridge
synced_playlists
- Stores playlists selected for sync
pathis the stable identifier (survives server database resets)
synced_tracks
- Downloaded track metadata and local file paths
- No foreign key constraints to allow orphan detection
playlist_tracks
- Join table linking playlists to tracks
- Cascade delete on playlist removal
- Intentionally no FK to tracks (orphan detection)
playlist_cache
- Caches playlist metadata for offline viewing
pending_events
- Stores playback events (play/skip) to sync back to server
- Includes retry_count for failed sync attempts (max 5 retries)
sync_history
- Records of past sync operations with statistics
sync_history_playlists
- Details of which playlists were included in each sync
- Flutter 3.5+: Cross-platform UI framework
- Provider: State management
- sqflite: SQLite database
- Dio: HTTP client with download progress
- workmanager: Background task scheduling (Android WorkManager wrapper)
- path_provider: File system access
- permission_handler: Android permissions
- Storage Access Framework (SAF): Android's secure file access system
The app uses OwnTone's REST API and DAAP protocol:
REST Endpoints:
GET /api/library/playlists- List playlistsGET /api/library/playlists/{id}/tracks- List tracks in playlistGET /api/library/tracks/{id}- Get track metadataPUT /api/library/tracks/{id}- Update track statistics (play count, skip count)
DAAP Downloads:
GET /databases/1/items/{id}.dat- Download track file- Header:
Accept-Codecs: mpeg,alac,flac,wav
File extensions determined from Content-Type header via HEAD request.
Event Sync (runs first, if enabled):
- Fetch unsynced events from local database
- Group events by track_id
- For each track, fetch current stats from server
- Accumulate new play/skip counts with server counts
- Update timestamps (use most recent)
- Send PUT request to update server
- Delete successfully synced events
- Retry failed events (up to 5 times, then delete)
Playlist Sync:
- Fetch server playlists - Load current state from OwnTone
- Playlist recovery - If playlist ID changed, recover by path
- Diff calculation - Compare server vs local to find new/missing tracks
- Download tracks - Only download if file doesn't exist locally
- Rebuild relationships - Clear and rebuild playlist-track join table
- Generate M3U files - Create playlist files with relative paths
- Cleanup - Optionally delete orphaned tracks
Background sync uses Android WorkManager:
- Registration - When schedule enabled, calculate delay until next scheduled time and register one-time work request
- Execution - Work runs at scheduled time if conditions met (WiFi, charging)
- Event sync - If enabled, sync playback events first
- Playlist sync - Download new tracks, update relationships
- Progress broadcasting - Via foreground notification and EventChannel to Flutter
- Schedule next - After completion, calculate and schedule tomorrow's sync
- History logging - Record sync results in database
WorkManager ensures:
- Respects battery optimization settings
- Waits for required conditions (WiFi, charging)
- Survives app restarts and device reboots
- Provides foreground notification during sync
Event tracking uses Android's NotificationListenerService:
- Notification monitoring - Listen for media notifications from music players
- MediaController extraction - Get playback state from notification's MediaSession
- Position tracking - Track last known position and timestamp for extrapolation
- State change detection:
- Track change: Extrapolate final position from last update, determine play vs skip
- Pause: Update position and timestamp
- Stop: Process track end with extrapolated position
- Event classification:
- Play: Track reached ≥90% completion
- Skip: Track played ≥3 seconds but <90% completion
- Ignore: Track played <3 seconds
- Track matching - Fuzzy match against local database (title, artist, duration ±5s)
- Event storage - Store in pending_events table for next sync
All music file operations go through Android's SAF:
- User grants access to Music folder via system picker
- App stores persistent URI permission
- File operations use DocumentFile API
- Artwork stored in app-private storage (no SAF needed)
- M3U playlists use relative paths for compatibility
Sync progress flows from Kotlin to Flutter:
- BackgroundSyncWorker updates progress
- SyncProgressBroadcaster creates foreground notification
- EventChannel broadcasts progress to Flutter
- SyncProvider receives updates via stream
- UI displays progress in real-time
Prerequisites:
- Flutter SDK 3.5 or higher
- Android SDK (min SDK 29, target SDK 35)
- Android device or emulator
Setup:
flutter pub get
flutter runManual testing checklist:
- Server configuration and validation
- Playlist loading (online and offline)
- Manual sync with progress tracking
- Sync cancellation
- Orphaned file deletion
- Schedule configuration
- Background sync execution (check via notification)
- Playlist recovery after server DB reset
- Permission handling (storage, notification listener, battery optimization)
- Event tracking (play/skip detection in various music players)
- Event sync to server
Your OwnTone server must be:
- Accessible on your local network
- Running with remote access enabled
- Not requiring authentication (or using basic auth)
Storage usage depends on your library:
- Average MP3: ~5-10 MB per track
- Lossless FLAC: ~30-50 MB per track
- Ensure sufficient free space before syncing large playlists
- Local network access to OwnTone server
Playlists won't load
- Verify server URL is correct
- Check server is running:
systemctl status owntone - Ensure device is on same network as server
Sync fails immediately
- Check storage permissions are granted
- Verify sufficient free space
- Check server logs for errors
Background sync not working
- Verify schedule is enabled and saved
- Check battery optimization is disabled for the app
- Review Android logs:
adb logcat | grep BackgroundSyncWorker
Files not appearing in music player
- Trigger media scan: Settings → Storage → Cached data → Clear
- Check file location:
/storage/emulated/0/Music/ - Verify M3U files exist in
playlists/directory
Event tracking not working
- Verify notification listener permission is granted
- Check the setting is enabled in Server Configuration
- Review logs:
adb logcat | grep MediaNotificationListener - Some music players may not expose MediaSession properly
This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details.