implemented codable for network json decoding and userdefault storage

This commit is contained in:
aj 2020-03-06 23:36:51 +00:00
parent 43de19246e
commit d012566f04
12 changed files with 351 additions and 215 deletions

View File

@ -30,7 +30,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var controller: UIViewController
if keychain["username"] != nil && keychain["password"] != nil {
let liveUser = LiveUser(playlists: [], tags: [], username: keychain["username"]!)
let liveUser = LiveUser(playlists: [], tags: [], username: keychain["username"]!).loadUserDefaults()
controller = UIHostingController(rootView: contentView.environmentObject(liveUser))
} else {
let storyboard = UIStoryboard(name: "Main", bundle: nil)

View File

@ -7,6 +7,8 @@
//
import Foundation
import Alamofire
import SwiftyJSON
class LiveUser: ObservableObject {
@ -26,4 +28,80 @@ class LiveUser: ObservableObject {
}
self.playlists[index] = playlistIn
}
func refreshPlaylists() {
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")
}
let playlists = json["playlists"].arrayValue
// update state
self.playlists = PlaylistApi.fromJSON(playlist: playlists).sorted(by: { $0.name.lowercased() < $1.name.lowercased() })
let encoder = JSONEncoder()
let defaults = UserDefaults.standard
do {
defaults.set(String(data: try encoder.encode(playlists), encoding: .utf8), forKey: "playlists")
} catch {
print("error encoding playlists: \(error)")
}
}
}
func refreshTags() {
let api = TagApi.getTags
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
guard let data = response.data else {
fatalError("error getting tags")
}
guard let json = try? JSON(data: data) else {
fatalError("error parsing reponse")
}
let tags = json["tags"].arrayValue
// update state
self.tags = TagApi.fromJSON(tag: tags).sorted(by: { $0.name.lowercased() < $1.name.lowercased() })
let encoder = JSONEncoder()
let defaults = UserDefaults.standard
do {
defaults.set(String(data: try encoder.encode(tags), encoding: .utf8), forKey: "tags")
} catch {
print("error encoding tags: \(error)")
}
}
}
func loadUserDefaults() -> LiveUser {
let defaults = UserDefaults.standard
let decoder = JSONDecoder()
let _strPlaylists = defaults.string(forKey: "playlists")
let _strTags = defaults.string(forKey: "tags")
do {
if let _strPlaylists = _strPlaylists {
self.playlists = (try decoder.decode([Playlist].self, from: _strPlaylists.data(using: .utf8)!)).sorted(by: { $0.name.lowercased() < $1.name.lowercased() })
}
if let _strTags = _strTags {
self.tags = (try decoder.decode([Tag].self, from: _strTags.data(using: .utf8)!)).sorted(by: { $0.name.lowercased() < $1.name.lowercased() })
}
} catch {
print("error decoding: \(error)")
}
return self
}
}

View File

