music: build saf loader playlist boilerplate

This commit is contained in:
Alexander Capehart 2024-11-23 17:18:02 -07:00
parent ba9ab5a445
commit c4f4797028
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
11 changed files with 140 additions and 399 deletions

View file

@ -19,6 +19,8 @@
package org.oxycblt.auxio.music.stack.explore package org.oxycblt.auxio.music.stack.explore
import android.net.Uri import android.net.Uri
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.stack.interpret.model.SongImpl import org.oxycblt.auxio.music.stack.interpret.model.SongImpl
import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.stack.explore.fs.Path import org.oxycblt.auxio.music.stack.explore.fs.Path
@ -61,6 +63,21 @@ data class AudioFile(
var genreNames: List<String> = listOf() var genreNames: List<String> = listOf()
) )
interface PlaylistFile { data class PlaylistFile(
val name: String val name: String,
val songPointers: List<SongPointer>,
val editor: PlaylistHandle
)
interface PlaylistHandle {
val uid: Music.UID
suspend fun rename(name: String)
suspend fun add(songs: List<Song>)
suspend fun rewrite(songs: List<Song>)
suspend fun delete()
}
sealed interface SongPointer {
data class UID(val uid: Music.UID) : SongPointer
// data class Path(val options: List<Path>) : SongPointer
} }

View file

@ -16,7 +16,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.user package org.oxycblt.auxio.music.stack.explore.playlists
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Database import androidx.room.Database
@ -38,7 +38,7 @@ import org.oxycblt.auxio.music.Music
version = 30, version = 30,
exportSchema = false) exportSchema = false)
@TypeConverters(Music.UID.TypeConverters::class) @TypeConverters(Music.UID.TypeConverters::class)
abstract class UserMusicDatabase : RoomDatabase() { abstract class PlaylistDatabase : RoomDatabase() {
abstract fun playlistDao(): PlaylistDao abstract fun playlistDao(): PlaylistDao
} }

View file

@ -16,7 +16,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.user package org.oxycblt.auxio.music.stack.explore.playlists
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Embedded import androidx.room.Embedded
@ -27,7 +27,7 @@ import androidx.room.Relation
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
/** /**
* Raw playlist information persisted to [UserMusicDatabase]. * Raw playlist information persisted to [PlaylistDatabase].
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */

View file

@ -0,0 +1,26 @@
package org.oxycblt.auxio.music.stack.explore.playlists
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import org.oxycblt.auxio.music.stack.explore.PlaylistFile
import org.oxycblt.auxio.music.stack.explore.SongPointer
import javax.inject.Inject
interface StoredPlaylists {
fun read(): Flow<PlaylistFile>
}
class StoredPlaylistsImpl @Inject constructor(
private val playlistDao: PlaylistDao
) : StoredPlaylists {
override fun read() = flow {
emitAll(playlistDao.readRawPlaylists()
.asFlow()
.map {
TODO()
})
}
}

View file

@ -16,7 +16,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.user package org.oxycblt.auxio.music.stack.explore.playlists
import android.content.Context import android.content.Context
import androidx.room.Room import androidx.room.Room
@ -29,19 +29,19 @@ import dagger.hilt.components.SingletonComponent
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
interface UserModule { interface PlaylistModule {
@Binds fun userLibraryFactory(factory: UserLibraryFactoryImpl): UserLibrary.Factory @Binds fun storedPlaylists(impl: StoredPlaylistsImpl): StoredPlaylists
} }
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class UserRoomModule { class PlaylistRoomModule {
@Provides fun playlistDao(database: UserMusicDatabase) = database.playlistDao() @Provides fun playlistDao(database: PlaylistDatabase) = database.playlistDao()
@Provides @Provides
fun userMusicDatabase(@ApplicationContext context: Context) = fun playlistDatabase(@ApplicationContext context: Context) =
Room.databaseBuilder( Room.databaseBuilder(
context.applicationContext, UserMusicDatabase::class.java, "user_music.db") context.applicationContext, PlaylistDatabase::class.java, "user_music.db")
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.build() .build()
} }

View file

