mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-05-29 21:10:24 +02:00
fixed m3u8 parsing bug that caused Patreon video downloads to crash Grayjay
This commit is contained in:
parent
2bcd59cbfa
commit
f4610d0df5
@ -46,36 +46,53 @@ class HLS {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return MasterPlaylist(variantPlaylists, mediaRenditions, sessionDataList, independentSegments)
|
return MasterPlaylist(
|
||||||
|
variantPlaylists, mediaRenditions, sessionDataList, independentSegments
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseVariantPlaylist(content: String, sourceUrl: String): VariantPlaylist {
|
fun parseVariantPlaylist(content: String, sourceUrl: String): VariantPlaylist {
|
||||||
val lines = content.lines()
|
val lines = content.lines()
|
||||||
val version = lines.find { it.startsWith("#EXT-X-VERSION:") }?.substringAfter(":")?.toIntOrNull()
|
val version =
|
||||||
val targetDuration = lines.find { it.startsWith("#EXT-X-TARGETDURATION:") }?.substringAfter(":")?.toIntOrNull()
|
lines.find { it.startsWith("#EXT-X-VERSION:") }?.substringAfter(":")?.toIntOrNull()
|
||||||
val mediaSequence = lines.find { it.startsWith("#EXT-X-MEDIA-SEQUENCE:") }?.substringAfter(":")?.toLongOrNull()
|
val targetDuration =
|
||||||
val discontinuitySequence = lines.find { it.startsWith("#EXT-X-DISCONTINUITY-SEQUENCE:") }?.substringAfter(":")?.toIntOrNull()
|
lines.find { it.startsWith("#EXT-X-TARGETDURATION:") }?.substringAfter(":")
|
||||||
val programDateTime = lines.find { it.startsWith("#EXT-X-PROGRAM-DATE-TIME:") }?.substringAfter(":")?.let {
|
?.toIntOrNull()
|
||||||
ZonedDateTime.parse(it, DateTimeFormatter.ISO_DATE_TIME)
|
val mediaSequence =
|
||||||
}
|
lines.find { it.startsWith("#EXT-X-MEDIA-SEQUENCE:") }?.substringAfter(":")
|
||||||
val playlistType = lines.find { it.startsWith("#EXT-X-PLAYLIST-TYPE:") }?.substringAfter(":")
|
?.toLongOrNull()
|
||||||
val streamInfo = lines.find { it.startsWith("#EXT-X-STREAM-INF:") }?.let { parseStreamInfo(it) }
|
val discontinuitySequence =
|
||||||
|
lines.find { it.startsWith("#EXT-X-DISCONTINUITY-SEQUENCE:") }?.substringAfter(":")
|
||||||
|
?.toIntOrNull()
|
||||||
|
val programDateTime =
|
||||||
|
lines.find { it.startsWith("#EXT-X-PROGRAM-DATE-TIME:") }?.substringAfter(":")
|
||||||
|
?.let {
|
||||||
|
ZonedDateTime.parse(it, DateTimeFormatter.ISO_DATE_TIME)
|
||||||
|
}
|
||||||
|
val playlistType =
|
||||||
|
lines.find { it.startsWith("#EXT-X-PLAYLIST-TYPE:") }?.substringAfter(":")
|
||||||
|
val streamInfo =
|
||||||
|
lines.find { it.startsWith("#EXT-X-STREAM-INF:") }?.let { parseStreamInfo(it) }
|
||||||
|
|
||||||
val segments = mutableListOf<Segment>()
|
val segments = mutableListOf<Segment>()
|
||||||
var currentSegment: MediaSegment? = null
|
var currentSegment: MediaSegment? = null
|
||||||
lines.forEach { line ->
|
lines.forEach { line ->
|
||||||
when {
|
when {
|
||||||
line.startsWith("#EXTINF:") -> {
|
line.startsWith("#EXTINF:") -> {
|
||||||
val duration = line.substringAfter(":").substringBefore(",").toDoubleOrNull()
|
val duration =
|
||||||
?: throw Exception("Invalid segment duration format")
|
line.substringAfter(":").substringBefore(",").toDoubleOrNull()
|
||||||
|
?: throw Exception("Invalid segment duration format")
|
||||||
currentSegment = MediaSegment(duration = duration)
|
currentSegment = MediaSegment(duration = duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
line == "#EXT-X-DISCONTINUITY" -> {
|
line == "#EXT-X-DISCONTINUITY" -> {
|
||||||
segments.add(DiscontinuitySegment())
|
segments.add(DiscontinuitySegment())
|
||||||
}
|
}
|
||||||
line =="#EXT-X-ENDLIST" -> {
|
|
||||||
|
line == "#EXT-X-ENDLIST" -> {
|
||||||
segments.add(EndListSegment())
|
segments.add(EndListSegment())
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
currentSegment?.let {
|
currentSegment?.let {
|
||||||
it.uri = resolveUrl(sourceUrl, line)
|
it.uri = resolveUrl(sourceUrl, line)
|
||||||
@ -86,22 +103,51 @@ class HLS {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return VariantPlaylist(version, targetDuration, mediaSequence, discontinuitySequence, programDateTime, playlistType, streamInfo, segments)
|
return VariantPlaylist(
|
||||||
|
version,
|
||||||
|
targetDuration,
|
||||||
|
mediaSequence,
|
||||||
|
discontinuitySequence,
|
||||||
|
programDateTime,
|
||||||
|
playlistType,
|
||||||
|
streamInfo,
|
||||||
|
segments
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseAndGetVideoSources(source: Any, content: String, url: String): List<HLSVariantVideoUrlSource> {
|
fun parseAndGetVideoSources(
|
||||||
|
source: Any, content: String, url: String
|
||||||
|
): List<HLSVariantVideoUrlSource> {
|
||||||
val masterPlaylist: MasterPlaylist
|
val masterPlaylist: MasterPlaylist
|
||||||
try {
|
try {
|
||||||
masterPlaylist = parseMasterPlaylist(content, url)
|
masterPlaylist = parseMasterPlaylist(content, url)
|
||||||
return masterPlaylist.getVideoSources()
|
return masterPlaylist.getVideoSources()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
if (content.lines().any { it.startsWith("#EXTINF:") }) {
|
if (content.lines().any { it.startsWith("#EXTINF:") }) {
|
||||||
return if (source is IHLSManifestSource) {
|
return when (source) {
|
||||||
listOf(HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, url))
|
is IHLSManifestSource -> {
|
||||||
} else if (source is IHLSManifestAudioSource) {
|
listOf(
|
||||||
listOf()
|
HLSVariantVideoUrlSource(
|
||||||
} else {
|
"variant",
|
||||||
throw NotImplementedError()
|
0,
|
||||||
|
0,
|
||||||
|
"application/vnd.apple.mpegurl",
|
||||||
|
"",
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is IHLSManifestAudioSource -> {
|
||||||
|
listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw e
|
throw e
|
||||||
@ -109,19 +155,38 @@ class HLS {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseAndGetAudioSources(source: Any, content: String, url: String): List<HLSVariantAudioUrlSource> {
|
fun parseAndGetAudioSources(
|
||||||
|
source: Any, content: String, url: String
|
||||||
|
): List<HLSVariantAudioUrlSource> {
|
||||||
val masterPlaylist: MasterPlaylist
|
val masterPlaylist: MasterPlaylist
|
||||||
try {
|
try {
|
||||||
masterPlaylist = parseMasterPlaylist(content, url)
|
masterPlaylist = parseMasterPlaylist(content, url)
|
||||||
return masterPlaylist.getAudioSources()
|
return masterPlaylist.getAudioSources()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
if (content.lines().any { it.startsWith("#EXTINF:") }) {
|
if (content.lines().any { it.startsWith("#EXTINF:") }) {
|
||||||
return if (source is IHLSManifestSource) {
|
return when (source) {
|
||||||
listOf()
|
is IHLSManifestSource -> {
|
||||||
} else if (source is IHLSManifestAudioSource) {
|
listOf()
|
||||||
listOf(HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, url))
|
}
|
||||||
} else {
|
|
||||||
throw NotImplementedError()
|
is IHLSManifestAudioSource -> {
|
||||||
|
listOf(
|
||||||
|
HLSVariantAudioUrlSource(
|
||||||
|
"variant",
|
||||||
|
0,
|
||||||
|
"application/vnd.apple.mpegurl",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw e
|
throw e
|
||||||
@ -182,13 +247,14 @@ class HLS {
|
|||||||
|
|
||||||
private fun parseAttributes(content: String): Map<String, String> {
|
private fun parseAttributes(content: String): Map<String, String> {
|
||||||
val attributes = mutableMapOf<String, String>()
|
val attributes = mutableMapOf<String, String>()
|
||||||
val attributePairs = content.substringAfter(":").splitToSequence(',')
|
val maybeAttributePairs = content.substringAfter(":").splitToSequence(',')
|
||||||
|
|
||||||
var currentPair = StringBuilder()
|
var currentPair = StringBuilder()
|
||||||
for (pair in attributePairs) {
|
for (pair in maybeAttributePairs) {
|
||||||
currentPair.append(pair)
|
currentPair.append(pair)
|
||||||
if (currentPair.count { it == '\"' } % 2 == 0) { // Check if the number of quotes is even
|
if (currentPair.count { it == '\"' } % 2 == 0) { // Check if the number of quotes is even
|
||||||
val (key, value) = currentPair.toString().split('=')
|
val key = currentPair.toString().substringBefore("=")
|
||||||
|
val value = currentPair.toString().substringAfter("=")
|
||||||
attributes[key.trim()] = value.trim().removeSurrounding("\"")
|
attributes[key.trim()] = value.trim().removeSurrounding("\"")
|
||||||
currentPair = StringBuilder() // Reset for the next attribute
|
currentPair = StringBuilder() // Reset for the next attribute
|
||||||
} else {
|
} else {
|
||||||
@ -201,33 +267,30 @@ class HLS {
|
|||||||
|
|
||||||
private val _quoteList = listOf("GROUP-ID", "NAME", "URI", "CODECS", "AUDIO", "VIDEO")
|
private val _quoteList = listOf("GROUP-ID", "NAME", "URI", "CODECS", "AUDIO", "VIDEO")
|
||||||
private fun shouldQuote(key: String, value: String?): Boolean {
|
private fun shouldQuote(key: String, value: String?): Boolean {
|
||||||
if (value == null)
|
if (value == null) return false
|
||||||
return false;
|
|
||||||
|
|
||||||
if (value.contains(','))
|
if (value.contains(',')) return true
|
||||||
return true;
|
|
||||||
|
|
||||||
return _quoteList.contains(key)
|
return _quoteList.contains(key)
|
||||||
}
|
}
|
||||||
private fun appendAttributes(stringBuilder: StringBuilder, vararg attributes: Pair<String, String?>) {
|
|
||||||
attributes.filter { it.second != null }
|
private fun appendAttributes(
|
||||||
.joinToString(",") {
|
stringBuilder: StringBuilder, vararg attributes: Pair<String, String?>
|
||||||
|
) {
|
||||||
|
attributes.filter { it.second != null }.joinToString(",") {
|
||||||
val value = it.second
|
val value = it.second
|
||||||
"${it.first}=${if (shouldQuote(it.first, it.second)) "\"$value\"" else value}"
|
"${it.first}=${if (shouldQuote(it.first, it.second)) "\"$value\"" else value}"
|
||||||
}
|
}.let { if (it.isNotEmpty()) stringBuilder.append(it) }
|
||||||
.let { if (it.isNotEmpty()) stringBuilder.append(it) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SessionData(
|
data class SessionData(
|
||||||
val dataId: String,
|
val dataId: String, val value: String
|
||||||
val value: String
|
|
||||||
) {
|
) {
|
||||||
fun toM3U8Line(): String = buildString {
|
fun toM3U8Line(): String = buildString {
|
||||||
append("#EXT-X-SESSION-DATA:")
|
append("#EXT-X-SESSION-DATA:")
|
||||||
appendAttributes(this,
|
appendAttributes(
|
||||||
"DATA-ID" to dataId,
|
this, "DATA-ID" to dataId, "VALUE" to value
|
||||||
"VALUE" to value
|
|
||||||
)
|
)
|
||||||
append("\n")
|
append("\n")
|
||||||
}
|
}
|
||||||
@ -246,7 +309,8 @@ class HLS {
|
|||||||
) {
|
) {
|
||||||
fun toM3U8Line(): String = buildString {
|
fun toM3U8Line(): String = buildString {
|
||||||
append("#EXT-X-STREAM-INF:")
|
append("#EXT-X-STREAM-INF:")
|
||||||
appendAttributes(this,
|
appendAttributes(
|
||||||
|
this,
|
||||||
"BANDWIDTH" to bandwidth?.toString(),
|
"BANDWIDTH" to bandwidth?.toString(),
|
||||||
"RESOLUTION" to resolution,
|
"RESOLUTION" to resolution,
|
||||||
"CODECS" to codecs,
|
"CODECS" to codecs,
|
||||||
@ -273,7 +337,8 @@ class HLS {
|
|||||||
) {
|
) {
|
||||||
fun toM3U8Line(): String = buildString {
|
fun toM3U8Line(): String = buildString {
|
||||||
append("#EXT-X-MEDIA:")
|
append("#EXT-X-MEDIA:")
|
||||||
appendAttributes(this,
|
appendAttributes(
|
||||||
|
this,
|
||||||
"TYPE" to type,
|
"TYPE" to type,
|
||||||
"URI" to uri,
|
"URI" to uri,
|
||||||
"GROUP-ID" to groupID,
|
"GROUP-ID" to groupID,
|
||||||
@ -326,8 +391,20 @@ class HLS {
|
|||||||
height = resolutionTokens[1].toIntOrNull()
|
height = resolutionTokens[1].toIntOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
val suffix = listOf(it.streamInfo.video, it.streamInfo.codecs).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
|
val suffix = listOf(
|
||||||
HLSVariantVideoUrlSource(suffix, width ?: 0, height ?: 0, "application/vnd.apple.mpegurl", it.streamInfo.codecs ?: "", it.streamInfo.bandwidth, 0, false, it.url)
|
it.streamInfo.video, it.streamInfo.codecs
|
||||||
|
).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
|
||||||
|
HLSVariantVideoUrlSource(
|
||||||
|
suffix,
|
||||||
|
width ?: 0,
|
||||||
|
height ?: 0,
|
||||||
|
"application/vnd.apple.mpegurl",
|
||||||
|
it.streamInfo.codecs ?: "",
|
||||||
|
it.streamInfo.bandwidth,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
it.url
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -337,9 +414,19 @@ class HLS {
|
|||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
}
|
}
|
||||||
|
|
||||||
val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
|
val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }
|
||||||
|
.joinToString(", ")
|
||||||
return@mapNotNull when (it.type) {
|
return@mapNotNull when (it.type) {
|
||||||
"AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, it.uri)
|
"AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" }
|
||||||
|
?: "Audio (${suffix})",
|
||||||
|
0,
|
||||||
|
"application/vnd.apple.mpegurl",
|
||||||
|
"",
|
||||||
|
it.language ?: "",
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
it.uri)
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -351,9 +438,12 @@ class HLS {
|
|||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
}
|
}
|
||||||
|
|
||||||
val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
|
val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }
|
||||||
|
.joinToString(", ")
|
||||||
return@mapNotNull when (it.type) {
|
return@mapNotNull when (it.type) {
|
||||||
"SUBTITLE" -> HLSVariantSubtitleUrlSource(it.name?.ifEmpty { "Subtitle (${suffix})" } ?: "Subtitle (${suffix})", it.uri, "application/vnd.apple.mpegurl")
|
"SUBTITLE" -> HLSVariantSubtitleUrlSource(it.name?.ifEmpty { "Subtitle (${suffix})" }
|
||||||
|
?: "Subtitle (${suffix})", it.uri, "application/vnd.apple.mpegurl")
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -397,9 +487,8 @@ class HLS {
|
|||||||
abstract fun toM3U8Line(): String
|
abstract fun toM3U8Line(): String
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MediaSegment (
|
data class MediaSegment(
|
||||||
val duration: Double,
|
val duration: Double, var uri: String = ""
|
||||||
var uri: String = ""
|
|
||||||
) : Segment() {
|
) : Segment() {
|
||||||
override fun toM3U8Line(): String = buildString {
|
override fun toM3U8Line(): String = buildString {
|
||||||
append("#EXTINF:${duration},\n")
|
append("#EXTINF:${duration},\n")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user