From b80152ba13c8a2ba7841663682b6c5da861b2efa Mon Sep 17 00:00:00 2001 From: aj Date: Tue, 18 Feb 2020 21:43:30 +0000 Subject: [PATCH] working model for network layer, added user object --- Music Tools.xcodeproj/project.pbxproj | 50 ++++++++ .../xcshareddata/swiftpm/Package.resolved | 18 +++ Music Tools/Application/Main.storyboard | 7 ++ Music Tools/Application/SceneDelegate.swift | 5 + Music Tools/Model/Playlist.swift | 16 ++- Music Tools/Model/User.swift | 58 ++++++++++ Music Tools/Network/Network.swift | 108 ++++++++---------- Music Tools/Network/PlaylistApi.swift | 54 +++++++++ Music Tools/Network/UserApi.swift | 53 +++++++++ Music Tools/Views/PlaylistRow.swift | 8 +- Music Tools/Views/PlaylistView.swift | 30 +++-- Music Tools/Views/RootView.swift | 29 +++-- 12 files changed, 351 insertions(+), 85 deletions(-) create mode 100644 Music Tools/Application/Main.storyboard create mode 100644 Music Tools/Model/User.swift create mode 100644 Music Tools/Network/PlaylistApi.swift create mode 100644 Music Tools/Network/UserApi.swift diff --git a/Music Tools.xcodeproj/project.pbxproj b/Music Tools.xcodeproj/project.pbxproj index 41c000a..20236e6 100644 --- a/Music Tools.xcodeproj/project.pbxproj +++ b/Music Tools.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + E97AF45623FC4E7800635494 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97AF45523FC4E7800635494 /* User.swift */; }; + E97AF45923FC50EC00635494 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = E97AF45823FC50EC00635494 /* SwiftyJSON */; }; + E97AF45B23FC748D00635494 /* UserApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97AF45A23FC748D00635494 /* UserApi.swift */; }; + E97AF45E23FC83AF00635494 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = E97AF45D23FC83AF00635494 /* KeychainAccess */; }; + E97AF46023FC85D600635494 /* PlaylistApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97AF45F23FC85D600635494 /* PlaylistApi.swift */; }; + E97AF46223FC89CC00635494 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E97AF46123FC89CB00635494 /* Main.storyboard */; }; E98254BD23F9B7A90056D9D3 /* Playlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98254BC23F9B7A90056D9D3 /* Playlist.swift */; }; E98254C223F9FFF90056D9D3 /* PlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98254C123F9FFF90056D9D3 /* PlaylistView.swift */; }; E98254C823FA25D20056D9D3 /* PlaylistList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98254C723FA25D20056D9D3 /* PlaylistList.swift */; }; @@ -42,6 +48,10 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + E97AF45523FC4E7800635494 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + E97AF45A23FC748D00635494 /* UserApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserApi.swift; sourceTree = ""; }; + E97AF45F23FC85D600635494 /* PlaylistApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistApi.swift; sourceTree = ""; }; + E97AF46123FC89CB00635494 /* Main.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; E98254BC23F9B7A90056D9D3 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = ""; }; E98254C123F9FFF90056D9D3 /* PlaylistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistView.swift; sourceTree = ""; }; E98254C723FA25D20056D9D3 /* PlaylistList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistList.swift; sourceTree = ""; }; @@ -69,7 +79,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E97AF45923FC50EC00635494 /* SwiftyJSON in Frameworks */, E98254D923FB53780056D9D3 /* Alamofire in Frameworks */, + E97AF45E23FC83AF00635494 /* KeychainAccess in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -94,6 +106,7 @@ isa = PBXGroup; children = ( E98254BC23F9B7A90056D9D3 /* Playlist.swift */, + E97AF45523FC4E7800635494 /* User.swift */, ); path = Model; sourceTree = ""; @@ -102,6 +115,8 @@ isa = PBXGroup; children = ( E98254DA23FB64740056D9D3 /* Network.swift */, + E97AF45A23FC748D00635494 /* UserApi.swift */, + E97AF45F23FC85D600635494 /* PlaylistApi.swift */, ); path = Network; sourceTree = ""; @@ -124,6 +139,7 @@ E9EA691523F9A54B0012C3E8 /* LaunchScreen.storyboard */, E9EA690A23F9A5430012C3E8 /* AppDelegate.swift */, E9EA690C23F9A5430012C3E8 /* SceneDelegate.swift */, + E97AF46123FC89CB00635494 /* Main.storyboard */, ); path = Application; sourceTree = ""; @@ -206,6 +222,8 @@ name = "Music Tools"; packageProductDependencies = ( E98254D823FB53780056D9D3 /* Alamofire */, + E97AF45823FC50EC00635494 /* SwiftyJSON */, + E97AF45D23FC83AF00635494 /* KeychainAccess */, ); productName = "Music Tools"; productReference = E9EA690723F9A5430012C3E8 /* Music Tools.app */; @@ -281,6 +299,8 @@ mainGroup = E9EA68FE23F9A5430012C3E8; packageReferences = ( E98254D723FB53770056D9D3 /* XCRemoteSwiftPackageReference "alamofire" */, + E97AF45723FC50EC00635494 /* XCRemoteSwiftPackageReference "swiftyjson" */, + E97AF45C23FC83AF00635494 /* XCRemoteSwiftPackageReference "keychainaccess" */, ); productRefGroup = E9EA690823F9A5430012C3E8 /* Products */; projectDirPath = ""; @@ -301,6 +321,7 @@ E9EA691723F9A54B0012C3E8 /* LaunchScreen.storyboard in Resources */, E9EA691423F9A54B0012C3E8 /* Preview Assets.xcassets in Resources */, E9EA691123F9A54A0012C3E8 /* Assets.xcassets in Resources */, + E97AF46223FC89CC00635494 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -329,11 +350,14 @@ E9EA690B23F9A5430012C3E8 /* AppDelegate.swift in Sources */, E9EA690D23F9A5430012C3E8 /* SceneDelegate.swift in Sources */, E98254DB23FB64740056D9D3 /* Network.swift in Sources */, + E97AF46023FC85D600635494 /* PlaylistApi.swift in Sources */, E98254C823FA25D20056D9D3 /* PlaylistList.swift in Sources */, E9EA690F23F9A5430012C3E8 /* RootView.swift in Sources */, E98254BD23F9B7A90056D9D3 /* Playlist.swift in Sources */, E98254C223F9FFF90056D9D3 /* PlaylistView.swift in Sources */, + E97AF45623FC4E7800635494 /* User.swift in Sources */, E98254D023FB00B60056D9D3 /* LoginScreen.swift in Sources */, + E97AF45B23FC748D00635494 /* UserApi.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -660,6 +684,22 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + E97AF45723FC50EC00635494 /* XCRemoteSwiftPackageReference "swiftyjson" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/swiftyjson/swiftyjson"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.0.0; + }; + }; + E97AF45C23FC83AF00635494 /* XCRemoteSwiftPackageReference "keychainaccess" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kishikawakatsumi/keychainaccess"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.1.0; + }; + }; E98254D723FB53770056D9D3 /* XCRemoteSwiftPackageReference "alamofire" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/alamofire/alamofire.git"; @@ -671,6 +711,16 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + E97AF45823FC50EC00635494 /* SwiftyJSON */ = { + isa = XCSwiftPackageProductDependency; + package = E97AF45723FC50EC00635494 /* XCRemoteSwiftPackageReference "swiftyjson" */; + productName = SwiftyJSON; + }; + E97AF45D23FC83AF00635494 /* KeychainAccess */ = { + isa = XCSwiftPackageProductDependency; + package = E97AF45C23FC83AF00635494 /* XCRemoteSwiftPackageReference "keychainaccess" */; + productName = KeychainAccess; + }; E98254D823FB53780056D9D3 /* Alamofire */ = { isa = XCSwiftPackageProductDependency; package = E98254D723FB53770056D9D3 /* XCRemoteSwiftPackageReference "alamofire" */; diff --git a/Music Tools.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Music Tools.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2b10548..20b2b67 100644 --- a/Music Tools.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Music Tools.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -9,6 +9,24 @@ "revision": "0c8cb78d05b6d067ee331c05058ff4dedcb45ffa", "version": "5.0.0" } + }, + { + "package": "KeychainAccess", + "repositoryURL": "https://github.com/kishikawakatsumi/keychainaccess", + "state": { + "branch": null, + "revision": "b920ad7df3c73189dcdd4aa05c540849b2010dbf", + "version": "4.1.0" + } + }, + { + "package": "SwiftyJSON", + "repositoryURL": "https://github.com/swiftyjson/swiftyjson", + "state": { + "branch": null, + "revision": "2b6054efa051565954e1d2b9da831680026cd768", + "version": "5.0.0" + } } ] }, diff --git a/Music Tools/Application/Main.storyboard b/Music Tools/Application/Main.storyboard new file mode 100644 index 0000000..f9a048e --- /dev/null +++ b/Music Tools/Application/Main.storyboard @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Music Tools/Application/SceneDelegate.swift b/Music Tools/Application/SceneDelegate.swift index d4ce42d..ba4c2b4 100644 --- a/Music Tools/Application/SceneDelegate.swift +++ b/Music Tools/Application/SceneDelegate.swift @@ -8,6 +8,7 @@ import UIKit import SwiftUI +import KeychainAccess class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -21,6 +22,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Create the SwiftUI view that provides the window contents. let contentView = RootView() + + let keychain = Keychain(service: "xyz.sarsoo.music.login") + keychain["username"] = "" + keychain["password"] = "" // Use a UIHostingController as window root view controller. if let windowScene = scene as? UIWindowScene { diff --git a/Music Tools/Model/Playlist.swift b/Music Tools/Model/Playlist.swift index 014cf4c..1c97684 100644 --- a/Music Tools/Model/Playlist.swift +++ b/Music Tools/Model/Playlist.swift @@ -7,7 +7,7 @@ // import UIKit - +import SwiftyJSON class Playlist: Identifiable { @@ -55,5 +55,19 @@ class Playlist: Identifiable { self.shuffle = shuffle } + static func fromDict(dictionary: JSON) -> Playlist { + return Playlist(name: dictionary["name"].stringValue, + uri: dictionary["uri"].stringValue, + username: dictionary["username"].stringValue, + + include_recommendations: dictionary["include_recommendations"].boolValue, + recommendation_sample: dictionary["recommendation_sample"].intValue, + include_library_tracks: dictionary["include_library_tracks"].boolValue, + + parts: dictionary["parts"].arrayObject as! Array, + playlist_references: dictionary["playlist_references"].arrayObject as! Array, + + shuffle: dictionary["shuffle"].boolValue) + } } diff --git a/Music Tools/Model/User.swift b/Music Tools/Model/User.swift new file mode 100644 index 0000000..967e9dc --- /dev/null +++ b/Music Tools/Model/User.swift @@ -0,0 +1,58 @@ +// +// User.swift +// Music Tools +// +// Created by Andy Pack on 18/02/2020. +// Copyright © 2020 Sarsoo. All rights reserved. +// + +import UIKit +import SwiftyJSON + +enum UserType: String { + case user = "user" + case admin = "admin" +} + +class User: Identifiable { + + //MARK: Properties + + var username: String + var email: String? + var type: UserType + + var last_login: String + var spotify_linked: Bool + var lastfm_username: String? + + //MARK: Initialization + + init(username: String, + email: String?, + type: UserType = .user, + + last_login: String, + spotify_linked: Bool, + lastfm_username: String?){ + + self.username = username + self.email = email + self.type = type + + self.last_login = last_login + self.spotify_linked = spotify_linked + self.lastfm_username = lastfm_username + } + + static func fromDict(dictionary: JSON) -> User { + return User(username: dictionary["username"].stringValue, + email: dictionary["username"].stringValue, + type: UserType(rawValue: dictionary["type"].stringValue) ?? .user, + last_login: dictionary["last_login"].stringValue, + spotify_linked: dictionary["spotify_linked"].boolValue, + lastfm_username: dictionary["lastfm_username"].stringValue) + } + +} + diff --git a/Music Tools/Network/Network.swift b/Music Tools/Network/Network.swift index 8ae40f8..9e90cd7 100644 --- a/Music Tools/Network/Network.swift +++ b/Music Tools/Network/Network.swift @@ -8,73 +8,63 @@ import Foundation import Alamofire +import SwiftyJSON +import KeychainAccess -class MusicToolsNetwork { +public enum AuthMethod { + case basic - var baseBath: String = "https://music.sarsoo.xyz/" - - public func request(path: String, - method: Alamofire.HTTPMethod, - parameters: [String:String]? , - encoder: Alamofire.ParameterEncoder?, - headers: Alamofire.HTTPHeaders? ) { + func auth(headers: Alamofire.HTTPHeaders?) -> Alamofire.HTTPHeaders { + switch self { + case .basic: + var txHeaders = headers ?? HTTPHeaders() + + let keychain = Keychain(service: "xyz.sarsoo.music.login") + txHeaders.add(.authorization(username: keychain["username"] ?? "", password: keychain["password"] ?? "")) + return txHeaders + } + } +} + +struct RequestBuilder { + static func buildRequest(apiRequest: ApiRequest) -> Alamofire.DataRequest { - guard let uwParameters = parameters else { - AF.request(baseBath + path, - method: method, - headers: headers ).validate().response { response in - debugPrint(response) + let txHeaders = apiRequest.authMethod?.auth(headers: apiRequest.headers) + + if apiRequest.parameters != nil { + if apiRequest.parameterType != nil { + + let txEncoder = apiRequest.parameterType ?? JSONParameterEncoder.default + + return AF.request(apiRequest.domain + apiRequest.path, + method: apiRequest.httpMethod, + parameters: apiRequest.parameters, + encoder: txEncoder, + headers: txHeaders) + } else { + return AF.request(apiRequest.domain + apiRequest.path, + method: apiRequest.httpMethod, + parameters: apiRequest.parameters, + headers: txHeaders) } - return } - - AF.request(baseBath + path, - method: method, - parameters: uwParameters, - headers: headers ).response { response in - debugPrint(response) - } - + return AF.request(apiRequest.domain + apiRequest.path, + method: apiRequest.httpMethod, + headers: txHeaders) } } -class BasicAuthNetwork: MusicToolsNetwork { - var username: String - var password: String - - init(username: String, password: String) { - self.username = username - self.password = password - } - - func getHeader() -> String { - return "\(username):\(password)".toBase64() - } - - public func authedRequest(path: String, - method: Alamofire.HTTPMethod, - parameters: [String:String]?, - encoder: Alamofire.ParameterEncoder?, - headers: Alamofire.HTTPHeaders? ) { - - let encoded = "\(username):\(password)".toBase64() - - var txHeaders = headers - - if headers == nil { - txHeaders = Alamofire.HTTPHeaders() - } - txHeaders?.add(name: "Authorization", value: "Basic \(encoded)") - - request(path: path, method: method, parameters: parameters, encoder: encoder, headers: txHeaders) - - } +struct ApiRequestDefaults { + static let authMethod: AuthMethod = .basic + static let domain: String = "https://music.sarsoo.xyz/" } -extension String { - - func toBase64() -> String { - return Data(self.utf8).base64EncodedString() - } - +protocol ApiRequest { + var domain: String { get } + var path: String { get } + var httpMethod: Alamofire.HTTPMethod { get } + var parameters: JSON? { get } + var parameterType: Alamofire.ParameterEncoder? { get } + var headers: HTTPHeaders? { get } + var authMethod: AuthMethod? { get } } diff --git a/Music Tools/Network/PlaylistApi.swift b/Music Tools/Network/PlaylistApi.swift new file mode 100644 index 0000000..283df9a --- /dev/null +++ b/Music Tools/Network/PlaylistApi.swift @@ -0,0 +1,54 @@ +// +// PlaylistApi.swift +// Music Tools +// +// Created by Andy Pack on 18/02/2020. +// Copyright © 2020 Sarsoo. All rights reserved. +// + +import Foundation +import Alamofire +import SwiftyJSON + +public enum PlaylistApi { + case getPlaylists +} + +extension PlaylistApi: ApiRequest { + var domain: String { + return ApiRequestDefaults.domain + } + + var path: String { + switch self { + case .getPlaylists: + return "api/playlists" + } + } + + var httpMethod: Alamofire.HTTPMethod { + switch self { + case .getPlaylists: + return .get + } + } + + var parameters: JSON? { + return nil + } + + var parameterType: ParameterEncoder? { + return nil + } + + var headers: HTTPHeaders? { + return nil + } + + var authMethod: AuthMethod? { + return ApiRequestDefaults.authMethod + } + + +} + diff --git a/Music Tools/Network/UserApi.swift b/Music Tools/Network/UserApi.swift new file mode 100644 index 0000000..24f9fc3 --- /dev/null +++ b/Music Tools/Network/UserApi.swift @@ -0,0 +1,53 @@ +// +// UserApi.swift +// Music Tools +// +// Created by Andy Pack on 18/02/2020. +// Copyright © 2020 Sarsoo. All rights reserved. +// + +import Foundation +import Alamofire +import SwiftyJSON + +public enum UserApi { + case getUser +} + +extension UserApi: ApiRequest { + var domain: String { + return ApiRequestDefaults.domain + } + + var path: String { + switch self { + case .getUser: + return "api/user" + } + } + + var httpMethod: Alamofire.HTTPMethod { + switch self { + case .getUser: + return .get + } + } + + var parameters: JSON? { + return nil + } + + var parameterType: ParameterEncoder? { + return nil + } + + var headers: HTTPHeaders? { + return nil + } + + var authMethod: AuthMethod? { + return ApiRequestDefaults.authMethod + } + + +} diff --git a/Music Tools/Views/PlaylistRow.swift b/Music Tools/Views/PlaylistRow.swift index 77d7e05..195519b 100644 --- a/Music Tools/Views/PlaylistRow.swift +++ b/Music Tools/Views/PlaylistRow.swift @@ -12,9 +12,11 @@ struct PlaylistRow: View { var playlist: Playlist var body: some View { - HStack { - Text(playlist.name) - Spacer() + NavigationLink(destination: PlaylistView(playlist: playlist)){ + HStack { + Text(playlist.name) + Spacer() + } } } } diff --git a/Music Tools/Views/PlaylistView.swift b/Music Tools/Views/PlaylistView.swift index 0624595..33716e1 100644 --- a/Music Tools/Views/PlaylistView.swift +++ b/Music Tools/Views/PlaylistView.swift @@ -10,6 +10,8 @@ import SwiftUI struct PlaylistView: View { var playlist: Playlist + @State private var recommendations: Bool = false + @State private var library_Tracks: Bool = false var body: some View { @@ -25,28 +27,32 @@ struct PlaylistView: View { .cornerRadius(18) .padding(.bottom, 20) - Toggle(isOn: /*@START_MENU_TOKEN@*//*@PLACEHOLDER=Value@*/.constant(true)/*@END_MENU_TOKEN@*/) { + Toggle(isOn: $recommendations) { Text("Spotify Recommendations") } - Stepper(value: /*@START_MENU_TOKEN@*//*@PLACEHOLDER=Value@*/.constant(4)/*@END_MENU_TOKEN@*/, in: /*@START_MENU_TOKEN@*//*@PLACEHOLDER=Range@*/1...10/*@END_MENU_TOKEN@*/){ - Text("#:") - .foregroundColor(Color.gray) - .multilineTextAlignment(.trailing) - .padding(.leading, 20) - Text("100") - .multilineTextAlignment(.trailing) - + if $recommendations.wrappedValue { + Stepper(value: /*@START_MENU_TOKEN@*//*@PLACEHOLDER=Value@*/.constant(4)/*@END_MENU_TOKEN@*/, in: /*@START_MENU_TOKEN@*//*@PLACEHOLDER=Range@*/1...10/*@END_MENU_TOKEN@*/){ + Text("#:") + .foregroundColor(Color.gray) + .multilineTextAlignment(.trailing) + .padding(.leading, 20) + Text("100") + .multilineTextAlignment(.trailing) + + } } - Toggle(isOn: /*@START_MENU_TOKEN@*//*@PLACEHOLDER=Value@*/.constant(true)/*@END_MENU_TOKEN@*/) { + Toggle(isOn: $library_Tracks) { Text("Library Tracks") } - - EditButton() } .padding() .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading) + .onAppear { + self.$recommendations.wrappedValue = self.playlist.include_recommendations + self.$library_Tracks.wrappedValue = self.playlist.include_library_tracks + } } } diff --git a/Music Tools/Views/RootView.swift b/Music Tools/Views/RootView.swift index e8c971a..66dd4fd 100644 --- a/Music Tools/Views/RootView.swift +++ b/Music Tools/Views/RootView.swift @@ -8,17 +8,17 @@ import SwiftUI import Alamofire +import SwiftyJSON struct RootView: View { @State private var selection = 0 + @State private var playlists: Array = [] var body: some View { TabView(selection: $selection){ NavigationView { - List(/*@START_MENU_TOKEN@*/0 ..< 5/*@END_MENU_TOKEN@*/) { item in - Text("Playlist") - .font(.title) - + List(playlists) { playlist in + PlaylistRow(playlist: playlist) } .navigationBarTitle(Text("Playlists").font(.title)) } @@ -63,12 +63,21 @@ struct RootView: View { } private func fetch() { - let net: BasicAuthNetwork = BasicAuthNetwork(username: "", password: "") - net.authedRequest(path: "api/playlist", - method: Alamofire.HTTPMethod.get, - parameters: ["name": ""], - encoder: nil, - headers: nil) + let api = PlaylistApi.getPlaylists + RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in + + guard let data = response.data else { + fatalError("error getting playlists") + } + + guard let json = try? JSON(data: data) else { + fatalError("error parsing reponse") + } + + self.playlists = json["playlists"].arrayValue.map({ dict in + Playlist.fromDict(dictionary: dict) + }).sorted(by: { $0.name.lowercased() < $1.name.lowercased() }) + } } }