From 8406caf78ec02fd7556f5bcc170e53919861fd59 Mon Sep 17 00:00:00 2001 From: Omar Albeik <8127757+omaralbeik@users.noreply.github.com> Date: Tue, 19 Apr 2022 19:24:19 +0200 Subject: [PATCH] Add walking over a playlist (#5) --- README.md | 6 +- Sources/M3UKit/Parsers/PlaylistParser.swift | 64 ++++++++++++++++++--- Tests/M3UKitTests/PlaylistTests.swift | 26 +++++++++ 3 files changed, 86 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 28c371e..a25f77f 100644 --- a/README.md +++ b/README.md @@ -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.4.0") + .package(url: "https://github.com/omaralbeik/M3UKit.git", from: "0.5.0") ] ``` @@ -121,7 +121,7 @@ $ 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.4.0' +pod 'M3UKit', :git => 'https://github.com/omaralbeik/M3UKit.git', :tag => '0.5.0' ``` ### Carthage @@ -129,7 +129,7 @@ pod 'M3UKit', :git => 'https://github.com/omaralbeik/M3UKit.git', :tag => '0.4.0 To integrate M3UKit into your Xcode project using [Carthage](https://github.com/Carthage/Carthage), specify it in your Cartfile: ``` -github "omaralbeik/M3UKit" ~> 0.4.0 +github "omaralbeik/M3UKit" ~> 0.5.0 ``` ### Manually diff --git a/Sources/M3UKit/Parsers/PlaylistParser.swift b/Sources/M3UKit/Parsers/PlaylistParser.swift index d48b17b..ff47664 100644 --- a/Sources/M3UKit/Parsers/PlaylistParser.swift +++ b/Sources/M3UKit/Parsers/PlaylistParser.swift @@ -40,13 +40,7 @@ public final class PlaylistParser: Parser { /// - Parameter input: source. /// - Returns: playlist. public func parse(_ input: PlaylistSource) throws -> Playlist { - guard let rawString = input.rawString else { - throw ParsingError.invalidSource - } - - guard rawString.starts(with: "#EXTM3U") else { - throw ParsingError.invalidSource - } + let rawString = try extractRawString(from: input) var channels: [Playlist.Channel] = [] @@ -85,6 +79,48 @@ public final class PlaylistParser: Parser { return Playlist(channels: channels) } + /// Walk over a playlist and return its channels one-by-one. + /// - Parameters: + /// - input: source. + /// - handler: Handler to be called with the parsed channel. + public func walk( + _ input: PlaylistSource, + handler: @escaping (Playlist.Channel) -> Void + ) throws { + let rawString = try extractRawString(from: input) + + let metadataParser = ChannelMetadataParser() + var lastMetadataLine: String? + var lastURL: URL? + var channelMetadataParsingError: Error? + var lineNumber = 0 + + rawString.enumerateLines { line, stop in + if metadataParser.isInfoLine(line) { + lastMetadataLine = line + } else if let url = URL(string: line) { + lastURL = url + } + + if let metadataLine = lastMetadataLine, let url = lastURL { + do { + let metadata = try metadataParser.parse((lineNumber, metadataLine)) + handler(.init(metadata: metadata, url: url)) + lastMetadataLine = nil + lastURL = nil + } catch { + channelMetadataParsingError = error + stop = true + } + } + lineNumber += 1 + } + + if let error = channelMetadataParsingError { + throw error + } + } + /// Parse a playlist on a queue with a completion handler. /// - Parameters: /// - input: source. @@ -118,4 +154,18 @@ public final class PlaylistParser: Parser { } } } + + // MARK: - Helpers + + private func extractRawString(from input: PlaylistSource) throws -> String { + guard let rawString = input.rawString else { + throw ParsingError.invalidSource + } + + guard rawString.starts(with: "#EXTM3U") else { + throw ParsingError.invalidSource + } + + return rawString + } } diff --git a/Tests/M3UKitTests/PlaylistTests.swift b/Tests/M3UKitTests/PlaylistTests.swift index 74652ac..b28d765 100644 --- a/Tests/M3UKitTests/PlaylistTests.swift +++ b/Tests/M3UKitTests/PlaylistTests.swift @@ -42,6 +42,32 @@ final class PlaylistTests: XCTestCase { XCTAssertThrowsError(try parser.parse(InvalidSource())) } + func testWalking() throws { + let parser = PlaylistParser() + var channels: [Playlist.Channel] = [] + + let exp = expectation(description: "Walking succeeded") + let validURL = Bundle.module.url(forResource: "valid", withExtension: "m3u")! + try parser.walk(validURL) { channel in + channels.append(channel) + if channels.count == 105 { + exp.fulfill() + } + } + + waitForExpectations(timeout: 1) + XCTAssertEqual(channels.count, 105) + } + + func testWalkingInvalidSource() { + let parser = PlaylistParser() + XCTAssertThrowsError(try parser.walk("") { _ in }) + + let invalidURL = Bundle.module.url(forResource: "invalid", withExtension: "m3u")! + XCTAssertThrowsError(try parser.walk(invalidURL) { _ in }) + } + + func testErrorDescription() { let error = PlaylistParser.ParsingError.invalidSource XCTAssertEqual(error.errorDescription, "The playlist is invalid")