sealed class DownloadState { object Idle : DownloadState() data class Downloading( val type: DownloadType, val title: String, val itemId: String, val currentTrackId: String? = null ) : DownloadState() data class Completed( val type: DownloadType, val title: String, val successCount: Int, val failedCount: Int = 0, val skippedCount: Int = 0 ) : DownloadState() data class Error(val title: String, val message: String) : DownloadState()}
LocalMusicModels.kt - Data classes for local tracks/albums
MetadataEditor.kt - JAudioTagger integration for ID3 editing
MediaStore queries:
suspend fun getAllTracks(): List<LocalTrack> { val uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI val projection = arrayOf( MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.DATA, // File path MediaStore.Audio.Media.DURATION, MediaStore.Audio.Media.ALBUM_ID ) // Query and parse results...}
Playlist storage: JSON in SharedPreferences (playlists pref file)
Lyrics
Location: features/lyrics/
LrcLibApi.kt - LrcLib.net API client
LyricsRepository.kt - Lyrics fetching and caching
LrcParser.kt - LRC format parser with timestamp sync
LrcLibModels.kt - API response models
LRC format example:
[00:12.00]Line 1 of lyrics[00:17.20]Line 2 of lyrics[00:21.10]Line 3 of lyrics
Parsing logic:
data class LyricLine(val timeMs: Long, val text: String)object LrcParser { fun parse(lrcContent: String): List<LyricLine> { // Parse [mm:ss.xx] timestamps and text } fun getActiveLineIndex(lyrics: List<LyricLine>, positionMs: Long): Int { // Binary search for current lyric based on playback position }}
Music Player
Location: features/player/
MusicService.kt - Media3 foreground service
PlayerController.kt - Singleton managing playback state
PlayerState.kt - Player state data class
CustomBitmapLoader.kt - Album art loading for notifications
PlayerState:
data class PlayerState( val currentTrack: LocalTrack? = null, val isPlaying: Boolean = false, val currentPosition: Long = 0L, val duration: Long = 0L, val isShuffleEnabled: Boolean = false, val repeatMode: RepeatMode = RepeatMode.OFF, val volume: Float = 1f, val lyrics: List<LyricLine> = emptyList(), val currentLyricIndex: Int = -1, val isLoadingLyrics: Boolean = false, val isCurrentTrackFavorite: Boolean = false, val playingSource: String = "")
Media3 integration:
MediaController for playback control
MediaSession for notification integration
ExoPlayer for audio rendering
Rust FFI (UniFFI)
Location: features/rusteer/
RustDeezerService.kt - Kotlin wrapper around Rust library
pub mod api;pub mod converters;pub mod crypto;pub mod error;pub mod models;mod rusteer;pub mod tagging;// Main interface (recommended)pub use rusteer::{BatchDownloadResult, DownloadQuality, DownloadResult, Rusteer};// Low-level APIspub use api::{DeezerApi, GatewayApi};pub use error::DeezerError;pub use models::{Album, Artist, Playlist, Track};pub mod bindings;uniffi::setup_scaffolding!();
rusteer.rs
Main download orchestration:
pub struct Rusteer { gateway: GatewayApi, api: DeezerApi,}impl Rusteer { pub async fn new(arl: &str) -> Result<Self, DeezerError> { let gateway = GatewayApi::new(arl).await?; let api = DeezerApi::new(); Ok(Self { gateway, api }) } pub async fn download_track(&self, track_id: &str) -> Result<DownloadResult, DeezerError> { // 1. Fetch track metadata // 2. Get download URL from gateway // 3. Download encrypted chunks // 4. Decrypt using Blowfish // 5. Write ID3 tags // 6. Return file path } pub async fn download_album(&self, album_id: &str) -> Result<BatchDownloadResult, DeezerError> { // Download all tracks in album }}
crypto/mod.rs
Decryption implementation:
use blowfish::Blowfish;use cipher::{BlockDecrypt, KeyInit};use md5::{Digest, Md5};pub fn decrypt_track(data: &[u8], track_id: &str) -> Vec<u8> { // Generate Blowfish key from track ID let key = generate_blowfish_key(track_id); let cipher = Blowfish::new_from_slice(&key).unwrap(); // Decrypt chunks (every 3rd 2048-byte chunk is encrypted) let mut decrypted = Vec::new(); for (i, chunk) in data.chunks(2048).enumerate() { if i % 3 == 0 && chunk.len() == 2048 { // Decrypt this chunk let mut block = chunk.to_vec(); cipher.decrypt_blocks(&mut block); decrypted.extend_from_slice(&block); } else { // Copy unencrypted chunk decrypted.extend_from_slice(chunk); } } decrypted}fn generate_blowfish_key(track_id: &str) -> Vec<u8> { let secret = b"g4el58wc0zvf9na1"; let mut hasher = Md5::new(); hasher.update(track_id.as_bytes()); let hash = hasher.finalize(); // XOR hash with secret...}
tagging.rs
ID3 and FLAC tagging:
use lofty::{Accessor, AudioFile, Probe, TagExt};use lofty::id3::v2::Id3v2Tag;use std::path::Path;pub fn write_tags( file_path: &Path, title: &str, artist: &str, album: &str, track_number: Option<u32>, album_art: Option<&[u8]>,) -> Result<(), Box<dyn std::error::Error>> { let mut tagged_file = Probe::open(file_path)?.read()?; let mut tag = Id3v2Tag::new(); tag.set_title(title.to_string()); tag.set_artist(artist.to_string()); tag.set_album(album.to_string()); if let Some(num) = track_number { tag.set_track(num); } if let Some(art) = album_art { tag.set_picture(/* album art bytes */); } tagged_file.set_tag(tag.into()); tagged_file.save_to_path(file_path)?; Ok(())}
# Built application files*.apk*.aab*.ipa*.ap_*.aab# Files for the ART/Dalvik VM*.dex# Java class files*.class# Generated filesbin/gen/out/build/.gradle/# Gradle files.gradle/local.properties# IntelliJ.idea/*.iml*.iws*.ipr# Keystore files*.jks*.keystorekeystore.properties# Rust build artifactsrusteer/target/rusteer/Cargo.lock# OS files.DS_StoreThumbs.db