all: eliminate refactor errors

This commit is contained in:
Alexander Capehart 2024-11-19 17:50:56 -07:00
parent f76eafc9d4
commit 556c5d5e0a
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
19 changed files with 300 additions and 396 deletions

View file

@ -30,12 +30,12 @@ import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.image.extractor.Cover import org.oxycblt.auxio.image.extractor.Cover
import org.oxycblt.auxio.image.extractor.ParentCover import org.oxycblt.auxio.image.extractor.ParentCover
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.stack.fs.MimeType
import org.oxycblt.auxio.music.stack.fs.Path
import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.Disc
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.music.info.ReleaseType
import org.oxycblt.auxio.music.stack.fs.MimeType
import org.oxycblt.auxio.music.stack.fs.Path
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
import org.oxycblt.auxio.util.concatLocalized import org.oxycblt.auxio.util.concatLocalized
import org.oxycblt.auxio.util.toUuidOrNull import org.oxycblt.auxio.util.toUuidOrNull

View file

@ -21,27 +21,20 @@ package org.oxycblt.auxio.music
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import java.util.LinkedList
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.oxycblt.auxio.music.cache.CacheRepository
import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.device.DeviceLibrary
import org.oxycblt.auxio.music.device.RawSong
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.metadata.Separators import org.oxycblt.auxio.music.metadata.Separators
import org.oxycblt.auxio.music.stack.Indexer
import org.oxycblt.auxio.music.user.MutableUserLibrary import org.oxycblt.auxio.music.user.MutableUserLibrary
import org.oxycblt.auxio.music.user.UserLibrary import org.oxycblt.auxio.music.user.UserLibrary
import org.oxycblt.auxio.util.DEFAULT_TIMEOUT
import org.oxycblt.auxio.util.forEachWithTimeout
import timber.log.Timber as L import timber.log.Timber as L
/** /**
@ -220,9 +213,7 @@ interface MusicRepository {
class MusicRepositoryImpl class MusicRepositoryImpl
@Inject @Inject
constructor( constructor(
private val cacheRepository: CacheRepository, private val indexer: Indexer,
private val mediaStoreExtractor: MediaStoreExtractor,
private val tagExtractor: TagExtractor,
private val deviceLibraryFactory: DeviceLibrary.Factory, private val deviceLibraryFactory: DeviceLibrary.Factory,
private val userLibraryFactory: UserLibrary.Factory, private val userLibraryFactory: UserLibrary.Factory,
private val musicSettings: MusicSettings private val musicSettings: MusicSettings
@ -355,9 +346,6 @@ constructor(
} }
private suspend fun indexImpl(context: Context, scope: CoroutineScope, withCache: Boolean) { private suspend fun indexImpl(context: Context, scope: CoroutineScope, withCache: Boolean) {
// TODO: Find a way to break up this monster of a method, preferably as another class.
val start = System.currentTimeMillis()
// Make sure we have permissions before going forward. Theoretically this would be better // Make sure we have permissions before going forward. Theoretically this would be better
// done at the UI level, but that intertwines logic and display too much. // done at the UI level, but that intertwines logic and display too much.
if (ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) == if (ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) ==
@ -367,8 +355,6 @@ constructor(
} }
// Obtain configuration information // Obtain configuration information
val constraints =
MediaStoreExtractor.Constraints(musicSettings.excludeNonMusic, musicSettings.musicDirs)
val separators = Separators.from(musicSettings.separators) val separators = Separators.from(musicSettings.separators)
val nameFactory = val nameFactory =
if (musicSettings.intelligentSorting) { if (musicSettings.intelligentSorting) {
@ -377,174 +363,7 @@ constructor(
Name.Known.SimpleFactory Name.Known.SimpleFactory
} }
// Begin with querying MediaStore and the music cache. The former is needed for Auxio val (deviceLibrary, userLibrary) = indexer.run(listOf(), separators, nameFactory)
// to figure out what songs are (probably) on the device, and the latter will be needed
// for discovery (described later). These have no shared state, so they are done in
// parallel.
L.d("Starting MediaStore query")
emitIndexingProgress(IndexingProgress.Indeterminate)
val mediaStoreQueryJob =
scope.async {
val query =
try {
mediaStoreExtractor.query(constraints)
} catch (e: Exception) {
// Normally, errors in an async call immediately bubble up to the Looper
// and crash the app. Thus, we have to wrap any error into a Result
// and then manually forward it to the try block that indexImpl is
// called from.
return@async Result.failure(e)
}
Result.success(query)
}
// Since this main thread is a co-routine, we can do operations in parallel in a way
// identical to calling async.
val cache =
if (withCache) {
L.d("Reading cache")
cacheRepository.readCache()
} else {
null
}
L.d("Awaiting MediaStore query")
val query = mediaStoreQueryJob.await().getOrThrow()
// We now have all the information required to start the "discovery" process. This
// is the point at which Auxio starts scanning each file given from MediaStore and
// transforming it into a music library. MediaStore normally
L.d("Starting discovery")
val incompleteSongs = Channel<RawSong>(Channel.UNLIMITED) // Not fully populated w/metadata
val completeSongs = Channel<RawSong>(Channel.UNLIMITED) // Populated with quality metadata
val processedSongs = Channel<RawSong>(Channel.UNLIMITED) // Transformed into SongImpl
// MediaStoreExtractor discovers all music on the device, and forwards them to either
// DeviceLibrary if cached metadata exists for it, or TagExtractor if cached metadata
// does not exist. In the latter situation, it also applies it's own (inferior) metadata.
L.d("Starting MediaStore discovery")
val mediaStoreJob =
scope.async {
try {
mediaStoreExtractor.consume(query, cache, incompleteSongs, completeSongs)
} catch (e: Exception) {
// To prevent a deadlock, we want to close the channel with an exception
// to cascade to and cancel all other routines before finally bubbling up
// to the main extractor loop.
L.e("MediaStore extraction failed: $e")
incompleteSongs.close(
Exception("MediaStore extraction failed: ${e.stackTraceToString()}"))
return@async
}
incompleteSongs.close()
}
// TagExtractor takes the incomplete songs from MediaStoreExtractor, parses up-to-date
// metadata for them, and then forwards it to DeviceLibrary.
L.d("Starting tag extraction")
val tagJob =
scope.async {
try {
tagExtractor.consume(incompleteSongs, completeSongs)
} catch (e: Exception) {
L.e("Tag extraction failed: $e")
completeSongs.close(
Exception("Tag extraction failed: ${e.stackTraceToString()}"))
return@async
}
completeSongs.close()
}
// DeviceLibrary constructs music parent instances as song information is provided,
// and then forwards them to the primary loading loop.
L.d("Starting DeviceLibrary creation")
val deviceLibraryJob =
scope.async(Dispatchers.Default) {
val deviceLibrary =
try {
deviceLibraryFactory.create(
completeSongs, processedSongs, separators, nameFactory)
} catch (e: Exception) {
L.e("DeviceLibrary creation failed: $e")
processedSongs.close(
Exception("DeviceLibrary creation failed: ${e.stackTraceToString()}"))
return@async Result.failure(e)
}
processedSongs.close()
Result.success(deviceLibrary)
}
// We could keep track of a total here, but we also need to collate this RawSong information
// for when we write the cache later on in the finalization step.
val rawSongs = LinkedList<RawSong>()
// Use a longer timeout so that dependent components can timeout and throw errors that
// provide more context than if we timed out here.
processedSongs.forEachWithTimeout(DEFAULT_TIMEOUT * 2) {
rawSongs.add(it)
// Since discovery takes up the bulk of the music loading process, we switch to
// indicating a defined amount of loaded songs in comparison to the projected amount
// of songs that were queried.
emitIndexingProgress(IndexingProgress.Songs(rawSongs.size, query.projectedTotal))
}
withTimeout(DEFAULT_TIMEOUT) {
mediaStoreJob.await()
tagJob.await()
}
// Deliberately done after the involved initialization step to make it less likely
// that the short-circuit occurs so quickly as to break the UI.
// TODO: Do not error, instead just wipe the entire library.
if (rawSongs.isEmpty()) {
L.e("Music library was empty")
throw NoMusicException()
}
// Now that the library is effectively loaded, we can start the finalization step, which
// involves writing new cache information and creating more music data that is derived
// from the library (e.g playlists)
L.d("Discovered ${rawSongs.size} songs, starting finalization")
// We have no idea how long the cache will take, and the playlist construction
// will be too fast to indicate, so switch back to an indeterminate state.
emitIndexingProgress(IndexingProgress.Indeterminate)
// The UserLibrary job is split into a query and construction step, a la MediaStore.
// This way, we can start working on playlists even as DeviceLibrary might still be
// working on parent information.
L.d("Starting UserLibrary query")
val userLibraryQueryJob =
scope.async {
val rawPlaylists =
try {
userLibraryFactory.query()
} catch (e: Exception) {
return@async Result.failure(e)
}
Result.success(rawPlaylists)
}
// The cache might not exist, or we might have encountered a song not present in it.
// Both situations require us to rewrite the cache in bulk. This is also done parallel
// since the playlist read will probably take some time.
// TODO: Read/write from the cache incrementally instead of in bulk?
if (cache == null || cache.invalidated) {
L.d("Writing cache [why=${cache?.invalidated}]")
cacheRepository.writeCache(rawSongs)
}
// Create UserLibrary once we finally get the required components for it.
L.d("Awaiting UserLibrary query")
val rawPlaylists = userLibraryQueryJob.await().getOrThrow()
L.d("Awaiting DeviceLibrary creation")
val deviceLibrary = deviceLibraryJob.await().getOrThrow()
L.d("Starting UserLibrary creation")
val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary, nameFactory)
// Loading process is functionally done, indicate such
L.d(
"Successfully indexed music library [device=$deviceLibrary " +
"user=$userLibrary time=${System.currentTimeMillis() - start}]")
emitIndexingCompletion(null)
val deviceLibraryChanged: Boolean val deviceLibraryChanged: Boolean
val userLibraryChanged: Boolean val userLibraryChanged: Boolean

View file

@ -20,10 +20,8 @@ package org.oxycblt.auxio.music.device
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
@ -32,13 +30,9 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.stack.fs.Path
import org.oxycblt.auxio.music.stack.fs.contentResolverSafe
import org.oxycblt.auxio.music.stack.fs.useQuery
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.metadata.Separators import org.oxycblt.auxio.music.metadata.Separators
import org.oxycblt.auxio.util.forEachWithTimeout import org.oxycblt.auxio.music.stack.fs.Path
import org.oxycblt.auxio.util.sendWithTimeout
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
import timber.log.Timber as L import timber.log.Timber as L
@ -341,7 +335,7 @@ class DeviceLibraryImpl(
) : DeviceLibrary { ) : DeviceLibrary {
// Use a mapping to make finding information based on it's UID much faster. // Use a mapping to make finding information based on it's UID much faster.
private val songUidMap = buildMap { songs.forEach { put(it.uid, it.finalize()) } } private val songUidMap = buildMap { songs.forEach { put(it.uid, it.finalize()) } }
// private val songPathMap = buildMap { songs.forEach { put(it.path, it) } } // private val songPathMap = buildMap { songs.forEach { put(it.path, it) } }
private val albumUidMap = buildMap { albums.forEach { put(it.uid, it.finalize()) } } private val albumUidMap = buildMap { albums.forEach { put(it.uid, it.finalize()) } }
private val artistUidMap = buildMap { artists.forEach { put(it.uid, it.finalize()) } } private val artistUidMap = buildMap { artists.forEach { put(it.uid, it.finalize()) } }
private val genreUidMap = buildMap { genres.forEach { put(it.uid, it.finalize()) } } private val genreUidMap = buildMap { genres.forEach { put(it.uid, it.finalize()) } }
@ -366,14 +360,15 @@ class DeviceLibraryImpl(
override fun findSongByPath(path: Path) = null override fun findSongByPath(path: Path) = null
override fun findSongForUri(context: Context, uri: Uri) = null override fun findSongForUri(context: Context, uri: Uri) = null
// context.contentResolverSafe.useQuery( // context.contentResolverSafe.useQuery(
// uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) { cursor -> // uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) { cursor ->
// cursor.moveToFirst() // cursor.moveToFirst()
// // We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a // // We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a
// // song. Do what we can to hopefully find the song the user wanted to open. // // song. Do what we can to hopefully find the song the user wanted to open.
// val displayName = // val displayName =
// cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) //
// val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)) // cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
// songs.find { it.path.name == displayName && it.size == size } // val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
// } // songs.find { it.path.name == displayName && it.size == size }
// }
} }

View file

@ -28,16 +28,13 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.stack.fs.MimeType
import org.oxycblt.auxio.music.stack.fs.toAlbumCoverUri
import org.oxycblt.auxio.music.stack.fs.toAudioUri
import org.oxycblt.auxio.music.stack.fs.toSongCoverUri
import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.Disc
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.music.info.ReleaseType
import org.oxycblt.auxio.music.metadata.Separators import org.oxycblt.auxio.music.metadata.Separators
import org.oxycblt.auxio.music.stack.extractor.parseId3GenreNames import org.oxycblt.auxio.music.stack.extractor.parseId3GenreNames
import org.oxycblt.auxio.music.stack.fs.MimeType
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
import org.oxycblt.auxio.util.positiveOrNull import org.oxycblt.auxio.util.positiveOrNull
import org.oxycblt.auxio.util.toUuidOrNull import org.oxycblt.auxio.util.toUuidOrNull
@ -76,31 +73,28 @@ class SongImpl(
} }
override val name = override val name =
nameFactory.parse( nameFactory.parse(
requireNotNull(rawSong.name) { "Invalid raw ${rawSong.path}: No title" }, requireNotNull(rawSong.name) { "Invalid raw ${rawSong.file.path}: No title" },
rawSong.sortName) rawSong.sortName)
override val track = rawSong.track override val track = rawSong.track
override val disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) } override val disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) }
override val date = rawSong.date override val date = rawSong.date
override val uri = override val uri = rawSong.file.uri
requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.path}: No id" }.toAudioUri() override val path = rawSong.file.path
override val path = requireNotNull(rawSong.path) { "Invalid raw ${rawSong.path}: No path" } override val mimeType = MimeType(fromExtension = rawSong.file.mimeType, fromFormat = null)
override val mimeType = override val size =
MimeType( requireNotNull(rawSong.file.size) { "Invalid raw ${rawSong.file.path}: No size" }
fromExtension =
requireNotNull(rawSong.extensionMimeType) {
"Invalid raw ${rawSong.path}: No mime type"
},
fromFormat = null)
override val size = requireNotNull(rawSong.size) { "Invalid raw ${rawSong.path}: No size" }
override val durationMs = override val durationMs =
requireNotNull(rawSong.durationMs) { "Invalid raw ${rawSong.path}: No duration" } requireNotNull(rawSong.durationMs) { "Invalid raw ${rawSong.file.path}: No duration" }
override val replayGainAdjustment = override val replayGainAdjustment =
ReplayGainAdjustment( ReplayGainAdjustment(
track = rawSong.replayGainTrackAdjustment, album = rawSong.replayGainAlbumAdjustment) track = rawSong.replayGainTrackAdjustment, album = rawSong.replayGainAlbumAdjustment)
// TODO: See what we want to do with date added now that we can't get it anymore.
override val dateAdded = override val dateAdded =
requireNotNull(rawSong.dateAdded) { "Invalid raw ${rawSong.path}: No date added" } requireNotNull(rawSong.file.lastModified) {
"Invalid raw ${rawSong.file.path}: No date added"
}
private var _album: AlbumImpl? = null private var _album: AlbumImpl? = null
override val album: Album override val album: Album
@ -114,18 +108,8 @@ class SongImpl(
override val genres: List<Genre> override val genres: List<Genre>
get() = _genres get() = _genres
override val cover = // TODO: Rebuild cover system
rawSong.coverPerceptualHash?.let { override val cover = Cover.External(rawSong.file.uri)
// We were able to confirm that the song had a parsable cover and can be used on
// a per-song basis. Otherwise, just fall back to a per-album cover instead, as
// it implies either a cover.jpg pattern is used (likely) or ExoPlayer does not
// support the cover metadata of a given spec (unlikely).
Cover.Embedded(
requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.path}: No id" }
.toSongCoverUri(),
uri,
it)
} ?: Cover.External(requireNotNull(rawSong.albumMediaStoreId).toAlbumCoverUri())
/** /**
* The [RawAlbum] instances collated by the [Song]. This can be used to group [Song]s into an * The [RawAlbum] instances collated by the [Song]. This can be used to group [Song]s into an
@ -184,14 +168,10 @@ class SongImpl(
rawAlbum = rawAlbum =
RawAlbum( RawAlbum(
mediaStoreId =
requireNotNull(rawSong.albumMediaStoreId) {
"Invalid raw ${rawSong.path}: No album id"
},
musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(), musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(),
name = name =
requireNotNull(rawSong.albumName) { requireNotNull(rawSong.albumName) {
"Invalid raw ${rawSong.path}: No album name" "Invalid raw ${rawSong.file.path}: No album name"
}, },
sortName = rawSong.albumSortName, sortName = rawSong.albumSortName,
releaseType = ReleaseType.parse(separators.split(rawSong.releaseTypes)), releaseType = ReleaseType.parse(separators.split(rawSong.releaseTypes)),

View file

@ -22,9 +22,9 @@ import java.util.UUID
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.stack.fs.DeviceFile
import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.music.info.ReleaseType
import org.oxycblt.auxio.music.stack.fs.DeviceFile
/** /**
* Raw information about a [SongImpl] obtained from the filesystem/Extractor instances. * Raw information about a [SongImpl] obtained from the filesystem/Extractor instances.
@ -53,10 +53,6 @@ data class RawSong(
var subtitle: String? = null, var subtitle: String? = null,
/** @see Song.date */ /** @see Song.date */
var date: Date? = null, var date: Date? = null,
/** @see Song.cover */
var coverPerceptualHash: String? = null,
/** @see RawAlbum.mediaStoreId */
var albumMediaStoreId: Long? = null,
/** @see RawAlbum.musicBrainzId */ /** @see RawAlbum.musicBrainzId */
var albumMusicBrainzId: String? = null, var albumMusicBrainzId: String? = null,
/** @see RawAlbum.name */ /** @see RawAlbum.name */
@ -87,11 +83,6 @@ data class RawSong(
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
data class RawAlbum( data class RawAlbum(
/**
* The ID of the [AlbumImpl]'s grouping, obtained from MediaStore. Note that this ID is highly
* unstable and should only be used for accessing the system-provided cover art.
*/
val mediaStoreId: Long,
/** @see Music.uid */ /** @see Music.uid */
override val musicBrainzId: UUID?, override val musicBrainzId: UUID?,
/** @see Music.name */ /** @see Music.name */

View file

@ -27,12 +27,12 @@ import java.io.InputStreamReader
import java.io.OutputStream import java.io.OutputStream
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.music.stack.extractor.correctWhitespace
import org.oxycblt.auxio.music.stack.fs.Components import org.oxycblt.auxio.music.stack.fs.Components
import org.oxycblt.auxio.music.stack.fs.Path import org.oxycblt.auxio.music.stack.fs.Path
import org.oxycblt.auxio.music.stack.fs.Volume import org.oxycblt.auxio.music.stack.fs.Volume
import org.oxycblt.auxio.music.stack.fs.VolumeManager import org.oxycblt.auxio.music.stack.fs.VolumeManager
import org.oxycblt.auxio.music.stack.extractor.correctWhitespace
import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
import timber.log.Timber as L import timber.log.Timber as L

View file

@ -119,7 +119,6 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties.
return AudioProperties( return AudioProperties(
bitrate, bitrate,
sampleRate, sampleRate,
MimeType(fromExtension = song.mimeType.fromExtension, fromFormat = formatMimeType) MimeType(fromExtension = song.mimeType.fromExtension, fromFormat = formatMimeType))
)
} }
} }

