music: make extractors injectable
Refactor the music module to make each individual extractor able to be injected directly.
This commit is contained in:
parent
ae0c68c273
commit
6e55801513
34 changed files with 564 additions and 469 deletions
|
@ -32,6 +32,7 @@ import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
|
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
||||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
|
@ -40,7 +41,6 @@ import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.list.Sort
|
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.util.*
|
||||||
|
|
|
@ -32,6 +32,7 @@ import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
|
||||||
import org.oxycblt.auxio.detail.recycler.DetailAdapter
|
import org.oxycblt.auxio.detail.recycler.DetailAdapter
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
||||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
|
@ -39,7 +40,6 @@ import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.list.Sort
|
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.util.collect
|
import org.oxycblt.auxio.util.collect
|
||||||
|
|
|
@ -32,12 +32,12 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.detail.recycler.SortHeader
|
import org.oxycblt.auxio.detail.recycler.SortHeader
|
||||||
import org.oxycblt.auxio.list.BasicHeader
|
import org.oxycblt.auxio.list.BasicHeader
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.music.*
|
|
||||||
import org.oxycblt.auxio.music.library.Library
|
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.music.metadata.AudioInfo
|
import org.oxycblt.auxio.music.metadata.AudioInfo
|
||||||
import org.oxycblt.auxio.music.metadata.Disc
|
import org.oxycblt.auxio.music.metadata.Disc
|
||||||
import org.oxycblt.auxio.music.metadata.ReleaseType
|
import org.oxycblt.auxio.music.metadata.ReleaseType
|
||||||
|
import org.oxycblt.auxio.music.model.Library
|
||||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.util.*
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ import org.oxycblt.auxio.detail.recycler.DetailAdapter
|
||||||
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
|
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
||||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
|
@ -40,7 +41,6 @@ import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.list.Sort
|
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.util.collect
|
import org.oxycblt.auxio.util.collect
|
||||||
|
|
|
@ -50,11 +50,11 @@ import org.oxycblt.auxio.home.list.ArtistListFragment
|
||||||
import org.oxycblt.auxio.home.list.GenreListFragment
|
import org.oxycblt.auxio.home.list.GenreListFragment
|
||||||
import org.oxycblt.auxio.home.list.SongListFragment
|
import org.oxycblt.auxio.home.list.SongListFragment
|
||||||
import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy
|
import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy
|
||||||
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.list.selection.SelectionFragment
|
import org.oxycblt.auxio.list.selection.SelectionFragment
|
||||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.music.library.Library
|
import org.oxycblt.auxio.music.model.Library
|
||||||
import org.oxycblt.auxio.list.Sort
|
|
||||||
import org.oxycblt.auxio.music.system.Indexer
|
import org.oxycblt.auxio.music.system.Indexer
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
|
|
|
@ -23,9 +23,9 @@ import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.oxycblt.auxio.home.tabs.Tab
|
import org.oxycblt.auxio.home.tabs.Tab
|
||||||
import org.oxycblt.auxio.music.*
|
|
||||||
import org.oxycblt.auxio.music.library.Library
|
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
|
import org.oxycblt.auxio.music.*
|
||||||
|
import org.oxycblt.auxio.music.model.Library
|
||||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
|
|
@ -31,13 +31,13 @@ import org.oxycblt.auxio.home.HomeViewModel
|
||||||
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
||||||
import org.oxycblt.auxio.list.*
|
import org.oxycblt.auxio.list.*
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
||||||
import org.oxycblt.auxio.list.adapter.ListDiffer
|
import org.oxycblt.auxio.list.adapter.ListDiffer
|
||||||
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.AlbumViewHolder
|
import org.oxycblt.auxio.list.recycler.AlbumViewHolder
|
||||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.list.Sort
|
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.playback.secsToMs
|
import org.oxycblt.auxio.playback.secsToMs
|
||||||
|
|
|
@ -29,6 +29,7 @@ import org.oxycblt.auxio.home.HomeViewModel
|
||||||
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
||||||
import org.oxycblt.auxio.list.*
|
import org.oxycblt.auxio.list.*
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
||||||
import org.oxycblt.auxio.list.adapter.ListDiffer
|
import org.oxycblt.auxio.list.adapter.ListDiffer
|
||||||
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
||||||
|
@ -38,7 +39,6 @@ import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.list.Sort
|
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
|
|
|
@ -29,6 +29,7 @@ import org.oxycblt.auxio.home.HomeViewModel
|
||||||
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
||||||
import org.oxycblt.auxio.list.*
|
import org.oxycblt.auxio.list.*
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
||||||
import org.oxycblt.auxio.list.adapter.ListDiffer
|
import org.oxycblt.auxio.list.adapter.ListDiffer
|
||||||
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
||||||
|
@ -38,7 +39,6 @@ import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.list.Sort
|
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
|
|
|
@ -31,6 +31,7 @@ import org.oxycblt.auxio.home.HomeViewModel
|
||||||
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
||||||
import org.oxycblt.auxio.list.*
|
import org.oxycblt.auxio.list.*
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
||||||
import org.oxycblt.auxio.list.adapter.ListDiffer
|
import org.oxycblt.auxio.list.adapter.ListDiffer
|
||||||
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
||||||
|
@ -40,7 +41,6 @@ import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.list.Sort
|
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.playback.secsToMs
|
import org.oxycblt.auxio.playback.secsToMs
|
||||||
|
|
|
@ -30,12 +30,12 @@ import coil.size.Size
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.source
|
import okio.source
|
||||||
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
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.list.Sort
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Keyer] implementation for [Music] data.
|
* A [Keyer] implementation for [Music] data.
|
||||||
|
|
|
@ -21,8 +21,8 @@ import androidx.annotation.IdRes
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.*
|
|
||||||
import org.oxycblt.auxio.list.Sort.Mode
|
import org.oxycblt.auxio.list.Sort.Mode
|
||||||
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.music.metadata.Date
|
import org.oxycblt.auxio.music.metadata.Date
|
||||||
import org.oxycblt.auxio.music.metadata.Disc
|
import org.oxycblt.auxio.music.metadata.Disc
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.music.library.Library
|
import org.oxycblt.auxio.music.model.Library
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [ViewModel] that manages the current selection.
|
* A [ViewModel] that manages the current selection.
|
||||||
|
|
|
@ -17,22 +17,18 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.content.Context
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import org.oxycblt.auxio.music.metadata.AudioInfo
|
|
||||||
import org.oxycblt.auxio.music.system.Indexer
|
import org.oxycblt.auxio.music.system.Indexer
|
||||||
|
import org.oxycblt.auxio.music.system.IndexerImpl
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
class MusicModule {
|
interface MusicModule {
|
||||||
@Singleton @Provides fun musicRepository() = MusicRepository.new()
|
@Singleton @Binds fun musicRepository(musicRepository: MusicRepositoryImpl): MusicRepository
|
||||||
@Singleton @Provides fun indexer() = Indexer.new()
|
@Singleton @Binds fun indexer(indexer: IndexerImpl): Indexer
|
||||||
@Provides fun settings(@ApplicationContext context: Context) = MusicSettings.from(context)
|
@Binds fun settings(musicSettingsImpl: MusicSettingsImpl): MusicSettings
|
||||||
@Provides
|
|
||||||
fun audioInfoProvider(@ApplicationContext context: Context) = AudioInfo.Provider.from(context)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import org.oxycblt.auxio.music.library.Library
|
import javax.inject.Inject
|
||||||
|
import org.oxycblt.auxio.music.model.Library
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A repository granting access to the music library.
|
* A repository granting access to the music library.
|
||||||
|
@ -60,17 +61,9 @@ interface MusicRepository {
|
||||||
*/
|
*/
|
||||||
fun onLibraryChanged(library: Library?)
|
fun onLibraryChanged(library: Library?)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Create a new instance.
|
|
||||||
* @return A newly-created implementation of [MusicRepository].
|
|
||||||
*/
|
|
||||||
fun new(): MusicRepository = RealMusicRepository()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RealMusicRepository : MusicRepository {
|
class MusicRepositoryImpl @Inject constructor() : MusicRepository {
|
||||||
private val listeners = mutableListOf<MusicRepository.Listener>()
|
private val listeners = mutableListOf<MusicRepository.Listener>()
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
|
|
|
@ -20,6 +20,8 @@ package org.oxycblt.auxio.music
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.storage.StorageManager
|
import android.os.storage.StorageManager
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.music.storage.Directory
|
import org.oxycblt.auxio.music.storage.Directory
|
||||||
|
@ -67,11 +69,11 @@ interface MusicSettings : Settings<MusicSettings.Listener> {
|
||||||
* Get a framework-backed implementation.
|
* Get a framework-backed implementation.
|
||||||
* @param context [Context] required.
|
* @param context [Context] required.
|
||||||
*/
|
*/
|
||||||
fun from(context: Context): MusicSettings = RealMusicSettings(context)
|
fun from(context: Context): MusicSettings = MusicSettingsImpl(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RealMusicSettings(context: Context) :
|
class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context) :
|
||||||
Settings.Real<MusicSettings.Listener>(context), MusicSettings {
|
Settings.Real<MusicSettings.Listener>(context), MusicSettings {
|
||||||
private val storageManager = context.getSystemServiceCompat(StorageManager::class)
|
private val storageManager = context.getSystemServiceCompat(StorageManager::class)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2022 Auxio Project
|
* Copyright (c) 2023 Auxio Project
|
||||||
*
|
*
|
||||||
* 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
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
* 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.extractor
|
package org.oxycblt.auxio.music.cache
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
|
@ -28,126 +28,23 @@ import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import org.oxycblt.auxio.music.Song
|
|
||||||
import org.oxycblt.auxio.music.library.RawSong
|
|
||||||
import org.oxycblt.auxio.music.metadata.Date
|
import org.oxycblt.auxio.music.metadata.Date
|
||||||
import org.oxycblt.auxio.music.metadata.correctWhitespace
|
import org.oxycblt.auxio.music.metadata.correctWhitespace
|
||||||
import org.oxycblt.auxio.music.metadata.splitEscaped
|
import org.oxycblt.auxio.music.metadata.splitEscaped
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.music.model.RawSong
|
||||||
|
|
||||||
/**
|
|
||||||
* A cache of music metadata obtained in prior music loading operations. Obtain an instance with
|
|
||||||
* [MetadataCacheRepository].
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
|
||||||
*/
|
|
||||||
interface MetadataCache {
|
|
||||||
/** Whether this cache has encountered a [RawSong] that did not have a cache entry. */
|
|
||||||
val invalidated: Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Populate a [RawSong] from a cache entry, if it exists.
|
|
||||||
* @param rawSong The [RawSong] to populate.
|
|
||||||
* @return true if a cache entry could be applied to [rawSong], false otherwise.
|
|
||||||
*/
|
|
||||||
fun populate(rawSong: RawSong): Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
private class RealMetadataCache(cachedSongs: List<CachedSong>) : MetadataCache {
|
|
||||||
private val cacheMap = buildMap {
|
|
||||||
for (cachedSong in cachedSongs) {
|
|
||||||
put(cachedSong.mediaStoreId, cachedSong)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var invalidated = false
|
|
||||||
override fun populate(rawSong: RawSong): Boolean {
|
|
||||||
|
|
||||||
// For a cached raw song to be used, it must exist within the cache and have matching
|
|
||||||
// addition and modification timestamps. Technically the addition timestamp doesn't
|
|
||||||
// exist, but to safeguard against possible OEM-specific timestamp incoherence, we
|
|
||||||
// check for it anyway.
|
|
||||||
val cachedSong = cacheMap[rawSong.mediaStoreId]
|
|
||||||
if (cachedSong != null &&
|
|
||||||
cachedSong.dateAdded == rawSong.dateAdded &&
|
|
||||||
cachedSong.dateModified == rawSong.dateModified) {
|
|
||||||
cachedSong.copyToRaw(rawSong)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// We could not populate this song. This means our cache is stale and should be
|
|
||||||
// re-written with newly-loaded music.
|
|
||||||
invalidated = true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A repository allowing access to cached metadata obtained in prior music loading operations.
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
|
||||||
*/
|
|
||||||
interface MetadataCacheRepository {
|
|
||||||
/**
|
|
||||||
* Read the current [MetadataCache], if it exists.
|
|
||||||
* @return The stored [MetadataCache], or null if it could not be obtained.
|
|
||||||
*/
|
|
||||||
suspend fun readCache(): MetadataCache?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write the list of newly-loaded [RawSong]s to the cache, replacing the prior data.
|
|
||||||
* @param rawSongs The [rawSongs] to write to the cache.
|
|
||||||
*/
|
|
||||||
suspend fun writeCache(rawSongs: List<RawSong>)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Create a framework-backed instance.
|
|
||||||
* @param context [Context] required.
|
|
||||||
* @return A new instance.
|
|
||||||
*/
|
|
||||||
fun from(context: Context): MetadataCacheRepository = RealMetadataCacheRepository(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class RealMetadataCacheRepository(private val context: Context) : MetadataCacheRepository {
|
|
||||||
private val cachedSongsDao: CachedSongsDao by lazy {
|
|
||||||
MetadataCacheDatabase.getInstance(context).cachedSongsDao()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun readCache() =
|
|
||||||
try {
|
|
||||||
// Faster to load the whole database into memory than do a query on each
|
|
||||||
// populate call.
|
|
||||||
RealMetadataCache(cachedSongsDao.readSongs())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logE("Unable to load cache database.")
|
|
||||||
logE(e.stackTraceToString())
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun writeCache(rawSongs: List<RawSong>) {
|
|
||||||
try {
|
|
||||||
// Still write out whatever data was extracted.
|
|
||||||
cachedSongsDao.nukeSongs()
|
|
||||||
cachedSongsDao.insertSongs(rawSongs.map(CachedSong::fromRaw))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logE("Unable to save cache database.")
|
|
||||||
logE(e.stackTraceToString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Database(entities = [CachedSong::class], version = 27, exportSchema = false)
|
@Database(entities = [CachedSong::class], version = 27, exportSchema = false)
|
||||||
private abstract class MetadataCacheDatabase : RoomDatabase() {
|
abstract class CacheDatabase : RoomDatabase() {
|
||||||
abstract fun cachedSongsDao(): CachedSongsDao
|
abstract fun cachedSongsDao(): CachedSongsDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Volatile private var INSTANCE: MetadataCacheDatabase? = null
|
@Volatile private var INSTANCE: CacheDatabase? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get/create the shared instance of this database.
|
* Get/create the shared instance of this database.
|
||||||
* @param context [Context] required.
|
* @param context [Context] required.
|
||||||
*/
|
*/
|
||||||
fun getInstance(context: Context): MetadataCacheDatabase {
|
fun getInstance(context: Context): CacheDatabase {
|
||||||
val instance = INSTANCE
|
val instance = INSTANCE
|
||||||
if (instance != null) {
|
if (instance != null) {
|
||||||
return instance
|
return instance
|
||||||
|
@ -157,8 +54,8 @@ private abstract class MetadataCacheDatabase : RoomDatabase() {
|
||||||
val newInstance =
|
val newInstance =
|
||||||
Room.databaseBuilder(
|
Room.databaseBuilder(
|
||||||
context.applicationContext,
|
context.applicationContext,
|
||||||
MetadataCacheDatabase::class.java,
|
CacheDatabase::class.java,
|
||||||
"auxio_metadata_cache.db")
|
"auxio_tag_cache.db")
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
.fallbackToDestructiveMigrationFrom(0)
|
.fallbackToDestructiveMigrationFrom(0)
|
||||||
.fallbackToDestructiveMigrationOnDowngrade()
|
.fallbackToDestructiveMigrationOnDowngrade()
|
||||||
|
@ -171,7 +68,7 @@ private abstract class MetadataCacheDatabase : RoomDatabase() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
private interface CachedSongsDao {
|
interface CachedSongsDao {
|
||||||
@Query("SELECT * FROM ${CachedSong.TABLE_NAME}") suspend fun readSongs(): List<CachedSong>
|
@Query("SELECT * FROM ${CachedSong.TABLE_NAME}") suspend fun readSongs(): List<CachedSong>
|
||||||
@Query("DELETE FROM ${CachedSong.TABLE_NAME}") suspend fun nukeSongs()
|
@Query("DELETE FROM ${CachedSong.TABLE_NAME}") suspend fun nukeSongs()
|
||||||
@Insert suspend fun insertSongs(songs: List<CachedSong>)
|
@Insert suspend fun insertSongs(songs: List<CachedSong>)
|
||||||
|
@ -179,7 +76,7 @@ private interface CachedSongsDao {
|
||||||
|
|
||||||
@Entity(tableName = CachedSong.TABLE_NAME)
|
@Entity(tableName = CachedSong.TABLE_NAME)
|
||||||
@TypeConverters(CachedSong.Converters::class)
|
@TypeConverters(CachedSong.Converters::class)
|
||||||
private data class CachedSong(
|
data class CachedSong(
|
||||||
/**
|
/**
|
||||||
* The ID of the [Song]'s audio file, obtained from MediaStore. Note that this ID is highly
|
* The ID of the [Song]'s audio file, obtained from MediaStore. Note that this ID is highly
|
||||||
* unstable and should only be used for accessing the audio file.
|
* unstable and should only be used for accessing the audio file.
|
|
@ -15,19 +15,19 @@
|
||||||
* 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.playback.persist
|
package org.oxycblt.auxio.music.cache
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import android.content.Context
|
||||||
import org.oxycblt.auxio.music.Music
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import org.oxycblt.auxio.music.extractor.CacheRepository
|
||||||
|
|
||||||
/**
|
@Module
|
||||||
* Defines conversions used in the persistence table.
|
@InstallIn(SingletonComponent::class)
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
class CacheModule {
|
||||||
*/
|
@Provides
|
||||||
object PersistenceConverters {
|
fun cacheRepository(@ApplicationContext context: Context) = CacheRepository.from(context)
|
||||||
/** @see [Music.UID.toString] */
|
|
||||||
@TypeConverter fun fromMusicUID(uid: Music.UID?) = uid?.toString()
|
|
||||||
|
|
||||||
/** @see [Music.UID.fromString] */
|
|
||||||
@TypeConverter fun toMusicUid(string: String?) = string?.let(Music.UID::fromString)
|
|
||||||
}
|
}
|
126
app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt
vendored
Normal file
126
app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt
vendored
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* 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.extractor
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.oxycblt.auxio.music.cache.CacheDatabase
|
||||||
|
import org.oxycblt.auxio.music.cache.CachedSong
|
||||||
|
import org.oxycblt.auxio.music.cache.CachedSongsDao
|
||||||
|
import org.oxycblt.auxio.music.model.RawSong
|
||||||
|
import org.oxycblt.auxio.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cache of music metadata obtained in prior music loading operations. Obtain an instance with
|
||||||
|
* [CacheRepository].
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
interface Cache {
|
||||||
|
/** Whether this cache has encountered a [RawSong] that did not have a cache entry. */
|
||||||
|
val invalidated: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populate a [RawSong] from a cache entry, if it exists.
|
||||||
|
* @param rawSong The [RawSong] to populate.
|
||||||
|
* @return true if a cache entry could be applied to [rawSong], false otherwise.
|
||||||
|
*/
|
||||||
|
fun populate(rawSong: RawSong): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RealCache(cachedSongs: List<CachedSong>) : Cache {
|
||||||
|
private val cacheMap = buildMap {
|
||||||
|
for (cachedSong in cachedSongs) {
|
||||||
|
put(cachedSong.mediaStoreId, cachedSong)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var invalidated = false
|
||||||
|
override fun populate(rawSong: RawSong): Boolean {
|
||||||
|
|
||||||
|
// For a cached raw song to be used, it must exist within the cache and have matching
|
||||||
|
// addition and modification timestamps. Technically the addition timestamp doesn't
|
||||||
|
// exist, but to safeguard against possible OEM-specific timestamp incoherence, we
|
||||||
|
// check for it anyway.
|
||||||
|
val cachedSong = cacheMap[rawSong.mediaStoreId]
|
||||||
|
if (cachedSong != null &&
|
||||||
|
cachedSong.dateAdded == rawSong.dateAdded &&
|
||||||
|
cachedSong.dateModified == rawSong.dateModified) {
|
||||||
|
cachedSong.copyToRaw(rawSong)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// We could not populate this song. This means our cache is stale and should be
|
||||||
|
// re-written with newly-loaded music.
|
||||||
|
invalidated = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A repository allowing access to cached metadata obtained in prior music loading operations.
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
interface CacheRepository {
|
||||||
|
/**
|
||||||
|
* Read the current [Cache], if it exists.
|
||||||
|
* @return The stored [Cache], or null if it could not be obtained.
|
||||||
|
*/
|
||||||
|
suspend fun readCache(): Cache?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the list of newly-loaded [RawSong]s to the cache, replacing the prior data.
|
||||||
|
* @param rawSongs The [rawSongs] to write to the cache.
|
||||||
|
*/
|
||||||
|
suspend fun writeCache(rawSongs: List<RawSong>)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Create a framework-backed instance.
|
||||||
|
* @param context [Context] required.
|
||||||
|
* @return A new instance.
|
||||||
|
*/
|
||||||
|
fun from(context: Context): CacheRepository = RealCacheRepository(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RealCacheRepository(private val context: Context) : CacheRepository {
|
||||||
|
private val cachedSongsDao: CachedSongsDao by lazy {
|
||||||
|
CacheDatabase.getInstance(context).cachedSongsDao()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun readCache() =
|
||||||
|
try {
|
||||||
|
// Faster to load the whole database into memory than do a query on each
|
||||||
|
// populate call.
|
||||||
|
RealCache(cachedSongsDao.readSongs())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logE("Unable to load cache database.")
|
||||||
|
logE(e.stackTraceToString())
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun writeCache(rawSongs: List<RawSong>) {
|
||||||
|
try {
|
||||||
|
// Still write out whatever data was extracted.
|
||||||
|
cachedSongsDao.nukeSongs()
|
||||||
|
cachedSongsDao.insertSongs(rawSongs.map(CachedSong::fromRaw))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logE("Unable to save cache database.")
|
||||||
|
logE(e.stackTraceToString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,8 @@ package org.oxycblt.auxio.music.metadata
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.MediaExtractor
|
import android.media.MediaExtractor
|
||||||
import android.media.MediaFormat
|
import android.media.MediaFormat
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.storage.MimeType
|
import org.oxycblt.auxio.music.storage.MimeType
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
@ -46,18 +48,15 @@ data class AudioInfo(
|
||||||
* @return The [AudioInfo] of the [Song], if possible to obtain.
|
* @return The [AudioInfo] of the [Song], if possible to obtain.
|
||||||
*/
|
*/
|
||||||
suspend fun extract(song: Song): AudioInfo
|
suspend fun extract(song: Song): AudioInfo
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Get a framework-backed implementation.
|
|
||||||
* @param context [Context] required.
|
|
||||||
*/
|
|
||||||
fun from(context: Context): Provider = RealAudioInfoProvider(context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RealAudioInfoProvider(private val context: Context) : AudioInfo.Provider {
|
/**
|
||||||
|
* A framework-backed implementation of [AudioInfo.Provider].
|
||||||
|
* @param context [Context] required to read audio files.
|
||||||
|
*/
|
||||||
|
class AudioInfoProviderImpl @Inject constructor(@ApplicationContext private val context: Context) :
|
||||||
|
AudioInfo.Provider {
|
||||||
// While we would use ExoPlayer to extract this information, it doesn't support
|
// While we would use ExoPlayer to extract this information, it doesn't support
|
||||||
// common data like bit rate in progressive data sources due to there being no
|
// common data like bit rate in progressive data sources due to there being no
|
||||||
// demand. Thus, we are stuck with the inferior OS-provided MediaExtractor.
|
// demand. Thus, we are stuck with the inferior OS-provided MediaExtractor.
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
interface MetadataModule {
|
||||||
|
@Binds fun tagExtractor(tagExtractor: TagExtractorImpl): TagExtractor
|
||||||
|
@Binds fun audioInfoProvider(audioInfoProvider: AudioInfoProviderImpl): AudioInfo.Provider
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2022 Auxio Project
|
* Copyright (c) 2023 Auxio Project
|
||||||
*
|
*
|
||||||
* 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
|
||||||
|
@ -15,19 +15,17 @@
|
||||||
* 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.extractor
|
package org.oxycblt.auxio.music.metadata
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.text.isDigitsOnly
|
import androidx.core.text.isDigitsOnly
|
||||||
import com.google.android.exoplayer2.MediaItem
|
import com.google.android.exoplayer2.MediaItem
|
||||||
import com.google.android.exoplayer2.MetadataRetriever
|
import com.google.android.exoplayer2.MetadataRetriever
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import org.oxycblt.auxio.music.library.RawSong
|
import org.oxycblt.auxio.music.model.RawSong
|
||||||
import org.oxycblt.auxio.music.metadata.Date
|
|
||||||
import org.oxycblt.auxio.music.metadata.TextTags
|
|
||||||
import org.oxycblt.auxio.music.metadata.parseId3v2PositionField
|
|
||||||
import org.oxycblt.auxio.music.metadata.parseVorbisPositionField
|
|
||||||
import org.oxycblt.auxio.music.storage.toAudioUri
|
import org.oxycblt.auxio.music.storage.toAudioUri
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
@ -35,17 +33,30 @@ import org.oxycblt.auxio.util.logW
|
||||||
/**
|
/**
|
||||||
* The extractor that leverages ExoPlayer's [MetadataRetriever] API to parse metadata. This is the
|
* The extractor that leverages ExoPlayer's [MetadataRetriever] API to parse metadata. This is the
|
||||||
* last step in the music extraction process and is mostly responsible for papering over the bad
|
* last step in the music extraction process and is mostly responsible for papering over the bad
|
||||||
* metadata that [RealMediaStoreExtractor] produces.
|
* metadata that other extractors produce.
|
||||||
*
|
*
|
||||||
* @param context [Context] required for reading audio files.
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class MetadataExtractor(private val context: Context) {
|
interface TagExtractor {
|
||||||
|
/**
|
||||||
|
* Extract the metadata of songs from [incompleteSongs] and send them to [completeSongs]. Will
|
||||||
|
* terminate as soon as [incompleteSongs] is closed.
|
||||||
|
* @param incompleteSongs A [Channel] of incomplete songs to process.
|
||||||
|
* @param completeSongs A [Channel] to send completed songs to.
|
||||||
|
*/
|
||||||
|
suspend fun consume(incompleteSongs: Channel<RawSong>, completeSongs: Channel<RawSong>)
|
||||||
|
}
|
||||||
|
|
||||||
|
class TagExtractorImpl @Inject constructor(@ApplicationContext private val context: Context) :
|
||||||
|
TagExtractor {
|
||||||
|
override suspend fun consume(
|
||||||
|
incompleteSongs: Channel<RawSong>,
|
||||||
|
completeSongs: Channel<RawSong>
|
||||||
|
) {
|
||||||
// We can parallelize MetadataRetriever Futures to work around it's speed issues,
|
// We can parallelize MetadataRetriever Futures to work around it's speed issues,
|
||||||
// producing similar throughput's to other kinds of manual metadata extraction.
|
// producing similar throughput's to other kinds of manual metadata extraction.
|
||||||
private val taskPool: Array<Task?> = arrayOfNulls(TASK_CAPACITY)
|
val taskPool: Array<Task?> = arrayOfNulls(TASK_CAPACITY)
|
||||||
|
|
||||||
suspend fun consume(incompleteSongs: Channel<RawSong>, completeSongs: Channel<RawSong>) {
|
|
||||||
spin@ while (true) {
|
spin@ while (true) {
|
||||||
// Spin until there is an open slot we can insert a task in.
|
// Spin until there is an open slot we can insert a task in.
|
||||||
for (i in taskPool.indices) {
|
for (i in taskPool.indices) {
|
||||||
|
@ -94,7 +105,7 @@ class MetadataExtractor(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps a [MetadataExtractor] future and processes it into a [RawSong] when completed.
|
* Wraps a [TagExtractor] future and processes it into a [RawSong] when completed.
|
||||||
* @param context [Context] required to open the audio file.
|
* @param context [Context] required to open the audio file.
|
||||||
* @param rawSong [RawSong] to process.
|
* @param rawSong [RawSong] to process.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
|
@ -15,7 +15,7 @@
|
||||||
* 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.library
|
package org.oxycblt.auxio.music.model
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
|
@ -15,7 +15,7 @@
|
||||||
* 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.library
|
package org.oxycblt.auxio.music.model
|
||||||
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
|
@ -15,7 +15,7 @@
|
||||||
* 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.library
|
package org.oxycblt.auxio.music.model
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
|
@ -15,13 +15,12 @@
|
||||||
* 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.extractor
|
package org.oxycblt.auxio.music.storage
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.storage.StorageManager
|
import android.os.storage.StorageManager
|
||||||
import android.os.storage.StorageVolume
|
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.database.getIntOrNull
|
import androidx.core.database.getIntOrNull
|
||||||
|
@ -30,91 +29,77 @@ import java.io.File
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
import org.oxycblt.auxio.music.library.RawSong
|
import org.oxycblt.auxio.music.extractor.Cache
|
||||||
import org.oxycblt.auxio.music.metadata.Date
|
import org.oxycblt.auxio.music.metadata.Date
|
||||||
import org.oxycblt.auxio.music.metadata.parseId3v2PositionField
|
import org.oxycblt.auxio.music.metadata.parseId3v2PositionField
|
||||||
import org.oxycblt.auxio.music.metadata.transformPositionField
|
import org.oxycblt.auxio.music.metadata.transformPositionField
|
||||||
import org.oxycblt.auxio.music.storage.Directory
|
import org.oxycblt.auxio.music.model.RawSong
|
||||||
import org.oxycblt.auxio.music.storage.contentResolverSafe
|
|
||||||
import org.oxycblt.auxio.music.storage.directoryCompat
|
|
||||||
import org.oxycblt.auxio.music.storage.mediaStoreVolumeNameCompat
|
|
||||||
import org.oxycblt.auxio.music.storage.safeQuery
|
|
||||||
import org.oxycblt.auxio.music.storage.storageVolumesCompat
|
|
||||||
import org.oxycblt.auxio.music.storage.useQuery
|
|
||||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The layer that loads music from the [MediaStore] database. This is an intermediate step in the
|
* The layer that loads music from the [MediaStore] database. This is an intermediate step in the
|
||||||
* music extraction process and primarily intended for redundancy for files not natively supported
|
* music extraction process and primarily intended for redundancy for files not natively supported
|
||||||
* by [MetadataExtractor]. Solely relying on this is not recommended, as it often produces bad
|
* by other extractors. Solely relying on this is not recommended, as it often produces bad
|
||||||
* metadata.
|
* metadata.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
interface MediaStoreExtractor {
|
interface MediaStoreExtractor {
|
||||||
/**
|
/**
|
||||||
* Query the media database, initializing this instance in the process.
|
* Query the media database.
|
||||||
* @return The new [Cursor] returned by the media databases.
|
* @return A new [Query] returned from the media database.
|
||||||
*/
|
*/
|
||||||
suspend fun query(): Cursor
|
suspend fun query(): Query
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consume the [Cursor] loaded after [query].
|
* Consume the [Cursor] loaded after [query].
|
||||||
* @param cache A [MetadataCache] used to avoid extracting metadata for cached songs, or null if
|
* @param query The [Query] to consume.
|
||||||
* no [MetadataCache] was available.
|
* @param cache A [Cache] used to avoid extracting metadata for cached songs, or null if no
|
||||||
* @param incompleteSongs A channel where songs that could not be retrieved from the
|
* [Cache] was available.
|
||||||
* [MetadataCache] should be sent to.
|
* @param incompleteSongs A channel where songs that could not be retrieved from the [Cache]
|
||||||
|
* should be sent to.
|
||||||
* @param completeSongs A channel where completed songs should be sent to.
|
* @param completeSongs A channel where completed songs should be sent to.
|
||||||
*/
|
*/
|
||||||
suspend fun consume(
|
suspend fun consume(
|
||||||
cache: MetadataCache?,
|
query: Query,
|
||||||
|
cache: Cache?,
|
||||||
incompleteSongs: Channel<RawSong>,
|
incompleteSongs: Channel<RawSong>,
|
||||||
completeSongs: Channel<RawSong>
|
completeSongs: Channel<RawSong>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** A black-box interface representing a query from the media database. */
|
||||||
|
interface Query {
|
||||||
|
val projectedTotal: Int
|
||||||
|
fun moveToNext(): Boolean
|
||||||
|
fun close()
|
||||||
|
fun populateFileInfo(rawSong: RawSong)
|
||||||
|
fun populateTags(rawSong: RawSong)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Create a framework-backed instance.
|
* Create a framework-backed instance.
|
||||||
* @param context [Context] required.
|
* @param context [Context] required.
|
||||||
|
* @param musicSettings [MusicSettings] required.
|
||||||
* @return A new [RealMediaStoreExtractor] that will work best on the device's API level.
|
* @return A new [RealMediaStoreExtractor] that will work best on the device's API level.
|
||||||
*/
|
*/
|
||||||
fun from(context: Context): MediaStoreExtractor =
|
fun from(context: Context, musicSettings: MusicSettings): MediaStoreExtractor =
|
||||||
when {
|
when {
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Api30MediaStoreExtractor(context)
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Api29MediaStoreExtractor(context)
|
Api30MediaStoreExtractor(context, musicSettings)
|
||||||
else -> Api21MediaStoreExtractor(context)
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ->
|
||||||
|
Api29MediaStoreExtractor(context, musicSettings)
|
||||||
|
else -> Api21MediaStoreExtractor(context, musicSettings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private abstract class RealMediaStoreExtractor(private val context: Context) : MediaStoreExtractor {
|
private abstract class RealMediaStoreExtractor(
|
||||||
private var cursor: Cursor? = null
|
protected val context: Context,
|
||||||
private var idIndex = -1
|
private val musicSettings: MusicSettings
|
||||||
private var titleIndex = -1
|
) : MediaStoreExtractor {
|
||||||
private var displayNameIndex = -1
|
final override suspend fun query(): MediaStoreExtractor.Query {
|
||||||
private var mimeTypeIndex = -1
|
|
||||||
private var sizeIndex = -1
|
|
||||||
private var dateAddedIndex = -1
|
|
||||||
private var dateModifiedIndex = -1
|
|
||||||
private var durationIndex = -1
|
|
||||||
private var yearIndex = -1
|
|
||||||
private var albumIndex = -1
|
|
||||||
private var albumIdIndex = -1
|
|
||||||
private var artistIndex = -1
|
|
||||||
private var albumArtistIndex = -1
|
|
||||||
private val genreNamesMap = mutableMapOf<Long, String>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The [StorageVolume]s currently scanned by [MediaStore]. This should be used to transform path
|
|
||||||
* information from the database into volume-aware paths.
|
|
||||||
*/
|
|
||||||
protected var volumes = listOf<StorageVolume>()
|
|
||||||
private set
|
|
||||||
|
|
||||||
override suspend fun query(): Cursor {
|
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
val musicSettings = MusicSettings.from(context)
|
|
||||||
val storageManager = context.getSystemServiceCompat(StorageManager::class)
|
|
||||||
|
|
||||||
val args = mutableListOf<String>()
|
val args = mutableListOf<String>()
|
||||||
var selector = BASE_SELECTOR
|
var selector = BASE_SELECTOR
|
||||||
|
@ -156,30 +141,14 @@ private abstract class RealMediaStoreExtractor(private val context: Context) : M
|
||||||
// Now we can actually query MediaStore.
|
// Now we can actually query MediaStore.
|
||||||
logD("Starting song query [proj: ${projection.toList()}, selector: $selector, args: $args]")
|
logD("Starting song query [proj: ${projection.toList()}, selector: $selector, args: $args]")
|
||||||
val cursor =
|
val cursor =
|
||||||
context.contentResolverSafe
|
context.contentResolverSafe.safeQuery(
|
||||||
.safeQuery(
|
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||||
projection,
|
projection,
|
||||||
selector,
|
selector,
|
||||||
args.toTypedArray())
|
args.toTypedArray())
|
||||||
.also { cursor = it }
|
|
||||||
logD("Song query succeeded [Projected total: ${cursor.count}]")
|
logD("Song query succeeded [Projected total: ${cursor.count}]")
|
||||||
|
|
||||||
// Set up cursor indices for later use.
|
val genreNamesMap = mutableMapOf<Long, String>()
|
||||||
idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
|
|
||||||
titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE)
|
|
||||||
displayNameIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME)
|
|
||||||
mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.MIME_TYPE)
|
|
||||||
sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.SIZE)
|
|
||||||
dateAddedIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATE_ADDED)
|
|
||||||
dateModifiedIndex =
|
|
||||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATE_MODIFIED)
|
|
||||||
durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION)
|
|
||||||
yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR)
|
|
||||||
albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM)
|
|
||||||
albumIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM_ID)
|
|
||||||
artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST)
|
|
||||||
albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST)
|
|
||||||
|
|
||||||
// Since we can't obtain the genre tag from a song query, we must construct our own
|
// Since we can't obtain the genre tag from a song query, we must construct our own
|
||||||
// equivalent from genre database queries. Theoretically, this isn't needed since
|
// equivalent from genre database queries. Theoretically, this isn't needed since
|
||||||
|
@ -211,34 +180,31 @@ private abstract class RealMediaStoreExtractor(private val context: Context) : M
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
volumes = storageManager.storageVolumesCompat
|
|
||||||
logD("Finished initialization in ${System.currentTimeMillis() - start}ms")
|
logD("Finished initialization in ${System.currentTimeMillis() - start}ms")
|
||||||
|
return wrapQuery(cursor, genreNamesMap)
|
||||||
return cursor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun consume(
|
final override suspend fun consume(
|
||||||
cache: MetadataCache?,
|
query: MediaStoreExtractor.Query,
|
||||||
|
cache: Cache?,
|
||||||
incompleteSongs: Channel<RawSong>,
|
incompleteSongs: Channel<RawSong>,
|
||||||
completeSongs: Channel<RawSong>
|
completeSongs: Channel<RawSong>
|
||||||
) {
|
) {
|
||||||
val cursor = requireNotNull(cursor) { "Must call query first before running consume" }
|
while (query.moveToNext()) {
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
val rawSong = RawSong()
|
val rawSong = RawSong()
|
||||||
populateFileData(cursor, rawSong)
|
query.populateFileInfo(rawSong)
|
||||||
if (cache?.populate(rawSong) == true) {
|
if (cache?.populate(rawSong) == true) {
|
||||||
completeSongs.send(rawSong)
|
completeSongs.send(rawSong)
|
||||||
} else {
|
} else {
|
||||||
populateMetadata(cursor, rawSong)
|
query.populateFileInfo(rawSong)
|
||||||
incompleteSongs.send(rawSong)
|
incompleteSongs.send(rawSong)
|
||||||
}
|
}
|
||||||
yield()
|
yield()
|
||||||
}
|
}
|
||||||
// Free the cursor and signal that no more incomplete songs will be produced by
|
// Free the cursor and signal that no more incomplete songs will be produced by
|
||||||
// this extractor.
|
// this extractor.
|
||||||
cursor.close()
|
query.close()
|
||||||
incompleteSongs.close()
|
incompleteSongs.close()
|
||||||
this.cursor = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -280,19 +246,43 @@ private abstract class RealMediaStoreExtractor(private val context: Context) : M
|
||||||
*/
|
*/
|
||||||
protected abstract fun addDirToSelector(dir: Directory, args: MutableList<String>): Boolean
|
protected abstract fun addDirToSelector(dir: Directory, args: MutableList<String>): Boolean
|
||||||
|
|
||||||
/**
|
protected abstract fun wrapQuery(
|
||||||
* Populate a [RawSong] with the "File Data" of the given [MediaStore] [Cursor], which is the
|
cursor: Cursor,
|
||||||
* data that cannot be cached. This includes any information not intrinsic to the file and
|
genreNamesMap: Map<Long, String>
|
||||||
* instead dependent on the file-system, which could change without invalidating the cache due
|
): MediaStoreExtractor.Query
|
||||||
* to volume additions or removals.
|
|
||||||
* @param cursor The [Cursor] to read from.
|
abstract class Query(
|
||||||
* @param rawSong The [RawSong] to populate.
|
protected val cursor: Cursor,
|
||||||
* @see populateMetadata
|
private val genreNamesMap: Map<Long, String>
|
||||||
*/
|
) : MediaStoreExtractor.Query {
|
||||||
protected open fun populateFileData(cursor: Cursor, rawSong: RawSong) {
|
private val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
|
||||||
|
private val titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE)
|
||||||
|
private val displayNameIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME)
|
||||||
|
private val mimeTypeIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.MIME_TYPE)
|
||||||
|
private val sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.SIZE)
|
||||||
|
private val dateAddedIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATE_ADDED)
|
||||||
|
private val dateModifiedIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATE_MODIFIED)
|
||||||
|
private val durationIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION)
|
||||||
|
private val yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR)
|
||||||
|
private val albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM)
|
||||||
|
private val albumIdIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM_ID)
|
||||||
|
private val artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST)
|
||||||
|
private val albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST)
|
||||||
|
|
||||||
|
final override val projectedTotal = cursor.count
|
||||||
|
final override fun moveToNext() = cursor.moveToNext()
|
||||||
|
final override fun close() = cursor.close()
|
||||||
|
|
||||||
|
override fun populateFileInfo(rawSong: RawSong) {
|
||||||
rawSong.mediaStoreId = cursor.getLong(idIndex)
|
rawSong.mediaStoreId = cursor.getLong(idIndex)
|
||||||
rawSong.dateAdded = cursor.getLong(dateAddedIndex)
|
rawSong.dateAdded = cursor.getLong(dateAddedIndex)
|
||||||
rawSong.dateModified = cursor.getLong(dateAddedIndex)
|
rawSong.dateModified = cursor.getLong(dateModifiedIndex)
|
||||||
// Try to use the DISPLAY_NAME column to obtain a (probably sane) file name
|
// Try to use the DISPLAY_NAME column to obtain a (probably sane) file name
|
||||||
// from the android system.
|
// from the android system.
|
||||||
rawSong.fileName = cursor.getStringOrNull(displayNameIndex)
|
rawSong.fileName = cursor.getStringOrNull(displayNameIndex)
|
||||||
|
@ -300,15 +290,7 @@ private abstract class RealMediaStoreExtractor(private val context: Context) : M
|
||||||
rawSong.albumMediaStoreId = cursor.getLong(albumIdIndex)
|
rawSong.albumMediaStoreId = cursor.getLong(albumIdIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun populateTags(rawSong: RawSong) {
|
||||||
* Populate a [RawSong] with the Metadata of the given [MediaStore] [Cursor], which is the data
|
|
||||||
* about a [RawSong] that can be cached. This includes any information intrinsic to the file or
|
|
||||||
* it's file format, such as music tags.
|
|
||||||
* @param cursor The [Cursor] to read from.
|
|
||||||
* @param rawSong The [RawSong] to populate.
|
|
||||||
* @see populateFileData
|
|
||||||
*/
|
|
||||||
protected open fun populateMetadata(cursor: Cursor, rawSong: RawSong) {
|
|
||||||
// Song title
|
// Song title
|
||||||
rawSong.name = cursor.getString(titleIndex)
|
rawSong.name = cursor.getString(titleIndex)
|
||||||
// Size (in bytes)
|
// Size (in bytes)
|
||||||
|
@ -320,7 +302,8 @@ private abstract class RealMediaStoreExtractor(private val context: Context) : M
|
||||||
// This is one of the major weaknesses of using MediaStore, hence the redundancy layers.
|
// This is one of the major weaknesses of using MediaStore, hence the redundancy layers.
|
||||||
rawSong.date = cursor.getStringOrNull(yearIndex)?.let(Date::from)
|
rawSong.date = cursor.getStringOrNull(yearIndex)?.let(Date::from)
|
||||||
// A non-existent album name should theoretically be the name of the folder it contained
|
// A non-existent album name should theoretically be the name of the folder it contained
|
||||||
// in, but in practice it is more often "0" (as in /storage/emulated/0), even when it the
|
// in, but in practice it is more often "0" (as in /storage/emulated/0), even when it
|
||||||
|
// the
|
||||||
// file is not actually in the root internal storage directory. We can't do anything to
|
// file is not actually in the root internal storage directory. We can't do anything to
|
||||||
// fix this, really.
|
// fix this, really.
|
||||||
rawSong.albumName = cursor.getString(albumIndex)
|
rawSong.albumName = cursor.getString(albumIndex)
|
||||||
|
@ -337,6 +320,7 @@ private abstract class RealMediaStoreExtractor(private val context: Context) : M
|
||||||
// Get the genre value we had to query for in initialization
|
// Get the genre value we had to query for in initialization
|
||||||
genreNamesMap[rawSong.mediaStoreId]?.let { rawSong.genreNames = listOf(it) }
|
genreNamesMap[rawSong.mediaStoreId]?.let { rawSong.genreNames = listOf(it) }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
|
@ -364,18 +348,11 @@ private abstract class RealMediaStoreExtractor(private val context: Context) : M
|
||||||
// Note: The separation between version-specific backends may not be the cleanest. To preserve
|
// Note: The separation between version-specific backends may not be the cleanest. To preserve
|
||||||
// speed, we only want to add redundancy on known issues, not with possible issues.
|
// speed, we only want to add redundancy on known issues, not with possible issues.
|
||||||
|
|
||||||
private class Api21MediaStoreExtractor(context: Context) : RealMediaStoreExtractor(context) {
|
// Note: The separation between version-specific backends may not be the cleanest. To preserve
|
||||||
private var trackIndex = -1
|
// speed, we only want to add redundancy on known issues, not with possible issues.
|
||||||
private var dataIndex = -1
|
|
||||||
|
|
||||||
override suspend fun query(): Cursor {
|
|
||||||
val cursor = super.query()
|
|
||||||
// Set up cursor indices for later use.
|
|
||||||
trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
|
||||||
dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA)
|
|
||||||
return cursor
|
|
||||||
}
|
|
||||||
|
|
||||||
|
private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSettings) :
|
||||||
|
RealMediaStoreExtractor(context, musicSettings) {
|
||||||
override val projection: Array<String>
|
override val projection: Array<String>
|
||||||
get() =
|
get() =
|
||||||
super.projection +
|
super.projection +
|
||||||
|
@ -398,11 +375,26 @@ private class Api21MediaStoreExtractor(context: Context) : RealMediaStoreExtract
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun populateFileData(cursor: Cursor, rawSong: RawSong) {
|
override fun wrapQuery(
|
||||||
super.populateFileData(cursor, rawSong)
|
cursor: Cursor,
|
||||||
|
genreNamesMap: Map<Long, String>,
|
||||||
|
): MediaStoreExtractor.Query =
|
||||||
|
Query(cursor, genreNamesMap, context.getSystemServiceCompat(StorageManager::class))
|
||||||
|
|
||||||
|
private class Query(
|
||||||
|
cursor: Cursor,
|
||||||
|
genreNamesMap: Map<Long, String>,
|
||||||
|
storageManager: StorageManager
|
||||||
|
) : RealMediaStoreExtractor.Query(cursor, genreNamesMap) {
|
||||||
|
// Set up cursor indices for later use.
|
||||||
|
private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
||||||
|
private val dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA)
|
||||||
|
private val volumes = storageManager.storageVolumesCompat
|
||||||
|
|
||||||
|
override fun populateFileInfo(rawSong: RawSong) {
|
||||||
|
super.populateFileInfo(rawSong)
|
||||||
|
|
||||||
val data = cursor.getString(dataIndex)
|
val data = cursor.getString(dataIndex)
|
||||||
|
|
||||||
// On some OEM devices below API 29, DISPLAY_NAME may not be present. I assume
|
// On some OEM devices below API 29, DISPLAY_NAME may not be present. I assume
|
||||||
// that this only applies to below API 29, as beyond API 29, this column not being
|
// that this only applies to below API 29, as beyond API 29, this column not being
|
||||||
// present would completely break the scoped storage system. Fill it in with DATA
|
// present would completely break the scoped storage system. Fill it in with DATA
|
||||||
|
@ -424,8 +416,8 @@ private class Api21MediaStoreExtractor(context: Context) : RealMediaStoreExtract
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun populateMetadata(cursor: Cursor, rawSong: RawSong) {
|
override fun populateTags(rawSong: RawSong) {
|
||||||
super.populateMetadata(cursor, rawSong)
|
super.populateTags(rawSong)
|
||||||
// See unpackTrackNo/unpackDiscNo for an explanation
|
// See unpackTrackNo/unpackDiscNo for an explanation
|
||||||
// of how this column is set up.
|
// of how this column is set up.
|
||||||
val rawTrack = cursor.getIntOrNull(trackIndex)
|
val rawTrack = cursor.getIntOrNull(trackIndex)
|
||||||
|
@ -434,6 +426,7 @@ private class Api21MediaStoreExtractor(context: Context) : RealMediaStoreExtract
|
||||||
rawTrack.unpackDiscNo()?.let { rawSong.disc = it }
|
rawTrack.unpackDiscNo()?.let { rawSong.disc = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -442,20 +435,10 @@ private class Api21MediaStoreExtractor(context: Context) : RealMediaStoreExtract
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
private open class BaseApi29MediaStoreExtractor(context: Context) :
|
private abstract class BaseApi29MediaStoreExtractor(
|
||||||
RealMediaStoreExtractor(context) {
|
context: Context,
|
||||||
private var volumeIndex = -1
|
musicSettings: MusicSettings
|
||||||
private var relativePathIndex = -1
|
) : RealMediaStoreExtractor(context, musicSettings) {
|
||||||
|
|
||||||
override suspend fun query(): Cursor {
|
|
||||||
val cursor = super.query()
|
|
||||||
// Set up cursor indices for later use.
|
|
||||||
volumeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.VOLUME_NAME)
|
|
||||||
relativePathIndex =
|
|
||||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.RELATIVE_PATH)
|
|
||||||
return cursor
|
|
||||||
}
|
|
||||||
|
|
||||||
override val projection: Array<String>
|
override val projection: Array<String>
|
||||||
get() =
|
get() =
|
||||||
super.projection +
|
super.projection +
|
||||||
|
@ -484,8 +467,19 @@ private open class BaseApi29MediaStoreExtractor(context: Context) :
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun populateFileData(cursor: Cursor, rawSong: RawSong) {
|
abstract class Query(
|
||||||
super.populateFileData(cursor, rawSong)
|
cursor: Cursor,
|
||||||
|
genreNamesMap: Map<Long, String>,
|
||||||
|
storageManager: StorageManager
|
||||||
|
) : RealMediaStoreExtractor.Query(cursor, genreNamesMap) {
|
||||||
|
private val volumeIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.VOLUME_NAME)
|
||||||
|
private val relativePathIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.RELATIVE_PATH)
|
||||||
|
private val volumes = storageManager.storageVolumesCompat
|
||||||
|
|
||||||
|
final override fun populateFileInfo(rawSong: RawSong) {
|
||||||
|
super.populateFileInfo(rawSong)
|
||||||
// Find the StorageVolume whose MediaStore name corresponds to this song.
|
// Find the StorageVolume whose MediaStore name corresponds to this song.
|
||||||
// This is combined with the plain relative path column to create the directory.
|
// This is combined with the plain relative path column to create the directory.
|
||||||
val volumeName = cursor.getString(volumeIndex)
|
val volumeName = cursor.getString(volumeIndex)
|
||||||
|
@ -495,6 +489,7 @@ private open class BaseApi29MediaStoreExtractor(context: Context) :
|
||||||
rawSong.directory = Directory.from(volume, relativePath)
|
rawSong.directory = Directory.from(volume, relativePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -505,22 +500,26 @@ private open class BaseApi29MediaStoreExtractor(context: Context) :
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
private open class Api29MediaStoreExtractor(context: Context) :
|
private class Api29MediaStoreExtractor(context: Context, musicSettings: MusicSettings) :
|
||||||
BaseApi29MediaStoreExtractor(context) {
|
BaseApi29MediaStoreExtractor(context, musicSettings) {
|
||||||
private var trackIndex = -1
|
|
||||||
|
|
||||||
override suspend fun query(): Cursor {
|
|
||||||
val cursor = super.query()
|
|
||||||
// Set up cursor indices for later use.
|
|
||||||
trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
|
||||||
return cursor
|
|
||||||
}
|
|
||||||
|
|
||||||
override val projection: Array<String>
|
override val projection: Array<String>
|
||||||
get() = super.projection + arrayOf(MediaStore.Audio.AudioColumns.TRACK)
|
get() = super.projection + arrayOf(MediaStore.Audio.AudioColumns.TRACK)
|
||||||
|
|
||||||
override fun populateMetadata(cursor: Cursor, rawSong: RawSong) {
|
override fun wrapQuery(
|
||||||
super.populateMetadata(cursor, rawSong)
|
cursor: Cursor,
|
||||||
|
genreNamesMap: Map<Long, String>
|
||||||
|
): MediaStoreExtractor.Query =
|
||||||
|
Query(cursor, genreNamesMap, context.getSystemServiceCompat(StorageManager::class))
|
||||||
|
|
||||||
|
private class Query(
|
||||||
|
cursor: Cursor,
|
||||||
|
genreNamesMap: Map<Long, String>,
|
||||||
|
storageManager: StorageManager
|
||||||
|
) : BaseApi29MediaStoreExtractor.Query(cursor, genreNamesMap, storageManager) {
|
||||||
|
private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
||||||
|
override fun populateTags(rawSong: RawSong) {
|
||||||
|
super.populateTags(rawSong)
|
||||||
// This extractor is volume-aware, but does not support the modern track columns.
|
// This extractor is volume-aware, but does not support the modern track columns.
|
||||||
// Use the old column instead. See unpackTrackNo/unpackDiscNo for an explanation
|
// Use the old column instead. See unpackTrackNo/unpackDiscNo for an explanation
|
||||||
// of how this column is set up.
|
// of how this column is set up.
|
||||||
|
@ -530,6 +529,7 @@ private open class Api29MediaStoreExtractor(context: Context) :
|
||||||
rawTrack.unpackDiscNo()?.let { rawSong.disc = it }
|
rawTrack.unpackDiscNo()?.let { rawSong.disc = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -539,18 +539,8 @@ private open class Api29MediaStoreExtractor(context: Context) :
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
private class Api30MediaStoreExtractor(context: Context) : BaseApi29MediaStoreExtractor(context) {
|
private class Api30MediaStoreExtractor(context: Context, musicSettings: MusicSettings) :
|
||||||
private var trackIndex: Int = -1
|
BaseApi29MediaStoreExtractor(context, musicSettings) {
|
||||||
private var discIndex: Int = -1
|
|
||||||
|
|
||||||
override suspend fun query(): Cursor {
|
|
||||||
val cursor = super.query()
|
|
||||||
// Set up cursor indices for later use.
|
|
||||||
trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER)
|
|
||||||
discIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISC_NUMBER)
|
|
||||||
return cursor
|
|
||||||
}
|
|
||||||
|
|
||||||
override val projection: Array<String>
|
override val projection: Array<String>
|
||||||
get() =
|
get() =
|
||||||
super.projection +
|
super.projection +
|
||||||
|
@ -560,15 +550,34 @@ private class Api30MediaStoreExtractor(context: Context) : BaseApi29MediaStoreEx
|
||||||
MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER,
|
MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER,
|
||||||
MediaStore.Audio.AudioColumns.DISC_NUMBER)
|
MediaStore.Audio.AudioColumns.DISC_NUMBER)
|
||||||
|
|
||||||
override fun populateMetadata(cursor: Cursor, rawSong: RawSong) {
|
override fun wrapQuery(
|
||||||
super.populateMetadata(cursor, rawSong)
|
cursor: Cursor,
|
||||||
|
genreNamesMap: Map<Long, String>
|
||||||
|
): MediaStoreExtractor.Query =
|
||||||
|
Query(cursor, genreNamesMap, context.getSystemServiceCompat(StorageManager::class))
|
||||||
|
|
||||||
|
private class Query(
|
||||||
|
cursor: Cursor,
|
||||||
|
genreNamesMap: Map<Long, String>,
|
||||||
|
storageManager: StorageManager
|
||||||
|
) : BaseApi29MediaStoreExtractor.Query(cursor, genreNamesMap, storageManager) {
|
||||||
|
private val trackIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER)
|
||||||
|
private val discIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISC_NUMBER)
|
||||||
|
|
||||||
|
override fun populateTags(rawSong: RawSong) {
|
||||||
|
super.populateTags(rawSong)
|
||||||
// Both CD_TRACK_NUMBER and DISC_NUMBER tend to be formatted as they are in
|
// Both CD_TRACK_NUMBER and DISC_NUMBER tend to be formatted as they are in
|
||||||
// the tag itself, which is to say that it is formatted as NN/TT tracks, where
|
// the tag itself, which is to say that it is formatted as NN/TT tracks, where
|
||||||
// N is the number and T is the total. Parse the number while ignoring the
|
// N is the number and T is the total. Parse the number while ignoring the
|
||||||
// total, as we have no use for it.
|
// total, as we have no use for it.
|
||||||
cursor.getStringOrNull(trackIndex)?.parseId3v2PositionField()?.let { rawSong.track = it }
|
cursor.getStringOrNull(trackIndex)?.parseId3v2PositionField()?.let {
|
||||||
|
rawSong.track = it
|
||||||
|
}
|
||||||
cursor.getStringOrNull(discIndex)?.parseId3v2PositionField()?.let { rawSong.disc = it }
|
cursor.getStringOrNull(discIndex)?.parseId3v2PositionField()?.let { rawSong.disc = it }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
*
|
||||||
|
* 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.storage
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
class StorageModule {
|
||||||
|
@Provides
|
||||||
|
fun mediaStoreExtractor(@ApplicationContext context: Context, musicSettings: MusicSettings) =
|
||||||
|
MediaStoreExtractor.from(context, musicSettings)
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
|
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
|
||||||
|
@ -35,8 +36,10 @@ import kotlinx.coroutines.yield
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.music.extractor.*
|
import org.oxycblt.auxio.music.extractor.*
|
||||||
import org.oxycblt.auxio.music.library.Library
|
import org.oxycblt.auxio.music.metadata.TagExtractor
|
||||||
import org.oxycblt.auxio.music.library.RawSong
|
import org.oxycblt.auxio.music.model.Library
|
||||||
|
import org.oxycblt.auxio.music.model.RawSong
|
||||||
|
import org.oxycblt.auxio.music.storage.MediaStoreExtractor
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
@ -211,16 +214,17 @@ interface Indexer {
|
||||||
} else {
|
} else {
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new instance.
|
|
||||||
* @return A newly-created implementation of [Indexer].
|
|
||||||
*/
|
|
||||||
fun new(): Indexer = RealIndexer()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RealIndexer : Indexer {
|
class IndexerImpl
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val musicSettings: MusicSettings,
|
||||||
|
private val cacheRepository: CacheRepository,
|
||||||
|
private val mediaStoreExtractor: MediaStoreExtractor,
|
||||||
|
private val tagExtractor: TagExtractor
|
||||||
|
) : Indexer {
|
||||||
@Volatile private var lastResponse: Result<Library>? = null
|
@Volatile private var lastResponse: Result<Library>? = null
|
||||||
@Volatile private var indexingState: Indexer.Indexing? = null
|
@Volatile private var indexingState: Indexer.Indexing? = null
|
||||||
@Volatile private var controller: Indexer.Controller? = null
|
@Volatile private var controller: Indexer.Controller? = null
|
||||||
|
@ -332,19 +336,15 @@ private class RealIndexer : Indexer {
|
||||||
// how long a media database query will take.
|
// how long a media database query will take.
|
||||||
emitIndexing(Indexer.Indexing.Indeterminate)
|
emitIndexing(Indexer.Indexing.Indeterminate)
|
||||||
|
|
||||||
val metadataCacheRepository = MetadataCacheRepository.from(context)
|
|
||||||
val mediaStoreExtractor = MediaStoreExtractor.from(context)
|
|
||||||
val metadataExtractor = MetadataExtractor(context)
|
|
||||||
|
|
||||||
// Do the initial query of the cache and media databases in parallel.
|
// Do the initial query of the cache and media databases in parallel.
|
||||||
val mediaStoreQueryJob = scope.async { mediaStoreExtractor.query() }
|
val mediaStoreQueryJob = scope.async { mediaStoreExtractor.query() }
|
||||||
val cache =
|
val cache =
|
||||||
if (withCache) {
|
if (withCache) {
|
||||||
metadataCacheRepository.readCache()
|
cacheRepository.readCache()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
val total = mediaStoreQueryJob.await().count
|
val query = mediaStoreQueryJob.await()
|
||||||
|
|
||||||
// Now start processing the queried song information in parallel. Songs that can't be
|
// Now start processing the queried song information in parallel. Songs that can't be
|
||||||
// received from the cache are consisted incomplete and pushed to a separate channel
|
// received from the cache are consisted incomplete and pushed to a separate channel
|
||||||
|
@ -352,14 +352,16 @@ private class RealIndexer : Indexer {
|
||||||
val completeSongs = Channel<RawSong>(Channel.UNLIMITED)
|
val completeSongs = Channel<RawSong>(Channel.UNLIMITED)
|
||||||
val incompleteSongs = Channel<RawSong>(Channel.UNLIMITED)
|
val incompleteSongs = Channel<RawSong>(Channel.UNLIMITED)
|
||||||
val mediaStoreJob =
|
val mediaStoreJob =
|
||||||
scope.async { mediaStoreExtractor.consume(cache, incompleteSongs, completeSongs) }
|
scope.async {
|
||||||
val metadataJob = scope.async { metadataExtractor.consume(incompleteSongs, completeSongs) }
|
mediaStoreExtractor.consume(query, cache, incompleteSongs, completeSongs)
|
||||||
|
}
|
||||||
|
val metadataJob = scope.async { tagExtractor.consume(incompleteSongs, completeSongs) }
|
||||||
|
|
||||||
// Await completed raw songs as they are processed.
|
// Await completed raw songs as they are processed.
|
||||||
val rawSongs = LinkedList<RawSong>()
|
val rawSongs = LinkedList<RawSong>()
|
||||||
for (rawSong in completeSongs) {
|
for (rawSong in completeSongs) {
|
||||||
rawSongs.add(rawSong)
|
rawSongs.add(rawSong)
|
||||||
emitIndexing(Indexer.Indexing.Songs(rawSongs.size, total))
|
emitIndexing(Indexer.Indexing.Songs(rawSongs.size, query.projectedTotal))
|
||||||
}
|
}
|
||||||
mediaStoreJob.await()
|
mediaStoreJob.await()
|
||||||
metadataJob.await()
|
metadataJob.await()
|
||||||
|
@ -367,10 +369,9 @@ private class RealIndexer : Indexer {
|
||||||
// Successfully loaded the library, now save the cache and create the library in
|
// Successfully loaded the library, now save the cache and create the library in
|
||||||
// parallel.
|
// parallel.
|
||||||
emitIndexing(Indexer.Indexing.Indeterminate)
|
emitIndexing(Indexer.Indexing.Indeterminate)
|
||||||
val libraryJob =
|
val libraryJob = scope.async(Dispatchers.Main) { Library.from(rawSongs, musicSettings) }
|
||||||
scope.async(Dispatchers.Main) { Library.from(rawSongs, MusicSettings.from(context)) }
|
|
||||||
if (cache == null || cache.invalidated) {
|
if (cache == null || cache.invalidated) {
|
||||||
metadataCacheRepository.writeCache(rawSongs)
|
cacheRepository.writeCache(rawSongs)
|
||||||
}
|
}
|
||||||
return libraryJob.await()
|
return libraryJob.await()
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.music.library.Library
|
import org.oxycblt.auxio.music.model.Library
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -27,6 +27,7 @@ import androidx.room.PrimaryKey
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverter
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
|
@ -39,7 +40,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
entities = [PlaybackState::class, QueueHeapItem::class, QueueMappingItem::class],
|
entities = [PlaybackState::class, QueueHeapItem::class, QueueMappingItem::class],
|
||||||
version = 27,
|
version = 27,
|
||||||
exportSchema = false)
|
exportSchema = false)
|
||||||
@TypeConverters(PersistenceConverters::class)
|
@TypeConverters(PersistenceDatabase.Converters::class)
|
||||||
abstract class PersistenceDatabase : RoomDatabase() {
|
abstract class PersistenceDatabase : RoomDatabase() {
|
||||||
/**
|
/**
|
||||||
* Get the current [PlaybackStateDao].
|
* Get the current [PlaybackStateDao].
|
||||||
|
@ -53,6 +54,14 @@ abstract class PersistenceDatabase : RoomDatabase() {
|
||||||
*/
|
*/
|
||||||
abstract fun queueDao(): QueueDao
|
abstract fun queueDao(): QueueDao
|
||||||
|
|
||||||
|
object Converters {
|
||||||
|
/** @see [Music.UID.toString] */
|
||||||
|
@TypeConverter fun fromMusicUID(uid: Music.UID?) = uid?.toString()
|
||||||
|
|
||||||
|
/** @see [Music.UID.fromString] */
|
||||||
|
@TypeConverter fun toMusicUid(string: String?) = string?.let(Music.UID::fromString)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Volatile private var INSTANCE: PersistenceDatabase? = null
|
@Volatile private var INSTANCE: PersistenceDatabase? = null
|
||||||
|
|
||||||
|
@ -145,10 +154,6 @@ interface QueueDao {
|
||||||
suspend fun insertMapping(mapping: List<QueueMappingItem>)
|
suspend fun insertMapping(mapping: List<QueueMappingItem>)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A raw representation of the persisted playback state.
|
|
||||||
* @author Alexander Capehart
|
|
||||||
*/
|
|
||||||
@Entity(tableName = PlaybackState.TABLE_NAME)
|
@Entity(tableName = PlaybackState.TABLE_NAME)
|
||||||
data class PlaybackState(
|
data class PlaybackState(
|
||||||
@PrimaryKey val id: Int,
|
@PrimaryKey val id: Int,
|
||||||
|
@ -163,10 +168,6 @@ data class PlaybackState(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A raw representation of the an individual item in the persisted queue's heap.
|
|
||||||
* @author Alexander Capehart
|
|
||||||
*/
|
|
||||||
@Entity(tableName = QueueHeapItem.TABLE_NAME)
|
@Entity(tableName = QueueHeapItem.TABLE_NAME)
|
||||||
data class QueueHeapItem(@PrimaryKey val id: Int, val uid: Music.UID) {
|
data class QueueHeapItem(@PrimaryKey val id: Int, val uid: Music.UID) {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -174,10 +175,6 @@ data class QueueHeapItem(@PrimaryKey val id: Int, val uid: Music.UID) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A raw representation of the heap indices at a particular position in the persisted queue.
|
|
||||||
* @author Alexander Capehart
|
|
||||||
*/
|
|
||||||
@Entity(tableName = QueueMappingItem.TABLE_NAME)
|
@Entity(tableName = QueueMappingItem.TABLE_NAME)
|
||||||
data class QueueMappingItem(
|
data class QueueMappingItem(
|
||||||
@PrimaryKey val id: Int,
|
@PrimaryKey val id: Int,
|
||||||
|
|
|
@ -19,7 +19,7 @@ package org.oxycblt.auxio.playback.persist
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.library.Library
|
import org.oxycblt.auxio.music.model.Library
|
||||||
import org.oxycblt.auxio.playback.queue.Queue
|
import org.oxycblt.auxio.playback.queue.Queue
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
|
@ -48,7 +48,7 @@ import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.library.Library
|
import org.oxycblt.auxio.music.model.Library
|
||||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
import org.oxycblt.auxio.playback.persist.PersistenceRepository
|
import org.oxycblt.auxio.playback.persist.PersistenceRepository
|
||||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
|
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
|
||||||
|
|
|
@ -30,9 +30,9 @@ import kotlinx.coroutines.yield
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.list.BasicHeader
|
import org.oxycblt.auxio.list.BasicHeader
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.music.*
|
|
||||||
import org.oxycblt.auxio.music.library.Library
|
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
|
import org.oxycblt.auxio.music.*
|
||||||
|
import org.oxycblt.auxio.music.model.Library
|
||||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
* 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.library
|
package org.oxycblt.auxio.music.model
|
||||||
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
Loading…
Reference in a new issue