From 27e39b6c10d3f3d9fc0f0fa5ac9b4d7b323292f4 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sun, 26 May 2024 21:50:34 -0600 Subject: [PATCH 01/20] music: interpret m3u paths as relative & absolute Resolves #673 --- .../org/oxycblt/auxio/music/MusicViewModel.kt | 3 +- .../music/external/ExternalPlaylistManager.kt | 4 +- .../org/oxycblt/auxio/music/external/M3U.kt | 55 +++++++++++++------ 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt index 4c45dfc84..afcb572bf 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -164,7 +164,8 @@ constructor( } val deviceLibrary = musicRepository.deviceLibrary ?: return@launch - val songs = importedPlaylist.paths.mapNotNull(deviceLibrary::findSongByPath) + val songs = importedPlaylist.paths.mapNotNull { + it.firstNotNullOfOrNull(deviceLibrary::findSongByPath) } if (songs.isEmpty()) { logE("No songs found") diff --git a/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt b/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt index 1cf0b0810..a1171efee 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt @@ -76,7 +76,9 @@ data class ExportConfig(val absolute: Boolean, val windowsPaths: Boolean) * @see ExternalPlaylistManager * @see M3U */ -data class ImportedPlaylist(val name: String?, val paths: List) +data class ImportedPlaylist(val name: String?, val paths: List) + +typealias PossiblePaths = List class ExternalPlaylistManagerImpl @Inject diff --git a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt index 11ce3dea1..28a391c27 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt @@ -75,7 +75,7 @@ interface M3U { class M3UImpl @Inject constructor(@ApplicationContext private val context: Context) : M3U { override fun read(stream: InputStream, workingDirectory: Path): ImportedPlaylist? { val reader = BufferedReader(InputStreamReader(stream)) - val paths = mutableListOf() + val paths = mutableListOf() var name: String? = null consumeFile@ while (true) { @@ -112,39 +112,62 @@ class M3UImpl @Inject constructor(@ApplicationContext private val context: Conte } // There is basically no formal specification of file paths in M3U, and it differs - // based on the US that generated it. These are the paths though that I assume most - // programs will generate. - val components = + // based on the programs that generated it. These are the paths though that I assume + // most programs will generate. Note that we do end up proposing multiple + // interpretations + val possibilities = when { path.startsWith('/') -> { // Unix absolute path. Note that we still assume this absolute path is in // the same volume as the M3U file. There's no sane way to map the volume // to the phone's volumes, so this is the only thing we can do. - Components.parseUnix(path) + val absoluteInterpretation = Components.parseUnix(path) + val relativeInterpretation = absoluteInterpretation.absoluteTo(workingDirectory.components) + listOf(absoluteInterpretation, relativeInterpretation) } + path.startsWith("./") -> { // Unix relative path, resolve it - Components.parseUnix(path).absoluteTo(workingDirectory.components) + val absoluteInterpretation = Components.parseUnix(path) + val relativeInterpretation = absoluteInterpretation.absoluteTo(workingDirectory.components) + listOf(relativeInterpretation, absoluteInterpretation) } + path.matches(WINDOWS_VOLUME_PREFIX_REGEX) -> { // Windows absolute path, we should get rid of the volume prefix, but - // otherwise - // the rest should be fine. Again, we have to disregard what the volume - // actually - // is since there's no sane way to map it to the phone's volumes. - Components.parseWindows(path.substring(2)) + // otherwise the rest should be fine. Again, we have to disregard what the + // volume actually is since there's no sane way to map it to the phone's volumes. + val absoluteInterpretation = Components.parseWindows(path.substring(2)) + val relativeInterpretation = absoluteInterpretation.absoluteTo(workingDirectory.components) + listOf(absoluteInterpretation, relativeInterpretation) } + + path.startsWith("\\") -> { + // Weird unix/windows hybrid absolute path that appears sometimes + val absoluteInterpretation = Components.parseWindows(path) + val relativeInterpretation = absoluteInterpretation.absoluteTo(workingDirectory.components) + listOf(absoluteInterpretation, relativeInterpretation) + } + path.startsWith(".\\") -> { - // Windows relative path, we need to remove the .\\ prefix - Components.parseWindows(path).absoluteTo(workingDirectory.components) + // Windows-style relative path + val absoluteInterpretation = Components.parseWindows(path) + val relativeInterpretation = absoluteInterpretation.absoluteTo(workingDirectory.components) + listOf(relativeInterpretation, absoluteInterpretation) } + else -> { - // No clue, parse by all separators and assume it's relative. - Components.parseAny(path).absoluteTo(workingDirectory.components) + // No clue, just go wild and assume all possible combinations. + val unixAbsoluteInterpretation = Components.parseUnix(path) + val unixRelativeInterpretation = unixAbsoluteInterpretation.absoluteTo(workingDirectory.components) + val windowsAbsoluteInterpretation = Components.parseWindows(path) + val windowsRelativeInterpretation = windowsAbsoluteInterpretation.absoluteTo(workingDirectory.components) + listOf(unixRelativeInterpretation, unixAbsoluteInterpretation, + windowsRelativeInterpretation, windowsAbsoluteInterpretation) } } - paths.add(Path(workingDirectory.volume, components)) + paths.add(possibilities.map { Path(workingDirectory.volume, it) }) } return if (paths.isNotEmpty()) { From 248fc89c9bbab039942765110561b0e531a5e45a Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 27 May 2024 20:33:48 +0000 Subject: [PATCH 02/20] actions: run on all branches --- .github/workflows/android.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 763732ee5..61ec47e0c 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -2,9 +2,9 @@ name: Android CI on: push: - branches: [ "dev" ] + branches: [] pull_request: - branches: [ "dev" ] + branches: [] jobs: build: From 1c74f052221a0a1c3730811e7fc479639807b5b5 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 8 Jun 2024 11:57:32 -0600 Subject: [PATCH 03/20] all: fixes/reformat --- .../java/org/oxycblt/auxio/AuxioService.kt | 4 --- .../org/oxycblt/auxio/music/MusicViewModel.kt | 6 ++-- .../org/oxycblt/auxio/music/external/M3U.kt | 36 +++++++++++-------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt index fd44f6a5b..64121e6d1 100644 --- a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt +++ b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt @@ -28,8 +28,6 @@ import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import org.oxycblt.auxio.music.service.IndexerServiceFragment import org.oxycblt.auxio.playback.service.MediaSessionServiceFragment -import org.oxycblt.auxio.tasker.indicateServiceRunning -import org.oxycblt.auxio.tasker.indicateServiceStopped @AndroidEntryPoint class AuxioService : MediaLibraryService(), ForegroundListener { @@ -42,7 +40,6 @@ class AuxioService : MediaLibraryService(), ForegroundListener { super.onCreate() mediaSessionFragment.attach(this, this) indexingFragment.attach(this) - indicateServiceRunning() } override fun onBind(intent: Intent?): IBinder? { @@ -73,7 +70,6 @@ class AuxioService : MediaLibraryService(), ForegroundListener { override fun onDestroy() { super.onDestroy() - indicateServiceStopped() indexingFragment.release() mediaSessionFragment.release() } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt index afcb572bf..99e3fee4d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -164,8 +164,10 @@ constructor( } val deviceLibrary = musicRepository.deviceLibrary ?: return@launch - val songs = importedPlaylist.paths.mapNotNull { - it.firstNotNullOfOrNull(deviceLibrary::findSongByPath) } + val songs = + importedPlaylist.paths.mapNotNull { + it.firstNotNullOfOrNull(deviceLibrary::findSongByPath) + } if (songs.isEmpty()) { logE("No songs found") diff --git a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt index 28a391c27..ddb174d5e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt @@ -122,48 +122,54 @@ class M3UImpl @Inject constructor(@ApplicationContext private val context: Conte // the same volume as the M3U file. There's no sane way to map the volume // to the phone's volumes, so this is the only thing we can do. val absoluteInterpretation = Components.parseUnix(path) - val relativeInterpretation = absoluteInterpretation.absoluteTo(workingDirectory.components) + val relativeInterpretation = + absoluteInterpretation.absoluteTo(workingDirectory.components) listOf(absoluteInterpretation, relativeInterpretation) } - path.startsWith("./") -> { // Unix relative path, resolve it val absoluteInterpretation = Components.parseUnix(path) - val relativeInterpretation = absoluteInterpretation.absoluteTo(workingDirectory.components) + val relativeInterpretation = + absoluteInterpretation.absoluteTo(workingDirectory.components) listOf(relativeInterpretation, absoluteInterpretation) } - path.matches(WINDOWS_VOLUME_PREFIX_REGEX) -> { // Windows absolute path, we should get rid of the volume prefix, but // otherwise the rest should be fine. Again, we have to disregard what the - // volume actually is since there's no sane way to map it to the phone's volumes. + // volume actually is since there's no sane way to map it to the phone's + // volumes. val absoluteInterpretation = Components.parseWindows(path.substring(2)) - val relativeInterpretation = absoluteInterpretation.absoluteTo(workingDirectory.components) + val relativeInterpretation = + absoluteInterpretation.absoluteTo(workingDirectory.components) listOf(absoluteInterpretation, relativeInterpretation) } - path.startsWith("\\") -> { // Weird unix/windows hybrid absolute path that appears sometimes val absoluteInterpretation = Components.parseWindows(path) - val relativeInterpretation = absoluteInterpretation.absoluteTo(workingDirectory.components) + val relativeInterpretation = + absoluteInterpretation.absoluteTo(workingDirectory.components) listOf(absoluteInterpretation, relativeInterpretation) } - path.startsWith(".\\") -> { // Windows-style relative path val absoluteInterpretation = Components.parseWindows(path) - val relativeInterpretation = absoluteInterpretation.absoluteTo(workingDirectory.components) + val relativeInterpretation = + absoluteInterpretation.absoluteTo(workingDirectory.components) listOf(relativeInterpretation, absoluteInterpretation) } - else -> { // No clue, just go wild and assume all possible combinations. val unixAbsoluteInterpretation = Components.parseUnix(path) - val unixRelativeInterpretation = unixAbsoluteInterpretation.absoluteTo(workingDirectory.components) + val unixRelativeInterpretation = + unixAbsoluteInterpretation.absoluteTo(workingDirectory.components) val windowsAbsoluteInterpretation = Components.parseWindows(path) - val windowsRelativeInterpretation = windowsAbsoluteInterpretation.absoluteTo(workingDirectory.components) - listOf(unixRelativeInterpretation, unixAbsoluteInterpretation, - windowsRelativeInterpretation, windowsAbsoluteInterpretation) + val windowsRelativeInterpretation = + windowsAbsoluteInterpretation.absoluteTo(workingDirectory.components) + listOf( + unixRelativeInterpretation, + unixAbsoluteInterpretation, + windowsRelativeInterpretation, + windowsAbsoluteInterpretation) } } From c4a3d5290309824043e3c4b4fc7ee235c67e40ec Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 8 Jun 2024 12:19:54 -0600 Subject: [PATCH 04/20] playback: fix skip backward rewind w/enabled New player setup accidentally broke rewind at beginning behavior when rewind before skip is off. Resolves #785 --- CHANGELOG.md | 2 ++ .../oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3907a19ae..c0dafdad5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ #### What's Fixed - Fixed repeat mode not restoring on startup +- Fixed rewinding not occuring when skipping back at the beginning of the queue if +rewind before skipping was turned off #### What's Changed - For the time being, the media notification will not follow Album Covers or 1:1 Covers settings diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt index 8413738b9..2898e4237 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt @@ -270,8 +270,10 @@ class ExoPlaybackStateHolder( override fun prev() { if (playbackSettings.rewindWithPrev) { player.seekToPrevious() - } else { + } else if (player.hasPreviousMediaItem()) { player.seekToPreviousMediaItem() + } else { + player.seekTo(0) } if (!playbackSettings.rememberPause) { player.play() From 8b2634df4d4f30e83b46ec0abd1457f6427c922a Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 8 Jun 2024 15:06:04 -0600 Subject: [PATCH 05/20] music: handle total absolute m3u paths Some players like generating M3Us with paths starting with /storage/.../..., so I need to handle those too. --- .../org/oxycblt/auxio/music/external/M3U.kt | 163 ++++++++++-------- .../java/org/oxycblt/auxio/music/fs/Fs.kt | 3 + 2 files changed, 98 insertions(+), 68 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt index ddb174d5e..908ced355 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt @@ -20,18 +20,21 @@ package org.oxycblt.auxio.music.external import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.fs.Components +import org.oxycblt.auxio.music.fs.Path +import org.oxycblt.auxio.music.fs.Volume +import org.oxycblt.auxio.music.fs.VolumeManager +import org.oxycblt.auxio.music.metadata.correctWhitespace +import org.oxycblt.auxio.music.resolveNames +import org.oxycblt.auxio.util.logE +import org.oxycblt.auxio.util.unlikelyToBeNull import java.io.BufferedReader import java.io.BufferedWriter import java.io.InputStream import java.io.InputStreamReader import java.io.OutputStream import javax.inject.Inject -import org.oxycblt.auxio.music.Playlist -import org.oxycblt.auxio.music.fs.Components -import org.oxycblt.auxio.music.fs.Path -import org.oxycblt.auxio.music.metadata.correctWhitespace -import org.oxycblt.auxio.music.resolveNames -import org.oxycblt.auxio.util.logE /** * Minimal M3U file format implementation. @@ -72,8 +75,12 @@ interface M3U { } } -class M3UImpl @Inject constructor(@ApplicationContext private val context: Context) : M3U { +class M3UImpl @Inject constructor( + @ApplicationContext private val context: Context, + private val volumeManager: VolumeManager +) : M3U { override fun read(stream: InputStream, workingDirectory: Path): ImportedPlaylist? { + val volumes = volumeManager.getVolumes() val reader = BufferedReader(InputStreamReader(stream)) val paths = mutableListOf() var name: String? = null @@ -112,68 +119,14 @@ class M3UImpl @Inject constructor(@ApplicationContext private val context: Conte } // There is basically no formal specification of file paths in M3U, and it differs - // based on the programs that generated it. These are the paths though that I assume - // most programs will generate. Note that we do end up proposing multiple - // interpretations - val possibilities = - when { - path.startsWith('/') -> { - // Unix absolute path. Note that we still assume this absolute path is in - // the same volume as the M3U file. There's no sane way to map the volume - // to the phone's volumes, so this is the only thing we can do. - val absoluteInterpretation = Components.parseUnix(path) - val relativeInterpretation = - absoluteInterpretation.absoluteTo(workingDirectory.components) - listOf(absoluteInterpretation, relativeInterpretation) - } - path.startsWith("./") -> { - // Unix relative path, resolve it - val absoluteInterpretation = Components.parseUnix(path) - val relativeInterpretation = - absoluteInterpretation.absoluteTo(workingDirectory.components) - listOf(relativeInterpretation, absoluteInterpretation) - } - path.matches(WINDOWS_VOLUME_PREFIX_REGEX) -> { - // Windows absolute path, we should get rid of the volume prefix, but - // otherwise the rest should be fine. Again, we have to disregard what the - // volume actually is since there's no sane way to map it to the phone's - // volumes. - val absoluteInterpretation = Components.parseWindows(path.substring(2)) - val relativeInterpretation = - absoluteInterpretation.absoluteTo(workingDirectory.components) - listOf(absoluteInterpretation, relativeInterpretation) - } - path.startsWith("\\") -> { - // Weird unix/windows hybrid absolute path that appears sometimes - val absoluteInterpretation = Components.parseWindows(path) - val relativeInterpretation = - absoluteInterpretation.absoluteTo(workingDirectory.components) - listOf(absoluteInterpretation, relativeInterpretation) - } - path.startsWith(".\\") -> { - // Windows-style relative path - val absoluteInterpretation = Components.parseWindows(path) - val relativeInterpretation = - absoluteInterpretation.absoluteTo(workingDirectory.components) - listOf(relativeInterpretation, absoluteInterpretation) - } - else -> { - // No clue, just go wild and assume all possible combinations. - val unixAbsoluteInterpretation = Components.parseUnix(path) - val unixRelativeInterpretation = - unixAbsoluteInterpretation.absoluteTo(workingDirectory.components) - val windowsAbsoluteInterpretation = Components.parseWindows(path) - val windowsRelativeInterpretation = - windowsAbsoluteInterpretation.absoluteTo(workingDirectory.components) - listOf( - unixRelativeInterpretation, - unixAbsoluteInterpretation, - windowsRelativeInterpretation, - windowsAbsoluteInterpretation) - } - } + // based on the programs that generated it. I more or less have to consider any possible + // interpretation as valid. + val interpretations = interpretPath(path) + val possibilities = interpretations.flatMap { + expandInterpretation(it, workingDirectory, volumes) + } - paths.add(possibilities.map { Path(workingDirectory.volume, it) }) + paths.add(possibilities) } return if (paths.isNotEmpty()) { @@ -184,6 +137,80 @@ class M3UImpl @Inject constructor(@ApplicationContext private val context: Conte } } + private data class InterpretedPath( + val components: Components, + val likelyAbsolute: Boolean + ) + + private fun interpretPath(path: String): List = + when { + path.startsWith('/') -> + listOf(InterpretedPath(Components.parseUnix(path), true)) + + path.startsWith("./") -> listOf( + InterpretedPath( + Components.parseUnix(path), + false + ) + ) + + path.matches(WINDOWS_VOLUME_PREFIX_REGEX) -> listOf( + InterpretedPath( + Components.parseWindows( + path.substring(2) + ), true + ) + ) + + path.startsWith("\\") -> listOf( + InterpretedPath( + Components.parseWindows(path), + true + ) + ) + + path.startsWith(".\\") -> listOf( + InterpretedPath( + Components.parseWindows(path), + false + ) + ) + + else -> listOf( + InterpretedPath(Components.parseUnix(path), false), + InterpretedPath(Components.parseWindows(path), true) + ) + } + + private fun expandInterpretation( + path: InterpretedPath, + workingDirectory: Path, + volumes: List + ): List { + val absoluteInterpretation = Path(workingDirectory.volume, path.components) + val relativeInterpretation = + Path(workingDirectory.volume, path.components.absoluteTo(workingDirectory.components)) + val volumeExactMatch = volumes.find { it.components?.contains(path.components) == true } + val volumeInterpretation = volumeExactMatch?.let { + val components = unlikelyToBeNull(volumeExactMatch.components) + .containing(path.components) + Path(volumeExactMatch, components) + } + return if (path.likelyAbsolute) { + listOfNotNull( + volumeInterpretation, + absoluteInterpretation, + relativeInterpretation + ) + } else { + listOfNotNull( + relativeInterpretation, + volumeInterpretation, + absoluteInterpretation + ) + } + } + override fun write( playlist: Playlist, outputStream: OutputStream, diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt index 2639ec207..00f486a3c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt @@ -158,6 +158,9 @@ value class Components private constructor(val components: List) { return components == other.components.take(components.size) } + fun containing(other: Components) = + Components(components + other.components.drop(components.size)) + companion object { /** * Parses a path string into a [Components] instance by the unix path separator (/). From d906b87d766d6c2acfacbdf5153a0ec12be2ac0c Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 8 Jun 2024 19:20:18 -0600 Subject: [PATCH 06/20] all: reformat --- .../org/oxycblt/auxio/music/external/M3U.kt | 95 ++++++------------- 1 file changed, 30 insertions(+), 65 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt index 908ced355..211f6cea6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt @@ -20,6 +20,12 @@ package org.oxycblt.auxio.music.external import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.BufferedReader +import java.io.BufferedWriter +import java.io.InputStream +import java.io.InputStreamReader +import java.io.OutputStream +import javax.inject.Inject import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.fs.Components import org.oxycblt.auxio.music.fs.Path @@ -29,12 +35,6 @@ import org.oxycblt.auxio.music.metadata.correctWhitespace import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.unlikelyToBeNull -import java.io.BufferedReader -import java.io.BufferedWriter -import java.io.InputStream -import java.io.InputStreamReader -import java.io.OutputStream -import javax.inject.Inject /** * Minimal M3U file format implementation. @@ -75,7 +75,9 @@ interface M3U { } } -class M3UImpl @Inject constructor( +class M3UImpl +@Inject +constructor( @ApplicationContext private val context: Context, private val volumeManager: VolumeManager ) : M3U { @@ -122,9 +124,8 @@ class M3UImpl @Inject constructor( // based on the programs that generated it. I more or less have to consider any possible // interpretation as valid. val interpretations = interpretPath(path) - val possibilities = interpretations.flatMap { - expandInterpretation(it, workingDirectory, volumes) - } + val possibilities = + interpretations.flatMap { expandInterpretation(it, workingDirectory, volumes) } paths.add(possibilities) } @@ -137,49 +138,20 @@ class M3UImpl @Inject constructor( } } - private data class InterpretedPath( - val components: Components, - val likelyAbsolute: Boolean - ) + private data class InterpretedPath(val components: Components, val likelyAbsolute: Boolean) private fun interpretPath(path: String): List = when { - path.startsWith('/') -> - listOf(InterpretedPath(Components.parseUnix(path), true)) - - path.startsWith("./") -> listOf( - InterpretedPath( - Components.parseUnix(path), - false - ) - ) - - path.matches(WINDOWS_VOLUME_PREFIX_REGEX) -> listOf( - InterpretedPath( - Components.parseWindows( - path.substring(2) - ), true - ) - ) - - path.startsWith("\\") -> listOf( - InterpretedPath( - Components.parseWindows(path), - true - ) - ) - - path.startsWith(".\\") -> listOf( - InterpretedPath( - Components.parseWindows(path), - false - ) - ) - - else -> listOf( - InterpretedPath(Components.parseUnix(path), false), - InterpretedPath(Components.parseWindows(path), true) - ) + path.startsWith('/') -> listOf(InterpretedPath(Components.parseUnix(path), true)) + path.startsWith("./") -> listOf(InterpretedPath(Components.parseUnix(path), false)) + path.matches(WINDOWS_VOLUME_PREFIX_REGEX) -> + listOf(InterpretedPath(Components.parseWindows(path.substring(2)), true)) + path.startsWith("\\") -> listOf(InterpretedPath(Components.parseWindows(path), true)) + path.startsWith(".\\") -> listOf(InterpretedPath(Components.parseWindows(path), false)) + else -> + listOf( + InterpretedPath(Components.parseUnix(path), false), + InterpretedPath(Components.parseWindows(path), true)) } private fun expandInterpretation( @@ -191,23 +163,16 @@ class M3UImpl @Inject constructor( val relativeInterpretation = Path(workingDirectory.volume, path.components.absoluteTo(workingDirectory.components)) val volumeExactMatch = volumes.find { it.components?.contains(path.components) == true } - val volumeInterpretation = volumeExactMatch?.let { - val components = unlikelyToBeNull(volumeExactMatch.components) - .containing(path.components) - Path(volumeExactMatch, components) - } + val volumeInterpretation = + volumeExactMatch?.let { + val components = + unlikelyToBeNull(volumeExactMatch.components).containing(path.components) + Path(volumeExactMatch, components) + } return if (path.likelyAbsolute) { - listOfNotNull( - volumeInterpretation, - absoluteInterpretation, - relativeInterpretation - ) + listOfNotNull(volumeInterpretation, absoluteInterpretation, relativeInterpretation) } else { - listOfNotNull( - relativeInterpretation, - volumeInterpretation, - absoluteInterpretation - ) + listOfNotNull(relativeInterpretation, volumeInterpretation, absoluteInterpretation) } } From 643defd9e4c17f6edbfb1cb8fb9b5816fb3e6895 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 8 Jun 2024 19:21:06 -0600 Subject: [PATCH 07/20] playback: fix play song by itself Accidental misup led to it playing from all songs instead --- .../java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt index 82685f051..f53745632 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt @@ -91,7 +91,8 @@ constructor( override val shuffled: Boolean ) : PlaybackCommand - override fun song(song: Song, shuffle: ShuffleMode) = newCommand(song, shuffle) + override fun song(song: Song, shuffle: ShuffleMode) = + newCommand(song, null, listOf(song), shuffle) override fun songFromAll(song: Song, shuffle: ShuffleMode) = newCommand(song, shuffle) @@ -105,7 +106,7 @@ constructor( newCommand(song, genre, song.genres, listSettings.genreSongSort, shuffle) override fun songFromPlaylist(song: Song, playlist: Playlist, shuffle: ShuffleMode) = - newCommand(song, playlist, playlist.songs, listSettings.playlistSort, shuffle) + newCommand(song, playlist, playlist.songs, shuffle) override fun all(shuffle: ShuffleMode) = newCommand(null, shuffle) From 111cb9688f59bca20d0a7b0255d6a72f48b51fad Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 8 Jun 2024 21:44:15 -0600 Subject: [PATCH 08/20] tasker: completely remove --- app/src/main/java/org/oxycblt/auxio/AuxioService.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt index fd44f6a5b..64121e6d1 100644 --- a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt +++ b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt @@ -28,8 +28,6 @@ import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import org.oxycblt.auxio.music.service.IndexerServiceFragment import org.oxycblt.auxio.playback.service.MediaSessionServiceFragment -import org.oxycblt.auxio.tasker.indicateServiceRunning -import org.oxycblt.auxio.tasker.indicateServiceStopped @AndroidEntryPoint class AuxioService : MediaLibraryService(), ForegroundListener { @@ -42,7 +40,6 @@ class AuxioService : MediaLibraryService(), ForegroundListener { super.onCreate() mediaSessionFragment.attach(this, this) indexingFragment.attach(this) - indicateServiceRunning() } override fun onBind(intent: Intent?): IBinder? { @@ -73,7 +70,6 @@ class AuxioService : MediaLibraryService(), ForegroundListener { override fun onDestroy() { super.onDestroy() - indicateServiceStopped() indexingFragment.release() mediaSessionFragment.release() } From b0703b4d0e3d10e9a723d0ea77e00fd89580f41d Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 8 Jun 2024 21:44:48 -0600 Subject: [PATCH 09/20] playback: fix widget not resetting on service end --- .../playback/service/PlaybackActionHandler.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackActionHandler.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackActionHandler.kt index 8597770de..6d0c1fbeb 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackActionHandler.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackActionHandler.kt @@ -49,13 +49,15 @@ constructor( @ApplicationContext private val context: Context, private val playbackManager: PlaybackStateManager, private val playbackSettings: PlaybackSettings, - private val systemReceiver: SystemPlaybackReceiver + private val widgetComponent: WidgetComponent ) : PlaybackStateManager.Listener, PlaybackSettings.Listener { interface Callback { fun onCustomLayoutChanged(layout: List) } + private val systemReceiver = + SystemPlaybackReceiver(playbackManager, playbackSettings, widgetComponent) private var callback: Callback? = null fun attach(callback: Callback) { @@ -71,6 +73,7 @@ constructor( playbackManager.removeListener(this) playbackSettings.unregisterListener(this) context.unregisterReceiver(systemReceiver) + widgetComponent.release() } fun withCommands(commands: SessionCommands) = @@ -180,12 +183,10 @@ object PlaybackActions { * A [BroadcastReceiver] for receiving playback-specific [Intent]s from the system that require an * active [IntentFilter] to be registered. */ -class SystemPlaybackReceiver -@Inject -constructor( - val playbackManager: PlaybackStateManager, - val playbackSettings: PlaybackSettings, - val widgetComponent: WidgetComponent +class SystemPlaybackReceiver( + private val playbackManager: PlaybackStateManager, + private val playbackSettings: PlaybackSettings, + private val widgetComponent: WidgetComponent ) : BroadcastReceiver() { private var initialHeadsetPlugEventHandled = false From d117f160810510a50c052b71c0a3ac27ff9ff13d Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sun, 9 Jun 2024 13:13:54 -0600 Subject: [PATCH 10/20] image: prefer exoplayer over aosp covers Will actually handle files with multiple covers. Could lead to more performance concerns, but that's also the same with AOSP too. --- .../java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt index d28979b6b..f1be38db3 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt @@ -157,8 +157,8 @@ constructor( } private suspend fun extractQualityCover(cover: Cover.Embedded) = - extractAospMetadataCover(cover) - ?: extractExoplayerCover(cover) ?: extractMediaStoreCover(cover) + extractExoplayerCover(cover) + ?: extractAospMetadataCover(cover) ?: extractMediaStoreCover(cover) private fun extractAospMetadataCover(cover: Cover.Embedded): InputStream? = MediaMetadataRetriever().run { From dbe7bdf1c3e19f0c48ae8f099d93c0bade55691f Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sun, 9 Jun 2024 16:50:04 -0600 Subject: [PATCH 11/20] music: fix m3u volume processing --- app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt index 00f486a3c..9f3cbff3b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt @@ -158,8 +158,7 @@ value class Components private constructor(val components: List) { return components == other.components.take(components.size) } - fun containing(other: Components) = - Components(components + other.components.drop(components.size)) + fun containing(other: Components) = Components(other.components.drop(components.size)) companion object { /** From cff700231e04e25745e77dd0bcc86207f5b6ac7c Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sun, 9 Jun 2024 16:52:16 -0600 Subject: [PATCH 12/20] playback: fix android auto queue crash --- .../auxio/playback/service/MediaSessionPlayer.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionPlayer.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionPlayer.kt index 6b56d334a..493969467 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionPlayer.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionPlayer.kt @@ -173,6 +173,14 @@ class MediaSessionPlayer( playbackManager.repeatMode(appRepeatMode) } + override fun seekToDefaultPosition() { + playbackManager.seekTo(0) + } + + override fun seekToDefaultPosition(mediaItemIndex: Int) { + playbackManager.goto(mediaItemIndex) + } + override fun seekToNext() = playbackManager.next() override fun seekToNextMediaItem() = playbackManager.next() @@ -278,10 +286,6 @@ class MediaSessionPlayer( override fun setPlaybackSpeed(speed: Float) = notAllowed() - override fun seekToDefaultPosition() = notAllowed() - - override fun seekToDefaultPosition(mediaItemIndex: Int) = notAllowed() - override fun seekForward() = notAllowed() override fun seekBack() = notAllowed() From a9e7ae398c0b5cd512d35bc1e9a298ff50f25bf9 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sun, 9 Jun 2024 16:52:27 -0600 Subject: [PATCH 13/20] playback: fix service memory leak --- .../auxio/playback/service/MediaSessionServiceFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionServiceFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionServiceFragment.kt index fb884cee3..69f2aab6d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionServiceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionServiceFragment.kt @@ -129,7 +129,7 @@ constructor( fun release() { waitJob.cancel() - mediaSession.release() + mediaItemBrowser.release() actionHandler.release() exoHolder.release() playbackManager.removeListener(this) From 4f71dba90e02f446e4cae2c4f18922d58fef702b Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sun, 9 Jun 2024 19:40:42 -0600 Subject: [PATCH 14/20] playback: fix various android auto issues - Broken queue - Unusable item details --- .../auxio/music/service/MediaItemBrowser.kt | 28 +++++++++++++++---- .../music/service/MediaItemTranslation.kt | 8 +++--- .../playback/service/MediaSessionPlayer.kt | 26 ++++++----------- media | 2 +- 4 files changed, 37 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemBrowser.kt b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemBrowser.kt index 63b68925d..0ca607167 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemBrowser.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemBrowser.kt @@ -19,6 +19,9 @@ package org.oxycblt.auxio.music.service import android.content.Context +import android.os.Bundle +import androidx.annotation.StringRes +import androidx.media.utils.MediaConstants import androidx.media3.common.MediaItem import androidx.media3.session.MediaSession.ControllerInfo import dagger.hilt.android.qualifiers.ApplicationContext @@ -29,6 +32,7 @@ import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.async +import org.oxycblt.auxio.R import org.oxycblt.auxio.list.ListSettings import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.music.Album @@ -213,27 +217,41 @@ constructor( return when (val item = musicRepository.find(uid)) { is Album -> { val songs = listSettings.albumSongSort.songs(item.songs) - songs.map { it.toMediaItem(context, item) } + songs.map { it.toMediaItem(context, item).withHeader(R.string.lbl_songs) } } is Artist -> { val albums = ARTIST_ALBUMS_SORT.albums(item.explicitAlbums + item.implicitAlbums) val songs = listSettings.artistSongSort.songs(item.songs) - albums.map { it.toMediaItem(context) } + songs.map { it.toMediaItem(context, item) } + albums.map { it.toMediaItem(context).withHeader(R.string.lbl_albums) } + + songs.map { it.toMediaItem(context, item).withHeader(R.string.lbl_songs) } } is Genre -> { val artists = GENRE_ARTISTS_SORT.artists(item.artists) val songs = listSettings.genreSongSort.songs(item.songs) - artists.map { it.toMediaItem(context) } + - songs.map { it.toMediaItem(context, null) } + artists.map { it.toMediaItem(context).withHeader(R.string.lbl_artists) } + + songs.map { it.toMediaItem(context, null).withHeader(R.string.lbl_songs) } } is Playlist -> { - item.songs.map { it.toMediaItem(context, item) } + item.songs.map { it.toMediaItem(context, item).withHeader(R.string.lbl_songs) } } is Song, null -> return null } } + private fun MediaItem.withHeader(@StringRes res: Int): MediaItem { + val oldExtras = mediaMetadata.extras ?: Bundle() + val newExtras = + Bundle(oldExtras).apply { + putString( + MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, + context.getString(res)) + } + return buildUpon() + .setMediaMetadata(mediaMetadata.buildUpon().setExtras(newExtras).build()) + .build() + } + private fun getCategorySize( category: MediaSessionUID.Category, musicRepository: MusicRepository diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt index bde25c1b5..13fdb581e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt @@ -103,7 +103,7 @@ fun Album.toMediaItem(context: Context): MediaItem { .setReleaseMonth(dates?.min?.month) .setReleaseDay(dates?.min?.day) .setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM) - .setIsPlayable(true) + .setIsPlayable(false) .setIsBrowsable(true) .setArtworkUri(cover.single.mediaStoreCoverUri) .setExtras(Bundle().apply { putString("uid", mediaSessionUID.toString()) }) @@ -133,7 +133,7 @@ fun Artist.toMediaItem(context: Context): MediaItem { context.getString(R.string.def_song_count) })) .setMediaType(MediaMetadata.MEDIA_TYPE_ARTIST) - .setIsPlayable(true) + .setIsPlayable(false) .setIsBrowsable(true) .setGenre(genres.resolveNames(context)) .setArtworkUri(cover.single.mediaStoreCoverUri) @@ -157,7 +157,7 @@ fun Genre.toMediaItem(context: Context): MediaItem { context.getString(R.string.def_song_count) }) .setMediaType(MediaMetadata.MEDIA_TYPE_GENRE) - .setIsPlayable(true) + .setIsPlayable(false) .setIsBrowsable(true) .setArtworkUri(cover.single.mediaStoreCoverUri) .setExtras(Bundle().apply { putString("uid", mediaSessionUID.toString()) }) @@ -180,7 +180,7 @@ fun Playlist.toMediaItem(context: Context): MediaItem { context.getString(R.string.def_song_count) }) .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST) - .setIsPlayable(true) + .setIsPlayable(false) .setIsBrowsable(true) .setArtworkUri(cover?.single?.mediaStoreCoverUri) .setExtras(Bundle().apply { putString("uid", mediaSessionUID.toString()) }) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionPlayer.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionPlayer.kt index 493969467..f5ea4215c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionPlayer.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionPlayer.kt @@ -173,12 +173,13 @@ class MediaSessionPlayer( playbackManager.repeatMode(appRepeatMode) } - override fun seekToDefaultPosition() { - playbackManager.seekTo(0) - } - override fun seekToDefaultPosition(mediaItemIndex: Int) { - playbackManager.goto(mediaItemIndex) + val indices = unscrambleQueueIndices() + val fakeIndex = indices.indexOf(mediaItemIndex) + if (fakeIndex < 0) { + return + } + playbackManager.goto(fakeIndex) } override fun seekToNext() = playbackManager.next() @@ -191,18 +192,9 @@ class MediaSessionPlayer( override fun seekTo(positionMs: Long) = playbackManager.seekTo(positionMs) - override fun seekTo(mediaItemIndex: Int, positionMs: Long) { - val indices = unscrambleQueueIndices() - val fakeIndex = indices.indexOf(mediaItemIndex) - if (fakeIndex < 0) { - return - } - playbackManager.goto(fakeIndex) - if (positionMs == C.TIME_UNSET) { - return - } - playbackManager.seekTo(positionMs) - } + override fun seekTo(mediaItemIndex: Int, positionMs: Long) = notAllowed() + + override fun seekToDefaultPosition() = notAllowed() override fun addMediaItems(index: Int, mediaItems: MutableList) { val deviceLibrary = musicRepository.deviceLibrary ?: return diff --git a/media b/media index 00124cbac..9fc2401b8 160000 --- a/media +++ b/media @@ -1 +1 @@ -Subproject commit 00124cbac493c06a24e19b01893946bdaf8faf58 +Subproject commit 9fc2401b8fdc2b23905402462e775c6db4e1527f From ba0d2cd879de6b55e230800616d1fda93e34d365 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sun, 9 Jun 2024 20:25:33 -0600 Subject: [PATCH 15/20] playback: add tab icons --- .../music/service/MediaItemTranslation.kt | 62 +++++++++++++++--- .../res/drawable-hdpi/ic_album_bitmap_24.png | Bin 0 -> 676 bytes .../res/drawable-hdpi/ic_artist_bitmap_24.png | Bin 0 -> 531 bytes .../res/drawable-hdpi/ic_genre_bitmap_24.png | Bin 0 -> 432 bytes .../drawable-hdpi/ic_playlist_bitmap_24.png | Bin 0 -> 259 bytes .../res/drawable-hdpi/ic_song_bitmap_24.png | Bin 0 -> 291 bytes .../res/drawable-mdpi/ic_album_bitmap_24.png | Bin 0 -> 375 bytes .../res/drawable-mdpi/ic_artist_bitmap_24.png | Bin 0 -> 313 bytes .../res/drawable-mdpi/ic_genre_bitmap_24.png | Bin 0 -> 241 bytes .../drawable-mdpi/ic_playlist_bitmap_24.png | Bin 0 -> 182 bytes .../res/drawable-mdpi/ic_song_bitmap_24.png | Bin 0 -> 175 bytes .../res/drawable-xhdpi/ic_album_bitmap_24.png | Bin 0 -> 894 bytes .../drawable-xhdpi/ic_artist_bitmap_24.png | Bin 0 -> 642 bytes .../res/drawable-xhdpi/ic_genre_bitmap_24.png | Bin 0 -> 422 bytes .../drawable-xhdpi/ic_playlist_bitmap_24.png | Bin 0 -> 328 bytes .../res/drawable-xhdpi/ic_song_bitmap_24.png | Bin 0 -> 326 bytes .../drawable-xxhdpi/ic_album_bitmap_24.png | Bin 0 -> 1644 bytes .../drawable-xxhdpi/ic_artist_bitmap_24.png | Bin 0 -> 1092 bytes .../drawable-xxhdpi/ic_genre_bitmap_24.png | Bin 0 -> 812 bytes .../drawable-xxhdpi/ic_playlist_bitmap_24.png | Bin 0 -> 559 bytes .../res/drawable-xxhdpi/ic_song_bitmap_24.png | Bin 0 -> 560 bytes .../drawable-xxxhdpi/ic_album_bitmap_24.png | Bin 0 -> 1774 bytes .../drawable-xxxhdpi/ic_artist_bitmap_24.png | Bin 0 -> 1150 bytes .../drawable-xxxhdpi/ic_genre_bitmap_24.png | Bin 0 -> 784 bytes .../ic_playlist_bitmap_24.png | Bin 0 -> 672 bytes .../drawable-xxxhdpi/ic_song_bitmap_24.png | Bin 0 -> 687 bytes 26 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 app/src/main/res/drawable-hdpi/ic_album_bitmap_24.png create mode 100644 app/src/main/res/drawable-hdpi/ic_artist_bitmap_24.png create mode 100644 app/src/main/res/drawable-hdpi/ic_genre_bitmap_24.png create mode 100644 app/src/main/res/drawable-hdpi/ic_playlist_bitmap_24.png create mode 100644 app/src/main/res/drawable-hdpi/ic_song_bitmap_24.png create mode 100644 app/src/main/res/drawable-mdpi/ic_album_bitmap_24.png create mode 100644 app/src/main/res/drawable-mdpi/ic_artist_bitmap_24.png create mode 100644 app/src/main/res/drawable-mdpi/ic_genre_bitmap_24.png create mode 100644 app/src/main/res/drawable-mdpi/ic_playlist_bitmap_24.png create mode 100644 app/src/main/res/drawable-mdpi/ic_song_bitmap_24.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_album_bitmap_24.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_artist_bitmap_24.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_genre_bitmap_24.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_playlist_bitmap_24.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_song_bitmap_24.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_album_bitmap_24.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_artist_bitmap_24.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_genre_bitmap_24.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_playlist_bitmap_24.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_song_bitmap_24.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_album_bitmap_24.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_artist_bitmap_24.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_genre_bitmap_24.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_playlist_bitmap_24.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_song_bitmap_24.png diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt index 13fdb581e..9a5bb53c2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt @@ -19,10 +19,15 @@ package org.oxycblt.auxio.music.service import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.os.Bundle +import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import androidx.media.utils.MediaConstants import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata +import java.io.ByteArrayOutputStream import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.music.Album @@ -37,14 +42,27 @@ import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.util.getPlural fun MediaSessionUID.Category.toMediaItem(context: Context): MediaItem { + // TODO: Make custom overflow menu for compat + val style = + Bundle().apply { + putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM) + } val metadata = MediaMetadata.Builder() .setTitle(context.getString(nameRes)) .setIsPlayable(false) .setIsBrowsable(true) .setMediaType(mediaType) - .build() - return MediaItem.Builder().setMediaId(toString()).setMediaMetadata(metadata).build() + .setExtras(style) + if (bitmapRes != null) { + val data = ByteArrayOutputStream() + BitmapFactory.decodeResource(context.resources, bitmapRes) + .compress(Bitmap.CompressFormat.PNG, 100, data) + metadata.setArtworkData(data.toByteArray(), MediaMetadata.PICTURE_TYPE_FILE_ICON) + } + return MediaItem.Builder().setMediaId(toString()).setMediaMetadata(metadata.build()).build() } fun Song.toMediaItem(context: Context, parent: MusicParent?): MediaItem { @@ -205,14 +223,38 @@ fun MediaItem.toSong(deviceLibrary: DeviceLibrary): Song? { } sealed interface MediaSessionUID { - enum class Category(val id: String, @StringRes val nameRes: Int, val mediaType: Int?) : - MediaSessionUID { - ROOT("root", R.string.info_app_name, null), - SONGS("songs", R.string.lbl_songs, MediaMetadata.MEDIA_TYPE_MUSIC), - ALBUMS("albums", R.string.lbl_albums, MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS), - ARTISTS("artists", R.string.lbl_artists, MediaMetadata.MEDIA_TYPE_FOLDER_ARTISTS), - GENRES("genres", R.string.lbl_genres, MediaMetadata.MEDIA_TYPE_FOLDER_GENRES), - PLAYLISTS("playlists", R.string.lbl_playlists, MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS); + enum class Category( + val id: String, + @StringRes val nameRes: Int, + @DrawableRes val bitmapRes: Int?, + val mediaType: Int? + ) : MediaSessionUID { + ROOT("root", R.string.info_app_name, null, null), + SONGS( + "songs", + R.string.lbl_songs, + R.drawable.ic_song_bitmap_24, + MediaMetadata.MEDIA_TYPE_MUSIC), + ALBUMS( + "albums", + R.string.lbl_albums, + R.drawable.ic_album_bitmap_24, + MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS), + ARTISTS( + "artists", + R.string.lbl_artists, + R.drawable.ic_artist_bitmap_24, + MediaMetadata.MEDIA_TYPE_FOLDER_ARTISTS), + GENRES( + "genres", + R.string.lbl_genres, + R.drawable.ic_genre_bitmap_24, + MediaMetadata.MEDIA_TYPE_FOLDER_GENRES), + PLAYLISTS( + "playlists", + R.string.lbl_playlists, + R.drawable.ic_playlist_bitmap_24, + MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS); override fun toString() = "$ID_CATEGORY:$id" diff --git a/app/src/main/res/drawable-hdpi/ic_album_bitmap_24.png b/app/src/main/res/drawable-hdpi/ic_album_bitmap_24.png new file mode 100644 index 0000000000000000000000000000000000000000..c25d1465bb0b14a71d84918535366c314adb091f GIT binary patch literal 676 zcmV;V0$crwP)<;T&hfF=cgFUen|sDnhnX#jJK{&NC;kzyGimeeAZwOl|EWRduf#p^ zcOLxT5TZfbrEm1R6J+>S{8brrR9=X?(RXafWKsMa%=zbEEUng>xFRlO(&ia4G;t+) zm5Xtlkw8cBNc@}4`28wwi7%o)0!`L_4<2}qKPh$k6pUD@mr*wi^vQj_*Fn^^!cxwZ zWyav0xDa)tg1*_KObU*cQK#C*@a)L?2##61{i-f}9!nniN|X_r+uJ zB)8Y2t^hAfIaU0-QF9BHHa+$Z#8k*u)BrtyEuJ~!$Wt`2AJFsd?2V4G25+7UcAS{9 zZB!Eji9g)ngNUXvDLlpVtYNH9)bwHLI4FjDF%GPKWg<^wf1u|doMv#vSvrS`aIF&G zs0=!c8x@~7F5ZjrJ`}uk!j(##q>Te!iHDiAV|>5j!;W6!moeUl-QFh}rEaN`;($zK zK9hEgA6IXeTjd!?`Q}J+Z;A>#UeF3` zmaeeC>X&@pKS|lBz6dl~OCRqs*(D$3NwHF8nPL14TDF_dAlf{mFevTmeFk}0%Bhm* zlW;m8iXN7#uWPgLDeYlt^Xyn!{+^`+nZY4#o)JUSzUe*VDd0crP+S}ed4}x(0000< KMNUMnLSTaB3_*4P literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_artist_bitmap_24.png b/app/src/main/res/drawable-hdpi/ic_artist_bitmap_24.png new file mode 100644 index 0000000000000000000000000000000000000000..7a4ec1d2444c353938c1df8d8fd96a30e3c93510 GIT binary patch literal 531 zcmV+u0_^>XP)Ap1wn$nop0#@enVFltyPL@b1qB5K{hvf+92|jnh2u%r*Kna}3z!Ba@a@34 zS$f>Oh6&Ajz?0`;49}p~&1;y@Y{qJS1^N|^&3wVlt637giac0W1~}*OtyzsXtoaRV ze$~#=c&hrb5FC?exlPd*FyZmpD|9J;E5vct6-rw_mQyfp=<#t6J$Sr^M|wPYsp`i< z@Y9p<;qh_lr=00oLh#&^P;`Aph>d~+@T%}08h3q0wbp0Ckdz;1zVBhz zSFh@-FVW^)1WOW~h_a=}?@llR*1;|~0k_I2*wYwSFK6d!eq#O9LSj~;eRB=Iq;lz9 z=(&+-KV{u|i9$jS%z;C2Cx4r7UE{4_(`i@>JD^Q#t>_$kuoru>cb9pXJUElef6dd9 zSI@i_FtK}yniRkw=uo#FTL~oF7S|n|3W9y=5)m9LCizI>;Z6X4*cbd+0=fW`k zf3^7MVsH<9D5K*!-s7*NQ=OQYvNpk@jcDHiT&owbqaFa63Z#8k4MY~(M=x;S$ zQo@tTHGLE1no1;LW}_w+jN0I}AVefhO;IqLr|?(e%>+qTQ;}eNU_$xUgcn}|r{Eq; zN$K;T%T`E5@sat+`{v(P!#IhbPdCza56TCre}9t1;ep2KLSm;jkB@caTA3a z_=<_h8RF4!XV4V{%W atNH}xJ-DD@A%!#m0000-OL+cZHC1c40^ z4e+^uL+zUB{u!n44{o%O2cpoY=xS`MkONZw002ov JPDHLkV1oK>a#8>Q literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_song_bitmap_24.png b/app/src/main/res/drawable-hdpi/ic_song_bitmap_24.png new file mode 100644 index 0000000000000000000000000000000000000000..9eb67934bbdc24017eef30d7e418c90402bb32a6 GIT binary patch literal 291 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBUU|AWhE&A8o$AQfWFX-BkSXR! z1@mLK{6=kYIhTBwp2D}2DwcmV-*my>TXE(_y=SWWic{v>D{cvy`uGp0QqifClEDE# z&H%|BHfrfU4?oSAW7v8+x#*aPUO!N9Ma`DOO>+#F-k9z2kooC{#-bk%mR-CphuHES zFv>kF+wuK&V?fAZ**$gB_{&t;vJ&Mb(^S&0I-ZoW+hL*l9LQ+(UjAf{mB!BEms941 zyQkGzH`_kHIx#i>#j(oLl$+)y&-Pdu)lY9t51V7|`005-M7Cvm+aCW{dzbjX+7MQJ mPUh#o>8yuK51+U6(~O(nzJ0d< literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_album_bitmap_24.png b/app/src/main/res/drawable-mdpi/ic_album_bitmap_24.png new file mode 100644 index 0000000000000000000000000000000000000000..1047e230c8d87418588c3dc9a40b29eaf53be049 GIT binary patch literal 375 zcmV--0f_#IP)o5{OPbK*3XB`zA- zmJ8#lY6sI=Ks>2AavJOH7kg29ZRPU3!qkqYwQw6eI_<6-WG_+nL(YCp^L}Uqj8a3t zX%(;0??8S!=`GD}8~p^5w(DA&!{ZIHcFi$0dX{4s9&XnLY07o~AC8CQ{AK<0SpXGNuEXV0R9GIV$RqTC^IIPqL7kzI@WgA$+fu@GPV(7hLP1LVc$Tvjb#$*K$Ql00000 LNkvXXu0mjf1^0(S literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_genre_bitmap_24.png b/app/src/main/res/drawable-mdpi/ic_genre_bitmap_24.png new file mode 100644 index 0000000000000000000000000000000000000000..d5034c0d851153d009c18af7c3951cd2396ac345 GIT binary patch literal 241 zcmVgpJ(l z9NmwK6U9GY2QBShFo@k0T}U1`b}tBorV8FQ=bBr)>kT3^cu6coG(0l`~Np1cYE0l|}pK*0YX;whjcEO^K%|Ecnd z*PT3Sy4UI1+2G0+6b#e#>b;%m&)JogC6-uXW&l_m&SF?GZ$E*edDB1$gKQf+}C`g z0p8Ew6cT5$pC7Xb;}*gb<#oEpHfrMdQ1WezJ3h>C&G?~^jJ!1NwIroB#V}U=NakA8 zxIswCO2to%&sEd&0LA4(Nw^15JtWqhjIp`cgt)I7(kZ34W{s_AEp^?C#7 znY=aMuz?L9coQ+>QyRVajPxV)O>lNq_tyRu`#W1J-oA z@aguO@h#jTaJE;u*CXuQ1IQi91qF5%6hK z_cnW~i?H?`_|w*6YovBZeIl4sVp!At(<*$Y7z4R#Rs-QrzNPp~#%!uz2K+&C0LPeZ z)pY>h633va0|@=PO^s*j7XjaPME#*^Ixx|x1Xbr@_<+@-T3H>~*7i)ar>f}y-FbLR zH5t}+QYfgEC+eF4-vCDKm1;Ubx8GG45o^0H6qNZMJ{^<1*7nS6_051UrcryNnhrF9 zwnw15Rz&`tw|QM?&%BGC3e{_S2Q2}3pg-WJ1U*qr2BvqRclg(Zc&+W3EnVnVH67p) z4B~~KMq3xM+OeKhT`O zr`UX;;Wh772W$jpJsb U?VH8hAOHXW07*qoM6N<$f)wev00000 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_artist_bitmap_24.png b/app/src/main/res/drawable-xhdpi/ic_artist_bitmap_24.png new file mode 100644 index 0000000000000000000000000000000000000000..880afde6446edbeb9bb3cf9dba96e7f93f5fbaff GIT binary patch literal 642 zcmV-|0)737P)Nkl`w&uZ5GGfBb=+*j~Js=HQ4 zQRZ~p(bo`~FM(t5FEe0Qn0qQ6zLicwp-pfi;r;!OYQ$fLv|AE>Z3;8Y5Ppd|A`c0FYR}^TrQW( c<@(?B1nD=xwcIFRtpET307*qoM6N<$f`^$fY5)KL literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_genre_bitmap_24.png b/app/src/main/res/drawable-xhdpi/ic_genre_bitmap_24.png new file mode 100644 index 0000000000000000000000000000000000000000..7ac11d34d6934ddc3f5e3b0c9aa14cddbabccbdd GIT binary patch literal 422 zcmV;X0a^ZuP)7oV%ECtwtn7S*fDfQxqmaVZTCfsKX+Ve=6g$Bxg&^de)_=HJS%3C& z7Yk>E`QV3zxmmv1+k=q7AP9mWP*O@m z1wG{@3+^>%4SC6g`%PI>{temYJYbr6(Mg$$Xf!T>xyVC{^W(! z?+y5r&vXpoTRzh=guOfjHj!iG9$9E)J(3zdTMpquUW8^UREyMxm7a+Qc{%x2^5qae z-h>NMb8T3*EhFwX2U1g#yh+XVkjT2MO}XD3NPUpx_g&W^%9^jgKOnS4g3FG&y8-q| z@R90=RG*i1Q}lEnNbr*C&r~0mdzheNN60J5N%piZC(rVq$S&{q=Ro43BHkzVr=Nk$ zSw(PFZjlY%$CT83z%LcqGIDLk?&LEfh8!SQ$Xuo6N1Uuh-2To7K@gbo1^MOZyy)kd QMgRZ+07*qoM6N<$g3)Znx&QzG literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_playlist_bitmap_24.png b/app/src/main/res/drawable-xhdpi/ic_playlist_bitmap_24.png new file mode 100644 index 0000000000000000000000000000000000000000..d88b412c9306a4e85140534b33c784c92ab6ccfe GIT binary patch literal 328 zcmV-O0k{5%P)i0dFUGHo>m+7+tvXPH~3n<~3Fngb+fA87ZaL zFSWNHeMM-jt-bx|D?(#!?d?Zj5gKc2Z$J9F;co^lp#@q~eE|m6)>=Q-!!WS6*7~s? zhJm%U){pft46LoSeyn$o7HEwYAPkI(d0(t{G+j(L)A%`m-Rj>A1EXS|W7kinJJ04s zZx{wfmC5rvziIfKKMVt-%H;WDGdouj{mrqV1`fF-`kP}zJvro(=xoThie|4O+0vtR4;&I=)g a=$1E>9tr8wnhKc!0000@>|@4Ay3*;8{5Ey=SFTfIo5YyYt`>vR+M%5bF-BIZ0~ zHO^u%HEozB<>1S^U>RG0@gD}gtsmdmxS!7y>wO&2egBxufz)gBbXg~IU%%mgNT+_m z9rZ+g&S~*KwYX3|0ZvWKwSLH^zzR};S^$Bn13l+YbRlDSjbIO00oY@xXIzQ9j)cLoh*vx%Y mY$bGjquH@}`dy2DNScQ{Xs~iG)BOev9|liXKbLh*2~7a{UyOwS literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_album_bitmap_24.png b/app/src/main/res/drawable-xxhdpi/ic_album_bitmap_24.png new file mode 100644 index 0000000000000000000000000000000000000000..68d136f59d9d79d1064349c3a04152ec6063b65d GIT binary patch literal 1644 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 QHUIzs07*qoM6N<$f|cG__5c6? literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_artist_bitmap_24.png b/app/src/main/res/drawable-xxxhdpi/ic_artist_bitmap_24.png new file mode 100644 index 0000000000000000000000000000000000000000..e515a4134a20a006477d86a68f6a4726f0e14203 GIT binary patch literal 1150 zcmV-^1cCdBP)o8+5_Zn9AM1_V2*ODkYiIT+?3`o&P26U6gdnbhp0T?Z zW5=%8z7|fHqmvE*4jg|L#Nd8%K#sO%%k4|SbRECpHVHfD*pJm*f^ekxU53uzoX&DG z+op@a^h4NY+m3HLHk@Pov6>#jgYTon?}B)HaVJySvY!%AwhXmIUn3^QWPeX3-_*1W z#S!D*0kP(>^HNpUL@ew@3`dTChu~!6I4;$O{gp7Y&zv^gPYKq}@l~n~`>O_Jw{}kN z?5_mG&A3m;-yAh<#^D6JzI`ALguN%&%i8$G$o{rtcCL4yr;i_Pvta({FN8(JkIGkZZohUG5kJaig4y)j z2U!q{h##>gS|?nC?IAh;)q6zncZs;qW8VjD5x;oj&k~c;=ZQ(y<~Hb({mubLjQ{8i z!CB&JZue4|PuouJ^I-f}K-|r=k7~VHeJEL9HqeJF6>DHf!iSTfW9b9`<3J#mnnRSC zR@oh~%=(S5A3Vkn!d$tq<9mPfK%f1cL+PHccMRfBp1FyW76DttTwP<%orSo9FlXG+ zl(Jg@Sa$N0vR_jpPR38C%|2~|ZrN8B#8J-8@F`{A{;-bZCuQ$qw8vpN%?||P=sr6Q zYvj&;#G3!%*q11+v2CBnejd!$>U4gvwKlnOQ6hJ_ikK(1t@nDtEJ+3ASt0TQ5h4zR;72<4LextNJ<0 zz;8l*DCfagb+dqA%qY@2qAV7l(<;uum9_jc~Zz{dt6u9q2j za~Mq=m@^xg7BEQNU<#1-W)#^F?Xa!k?aOs>Zw#I(MfW8y=AUm|-0rg`-T1lcwtxdN zd_dzMpup{t^SSkNg8zR#dHjG|%FTYu|6G+{H*PQz{*-y>+L;Od!rLBk{WM9;6+5w< zGrO?4W>$gruGoL7=lA8WSn&0U`@P!#294LI*7wOXyIo-WRDAD`MS#QY6a6+nHXgW8 z_No3Cn3^ol&7-(q)~U|AaKWjcI%e1IitJLk#o z{Kglod_DHX?_j!nt>ww@W>%dXr%!(m@W$M1dGd7wlkw~cd-ATQD1Nu}fAwzT2Ri}L z{`22sPyBpXnw+tV^W^dO(gVP3%W zn%{YbhTg@#!jtUxM^`E86=$ZIGFDxg``Mp$qIdD7Guccs=XgC>JGZO-EL_mAYvr=j z(b{{svr;_Ivlh;?w>&;)x7|mJd7lgA^6CPWy{oi4ihq{3?#ggk%lYSJeXq{Ta>qLc z?(267+~#`R{_vf$Dc2>N__)8~NzsXamRImf=$(1|SXTc1$BzaM#%T_5#wl*L%NJ;d z{C`>gNomzDo~%uE-&aHjm1Q3Gxp&C*D|f{wsTYB@Y{HVUmjnM^mA9;tm9aZ_cX`u> z#D7^SY*()T{-OWT%3{vby*pM+R9Vk^rIzbivbSg4?&vFflbrvoRLIi#9wz@N`}rU5 zM|Tff1uV!7v&ydg_&)jii|W}2?(A>bx1956tM!YEf4DzPYWdUunyqKSyZTjErcGS$ z`)8iO`ajtL-}5?dcAeg%7I>buT=Uhm$;q}EKce%Cg|CY&o0TnSbX8ZsCF1`S9VwCX iN4G7$&IC(_ZyNubZ8L4{eZjL9B;@Jp=d#Wzp$PyuD0O52 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_playlist_bitmap_24.png b/app/src/main/res/drawable-xxxhdpi/ic_playlist_bitmap_24.png new file mode 100644 index 0000000000000000000000000000000000000000..b0a08a8b3387b5aff03fcf1c7cb93a9c0baf5c83 GIT binary patch literal 672 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U`qFNaSW-5dwc7l?-WM?wuDy; z7*AJwM(&9bSV$y?U_ed5>EAOED9-OwdUVMQ~y z$u)OF@w_$tNqTo8*wd;Z&Wo6U2XOOIFHOSUVw{(N<_ zUS~Sc0q-o%M?W(zH$FeFIMZf7Si!&4In(|>>X~|DF_;@?yl%rEl^nO)w{d5oVuZJF#XGmOIpeM z55!z5pB^Upn>8k)^o@8y;?<4u33@Nvy|vFw{>$`um;RytZ`?hv|4CHk??15Po%F`& z-$x}MTgkiH+)$4*^pijK*Jkq(@8UUA|NoUY%(Cy>_xVpndvam=w4WDKerorz_MP4* zD|gTe~DWM4fpGH!F literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_song_bitmap_24.png b/app/src/main/res/drawable-xxxhdpi/ic_song_bitmap_24.png new file mode 100644 index 0000000000000000000000000000000000000000..c0869bc1d56a02d2e443c9a52b324b9bdb13e69a GIT binary patch literal 687 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U@G%;aSW-5dwZ+VSK3j;HPCzl zn?{4#Bc>qs+#|UyOd1NB4I&ff)unCX(7n#a`ll=`$#dF0t;Tc0CAib6tMIyZ zRAhYAzHK%6KQDf2F#4VRcV~WWS?Qg~m#^1czreb7$?WAx=TB8D_xm6JlKk^dcY*EN zEluwtZdiY=c(_sg;Vdhg`FRyxH&tS)tJY4rJ#D^D*6M=4E3bZAfqH}=ZATsK-T$^C{FijMn*tJ*73{! z{NIh|h8?o?mXv&#Ze{wTSN!l_pZ3-szVF7eVn$YA)%zn(9k3~|zr3$yqEBfDjxkL~;Z-7Pv?>$CRQKj&wCuU~(7Ki%L_ z{G@xo_b;2q{b^eMnP1uV_1S*nS3Yf>qFcxL*EE0UJiQfv#5e!S{yF`BT Date: Wed, 12 Jun 2024 19:12:20 -0600 Subject: [PATCH 16/20] music: disable timeouts This isn't working right now due to how LONG it takes to actually load images. --- .../java/org/oxycblt/auxio/util/StateUtil.kt | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt index d723dd5e3..71927c70c 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt @@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout +import org.oxycblt.auxio.BuildConfig /** * A wrapper around [StateFlow] exposing a one-time consumable event. @@ -166,7 +167,13 @@ suspend fun SendChannel.sendWithTimeout(element: E, timeout: Long = DEFAU try { withTimeout(timeout) { send(element) } } catch (e: TimeoutCancellationException) { - throw TimeoutException("Timed out sending element $element to channel: $e") + logE("Failed to send element to channel $e in ${timeout}ms.") + if (BuildConfig.DEBUG) { + throw TimeoutException("Timed out sending element to channel: $e") + } else { + logE(e.stackTraceToString()) + send(element) + } } } @@ -203,7 +210,13 @@ suspend fun ReceiveChannel.forEachWithTimeout( subsequent = true } } catch (e: TimeoutCancellationException) { - throw TimeoutException("Timed out receiving element from channel: $e") + logE("Failed to send element to channel $e in ${timeout}ms.") + if (BuildConfig.DEBUG) { + throw TimeoutException("Timed out sending element to channel: $e") + } else { + logE(e.stackTraceToString()) + handler() + } } } } From 96d4a84f52d10fcd137e8eecf581fb32b8e8e9d8 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 12 Jun 2024 20:32:32 -0600 Subject: [PATCH 17/20] playback: fix parent restore A single missed savedState access blew up parent restore silently, and in some other cases with non-destructive queue restores would also not restore the parent. --- .../service/ExoPlaybackStateHolder.kt | 31 +++++++++++++------ .../playback/state/PlaybackStateManager.kt | 11 ++----- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt index 2898e4237..058b33403 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt @@ -367,17 +367,28 @@ class ExoPlaybackStateHolder( rawQueue: RawQueue, ack: StateAck.NewPlayback? ) { - this.parent = parent - player.setMediaItems(rawQueue.heap.map { it.toMediaItem(context, null) }) - if (rawQueue.isShuffled) { - player.shuffleModeEnabled = true - player.setShuffleOrder(BetterShuffleOrder(rawQueue.shuffledMapping.toIntArray())) - } else { - player.shuffleModeEnabled = false + logD("Applying saved state") + var sendEvent = false + if (this.parent != parent) { + this.parent = parent + sendEvent = true + } + if (rawQueue != resolveQueue()) { + player.setMediaItems(rawQueue.heap.map { it.toMediaItem(context, null) }) + if (rawQueue.isShuffled) { + player.shuffleModeEnabled = true + player.setShuffleOrder(BetterShuffleOrder(rawQueue.shuffledMapping.toIntArray())) + } else { + player.shuffleModeEnabled = false + } + player.seekTo(rawQueue.heapIndex, C.TIME_UNSET) + player.prepare() + player.pause() + sendEvent = true + } + if (sendEvent) { + ack?.let { playbackManager.ack(this, it) } } - player.seekTo(rawQueue.heapIndex, C.TIME_UNSET) - player.prepare() - ack?.let { playbackManager.ack(this, it) } } override fun endSession() { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 347b099ca..494ab2c0e 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -795,15 +795,8 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { index }) - // Valid state where something needs to be played, direct the stateholder to apply - // this new state. - val oldStateMirror = stateMirror - if (oldStateMirror.rawQueue != rawQueue) { - logD("Queue changed, must reload player") - stateHolder.playing(false) - stateHolder.applySavedState(parent, rawQueue, StateAck.NewPlayback) - stateHolder.seekTo(savedState.positionMs) - } + stateHolder.applySavedState(savedState.parent, rawQueue, StateAck.NewPlayback) + stateHolder.seekTo(savedState.positionMs) stateHolder.repeatMode(savedState.repeatMode) isInitialized = true From 5861d1db87cb70aeaade498511d8c720d25c3f42 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 13 Jun 2024 19:49:53 -0600 Subject: [PATCH 18/20] music: use both ogg/mp3 style mb tags at once Apparently both can exist on both types of files, and grouping will break as a result due to MBID mismatch. --- .../oxycblt/auxio/music/metadata/TagWorker.kt | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt index 202f364df..fe8ca0c46 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt @@ -140,7 +140,9 @@ private class TagWorkerImpl( private fun populateWithId3v2(textFrames: Map>) { // Song - textFrames["TXXX:musicbrainz release track id"]?.let { rawSong.musicBrainzId = it.first() } + (textFrames["TXXX:musicbrainz release track id"] + ?: textFrames["TXXX:musicbrainz_releasetrackid"]) + ?.let { rawSong.musicBrainzId = it.first() } textFrames["TIT2"]?.let { rawSong.name = it.first() } textFrames["TSOT"]?.let { rawSong.sortName = it.first() } @@ -170,7 +172,9 @@ private class TagWorkerImpl( ?.let { rawSong.date = it } // Album - textFrames["TXXX:musicbrainz album id"]?.let { rawSong.albumMusicBrainzId = it.first() } + (textFrames["TXXX:musicbrainz album id"] ?: textFrames["TXXX:musicbrainz_albumid"])?.let { + rawSong.albumMusicBrainzId = it.first() + } textFrames["TALB"]?.let { rawSong.albumName = it.first() } textFrames["TSOA"]?.let { rawSong.albumSortName = it.first() } (textFrames["TXXX:musicbrainz album type"] @@ -180,7 +184,9 @@ private class TagWorkerImpl( ?.let { rawSong.releaseTypes = it } // Artist - textFrames["TXXX:musicbrainz artist id"]?.let { rawSong.artistMusicBrainzIds = it } + (textFrames["TXXX:musicbrainz artist id"] ?: textFrames["TXXX:musicbrainz_artistid"])?.let { + rawSong.artistMusicBrainzIds = it + } (textFrames["TXXX:artists"] ?: textFrames["TPE1"])?.let { rawSong.artistNames = it } (textFrames["TXXX:artistssort"] ?: textFrames["TXXX:artists_sort"] ?: textFrames["TXXX:artists sort"] @@ -188,9 +194,9 @@ private class TagWorkerImpl( ?.let { rawSong.artistSortNames = it } // Album artist - textFrames["TXXX:musicbrainz album artist id"]?.let { - rawSong.albumArtistMusicBrainzIds = it - } + (textFrames["TXXX:musicbrainz album artist id"] + ?: textFrames["TXXX:musicbrainz_albumartistid"]) + ?.let { rawSong.albumArtistMusicBrainzIds = it } (textFrames["TXXX:albumartists"] ?: textFrames["TXXX:album_artists"] ?: textFrames["TXXX:album artists"] ?: textFrames["TPE2"]) @@ -261,7 +267,9 @@ private class TagWorkerImpl( private fun populateWithVorbis(comments: Map>) { // Song - comments["musicbrainz_releasetrackid"]?.let { rawSong.musicBrainzId = it.first() } + (comments["musicbrainz_releasetrackid"] ?: comments["musicbrainz release track id"])?.let { + rawSong.musicBrainzId = it.first() + } comments["title"]?.let { rawSong.name = it.first() } comments["titlesort"]?.let { rawSong.sortName = it.first() } @@ -290,20 +298,28 @@ private class TagWorkerImpl( ?.let { rawSong.date = it } // Album - comments["musicbrainz_albumid"]?.let { rawSong.albumMusicBrainzId = it.first() } + (comments["musicbrainz_albumid"] ?: comments["musicbrainz album id"])?.let { + rawSong.albumMusicBrainzId = it.first() + } comments["album"]?.let { rawSong.albumName = it.first() } comments["albumsort"]?.let { rawSong.albumSortName = it.first() } - comments["releasetype"]?.let { rawSong.releaseTypes = it } + (comments["releasetype"] ?: comments["musicbrainz album type"])?.let { + rawSong.releaseTypes = it + } // Artist - comments["musicbrainz_artistid"]?.let { rawSong.artistMusicBrainzIds = it } + (comments["musicbrainz_artistid"] ?: comments["musicbrainz artist id"])?.let { + rawSong.artistMusicBrainzIds = it + } (comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it } (comments["artistssort"] ?: comments["artists_sort"] ?: comments["artists sort"] ?: comments["artistsort"]) ?.let { rawSong.artistSortNames = it } // Album artist - comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = it } + (comments["musicbrainz_albumartistid"] ?: comments["musicbrainz album artist id"])?.let { + rawSong.albumArtistMusicBrainzIds = it + } (comments["albumartists"] ?: comments["album_artists"] ?: comments["album artists"] ?: comments["albumartist"]) From 296d9c3ca3e82f2ef5d0da6dc634a6f1ca9188f6 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 20 Jun 2024 21:25:32 -0600 Subject: [PATCH 19/20] music: disable perceptual cover art keying Too slow, need to aggressively optimize the music loader before even THINKING about this, and if anything likely defer it. --- .../oxycblt/auxio/image/extractor/DHash.kt | 1 + .../auxio/music/cache/CacheDatabase.kt | 2 +- .../oxycblt/auxio/music/metadata/TagWorker.kt | 27 ++++++++++++++----- .../auxio/music/service/MediaItemBrowser.kt | 2 -- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/DHash.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/DHash.kt index 0b0949efd..1e7809606 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/DHash.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/DHash.kt @@ -26,6 +26,7 @@ import android.graphics.ColorMatrixColorFilter import android.graphics.Paint import java.math.BigInteger +@Suppress("UNUSED") fun Bitmap.dHash(hashSize: Int = 16): String { // Step 1: Resize the bitmap to a fixed size val resizedBitmap = Bitmap.createScaledBitmap(this, hashSize + 1, hashSize, true) diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt index 00f8eb43b..2a7113066 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt @@ -32,7 +32,7 @@ import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.metadata.correctWhitespace import org.oxycblt.auxio.music.metadata.splitEscaped -@Database(entities = [CachedSong::class], version = 45, exportSchema = false) +@Database(entities = [CachedSong::class], version = 46, exportSchema = false) abstract class CacheDatabase : RoomDatabase() { abstract fun cachedSongsDao(): CachedSongsDao } diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt index fe8ca0c46..d30e5324e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt @@ -18,7 +18,6 @@ package org.oxycblt.auxio.music.metadata -import android.graphics.BitmapFactory import androidx.core.text.isDigitsOnly import androidx.media3.common.MediaItem import androidx.media3.exoplayer.MetadataRetriever @@ -26,8 +25,8 @@ import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.TrackGroupArray import java.util.concurrent.Future import javax.inject.Inject +import kotlin.math.min import org.oxycblt.auxio.image.extractor.CoverExtractor -import org.oxycblt.auxio.image.extractor.dHash import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.fs.toAudioUri import org.oxycblt.auxio.music.info.Date @@ -106,10 +105,24 @@ private class TagWorkerImpl( populateWithId3v2(textTags.id3v2) populateWithVorbis(textTags.vorbis) - val coverInputStream = coverExtractor.findCoverDataInMetadata(metadata) - val bitmap = coverInputStream?.use { BitmapFactory.decodeStream(it) } - rawSong.coverPerceptualHash = bitmap?.dHash() - bitmap?.recycle() + coverExtractor.findCoverDataInMetadata(metadata)?.use { + val available = it.available() + val skip = min(available / 2L, available - COVER_KEY_SAMPLE.toLong()) + it.skip(skip) + val bytes = ByteArray(COVER_KEY_SAMPLE) + it.read(bytes) + + @OptIn(ExperimentalStdlibApi::class) val byteString = bytes.toHexString() + + rawSong.coverPerceptualHash = byteString + } + + // OPTIONAL: Nicer cover art keying using an actual perceptual hash + // Really bad idea if you have big cover arts. Okay idea if you have different + // formats for the same cover art. + // val bitmap = coverInputStream?.use { BitmapFactory.decodeStream(it) } + // rawSong.coverPerceptualHash = bitmap?.dHash() + // bitmap?.recycle() // OPUS base gain interpretation code: This is likely not needed, as the media player // should be using the base gain already. Uncomment if that's not the case. @@ -376,6 +389,8 @@ private class TagWorkerImpl( first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()?.nonZeroOrNull() private companion object { + val COVER_KEY_SAMPLE = 32 + val COMPILATION_ALBUM_ARTISTS = listOf("Various Artists") val COMPILATION_RELEASE_TYPES = listOf("compilation") diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemBrowser.kt b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemBrowser.kt index 0ca607167..93841a63f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemBrowser.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemBrowser.kt @@ -61,8 +61,6 @@ constructor( private var invalidator: Invalidator? = null interface Invalidator { - data class ParentId(val id: String, val itemCount: Int) - fun invalidate(ids: Map) fun invalidate(controller: ControllerInfo, query: String, itemCount: Int) From 17d16d20c7e7c3a848ac426a740ec0487c650df2 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 20 Jun 2024 21:55:02 -0600 Subject: [PATCH 20/20] info: update changelog --- CHANGELOG.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0dafdad5..32e65b5de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,16 @@ #### What's Improved - Album covers are now loaded on a per-song basis -- Correctly interpret MP4 sort tags +- MP4 sort tags are now correctly interpreted +- Support multi-value MP4 tags with multiple `data` sub-atoms are parsed correctly +- M3U paths are now interpreted both as relative and absolute regardless of the format +- Added support for M3U paths starting with /storage/ #### What's Fixed - Fixed repeat mode not restoring on startup - Fixed rewinding not occuring when skipping back at the beginning of the queue if rewind before skipping was turned off +- Fixed artist duplication when inconsistent MusicBrainz ID tag naming was used #### What's Changed - For the time being, the media notification will not follow Album Covers or 1:1 Covers settings @@ -22,6 +26,11 @@ rewind before skipping was turned off #### dev -> dev1 changes - Re-added ability to open app from clicking on notification - Removed tasker plugin +- Support multi-value MP4 tags with multiple `data` sub-atoms are parsed correctly +- M3U paths are now interpreted both as relative and absolute regardless of the format +- Added support for M3U paths starting with /storage/ +- Fixed artist duplication when inconsistent MusicBrainz ID tag naming was used +- Made album cover keying more efficient at the cost of resillients ## 3.4.3