@ -1,10 +1,14 @@
package org.oxycblt.auxio.music.stack.interpret.linker package org.oxycblt.auxio.music.stack.interpret.linker
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.stack.explore.PlaylistFile
import org.oxycblt.auxio.music.stack.interpret.model.AlbumImpl import org.oxycblt.auxio.music.stack.interpret.model.AlbumImpl
import org.oxycblt.auxio.music.stack.interpret.model.ArtistImpl import org.oxycblt.auxio.music.stack.interpret.model.ArtistImpl
import org.oxycblt.auxio.music.stack.interpret.model.GenreImpl import org.oxycblt.auxio.music.stack.interpret.model.GenreImpl
import org.oxycblt.auxio.music.stack.interpret.model.PlaylistImpl
import org.oxycblt.auxio.music.stack.interpret.model.SongImpl import org.oxycblt.auxio.music.stack.interpret.model.SongImpl
import org.oxycblt.auxio.music.stack.interpret.prepare.PreAlbum import org.oxycblt.auxio.music.stack.interpret.prepare.PreAlbum
import org.oxycblt.auxio.music.stack.interpret.prepare.PrePlaylist
import org.oxycblt.auxio.music.stack.interpret.prepare.PreSong import org.oxycblt.auxio.music.stack.interpret.prepare.PreSong
interface LinkedSong { interface LinkedSong {
@ -19,6 +23,11 @@ interface LinkedAlbum {
val artists: Linked<List<ArtistImpl>, AlbumImpl> val artists: Linked<List<ArtistImpl>, AlbumImpl>
} }
interface LinkedPlaylist {
val prePlaylist: PrePlaylist
val songs: Linked<List<SongImpl>, PlaylistImpl>
}
interface Linked<P, C> { interface Linked<P, C> {
fun resolve(child: C): P fun resolve(child: C): P
} }

View file

@ -0,0 +1,15 @@
package org.oxycblt.auxio.music.stack.interpret.linker
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import org.oxycblt.auxio.music.stack.explore.PlaylistFile
import org.oxycblt.auxio.music.stack.interpret.model.GenreImpl
import org.oxycblt.auxio.music.stack.interpret.model.PlaylistImpl
import org.oxycblt.auxio.music.stack.interpret.model.SongImpl
import org.oxycblt.auxio.music.stack.interpret.prepare.PreSong
class PlaylistLinker {
fun register(playlists: Flow<PlaylistFile>, linkedSongs: Flow<AlbumLinker.LinkedSong>): Flow<LinkedPlaylist> = emptyFlow()
fun resolve(): Collection<PlaylistImpl> = setOf()
}

View file

@ -0,0 +1,51 @@
/*
* Copyright (c) 2023 Auxio Project
* PlaylistImpl.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.stack.interpret.model
import org.oxycblt.auxio.image.extractor.ParentCover
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.stack.explore.PlaylistFile
import org.oxycblt.auxio.music.stack.explore.playlists.RawPlaylist
import org.oxycblt.auxio.music.stack.interpret.linker.LinkedPlaylist
class PlaylistImpl(linkedPlaylist: LinkedPlaylist) : Playlist {
private val prePlaylist = linkedPlaylist.prePlaylist
override val uid = prePlaylist.handle.uid
override val name: Name.Known = prePlaylist.name
override val songs = linkedPlaylist.songs.resolve(this)
override val durationMs = songs.sumOf { it.durationMs }
override val cover = songs.takeIf { it.isNotEmpty() }?.let { ParentCover.from(it.first(), it) }
private var hashCode = uid.hashCode()
init {
hashCode = 31 * hashCode + prePlaylist.hashCode()
hashCode = 31 * hashCode + songs.hashCode()
}
override fun equals(other: Any?) =
other is PlaylistImpl && prePlaylist == other.prePlaylist && songs == other.songs
override fun hashCode() = hashCode
override fun toString() = "Playlist(uid=$uid, name=$name)"
}

View file

@ -6,13 +6,13 @@ import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.Disc
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.music.info.ReleaseType
import org.oxycblt.auxio.music.stack.explore.PlaylistFile
import org.oxycblt.auxio.music.stack.explore.PlaylistHandle
import org.oxycblt.auxio.music.stack.explore.fs.MimeType import org.oxycblt.auxio.music.stack.explore.fs.MimeType
import org.oxycblt.auxio.music.stack.explore.fs.Path import org.oxycblt.auxio.music.stack.explore.fs.Path
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
import java.util.UUID import java.util.UUID
interface PrePlaylist
data class PreSong( data class PreSong(
val musicBrainzId: UUID?, val musicBrainzId: UUID?,
val name: Name, val name: Name,
@ -51,3 +51,9 @@ data class PreGenre(
val name: Name, val name: Name,
val rawName: String?, val rawName: String?,
) )
data class PrePlaylist(
val name: Name.Known,
val rawName: String?,
val handle: PlaylistHandle
)

View file

@ -1,103 +0,0 @@
/*
* Copyright (c) 2023 Auxio Project
* PlaylistImpl.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.user
import org.oxycblt.auxio.image.extractor.ParentCover
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.model.DeviceLibrary
import org.oxycblt.auxio.music.info.Name
class PlaylistImpl
private constructor(
override val uid: Music.UID,
override val name: Name.Known,
override val songs: List<Song>
) : Playlist {
override val durationMs = songs.sumOf { it.durationMs }
private var hashCode = uid.hashCode()
init {
hashCode = 31 * hashCode + name.hashCode()
hashCode = 31 * hashCode + songs.hashCode()
}
override fun equals(other: Any?) =
other is PlaylistImpl && uid == other.uid && name == other.name && songs == other.songs
override fun hashCode() = hashCode
override fun toString() = "Playlist(uid=$uid, name=$name)"
override val cover = songs.takeIf { it.isNotEmpty() }?.let { ParentCover.from(it.first(), it) }
/**
* Clone the data in this instance to a new [PlaylistImpl] with the given [name].
*
* @param name The new name to use.
* @param nameFactory The [Name.Known.Factory] to interpret name information with.
*/
fun edit(name: String, nameFactory: Name.Known.Factory) =
PlaylistImpl(uid, nameFactory.parse(name, null), songs)
/**
* 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, name, 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 nameFactory The [Name.Known.Factory] to interpret name information with.
*/
fun from(name: String, songs: List<Song>, nameFactory: Name.Known.Factory) =
PlaylistImpl(Music.UID.auxio(MusicType.PLAYLISTS), nameFactory.parse(name, null), songs)
/**
* Populate a new instance from a read [RawPlaylist].
*
* @param rawPlaylist The [RawPlaylist] to read from.
* @param deviceLibrary The [DeviceLibrary] to initialize from.
* @param nameFactory The [Name.Known.Factory] to interpret name information with.
*/
fun fromRaw(
rawPlaylist: RawPlaylist,
deviceLibrary: DeviceLibrary,
nameFactory: Name.Known.Factory
) =
PlaylistImpl(
rawPlaylist.playlistInfo.playlistUid,
nameFactory.parse(rawPlaylist.playlistInfo.name, null),
rawPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.songUid) })
}
}

