Skip to content

Commit 5f303b1

Browse files
authored
Merge pull request #1112 from OxygenCobalt/dev
v4.0.5
2 parents 2b51b83 + 7d2a060 commit 5f303b1

85 files changed

Lines changed: 3309 additions & 568 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENT.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CLAUDE.md

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 4.0.5
4+
5+
#### What's Improved
6+
- Re-added folder exclusion
7+
8+
#### What's Fixed
9+
- Fixed slow loading on large libraries
10+
311
## 4.0.3
412

513
#### What's Improved

CLAUDE.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
Auxio is a modern Android music player written in Kotlin that emphasizes simplicity and performance. It uses Media3 ExoPlayer for playback and includes a custom native music indexing library called musikr.
8+
9+
## Build System & Commands
10+
11+
### Prerequisites
12+
- `cmake` and `ninja-build` must be installed before building
13+
- Project uses git submodules - clone with `git clone --recurse-submodules`
14+
- **Cannot build on Windows** - requires Unix-based system for Media3 build scripts
15+
16+
### Common Build Commands
17+
```bash
18+
# Build debug version
19+
./gradlew assembleDebug
20+
21+
# Build release version
22+
./gradlew assembleRelease
23+
24+
# Run tests
25+
./gradlew test
26+
27+
# Run connected tests
28+
./gradlew connectedAndroidTest
29+
30+
# Code formatting (Spotless)
31+
./gradlew spotlessApply
32+
33+
# Check code formatting
34+
./gradlew spotlessCheck
35+
```
36+
37+
### Development Modules
38+
- `app/` - Main Android application
39+
- `musikr/` - Native music indexing library with TagLib integration
40+
- `media/` - Patched Media3 ExoPlayer libraries (git submodule)
41+
42+
## Architecture Overview
43+
44+
### Core Architecture Pattern
45+
- **MVVM** with ViewModels for each major screen (Home, Detail, Playback, Search)
46+
- **Repository Pattern** - MusicRepository as single source of truth for music data
47+
- **Dependency Injection** - Hilt/Dagger for modular dependency management
48+
- **Single Activity Architecture** with Navigation Components
49+
- **Service-Oriented** background music processing
50+
51+
### Key Modules & Components
52+
53+
#### Music Management (`app/src/main/java/org/oxycblt/auxio/music/`)
54+
- `MusicRepository` - Central music library state management
55+
- `MusicViewModel` - UI layer for music data access
56+
- `service/` - MediaBrowserServiceCompat integration
57+
- `shim/` - Abstraction layer for musikr integration
58+
59+
#### Playback Engine (`app/src/main/java/org/oxycblt/auxio/playback/`)
60+
- `PlaybackViewModel` - Main playback UI interface
61+
- `PlaybackStateManager` - Core playback state management
62+
- `service/` - Background ExoPlayer integration
63+
- `queue/` - Playback queue management
64+
- `persist/` - Playback state persistence (Room database)
65+
- `replaygain/` - Audio processing for volume normalization
66+
67+
#### UI Structure (`app/src/main/java/org/oxycblt/auxio/`)
68+
- `home/` - Main library browsing with customizable tabs
69+
- `detail/` - Album, artist, genre, and playlist detail views
70+
- `search/` - Music search functionality
71+
- `list/` - Shared list components with sorting/filtering
72+
- `ui/` - Custom UI components, theming, animations
73+
74+
#### Native Music Library (`musikr/`)
75+
- **Pipeline-based indexing** - Multi-stage scanning (Explore → Extract → Evaluate)
76+
- **TagLib integration** - Native C++ metadata extraction
77+
- **File system caching** - Device storage access optimization
78+
- **Cover art management** - Embedded and external cover support
79+
80+
### Data Flow
81+
1. musikr scans and indexes music files from device storage
82+
2. MusicRepository manages indexed library and exposes to UI
83+
3. ViewModels provide reactive state for UI components using StateFlow
84+
4. PlaybackService handles audio playback via ExoPlayer
85+
5. UI observes ViewModels and updates reactively
86+
87+
## Code Conventions
88+
89+
### Language & Libraries
90+
- **Kotlin** - Primary language with coroutines for async operations
91+
- **Hilt/Dagger** - Dependency injection
92+
- **Room** - Local database for persistence
93+
- **Navigation Components** - Fragment navigation
94+
- **ViewBinding** - Type-safe view access
95+
- **StateFlow/LiveData** - Reactive state management
96+
- **Coil** - Image loading for cover art
97+
98+
### Code Style
99+
- Uses Spotless with ktfmt Dropbox style formatting
100+
- License header required (see NOTICE file)
101+
- C++ code uses Eclipse CDT formatting (eclipse-cdt.xml)
102+
103+
## Testing
104+
105+
### Test Structure
106+
- Unit tests: `src/test/` directories
107+
- Integration tests: `src/androidTest/` directories
108+
- musikr module includes Robolectric tests
109+
110+
### Test Dependencies
111+
- JUnit 4 for unit testing
112+
- MockK for mocking
113+
- Robolectric for Android unit tests
114+
- Espresso for UI tests
115+
116+
## Key Development Notes
117+
118+
- **Custom Media3**: Uses patched Media3 ExoPlayer for enhanced playback features
119+
- **Native Dependencies**: musikr module builds TagLib from source during compilation
120+
- **Permission Handling**: Requires storage permissions for music file access
121+
- **Background Services**: Foreground service for continuous music playback
122+
- **Widget Support**: Multiple widget layouts with automatic sizing adaptation
123+
- **ReplayGain**: Full support for MP3, FLAC, OGG, OPUS, and MP4 files

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
<h1 align="center"><b>Auxio</b></h1>
33
<h4 align="center">A simple, rational music player for android.</h4>
44
<p align="center">
5-
<a href="https://github.com/oxygencobalt/Auxio/releases/tag/v4.0.4">
6-
<img alt="Latest Version" src="https://img.shields.io/static/v1?label=tag&message=v4.0.4&color=64B5F6&style=flat">
5+
<a href="https://github.com/oxygencobalt/Auxio/releases/tag/v4.0.5">
6+
<img alt="Latest Version" src="https://img.shields.io/static/v1?label=tag&message=v4.0.5&color=64B5F6&style=flat">
77
</a>
88
<a href="https://github.com/oxygencobalt/Auxio/releases/">
99
<img alt="Releases" src="https://img.shields.io/github/downloads/OxygenCobalt/Auxio/total.svg?color=4B95DE&style=flat">

