music: interpret m3u paths as relative & absolute

Resolves #673
This commit is contained in:
Alexander Capehart 2024-05-26 21:50:34 -06:00
parent 0f691ee65b
commit 27e39b6c10
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
3 changed files with 44 additions and 18 deletions

View file

@ -164,7 +164,8 @@ constructor(
} }
val deviceLibrary = musicRepository.deviceLibrary ?: return@launch 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()) { if (songs.isEmpty()) {
logE("No songs found") logE("No songs found")

View file

@ -76,7 +76,9 @@ data class ExportConfig(val absolute: Boolean, val windowsPaths: Boolean)
* @see ExternalPlaylistManager * @see ExternalPlaylistManager
* @see M3U * @see M3U
*/ */
data class ImportedPlaylist(val name: String?, val paths: List<Path>) data class ImportedPlaylist(val name: String?, val paths: List<PossiblePaths>)
typealias PossiblePaths = List<Path>
class ExternalPlaylistManagerImpl class ExternalPlaylistManagerImpl
@Inject @Inject

View file

@ -75,7 +75,7 @@ interface M3U {
class M3UImpl @Inject constructor(@ApplicationContext private val context: Context) : M3U { class M3UImpl @Inject constructor(@ApplicationContext private val context: Context) : M3U {
override fun read(stream: InputStream, workingDirectory: Path): ImportedPlaylist? { override fun read(stream: InputStream, workingDirectory: Path): ImportedPlaylist? {
val reader = BufferedReader(InputStreamReader(stream)) val reader = BufferedReader(InputStreamReader(stream))
val paths = mutableListOf<Path>() val paths = mutableListOf<PossiblePaths>()
var name: String? = null var name: String? = null
consumeFile@ while (true) { 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 // 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 // based on the programs that generated it. These are the paths though that I assume
// programs will generate. // most programs will generate. Note that we do end up proposing multiple
val components = // interpretations
val possibilities =
when { when {
path.startsWith('/') -> { path.startsWith('/') -> {
// Unix absolute path. Note that we still assume this absolute path is in // 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 // 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. // 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("./") -> { path.startsWith("./") -> {
// Unix relative path, resolve it // 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) -> { path.matches(WINDOWS_VOLUME_PREFIX_REGEX) -> {
// Windows absolute path, we should get rid of the volume prefix, but // Windows absolute path, we should get rid of the volume prefix, but
// otherwise // otherwise the rest should be fine. Again, we have to disregard what the
// the rest should be fine. Again, we have to disregard what the volume // volume actually is since there's no sane way to map it to the phone's volumes.
// actually val absoluteInterpretation = Components.parseWindows(path.substring(2))
// is since there's no sane way to map it to the phone's volumes. val relativeInterpretation = absoluteInterpretation.absoluteTo(workingDirectory.components)
Components.parseWindows(path.substring(2)) 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(".\\") -> { path.startsWith(".\\") -> {
// Windows relative path, we need to remove the .\\ prefix // Windows-style relative path
Components.parseWindows(path).absoluteTo(workingDirectory.components) val absoluteInterpretation = Components.parseWindows(path)
val relativeInterpretation = absoluteInterpretation.absoluteTo(workingDirectory.components)
listOf(relativeInterpretation, absoluteInterpretation)
} }
else -> { else -> {
// No clue, parse by all separators and assume it's relative. // No clue, just go wild and assume all possible combinations.
Components.parseAny(path).absoluteTo(workingDirectory.components) 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()) { return if (paths.isNotEmpty()) {