all: eliminate refactor errors
This commit is contained in:
parent
f76eafc9d4
commit
556c5d5e0a
19 changed files with 300 additions and 396 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)),
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -71,4 +96,4 @@ class IndexerImpl @Inject constructor(
|
||||||
val songs = filterIsInstance<TagResult.Hit>().map { it.rawSong }
|
val songs = filterIsInstance<TagResult.Hit>().map { it.rawSong }
|
||||||
return files to songs
|
return files to songs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -15,13 +15,12 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* 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 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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue