music: add framework for playlist mutation
Add the boilerplate for basic playlist creation/addition. No integration in UI yet.
This commit is contained in:
parent
70e824c4e7
commit
9988a1b76b
14 changed files with 142 additions and 107 deletions
|
@ -24,9 +24,6 @@ deletion
|
||||||
#### What's Changed
|
#### What's Changed
|
||||||
- "Ignore articles when sorting" is now "Intelligent sorting"
|
- "Ignore articles when sorting" is now "Intelligent sorting"
|
||||||
|
|
||||||
#### What's Changed
|
|
||||||
- "Ignore articles when sorting" is now "Intelligent sorting"
|
|
||||||
|
|
||||||
## 3.0.3
|
## 3.0.3
|
||||||
|
|
||||||
#### What's New
|
#### What's New
|
||||||
|
|
|
@ -53,7 +53,7 @@ class DetailViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val musicRepository: MusicRepository,
|
private val musicRepository: MusicRepository,
|
||||||
private val audioInfoProvider: AudioInfo.Provider,
|
private val audioInfoFactory: AudioInfo.Factory,
|
||||||
private val musicSettings: MusicSettings,
|
private val musicSettings: MusicSettings,
|
||||||
private val playbackSettings: PlaybackSettings
|
private val playbackSettings: PlaybackSettings
|
||||||
) : ViewModel(), MusicRepository.UpdateListener {
|
) : ViewModel(), MusicRepository.UpdateListener {
|
||||||
|
@ -308,7 +308,7 @@ constructor(
|
||||||
_songAudioInfo.value = null
|
_songAudioInfo.value = null
|
||||||
currentSongJob =
|
currentSongJob =
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val info = audioInfoProvider.extract(song)
|
val info = audioInfoFactory.extract(song)
|
||||||
yield()
|
yield()
|
||||||
_songAudioInfo.value = info
|
_songAudioInfo.value = info
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,8 +172,8 @@ constructor(
|
||||||
private val cacheRepository: CacheRepository,
|
private val cacheRepository: CacheRepository,
|
||||||
private val mediaStoreExtractor: MediaStoreExtractor,
|
private val mediaStoreExtractor: MediaStoreExtractor,
|
||||||
private val tagExtractor: TagExtractor,
|
private val tagExtractor: TagExtractor,
|
||||||
private val deviceLibraryProvider: DeviceLibrary.Provider,
|
private val deviceLibraryFactory: DeviceLibrary.Factory,
|
||||||
private val userLibraryProvider: UserLibrary.Provider
|
private val userLibraryFactory: UserLibrary.Factory
|
||||||
) : MusicRepository {
|
) : MusicRepository {
|
||||||
private val updateListeners = mutableListOf<MusicRepository.UpdateListener>()
|
private val updateListeners = mutableListOf<MusicRepository.UpdateListener>()
|
||||||
private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>()
|
private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>()
|
||||||
|
@ -314,9 +314,9 @@ constructor(
|
||||||
val deviceLibraryChannel = Channel<DeviceLibrary>()
|
val deviceLibraryChannel = Channel<DeviceLibrary>()
|
||||||
val deviceLibraryJob =
|
val deviceLibraryJob =
|
||||||
worker.scope.async(Dispatchers.Main) {
|
worker.scope.async(Dispatchers.Main) {
|
||||||
deviceLibraryProvider.create(rawSongs).also { deviceLibraryChannel.send(it) }
|
deviceLibraryFactory.create(rawSongs).also { deviceLibraryChannel.send(it) }
|
||||||
}
|
}
|
||||||
val userLibraryJob = worker.scope.async { userLibraryProvider.read(deviceLibraryChannel) }
|
val userLibraryJob = worker.scope.async { userLibraryFactory.read(deviceLibraryChannel) }
|
||||||
if (cache == null || cache.invalidated) {
|
if (cache == null || cache.invalidated) {
|
||||||
cacheRepository.writeCache(rawSongs)
|
cacheRepository.writeCache(rawSongs)
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,7 +89,7 @@ interface DeviceLibrary {
|
||||||
fun findGenre(uid: Music.UID): Genre?
|
fun findGenre(uid: Music.UID): Genre?
|
||||||
|
|
||||||
/** Constructs a [DeviceLibrary] implementation in an asynchronous manner. */
|
/** Constructs a [DeviceLibrary] implementation in an asynchronous manner. */
|
||||||
interface Provider {
|
interface Factory {
|
||||||
/**
|
/**
|
||||||
* Create a new [DeviceLibrary].
|
* Create a new [DeviceLibrary].
|
||||||
*
|
*
|
||||||
|
@ -110,8 +110,8 @@ interface DeviceLibrary {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeviceLibraryProviderImpl @Inject constructor(private val musicSettings: MusicSettings) :
|
class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: MusicSettings) :
|
||||||
DeviceLibrary.Provider {
|
DeviceLibrary.Factory {
|
||||||
override suspend fun create(rawSongs: List<RawSong>): DeviceLibrary =
|
override suspend fun create(rawSongs: List<RawSong>): DeviceLibrary =
|
||||||
DeviceLibraryImpl(rawSongs, musicSettings)
|
DeviceLibraryImpl(rawSongs, musicSettings)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,5 @@ import dagger.hilt.components.SingletonComponent
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface DeviceModule {
|
interface DeviceModule {
|
||||||
@Binds
|
@Binds fun deviceLibraryProvider(factory: DeviceLibraryFactoryImpl): DeviceLibrary.Factory
|
||||||
fun deviceLibraryProvider(providerImpl: DeviceLibraryProviderImpl): DeviceLibrary.Provider
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -342,8 +342,8 @@ class ArtistImpl(
|
||||||
override val durationMs: Long?
|
override val durationMs: Long?
|
||||||
override val isCollaborator: Boolean
|
override val isCollaborator: Boolean
|
||||||
|
|
||||||
// Note: Append song contents to MusicParent equality so that Groups with
|
// Note: Append song contents to MusicParent equality so that artists with
|
||||||
// the same UID but different contents are not equal.
|
// the same UID but different songs are not equal.
|
||||||
override fun hashCode() = 31 * uid.hashCode() + songs.hashCode()
|
override fun hashCode() = 31 * uid.hashCode() + songs.hashCode()
|
||||||
override fun equals(other: Any?) =
|
override fun equals(other: Any?) =
|
||||||
other is ArtistImpl && uid == other.uid && songs == other.songs
|
other is ArtistImpl && uid == other.uid && songs == other.songs
|
||||||
|
|
|
@ -186,13 +186,13 @@ class RawGenre(
|
||||||
/** @see Music.rawName */
|
/** @see Music.rawName */
|
||||||
val name: String? = null
|
val name: String? = null
|
||||||
) {
|
) {
|
||||||
// Only group by the lowercase genre name. This allows Genre grouping to be
|
|
||||||
// case-insensitive, which may be helpful in some libraries with different ways of
|
|
||||||
// formatting genres.
|
|
||||||
|
|
||||||
// Cache the hashCode for HashMap efficiency.
|
// Cache the hashCode for HashMap efficiency.
|
||||||
private val hashCode = name?.lowercase().hashCode()
|
private val hashCode = name?.lowercase().hashCode()
|
||||||
|
|
||||||
|
// Only group by the lowercase genre name. This allows Genre grouping to be
|
||||||
|
// case-insensitive, which may be helpful in some libraries with different ways of
|
||||||
|
// formatting genres.
|
||||||
override fun hashCode() = hashCode
|
override fun hashCode() = hashCode
|
||||||
|
|
||||||
override fun equals(other: Any?) =
|
override fun equals(other: Any?) =
|
||||||
|
|
|
@ -178,8 +178,8 @@ private abstract class BaseMediaStoreExtractor(
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
// Assume that a song can't inhabit multiple genre entries, as I
|
// Assume that a song can't inhabit multiple genre entries, as I
|
||||||
// doubt
|
// doubt MediaStore is actually aware that songs can have multiple
|
||||||
// MediaStore is actually aware that songs can have multiple genres.
|
// genres.
|
||||||
genreNamesMap[cursor.getLong(songIdIndex)] = name
|
genreNamesMap[cursor.getLong(songIdIndex)] = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -311,9 +311,8 @@ private abstract class BaseMediaStoreExtractor(
|
||||||
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
|
// in, but in practice it is more often "0" (as in /storage/emulated/0), even when it
|
||||||
// the
|
// the file is not actually in the root internal storage directory. We can't do
|
||||||
// file is not actually in the root internal storage directory. We can't do anything to
|
// anything to fix this, really.
|
||||||
// fix this, really.
|
|
||||||
rawSong.albumName = cursor.getString(albumIndex)
|
rawSong.albumName = cursor.getString(albumIndex)
|
||||||
// Android does not make a non-existent artist tag null, it instead fills it in
|
// Android does not make a non-existent artist tag null, it instead fills it in
|
||||||
// as <unknown>, which makes absolutely no sense given how other columns default
|
// as <unknown>, which makes absolutely no sense given how other columns default
|
||||||
|
@ -356,9 +355,6 @@ private abstract class BaseMediaStoreExtractor(
|
||||||
// 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.
|
||||||
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSettings) :
|
private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSettings) :
|
||||||
BaseMediaStoreExtractor(context, musicSettings) {
|
BaseMediaStoreExtractor(context, musicSettings) {
|
||||||
override val projection: Array<String>
|
override val projection: Array<String>
|
||||||
|
|
|
@ -43,7 +43,7 @@ data class AudioInfo(
|
||||||
val resolvedMimeType: MimeType
|
val resolvedMimeType: MimeType
|
||||||
) {
|
) {
|
||||||
/** Implements the process of extracting [AudioInfo] from a given [Song]. */
|
/** Implements the process of extracting [AudioInfo] from a given [Song]. */
|
||||||
interface Provider {
|
interface Factory {
|
||||||
/**
|
/**
|
||||||
* Extract the [AudioInfo] of a given [Song].
|
* Extract the [AudioInfo] of a given [Song].
|
||||||
*
|
*
|
||||||
|
@ -55,12 +55,12 @@ data class AudioInfo(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A framework-backed implementation of [AudioInfo.Provider].
|
* A framework-backed implementation of [AudioInfo.Factory].
|
||||||
*
|
*
|
||||||
* @param context [Context] required to read audio files.
|
* @param context [Context] required to read audio files.
|
||||||
*/
|
*/
|
||||||
class AudioInfoProviderImpl @Inject constructor(@ApplicationContext private val context: Context) :
|
class AudioInfoFactoryImpl @Inject constructor(@ApplicationContext private val context: Context) :
|
||||||
AudioInfo.Provider {
|
AudioInfo.Factory {
|
||||||
|
|
||||||
override suspend fun extract(song: Song): AudioInfo {
|
override suspend fun extract(song: Song): AudioInfo {
|
||||||
// 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
|
||||||
|
|
|
@ -26,7 +26,7 @@ import dagger.hilt.components.SingletonComponent
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface MetadataModule {
|
interface MetadataModule {
|
||||||
@Binds fun tagExtractor(tagExtractor: TagExtractorImpl): TagExtractor
|
@Binds fun tagExtractor(extractor: TagExtractorImpl): TagExtractor
|
||||||
@Binds fun tagWorkerFactory(taskFactory: TagWorkerImpl.Factory): TagWorker.Factory
|
@Binds fun tagWorkerFactory(factory: TagWorkerFactoryImpl): TagWorker.Factory
|
||||||
@Binds fun audioInfoProvider(audioInfoProvider: AudioInfoProviderImpl): AudioInfo.Provider
|
@Binds fun audioInfoProvider(factory: AudioInfoFactoryImpl): AudioInfo.Factory
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,14 +56,26 @@ interface TagWorker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TagWorkerImpl
|
class TagWorkerFactoryImpl
|
||||||
private constructor(private val rawSong: RawSong, private val future: Future<TrackGroupArray>) :
|
@Inject
|
||||||
TagWorker {
|
constructor(private val mediaSourceFactory: MediaSource.Factory) : TagWorker.Factory {
|
||||||
/**
|
override fun create(rawSong: RawSong): TagWorker =
|
||||||
* Try to get a completed song from this [TagWorker], if it has finished processing.
|
// Note that we do not leverage future callbacks. This is because errors in the
|
||||||
*
|
// (highly fallible) extraction process will not bubble up to Indexer when a
|
||||||
* @return A [RawSong] instance if processing has completed, null otherwise.
|
// listener is used, instead crashing the app entirely.
|
||||||
*/
|
TagWorkerImpl(
|
||||||
|
rawSong,
|
||||||
|
MetadataRetriever.retrieveMetadata(
|
||||||
|
mediaSourceFactory,
|
||||||
|
MediaItem.fromUri(
|
||||||
|
requireNotNull(rawSong.mediaStoreId) { "Invalid raw: No id" }.toAudioUri())))
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TagWorkerImpl(
|
||||||
|
private val rawSong: RawSong,
|
||||||
|
private val future: Future<TrackGroupArray>
|
||||||
|
) : TagWorker {
|
||||||
|
|
||||||
override fun poll(): RawSong? {
|
override fun poll(): RawSong? {
|
||||||
if (!future.isDone) {
|
if (!future.isDone) {
|
||||||
// Not done yet, nothing to do.
|
// Not done yet, nothing to do.
|
||||||
|
@ -95,12 +107,6 @@ private constructor(private val rawSong: RawSong, private val future: Future<Tra
|
||||||
return rawSong
|
return rawSong
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Complete this instance's [RawSong] with ID3v2 Text Identification Frames.
|
|
||||||
*
|
|
||||||
* @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more
|
|
||||||
* values.
|
|
||||||
*/
|
|
||||||
private fun populateWithId3v2(textFrames: Map<String, List<String>>) {
|
private fun populateWithId3v2(textFrames: Map<String, List<String>>) {
|
||||||
// Song
|
// Song
|
||||||
textFrames["TXXX:musicbrainz release track id"]?.let { rawSong.musicBrainzId = it.first() }
|
textFrames["TXXX:musicbrainz release track id"]?.let { rawSong.musicBrainzId = it.first() }
|
||||||
|
@ -169,16 +175,6 @@ private constructor(private val rawSong: RawSong, private val future: Future<Tra
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the ID3v2.3 timestamp specification into a [Date] from the given Text Identification
|
|
||||||
* Frames.
|
|
||||||
*
|
|
||||||
* @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more
|
|
||||||
* values.
|
|
||||||
* @return A [Date] of a year value from TORY/TYER, a month and day value from TDAT, and a
|
|
||||||
* hour/minute value from TIME. No second value is included. The latter two fields may not be
|
|
||||||
* included in they cannot be parsed. Will be null if a year value could not be parsed.
|
|
||||||
*/
|
|
||||||
private fun parseId3v23Date(textFrames: Map<String, List<String>>): Date? {
|
private fun parseId3v23Date(textFrames: Map<String, List<String>>): Date? {
|
||||||
// Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY
|
// Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY
|
||||||
// is present.
|
// is present.
|
||||||
|
@ -212,11 +208,6 @@ private constructor(private val rawSong: RawSong, private val future: Future<Tra
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Complete this instance's [RawSong] with Vorbis comments.
|
|
||||||
*
|
|
||||||
* @param comments A mapping between vorbis comment names and one or more vorbis comment values.
|
|
||||||
*/
|
|
||||||
private fun populateWithVorbis(comments: Map<String, List<String>>) {
|
private fun populateWithVorbis(comments: Map<String, List<String>>) {
|
||||||
// Song
|
// Song
|
||||||
comments["musicbrainz_releasetrackid"]?.let { rawSong.musicBrainzId = it.first() }
|
comments["musicbrainz_releasetrackid"]?.let { rawSong.musicBrainzId = it.first() }
|
||||||
|
@ -277,21 +268,6 @@ private constructor(private val rawSong: RawSong, private val future: Future<Tra
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory @Inject constructor(private val mediaSourceFactory: MediaSource.Factory) :
|
|
||||||
TagWorker.Factory {
|
|
||||||
override fun create(rawSong: RawSong) =
|
|
||||||
// Note that we do not leverage future callbacks. This is because errors in the
|
|
||||||
// (highly fallible) extraction process will not bubble up to Indexer when a
|
|
||||||
// listener is used, instead crashing the app entirely.
|
|
||||||
TagWorkerImpl(
|
|
||||||
rawSong,
|
|
||||||
MetadataRetriever.retrieveMetadata(
|
|
||||||
mediaSourceFactory,
|
|
||||||
MediaItem.fromUri(
|
|
||||||
requireNotNull(rawSong.mediaStoreId) { "Invalid raw: No id" }
|
|
||||||
.toAudioUri())))
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
val COMPILATION_ALBUM_ARTISTS = listOf("Various Artists")
|
val COMPILATION_ALBUM_ARTISTS = listOf("Various Artists")
|
||||||
val COMPILATION_RELEASE_TYPES = listOf("compilation")
|
val COMPILATION_RELEASE_TYPES = listOf("compilation")
|
||||||
|
|
|
@ -27,29 +27,60 @@ class PlaylistImpl
|
||||||
private constructor(
|
private constructor(
|
||||||
override val uid: Music.UID,
|
override val uid: Music.UID,
|
||||||
override val rawName: String,
|
override val rawName: String,
|
||||||
override val songs: List<Song>,
|
override val sortName: SortName,
|
||||||
musicSettings: MusicSettings
|
override val songs: List<Song>
|
||||||
) : Playlist {
|
) : Playlist {
|
||||||
constructor(
|
|
||||||
name: String,
|
|
||||||
songs: List<Song>,
|
|
||||||
musicSettings: MusicSettings
|
|
||||||
) : this(Music.UID.auxio(MusicMode.PLAYLISTS, UUID.randomUUID()), name, songs, musicSettings)
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
rawPlaylist: RawPlaylist,
|
|
||||||
deviceLibrary: DeviceLibrary,
|
|
||||||
musicSettings: MusicSettings
|
|
||||||
) : this(
|
|
||||||
rawPlaylist.playlistInfo.playlistUid,
|
|
||||||
rawPlaylist.playlistInfo.name,
|
|
||||||
rawPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.songUid) },
|
|
||||||
musicSettings)
|
|
||||||
|
|
||||||
override fun resolveName(context: Context) = rawName
|
override fun resolveName(context: Context) = rawName
|
||||||
override val rawSortName = null
|
override val rawSortName = null
|
||||||
override val sortName = SortName(rawName, musicSettings)
|
|
||||||
override val durationMs = songs.sumOf { it.durationMs }
|
override val durationMs = songs.sumOf { it.durationMs }
|
||||||
override val albums =
|
override val albums =
|
||||||
songs.groupBy { it.album }.entries.sortedByDescending { it.value.size }.map { it.key }
|
songs.groupBy { it.album }.entries.sortedByDescending { it.value.size }.map { it.key }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clone the data in this instance to a new [PlaylistImpl] with the given [Song]s.
|
||||||
|
*
|
||||||
|
* @param songs The new [Song]s to use.
|
||||||
|
*/
|
||||||
|
fun edit(songs: List<Song>) = PlaylistImpl(uid, rawName, sortName, songs)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clone the data in this instance to a new [PlaylistImpl] with the given [edits].
|
||||||
|
*
|
||||||
|
* @param edits The edits to make to the [Song]s of the playlist.
|
||||||
|
*/
|
||||||
|
inline fun edit(edits: MutableList<Song>.() -> Unit) = edit(songs.toMutableList().apply(edits))
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Create a new instance with a novel UID.
|
||||||
|
*
|
||||||
|
* @param name The name of the playlist.
|
||||||
|
* @param songs The songs to initially populate the playlist with.
|
||||||
|
* @param musicSettings [MusicSettings] required for name configuration.
|
||||||
|
*/
|
||||||
|
fun new(name: String, songs: List<Song>, musicSettings: MusicSettings) =
|
||||||
|
PlaylistImpl(
|
||||||
|
Music.UID.auxio(MusicMode.PLAYLISTS, UUID.randomUUID()),
|
||||||
|
name,
|
||||||
|
SortName(name, musicSettings),
|
||||||
|
songs)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populate a new instance from a read [RawPlaylist].
|
||||||
|
*
|
||||||
|
* @param rawPlaylist The [RawPlaylist] to read from.
|
||||||
|
* @param deviceLibrary The [DeviceLibrary] to initialize from.
|
||||||
|
* @param musicSettings [MusicSettings] required for name configuration.
|
||||||
|
*/
|
||||||
|
fun fromRaw(
|
||||||
|
rawPlaylist: RawPlaylist,
|
||||||
|
deviceLibrary: DeviceLibrary,
|
||||||
|
musicSettings: MusicSettings
|
||||||
|
) =
|
||||||
|
PlaylistImpl(
|
||||||
|
rawPlaylist.playlistInfo.playlistUid,
|
||||||
|
rawPlaylist.playlistInfo.name,
|
||||||
|
SortName(rawPlaylist.playlistInfo.name, musicSettings),
|
||||||
|
rawPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.songUid) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,23 +45,46 @@ interface UserLibrary {
|
||||||
fun findPlaylist(uid: Music.UID): Playlist?
|
fun findPlaylist(uid: Music.UID): Playlist?
|
||||||
|
|
||||||
/** Constructs a [UserLibrary] implementation in an asynchronous manner. */
|
/** Constructs a [UserLibrary] implementation in an asynchronous manner. */
|
||||||
interface Provider {
|
interface Factory {
|
||||||
/**
|
/**
|
||||||
* Create a new [UserLibrary].
|
* Create a new [UserLibrary].
|
||||||
*
|
*
|
||||||
* @param deviceLibrary Asynchronously populated [DeviceLibrary] that can be obtained later.
|
* @param deviceLibrary Asynchronously populated [DeviceLibrary] that can be obtained later.
|
||||||
* This allows database information to be read before the actual instance is constructed.
|
* This allows database information to be read before the actual instance is constructed.
|
||||||
* @return A new [UserLibrary] with the required implementation.
|
* @return A new [MutableUserLibrary] with the required implementation.
|
||||||
*/
|
*/
|
||||||
suspend fun read(deviceLibrary: Channel<DeviceLibrary>): UserLibrary
|
suspend fun read(deviceLibrary: Channel<DeviceLibrary>): MutableUserLibrary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserLibraryProviderImpl
|
/**
|
||||||
|
* A mutable instance of [UserLibrary]. Not meant for use outside of the music module. Use
|
||||||
|
* [MusicRepository] instead.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
interface MutableUserLibrary : UserLibrary {
|
||||||
|
/**
|
||||||
|
* Make a new [Playlist].
|
||||||
|
*
|
||||||
|
* @param name The name of the [Playlist].
|
||||||
|
* @param songs The songs to place in the [Playlist].
|
||||||
|
*/
|
||||||
|
fun createPlaylist(name: String, songs: List<Song>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add [Song]s to a [Playlist].
|
||||||
|
*
|
||||||
|
* @param playlist The [Playlist] to add to. Must currently exist.
|
||||||
|
*/
|
||||||
|
fun addToPlaylist(playlist: Playlist, songs: List<Song>)
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserLibraryFactoryImpl
|
||||||
@Inject
|
@Inject
|
||||||
constructor(private val playlistDao: PlaylistDao, private val musicSettings: MusicSettings) :
|
constructor(private val playlistDao: PlaylistDao, private val musicSettings: MusicSettings) :
|
||||||
UserLibrary.Provider {
|
UserLibrary.Factory {
|
||||||
override suspend fun read(deviceLibrary: Channel<DeviceLibrary>): UserLibrary =
|
override suspend fun read(deviceLibrary: Channel<DeviceLibrary>): MutableUserLibrary =
|
||||||
UserLibraryImpl(playlistDao, deviceLibrary.receive(), musicSettings)
|
UserLibraryImpl(playlistDao, deviceLibrary.receive(), musicSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,15 +92,28 @@ private class UserLibraryImpl(
|
||||||
private val playlistDao: PlaylistDao,
|
private val playlistDao: PlaylistDao,
|
||||||
private val deviceLibrary: DeviceLibrary,
|
private val deviceLibrary: DeviceLibrary,
|
||||||
private val musicSettings: MusicSettings
|
private val musicSettings: MusicSettings
|
||||||
) : UserLibrary {
|
) : MutableUserLibrary {
|
||||||
private val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>()
|
private val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>()
|
||||||
override val playlists: List<Playlist>
|
override val playlists: List<Playlist>
|
||||||
get() = playlistMap.values.toList()
|
get() = playlistMap.values.toList()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val playlist = PlaylistImpl("Playlist 1", deviceLibrary.songs.slice(58..100), musicSettings)
|
// TODO: Actually read playlists
|
||||||
playlistMap[playlist.uid] = playlist
|
createPlaylist("Playlist 1", deviceLibrary.songs.slice(58..100))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findPlaylist(uid: Music.UID) = playlistMap[uid]
|
override fun findPlaylist(uid: Music.UID) = playlistMap[uid]
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun createPlaylist(name: String, songs: List<Song>) {
|
||||||
|
val playlistImpl = PlaylistImpl.new(name, songs, musicSettings)
|
||||||
|
playlistMap[playlistImpl.uid] = playlistImpl
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun addToPlaylist(playlist: Playlist, songs: List<Song>) {
|
||||||
|
val playlistImpl =
|
||||||
|
requireNotNull(playlistMap[playlist.uid]) { "Cannot add to invalid playlist" }
|
||||||
|
playlistMap[playlist.uid] = playlistImpl.edit { addAll(songs) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ import dagger.hilt.components.SingletonComponent
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface UserModule {
|
interface UserModule {
|
||||||
@Binds fun userLibaryProvider(provider: UserLibraryProviderImpl): UserLibrary.Provider
|
@Binds fun userLibaryFactory(factory: UserLibraryFactoryImpl): UserLibrary.Factory
|
||||||
}
|
}
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|
Loading…
Reference in a new issue