Skip to content

Commit

Permalink
Add method to resolve nested playlists recursively
Browse files Browse the repository at this point in the history
  • Loading branch information
BjoernPetersen committed Oct 20, 2020
1 parent 49f0124 commit fef9f61
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 0 deletions.
57 changes: 57 additions & 0 deletions src/main/kotlin/net/bjoernpetersen/m3u/M3uParser.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.bjoernpetersen.m3u

import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.nio.charset.Charset
Expand All @@ -11,6 +12,7 @@ import kotlin.streams.asSequence
import mu.KotlinLogging
import net.bjoernpetersen.m3u.model.M3uEntry
import net.bjoernpetersen.m3u.model.MediaLocation
import net.bjoernpetersen.m3u.model.MediaPath

/**
* Can be used to parse `.m3u` files.
Expand Down Expand Up @@ -41,7 +43,10 @@ object M3uParser {
* @param m3uFile a path to an .m3u file
* @param charset the file's encoding, defaults to UTF-8
* @return a list of all contained entries in order
* @throws IOException if file can't be read
* @throws IllegalArgumentException if file is not a regular file
*/
@Throws(IOException::class)
@JvmStatic
@JvmOverloads
fun parse(m3uFile: Path, charset: Charset = Charsets.UTF_8): List<M3uEntry> {
Expand Down Expand Up @@ -79,6 +84,23 @@ object M3uParser {
return parse(m3uContent.lineSequence(), baseDir)
}

/**
* Recursively resolves all playlist files contained as entries in the given list.
*
* Note that unresolvable playlist file entries will be dropped.
*
* @param entries a list of playlist entries
* @param charset the encoding to be used to read nested playlist files, defaults to UTF-8
*/
@JvmStatic
@JvmOverloads
fun resolveNestedPlaylists(
entries: List<M3uEntry>,
charset: Charset = Charsets.UTF_8
): List<M3uEntry> {
return resolveRecursively(entries, charset)
}

// TODO: fix detekt issues
@Suppress("NestedBlockDepth", "ReturnCount")
private fun parse(lines: Sequence<String>, baseDir: Path?): List<M3uEntry> {
Expand Down Expand Up @@ -147,4 +169,39 @@ object M3uParser {
val title = infoMatch.groups[TITLE]?.value
return M3uEntry(mediaLocation, duration, title)
}

private fun resolveRecursively(
source: List<M3uEntry>,
charset: Charset,
result: MutableList<M3uEntry> = LinkedList()
): List<M3uEntry> {
for (entry in source) {
val location = entry.location
if (location is MediaPath && location.isPlaylistPath) {
resolveNestedPlaylist(location.path, charset, result)
} else {
result.add(entry)
}
}
return result
}

private fun resolveNestedPlaylist(
path: Path,
charset: Charset,
result: MutableList<M3uEntry>
) {
if (!Files.isRegularFile(path)) {
return
}

val parsed = try {
parse(path, charset)
} catch (e: IOException) {
logger.warn(e) { "Could not parse nested playlist file: $path" }
return
}

resolveRecursively(parsed, charset, result)
}
}
48 changes: 48 additions & 0 deletions src/test/kotlin/net/bjoernpetersen/m3u/M3uParserExampleTest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package net.bjoernpetersen.m3u

import java.nio.file.Files
import java.nio.file.Paths
import net.bjoernpetersen.m3u.model.M3uEntry
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.assertEquals
Expand Down Expand Up @@ -96,4 +98,50 @@ class M3uParserExampleTest {
}
}
}

@Test
fun testRecursiveResolution() {
val files = listOf("rec_1.m3u", "rec_2.m3u", "rec_3.m3u")
exportFiles(files)
try {
val initial = M3uParser.parse(Paths.get(FILE_DIR, "rec_1.m3u"))
assertThat(initial)
.hasSize(4)

assertThat(M3uParser.resolveNestedPlaylists(initial))
.isNotSameAs(initial)
.hasSize(6)
.matches { list ->
val locations = list.asSequence().map { it.location }.distinct().count()
locations == list.size
}
} finally {
deleteFiles(files)
}
}

private fun deleteFiles(paths: List<String>) {
paths.map { Paths.get(FILE_DIR, it) }.forEach {
Files.deleteIfExists(it)
}
}

private fun exportFiles(paths: List<String>) {
val parent = Paths.get(FILE_DIR)
if (!Files.isDirectory(parent)) {
Files.createDirectories(parent)
}
for (name in paths) {
val path = parent.resolve(name)
javaClass.getResourceAsStream(name).reader().use { reader ->
Files.newBufferedWriter(path, Charsets.UTF_8).use { writer ->
reader.copyTo(writer)
}
}
}
}

companion object {
const val FILE_DIR = "build/tmp/m3us"
}
}
4 changes: 4 additions & 0 deletions src/test/resources/net/bjoernpetersen/m3u/rec_1.m3u
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Test1.mp3
rec_2.m3u
missing.m3u
Test6.mp3
3 changes: 3 additions & 0 deletions src/test/resources/net/bjoernpetersen/m3u/rec_2.m3u
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Test2.mp3
rec_3.m3u
Test5.mp3
2 changes: 2 additions & 0 deletions src/test/resources/net/bjoernpetersen/m3u/rec_3.m3u
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Test3.mp3
Test4.mp3

0 comments on commit fef9f61

Please sign in to comment.