@ -6,16 +6,17 @@
// Copyright © 2020 Sarsoo. All rights reserved.
//
import Foundation
import UIKit
import SwiftyJSON
class Playlist: Identifiable, Equatable {
class Playlist: Identifiable, Equatable, Codable {
//MARK: Properties
var name: String
var uri: String
var username: String
var username: String?
var include_recommendations: Bool
var recommendation_sample: Int
@ -23,23 +24,83 @@ class Playlist: Identifiable, Equatable {
var parts: Array<String>
var playlist_references: Array<String>
var shuffle: Bool
var sort: String
var description_overwrite: String?
var description_suffix: String?
var last_updated: String
var lastfm_stat_count: Int
var lastfm_stat_album_count: Int
var lastfm_stat_artist_count: Int
var lastfm_stat_percent: Float
var lastfm_stat_album_percent: Float
var lastfm_stat_artist_percent: Float
var lastfm_stat_last_refresh: String
private enum CodingKeys: String, CodingKey {
case name
case uri
case username
case include_recommendations
case recommendation_sample
case include_library_tracks
case parts
case playlist_references
case shuffle
case sort
case description_overwrite
case description_suffix
case last_updated
case lastfm_stat_count
case lastfm_stat_album_count
case lastfm_stat_artist_count
case lastfm_stat_percent
case lastfm_stat_album_percent
case lastfm_stat_artist_percent
case lastfm_stat_last_refresh
}
//MARK: Initialization
init(name: String,
uri: String,
username: String,
uri: String = "spotify::",
username: String = "NO USER",
include_recommendations: Bool,
recommendation_sample: Int,
include_library_tracks: Bool,
include_recommendations: Bool = false,
recommendation_sample: Int = 0,
include_library_tracks: Bool = false,
parts: Array<String>,
playlist_references: Array<String>,
parts: Array<String> = [],
playlist_references: Array<String> = [],
shuffle: Bool = false,
shuffle: Bool){
sort: String = "NO SORT",
description_overwrite: String? = nil,
description_suffix: String? = nil,
last_updated: String = "",
lastfm_stat_count: Int = 0,
lastfm_stat_album_count: Int = 0,
lastfm_stat_artist_count: Int = 0,
lastfm_stat_percent: Float = 0,
lastfm_stat_album_percent: Float = 0,
lastfm_stat_artist_percent: Float = 0,
lastfm_stat_last_refresh: String = ""){
self.name = name
self.uri = uri
@ -51,61 +112,23 @@ class Playlist: Identifiable, Equatable {
self.parts = parts
self.playlist_references = playlist_references
self.shuffle = shuffle
}
static func fromDict(dictionary: JSON) -> Playlist? {
switch dictionary["type"].string {
case "default":
return Playlist(name: dictionary["name"].stringValue,
uri: dictionary["uri"].stringValue,
username: dictionary["username"].stringValue,
self.sort = sort
self.description_overwrite = description_overwrite
self.description_suffix = description_suffix
include_recommendations: dictionary["include_recommendations"].boolValue,
recommendation_sample: dictionary["recommendation_sample"].intValue,
include_library_tracks: dictionary["include_library_tracks"].boolValue,
self.last_updated = last_updated
parts: dictionary["parts"].arrayObject as! Array<String>,
playlist_references: dictionary["playlist_references"].arrayObject as! Array<String>,
self.lastfm_stat_count = lastfm_stat_count
self.lastfm_stat_album_count = lastfm_stat_album_count
self.lastfm_stat_artist_count = lastfm_stat_artist_count
shuffle: dictionary["shuffle"].boolValue)
case "recents":
return RecentsPlaylist(name: dictionary["name"].stringValue,
uri: dictionary["uri"].stringValue,
username: dictionary["username"].stringValue,
self.lastfm_stat_percent = lastfm_stat_percent
self.lastfm_stat_album_percent = lastfm_stat_album_percent
self.lastfm_stat_artist_percent = lastfm_stat_artist_percent
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<String>,
playlist_references: dictionary["playlist_references"].arrayObject as! Array<String>,
shuffle: dictionary["shuffle"].boolValue,
add_last_month: dictionary["add_last_month"].boolValue,
add_this_month: dictionary["add_this_month"].boolValue,
day_boundary: dictionary["day_boundary"].intValue)
case "fmchart":
return LastFMChartPlaylist(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<String>,
playlist_references: dictionary["playlist_references"].arrayObject as! Array<String>,
shuffle: dictionary["shuffle"].boolValue,
chart_range: LastFmRange(rawValue: dictionary["chart_range"].stringValue)!,
chart_limit: dictionary["chart_limit"].intValue)
default:
return nil
}
self.lastfm_stat_last_refresh = lastfm_stat_last_refresh
}
var link: String {
@ -117,6 +140,38 @@ class Playlist: Identifiable, Equatable {
return lhs.name == rhs.name
// && lhs.username == rhs.username
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
uri = try container.decode(String.self, forKey: .uri)
// username = try container.decode(String.self, forKey: .username)
include_recommendations = try container.decode(Bool.self, forKey: .include_recommendations)
recommendation_sample = try container.decode(Int.self, forKey: .recommendation_sample)
include_library_tracks = try container.decode(Bool.self, forKey: .include_library_tracks)
parts = try container.decode([String].self, forKey: .parts)
playlist_references = try container.decode([String].self, forKey: .playlist_references)
shuffle = try container.decode(Bool.self, forKey: .shuffle)
sort = try container.decode(String.self, forKey: .sort)
// description_overwrite = try container.decode(String.self, forKey: .description_overwrite)
// description_suffix = try container.decode(String.self, forKey: .description_suffix)
last_updated = try container.decode(String.self, forKey: .last_updated)
lastfm_stat_count = try container.decode(Int.self, forKey: .lastfm_stat_count)
lastfm_stat_album_count = try container.decode(Int.self, forKey: .lastfm_stat_album_count)
lastfm_stat_artist_count = try container.decode(Int.self, forKey: .lastfm_stat_artist_count)
lastfm_stat_percent = try container.decode(Float.self, forKey: .lastfm_stat_percent)
lastfm_stat_album_percent = try container.decode(Float.self, forKey: .lastfm_stat_album_percent)
lastfm_stat_artist_percent = try container.decode(Float.self, forKey: .lastfm_stat_artist_percent)
lastfm_stat_last_refresh = try container.decode(String.self, forKey: .lastfm_stat_last_refresh)
}
}
@ -128,34 +183,36 @@ class RecentsPlaylist: Playlist {
var add_this_month: Bool
var day_boundary: Int
private enum CodingKeys: String, CodingKey { case add_last_month; case add_this_month; case day_boundary }
//MARK: Initialization
init(name: String,
uri: String,
username: String,
username: String = "NO USER",
include_recommendations: Bool,
recommendation_sample: Int,
include_library_tracks: Bool,
parts: Array<String>,
playlist_references: Array<String>,
shuffle: Bool,
add_last_month: Bool,
add_this_month: Bool,
day_boundary: Int){
add_last_month: Bool = false,
add_this_month: Bool = false,
day_boundary: Int = 14){
self.add_last_month = add_last_month
self.add_this_month = add_this_month
self.day_boundary = day_boundary
super.init(name: name, uri: uri, username: username, include_recommendations: include_recommendations, recommendation_sample: recommendation_sample, include_library_tracks: include_library_tracks, parts: parts, playlist_references: playlist_references, shuffle: shuffle)
super.init(name: name, username: username)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
add_last_month = try container.decode(Bool.self, forKey: .add_last_month)
add_this_month = try container.decode(Bool.self, forKey: .add_this_month)
day_boundary = try container.decode(Int.self, forKey: .day_boundary)
try super.init(from: decoder)
}
}
enum LastFmRange: String {
enum LastFmRange: String, Decodable {
case overall = "OVERALL"
case week = "WEEK"
case month = "MONTH"
@ -171,27 +228,28 @@ class LastFMChartPlaylist: Playlist {
var chart_range: LastFmRange
var chart_limit: Int
private enum CodingKeys: String, CodingKey { case chart_range; case chart_limit }
//MARK: Initialization
init(name: String,
uri: String,
username: String,
username: String = "NO USER",
include_recommendations: Bool,
recommendation_sample: Int,
include_library_tracks: Bool,
parts: Array<String>,
playlist_references: Array<String>,
shuffle: Bool,
chart_range: LastFmRange,
chart_limit: Int){
chart_range: LastFmRange = .overall,
chart_limit: Int = 10){
self.chart_range = chart_range
self.chart_limit = chart_limit
super.init(name: name, uri: uri, username: username, include_recommendations: include_recommendations, recommendation_sample: recommendation_sample, include_library_tracks: include_library_tracks, parts: parts, playlist_references: playlist_references, shuffle: shuffle)
super.init(name: name, username: username)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
chart_range = try LastFmRange(rawValue: container.decode(String.self, forKey: .chart_range))!
chart_limit = try container.decode(Int.self, forKey: .chart_limit)
try super.init(from: decoder)
}
}

View File

@ -9,7 +9,7 @@
import UIKit
import SwiftyJSON
class Tag: Identifiable, Equatable {
class Tag: Identifiable, Equatable, Decodable {
//MARK: Properties
@ -58,22 +58,6 @@ class Tag: Identifiable, Equatable {
self.last_updated = last_updated
}
static func fromDict(dictionary: JSON) -> Tag {
return Tag(tag_id: dictionary["tag_id"].stringValue,
name: dictionary["name"].stringValue,
username: dictionary["username"].stringValue,
tracks: dictionary["tracks"].arrayValue,
albums: dictionary["albums"].arrayValue,
artists: dictionary["artists"].arrayValue,
count: dictionary["count"].intValue,
proportion: dictionary["proportion"].doubleValue,
total_user_scrobbles: dictionary["total_user_scrobbles"].intValue,
last_updated: dictionary["last_updated"].stringValue)
}
static func == (lhs: Tag, rhs: Tag) -> Bool {
return lhs.tag_id == rhs.tag_id
// && lhs.username == rhs.username

View File

@ -9,12 +9,12 @@
import UIKit
import SwiftyJSON
enum UserType: String {
enum UserType: String, Decodable {
case user = "user"
case admin = "admin"
}
class User: Identifiable {
class User: Identifiable, Decodable {
//MARK: Properties
@ -44,15 +44,5 @@ class User: Identifiable {
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)
}
}

View File

@ -110,6 +110,68 @@ extension PlaylistApi: ApiRequest {
return ApiRequestDefaults.authMethod
}
static func fromJSON(playlist: Data) -> Playlist? {
let decoder = JSONDecoder()
do {
let json = try JSON(data: playlist)
switch json["type"].string {
case "default":
let playlist = try decoder.decode(Playlist.self, from: playlist)
return playlist
case "recents":
let playlist = try decoder.decode(RecentsPlaylist.self, from: playlist)
return playlist
case "fmchart":
let playlist = try decoder.decode(LastFMChartPlaylist.self, from: playlist)
return playlist
default:
return nil
}
} catch {
print(error)
}
return nil
}
static func fromJSON(playlist: JSON) -> Playlist? {
let _json = playlist.rawString()?.data(using: .utf8)
if let data = _json {
let decoder = JSONDecoder()
do {
switch playlist["type"].string {
case "default":
let playlist = try decoder.decode(Playlist.self, from: data)
return playlist
case "recents":
let playlist = try decoder.decode(RecentsPlaylist.self, from: data)
return playlist
case "fmchart":
let playlist = try decoder.decode(LastFMChartPlaylist.self, from: data)
return playlist
default:
return nil
}
} catch {
print(error)
}
}
print(playlist)
return nil
}
static func fromJSON(playlist: [JSON]) -> [Playlist] {
var _playlists: [Playlist] = []
for dict in playlist {
let _iter = self.fromJSON(playlist: dict)
if let returned = _iter {
_playlists.append(returned)
}
}
return _playlists
}
}

View File

@ -100,6 +100,32 @@ extension TagApi: ApiRequest {
return ApiRequestDefaults.authMethod
}
static func fromJSON(tag: JSON) -> Tag? {
let _json = tag.rawString()?.data(using: .utf8)
if let data = _json {
let decoder = JSONDecoder()
do {
let _tag = try decoder.decode(Tag.self, from: data)
return _tag
} catch {
print(error)
}
}
return nil
}
// TODO this loop could be condensed
static func fromJSON(tag: [JSON]) -> [Tag] {
var _tags: [Tag] = []
for dict in tag {
let _iter = self.fromJSON(tag: dict)
if let returned = _iter {
_tags.append(returned)
}
}
return _tags
}
}

View File

@ -77,13 +77,13 @@ struct AddPlaylistSheet: View {
var playlist: Playlist? = nil
switch PlaylistType(rawValue: selectedType) ?? .defaultPlaylist {
case .defaultPlaylist:
playlist = Playlist(name: name, uri: "", username: username, include_recommendations: false, recommendation_sample: 10, include_library_tracks: false, parts: [], playlist_references: [], shuffle: false)
playlist = Playlist(name: name, username: username)
break
case .recents:
playlist = RecentsPlaylist(name: name, uri: "", username: username, include_recommendations: false, recommendation_sample: 10, include_library_tracks: false, parts: [], playlist_references: [], shuffle: false, add_last_month: false, add_this_month: false, day_boundary: 14)
playlist = RecentsPlaylist(name: name, username: username)
break
case .fmchart:
playlist = LastFMChartPlaylist(name: name, uri: "", username: username, include_recommendations: false, recommendation_sample: 10, include_library_tracks: false, parts: [], playlist_references: [], shuffle: false, chart_range: .month, chart_limit: 10)
playlist = LastFMChartPlaylist(name: name, username: username)
break
}

View File

@ -46,8 +46,8 @@ struct PlaylistRow: View {
struct PlaylistRow_Previews: PreviewProvider {
static var previews: some View {
PlaylistRow(playlist:
.constant(Playlist(name: "", uri: "", username: "", include_recommendations: true, recommendation_sample: 1, include_library_tracks: true, parts: [], playlist_references: [], shuffle: true))
)
PlaylistView(playlist: .constant(
Playlist(name: "playlist name", username: "username")
))
}
}

View File

@ -180,10 +180,7 @@ struct PlaylistView: View {
fatalError("error getting playlist")
}
guard let json = try? JSON(data: data) else {
fatalError("error parsing reponse")
}
self.playlist = Playlist.fromDict(dictionary: json)!
self.playlist = PlaylistApi.fromJSON(playlist: data)!
self.isRefreshing = false
}
//TODO: do better error checking
@ -193,18 +190,7 @@ struct PlaylistView: View {
struct PlaylistView_Previews: PreviewProvider {
static var previews: some View {
PlaylistView(playlist: .constant(
Playlist(name: "playlist name",
uri: "uri",
username: "username",
include_recommendations: true,
recommendation_sample: 5,
include_library_tracks: true,
parts: ["name"],
playlist_references: ["ref name"],
shuffle: true)
Playlist(name: "playlist name", username: "username")
))
}
}

View File

@ -44,7 +44,8 @@ struct RootView: View {
}
}
.pullToRefresh(isShowing: $isRefreshingPlaylists) {
self.refreshPlaylists()
self.liveUser.refreshPlaylists()
self.isRefreshingPlaylists = false
}
.navigationBarTitle(Text("Playlists").font(.title))
@ -85,7 +86,8 @@ struct RootView: View {
}
}
.pullToRefresh(isShowing: $isRefreshingTags) {
self.refreshTags()
self.liveUser.refreshTags()
self.isRefreshingTags = false
}
.navigationBarTitle(Text("Tags").font(.title))
@ -124,61 +126,8 @@ struct RootView: View {
}
private func fetchAll() {
refreshPlaylists()
refreshTags()
}
public func refreshPlaylists() {
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")
}
let playlists = json["playlists"].arrayValue
// parse playlists
.map({ dict in
Playlist.fromDict(dictionary: dict)!
})
// sort
.sorted(by: { $0.name.lowercased() < $1.name.lowercased() })
// update state
self.liveUser.playlists = playlists
self.isRefreshingPlaylists = false
}
//TODO: do better error checking
}
public func refreshTags() {
let tagApi = TagApi.getTags
RequestBuilder.buildRequest(apiRequest: tagApi).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")
}
let tags = json["tags"].arrayValue
// parse playlists
.map({ dict in
Tag.fromDict(dictionary: dict)
})
// sort
.sorted(by: { $0.name.lowercased() < $1.name.lowercased() })
// update state
self.liveUser.tags = tags
self.isRefreshingTags = false
}
self.liveUser.refreshPlaylists()
self.liveUser.refreshTags()
}
}

View File

@ -113,7 +113,10 @@ struct TagView: View {
guard let json = try? JSON(data: data) else {
fatalError("error parsing reponse")
}
self.tag = Tag.fromDict(dictionary: json["tag"])
let _tag = TagApi.fromJSON(tag: json["tag"])
if let tag = _tag {
self.tag = tag
}
self.isRefreshing = false
}
//TODO: do better error checking