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

Commit

Permalink
Parse kind from URL path (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
omaralbeik authored Nov 13, 2022
1 parent 7049452 commit 40c7d2e
Show file tree
Hide file tree
Showing 12 changed files with 161 additions and 15 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -85,6 +86,7 @@ Playlist
Media
├── duration
├── attributes
├── kind
├── name
└── url
```
Expand All @@ -103,6 +105,14 @@ Attributes
└── episodeNumber
```

```
Kind
├── movie
├── series
├── live
└── unknown
```

---

## Installation
Expand All @@ -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")
]
```

Expand All @@ -130,15 +140,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.6.0'
pod 'M3UKit', :git => 'https://github.com/omaralbeik/M3UKit.git', :tag => '0.7.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.6.0
github "omaralbeik/M3UKit" ~> 0.7.0
```

### Manually
Expand Down
13 changes: 12 additions & 1 deletion Sources/M3UKit/Models/Media.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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
}
Expand All @@ -65,6 +73,9 @@ extension Playlist {
/// Attributes.
public var attributes: Attributes

/// Kind.
public var kind: Kind

/// Media name.
public var name: String

Expand Down
34 changes: 34 additions & 0 deletions Sources/M3UKit/Models/MediaKind.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
4 changes: 2 additions & 2 deletions Sources/M3UKit/Parsers/MediaAttributesParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions Sources/M3UKit/Parsers/MediaKindParser.swift
Original file line number Diff line number Diff line change
@@ -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\/"#
}
4 changes: 2 additions & 2 deletions Sources/M3UKit/Parsers/MediaMetadataParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
8 changes: 6 additions & 2 deletions Sources/M3UKit/Parsers/PlaylistParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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 {
Expand Down Expand Up @@ -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?
Expand All @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions Sources/M3UKit/Parsers/RegularExpression.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ struct RegularExpression {
self.regex = regex
}

func numberOfMatches(source: String) -> Int {
let sourceRange = NSRange(source.startIndex..<source.endIndex, in: source)
return regex.numberOfMatches(in: source, range: sourceRange)
}

func firstMatch(in source: String) -> String? {
let sourceRange = NSRange(source.startIndex..<source.endIndex, in: source)
guard
Expand Down
2 changes: 1 addition & 1 deletion Sources/M3UKit/Parsers/SeasonEpisodeParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
final class SeasonEpisodeParser: Parser {
typealias Show = (name: String, se: (s: Int, e: Int)?)

func parse(_ input: String) throws -> Show {
func parse(_ input: String) -> Show {
let ranges = seasonEpisodeRegex.matchingRanges(in: input)
guard
ranges.count == 3,
Expand Down
8 changes: 4 additions & 4 deletions Tests/M3UKitTests/MediaAttributesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)
Expand Down
35 changes: 35 additions & 0 deletions Tests/M3UKitTests/MediaKindTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
3 changes: 3 additions & 0 deletions Tests/M3UKitTests/MediaTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,21 @@ 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
)

XCTAssertEqual(media.duration, duration)
XCTAssertEqual(media.attributes, attributes)
XCTAssertEqual(media.name, name)
XCTAssertEqual(media.kind, kind)
XCTAssertEqual(media.url, url)
}

Expand Down

0 comments on commit 40c7d2e

Please sign in to comment.