├── .gitignore
├── LICENSE.md
├── PRIVACY.md
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── mrsep
│ │ └── musicrecognizer
│ │ └── ExampleInstrumentedTest.kt
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── mrsep
│ │ └── musicrecognizer
│ │ ├── MusicRecognizerApp.kt
│ │ ├── crash
│ │ └── PermissionsCollector.kt
│ │ ├── di
│ │ ├── AndroidAppRestarter.kt
│ │ ├── AppDeeplinkRouter.kt
│ │ ├── GlueModule.kt
│ │ └── ServiceStarter.kt
│ │ └── presentation
│ │ ├── AppNavigation.kt
│ │ ├── AppNavigationBar.kt
│ │ ├── AppNavigationRail.kt
│ │ ├── MainActivity.kt
│ │ ├── MainActivityViewModel.kt
│ │ └── TopLevelDestination.kt
│ └── res
│ ├── drawable
│ ├── ic_shortcut_background.xml
│ └── ic_shortcut_recognize.xml
│ ├── raw
│ └── aboutlibraries.json
│ ├── resources.properties
│ ├── values-night
│ └── themes.xml
│ ├── values
│ ├── colors.xml
│ └── themes.xml
│ └── xml
│ ├── backup_rules.xml
│ ├── data_extraction_rules.xml
│ └── files.xml
├── build-logic
├── convention
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── kotlin
│ │ ├── AndroidApplicationComposeConventionPlugin.kt
│ │ ├── AndroidApplicationConventionPlugin.kt
│ │ ├── AndroidFeatureConventionPlugin.kt
│ │ ├── AndroidLibraryComposeConventionPlugin.kt
│ │ ├── AndroidLibraryConventionPlugin.kt
│ │ ├── HiltConventionPlugin.kt
│ │ ├── JvmLibraryConventionPlugin.kt
│ │ └── com
│ │ └── mrsep
│ │ └── musicrecognizer
│ │ ├── AndroidCompose.kt
│ │ ├── Detekt.kt
│ │ ├── KotlinAndroid.kt
│ │ └── ProjectExtensions.kt
├── gradle.properties
└── settings.gradle.kts
├── build.gradle.kts
├── compose_compiler_config.conf
├── core
├── audio
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── mrsep
│ │ │ └── musicrecognizer
│ │ │ └── core
│ │ │ └── audio
│ │ │ ├── audioplayer
│ │ │ ├── MediaPlayerController.kt
│ │ │ ├── PlayerController.kt
│ │ │ └── PlayerStatus.kt
│ │ │ ├── audiorecord
│ │ │ ├── AudioCaptureConfig.kt
│ │ │ ├── AudioRecordDispatcher.kt
│ │ │ ├── AudioRecordingControllerFactory.kt
│ │ │ ├── DeviceFirstAdtsRecordingController.kt
│ │ │ ├── encoder
│ │ │ │ ├── AdtsRecordingController.kt
│ │ │ │ └── UnsafeSilenceTracker.kt
│ │ │ └── soundsource
│ │ │ │ ├── DefaultSoundSource.kt
│ │ │ │ ├── SoundLevelMeter.kt
│ │ │ │ ├── SoundSource.kt
│ │ │ │ ├── SoundSourceConfig.kt
│ │ │ │ ├── SoundSourceConfigProvider.kt
│ │ │ │ └── VisualizerSoundSource.kt
│ │ │ └── di
│ │ │ └── AudioModule.kt
│ │ └── test
│ │ └── java
│ │ └── com
│ │ └── mrsep
│ │ └── musicrecognizer
│ │ └── core
│ │ └── audio
│ │ └── audiorecord
│ │ └── encoder
│ │ └── UnsafeSilenceTrackerTest.kt
├── common
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── java
│ │ └── com
│ │ └── mrsep
│ │ └── musicrecognizer
│ │ └── core
│ │ └── common
│ │ ├── BidirectionalMapper.kt
│ │ ├── Containter.kt
│ │ ├── DispatchersProvider.kt
│ │ ├── Mapper.kt
│ │ ├── di
│ │ ├── CoroutineScopeModule.kt
│ │ ├── DispatcherModule.kt
│ │ ├── DispatchersProviderModule.kt
│ │ └── UtilModule.kt
│ │ └── util
│ │ ├── AppDateTimeFormatter.kt
│ │ ├── ContextExt.kt
│ │ └── NavigationExt.kt
├── data
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── mrsep
│ │ └── musicrecognizer
│ │ └── core
│ │ └── data
│ │ ├── ConnectivityManagerNetworkMonitor.kt
│ │ ├── di
│ │ └── RepositoryModule.kt
│ │ ├── enqueued
│ │ ├── EnqueuedRecognitionMapper.kt
│ │ ├── EnqueuedRecognitionRepositoryImpl.kt
│ │ ├── RecordingFileDataSource.kt
│ │ └── RecordingFileDataSourceImpl.kt
│ │ ├── preferences
│ │ ├── PreferencesRepositoryImpl.kt
│ │ ├── PreferencesToDomainMapper.kt
│ │ └── PreferencesToProtoMapper.kt
│ │ └── track
│ │ ├── TrackMapper.kt
│ │ ├── TrackPreviewMapper.kt
│ │ └── TrackRepositoryImpl.kt
├── database
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ ├── schemas
│ │ └── com.mrsep.musicrecognizer.core.database.ApplicationDatabase
│ │ │ ├── 1.json
│ │ │ ├── 2.json
│ │ │ ├── 3.json
│ │ │ ├── 4.json
│ │ │ ├── 5.json
│ │ │ ├── 6.json
│ │ │ ├── 7.json
│ │ │ └── 8.json
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── mrsep
│ │ └── musicrecognizer
│ │ └── core
│ │ └── database
│ │ ├── ApplicationDatabase.kt
│ │ ├── DatabaseBackupProvider.kt
│ │ ├── DatabaseUtils.kt
│ │ ├── DurationRoomConverter.kt
│ │ ├── FileRoomConverter.kt
│ │ ├── InstantRoomConverter.kt
│ │ ├── LocalDateRoomConverter.kt
│ │ ├── di
│ │ └── DatabaseModule.kt
│ │ ├── enqueued
│ │ ├── EnqueuedRecognitionDao.kt
│ │ └── model
│ │ │ ├── EnqueuedRecognitionEntity.kt
│ │ │ ├── EnqueuedRecognitionEntityWithTrack.kt
│ │ │ └── RemoteRecognitionResultType.kt
│ │ ├── migration
│ │ ├── AutoMigrationSpec3To4.kt
│ │ ├── DatabaseMigrationUtils.kt
│ │ ├── Migration5To6.kt
│ │ ├── Migration6To7.kt
│ │ └── Migration7To8.kt
│ │ └── track
│ │ ├── TrackDao.kt
│ │ ├── TrackEntity.kt
│ │ └── TrackPreviewTuple.kt
├── datastore
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── com
│ │ │ └── mrsep
│ │ │ └── musicrecognizer
│ │ │ └── core
│ │ │ └── datastore
│ │ │ ├── DatastoreModule.kt
│ │ │ ├── RequiredMusicServicesMigration.kt
│ │ │ └── UserPreferencesProtoSerializer.kt
│ │ └── proto
│ │ ├── acr_cloud_config_proto.proto
│ │ ├── audio_capture_mode_proto.proto
│ │ ├── music_service_proto.proto
│ │ ├── recognition_provider_proto.proto
│ │ └── user_preferences_proto.proto
├── domain
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── mrsep
│ │ └── musicrecognizer
│ │ └── core
│ │ └── domain
│ │ ├── preferences
│ │ ├── FallbackPolicy.kt
│ │ ├── HapticFeedback.kt
│ │ ├── LyricsStyle.kt
│ │ ├── PreferencesRepository.kt
│ │ ├── RecognitionServiceConfig.kt
│ │ ├── TrackFilter.kt
│ │ └── UserPreferences.kt
│ │ ├── recognition
│ │ ├── AudioRecording.kt
│ │ ├── AudioRecordingController.kt
│ │ ├── ConfigValidator.kt
│ │ ├── EnqueuedRecognitionRepository.kt
│ │ ├── EnqueuedRecognitionScheduler.kt
│ │ ├── NetworkMonitor.kt
│ │ ├── RecognitionInteractor.kt
│ │ ├── RecognitionServiceFactory.kt
│ │ ├── RemoteRecognitionService.kt
│ │ ├── TrackMetadataEnhancer.kt
│ │ ├── TrackMetadataEnhancerScheduler.kt
│ │ └── model
│ │ │ ├── EnqueuedRecognition.kt
│ │ │ ├── RecognitionProvider.kt
│ │ │ ├── RecognitionResult.kt
│ │ │ ├── RecognitionStatus.kt
│ │ │ ├── RecognitionTask.kt
│ │ │ ├── RecordingScheme.kt
│ │ │ ├── RemoteMetadataEnhancingResult.kt
│ │ │ └── RemoteRecognitionResult.kt
│ │ └── track
│ │ ├── TrackRepository.kt
│ │ └── model
│ │ ├── Lyrics.kt
│ │ ├── MusicService.kt
│ │ ├── SearchResult.kt
│ │ ├── Track.kt
│ │ ├── TrackDataField.kt
│ │ └── TrackPreview.kt
├── network
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── mrsep
│ │ └── musicrecognizer
│ │ └── core
│ │ └── network
│ │ ├── HttpFileLoggingInterceptor.kt
│ │ └── NetworkModule.kt
├── recognition
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ ├── androidTest
│ │ └── java
│ │ │ └── com
│ │ │ └── mrsep
│ │ │ └── musicrecognizer
│ │ │ └── core
│ │ │ └── recognition
│ │ │ └── lyrics
│ │ │ └── LyricsConverterTest.kt
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── assets
│ │ │ └── test_sample_3500ms.ogg
│ │ └── java
│ │ │ └── com
│ │ │ └── mrsep
│ │ │ └── musicrecognizer
│ │ │ └── core
│ │ │ └── recognition
│ │ │ ├── ConfigValidatorImpl.kt
│ │ │ ├── RecognitionServiceFactoryImpl.kt
│ │ │ ├── acrcloud
│ │ │ ├── AcrCloudRecognitionService.kt
│ │ │ └── json
│ │ │ │ ├── AcrCloudResponseJson.kt
│ │ │ │ └── AcrCloudResponseJsonMapper.kt
│ │ │ ├── artwork
│ │ │ ├── ArtworkFetcher.kt
│ │ │ ├── ArtworkFetcherImpl.kt
│ │ │ └── TrackArtwork.kt
│ │ │ ├── audd
│ │ │ ├── AuddRecognitionService.kt
│ │ │ ├── json
│ │ │ │ ├── AppleMusicJson.kt
│ │ │ │ ├── AuddResponseJson.kt
│ │ │ │ ├── AuddResponseJsonMapper.kt
│ │ │ │ ├── DeezerJson.kt
│ │ │ │ ├── LyricsJson.kt
│ │ │ │ ├── MusicbrainzJson.kt
│ │ │ │ ├── NapsterJson.kt
│ │ │ │ └── SpotifyJson.kt
│ │ │ └── websocket
│ │ │ │ ├── SocketEvent.kt
│ │ │ │ ├── WebSocketSession.kt
│ │ │ │ └── WebSocketSessionImpl.kt
│ │ │ ├── di
│ │ │ └── RecognitionModule.kt
│ │ │ ├── enhancer
│ │ │ └── odesli
│ │ │ │ ├── OdesliApiProviderSerializer.kt
│ │ │ │ ├── OdesliMetadataEnhancer.kt
│ │ │ │ ├── OdesliResponseJson.kt
│ │ │ │ └── OdesliResponseJsonMapper.kt
│ │ │ └── lyrics
│ │ │ ├── LrcLibResponseJson.kt
│ │ │ ├── LyricsConverter.kt
│ │ │ ├── LyricsFetcher.kt
│ │ │ └── LyricsFetcherImpl.kt
│ │ └── test
│ │ ├── java
│ │ ├── android
│ │ │ └── util
│ │ │ │ └── Log.kt
│ │ └── com
│ │ │ └── mrsep
│ │ │ └── musicrecognizer
│ │ │ └── core
│ │ │ └── recognition
│ │ │ ├── AuddRecognitionServiceTest.kt
│ │ │ ├── Fakes.kt
│ │ │ └── WebSocketBaseFake.kt
│ │ └── resources
│ │ ├── audd_response_auth_error.json
│ │ ├── audd_response_no_matches.json
│ │ └── audd_response_success.json
├── strings
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── res
│ │ ├── values-cs
│ │ └── strings.xml
│ │ ├── values-de
│ │ └── strings.xml
│ │ ├── values-es
│ │ └── strings.xml
│ │ ├── values-et
│ │ └── strings.xml
│ │ ├── values-fa
│ │ └── strings.xml
│ │ ├── values-fr
│ │ └── strings.xml
│ │ ├── values-gu
│ │ └── strings.xml
│ │ ├── values-it
│ │ └── strings.xml
│ │ ├── values-nl
│ │ └── strings.xml
│ │ ├── values-pl
│ │ └── strings.xml
│ │ ├── values-pt-rBR
│ │ └── strings.xml
│ │ ├── values-pt
│ │ └── strings.xml
│ │ ├── values-ro
│ │ └── strings.xml
│ │ ├── values-ru
│ │ └── strings.xml
│ │ ├── values-sk
│ │ └── strings.xml
│ │ ├── values-tr
│ │ └── strings.xml
│ │ ├── values-vi
│ │ └── strings.xml
│ │ └── values
│ │ └── strings.xml
└── ui
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ ├── debug
│ └── res
│ │ └── color
│ │ └── gradient_ic_launcher_foreground.xml
│ └── main
│ ├── java
│ └── com
│ │ └── mrsep
│ │ └── musicrecognizer
│ │ └── core
│ │ └── ui
│ │ ├── Extenstions.kt
│ │ ├── components
│ │ ├── DialogComponents.kt
│ │ ├── LoadingStub.kt
│ │ ├── MultiSelectionState.kt
│ │ ├── PasswordInputField.kt
│ │ ├── PermissionDialogs.kt
│ │ ├── Vinyl.kt
│ │ └── preferences
│ │ │ ├── PreferenceClickableItem.kt
│ │ │ ├── PreferenceGroup.kt
│ │ │ └── PreferenceSwitchItem.kt
│ │ ├── modifiers
│ │ └── AnimationModifier.kt
│ │ ├── theme
│ │ ├── Color.kt
│ │ ├── Theme.kt
│ │ └── Type.kt
│ │ └── util
│ │ ├── ContextExt.kt
│ │ ├── DimensionUtil.kt
│ │ └── ForwardingPainter.kt
│ └── res
│ ├── color-night-v31
│ ├── surface.xml
│ ├── surface_container_high.xml
│ └── surface_container_highest.xml
│ ├── color-v31
│ ├── surface.xml
│ ├── surface_container_high.xml
│ └── surface_container_highest.xml
│ ├── color
│ └── gradient_ic_launcher_foreground.xml
│ ├── drawable
│ ├── ic_amazon_24.xml
│ ├── ic_anghami_24.xml
│ ├── ic_apple_24.xml
│ ├── ic_audiomack_24.xml
│ ├── ic_audius_24.xml
│ ├── ic_boomplay_24.xml
│ ├── ic_deezer_24.xml
│ ├── ic_launcher_background.xml
│ ├── ic_launcher_foreground.xml
│ ├── ic_lines_24.xml
│ ├── ic_musicbrainz_24.xml
│ ├── ic_napster_24.xml
│ ├── ic_notification_listening.xml
│ ├── ic_notification_ready.xml
│ ├── ic_pandora_24.xml
│ ├── ic_retro_microphone.xml
│ ├── ic_soundcloud_24.xml
│ ├── ic_splash.xml
│ ├── ic_splash_foreground.xml
│ ├── ic_spotify_24.xml
│ ├── ic_tidal_24.xml
│ ├── ic_yandex_music_24.xml
│ ├── ic_youtube_24.xml
│ ├── ic_youtube_music_24.xml
│ ├── outline_album_24.xml
│ ├── outline_album_fill1_24.xml
│ ├── outline_arrow_back_24.xml
│ ├── outline_arrow_forward_24.xml
│ ├── outline_audio_file_24.xml
│ ├── outline_audio_file_fill1_24.xml
│ ├── outline_auto_mode_24.xml
│ ├── outline_cancel_schedule_send_24.xml
│ ├── outline_check_24.xml
│ ├── outline_close_24.xml
│ ├── outline_cloud_off_24.xml
│ ├── outline_content_copy_24.xml
│ ├── outline_dark_mode_24.xml
│ ├── outline_dark_mode_fill1_24.xml
│ ├── outline_delete_24.xml
│ ├── outline_deselect_24.xml
│ ├── outline_edit_24.xml
│ ├── outline_error_24.xml
│ ├── outline_favorite_24.xml
│ ├── outline_favorite_fill1_24.xml
│ ├── outline_filter_list_24.xml
│ ├── outline_format_size_24.xml
│ ├── outline_help_24.xml
│ ├── outline_image_24.xml
│ ├── outline_info_24.xml
│ ├── outline_key_off_24.xml
│ ├── outline_keyboard_arrow_down_24.xml
│ ├── outline_keyboard_arrow_up_24.xml
│ ├── outline_library_music_24.xml
│ ├── outline_library_music_fill1_24.xml
│ ├── outline_light_mode_24.xml
│ ├── outline_light_mode_fill1_24.xml
│ ├── outline_lines_48.xml
│ ├── outline_lines_shift1_48.xml
│ ├── outline_link_24.xml
│ ├── outline_list_24.xml
│ ├── outline_lyrics_24.xml
│ ├── outline_more_vert_24.xml
│ ├── outline_overview_24.xml
│ ├── outline_overview_fill1_24.xml
│ ├── outline_pause_24.xml
│ ├── outline_pause_fill1_24.xml
│ ├── outline_play_arrow_24.xml
│ ├── outline_play_arrow_fill1_24.xml
│ ├── outline_replay_24.xml
│ ├── outline_schedule_send_24.xml
│ ├── outline_search_24.xml
│ ├── outline_select_all_24.xml
│ ├── outline_send_24.xml
│ ├── outline_settings_24.xml
│ ├── outline_settings_fill1_24.xml
│ ├── outline_share_24.xml
│ ├── outline_speed_24.xml
│ ├── outline_stop_fill1_24.xml
│ ├── outline_travel_explore_24.xml
│ ├── outline_tune_24.xml
│ ├── outline_visibility_24.xml
│ ├── outline_visibility_off_24.xml
│ ├── rounded_bug_report_fill1_24.xml
│ ├── rounded_music_note_48.xml
│ ├── rounded_pause_48.xml
│ ├── rounded_play_arrow_48.xml
│ ├── rounded_priority_high_48.xml
│ └── rounded_question_mark_48.xml
│ ├── mipmap-anydpi-v26
│ └── ic_launcher.xml
│ ├── values-night-v31
│ └── colors.xml
│ ├── values-night-v34
│ └── colors.xml
│ ├── values-night
│ └── colors.xml
│ ├── values-v31
│ └── colors.xml
│ ├── values-v34
│ └── colors.xml
│ └── values
│ └── colors.xml
├── fastlane
└── metadata
│ └── android
│ ├── cs
│ ├── full_description.txt
│ ├── short_description.txt
│ └── title.txt
│ ├── en-US
│ ├── changelogs
│ │ ├── 1.txt
│ │ ├── 10.txt
│ │ ├── 11.txt
│ │ ├── 12.txt
│ │ ├── 13.txt
│ │ ├── 14.txt
│ │ ├── 15.txt
│ │ ├── 16.txt
│ │ ├── 17.txt
│ │ ├── 18.txt
│ │ ├── 19.txt
│ │ ├── 2.txt
│ │ ├── 20.txt
│ │ ├── 21.txt
│ │ ├── 22.txt
│ │ ├── 23.txt
│ │ ├── 24.txt
│ │ ├── 25.txt
│ │ ├── 26.txt
│ │ ├── 27.txt
│ │ ├── 28.txt
│ │ ├── 29.txt
│ │ ├── 3.txt
│ │ ├── 30.txt
│ │ ├── 4.txt
│ │ ├── 5.txt
│ │ ├── 6.txt
│ │ ├── 7.txt
│ │ ├── 8.txt
│ │ └── 9.txt
│ ├── full_description.txt
│ ├── images
│ │ ├── featureGraphic.png
│ │ ├── icon.png
│ │ └── phoneScreenshots
│ │ │ ├── 00.png
│ │ │ ├── 01.png
│ │ │ ├── 02.png
│ │ │ ├── 03.png
│ │ │ ├── 04.png
│ │ │ ├── 05.png
│ │ │ ├── 06.png
│ │ │ └── 07.png
│ ├── short_description.txt
│ └── title.txt
│ ├── es-ES
│ ├── full_description.txt
│ └── short_description.txt
│ ├── et
│ ├── full_description.txt
│ └── short_description.txt
│ ├── fr
│ ├── full_description.txt
│ ├── short_description.txt
│ └── title.txt
│ ├── it-IT
│ ├── full_description.txt
│ └── short_description.txt
│ ├── pl-PL
│ └── short_description.txt
│ ├── pt-BR
│ ├── full_description.txt
│ └── short_description.txt
│ ├── pt
│ ├── full_description.txt
│ └── short_description.txt
│ ├── ru
│ ├── changelogs
│ │ ├── 1.txt
│ │ ├── 10.txt
│ │ ├── 11.txt
│ │ ├── 12.txt
│ │ ├── 13.txt
│ │ ├── 14.txt
│ │ ├── 15.txt
│ │ ├── 16.txt
│ │ ├── 17.txt
│ │ ├── 18.txt
│ │ ├── 19.txt
│ │ ├── 2.txt
│ │ ├── 20.txt
│ │ ├── 21.txt
│ │ ├── 22.txt
│ │ ├── 23.txt
│ │ ├── 24.txt
│ │ ├── 25.txt
│ │ ├── 26.txt
│ │ ├── 27.txt
│ │ ├── 28.txt
│ │ ├── 29.txt
│ │ ├── 3.txt
│ │ ├── 30.txt
│ │ ├── 4.txt
│ │ ├── 5.txt
│ │ ├── 6.txt
│ │ ├── 7.txt
│ │ ├── 8.txt
│ │ └── 9.txt
│ ├── full_description.txt
│ ├── short_description.txt
│ └── title.txt
│ ├── sk
│ ├── full_description.txt
│ ├── short_description.txt
│ └── title.txt
│ └── tr
│ ├── full_description.txt
│ ├── short_description.txt
│ └── title.txt
├── feature
├── backup
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── mrsep
│ │ └── musicrecognizer
│ │ └── feature
│ │ └── backup
│ │ ├── AppBackupManager.kt
│ │ ├── AppRestartManager.kt
│ │ ├── data
│ │ ├── AppBackupManagerImpl.kt
│ │ ├── DatabaseBackupProviderImpl.kt
│ │ └── InstantJsonSerializer.kt
│ │ ├── di
│ │ └── BackupModule.kt
│ │ └── presentation
│ │ ├── BackupDialog.kt
│ │ ├── ExperimentalFeaturesScreen.kt
│ │ ├── ExperimentalFeaturesScreenNavigation.kt
│ │ ├── ExperimentalFeaturesViewModel.kt
│ │ └── RestoreDialog.kt
├── developer-mode
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ ├── androidTest
│ │ └── java
│ │ │ └── com
│ │ │ └── mrsep
│ │ │ └── musicrecognizer
│ │ │ └── feature
│ │ │ └── developermode
│ │ │ └── ExampleInstrumentedTest.kt
│ │ └── main
│ │ └── java
│ │ └── com
│ │ └── mrsep
│ │ └── musicrecognizer
│ │ └── feature
│ │ └── developermode
│ │ └── presentation
│ │ ├── AmplitudeVisualizer.kt
│ │ ├── DeveloperScreen.kt
│ │ ├── DeveloperScreenNavigation.kt
│ │ ├── DeveloperScreenTopBar.kt
│ │ └── DeveloperViewModel.kt
├── library
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── java
│ │ └── com
│ │ └── mrsep
│ │ └── musicrecognizer
│ │ └── feature
│ │ └── library
│ │ └── presentation
│ │ ├── library
│ │ ├── DeleteSelectedDialog.kt
│ │ ├── EmptyLibraryMessage.kt
│ │ ├── LibraryScreen.kt
│ │ ├── LibraryScreenNavigation.kt
│ │ ├── LibraryScreenTopBar.kt
│ │ ├── LibraryViewModel.kt
│ │ ├── NoFilteredTracksMessage.kt
│ │ ├── TrackFilterBottomSheet.kt
│ │ ├── TrackFilterState.kt
│ │ ├── TrackLazyColumn.kt
│ │ ├── TrackLazyGrid.kt
│ │ └── UnviewedTrackBadge.kt
│ │ ├── model
│ │ └── TrackUi.kt
│ │ └── search
│ │ ├── LibrarySearchScreen.kt
│ │ ├── LibrarySearchScreenNavigation.kt
│ │ ├── LibrarySearchViewModel.kt
│ │ ├── SearchResultUi.kt
│ │ ├── SearchScopeDropdownMenu.kt
│ │ ├── SearchScreenTopBar.kt
│ │ └── TrackSearchItem.kt
├── onboarding
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ ├── androidTest
│ │ └── java
│ │ │ └── com
│ │ │ └── mrsep
│ │ │ └── musicrecognizer
│ │ │ └── feature
│ │ │ └── onboarding
│ │ │ └── ExampleInstrumentedTest.kt
│ │ └── main
│ │ └── java
│ │ └── com
│ │ └── mrsep
│ │ └── musicrecognizer
│ │ └── feature
│ │ └── onboarding
│ │ └── presentation
│ │ ├── FinalPage.kt
│ │ ├── OnboardingNavigation.kt
│ │ ├── OnboardingScreen.kt
│ │ ├── OnboardingViewModel.kt
│ │ ├── PermissionsPage.kt
│ │ ├── TokenPage.kt
│ │ ├── WelcomePage.kt
│ │ └── common
│ │ ├── PageIndicator.kt
│ │ └── PagerControls.kt
├── preferences
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ ├── androidTest
│ │ └── java
│ │ │ └── com
│ │ │ └── mrsep
│ │ │ └── musicrecognizer
│ │ │ └── feature
│ │ │ └── preferences
│ │ │ └── ExampleInstrumentedTest.kt
│ │ └── main
│ │ └── java
│ │ └── com
│ │ └── mrsep
│ │ └── musicrecognizer
│ │ └── feature
│ │ └── preferences
│ │ ├── RecognitionServiceStarter.kt
│ │ └── presentation
│ │ ├── AudioSourceDialog.kt
│ │ ├── FallbackPolicyDialog.kt
│ │ ├── HapticFeedbackDialog.kt
│ │ ├── NotificationServiceSwitch.kt
│ │ ├── PreferencesNavigation.kt
│ │ ├── PreferencesScreen.kt
│ │ ├── PreferencesTopBar.kt
│ │ ├── PreferencesViewModel.kt
│ │ ├── RequiredServicesDialog.kt
│ │ ├── ThemeDialog.kt
│ │ ├── about
│ │ ├── AboutScreen.kt
│ │ ├── AboutScreenNavigation.kt
│ │ ├── AboutScreenTopBar.kt
│ │ ├── AppLicenseScreen.kt
│ │ ├── AppLicenseScreenNavigation.kt
│ │ ├── SoftwareDetailsScreen.kt
│ │ ├── SoftwareDetailsScreenNavigation.kt
│ │ ├── SoftwareScreen.kt
│ │ └── SoftwareScreenNavigation.kt
│ │ └── serviceconfig
│ │ ├── AcrCloudHelpDialog.kt
│ │ ├── AcrCloudPreferences.kt
│ │ ├── AuddHelpDialog.kt
│ │ ├── AuddPreferences.kt
│ │ └── RecognitionServiceDialog.kt
├── recognition
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── mrsep
│ │ │ └── musicrecognizer
│ │ │ └── feature
│ │ │ └── recognition
│ │ │ ├── DeeplinkRouter.kt
│ │ │ ├── RecognitionStatusHolders.kt
│ │ │ ├── di
│ │ │ ├── PlatformModule.kt
│ │ │ ├── RecognitionModule.kt
│ │ │ └── RecognitionStatusHolderModule.kt
│ │ │ ├── domain
│ │ │ └── RecognitionInteractorImpl.kt
│ │ │ ├── platform
│ │ │ ├── VibrationManager.kt
│ │ │ └── VibrationManagerImpl.kt
│ │ │ ├── presentation
│ │ │ ├── model
│ │ │ │ ├── EnqueuedRecognitionUi.kt
│ │ │ │ ├── PlayerStatusUi.kt
│ │ │ │ ├── RemoteRecognitionResultUi.kt
│ │ │ │ └── TrackUi.kt
│ │ │ ├── queuescreen
│ │ │ │ ├── DeleteSelectedDialog.kt
│ │ │ │ ├── EmptyQueueMessage.kt
│ │ │ │ ├── QueueScreen.kt
│ │ │ │ ├── QueueScreenNavigation.kt
│ │ │ │ ├── QueueScreenTopBar.kt
│ │ │ │ ├── QueueScreenViewModel.kt
│ │ │ │ ├── RecognitionActionsBottomSheet.kt
│ │ │ │ ├── RecognitionLazyColumn.kt
│ │ │ │ ├── RecognitionLazyColumnItem.kt
│ │ │ │ ├── RecognitionLazyGrid.kt
│ │ │ │ ├── RecognitionLazyGridItem.kt
│ │ │ │ ├── RecordingShareUtils.kt
│ │ │ │ └── RenameRecognitionDialog.kt
│ │ │ └── recognitionscreen
│ │ │ │ ├── BasicRecognitionButton.kt
│ │ │ │ ├── DebugBuildLabel.kt
│ │ │ │ ├── OfflineModePopup.kt
│ │ │ │ ├── RecognitionButtonWithTitle.kt
│ │ │ │ ├── RecognitionNavigation.kt
│ │ │ │ ├── RecognitionScreen.kt
│ │ │ │ ├── RecognitionViewModel.kt
│ │ │ │ ├── RippleAnimated.kt
│ │ │ │ ├── WaveAnimated.kt
│ │ │ │ └── shields
│ │ │ │ ├── ApiUsageLimitedShield.kt
│ │ │ │ ├── AuthErrorShield.kt
│ │ │ │ ├── BadConnectionShield.kt
│ │ │ │ ├── BaseShield.kt
│ │ │ │ ├── FatalErrorShield.kt
│ │ │ │ ├── NoMatchesShield.kt
│ │ │ │ └── ScheduledOfflineShield.kt
│ │ │ ├── scheduler
│ │ │ ├── EnqueuedRecognitionSchedulerImpl.kt
│ │ │ ├── EnqueuedRecognitionWorker.kt
│ │ │ ├── TrackMetadataEnhancerSchedulerImpl.kt
│ │ │ └── TrackMetadataEnhancerWorker.kt
│ │ │ ├── service
│ │ │ ├── DisableRecognitionControlServiceReceiver.kt
│ │ │ ├── OneTimeRecognitionTileService.kt
│ │ │ ├── RecognitionControlActivity.kt
│ │ │ ├── RecognitionControlService.kt
│ │ │ ├── ResultNotificationHelper.kt
│ │ │ ├── ServiceNotificationHelper.kt
│ │ │ ├── ServiceStartupReceiver.kt
│ │ │ └── ext
│ │ │ │ └── ImageUtil.kt
│ │ │ └── widget
│ │ │ ├── RecognitionWidget.kt
│ │ │ ├── RecognitionWidgetEntryPoint.kt
│ │ │ ├── RecognitionWidgetReceiver.kt
│ │ │ ├── ResetWidgetStatusWorker.kt
│ │ │ ├── WidgetUiState.kt
│ │ │ ├── ui
│ │ │ ├── AnimatedRecognitionButton.kt
│ │ │ ├── ArtworkRoundedPlaceholder.kt
│ │ │ ├── CircleLayoutContent.kt
│ │ │ ├── HorizontalLayoutContent.kt
│ │ │ ├── SquareLayoutContent.kt
│ │ │ ├── StatusInfo.kt
│ │ │ ├── TrackInfo.kt
│ │ │ ├── VerticalLayoutContent.kt
│ │ │ ├── WidgetBackground.kt
│ │ │ ├── WidgetLayout.kt
│ │ │ └── WidgetVerticalDivider.kt
│ │ │ └── util
│ │ │ ├── FontUtils.kt
│ │ │ └── ImageUtils.kt
│ │ └── res
│ │ ├── anim
│ │ ├── widget_recognition_button_scale_in.xml
│ │ └── widget_recognition_button_scale_out.xml
│ │ ├── color
│ │ └── widget_artwork_background.xml
│ │ ├── drawable-night-xxhdpi
│ │ └── recognition_widget_preview.webp
│ │ ├── drawable-xxhdpi
│ │ └── recognition_widget_preview.webp
│ │ ├── drawable
│ │ ├── widget_artwork_fade_gradient.xml
│ │ ├── widget_artwork_shape.xml
│ │ ├── widget_background_shape.xml
│ │ ├── widget_circle_background_shape.xml
│ │ └── widget_recognition_button_shape.xml
│ │ ├── layout-v31
│ │ └── recognition_widget_preview.xml
│ │ ├── layout
│ │ ├── recognition_widget_loading.xml
│ │ └── widget_flipper_container.xml
│ │ ├── values-v31
│ │ └── dimens.xml
│ │ ├── values-v33
│ │ └── styles.xml
│ │ ├── values
│ │ └── dimens.xml
│ │ └── xml
│ │ ├── recognition_widget_info.xml
│ │ └── recognition_widget_samsung_info.xml
└── track
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── mrsep
│ │ └── musicrecognizer
│ │ └── feature
│ │ └── track
│ │ └── ExampleInstrumentedTest.kt
│ └── main
│ └── java
│ └── com
│ └── mrsep
│ └── musicrecognizer
│ └── feature
│ └── track
│ └── presentation
│ ├── lyrics
│ ├── AutoScrollToolbar.kt
│ ├── FontSizeSwitcher.kt
│ ├── LyricsPlayer.kt
│ ├── LyricsScreen.kt
│ ├── LyricsScreenNavigation.kt
│ ├── LyricsScreenTopBar.kt
│ ├── LyricsStyleBottomSheet.kt
│ ├── LyricsViewModel.kt
│ ├── PlainLyricsContent.kt
│ └── SyncedLyricsContent.kt
│ ├── track
│ ├── AlbumArtwork.kt
│ ├── AlbumArtworkShield.kt
│ ├── FadeModifiers.kt
│ ├── MusicServiceChipsFlowRow.kt
│ ├── ShareBottomSheet.kt
│ ├── TrackActionsBottomBar.kt
│ ├── TrackExtrasDialog.kt
│ ├── TrackInfoColumn.kt
│ ├── TrackNotFoundMessage.kt
│ ├── TrackScreen.kt
│ ├── TrackScreenNavigation.kt
│ ├── TrackScreenTopBar.kt
│ ├── TrackSection.kt
│ ├── TrackUi.kt
│ ├── TrackViewModel.kt
│ └── WebSearchBottomSheet.kt
│ └── utils
│ ├── ColorUtils.kt
│ └── ImageShareUtils.kt
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── img
├── get-it-on-f-droid.png
└── get-it-on-github.png
└── settings.gradle.kts
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | # With R8 full mode generic signatures are stripped for classes that are not kept.
24 | # Suspend functions are wrapped in continuations where the type argument is used.
25 | -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/mrsep/musicrecognizer/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.mrsep.musicrecognizer", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/mrsep/musicrecognizer/di/AndroidAppRestarter.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.di
2 |
3 | import android.content.Context
4 | import com.mrsep.musicrecognizer.feature.backup.AppRestartManager
5 | import com.mrsep.musicrecognizer.presentation.MainActivity.Companion.restartApplicationOnRestore
6 | import dagger.hilt.android.qualifiers.ApplicationContext
7 | import javax.inject.Inject
8 |
9 | class AndroidAppRestarter @Inject constructor(
10 | @ApplicationContext private val appContext: Context,
11 | ): AppRestartManager {
12 |
13 | override fun restartApplicationOnRestore() {
14 | appContext.restartApplicationOnRestore()
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/mrsep/musicrecognizer/di/GlueModule.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.di
2 |
3 | import com.mrsep.musicrecognizer.feature.backup.AppRestartManager
4 | import com.mrsep.musicrecognizer.feature.preferences.RecognitionServiceStarter
5 | import com.mrsep.musicrecognizer.feature.recognition.DeeplinkRouter
6 | import dagger.Binds
7 | import dagger.Module
8 | import dagger.hilt.InstallIn
9 | import dagger.hilt.components.SingletonComponent
10 |
11 | @Suppress("unused")
12 | @Module
13 | @InstallIn(SingletonComponent::class)
14 | interface GlueModule {
15 |
16 | @Binds
17 | fun bindAppRestartManager(impl: AndroidAppRestarter): AppRestartManager
18 |
19 | @Binds
20 | fun bindDeeplinkRouter(impl: AppDeeplinkRouter): DeeplinkRouter
21 |
22 | @Binds
23 | fun bindRecognitionServiceStarter(impl: ServiceStarter): RecognitionServiceStarter
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/mrsep/musicrecognizer/di/ServiceStarter.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.di
2 |
3 | import android.content.Context
4 | import com.mrsep.musicrecognizer.feature.preferences.RecognitionServiceStarter
5 | import com.mrsep.musicrecognizer.feature.recognition.service.RecognitionControlService
6 | import dagger.hilt.android.qualifiers.ApplicationContext
7 | import javax.inject.Inject
8 |
9 | class ServiceStarter @Inject constructor(
10 | @ApplicationContext private val appContext: Context,
11 | ) : RecognitionServiceStarter {
12 |
13 | override fun startServiceHoldMode() {
14 | RecognitionControlService.startHoldMode(appContext, false)
15 | }
16 |
17 | override fun stopServiceHoldMode() {
18 | RecognitionControlService.stopHoldMode(appContext)
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_shortcut_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_shortcut_recognize.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/resources.properties:
--------------------------------------------------------------------------------
1 | unqualifiedResLocale=en-US
2 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFF5F5F5
4 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/files.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import com.android.build.api.dsl.ApplicationExtension
2 | import com.mrsep.musicrecognizer.configureAndroidCompose
3 | import org.gradle.api.Plugin
4 | import org.gradle.api.Project
5 | import org.gradle.kotlin.dsl.apply
6 | import org.gradle.kotlin.dsl.getByType
7 |
8 | class AndroidApplicationComposeConventionPlugin : Plugin {
9 |
10 | override fun apply(target: Project) {
11 | with(target) {
12 | apply(plugin = "com.android.application")
13 | apply(plugin = "org.jetbrains.kotlin.plugin.compose")
14 |
15 | val extension = extensions.getByType()
16 | configureAndroidCompose(extension)
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import com.android.build.api.dsl.ApplicationExtension
2 | import com.mrsep.musicrecognizer.configureDetekt
3 | import com.mrsep.musicrecognizer.configureKotlinAndroid
4 | import com.mrsep.musicrecognizer.libs
5 | import io.gitlab.arturbosch.detekt.extensions.DetektExtension
6 | import org.gradle.api.Plugin
7 | import org.gradle.api.Project
8 | import org.gradle.kotlin.dsl.configure
9 | import org.gradle.kotlin.dsl.getByType
10 |
11 | class AndroidApplicationConventionPlugin : Plugin {
12 |
13 | override fun apply(target: Project) {
14 | with(target) {
15 | with(pluginManager) {
16 | apply("com.android.application")
17 | apply("org.jetbrains.kotlin.android")
18 | apply("io.gitlab.arturbosch.detekt")
19 | }
20 |
21 | configureDetekt(extensions.getByType())
22 |
23 | extensions.configure {
24 | configureKotlinAndroid(this)
25 | defaultConfig.targetSdk = libs.findVersion("sdkTarget").get().requiredVersion.toInt()
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import com.android.build.gradle.LibraryExtension
2 | import com.mrsep.musicrecognizer.configureAndroidCompose
3 | import org.gradle.api.Plugin
4 | import org.gradle.api.Project
5 | import org.gradle.kotlin.dsl.apply
6 | import org.gradle.kotlin.dsl.getByType
7 |
8 | class AndroidLibraryComposeConventionPlugin : Plugin {
9 |
10 | override fun apply(target: Project) {
11 | with(target) {
12 | apply(plugin = "com.android.library")
13 | apply(plugin = "org.jetbrains.kotlin.plugin.compose")
14 |
15 | val extension = extensions.getByType()
16 | configureAndroidCompose(extension)
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import com.mrsep.musicrecognizer.configureKotlinJvm
2 | import com.mrsep.musicrecognizer.libs
3 | import org.gradle.api.Plugin
4 | import org.gradle.api.Project
5 | import org.gradle.kotlin.dsl.apply
6 | import org.gradle.kotlin.dsl.dependencies
7 |
8 | class JvmLibraryConventionPlugin : Plugin {
9 | override fun apply(target: Project) {
10 | with(target) {
11 | apply(plugin = "org.jetbrains.kotlin.jvm")
12 |
13 | configureKotlinJvm()
14 | dependencies {
15 |
16 | }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/com/mrsep/musicrecognizer/Detekt.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer
2 |
3 | import io.gitlab.arturbosch.detekt.Detekt
4 | import io.gitlab.arturbosch.detekt.extensions.DetektExtension
5 | import org.gradle.api.Project
6 | import org.gradle.kotlin.dsl.dependencies
7 | import org.gradle.kotlin.dsl.named
8 |
9 | internal fun Project.configureDetekt(extension: DetektExtension) = extension.apply {
10 | tasks.named("detekt") {
11 | reports {
12 | html.required.set(true)
13 | xml.required.set(false)
14 | txt.required.set(false)
15 | sarif.required.set(false)
16 | md.required.set(false)
17 | }
18 | }
19 | dependencies {
20 | "detektPlugins"(libs.findLibrary("detekt-formatting").get())
21 | "detektPlugins"(libs.findLibrary("detekt-compose").get())
22 | }
23 | }
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/com/mrsep/musicrecognizer/ProjectExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer
2 |
3 | import org.gradle.api.Project
4 | import org.gradle.api.artifacts.VersionCatalog
5 | import org.gradle.api.artifacts.VersionCatalogsExtension
6 | import org.gradle.kotlin.dsl.getByType
7 |
8 | val Project.libs
9 | get(): VersionCatalog = extensions.getByType().named("libs")
10 |
--------------------------------------------------------------------------------
/build-logic/gradle.properties:
--------------------------------------------------------------------------------
1 | # Gradle properties are not passed to included builds https://github.com/gradle/gradle/issues/2534
2 |
--------------------------------------------------------------------------------
/build-logic/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
4 |
5 | dependencyResolutionManagement {
6 | repositories {
7 | google()
8 | mavenCentral()
9 | }
10 | versionCatalogs {
11 | create("libs") {
12 | from(files("../gradle/libs.versions.toml"))
13 | }
14 | }
15 | }
16 |
17 | rootProject.name = "build-logic"
18 | include(":convention")
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application) apply false
3 | alias(libs.plugins.android.library) apply false
4 | alias(libs.plugins.compose) apply false
5 | alias(libs.plugins.kotlin.android) apply false
6 | alias(libs.plugins.kotlin.parcelize) apply false
7 | alias(libs.plugins.kotlin.serialization) apply false
8 | alias(libs.plugins.ksp) apply false
9 | alias(libs.plugins.hilt) apply false
10 | alias(libs.plugins.protobuf) apply false
11 | alias(libs.plugins.detekt) apply false
12 | alias(libs.plugins.aboutLibraries) apply false
13 | }
--------------------------------------------------------------------------------
/compose_compiler_config.conf:
--------------------------------------------------------------------------------
1 | java.time.ZoneId
2 | java.time.ZoneOffset
3 | java.time.Instant
4 | java.time.Duration
5 | java.time.LocalDate
6 | java.time.LocalDateTime
7 | java.time.LocalTime
8 | java.time.ZonedDateTime
9 | java.util.Date
10 | java.util.Locale
11 | java.io.File
12 |
13 | kotlin.time.Duration
14 | kotlin.collections.*
15 |
16 | android.net.Uri
17 |
18 | com.mrsep.musicrecognizer.core.domain.*
--------------------------------------------------------------------------------
/core/audio/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/audio/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.musicrecognizer.android.library)
3 | alias(libs.plugins.musicrecognizer.hilt)
4 | }
5 |
6 | android {
7 | namespace = "com.mrsep.musicrecognizer.core.audio"
8 | }
9 |
10 | dependencies {
11 | implementation(projects.core.common)
12 | implementation(projects.core.domain)
13 | implementation(libs.kotlinx.coroutinesAndroid)
14 |
15 | testImplementation(libs.junit4)
16 | testImplementation(libs.kotlinx.coroutinesTest)
17 | }
--------------------------------------------------------------------------------
/core/audio/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/core/audio/src/main/java/com/mrsep/musicrecognizer/core/audio/audioplayer/PlayerController.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.audio.audioplayer
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import java.io.File
5 |
6 | interface PlayerController {
7 |
8 | val statusFlow: Flow
9 | val playbackPositionFlow: Flow
10 |
11 | fun start(id: Int, file: File)
12 | fun pause()
13 | fun resume()
14 | fun stop()
15 | }
16 |
--------------------------------------------------------------------------------
/core/audio/src/main/java/com/mrsep/musicrecognizer/core/audio/audioplayer/PlayerStatus.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.audio.audioplayer
2 |
3 | import java.io.File
4 | import kotlin.time.Duration
5 |
6 | sealed class PlayerStatus {
7 |
8 | data object Idle : PlayerStatus()
9 |
10 | data class Started(
11 | val id: Int,
12 | val record: File,
13 | val duration: Duration
14 | ) : PlayerStatus()
15 |
16 | data class Paused(
17 | val id: Int,
18 | val record: File,
19 | val duration: Duration
20 | ) : PlayerStatus()
21 |
22 | data class Error(
23 | val id: Int,
24 | val record: File,
25 | val message: String
26 | ) : PlayerStatus()
27 | }
28 |
--------------------------------------------------------------------------------
/core/audio/src/main/java/com/mrsep/musicrecognizer/core/audio/audiorecord/AudioCaptureConfig.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.audio.audiorecord
2 |
3 | import android.media.projection.MediaProjection
4 |
5 | sealed class AudioCaptureConfig {
6 |
7 | data object Microphone : AudioCaptureConfig()
8 | data class Device(val mediaProjection: MediaProjection?) : AudioCaptureConfig()
9 | data class Auto(val mediaProjection: MediaProjection?) : AudioCaptureConfig()
10 | }
11 |
--------------------------------------------------------------------------------
/core/audio/src/main/java/com/mrsep/musicrecognizer/core/audio/audiorecord/AudioRecordDispatcher.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.audio.audiorecord
2 |
3 | import android.os.Handler
4 | import android.os.HandlerThread
5 | import android.os.Process
6 | import kotlinx.coroutines.android.asCoroutineDispatcher
7 |
8 | /**
9 | * CoroutineDispatcher for audio recording purposes. It uses a dedicated thread
10 | * and elevates its priority to [android.os.Process.THREAD_PRIORITY_URGENT_AUDIO].
11 | */
12 |
13 | internal val AudioRecordHandler = HandlerThread(
14 | "myRecordThread",
15 | Process.THREAD_PRIORITY_URGENT_AUDIO
16 | )
17 | .apply { start() }
18 | .run { Handler(this.looper) }
19 | internal val AudioRecordDispatcher = AudioRecordHandler.asCoroutineDispatcher()
20 |
21 | internal val AudioEncoderHandler = HandlerThread(
22 | "myEncoderThread",
23 | Process.THREAD_PRIORITY_URGENT_AUDIO
24 | )
25 | .apply { start() }
26 | .run { Handler(this.looper) }
27 | internal val AudioEncoderDispatcher = AudioEncoderHandler.asCoroutineDispatcher()
28 |
--------------------------------------------------------------------------------
/core/audio/src/main/java/com/mrsep/musicrecognizer/core/audio/audiorecord/soundsource/SoundSource.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.audio.audiorecord.soundsource
2 |
3 | import kotlinx.coroutines.flow.SharedFlow
4 | import kotlinx.coroutines.flow.StateFlow
5 |
6 | internal interface SoundSource {
7 |
8 | /**
9 | * Null value represents that there is no available SoundSourceConfig for this device.
10 | * In this case, audio recording is not allowed.
11 | */
12 | val params: SoundSourceConfig?
13 |
14 | /**
15 | * Represents the current sound level normalized to a range between 0.0 and 1.0.
16 | * Values are rounded to two decimal places.
17 | */
18 | val soundLevel: StateFlow
19 |
20 | val pcmChunkFlow: SharedFlow>
21 | }
22 |
--------------------------------------------------------------------------------
/core/audio/src/main/java/com/mrsep/musicrecognizer/core/audio/di/AudioModule.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.audio.di
2 |
3 | import com.mrsep.musicrecognizer.core.audio.audioplayer.MediaPlayerController
4 | import com.mrsep.musicrecognizer.core.audio.audioplayer.PlayerController
5 | import dagger.Binds
6 | import dagger.Module
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 | import javax.inject.Singleton
10 |
11 | @Suppress("unused")
12 | @Module
13 | @InstallIn(SingletonComponent::class)
14 | internal interface AudioModule {
15 |
16 | @Binds
17 | @Singleton
18 | fun bindPlayerController(impl: MediaPlayerController): PlayerController
19 | }
--------------------------------------------------------------------------------
/core/common/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/common/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.musicrecognizer.android.library)
3 | alias(libs.plugins.musicrecognizer.hilt)
4 | }
5 |
6 | android {
7 | namespace = "com.mrsep.musicrecognizer.core.common"
8 | }
9 |
10 | dependencies {
11 | implementation(projects.core.ui)
12 | implementation(projects.core.strings)
13 |
14 | implementation(libs.kotlinx.coroutinesAndroid)
15 | implementation(libs.androidx.navigation.compose)
16 |
17 | implementation(libs.androidx.navigation.compose)
18 | }
--------------------------------------------------------------------------------
/core/common/src/main/java/com/mrsep/musicrecognizer/core/common/BidirectionalMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.common
2 |
3 | interface BidirectionalMapper : Mapper {
4 |
5 | fun reverseMap(input: O): I
6 | }
7 |
--------------------------------------------------------------------------------
/core/common/src/main/java/com/mrsep/musicrecognizer/core/common/Containter.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.common
2 |
3 | sealed class Container
4 |
5 | data class Success(val value: T) : Container()
6 |
7 | data class Error(val reason: E) : Container()
8 |
9 | inline fun Container.onSuccess(block: T.() -> Unit): Container {
10 | if (this is Success) {
11 | this.value.block()
12 | }
13 | return this
14 | }
15 |
16 | inline fun Container.onError(block: E.() -> Unit): Container {
17 | if (this is Error) {
18 | this.reason.block()
19 | }
20 | return this
21 | }
22 |
23 | inline fun Container.map(successMapper: (T) -> R, errorMapper: (E) -> F): Container {
24 | return when (this) {
25 | is Error -> Error(errorMapper(this.reason))
26 | is Success -> Success(successMapper(this.value))
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/core/common/src/main/java/com/mrsep/musicrecognizer/core/common/DispatchersProvider.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.common
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.Dispatchers
5 | import javax.inject.Inject
6 |
7 | interface DispatchersProvider {
8 | val main: CoroutineDispatcher
9 | val mainImmediate: CoroutineDispatcher
10 | val default: CoroutineDispatcher
11 | val io: CoroutineDispatcher
12 | }
13 |
14 | class DefaultDispatchersProvider @Inject constructor() : DispatchersProvider {
15 | override val main = Dispatchers.Main
16 | override val mainImmediate = Dispatchers.Main.immediate
17 | override val default = Dispatchers.Default
18 | override val io = Dispatchers.IO
19 | }
20 |
21 | class TestDispatchersProvider(testDispatcher: CoroutineDispatcher) : DispatchersProvider {
22 | override val main = testDispatcher
23 | override val mainImmediate = testDispatcher
24 | override val default = testDispatcher
25 | override val io = testDispatcher
26 | }
27 |
--------------------------------------------------------------------------------
/core/common/src/main/java/com/mrsep/musicrecognizer/core/common/Mapper.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.common
2 |
3 | interface Mapper {
4 |
5 | fun map(input: I): O
6 | }
7 |
--------------------------------------------------------------------------------
/core/common/src/main/java/com/mrsep/musicrecognizer/core/common/di/CoroutineScopeModule.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.common.di
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import kotlinx.coroutines.CoroutineDispatcher
8 | import kotlinx.coroutines.CoroutineScope
9 | import kotlinx.coroutines.SupervisorJob
10 | import javax.inject.Qualifier
11 | import javax.inject.Singleton
12 |
13 | @InstallIn(SingletonComponent::class)
14 | @Module
15 | class CoroutineScopeModule {
16 |
17 | @Singleton
18 | @ApplicationScope
19 | @Provides
20 | fun provideApplicationScope(
21 | @DefaultDispatcher defaultDispatcher: CoroutineDispatcher
22 | ): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher)
23 | }
24 |
25 | @Retention(AnnotationRetention.RUNTIME)
26 | @Qualifier
27 | annotation class ApplicationScope
28 |
--------------------------------------------------------------------------------
/core/common/src/main/java/com/mrsep/musicrecognizer/core/common/di/DispatcherModule.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.common.di
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import kotlinx.coroutines.CoroutineDispatcher
8 | import kotlinx.coroutines.Dispatchers
9 | import javax.inject.Qualifier
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | class DispatcherModule {
14 |
15 | @DefaultDispatcher
16 | @Provides
17 | fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
18 |
19 | @IoDispatcher
20 | @Provides
21 | fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
22 |
23 | @MainDispatcher
24 | @Provides
25 | fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
26 | }
27 |
28 | @Retention(AnnotationRetention.BINARY)
29 | @Qualifier
30 | annotation class DefaultDispatcher
31 |
32 | @Retention(AnnotationRetention.BINARY)
33 | @Qualifier
34 | annotation class IoDispatcher
35 |
36 | @Retention(AnnotationRetention.BINARY)
37 | @Qualifier
38 | annotation class MainDispatcher
39 |
--------------------------------------------------------------------------------
/core/common/src/main/java/com/mrsep/musicrecognizer/core/common/di/DispatchersProviderModule.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.common.di
2 |
3 | import com.mrsep.musicrecognizer.core.common.DefaultDispatchersProvider
4 | import com.mrsep.musicrecognizer.core.common.DispatchersProvider
5 | import dagger.Binds
6 | import dagger.Module
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 | import javax.inject.Singleton
10 |
11 | @Suppress("unused")
12 | @Module
13 | @InstallIn(SingletonComponent::class)
14 | interface DispatchersProviderModule {
15 |
16 | @Binds
17 | @Singleton
18 | fun bindDispatchersProvider(implementation: DefaultDispatchersProvider): DispatchersProvider
19 | }
20 |
--------------------------------------------------------------------------------
/core/common/src/main/java/com/mrsep/musicrecognizer/core/common/di/UtilModule.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.common.di
2 |
3 | import com.mrsep.musicrecognizer.core.common.util.AppDateTimeFormatter
4 | import com.mrsep.musicrecognizer.core.common.util.AppDateTimeFormatterImpl
5 | import dagger.Binds
6 | import dagger.Module
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 |
10 | @Suppress("unused")
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | internal interface UtilModule {
14 |
15 | @Binds
16 | fun bindAppDateTimeFormatter(implementation: AppDateTimeFormatterImpl): AppDateTimeFormatter
17 | }
18 |
--------------------------------------------------------------------------------
/core/common/src/main/java/com/mrsep/musicrecognizer/core/common/util/NavigationExt.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.common.util
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.navigation.NavBackStackEntry
5 |
6 | /**
7 | * If the lifecycle is not resumed it means this NavBackStackEntry already processed a nav event.
8 | *
9 | * This is used to de-duplicate navigation events.
10 | */
11 | val NavBackStackEntry.lifecycleIsResumed get() =
12 | this.lifecycle.currentState == Lifecycle.State.RESUMED
13 |
--------------------------------------------------------------------------------
/core/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/data/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.musicrecognizer.android.library)
3 | alias(libs.plugins.musicrecognizer.hilt)
4 | }
5 |
6 | android {
7 | namespace = "com.mrsep.musicrecognizer.core.data"
8 | }
9 |
10 | dependencies {
11 | implementation(projects.core.domain)
12 | implementation(projects.core.recognition)
13 | implementation(projects.core.database)
14 | implementation(projects.core.datastore)
15 | implementation(projects.core.network)
16 | implementation(projects.core.common)
17 |
18 | implementation(libs.kotlinx.coroutinesAndroid)
19 | implementation(libs.androidx.core.ktx)
20 | }
--------------------------------------------------------------------------------
/core/data/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/core/data/src/main/java/com/mrsep/musicrecognizer/core/data/enqueued/RecordingFileDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.data.enqueued
2 |
3 | import com.mrsep.musicrecognizer.core.domain.recognition.AudioRecording
4 | import java.io.File
5 | import java.time.Instant
6 | import java.util.zip.ZipInputStream
7 |
8 | interface RecordingFileDataSource {
9 |
10 | fun getFiles(): Array
11 |
12 | fun getTotalSize(): Long
13 |
14 | suspend fun write(recording: ByteArray, timestamp: Instant): File?
15 |
16 | suspend fun import(inputStream: ZipInputStream, recordingName: String): File?
17 |
18 | suspend fun read(file: File): AudioRecording?
19 |
20 | suspend fun delete(file: File): Boolean
21 |
22 | suspend fun deleteAll(): Boolean
23 | }
24 |
--------------------------------------------------------------------------------
/core/data/src/main/java/com/mrsep/musicrecognizer/core/data/track/TrackPreviewMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.data.track
2 |
3 | import com.mrsep.musicrecognizer.core.database.track.TrackPreviewTuple
4 | import com.mrsep.musicrecognizer.core.domain.track.model.TrackPreview
5 |
6 | internal fun TrackPreviewTuple.toDomain() = TrackPreview(
7 | id = id,
8 | title = title,
9 | artist = artist,
10 | album = album,
11 | artworkThumbUrl = artworkThumbnail ?: artwork,
12 | recognitionDate = recognitionDate,
13 | isViewed = isViewed
14 | )
--------------------------------------------------------------------------------
/core/database/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/database/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.musicrecognizer.android.library)
3 | alias(libs.plugins.musicrecognizer.hilt)
4 | alias(libs.plugins.ksp)
5 | }
6 |
7 | android {
8 | namespace = "com.mrsep.musicrecognizer.core.database"
9 |
10 | defaultConfig {
11 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
12 | consumerProguardFiles("consumer-rules.pro")
13 | }
14 |
15 | ksp {
16 | arg("room.generateKotlin", "true")
17 | arg("room.schemaLocation", "$projectDir/schemas")
18 | }
19 | }
20 |
21 | dependencies {
22 | implementation(projects.core.domain)
23 |
24 | implementation(libs.kotlinx.coroutinesAndroid)
25 | implementation(libs.androidx.core.ktx)
26 | api(libs.room.ktx)
27 | implementation(libs.room.runtime)
28 | ksp(libs.room.compiler)
29 | }
--------------------------------------------------------------------------------
/core/database/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aleksey-saenko/MusicRecognizer/ddf4ebf34c27d4e9ad21bff794811cf9ff214602/core/database/consumer-rules.pro
--------------------------------------------------------------------------------
/core/database/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/core/database/src/main/java/com/mrsep/musicrecognizer/core/database/DatabaseBackupProvider.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.database
2 |
3 | import java.io.InputStream
4 |
5 | interface DatabaseBackupProvider {
6 |
7 | fun getDatabaseBackup(): InputStream?
8 |
9 | fun onDatabaseRestored()
10 | }
11 |
--------------------------------------------------------------------------------
/core/database/src/main/java/com/mrsep/musicrecognizer/core/database/DatabaseUtils.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.database
2 |
3 | typealias SQLSearchPattern = String
4 |
5 | internal object DatabaseUtils {
6 |
7 | // Workaround for https://issuetracker.google.com/issues/73634057
8 | // This has limitations, for example when selecting with ordering. Use with caution.
9 | private const val MAX_SQLITE_ARGS = 990
10 |
11 | suspend fun Iterable.eachDbChunk(action: suspend (List) -> Unit) =
12 | dbChunked().forEach { action(it) }
13 |
14 | suspend fun Iterable.dbChunkedMap(transform: suspend (List) -> Iterable): List =
15 | dbChunked().flatMap { transform(it) }
16 |
17 | private fun Iterable.dbChunked(): List> = chunked(MAX_SQLITE_ARGS)
18 |
19 | const val ESCAPE_SYMBOL = "/"
20 | fun createSearchPatternForSQLite(query: String): SQLSearchPattern {
21 | return query
22 | .replace(ESCAPE_SYMBOL, "${ESCAPE_SYMBOL}/${ESCAPE_SYMBOL}")
23 | .replace("%", "${ESCAPE_SYMBOL}%")
24 | .replace("_", "${ESCAPE_SYMBOL}_")
25 | .run { "%$this%" }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/core/database/src/main/java/com/mrsep/musicrecognizer/core/database/DurationRoomConverter.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.database
2 |
3 | import androidx.room.TypeConverter
4 | import kotlin.time.Duration
5 | import kotlin.time.Duration.Companion.milliseconds
6 |
7 | internal class DurationRoomConverter {
8 |
9 | @TypeConverter
10 | fun durationToTimestamp(duration: Duration): Long {
11 | return duration.inWholeMilliseconds
12 | }
13 |
14 | @TypeConverter
15 | fun timestampToDuration(millis: Long): Duration {
16 | return millis.milliseconds
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/core/database/src/main/java/com/mrsep/musicrecognizer/core/database/FileRoomConverter.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.database
2 |
3 | import androidx.room.TypeConverter
4 | import java.io.File
5 |
6 | internal class FileRoomConverter {
7 |
8 | @TypeConverter
9 | fun stringToFile(filepath: String): File {
10 | return File(filepath)
11 | }
12 |
13 | @TypeConverter
14 | fun fileToString(file: File): String {
15 | return file.absolutePath
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/core/database/src/main/java/com/mrsep/musicrecognizer/core/database/InstantRoomConverter.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.database
2 |
3 | import androidx.room.TypeConverter
4 | import java.time.Instant
5 |
6 | internal class InstantRoomConverter {
7 |
8 | @TypeConverter
9 | fun instantToTimestamp(instant: Instant): Long {
10 | return instant.toEpochMilli()
11 | }
12 |
13 | @TypeConverter
14 | fun timestampToInstant(epochMillis: Long): Instant {
15 | return Instant.ofEpochMilli(epochMillis)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/core/database/src/main/java/com/mrsep/musicrecognizer/core/database/LocalDateRoomConverter.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.database
2 |
3 | import androidx.room.TypeConverter
4 | import java.time.LocalDate
5 |
6 | internal class LocalDateRoomConverter {
7 |
8 | @TypeConverter
9 | fun timestampToLocalDate(epochDay: Long): LocalDate {
10 | return LocalDate.ofEpochDay(epochDay)
11 | }
12 |
13 | @TypeConverter
14 | fun localDateToTimestamp(date: LocalDate): Long {
15 | return date.toEpochDay()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/core/database/src/main/java/com/mrsep/musicrecognizer/core/database/enqueued/model/EnqueuedRecognitionEntityWithTrack.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.database.enqueued.model
2 |
3 | import androidx.room.Embedded
4 | import androidx.room.Relation
5 | import com.mrsep.musicrecognizer.core.database.track.TrackEntity
6 |
7 | data class EnqueuedRecognitionEntityWithTrack(
8 | @Embedded val enqueued: EnqueuedRecognitionEntity,
9 | @Relation(
10 | parentColumn = "result_track_id",
11 | entityColumn = "id"
12 | )
13 | val track: TrackEntity?
14 | )
15 |
--------------------------------------------------------------------------------
/core/database/src/main/java/com/mrsep/musicrecognizer/core/database/enqueued/model/RemoteRecognitionResultType.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.database.enqueued.model
2 |
3 | enum class RemoteRecognitionResultType {
4 | Success,
5 | NoMatches,
6 | BadConnection,
7 | BadRecording,
8 | AuthError,
9 | ApiUsageLimited,
10 | HttpError,
11 | UnhandledError
12 | }
13 |
--------------------------------------------------------------------------------
/core/database/src/main/java/com/mrsep/musicrecognizer/core/database/migration/AutoMigrationSpec3To4.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.database.migration
2 |
3 | import androidx.room.RenameColumn
4 | import androidx.room.migration.AutoMigrationSpec
5 |
6 | @RenameColumn(
7 | tableName = "track",
8 | fromColumnName = "mb_id",
9 | toColumnName = "id"
10 | )
11 | @RenameColumn(
12 | tableName = "enqueued_recognition",
13 | fromColumnName = "result_mb_id",
14 | toColumnName = "result_track_id"
15 | )
16 | internal class AutoMigrationSpec3To4 : AutoMigrationSpec
17 |
--------------------------------------------------------------------------------
/core/database/src/main/java/com/mrsep/musicrecognizer/core/database/migration/Migration7To8.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.database.migration
2 |
3 | import androidx.room.migration.Migration
4 | import androidx.sqlite.db.SupportSQLiteDatabase
5 |
6 | internal val Migration7To8 = object : Migration(7, 8) {
7 |
8 | override fun migrate(db: SupportSQLiteDatabase) {
9 | db.execSQL("ALTER TABLE track ADD COLUMN is_lyrics_synced INTEGER NOT NULL DEFAULT 0")
10 | db.execSQL("UPDATE track SET recognition_date = recognition_date * 1000")
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/core/database/src/main/java/com/mrsep/musicrecognizer/core/database/track/TrackPreviewTuple.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.database.track
2 |
3 | import androidx.room.ColumnInfo
4 | import java.time.Instant
5 |
6 | data class TrackPreviewTuple(
7 | @ColumnInfo(name = "id")
8 | val id: String,
9 | @ColumnInfo(name = "title")
10 | val title: String,
11 | @ColumnInfo(name = "artist")
12 | val artist: String,
13 | @ColumnInfo(name = "album")
14 | val album: String?,
15 | @ColumnInfo(name = "recognition_date")
16 | val recognitionDate: Instant,
17 | @ColumnInfo(name = "link_artwork_thumb")
18 | val artworkThumbnail: String?,
19 | @ColumnInfo(name = "link_artwork")
20 | val artwork: String?,
21 | @ColumnInfo(name = "is_viewed")
22 | val isViewed: Boolean,
23 | )
24 |
--------------------------------------------------------------------------------
/core/datastore/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/datastore/consumer-rules.pro:
--------------------------------------------------------------------------------
1 | # Keep DataStore fields
2 | -keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite* {
3 | ;
4 | }
--------------------------------------------------------------------------------
/core/datastore/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/core/datastore/src/main/proto/acr_cloud_config_proto.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option java_package = "com.mrsep.musicrecognizer.core.datastore";
4 | option java_multiple_files = true;
5 |
6 | message AcrCloudConfigProto {
7 | string host = 1;
8 | string access_key = 2;
9 | string access_secret = 3;
10 | }
--------------------------------------------------------------------------------
/core/datastore/src/main/proto/audio_capture_mode_proto.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option java_package = "com.mrsep.musicrecognizer.core.datastore";
4 | option java_multiple_files = true;
5 |
6 | enum AudioCaptureModeProto {
7 | Unspecified = 0;
8 | Microphone = 1;
9 | Device = 2;
10 | Auto = 3;
11 | }
--------------------------------------------------------------------------------
/core/datastore/src/main/proto/music_service_proto.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option java_package = "com.mrsep.musicrecognizer.core.datastore";
4 | option java_multiple_files = true;
5 |
6 | enum MusicServiceProto {
7 | AmazonMusic = 0;
8 | Anghami = 1;
9 | AppleMusic = 2;
10 | Audiomack = 3;
11 | Audius = 4;
12 | Boomplay = 5;
13 | Deezer = 6;
14 | MusicBrainz = 7;
15 | Napster = 8;
16 | Pandora = 9;
17 | Soundcloud = 10;
18 | Spotify = 11;
19 | Tidal = 12;
20 | YandexMusic = 13;
21 | Youtube = 14;
22 | YoutubeMusic = 15;
23 | }
--------------------------------------------------------------------------------
/core/datastore/src/main/proto/recognition_provider_proto.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option java_package = "com.mrsep.musicrecognizer.core.datastore";
4 | option java_multiple_files = true;
5 |
6 | enum RecognitionProviderProto {
7 | Audd = 0;
8 | AcrCloud = 1;
9 | }
--------------------------------------------------------------------------------
/core/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/domain/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.musicrecognizer.jvm.library)
3 | alias(libs.plugins.musicrecognizer.hilt)
4 | }
5 |
6 | dependencies {
7 | implementation(libs.kotlinx.coroutinesCore)
8 | }
--------------------------------------------------------------------------------
/core/domain/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/core/domain/src/main/java/com/mrsep/musicrecognizer/core/domain/preferences/FallbackPolicy.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.domain.preferences
2 |
3 | data class FallbackPolicy(
4 | val noMatches: FallbackAction,
5 | val badConnection: FallbackAction,
6 | val anotherFailure: FallbackAction,
7 | )
8 |
--------------------------------------------------------------------------------
/core/domain/src/main/java/com/mrsep/musicrecognizer/core/domain/preferences/HapticFeedback.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.domain.preferences
2 |
3 | data class HapticFeedback(
4 | val vibrateOnTap: Boolean,
5 | val vibrateOnResult: Boolean,
6 | )
7 |
--------------------------------------------------------------------------------
/core/domain/src/main/java/com/mrsep/musicrecognizer/core/domain/preferences/LyricsStyle.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.domain.preferences
2 |
3 | data class LyricsStyle(
4 | val fontSize: FontSize,
5 | val isBold: Boolean,
6 | val isHighContrast: Boolean,
7 | val alignToStart: Boolean,
8 | )
9 |
--------------------------------------------------------------------------------
/core/domain/src/main/java/com/mrsep/musicrecognizer/core/domain/preferences/RecognitionServiceConfig.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.domain.preferences
2 |
3 | sealed class RecognitionServiceConfig
4 |
5 | data class AuddConfig(
6 | val apiToken: String,
7 | ) : RecognitionServiceConfig()
8 |
9 | data class AcrCloudConfig(
10 | val host: String,
11 | val accessKey: String,
12 | val accessSecret: String,
13 | ) : RecognitionServiceConfig()
14 |
--------------------------------------------------------------------------------
/core/domain/src/main/java/com/mrsep/musicrecognizer/core/domain/preferences/TrackFilter.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.domain.preferences
2 |
3 | data class TrackFilter(
4 | val favoritesMode: FavoritesMode,
5 | val sortBy: SortBy,
6 | val orderBy: OrderBy,
7 | val dateRange: LongRange,
8 | ) {
9 | val isDefault by lazy { this == getDefault() }
10 |
11 | companion object {
12 | fun getDefault() = TrackFilter(
13 | favoritesMode = FavoritesMode.All,
14 | sortBy = SortBy.RecognitionDate,
15 | orderBy = OrderBy.Desc,
16 | dateRange = Long.MIN_VALUE..Long.MAX_VALUE,
17 | )
18 | }
19 | }
20 |
21 | enum class FavoritesMode { All, OnlyFavorites, ExcludeFavorites }
22 | enum class SortBy { RecognitionDate, Title, Artist, ReleaseDate }
23 | enum class OrderBy { Asc, Desc }
24 |
--------------------------------------------------------------------------------
/core/domain/src/main/java/com/mrsep/musicrecognizer/core/domain/recognition/AudioRecording.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.domain.recognition
2 |
3 | import java.time.Instant
4 | import kotlin.time.Duration
5 |
6 | class AudioRecording(
7 | val data: ByteArray,
8 | val duration: Duration,
9 | val nonSilenceDuration: Duration,
10 | val startTimestamp: Instant,
11 | val isFallback: Boolean,
12 | )
13 |
--------------------------------------------------------------------------------
/core/domain/src/main/java/com/mrsep/musicrecognizer/core/domain/recognition/AudioRecordingController.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.domain.recognition
2 |
3 | import com.mrsep.musicrecognizer.core.domain.recognition.model.RecordingScheme
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface AudioRecordingController {
7 |
8 | val soundLevel: Flow
9 |
10 | fun audioRecordingFlow(scheme: RecordingScheme): Flow>
11 | }
12 |
--------------------------------------------------------------------------------
/core/domain/src/main/java/com/mrsep/musicrecognizer/core/domain/recognition/ConfigValidator.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.domain.recognition
2 |
3 | import com.mrsep.musicrecognizer.core.domain.preferences.RecognitionServiceConfig
4 |
5 | interface ConfigValidator {
6 |
7 | suspend fun validate(config: RecognitionServiceConfig): ConfigValidationResult
8 | }
9 |
10 | sealed class ConfigValidationResult {
11 |
12 | data object Success : ConfigValidationResult()
13 |
14 | sealed class Error : ConfigValidationResult() {
15 | data object Empty : Error()
16 | data object AuthError : Error()
17 | data object ApiUsageLimited : Error()
18 | data object BadConnection : Error()
19 | data object UnknownError : Error()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/core/domain/src/main/java/com/mrsep/musicrecognizer/core/domain/recognition/EnqueuedRecognitionRepository.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.domain.recognition
2 |
3 | import com.mrsep.musicrecognizer.core.domain.recognition.model.EnqueuedRecognition
4 | import kotlinx.coroutines.flow.Flow
5 | import java.io.File
6 |
7 | interface EnqueuedRecognitionRepository {
8 |
9 | suspend fun update(recognition: EnqueuedRecognition)
10 |
11 | suspend fun updateTitle(recognitionId: Int, newTitle: String)
12 |
13 | suspend fun delete(recognitionIds: List)
14 |
15 | suspend fun deleteAll()
16 |
17 | suspend fun createRecognition(audioRecording: AudioRecording, title: String): Int?
18 |
19 | suspend fun getRecordingFile(recognitionId: Int): File?
20 |
21 | suspend fun getRecording(recognitionId: Int): AudioRecording?
22 |
23 | fun getRecognitionFlow(recognitionId: Int): Flow
24 |
25 | fun getAllRecognitionsFlow(): Flow>
26 | }
27 |
--------------------------------------------------------------------------------
/core/domain/src/main/java/com/mrsep/musicrecognizer/core/domain/recognition/EnqueuedRecognitionScheduler.kt:
--------------------------------------------------------------------------------
1 | package com.mrsep.musicrecognizer.core.domain.recognition
2 |
3 | import com.mrsep.musicrecognizer.core.domain.recognition.model.ScheduledJobStatus
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface EnqueuedRecognitionScheduler {
7 |
8 | fun enqueue(recognitionIds: List, forceLaunch: Boolean)
9 |
10 | fun cancel(recognitionIds: List)
11 |
12 | fun cancelAll()
13 |
14 | fun getJobStatusFlow(recognitionId: Int): Flow
15 |
16 | fun getJobStatusForAllFlow(): Flow