music: fix m3u export

Wasn't correctly writing and also naively relative-izing paths. Those
should be fixed now, I hope.
This commit is contained in:
Alexander Capehart 2023-12-21 20:55:49 -07:00
parent d59230be6d
commit c3f67d4dc5
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
2 changed files with 47 additions and 23 deletions

View file

@ -20,19 +20,19 @@ 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 java.io.BufferedReader import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.InputStreamReader import java.io.InputStreamReader
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.Components
import org.oxycblt.auxio.music.fs.Path import org.oxycblt.auxio.music.fs.Path
import org.oxycblt.auxio.music.metadata.correctWhitespace import org.oxycblt.auxio.music.metadata.correctWhitespace
import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import java.io.BufferedWriter
import java.io.OutputStream
/** /**
* Minimal M3U file format implementation. * Minimal M3U file format implementation.
@ -53,10 +53,11 @@ interface M3U {
/** /**
* Writes the given [playlist] to the given [outputStream] in the M3U format,. * Writes the given [playlist] to the given [outputStream] in the M3U format,.
*
* @param playlist The playlist to write. * @param playlist The playlist to write.
* @param outputStream The stream to write the M3U file to. * @param outputStream The stream to write the M3U file to.
* @param workingDirectory The directory that the M3U file is contained in. This is used to * @param workingDirectory The directory that the M3U file is contained in. This is used to
* create relative paths to where the M3U file is assumed to be stored. * create relative paths to where the M3U file is assumed to be stored.
*/ */
fun write(playlist: Playlist, outputStream: OutputStream, workingDirectory: Path) fun write(playlist: Playlist, outputStream: OutputStream, workingDirectory: Path)
} }
@ -128,17 +129,13 @@ class M3UImpl @Inject constructor(@ApplicationContext private val context: Conte
} }
} }
override fun write( override fun write(playlist: Playlist, outputStream: OutputStream, workingDirectory: Path) {
playlist: Playlist,
outputStream: OutputStream,
workingDirectory: Path
) {
val writer = outputStream.bufferedWriter() val writer = outputStream.bufferedWriter()
// Try to be as compliant to the spec as possible while also cramming it full of extensions // Try to be as compliant to the spec as possible while also cramming it full of extensions
// I imagine other players will use. // I imagine other players will use.
writer.writeLine("#EXTM3U") writer.writeLine("#EXTM3U")
writer.writeLine("#EXTENC:UTF-8") writer.writeLine("#EXTENC:UTF-8")
writer.writeLine("#PLAYLIST:${playlist.name}") writer.writeLine("#PLAYLIST:${playlist.name.resolve(context)}")
for (song in playlist.songs) { for (song in playlist.songs) {
val relativePath = song.path.components.relativeTo(workingDirectory.components) val relativePath = song.path.components.relativeTo(workingDirectory.components)
writer.writeLine("#EXTINF:${song.durationMs},${song.name.resolve(context)}") writer.writeLine("#EXTINF:${song.durationMs},${song.name.resolve(context)}")
@ -147,6 +144,7 @@ class M3UImpl @Inject constructor(@ApplicationContext private val context: Conte
writer.writeLine("#EXTGEN:${song.genres.resolveNames(context)}") writer.writeLine("#EXTGEN:${song.genres.resolveNames(context)}")
writer.writeLine(relativePath.toString()) writer.writeLine(relativePath.toString())
} }
writer.flush()
} }
private fun BufferedWriter.writeLine(line: String) { private fun BufferedWriter.writeLine(line: String) {
@ -154,9 +152,7 @@ class M3UImpl @Inject constructor(@ApplicationContext private val context: Conte
newLine() newLine()
} }
private fun Components.absoluteTo( private fun Components.absoluteTo(workingDirectory: Components): Components {
workingDirectory: Components
): Components {
var absoluteComponents = workingDirectory var absoluteComponents = workingDirectory
for (component in components) { for (component in components) {
when (component) { when (component) {
@ -172,18 +168,44 @@ class M3UImpl @Inject constructor(@ApplicationContext private val context: Conte
return absoluteComponents return absoluteComponents
} }
private fun Components.relativeTo( private fun Components.relativeTo(workingDirectory: Components): Components {
workingDirectory: Components // We want to find the common prefix of the working directory and path, and then
): Components { // and them combine them with the correct relative elements to make sure they
var relativeComponents = Components.parse(".") // resolve the same.
var commonIndex = 0 var commonIndex = 0
while (commonIndex < components.size && while (commonIndex < components.size &&
commonIndex < workingDirectory.components.size && commonIndex < workingDirectory.components.size &&
components[commonIndex] == workingDirectory.components[commonIndex]) { components[commonIndex] == workingDirectory.components[commonIndex]) {
++commonIndex ++commonIndex
relativeComponents = relativeComponents.child("..")
} }
return relativeComponents.concat(depth(commonIndex))
}
var relativeComponents = Components.parse(".")
// TODO: Simplify this logic
when {
commonIndex == components.size && commonIndex == workingDirectory.components.size -> {
// The paths are the same. This shouldn't occur.
}
commonIndex == components.size -> {
// The working directory is deeper in the path, backtrack.
for (i in 0..workingDirectory.components.size - commonIndex - 1) {
relativeComponents = relativeComponents.child("..")
}
}
commonIndex == workingDirectory.components.size -> {
// Working directory is shallower than the path, can just append the
// non-common remainder of the path
relativeComponents = relativeComponents.child(depth(commonIndex))
}
else -> {
// The paths are siblings. Backtrack and append as needed.
for (i in 0..workingDirectory.components.size - commonIndex - 1) {
relativeComponents = relativeComponents.child("..")
}
relativeComponents = relativeComponents.child(depth(commonIndex))
}
}
return relativeComponents
}
} }

View file

@ -122,8 +122,9 @@ value class Components private constructor(val components: List<String>) {
} }
/** /**
* Removes the first [n] elements of the path, effectively resulting in a path that is n * Removes the first [n] elements of the path, effectively resulting in a path that is n levels
* levels deep. * deep.
*
* @param n The number of elements to remove. * @param n The number of elements to remove.
* @return The new [Components] instance. * @return The new [Components] instance.
*/ */
@ -131,10 +132,11 @@ value class Components private constructor(val components: List<String>) {
/** /**
* Concatenates this [Components] instance with another. * Concatenates this [Components] instance with another.
*
* @param other The [Components] instance to concatenate with. * @param other The [Components] instance to concatenate with.
* @return The new [Components] instance. * @return The new [Components] instance.
*/ */
fun concat(other: Components) = Components(components + other.components) fun child(other: Components) = Components(components + other.components)
companion object { companion object {
/** /**