music: handle total absolute m3u paths

Some players like generating M3Us with paths starting with
/storage/.../..., so I need to handle those too.
This commit is contained in:
Alexander Capehart 2024-06-08 15:06:04 -06:00
parent 1c74f05222
commit 8b2634df4d
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
2 changed files with 98 additions and 68 deletions

View file

@ -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<PossiblePaths>()
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<InterpretedPath> =
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<Volume>
): List<Path> {
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,

View file

@ -158,6 +158,9 @@ value class Components private constructor(val components: List<String>) {
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 (/).