app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ android {
1818

1919
defaultConfig {
2020
applicationId namespace
21-
versionName "4.0.4"
22-
versionCode 63
21+
versionName "4.0.5"
22+
versionCode 64
2323

2424
minSdk min_sdk
2525
targetSdk target_sdk

app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import kotlinx.coroutines.withContext
2929
import kotlinx.coroutines.yield
3030
import org.oxycblt.auxio.image.covers.SettingCovers
3131
import org.oxycblt.auxio.music.MusicRepository.IndexingWorker
32+
import org.oxycblt.auxio.music.shim.WriteOnlyFileTreeCache
3233
import org.oxycblt.auxio.music.shim.WriteOnlyMutableCache
3334
import org.oxycblt.musikr.IndexingProgress
3435
import org.oxycblt.musikr.Interpretation
@@ -37,9 +38,11 @@ import org.oxycblt.musikr.Music
3738
import org.oxycblt.musikr.Musikr
3839
import org.oxycblt.musikr.MutableLibrary
3940
import org.oxycblt.musikr.Playlist
41+
import org.oxycblt.musikr.Query
4042
import org.oxycblt.musikr.Song
4143
import org.oxycblt.musikr.Storage
4244
import org.oxycblt.musikr.cache.MutableCache
45+
import org.oxycblt.musikr.fs.device.FileTreeCache
4346
import org.oxycblt.musikr.playlist.db.StoredPlaylists
4447
import org.oxycblt.musikr.tag.interpret.Naming
4548
import org.oxycblt.musikr.tag.interpret.Separators
@@ -385,16 +388,20 @@ constructor(
385388
Naming.simple()
386389
}
387390
val locations = musicSettings.musicLocations
391+
val excludedLocations = musicSettings.excludedLocations
388392
val withHidden = musicSettings.withHidden
389393

390394
val currentRevision = musicSettings.revision
391395
val newRevision = currentRevision?.takeIf { withCache } ?: UUID.randomUUID()
392396
val cache = if (withCache) cache else WriteOnlyMutableCache(cache)
393397
val covers = settingCovers.mutate(context, newRevision)
394-
val storage = Storage(cache, covers, storedPlaylists)
398+
val fileTreeCache =
399+
if (withCache) FileTreeCache.from(context)
400+
else WriteOnlyFileTreeCache(FileTreeCache.from(context))
401+
val query = Query(source = locations, exclude = excludedLocations)
402+
val storage = Storage(cache, covers, storedPlaylists, fileTreeCache)
395403
val interpretation = Interpretation(nameFactory, separators, withHidden)
396-
val result =
397-
Musikr.new(context, storage, interpretation).run(locations, ::emitIndexingProgress)
404+
val result = Musikr.new(context, storage, interpretation).run(query, ::emitIndexingProgress)
398405
// Music loading completed, update the revision right now so we re-use this work
399406
// later.
400407
musicSettings.revision = newRevision

app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ package org.oxycblt.auxio.music
2020

2121
import android.content.Context
2222
import androidx.core.content.edit
23+
import androidx.core.net.toUri
2324
import dagger.hilt.android.qualifiers.ApplicationContext
2425
import java.util.UUID
2526
import javax.inject.Inject
2627
import org.oxycblt.auxio.R
2728
import org.oxycblt.auxio.settings.Settings
28-
import org.oxycblt.musikr.fs.MusicLocation
29+
import org.oxycblt.musikr.fs.Location
2930
import timber.log.Timber as L
3031

3132
/**
@@ -37,7 +38,9 @@ interface MusicSettings : Settings<MusicSettings.Listener> {
3738
/** The current library revision. */
3839
var revision: UUID?
3940
/** The locations of music to load. */
40-
var musicLocations: List<MusicLocation>
41+
var musicLocations: List<Location.Opened>
42+
/** The locations to exclude from music loading. */
43+
var excludedLocations: List<Location.Unopened>
4144
/** Whether to exclude non-music audio files from the music library. */
4245
val excludeNonMusic: Boolean
4346
/** Whether to ignore hidden files and directories during music loading. */
@@ -74,24 +77,39 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext private val cont
7477
}
7578
}
7679

