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:
parent
1c74f05222
commit
8b2634df4d
2 changed files with 98 additions and 68 deletions
|
@ -20,18 +20,21 @@ package org.oxycblt.auxio.music.external
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
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.BufferedReader
|
||||||
import java.io.BufferedWriter
|
import java.io.BufferedWriter
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import javax.inject.Inject
|
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.
|
* 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? {
|
override fun read(stream: InputStream, workingDirectory: Path): ImportedPlaylist? {
|
||||||
|
val volumes = volumeManager.getVolumes()
|
||||||
val reader = BufferedReader(InputStreamReader(stream))
|
val reader = BufferedReader(InputStreamReader(stream))
|
||||||
val paths = mutableListOf<PossiblePaths>()
|
val paths = mutableListOf<PossiblePaths>()
|
||||||
var name: String? = null
|
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
|
// 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
|
// based on the programs that generated it. I more or less have to consider any possible
|
||||||
// most programs will generate. Note that we do end up proposing multiple
|
// interpretation as valid.
|
||||||
// interpretations
|
val interpretations = interpretPath(path)
|
||||||
val possibilities =
|
val possibilities = interpretations.flatMap {
|
||||||
when {
|
expandInterpretation(it, workingDirectory, volumes)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
paths.add(possibilities.map { Path(workingDirectory.volume, it) })
|
paths.add(possibilities)
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (paths.isNotEmpty()) {
|
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(
|
override fun write(
|
||||||
playlist: Playlist,
|
playlist: Playlist,
|
||||||
outputStream: OutputStream,
|
outputStream: OutputStream,
|
||||||
|
|
|
@ -158,6 +158,9 @@ value class Components private constructor(val components: List<String>) {
|
||||||
return components == other.components.take(components.size)
|
return components == other.components.take(components.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun containing(other: Components) =
|
||||||
|
Components(components + other.components.drop(components.size))
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Parses a path string into a [Components] instance by the unix path separator (/).
|
* Parses a path string into a [Components] instance by the unix path separator (/).
|
||||||
|
|
Loading…
Reference in a new issue