View file

@ -22,19 +22,10 @@ import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import org.oxycblt.auxio.music.stack.extractor.ExoPlayerTagExtractor
import org.oxycblt.auxio.music.stack.extractor.ExoPlayerTagExtractorImpl
import org.oxycblt.auxio.music.stack.extractor.TagInterpreter2
import org.oxycblt.auxio.music.stack.extractor.TagInterpreter2Impl
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
interface MetadataModule { interface MetadataModule {
@Binds fun tagInterpreter(interpreter: TagInterpreterImpl): TagInterpreter @Binds
fun audioPropertiesFactory(interpreter: AudioPropertiesFactoryImpl): AudioProperties.Factory
@Binds fun tagInterpreter2(interpreter: TagInterpreter2Impl): TagInterpreter2
@Binds fun exoPlayerTagExtractor(extractor: ExoPlayerTagExtractorImpl): ExoPlayerTagExtractor
@Binds fun audioPropertiesFactory(factory: AudioPropertiesFactoryImpl): AudioProperties.Factory
} }

View file

@ -1,3 +1,21 @@
/*
* Copyright (c) 2024 Auxio Project
* Separators.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.metadata package org.oxycblt.auxio.music.metadata
import org.oxycblt.auxio.music.stack.extractor.correctWhitespace import org.oxycblt.auxio.music.stack.extractor.correctWhitespace

View file

@ -1,6 +1,25 @@
/*
* Copyright (c) 2024 Auxio Project
* Indexer.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.stack package org.oxycblt.auxio.music.stack
import android.net.Uri import android.net.Uri
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -14,7 +33,6 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.toList
import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.device.DeviceLibrary
import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.device.RawSong
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
@ -24,44 +42,51 @@ import org.oxycblt.auxio.music.stack.extractor.ExoPlayerTagExtractor
import org.oxycblt.auxio.music.stack.extractor.TagResult import org.oxycblt.auxio.music.stack.extractor.TagResult
import org.oxycblt.auxio.music.stack.fs.DeviceFile import org.oxycblt.auxio.music.stack.fs.DeviceFile
import org.oxycblt.auxio.music.stack.fs.DeviceFiles import org.oxycblt.auxio.music.stack.fs.DeviceFiles
import org.oxycblt.auxio.music.user.MutableUserLibrary
import org.oxycblt.auxio.music.user.UserLibrary import org.oxycblt.auxio.music.user.UserLibrary
import javax.inject.Inject
interface Indexer { interface Indexer {
suspend fun run(uris: List<Uri>, separators: Separators, nameFactory: Name.Known.Factory): LibraryResult suspend fun run(
uris: List<Uri>,
separators: Separators,
nameFactory: Name.Known.Factory
): LibraryResult
} }
data class LibraryResult(val deviceLibrary: DeviceLibrary, val userLibrary: UserLibrary) data class LibraryResult(val deviceLibrary: DeviceLibrary, val userLibrary: MutableUserLibrary)
class IndexerImpl @Inject constructor( class IndexerImpl
@Inject
constructor(
private val deviceFiles: DeviceFiles, private val deviceFiles: DeviceFiles,
private val tagCache: TagCache, private val tagCache: TagCache,
private val tagExtractor: ExoPlayerTagExtractor, private val tagExtractor: ExoPlayerTagExtractor,
private val deviceLibraryFactory: DeviceLibrary.Factory, private val deviceLibraryFactory: DeviceLibrary.Factory,
private val userLibraryFactory: UserLibrary.Factory private val userLibraryFactory: UserLibrary.Factory
) : Indexer { ) : Indexer {
override suspend fun run(uris: List<Uri>, separators: Separators, nameFactory: Name.Known.Factory) = coroutineScope { override suspend fun run(
val deviceFiles = deviceFiles.explore(uris.asFlow()) uris: List<Uri>,
.flowOn(Dispatchers.IO) separators: Separators,
.buffer() nameFactory: Name.Known.Factory
val tagRead = tagCache.read(deviceFiles) ) = coroutineScope {
.flowOn(Dispatchers.IO) val deviceFiles = deviceFiles.explore(uris.asFlow()).flowOn(Dispatchers.IO).buffer()
.buffer() val tagRead = tagCache.read(deviceFiles).flowOn(Dispatchers.IO).buffer()
val (cacheFiles, cacheSongs) = tagRead.split() val (cacheFiles, cacheSongs) = tagRead.split()
val tagExtractor = val tagExtractor = tagExtractor.process(cacheFiles).flowOn(Dispatchers.IO).buffer()
tagExtractor.process(cacheFiles)
.flowOn(Dispatchers.IO)
.buffer()
val (_, extractorSongs) = tagExtractor.split() val (_, extractorSongs) = tagExtractor.split()
val sharedExtractorSongs = extractorSongs.shareIn( val sharedExtractorSongs =
CoroutineScope(Dispatchers.Main), extractorSongs.shareIn(
started = SharingStarted.WhileSubscribed(), CoroutineScope(Dispatchers.Main),
replay = Int.MAX_VALUE started = SharingStarted.WhileSubscribed(),
) replay = Int.MAX_VALUE)
val tagWrite = async(Dispatchers.IO) { tagCache.write(merge(cacheSongs, sharedExtractorSongs)) } val tagWrite =
async(Dispatchers.IO) { tagCache.write(merge(cacheSongs, sharedExtractorSongs)) }
val rawPlaylists = async(Dispatchers.IO) { userLibraryFactory.query() } val rawPlaylists = async(Dispatchers.IO) { userLibraryFactory.query() }
val deviceLibrary = deviceLibraryFactory.create(merge(cacheSongs, sharedExtractorSongs), {}, separators, nameFactory) val deviceLibrary =
val userLibrary = userLibraryFactory.create(rawPlaylists.await(), deviceLibrary, nameFactory) deviceLibraryFactory.create(
merge(cacheSongs, sharedExtractorSongs), {}, separators, nameFactory)
val userLibrary =
userLibraryFactory.create(rawPlaylists.await(), deviceLibrary, nameFactory)
tagWrite.await() tagWrite.await()
LibraryResult(deviceLibrary, userLibrary) LibraryResult(deviceLibrary, userLibrary)
} }

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2024 Auxio Project
* StackModule.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.stack
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
interface StackModule {
@Binds fun indexer(impl: IndexerImpl): Indexer
}

View file

@ -40,8 +40,7 @@ class TagDatabaseModule {
@Singleton @Singleton
@Provides @Provides
fun database(@ApplicationContext context: Context) = fun database(@ApplicationContext context: Context) =
Room.databaseBuilder( Room.databaseBuilder(context.applicationContext, TagDatabase::class.java, "music_cache.db")
context.applicationContext, TagDatabase::class.java, "music_cache.db")
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.build() .build()

View file

@ -1,20 +1,37 @@
/*
* Copyright (c) 2024 Auxio Project
* TagCache.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.stack.cache package org.oxycblt.auxio.music.stack.cache
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.transform
import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.device.RawSong
import org.oxycblt.auxio.music.stack.fs.DeviceFile
import org.oxycblt.auxio.music.stack.extractor.TagResult import org.oxycblt.auxio.music.stack.extractor.TagResult
import javax.inject.Inject import org.oxycblt.auxio.music.stack.fs.DeviceFile
interface TagCache { interface TagCache {
fun read(files: Flow<DeviceFile>): Flow<TagResult> fun read(files: Flow<DeviceFile>): Flow<TagResult>
suspend fun write(rawSongs: Flow<RawSong>) suspend fun write(rawSongs: Flow<RawSong>)
} }
class TagCacheImpl @Inject constructor( class TagCacheImpl @Inject constructor(private val tagDao: TagDao) : TagCache {
private val tagDao: TagDao
) : TagCache {
override fun read(files: Flow<DeviceFile>) = override fun read(files: Flow<DeviceFile>) =
files.transform<DeviceFile, TagResult> { file -> files.transform<DeviceFile, TagResult> { file ->
val tags = tagDao.selectTags(file.uri.toString(), file.lastModified) val tags = tagDao.selectTags(file.uri.toString(), file.lastModified)
@ -28,8 +45,6 @@ class TagCacheImpl @Inject constructor(
} }
override suspend fun write(rawSongs: Flow<RawSong>) { override suspend fun write(rawSongs: Flow<RawSong>) {
rawSongs.collect { rawSong -> rawSongs.collect { rawSong -> tagDao.updateTags(Tags.fromRaw(rawSong)) }
tagDao.updateTags(Tags.fromRaw(rawSong))
}
} }
} }

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2023 Auxio Project * Copyright (c) 2023 Auxio Project
* CacheDatabase.kt is part of Auxio. * TagDatabase.kt is part of Auxio.
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -43,16 +43,15 @@ interface TagDao {
@Query("SELECT * FROM Tags WHERE uri = :uri AND dateModified = :dateModified") @Query("SELECT * FROM Tags WHERE uri = :uri AND dateModified = :dateModified")
suspend fun selectTags(uri: String, dateModified: Long): Tags? suspend fun selectTags(uri: String, dateModified: Long): Tags?
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateTags(tags: Tags)
suspend fun updateTags(tags: Tags)
} }
@Entity @Entity
@TypeConverters(Tags.Converters::class) @TypeConverters(Tags.Converters::class)
data class Tags( data class Tags(
/** /**
* The Uri of the [RawSong]'s audio file, obtained from SAF. * The Uri of the [RawSong]'s audio file, obtained from SAF. This should ideally be a black box
* This should ideally be a black box only used for comparison. * only used for comparison.
*/ */
@PrimaryKey val uri: String, @PrimaryKey val uri: String,
/** The latest date the [RawSong]'s audio file was modified, as a unix epoch timestamp. */ /** The latest date the [RawSong]'s audio file was modified, as a unix epoch timestamp. */
@ -77,8 +76,6 @@ data class Tags(
var subtitle: String? = null, var subtitle: String? = null,
/** @see RawSong.date */ /** @see RawSong.date */
var date: Date? = null, var date: Date? = null,
/** @see RawSong.coverPerceptualHash */
var coverPerceptualHash: String? = null,
/** @see RawSong.albumMusicBrainzId */ /** @see RawSong.albumMusicBrainzId */
var albumMusicBrainzId: String? = null, var albumMusicBrainzId: String? = null,
/** @see RawSong.albumName */ /** @see RawSong.albumName */
@ -117,8 +114,6 @@ data class Tags(
rawSong.subtitle = subtitle rawSong.subtitle = subtitle
rawSong.date = date rawSong.date = date
rawSong.coverPerceptualHash = coverPerceptualHash
rawSong.albumMusicBrainzId = albumMusicBrainzId rawSong.albumMusicBrainzId = albumMusicBrainzId
rawSong.albumName = albumName rawSong.albumName = albumName
rawSong.albumSortName = albumSortName rawSong.albumSortName = albumSortName
@ -163,7 +158,6 @@ data class Tags(
disc = rawSong.disc, disc = rawSong.disc,
subtitle = rawSong.subtitle, subtitle = rawSong.subtitle,
date = rawSong.date, date = rawSong.date,
coverPerceptualHash = rawSong.coverPerceptualHash,
albumMusicBrainzId = rawSong.albumMusicBrainzId, albumMusicBrainzId = rawSong.albumMusicBrainzId,
albumName = requireNotNull(rawSong.albumName) { "Invalid raw: No album name" }, albumName = requireNotNull(rawSong.albumName) { "Invalid raw: No album name" },
albumSortName = rawSong.albumSortName, albumSortName = rawSong.albumSortName,

View file

@ -1,3 +1,21 @@
/*
* Copyright (c) 2024 Auxio Project
* ExoPlayerTagExtractor.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.stack.extractor package org.oxycblt.auxio.music.stack.extractor
import android.os.HandlerThread import android.os.HandlerThread
@ -5,17 +23,18 @@ import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.MetadataRetriever import androidx.media3.exoplayer.MetadataRetriever
import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.TrackGroupArray import androidx.media3.exoplayer.source.TrackGroupArray
import java.util.concurrent.Future
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.device.RawSong
import org.oxycblt.auxio.music.stack.fs.DeviceFile import org.oxycblt.auxio.music.stack.fs.DeviceFile
import java.util.concurrent.Future
import javax.inject.Inject
import timber.log.Timber as L import timber.log.Timber as L
interface TagResult { interface TagResult {
class Hit(val rawSong: RawSong) : TagResult class Hit(val rawSong: RawSong) : TagResult
class Miss(val file: DeviceFile) : TagResult class Miss(val file: DeviceFile) : TagResult
} }
@ -23,27 +42,23 @@ interface ExoPlayerTagExtractor {
fun process(deviceFiles: Flow<DeviceFile>): Flow<TagResult> fun process(deviceFiles: Flow<DeviceFile>): Flow<TagResult>
} }
class ExoPlayerTagExtractorImpl @Inject constructor( class ExoPlayerTagExtractorImpl
@Inject
constructor(
private val mediaSourceFactory: MediaSource.Factory, private val mediaSourceFactory: MediaSource.Factory,
private val tagInterpreter2: TagInterpreter2, private val tagInterpreter2: TagInterpreter,
) : ExoPlayerTagExtractor { ) : ExoPlayerTagExtractor {
override fun process(deviceFiles: Flow<DeviceFile>) = flow { override fun process(deviceFiles: Flow<DeviceFile>) = flow {
val threadPool = ThreadPool(8, Handler(this)) val threadPool = ThreadPool(8, Handler(this))
deviceFiles.collect { file -> deviceFiles.collect { file -> threadPool.enqueue(file) }
threadPool.enqueue(file)
}
threadPool.empty() threadPool.empty()
} }
private inner class Handler( private inner class Handler(private val collector: FlowCollector<TagResult>) :
private val collector: FlowCollector<TagResult> ThreadPool.Handler<DeviceFile, TrackGroupArray> {
) : ThreadPool.Handler<DeviceFile, TrackGroupArray> {
override suspend fun produce(thread: HandlerThread, input: DeviceFile) = override suspend fun produce(thread: HandlerThread, input: DeviceFile) =
MetadataRetriever.retrieveMetadata( MetadataRetriever.retrieveMetadata(
mediaSourceFactory, mediaSourceFactory, MediaItem.fromUri(input.uri), thread)
MediaItem.fromUri(input.uri),
thread
)
override suspend fun consume(input: DeviceFile, output: TrackGroupArray) { override suspend fun consume(input: DeviceFile, output: TrackGroupArray) {
if (output.isEmpty) { if (output.isEmpty) {
@ -76,10 +91,7 @@ class ExoPlayerTagExtractorImpl @Inject constructor(
private class ThreadPool<I, O>(size: Int, private val handler: Handler<I, O>) { private class ThreadPool<I, O>(size: Int, private val handler: Handler<I, O>) {
private val slots = private val slots =
Array<Slot<I, O>>(size) { Array<Slot<I, O>>(size) {
Slot( Slot(thread = HandlerThread("Auxio:ThreadPool:$it"), task = null)
thread = HandlerThread("Auxio:ThreadPool:$it"),
task = null
)
} }
suspend fun enqueue(input: I) { suspend fun enqueue(input: I) {
@ -114,25 +126,19 @@ private class ThreadPool<I, O>(size: Int, private val handler: Handler<I, O>) {
// In-practice this should never block, as all clients // In-practice this should never block, as all clients
// check if the future is done before calling this function. // check if the future is done before calling this function.
// If you don't maintain that invariant, this will explode. // If you don't maintain that invariant, this will explode.
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext") handler.consume(input, future.get())
handler.consume(input, future.get())
} catch (e: Exception) { } catch (e: Exception) {
L.e("Failed to complete task for $input, ${e.stackTraceToString()}") L.e("Failed to complete task for $input, ${e.stackTraceToString()}")
} }
} }
private data class Slot<I, O>( private data class Slot<I, O>(val thread: HandlerThread, var task: Task<I, O>?)
val thread: HandlerThread,
var task: Task<I, O>?
)
private data class Task<I, O>( private data class Task<I, O>(val input: I, val future: Future<O>)
val input: I,
val future: Future<O>
)
interface Handler<I, O> { interface Handler<I, O> {
suspend fun produce(thread: HandlerThread, input: I): Future<O> suspend fun produce(thread: HandlerThread, input: I): Future<O>
suspend fun consume(input: I, output: O) suspend fun consume(input: I, output: O)
} }
} }

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2024 Auxio Project
* ExtractorModule.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.stack.extractor
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
interface MetadataModule {
@Binds fun tagInterpreter(interpreter: TagInterpreterImpl): TagInterpreter
@Binds fun exoPlayerTagExtractor(extractor: ExoPlayerTagExtractorImpl): ExoPlayerTagExtractor
}

View file

@ -21,7 +21,6 @@ package org.oxycblt.auxio.music.stack.extractor
import androidx.core.text.isDigitsOnly import androidx.core.text.isDigitsOnly
import androidx.media3.exoplayer.MetadataRetriever import androidx.media3.exoplayer.MetadataRetriever
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.image.extractor.CoverExtractor
import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.device.RawSong
import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.nonZeroOrNull
@ -32,7 +31,7 @@ import org.oxycblt.auxio.util.nonZeroOrNull
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
interface TagInterpreter2 { interface TagInterpreter {
/** /**
* Poll to see if this worker is done processing. * Poll to see if this worker is done processing.
* *
@ -41,8 +40,7 @@ interface TagInterpreter2 {
fun interpretOn(textTags: TextTags, rawSong: RawSong) fun interpretOn(textTags: TextTags, rawSong: RawSong)
} }
class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverExtractor) : class TagInterpreterImpl @Inject constructor() : TagInterpreter {
TagInterpreter2 {
override fun interpretOn(textTags: TextTags, rawSong: RawSong) { override fun interpretOn(textTags: TextTags, rawSong: RawSong) {
populateWithId3v2(rawSong, textTags.id3v2) populateWithId3v2(rawSong, textTags.id3v2)
populateWithVorbis(rawSong, textTags.vorbis) populateWithVorbis(rawSong, textTags.vorbis)
@ -51,7 +49,7 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE
private fun populateWithId3v2(rawSong: RawSong, textFrames: Map<String, List<String>>) { private fun populateWithId3v2(rawSong: RawSong, textFrames: Map<String, List<String>>) {
// Song // Song
(textFrames["TXXX:musicbrainz release track id"] (textFrames["TXXX:musicbrainz release track id"]
?: textFrames["TXXX:musicbrainz_releasetrackid"]) ?: textFrames["TXXX:musicbrainz_releasetrackid"])
?.let { rawSong.musicBrainzId = it.first() } ?.let { rawSong.musicBrainzId = it.first() }
textFrames["TIT2"]?.let { rawSong.name = it.first() } textFrames["TIT2"]?.let { rawSong.name = it.first() }
textFrames["TSOT"]?.let { rawSong.sortName = it.first() } textFrames["TSOT"]?.let { rawSong.sortName = it.first() }
@ -76,9 +74,9 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE
// TODO: Handle dates that are in "January" because the actual specific release date // TODO: Handle dates that are in "January" because the actual specific release date
// isn't known? // isn't known?
(textFrames["TDOR"]?.run { Date.from(first()) } (textFrames["TDOR"]?.run { Date.from(first()) }
?: textFrames["TDRC"]?.run { Date.from(first()) } ?: textFrames["TDRC"]?.run { Date.from(first()) }
?: textFrames["TDRL"]?.run { Date.from(first()) } ?: textFrames["TDRL"]?.run { Date.from(first()) }
?: parseId3v23Date(textFrames)) ?: parseId3v23Date(textFrames))
?.let { rawSong.date = it } ?.let { rawSong.date = it }
// Album // Album
@ -88,10 +86,10 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE
textFrames["TALB"]?.let { rawSong.albumName = it.first() } textFrames["TALB"]?.let { rawSong.albumName = it.first() }
textFrames["TSOA"]?.let { rawSong.albumSortName = it.first() } textFrames["TSOA"]?.let { rawSong.albumSortName = it.first() }
(textFrames["TXXX:musicbrainz album type"] (textFrames["TXXX:musicbrainz album type"]
?: textFrames["TXXX:releasetype"] ?: textFrames["TXXX:releasetype"]
?: ?:
// This is a non-standard iTunes extension // This is a non-standard iTunes extension
textFrames["GRP1"]) textFrames["GRP1"])
?.let { rawSong.releaseTypes = it } ?.let { rawSong.releaseTypes = it }
// Artist // Artist
@ -102,31 +100,31 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE
rawSong.artistNames = it rawSong.artistNames = it
} }
(textFrames["TXXX:artistssort"] (textFrames["TXXX:artistssort"]
?: textFrames["TXXX:artists_sort"] ?: textFrames["TXXX:artists_sort"]
?: textFrames["TXXX:artists sort"] ?: textFrames["TXXX:artists sort"]
?: textFrames["TSOP"] ?: textFrames["TSOP"]
?: textFrames["artistsort"] ?: textFrames["artistsort"]
?: textFrames["TXXX:artist sort"]) ?: textFrames["TXXX:artist sort"])
?.let { rawSong.artistSortNames = it } ?.let { rawSong.artistSortNames = it }
// Album artist // Album artist
(textFrames["TXXX:musicbrainz album artist id"] (textFrames["TXXX:musicbrainz album artist id"]
?: textFrames["TXXX:musicbrainz_albumartistid"]) ?: textFrames["TXXX:musicbrainz_albumartistid"])
?.let { rawSong.albumArtistMusicBrainzIds = it } ?.let { rawSong.albumArtistMusicBrainzIds = it }
(textFrames["TXXX:albumartists"] (textFrames["TXXX:albumartists"]
?: textFrames["TXXX:album_artists"] ?: textFrames["TXXX:album_artists"]
?: textFrames["TXXX:album artists"] ?: textFrames["TXXX:album artists"]
?: textFrames["TPE2"] ?: textFrames["TPE2"]
?: textFrames["TXXX:albumartist"] ?: textFrames["TXXX:albumartist"]
?: textFrames["TXXX:album artist"]) ?: textFrames["TXXX:album artist"])
?.let { rawSong.albumArtistNames = it } ?.let { rawSong.albumArtistNames = it }
(textFrames["TXXX:albumartistssort"] (textFrames["TXXX:albumartistssort"]
?: textFrames["TXXX:albumartists_sort"] ?: textFrames["TXXX:albumartists_sort"]
?: textFrames["TXXX:albumartists sort"] ?: textFrames["TXXX:albumartists sort"]
?: textFrames["TXXX:albumartistsort"] ?: textFrames["TXXX:albumartistsort"]
// This is a non-standard iTunes extension // This is a non-standard iTunes extension
?: textFrames["TSO2"] ?: textFrames["TSO2"]
?: textFrames["TXXX:album artist sort"]) ?: textFrames["TXXX:album artist sort"])
?.let { rawSong.albumArtistSortNames = it } ?.let { rawSong.albumArtistSortNames = it }
// Genre // Genre
@ -197,16 +195,14 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE
// Track. // Track.
parseVorbisPositionField( parseVorbisPositionField(
comments["tracknumber"]?.first(), comments["tracknumber"]?.first(),
(comments["totaltracks"] ?: comments["tracktotal"] ?: comments["trackc"])?.first() (comments["totaltracks"] ?: comments["tracktotal"] ?: comments["trackc"])?.first())
)
?.let { rawSong.track = it } ?.let { rawSong.track = it }
// Disc and it's subtitle name. // Disc and it's subtitle name.
parseVorbisPositionField( parseVorbisPositionField(
comments["discnumber"]?.first(), comments["discnumber"]?.first(),
(comments["totaldiscs"] ?: comments["disctotal"] ?: comments["discc"])?.first() (comments["totaldiscs"] ?: comments["disctotal"] ?: comments["discc"])?.first())
)
?.let { rawSong.disc = it } ?.let { rawSong.disc = it }
comments["discsubtitle"]?.let { rawSong.subtitle = it.first() } comments["discsubtitle"]?.let { rawSong.subtitle = it.first() }
@ -217,8 +213,8 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE
// 3. Year, as old vorbis tags tended to use this (I know this because it's the only // 3. Year, as old vorbis tags tended to use this (I know this because it's the only
// date tag that android supports, so it must be 15 years old or more!) // date tag that android supports, so it must be 15 years old or more!)
(comments["originaldate"]?.run { Date.from(first()) } (comments["originaldate"]?.run { Date.from(first()) }
?: comments["date"]?.run { Date.from(first()) } ?: comments["date"]?.run { Date.from(first()) }
?: comments["year"]?.run { Date.from(first()) }) ?: comments["year"]?.run { Date.from(first()) })
?.let { rawSong.date = it } ?.let { rawSong.date = it }
// Album // Album
@ -237,10 +233,10 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE
} }
(comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it } (comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it }
(comments["artistssort"] (comments["artistssort"]
?: comments["artists_sort"] ?: comments["artists_sort"]
?: comments["artists sort"] ?: comments["artists sort"]
?: comments["artistsort"] ?: comments["artistsort"]
?: comments["artist sort"]) ?: comments["artist sort"])
?.let { rawSong.artistSortNames = it } ?.let { rawSong.artistSortNames = it }
// Album artist // Album artist
@ -248,16 +244,16 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE
rawSong.albumArtistMusicBrainzIds = it rawSong.albumArtistMusicBrainzIds = it
} }
(comments["albumartists"] (comments["albumartists"]
?: comments["album_artists"] ?: comments["album_artists"]
?: comments["album artists"] ?: comments["album artists"]
?: comments["albumartist"] ?: comments["albumartist"]
?: comments["album artist"]) ?: comments["album artist"])
?.let { rawSong.albumArtistNames = it } ?.let { rawSong.albumArtistNames = it }
(comments["albumartistssort"] (comments["albumartistssort"]
?: comments["albumartists_sort"] ?: comments["albumartists_sort"]
?: comments["albumartists sort"] ?: comments["albumartists sort"]
?: comments["albumartistsort"] ?: comments["albumartistsort"]
?: comments["album artist sort"]) ?: comments["album artist sort"])
?.let { rawSong.albumArtistSortNames = it } ?.let { rawSong.albumArtistSortNames = it }
// Genre // Genre
@ -281,10 +277,10 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE
// normally the only tag used for opus files, but some software still writes replay gain // normally the only tag used for opus files, but some software still writes replay gain
// tags anyway. // tags anyway.
(comments["r128_track_gain"]?.parseR128Adjustment() (comments["r128_track_gain"]?.parseR128Adjustment()
?: comments["replaygain_track_gain"]?.parseReplayGainAdjustment()) ?: comments["replaygain_track_gain"]?.parseReplayGainAdjustment())
?.let { rawSong.replayGainTrackAdjustment = it } ?.let { rawSong.replayGainTrackAdjustment = it }
(comments["r128_album_gain"]?.parseR128Adjustment() (comments["r128_album_gain"]?.parseR128Adjustment()
?: comments["replaygain_album_gain"]?.parseReplayGainAdjustment()) ?: comments["replaygain_album_gain"]?.parseReplayGainAdjustment())
?.let { rawSong.replayGainAlbumAdjustment = it } ?.let { rawSong.replayGainAlbumAdjustment = it }
} }

View file

@ -37,8 +37,12 @@ interface DeviceFiles {
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class DeviceFilesImpl @Inject constructor(@ApplicationContext private val context: Context) : class DeviceFilesImpl
DeviceFiles { @Inject
constructor(
@ApplicationContext private val context: Context,
private val volumeManager: VolumeManager
) : DeviceFiles {
private val contentResolver = context.contentResolverSafe private val contentResolver = context.contentResolverSafe
override fun explore(uris: Flow<Uri>): Flow<DeviceFile> = override fun explore(uris: Flow<Uri>): Flow<DeviceFile> =
@ -49,6 +53,9 @@ class DeviceFilesImpl @Inject constructor(@ApplicationContext private val contex
uri: Uri, uri: Uri,
relativePath: Components relativePath: Components
): Flow<DeviceFile> = flow { ): Flow<DeviceFile> = flow {
// TODO: Temporary to maintain path api parity
// Figure out what we actually want to do to paths now in saf world.
val external = volumeManager.getInternalVolume()
contentResolver.useQuery(uri, PROJECTION) { cursor -> contentResolver.useQuery(uri, PROJECTION) { cursor ->
val childUriIndex = val childUriIndex =
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID) cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
@ -77,7 +84,7 @@ class DeviceFilesImpl @Inject constructor(@ApplicationContext private val contex
// rather than just being a glorified async. // rather than just being a glorified async.
val lastModified = cursor.getLong(lastModifiedIndex) val lastModified = cursor.getLong(lastModifiedIndex)
val size = cursor.getLong(sizeIndex) val size = cursor.getLong(sizeIndex)
emit(DeviceFile(childUri, mimeType, path, size, lastModified)) emit(DeviceFile(childUri, mimeType, Path(external, path), size, lastModified))
} }
} }
// Hypothetically, we could just emitAll as we recurse into a new directory, // Hypothetically, we could just emitAll as we recurse into a new directory,
@ -96,9 +103,14 @@ class DeviceFilesImpl @Inject constructor(@ApplicationContext private val contex
DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_MIME_TYPE,
DocumentsContract.Document.COLUMN_SIZE, DocumentsContract.Document.COLUMN_SIZE,
DocumentsContract.Document.COLUMN_LAST_MODIFIED DocumentsContract.Document.COLUMN_LAST_MODIFIED)
)
} }
} }
data class DeviceFile(val uri: Uri, val mimeType: String, val path: Components, val size: Long, val lastModified: Long) data class DeviceFile(
val uri: Uri,
val mimeType: String,
val path: Path,
val size: Long,
val lastModified: Long
)

View file

@ -51,4 +51,6 @@ class FsModule {
interface FsBindsModule { interface FsBindsModule {
@Binds @Binds
fun documentPathFactory(documentTreePathFactory: DocumentPathFactoryImpl): DocumentPathFactory fun documentPathFactory(documentTreePathFactory: DocumentPathFactoryImpl): DocumentPathFactory
@Binds fun deviceFiles(deviceFilesImpl: DeviceFilesImpl): DeviceFiles
} }