View file

@ -1,280 +0,0 @@
/*
* Copyright (c) 2023 Auxio Project
* UserLibrary.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.user
import java.lang.Exception
import javax.inject.Inject
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.model.DeviceLibrary
import org.oxycblt.auxio.music.info.Name
import timber.log.Timber as L
/**
* Organized library information controlled by the user.
*
* Unlike [DeviceLibrary], [UserLibrary]s can be mutated without needing to clone the instance. It
* is also not backed by library information, rather an app database with in-memory caching. It is
* generally not expected to create this yourself, and instead rely on MusicRepository.
*
* @author Alexander Capehart
*
* TODO: Communicate errors
* TODO: How to handle empty playlists that appear because all of their songs have disappeared?
*/
interface UserLibrary {
/** The current user-defined playlists. */
val playlists: Collection<Playlist>
/**
* Find a [Playlist] instance corresponding to the given [Music.UID].
*
* @param uid The [Music.UID] to search for.
* @return The corresponding [Song], or null if one was not found.
*/
fun findPlaylist(uid: Music.UID): Playlist?
/**
* Finds a playlist by it's [name]. Since all [Playlist] names must be unique, this will always
* return at most 1 value.
*
* @param name The name [String] to search for.
*/
fun findPlaylist(name: String): Playlist?
/** Constructs a [UserLibrary] implementation in an asynchronous manner. */
interface Factory {
/**
* Read all [RawPlaylist] information from the database, which can be transformed into a
* [UserLibrary] later.
*
* @return A list of [RawPlaylist]s.
*/
suspend fun query(): List<RawPlaylist>
/**
* Create a new [UserLibrary] from read [RawPlaylist] instances and a precursor
* [DeviceLibrary].
*
* @param rawPlaylists The [RawPlaylist]s to use.
* @param deviceLibrary The [DeviceLibrary] to use.
* @return The new [UserLibrary] instance.
*/
suspend fun create(
rawPlaylists: List<RawPlaylist>,
deviceLibrary: DeviceLibrary,
nameFactory: Name.Known.Factory
): MutableUserLibrary
}
}
/**
* 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].
* @return The new [Playlist] instance, or null if one could not be created.
*/
suspend fun createPlaylist(name: String, songs: List<Song>): Playlist?
/**
* Rename a [Playlist].
*
* @param playlist The [Playlist] to rename.
* @param name The name of the new [Playlist].
* @return True if the [Playlist] was successfully renamed, false otherwise.
*/
suspend fun renamePlaylist(playlist: Playlist, name: String): Boolean
/**
* Delete a [Playlist].
*
* @param playlist The playlist to delete.
* @return True if the [Playlist] was successfully deleted, false otherwise.
*/
suspend fun deletePlaylist(playlist: Playlist): Boolean
/**
* Add [Song]s to a [Playlist].
*
* @param playlist The [Playlist] to add to. Must currently exist.
* @param songs The [Song]s to add to the [Playlist].
* @return True if the [Song]s were successfully added, false otherwise.
*/
suspend fun addToPlaylist(playlist: Playlist, songs: List<Song>): Boolean
/**
* Update the [Song]s of a [Playlist].
*
* @param playlist The [Playlist] to update.
* @param songs The new [Song]s to be contained in the [Playlist].
* @return True if the [Playlist] was successfully updated, false otherwise.
*/
suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>): Boolean
}
class UserLibraryFactoryImpl @Inject constructor(private val playlistDao: PlaylistDao) :
UserLibrary.Factory {
override suspend fun query() =
try {
val rawPlaylists = playlistDao.readRawPlaylists()
L.d("Successfully read ${rawPlaylists.size} playlists")
rawPlaylists
} catch (e: Exception) {
L.e("Unable to read playlists: $e")
listOf()
}
override suspend fun create(
rawPlaylists: List<RawPlaylist>,
deviceLibrary: DeviceLibrary,
nameFactory: Name.Known.Factory
): MutableUserLibrary {
val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>()
for (rawPlaylist in rawPlaylists) {
val playlistImpl = PlaylistImpl.fromRaw(rawPlaylist, deviceLibrary, nameFactory)
playlistMap[playlistImpl.uid] = playlistImpl
}
return UserLibraryImpl(playlistDao, playlistMap, nameFactory)
}
}
private class UserLibraryImpl(
private val playlistDao: PlaylistDao,
private val playlistMap: MutableMap<Music.UID, PlaylistImpl>,
private val nameFactory: Name.Known.Factory
) : MutableUserLibrary {
override fun hashCode() = playlistMap.hashCode()
override fun equals(other: Any?) = other is UserLibraryImpl && other.playlistMap == playlistMap
override fun toString() = "UserLibrary(playlists=${playlists.size})"
override val playlists: Collection<Playlist>
get() = playlistMap.values.toSet()
override fun findPlaylist(uid: Music.UID) = playlistMap[uid]
override fun findPlaylist(name: String) = playlistMap.values.find { it.name.raw == name }
override suspend fun createPlaylist(name: String, songs: List<Song>): Playlist? {
val playlistImpl = PlaylistImpl.from(name, songs, nameFactory)
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
val rawPlaylist =
RawPlaylist(
PlaylistInfo(playlistImpl.uid, playlistImpl.name.raw),
playlistImpl.songs.map { PlaylistSong(it.uid) })
return try {
playlistDao.insertPlaylist(rawPlaylist)
L.d("Successfully created playlist $name with ${songs.size} songs")
playlistImpl
} catch (e: Exception) {
L.e("Unable to create playlist $name with ${songs.size} songs")
L.e(e.stackTraceToString())
synchronized(this) { playlistMap.remove(playlistImpl.uid) }
null
}
}
override suspend fun renamePlaylist(playlist: Playlist, name: String): Boolean {
val playlistImpl =
synchronized(this) {
requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" }
.also { playlistMap[it.uid] = it.edit(name, nameFactory) }
}
return try {
playlistDao.replacePlaylistInfo(PlaylistInfo(playlist.uid, name))
L.d("Successfully renamed $playlist to $name")
true
} catch (e: Exception) {
L.e("Unable to rename $playlist to $name: $e")
L.e(e.stackTraceToString())
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
false
}
}
override suspend fun deletePlaylist(playlist: Playlist): Boolean {
val playlistImpl =
synchronized(this) {
requireNotNull(playlistMap[playlist.uid]) { "Cannot remove invalid playlist" }
.also { playlistMap.remove(it.uid) }
}
return try {
playlistDao.deletePlaylist(playlist.uid)
L.d("Successfully deleted $playlist")
true
} catch (e: Exception) {
L.e("Unable to delete $playlist: $e")
L.e(e.stackTraceToString())
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
false
}
}
override suspend fun addToPlaylist(playlist: Playlist, songs: List<Song>): Boolean {
val playlistImpl =
synchronized(this) {
requireNotNull(playlistMap[playlist.uid]) { "Cannot add to invalid playlist" }
.also { playlistMap[it.uid] = it.edit { addAll(songs) } }
}
return try {
playlistDao.insertPlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) })
L.d("Successfully added ${songs.size} songs to $playlist")
true
} catch (e: Exception) {
L.e("Unable to add ${songs.size} songs to $playlist: $e")
L.e(e.stackTraceToString())
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
false
}
}
override suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>): Boolean {
val playlistImpl =
synchronized(this) {
requireNotNull(playlistMap[playlist.uid]) { "Cannot rewrite invalid playlist" }
.also { playlistMap[it.uid] = it.edit(songs) }
}
return try {
playlistDao.replacePlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) })
L.d("Successfully rewrote $playlist with ${songs.size} songs")
true
} catch (e: Exception) {
L.e("Unable to rewrite $playlist with ${songs.size} songs: $e")
L.e(e.stackTraceToString())
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
false
}
}
}