├── .github └── workflows │ └── swift.yml ├── .gitignore ├── LICENSE ├── Package.swift ├── PodcastAPI.podspec ├── README.md ├── Sources ├── ExampleCommandLineApp │ └── main.swift ├── ExampleIOSApp │ ├── ExampleIOSApp.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── ExampleIOSApp.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── ExampleIOSApp │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── SceneDelegate.swift │ │ └── ViewController.swift │ ├── Podfile │ └── Podfile.lock └── PodcastAPI │ ├── ApiResponse.swift │ ├── PodcastAPI.swift │ ├── PodcastApiError.swift │ └── SwiftyJSON.swift └── Tests └── PodcastAPITests └── PodcastAPITests.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Build 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | 92 | .swiftpm/ 93 | 94 | Pods/ 95 | .DS_Store 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Listen Notes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "PodcastAPI", 8 | products: [ 9 | // Products define the executables and libraries a package produces, and make them visible to other packages. 10 | .library( 11 | name: "PodcastAPI", 12 | targets: ["PodcastAPI"]), 13 | .executable( 14 | name: "ExampleCommandLineApp", 15 | targets: ["ExampleCommandLineApp"]), 16 | ], 17 | dependencies: [ 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 22 | .target( 23 | name: "PodcastAPI", 24 | dependencies: []), 25 | .testTarget( 26 | name: "PodcastAPITests", 27 | dependencies: ["PodcastAPI"]), 28 | .target( 29 | name: "ExampleCommandLineApp", 30 | dependencies: ["PodcastAPI"]), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /PodcastAPI.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "PodcastAPI" 3 | s.version = "1.1.8" 4 | s.summary = "The Official Swift Library for the Listen Notes Podcast API." 5 | s.homepage = "https://www.listennotes.com/api/" 6 | s.license = { :type => "MIT" } 7 | s.author = { "Listen Notes, Inc." => "hello@listennotes.com" } 8 | s.social_media_url = "https://twitter.com/ListenNotes" 9 | 10 | s.requires_arc = true 11 | s.swift_version = "5.0" 12 | s.osx.deployment_target = "13.3.99" 13 | s.ios.deployment_target = "16.4" 14 | # s.watchos.deployment_target = "3.0" 15 | # s.tvos.deployment_target = "9.0" 16 | s.source = { :git => "https://github.com/ListenNotes/podcast-api-swift.git", :tag => "#{s.version}" } 17 | s.source_files = "Sources/PodcastAPI/*.swift" 18 | end 19 | -------------------------------------------------------------------------------- /Sources/ExampleCommandLineApp/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // 4 | // Sample code to use PodcastAPI 5 | // 6 | // Created by Wenbin Fang on 5/12/21. 7 | // 8 | 9 | import Foundation 10 | import PodcastAPI 11 | 12 | let apiKey = ProcessInfo.processInfo.environment["LISTEN_API_KEY", default: ""] 13 | 14 | // For command line executables, we have to use synchronous requests, otherwise 15 | // the program would exit before requests return any response 16 | let client = PodcastAPI.Client(apiKey: apiKey, synchronousRequest: true) 17 | 18 | // By default, we do asynchronous requests. 19 | //let client = PodcastAPI.Client(apiKey: apiKey) 20 | 21 | // All parameters are passed via this Dictionary[String: String] 22 | // For all parameters, please refer to https://www.listennotes.com/api/docs/ 23 | var parameters: [String: String] = [:] 24 | 25 | parameters["q"] = "startup" 26 | parameters["sort_by_date"] = "1" 27 | client.search(parameters: parameters) { response in 28 | if let error = response.error { 29 | switch (error) { 30 | case PodcastApiError.apiConnectionError: 31 | print("Can't connect to Listen API server") 32 | case PodcastApiError.authenticationError: 33 | print("wrong api key") 34 | default: 35 | print("unknown error") 36 | } 37 | } else { 38 | // It's a SwiftyJSON object 39 | if let json = response.toJson() { 40 | print(json) 41 | } 42 | 43 | // Some account stats 44 | print("Your free quota this month: \(response.getFreeQuota()) requests") 45 | print("Your usage this month: \(response.getUsage()) requests") 46 | print("Your next billing date: \(response.getNextBillingDate())") 47 | } 48 | } 49 | 50 | //parameters["ids"] = "3302bc71139541baa46ecb27dbf6071a,68faf62be97149c280ebcc25178aa731,37589a3e121e40debe4cef3d9638932a,9cf19c590ff0484d97b18b329fed0c6a" 51 | //parameters["rsses"] = "https://rss.art19.com/recode-decode,https://rss.art19.com/the-daily,https://www.npr.org/rss/podcast.php?id=510331,https://www.npr.org/rss/podcast.php?id=510331" 52 | //client.batchFetchPodcasts(parameters: parameters) { response in 53 | // if let error = response.error { 54 | // switch (error) { 55 | // case PodcastApiError.apiConnectionError: 56 | // print("Can't connect to Listen API server") 57 | // case PodcastApiError.authenticationError: 58 | // print("wrong api key") 59 | // default: 60 | // print("unknown error") 61 | // } 62 | // } else { 63 | // if let json = response.toJson() { 64 | // print(json["podcasts"].count) 65 | // } 66 | // } 67 | //} 68 | 69 | //parameters["id"] = "4d3fe717742d4963a85562e9f84d8c79" 70 | //parameters["reason"] = "the podcaster wants to delete it" 71 | //client.deletePodcast(parameters: parameters) { response in 72 | // if let error = response.error { 73 | // switch (error) { 74 | // case PodcastApiError.apiConnectionError: 75 | // print("Can't connect to Listen API server") 76 | // case PodcastApiError.authenticationError: 77 | // print("wrong api key") 78 | // case PodcastApiError.notFoundError: 79 | // print("not found") 80 | // case PodcastApiError.invalidRequestError: 81 | // print("invalid request") 82 | // default: 83 | // print("unknown error") 84 | // } 85 | // } else { 86 | // if let json = response.toJson() { 87 | // print(json) 88 | // } 89 | // } 90 | //} 91 | 92 | // parameters["q"] = "evergrand stok" 93 | // client.spellcheck(parameters: parameters) { response in 94 | // if let error = response.error { 95 | // switch (error) { 96 | // case PodcastApiError.apiConnectionError: 97 | // print("Can't connect to Listen API server") 98 | // case PodcastApiError.authenticationError: 99 | // print("wrong api key") 100 | // default: 101 | // print("unknown error") 102 | // } 103 | // } else { 104 | // // It's a SwiftyJSON object 105 | // if let json = response.toJson() { 106 | // print(json) 107 | // } 108 | 109 | // // Some account stats 110 | // print(response.getFreeQuota()) 111 | // print(response.getUsage()) 112 | // print(response.getNextBillingDate()) 113 | // } 114 | // } 115 | 116 | 117 | // parameters["q"] = "evergrande" 118 | // client.fetchRelatedSearches(parameters: parameters) { response in 119 | // if let error = response.error { 120 | // switch (error) { 121 | // case PodcastApiError.apiConnectionError: 122 | // print("Can't connect to Listen API server") 123 | // case PodcastApiError.authenticationError: 124 | // print("wrong api key") 125 | // default: 126 | // print("unknown error") 127 | // } 128 | // } else { 129 | // // It's a SwiftyJSON object 130 | // if let json = response.toJson() { 131 | // print(json) 132 | // } 133 | 134 | // // Some account stats 135 | // print(response.getFreeQuota()) 136 | // print(response.getUsage()) 137 | // print(response.getNextBillingDate()) 138 | // } 139 | // } 140 | 141 | // client.fetchTrendingSearches(parameters: parameters) { response in 142 | // if let error = response.error { 143 | // switch (error) { 144 | // case PodcastApiError.apiConnectionError: 145 | // print("Can't connect to Listen API server") 146 | // case PodcastApiError.authenticationError: 147 | // print("wrong api key") 148 | // default: 149 | // print("unknown error") 150 | // } 151 | // } else { 152 | // // It's a SwiftyJSON object 153 | // if let json = response.toJson() { 154 | // print(json) 155 | // } 156 | 157 | // // Some account stats 158 | // print(response.getFreeQuota()) 159 | // print(response.getUsage()) 160 | // print(response.getNextBillingDate()) 161 | // } 162 | // } 163 | 164 | //parameters["q"] = "startup" 165 | //parameters["show_podcasts"] = "1" 166 | //client.typeahead(parameters: parameters) { response in 167 | // if let error = response.error { 168 | // switch (error) { 169 | // case PodcastApiError.apiConnectionError: 170 | // print("Can't connect to Listen API server") 171 | // case PodcastApiError.authenticationError: 172 | // print("wrong api key") 173 | // default: 174 | // print("unknown error") 175 | // } 176 | // } else { 177 | // // It's a SwiftyJSON object 178 | // if let json = response.toJson() { 179 | // print(json) 180 | // } 181 | // 182 | // // Some account stats 183 | // print(response.getFreeQuota()) 184 | // print(response.getUsage()) 185 | // print(response.getNextBillingDate()) 186 | // } 187 | //} 188 | 189 | //parameters["rss"] = "https://feeds.megaphone.fm/committed" 190 | //client.submitPodcast(parameters: parameters) { response in 191 | // if let error = response.error { 192 | // switch (error) { 193 | // case PodcastApiError.apiConnectionError: 194 | // print("Can't connect to Listen API server") 195 | // case PodcastApiError.authenticationError: 196 | // print("wrong api key") 197 | // default: 198 | // print("unknown error") 199 | // } 200 | // } else { 201 | // // It's a SwiftyJSON object 202 | // if let json = response.toJson() { 203 | // print(json) 204 | // } 205 | // 206 | // // Some account stats 207 | // print(response.getFreeQuota()) 208 | // print(response.getUsage()) 209 | // print(response.getNextBillingDate()) 210 | // } 211 | //} 212 | 213 | //parameters["ids"] = "c577d55b2b2b483c969fae3ceb58e362,0f34a9099579490993eec9e8c8cebb82" 214 | //client.batchFetchEpisodes(parameters: parameters) { response in 215 | // if let error = response.error { 216 | // switch (error) { 217 | // case PodcastApiError.apiConnectionError: 218 | // print("Can't connect to Listen API server") 219 | // case PodcastApiError.authenticationError: 220 | // print("wrong api key") 221 | // default: 222 | // print("unknown error") 223 | // } 224 | // } else { 225 | // // It's a SwiftyJSON object 226 | // if let json = response.toJson() { 227 | // print(json) 228 | // } 229 | // 230 | // // Some account stats 231 | // print(response.getFreeQuota()) 232 | // print(response.getUsage()) 233 | // print(response.getNextBillingDate()) 234 | // } 235 | //} 236 | 237 | //client.fetchMyPlaylists(parameters: parameters) { response in 238 | // if let error = response.error { 239 | // switch (error) { 240 | // case PodcastApiError.apiConnectionError: 241 | // print("Can't connect to Listen API server") 242 | // case PodcastApiError.authenticationError: 243 | // print("wrong api key") 244 | // default: 245 | // print("unknown error") 246 | // } 247 | // } else { 248 | // // It's a SwiftyJSON object 249 | // if let json = response.toJson() { 250 | // print(json) 251 | // } 252 | // 253 | // // Some account stats 254 | // print(response.getFreeQuota()) 255 | // print(response.getUsage()) 256 | // print(response.getNextBillingDate()) 257 | // } 258 | //} 259 | 260 | //parameters["id"] = "m1pe7z60bsw" 261 | //parameters["type"] = "episode_list" 262 | //client.fetchPlaylistById(parameters: parameters) { response in 263 | // if let error = response.error { 264 | // switch (error) { 265 | // case PodcastApiError.apiConnectionError: 266 | // print("Can't connect to Listen API server") 267 | // case PodcastApiError.authenticationError: 268 | // print("wrong api key") 269 | // default: 270 | // print("unknown error") 271 | // } 272 | // } else { 273 | // // It's a SwiftyJSON object 274 | // if let json = response.toJson() { 275 | // print(json) 276 | // } 277 | // 278 | // // Some account stats 279 | // print(response.getFreeQuota()) 280 | // print(response.getUsage()) 281 | // print(response.getNextBillingDate()) 282 | // } 283 | //} 284 | 285 | //parameters["id"] = "254444fa6cf64a43a95292a70eb6869b" 286 | //parameters["safe_mode"] = "0" 287 | //client.fetchRecommendationsForEpisode(parameters: parameters) { response in 288 | // if let error = response.error { 289 | // switch (error) { 290 | // case PodcastApiError.apiConnectionError: 291 | // print("Can't connect to Listen API server") 292 | // case PodcastApiError.authenticationError: 293 | // print("wrong api key") 294 | // default: 295 | // print("unknown error") 296 | // } 297 | // } else { 298 | // // It's a SwiftyJSON object 299 | // if let json = response.toJson() { 300 | // print(json) 301 | // } 302 | // 303 | // // Some account stats 304 | // print(response.getFreeQuota()) 305 | // print(response.getUsage()) 306 | // print(response.getNextBillingDate()) 307 | // } 308 | //} 309 | 310 | //parameters["id"] = "25212ac3c53240a880dd5032e547047b" 311 | //parameters["safe_mode"] = "0" 312 | //client.fetchRecommendationsForPodcast(parameters: parameters) { response in 313 | // if let error = response.error { 314 | // switch (error) { 315 | // case PodcastApiError.apiConnectionError: 316 | // print("Can't connect to Listen API server") 317 | // case PodcastApiError.authenticationError: 318 | // print("wrong api key") 319 | // default: 320 | // print("unknown error") 321 | // } 322 | // } else { 323 | // // It's a SwiftyJSON object 324 | // if let json = response.toJson() { 325 | // print(json) 326 | // } 327 | // 328 | // // Some account stats 329 | // print(response.getFreeQuota()) 330 | // print(response.getUsage()) 331 | // print(response.getNextBillingDate()) 332 | // } 333 | //} 334 | 335 | 336 | //client.justListen(parameters: parameters) { response in 337 | // if let error = response.error { 338 | // switch (error) { 339 | // case PodcastApiError.apiConnectionError: 340 | // print("Can't connect to Listen API server") 341 | // case PodcastApiError.authenticationError: 342 | // print("wrong api key") 343 | // default: 344 | // print("unknown error") 345 | // } 346 | // } else { 347 | // // It's a SwiftyJSON object 348 | // if let json = response.toJson() { 349 | // print(json) 350 | // } 351 | // 352 | // // Some account stats 353 | // print(response.getFreeQuota()) 354 | // print(response.getUsage()) 355 | // print(response.getNextBillingDate()) 356 | // } 357 | //} 358 | 359 | //client.fetchPodcastLanguages(parameters: parameters) { response in 360 | // if let error = response.error { 361 | // switch (error) { 362 | // case PodcastApiError.apiConnectionError: 363 | // print("Can't connect to Listen API server") 364 | // case PodcastApiError.authenticationError: 365 | // print("wrong api key") 366 | // default: 367 | // print("unknown error") 368 | // } 369 | // } else { 370 | // // It's a SwiftyJSON object 371 | // if let json = response.toJson() { 372 | // print(json) 373 | // } 374 | // 375 | // // Some account stats 376 | // print(response.getFreeQuota()) 377 | // print(response.getUsage()) 378 | // print(response.getNextBillingDate()) 379 | // } 380 | //} 381 | 382 | //client.fetchPodcastRegions(parameters: parameters) { response in 383 | // if let error = response.error { 384 | // switch (error) { 385 | // case PodcastApiError.apiConnectionError: 386 | // print("Can't connect to Listen API server") 387 | // case PodcastApiError.authenticationError: 388 | // print("wrong api key") 389 | // default: 390 | // print("unknown error") 391 | // } 392 | // } else { 393 | // // It's a SwiftyJSON object 394 | // if let json = response.toJson() { 395 | // print(json) 396 | // } 397 | // 398 | // // Some account stats 399 | // print(response.getFreeQuota()) 400 | // print(response.getUsage()) 401 | // print(response.getNextBillingDate()) 402 | // } 403 | //} 404 | 405 | //client.fetchPodcastGenres(parameters: parameters) { response in 406 | // if let error = response.error { 407 | // switch (error) { 408 | // case PodcastApiError.apiConnectionError: 409 | // print("Can't connect to Listen API server") 410 | // case PodcastApiError.authenticationError: 411 | // print("wrong api key") 412 | // default: 413 | // print("unknown error") 414 | // } 415 | // } else { 416 | // // It's a SwiftyJSON object 417 | // if let json = response.toJson() { 418 | // print(json) 419 | // } 420 | // 421 | // // Some account stats 422 | // print(response.getFreeQuota()) 423 | // print(response.getUsage()) 424 | // print(response.getNextBillingDate()) 425 | // } 426 | //} 427 | 428 | //parameters["id"] = "SDFKduyJ47r" 429 | //client.fetchCuratedPodcastsListById(parameters: parameters) { response in 430 | // if let error = response.error { 431 | // switch (error) { 432 | // case PodcastApiError.apiConnectionError: 433 | // print("Can't connect to Listen API server") 434 | // case PodcastApiError.authenticationError: 435 | // print("wrong api key") 436 | // default: 437 | // print("unknown error") 438 | // } 439 | // } else { 440 | // // It's a SwiftyJSON object 441 | // if let json = response.toJson() { 442 | // print(json) 443 | // } 444 | // 445 | // // Some account stats 446 | // print(response.getFreeQuota()) 447 | // print(response.getUsage()) 448 | // print(response.getNextBillingDate()) 449 | // } 450 | //} 451 | 452 | //parameters["id"] = "6b6d65930c5a4f71b254465871fed370" 453 | //parameters["show_transcript"] = "1" 454 | //client.fetchEpisodeById(parameters: parameters) { response in 455 | // if let error = response.error { 456 | // switch (error) { 457 | // case PodcastApiError.apiConnectionError: 458 | // print("Can't connect to Listen API server") 459 | // case PodcastApiError.authenticationError: 460 | // print("wrong api key") 461 | // default: 462 | // print("unknown error") 463 | // } 464 | // } else { 465 | // // It's a SwiftyJSON object 466 | // if let json = response.toJson() { 467 | // print(json) 468 | // } 469 | // 470 | // // Some account stats 471 | // print(response.getFreeQuota()) 472 | // print(response.getUsage()) 473 | // print(response.getNextBillingDate()) 474 | // } 475 | //} 476 | 477 | //parameters["id"] = "4d3fe717742d4963a85562e9f84d8c79" 478 | //parameters["sort"] = "oldest_first" 479 | //client.fetchPodcastById(parameters: parameters) { response in 480 | // if let error = response.error { 481 | // switch (error) { 482 | // case PodcastApiError.apiConnectionError: 483 | // print("Can't connect to Listen API server") 484 | // case PodcastApiError.authenticationError: 485 | // print("wrong api key") 486 | // default: 487 | // print("unknown error") 488 | // } 489 | // } else { 490 | // // It's a SwiftyJSON object 491 | // if let json = response.toJson() { 492 | // print(json) 493 | // } 494 | // 495 | // // Some account stats 496 | // print(response.getFreeQuota()) 497 | // print(response.getUsage()) 498 | // print(response.getNextBillingDate()) 499 | // } 500 | //} 501 | 502 | //client.fetchCuratedPodcastsLists(parameters: parameters) { response in 503 | // if let error = response.error { 504 | // switch (error) { 505 | // case PodcastApiError.apiConnectionError: 506 | // print("Can't connect to Listen API server") 507 | // case PodcastApiError.authenticationError: 508 | // print("wrong api key") 509 | // default: 510 | // print("unknown error") 511 | // } 512 | // } else { 513 | // // It's a SwiftyJSON object 514 | // if let json = response.toJson() { 515 | // print(json) 516 | // } 517 | // 518 | // // Some account stats 519 | // print(response.getFreeQuota()) 520 | // print(response.getUsage()) 521 | // print(response.getNextBillingDate()) 522 | // } 523 | //} 524 | 525 | //parameters["genre_id"] = "93" 526 | //parameters["page"] = "2" 527 | //client.fetchBestPodcasts(parameters: parameters) { response in 528 | // if let error = response.error { 529 | // switch (error) { 530 | // case PodcastApiError.apiConnectionError: 531 | // print("Can't connect to Listen API server") 532 | // case PodcastApiError.authenticationError: 533 | // print("wrong api key") 534 | // default: 535 | // print("unknown error") 536 | // } 537 | // } else { 538 | // // It's a SwiftyJSON object 539 | // if let json = response.toJson() { 540 | // print(json) 541 | // } 542 | // 543 | // // Some account stats 544 | // print(response.getFreeQuota()) 545 | // print(response.getUsage()) 546 | // print(response.getNextBillingDate()) 547 | // } 548 | //} 549 | -------------------------------------------------------------------------------- /Sources/ExampleIOSApp/ExampleIOSApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 51; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 80210AAEDD4AEC3D86F722C1 /* Pods_ExampleIOSApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CFBBDA990FCA3219AB6B394F /* Pods_ExampleIOSApp.framework */; }; 11 | B8915059264F06ED00EE660E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8915058264F06ED00EE660E /* AppDelegate.swift */; }; 12 | B891505B264F06ED00EE660E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B891505A264F06ED00EE660E /* SceneDelegate.swift */; }; 13 | B891505D264F06ED00EE660E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B891505C264F06ED00EE660E /* ViewController.swift */; }; 14 | B8915060264F06ED00EE660E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B891505E264F06ED00EE660E /* Main.storyboard */; }; 15 | B8915062264F06EF00EE660E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B8915061264F06EF00EE660E /* Assets.xcassets */; }; 16 | B8915065264F06EF00EE660E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B8915063264F06EF00EE660E /* LaunchScreen.storyboard */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 438B8D7B093D5DD60DF2A1D8 /* Pods-ExampleIOSApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ExampleIOSApp.debug.xcconfig"; path = "Target Support Files/Pods-ExampleIOSApp/Pods-ExampleIOSApp.debug.xcconfig"; sourceTree = ""; }; 21 | B18204F2D6D45CC98913C0B0 /* Pods-ExampleIOSApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ExampleIOSApp.release.xcconfig"; path = "Target Support Files/Pods-ExampleIOSApp/Pods-ExampleIOSApp.release.xcconfig"; sourceTree = ""; }; 22 | B8915055264F06ED00EE660E /* ExampleIOSApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExampleIOSApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | B8915058264F06ED00EE660E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | B891505A264F06ED00EE660E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 25 | B891505C264F06ED00EE660E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 26 | B891505F264F06ED00EE660E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 27 | B8915061264F06EF00EE660E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 28 | B8915064264F06EF00EE660E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 29 | B8915066264F06EF00EE660E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 30 | CFBBDA990FCA3219AB6B394F /* Pods_ExampleIOSApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ExampleIOSApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 31 | /* End PBXFileReference section */ 32 | 33 | /* Begin PBXFrameworksBuildPhase section */ 34 | B8915052264F06ED00EE660E /* Frameworks */ = { 35 | isa = PBXFrameworksBuildPhase; 36 | buildActionMask = 2147483647; 37 | files = ( 38 | 80210AAEDD4AEC3D86F722C1 /* Pods_ExampleIOSApp.framework in Frameworks */, 39 | ); 40 | runOnlyForDeploymentPostprocessing = 0; 41 | }; 42 | /* End PBXFrameworksBuildPhase section */ 43 | 44 | /* Begin PBXGroup section */ 45 | B1137B1F95ABEBA01C12BB78 /* Frameworks */ = { 46 | isa = PBXGroup; 47 | children = ( 48 | CFBBDA990FCA3219AB6B394F /* Pods_ExampleIOSApp.framework */, 49 | ); 50 | name = Frameworks; 51 | sourceTree = ""; 52 | }; 53 | B891504C264F06ED00EE660E = { 54 | isa = PBXGroup; 55 | children = ( 56 | B8915057264F06ED00EE660E /* ExampleIOSApp */, 57 | B8915056264F06ED00EE660E /* Products */, 58 | D34B9D13649227E4FB052EF4 /* Pods */, 59 | B1137B1F95ABEBA01C12BB78 /* Frameworks */, 60 | ); 61 | sourceTree = ""; 62 | }; 63 | B8915056264F06ED00EE660E /* Products */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | B8915055264F06ED00EE660E /* ExampleIOSApp.app */, 67 | ); 68 | name = Products; 69 | sourceTree = ""; 70 | }; 71 | B8915057264F06ED00EE660E /* ExampleIOSApp */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | B8915058264F06ED00EE660E /* AppDelegate.swift */, 75 | B891505A264F06ED00EE660E /* SceneDelegate.swift */, 76 | B891505C264F06ED00EE660E /* ViewController.swift */, 77 | B891505E264F06ED00EE660E /* Main.storyboard */, 78 | B8915061264F06EF00EE660E /* Assets.xcassets */, 79 | B8915063264F06EF00EE660E /* LaunchScreen.storyboard */, 80 | B8915066264F06EF00EE660E /* Info.plist */, 81 | ); 82 | path = ExampleIOSApp; 83 | sourceTree = ""; 84 | }; 85 | D34B9D13649227E4FB052EF4 /* Pods */ = { 86 | isa = PBXGroup; 87 | children = ( 88 | 438B8D7B093D5DD60DF2A1D8 /* Pods-ExampleIOSApp.debug.xcconfig */, 89 | B18204F2D6D45CC98913C0B0 /* Pods-ExampleIOSApp.release.xcconfig */, 90 | ); 91 | path = Pods; 92 | sourceTree = ""; 93 | }; 94 | /* End PBXGroup section */ 95 | 96 | /* Begin PBXNativeTarget section */ 97 | B8915054264F06ED00EE660E /* ExampleIOSApp */ = { 98 | isa = PBXNativeTarget; 99 | buildConfigurationList = B8915069264F06EF00EE660E /* Build configuration list for PBXNativeTarget "ExampleIOSApp" */; 100 | buildPhases = ( 101 | C01471C9629336815CC7E86D /* [CP] Check Pods Manifest.lock */, 102 | B8915051264F06ED00EE660E /* Sources */, 103 | B8915052264F06ED00EE660E /* Frameworks */, 104 | B8915053264F06ED00EE660E /* Resources */, 105 | D6DEB5BD38E8EA2A5ED05A7D /* [CP] Embed Pods Frameworks */, 106 | ); 107 | buildRules = ( 108 | ); 109 | dependencies = ( 110 | ); 111 | name = ExampleIOSApp; 112 | productName = ExampleIOSApp; 113 | productReference = B8915055264F06ED00EE660E /* ExampleIOSApp.app */; 114 | productType = "com.apple.product-type.application"; 115 | }; 116 | /* End PBXNativeTarget section */ 117 | 118 | /* Begin PBXProject section */ 119 | B891504D264F06ED00EE660E /* Project object */ = { 120 | isa = PBXProject; 121 | attributes = { 122 | LastSwiftUpdateCheck = 1250; 123 | LastUpgradeCheck = 1250; 124 | TargetAttributes = { 125 | B8915054264F06ED00EE660E = { 126 | CreatedOnToolsVersion = 12.5; 127 | }; 128 | }; 129 | }; 130 | buildConfigurationList = B8915050264F06ED00EE660E /* Build configuration list for PBXProject "ExampleIOSApp" */; 131 | compatibilityVersion = "Xcode 9.3"; 132 | developmentRegion = en; 133 | hasScannedForEncodings = 0; 134 | knownRegions = ( 135 | en, 136 | Base, 137 | ); 138 | mainGroup = B891504C264F06ED00EE660E; 139 | productRefGroup = B8915056264F06ED00EE660E /* Products */; 140 | projectDirPath = ""; 141 | projectRoot = ""; 142 | targets = ( 143 | B8915054264F06ED00EE660E /* ExampleIOSApp */, 144 | ); 145 | }; 146 | /* End PBXProject section */ 147 | 148 | /* Begin PBXResourcesBuildPhase section */ 149 | B8915053264F06ED00EE660E /* Resources */ = { 150 | isa = PBXResourcesBuildPhase; 151 | buildActionMask = 2147483647; 152 | files = ( 153 | B8915065264F06EF00EE660E /* LaunchScreen.storyboard in Resources */, 154 | B8915062264F06EF00EE660E /* Assets.xcassets in Resources */, 155 | B8915060264F06ED00EE660E /* Main.storyboard in Resources */, 156 | ); 157 | runOnlyForDeploymentPostprocessing = 0; 158 | }; 159 | /* End PBXResourcesBuildPhase section */ 160 | 161 | /* Begin PBXShellScriptBuildPhase section */ 162 | C01471C9629336815CC7E86D /* [CP] Check Pods Manifest.lock */ = { 163 | isa = PBXShellScriptBuildPhase; 164 | buildActionMask = 2147483647; 165 | files = ( 166 | ); 167 | inputFileListPaths = ( 168 | ); 169 | inputPaths = ( 170 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 171 | "${PODS_ROOT}/Manifest.lock", 172 | ); 173 | name = "[CP] Check Pods Manifest.lock"; 174 | outputFileListPaths = ( 175 | ); 176 | outputPaths = ( 177 | "$(DERIVED_FILE_DIR)/Pods-ExampleIOSApp-checkManifestLockResult.txt", 178 | ); 179 | runOnlyForDeploymentPostprocessing = 0; 180 | shellPath = /bin/sh; 181 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 182 | showEnvVarsInLog = 0; 183 | }; 184 | D6DEB5BD38E8EA2A5ED05A7D /* [CP] Embed Pods Frameworks */ = { 185 | isa = PBXShellScriptBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | ); 189 | inputFileListPaths = ( 190 | "${PODS_ROOT}/Target Support Files/Pods-ExampleIOSApp/Pods-ExampleIOSApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", 191 | ); 192 | name = "[CP] Embed Pods Frameworks"; 193 | outputFileListPaths = ( 194 | "${PODS_ROOT}/Target Support Files/Pods-ExampleIOSApp/Pods-ExampleIOSApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | shellPath = /bin/sh; 198 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ExampleIOSApp/Pods-ExampleIOSApp-frameworks.sh\"\n"; 199 | showEnvVarsInLog = 0; 200 | }; 201 | /* End PBXShellScriptBuildPhase section */ 202 | 203 | /* Begin PBXSourcesBuildPhase section */ 204 | B8915051264F06ED00EE660E /* Sources */ = { 205 | isa = PBXSourcesBuildPhase; 206 | buildActionMask = 2147483647; 207 | files = ( 208 | B891505D264F06ED00EE660E /* ViewController.swift in Sources */, 209 | B8915059264F06ED00EE660E /* AppDelegate.swift in Sources */, 210 | B891505B264F06ED00EE660E /* SceneDelegate.swift in Sources */, 211 | ); 212 | runOnlyForDeploymentPostprocessing = 0; 213 | }; 214 | /* End PBXSourcesBuildPhase section */ 215 | 216 | /* Begin PBXVariantGroup section */ 217 | B891505E264F06ED00EE660E /* Main.storyboard */ = { 218 | isa = PBXVariantGroup; 219 | children = ( 220 | B891505F264F06ED00EE660E /* Base */, 221 | ); 222 | name = Main.storyboard; 223 | sourceTree = ""; 224 | }; 225 | B8915063264F06EF00EE660E /* LaunchScreen.storyboard */ = { 226 | isa = PBXVariantGroup; 227 | children = ( 228 | B8915064264F06EF00EE660E /* Base */, 229 | ); 230 | name = LaunchScreen.storyboard; 231 | sourceTree = ""; 232 | }; 233 | /* End PBXVariantGroup section */ 234 | 235 | /* Begin XCBuildConfiguration section */ 236 | B8915067264F06EF00EE660E /* Debug */ = { 237 | isa = XCBuildConfiguration; 238 | buildSettings = { 239 | ALWAYS_SEARCH_USER_PATHS = NO; 240 | CLANG_ANALYZER_NONNULL = YES; 241 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 242 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 243 | CLANG_CXX_LIBRARY = "libc++"; 244 | CLANG_ENABLE_MODULES = YES; 245 | CLANG_ENABLE_OBJC_ARC = YES; 246 | CLANG_ENABLE_OBJC_WEAK = YES; 247 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 248 | CLANG_WARN_BOOL_CONVERSION = YES; 249 | CLANG_WARN_COMMA = YES; 250 | CLANG_WARN_CONSTANT_CONVERSION = YES; 251 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 252 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 253 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 254 | CLANG_WARN_EMPTY_BODY = YES; 255 | CLANG_WARN_ENUM_CONVERSION = YES; 256 | CLANG_WARN_INFINITE_RECURSION = YES; 257 | CLANG_WARN_INT_CONVERSION = YES; 258 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 259 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 260 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 261 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 262 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 263 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 264 | CLANG_WARN_STRICT_PROTOTYPES = YES; 265 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 266 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 267 | CLANG_WARN_UNREACHABLE_CODE = YES; 268 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 269 | COPY_PHASE_STRIP = NO; 270 | DEBUG_INFORMATION_FORMAT = dwarf; 271 | ENABLE_STRICT_OBJC_MSGSEND = YES; 272 | ENABLE_TESTABILITY = YES; 273 | GCC_C_LANGUAGE_STANDARD = gnu11; 274 | GCC_DYNAMIC_NO_PIC = NO; 275 | GCC_NO_COMMON_BLOCKS = YES; 276 | GCC_OPTIMIZATION_LEVEL = 0; 277 | GCC_PREPROCESSOR_DEFINITIONS = ( 278 | "DEBUG=1", 279 | "$(inherited)", 280 | ); 281 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 282 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 283 | GCC_WARN_UNDECLARED_SELECTOR = YES; 284 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 285 | GCC_WARN_UNUSED_FUNCTION = YES; 286 | GCC_WARN_UNUSED_VARIABLE = YES; 287 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 288 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 289 | MTL_FAST_MATH = YES; 290 | ONLY_ACTIVE_ARCH = YES; 291 | SDKROOT = iphoneos; 292 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 293 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 294 | }; 295 | name = Debug; 296 | }; 297 | B8915068264F06EF00EE660E /* Release */ = { 298 | isa = XCBuildConfiguration; 299 | buildSettings = { 300 | ALWAYS_SEARCH_USER_PATHS = NO; 301 | CLANG_ANALYZER_NONNULL = YES; 302 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 303 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 304 | CLANG_CXX_LIBRARY = "libc++"; 305 | CLANG_ENABLE_MODULES = YES; 306 | CLANG_ENABLE_OBJC_ARC = YES; 307 | CLANG_ENABLE_OBJC_WEAK = YES; 308 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 309 | CLANG_WARN_BOOL_CONVERSION = YES; 310 | CLANG_WARN_COMMA = YES; 311 | CLANG_WARN_CONSTANT_CONVERSION = YES; 312 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 313 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 314 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 315 | CLANG_WARN_EMPTY_BODY = YES; 316 | CLANG_WARN_ENUM_CONVERSION = YES; 317 | CLANG_WARN_INFINITE_RECURSION = YES; 318 | CLANG_WARN_INT_CONVERSION = YES; 319 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 320 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 321 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 322 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 323 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 324 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 325 | CLANG_WARN_STRICT_PROTOTYPES = YES; 326 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 327 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 328 | CLANG_WARN_UNREACHABLE_CODE = YES; 329 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 330 | COPY_PHASE_STRIP = NO; 331 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 332 | ENABLE_NS_ASSERTIONS = NO; 333 | ENABLE_STRICT_OBJC_MSGSEND = YES; 334 | GCC_C_LANGUAGE_STANDARD = gnu11; 335 | GCC_NO_COMMON_BLOCKS = YES; 336 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 337 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 338 | GCC_WARN_UNDECLARED_SELECTOR = YES; 339 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 340 | GCC_WARN_UNUSED_FUNCTION = YES; 341 | GCC_WARN_UNUSED_VARIABLE = YES; 342 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 343 | MTL_ENABLE_DEBUG_INFO = NO; 344 | MTL_FAST_MATH = YES; 345 | SDKROOT = iphoneos; 346 | SWIFT_COMPILATION_MODE = wholemodule; 347 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 348 | VALIDATE_PRODUCT = YES; 349 | }; 350 | name = Release; 351 | }; 352 | B891506A264F06EF00EE660E /* Debug */ = { 353 | isa = XCBuildConfiguration; 354 | baseConfigurationReference = 438B8D7B093D5DD60DF2A1D8 /* Pods-ExampleIOSApp.debug.xcconfig */; 355 | buildSettings = { 356 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 357 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 358 | CODE_SIGN_STYLE = Automatic; 359 | INFOPLIST_FILE = ExampleIOSApp/Info.plist; 360 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 361 | LD_RUNPATH_SEARCH_PATHS = ( 362 | "$(inherited)", 363 | "@executable_path/Frameworks", 364 | ); 365 | PRODUCT_BUNDLE_IDENTIFIER = com.listennotes.PodcastAPI.demo.ios.ExampleIOSApp; 366 | PRODUCT_NAME = "$(TARGET_NAME)"; 367 | SWIFT_VERSION = 5.0; 368 | TARGETED_DEVICE_FAMILY = "1,2"; 369 | }; 370 | name = Debug; 371 | }; 372 | B891506B264F06EF00EE660E /* Release */ = { 373 | isa = XCBuildConfiguration; 374 | baseConfigurationReference = B18204F2D6D45CC98913C0B0 /* Pods-ExampleIOSApp.release.xcconfig */; 375 | buildSettings = { 376 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 377 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 378 | CODE_SIGN_STYLE = Automatic; 379 | INFOPLIST_FILE = ExampleIOSApp/Info.plist; 380 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 381 | LD_RUNPATH_SEARCH_PATHS = ( 382 | "$(inherited)", 383 | "@executable_path/Frameworks", 384 | ); 385 | PRODUCT_BUNDLE_IDENTIFIER = com.listennotes.PodcastAPI.demo.ios.ExampleIOSApp; 386 | PRODUCT_NAME = "$(TARGET_NAME)"; 387 | SWIFT_VERSION = 5.0; 388 | TARGETED_DEVICE_FAMILY = "1,2"; 389 | }; 390 | name = Release; 391 | }; 392 | /* End XCBuildConfiguration section */ 393 | 394 | /* Begin XCConfigurationList section */ 395 | B8915050264F06ED00EE660E /* Build configuration list for PBXProject "ExampleIOSApp" */ = { 396 | isa = XCConfigurationList; 397 | buildConfigurations = ( 398 | B8915067264F06EF00EE660E /* Debug */, 399 | B8915068264F06EF00EE660E /* Release */, 400 | ); 401 | defaultConfigurationIsVisible = 0; 402 | defaultConfigurationName = Release; 403 | }; 404 | B8915069264F06EF00EE660E /* Build configuration list for PBXNativeTarget "ExampleIOSApp" */ = { 405 | isa = XCConfigurationList; 406 | buildConfigurations = ( 407 | B891506A264F06EF00EE660E /* Debug */, 408 | B891506B264F06EF00EE660E /* Release */, 409 | ); 410 | defaultConfigurationIsVisible = 0; 411 | defaultConfigurationName = Release; 412 | }; 413 | /* End XCConfigurationList section */ 414 | }; 415 | rootObject = B891504D264F06ED00EE660E /* Project object */; 416 | } 417 | -------------------------------------------------------------------------------- /Sources/ExampleIOSApp/ExampleIOSApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/ExampleIOSApp/ExampleIOSApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/ExampleIOSApp/ExampleIOSApp.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Sources/ExampleIOSApp/ExampleIOSApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/ExampleIOSApp/ExampleIOSApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ExampleIOSApp 4 | // 5 | // Created by Wenbin Fang on 5/14/21. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Sources/ExampleIOSApp/ExampleIOSApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/ExampleIOSApp/ExampleIOSApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/ExampleIOSApp/ExampleIOSApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/ExampleIOSApp/ExampleIOSApp/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Sources/ExampleIOSApp/ExampleIOSApp/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Sources/ExampleIOSApp/ExampleIOSApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Sources/ExampleIOSApp/ExampleIOSApp/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // ExampleIOSApp 4 | // 5 | // Created by Wenbin Fang on 5/14/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Sources/ExampleIOSApp/ExampleIOSApp/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ExampleIOSApp 4 | // 5 | // Created by Wenbin Fang on 5/14/21. 6 | // 7 | 8 | import UIKit 9 | import PodcastAPI 10 | 11 | class ViewController: UIViewController { 12 | @IBOutlet weak var displayLabel: UILabel! 13 | 14 | override func viewDidLoad() { 15 | 16 | 17 | super.viewDidLoad() 18 | 19 | // You may want to fetch apiKey from your own server here 20 | // Don't hard code an api key in the source code, because you can't 21 | // change the apiKey later once the app is at the hands of your users. 22 | let apiKey = "" 23 | let client = PodcastAPI.Client(apiKey: apiKey) 24 | 25 | // All parameters are passed via this Dictionary[String: String] 26 | // For all parameters, please refer to https://www.listennotes.com/api/docs/ 27 | var parameters: [String: String] = [:] 28 | 29 | parameters["q"] = "startup" 30 | parameters["sort_by_date"] = "1" 31 | client.search(parameters: parameters) { response in 32 | if let error = response.error { 33 | switch (error) { 34 | case PodcastApiError.apiConnectionError: 35 | print("Can't connect to Listen API server") 36 | case PodcastApiError.authenticationError: 37 | print("wrong api key") 38 | default: 39 | print("unknown error") 40 | } 41 | } else { 42 | var text = "" 43 | // It's a SwiftyJSON object 44 | if let json = response.toJson() { 45 | text += "Total search results: \(json["total"])" 46 | } 47 | 48 | // Update UI in the main thread 49 | DispatchQueue.main.async { 50 | self.displayLabel.text = text 51 | } 52 | } 53 | } 54 | } 55 | 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /Sources/ExampleIOSApp/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :ios, '16.4' 3 | 4 | target 'ExampleIOSApp' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for ExampleIOSApp 9 | pod 'PodcastAPI', :path => '../..' 10 | end 11 | -------------------------------------------------------------------------------- /Sources/ExampleIOSApp/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - PodcastAPI (1.1.6) 3 | 4 | DEPENDENCIES: 5 | - PodcastAPI (from `../..`) 6 | 7 | EXTERNAL SOURCES: 8 | PodcastAPI: 9 | :path: "../.." 10 | 11 | SPEC CHECKSUMS: 12 | PodcastAPI: 02ea00bda553e914356f57674d0845c194b7e07a 13 | 14 | PODFILE CHECKSUM: a0f3ac29f2e50f06e13ba6997a45f525dbae37c7 15 | 16 | COCOAPODS: 1.11.3 17 | -------------------------------------------------------------------------------- /Sources/PodcastAPI/ApiResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | // import SwiftyJSON 3 | 4 | public class ApiResponse { 5 | var data: Data? 6 | var response: HTTPURLResponse? 7 | var request: URLRequest? 8 | var httpError: Error? 9 | public var error: PodcastApiError? 10 | 11 | public init(request: URLRequest?, data: Data?, response: URLResponse?, httpError: Error?, apiError: PodcastApiError? ) { 12 | self.data = data 13 | self.response = response as? HTTPURLResponse 14 | self.httpError = httpError 15 | self.error = apiError 16 | self.request = request 17 | 18 | self.checkAndSetApiError() 19 | } 20 | 21 | public func toJson() -> JSON? { 22 | if let data = data { 23 | do { 24 | let json = try JSON(data: data) 25 | return json 26 | } catch { 27 | return nil 28 | } 29 | } 30 | return nil 31 | } 32 | 33 | public func getFreeQuota() -> Int { 34 | if let response = response { 35 | if let quota = response.allHeaderFields["x-listenapi-freequota"] as? String { 36 | return Int(quota) ?? -1 37 | } 38 | } 39 | return -1 40 | } 41 | 42 | public func getUsage() -> Int { 43 | if let response = response { 44 | if let usage = response.allHeaderFields["x-listenapi-usage"] as? String { 45 | return Int(usage) ?? -1 46 | } 47 | } 48 | return -1 49 | } 50 | 51 | public func getNextBillingDate() -> String { 52 | if let response = response { 53 | if let dateString = response.allHeaderFields["x-listenapi-nextbillingdate"] as? String { 54 | return dateString 55 | } 56 | } 57 | return "" 58 | } 59 | 60 | private func checkAndSetApiError() { 61 | if let httpResponse = self.response { 62 | switch httpResponse.statusCode { 63 | case 200..<300: 64 | self.error = nil 65 | case 400: 66 | self.error = PodcastApiError.invalidRequestError 67 | case 401: 68 | self.error = PodcastApiError.authenticationError 69 | case 404: 70 | self.error = PodcastApiError.notFoundError 71 | case 429: 72 | self.error = PodcastApiError.tooManyRequestsError 73 | case 400..<500: 74 | self.error = PodcastApiError.invalidRequestError 75 | case 500..<600: 76 | self.error = PodcastApiError.serverError 77 | default: 78 | self.error = nil 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/PodcastAPI/PodcastAPI.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let BASE_URL_PROD = "https://listen-api.listennotes.com/api/v2" 4 | let BASE_URL_TEST = "https://listen-api-test.listennotes.com/api/v2" 5 | let DEFAULT_USER_AGENT = "podcast-api-swift" 6 | 7 | public class Client { 8 | private var apiKey: String 9 | private var baseUrl: String = BASE_URL_PROD 10 | private var userAgent: String = DEFAULT_USER_AGENT 11 | private var responseTimeoutSec: Int = 30 12 | private var synchronousRequest: Bool = false 13 | 14 | public convenience init(apiKey: String) { 15 | self.init(apiKey: apiKey, synchronousRequest: false) 16 | } 17 | 18 | public init(apiKey: String, synchronousRequest: Bool) { 19 | self.apiKey = apiKey 20 | 21 | if apiKey.trimmingCharacters(in: .whitespacesAndNewlines).count == 0 { 22 | self.baseUrl = BASE_URL_TEST 23 | } 24 | 25 | self.synchronousRequest = synchronousRequest 26 | } 27 | 28 | public func setUserAgent(userAgent: String) { 29 | self.userAgent = userAgent 30 | } 31 | 32 | public func setResponseTimeoutSec(timeoutSec: Int) { 33 | self.responseTimeoutSec = timeoutSec 34 | } 35 | 36 | public func search(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 37 | self.sendHttpRequest(path: "search", method: "GET", parameters: parameters, completion: completion) 38 | 39 | } 40 | 41 | public func searchEpisodeTitles(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 42 | self.sendHttpRequest(path: "search_episode_titles", method: "GET", parameters: parameters, completion: completion) 43 | } 44 | 45 | public func typeahead(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 46 | self.sendHttpRequest(path: "typeahead", method: "GET", parameters: parameters, completion: completion) 47 | } 48 | 49 | public func spellcheck(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 50 | self.sendHttpRequest(path: "spellcheck", method: "GET", parameters: parameters, completion: completion) 51 | } 52 | 53 | public func fetchRelatedSearches(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 54 | self.sendHttpRequest(path: "related_searches", method: "GET", parameters: parameters, completion: completion) 55 | } 56 | 57 | public func fetchTrendingSearches(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 58 | self.sendHttpRequest(path: "trending_searches", method: "GET", parameters: parameters, completion: completion) 59 | } 60 | 61 | public func submitPodcast(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 62 | self.sendHttpRequest(path: "podcasts/submit", method: "POST", parameters: parameters, completion: completion) 63 | } 64 | 65 | public func batchFetchPodcasts(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 66 | self.sendHttpRequest(path: "podcasts", method: "POST", parameters: parameters, completion: completion) 67 | } 68 | 69 | public func batchFetchEpisodes(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 70 | self.sendHttpRequest(path: "episodes", method: "POST", parameters: parameters, completion: completion) 71 | } 72 | 73 | public func fetchMyPlaylists(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 74 | self.sendHttpRequest(path: "playlists", method: "GET", parameters: parameters, completion: completion) 75 | } 76 | 77 | public func fetchPlaylistById(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 78 | if let id = parameters["id"] { 79 | self.sendHttpRequest(path: "playlists/\(id)", method: "GET", 80 | parameters: parameters.filter { key, value in 81 | return key != "id" 82 | }, completion: completion) 83 | } else { 84 | completion(ApiResponse(request: nil, data: nil, response: nil, httpError: nil, apiError: PodcastApiError.invalidRequestError)) 85 | } 86 | } 87 | 88 | public func fetchRecommendationsForEpisode(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 89 | if let id = parameters["id"] { 90 | self.sendHttpRequest(path: "episodes/\(id)/recommendations", method: "GET", 91 | parameters: parameters.filter { key, value in 92 | return key != "id" 93 | }, completion: completion) 94 | } else { 95 | completion(ApiResponse(request: nil, data: nil, response: nil, httpError: nil, apiError: PodcastApiError.invalidRequestError)) 96 | } 97 | } 98 | 99 | public func fetchRecommendationsForPodcast(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 100 | if let id = parameters["id"] { 101 | self.sendHttpRequest(path: "podcasts/\(id)/recommendations", method: "GET", 102 | parameters: parameters.filter { key, value in 103 | return key != "id" 104 | }, completion: completion) 105 | } else { 106 | completion(ApiResponse(request: nil, data: nil, response: nil, httpError: nil, apiError: PodcastApiError.invalidRequestError)) 107 | } 108 | } 109 | 110 | public func justListen(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 111 | self.sendHttpRequest(path: "just_listen", method: "GET", parameters: parameters, completion: completion) 112 | } 113 | 114 | public func fetchPodcastLanguages(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 115 | self.sendHttpRequest(path: "languages", method: "GET", parameters: parameters, completion: completion) 116 | } 117 | 118 | public func fetchPodcastRegions(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 119 | self.sendHttpRequest(path: "regions", method: "GET", parameters: parameters, completion: completion) 120 | } 121 | 122 | public func fetchPodcastGenres(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 123 | self.sendHttpRequest(path: "genres", method: "GET", parameters: parameters, completion: completion) 124 | } 125 | 126 | public func fetchCuratedPodcastsListById(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 127 | if let id = parameters["id"] { 128 | self.sendHttpRequest(path: "curated_podcasts/\(id)", method: "GET", 129 | parameters: parameters.filter { key, value in 130 | return key != "id" 131 | }, completion: completion) 132 | } else { 133 | completion(ApiResponse(request: nil, data: nil, response: nil, httpError: nil, apiError: PodcastApiError.invalidRequestError)) 134 | } 135 | } 136 | 137 | public func fetchEpisodeById(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 138 | if let id = parameters["id"] { 139 | self.sendHttpRequest(path: "episodes/\(id)", method: "GET", 140 | parameters: parameters.filter { key, value in 141 | return key != "id" 142 | }, completion: completion) 143 | } else { 144 | completion(ApiResponse(request: nil, data: nil, response: nil, httpError: nil, apiError: PodcastApiError.invalidRequestError)) 145 | } 146 | } 147 | 148 | public func fetchPodcastById(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 149 | if let id = parameters["id"] { 150 | self.sendHttpRequest(path: "podcasts/\(id)", method: "GET", 151 | parameters: parameters.filter { key, value in 152 | return key != "id" 153 | }, completion: completion) 154 | } else { 155 | completion(ApiResponse(request: nil, data: nil, response: nil, httpError: nil, apiError: PodcastApiError.invalidRequestError)) 156 | } 157 | } 158 | 159 | public func fetchCuratedPodcastsLists(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 160 | self.sendHttpRequest(path: "curated_podcasts", method: "GET", parameters: parameters, completion: completion) 161 | } 162 | 163 | public func fetchBestPodcasts(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 164 | self.sendHttpRequest(path: "best_podcasts", method: "GET", parameters: parameters, completion: completion) 165 | } 166 | 167 | public func deletePodcast(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 168 | if let id = parameters["id"] { 169 | self.sendHttpRequest(path: "podcasts/\(id)", method: "DELETE", 170 | parameters: parameters.filter { key, value in 171 | return key != "id" 172 | }, completion: completion) 173 | } else { 174 | completion(ApiResponse(request: nil, data: nil, response: nil, httpError: nil, apiError: PodcastApiError.invalidRequestError)) 175 | } 176 | } 177 | 178 | public func fetchAudienceForPodcast(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 179 | if let id = parameters["id"] { 180 | self.sendHttpRequest(path: "podcasts/\(id)/audience", method: "GET", 181 | parameters: parameters.filter { key, value in 182 | return key != "id" 183 | }, completion: completion) 184 | } else { 185 | completion(ApiResponse(request: nil, data: nil, response: nil, httpError: nil, apiError: PodcastApiError.invalidRequestError)) 186 | } 187 | } 188 | 189 | public func fetchPodcastsByDomain(parameters: [String: String], completion: @escaping (ApiResponse) -> ()) { 190 | if let domain_name = parameters["domain_name"] { 191 | self.sendHttpRequest(path: "podcasts/domains/\(domain_name)", method: "GET", 192 | parameters: parameters.filter { key, value in 193 | return key != "domain_name" 194 | }, completion: completion) 195 | } else { 196 | completion(ApiResponse(request: nil, data: nil, response: nil, httpError: nil, apiError: PodcastApiError.invalidRequestError)) 197 | } 198 | } 199 | 200 | func sendHttpRequest(path: String, method: String, parameters: [String: String], completion: ((ApiResponse) -> ())?) { 201 | let urlString = "\(self.baseUrl)/\(path)" 202 | 203 | var request: URLRequest 204 | 205 | if method == "POST" { 206 | request = URLRequest(url: URL(string: urlString)!) 207 | request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") 208 | var data = [String]() 209 | for(key, value) in parameters { 210 | data.append(key + "=\(value)") 211 | } 212 | let postData = data.map { String($0) }.joined(separator: "&") 213 | request.httpBody = postData.data(using: .utf8) 214 | } else { 215 | var components = URLComponents(string: urlString)! 216 | components.queryItems = parameters.map { (key, value) in 217 | URLQueryItem(name: key, value: value) 218 | } 219 | components.percentEncodedQuery = components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B") 220 | request = URLRequest(url: components.url!) 221 | } 222 | request.httpMethod = method 223 | request.setValue(self.apiKey, forHTTPHeaderField: "X-ListenAPI-Key") 224 | request.setValue(self.userAgent, forHTTPHeaderField: "User-Agent") 225 | request.timeoutInterval = TimeInterval(self.responseTimeoutSec) 226 | 227 | let sema: DispatchSemaphore? = self.synchronousRequest ? DispatchSemaphore(value: 0) : nil; 228 | let task = URLSession.shared.dataTask(with: request) {(data, response, error) in 229 | if let error = error { 230 | completion?(ApiResponse(request: request, data: data, response: response, httpError: error, apiError: PodcastApiError.apiConnectionError)) 231 | if let sema = sema { 232 | sema.signal() 233 | } 234 | return 235 | } 236 | completion?(ApiResponse(request: request, data: data, response: response, httpError: error, apiError: nil)) 237 | if let sema = sema { 238 | sema.signal() 239 | } 240 | } 241 | 242 | task.resume() 243 | if let sema = sema { 244 | sema.wait() 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /Sources/PodcastAPI/PodcastApiError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wenbin Fang on 5/13/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum PodcastApiError: Error { 11 | case authenticationError 12 | case apiConnectionError 13 | case tooManyRequestsError 14 | case invalidRequestError 15 | case notFoundError 16 | case serverError 17 | } 18 | -------------------------------------------------------------------------------- /Sources/PodcastAPI/SwiftyJSON.swift: -------------------------------------------------------------------------------- 1 | // SwiftyJSON.swift 2 | // 3 | // Copyright (c) 2014 - 2017 Ruoyu Fu, Pinglin Tang 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | import Foundation 24 | 25 | // MARK: - Error 26 | // swiftlint:disable line_length 27 | public enum SwiftyJSONError: Int, Swift.Error { 28 | case unsupportedType = 999 29 | case indexOutOfBounds = 900 30 | case elementTooDeep = 902 31 | case wrongType = 901 32 | case notExist = 500 33 | case invalidJSON = 490 34 | } 35 | 36 | extension SwiftyJSONError: CustomNSError { 37 | 38 | /// return the error domain of SwiftyJSONError 39 | public static var errorDomain: String { return "com.swiftyjson.SwiftyJSON" } 40 | 41 | /// return the error code of SwiftyJSONError 42 | public var errorCode: Int { return self.rawValue } 43 | 44 | /// return the userInfo of SwiftyJSONError 45 | public var errorUserInfo: [String: Any] { 46 | switch self { 47 | case .unsupportedType: 48 | return [NSLocalizedDescriptionKey: "It is an unsupported type."] 49 | case .indexOutOfBounds: 50 | return [NSLocalizedDescriptionKey: "Array Index is out of bounds."] 51 | case .wrongType: 52 | return [NSLocalizedDescriptionKey: "Couldn't merge, because the JSONs differ in type on top level."] 53 | case .notExist: 54 | return [NSLocalizedDescriptionKey: "Dictionary key does not exist."] 55 | case .invalidJSON: 56 | return [NSLocalizedDescriptionKey: "JSON is invalid."] 57 | case .elementTooDeep: 58 | return [NSLocalizedDescriptionKey: "Element too deep. Increase maxObjectDepth and make sure there is no reference loop."] 59 | } 60 | } 61 | } 62 | 63 | // MARK: - JSON Type 64 | 65 | /** 66 | JSON's type definitions. 67 | 68 | See http://www.json.org 69 | */ 70 | public enum Type: Int { 71 | case number 72 | case string 73 | case bool 74 | case array 75 | case dictionary 76 | case null 77 | case unknown 78 | } 79 | 80 | // MARK: - JSON Base 81 | 82 | public struct JSON { 83 | 84 | /** 85 | Creates a JSON using the data. 86 | 87 | - parameter data: The NSData used to convert to json.Top level object in data is an NSArray or NSDictionary 88 | - parameter opt: The JSON serialization reading options. `[]` by default. 89 | 90 | - returns: The created JSON 91 | */ 92 | public init(data: Data, options opt: JSONSerialization.ReadingOptions = []) throws { 93 | let object: Any = try JSONSerialization.jsonObject(with: data, options: opt) 94 | self.init(jsonObject: object) 95 | } 96 | 97 | /** 98 | Creates a JSON object 99 | - note: this does not parse a `String` into JSON, instead use `init(parseJSON: String)` 100 | 101 | - parameter object: the object 102 | 103 | - returns: the created JSON object 104 | */ 105 | public init(_ object: Any) { 106 | switch object { 107 | case let object as Data: 108 | do { 109 | try self.init(data: object) 110 | } catch { 111 | self.init(jsonObject: NSNull()) 112 | } 113 | default: 114 | self.init(jsonObject: object) 115 | } 116 | } 117 | 118 | /** 119 | Parses the JSON string into a JSON object 120 | 121 | - parameter json: the JSON string 122 | 123 | - returns: the created JSON object 124 | */ 125 | public init(parseJSON jsonString: String) { 126 | if let data = jsonString.data(using: .utf8) { 127 | self.init(data) 128 | } else { 129 | self.init(NSNull()) 130 | } 131 | } 132 | 133 | /** 134 | Creates a JSON using the object. 135 | 136 | - parameter jsonObject: The object must have the following properties: All objects are NSString/String, NSNumber/Int/Float/Double/Bool, NSArray/Array, NSDictionary/Dictionary, or NSNull; All dictionary keys are NSStrings/String; NSNumbers are not NaN or infinity. 137 | 138 | - returns: The created JSON 139 | */ 140 | fileprivate init(jsonObject: Any) { 141 | object = jsonObject 142 | } 143 | 144 | /** 145 | Merges another JSON into this JSON, whereas primitive values which are not present in this JSON are getting added, 146 | present values getting overwritten, array values getting appended and nested JSONs getting merged the same way. 147 | 148 | - parameter other: The JSON which gets merged into this JSON 149 | 150 | - throws `ErrorWrongType` if the other JSONs differs in type on the top level. 151 | */ 152 | public mutating func merge(with other: JSON) throws { 153 | try self.merge(with: other, typecheck: true) 154 | } 155 | 156 | /** 157 | Merges another JSON into this JSON and returns a new JSON, whereas primitive values which are not present in this JSON are getting added, 158 | present values getting overwritten, array values getting appended and nested JSONS getting merged the same way. 159 | 160 | - parameter other: The JSON which gets merged into this JSON 161 | 162 | - throws `ErrorWrongType` if the other JSONs differs in type on the top level. 163 | 164 | - returns: New merged JSON 165 | */ 166 | public func merged(with other: JSON) throws -> JSON { 167 | var merged = self 168 | try merged.merge(with: other, typecheck: true) 169 | return merged 170 | } 171 | 172 | /** 173 | Private woker function which does the actual merging 174 | Typecheck is set to true for the first recursion level to prevent total override of the source JSON 175 | */ 176 | fileprivate mutating func merge(with other: JSON, typecheck: Bool) throws { 177 | if type == other.type { 178 | switch type { 179 | case .dictionary: 180 | for (key, _) in other { 181 | try self[key].merge(with: other[key], typecheck: false) 182 | } 183 | case .array: 184 | self = JSON(arrayValue + other.arrayValue) 185 | default: 186 | self = other 187 | } 188 | } else { 189 | if typecheck { 190 | throw SwiftyJSONError.wrongType 191 | } else { 192 | self = other 193 | } 194 | } 195 | } 196 | 197 | /// Private object 198 | fileprivate var rawArray: [Any] = [] 199 | fileprivate var rawDictionary: [String: Any] = [:] 200 | fileprivate var rawString: String = "" 201 | fileprivate var rawNumber: NSNumber = 0 202 | fileprivate var rawNull: NSNull = NSNull() 203 | fileprivate var rawBool: Bool = false 204 | 205 | /// JSON type, fileprivate setter 206 | public fileprivate(set) var type: Type = .null 207 | 208 | /// Error in JSON, fileprivate setter 209 | public fileprivate(set) var error: SwiftyJSONError? 210 | 211 | /// Object in JSON 212 | public var object: Any { 213 | get { 214 | switch type { 215 | case .array: return rawArray 216 | case .dictionary: return rawDictionary 217 | case .string: return rawString 218 | case .number: return rawNumber 219 | case .bool: return rawBool 220 | default: return rawNull 221 | } 222 | } 223 | set { 224 | error = nil 225 | switch unwrap(newValue) { 226 | case let number as NSNumber: 227 | if number.isBool { 228 | type = .bool 229 | rawBool = number.boolValue 230 | } else { 231 | type = .number 232 | rawNumber = number 233 | } 234 | case let string as String: 235 | type = .string 236 | rawString = string 237 | case _ as NSNull: 238 | type = .null 239 | case Optional.none: 240 | type = .null 241 | case let array as [Any]: 242 | type = .array 243 | rawArray = array 244 | case let dictionary as [String: Any]: 245 | type = .dictionary 246 | rawDictionary = dictionary 247 | default: 248 | type = .unknown 249 | error = SwiftyJSONError.unsupportedType 250 | } 251 | } 252 | } 253 | 254 | /// The static null JSON 255 | @available(*, unavailable, renamed:"null") 256 | public static var nullJSON: JSON { return null } 257 | public static var null: JSON { return JSON(NSNull()) } 258 | } 259 | 260 | /// Private method to unwarp an object recursively 261 | private func unwrap(_ object: Any) -> Any { 262 | switch object { 263 | case let json as JSON: 264 | return unwrap(json.object) 265 | case let array as [Any]: 266 | return array.map(unwrap) 267 | case let dictionary as [String: Any]: 268 | var d = dictionary 269 | dictionary.forEach { pair in 270 | d[pair.key] = unwrap(pair.value) 271 | } 272 | return d 273 | default: 274 | return object 275 | } 276 | } 277 | 278 | public enum Index: Comparable { 279 | case array(Int) 280 | case dictionary(DictionaryIndex) 281 | case null 282 | 283 | static public func == (lhs: Index, rhs: Index) -> Bool { 284 | switch (lhs, rhs) { 285 | case (.array(let left), .array(let right)): return left == right 286 | case (.dictionary(let left), .dictionary(let right)): return left == right 287 | case (.null, .null): return true 288 | default: return false 289 | } 290 | } 291 | 292 | static public func < (lhs: Index, rhs: Index) -> Bool { 293 | switch (lhs, rhs) { 294 | case (.array(let left), .array(let right)): return left < right 295 | case (.dictionary(let left), .dictionary(let right)): return left < right 296 | default: return false 297 | } 298 | } 299 | } 300 | 301 | public typealias JSONIndex = Index 302 | public typealias JSONRawIndex = Index 303 | 304 | extension JSON: Swift.Collection { 305 | 306 | public typealias Index = JSONRawIndex 307 | 308 | public var startIndex: Index { 309 | switch type { 310 | case .array: return .array(rawArray.startIndex) 311 | case .dictionary: return .dictionary(rawDictionary.startIndex) 312 | default: return .null 313 | } 314 | } 315 | 316 | public var endIndex: Index { 317 | switch type { 318 | case .array: return .array(rawArray.endIndex) 319 | case .dictionary: return .dictionary(rawDictionary.endIndex) 320 | default: return .null 321 | } 322 | } 323 | 324 | public func index(after i: Index) -> Index { 325 | switch i { 326 | case .array(let idx): return .array(rawArray.index(after: idx)) 327 | case .dictionary(let idx): return .dictionary(rawDictionary.index(after: idx)) 328 | default: return .null 329 | } 330 | } 331 | 332 | public subscript (position: Index) -> (String, JSON) { 333 | switch position { 334 | case .array(let idx): return (String(idx), JSON(rawArray[idx])) 335 | case .dictionary(let idx): return (rawDictionary[idx].key, JSON(rawDictionary[idx].value)) 336 | default: return ("", JSON.null) 337 | } 338 | } 339 | } 340 | 341 | // MARK: - Subscript 342 | 343 | /** 344 | * To mark both String and Int can be used in subscript. 345 | */ 346 | public enum JSONKey { 347 | case index(Int) 348 | case key(String) 349 | } 350 | 351 | public protocol JSONSubscriptType { 352 | var jsonKey: JSONKey { get } 353 | } 354 | 355 | extension Int: JSONSubscriptType { 356 | public var jsonKey: JSONKey { 357 | return JSONKey.index(self) 358 | } 359 | } 360 | 361 | extension String: JSONSubscriptType { 362 | public var jsonKey: JSONKey { 363 | return JSONKey.key(self) 364 | } 365 | } 366 | 367 | extension JSON { 368 | 369 | /// If `type` is `.array`, return json whose object is `array[index]`, otherwise return null json with error. 370 | fileprivate subscript(index index: Int) -> JSON { 371 | get { 372 | if type != .array { 373 | var r = JSON.null 374 | r.error = self.error ?? SwiftyJSONError.wrongType 375 | return r 376 | } else if rawArray.indices.contains(index) { 377 | return JSON(rawArray[index]) 378 | } else { 379 | var r = JSON.null 380 | r.error = SwiftyJSONError.indexOutOfBounds 381 | return r 382 | } 383 | } 384 | set { 385 | if type == .array && 386 | rawArray.indices.contains(index) && 387 | newValue.error == nil { 388 | rawArray[index] = newValue.object 389 | } 390 | } 391 | } 392 | 393 | /// If `type` is `.dictionary`, return json whose object is `dictionary[key]` , otherwise return null json with error. 394 | fileprivate subscript(key key: String) -> JSON { 395 | get { 396 | var r = JSON.null 397 | if type == .dictionary { 398 | if let o = rawDictionary[key] { 399 | r = JSON(o) 400 | } else { 401 | r.error = SwiftyJSONError.notExist 402 | } 403 | } else { 404 | r.error = self.error ?? SwiftyJSONError.wrongType 405 | } 406 | return r 407 | } 408 | set { 409 | if type == .dictionary && newValue.error == nil { 410 | rawDictionary[key] = newValue.object 411 | } 412 | } 413 | } 414 | 415 | /// If `sub` is `Int`, return `subscript(index:)`; If `sub` is `String`, return `subscript(key:)`. 416 | fileprivate subscript(sub sub: JSONSubscriptType) -> JSON { 417 | get { 418 | switch sub.jsonKey { 419 | case .index(let index): return self[index: index] 420 | case .key(let key): return self[key: key] 421 | } 422 | } 423 | set { 424 | switch sub.jsonKey { 425 | case .index(let index): self[index: index] = newValue 426 | case .key(let key): self[key: key] = newValue 427 | } 428 | } 429 | } 430 | 431 | /** 432 | Find a json in the complex data structures by using array of Int and/or String as path. 433 | 434 | Example: 435 | 436 | ``` 437 | let json = JSON[data] 438 | let path = [9,"list","person","name"] 439 | let name = json[path] 440 | ``` 441 | 442 | The same as: let name = json[9]["list"]["person"]["name"] 443 | 444 | - parameter path: The target json's path. 445 | 446 | - returns: Return a json found by the path or a null json with error 447 | */ 448 | public subscript(path: [JSONSubscriptType]) -> JSON { 449 | get { 450 | return path.reduce(self) { $0[sub: $1] } 451 | } 452 | set { 453 | switch path.count { 454 | case 0: return 455 | case 1: self[sub:path[0]].object = newValue.object 456 | default: 457 | var aPath = path 458 | aPath.remove(at: 0) 459 | var nextJSON = self[sub: path[0]] 460 | nextJSON[aPath] = newValue 461 | self[sub: path[0]] = nextJSON 462 | } 463 | } 464 | } 465 | 466 | /** 467 | Find a json in the complex data structures by using array of Int and/or String as path. 468 | 469 | - parameter path: The target json's path. Example: 470 | 471 | let name = json[9,"list","person","name"] 472 | 473 | The same as: let name = json[9]["list"]["person"]["name"] 474 | 475 | - returns: Return a json found by the path or a null json with error 476 | */ 477 | public subscript(path: JSONSubscriptType...) -> JSON { 478 | get { 479 | return self[path] 480 | } 481 | set { 482 | self[path] = newValue 483 | } 484 | } 485 | } 486 | 487 | // MARK: - LiteralConvertible 488 | 489 | extension JSON: Swift.ExpressibleByStringLiteral { 490 | 491 | public init(stringLiteral value: StringLiteralType) { 492 | self.init(value) 493 | } 494 | 495 | public init(extendedGraphemeClusterLiteral value: StringLiteralType) { 496 | self.init(value) 497 | } 498 | 499 | public init(unicodeScalarLiteral value: StringLiteralType) { 500 | self.init(value) 501 | } 502 | } 503 | 504 | extension JSON: Swift.ExpressibleByIntegerLiteral { 505 | 506 | public init(integerLiteral value: IntegerLiteralType) { 507 | self.init(value) 508 | } 509 | } 510 | 511 | extension JSON: Swift.ExpressibleByBooleanLiteral { 512 | 513 | public init(booleanLiteral value: BooleanLiteralType) { 514 | self.init(value) 515 | } 516 | } 517 | 518 | extension JSON: Swift.ExpressibleByFloatLiteral { 519 | 520 | public init(floatLiteral value: FloatLiteralType) { 521 | self.init(value) 522 | } 523 | } 524 | 525 | extension JSON: Swift.ExpressibleByDictionaryLiteral { 526 | public init(dictionaryLiteral elements: (String, Any)...) { 527 | let dictionary = elements.reduce(into: [String: Any](), { $0[$1.0] = $1.1}) 528 | self.init(dictionary) 529 | } 530 | } 531 | 532 | extension JSON: Swift.ExpressibleByArrayLiteral { 533 | 534 | public init(arrayLiteral elements: Any...) { 535 | self.init(elements) 536 | } 537 | } 538 | 539 | // MARK: - Raw 540 | 541 | extension JSON: Swift.RawRepresentable { 542 | 543 | public init?(rawValue: Any) { 544 | if JSON(rawValue).type == .unknown { 545 | return nil 546 | } else { 547 | self.init(rawValue) 548 | } 549 | } 550 | 551 | public var rawValue: Any { 552 | return object 553 | } 554 | 555 | public func rawData(options opt: JSONSerialization.WritingOptions = JSONSerialization.WritingOptions(rawValue: 0)) throws -> Data { 556 | guard JSONSerialization.isValidJSONObject(object) else { 557 | throw SwiftyJSONError.invalidJSON 558 | } 559 | 560 | return try JSONSerialization.data(withJSONObject: object, options: opt) 561 | } 562 | 563 | public func rawString(_ encoding: String.Encoding = .utf8, options opt: JSONSerialization.WritingOptions = .prettyPrinted) -> String? { 564 | do { 565 | return try _rawString(encoding, options: [.jsonSerialization: opt]) 566 | } catch { 567 | print("Could not serialize object to JSON because:", error.localizedDescription) 568 | return nil 569 | } 570 | } 571 | 572 | public func rawString(_ options: [writingOptionsKeys: Any]) -> String? { 573 | let encoding = options[.encoding] as? String.Encoding ?? String.Encoding.utf8 574 | let maxObjectDepth = options[.maxObjextDepth] as? Int ?? 10 575 | do { 576 | return try _rawString(encoding, options: options, maxObjectDepth: maxObjectDepth) 577 | } catch { 578 | print("Could not serialize object to JSON because:", error.localizedDescription) 579 | return nil 580 | } 581 | } 582 | 583 | fileprivate func _rawString(_ encoding: String.Encoding = .utf8, options: [writingOptionsKeys: Any], maxObjectDepth: Int = 10) throws -> String? { 584 | guard maxObjectDepth > 0 else { throw SwiftyJSONError.invalidJSON } 585 | switch type { 586 | case .dictionary: 587 | do { 588 | if !(options[.castNilToNSNull] as? Bool ?? false) { 589 | let jsonOption = options[.jsonSerialization] as? JSONSerialization.WritingOptions ?? JSONSerialization.WritingOptions.prettyPrinted 590 | let data = try rawData(options: jsonOption) 591 | return String(data: data, encoding: encoding) 592 | } 593 | 594 | guard let dict = object as? [String: Any?] else { 595 | return nil 596 | } 597 | let body = try dict.keys.map { key throws -> String in 598 | guard let value = dict[key] else { 599 | return "\"\(key)\": null" 600 | } 601 | guard let unwrappedValue = value else { 602 | return "\"\(key)\": null" 603 | } 604 | 605 | let nestedValue = JSON(unwrappedValue) 606 | guard let nestedString = try nestedValue._rawString(encoding, options: options, maxObjectDepth: maxObjectDepth - 1) else { 607 | throw SwiftyJSONError.elementTooDeep 608 | } 609 | if nestedValue.type == .string { 610 | return "\"\(key)\": \"\(nestedString.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\""))\"" 611 | } else { 612 | return "\"\(key)\": \(nestedString)" 613 | } 614 | } 615 | 616 | return "{\(body.joined(separator: ","))}" 617 | } catch _ { 618 | return nil 619 | } 620 | case .array: 621 | do { 622 | if !(options[.castNilToNSNull] as? Bool ?? false) { 623 | let jsonOption = options[.jsonSerialization] as? JSONSerialization.WritingOptions ?? JSONSerialization.WritingOptions.prettyPrinted 624 | let data = try rawData(options: jsonOption) 625 | return String(data: data, encoding: encoding) 626 | } 627 | 628 | guard let array = object as? [Any?] else { 629 | return nil 630 | } 631 | let body = try array.map { value throws -> String in 632 | guard let unwrappedValue = value else { 633 | return "null" 634 | } 635 | 636 | let nestedValue = JSON(unwrappedValue) 637 | guard let nestedString = try nestedValue._rawString(encoding, options: options, maxObjectDepth: maxObjectDepth - 1) else { 638 | throw SwiftyJSONError.invalidJSON 639 | } 640 | if nestedValue.type == .string { 641 | return "\"\(nestedString.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\""))\"" 642 | } else { 643 | return nestedString 644 | } 645 | } 646 | 647 | return "[\(body.joined(separator: ","))]" 648 | } catch _ { 649 | return nil 650 | } 651 | case .string: return rawString 652 | case .number: return rawNumber.stringValue 653 | case .bool: return rawBool.description 654 | case .null: return "null" 655 | default: return nil 656 | } 657 | } 658 | } 659 | 660 | // MARK: - Printable, DebugPrintable 661 | 662 | extension JSON: Swift.CustomStringConvertible, Swift.CustomDebugStringConvertible { 663 | 664 | public var description: String { 665 | return rawString(options: .prettyPrinted) ?? "unknown" 666 | } 667 | 668 | public var debugDescription: String { 669 | return description 670 | } 671 | } 672 | 673 | // MARK: - Array 674 | 675 | extension JSON { 676 | 677 | //Optional [JSON] 678 | public var array: [JSON]? { 679 | return type == .array ? rawArray.map { JSON($0) } : nil 680 | } 681 | 682 | //Non-optional [JSON] 683 | public var arrayValue: [JSON] { 684 | return self.array ?? [] 685 | } 686 | 687 | //Optional [Any] 688 | public var arrayObject: [Any]? { 689 | get { 690 | switch type { 691 | case .array: return rawArray 692 | default: return nil 693 | } 694 | } 695 | set { 696 | self.object = newValue ?? NSNull() 697 | } 698 | } 699 | } 700 | 701 | // MARK: - Dictionary 702 | 703 | extension JSON { 704 | 705 | //Optional [String : JSON] 706 | public var dictionary: [String: JSON]? { 707 | if type == .dictionary { 708 | var d = [String: JSON](minimumCapacity: rawDictionary.count) 709 | rawDictionary.forEach { pair in 710 | d[pair.key] = JSON(pair.value) 711 | } 712 | return d 713 | } else { 714 | return nil 715 | } 716 | } 717 | 718 | //Non-optional [String : JSON] 719 | public var dictionaryValue: [String: JSON] { 720 | return dictionary ?? [:] 721 | } 722 | 723 | //Optional [String : Any] 724 | 725 | public var dictionaryObject: [String: Any]? { 726 | get { 727 | switch type { 728 | case .dictionary: return rawDictionary 729 | default: return nil 730 | } 731 | } 732 | set { 733 | object = newValue ?? NSNull() 734 | } 735 | } 736 | } 737 | 738 | // MARK: - Bool 739 | 740 | extension JSON { // : Swift.Bool 741 | 742 | //Optional bool 743 | public var bool: Bool? { 744 | get { 745 | switch type { 746 | case .bool: return rawBool 747 | default: return nil 748 | } 749 | } 750 | set { 751 | object = newValue ?? NSNull() 752 | } 753 | } 754 | 755 | //Non-optional bool 756 | public var boolValue: Bool { 757 | get { 758 | switch type { 759 | case .bool: return rawBool 760 | case .number: return rawNumber.boolValue 761 | case .string: return ["true", "y", "t", "yes", "1"].contains { rawString.caseInsensitiveCompare($0) == .orderedSame } 762 | default: return false 763 | } 764 | } 765 | set { 766 | object = newValue 767 | } 768 | } 769 | } 770 | 771 | // MARK: - String 772 | 773 | extension JSON { 774 | 775 | //Optional string 776 | public var string: String? { 777 | get { 778 | switch type { 779 | case .string: return object as? String 780 | default: return nil 781 | } 782 | } 783 | set { 784 | object = newValue ?? NSNull() 785 | } 786 | } 787 | 788 | //Non-optional string 789 | public var stringValue: String { 790 | get { 791 | switch type { 792 | case .string: return object as? String ?? "" 793 | case .number: return rawNumber.stringValue 794 | case .bool: return (object as? Bool).map { String($0) } ?? "" 795 | default: return "" 796 | } 797 | } 798 | set { 799 | object = newValue 800 | } 801 | } 802 | } 803 | 804 | // MARK: - Number 805 | 806 | extension JSON { 807 | 808 | //Optional number 809 | public var number: NSNumber? { 810 | get { 811 | switch type { 812 | case .number: return rawNumber 813 | case .bool: return NSNumber(value: rawBool ? 1 : 0) 814 | default: return nil 815 | } 816 | } 817 | set { 818 | object = newValue ?? NSNull() 819 | } 820 | } 821 | 822 | //Non-optional number 823 | public var numberValue: NSNumber { 824 | get { 825 | switch type { 826 | case .string: 827 | let decimal = NSDecimalNumber(string: object as? String) 828 | return decimal == .notANumber ? .zero : decimal 829 | case .number: return object as? NSNumber ?? NSNumber(value: 0) 830 | case .bool: return NSNumber(value: rawBool ? 1 : 0) 831 | default: return NSNumber(value: 0.0) 832 | } 833 | } 834 | set { 835 | object = newValue 836 | } 837 | } 838 | } 839 | 840 | // MARK: - Null 841 | 842 | extension JSON { 843 | 844 | public var null: NSNull? { 845 | set { 846 | object = NSNull() 847 | } 848 | get { 849 | switch type { 850 | case .null: return rawNull 851 | default: return nil 852 | } 853 | } 854 | } 855 | public func exists() -> Bool { 856 | if let errorValue = error, (400...1000).contains(errorValue.errorCode) { 857 | return false 858 | } 859 | return true 860 | } 861 | } 862 | 863 | // MARK: - URL 864 | 865 | extension JSON { 866 | 867 | //Optional URL 868 | public var url: URL? { 869 | get { 870 | switch type { 871 | case .string: 872 | // Check for existing percent escapes first to prevent double-escaping of % character 873 | if rawString.range(of: "%[0-9A-Fa-f]{2}", options: .regularExpression, range: nil, locale: nil) != nil { 874 | return Foundation.URL(string: rawString) 875 | } else if let encodedString_ = rawString.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) { 876 | // We have to use `Foundation.URL` otherwise it conflicts with the variable name. 877 | return Foundation.URL(string: encodedString_) 878 | } else { 879 | return nil 880 | } 881 | default: 882 | return nil 883 | } 884 | } 885 | set { 886 | object = newValue?.absoluteString ?? NSNull() 887 | } 888 | } 889 | } 890 | 891 | // MARK: - Int, Double, Float, Int8, Int16, Int32, Int64 892 | 893 | extension JSON { 894 | 895 | public var double: Double? { 896 | get { 897 | return number?.doubleValue 898 | } 899 | set { 900 | if let newValue = newValue { 901 | object = NSNumber(value: newValue) 902 | } else { 903 | object = NSNull() 904 | } 905 | } 906 | } 907 | 908 | public var doubleValue: Double { 909 | get { 910 | return numberValue.doubleValue 911 | } 912 | set { 913 | object = NSNumber(value: newValue) 914 | } 915 | } 916 | 917 | public var float: Float? { 918 | get { 919 | return number?.floatValue 920 | } 921 | set { 922 | if let newValue = newValue { 923 | object = NSNumber(value: newValue) 924 | } else { 925 | object = NSNull() 926 | } 927 | } 928 | } 929 | 930 | public var floatValue: Float { 931 | get { 932 | return numberValue.floatValue 933 | } 934 | set { 935 | object = NSNumber(value: newValue) 936 | } 937 | } 938 | 939 | public var int: Int? { 940 | get { 941 | return number?.intValue 942 | } 943 | set { 944 | if let newValue = newValue { 945 | object = NSNumber(value: newValue) 946 | } else { 947 | object = NSNull() 948 | } 949 | } 950 | } 951 | 952 | public var intValue: Int { 953 | get { 954 | return numberValue.intValue 955 | } 956 | set { 957 | object = NSNumber(value: newValue) 958 | } 959 | } 960 | 961 | public var uInt: UInt? { 962 | get { 963 | return number?.uintValue 964 | } 965 | set { 966 | if let newValue = newValue { 967 | object = NSNumber(value: newValue) 968 | } else { 969 | object = NSNull() 970 | } 971 | } 972 | } 973 | 974 | public var uIntValue: UInt { 975 | get { 976 | return numberValue.uintValue 977 | } 978 | set { 979 | object = NSNumber(value: newValue) 980 | } 981 | } 982 | 983 | public var int8: Int8? { 984 | get { 985 | return number?.int8Value 986 | } 987 | set { 988 | if let newValue = newValue { 989 | object = NSNumber(value: Int(newValue)) 990 | } else { 991 | object = NSNull() 992 | } 993 | } 994 | } 995 | 996 | public var int8Value: Int8 { 997 | get { 998 | return numberValue.int8Value 999 | } 1000 | set { 1001 | object = NSNumber(value: Int(newValue)) 1002 | } 1003 | } 1004 | 1005 | public var uInt8: UInt8? { 1006 | get { 1007 | return number?.uint8Value 1008 | } 1009 | set { 1010 | if let newValue = newValue { 1011 | object = NSNumber(value: newValue) 1012 | } else { 1013 | object = NSNull() 1014 | } 1015 | } 1016 | } 1017 | 1018 | public var uInt8Value: UInt8 { 1019 | get { 1020 | return numberValue.uint8Value 1021 | } 1022 | set { 1023 | object = NSNumber(value: newValue) 1024 | } 1025 | } 1026 | 1027 | public var int16: Int16? { 1028 | get { 1029 | return number?.int16Value 1030 | } 1031 | set { 1032 | if let newValue = newValue { 1033 | object = NSNumber(value: newValue) 1034 | } else { 1035 | object = NSNull() 1036 | } 1037 | } 1038 | } 1039 | 1040 | public var int16Value: Int16 { 1041 | get { 1042 | return numberValue.int16Value 1043 | } 1044 | set { 1045 | object = NSNumber(value: newValue) 1046 | } 1047 | } 1048 | 1049 | public var uInt16: UInt16? { 1050 | get { 1051 | return number?.uint16Value 1052 | } 1053 | set { 1054 | if let newValue = newValue { 1055 | object = NSNumber(value: newValue) 1056 | } else { 1057 | object = NSNull() 1058 | } 1059 | } 1060 | } 1061 | 1062 | public var uInt16Value: UInt16 { 1063 | get { 1064 | return numberValue.uint16Value 1065 | } 1066 | set { 1067 | object = NSNumber(value: newValue) 1068 | } 1069 | } 1070 | 1071 | public var int32: Int32? { 1072 | get { 1073 | return number?.int32Value 1074 | } 1075 | set { 1076 | if let newValue = newValue { 1077 | object = NSNumber(value: newValue) 1078 | } else { 1079 | object = NSNull() 1080 | } 1081 | } 1082 | } 1083 | 1084 | public var int32Value: Int32 { 1085 | get { 1086 | return numberValue.int32Value 1087 | } 1088 | set { 1089 | object = NSNumber(value: newValue) 1090 | } 1091 | } 1092 | 1093 | public var uInt32: UInt32? { 1094 | get { 1095 | return number?.uint32Value 1096 | } 1097 | set { 1098 | if let newValue = newValue { 1099 | object = NSNumber(value: newValue) 1100 | } else { 1101 | object = NSNull() 1102 | } 1103 | } 1104 | } 1105 | 1106 | public var uInt32Value: UInt32 { 1107 | get { 1108 | return numberValue.uint32Value 1109 | } 1110 | set { 1111 | object = NSNumber(value: newValue) 1112 | } 1113 | } 1114 | 1115 | public var int64: Int64? { 1116 | get { 1117 | return number?.int64Value 1118 | } 1119 | set { 1120 | if let newValue = newValue { 1121 | object = NSNumber(value: newValue) 1122 | } else { 1123 | object = NSNull() 1124 | } 1125 | } 1126 | } 1127 | 1128 | public var int64Value: Int64 { 1129 | get { 1130 | return numberValue.int64Value 1131 | } 1132 | set { 1133 | object = NSNumber(value: newValue) 1134 | } 1135 | } 1136 | 1137 | public var uInt64: UInt64? { 1138 | get { 1139 | return number?.uint64Value 1140 | } 1141 | set { 1142 | if let newValue = newValue { 1143 | object = NSNumber(value: newValue) 1144 | } else { 1145 | object = NSNull() 1146 | } 1147 | } 1148 | } 1149 | 1150 | public var uInt64Value: UInt64 { 1151 | get { 1152 | return numberValue.uint64Value 1153 | } 1154 | set { 1155 | object = NSNumber(value: newValue) 1156 | } 1157 | } 1158 | } 1159 | 1160 | // MARK: - Comparable 1161 | 1162 | extension JSON: Swift.Comparable {} 1163 | 1164 | public func == (lhs: JSON, rhs: JSON) -> Bool { 1165 | 1166 | switch (lhs.type, rhs.type) { 1167 | case (.number, .number): return lhs.rawNumber == rhs.rawNumber 1168 | case (.string, .string): return lhs.rawString == rhs.rawString 1169 | case (.bool, .bool): return lhs.rawBool == rhs.rawBool 1170 | case (.array, .array): return lhs.rawArray as NSArray == rhs.rawArray as NSArray 1171 | case (.dictionary, .dictionary): return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary 1172 | case (.null, .null): return true 1173 | default: return false 1174 | } 1175 | } 1176 | 1177 | public func <= (lhs: JSON, rhs: JSON) -> Bool { 1178 | 1179 | switch (lhs.type, rhs.type) { 1180 | case (.number, .number): return lhs.rawNumber <= rhs.rawNumber 1181 | case (.string, .string): return lhs.rawString <= rhs.rawString 1182 | case (.bool, .bool): return lhs.rawBool == rhs.rawBool 1183 | case (.array, .array): return lhs.rawArray as NSArray == rhs.rawArray as NSArray 1184 | case (.dictionary, .dictionary): return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary 1185 | case (.null, .null): return true 1186 | default: return false 1187 | } 1188 | } 1189 | 1190 | public func >= (lhs: JSON, rhs: JSON) -> Bool { 1191 | 1192 | switch (lhs.type, rhs.type) { 1193 | case (.number, .number): return lhs.rawNumber >= rhs.rawNumber 1194 | case (.string, .string): return lhs.rawString >= rhs.rawString 1195 | case (.bool, .bool): return lhs.rawBool == rhs.rawBool 1196 | case (.array, .array): return lhs.rawArray as NSArray == rhs.rawArray as NSArray 1197 | case (.dictionary, .dictionary): return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary 1198 | case (.null, .null): return true 1199 | default: return false 1200 | } 1201 | } 1202 | 1203 | public func > (lhs: JSON, rhs: JSON) -> Bool { 1204 | 1205 | switch (lhs.type, rhs.type) { 1206 | case (.number, .number): return lhs.rawNumber > rhs.rawNumber 1207 | case (.string, .string): return lhs.rawString > rhs.rawString 1208 | default: return false 1209 | } 1210 | } 1211 | 1212 | public func < (lhs: JSON, rhs: JSON) -> Bool { 1213 | 1214 | switch (lhs.type, rhs.type) { 1215 | case (.number, .number): return lhs.rawNumber < rhs.rawNumber 1216 | case (.string, .string): return lhs.rawString < rhs.rawString 1217 | default: return false 1218 | } 1219 | } 1220 | 1221 | private let trueNumber = NSNumber(value: true) 1222 | private let falseNumber = NSNumber(value: false) 1223 | private let trueObjCType = String(cString: trueNumber.objCType) 1224 | private let falseObjCType = String(cString: falseNumber.objCType) 1225 | 1226 | // MARK: - NSNumber: Comparable 1227 | 1228 | extension NSNumber { 1229 | fileprivate var isBool: Bool { 1230 | let objCType = String(cString: self.objCType) 1231 | if (self.compare(trueNumber) == .orderedSame && objCType == trueObjCType) || (self.compare(falseNumber) == .orderedSame && objCType == falseObjCType) { 1232 | return true 1233 | } else { 1234 | return false 1235 | } 1236 | } 1237 | } 1238 | 1239 | func == (lhs: NSNumber, rhs: NSNumber) -> Bool { 1240 | switch (lhs.isBool, rhs.isBool) { 1241 | case (false, true): return false 1242 | case (true, false): return false 1243 | default: return lhs.compare(rhs) == .orderedSame 1244 | } 1245 | } 1246 | 1247 | func != (lhs: NSNumber, rhs: NSNumber) -> Bool { 1248 | return !(lhs == rhs) 1249 | } 1250 | 1251 | func < (lhs: NSNumber, rhs: NSNumber) -> Bool { 1252 | 1253 | switch (lhs.isBool, rhs.isBool) { 1254 | case (false, true): return false 1255 | case (true, false): return false 1256 | default: return lhs.compare(rhs) == .orderedAscending 1257 | } 1258 | } 1259 | 1260 | func > (lhs: NSNumber, rhs: NSNumber) -> Bool { 1261 | 1262 | switch (lhs.isBool, rhs.isBool) { 1263 | case (false, true): return false 1264 | case (true, false): return false 1265 | default: return lhs.compare(rhs) == ComparisonResult.orderedDescending 1266 | } 1267 | } 1268 | 1269 | func <= (lhs: NSNumber, rhs: NSNumber) -> Bool { 1270 | 1271 | switch (lhs.isBool, rhs.isBool) { 1272 | case (false, true): return false 1273 | case (true, false): return false 1274 | default: return lhs.compare(rhs) != .orderedDescending 1275 | } 1276 | } 1277 | 1278 | func >= (lhs: NSNumber, rhs: NSNumber) -> Bool { 1279 | 1280 | switch (lhs.isBool, rhs.isBool) { 1281 | case (false, true): return false 1282 | case (true, false): return false 1283 | default: return lhs.compare(rhs) != .orderedAscending 1284 | } 1285 | } 1286 | 1287 | public enum writingOptionsKeys { 1288 | case jsonSerialization 1289 | case castNilToNSNull 1290 | case maxObjextDepth 1291 | case encoding 1292 | } 1293 | 1294 | // MARK: - JSON: Codable 1295 | extension JSON: Codable { 1296 | private static var codableTypes: [Codable.Type] { 1297 | return [ 1298 | Bool.self, 1299 | Int.self, 1300 | Int8.self, 1301 | Int16.self, 1302 | Int32.self, 1303 | Int64.self, 1304 | UInt.self, 1305 | UInt8.self, 1306 | UInt16.self, 1307 | UInt32.self, 1308 | UInt64.self, 1309 | Double.self, 1310 | String.self, 1311 | [JSON].self, 1312 | [String: JSON].self 1313 | ] 1314 | } 1315 | public init(from decoder: Decoder) throws { 1316 | var object: Any? 1317 | 1318 | if let container = try? decoder.singleValueContainer(), !container.decodeNil() { 1319 | for type in JSON.codableTypes { 1320 | if object != nil { 1321 | break 1322 | } 1323 | // try to decode value 1324 | switch type { 1325 | case let boolType as Bool.Type: 1326 | object = try? container.decode(boolType) 1327 | case let intType as Int.Type: 1328 | object = try? container.decode(intType) 1329 | case let int8Type as Int8.Type: 1330 | object = try? container.decode(int8Type) 1331 | case let int32Type as Int32.Type: 1332 | object = try? container.decode(int32Type) 1333 | case let int64Type as Int64.Type: 1334 | object = try? container.decode(int64Type) 1335 | case let uintType as UInt.Type: 1336 | object = try? container.decode(uintType) 1337 | case let uint8Type as UInt8.Type: 1338 | object = try? container.decode(uint8Type) 1339 | case let uint16Type as UInt16.Type: 1340 | object = try? container.decode(uint16Type) 1341 | case let uint32Type as UInt32.Type: 1342 | object = try? container.decode(uint32Type) 1343 | case let uint64Type as UInt64.Type: 1344 | object = try? container.decode(uint64Type) 1345 | case let doubleType as Double.Type: 1346 | object = try? container.decode(doubleType) 1347 | case let stringType as String.Type: 1348 | object = try? container.decode(stringType) 1349 | case let jsonValueArrayType as [JSON].Type: 1350 | object = try? container.decode(jsonValueArrayType) 1351 | case let jsonValueDictType as [String: JSON].Type: 1352 | object = try? container.decode(jsonValueDictType) 1353 | default: 1354 | break 1355 | } 1356 | } 1357 | } 1358 | self.init(object ?? NSNull()) 1359 | } 1360 | public func encode(to encoder: Encoder) throws { 1361 | var container = encoder.singleValueContainer() 1362 | if object is NSNull { 1363 | try container.encodeNil() 1364 | return 1365 | } 1366 | switch object { 1367 | case let intValue as Int: 1368 | try container.encode(intValue) 1369 | case let int8Value as Int8: 1370 | try container.encode(int8Value) 1371 | case let int32Value as Int32: 1372 | try container.encode(int32Value) 1373 | case let int64Value as Int64: 1374 | try container.encode(int64Value) 1375 | case let uintValue as UInt: 1376 | try container.encode(uintValue) 1377 | case let uint8Value as UInt8: 1378 | try container.encode(uint8Value) 1379 | case let uint16Value as UInt16: 1380 | try container.encode(uint16Value) 1381 | case let uint32Value as UInt32: 1382 | try container.encode(uint32Value) 1383 | case let uint64Value as UInt64: 1384 | try container.encode(uint64Value) 1385 | case let doubleValue as Double: 1386 | try container.encode(doubleValue) 1387 | case let boolValue as Bool: 1388 | try container.encode(boolValue) 1389 | case let stringValue as String: 1390 | try container.encode(stringValue) 1391 | case is [Any]: 1392 | let jsonValueArray = array ?? [] 1393 | try container.encode(jsonValueArray) 1394 | case is [String: Any]: 1395 | let jsonValueDictValue = dictionary ?? [:] 1396 | try container.encode(jsonValueDictValue) 1397 | default: 1398 | break 1399 | } 1400 | } 1401 | } -------------------------------------------------------------------------------- /Tests/PodcastAPITests/PodcastAPITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import PodcastAPI 3 | 4 | final class PodcastAPITests: XCTestCase { 5 | func testSetApiKey() { 6 | var client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 7 | var parameters: [String: String] = [:] 8 | parameters["q"] = "startup" 9 | client.search(parameters: parameters) { response in 10 | // Correct base url 11 | if let url = response.request?.url?.absoluteString { 12 | XCTAssertTrue(url.contains(BASE_URL_TEST)) 13 | } 14 | 15 | if let headers = response.request?.allHTTPHeaderFields { 16 | if let userAgent = headers["User-Agent"] { 17 | XCTAssertEqual(userAgent, DEFAULT_USER_AGENT) 18 | } else { 19 | XCTFail("Shouldn't be here") 20 | } 21 | } else { 22 | XCTFail("Shouldn't be here") 23 | } 24 | } 25 | 26 | client = PodcastAPI.Client(apiKey: "fake api", synchronousRequest: true) 27 | client.search(parameters: parameters) { response in 28 | // Current base url 29 | if let url = response.request?.url?.absoluteString { 30 | XCTAssertTrue(url.contains(BASE_URL_PROD)) 31 | } else { 32 | XCTFail("Shouldn't be here") 33 | } 34 | 35 | // Correct error 36 | if let error = response.error { 37 | XCTAssertEqual(error, PodcastApiError.authenticationError) 38 | } else { 39 | XCTFail("Shouldn't be here") 40 | } 41 | } 42 | } 43 | 44 | func testSearch() { 45 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 46 | var parameters: [String: String] = [:] 47 | parameters["q"] = "startup" 48 | parameters["sort_by_date"] = "1" 49 | client.search(parameters: parameters) { response in 50 | // No error 51 | if let _ = response.error { 52 | XCTFail("Shouldn't be here") 53 | } 54 | 55 | XCTAssertEqual(response.request?.httpMethod!, "GET") 56 | 57 | // Correct query strings 58 | if let url = response.request?.url?.absoluteString { 59 | XCTAssertTrue(url.contains("/api/v2/search")) 60 | XCTAssertTrue(url.contains("q=startup")) 61 | XCTAssertTrue(url.contains("sort_by_date=1")) 62 | } else { 63 | XCTFail("Shouldn't be here") 64 | } 65 | 66 | // Correct response 67 | if let json = response.toJson() { 68 | XCTAssertTrue(json["total"] > 0) 69 | } else { 70 | XCTFail("Shouldn't be here") 71 | } 72 | } 73 | } 74 | 75 | func testSearchEpisodeTitles() { 76 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 77 | var parameters: [String: String] = [:] 78 | parameters["q"] = "startup222" 79 | parameters["podcast_id"] = "12334" 80 | client.searchEpisodeTitles(parameters: parameters) { response in 81 | // No error 82 | if let _ = response.error { 83 | XCTFail("Shouldn't be here") 84 | } 85 | 86 | XCTAssertEqual(response.request?.httpMethod!, "GET") 87 | 88 | // Correct query strings 89 | if let url = response.request?.url?.absoluteString { 90 | XCTAssertTrue(url.contains("/api/v2/search_episode_titles")) 91 | XCTAssertTrue(url.contains("q=startup222")) 92 | XCTAssertTrue(url.contains("podcast_id=12334")) 93 | } else { 94 | XCTFail("Shouldn't be here") 95 | } 96 | 97 | // Correct response 98 | if let json = response.toJson() { 99 | XCTAssertTrue(json["total"] > 0) 100 | } else { 101 | XCTFail("Shouldn't be here") 102 | } 103 | } 104 | } 105 | 106 | func testTypeahead() { 107 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 108 | var parameters: [String: String] = [:] 109 | parameters["q"] = "startup" 110 | parameters["show_podcasts"] = "1" 111 | client.typeahead(parameters: parameters) { response in 112 | // No error 113 | if let _ = response.error { 114 | XCTFail("Shouldn't be here") 115 | } 116 | XCTAssertEqual(response.request?.httpMethod!, "GET") 117 | // Correct query strings 118 | if let url = response.request?.url?.absoluteString { 119 | XCTAssertTrue(url.contains("/api/v2/typeahead")) 120 | XCTAssertTrue(url.contains("q=startup")) 121 | XCTAssertTrue(url.contains("show_podcasts=1")) 122 | } else { 123 | XCTFail("Shouldn't be here") 124 | } 125 | 126 | // Correct response 127 | if let json = response.toJson() { 128 | XCTAssertTrue(json["terms"].count > 0) 129 | } else { 130 | XCTFail("Shouldn't be here") 131 | } 132 | } 133 | } 134 | 135 | func testSpellcheck() { 136 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 137 | var parameters: [String: String] = [:] 138 | parameters["q"] = "startup" 139 | client.spellcheck(parameters: parameters) { response in 140 | // No error 141 | if let _ = response.error { 142 | XCTFail("Shouldn't be here") 143 | } 144 | XCTAssertEqual(response.request?.httpMethod!, "GET") 145 | // Correct query strings 146 | if let url = response.request?.url?.absoluteString { 147 | XCTAssertTrue(url.contains("/api/v2/spellcheck")) 148 | XCTAssertTrue(url.contains("q=startup")) 149 | } else { 150 | XCTFail("Shouldn't be here") 151 | } 152 | 153 | // Correct response 154 | if let json = response.toJson() { 155 | XCTAssertTrue(json["tokens"].count > 0) 156 | } else { 157 | XCTFail("Shouldn't be here") 158 | } 159 | } 160 | } 161 | 162 | func testFetchRelatedSearches() { 163 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 164 | var parameters: [String: String] = [:] 165 | parameters["q"] = "startup" 166 | client.fetchRelatedSearches(parameters: parameters) { response in 167 | // No error 168 | if let _ = response.error { 169 | XCTFail("Shouldn't be here") 170 | } 171 | XCTAssertEqual(response.request?.httpMethod!, "GET") 172 | // Correct query strings 173 | if let url = response.request?.url?.absoluteString { 174 | XCTAssertTrue(url.contains("/api/v2/related_searches")) 175 | XCTAssertTrue(url.contains("q=startup")) 176 | } else { 177 | XCTFail("Shouldn't be here") 178 | } 179 | 180 | // Correct response 181 | if let json = response.toJson() { 182 | XCTAssertTrue(json["terms"].count > 0) 183 | } else { 184 | XCTFail("Shouldn't be here") 185 | } 186 | } 187 | } 188 | 189 | func testFetchTrendingSearches() { 190 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 191 | let parameters: [String: String] = [:] 192 | client.fetchTrendingSearches(parameters: parameters) { response in 193 | // No error 194 | if let _ = response.error { 195 | XCTFail("Shouldn't be here") 196 | } 197 | XCTAssertEqual(response.request?.httpMethod!, "GET") 198 | // Correct query strings 199 | if let url = response.request?.url?.absoluteString { 200 | XCTAssertTrue(url.contains("/api/v2/trending_searches")) 201 | } else { 202 | XCTFail("Shouldn't be here") 203 | } 204 | 205 | // Correct response 206 | if let json = response.toJson() { 207 | XCTAssertTrue(json["terms"].count > 0) 208 | } else { 209 | XCTFail("Shouldn't be here") 210 | } 211 | } 212 | } 213 | 214 | func testFetchBestPodcasts() { 215 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 216 | var parameters: [String: String] = [:] 217 | parameters["genre_id"] = "23" 218 | client.fetchBestPodcasts(parameters: parameters) { response in 219 | // No error 220 | if let _ = response.error { 221 | XCTFail("Shouldn't be here") 222 | } 223 | XCTAssertEqual(response.request?.httpMethod!, "GET") 224 | // Correct query strings 225 | if let url = response.request?.url?.absoluteString { 226 | XCTAssertTrue(url.contains("/api/v2/best_podcast")) 227 | XCTAssertTrue(url.contains("genre_id=23")) 228 | } else { 229 | XCTFail("Shouldn't be here") 230 | } 231 | 232 | // Correct response 233 | if let json = response.toJson() { 234 | XCTAssertTrue(json["total"] > 0) 235 | } else { 236 | XCTFail("Shouldn't be here") 237 | } 238 | } 239 | } 240 | 241 | func testPodcastById() { 242 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 243 | var parameters: [String: String] = [:] 244 | let id = "fake_id" 245 | parameters["id"] = id 246 | parameters["next_episode_pub_date"] = "1479154463000" 247 | client.fetchPodcastById(parameters: parameters) { response in 248 | // No error 249 | if let _ = response.error { 250 | XCTFail("Shouldn't be here") 251 | } 252 | XCTAssertEqual(response.request?.httpMethod!, "GET") 253 | // Correct query strings 254 | if let url = response.request?.url?.absoluteString { 255 | XCTAssertTrue(url.contains("/api/v2/podcasts/\(id)")) 256 | XCTAssertTrue(url.contains("next_episode_pub_date=1479154463000")) 257 | XCTAssertFalse(url.contains("id=\(id)")) 258 | } else { 259 | XCTFail("Shouldn't be here") 260 | } 261 | 262 | // Correct response 263 | if let json = response.toJson() { 264 | XCTAssertTrue(json["episodes"].count > 0) 265 | } else { 266 | XCTFail("Shouldn't be here") 267 | } 268 | } 269 | } 270 | 271 | func testEpisodeById() { 272 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 273 | var parameters: [String: String] = [:] 274 | let id = "fake_id" 275 | parameters["id"] = id 276 | parameters["show_transcript"] = "1" 277 | client.fetchEpisodeById(parameters: parameters) { response in 278 | // No error 279 | if let _ = response.error { 280 | XCTFail("Shouldn't be here") 281 | } 282 | XCTAssertEqual(response.request?.httpMethod!, "GET") 283 | // Correct query strings 284 | if let url = response.request?.url?.absoluteString { 285 | XCTAssertTrue(url.contains("/api/v2/episodes/\(id)")) 286 | XCTAssertTrue(url.contains("show_transcript=1")) 287 | XCTAssertFalse(url.contains("id=\(id)")) 288 | } else { 289 | XCTFail("Shouldn't be here") 290 | } 291 | 292 | // Correct response 293 | if let json = response.toJson() { 294 | XCTAssertTrue(json["podcast"]["rss"].stringValue.count > 0) 295 | } else { 296 | XCTFail("Shouldn't be here") 297 | } 298 | } 299 | } 300 | 301 | func testFetchCuratedPodcastsListById() { 302 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 303 | var parameters: [String: String] = [:] 304 | let id = "fake_id" 305 | parameters["id"] = id 306 | client.fetchCuratedPodcastsListById(parameters: parameters) { response in 307 | // No error 308 | if let _ = response.error { 309 | XCTFail("Shouldn't be here") 310 | } 311 | XCTAssertEqual(response.request?.httpMethod!, "GET") 312 | // Correct query strings 313 | if let url = response.request?.url?.absoluteString { 314 | XCTAssertTrue(url.contains("/api/v2/curated_podcasts/\(id)")) 315 | XCTAssertFalse(url.contains("id=\(id)")) 316 | } else { 317 | XCTFail("Shouldn't be here") 318 | } 319 | 320 | // Correct response 321 | if let json = response.toJson() { 322 | XCTAssertTrue(json["podcasts"].count > 0) 323 | } else { 324 | XCTFail("Shouldn't be here") 325 | } 326 | } 327 | } 328 | 329 | func testFetchCuratedPodcastsLists() { 330 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 331 | var parameters: [String: String] = [:] 332 | parameters["page"] = "2" 333 | client.fetchCuratedPodcastsLists(parameters: parameters) { response in 334 | // No error 335 | if let _ = response.error { 336 | XCTFail("Shouldn't be here") 337 | } 338 | XCTAssertEqual(response.request?.httpMethod!, "GET") 339 | // Correct query strings 340 | if let url = response.request?.url?.absoluteString { 341 | XCTAssertTrue(url.contains("/api/v2/curated_podcasts")) 342 | XCTAssertTrue(url.contains("page=2")) 343 | } else { 344 | XCTFail("Shouldn't be here") 345 | } 346 | 347 | // Correct response 348 | if let json = response.toJson() { 349 | XCTAssertTrue(json["total"] > 0) 350 | } else { 351 | XCTFail("Shouldn't be here") 352 | } 353 | } 354 | } 355 | 356 | func testFetchPodcastGenres() { 357 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 358 | var parameters: [String: String] = [:] 359 | parameters["top_level_only"] = "1" 360 | client.fetchPodcastGenres(parameters: parameters) { response in 361 | // No error 362 | if let _ = response.error { 363 | XCTFail("Shouldn't be here") 364 | } 365 | XCTAssertEqual(response.request?.httpMethod!, "GET") 366 | // Correct query strings 367 | if let url = response.request?.url?.absoluteString { 368 | XCTAssertTrue(url.contains("/api/v2/genres")) 369 | XCTAssertTrue(url.contains("top_level_only=1")) 370 | } else { 371 | XCTFail("Shouldn't be here") 372 | } 373 | 374 | // Correct response 375 | if let json = response.toJson() { 376 | XCTAssertTrue(json["genres"].count > 0) 377 | } else { 378 | XCTFail("Shouldn't be here") 379 | } 380 | } 381 | } 382 | 383 | func testFetchPodcastRegions() { 384 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 385 | let parameters: [String: String] = [:] 386 | client.fetchPodcastRegions(parameters: parameters) { response in 387 | // No error 388 | if let _ = response.error { 389 | XCTFail("Shouldn't be here") 390 | } 391 | XCTAssertEqual(response.request?.httpMethod!, "GET") 392 | // Correct query strings 393 | if let url = response.request?.url?.absoluteString { 394 | XCTAssertTrue(url.contains("/api/v2/regions")) 395 | } else { 396 | XCTFail("Shouldn't be here") 397 | } 398 | 399 | // Correct response 400 | if let json = response.toJson() { 401 | XCTAssertTrue(json["regions"].count > 0) 402 | } else { 403 | XCTFail("Shouldn't be here") 404 | } 405 | } 406 | } 407 | 408 | func testFetchPodcastLanguages() { 409 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 410 | let parameters: [String: String] = [:] 411 | client.fetchPodcastLanguages(parameters: parameters) { response in 412 | // No error 413 | if let _ = response.error { 414 | XCTFail("Shouldn't be here") 415 | } 416 | XCTAssertEqual(response.request?.httpMethod!, "GET") 417 | // Correct query strings 418 | if let url = response.request?.url?.absoluteString { 419 | XCTAssertTrue(url.contains("/api/v2/languages")) 420 | } else { 421 | XCTFail("Shouldn't be here") 422 | } 423 | 424 | // Correct response 425 | if let json = response.toJson() { 426 | XCTAssertTrue(json["languages"].count > 0) 427 | } else { 428 | XCTFail("Shouldn't be here") 429 | } 430 | } 431 | } 432 | 433 | func testJustListen() { 434 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 435 | let parameters: [String: String] = [:] 436 | client.justListen(parameters: parameters) { response in 437 | // No error 438 | if let _ = response.error { 439 | XCTFail("Shouldn't be here") 440 | } 441 | XCTAssertEqual(response.request?.httpMethod!, "GET") 442 | // Correct query strings 443 | if let url = response.request?.url?.absoluteString { 444 | XCTAssertTrue(url.contains("/api/v2/just_listen")) 445 | } else { 446 | XCTFail("Shouldn't be here") 447 | } 448 | 449 | // Correct response 450 | if let json = response.toJson() { 451 | XCTAssertTrue(json["audio_length_sec"] > 0) 452 | } else { 453 | XCTFail("Shouldn't be here") 454 | } 455 | } 456 | } 457 | 458 | func testFetchRecommendationsForPodcast() { 459 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 460 | var parameters: [String: String] = [:] 461 | let id = "fake_id" 462 | parameters["id"] = id 463 | parameters["safe_mode"] = "0" 464 | client.fetchRecommendationsForPodcast(parameters: parameters) { response in 465 | // No error 466 | if let _ = response.error { 467 | XCTFail("Shouldn't be here") 468 | } 469 | XCTAssertEqual(response.request?.httpMethod!, "GET") 470 | // Correct query strings 471 | if let url = response.request?.url?.absoluteString { 472 | XCTAssertTrue(url.contains("/api/v2/podcasts/\(id)/recommendations")) 473 | XCTAssertTrue(url.contains("safe_mode=0")) 474 | XCTAssertFalse(url.contains("id=\(id)")) 475 | } else { 476 | XCTFail("Shouldn't be here") 477 | } 478 | 479 | // Correct response 480 | if let json = response.toJson() { 481 | XCTAssertTrue(json["recommendations"].count > 0) 482 | } else { 483 | XCTFail("Shouldn't be here") 484 | } 485 | } 486 | } 487 | 488 | func testFetchRecommendationsForEpisode() { 489 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 490 | var parameters: [String: String] = [:] 491 | let id = "fake_id" 492 | parameters["id"] = id 493 | parameters["safe_mode"] = "0" 494 | client.fetchRecommendationsForEpisode(parameters: parameters) { response in 495 | // No error 496 | if let _ = response.error { 497 | XCTFail("Shouldn't be here") 498 | } 499 | XCTAssertEqual(response.request?.httpMethod!, "GET") 500 | // Correct query strings 501 | if let url = response.request?.url?.absoluteString { 502 | XCTAssertTrue(url.contains("/api/v2/episodes/\(id)/recommendations")) 503 | XCTAssertTrue(url.contains("safe_mode=0")) 504 | XCTAssertFalse(url.contains("id=\(id)")) 505 | } else { 506 | XCTFail("Shouldn't be here") 507 | } 508 | 509 | // Correct response 510 | if let json = response.toJson() { 511 | XCTAssertTrue(json["recommendations"].count > 0) 512 | } else { 513 | XCTFail("Shouldn't be here") 514 | } 515 | } 516 | } 517 | 518 | func testFetchPlaylistById() { 519 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 520 | var parameters: [String: String] = [:] 521 | let id = "fake_id" 522 | parameters["id"] = id 523 | parameters["sort"] = "recent_added_first" 524 | client.fetchPlaylistById(parameters: parameters) { response in 525 | // No error 526 | if let _ = response.error { 527 | XCTFail("Shouldn't be here") 528 | } 529 | XCTAssertEqual(response.request?.httpMethod!, "GET") 530 | // Correct query strings 531 | if let url = response.request?.url?.absoluteString { 532 | XCTAssertTrue(url.contains("/api/v2/playlists/\(id)")) 533 | XCTAssertTrue(url.contains("sort=recent_added_first")) 534 | XCTAssertFalse(url.contains("id=\(id)")) 535 | } else { 536 | XCTFail("Shouldn't be here") 537 | } 538 | 539 | // Correct response 540 | if let json = response.toJson() { 541 | XCTAssertTrue(json["items"].count > 0) 542 | } else { 543 | XCTFail("Shouldn't be here") 544 | } 545 | } 546 | } 547 | 548 | func testFetchMyPlaylists() { 549 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 550 | var parameters: [String: String] = [:] 551 | parameters["page"] = "2" 552 | client.fetchMyPlaylists(parameters: parameters) { response in 553 | // No error 554 | if let _ = response.error { 555 | XCTFail("Shouldn't be here") 556 | } 557 | XCTAssertEqual(response.request?.httpMethod!, "GET") 558 | // Correct query strings 559 | if let url = response.request?.url?.absoluteString { 560 | XCTAssertTrue(url.contains("/api/v2/playlists")) 561 | XCTAssertTrue(url.contains("page=2")) 562 | } else { 563 | XCTFail("Shouldn't be here") 564 | } 565 | 566 | // Correct response 567 | if let json = response.toJson() { 568 | XCTAssertTrue(json["playlists"].count > 0) 569 | } else { 570 | XCTFail("Shouldn't be here") 571 | } 572 | } 573 | } 574 | 575 | func testBatchFetchPodcasts() { 576 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 577 | var parameters: [String: String] = [:] 578 | let ids = "2,2342,232,2442,232" 579 | parameters["ids"] = ids 580 | client.batchFetchPodcasts(parameters: parameters) { response in 581 | // No error 582 | if let _ = response.error { 583 | XCTFail("Shouldn't be here") 584 | } 585 | XCTAssertEqual(response.request?.httpMethod!, "POST") 586 | // Correct query strings 587 | if let url = response.request?.url?.absoluteString { 588 | XCTAssertTrue(url.contains("/api/v2/podcasts")) 589 | } else { 590 | XCTFail("Shouldn't be here") 591 | } 592 | 593 | if let data = response.request?.httpBody { 594 | if let postData = String(data: data, encoding: .utf8) { 595 | XCTAssertEqual(postData, "ids=\(ids)") 596 | } else { 597 | XCTFail("Shouldn't be here") 598 | } 599 | } else { 600 | XCTFail("Shouldn't be here") 601 | } 602 | 603 | // Correct response 604 | if let json = response.toJson() { 605 | XCTAssertTrue(json["podcasts"].count > 0) 606 | } else { 607 | XCTFail("Shouldn't be here") 608 | } 609 | } 610 | } 611 | 612 | func testBatchFetchEpisodes() { 613 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 614 | var parameters: [String: String] = [:] 615 | let ids = "2,2342,232,2442,232" 616 | parameters["ids"] = ids 617 | client.batchFetchEpisodes(parameters: parameters) { response in 618 | // No error 619 | if let _ = response.error { 620 | XCTFail("Shouldn't be here") 621 | } 622 | XCTAssertEqual(response.request?.httpMethod!, "POST") 623 | // Correct query strings 624 | if let url = response.request?.url?.absoluteString { 625 | XCTAssertTrue(url.contains("/api/v2/episodes")) 626 | } else { 627 | XCTFail("Shouldn't be here") 628 | } 629 | 630 | if let data = response.request?.httpBody { 631 | if let postData = String(data: data, encoding: .utf8) { 632 | XCTAssertEqual(postData, "ids=\(ids)") 633 | } else { 634 | XCTFail("Shouldn't be here") 635 | } 636 | } else { 637 | XCTFail("Shouldn't be here") 638 | } 639 | 640 | // Correct response 641 | if let json = response.toJson() { 642 | XCTAssertTrue(json["episodes"].count > 0) 643 | } else { 644 | XCTFail("Shouldn't be here") 645 | } 646 | } 647 | } 648 | 649 | func testDeletePodcast() { 650 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 651 | var parameters: [String: String] = [:] 652 | let id = "fake_id" 653 | parameters["id"] = id 654 | let reason = "asdfasfs" 655 | parameters["reason"] = reason 656 | client.deletePodcast(parameters: parameters) { response in 657 | // No error 658 | if let _ = response.error { 659 | XCTFail("Shouldn't be here") 660 | } 661 | XCTAssertEqual(response.request?.httpMethod!, "DELETE") 662 | // Correct query strings 663 | if let url = response.request?.url?.absoluteString { 664 | XCTAssertTrue(url.contains("/api/v2/podcasts/\(id)")) 665 | XCTAssertTrue(url.contains("reason=\(reason)")) 666 | XCTAssertFalse(url.contains("id=\(id)")) 667 | } else { 668 | XCTFail("Shouldn't be here") 669 | } 670 | 671 | // Correct response 672 | if let json = response.toJson() { 673 | XCTAssertTrue(json["status"].stringValue.count > 0) 674 | } else { 675 | XCTFail("Shouldn't be here") 676 | } 677 | } 678 | } 679 | 680 | func testSubmitPodcast() { 681 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 682 | var parameters: [String: String] = [:] 683 | let rss = "http://myrss.com/rss" 684 | parameters["rss"] = rss 685 | client.submitPodcast(parameters: parameters) { response in 686 | // No error 687 | if let _ = response.error { 688 | XCTFail("Shouldn't be here") 689 | } 690 | XCTAssertEqual(response.request?.httpMethod!, "POST") 691 | // Correct query strings 692 | if let url = response.request?.url?.absoluteString { 693 | XCTAssertTrue(url.contains("/api/v2/podcasts/submit")) 694 | } else { 695 | XCTFail("Shouldn't be here") 696 | } 697 | 698 | if let data = response.request?.httpBody { 699 | if let postData = String(data: data, encoding: .utf8) { 700 | XCTAssertEqual(postData, "rss=\(rss)") 701 | } else { 702 | XCTFail("Shouldn't be here") 703 | } 704 | } else { 705 | XCTFail("Shouldn't be here") 706 | } 707 | 708 | // Correct response 709 | if let json = response.toJson() { 710 | XCTAssertTrue(json["status"].stringValue.count > 0) 711 | } else { 712 | XCTFail("Shouldn't be here") 713 | } 714 | } 715 | } 716 | 717 | func testFetchAudienceForPodcast() { 718 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 719 | var parameters: [String: String] = [:] 720 | let id = "fake_id" 721 | parameters["id"] = id 722 | client.fetchAudienceForPodcast(parameters: parameters) { response in 723 | // No error 724 | if let _ = response.error { 725 | XCTFail("Shouldn't be here") 726 | } 727 | XCTAssertEqual(response.request?.httpMethod!, "GET") 728 | // Correct query strings 729 | if let url = response.request?.url?.absoluteString { 730 | XCTAssertTrue(url.contains("/api/v2/podcasts/\(id)/audience")) 731 | XCTAssertFalse(url.contains("id=\(id)")) 732 | } else { 733 | XCTFail("Shouldn't be here") 734 | } 735 | 736 | // Correct response 737 | if let json = response.toJson() { 738 | XCTAssertTrue(json["by_regions"].count > 0) 739 | } else { 740 | XCTFail("Shouldn't be here") 741 | } 742 | } 743 | } 744 | 745 | func testFetchPodcastsByDomain() { 746 | let client = PodcastAPI.Client(apiKey: "", synchronousRequest: true) 747 | var parameters: [String: String] = [:] 748 | let domain_name = "npr.org" 749 | parameters["domain_name"] = domain_name 750 | parameters["page"] = "4" 751 | client.fetchPodcastsByDomain(parameters: parameters) { response in 752 | // No error 753 | if let _ = response.error { 754 | XCTFail("Shouldn't be here") 755 | } 756 | XCTAssertEqual(response.request?.httpMethod!, "GET") 757 | // Correct query strings 758 | if let url = response.request?.url?.absoluteString { 759 | XCTAssertTrue(url.contains("/api/v2/podcasts/domains/\(domain_name)")) 760 | XCTAssertFalse(url.contains("domain_name=\(domain_name)")) 761 | } else { 762 | XCTFail("Shouldn't be here") 763 | } 764 | 765 | // Correct response 766 | if let json = response.toJson() { 767 | XCTAssertTrue(json["podcasts"].count > 0) 768 | } else { 769 | XCTFail("Shouldn't be here") 770 | } 771 | } 772 | } 773 | } 774 | --------------------------------------------------------------------------------