Skip to content
This repository has been archived by the owner on Jun 15, 2024. It is now read-only.

Commit

Permalink
Fix a bug m3u files with #EXTVLCOPT lines fail to parse (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
omaralbeik authored Apr 8, 2022
1 parent ae21917 commit e2c12e1
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 38 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ The [Swift Package Manager](https://swift.org/package-manager/) is a tool for ma

```swift
dependencies: [
.package(url: "https://github.com/omaralbeik/M3UKit.git", from: "0.3.0")
.package(url: "https://github.com/omaralbeik/M3UKit.git", from: "0.4.0")
]
```

Expand All @@ -121,15 +121,15 @@ $ swift build
To integrate M3UKit into your Xcode project using [CocoaPods](https://cocoapods.org), specify it in your Podfile:

```rb
pod 'M3UKit', :git => 'https://github.com/omaralbeik/M3UKit.git', :tag => '0.3.0'
pod 'M3UKit', :git => 'https://github.com/omaralbeik/M3UKit.git', :tag => '0.4.0'
```

### Carthage

To integrate M3UKit into your Xcode project using [Carthage](https://github.com/Carthage/Carthage), specify it in your Cartfile:

```
github "omaralbeik/M3UKit" ~> 0.3.0
github "omaralbeik/M3UKit" ~> 0.4.0
```

### Manually
Expand Down
20 changes: 20 additions & 0 deletions Sources/M3UKit/Models/Channel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ import Foundation
extension Playlist {
/// Object representing a TV channel.
public struct Channel: Equatable, Hashable, Codable {
typealias Metadata = (
duration: Int,
attributes: Attributes,
name: String
)

init(metadata: Metadata, url: URL) {
self.init(
duration: metadata.duration,
attributes: metadata.attributes,
name: metadata.name,
url: url
)
}

/// Create a new channel object.
/// - Parameters:
/// - duration: duration.
Expand Down Expand Up @@ -55,5 +70,10 @@ extension Playlist {

/// Channel URL.
public var url: URL

/// Whether the channel is a live channel (its URL ends with .m3u8) or not.
public var isLive: Bool {
url.absoluteString.hasSuffix(".m3u8")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,37 +23,41 @@

import Foundation

final class ChannelParser: Parser {
final class ChannelMetadataParser: Parser {
enum ParsingError: LocalizedError {
case invalidChannel
case missingDuration(Int, String)

var errorDescription: String? {
switch self {
case .missingDuration(let lineNumber, let rawString):
return "Line \(lineNumber): Missing duration in line \"\(rawString)\""
}
}
}

func parse(_ input: (line: Int, rawString: String)) throws -> Playlist.Channel.Metadata {
let duration = try extractDuration(input)
let attributes = try attributesParser.parse(input.rawString)
let name = extractName(input.rawString)
return (duration, attributes, name)
}

func parse(_ input: (metadata: String, url: URL)) throws -> Playlist.Channel {
let duration = try extractDuration(input.metadata)
let attributes = try attributesParser.parse(input.metadata)
let name = extractName(input.metadata)
let url = input.url

return .init(
duration: duration,
attributes: attributes,
name: name,
url: url
)
func isInfoLine(_ input: String) -> Bool {
return input.starts(with: "#EXTINF:")
}

func extractDuration(_ metadata: String) throws -> Int {
func extractDuration(_ input: (line: Int, rawString: String)) throws -> Int {
guard
let match = durationRegex.firstMatch(in: metadata),
let match = durationRegex.firstMatch(in: input.rawString),
let duration = Int(match)
else {
throw ParsingError.invalidChannel
throw ParsingError.missingDuration(input.line, input.rawString)
}
return duration
}

func extractName(_ metadata: String) -> String {
return nameRegex.firstMatch(in: metadata) ?? ""
func extractName(_ input: String) -> String {
return nameRegex.firstMatch(in: input) ?? ""
}

let attributesParser = ChannelAttributesParser()
Expand Down
37 changes: 25 additions & 12 deletions Sources/M3UKit/Parsers/PlaylistParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import Foundation
public final class PlaylistParser: Parser {
enum ParsingError: LocalizedError {
case invalidSource

var errorDescription: String? {
"The playlist is invalid"
}
}

/// Create a new parser.
Expand All @@ -44,28 +48,37 @@ public final class PlaylistParser: Parser {
throw ParsingError.invalidSource
}

let channelParser = ChannelParser()

var channels: [Playlist.Channel] = []
var channelParsingError: Error?
var lastMetadata: String?

let metadataParser = ChannelMetadataParser()
var lastMetadataLine: String?
var lastURL: URL?
var channelMetadataParsingError: Error?
var lineNumber = 0

rawString.enumerateLines { line, stop in
if let url = URL(string: line) {
guard let metadata = lastMetadata else { return }
if metadataParser.isInfoLine(line) {
lastMetadataLine = line
} else if let url = URL(string: line) {
lastURL = url
}

if let metadataLine = lastMetadataLine, let url = lastURL {
do {
let channel = try channelParser.parse((metadata, url))
channels.append(channel)
let metadata = try metadataParser.parse((lineNumber, metadataLine))
channels.append(.init(metadata: metadata, url: url))
lastMetadataLine = nil
lastURL = nil
} catch {
channelParsingError = error
channelMetadataParsingError = error
stop = true
}
} else {
lastMetadata = line
}

lineNumber += 1
}

if let error = channelParsingError {
if let error = channelMetadataParsingError {
throw error
}

Expand Down
39 changes: 36 additions & 3 deletions Tests/M3UKitTests/ChannelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,48 @@ final class ChannelTests: XCTestCase {
XCTAssertEqual(channel.name, name)
XCTAssertEqual(channel.url, url)
}

func testIsLive() {
let liveChannel = Playlist.Channel(
duration: -1,
attributes: .init(),
name: "Channel",
url: URL(string: "https://cnn-cnninternational-1-de.samsung.wurl.com/manifest/playlist.m3u8")!
)
XCTAssert(liveChannel.isLive)

let channel = Playlist.Channel(
duration: -1,
attributes: .init(),
name: "Channel",
url: URL(string: "https://not.a/real/url")!
)
XCTAssertFalse(channel.isLive)
}

func testExtractingDuration() throws {
let parser = ChannelParser()
XCTAssertThrowsError(try parser.extractDuration("invalid"))
let parser = ChannelMetadataParser()
XCTAssertThrowsError(try parser.extractDuration((1, "invalid")))
}

func testExtractingName() throws {
let parser = ChannelParser()
let parser = ChannelMetadataParser()
XCTAssertEqual(parser.extractName("invalid"), "")
XCTAssertEqual(parser.extractName(",valid"), "valid")
}

func testIsInfoLine() {
let parser = ChannelMetadataParser()
XCTAssertTrue(parser.isInfoLine("#EXTINF:-1 tvg-id="))
XCTAssertFalse(parser.isInfoLine("#EXTVLCOPT:http-user-agent"))
}

func testErrorDescription() {
let error = ChannelMetadataParser.ParsingError.missingDuration(3, "invalid line")
XCTAssertEqual(
error.errorDescription,
"Line 3: Missing duration in line \"invalid line\""
)
}

}
5 changes: 5 additions & 0 deletions Tests/M3UKitTests/PlaylistTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ final class PlaylistTests: XCTestCase {
XCTAssertThrowsError(try parser.parse(InvalidSource()))
}

func testErrorDescription() {
let error = PlaylistParser.ParsingError.invalidSource
XCTAssertEqual(error.errorDescription, "The playlist is invalid")
}

func testParsingValidSourceWithACallback() {
let parser = PlaylistParser()
var channels: [Playlist.Channel] = []
Expand Down
2 changes: 1 addition & 1 deletion Tests/M3UKitTests/Resources/invalid.m3u
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#EXTM3U x-tvg-url="https://iptv-org.github.io/epg/guides/al/ipko.com.epg.xml,https://iptv-org.github.io/epg/guides/be/telenettv.be.epg.xml,https://iptv-org.github.io/epg/guides/bf/canalplus-afrique.com.epg.xml,https://iptv-org.github.io/epg/guides/ch/tv.blue.ch.epg.xml,https://iptv-org.github.io/epg/guides/dk/allente.se.epg.xml,https://iptv-org.github.io/epg/guides/gr/cosmote.gr.epg.xml,https://iptv-org.github.io/epg/guides/it/guidatv.sky.it.epg.xml,https://iptv-org.github.io/epg/guides/my/astro.com.my.epg.xml,https://iptv-org.github.io/epg/guides/nl/delta.nl.epg.xml,https://iptv-org.github.io/epg/guides/nl/ziggogo.tv.epg.xml,https://iptv-org.github.io/epg/guides/se/allente.se.epg.xml,https://iptv-org.github.io/epg/guides/tr/digiturk.com.tr.epg.xml,https://iptv-org.github.io/epg/guides/uk/bt.com.epg.xml"
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-country="INT" tvg-language="English" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="News",BBC News HD (720p) [Geo-blocked]
#EXTINF: tvg-id="BBCNews.uk" tvg-country="INT" tvg-language="English" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="News",BBC News HD (720p) [Geo-blocked]
https://cdnuk001.broadcastcdn.net/KUK-BBCNEWSHD/index.m3u8
#EXTINF:-1 tvg-id="BBCWorldNews.uk" tvg-country="INT" tvg-language="English" tvg-logo="https://i.imgur.com/Nx0BRdV.png" group-title="News",BBC World News (576p)
http://103.199.161.254/Content/bbcworld/Live/Channel(BBCworld)/index.m3u8
Expand Down

0 comments on commit e2c12e1

Please sign in to comment.