summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFinn Behrens <me@kloenk.de>2021-04-07 13:46:43 +0200
committerFinn Behrens <me@kloenk.de>2021-04-07 13:46:43 +0200
commit9313281e65bd67a329038c756d02a231e1300371 (patch)
tree6a33be228188b2187520822dabc6de46e7231578
parent70bd9461c1f3843c0624ae4d61a3b18812e0273b (diff)
downloadMusicConverter-main.tar.gz
MusicConverter-main.tar.xz
MusicConverter-main.zip
-rw-r--r--MusicConverter.xcodeproj/xcuserdata/kloenk.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist48
-rw-r--r--MusicConverter/Model/Napster.swift53
-rw-r--r--MusicConverter/View/Napster/NapsterPlaylistView.swift58
-rw-r--r--MusicConverter/View/Napster/NapsterSongListItemView.swift4
-rw-r--r--MusicConverter/helpers/MusicKITApi.swift148
5 files changed, 283 insertions, 28 deletions
diff --git a/MusicConverter.xcodeproj/xcuserdata/kloenk.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/MusicConverter.xcodeproj/xcuserdata/kloenk.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
index a74cf9d..6c0ca65 100644
--- a/MusicConverter.xcodeproj/xcuserdata/kloenk.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
+++ b/MusicConverter.xcodeproj/xcuserdata/kloenk.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
@@ -84,5 +84,53 @@
</Locations>
</BreakpointContent>
</BreakpointProxy>
+ <BreakpointProxy
+ BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
+ <BreakpointContent
+ uuid = "17CC2709-4758-458E-9A21-A3FD8822FE2E"
+ shouldBeEnabled = "No"
+ ignoreCount = "0"
+ continueAfterRunningActions = "No"
+ filePath = "MusicConverter/View/Napster/NapsterPlaylistView.swift"
+ startingColumnNumber = "9223372036854775807"
+ endingColumnNumber = "9223372036854775807"
+ startingLineNumber = "122"
+ endingLineNumber = "122"
+ landmarkName = "createPlaylist()"
+ landmarkType = "7">
+ <Locations>
+ <Location
+ uuid = "17CC2709-4758-458E-9A21-A3FD8822FE2E - bcb98dd65149bfc5"
+ shouldBeEnabled = "Yes"
+ ignoreCount = "0"
+ continueAfterRunningActions = "No"
+ symbolName = "MusicConverter.NapsterPlaylistView_CreatButton.createPlaylist() -&gt; ()"
+ moduleName = "MusicConverter"
+ usesParentBreakpointCondition = "Yes"
+ urlString = "file:///Users/kloenk/Developer/Xcode/MusicConverter/MusicConverter/View/Napster/NapsterPlaylistView.swift"
+ startingColumnNumber = "9223372036854775807"
+ endingColumnNumber = "9223372036854775807"
+ startingLineNumber = "122"
+ endingLineNumber = "122"
+ offsetFromSymbolStart = "200">
+ </Location>
+ <Location
+ uuid = "17CC2709-4758-458E-9A21-A3FD8822FE2E - 127fe7a4149bb044"
+ shouldBeEnabled = "Yes"
+ ignoreCount = "0"
+ continueAfterRunningActions = "No"
+ symbolName = "closure #1 (MusicConverter.Napster.AppleChoice) -&gt; Swift.Bool in MusicConverter.NapsterPlaylistView_CreatButton.createPlaylist() -&gt; ()"
+ moduleName = "MusicConverter"
+ usesParentBreakpointCondition = "Yes"
+ urlString = "file:///Users/kloenk/Developer/Xcode/MusicConverter/MusicConverter/View/Napster/NapsterPlaylistView.swift"
+ startingColumnNumber = "9223372036854775807"
+ endingColumnNumber = "9223372036854775807"
+ startingLineNumber = "123"
+ endingLineNumber = "123"
+ offsetFromSymbolStart = "60">
+ </Location>
+ </Locations>
+ </BreakpointContent>
+ </BreakpointProxy>
</Breakpoints>
</Bucket>
diff --git a/MusicConverter/Model/Napster.swift b/MusicConverter/Model/Napster.swift
index 21d8919..b45e7db 100644
--- a/MusicConverter/Model/Napster.swift
+++ b/MusicConverter/Model/Napster.swift
@@ -26,6 +26,11 @@ class Napster: ObservableObject {
cachedNapsterSource?.name ?? id
}
+ /*var synced: Bool {
+ self.appleChoices.count == self.cachedNapsterSource?.trackCount ?? -1
+ }*/
+ var synced = false
+
init(id: String, api: AppleMusicApi) {
self.id = id
self.api = api
@@ -43,6 +48,7 @@ class Napster: ObservableObject {
public func update(completion: @escaping (Error?) -> Void) {
self.cachedNapsterSource = NANapsterPlaylist(id: id)
+ self.appleChoices = []; // TODO: keep choices, but update
self.cachedNapsterSource?.update { result in
switch result {
case .failure(let e):
@@ -91,12 +97,14 @@ class Napster: ObservableObject {
}
completion(.success(d))
}
+ // error also changes self
DispatchQueue.main.async {
self.objectWillChange.send()
}
}
}
+ // TODO: do not add songs without playbackParams
public func resolveTracks(completion: @escaping (Error?) -> Void) {
guard self.tracks != nil else {
completion(NapsterError.TracksEmpty)
@@ -109,14 +117,35 @@ class Napster: ObservableObject {
return
}
do {
- try self.appleChoices.append(AppleChoice(id: self.appleChoices.count, songs: songs.map({s in AppleMusicApi.SongInfo(song: s)})))
+ let choiceSongs = songs.filter { song in
+ //return true //song.attributes.playParams != nil
+ return song.attributes.playParams != nil
+ }.map { song in
+ AppleMusicApi.SongInfo(song: song)
+ }
+ if choiceSongs.count > 0 {
+ try self.appleChoices.append(AppleChoice(id: self.appleChoices.count, songs: choiceSongs))
+ } else {
+ print("did not found any playable songs for \(track.name)")
+ }
} catch {
print("error adding to choices: \(error)")
}
DispatchQueue.main.async {
self.objectWillChange.send()
}
- }, completion: completion) // TODO: do I need the completion?
+ }, completion: { error in
+ if let error = error {
+ print("error resolving: \(error)")
+ } else {
+ self.synced = true
+ print("finisched syncing")
+ }
+ DispatchQueue.main.async {
+ self.objectWillChange.send()
+ }
+ completion(error)
+ })
}
public func updateId(_ id: String) {
@@ -124,6 +153,8 @@ class Napster: ObservableObject {
// Invalidate caches
}
+ //public func createApplePlaylistAPI(completion: @escaping (Result<))
+
public func createApplePlaylist(completion: @escaping (Result<MPMediaPlaylist, Error>) -> Void) {
/*self.musicPlayer.setQueue(with: self.appleChoices.map({s in
s.song.attributes.playParams.id
@@ -158,7 +189,11 @@ class Napster: ObservableObject {
return
}
- guard self.appleChoices.count > 0 else {
+ /*guard self.appleChoices.count > 0 else {
+ completion(.failure(NapsterError.NotEnoughSongs))
+ return
+ }*/
+ guard self.synced else {
completion(.failure(NapsterError.NotEnoughSongs))
return
}
@@ -169,20 +204,26 @@ class Napster: ObservableObject {
var callback: (Error?) -> Void = {_ in}
callback = { error in
+ print("callback: adding song \(self.appleChoices[pos].song.name) to playlist")
guard error == nil else {
completion(.failure(error!))
return
}
- if pos < self.appleChoices.count {
+ if pos < self.appleChoices.count - 1 {
pos += 1;
- playlist.addItem(withProductID: self.appleChoices[pos].song.attributes.playParams.id, completionHandler: callback)
+ while self.appleChoices[pos].song.attributes.playParams?.id == nil {
+ print("did not found a id for song \(self.appleChoices[pos].song.name)")
+ pos += 1;
+ }
+ playlist.addItem(withProductID: self.appleChoices[pos].song.attributes.playParams!.id, completionHandler: callback)
} else {
completion(.success(()))
}
}
- playlist.addItem(withProductID: self.appleChoices[pos].song.attributes.playParams.id, completionHandler: callback)
+ print("add first item")
+ playlist.addItem(withProductID: self.appleChoices[pos].song.attributes.playParams!.id, completionHandler: callback)
}
public struct AppleChoice: Identifiable, CustomStringConvertible {
diff --git a/MusicConverter/View/Napster/NapsterPlaylistView.swift b/MusicConverter/View/Napster/NapsterPlaylistView.swift
index ef907fe..0180dbb 100644
--- a/MusicConverter/View/Napster/NapsterPlaylistView.swift
+++ b/MusicConverter/View/Napster/NapsterPlaylistView.swift
@@ -17,7 +17,7 @@ struct NapsterPlaylistView: View {
self.napsterPlaylist = Napster(id: id, api: api)
self.napsterPlaylist.update(completion: {e in
- if e == nil {
+ if e != nil {
print("error updating: \(e!)")
return
}
@@ -37,7 +37,7 @@ struct NapsterPlaylistView: View {
Divider()
Button(action: { showingSongs.toggle() }) {
- Text("Songs")
+ Text("Songs (\(self.napsterPlaylist.appleChoices.count))")
}
}
Text("trackCount: \(napsterPlaylist.tracks?.count ?? 0)")
@@ -57,6 +57,7 @@ struct NapsterPlaylistView: View {
return
}
self.updated = true
+
})
}) {
Text("Update")
@@ -78,26 +79,30 @@ struct NapsterPlaylistView_CreatButton: View {
}
var body: some View {
- switch self.state {
- case .none:
- Button(action: {
- DispatchQueue.global(qos: .userInitiated).async {
- self.createPlaylist()
- }
- },
- label: {
- Text("Create playlist")
- })
- case .creating:
+ if self.napsterPlaylist.synced {
+ switch self.state {
+ case .none:
+ Button(action: {
+ DispatchQueue.global(qos: .userInitiated).async {
+ self.createPlaylist()
+ }
+ },
+ label: {
+ Text("Create playlist")
+ })
+ case .creating:
+ ProgressView()
+ case .created:
+ Text("done")
+ }
+ } else {
ProgressView()
- case .created:
- Text("done")
}
}
func createPlaylist() {
self.state = .creating
- self.napsterPlaylist.createApplePlaylist(completion: {result in
+ /*self.napsterPlaylist.createApplePlaylist(completion: {result in
switch result {
case .failure(let e):
print("error creating: \(e)")
@@ -113,6 +118,27 @@ struct NapsterPlaylistView_CreatButton: View {
}
})
}
+ })*/
+ self.napsterPlaylist.api.createPlaylistAPI(name: self.napsterPlaylist.name, tracks: self.napsterPlaylist.appleChoices.filter { song in
+ if song.song.attributes.playParams != nil {
+ print("including")
+ return true
+ }
+ return false
+ }.map {song in
+ AppleMusicApi.LibraryPlaylistRequestTrack(id: song.song.attributes.playParams!.id, type: .songs)
+ } , description: "MusicConverter", completion: {result in
+ switch result {
+ case .failure(let e):
+ print("error creating playist: \(e)")
+ self.state = .none
+ case .success(_):
+ print("created playlist")
+ DispatchQueue.main.async {
+ //self.state = .created
+ self.state = .none
+ }
+ }
})
}
}
diff --git a/MusicConverter/View/Napster/NapsterSongListItemView.swift b/MusicConverter/View/Napster/NapsterSongListItemView.swift
index 8fbca75..c40cecb 100644
--- a/MusicConverter/View/Napster/NapsterSongListItemView.swift
+++ b/MusicConverter/View/Napster/NapsterSongListItemView.swift
@@ -21,8 +21,8 @@ struct NapsterSongListItemView: View {
return Image(systemName: "photo")
}*/
- print("id: \(self.choice.song.attributes.playParams.id)")
- let mpContentItem = MPContentItem(identifier: self.choice.song.attributes.playParams.id)
+ print("id: \(self.choice.song.attributes.playParams?.id ?? "error")")
+ let mpContentItem = MPContentItem(identifier: self.choice.song.attributes.playParams?.id ?? "error")
print("mpContentItem: \(mpContentItem)")
return Text(mpContentItem.title ?? "error")
}
diff --git a/MusicConverter/helpers/MusicKITApi.swift b/MusicConverter/helpers/MusicKITApi.swift
index 0b394a3..e47241a 100644
--- a/MusicConverter/helpers/MusicKITApi.swift
+++ b/MusicConverter/helpers/MusicKITApi.swift
@@ -72,7 +72,7 @@ class AppleMusicApi: ObservableObject {
do {
let decodedData = try JSONDecoder().decode(musicData<StoreFront>.self, from: data!)
- self.storeFront = decodedData.data.first
+ self.storeFront = decodedData.data!.first
guard self.storeFront != nil else {
completion(.failure(MusicKitError.NoStoreFront))
return
@@ -80,6 +80,7 @@ class AppleMusicApi: ObservableObject {
completion(.success(self.storeFront!.id))
} catch {
print("data: \(String(decoding: data!, as: UTF8.self))")
+ print("error decoding: \(error)")
completion(.failure(error))
return
}
@@ -112,9 +113,10 @@ class AppleMusicApi: ObservableObject {
do {
let decodedData = try JSONDecoder().decode(musicData<Song>.self, from: data!)
- completion(.success(decodedData.data))
+ completion(.success(decodedData.data!))
} catch {
print("data: \(String(decoding: data!, as: UTF8.self))")
+ print("error song: \(error)")
completion(.failure(error))
return
}
@@ -207,6 +209,8 @@ class AppleMusicApi: ObservableObject {
}
pos += 1;
self.fetchForIsrc(isrc: isrc!, completion: callback)
+ } else {
+ completion(nil)
}
}
}
@@ -226,8 +230,144 @@ class AppleMusicApi: ObservableObject {
self.fetchForIsrc(isrc: isrc!, completion: callback)
}
+ public func createPlaylistAPI(name: String, tracks: [LibraryPlaylistRequestTrack]? = nil, description: String? = nil, completion: @escaping (Result<[LibraryPlaylist], Error>) -> Void) {
+ let request = LibraryPlaylistCreationRequest(name: name, tracks: tracks, description: description)
+ createPlaylistAPI(creationRequest: request, completion: completion)
+ }
+
+ public func createPlaylistAPI(creationRequest: LibraryPlaylistCreationRequest, completion: @escaping (Result<[LibraryPlaylist], Error>) -> Void) {
+
+
+ guard let url = URL(string: "https://api.music.apple.com/v1/me/library/playlists") else {
+ completion(.failure(MusicKitError.InvalidUrl))
+ return
+ }
+ var playlistRequest = self.buildRequest(url: url)
+ playlistRequest.httpMethod = "POST"
+ playlistRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
+ do {
+ playlistRequest.httpBody = try JSONEncoder().encode(creationRequest)
+ let str = String(data: playlistRequest.httpBody!, encoding: .utf8)
+ print("request: \(str!)")
+ } catch {
+ completion(.failure(error))
+ return
+ }
+
+ URLSession.shared.dataTask(with: playlistRequest) { (data, response, error) in
+ guard error == nil else {
+ completion(.failure(error!))
+ return
+ }
+
+ do {
+ let decodedData = try JSONDecoder().decode(musicData<LibraryPlaylist>.self, from: data!)
+ if decodedData.hasError {
+ decodedData.printErrors()
+ completion(.failure(decodedData.errors!.first!))
+ return
+ }
+ completion(.success(decodedData.data!))
+ } catch {
+ print("error playlistCreationRequest: \(error)")
+ print("json response: \(String(data: data!, encoding: .utf8) ?? "error")")
+ completion(.failure(error))
+ return
+ }
+ //completion(.success())
+ }
+ .resume()
+ }
+
+ // TODO: https://developer.apple.com/documentation/applemusicapi/responseroot
struct musicData<T: Decodable>: Decodable {
- var data: [T]
+ var data: [T]?
+ var errors: [ResposeError]?
+
+ var hasError: Bool {
+ return errors != nil && errors?.count ?? 0 > 0
+ }
+
+ func printErrors() {
+ for error in errors ?? [] {
+ print("error: \(error)")
+ }
+ }
+
+ public struct ResposeError: Codable, Error {
+ var code: String
+ var detail: String?
+ var id: String
+ // var source
+ var status: String
+ var title: String
+ }
+ }
+
+ public struct LibraryPlaylistCreationRequest: Codable {
+ var attributes: Attributes
+ var relationships: Relationships?
+
+ init(name: String, tracks: [LibraryPlaylistRequestTrack]? = nil, description: String? = nil) {
+ self.attributes = Attributes(name: name, description: description)
+
+ if let tracks = tracks {
+ self.relationships = Relationships(tracks: Relationships.TracksData(data: tracks))
+ }
+ }
+
+ public struct Attributes: Codable {
+ var name: String
+ var description: String?
+ }
+
+ public struct Relationships: Codable {
+ var tracks: TracksData
+
+ public struct TracksData: Codable {
+ var data: [LibraryPlaylistRequestTrack]
+ }
+ }
+ }
+
+ public struct LibraryPlaylistRequestTrack: Codable, Identifiable {
+ var id: String
+ var type: TrackType = .songs
+
+ public enum TrackType: String, Codable {
+ case songs = "songs"
+ case music_videos = "music-videos"
+ case library_songs = "library-songs"
+ case library_music_videos = "library-music-videos"
+ }
+ }
+
+ public struct LibraryPlaylist: Decodable {
+ var attributes: Attributes?
+ //var relationships: Relationships?
+ var type: String
+ var id: String
+
+ /*var id: String? {
+ attributes?.playParams?.id
+ }*/
+
+ var name: String? {
+ attributes?.name
+ }
+
+ public struct Attributes: Decodable {
+ var artwork: Song.Attributes.Artwork?
+ //var description: String?
+ var name: String
+ var playParams: Song.Attributes.PlayParameters?
+ var canEdit: Bool
+ }
+
+ // TODO
+ public struct Relationships: Decodable {
+
+ }
}
public struct StoreFront: Decodable {
@@ -289,7 +429,7 @@ class AppleMusicApi: ObservableObject {
var genreNames: [String]
var isrc: String
var name: String
- var playParams: PlayParameters
+ var playParams: PlayParameters?
// var previews: [Preview] (required)
var releaseDate: String
/// (Required) The number of the song in the album’s track list.