diff --git a/app/src/main/java/xyz/cottongin/radio247/data/importing/M3uParser.kt b/app/src/main/java/xyz/cottongin/radio247/data/importing/M3uParser.kt index 51c70bd..68bb812 100644 --- a/app/src/main/java/xyz/cottongin/radio247/data/importing/M3uParser.kt +++ b/app/src/main/java/xyz/cottongin/radio247/data/importing/M3uParser.kt @@ -27,6 +27,7 @@ object M3uParser { trimmed.startsWith("#") -> continue else -> { val url = trimmed + if (url.isBlank()) continue val name = currentName ?: deriveNameFromUrl(url) result.add(ParsedStation(name = name, url = url, artworkUrl = currentArtworkUrl)) currentName = null @@ -37,14 +38,4 @@ object M3uParser { return result } - - private fun deriveNameFromUrl(url: String): String { - val path = try { - java.net.URI(url).path - } catch (_: Exception) { - url - } - val lastSegment = path.trimEnd('/').substringAfterLast('/') - return lastSegment.ifEmpty { url } - } } diff --git a/app/src/main/java/xyz/cottongin/radio247/data/importing/ParserUtils.kt b/app/src/main/java/xyz/cottongin/radio247/data/importing/ParserUtils.kt new file mode 100644 index 0000000..9a243c0 --- /dev/null +++ b/app/src/main/java/xyz/cottongin/radio247/data/importing/ParserUtils.kt @@ -0,0 +1,11 @@ +package xyz.cottongin.radio247.data.importing + +internal fun deriveNameFromUrl(url: String): String { + val path = try { + java.net.URI(url).path + } catch (_: Exception) { + url + } + val lastSegment = path.trimEnd('/').substringAfterLast('/') + return lastSegment.ifEmpty { url } +} diff --git a/app/src/main/java/xyz/cottongin/radio247/data/importing/PlaylistExporter.kt b/app/src/main/java/xyz/cottongin/radio247/data/importing/PlaylistExporter.kt index 1510243..630d9b7 100644 --- a/app/src/main/java/xyz/cottongin/radio247/data/importing/PlaylistExporter.kt +++ b/app/src/main/java/xyz/cottongin/radio247/data/importing/PlaylistExporter.kt @@ -7,7 +7,8 @@ object PlaylistExporter { val sb = StringBuilder() sb.append("#EXTM3U\n") for (station in stations) { - sb.append("#EXTINF:-1,").append(station.name).append("\n") + val safeName = station.name.replace("\n", " ").replace("\r", " ") + sb.append("#EXTINF:-1,").append(safeName).append("\n") if (station.defaultArtworkUrl != null) { sb.append("#EXTIMG:").append(station.defaultArtworkUrl).append("\n") } @@ -20,8 +21,9 @@ object PlaylistExporter { val sb = StringBuilder() sb.append("[playlist]\n") stations.forEachIndexed { index, station -> + val safeName = station.name.replace("\n", " ").replace("\r", " ") sb.append("File").append(index + 1).append("=").append(station.url).append("\n") - sb.append("Title").append(index + 1).append("=").append(station.name).append("\n") + sb.append("Title").append(index + 1).append("=").append(safeName).append("\n") } sb.append("NumberOfEntries=").append(stations.size).append("\n") sb.append("Version=2\n") diff --git a/app/src/main/java/xyz/cottongin/radio247/data/importing/PlsParser.kt b/app/src/main/java/xyz/cottongin/radio247/data/importing/PlsParser.kt index 6236033..2d79934 100644 --- a/app/src/main/java/xyz/cottongin/radio247/data/importing/PlsParser.kt +++ b/app/src/main/java/xyz/cottongin/radio247/data/importing/PlsParser.kt @@ -34,26 +34,17 @@ object PlsParser { } } - return entries.keys.sorted().map { index -> + return entries.keys.sorted().mapNotNull { index -> val entry = entries[index]!! - val url = entry["url"] ?: return@map null + val url = (entry["url"] ?: return@mapNotNull null).trim() + if (url.isBlank()) return@mapNotNull null val name = entry["name"] ?: deriveNameFromUrl(url) ParsedStation(name = name, url = url) - }.filterNotNull() + } } private fun parseIndex(key: String, prefix: String): Int? { val suffix = key.removePrefix(prefix) return suffix.toIntOrNull() } - - private fun deriveNameFromUrl(url: String): String { - val path = try { - java.net.URI(url).path - } catch (_: Exception) { - url - } - val lastSegment = path.trimEnd('/').substringAfterLast('/') - return lastSegment.ifEmpty { url } - } } diff --git a/app/src/test/java/xyz/cottongin/radio247/data/importing/M3uParserTest.kt b/app/src/test/java/xyz/cottongin/radio247/data/importing/M3uParserTest.kt index e2cc187..c3cdace 100644 --- a/app/src/test/java/xyz/cottongin/radio247/data/importing/M3uParserTest.kt +++ b/app/src/test/java/xyz/cottongin/radio247/data/importing/M3uParserTest.kt @@ -78,6 +78,27 @@ class M3uParserTest { assertEquals("http://stream.example.com/radio", result[0].url) } + @Test + fun skipEntriesWithBlankUrls() { + val content = """ + #EXTM3U + #EXTINF:-1,Station One + + http://stream1.example.com/radio + #EXTINF:-1,Station Two + + http://stream2.example.com/radio + """.trimIndent() + + val result = M3uParser.parse(content) + + assertEquals(2, result.size) + assertEquals("Station One", result[0].name) + assertEquals("http://stream1.example.com/radio", result[0].url) + assertEquals("Station Two", result[1].name) + assertEquals("http://stream2.example.com/radio", result[1].url) + } + @Test fun handleCrlfLineEndings() { val content = "#EXTM3U\r\n#EXTINF:-1,CRLF Station\r\nhttp://stream.example.com/radio\r\n" diff --git a/app/src/test/java/xyz/cottongin/radio247/data/importing/PlaylistExporterTest.kt b/app/src/test/java/xyz/cottongin/radio247/data/importing/PlaylistExporterTest.kt index 3a47b94..f9dec27 100644 --- a/app/src/test/java/xyz/cottongin/radio247/data/importing/PlaylistExporterTest.kt +++ b/app/src/test/java/xyz/cottongin/radio247/data/importing/PlaylistExporterTest.kt @@ -8,6 +8,29 @@ import org.junit.Test class PlaylistExporterTest { + @Test + fun sanitizeNewlinesInStationNamesForM3u() { + val stations = listOf( + Station( + id = 1, + name = "Line One\nLine Two", + url = "http://stream.example.com/radio", + playlistId = null, + sortOrder = 0, + starred = false, + defaultArtworkUrl = null + ) + ) + + val result = PlaylistExporter.toM3u(stations) + + assertFalse(result.contains("#EXTINF:-1,Line One\nLine Two")) + assertTrue(result.contains("#EXTINF:-1,Line One Line Two")) + val parsed = M3uParser.parse(result) + assertEquals(1, parsed.size) + assertEquals("Line One Line Two", parsed[0].name) + } + @Test fun exportStationsToM3uFormatWithArtwork() { val stations = listOf( diff --git a/app/src/test/java/xyz/cottongin/radio247/data/importing/PlsParserTest.kt b/app/src/test/java/xyz/cottongin/radio247/data/importing/PlsParserTest.kt index f0e4723..72c21a1 100644 --- a/app/src/test/java/xyz/cottongin/radio247/data/importing/PlsParserTest.kt +++ b/app/src/test/java/xyz/cottongin/radio247/data/importing/PlsParserTest.kt @@ -56,6 +56,28 @@ class PlsParserTest { assertEquals("http://stream.example.com/radio", result[0].url) } + @Test + fun skipEntriesWithEmptyUrls() { + val content = """ + [playlist] + File1=http://stream1.example.com/radio + Title1=Valid Station + File2= + Title2=Empty URL Station + File3=http://stream3.example.com/radio + Title3=Another Valid + NumberOfEntries=3 + """.trimIndent() + + val result = PlsParser.parse(content) + + assertEquals(2, result.size) + assertEquals("Valid Station", result[0].name) + assertEquals("http://stream1.example.com/radio", result[0].url) + assertEquals("Another Valid", result[1].name) + assertEquals("http://stream3.example.com/radio", result[1].url) + } + @Test fun handleOutOfOrderEntries() { val content = """