77-
override var musicLocations: List<MusicLocation>
80+
override var musicLocations: List<Location.Opened>
7881
get() {
7982
val locations =
8083
sharedPreferences.getString(getString(R.string.set_key_music_locations), null)
8184
?: return emptyList()
82-
return MusicLocation.existing(context, locations)
85+
return locations.toOpenedLocations()
8386
}
8487
set(value) {
8588
sharedPreferences.edit {
86-
putString(
87-
getString(R.string.set_key_music_locations), MusicLocation.toString(value))
89+
putString(getString(R.string.set_key_music_locations), value.stringify())
8890
commit()
8991
// Sometimes changing this setting just won't actually trigger the listener.
9092
// Only this one. No idea why.
9193
listener?.onMusicLocationsChanged()
9294
}
9395
}
9496

97+
override var excludedLocations: List<Location.Unopened>
98+
get() {
99+
val locations =
100+
sharedPreferences.getString(getString(R.string.set_key_excluded_locations), null)
101+
?: return emptyList()
102+
return locations.toUnopenedLocations()
103+
}
104+
set(value) {
105+
sharedPreferences.edit {
106+
putString(
107+
getString(R.string.set_key_excluded_locations), value.stringifyLocations())
108+
commit()
109+
listener?.onMusicLocationsChanged()
110+
}
111+
}
112+
95113
override val excludeNonMusic: Boolean
96114
get() = sharedPreferences.getBoolean(getString(R.string.set_key_exclude_non_music), true)
97115

@@ -119,7 +137,8 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext private val cont
119137
// TODO: Differentiate "hard reloads" (Need the cache) and "Soft reloads"
120138
// (just need to manipulate data)
121139
when (key) {
122-
getString(R.string.set_key_music_locations) -> {
140+
getString(R.string.set_key_music_locations),
141+
getString(R.string.set_key_excluded_locations) -> {
123142
L.d("Dispatching music locations change")
124143
listener.onMusicLocationsChanged()
125144
}
@@ -136,4 +155,55 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext private val cont
136155
}
137156
}
138157
}
158+
159+
private fun List<Location.Opened>.stringify(): String =
160+
joinToString(separator = ";") { it.uri.toString().replace(";", "\\;") }
161+
162+
private fun String.toOpenedLocations(): List<Location.Opened> =
163+
splitEscaped { it == ';' }
164+
.mapNotNull { Location.Unopened.from(context, it.toUri())?.open(context) }
165+
166+
private fun List<Location>.stringifyLocations(): String =
167+
joinToString(separator = ";") { it.uri.toString().replace(";", "\\;") }
168+
169+
private fun String.toUnopenedLocations(): List<Location.Unopened> =
170+
splitEscaped { it == ';' }.mapNotNull { Location.Unopened.from(context, it.toUri()) }
171+
172+
private inline fun String.splitEscaped(selector: (Char) -> Boolean): List<String> {
173+
val split = mutableListOf<String>()
174+
var currentString = ""
175+
var i = 0
176+
177+
while (i < length) {
178+
val a = get(i)
179+
val b = getOrNull(i + 1)
180+
181+
if (selector(a)) {
182+
// Non-escaped separator, split the string here, making sure any stray whitespace
183+
// is removed.
184+
split.add(currentString)
185+
currentString = ""
186+
i++
187+
continue
188+
}
189+
190+
if (b != null && a == '\\' && selector(b)) {
191+
// Is an escaped character, add the non-escaped variant and skip two
192+
// characters to move on to the next one.
193+
currentString += b
194+
i += 2
195+
} else {
196+
// Non-escaped, increment normally.
197+
currentString += a
198+
i++
199+
}
200+
}
201+
202+
if (currentString.isNotEmpty()) {
203+
// Had an in-progress split string that is now terminated, add it.
204+
split.add(currentString)
205+
}
206+
207+
return split
208+
}
139209
}

0 commit comments

Comments
 (0)