diff --git a/README.md b/README.md index 94ff699..4ffef44 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Modern framework for parsing [m3u](https://en.wikipedia.org/wiki/M3U) files. - [x] Capable of parsing large playlists with hundreds of thousands of media items. - [x] Sync/Async parsing. - [x] Season/Episode number extraction for TV show media items. +- [x] Media kind extraction from URL path. ## Usage @@ -85,6 +86,7 @@ Playlist Media ├── duration ├── attributes +├── kind ├── name └── url ``` @@ -103,6 +105,14 @@ Attributes └── episodeNumber ``` +``` +Kind +├── movie +├── series +├── live +└── unknown +``` + --- ## Installation @@ -115,7 +125,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.6.0") + .package(url: "https://github.com/omaralbeik/M3UKit.git", from: "0.7.0") ] ``` @@ -130,7 +140,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.6.0' +pod 'M3UKit', :git => 'https://github.com/omaralbeik/M3UKit.git', :tag => '0.7.0' ``` ### Carthage @@ -138,7 +148,7 @@ pod 'M3UKit', :git => 'https://github.com/omaralbeik/M3UKit.git', :tag => '0.6.0 To integrate M3UKit into your Xcode project using [Carthage](https://github.com/Carthage/Carthage), specify it in your Cartfile: ``` -github "omaralbeik/M3UKit" ~> 0.6.0 +github "omaralbeik/M3UKit" ~> 0.7.0 ``` ### Manually diff --git a/Sources/M3UKit/Models/Media.swift b/Sources/M3UKit/Models/Media.swift index fa419e6..893051b 100644 --- a/Sources/M3UKit/Models/Media.swift +++ b/Sources/M3UKit/Models/Media.swift @@ -32,10 +32,15 @@ extension Playlist { name: String ) - init(metadata: Metadata, url: URL) { + init( + metadata: Metadata, + kind: Kind, + url: URL + ) { self.init( duration: metadata.duration, attributes: metadata.attributes, + kind: kind, name: metadata.name, url: url ) @@ -45,16 +50,19 @@ extension Playlist { /// - Parameters: /// - duration: duration. /// - attributes: attributes. + /// - kind: kind. /// - name: name. /// - url: url. public init( duration: Int, attributes: Attributes, + kind: Kind, name: String, url: URL ) { self.duration = duration self.attributes = attributes + self.kind = kind self.name = name self.url = url } @@ -65,6 +73,9 @@ extension Playlist { /// Attributes. public var attributes: Attributes + /// Kind. + public var kind: Kind + /// Media name. public var name: String diff --git a/Sources/M3UKit/Models/MediaKind.swift b/Sources/M3UKit/Models/MediaKind.swift new file mode 100644 index 0000000..0819a4e --- /dev/null +++ b/Sources/M3UKit/Models/MediaKind.swift @@ -0,0 +1,34 @@ +// +// M3UKit +// +// Copyright (c) 2022 Omar Albeik +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +extension Playlist.Media { + /// Enum representing media kind. + public enum Kind: Equatable, Hashable, Codable { + case movie + case series + case live + case unknown + } +} diff --git a/Sources/M3UKit/Parsers/MediaAttributesParser.swift b/Sources/M3UKit/Parsers/MediaAttributesParser.swift index f9ee7d9..2a798be 100644 --- a/Sources/M3UKit/Parsers/MediaAttributesParser.swift +++ b/Sources/M3UKit/Parsers/MediaAttributesParser.swift @@ -24,13 +24,13 @@ final class MediaAttributesParser: Parser { typealias Attributes = Playlist.Media.Attributes - func parse(_ input: String) throws -> Attributes { + func parse(_ input: String) -> Attributes { var attributes = Attributes() if let id = idRegex.firstMatch(in: input) { attributes.id = id } if let name = nameRegex.firstMatch(in: input) { - let show = try seasonEpisodeParser.parse(name) + let show = seasonEpisodeParser.parse(name) attributes.name = show.name attributes.seasonNumber = show.se?.s attributes.episodeNumber = show.se?.e diff --git a/Sources/M3UKit/Parsers/MediaKindParser.swift b/Sources/M3UKit/Parsers/MediaKindParser.swift new file mode 100644 index 0000000..e515666 --- /dev/null +++ b/Sources/M3UKit/Parsers/MediaKindParser.swift @@ -0,0 +1,44 @@ +// +// M3UKit +// +// Copyright (c) 2022 Omar Albeik +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +final class MediaKindParser: Parser { + func parse(_ input: URL) -> Playlist.Media.Kind { + let string = input.absoluteString + if seriesRegex.numberOfMatches(source: string) == 1 { + return .series + } + if moviesRegex.numberOfMatches(source: string) == 1 { + return .movie + } + if liveRegex.numberOfMatches(source: string) == 1 { + return .live + } + return .unknown + } + + let moviesRegex: RegularExpression = #"\/movies\/"# + let seriesRegex: RegularExpression = #"\/series\/"# + let liveRegex: RegularExpression = #"\/live\/"# +} diff --git a/Sources/M3UKit/Parsers/MediaMetadataParser.swift b/Sources/M3UKit/Parsers/MediaMetadataParser.swift index dd0f62a..f7c71b6 100644 --- a/Sources/M3UKit/Parsers/MediaMetadataParser.swift +++ b/Sources/M3UKit/Parsers/MediaMetadataParser.swift @@ -37,8 +37,8 @@ final class MediaMetadataParser: Parser { func parse(_ input: (line: Int, rawString: String)) throws -> Playlist.Media.Metadata { let duration = try extractDuration(input) - let attributes = try attributesParser.parse(input.rawString) - let name = try seasonEpisodeParser.parse(extractName(input.rawString)).name + let attributes = attributesParser.parse(input.rawString) + let name = seasonEpisodeParser.parse(extractName(input.rawString)).name return (duration, attributes, name) } diff --git a/Sources/M3UKit/Parsers/PlaylistParser.swift b/Sources/M3UKit/Parsers/PlaylistParser.swift index 38f66a6..23c65be 100644 --- a/Sources/M3UKit/Parsers/PlaylistParser.swift +++ b/Sources/M3UKit/Parsers/PlaylistParser.swift @@ -45,6 +45,7 @@ public final class PlaylistParser: Parser { var medias: [Playlist.Media] = [] let metadataParser = MediaMetadataParser() + let kindParser = MediaKindParser() var lastMetadataLine: String? var lastURL: URL? var mediaMetadataParsingError: Error? @@ -60,7 +61,8 @@ public final class PlaylistParser: Parser { if let metadataLine = lastMetadataLine, let url = lastURL { do { let metadata = try metadataParser.parse((lineNumber, metadataLine)) - medias.append(.init(metadata: metadata, url: url)) + let kind = kindParser.parse(url) + medias.append(.init(metadata: metadata, kind: kind, url: url)) lastMetadataLine = nil lastURL = nil } catch { @@ -90,6 +92,7 @@ public final class PlaylistParser: Parser { let rawString = try extractRawString(from: input) let metadataParser = MediaMetadataParser() + let kindParser = MediaKindParser() var lastMetadataLine: String? var lastURL: URL? var mediaMetadataParsingError: Error? @@ -105,7 +108,8 @@ public final class PlaylistParser: Parser { if let metadataLine = lastMetadataLine, let url = lastURL { do { let metadata = try metadataParser.parse((lineNumber, metadataLine)) - handler(.init(metadata: metadata, url: url)) + let kind = kindParser.parse(url) + handler(.init(metadata: metadata, kind: kind, url: url)) lastMetadataLine = nil lastURL = nil } catch { diff --git a/Sources/M3UKit/Parsers/RegularExpression.swift b/Sources/M3UKit/Parsers/RegularExpression.swift index e669331..85a9633 100644 --- a/Sources/M3UKit/Parsers/RegularExpression.swift +++ b/Sources/M3UKit/Parsers/RegularExpression.swift @@ -30,6 +30,11 @@ struct RegularExpression { self.regex = regex } + func numberOfMatches(source: String) -> Int { + let sourceRange = NSRange(source.startIndex.. String? { let sourceRange = NSRange(source.startIndex.. Show { + func parse(_ input: String) -> Show { let ranges = seasonEpisodeRegex.matchingRanges(in: input) guard ranges.count == 3, diff --git a/Tests/M3UKitTests/MediaAttributesTests.swift b/Tests/M3UKitTests/MediaAttributesTests.swift index fe975a6..4bb617c 100644 --- a/Tests/M3UKitTests/MediaAttributesTests.swift +++ b/Tests/M3UKitTests/MediaAttributesTests.swift @@ -62,13 +62,13 @@ final class MediaAttributesTests: XCTestCase { XCTAssertEqual(attributes.episodeNumber, episodeNumber) } - func testParsing() throws { + func testParsing() { let rawMedia = """ #EXTINF:-1 tvg-name="DWEnglish.de" tvg-id="DWEnglish.de" tvg-country="INT" tvg-language="English" tvg-logo="https://i.imgur.com/A1xzjOI.png" tvg-chno="1" tvg-shift="0" group-title="News",DW English (1080p) https://dwamdstream102.akamaized.net/hls/live/2015525/dwstream102/index.m3u8 """ let parser = MediaAttributesParser() - let attributes = try parser.parse(rawMedia) + let attributes = parser.parse(rawMedia) XCTAssertEqual(attributes.name, "DWEnglish.de") XCTAssertEqual(attributes.id, "DWEnglish.de") XCTAssertEqual(attributes.country, "INT") @@ -79,10 +79,10 @@ https://dwamdstream102.akamaized.net/hls/live/2015525/dwstream102/index.m3u8 XCTAssertEqual(attributes.groupTitle, "News") } - func testSeasonEpisodeParsing() throws { + func testSeasonEpisodeParsing() { let parser = SeasonEpisodeParser() let input = "Kyou Kara Ore Wa!! LIVE ACTION S01 E09" - let output = try parser.parse(input) + let output = parser.parse(input) XCTAssertEqual(output.name, "Kyou Kara Ore Wa!! LIVE ACTION") XCTAssertEqual(output.se?.s, 1) XCTAssertEqual(output.se?.e, 9) diff --git a/Tests/M3UKitTests/MediaKindTests.swift b/Tests/M3UKitTests/MediaKindTests.swift new file mode 100644 index 0000000..b5d251b --- /dev/null +++ b/Tests/M3UKitTests/MediaKindTests.swift @@ -0,0 +1,35 @@ +// +// M3UKit +// +// Copyright (c) 2022 Omar Albeik +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import XCTest +@testable import M3UKit + +final class MediaKindTests: XCTestCase { + func testParsing() { + let parser = MediaKindParser() + XCTAssertEqual(parser.parse(URL(string: "test.com/movies/123456")!), .movie) + XCTAssertEqual(parser.parse(URL(string: "test.com/live/123456")!), .live) + XCTAssertEqual(parser.parse(URL(string: "test.com/series/123456")!), .series) + XCTAssertEqual(parser.parse(URL(string: "test.com/123456")!), .unknown) + } +} diff --git a/Tests/M3UKitTests/MediaTests.swift b/Tests/M3UKitTests/MediaTests.swift index ffba29e..ece6a15 100644 --- a/Tests/M3UKitTests/MediaTests.swift +++ b/Tests/M3UKitTests/MediaTests.swift @@ -29,11 +29,13 @@ final class MediaTests: XCTestCase { let duration = 0 let attributes = Playlist.Media.Attributes() let name = "name" + let kind = Playlist.Media.Kind.live let url = URL(string: "https://not.a/real/url")! let media = Playlist.Media( duration: duration, attributes: attributes, + kind: kind, name: name, url: url ) @@ -41,6 +43,7 @@ final class MediaTests: XCTestCase { XCTAssertEqual(media.duration, duration) XCTAssertEqual(media.attributes, attributes) XCTAssertEqual(media.name, name) + XCTAssertEqual(media.kind, kind) XCTAssertEqual(media.url, url) }