diff --git a/README.md b/README.md index c8c568e..b7d800b 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,14 @@ val someApi = TODO("Not a real API object") // Returns a playlist in the M3U format val m3uContent: String = someApi.getPlaylist("Best of Willy Astor") val entries: List = M3uParser.parse(m3uContent) + + +// You can also use a lambda for processing each entry instead of returning a List +val m3uFile = Paths.get("myplaylist.m3u") +M3uParser.parse(m3uFile) { entry -> + println(entry->name) +} + ``` ### Nested playlists diff --git a/buildConfig/detekt.yml b/buildConfig/detekt.yml index 1deb425..4354d8b 100644 --- a/buildConfig/detekt.yml +++ b/buildConfig/detekt.yml @@ -3,3 +3,8 @@ comments: active: true UndocumentedPublicFunction: active: true +complexity: + TooManyFunctions: + ignoreOverridden: true + thresholdInClasses: 15 + thresholdInObjects: 15 diff --git a/src/main/kotlin/net/bjoernpetersen/m3u/M3uParser.kt b/src/main/kotlin/net/bjoernpetersen/m3u/M3uParser.kt index fa67b0c..855d6e9 100644 --- a/src/main/kotlin/net/bjoernpetersen/m3u/M3uParser.kt +++ b/src/main/kotlin/net/bjoernpetersen/m3u/M3uParser.kt @@ -55,7 +55,28 @@ object M3uParser { @JvmOverloads fun parse(m3uFile: Path, charset: Charset = Charsets.UTF_8): List { require(Files.isRegularFile(m3uFile)) { "$m3uFile is not a file" } - return parse(Files.lines(m3uFile, charset).asSequence(), m3uFile.parent) + val entries = mutableListOf() + parse(Files.lines(m3uFile, charset).asSequence(), m3uFile.parent) { entry -> entries.add(entry) } + return entries + } + + /** + * Parses the specified file. + * + * Comment lines and lines which can't be parsed are dropped. + * + * @param m3uFile a path to an .m3u file + * @param charset the file's encoding, defaults to UTF-8 + * @param processFun a Unit to process each m3uentry + * @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, processFun: (M3uEntry) -> Unit) { + require(Files.isRegularFile(m3uFile)) { "$m3uFile is not a file" } + parse(Files.lines(m3uFile, charset).asSequence(), m3uFile.parent, processFun) } /** @@ -70,7 +91,30 @@ object M3uParser { @JvmStatic @JvmOverloads fun parse(m3uContentReader: InputStreamReader, baseDir: Path? = null): List { - return m3uContentReader.buffered().useLines { parse(it, baseDir) } + val entries = mutableListOf() + m3uContentReader.buffered().useLines { + parse(it, baseDir) { + entry -> + entries.add(entry) + } + } + return entries + } + + /** + * Parses the [InputStream] from the specified reader. + * + * Comment lines and lines which can't be parsed are dropped. + * + * @param m3uContentReader a reader reading the content of an `.m3u` file + * @param baseDir a base dir for resolving relative paths + * @param processFun a Unit to process each m3uentry + * + */ + @JvmStatic + @JvmOverloads + fun parse(m3uContentReader: InputStreamReader, baseDir: Path? = null, processFun: (M3uEntry) -> Unit) { + return m3uContentReader.buffered().useLines { parse(it, baseDir, processFun) } } /** @@ -85,7 +129,28 @@ object M3uParser { @JvmStatic @JvmOverloads fun parse(m3uContent: String, baseDir: Path? = null): List { - return parse(m3uContent.lineSequence(), baseDir) + val entries = mutableListOf() + parse(m3uContent, baseDir) { + entry -> + entries.add(entry) + } + return entries + } + + /** + * Parses the specified content of a `.m3u` file. + * + * Comment lines and lines which can't be parsed are dropped. + * + * @param m3uContent the content of a `.m3u` file + * @param baseDir a base dir for resolving relative paths + * @param processFun a Unit to process each m3uentry + * + */ + @JvmStatic + @JvmOverloads + fun parse(m3uContent: String, baseDir: Path? = null, processFun: (M3uEntry) -> Unit) { + parse(m3uContent.lineSequence(), baseDir, processFun) } /** @@ -106,14 +171,14 @@ object M3uParser { } @Suppress("NestedBlockDepth", "ReturnCount") - private fun parse(lines: Sequence, baseDir: Path?): List { + private fun parse(lines: Sequence, baseDir: Path?, processFun: (M3uEntry) -> Unit) { val filtered = lines .filterNot { it.isBlank() } .map { it.trimEnd() } .dropWhile { it == EXTENDED_HEADER } .iterator() - if (!filtered.hasNext()) return emptyList() + if (!filtered.hasNext()) return val entries = LinkedList() @@ -134,7 +199,7 @@ object M3uParser { if (filtered.hasNext()) { currentLine = filtered.next() } else { - return entries + return } } @@ -149,13 +214,13 @@ object M3uParser { match = null if (entry != null) { - entries.add(entry) + processFun(entry) } else { logger.warn { "Ignored line $currentLine" } } } - return entries + return } private fun parseSimple(location: String, baseDir: Path?): M3uEntry? { diff --git a/src/test/kotlin/net/bjoernpetersen/m3u/M3uParserExampleTest.kt b/src/test/kotlin/net/bjoernpetersen/m3u/M3uParserExampleTest.kt index 6773be0..dd870ab 100644 --- a/src/test/kotlin/net/bjoernpetersen/m3u/M3uParserExampleTest.kt +++ b/src/test/kotlin/net/bjoernpetersen/m3u/M3uParserExampleTest.kt @@ -124,6 +124,36 @@ class M3uParserExampleTest { assertEquals("https://i.imgur.com/CnIVW9o.jpg", withLogo.metadata.logo) } + @TestFactory + fun testWikiLambda(): List { + return listOf( + "wiki_mixed.m3u", + "wiki_mixed_empty_lines.m3u", + ).map { name -> + dynamicTest(name) { + val entries = mutableListOf() + M3uParser.parse(javaClass.getResourceAsStream(name).reader()) { entry -> + entries.add(entry) + } + assertEquals(7, entries.size) + + val simple = listOf(entries[1], entries[4]) + simple.forEach { + assertThat(it) + .returns(null, M3uEntry::duration) + .returns(null, M3uEntry::title) + } + + val extended = entries.filterNot { it in simple } + extended.forEach { entry -> + assertThat(entry) + .matches { it.duration != null } + .matches { it.title != null } + } + } + } + } + @Test fun testRecursiveResolution() { val files = listOf("rec_1.m3u", "rec_2.m3u", "rec_3.m3u")