From 27e39b6c10d3f3d9fc0f0fa5ac9b4d7b323292f4 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sun, 26 May 2024 21:50:34 -0600 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 8b2634df4d4f30e83b46ec0abd1457f6427c922a Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 8 Jun 2024 15:06:04 -0600 Subject: [PATCH 4/9] 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 5/9] 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 d117f160810510a50c052b71c0a3ac27ff9ff13d Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sun, 9 Jun 2024 13:13:54 -0600 Subject: [PATCH 6/9] 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 7/9] 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 5a36cfee67f06b3bb180765b38dc32edb88266b3 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 12 Jun 2024 19:12:20 -0600 Subject: [PATCH 8/9] 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 5861d1db87cb70aeaade498511d8c720d25c3f42 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 13 Jun 2024 19:49:53 -0600 Subject: [PATCH 9/9] 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"])