Initial creation of the NPOKit framework
|
@ -0,0 +1,76 @@
|
|||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## Build generated
|
||||
build/
|
||||
DerivedData/
|
||||
|
||||
## Various settings
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata/
|
||||
|
||||
## Other
|
||||
*.moved-aside
|
||||
*.xcuserstate
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
# Swift Package Manager
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
.build/
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
#
|
||||
# Pods/
|
||||
|
||||
# Carthage
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||
# screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
|
||||
# Swift Packagemanager
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
|
||||
# some devnotes that don't need to go into the repo
|
||||
devnotes.md
|
||||
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
disabled_rules:
|
||||
- line_length
|
||||
- trailing_whitespace
|
||||
- todo
|
||||
- large_tuple
|
||||
- notification_center_detachment
|
||||
opt_in_rules:
|
||||
- force_unwrapping
|
||||
excluded:
|
||||
- Carthage
|
||||
- Pods
|
||||
- NPOKit/Carthage
|
||||
- NPOKit/Pods
|
||||
variable_name:
|
||||
min_length: 2
|
||||
file_length:
|
||||
warning: 600
|
||||
error: 1200
|
||||
type_body_length:
|
||||
warning: 350
|
||||
error: 500
|
||||
excluded:
|
||||
- Pods
|
|
@ -0,0 +1,17 @@
|
|||
language: objective-c
|
||||
os: osx
|
||||
osx_image: xcode9
|
||||
env:
|
||||
global:
|
||||
- NSUnbufferedIO=YES
|
||||
install: ./bin/install_swiftlint.sh
|
||||
script:
|
||||
- set -o pipefail
|
||||
- swift --version
|
||||
- swift package generate-xcodeproj
|
||||
- xcodebuild -version
|
||||
- xcodebuild -showsdks
|
||||
- xcodebuild -list
|
||||
- xcpretty -v
|
||||
- xcodebuild -scheme "NPOKit-Package" -sdk "appletvsimulator11.0" -configuration Release clean build ONLY_ACTIVE_ARCH=NO CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO RUN_CLANG_STATIC_ANALYZER=YES|xcpretty -c
|
||||
- swift build --verbose
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2018 Jeroen Wesbeek
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,3 @@
|
|||
ASSETS
|
||||
------
|
||||
The included broadcaster and channel logos are copyright by the Nederlandse Publieke Omroep (NPO) and their respective copyright owners. The included imagery is included for demonstration purposes and can be considered as art.
|
|
@ -0,0 +1,21 @@
|
|||
Pod::Spec.new do |spec|
|
||||
spec.name = 'NPOKit'
|
||||
spec.version = '0.0.1'
|
||||
spec.summary = 'NPOKit framework for interfacing with the Dutch Public Broadcaster'
|
||||
spec.homepage = 'https://github.com/4np/NPOKit'
|
||||
spec.license = { type: 'APACHE', file: 'LICENSE' }
|
||||
spec.authors = { "Jeroen Wesbeek" => 'github@osx.eu' }
|
||||
spec.documentation_url = 'https://github.com/4np/NPOKit/blob/master/README.md'
|
||||
|
||||
spec.platforms = { :ios => '11.0', :osx => '12.0', :tvos => '11.0' }
|
||||
spec.requires_arc = true
|
||||
spec.pod_target_xcconfig = { 'SWIFT_VERSION' => '4.0' }
|
||||
spec.source = { :git => 'https://github.com/4np/NPOKit.git', :tag => "#{spec.version}" }
|
||||
|
||||
spec.default_subspecs = 'Core'
|
||||
|
||||
# main NPOKit Framework
|
||||
spec.subspec 'Core' do |core|
|
||||
core.source_files = 'Sources/NPOKit/**/*.{swift}'
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
// swift-tools-version:4.0
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "NPOKit",
|
||||
products: [
|
||||
// Products define the executables and libraries produced by a package, and make them visible to other packages.
|
||||
.library(
|
||||
name: "NPOKit",
|
||||
targets: ["NPOKit"])
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
|
||||
.target(
|
||||
name: "NPOKit",
|
||||
dependencies: []),
|
||||
.testTarget(
|
||||
name: "NPOKitTests",
|
||||
dependencies: ["NPOKit"])
|
||||
]
|
||||
)
|
|
@ -0,0 +1,221 @@
|
|||
# NPOKit
|
||||
|
||||
[](https://travis-ci.org/4np/NPOKit)
|
||||
[](https://github.com/4np/UitzendingGemist/releases/latest)
|
||||
[](https://developer.apple.com/tvos/)
|
||||
[](https://swift.org)
|
||||
[](https://github.com/4np/NPOKit/issues)
|
||||
[](https://github.com/4np/NPOKit/issues?q=is%3Aissue+is%3Aclosed)
|
||||
|
||||
`NPOKit` is a `Swift 4` framework for interfacing with Dutch Public Broadcaster's (_Nederlandse Publieke Omroep_ - [NPO](https://www.npo.nl)) APIs. It supports fetching _programs_, _episodes_ and _video steams_ for playback.
|
||||
|
||||
_Note: This project is in active development so method signatures might change between versions without notice! Consider this in alpha stage... Changes that are likely coming are removing the failure closure in favor of an error argument. Note that this has currently only been tested on tvOS!_
|
||||
|
||||
## Installation
|
||||
|
||||
Add something similar to the following lines to your `Podfile`. You may need to adjust based on your platform, version/branch etc.
|
||||
|
||||
### Cocoapods
|
||||
|
||||
Using cocoapods is the most common way of installing frameworks.
|
||||
|
||||
```
|
||||
source 'https://github.com/CocoaPods/Specs.git'
|
||||
platform :tvos, '11.0'
|
||||
use_frameworks!
|
||||
|
||||
pod 'NPOKit', :git => 'https://github.com/4np/NPOKit.git'
|
||||
```
|
||||
|
||||
### Swift Package Manager
|
||||
|
||||
Add the following entry to your package's dependencies:
|
||||
|
||||
```
|
||||
.package(url: "https://github.com/4np/NPOKit.git", from: "0.0.1")
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Fetching programs
|
||||
|
||||
Below you'll find some sample code on how to implement some paginated fetching of programs. Unfortunately the API currently does not support sorting in alphabetical order, so the result will be based by the sort order the NPO returns (which is by most used).
|
||||
|
||||
```
|
||||
func getProgramPaginator(successHandler: @escaping Paginator<Program>.SuccessHandler,
|
||||
failureHandler: Paginator<Program>.FailureHandler? = nil) -> Paginator<Program>
|
||||
```
|
||||
|
||||
#### Example:
|
||||
|
||||
This code assumes you use a _scroll view_ in your user interface, so something like a _table view_ or a _collection view_.
|
||||
|
||||
```swift
|
||||
import NPOKit
|
||||
|
||||
class MyViewController: UIViewController {
|
||||
private var paginator: Paginator<Item>?
|
||||
private var programs = [Item]()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// set up paginator
|
||||
setupPaginator()
|
||||
}
|
||||
|
||||
// MARK: Networking
|
||||
|
||||
private func setupPaginator() {
|
||||
// set up paginator
|
||||
paginator = NPOKit.shared.getProgramPaginator(successHandler: { [weak self] (paginator, programs) in
|
||||
// on the main queue as this might involve UI updating
|
||||
DispatchQueue.main.async {
|
||||
// append the new batch of programs
|
||||
self?.programs.append(contentsOf: programs)
|
||||
// TODO: make sure you update the UI accordingly...
|
||||
}
|
||||
}, failureHandler: { (_) in
|
||||
print("failure :'(")
|
||||
})
|
||||
|
||||
// fetch the first page
|
||||
paginator?.next()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UIScrollViewDelegate
|
||||
extension ProgramsViewController: UIScrollViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
guard let collectionView = collectionView, let paginator = paginator else { return }
|
||||
|
||||
let numberOfPagesToInitiallyFetch = 2
|
||||
let yOffsetToLoadNextPage = collectionView.contentSize.height - (collectionView.bounds.height * CGFloat(numberOfPagesToInitiallyFetch))
|
||||
|
||||
guard scrollView.contentOffset.y > yOffsetToLoadNextPage else { return }
|
||||
|
||||
// fetch the next page of programs...
|
||||
paginator.next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fetching episodes
|
||||
|
||||
Fetching episodes works very much like fetching programs (see above), it just requires a `program` argument when setting up the paginator:
|
||||
|
||||
```
|
||||
func getEpisodePaginator(for item: Item,
|
||||
successHandler: @escaping Paginator<Episode>.SuccessHandler,
|
||||
failureHandler: Paginator<Episode>.FailureHandler? = nil) -> Paginator<Episode>
|
||||
```
|
||||
|
||||
### Fetching images
|
||||
|
||||
`Item` bases resources (like `Program` and `Episode`) may provide images for different usages. The most common way you would use those images on `tvOS` are for populating collection view cells, or by showing a header:
|
||||
|
||||
```
|
||||
func fetchCollectionImage(for item: Item, completionHandler completed: @escaping (UIImage?, URLSessionDataTask?, Error?) -> Void) -> URLSessionDataTask?
|
||||
|
||||
func fetchHeaderImage(for item: Item, completionHandler: @escaping (UIImage?, URLSessionDataTask?, Error?) -> Void) -> URLSessionDataTask?
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
`NPOKit` support logging through any logging framework your application uses by providing a `logging wrapper`. Below is an example of how to bind based on Dave Wood's [XCGLogger](https://github.com/DaveWoodCom/XCGLogger) that was developed completely in Swift.
|
||||
|
||||
### LoggerWrapper
|
||||
|
||||
First, you need to set up the `LoggerWrapper` which inherits from `NPOKitLogger`. It basically normalizes the logging calls to your logging framework of choice, in this case `XCGLogger`:
|
||||
|
||||
```
|
||||
import Foundation
|
||||
import NPOKit
|
||||
|
||||
// Logging Wrapper
|
||||
class LoggerWrapper: NPOKitLogger {
|
||||
public static let shared = LoggerWrapper()
|
||||
|
||||
override func verbose(_ closure: @autoclosure () -> Any?, functionName: StaticString = #function, fileName: StaticString = #file, lineNumber: Int = #line, userInfo: [String: Any] = [:]) {
|
||||
log.verbose(closure, functionName: functionName, fileName: fileName, lineNumber: lineNumber, userInfo: userInfo)
|
||||
}
|
||||
|
||||
override func debug(_ closure: @autoclosure () -> Any?, functionName: StaticString = #function, fileName: StaticString = #file, lineNumber: Int = #line, userInfo: [String: Any] = [:]) {
|
||||
log.debug(closure, functionName: functionName, fileName: fileName, lineNumber: lineNumber, userInfo: userInfo)
|
||||
}
|
||||
|
||||
override func info(_ closure: @autoclosure () -> Any?, functionName: StaticString = #function, fileName: StaticString = #file, lineNumber: Int = #line, userInfo: [String: Any] = [:]) {
|
||||
log.info(closure, functionName: functionName, fileName: fileName, lineNumber: lineNumber, userInfo: userInfo)
|
||||
}
|
||||
|
||||
override func warning(_ closure: @autoclosure () -> Any?, functionName: StaticString = #function, fileName: StaticString = #file, lineNumber: Int = #line, userInfo: [String: Any] = [:]) {
|
||||
log.warning(closure, functionName: functionName, fileName: fileName, lineNumber: lineNumber, userInfo: userInfo)
|
||||
}
|
||||
|
||||
override func error(_ closure: @autoclosure () -> Any?, functionName: StaticString = #function, fileName: StaticString = #file, lineNumber: Int = #line, userInfo: [String: Any] = [:]) {
|
||||
log.error(closure, functionName: functionName, fileName: fileName, lineNumber: lineNumber, userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AppDelegate
|
||||
|
||||
In your `AppDelegate`'s `application:didFinishLaunchingWithOptions:` you need to bind your `LoggerWrapper` to `NPOKit`, and logging will work. Set the loglevel to `debug` to get debug information or `verbose` to more elaborate information like `GET` and `POST` requests.
|
||||
|
||||
```
|
||||
import XCGLogger
|
||||
import NPOKit
|
||||
|
||||
let log = XCGLogger.default
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
|
||||
// configure logging
|
||||
log.setup(level: .verbose, showThreadName: false, showLevel: true, showFileNames: true, showLineNumbers: true)
|
||||
|
||||
// log entry
|
||||
log.info("Application launched.")
|
||||
log.logAppDetails()
|
||||
|
||||
// bind logger to NPOKit
|
||||
NPOKit.shared.log = LoggerWrapper.shared
|
||||
|
||||
return true
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
# Working on `NPOKit`
|
||||
|
||||
In order to work on `NPOKit` in `Xcode`, you need to generate an Xcode project. Run the following command in the project root:
|
||||
|
||||
```
|
||||
swift package generate-xcodeproj
|
||||
```
|
||||
|
||||
_Note: the Xcode project will not be comitted to git._
|
||||
|
||||
|
||||
# License
|
||||
|
||||
See the accompanying [LICENSE](LICENSE) and [NOTICE](NOTICE) files for more information.
|
||||
|
||||
```
|
||||
Copyright 2018 Jeroen Wesbeek
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
```
|
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// CrossPlatform.swift
|
||||
// NPOKitPackageDescription
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 02/02/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
import UIKit
|
||||
|
||||
public typealias UXImage = UIImage
|
||||
|
||||
extension UIImage {
|
||||
convenience init?(withName name: String) {
|
||||
self.init(named: name)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(OSX)
|
||||
import Cocoa
|
||||
|
||||
public typealias UXImage = NSImage
|
||||
|
||||
extension NSImage {
|
||||
convenience init?(withName name: String) {
|
||||
self.init(named: NSImage.Name(name))
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
// String+Obfuscation.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 01/02/2018.
|
||||
// Copyright © 2018 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
/// Just come random key for XOR-ing so we don't have people Googling their
|
||||
/// way here when searching for specific strings...
|
||||
private static let xorKey = [UInt8]("5thjdfgd8fhfsd83hjeafsjds84hjs8w".utf8)
|
||||
|
||||
/// Obfuscate a String (String > XOR > Hex)
|
||||
var obfuscated: String {
|
||||
let text = [UInt8](self.utf8)
|
||||
let encrypted = text.enumerated().map({ $0.element ^ String.xorKey[$0.offset] })
|
||||
return encrypted.map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
|
||||
/// Deobfuscate a String (Hex > XOR > String)
|
||||
var deobfuscated: String? {
|
||||
var data = Data(capacity: self.count / 2)
|
||||
|
||||
guard let regex = try? NSRegularExpression(pattern: "[0-9a-f]{1,2}", options: .caseInsensitive) else { return nil }
|
||||
|
||||
regex.enumerateMatches(in: self, options: .anchored, range: NSRange(location: 0, length: utf16.count)) { (result, _, _) in
|
||||
guard let match = result?.range, let range = Range(match, in: self), let number = UInt8(self[range], radix: 16) else { return }
|
||||
var mutableNumber = number
|
||||
data.append(&mutableNumber, count: 1)
|
||||
}
|
||||
|
||||
let decrypted = data.enumerated().map({ $0.element ^ String.xorKey[$0.offset] })
|
||||
return String(bytes: decrypted, encoding: .utf8)
|
||||
}
|
||||
}
|
BIN
Sources/NPOKit/Media.xcassets/Broadcaster Logos/AVROTROS.imageset/AVRO-TROS.png
vendored
Normal file
After Width: | Height: | Size: 93 KiB |
21
Sources/NPOKit/Media.xcassets/Broadcaster Logos/AVROTROS.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "AVRO-TROS.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
21
Sources/NPOKit/Media.xcassets/Broadcaster Logos/KRO-NCRV.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "KRO-NCRV.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Sources/NPOKit/Media.xcassets/Broadcaster Logos/KRO-NCRV.imageset/KRO-NCRV.png
vendored
Normal file
After Width: | Height: | Size: 75 KiB |
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Omroep Max.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Sources/NPOKit/Media.xcassets/Broadcaster Logos/MAX.imageset/Omroep Max.png
vendored
Normal file
After Width: | Height: | Size: 45 KiB |
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "NPO_101.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 57 KiB |
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "NPO_BEST.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 46 KiB |
21
Sources/NPOKit/Media.xcassets/Channel Logos/cultura24.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "NPO_CULTURA.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Sources/NPOKit/Media.xcassets/Channel Logos/cultura24.imageset/NPO_CULTURA.png
vendored
Normal file
After Width: | Height: | Size: 45 KiB |
21
Sources/NPOKit/Media.xcassets/Channel Logos/journaal24.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "NPO_NIEUWS.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Sources/NPOKit/Media.xcassets/Channel Logos/journaal24.imageset/NPO_NIEUWS.png
vendored
Normal file
After Width: | Height: | Size: 47 KiB |
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "NPO_1.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 27 KiB |
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "NPO_2.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 27 KiB |
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "NPO_3.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 28 KiB |
21
Sources/NPOKit/Media.xcassets/Channel Logos/omroepBrabant.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "omroepBrabant.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Sources/NPOKit/Media.xcassets/Channel Logos/omroepBrabant.imageset/omroepBrabant.png
vendored
Normal file
After Width: | Height: | Size: 55 KiB |
21
Sources/NPOKit/Media.xcassets/Channel Logos/omroepGelderland.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "omroepGelderland.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Sources/NPOKit/Media.xcassets/Channel Logos/omroepGelderland.imageset/omroepGelderland.png
vendored
Normal file
After Width: | Height: | Size: 37 KiB |
21
Sources/NPOKit/Media.xcassets/Channel Logos/politiek24.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "NPO_POLITIEK.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Sources/NPOKit/Media.xcassets/Channel Logos/politiek24.imageset/NPO_POLITIEK.png
vendored
Normal file
After Width: | Height: | Size: 50 KiB |
21
Sources/NPOKit/Media.xcassets/Channel Logos/rtvDrenthe.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "rtvDrenthe.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Sources/NPOKit/Media.xcassets/Channel Logos/rtvDrenthe.imageset/rtvDrenthe.png
vendored
Normal file
After Width: | Height: | Size: 30 KiB |
21
Sources/NPOKit/Media.xcassets/Channel Logos/zappelin24.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "NPO_ZAPP.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Sources/NPOKit/Media.xcassets/Channel Logos/zappelin24.imageset/NPO_ZAPP.png
vendored
Normal file
After Width: | Height: | Size: 53 KiB |
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "NPO_ZAPP_XTRA.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Sources/NPOKit/Media.xcassets/Channel Logos/zappxtra.imageset/NPO_ZAPP_XTRA.png
vendored
Normal file
After Width: | Height: | Size: 95 KiB |
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
//
|
||||
// Channel.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 27/10/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// Some unknown (incorrect?) channel names the API returns:
|
||||
// - POMS_S_KRO_098910 -> Spoorloos
|
||||
// - KN_1684845 -> Mijn Maria
|
||||
// - OPVO -> The Passion
|
||||
// - POMS_S_MAX_374456 -> Zwarte Zwanen
|
||||
// - POW_03344601 -> Vier seizoenen aan de Amstel
|
||||
// - VPWON_1246643 -> Ondersteboven Nederland in de jaren 60
|
||||
// - POMS_S_NCRV_059866 -> Schepper & Co
|
||||
// - RAD5 -> De dienst van Freek
|
||||
|
||||
public enum Channel: String, Codable {
|
||||
case unkown
|
||||
case npo1 = "NED1"
|
||||
case npo2 = "NED2"
|
||||
case npo3 = "NED3"
|
||||
case zappelin = "ZAPP"
|
||||
case zappelinExtra = "ZAPPE"
|
||||
case cultura = "CULT"
|
||||
case omroepGelderland = "GELD"
|
||||
case rtvDrenthe = "TVDR"
|
||||
case omroepBrabant = "BRAB"
|
||||
}
|
||||
|
||||
extension Channel {
|
||||
public var name: String? {
|
||||
switch self {
|
||||
case .unkown:
|
||||
return nil
|
||||
case .npo1:
|
||||
return "NPO 1"
|
||||
case .npo2:
|
||||
return "NPO 2"
|
||||
case .npo3:
|
||||
return "NPO 3"
|
||||
case .zappelin:
|
||||
return "Zappelin"
|
||||
case .zappelinExtra:
|
||||
return "Zappelin Xtra"
|
||||
case .cultura:
|
||||
return "Cultura"
|
||||
case .omroepGelderland:
|
||||
return "Omroep Gelderland"
|
||||
case .rtvDrenthe:
|
||||
return "RTV Drenthe"
|
||||
case .omroepBrabant:
|
||||
return "Omroep Brabant"
|
||||
}
|
||||
}
|
||||
|
||||
public var logo: UXImage? {
|
||||
switch self {
|
||||
case .unkown:
|
||||
return nil
|
||||
case .npo1:
|
||||
return UXImage(withName: "npo1")
|
||||
case .npo2:
|
||||
return UXImage(withName: "npo2")
|
||||
case .npo3:
|
||||
return UXImage(withName: "npo3")
|
||||
case .zappelin:
|
||||
return UXImage(withName: "zappelin24")
|
||||
case .zappelinExtra:
|
||||
return UXImage(withName: "zappxtra")
|
||||
case .cultura:
|
||||
return UXImage(withName: "cultura24")
|
||||
case .omroepGelderland:
|
||||
return UXImage(withName: "omroepGelderland")
|
||||
case .rtvDrenthe:
|
||||
return UXImage(withName: "rtvDrenthe")
|
||||
case .omroepBrabant:
|
||||
return UXImage(withName: "omroepBrabant")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// Component.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 27/10/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// There is also a component 'subType' but that always
|
||||
/// seems to be 'default' (at least for now) and default
|
||||
/// does not work well inside enums...
|
||||
enum ComponentType: String, Codable {
|
||||
case unknown
|
||||
|
||||
// Catalogue component types
|
||||
case filter
|
||||
case grid
|
||||
case loadmore
|
||||
|
||||
// Home component types
|
||||
case spotlightheader
|
||||
case continuewatching
|
||||
case subscription
|
||||
case lane
|
||||
}
|
||||
|
||||
struct Component: Codable {
|
||||
var id: String
|
||||
var title: String?
|
||||
private var typeName: String
|
||||
private var subTypeName: String
|
||||
var isPlaceholder: Bool = false
|
||||
var filters: [Filter]?
|
||||
var data: ComponentData?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id = "id"
|
||||
case title
|
||||
case typeName = "type"
|
||||
case subTypeName = "subType"
|
||||
case isPlaceholder
|
||||
case filters
|
||||
case data
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Calculated properties
|
||||
extension Component {
|
||||
|
||||
// MARK: Enums
|
||||
|
||||
var type: ComponentType? {
|
||||
return ComponentType(rawValue: typeName)
|
||||
}
|
||||
}
|
||||
|
||||
struct ComponentData: Codable {
|
||||
var total: Int
|
||||
var count: Int
|
||||
var items: [Item]
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// Filter.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 14/12/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct Filter: Codable {
|
||||
public private(set) var title: String
|
||||
public private(set) var type: String
|
||||
internal var argumentName: String
|
||||
public private(set) var options = [FilterOption]()
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case title
|
||||
case type = "filterType"
|
||||
case argumentName = "filterArgument"
|
||||
case options
|
||||
}
|
||||
}
|
||||
|
||||
extension Filter: Equatable { }
|
||||
|
||||
public func == (lhs: Filter, rhs: Filter) -> Bool {
|
||||
return lhs.type == rhs.type && lhs.argumentName == rhs.argumentName
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// FilterLink.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 18/12/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FilterLink: Codable {
|
||||
internal var href: URL
|
||||
internal var collectionIdentifier: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case href
|
||||
case collectionIdentifier = "collectionId"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// FilterOption.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 18/12/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct FilterOption: Codable {
|
||||
public internal(set) var title: String
|
||||
internal var value: String?
|
||||
public internal(set) var isDefault = false
|
||||
private var links: [String: FilterLink]?
|
||||
|
||||
var isCollection: Bool {
|
||||
return collectionIdentifier != nil
|
||||
}
|
||||
|
||||
var collectionIdentifier: String? {
|
||||
return links?["page"]?.collectionIdentifier
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case title = "display"
|
||||
case value
|
||||
case isDefault = "default"
|
||||
case links = "_links"
|
||||
}
|
||||
}
|
||||
|
||||
extension FilterOption: Equatable { }
|
||||
|
||||
public func == (lhs: FilterOption, rhs: FilterOption) -> Bool {
|
||||
return lhs.value == rhs.value && lhs.title == rhs.title
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// ProgramFilter.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 18/12/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct ProgramFilter {
|
||||
public var filter: Filter
|
||||
public var option: FilterOption
|
||||
|
||||
public init(filter: Filter, option: FilterOption) {
|
||||
self.filter = filter
|
||||
self.option = option
|
||||
}
|
||||
}
|
||||
|
||||
extension ProgramFilter: Equatable { }
|
||||
|
||||
public func == (lhs: ProgramFilter, rhs: ProgramFilter) -> Bool {
|
||||
return lhs.filter == rhs.filter && lhs.option == rhs.option
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// GenreItem.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 27/10/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct GenreItem: Codable {
|
||||
var id: String
|
||||
public private(set) var terms: [String]
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
//
|
||||
// NPOGenre.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 30/06/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum NPOGenre: Int {
|
||||
enum NPOSubGenre {
|
||||
case movie(Int) // Film
|
||||
case series(Int) // Series
|
||||
case sport(Int) // Sport
|
||||
case music(Int) // Muziek
|
||||
case amusement(Int) // Amusement
|
||||
case informative(Int) // Informatief / Sport informatie
|
||||
case documentary(Int) // Documentaire
|
||||
|
||||
case thriller(Int) // Spanning
|
||||
case animation(Int) // Animatie
|
||||
case drama(Int) // Drama
|
||||
case humor(Int) // Komisch
|
||||
|
||||
case soap(Int) // Soap serie
|
||||
case detective(Int) // Detectives
|
||||
|
||||
case game(Int) // Spel / Quiz / (Sport) wedstrijd
|
||||
|
||||
case classical(Int) // Muziek - Klassiek
|
||||
case popular(Int) // Muziek - Populair
|
||||
|
||||
case standUp(Int) // Cabaret
|
||||
|
||||
case news(Int) // Nieuws / actualiteiten
|
||||
case health(Int) // Gezondheid / opvoeding
|
||||
case food(Int) // Koken / eten
|
||||
case art(Int) // Kunst / cultuur
|
||||
case nature(Int) // Natuur
|
||||
case religious(Int) // Religieus
|
||||
case science(Int) // Wetenschap
|
||||
case consumerInfo(Int) // Consumenten informatie
|
||||
case travel(Int) // Reizen
|
||||
case history(Int) // Geschiedenis
|
||||
case home(Int) // Wonen / tuin
|
||||
}
|
||||
|
||||
case youth = 1 // Jeugd
|
||||
case film = 12 // Film
|
||||
case series = 17 // Serie
|
||||
case sport = 23 // Sport
|
||||
case music = 26 // Muziek
|
||||
case amusement = 29 // Amusement
|
||||
case informative = 33 // Informatief
|
||||
case documentary = 46 // Documentaire
|
||||
|
||||
func all() -> [NPOGenre] {
|
||||
return [.youth, .film, .series, .sport, .music, .amusement, .informative, .documentary]
|
||||
}
|
||||
|
||||
func subgenres() -> [NPOSubGenre] {
|
||||
switch self {
|
||||
case .youth:
|
||||
return [.movie(12), .series(17), .sport(23), .music(26), .amusement(29), .informative(33), .documentary(46)]
|
||||
case .film:
|
||||
return [.thriller(14), .animation(15), .drama(16), .humor(13)]
|
||||
case .series:
|
||||
return [.thriller(19), .animation(20), .drama(21), .soap(22), .humor(18), .detective(55)]
|
||||
case .sport:
|
||||
return [.informative(24), .game(25)]
|
||||
case .music:
|
||||
return [.classical(27), .popular(28)]
|
||||
case .amusement:
|
||||
return [.game(30), .standUp(31), .humor(32)]
|
||||
case .informative:
|
||||
return [.game(34), .news(35), .health(36), .food(37), .art(38), .nature(39), .religious(40), .science(41),
|
||||
.consumerInfo(42), .travel(43), .history(44), .home(45)]
|
||||
case .documentary:
|
||||
return [.health(47), .food(48), .art(49), .nature(50), .religious(51), .science(52), .travel(53), .history(54)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//enum NPOSubgenre: Int {
|
||||
// case humor
|
||||
// case thriller
|
||||
// case animation
|
||||
// case drama
|
||||
// case soap
|
||||
// case detective = 38
|
||||
//}
|
||||
//
|
||||
//enum NPOGenre: Int {
|
||||
// // Jeugd
|
||||
// case youth = 1
|
||||
// case animation = 4
|
||||
// case gameQuiz = 10
|
||||
// case movie = 2
|
||||
// case nature = 11
|
||||
// case series = 3
|
||||
// case sport = 5
|
||||
// case music = 6
|
||||
// case amusement = 7
|
||||
// case informative = 8
|
||||
// case documentary = 9
|
||||
//
|
||||
// // Film
|
||||
// case film = 12
|
||||
// case humor = 13
|
||||
// case thriller = 14
|
||||
// case animation = 15
|
||||
// case drama = 16
|
||||
//
|
||||
// // Serie
|
||||
// case series = 17
|
||||
// case humor = 18
|
||||
// case thriller = 19
|
||||
// case animation = 20
|
||||
// case drama = 21
|
||||
// case soap = 22
|
||||
// case detective = 55
|
||||
//
|
||||
// // Sport
|
||||
// case sport = 23
|
||||
// case information = 24
|
||||
// case match = 25
|
||||
//
|
||||
// // Music
|
||||
// case music = 26
|
||||
// case classic = 27
|
||||
// case popular = 28
|
||||
//
|
||||
// // Amusement
|
||||
// case amusement = 29
|
||||
// case game = 30 // Spel/Quiz
|
||||
// case standup = 31 // Cabaret
|
||||
// case humor = 32 // Komisch
|
||||
//
|
||||
// // Informatief
|
||||
// case informative = 33
|
||||
// case game = 34 // Spel/Quiz
|
||||
// case news = 35 // Nieuws/Actualiteiten
|
||||
// case health = 36 // Gezondheid/opvoeding
|
||||
// case food = 37 // Koken/eten
|
||||
// case art = 38 // Kunst/cultuur
|
||||
// case nature = 39 // Natuur
|
||||
// case religious = 40 // Religieus
|
||||
// case science = 41 // Wetenschap
|
||||
// case consumerInformation = 42 // Consumenten informatie
|
||||
// case travel = 43 // Reizen
|
||||
// case history = 44 // Geschiedenis
|
||||
// case home = 45 // Wonen/Tuin
|
||||
//
|
||||
// // Documentaire
|
||||
// case documentary = 46
|
||||
// case health = 47 // Gezondheid/opvoeding
|
||||
// case food = 48 // Koken/eten
|
||||
// case art = 49 // Kunst/cultuur
|
||||
// case nature = 50 // Natuur
|
||||
// case religious = 51 // Religieus
|
||||
// case science = 52 // Wetenschap
|
||||
// case travel = 53 // Reizen
|
||||
// case history = 54 // Geschiedenis
|
||||
//}
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// Image.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 27/10/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Format: Codable {
|
||||
var width: Int?
|
||||
var height: Int?
|
||||
var source: URL?
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// FormatContainer.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 27/10/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FormatContainer: Codable {
|
||||
var original: Format?
|
||||
var tv: Format?
|
||||
var expanded: Format?
|
||||
var maf: Format?
|
||||
var phone: Format?
|
||||
var tablet: Format?
|
||||
var web: Format?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case original
|
||||
case tv
|
||||
case expanded = "tv-expanded"
|
||||
case maf
|
||||
case phone
|
||||
case tablet
|
||||
case web
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// ImageContainer.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 27/10/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ImageContainer: Codable {
|
||||
var original: NPOImage?
|
||||
var header: NPOImage?
|
||||
var gridTile: NPOImage?
|
||||
var laneTile: NPOImage?
|
||||
var playerPoster: NPOImage?
|
||||
var playerRecommendation: NPOImage?
|
||||
var playerPostPlay: NPOImage?
|
||||
var searchSuggestion: NPOImage?
|
||||
var chromecastPostPlay: NPOImage?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case original
|
||||
case header
|
||||
case gridTile = "grid.tile"
|
||||
case laneTile = "lane.tile"
|
||||
case playerPoster = "player.poster"
|
||||
case playerRecommendation = "player.recommedation"
|
||||
case playerPostPlay = "player.post-play"
|
||||
case searchSuggestion = "search.suggestion"
|
||||
case chromecastPostPlay = "chromecast.post-play"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// NPOImage.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 27/10/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct NPOImage: Codable {
|
||||
var title: String
|
||||
var formats: FormatContainer
|
||||
|
||||
// We don't really care about these
|
||||
var alt: String?
|
||||
var source: String?
|
||||
var created: Date?
|
||||
var updated: Date?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case title
|
||||
case formats
|
||||
case alt
|
||||
case source
|
||||
case created = "createdAt"
|
||||
case updated = "updatedAt"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
//
|
||||
// Item.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 27/10/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Typealiases
|
||||
public typealias Program = Item
|
||||
public typealias Episode = Item
|
||||
|
||||
public enum ItemType: String, Codable {
|
||||
case series
|
||||
case broadcast
|
||||
case fragment
|
||||
}
|
||||
|
||||
public struct Item: Pageable {
|
||||
var id: String
|
||||
public private(set) var title: String
|
||||
public private(set) var description: String?
|
||||
private var typeName: String
|
||||
private var channelName: String?
|
||||
public private(set) var genres: [GenreItem]
|
||||
public private(set) var broadcasters: [String]
|
||||
var images: ImageContainer
|
||||
public private(set) var isOnlyOnNPOPlus: Bool
|
||||
|
||||
// franchise
|
||||
var franchiseTitle: String?
|
||||
public private(set) var broadcastDate: Date?
|
||||
var seasonNumber: Int?
|
||||
var episodeNumber: Int?
|
||||
public private(set) var episodeTitle: String?
|
||||
var duration: TimeInterval?
|
||||
|
||||
// We don't really care about these:
|
||||
// var sterTitle: String
|
||||
// var links: ...
|
||||
// var shareText: String?
|
||||
// var shareURL: URL?
|
||||
// var website: URL?
|
||||
// var twitter: URL?
|
||||
// var facebook: URL?
|
||||
// var googlePlus: URL?
|
||||
// var pinterest: URL?
|
||||
// var instagram: URL?
|
||||
// var snapchat: URL?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id = "id"
|
||||
case title
|
||||
case description
|
||||
case typeName = "type"
|
||||
case channelName = "channel"
|
||||
case genres
|
||||
case broadcasters
|
||||
case images
|
||||
case isOnlyOnNPOPlus = "isOnlyOnNpoPlus"
|
||||
|
||||
// franchise
|
||||
case franchiseTitle
|
||||
case broadcastDate
|
||||
case seasonNumber
|
||||
case episodeNumber
|
||||
case episodeTitle
|
||||
case duration
|
||||
|
||||
// case sterTitle
|
||||
// case links = "_links"
|
||||
// case shareText
|
||||
// case shareURL = "shareUrl"
|
||||
// case website = "websiteUrl"
|
||||
// case twitter = "twitterUrl"
|
||||
// case facebook = "facebookUrl"
|
||||
// case googlePlus = "googlePlusUrl"
|
||||
// case pinterest = "pinterestUrl"
|
||||
// case instagram = "instagramUrl"
|
||||
// case snapchat = "snapchatUrl"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Calculated properties
|
||||
extension Item {
|
||||
|
||||
// MARK: Enums
|
||||
|
||||
var type: ItemType? {
|
||||
return ItemType(rawValue: typeName)
|
||||
}
|
||||
|
||||
public var channel: Channel? {
|
||||
guard let name = channelName, let channel = Channel(rawValue: name) else { return nil }
|
||||
return channel
|
||||
}
|
||||
|
||||
// MARK: Image URLs
|
||||
|
||||
var collectionImageURL: URL? {
|
||||
// try to return the source in order of preference
|
||||
if let source = images.gridTile?.formats.tablet?.source { // generally 320x180
|
||||
return source
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var headerImageURL: URL? {
|
||||
// try to return the source in order of preference
|
||||
if let source = images.header?.formats.tv?.source {
|
||||
return source
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// LegacyPlaylist.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 07/12/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct LegacyPlaylist: Codable {
|
||||
public private(set) var errorCode = 0
|
||||
public private(set) var wait = 0
|
||||
public private(set) var url: URL
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case errorCode = "errorcode"
|
||||
case wait
|
||||
case url
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// LegacyStream.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 07/12/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct LegacyStream: Codable {
|
||||
public private(set) var limited = false
|
||||
public private(set) var items = [[LegacyStreamItem]]()
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case limited
|
||||
case items
|
||||
}
|
||||
|
||||
internal func hlsItem() -> LegacyStreamItem? {
|
||||
for item in items {
|
||||
if let hlsItem = item.first(where: { $0.format == "hls" }) {
|
||||
return hlsItem
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// LegacyStreamItem.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 07/12/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct LegacyStreamItem: Codable {
|
||||
internal private(set) var label: String
|
||||
internal private(set) var contentType: String
|
||||
private var rawURL: URL
|
||||
internal private(set) var format: String
|
||||
|
||||
internal var url: URL? {
|
||||
var components = URLComponents(url: rawURL, resolvingAgainstBaseURL: false)
|
||||
|
||||
// remove JSONP query items
|
||||
let queryItems = components?.queryItems?.filter { !["type", "callback"].contains($0.name) }
|
||||
components?.queryItems = queryItems
|
||||
|
||||
return components?.url
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case label
|
||||
case contentType
|
||||
case rawURL = "url"
|
||||
case format
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// Token.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 07/12/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
internal struct Token: Codable {
|
||||
private static let lifetime = 3600 // token lifetime in seconds (1h)
|
||||
internal let date = Date()
|
||||
public private(set) var value: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case value = "token"
|
||||
}
|
||||
|
||||
internal var age: Double {
|
||||
return Date().timeIntervalSince(date)
|
||||
}
|
||||
|
||||
private var expiryDate: Date? {
|
||||
return Date(timeInterval: Double(Token.lifetime), since: date)
|
||||
}
|
||||
|
||||
internal var hasExpired: Bool {
|
||||
guard let expiryDate = self.expiryDate else {
|
||||
return true
|
||||
}
|
||||
|
||||
let now = Date()
|
||||
let comparison = expiryDate.compare(now)
|
||||
return (comparison == .orderedAscending || comparison == .orderedSame)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// Pageable.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 27/10/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol Pageable: Codable {
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// PagedItems.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 29/10/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct PagedItems: Codable {
|
||||
var title: String?
|
||||
var components: [Component]?
|
||||
//var links: Links
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case title
|
||||
case components
|
||||
//case links = "_links"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
//
|
||||
// Paginator.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 29/10/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class Paginator<T> where T: Pageable {
|
||||
public typealias FetchHandler = (_ paginator: Paginator<T>, _ page: Int, _ pageSize: Int) -> Void
|
||||
public typealias SuccessHandler = (Paginator, [T]) -> Void
|
||||
public typealias FailureHandler = (Paginator) -> Void
|
||||
|
||||
private enum PaginatorRequestStatus {
|
||||
case none, inProgress, done
|
||||
}
|
||||
private var requestStatus: PaginatorRequestStatus = .none
|
||||
|
||||
private var fetchHandler: FetchHandler
|
||||
private var successHandler: SuccessHandler
|
||||
private var failureHandler: FailureHandler?
|
||||
|
||||
public private(set) var page: Int = 0
|
||||
public private(set) var pageSize: Int = 20
|
||||
public private(set) var numberOfPages = 100000
|
||||
public private(set) var filters: [Filter]?
|
||||
|
||||
init<T>(ofType: T.Type,
|
||||
pageSize: Int,
|
||||
fetchHandler: @escaping FetchHandler,
|
||||
successHandler: @escaping SuccessHandler,
|
||||
failureHandler: FailureHandler? = nil) where T: Pageable {
|
||||
self.pageSize = pageSize
|
||||
self.fetchHandler = fetchHandler
|
||||
self.successHandler = successHandler
|
||||
self.failureHandler = failureHandler
|
||||
}
|
||||
|
||||
public func success(results: [T], numberOfPages: Int?, filters: [Filter]?) {
|
||||
requestStatus = .done
|
||||
self.page += 1
|
||||
if let numberOfPages = numberOfPages {
|
||||
self.numberOfPages = numberOfPages
|
||||
}
|
||||
if let filters = filters {
|
||||
self.filters = filters
|
||||
}
|
||||
successHandler(self, results)
|
||||
}
|
||||
|
||||
public func failure() {
|
||||
requestStatus = .done
|
||||
failureHandler?(self)
|
||||
}
|
||||
|
||||
public func next() {
|
||||
guard page < numberOfPages && requestStatus != .inProgress else { return }
|
||||
requestStatus = .inProgress
|
||||
fetchHandler(self, page + 1, pageSize)
|
||||
}
|
||||
|
||||
public func reset() {
|
||||
page = 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// Stream.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 31/10/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct Stream: Codable {
|
||||
var url: URL
|
||||
var licenseToken: String
|
||||
var licenseServer: URL
|
||||
var certificateURL: URL?
|
||||
var profile: String
|
||||
var drm: String
|
||||
var ip: String
|
||||
var isLegacy: Bool
|
||||
var isLive: Bool
|
||||
var isCatchupAvailable: Bool
|
||||
var heartbeatUrl: URL?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case url
|
||||
case licenseToken
|
||||
case licenseServer
|
||||
case certificateURL = "certificateUrl"
|
||||
case profile
|
||||
case drm
|
||||
case ip
|
||||
case isLegacy = "legacy"
|
||||
case isLive = "live"
|
||||
case isCatchupAvailable = "catchupAvailable"
|
||||
case heartbeatUrl
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// NPOKit+Episodes.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 31/10/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension NPOKit {
|
||||
|
||||
// MARK: Episodes
|
||||
|
||||
func getEpisodePaginator(for item: Item,
|
||||
successHandler: @escaping Paginator<Episode>.SuccessHandler,
|
||||
failureHandler: Paginator<Episode>.FailureHandler? = nil) -> Paginator<Episode> {
|
||||
// define how to fetch paginated data
|
||||
let fetchHandler: Paginator<Item>.FetchHandler = { [weak self] (paginator, page, pageSize) in
|
||||
self?.getEpisodes(
|
||||
forEndPoint: "media/series/\(item.id)/episodes?page=\(page)",
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
success: { (episodes, numberOfPages) in paginator.success(results: episodes, numberOfPages: numberOfPages, filters: nil) },
|
||||
failure: { paginator.failure() })
|
||||
}
|
||||
|
||||
// initialize the paginator
|
||||
return Paginator(ofType: Episode.self, pageSize: 20, fetchHandler: fetchHandler, successHandler: successHandler, failureHandler: failureHandler)
|
||||
}
|
||||
|
||||
// MARK: Generic fetcher
|
||||
|
||||
private func getEpisodes(forEndPoint endPoint: String,
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
success: @escaping (_ programs: [Episode], _ numberOfPages: Int) -> Void,
|
||||
failure: @escaping () -> Void) {
|
||||
// define data task completion handler
|
||||
let completionHandler: (Data?, URLResponse?, Error?) -> Void = { [weak self] (data, response, error) in
|
||||
guard error == nil, let httpResponse = response as? HTTPURLResponse, (httpResponse.statusCode == 200), let responseData = data else {
|
||||
self?.log?.error("Request failed... (\(String(describing: error?.localizedDescription))) \(String(describing: response))")
|
||||
failure()
|
||||
return
|
||||
}
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
if #available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
}
|
||||
|
||||
guard let componentData = try? decoder.decode(ComponentData.self, from: responseData) else {
|
||||
self?.log?.error("Could not decode JSON")
|
||||
failure()
|
||||
return
|
||||
}
|
||||
|
||||
// determine the number of pages
|
||||
let totalResults = componentData.total
|
||||
let numberOfPages = Int(ceil(Double(totalResults) / Double(pageSize)))
|
||||
|
||||
// success handler
|
||||
success(componentData.items, numberOfPages)
|
||||
}
|
||||
|
||||
// create task
|
||||
guard let task = dataTask(forEndpoint: endPoint, postData: nil, completionHandler: completionHandler) else {
|
||||
log?.error("Could not create data task...")
|
||||
failure()
|
||||
return
|
||||
}
|
||||
|
||||
// execute task
|
||||
task.resume()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
//
|
||||
// NPOKit+Generics.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 08/12/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension NPOKit {
|
||||
|
||||
// MARK: Fetch model
|
||||
|
||||
internal func fetchModel<T: Codable>(ofType type: T.Type, forEndpoint endPoint: String, postData: Data?, completionHandler: @escaping (T?, Error?) -> Void) {
|
||||
//swiftlint:disable:next force_unwrapping
|
||||
let url = URL(string: endPoint, relativeTo: apiURL)!
|
||||
fetchModel(ofType: type, forURL: url, postData: postData, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
internal func fetchModel<T: Codable>(ofType type: T.Type, forLegacyEndpoint endPoint: String, postData: Data?, completionHandler: @escaping (T?, Error?) -> Void) {
|
||||
//swiftlint:disable:next force_unwrapping
|
||||
let url = URL(string: endPoint, relativeTo: legacyAPIURL)!
|
||||
fetchModel(ofType: type, forURL: url, postData: postData, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
internal func fetchModel<T: Codable>(ofType type: T.Type, forURL url: URL, postData: Data?, completionHandler: @escaping (T?, Error?) -> Void) {
|
||||
let task = dataTask(forUrl: url, postData: postData, cachePolicy: .reloadIgnoringLocalCacheData) { (data, response, error) in
|
||||
let decoder = JSONDecoder()
|
||||
if #available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
}
|
||||
|
||||
guard error == nil, let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let data = data, let element = try? decoder.decode(type, from: data) else {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
completionHandler(element, nil)
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
// MARK: Data Task
|
||||
|
||||
internal func dataTask(forEndpoint endPoint: String, postData: Data?, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask? {
|
||||
//swiftlint:disable:next force_unwrapping
|
||||
let url = URL(string: endPoint, relativeTo: apiURL)!
|
||||
return dataTask(forUrl: url, postData: postData, cachePolicy: .returnCacheDataElseLoad, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
internal func legacyDataTask(forEndpoint endPoint: String, postData: Data?, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask? {
|
||||
//swiftlint:disable:next force_unwrapping
|
||||
let url = URL(string: endPoint, relativeTo: legacyAPIURL)!
|
||||
return dataTask(forUrl: url, postData: postData, cachePolicy: .reloadIgnoringLocalCacheData, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
internal func dataTask(forUrl url: URL, postData: Data?, cachePolicy: URLRequest.CachePolicy, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
|
||||
// create request
|
||||
var request = URLRequest(url: url, cachePolicy: cachePolicy, timeoutInterval: cacheInterval)
|
||||
request.addValue(getUserAgent(), forHTTPHeaderField: "User-Agent")
|
||||
request.addValue("application/json", forHTTPHeaderField: "Accept")
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
// add token if this is the 2.0 API
|
||||
if let apiString = apiURL?.absoluteString, url.absoluteString.starts(with: apiString), let key = apiKey.deobfuscated {
|
||||
request.addValue(key, forHTTPHeaderField: "ApiKey")
|
||||
request.addValue("tv", forHTTPHeaderField: "X-Npo-Platform")
|
||||
}
|
||||
|
||||
// add post data if needed
|
||||
if let data = postData {
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = data
|
||||
log?.verbose("POST: \(request)")
|
||||
} else {
|
||||
log?.verbose("GET : \(request)")
|
||||
}
|
||||
|
||||
// create task
|
||||
return URLSession.shared.dataTask(with: request, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
// MARK: User Agent
|
||||
|
||||
private func getUserAgent() -> String {
|
||||
// Pretend we're a Samsung TV
|
||||
return "Mozilla/5.0 (SMART-TV; Linux; Tizen 2.3) AppleWebkit/538.1 (KHTML, like Gecko) SamsungBrowser/1.0 TV Safari/538.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// NPOKit+Images.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 28/10/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension NPOKit {
|
||||
|
||||
// MARK: Private methods
|
||||
|
||||
private func fetchImage(url: URL, completionHandler: @escaping (UXImage?, URLSessionDataTask?, Error?) -> Void) -> URLSessionDataTask? {
|
||||
return fetchImage(url: url, cachePolicy: .returnCacheDataElseLoad, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
private func fetchImage(url: URL, cachePolicy: URLRequest.CachePolicy, completionHandler: @escaping (UXImage?, URLSessionDataTask?, Error?) -> Void) -> URLSessionDataTask? {
|
||||
guard let cacheInterval = TimeInterval(exactly: 600) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: cacheInterval)
|
||||
let session = URLSession.shared
|
||||
var task: URLSessionDataTask!
|
||||
task = session.dataTask(with: request) { (data, _, error) in
|
||||
guard let data = data, let image = UXImage(data: data) else {
|
||||
DispatchQueue.main.async {
|
||||
completionHandler(nil, task, error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completionHandler(image, task, error)
|
||||
}
|
||||
}
|
||||
|
||||
// execute task
|
||||
task.resume()
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
// MARK: Public API
|
||||
|
||||
func fetchCollectionImage(for item: Item, completionHandler completed: @escaping (UXImage?, URLSessionDataTask?, Error?) -> Void) -> URLSessionDataTask? {
|
||||
guard let url = item.collectionImageURL else { return nil }
|
||||
return fetchImage(url: url, completionHandler: completed)
|
||||
}
|
||||
|
||||
func fetchHeaderImage(for item: Item, completionHandler: @escaping (UXImage?, URLSessionDataTask?, Error?) -> Void) -> URLSessionDataTask? {
|
||||
guard let url = item.headerImageURL else { return nil }
|
||||
return fetchImage(url: url, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// NPOKit+LegacyStream.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 07/12/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension NPOKit {
|
||||
func legacyPlaylist(for item: Item, completionHandler: @escaping (LegacyPlaylist?, Error?) -> Void) {
|
||||
legacyStream(for: item) { [weak self] (stream, error) in
|
||||
guard let url = stream?.hlsItem()?.url else {
|
||||
completionHandler(nil, error ?? NSError(domain: "eu.osx.tvos.NPO.error", code: -2, userInfo: nil))
|
||||
return
|
||||
}
|
||||
|
||||
// fetch playlist
|
||||
self?.fetchModel(ofType: LegacyPlaylist.self, forURL: url, postData: nil, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
func legacyStream(for item: Item, completionHandler: @escaping (LegacyStream?, Error?) -> Void) {
|
||||
getToken { [weak self] (token, error) in
|
||||
guard let token = token else {
|
||||
completionHandler(nil, error ?? NSError(domain: "eu.osx.tvos.NPO.error", code: -1, userInfo: nil))
|
||||
return
|
||||
}
|
||||
|
||||
// fetch stream
|
||||
self?.fetchModel(ofType: LegacyStream.self, forLegacyEndpoint: "/app.php/\(item.id)?adaptive=yes&token=\(token.value)", postData: nil, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
private func getToken(completionHandler: @escaping (Token?, Error?) -> Void) {
|
||||
// use cached token?
|
||||
if let token = self.legacyToken, !token.hasExpired {
|
||||
completionHandler(token, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// fetch new token
|
||||
fetchModel(ofType: Token.self, forLegacyEndpoint: "/app.php/auth", postData: nil, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
//
|
||||
// NPOKit+Programs.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 28/06/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
internal enum Genre: String {
|
||||
case none = ""
|
||||
case documentary = "documentaire"
|
||||
case movie = "film"
|
||||
case youth = "jeugd"
|
||||
case series = "serie"
|
||||
case sport
|
||||
case music = "muziek"
|
||||
case amusement
|
||||
case informative = "informatief"
|
||||
|
||||
static func all() -> [Genre] {
|
||||
return [.documentary, .movie, .youth, .series, .sport, .music, .amusement, .informative]
|
||||
}
|
||||
}
|
||||
|
||||
public extension NPOKit {
|
||||
|
||||
// MARK: Program paginator
|
||||
|
||||
func getProgramPaginator(successHandler: @escaping Paginator<Program>.SuccessHandler,
|
||||
failureHandler: Paginator<Program>.FailureHandler? = nil) -> Paginator<Program> {
|
||||
return getProgramPaginator(using: nil, successHandler: successHandler, failureHandler: failureHandler)
|
||||
}
|
||||
|
||||
func getProgramPaginator(using programFilters: [ProgramFilter]?,
|
||||
successHandler: @escaping Paginator<Program>.SuccessHandler,
|
||||
failureHandler: Paginator<Program>.FailureHandler? = nil) -> Paginator<Program> {
|
||||
// define how to fetch paginated data
|
||||
let fetchHandler: Paginator<Program>.FetchHandler = { [weak self] (paginator, page, pageSize) in
|
||||
let endpoint = self?.getEndpoint(for: "page/catalogue", page: page, using: programFilters) ?? "page/catalogue?page=\(page)"
|
||||
|
||||
self?.getPrograms(
|
||||
forEndpoint: endpoint,
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
success: { (programs, numberOfPages, filters) in paginator.success(results: programs, numberOfPages: numberOfPages, filters: filters) },
|
||||
failure: { paginator.failure() })
|
||||
}
|
||||
|
||||
// initialize the paginator
|
||||
return Paginator(ofType: Program.self, pageSize: 20, fetchHandler: fetchHandler, successHandler: successHandler, failureHandler: failureHandler)
|
||||
}
|
||||
|
||||
private func getEndpoint(for base: String, page: Int, using programFilters: [ProgramFilter]?) -> String {
|
||||
var endpoint = "\(base)?page=\(page)"
|
||||
|
||||
guard let programFilters = programFilters else { return endpoint }
|
||||
|
||||
for programFilter in programFilters {
|
||||
guard let value = programFilter.option.value else { continue }
|
||||
endpoint += "&\(programFilter.filter.argumentName)=\(value)"
|
||||
|
||||
// when the current filter is a date filter and we're trying
|
||||
// to filter for a particular day we should also send dateTo
|
||||
if programFilter.filter.argumentName == "dateFrom" && !programFilter.option.title.contains("Afgelopen") {
|
||||
endpoint += "&dateTo=\(value)"
|
||||
}
|
||||
}
|
||||
|
||||
return endpoint
|
||||
}
|
||||
|
||||
private func getPrograms(forEndpoint endpoint: String,
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
success: @escaping (_ programs: [Program], _ numberOfPages: Int, _ filters: [Filter]?) -> Void,
|
||||
failure: @escaping () -> Void) {
|
||||
// define data task completion handler
|
||||
let completionHandler: (Data?, URLResponse?, Error?) -> Void = { [weak self] (data, response, error) in
|
||||
guard error == nil, let httpResponse = response as? HTTPURLResponse, (httpResponse.statusCode == 200), let data = data else {
|
||||
self?.log?.error("Request failed... (\(String(describing: error?.localizedDescription))) \(String(describing: response))")
|
||||
failure()
|
||||
return
|
||||
}
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
if #available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
}
|
||||
|
||||
// guard let pagedItems = try? decoder.decode(PagedItems.self, from: data) else {
|
||||
// DDLogError("Could not decode JSON")
|
||||
// failure()
|
||||
// return
|
||||
// }
|
||||
|
||||
let pagedItems: PagedItems
|
||||
do {
|
||||
pagedItems = try decoder.decode(PagedItems.self, from: data)
|
||||
} catch {
|
||||
// see https://developer.apple.com/documentation/swift/decodingerror
|
||||
self?.log?.error("Could not decode JSON (\(error)))")
|
||||
failure()
|
||||
return
|
||||
}
|
||||
|
||||
// get the program data
|
||||
guard let itemData = pagedItems.components?.filter({ $0.type == .grid }).first?.data else {
|
||||
self?.log?.error("Could not get program data, end of pagination?")
|
||||
failure()
|
||||
return
|
||||
}
|
||||
|
||||
// determine the number of pages
|
||||
let totalResults = itemData.total
|
||||
let numberOfPages = Int(ceil(Double(totalResults) / Double(pageSize)))
|
||||
|
||||
// get the filters
|
||||
let filters = pagedItems.components?.filter({ $0.type == .filter }).first?.filters
|
||||
|
||||
// success handler
|
||||
success(itemData.items, numberOfPages, filters)
|
||||
}
|
||||
|
||||
// create task
|
||||
guard let task = dataTask(forEndpoint: endpoint, postData: nil, completionHandler: completionHandler) else {
|
||||
log?.error("Could not create data task...")
|
||||
failure()
|
||||
return
|
||||
}
|
||||
|
||||
// execute task
|
||||
task.resume()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// NPOKit+Stream.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 31/10/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension NPOKit {
|
||||
|
||||
func stream(for item: Item, completionHandler: @escaping (Stream?, Error?) -> Void) {
|
||||
// tv payload
|
||||
let payload = "{\"profile\":\"hls\",\"viewer\":1061049068313,\"options\":{\"startOver\":false}}"
|
||||
let jsonPayloadData = payload.data(using: .utf8)
|
||||
fetchModel(ofType: Stream.self, forEndpoint: "media/\(item.id)/stream", postData: jsonPayloadData, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// NPOKit.swift
|
||||
// NPO
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 23/06/2017.
|
||||
// Copyright © 2017 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class NPOKit {
|
||||
open static let shared = NPOKit()
|
||||
internal var apiKey = "50405d0c015250575e03090047565957510b575053435a53100e55095f160f12"
|
||||
internal var apiURL = URL(string: "https://start-api.npo.nl")
|
||||
internal var legacyAPIURL = URL(string: "https://ida.omroep.nl")
|
||||
//swiftlint:disable:next force_unwrapping
|
||||
internal let cacheInterval = TimeInterval(exactly: 600)!
|
||||
public var log: NPOKitLogger?
|
||||
|
||||
// legacy token
|
||||
internal var legacyToken: Token?
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
private init() {
|
||||
log?.info("NPOKit initialized.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// NPOKitLogger.swift
|
||||
// NPOKit
|
||||
//
|
||||
// Created by Jeroen Wesbeek on 01/02/2018.
|
||||
// Copyright © 2018 Jeroen Wesbeek. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Logging Wrapper
|
||||
open class NPOKitLogger {
|
||||
public init() {
|
||||
verbose("Initialized logger.")
|
||||
}
|
||||
|
||||
open func verbose(_ closure: @autoclosure () -> Any?, functionName: StaticString = #function, fileName: StaticString = #file, lineNumber: Int = #line, userInfo: [String: Any] = [:]) {
|
||||
// by default no logging
|
||||
}
|
||||
|
||||
open func debug(_ closure: @autoclosure () -> Any?, functionName: StaticString = #function, fileName: StaticString = #file, lineNumber: Int = #line, userInfo: [String: Any] = [:]) {
|
||||
// by default no logging
|
||||
}
|
||||
|
||||
open func info(_ closure: @autoclosure () -> Any?, functionName: StaticString = #function, fileName: StaticString = #file, lineNumber: Int = #line, userInfo: [String: Any] = [:]) {
|
||||
// by default no logging
|
||||
}
|
||||
|
||||
open func warning(_ closure: @autoclosure () -> Any?, functionName: StaticString = #function, fileName: StaticString = #file, lineNumber: Int = #line, userInfo: [String: Any] = [:]) {
|
||||
// by default no logging
|
||||
}
|
||||
|
||||
open func error(_ closure: @autoclosure () -> Any?, functionName: StaticString = #function, fileName: StaticString = #file, lineNumber: Int = #line, userInfo: [String: Any] = [:]) {
|
||||
// by default no logging
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import XCTest
|
||||
@testable import NPOKitTests
|
||||
|
||||
XCTMain([
|
||||
testCase(NPOKitTests.allTests),
|
||||
])
|
|
@ -0,0 +1,16 @@
|
|||
import XCTest
|
||||
@testable import NPOKit
|
||||
|
||||
class NPOKitTests: XCTestCase {
|
||||
func testExample() {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct
|
||||
// results.
|
||||
XCTAssertEqual(NPOKit().text, "Hello, World!")
|
||||
}
|
||||
|
||||
|
||||
static var allTests = [
|
||||
("testExample", testExample),
|
||||
]
|
||||
}
|