From ef085e4f85336f88b809b3c99035973d373de96d Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Wed, 26 Apr 2017 10:35:10 -0600 Subject: [PATCH 01/20] Switched to a guard statement instead of an if conditional without any elses, improved error message slightly --- Sources/Metadata.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Metadata.swift b/Sources/Metadata.swift index faa9a27..d01dfbf 100644 --- a/Sources/Metadata.swift +++ b/Sources/Metadata.swift @@ -135,9 +135,9 @@ class Metadata { private func hasDependencies() throws { #if os(Linux) let (rc, _) = execute("which exiftool") - if rc != 0 { + guard rc == 0 else { throw MetadataError.missingDependency(dependency: "exiftool", - helpText: "On Ubuntu systems, try installing the 'libimage-exiftool-perl' package.") + helpText: "On Ubuntu systems, try installing the 'libimage-exiftool-perl' package by running `sudo apt-get install -y libimage-exiftool-perl`") } #endif } From 8f4241ca86dd26352fe716608c3eb3f185d2cd9a Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Thu, 16 Nov 2017 16:57:40 -0700 Subject: [PATCH 02/20] Removed cocaopods, updated to swift 4 and swift-tools version 4.0, removed JSON dependency by implementing Codable --- .gitignore | 2 + .swift-version | 2 +- Downpour.podspec | 28 ------- Package.swift | 27 +++++-- README.md | 9 +-- Sources/{ => Downpour}/Downpour.swift | 23 +++--- Sources/{ => Downpour}/DownpourType.swift | 0 Sources/{ => Downpour}/Metadata.swift | 74 ++++++++++++------- .../String + CleanedString.swift | 12 ++- Tests/DownpourTests/DownpourTests.swift | 4 +- 10 files changed, 97 insertions(+), 84 deletions(-) delete mode 100644 Downpour.podspec rename Sources/{ => Downpour}/Downpour.swift (94%) rename Sources/{ => Downpour}/DownpourType.swift (100%) rename Sources/{ => Downpour}/Metadata.swift (86%) rename Sources/{ => Downpour}/String + CleanedString.swift (66%) diff --git a/.gitignore b/.gitignore index 196e2f7..2b3198d 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,8 @@ playground.xcworkspace Packages/ .build/ Package.pins +Package.resolved +.AppleDouble # CocoaPods # diff --git a/.swift-version b/.swift-version index 8c50098..5186d07 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -3.1 +4.0 diff --git a/Downpour.podspec b/Downpour.podspec deleted file mode 100644 index 3f093b7..0000000 --- a/Downpour.podspec +++ /dev/null @@ -1,28 +0,0 @@ -Pod::Spec.new do |s| - s.name = "Downpour" - s.version = "0.2.0" - s.summary = "Get TV & Movie info from downloaded filenames" - - s.description = <<-DESC -Downpour was built for [Fetch](http://getfetchapp.com) — a Put.io client — to parse TV & Movie information from downloaded files. It can gather the following from a raw file name: - -- TV or movie title -- Year of release -- TV season number -- TV episode number - DESC - - s.homepage = "https://github.com/steve228uk/Downpour" - # s.screenshots = "www.example.com/screenshots_1", "www.example.com/screenshots_2" - s.license = 'MIT' - s.author = { "Stephen Radford" => "steve228uk@gmail.com" } - s.source = { :git => "https://github.com/steve228uk/Downpour.git", :tag => s.version.to_s } - s.social_media_url = 'https://twitter.com/steve228uk' - - s.osx.deployment_target = '10.10' - s.ios.deployment_target = '8.0' - s.tvos.deployment_target = '9.0' - - s.source_files = 'Sources/**/*' - -end diff --git a/Package.swift b/Package.swift index 0968793..812f7ea 100644 --- a/Package.swift +++ b/Package.swift @@ -1,12 +1,25 @@ -import PackageDescription - -var dependencies: [Package.Dependency] = [.Package(url: "https://github.com/Ponyboy47/PathKit.git", majorVersion: 0, minor: 8)] +// swift-tools-version:4.0 -#if os(Linux) -dependencies.append(.Package(url: "https://github.com/vdka/JSON.git", majorVersion: 0, minor: 16)) -#endif +import PackageDescription let package = Package( name: "Downpour", - dependencies: dependencies + products: [ + .library(name: "Downpour", targets: ["Downpour"]) + ], + dependencies: [ + .package(url: "https://github.com/Ponyboy47/PathKit.git", .upToNextMinor(from: "0.9.0")) + ], + targets: [ + .target( + name: "Downpour", + dependencies: ["PathKit"], + path: "Sources" + ), + .testTarget( + name: "DownpourTests", + dependencies: ["Downpour"], + path: "Tests/DownpourTests" + ) + ] ) diff --git a/README.md b/README.md index d20859c..d930368 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,9 @@ if downpour.type == .TV { ## Installation -Install manually by copying the contents of the `Sources` directory to your project or install via CocoaPods. - -```ruby -pod 'Downpour' +Add to your project using the Swift Package Manager by adding the following dependency to your Package.swift: +```swift +.package(url: "https://github.com/Ponyboy47/Downpour.git", .upToNextMinor(from: "0.5.0")) ``` -**Note:** For Swift 2.3 please use `0.1.0` +For swift 3 use 0.4.x diff --git a/Sources/Downpour.swift b/Sources/Downpour/Downpour.swift similarity index 94% rename from Sources/Downpour.swift rename to Sources/Downpour/Downpour.swift index 5107d52..440f2ad 100644 --- a/Sources/Downpour.swift +++ b/Sources/Downpour/Downpour.swift @@ -36,22 +36,23 @@ open class Downpour: CustomStringConvertible { /// Both the season and the episode together. lazy open var seasonEpisode: String? = { + var matchedSubstring: Substring? = nil if let match = self.rawString.range(of: self.patterns["pretty"]!, options: [.regularExpression, .caseInsensitive]) { - return self.rawString[match] + matchedSubstring = self.rawString[match] } else if var match = self.rawString.range(of: self.patterns["tricky"]!, options: [.regularExpression, .caseInsensitive]) { match = self.rawString.index(after: match.lowerBound).. String? { - guard data.keys.contains(key) else { return nil } - return data[key] + subscript(key: String) -> String { + switch key { + case AVMetadataCommonKeyTitle: + return title + // case AVMetadataCommonKeyPublisher: + // return publisher + case AVMetadataCommonKeyCreationDate: + return creationDate + case AVMetadataCommonKeyType: + return type + case AVMetadataCommonKeyFormat: + return format + case AVMetadataCommonKeyCopyrights: + return copyrights + case AVMetadataCommonKeyAlbumName: + return albumName + case AVMetadataCommonKeyArtist: + return artist + case AVMetadataCommonKeyArtwork: + return artwork + default: + return "" + } } } @@ -221,10 +238,11 @@ class Metadata { guard let stdout = output.stdout else { throw MetadataError.couldNotGetMetadata(error: "File does not contain any metadata") } - metadataJSON = try Metadata(stdout) + + metadataJSON = try JSONDecoder().decode(Metadata.self, from: stdout.data(using: .utf8)!) } // Try and retrieve the specified property - guard let property = metadataJSON?.get(key) else { + guard let property = metadataJSON?[key] else { // Throw an error if the key doesn't exist throw MetadataError.missingMetadataKey(key: key) } diff --git a/Sources/String + CleanedString.swift b/Sources/Downpour/String + CleanedString.swift similarity index 66% rename from Sources/String + CleanedString.swift rename to Sources/Downpour/String + CleanedString.swift index 846dfb4..a76ac5c 100644 --- a/Sources/String + CleanedString.swift +++ b/Sources/Downpour/String + CleanedString.swift @@ -8,8 +8,16 @@ import Foundation -extension String { +extension Substring { + var cleanedString: String { + var cleaned = String(self) + cleaned = cleaned.trimmingCharacters(in: CharacterSet(charactersIn: " -.([]{}))_")) + cleaned = cleaned.replacingOccurrences(of: ".", with: " ") + return cleaned + } +} +extension String { var cleanedString: String { var cleaned = self cleaned = cleaned.trimmingCharacters(in: CharacterSet(charactersIn: " -.([]{}))_")) @@ -17,7 +25,7 @@ extension String { return cleaned } - subscript (r: CountableClosedRange) -> String { + subscript (r: CountableClosedRange) -> Substring { get { let startIndex = self.index(self.startIndex, offsetBy: r.lowerBound) let endIndex = self.index(startIndex, offsetBy: r.upperBound - r.lowerBound) diff --git a/Tests/DownpourTests/DownpourTests.swift b/Tests/DownpourTests/DownpourTests.swift index e38fcb4..e7b8f72 100644 --- a/Tests/DownpourTests/DownpourTests.swift +++ b/Tests/DownpourTests/DownpourTests.swift @@ -23,9 +23,9 @@ class DownpourTests: XCTestCase { } func testMovie1() { - let downpour = Downpour(name: "Movie.Name.2015.1080p.mp4") + let downpour = Downpour(name: "Movie.Name.2013.1080p.BluRay.H264.AAC.mp4") XCTAssertEqual(downpour.title, "Movie Name") - XCTAssertEqual(downpour.year, "2015") + XCTAssertEqual(downpour.year, "2013") XCTAssertNil(downpour.season) XCTAssertNil(downpour.episode) XCTAssertEqual(downpour.type, .some(.movie)) From a182b26b4f941530a1065daf72428a6809a4adda Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Mon, 20 Nov 2017 12:48:54 -0700 Subject: [PATCH 03/20] Removed unnecessary string.characters and fixed compilation for macOS/Linux with swift 4 --- Sources/Downpour/Downpour.swift | 21 +++--- Sources/Downpour/Metadata.swift | 113 ++++++++++++++------------------ 2 files changed, 59 insertions(+), 75 deletions(-) diff --git a/Sources/Downpour/Downpour.swift b/Sources/Downpour/Downpour.swift index 440f2ad..0918c6a 100644 --- a/Sources/Downpour/Downpour.swift +++ b/Sources/Downpour/Downpour.swift @@ -58,25 +58,24 @@ open class Downpour: CustomStringConvertible { /// The TV Season - e.g. 02 lazy open var season: String? = { if let both = self.seasonEpisode?.cleanedString { - guard both.characters.count <= 7 else { + guard both.count <= 7 else { let match = self.rawString.range(of: self.patterns["altSeasonSingle"]!, options: [.regularExpression, .caseInsensitive]) let string = String(self.rawString[match!]) let startIndex = string.startIndex - let endIndex = string.characters.index(string.startIndex, offsetBy: 6) + let endIndex = string.index(string.startIndex, offsetBy: 6) return string.replacingCharacters(in: startIndex..= 1 else { + guard pieces[0].count <= 2 && pieces[0].count >= 1 else { let startIndex = pieces[0].index(after: pieces[0].startIndex) return pieces[0][startIndex.. String { + subscript(key: AVMetadataKey) -> String { switch key { - case AVMetadataCommonKeyTitle: + case .commonKeyTitle: return title - // case AVMetadataCommonKeyPublisher: - // return publisher - case AVMetadataCommonKeyCreationDate: + case .commonKeyCreationDate: return creationDate - case AVMetadataCommonKeyType: + case .commonKeyType: return type - case AVMetadataCommonKeyFormat: + case .commonKeyFormat: return format - case AVMetadataCommonKeyCopyrights: + case .commonKeyCopyrights: return copyrights - case AVMetadataCommonKeyAlbumName: + case .commonKeyAlbumName: return albumName - case AVMetadataCommonKeyArtist: + case .commonKeyArtist: return artist - case AVMetadataCommonKeyArtwork: + case .commonKeyArtwork: return artwork default: return "" @@ -203,7 +189,7 @@ class Metadata { - Returns: The string value of the common metadata, or nil. If an error occured, this will print it out */ - public func getCommonMetadata(_ key: String) -> String? { + public subscript(_ key: AVMetadataKey) -> String? { do { // Try and return the Common Metadata value return try getCM(key) @@ -221,7 +207,7 @@ class Metadata { - Returns: The string value of the common metadata, or nil. */ - private func getCM(_ key: String) throws -> String? { + private func getCM(_ key: AVMetadataKey) throws -> String? { #if os(Linux) // If we're running Linux, check to see if we've saved an exiftool metadata JSON object if metadataJSON == nil { @@ -244,7 +230,7 @@ class Metadata { // Try and retrieve the specified property guard let property = metadataJSON?[key] else { // Throw an error if the key doesn't exist - throw MetadataError.missingMetadataKey(key: key) + throw MetadataError.missingMetadataKey(key: key.rawValue) } // Otherwise, return the property return property @@ -265,10 +251,11 @@ class Metadata { let metadata = AVMetadataItem.metadataItems(from: metadataItems!, withKey: key, keySpace: nil) // Throws an error if there is no common metadata for the key guard metadata.count > 0 else { - throw MetadataError.missingMetadataKey(key: key) + throw MetadataError.missingMetadataKey(key: key.rawValue) } // Return the first metadata item's string value return metadata.first?.stringValue #endif } } + From 9383290a3ad47ce87f6b2bfac5b2ac14927a1c00 Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Wed, 9 May 2018 10:43:53 -0600 Subject: [PATCH 04/20] Update to swift 4.1 --- .gitignore | 5 +++-- .swift-version | 2 +- Package.swift | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 2b3198d..b8f330e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,8 +31,9 @@ xcuserdata timeline.xctimeline playground.xcworkspace -## Vim swap files -*.swp +## Vim swap and session files +*.sw[po] +*.vimsession # Swift Package Manager # diff --git a/.swift-version b/.swift-version index 5186d07..7d5c902 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -4.0 +4.1 diff --git a/Package.swift b/Package.swift index 812f7ea..faad7d2 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,7 @@ let package = Package( .library(name: "Downpour", targets: ["Downpour"]) ], dependencies: [ - .package(url: "https://github.com/Ponyboy47/PathKit.git", .upToNextMinor(from: "0.9.0")) + .package(url: "https://github.com/Ponyboy47/PathKit.git", .upToNextMinor(from: "0.10.0")) ], targets: [ .target( From 28307651359cb76754b2592b14453f11315b5f0e Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Wed, 9 May 2018 10:54:19 -0600 Subject: [PATCH 05/20] Add support for 4-digit year based seasons --- Sources/Downpour/Downpour.swift | 48 ++++++++++--------- Sources/Downpour/String + CleanedString.swift | 4 ++ Tests/DownpourTests/DownpourTests.swift | 19 +++++++- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/Sources/Downpour/Downpour.swift b/Sources/Downpour/Downpour.swift index 0918c6a..e27e69f 100644 --- a/Sources/Downpour/Downpour.swift +++ b/Sources/Downpour/Downpour.swift @@ -23,31 +23,33 @@ open class Downpour: CustomStringConvertible { }() /// The patterns that will be used to fetch various pieces of information from the rawString. - let patterns: [String: String] = [ - "pretty": "S\\d{1,2}[\\-\\.\\s_]?E\\d{1,2}", - "tricky": "[^\\d]\\d{1,2}[X\\-\\.\\s_]\\d{1,2}([^\\d]|$)", - "combined": "(?:S)?\\d{1,2}[EX\\-\\.\\s_]\\d{1,2}([^\\d]|$)", - "altSeason": "Season \\d{1,2} Episode \\d{1,2}", - "altSeasonSingle": "Season \\d{1,2}", - "altEpisodeSingle": "Episode \\d{1,2}", - "altSeason2": "[\\s_\\.\\-\\[]\\d{3}[\\s_\\.\\-\\]]", - "year": "[\\(?:\\.\\s_\\[](?:19|(?:[2-9])(?:[0-9]))\\d{2}[\\]\\s_\\.\\)]" - ] + enum Pattern: String { + case pretty = "S(\\d{4}|\\d{1,2})[\\-\\.\\s_]?E\\d{1,2}" + case tricky = "[^\\d](\\d{4}|\\d{1,2})[X\\-\\.\\s_]\\d{1,2}([^\\d]|$)" + case combined = "(?:S)?(\\d{4}|\\d{1,2})[EX\\-\\.\\s_]\\d{1,2}([^\\d]|$)" + case altSeason = "Season (\\d{4}|\\d{1,2}) Episode \\d{1,2}" + case altSeasonSingle = "Season (\\d{4}|\\d{1,2})" + case altEpisodeSingle = "Episode \\d{1,2}" + case altSeason2 = "[\\s_\\.\\-\\[]\\d{3}[\\s_\\.\\-\\]]" + case year = "[\\(?:\\.\\s_\\[](?:19|(?:[2-9])(?:[0-9]))\\d{2}[\\]\\s_\\.\\)]" + } + + static let regexOptions: String.CompareOptions = [.regularExpression, .caseInsensitive] /// Both the season and the episode together. lazy open var seasonEpisode: String? = { var matchedSubstring: Substring? = nil - if let match = self.rawString.range(of: self.patterns["pretty"]!, options: [.regularExpression, .caseInsensitive]) { + if let match = self.rawString.range(of: Pattern.pretty, options: Downpour.regexOptions) { matchedSubstring = self.rawString[match] - } else if var match = self.rawString.range(of: self.patterns["tricky"]!, options: [.regularExpression, .caseInsensitive]) { + } else if var match = self.rawString.range(of: Pattern.tricky, options: Downpour.regexOptions) { match = self.rawString.index(after: match.lowerBound).. Range? { + return self.range(of: pattern.rawValue, options: options) + } + subscript (r: CountableClosedRange) -> Substring { get { let startIndex = self.index(self.startIndex, offsetBy: r.lowerBound) diff --git a/Tests/DownpourTests/DownpourTests.swift b/Tests/DownpourTests/DownpourTests.swift index e7b8f72..31aac74 100644 --- a/Tests/DownpourTests/DownpourTests.swift +++ b/Tests/DownpourTests/DownpourTests.swift @@ -9,7 +9,6 @@ import XCTest @testable import Downpour - class DownpourTests: XCTestCase { override func setUp() { @@ -130,6 +129,22 @@ class DownpourTests: XCTestCase { XCTAssertNil(downpour.year) } + func testStandardShow11() { + let downpour = Downpour(name: "Show Name - s2005e01") + XCTAssertEqual(downpour.title, "Show Name") + XCTAssertEqual(downpour.season, "2005") + XCTAssertEqual(downpour.episode, "01") + XCTAssertEqual(downpour.type, .some(.tv)) + } + + func testStandardShow12() { + let downpour = Downpour(name: "Show Name - s05e01") + XCTAssertEqual(downpour.title, "Show Name") + XCTAssertEqual(downpour.season, "05") + XCTAssertEqual(downpour.episode, "01") + XCTAssertEqual(downpour.type, .some(.tv)) + } + func testFOVShow1() { let downpour = Downpour(name: "Show_Name.1x02.Source_Quality_Etc-Group") XCTAssertEqual(downpour.title, "Show_Name") // FIXME @@ -201,6 +216,8 @@ extension DownpourTests { ("testStandardShow8", testStandardShow8), ("testStandardShow9", testStandardShow9), ("testStandardShow10", testStandardShow10), + ("testStandardShow11", testStandardShow11), + ("testStandardShow12", testStandardShow12), ("testFOVShow1", testFOVShow1), ("testFOVShow2", testFOVShow2), ("testFOVShow3", testFOVShow3), From 6a6a52e27e35ddd29772010f5eeed4abb4647241 Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Tue, 11 Sep 2018 13:59:48 -0600 Subject: [PATCH 06/20] WIP --- .swift-version | 2 +- Package.swift | 5 +- README.md | 51 +- Sources/Downpour/AVMetadata.swift | 47 ++ Sources/Downpour/Downpour+Protocol.swift | 42 ++ Sources/Downpour/Downpour.swift | 25 +- Sources/Downpour/DownpourType.swift | 14 - Sources/Downpour/ExifTool.swift | 85 ++++ Sources/Downpour/Metadata.swift | 444 ++++++++---------- Sources/Downpour/String + CleanedString.swift | 2 +- Sources/Downpour/_Metadata.swift | 260 ++++++++++ 11 files changed, 700 insertions(+), 277 deletions(-) create mode 100644 Sources/Downpour/AVMetadata.swift create mode 100644 Sources/Downpour/Downpour+Protocol.swift delete mode 100644 Sources/Downpour/DownpourType.swift create mode 100644 Sources/Downpour/ExifTool.swift create mode 100644 Sources/Downpour/_Metadata.swift diff --git a/.swift-version b/.swift-version index 7d5c902..0597c57 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -4.1 +4.2-CONVERGENCE diff --git a/Package.swift b/Package.swift index faad7d2..c8fe0fe 100644 --- a/Package.swift +++ b/Package.swift @@ -8,12 +8,13 @@ let package = Package( .library(name: "Downpour", targets: ["Downpour"]) ], dependencies: [ - .package(url: "https://github.com/Ponyboy47/PathKit.git", .upToNextMinor(from: "0.10.0")) + .package(url: "https://github.com/Ponyboy47/TrailBlazer.git", from: "0.11.0"), + .package(url: "https://github.com/kareman/SwiftShell.git", from: "4.1.0") ], targets: [ .target( name: "Downpour", - dependencies: ["PathKit"], + dependencies: ["TrailBlazer", "SwiftShell"], path: "Sources" ), .testTarget( diff --git a/README.md b/README.md index d930368..e80eae8 100644 --- a/README.md +++ b/README.md @@ -3,34 +3,71 @@ Downpour was built for [Fetch](http://getfetchapp.com) — a Put.io client — to parse TV & Movie information from downloaded files. It can be used on any platform that can run Swift as it only relies on Foundation. -It can gather the following from a raw file name: +It can gather the following from a raw video file name: - TV or movie title - Year of release - TV season number - TV episode number +It can gather the following from an audio file on macOS: + +- Title +- Creation Date +- Type +- Format +- Copyrights +- Album +- Artist +- Artwork +- Publisher +- Creator +- Subject +- Summary (AKA Description) +- Contributer +- Last Modified Date +- Language +- Author + +And from Linux (Ubuntu if the libimage-exiftool-perl package is installed): + +- Title +- Creation Date +- Type +- Format +- Copyrights +- Album +- Artist +- Artwork + +NOTE: None of the fields are guaranteed to be there or even picked up, it's kinda hard to extract metadata from file names with only a few clever regexes and audio data from files is difficult to do cross-platform. Please open an issue if you know the data is there, but it's not being picked up. Also, it means everything is Optional and be sure to use `guard/if let` or nil-coalescing (`??`) to program safely. :) + ## Usage Using Downpour is easy. Just create a new instance and it'll do the rest. ```swift -let torrent = Downpour(string: filename) +let dvd_rip = Downpour(string: filename) -let title = torrent.title -let year = torrent.year +let title = dvd_rip.title +let year = dvd_rip.year if downpour.type == .TV { - let season = torrent.season - let episode = torrent.episode + let season = dvd_rip.season + let episode = dvd_rip.episode } ``` +### Common Scenarios: +- Backing up your dvd/blu-ray collection + - Designed to work with media ripped using the popular [MakeMKV](http://makemkv.com) utility +- Organizing your media files + ## Installation Add to your project using the Swift Package Manager by adding the following dependency to your Package.swift: ```swift -.package(url: "https://github.com/Ponyboy47/Downpour.git", .upToNextMinor(from: "0.5.0")) +.package(url: "https://github.com/Ponyboy47/Downpour.git", from: "0.8.0") ``` For swift 3 use 0.4.x diff --git a/Sources/Downpour/AVMetadata.swift b/Sources/Downpour/AVMetadata.swift new file mode 100644 index 0000000..be143a5 --- /dev/null +++ b/Sources/Downpour/AVMetadata.swift @@ -0,0 +1,47 @@ +#if !os(Linux) +import Foundation +import AVFoundation + +public class AVMetadata: Metadata { + public lazy var title: String = { return self[.commonKeyTitle] ?? path.string }() + public let type: MetadataType = .audio + public lazy var creationDate: Date? = { + guard let dateString = self.creationDateString else { return nil } + return self.dateFormatter.date(from: dateString) + } + public lazy var creationDateString: String? = { return self[.commonKeyCreationDate] }() + public lazy var format: String? = { return self[.commonKeyFormat] }() + public lazy var copyrights: String? = { return self[.commonKeyCopyrights] }() + public lazy var album: String? = { return self[.commonKeyAlbumName] }() + public lazy var artist: String? = { return self[.commonKeyArtist] }() + public lazy var artwork: String? = { return self[.commonKeyArtwork] }() + public lazy var publisher: String? = { return self[.commonKeyPublisher] }() + public lazy var creator: String? = { return self[.commonKeyCreator] }() + public lazy var subject: String? = { return self[.commonKeySubject] }() + public lazy var summary: String? = { return self[.commonKeyDescription] }() + public lazy var lastModifiedDate: Date? = { + guard let dateString = self.lastModifiedDateString else { return nil } + return self.dateFormatter.date(from: dateString) + } + public lazy var lastModifiedDateString: String? = { return self[.commonKeyLastModifiedDate] }() + public lazy var language: String? = { return self[.commonKeyLanguage] }() + public lazy var author: String? = { return self[.commonKeyAuthor] }() + + private lazy var dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + return dateFormatter + }() + + private let metadata: [AVMetadataItem] + + public init(file path: FilePath) { + let asset = AVAsset(url: path.url) + metadata = asset.commonMetadata + } + + public subscript(_ key: AVMetadataKey) -> String? { + return AVMetadataItem.metadataItems(from: metadata, withKey: key, keySpace: nil).first?.stringValue + } +} +#endif diff --git a/Sources/Downpour/Downpour+Protocol.swift b/Sources/Downpour/Downpour+Protocol.swift new file mode 100644 index 0000000..c9908ff --- /dev/null +++ b/Sources/Downpour/Downpour+Protocol.swift @@ -0,0 +1,42 @@ +import TrailBlazer + +@dynamicMemberLookup +public protocol Downpourable { + associatedtype MetadataType: Metadata + var metadata: MetadataType? { get set } + + init() + init(metadata: MetadataType) + subscript(dynamicMember member: String) -> MetadataValue? { get } +} + +public extension Downpourable { + public init(metadata: MetadataType) { + self.init() + self.metadata = metadata + } + + public init?(file path: FilePath) { + guard let _md = MetadataType(file: path) else { return nil } + self.init(metadata: _md) + } +} + +public protocol Metadata { + var type: MetadataType { get } + var title: String { get } + + init?(file path: FilePath) +} + +public enum MetadataType { + public enum VideoType { + case tv + case movie + case unknown + } + + case video(VideoType) + case audio + case unknown +} diff --git a/Sources/Downpour/Downpour.swift b/Sources/Downpour/Downpour.swift index e27e69f..6f16b28 100644 --- a/Sources/Downpour/Downpour.swift +++ b/Sources/Downpour/Downpour.swift @@ -7,15 +7,15 @@ // import Foundation -import PathKit +import TrailBlazer -open class Downpour: CustomStringConvertible { +open class Downpour: CustomStringConvertible, Downpourable { /// The raw string that has not yet been parsed by Downpour. - var rawString: String + private var rawString: String /// The full path to the file - var fullPath: Path + private var fullPath: Path /// The metadata for the file (generally only useful for music files) lazy var metadata: Metadata? = { @@ -34,7 +34,7 @@ open class Downpour: CustomStringConvertible { case year = "[\\(?:\\.\\s_\\[](?:19|(?:[2-9])(?:[0-9]))\\d{2}[\\]\\s_\\.\\)]" } - static let regexOptions: String.CompareOptions = [.regularExpression, .caseInsensitive] + public static var regexOptions: String.CompareOptions = [.regularExpression, .caseInsensitive] /// Both the season and the episode together. lazy open var seasonEpisode: String? = { @@ -281,18 +281,11 @@ open class Downpour: CustomStringConvertible { // MARK: - Initializers - public init(name: String, path: Path? = nil) { - rawString = name - if let p = path { - fullPath = p.isFile ? p : p + name - } else { - fullPath = Path(name) - } + public init(filename: String) { + rawString = filename } - public init(fullPath: Path) { - self.fullPath = fullPath - self.rawString = fullPath.lastComponent + public init(path: FilePath) { + rawString = path.lastComponent ?? path.string } - } diff --git a/Sources/Downpour/DownpourType.swift b/Sources/Downpour/DownpourType.swift deleted file mode 100644 index a78a63f..0000000 --- a/Sources/Downpour/DownpourType.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// DownpourType.swift -// Downpour -// -// Created by Stephen Radford on 18/05/2016. -// Copyright © 2015 Stephen Radford. All rights reserved. -// - -public enum DownpourType { - case tv - case movie - case music - case unknown -} diff --git a/Sources/Downpour/ExifTool.swift b/Sources/Downpour/ExifTool.swift new file mode 100644 index 0000000..5784574 --- /dev/null +++ b/Sources/Downpour/ExifTool.swift @@ -0,0 +1,85 @@ +#if os(Linux) +import SwiftShell +import TrailBlazer +import Foundation + +public struct ExifTool: Metadata, Decodable { + public let title: String + public let creation: Date? + public let filetype: String? + public let type: MetadataType + public let copyrights: [String]? + public let albumName: String? + public let artist: String? + public let artwork: String? + + private static var _dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + return dateFormatter + }() + + private enum CodingKeys: String, CodingKey { + case title = "Title" + case creationDate = "Date/Time Original" + case type = "File Type" + case format = "MIME Type" + case copyrights = "Copyright" + case albumName = "Album" + case artist = "Artist" + case artwork = "Picture" + } + + public init?(file path: FilePath) { + guard path.exists else { return nil } + guard SwiftShell.run("which", "exiftool").succeeded else { + #if os(macOS) + fatalError("Missing `exiftool` dependency! On macOS, try installing it with Homebrew by running `brew install exiftool`") + #else + fatalError("Missing `exiftool` dependency! On Ubuntu, try installing it by running `sudo apt install -y libimage-exiftool-perl`") + #endif + } + + let output = SwiftShell.run("exiftool", "-b", "-All", "-j", path.string).stdout + + guard let exifData = output.data(using: .utf8) else { return nil } + guard let data = try? JSONDecoder().decode(ExifTool.self, from: exifData) else { return nil } + + title = data.title.isEmpty ? path.lastComponentWithoutExtension ?? path.string : data.title + creation = data.creation + type = data.type + filetype = data.filetype + copyrights = data.copyrights + albumName = data.albumName + artist = data.artist + artwork = data.artwork + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + title = try container.decodeIfPresent(String.self, forKey: .title) ?? "" + if let creationString = try container.decodeIfPresent(String.self, forKey: .creationDate) { + creation = ExifTool._dateFormatter.date(from: creationString) + } else { + creation = nil + } + filetype = try container.decodeIfPresent(String.self, forKey: .type) + if let typeComponents = try container.decodeIfPresent(String.self, forKey: .format)?.lowercased().components(separatedBy: "/") { + if typeComponents.contains("video") { + type = .video(.unknown) + } else if typeComponents.contains("audio") { + type = .audio + } else { + type = .unknown + } + } else { + type = .unknown + } + copyrights = try container.decodeIfPresent(String.self, forKey: .copyrights)?.components(separatedBy: ", ") + albumName = try container.decodeIfPresent(String.self, forKey: .albumName) + artist = try container.decodeIfPresent(String.self, forKey: .artist) + artwork = try container.decodeIfPresent(String.self, forKey: .artwork) + } +} +#endif diff --git a/Sources/Downpour/Metadata.swift b/Sources/Downpour/Metadata.swift index 6ae99ea..9082d39 100644 --- a/Sources/Downpour/Metadata.swift +++ b/Sources/Downpour/Metadata.swift @@ -1,261 +1,233 @@ +import TrailBlazer import Foundation -import PathKit - -#if os(Linux) -// On Linux we define our own metadata keys that correspond with what exiftool -// uses for metadata key names - enum AVMetadataKey: String { - case commonKeyTitle = "Title" - case commonKeyCreationDate = "Date/Time Original" - case commonKeyType = "File Type" - case commonKeyFormat = "MIME Type" - case commonKeyCopyrights = "Copyright" - case commonKeyAlbumName = "Album" - case commonKeyArtist = "Artist" - case commonKeyArtwork = "Picture" - } -#else -// Mac OS/iOS includes the AVMetadataCommonKeys in the AVFoundation framework, -// along with the AVAsset class to make retriving file metadata easy -import AVFoundation -#endif - -class Metadata { - // These lazy vars will get metadata for all the common keys normally - // defined in AVFoundation. The vars are lazy, which means it will only - // perform the getter once - lazy var title: String? = { return self[.commonKeyTitle] }() - lazy var creationDate: Date? = { - guard let dateString = self.creationDateString else { return nil } - return self.dateFormatter.date(from: dateString) - }() - lazy var creationDateString: String? = { return self[.commonKeyCreationDate] }() - lazy var type: String? = { return self[.commonKeyType] }() - lazy var format: String? = { return self[.commonKeyFormat] }() - lazy var copyrights: String? = { return self[.commonKeyCopyrights] }() - lazy var album: String? = { return self[.commonKeyAlbumName] }() - lazy var artist: String? = { return self[.commonKeyArtist] }() - lazy var artwork: String? = { return self[.commonKeyArtwork] }() - - #if !os(Linux) - lazy var publisher: String? = { return self[.commonKeyPublisher] }() - lazy var creator: String? = { return self[.commonKeyCreator] }() - lazy var subject: String? = { return self[.commonKeySubject] }() - lazy var description: String? = { return self[.commonKeyDescription] }() - lazy var contributer: String? = { return self[.commonKeyContributor] }() - lazy var lastModifiedDate: Date? = { - guard let dateString = self.lastModifiedDateString else { return nil } - return self.dateFormatter.date(from: dateString) - }() - lazy var lastModifiedDateString: String? = { return self[.commonKeyLastModifiedDate] }() - lazy var identifier: String? = { return self[.commonKeyIdentifier] }() - lazy var source: String? = { return self[.commonKeySource] }() - lazy var language: String? = { return self[.commonKeyLanguage] }() - lazy var relation: String? = { return self[.commonKeyRelation] }() - lazy var location: String? = { return self[.commonKeyLocation] }() - lazy var author: String? = { return self[.commonKeyAuthor] }() - lazy var make: String? = { return self[.commonKeyMake] }() - lazy var model: String? = { return self[.commonKeyModel] }() - lazy var software: String? = { return self[.commonKeySoftware] }() - #endif - - /// A DateFormatter for the attributes that require a date from string in the ISO 8601 format - lazy var dateFormatter: DateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" - return dateFormatter - }() - // The saved metadata items, so that we don't have to continually get the AVAsset or run exiftool - #if os(Linux) - private struct Metadata: Decodable { - enum MetadataKeys: String, CodingKey { - case title = "Title" - case creationDate = "Date/Time Original" - case type = "File Type" - case format = "MIME Type" - case copyrights = "Copyright" - case albumName = "Album" - case artist = "Artist" - case artwork = "Picture" - } - var title: String - var creationDate: String - var type: String - var format: String - var copyrights: String - var albumName: String - var artist: String - var artwork: String - - subscript(key: AVMetadataKey) -> String { - switch key { - case .commonKeyTitle: - return title - case .commonKeyCreationDate: - return creationDate - case .commonKeyType: - return type - case .commonKeyFormat: - return format - case .commonKeyCopyrights: - return copyrights - case .commonKeyAlbumName: - return albumName - case .commonKeyArtist: - return artist - case .commonKeyArtwork: - return artwork - default: - return "" +open class VideoMetadata: Metadata, CustomStringConvertible { + public enum Pattern: String, CaseIterable { + case pretty = "S(\\d{4}|\\d{1,2})[\\-\\.\\s_]?E\\d{1,2}" + case tricky = "[^\\d](\\d{4}|\\d{1,2})[X\\-\\.\\s_]\\d{1,2}([^\\d]|$)" + case combined = "(?:S)?(\\d{4}|\\d{1,2})[EX\\-\\.\\s_]\\d{1,2}([^\\d]|$)" + case altSeason = "Season (\\d{4}|\\d{1,2}) Episode \\d{1,2}" + case altSeasonSingle = "Season (\\d{4}|\\d{1,2})" + case altEpisodeSingle = "Episode \\d{1,2}" + case altSeason2 = "[\\s_\\.\\-\\[]\\d{3}[\\s_\\.\\-\\]]" + case year = "[\\(?:\\.\\s_\\[](?:19|(?:[2-9])(?:[0-9]))\\d{2}[\\]\\s_\\.\\)]" + } + open static let regexOptions: String.CompareOptions = [.regularExpression, .caseInsensitive] + + open static let extensions: [String] = [ + "mkv", "flv", "vob", "ogv", "drc", + "gifv", "mng", "avi", "mov", "qt", + "wmv", "yuv", "rmvb", "asf", "amv", + "mp4", "m4p", "m4v", "mpg", "mp2", + "mpeg", "mpe", "mpv", "svi", "3g2", + "mx4", "roq", "nsv", "f4v", "f4p", + "f4a", "f4b" + ] + + open static let splitCharset = CharacterSet(charactersIn: "eExX-._ ") + + private let __rawString: String + private let __extension: String? + + open var _rawString: String { return __rawString } + open var _extension: String? { return __extension } + + open var description: String { + var desc: String = "\(Swift.type(of: self))(title: \(title)" + + switch type { + case .video(let videoType): + switch videoType { + case .tv: + desc += ", season: \(String(describing: season)), episode: \(String(describing: episode))" + default: break } + default: fatalError("Non video type found in VideoMetadata") } - } - private var metadataJSON: Metadata? - #else - private var metadataItems: [AVMetadataItem]? - #endif - - /// The path to the file - private var filepath: Path + if let year = self.year { + desc += ", year: \(year)" + } - /// The errors that occur within the Metadata class - private enum MetadataError: Swift.Error { - case missingDependency(dependency: String, helpText: String) - case couldNotGetMetadata(error: String) - case missingMetadataKey(key: String) + return desc + ")" } - /// Initializer that checks to make sure the dependencies are installed - init(_ path: Path) throws { - filepath = path - try hasDependencies() + public required init?(file path: FilePath) { + guard VideoMetadata.extensions.contains(path.extension ?? "") else { return nil } + guard let last = path.lastComponentWithoutExtension else { return nil } + __rawString = last + __extension = path.extension } - /// Checks to verify the system has any required dependencies. - /// - Throws: If a dependency is missing - private func hasDependencies() throws { - #if os(Linux) - let (rc, _) = execute("which exiftool") - guard rc == 0 else { - throw MetadataError.missingDependency(dependency: "exiftool", - helpText: "On Ubuntu systems, try installing the 'libimage-exiftool-perl' package by running `sudo apt-get install -y libimage-exiftool-perl`") + public init?(filename: String) { + let comps = filename.components(separatedBy: ".") + if comps.count > 1 { + __extension = comps.last + guard VideoMetadata.extensions.contains(__extension ?? "") else { return nil } + + __rawString = comps.dropLast().joined(separator: ".") + } else { + __rawString = filename + __extension = nil } - #endif } - #if os(Linux) - /// Struct used to capture the stdout and stderr of a command - private struct Output { - var stdout: String? - var stderr: String? - init (_ out: String?, _ err: String?) { - stdout = out - stderr = err + open lazy var type: MetadataType = { + guard let ext = _extension else { + return .video(.unknown) } - } - /** - Executes a cli command - - - Parameter command: The array of strings that form the command and arguments - - - Returns: A tuple of the return code and output - */ - private func execute(_ command: String...) -> (Int32, Output) { - let task = Process() - task.launchPath = "/usr/bin/env" - task.arguments = command - - let stdoutPipe = Pipe() - let stderrPipe = Pipe() - task.standardOutput = stdoutPipe - task.standardError = stderrPipe - task.launch() - let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile() - let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile() - let stdout = String(data: stdoutData, encoding: .utf8) - let stderr = String(data: stderrData, encoding: .utf8) - task.waitUntilExit() - return (task.terminationStatus, Output(stdout, stderr)) - } - #endif - - /** - Get the common metadata for the specified common metadata key - - - Parameter key: The common metadata key to retrieve from the common metadata for the file - - - Returns: The string value of the common metadata, or nil. If an error occured, this will print it out - */ - public subscript(_ key: AVMetadataKey) -> String? { - do { - // Try and return the Common Metadata value - return try getCM(key) - } catch { - // Print the error that occurred - print("Failed to get file metadata: \n\t\(error)") + // Sometimes it mestakes the x/h 264 as season 2, episode 64. I don't + // know of any shows that have 64 episode in a single season, so + // checking that the episode < 64 should be safe and will resolve these + // false positives + if season != nil && (episode ?? 64) < 64 { + return .video(.tv) } - // Return nil if an error occurs - return nil - } - /** - Gets the common metadata for the key, throws errors if the key doesn't exist or if metadata could not be retrieved - - Parameter key: The common metadata key to retrieve from the common metadata for the file - - - Returns: The string value of the common metadata, or nil. - */ - private func getCM(_ key: AVMetadataKey) throws -> String? { - #if os(Linux) - // If we're running Linux, check to see if we've saved an exiftool metadata JSON object - if metadataJSON == nil { - // If not, run the exiftool command to get the file's metadata - let (rc, output) = execute("exiftool -b -All -j \(filepath.absolute)") - // Throw an error if we failed to get the metadata - guard rc == 0 else { - var err: String = "" - if let stderr = output.stderr { - err = stderr - } - throw MetadataError.couldNotGetMetadata(error: err) - } - guard let stdout = output.stdout else { - throw MetadataError.couldNotGetMetadata(error: "File does not contain any metadata") + return .video(.movie) + }() + + /// Iterates through all of the patterns and returns any match found + private lazy var seasonEpisode: String? = { + var _match: Range? + var _patternMatched: Pattern? + for (index, pattern) in Pattern.allCases.enumerated() { + if let __match = _rawString.range(of: pattern, options: VideoMetadata.regexOptions) { + _match = __match + _patternMatched = Pattern.allCases[index] + break } + } + guard var match = _match, let patternMatched = _patternMatched else { return nil } + + let matchString: String? + switch patternMatched { + case .tricky: + match = _rawString.index(after: match.lowerBound).. 1 else { return nil } + // This will never fail + guard let first = pieces.first else { fatalError("Splitting a string resulted in an empty array") } + + // The size of the first part needs to be between 1 and 2 + if first.count <= 2 && first.count >= 1 { + return Int(first.cleanedString) + } + + let startIndex = first.index(after: first.startIndex) + return Int(first[startIndex.. 0 else { - throw MetadataError.missingMetadataKey(key: key.rawValue) + + if let title = _title { + var clean = title.cleanedString + + if let uncleanMatch = title.range(of: "\\d+\\.\\d+", options: .regularExpression), + let tooCleanMatch = clean.range(of: "\\d+ \\d+", options: .regularExpression), + uncleanMatch == tooCleanMatch { + clean = clean.replacingCharacters(in: tooCleanMatch, with: title[uncleanMatch]) + } + return clean } - // Return the first metadata item's string value - return metadata.first?.stringValue - #endif - } -} + return _rawString.cleanedString + }() +} diff --git a/Sources/Downpour/String + CleanedString.swift b/Sources/Downpour/String + CleanedString.swift index aa9d474..e5bf14a 100644 --- a/Sources/Downpour/String + CleanedString.swift +++ b/Sources/Downpour/String + CleanedString.swift @@ -25,7 +25,7 @@ extension String { return cleaned } - func range(of pattern: Downpour.Pattern, options: String.CompareOptions = []) -> Range? { + func range(of pattern: Enum, options: String.CompareOptions = []) -> Range? where Enum.RawValue == String { return self.range(of: pattern.rawValue, options: options) } diff --git a/Sources/Downpour/_Metadata.swift b/Sources/Downpour/_Metadata.swift new file mode 100644 index 0000000..e6f0a4c --- /dev/null +++ b/Sources/Downpour/_Metadata.swift @@ -0,0 +1,260 @@ +import Foundation + +#if os(Linux) +// On Linux we define our own metadata keys that correspond with what exiftool +// uses for metadata key names + enum AVMetadataKey: String { + case commonKeyTitle = "Title" + case commonKeyCreationDate = "Date/Time Original" + case commonKeyType = "File Type" + case commonKeyFormat = "MIME Type" + case commonKeyCopyrights = "Copyright" + case commonKeyAlbumName = "Album" + case commonKeyArtist = "Artist" + case commonKeyArtwork = "Picture" + } +#else +// Mac OS/iOS includes the AVMetadataCommonKeys in the AVFoundation framework, +// along with the AVAsset class to make retriving file metadata easy +import AVFoundation +#endif + +class _Metadata { + // These lazy vars will get metadata for all the common keys normally + // defined in AVFoundation. The vars are lazy, which means it will only + // perform the getter once + lazy var title: String? = { return self[.commonKeyTitle] }() + lazy var creationDate: Date? = { + guard let dateString = self.creationDateString else { return nil } + return self.dateFormatter.date(from: dateString) + }() + lazy var creationDateString: String? = { return self[.commonKeyCreationDate] }() + lazy var type: String? = { return self[.commonKeyType] }() + lazy var format: String? = { return self[.commonKeyFormat] }() + lazy var copyrights: String? = { return self[.commonKeyCopyrights] }() + lazy var album: String? = { return self[.commonKeyAlbumName] }() + lazy var artist: String? = { return self[.commonKeyArtist] }() + lazy var artwork: String? = { return self[.commonKeyArtwork] }() + + #if !os(Linux) + lazy var publisher: String? = { return self[.commonKeyPublisher] }() + lazy var creator: String? = { return self[.commonKeyCreator] }() + lazy var subject: String? = { return self[.commonKeySubject] }() + lazy var description: String? = { return self[.commonKeyDescription] }() + lazy var contributer: String? = { return self[.commonKeyContributor] }() + lazy var lastModifiedDate: Date? = { + guard let dateString = self.lastModifiedDateString else { return nil } + return self.dateFormatter.date(from: dateString) + }() + lazy var lastModifiedDateString: String? = { return self[.commonKeyLastModifiedDate] }() + lazy var identifier: String? = { return self[.commonKeyIdentifier] }() + lazy var source: String? = { return self[.commonKeySource] }() + lazy var language: String? = { return self[.commonKeyLanguage] }() + lazy var relation: String? = { return self[.commonKeyRelation] }() + lazy var location: String? = { return self[.commonKeyLocation] }() + lazy var author: String? = { return self[.commonKeyAuthor] }() + lazy var make: String? = { return self[.commonKeyMake] }() + lazy var model: String? = { return self[.commonKeyModel] }() + lazy var software: String? = { return self[.commonKeySoftware] }() + #endif + + /// A DateFormatter for the attributes that require a date from string in the ISO 8601 format + lazy var dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + return dateFormatter + }() + + // The saved metadata items, so that we don't have to continually get the AVAsset or run exiftool + #if os(Linux) + private struct Metadata: Decodable { + enum MetadataKeys: String, CodingKey { + case title = "Title" + case creationDate = "Date/Time Original" + case type = "File Type" + case format = "MIME Type" + case copyrights = "Copyright" + case albumName = "Album" + case artist = "Artist" + case artwork = "Picture" + } + var title: String + var creationDate: String + var type: String + var format: String + var copyrights: String + var albumName: String + var artist: String + var artwork: String + + subscript(key: AVMetadataKey) -> String { + switch key { + case .commonKeyTitle: + return title + case .commonKeyCreationDate: + return creationDate + case .commonKeyType: + return type + case .commonKeyFormat: + return format + case .commonKeyCopyrights: + return copyrights + case .commonKeyAlbumName: + return albumName + case .commonKeyArtist: + return artist + case .commonKeyArtwork: + return artwork + default: + return "" + } + } + } + + private var metadataJSON: Metadata? + #else + private var metadataItems: [AVMetadataItem]? + #endif + + /// The path to the file + private var filepath: Path + + /// The errors that occur within the Metadata class + private enum MetadataError: Swift.Error { + case missingDependency(dependency: String, helpText: String) + case couldNotGetMetadata(error: String) + case missingMetadataKey(key: String) + } + + /// Initializer that checks to make sure the dependencies are installed + init(_ path: Path) throws { + filepath = path + try hasDependencies() + } + + /// Checks to verify the system has any required dependencies. + /// - Throws: If a dependency is missing + private func hasDependencies() throws { + #if os(Linux) + let (rc, _) = execute("which exiftool") + guard rc == 0 else { + throw MetadataError.missingDependency(dependency: "exiftool", + helpText: "On Ubuntu systems, try installing the 'libimage-exiftool-perl' package by running `sudo apt-get install -y libimage-exiftool-perl`") + } + #endif + } + + #if os(Linux) + /// Struct used to capture the stdout and stderr of a command + private struct Output { + var stdout: String? + var stderr: String? + init (_ out: String?, _ err: String?) { + stdout = out + stderr = err + } + } + + /** + Executes a cli command + + - Parameter command: The array of strings that form the command and arguments + + - Returns: A tuple of the return code and output + */ + private func execute(_ command: String...) -> (Int32, Output) { + let task = Process() + task.launchPath = "/usr/bin/env" + task.arguments = command + + let stdoutPipe = Pipe() + let stderrPipe = Pipe() + task.standardOutput = stdoutPipe + task.standardError = stderrPipe + task.launch() + let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile() + let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile() + let stdout = String(data: stdoutData, encoding: .utf8) + let stderr = String(data: stderrData, encoding: .utf8) + task.waitUntilExit() + return (task.terminationStatus, Output(stdout, stderr)) + } + #endif + + /** + Get the common metadata for the specified common metadata key + + - Parameter key: The common metadata key to retrieve from the common metadata for the file + + - Returns: The string value of the common metadata, or nil. If an error occured, this will print it out + */ + public subscript(_ key: AVMetadataKey) -> String? { + do { + // Try and return the Common Metadata value + return try getCM(key) + } catch { + // Print the error that occurred + print("Failed to get file metadata: \n\t\(error)") + } + // Return nil if an error occurs + return nil + } + + /** + Gets the common metadata for the key, throws errors if the key doesn't exist or if metadata could not be retrieved + - Parameter key: The common metadata key to retrieve from the common metadata for the file + + - Returns: The string value of the common metadata, or nil. + */ + private func getCM(_ key: AVMetadataKey) throws -> String? { + #if os(Linux) + // If we're running Linux, check to see if we've saved an exiftool metadata JSON object + if metadataJSON == nil { + // If not, run the exiftool command to get the file's metadata + let (rc, output) = execute("exiftool -b -All -j \(filepath.absolute)") + // Throw an error if we failed to get the metadata + guard rc == 0 else { + var err: String = "" + if let stderr = output.stderr { + err = stderr + } + throw MetadataError.couldNotGetMetadata(error: err) + } + guard let stdout = output.stdout else { + throw MetadataError.couldNotGetMetadata(error: "File does not contain any metadata") + } + + metadataJSON = try JSONDecoder().decode(Metadata.self, from: stdout.data(using: .utf8)!) + } + // Try and retrieve the specified property + guard let property = metadataJSON?[key] else { + // Throw an error if the key doesn't exist + throw MetadataError.missingMetadataKey(key: key.rawValue) + } + // Otherwise, return the property + return property + #else + // If we're on macOS/iOS/tvOS/watchOS, check to see if we've saced the common metadataItems + if metadataItems == nil { + // If not, get the AVAsset from the filepath url + let asset = AVAsset(url: filepath.url) + // Save the common metadata items + metadataItems = asset.commonMetadata + // Make sure the asset had common metadata items + guard let _ = metadataItems else { + // Throw an error because the asset either has no common metadata, or something happened + throw MetadataError.couldNotGetMetadata(error: "Unkown problem getting common metadata from AVAsset") + } + } + // Try and get the metadata for the specified key + let metadata = AVMetadataItem.metadataItems(from: metadataItems!, withKey: key, keySpace: nil) + // Throws an error if there is no common metadata for the key + guard metadata.count > 0 else { + throw MetadataError.missingMetadataKey(key: key.rawValue) + } + // Return the first metadata item's string value + return metadata.first?.stringValue + #endif + } +} + From d67afc23a54bf6db7597a7d8c4f50455a8dd3749 Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Tue, 11 Sep 2018 14:06:47 -0600 Subject: [PATCH 07/20] Compiles on macOS --- Sources/Downpour/AVMetadata.swift | 13 +- Sources/Downpour/_Metadata.swift | 260 ------------------------------ 2 files changed, 9 insertions(+), 264 deletions(-) delete mode 100644 Sources/Downpour/_Metadata.swift diff --git a/Sources/Downpour/AVMetadata.swift b/Sources/Downpour/AVMetadata.swift index be143a5..b6684ec 100644 --- a/Sources/Downpour/AVMetadata.swift +++ b/Sources/Downpour/AVMetadata.swift @@ -1,6 +1,7 @@ #if !os(Linux) import Foundation import AVFoundation +import TrailBlazer public class AVMetadata: Metadata { public lazy var title: String = { return self[.commonKeyTitle] ?? path.string }() @@ -8,7 +9,7 @@ public class AVMetadata: Metadata { public lazy var creationDate: Date? = { guard let dateString = self.creationDateString else { return nil } return self.dateFormatter.date(from: dateString) - } + }() public lazy var creationDateString: String? = { return self[.commonKeyCreationDate] }() public lazy var format: String? = { return self[.commonKeyFormat] }() public lazy var copyrights: String? = { return self[.commonKeyCopyrights] }() @@ -22,7 +23,7 @@ public class AVMetadata: Metadata { public lazy var lastModifiedDate: Date? = { guard let dateString = self.lastModifiedDateString else { return nil } return self.dateFormatter.date(from: dateString) - } + }() public lazy var lastModifiedDateString: String? = { return self[.commonKeyLastModifiedDate] }() public lazy var language: String? = { return self[.commonKeyLanguage] }() public lazy var author: String? = { return self[.commonKeyAuthor] }() @@ -33,11 +34,15 @@ public class AVMetadata: Metadata { return dateFormatter }() + private let path: FilePath private let metadata: [AVMetadataItem] - public init(file path: FilePath) { + public required init?(file path: FilePath) { + guard path.exists else { return nil } + self.path = path + let asset = AVAsset(url: path.url) - metadata = asset.commonMetadata + self.metadata = asset.commonMetadata } public subscript(_ key: AVMetadataKey) -> String? { diff --git a/Sources/Downpour/_Metadata.swift b/Sources/Downpour/_Metadata.swift deleted file mode 100644 index e6f0a4c..0000000 --- a/Sources/Downpour/_Metadata.swift +++ /dev/null @@ -1,260 +0,0 @@ -import Foundation - -#if os(Linux) -// On Linux we define our own metadata keys that correspond with what exiftool -// uses for metadata key names - enum AVMetadataKey: String { - case commonKeyTitle = "Title" - case commonKeyCreationDate = "Date/Time Original" - case commonKeyType = "File Type" - case commonKeyFormat = "MIME Type" - case commonKeyCopyrights = "Copyright" - case commonKeyAlbumName = "Album" - case commonKeyArtist = "Artist" - case commonKeyArtwork = "Picture" - } -#else -// Mac OS/iOS includes the AVMetadataCommonKeys in the AVFoundation framework, -// along with the AVAsset class to make retriving file metadata easy -import AVFoundation -#endif - -class _Metadata { - // These lazy vars will get metadata for all the common keys normally - // defined in AVFoundation. The vars are lazy, which means it will only - // perform the getter once - lazy var title: String? = { return self[.commonKeyTitle] }() - lazy var creationDate: Date? = { - guard let dateString = self.creationDateString else { return nil } - return self.dateFormatter.date(from: dateString) - }() - lazy var creationDateString: String? = { return self[.commonKeyCreationDate] }() - lazy var type: String? = { return self[.commonKeyType] }() - lazy var format: String? = { return self[.commonKeyFormat] }() - lazy var copyrights: String? = { return self[.commonKeyCopyrights] }() - lazy var album: String? = { return self[.commonKeyAlbumName] }() - lazy var artist: String? = { return self[.commonKeyArtist] }() - lazy var artwork: String? = { return self[.commonKeyArtwork] }() - - #if !os(Linux) - lazy var publisher: String? = { return self[.commonKeyPublisher] }() - lazy var creator: String? = { return self[.commonKeyCreator] }() - lazy var subject: String? = { return self[.commonKeySubject] }() - lazy var description: String? = { return self[.commonKeyDescription] }() - lazy var contributer: String? = { return self[.commonKeyContributor] }() - lazy var lastModifiedDate: Date? = { - guard let dateString = self.lastModifiedDateString else { return nil } - return self.dateFormatter.date(from: dateString) - }() - lazy var lastModifiedDateString: String? = { return self[.commonKeyLastModifiedDate] }() - lazy var identifier: String? = { return self[.commonKeyIdentifier] }() - lazy var source: String? = { return self[.commonKeySource] }() - lazy var language: String? = { return self[.commonKeyLanguage] }() - lazy var relation: String? = { return self[.commonKeyRelation] }() - lazy var location: String? = { return self[.commonKeyLocation] }() - lazy var author: String? = { return self[.commonKeyAuthor] }() - lazy var make: String? = { return self[.commonKeyMake] }() - lazy var model: String? = { return self[.commonKeyModel] }() - lazy var software: String? = { return self[.commonKeySoftware] }() - #endif - - /// A DateFormatter for the attributes that require a date from string in the ISO 8601 format - lazy var dateFormatter: DateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" - return dateFormatter - }() - - // The saved metadata items, so that we don't have to continually get the AVAsset or run exiftool - #if os(Linux) - private struct Metadata: Decodable { - enum MetadataKeys: String, CodingKey { - case title = "Title" - case creationDate = "Date/Time Original" - case type = "File Type" - case format = "MIME Type" - case copyrights = "Copyright" - case albumName = "Album" - case artist = "Artist" - case artwork = "Picture" - } - var title: String - var creationDate: String - var type: String - var format: String - var copyrights: String - var albumName: String - var artist: String - var artwork: String - - subscript(key: AVMetadataKey) -> String { - switch key { - case .commonKeyTitle: - return title - case .commonKeyCreationDate: - return creationDate - case .commonKeyType: - return type - case .commonKeyFormat: - return format - case .commonKeyCopyrights: - return copyrights - case .commonKeyAlbumName: - return albumName - case .commonKeyArtist: - return artist - case .commonKeyArtwork: - return artwork - default: - return "" - } - } - } - - private var metadataJSON: Metadata? - #else - private var metadataItems: [AVMetadataItem]? - #endif - - /// The path to the file - private var filepath: Path - - /// The errors that occur within the Metadata class - private enum MetadataError: Swift.Error { - case missingDependency(dependency: String, helpText: String) - case couldNotGetMetadata(error: String) - case missingMetadataKey(key: String) - } - - /// Initializer that checks to make sure the dependencies are installed - init(_ path: Path) throws { - filepath = path - try hasDependencies() - } - - /// Checks to verify the system has any required dependencies. - /// - Throws: If a dependency is missing - private func hasDependencies() throws { - #if os(Linux) - let (rc, _) = execute("which exiftool") - guard rc == 0 else { - throw MetadataError.missingDependency(dependency: "exiftool", - helpText: "On Ubuntu systems, try installing the 'libimage-exiftool-perl' package by running `sudo apt-get install -y libimage-exiftool-perl`") - } - #endif - } - - #if os(Linux) - /// Struct used to capture the stdout and stderr of a command - private struct Output { - var stdout: String? - var stderr: String? - init (_ out: String?, _ err: String?) { - stdout = out - stderr = err - } - } - - /** - Executes a cli command - - - Parameter command: The array of strings that form the command and arguments - - - Returns: A tuple of the return code and output - */ - private func execute(_ command: String...) -> (Int32, Output) { - let task = Process() - task.launchPath = "/usr/bin/env" - task.arguments = command - - let stdoutPipe = Pipe() - let stderrPipe = Pipe() - task.standardOutput = stdoutPipe - task.standardError = stderrPipe - task.launch() - let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile() - let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile() - let stdout = String(data: stdoutData, encoding: .utf8) - let stderr = String(data: stderrData, encoding: .utf8) - task.waitUntilExit() - return (task.terminationStatus, Output(stdout, stderr)) - } - #endif - - /** - Get the common metadata for the specified common metadata key - - - Parameter key: The common metadata key to retrieve from the common metadata for the file - - - Returns: The string value of the common metadata, or nil. If an error occured, this will print it out - */ - public subscript(_ key: AVMetadataKey) -> String? { - do { - // Try and return the Common Metadata value - return try getCM(key) - } catch { - // Print the error that occurred - print("Failed to get file metadata: \n\t\(error)") - } - // Return nil if an error occurs - return nil - } - - /** - Gets the common metadata for the key, throws errors if the key doesn't exist or if metadata could not be retrieved - - Parameter key: The common metadata key to retrieve from the common metadata for the file - - - Returns: The string value of the common metadata, or nil. - */ - private func getCM(_ key: AVMetadataKey) throws -> String? { - #if os(Linux) - // If we're running Linux, check to see if we've saved an exiftool metadata JSON object - if metadataJSON == nil { - // If not, run the exiftool command to get the file's metadata - let (rc, output) = execute("exiftool -b -All -j \(filepath.absolute)") - // Throw an error if we failed to get the metadata - guard rc == 0 else { - var err: String = "" - if let stderr = output.stderr { - err = stderr - } - throw MetadataError.couldNotGetMetadata(error: err) - } - guard let stdout = output.stdout else { - throw MetadataError.couldNotGetMetadata(error: "File does not contain any metadata") - } - - metadataJSON = try JSONDecoder().decode(Metadata.self, from: stdout.data(using: .utf8)!) - } - // Try and retrieve the specified property - guard let property = metadataJSON?[key] else { - // Throw an error if the key doesn't exist - throw MetadataError.missingMetadataKey(key: key.rawValue) - } - // Otherwise, return the property - return property - #else - // If we're on macOS/iOS/tvOS/watchOS, check to see if we've saced the common metadataItems - if metadataItems == nil { - // If not, get the AVAsset from the filepath url - let asset = AVAsset(url: filepath.url) - // Save the common metadata items - metadataItems = asset.commonMetadata - // Make sure the asset had common metadata items - guard let _ = metadataItems else { - // Throw an error because the asset either has no common metadata, or something happened - throw MetadataError.couldNotGetMetadata(error: "Unkown problem getting common metadata from AVAsset") - } - } - // Try and get the metadata for the specified key - let metadata = AVMetadataItem.metadataItems(from: metadataItems!, withKey: key, keySpace: nil) - // Throws an error if there is no common metadata for the key - guard metadata.count > 0 else { - throw MetadataError.missingMetadataKey(key: key.rawValue) - } - // Return the first metadata item's string value - return metadata.first?.stringValue - #endif - } -} - From f45e54623c560c3fa44f043f78687e573a07dd0d Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Wed, 12 Sep 2018 13:26:30 -0600 Subject: [PATCH 08/20] Continue refactoring --- Sources/Downpour/Downpour+Protocol.swift | 30 +- Sources/Downpour/Downpour.swift | 285 +----------------- .../Downpour/{ => Metadata}/AVMetadata.swift | 22 +- .../ExifToolMetadata.swift} | 25 +- .../Downpour/Metadata/Metadata+Protocol.swift | 8 + .../Downpour/Metadata/MetadataFormat.swift | 61 ++++ .../VideoMetadata.swift} | 72 +++-- 7 files changed, 160 insertions(+), 343 deletions(-) rename Sources/Downpour/{ => Metadata}/AVMetadata.swift (67%) rename Sources/Downpour/{ExifTool.swift => Metadata/ExifToolMetadata.swift} (73%) create mode 100644 Sources/Downpour/Metadata/Metadata+Protocol.swift create mode 100644 Sources/Downpour/Metadata/MetadataFormat.swift rename Sources/Downpour/{Metadata.swift => Metadata/VideoMetadata.swift} (82%) diff --git a/Sources/Downpour/Downpour+Protocol.swift b/Sources/Downpour/Downpour+Protocol.swift index c9908ff..f5dfcc0 100644 --- a/Sources/Downpour/Downpour+Protocol.swift +++ b/Sources/Downpour/Downpour+Protocol.swift @@ -1,42 +1,16 @@ import TrailBlazer -@dynamicMemberLookup public protocol Downpourable { associatedtype MetadataType: Metadata - var metadata: MetadataType? { get set } + var metadata: MetadataType { get } - init() + init?(file path: FilePath) init(metadata: MetadataType) - subscript(dynamicMember member: String) -> MetadataValue? { get } } public extension Downpourable { - public init(metadata: MetadataType) { - self.init() - self.metadata = metadata - } - public init?(file path: FilePath) { guard let _md = MetadataType(file: path) else { return nil } self.init(metadata: _md) } } - -public protocol Metadata { - var type: MetadataType { get } - var title: String { get } - - init?(file path: FilePath) -} - -public enum MetadataType { - public enum VideoType { - case tv - case movie - case unknown - } - - case video(VideoType) - case audio - case unknown -} diff --git a/Sources/Downpour/Downpour.swift b/Sources/Downpour/Downpour.swift index 6f16b28..013ef85 100644 --- a/Sources/Downpour/Downpour.swift +++ b/Sources/Downpour/Downpour.swift @@ -9,283 +9,20 @@ import Foundation import TrailBlazer -open class Downpour: CustomStringConvertible, Downpourable { +public typealias VideoDownpour = Downpour +public typealias AudioDownpour = Downpour - /// The raw string that has not yet been parsed by Downpour. - private var rawString: String +open class Downpour<_MetadataType: Metadata>: CustomStringConvertible, Downpourable { + public typealias MetadataType = _MetadataType + public let metadata: MetadataType + public lazy var title: String = { metadata.title }() + public lazy var type: MetadataFormat = { metadata.type }() - /// The full path to the file - private var fullPath: Path - - /// The metadata for the file (generally only useful for music files) - lazy var metadata: Metadata? = { - return try? Metadata(self.fullPath) - }() - - /// The patterns that will be used to fetch various pieces of information from the rawString. - enum Pattern: String { - case pretty = "S(\\d{4}|\\d{1,2})[\\-\\.\\s_]?E\\d{1,2}" - case tricky = "[^\\d](\\d{4}|\\d{1,2})[X\\-\\.\\s_]\\d{1,2}([^\\d]|$)" - case combined = "(?:S)?(\\d{4}|\\d{1,2})[EX\\-\\.\\s_]\\d{1,2}([^\\d]|$)" - case altSeason = "Season (\\d{4}|\\d{1,2}) Episode \\d{1,2}" - case altSeasonSingle = "Season (\\d{4}|\\d{1,2})" - case altEpisodeSingle = "Episode \\d{1,2}" - case altSeason2 = "[\\s_\\.\\-\\[]\\d{3}[\\s_\\.\\-\\]]" - case year = "[\\(?:\\.\\s_\\[](?:19|(?:[2-9])(?:[0-9]))\\d{2}[\\]\\s_\\.\\)]" - } - - public static var regexOptions: String.CompareOptions = [.regularExpression, .caseInsensitive] - - /// Both the season and the episode together. - lazy open var seasonEpisode: String? = { - var matchedSubstring: Substring? = nil - if let match = self.rawString.range(of: Pattern.pretty, options: Downpour.regexOptions) { - matchedSubstring = self.rawString[match] - } else if var match = self.rawString.range(of: Pattern.tricky, options: Downpour.regexOptions) { - match = self.rawString.index(after: match.lowerBound)..= 1 else { - let startIndex = pieces[0].index(after: pieces[0].startIndex) - return pieces[0][startIndex..> 4) } + + public var description: String { + switch self { + case .video: return "video(\(format))" + case .subtitle: return "subtitle(\(format))" + case .audio: return "audio" + default: return "unknown" + } + } + + public static let video: MetadataFormat = 0b0000_0001 + public static let tv: MetadataFormat = .video(format: .tv) + public static let movie: MetadataFormat = .video(format: .movie) + public static func video(format: VideoFormat) -> MetadataFormat { + return MetadataFormat(rawValue: MetadataFormat.video.rawValue | (format.rawValue << 4)) + } + public static func video(_ format: VideoFormat) -> MetadataFormat { + return MetadataFormat(rawValue: MetadataFormat.video.rawValue | (format.rawValue << 4)) + } + + public static let subtitle: MetadataFormat = 0b0000_0011 + public static func subtitle(format: VideoFormat) -> MetadataFormat { + return MetadataFormat(rawValue: MetadataFormat.subtitle.rawValue | (format.rawValue << 4)) + } + public static func subtitle(_ format: VideoFormat) -> MetadataFormat { + return MetadataFormat(rawValue: MetadataFormat.subtitle.rawValue | (format.rawValue << 4)) + } + + public static let audio: MetadataFormat = 0b0000_0100 + public static let unknown: MetadataFormat = 0 + + public init(rawValue: UInt8) { self.rawValue = rawValue } + public init(integerLiteral value: UInt8) { self.init(rawValue: value) } +} + diff --git a/Sources/Downpour/Metadata.swift b/Sources/Downpour/Metadata/VideoMetadata.swift similarity index 82% rename from Sources/Downpour/Metadata.swift rename to Sources/Downpour/Metadata/VideoMetadata.swift index 9082d39..59b7aaf 100644 --- a/Sources/Downpour/Metadata.swift +++ b/Sources/Downpour/Metadata/VideoMetadata.swift @@ -12,32 +12,32 @@ open class VideoMetadata: Metadata, CustomStringConvertible { case altSeason2 = "[\\s_\\.\\-\\[]\\d{3}[\\s_\\.\\-\\]]" case year = "[\\(?:\\.\\s_\\[](?:19|(?:[2-9])(?:[0-9]))\\d{2}[\\]\\s_\\.\\)]" } - open static let regexOptions: String.CompareOptions = [.regularExpression, .caseInsensitive] - - open static let extensions: [String] = [ - "mkv", "flv", "vob", "ogv", "drc", - "gifv", "mng", "avi", "mov", "qt", - "wmv", "yuv", "rmvb", "asf", "amv", - "mp4", "m4p", "m4v", "mpg", "mp2", - "mpeg", "mpe", "mpv", "svi", "3g2", - "mx4", "roq", "nsv", "f4v", "f4p", - "f4a", "f4b" - ] - - open static let splitCharset = CharacterSet(charactersIn: "eExX-._ ") - - private let __rawString: String - private let __extension: String? - - open var _rawString: String { return __rawString } - open var _extension: String? { return __extension } + public static let regexOptions: String.CompareOptions = [.regularExpression, .caseInsensitive] + + public static let extensions: [MetadataFormat: [String]] = [ + .video: [ + "mkv", "flv", "vob", "ogv", "drc", + "gifv", "mng", "avi", "mov", "qt", + "wmv", "yuv", "rmvb", "asf", "amv", + "mp4", "m4p", "m4v", "mpg", "mp2", + "mpeg", "mpe", "mpv", "svi", "3g2", + "mx4", "roq", "nsv", "f4v", "f4p", + "f4a", "f4b" + ], + .subtitle: ["srt", "smi", "ssa", "ass", "vtt"] + ] + + public static let splitCharset = CharacterSet(charactersIn: "eExX-._ ") + + private let _rawString: String + private let _extension: String? open var description: String { var desc: String = "\(Swift.type(of: self))(title: \(title)" switch type { - case .video(let videoType): - switch videoType { + case .video, .subtitle: + switch type.format { case .tv: desc += ", season: \(String(describing: season)), episode: \(String(describing: episode))" default: break @@ -53,28 +53,28 @@ open class VideoMetadata: Metadata, CustomStringConvertible { } public required init?(file path: FilePath) { - guard VideoMetadata.extensions.contains(path.extension ?? "") else { return nil } + guard VideoMetadata.extensions.reduce([], { $0 + $1.value }).contains(path.extension ?? "") else { return nil } guard let last = path.lastComponentWithoutExtension else { return nil } - __rawString = last - __extension = path.extension + _rawString = last + _extension = path.extension } public init?(filename: String) { let comps = filename.components(separatedBy: ".") if comps.count > 1 { - __extension = comps.last - guard VideoMetadata.extensions.contains(__extension ?? "") else { return nil } + _extension = comps.last + guard VideoMetadata.extensions.reduce([], { $0 + $1.value }).contains(_extension ?? "") else { return nil } - __rawString = comps.dropLast().joined(separator: ".") + _rawString = comps.dropLast().joined(separator: ".") } else { - __rawString = filename - __extension = nil + _rawString = filename + _extension = nil } } - open lazy var type: MetadataType = { + open lazy var type: MetadataFormat = { guard let ext = _extension else { - return .video(.unknown) + return .video } // Sometimes it mestakes the x/h 264 as season 2, episode 64. I don't @@ -194,8 +194,8 @@ open class VideoMetadata: Metadata, CustomStringConvertible { let _title: String? switch type { - case .video(let videoType): - switch videoType { + case .video, .subtitle: + switch type.format { case .movie: if let year = self.year { let endIndex = _rawString.index(before: _rawString.range(of: String(year))!.lowerBound) @@ -231,3 +231,9 @@ open class VideoMetadata: Metadata, CustomStringConvertible { return _rawString.cleanedString }() } + +public extension Downpour where MetadataType: VideoMetadata { + public var season: Int? { return metadata.season } + public var episode: Int? { return metadata.episode } + public var year: Int? { return metadata.year } +} From 793e4ec5a275b7983dd86ed96662cbe1eb6bf3e8 Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Wed, 12 Sep 2018 13:56:41 -0600 Subject: [PATCH 09/20] Comiles on swift 4.1 for macOS --- Sources/Downpour/CaseIterable.swift | 6 ++++++ Sources/Downpour/Metadata/AVMetadata.swift | 2 +- Sources/Downpour/Metadata/VideoMetadata.swift | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 Sources/Downpour/CaseIterable.swift diff --git a/Sources/Downpour/CaseIterable.swift b/Sources/Downpour/CaseIterable.swift new file mode 100644 index 0000000..5a7fe8e --- /dev/null +++ b/Sources/Downpour/CaseIterable.swift @@ -0,0 +1,6 @@ +#if swift(>=4.2) +#else +protocol CaseIterable { + static var allCases: [Self] { get } +} +#endif diff --git a/Sources/Downpour/Metadata/AVMetadata.swift b/Sources/Downpour/Metadata/AVMetadata.swift index 4fd7f73..79af18b 100644 --- a/Sources/Downpour/Metadata/AVMetadata.swift +++ b/Sources/Downpour/Metadata/AVMetadata.swift @@ -65,7 +65,7 @@ public extension Downpour where MetadataType: AVMetadata { public var creator: String? { return metadata.creator } public var subject: String? { return metadata.subject } public var summary: String? { return metadata.summary } - public var lastModifiedDate: Date? { return metadata.lastModifiedData } + public var lastModifiedDate: Date? { return metadata.lastModifiedDate } public var language: String? { return metadata.language } public var author: String? { return metadata.author } } diff --git a/Sources/Downpour/Metadata/VideoMetadata.swift b/Sources/Downpour/Metadata/VideoMetadata.swift index 59b7aaf..81da03c 100644 --- a/Sources/Downpour/Metadata/VideoMetadata.swift +++ b/Sources/Downpour/Metadata/VideoMetadata.swift @@ -11,6 +11,11 @@ open class VideoMetadata: Metadata, CustomStringConvertible { case altEpisodeSingle = "Episode \\d{1,2}" case altSeason2 = "[\\s_\\.\\-\\[]\\d{3}[\\s_\\.\\-\\]]" case year = "[\\(?:\\.\\s_\\[](?:19|(?:[2-9])(?:[0-9]))\\d{2}[\\]\\s_\\.\\)]" + + #if swift(>=4.2) + #else + static var allCases: [Pattern] = [.pretty, .tricky, .combined, .altSeason, .altSeasonSingle, .altEpisodeSingle, .altSeason2, .year] + #endif } public static let regexOptions: String.CompareOptions = [.regularExpression, .caseInsensitive] From 0f34f669114b46454606d525c6d51d6e66fc1116 Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Wed, 12 Sep 2018 14:10:27 -0600 Subject: [PATCH 10/20] Compiles under swift 4.2 now --- Sources/Downpour/Downpour+Protocol.swift | 7 ------- Sources/Downpour/Downpour.swift | 5 +++++ Sources/Downpour/Metadata/AVMetadata.swift | 2 +- Sources/Downpour/Metadata/ExifToolMetadata.swift | 2 +- Sources/Downpour/Metadata/VideoMetadata.swift | 2 +- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Sources/Downpour/Downpour+Protocol.swift b/Sources/Downpour/Downpour+Protocol.swift index f5dfcc0..7770ae4 100644 --- a/Sources/Downpour/Downpour+Protocol.swift +++ b/Sources/Downpour/Downpour+Protocol.swift @@ -7,10 +7,3 @@ public protocol Downpourable { init?(file path: FilePath) init(metadata: MetadataType) } - -public extension Downpourable { - public init?(file path: FilePath) { - guard let _md = MetadataType(file: path) else { return nil } - self.init(metadata: _md) - } -} diff --git a/Sources/Downpour/Downpour.swift b/Sources/Downpour/Downpour.swift index 013ef85..bb04702 100644 --- a/Sources/Downpour/Downpour.swift +++ b/Sources/Downpour/Downpour.swift @@ -25,4 +25,9 @@ open class Downpour<_MetadataType: Metadata>: CustomStringConvertible, Downpoura public required init(metadata: MetadataType) { self.metadata = metadata } + + public required convenience init?(file path: FilePath) { + guard let _md = MetadataType(file: path) else { return nil } + self.init(metadata: _md) + } } diff --git a/Sources/Downpour/Metadata/AVMetadata.swift b/Sources/Downpour/Metadata/AVMetadata.swift index 79af18b..00ddaa1 100644 --- a/Sources/Downpour/Metadata/AVMetadata.swift +++ b/Sources/Downpour/Metadata/AVMetadata.swift @@ -52,7 +52,7 @@ public class AVMetadata: Metadata { } } -public extension Downpour where MetadataType: AVMetadata { +extension Downpour where MetadataType: AVMetadata { public var title: String { return metadata.title } public var type: MetadataFormat { return metadata.type } public var creationDate: Date? { return metadata.creationDate } diff --git a/Sources/Downpour/Metadata/ExifToolMetadata.swift b/Sources/Downpour/Metadata/ExifToolMetadata.swift index 1db2ac0..9753516 100644 --- a/Sources/Downpour/Metadata/ExifToolMetadata.swift +++ b/Sources/Downpour/Metadata/ExifToolMetadata.swift @@ -85,7 +85,7 @@ public struct ExifToolMetadata: Metadata, Decodable { } } -public extension Downpour where MetadataType == ExifToolMetadata { +extension Downpour where MetadataType == ExifToolMetadata { public var creation: Date? { return metadata.creation } public var filetype: String? { return metadata.filetype } public var copyrights: [String]? { return metadata.copyrights } diff --git a/Sources/Downpour/Metadata/VideoMetadata.swift b/Sources/Downpour/Metadata/VideoMetadata.swift index 81da03c..f1a9030 100644 --- a/Sources/Downpour/Metadata/VideoMetadata.swift +++ b/Sources/Downpour/Metadata/VideoMetadata.swift @@ -237,7 +237,7 @@ open class VideoMetadata: Metadata, CustomStringConvertible { }() } -public extension Downpour where MetadataType: VideoMetadata { +extension Downpour where MetadataType: VideoMetadata { public var season: Int? { return metadata.season } public var episode: Int? { return metadata.episode } public var year: Int? { return metadata.year } From 6d3bb75c190fcb9c4061577f1db407674dcd213a Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Wed, 12 Sep 2018 15:12:07 -0600 Subject: [PATCH 11/20] Tests now compile and pass --- .../Downpour/Metadata/MetadataFormat.swift | 24 +++- Sources/Downpour/Metadata/VideoMetadata.swift | 22 +++- Tests/DownpourTests/DownpourTests.swift | 124 +++++++++--------- Tests/LinuxMain.swift | 2 +- 4 files changed, 99 insertions(+), 73 deletions(-) diff --git a/Sources/Downpour/Metadata/MetadataFormat.swift b/Sources/Downpour/Metadata/MetadataFormat.swift index 7f61dc3..421088c 100644 --- a/Sources/Downpour/Metadata/MetadataFormat.swift +++ b/Sources/Downpour/Metadata/MetadataFormat.swift @@ -20,13 +20,19 @@ public struct MetadataFormat: RawRepresentable, ExpressibleByIntegerLiteral, Has public init(rawValue: UInt8) { self.rawValue = rawValue } public init(integerLiteral value: UInt8) { self.init(rawValue: value) } + + public func contains(_ element: VideoFormat) -> Bool { + return VideoFormat(rawValue: rawValue & element.rawValue) == element + } } public let rawValue: UInt8 - var format: VideoFormat { return VideoFormat(rawValue: rawValue >> 4) } + + private var noFormat: MetadataFormat { return MetadataFormat(rawValue: rawValue & 0b0000_1111) } + public var format: VideoFormat { return VideoFormat(rawValue: rawValue >> 4) } public var description: String { - switch self { + switch noFormat { case .video: return "video(\(format))" case .subtitle: return "subtitle(\(format))" case .audio: return "audio" @@ -44,7 +50,7 @@ public struct MetadataFormat: RawRepresentable, ExpressibleByIntegerLiteral, Has return MetadataFormat(rawValue: MetadataFormat.video.rawValue | (format.rawValue << 4)) } - public static let subtitle: MetadataFormat = 0b0000_0011 + public static let subtitle: MetadataFormat = 0b0000_0010 public static func subtitle(format: VideoFormat) -> MetadataFormat { return MetadataFormat(rawValue: MetadataFormat.subtitle.rawValue | (format.rawValue << 4)) } @@ -57,5 +63,15 @@ public struct MetadataFormat: RawRepresentable, ExpressibleByIntegerLiteral, Has public init(rawValue: UInt8) { self.rawValue = rawValue } public init(integerLiteral value: UInt8) { self.init(rawValue: value) } -} + public static func == (lhs: MetadataFormat, rhs: MetadataFormat) -> Bool { + guard lhs.noFormat.rawValue == rhs.noFormat.rawValue else { + return lhs.rawValue == rhs.rawValue + } + return [lhs.format, rhs.format].contains(.unknown) ? true : lhs.format == rhs.format + } + + public func contains(_ element: MetadataFormat) -> Bool { + return MetadataFormat(rawValue: rawValue & element.rawValue) == element + } +} diff --git a/Sources/Downpour/Metadata/VideoMetadata.swift b/Sources/Downpour/Metadata/VideoMetadata.swift index f1a9030..22b3c97 100644 --- a/Sources/Downpour/Metadata/VideoMetadata.swift +++ b/Sources/Downpour/Metadata/VideoMetadata.swift @@ -77,11 +77,12 @@ open class VideoMetadata: Metadata, CustomStringConvertible { } } - open lazy var type: MetadataFormat = { - guard let ext = _extension else { - return .video - } + public init(name: String) { + _rawString = name + _extension = nil + } + open lazy var type: MetadataFormat = { // Sometimes it mestakes the x/h 264 as season 2, episode 64. I don't // know of any shows that have 64 episode in a single season, so // checking that the episode < 64 should be safe and will resolve these @@ -97,7 +98,7 @@ open class VideoMetadata: Metadata, CustomStringConvertible { private lazy var seasonEpisode: String? = { var _match: Range? var _patternMatched: Pattern? - for (index, pattern) in Pattern.allCases.enumerated() { + for (index, pattern) in Pattern.allCases.filter({ return $0 != .year}).enumerated() { if let __match = _rawString.range(of: pattern, options: VideoMetadata.regexOptions) { _match = __match _patternMatched = Pattern.allCases[index] @@ -237,8 +238,17 @@ open class VideoMetadata: Metadata, CustomStringConvertible { }() } -extension Downpour where MetadataType: VideoMetadata { +extension Downpour where MetadataType == VideoMetadata { public var season: Int? { return metadata.season } public var episode: Int? { return metadata.episode } public var year: Int? { return metadata.year } + + public convenience init?(filename: String) { + guard let _md = VideoMetadata(filename: filename) else { return nil } + self.init(metadata: _md) + } + + public convenience init(name: String) { + self.init(metadata: VideoMetadata(name: name)) + } } diff --git a/Tests/DownpourTests/DownpourTests.swift b/Tests/DownpourTests/DownpourTests.swift index 31aac74..dd031e0 100644 --- a/Tests/DownpourTests/DownpourTests.swift +++ b/Tests/DownpourTests/DownpourTests.swift @@ -9,7 +9,7 @@ import XCTest @testable import Downpour -class DownpourTests: XCTestCase { +class DownpourVideoTests: XCTestCase { override func setUp() { super.setUp() @@ -24,185 +24,185 @@ class DownpourTests: XCTestCase { func testMovie1() { let downpour = Downpour(name: "Movie.Name.2013.1080p.BluRay.H264.AAC.mp4") XCTAssertEqual(downpour.title, "Movie Name") - XCTAssertEqual(downpour.year, "2013") + XCTAssertEqual(downpour.year ?? 0, 2013) XCTAssertNil(downpour.season) XCTAssertNil(downpour.episode) - XCTAssertEqual(downpour.type, .some(.movie)) + XCTAssertEqual(downpour.type, .movie) } func testMovie2() { let downpour = Downpour(name: "Movie_Name_2_2017_x264_RARBG.avi") XCTAssertEqual(downpour.title, "Movie_Name_2") // FIXME - XCTAssertEqual(downpour.year, "2017") + XCTAssertEqual(downpour.year ?? 0, 2017) XCTAssertNil(downpour.season) XCTAssertNil(downpour.episode) - XCTAssertEqual(downpour.type, .some(.movie)) + XCTAssertEqual(downpour.type, .movie) } func testStandardShow1() { let downpour = Downpour(name: "Mr.Show.Name.S01E02.Source.Quality.Etc-Group") XCTAssertEqual(downpour.title, "Mr Show Name") - XCTAssertEqual(downpour.season, "01") - XCTAssertEqual(downpour.episode, "02") - XCTAssertEqual(downpour.type, .some(.tv)) + XCTAssertEqual(downpour.season, 1) + XCTAssertEqual(downpour.episode, 2) + XCTAssertEqual(downpour.type, .tv) XCTAssertNil(downpour.year) } func testStandardShow2() { let downpour = Downpour(name: "Show.Name.S01E02") XCTAssertEqual(downpour.title, "Show Name") - XCTAssertEqual(downpour.season, "01") - XCTAssertEqual(downpour.episode, "02") - XCTAssertEqual(downpour.type, .some(.tv)) + XCTAssertEqual(downpour.season, 1) + XCTAssertEqual(downpour.episode, 2) + XCTAssertEqual(downpour.type, .tv) XCTAssertNil(downpour.year) } func testStandardShow3() { let downpour = Downpour(name: "Show Name - S01E02 - My Ep Name") XCTAssertEqual(downpour.title, "Show Name") - XCTAssertEqual(downpour.season, "01") - XCTAssertEqual(downpour.episode, "02") - XCTAssertEqual(downpour.type, .some(.tv)) + XCTAssertEqual(downpour.season, 1) + XCTAssertEqual(downpour.episode, 2) + XCTAssertEqual(downpour.type, .tv) XCTAssertNil(downpour.year) } func testStandardShow4() { let downpour = Downpour(name: "Show.2.0.Name.S01.E03.My.Ep.Name-Group") XCTAssertEqual(downpour.title, "Show 2.0 Name") - XCTAssertEqual(downpour.season, "01") - XCTAssertEqual(downpour.episode, "03") - XCTAssertEqual(downpour.type, .some(.tv)) + XCTAssertEqual(downpour.season, 1) + XCTAssertEqual(downpour.episode, 3) + XCTAssertEqual(downpour.type, .tv) XCTAssertNil(downpour.year) } func testStandardShow5() { let downpour = Downpour(name: "Show Name - S06E01 - 2009-12-20 - Ep Name") XCTAssertEqual(downpour.title, "Show Name") - XCTAssertEqual(downpour.season, "06") - XCTAssertEqual(downpour.episode, "01") - XCTAssertEqual(downpour.type, .some(.tv)) + XCTAssertEqual(downpour.season, 6) + XCTAssertEqual(downpour.episode, 1) + XCTAssertEqual(downpour.type, .tv) XCTAssertNil(downpour.year) } func testStandardShow6() { let downpour = Downpour(name: "Show Name - S06E01 - -30-") XCTAssertEqual(downpour.title, "Show Name") - XCTAssertEqual(downpour.season, "06") - XCTAssertEqual(downpour.episode, "01") - XCTAssertEqual(downpour.type, .some(.tv)) + XCTAssertEqual(downpour.season, 6) + XCTAssertEqual(downpour.episode, 1) + XCTAssertEqual(downpour.type, .tv) XCTAssertNil(downpour.year) } func testStandardShow7() { let downpour = Downpour(name: "Show.Name.S06E01.Other.WEB-DL") XCTAssertEqual(downpour.title, "Show Name") - XCTAssertEqual(downpour.season, "06") - XCTAssertEqual(downpour.episode, "01") - XCTAssertEqual(downpour.type, .some(.tv)) + XCTAssertEqual(downpour.season, 6) + XCTAssertEqual(downpour.episode, 1) + XCTAssertEqual(downpour.type, .tv) XCTAssertNil(downpour.year) } func testStandardShow8() { let downpour = Downpour(name: "Show.Name.S06E01 Some-Stuff Here") XCTAssertEqual(downpour.title, "Show Name") - XCTAssertEqual(downpour.season, "06") - XCTAssertEqual(downpour.episode, "01") - XCTAssertEqual(downpour.type, .some(.tv)) + XCTAssertEqual(downpour.season, 6) + XCTAssertEqual(downpour.episode, 1) + XCTAssertEqual(downpour.type, .tv) XCTAssertNil(downpour.year) } func testStandardShow9() { let downpour = Downpour(name: "Show.Name-0.2010.S01E02.Source.Quality.Etc-Group") XCTAssertEqual(downpour.title, "Show Name-0") // FIXME - XCTAssertEqual(downpour.season, "01") - XCTAssertEqual(downpour.episode, "02") - XCTAssertEqual(downpour.type, .some(.tv)) - XCTAssertEqual(downpour.year, "2010") + XCTAssertEqual(downpour.season, 1) + XCTAssertEqual(downpour.episode, 2) + XCTAssertEqual(downpour.type, .tv) + XCTAssertEqual(downpour.year ?? 0, 2010) } func testStandardShow10() { let downpour = Downpour(name: "Show-Name-S06E01-720p") XCTAssertEqual(downpour.title, "Show-Name") // FIXME - XCTAssertEqual(downpour.season, "06") - XCTAssertEqual(downpour.episode, "01") - XCTAssertEqual(downpour.type, .some(.tv)) + XCTAssertEqual(downpour.season, 6) + XCTAssertEqual(downpour.episode, 1) + XCTAssertEqual(downpour.type, .tv) XCTAssertNil(downpour.year) } func testStandardShow11() { let downpour = Downpour(name: "Show Name - s2005e01") XCTAssertEqual(downpour.title, "Show Name") - XCTAssertEqual(downpour.season, "2005") - XCTAssertEqual(downpour.episode, "01") - XCTAssertEqual(downpour.type, .some(.tv)) + XCTAssertEqual(downpour.season, 2005) + XCTAssertEqual(downpour.episode, 1) + XCTAssertEqual(downpour.type, .tv) } func testStandardShow12() { let downpour = Downpour(name: "Show Name - s05e01") XCTAssertEqual(downpour.title, "Show Name") - XCTAssertEqual(downpour.season, "05") - XCTAssertEqual(downpour.episode, "01") - XCTAssertEqual(downpour.type, .some(.tv)) + XCTAssertEqual(downpour.season, 5) + XCTAssertEqual(downpour.episode, 1) + XCTAssertEqual(downpour.type, .tv) } func testFOVShow1() { let downpour = Downpour(name: "Show_Name.1x02.Source_Quality_Etc-Group") XCTAssertEqual(downpour.title, "Show_Name") // FIXME - XCTAssertEqual(downpour.season, "1") - XCTAssertEqual(downpour.episode, "02") - XCTAssertEqual(downpour.type, .some(.tv)) + XCTAssertEqual(downpour.season, 1) + XCTAssertEqual(downpour.episode, 2) + XCTAssertEqual(downpour.type, .tv) XCTAssertNil(downpour.year) } func testFOVShow2() { let downpour = Downpour(name: "Show Name 1x02") XCTAssertEqual(downpour.title, "Show Name") - XCTAssertEqual(downpour.season, "1") - XCTAssertEqual(downpour.episode, "02") - XCTAssertEqual(downpour.type, .some(.tv)) + XCTAssertEqual(downpour.season, 1) + XCTAssertEqual(downpour.episode, 2) + XCTAssertEqual(downpour.type, .tv) XCTAssertNil(downpour.year) } func testFOVShow3() { let downpour = Downpour(name: "Show Name 1x02 x264 Test") XCTAssertEqual(downpour.title, "Show Name") - XCTAssertEqual(downpour.season, "1") - XCTAssertEqual(downpour.episode, "02") - XCTAssertEqual(downpour.type, .some(.tv)) + XCTAssertEqual(downpour.season, 1) + XCTAssertEqual(downpour.episode, 2) + XCTAssertEqual(downpour.type, .tv) XCTAssertNil(downpour.year) } func testFOVShow4() { let downpour = Downpour(name: "Show Name - 1x02 - My Ep Name") XCTAssertEqual(downpour.title, "Show Name") - XCTAssertEqual(downpour.season, "1") - XCTAssertEqual(downpour.episode, "02") - XCTAssertEqual(downpour.type, .some(.tv)) + XCTAssertEqual(downpour.season, 1) + XCTAssertEqual(downpour.episode, 2) + XCTAssertEqual(downpour.type, .tv) XCTAssertNil(downpour.year) } func testFOVShow5() { let downpour = Downpour(name: "Show Name 1x02 x264 Test") XCTAssertEqual(downpour.title, "Show Name") - XCTAssertEqual(downpour.season, "1") - XCTAssertEqual(downpour.episode, "02") - XCTAssertEqual(downpour.type, .some(.tv)) + XCTAssertEqual(downpour.season, 1) + XCTAssertEqual(downpour.episode, 2) + XCTAssertEqual(downpour.type, .tv) XCTAssertNil(downpour.year) } func testFOVShow6() { let downpour = Downpour(name: "Show Name - 1x02 - My Ep Name") XCTAssertEqual(downpour.title, "Show Name") - XCTAssertEqual(downpour.season, "1") - XCTAssertEqual(downpour.episode, "02") - XCTAssertEqual(downpour.type, .some(.tv)) + XCTAssertEqual(downpour.season, 1) + XCTAssertEqual(downpour.episode, 2) + XCTAssertEqual(downpour.type, .tv) XCTAssertNil(downpour.year) } } #if os(Linux) -extension DownpourTests { - static var allTests: [(String, (DownpourTests) -> () throws -> Void)] { +extension DownpourVideoTests { + static var allTests: [(String, (DownpourVideoTests) -> () throws -> Void)] { return [ ("testMovie1", testMovie1), ("testMovie2", testMovie2), diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 30d6c30..51228e1 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -2,5 +2,5 @@ import XCTest @testable import DownpourTests XCTMain([ - testCase(DownpourTests.allTests) + testCase(DownpourVideoTests.allTests) ]) From 06e0358254631e18182b8bab2e55faa0a944c1b6 Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Wed, 12 Sep 2018 15:25:27 -0600 Subject: [PATCH 12/20] Updated Readme --- README.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e80eae8..e8e6580 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Downpour -[![license](https://img.shields.io/github/license/mashape/apistatus.svg?maxAge=2592000)](https://github.com/steve228uk/Downpour/blob/master/LICENSE) [![Build Status](https://travis-ci.org/TryFetch/Downpour.svg?branch=master)](https://travis-ci.org/TryFetch/Downpour) +[![license](https://img.shields.io/badge/license-GPLv3-blue.svg)](https://github.com/Ponyboy47/Downpour/blob/master/LICENSE) [![Build Status](https://travis-ci.org/Ponyboy47/Downpour.svg?branch=master)](https://travis-ci.org/Ponyboy47/Downpour) Downpour was built for [Fetch](http://getfetchapp.com) — a Put.io client — to parse TV & Movie information from downloaded files. It can be used on any platform that can run Swift as it only relies on Foundation. @@ -42,17 +42,26 @@ And from Linux (Ubuntu if the libimage-exiftool-perl package is installed): NOTE: None of the fields are guaranteed to be there or even picked up, it's kinda hard to extract metadata from file names with only a few clever regexes and audio data from files is difficult to do cross-platform. Please open an issue if you know the data is there, but it's not being picked up. Also, it means everything is Optional and be sure to use `guard/if let` or nil-coalescing (`??`) to program safely. :) +## Installation +### Swift Package Manager: +This supports SPM installation for swift 4.2+ by adding the following to your Package.swift dependencies: +```swift +.package(url: "https://github.com/Ponyboy47/Downpour.git", from: "0.7.0") +``` +For swift 4.0 or 4.1 use 0.6.x +For swift 3 use 0.4.x + ## Usage Using Downpour is easy. Just create a new instance and it'll do the rest. ```swift -let dvd_rip = Downpour(string: filename) +let dvd_rip = Downpour(filename: filename) let title = dvd_rip.title let year = dvd_rip.year -if downpour.type == .TV { +if downpour.type == .tv { let season = dvd_rip.season let episode = dvd_rip.episode } @@ -63,11 +72,3 @@ if downpour.type == .TV { - Designed to work with media ripped using the popular [MakeMKV](http://makemkv.com) utility - Organizing your media files -## Installation - -Add to your project using the Swift Package Manager by adding the following dependency to your Package.swift: -```swift -.package(url: "https://github.com/Ponyboy47/Downpour.git", from: "0.8.0") -``` - -For swift 3 use 0.4.x From b47e9e99d49c19ba4ac7ef80a0af97808152dd28 Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Wed, 12 Sep 2018 15:26:50 -0600 Subject: [PATCH 13/20] Fixed versions --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e8e6580..72eb0e8 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,11 @@ NOTE: None of the fields are guaranteed to be there or even picked up, it's kind ## Installation ### Swift Package Manager: -This supports SPM installation for swift 4.2+ by adding the following to your Package.swift dependencies: +This supports SPM installation for swift 4.1+ by adding the following to your Package.swift dependencies: ```swift .package(url: "https://github.com/Ponyboy47/Downpour.git", from: "0.7.0") ``` -For swift 4.0 or 4.1 use 0.6.x +For swift 4.0 use 0.6.x For swift 3 use 0.4.x ## Usage From f6ae0f51f015604fa99481b05a1e6a43dea3f805 Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Wed, 12 Sep 2018 15:34:07 -0600 Subject: [PATCH 14/20] Update travis config --- .travis.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1f2fac8..a7b7fcd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ os: - linux - osx -osx_image: xcode8.3 +osx_image: xcode9.4 dist: trusty sudo: required language: generic @@ -11,8 +11,9 @@ language: generic #before_install: # - sudo apt-get update # - sudo apt-get install -y libimage-exiftool-perl +before_install: + - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"; fi install: - - eval "$(curl -sL https://gist.githubusercontent.com/kylef/5c0475ff02b7c7671d2a/raw/9f442512a46d7a2af7b850d65a7e9bd31edfb09b/swiftenv-install.sh)" + - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then swiftenv install $(cat .swift-version) | grep -qe "\(been\|already\) installed"; fi script: - - swift build -c release - - swift test + - travis_wait swift test From 54d09a339acad97edbf57b551a958d12af3f8809 Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Mon, 17 Sep 2018 13:28:33 -0600 Subject: [PATCH 15/20] Update to official swift version 4.2 --- .swift-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.swift-version b/.swift-version index 0597c57..bf77d54 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -4.2-CONVERGENCE +4.2 From 499cd077a8d536dcb740a4ebd0e8330e2ce81004 Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Mon, 17 Sep 2018 13:29:23 -0600 Subject: [PATCH 16/20] Bump version for swift 4.2 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 72eb0e8..16d72a4 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ NOTE: None of the fields are guaranteed to be there or even picked up, it's kind ### Swift Package Manager: This supports SPM installation for swift 4.1+ by adding the following to your Package.swift dependencies: ```swift -.package(url: "https://github.com/Ponyboy47/Downpour.git", from: "0.7.0") +.package(url: "https://github.com/Ponyboy47/Downpour.git", from: "0.7.1") ``` For swift 4.0 use 0.6.x For swift 3 use 0.4.x From 9521b9b4663425f1ecb3b98786af8931a5bea4a5 Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Mon, 17 Sep 2018 14:18:15 -0600 Subject: [PATCH 17/20] Increase xcode version for travis ci so that OSX builds use swift 4.2 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a7b7fcd..497ab37 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ os: - linux - osx -osx_image: xcode9.4 +osx_image: xcode10 dist: trusty sudo: required language: generic From c6654c1041a0369388480afc11b599d668e73987 Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Tue, 26 Mar 2019 09:22:18 -0600 Subject: [PATCH 18/20] Upgrade to swift 5 --- .swift-version | 2 +- Package.swift | 6 +++--- README.md | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.swift-version b/.swift-version index bf77d54..819e07a 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -4.2 +5.0 diff --git a/Package.swift b/Package.swift index c8fe0fe..46ba89e 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:4.0 +// swift-tools-version:5.0 import PackageDescription @@ -8,8 +8,8 @@ let package = Package( .library(name: "Downpour", targets: ["Downpour"]) ], dependencies: [ - .package(url: "https://github.com/Ponyboy47/TrailBlazer.git", from: "0.11.0"), - .package(url: "https://github.com/kareman/SwiftShell.git", from: "4.1.0") + .package(url: "https://github.com/Ponyboy47/TrailBlazer.git", from: "0.15.0"), + .package(url: "https://github.com/Ponyboy47/SwiftShell.git", from: "4.2.0") ], targets: [ .target( diff --git a/README.md b/README.md index 16d72a4..e9ddcf1 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,11 @@ NOTE: None of the fields are guaranteed to be there or even picked up, it's kind ## Installation ### Swift Package Manager: -This supports SPM installation for swift 4.1+ by adding the following to your Package.swift dependencies: +This supports SPM installation for swift 5.0 by adding the following to your Package.swift dependencies: ```swift -.package(url: "https://github.com/Ponyboy47/Downpour.git", from: "0.7.1") +.package(url: "https://github.com/Ponyboy47/Downpour.git", from: "0.8.0") ``` -For swift 4.0 use 0.6.x +For swift 4.x use 0.7.x For swift 3 use 0.4.x ## Usage From 0f82489d8e791d48c705874ef218d69e6a4027f0 Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Tue, 2 Jul 2019 14:07:18 -0600 Subject: [PATCH 19/20] Bump some version stuff and use a better travis xcode image --- .swift-version | 2 +- .travis.yml | 2 +- Package.swift | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.swift-version b/.swift-version index 819e07a..6b244dc 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -5.0 +5.0.1 diff --git a/.travis.yml b/.travis.yml index 497ab37..174215e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ os: - linux - osx -osx_image: xcode10 +osx_image: xcode10.2 dist: trusty sudo: required language: generic diff --git a/Package.swift b/Package.swift index 46ba89e..5d003fd 100644 --- a/Package.swift +++ b/Package.swift @@ -8,8 +8,8 @@ let package = Package( .library(name: "Downpour", targets: ["Downpour"]) ], dependencies: [ - .package(url: "https://github.com/Ponyboy47/TrailBlazer.git", from: "0.15.0"), - .package(url: "https://github.com/Ponyboy47/SwiftShell.git", from: "4.2.0") + .package(url: "https://github.com/Ponyboy47/TrailBlazer.git", from: "0.16.0"), + .package(url: "https://github.com/kareman/SwiftShell.git", from: "5.0.0") ], targets: [ .target( From b65be562502f960cf798602b2bb89fe059d3314f Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Tue, 2 Jul 2019 15:09:11 -0600 Subject: [PATCH 20/20] Bump macOS version --- Package.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Package.swift b/Package.swift index 5d003fd..1054dce 100644 --- a/Package.swift +++ b/Package.swift @@ -4,6 +4,7 @@ import PackageDescription let package = Package( name: "Downpour", + platforms: [.macOS(.v10_14)], products: [ .library(name: "Downpour", targets: ["Downpour"]) ],