package org.fossify.musicplayer.helpers import android.content.Context import org.fossify.commons.extensions.addBit import org.fossify.commons.extensions.getParentPath import org.fossify.commons.helpers.ensureBackgroundThread import org.fossify.musicplayer.extensions.* import org.fossify.musicplayer.inlines.indexOfFirstOrNull import org.fossify.musicplayer.models.* import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds class AudioHelper(private val context: Context) { private val config = context.config fun insertTracks(tracks: List) { context.tracksDAO.insertAll(tracks) } fun getTrack(mediaStoreId: Long): Track? { return context.tracksDAO.getTrackWithMediaStoreId(mediaStoreId) } fun getAllTracks(): ArrayList { val tracks = context.tracksDAO.getAll() .applyProperFilenames(config.showFilename) tracks.sortSafely(config.trackSorting) return tracks } fun getAllFolders(): ArrayList { val tracks = context.audioHelper.getAllTracks() val foldersMap = tracks.groupBy { it.folderName } val folders = ArrayList() val excludedFolders = config.excludedFolders for ((title, folderTracks) in foldersMap) { val path = (folderTracks.firstOrNull()?.path?.getParentPath() ?: "").removeSuffix("/") if (excludedFolders.contains(path)) { continue } val folder = Folder(title, folderTracks.size, path) folders.add(folder) } folders.sortSafely(config.folderSorting) return folders } fun getFolderTracks(folder: String): ArrayList { val tracks = context.tracksDAO.getTracksFromFolder(folder) .applyProperFilenames(config.showFilename) tracks.sortSafely(config.getProperFolderSorting(folder)) return tracks } fun updateTrackInfo(newPath: String, artist: String, title: String, oldPath: String) { context.tracksDAO.updateSongInfo(newPath, artist, title, oldPath) } fun deleteTrack(mediaStoreId: Long) { context.tracksDAO.removeTrack(mediaStoreId) } fun deleteTracks(tracks: List) { tracks.forEach { deleteTrack(it.mediaStoreId) } } fun removeTracksFromPlaylist(playlistId: Int, mediaStoreIds: List) { if (mediaStoreIds.isEmpty()) return context.tracksDAO.removeTracksFromPlaylist(playlistId, mediaStoreIds) } fun insertArtists(artists: List) { context.artistDAO.insertAll(artists) } fun getAllArtists(): ArrayList { val artists = context.artistDAO.getAll() as ArrayList artists.sortSafely(config.artistSorting) return artists } fun getArtistAlbums(artistId: Long): ArrayList { return context.albumsDAO.getArtistAlbums(artistId) as ArrayList } fun getArtistAlbums(artists: List): ArrayList { return artists.flatMap { getArtistAlbums(it.id) } as ArrayList } fun getArtistTracks(artistId: Long): ArrayList { return context.tracksDAO.getTracksFromArtist(artistId) .applyProperFilenames(config.showFilename) } fun getArtistTracks(artists: List): ArrayList { return getAlbumTracks( albums = getArtistAlbums(artists) ) } fun deleteArtist(id: Long) { context.artistDAO.deleteArtist(id) } fun deleteArtists(artists: List) { artists.forEach { deleteArtist(it.id) } } fun insertAlbums(albums: List) { context.albumsDAO.insertAll(albums) } fun getAlbum(albumId: Long): Album? { return context.albumsDAO.getAlbumWithId(albumId) } fun getAllAlbums(): ArrayList { val albums = context.albumsDAO.getAll() as ArrayList albums.sortSafely(config.albumSorting) return albums } fun getAlbumTracks(albumId: Long): ArrayList { val tracks = context.tracksDAO.getTracksFromAlbum(albumId) .applyProperFilenames(config.showFilename) tracks.sortWith(compareBy({ it.discNumber }, { it.trackId }, { it.title.lowercase() })) return tracks } fun getAlbumTracks(albums: List): ArrayList { return albums.flatMap { getAlbumTracks(it.id) } .applyProperFilenames(config.showFilename) } private fun deleteAlbum(id: Long) { context.albumsDAO.deleteAlbum(id) } fun deleteAlbums(albums: List) { albums.forEach { deleteAlbum(it.id) } } fun insertPlaylist(playlist: Playlist): Long { return context.playlistDAO.insert(playlist) } fun updatePlaylist(playlist: Playlist) { context.playlistDAO.update(playlist) } fun getAllPlaylists(): ArrayList { return context.playlistDAO.getAll() as ArrayList } fun getAllGenres(): ArrayList { val genres = context.genresDAO.getAll() as ArrayList genres.sortSafely(config.genreSorting) return genres } fun getGenreTracks(genreId: Long): ArrayList { val tracks = context.tracksDAO.getGenreTracks(genreId) .applyProperFilenames(config.showFilename) tracks.sortSafely(config.trackSorting) return tracks } fun getGenreTracks(genres: List): ArrayList { val tracks = genres.flatMap { context.tracksDAO.getGenreTracks(it.id) } .applyProperFilenames(config.showFilename) tracks.sortSafely(config.trackSorting) return tracks } private fun deleteGenre(id: Long) { context.genresDAO.deleteGenre(id) } fun deleteGenres(genres: List) { genres.forEach { deleteGenre(it.id) } } fun insertGenres(genres: List) { genres.forEach { context.genresDAO.insert(it) } } fun getPlaylistTracks(playlistId: Int): ArrayList { val tracks = context.tracksDAO.getTracksFromPlaylist(playlistId) .applyProperFilenames(config.showFilename) tracks.sortSafely(config.getProperPlaylistSorting(playlistId)) return tracks } fun getPlaylistTrackCount(playlistId: Int): Int { return context.tracksDAO.getTracksCountFromPlaylist(playlistId) } fun updateOrderInPlaylist(playlistId: Int, trackId: Long) { context.tracksDAO.updateOrderInPlaylist(playlistId, trackId) } fun deletePlaylists(playlists: ArrayList) { context.playlistDAO.deletePlaylists(playlists) playlists.forEach { context.tracksDAO.removePlaylistSongs(it.id) } } fun removeInvalidAlbumsArtists() { val tracks = context.tracksDAO.getAll() val albums = context.albumsDAO.getAll() val artists = context.artistDAO.getAll() val invalidAlbums = albums.filter { album -> tracks.none { it.albumId == album.id } } deleteAlbums(invalidAlbums) val invalidArtists = artists.filter { artist -> tracks.none { it.artistId == artist.id } } deleteArtists(invalidArtists) } fun getQueuedTracks(queueItems: List = context.queueDAO.getAll()): ArrayList { val allTracks = getAllTracks().associateBy { it.mediaStoreId } // make sure we fetch the songs in the order they were displayed in val tracks = queueItems.mapNotNull { queueItem -> val track = allTracks[queueItem.trackId] if (track != null) { if (queueItem.isCurrent) { track.flags = track.flags.addBit(FLAG_IS_CURRENT) } track } else { null } } return tracks as ArrayList } /** * Executes [callback] with current track as quickly as possible and then proceeds to load the complete queue with all tracks. */ fun getQueuedTracksLazily(callback: (tracks: List, startIndex: Int, startPositionMs: Long) -> Unit) { ensureBackgroundThread { var queueItems = context.queueDAO.getAll() if (queueItems.isEmpty()) { initQueue() queueItems = context.queueDAO.getAll() } val currentItem = context.queueDAO.getCurrent() if (currentItem == null) { callback(emptyList(), 0, 0) return@ensureBackgroundThread } val currentTrack = getTrack(currentItem.trackId) if (currentTrack == null) { callback(emptyList(), 0, 0) return@ensureBackgroundThread } // immediately return the current track. val startPositionMs = currentItem.lastPosition.seconds.inWholeMilliseconds callback(listOf(currentTrack), 0, startPositionMs) // return the rest of the queued tracks. val queuedTracks = getQueuedTracks(queueItems) val currentIndex = queuedTracks.indexOfFirstOrNull { it.mediaStoreId == currentTrack.mediaStoreId } ?: 0 callback(queuedTracks, currentIndex, startPositionMs) } } fun initQueue(): ArrayList { val tracks = getAllTracks() val queueItems = tracks.mapIndexed { index, mediaItem -> QueueItem(trackId = mediaItem.mediaStoreId, trackOrder = index, isCurrent = index == 0, lastPosition = 0) } resetQueue(queueItems) return tracks } fun resetQueue(items: List, currentTrackId: Long? = null, startPosition: Long? = null) { context.queueDAO.deleteAllItems() context.queueDAO.insertAll(items) if (currentTrackId != null && startPosition != null) { val startPositionSeconds = startPosition.milliseconds.inWholeSeconds.toInt() context.queueDAO.saveCurrentTrackProgress(currentTrackId, startPositionSeconds) } else if (currentTrackId != null) { context.queueDAO.saveCurrentTrack(currentTrackId) } } } private fun Collection.applyProperFilenames(showFilename: Int): ArrayList { return distinctBy { "${it.path}/${it.mediaStoreId}" } .onEach { it.title = it.getProperTitle(showFilename) } as ArrayList }