summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFinn Behrens <me@kloenk.de>2021-04-04 10:50:39 +0200
committerFinn Behrens <me@kloenk.de>2021-04-04 10:50:39 +0200
commit70bd9461c1f3843c0624ae4d61a3b18812e0273b (patch)
tree0d91743291a70837f5190bb96a4555ecc58f8790
parentf294922c88216828fd63d8bdfd48671e57d061f1 (diff)
downloadMusicConverter-70bd9461c1f3843c0624ae4d61a3b18812e0273b.tar.gz
MusicConverter-70bd9461c1f3843c0624ae4d61a3b18812e0273b.tar.xz
MusicConverter-70bd9461c1f3843c0624ae4d61a3b18812e0273b.zip
something
-rw-r--r--.gitignore1
-rw-r--r--MusicConverter.xcodeproj/project.pbxproj120
-rw-r--r--MusicConverter.xcodeproj/project.xcworkspace/contents.xcworkspacedata7
-rw-r--r--MusicConverter.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist8
-rw-r--r--MusicConverter.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved16
-rw-r--r--MusicConverter.xcodeproj/project.xcworkspace/xcuserdata/kloenk.xcuserdatad/xcdebugger/Expressions.xcexplist9
-rw-r--r--MusicConverter.xcodeproj/xcuserdata/kloenk.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist88
-rw-r--r--MusicConverter.xcodeproj/xcuserdata/kloenk.xcuserdatad/xcschemes/xcschememanagement.plist14
-rw-r--r--MusicConverter/ContentView.swift21
-rw-r--r--MusicConverter/Info.plist4
-rw-r--r--MusicConverter/Model/Napster.swift216
-rw-r--r--MusicConverter/MusicConverterApp.swift12
-rw-r--r--MusicConverter/View/ContentView.swift48
-rw-r--r--MusicConverter/View/Napster/NapsterNewView.swift42
-rw-r--r--MusicConverter/View/Napster/NapsterPlaylistView.swift124
-rw-r--r--MusicConverter/View/Napster/NapsterSongItemView.swift72
-rw-r--r--MusicConverter/View/Napster/NapsterSongListItemView.swift58
-rw-r--r--MusicConverter/View/Napster/NapsterSongListView.swift68
-rw-r--r--MusicConverter/View/New/NewView.swift49
-rw-r--r--MusicConverter/View/ProviderRow.swift39
-rw-r--r--MusicConverter/View/UpdateView.swift20
-rw-r--r--MusicConverter/helpers/MusicKITApi.swift348
-rw-r--r--MusicConverter/helpers/NaappleHelper.swift13
-rw-r--r--MusicConverter/helpers/NapsterPlaylistHelper.swift259
24 files changed, 1632 insertions, 24 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..21bebca
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+Config.xcconfig
diff --git a/MusicConverter.xcodeproj/project.pbxproj b/MusicConverter.xcodeproj/project.pbxproj
index c4cc0ae..9c652c9 100644
--- a/MusicConverter.xcodeproj/project.pbxproj
+++ b/MusicConverter.xcodeproj/project.pbxproj
@@ -3,10 +3,22 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 50;
+ objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
+ 959A766E2617513000A72D4F /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 959A766D2617513000A72D4F /* UpdateView.swift */; };
+ 959A76702617518400A72D4F /* NewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 959A766F2617518400A72D4F /* NewView.swift */; };
+ 959A76722617531400A72D4F /* NapsterNewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 959A76712617531400A72D4F /* NapsterNewView.swift */; };
+ 959A76752617534800A72D4F /* ProviderRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 959A76742617534800A72D4F /* ProviderRow.swift */; };
+ 959A767826175A7900A72D4F /* Napster.swift in Sources */ = {isa = PBXBuildFile; fileRef = 959A767726175A7900A72D4F /* Napster.swift */; };
+ 959A767B26175D1A00A72D4F /* NapsterPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 959A767A26175D1A00A72D4F /* NapsterPlaylistView.swift */; };
+ 95F952B3261762D100B25505 /* NapsterPlaylistHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95F952B1261762D100B25505 /* NapsterPlaylistHelper.swift */; };
+ 95F952B4261762D100B25505 /* NaappleHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95F952B2261762D100B25505 /* NaappleHelper.swift */; };
+ 95F952B626176EE400B25505 /* MusicKITApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95F952B526176EE400B25505 /* MusicKITApi.swift */; };
+ 95F952B926179E9A00B25505 /* NapsterSongListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95F952B826179E9A00B25505 /* NapsterSongListView.swift */; };
+ 95F952BB2617A2BB00B25505 /* NapsterSongListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95F952BA2617A2BB00B25505 /* NapsterSongListItemView.swift */; };
+ 95F952BD2617AEFE00B25505 /* NapsterSongItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95F952BC2617AEFE00B25505 /* NapsterSongItemView.swift */; };
95FE8C3F261738CF00EA526B /* MusicConverterApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95FE8C3E261738CF00EA526B /* MusicConverterApp.swift */; };
95FE8C41261738CF00EA526B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95FE8C40261738CF00EA526B /* ContentView.swift */; };
95FE8C43261738D100EA526B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 95FE8C42261738D100EA526B /* Assets.xcassets */; };
@@ -14,6 +26,19 @@
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
+ 959A766D2617513000A72D4F /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
+ 959A766F2617518400A72D4F /* NewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewView.swift; sourceTree = "<group>"; };
+ 959A76712617531400A72D4F /* NapsterNewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NapsterNewView.swift; sourceTree = "<group>"; };
+ 959A76742617534800A72D4F /* ProviderRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderRow.swift; sourceTree = "<group>"; };
+ 959A767726175A7900A72D4F /* Napster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Napster.swift; sourceTree = "<group>"; };
+ 959A767A26175D1A00A72D4F /* NapsterPlaylistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NapsterPlaylistView.swift; sourceTree = "<group>"; };
+ 95F952B1261762D100B25505 /* NapsterPlaylistHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NapsterPlaylistHelper.swift; sourceTree = "<group>"; };
+ 95F952B2261762D100B25505 /* NaappleHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NaappleHelper.swift; sourceTree = "<group>"; };
+ 95F952B526176EE400B25505 /* MusicKITApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicKITApi.swift; sourceTree = "<group>"; };
+ 95F952B72617701900B25505 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = SOURCE_ROOT; };
+ 95F952B826179E9A00B25505 /* NapsterSongListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NapsterSongListView.swift; sourceTree = "<group>"; };
+ 95F952BA2617A2BB00B25505 /* NapsterSongListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NapsterSongListItemView.swift; sourceTree = "<group>"; };
+ 95F952BC2617AEFE00B25505 /* NapsterSongItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NapsterSongItemView.swift; sourceTree = "<group>"; };
95FE8C3B261738CF00EA526B /* MusicConverter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MusicConverter.app; sourceTree = BUILT_PRODUCTS_DIR; };
95FE8C3E261738CF00EA526B /* MusicConverterApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicConverterApp.swift; sourceTree = "<group>"; };
95FE8C40261738CF00EA526B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -33,11 +58,63 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ 959A766B261750A200A72D4F /* View */ = {
+ isa = PBXGroup;
+ children = (
+ 959A767926175D0E00A72D4F /* Napster */,
+ 959A76732617531800A72D4F /* New */,
+ 95FE8C40261738CF00EA526B /* ContentView.swift */,
+ 959A766D2617513000A72D4F /* UpdateView.swift */,
+ 959A76742617534800A72D4F /* ProviderRow.swift */,
+ );
+ path = View;
+ sourceTree = "<group>";
+ };
+ 959A76732617531800A72D4F /* New */ = {
+ isa = PBXGroup;
+ children = (
+ 959A766F2617518400A72D4F /* NewView.swift */,
+ );
+ path = New;
+ sourceTree = "<group>";
+ };
+ 959A767626175A6D00A72D4F /* Model */ = {
+ isa = PBXGroup;
+ children = (
+ 959A767726175A7900A72D4F /* Napster.swift */,
+ );
+ path = Model;
+ sourceTree = "<group>";
+ };
+ 959A767926175D0E00A72D4F /* Napster */ = {
+ isa = PBXGroup;
+ children = (
+ 959A76712617531400A72D4F /* NapsterNewView.swift */,
+ 959A767A26175D1A00A72D4F /* NapsterPlaylistView.swift */,
+ 95F952B826179E9A00B25505 /* NapsterSongListView.swift */,
+ 95F952BC2617AEFE00B25505 /* NapsterSongItemView.swift */,
+ 95F952BA2617A2BB00B25505 /* NapsterSongListItemView.swift */,
+ );
+ path = Napster;
+ sourceTree = "<group>";
+ };
+ 95F952B0261762C300B25505 /* helpers */ = {
+ isa = PBXGroup;
+ children = (
+ 95F952B2261762D100B25505 /* NaappleHelper.swift */,
+ 95F952B1261762D100B25505 /* NapsterPlaylistHelper.swift */,
+ 95F952B526176EE400B25505 /* MusicKITApi.swift */,
+ );
+ path = helpers;
+ sourceTree = "<group>";
+ };
95FE8C32261738CF00EA526B = {
isa = PBXGroup;
children = (
+ 95F952B72617701900B25505 /* Config.xcconfig */,
95FE8C3D261738CF00EA526B /* MusicConverter */,
95FE8C3C261738CF00EA526B /* Products */,
+ 95FE8C5326173AA800EA526B /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -52,8 +129,10 @@
95FE8C3D261738CF00EA526B /* MusicConverter */ = {
isa = PBXGroup;
children = (
+ 95F952B0261762C300B25505 /* helpers */,
+ 959A767626175A6D00A72D4F /* Model */,
+ 959A766B261750A200A72D4F /* View */,
95FE8C3E261738CF00EA526B /* MusicConverterApp.swift */,
- 95FE8C40261738CF00EA526B /* ContentView.swift */,
95FE8C42261738D100EA526B /* Assets.xcassets */,
95FE8C47261738D100EA526B /* Info.plist */,
95FE8C44261738D100EA526B /* Preview Content */,
@@ -69,6 +148,13 @@
path = "Preview Content";
sourceTree = "<group>";
};
+ 95FE8C5326173AA800EA526B /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -85,6 +171,8 @@
dependencies = (
);
name = MusicConverter;
+ packageProductDependencies = (
+ );
productName = MusicConverter;
productReference = 95FE8C3B261738CF00EA526B /* MusicConverter.app */;
productType = "com.apple.product-type.application";
@@ -112,6 +200,9 @@
Base,
);
mainGroup = 95FE8C32261738CF00EA526B;
+ packageReferences = (
+ 959A767D2617603200A72D4F /* XCRemoteSwiftPackageReference "naapple" */,
+ );
productRefGroup = 95FE8C3C261738CF00EA526B /* Products */;
projectDirPath = "";
projectRoot = "";
@@ -138,8 +229,20 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 959A767826175A7900A72D4F /* Napster.swift in Sources */,
+ 95F952BB2617A2BB00B25505 /* NapsterSongListItemView.swift in Sources */,
+ 959A766E2617513000A72D4F /* UpdateView.swift in Sources */,
+ 959A76752617534800A72D4F /* ProviderRow.swift in Sources */,
+ 95F952BD2617AEFE00B25505 /* NapsterSongItemView.swift in Sources */,
+ 959A76722617531400A72D4F /* NapsterNewView.swift in Sources */,
+ 95F952B3261762D100B25505 /* NapsterPlaylistHelper.swift in Sources */,
+ 95F952B4261762D100B25505 /* NaappleHelper.swift in Sources */,
+ 959A76702617518400A72D4F /* NewView.swift in Sources */,
+ 95F952B926179E9A00B25505 /* NapsterSongListView.swift in Sources */,
95FE8C41261738CF00EA526B /* ContentView.swift in Sources */,
+ 95F952B626176EE400B25505 /* MusicKITApi.swift in Sources */,
95FE8C3F261738CF00EA526B /* MusicConverterApp.swift in Sources */,
+ 959A767B26175D1A00A72D4F /* NapsterPlaylistView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -264,6 +367,7 @@
};
95FE8C4B261738D100EA526B /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 95F952B72617701900B25505 /* Config.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
@@ -286,6 +390,7 @@
};
95FE8C4C261738D100EA526B /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 95F952B72617701900B25505 /* Config.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
@@ -328,6 +433,17 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
+
+/* Begin XCRemoteSwiftPackageReference section */
+ 959A767D2617603200A72D4F /* XCRemoteSwiftPackageReference "naapple" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/kloenk/naapple";
+ requirement = {
+ branch = main;
+ kind = branch;
+ };
+ };
+/* End XCRemoteSwiftPackageReference section */
};
rootObject = 95FE8C33261738CF00EA526B /* Project object */;
}
diff --git a/MusicConverter.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/MusicConverter.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/MusicConverter.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "self:">
+ </FileRef>
+</Workspace>
diff --git a/MusicConverter.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/MusicConverter.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/MusicConverter.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IDEDidComputeMac32BitWarning</key>
+ <true/>
+</dict>
+</plist>
diff --git a/MusicConverter.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/MusicConverter.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 0000000..0f25fc5
--- /dev/null
+++ b/MusicConverter.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,16 @@
+{
+ "object": {
+ "pins": [
+ {
+ "package": "Naapple",
+ "repositoryURL": "https://github.com/kloenk/naapple",
+ "state": {
+ "branch": "main",
+ "revision": "31b585dc8c43248a774d922d13ff1d399ce514a5",
+ "version": null
+ }
+ }
+ ]
+ },
+ "version": 1
+}
diff --git a/MusicConverter.xcodeproj/project.xcworkspace/xcuserdata/kloenk.xcuserdatad/xcdebugger/Expressions.xcexplist b/MusicConverter.xcodeproj/project.xcworkspace/xcuserdata/kloenk.xcuserdatad/xcdebugger/Expressions.xcexplist
new file mode 100644
index 0000000..62c00ac
--- /dev/null
+++ b/MusicConverter.xcodeproj/project.xcworkspace/xcuserdata/kloenk.xcuserdatad/xcdebugger/Expressions.xcexplist
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<VariablesViewState
+ version = "1.0">
+ <ContextStates>
+ <ContextState
+ contextName = "AppleMusicApi.updateUserToken(completion:):MusicKITApi.swift">
+ </ContextState>
+ </ContextStates>
+</VariablesViewState>
diff --git a/MusicConverter.xcodeproj/xcuserdata/kloenk.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/MusicConverter.xcodeproj/xcuserdata/kloenk.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
new file mode 100644
index 0000000..a74cf9d
--- /dev/null
+++ b/MusicConverter.xcodeproj/xcuserdata/kloenk.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Bucket
+ uuid = "F1664E07-F970-4613-BADC-D8658623B2BA"
+ type = "1"
+ version = "2.0">
+ <Breakpoints>
+ <BreakpointProxy
+ BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
+ <BreakpointContent
+ uuid = "B5E54FF9-ADF4-4143-BF6A-B3F12AC5F6C4"
+ shouldBeEnabled = "No"
+ ignoreCount = "0"
+ continueAfterRunningActions = "No"
+ filePath = "MusicConverter/helpers/MusicKITApi.swift"
+ startingColumnNumber = "9223372036854775807"
+ endingColumnNumber = "9223372036854775807"
+ startingLineNumber = "18"
+ endingLineNumber = "18"
+ landmarkName = "updateUserToken(completion:)"
+ landmarkType = "7">
+ </BreakpointContent>
+ </BreakpointProxy>
+ <BreakpointProxy
+ BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
+ <BreakpointContent
+ uuid = "B2FCD159-6936-404A-B414-22C361EA5E43"
+ shouldBeEnabled = "No"
+ ignoreCount = "0"
+ continueAfterRunningActions = "No"
+ filePath = "MusicConverter/helpers/MusicKITApi.swift"
+ startingColumnNumber = "9223372036854775807"
+ endingColumnNumber = "9223372036854775807"
+ startingLineNumber = "40"
+ endingLineNumber = "40"
+ landmarkName = "updateUserToken(completion:)"
+ landmarkType = "7">
+ </BreakpointContent>
+ </BreakpointProxy>
+ <BreakpointProxy
+ BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
+ <BreakpointContent
+ uuid = "77B0C78E-C0E2-4D4C-80B6-C3FEC2BDBC70"
+ shouldBeEnabled = "No"
+ ignoreCount = "0"
+ continueAfterRunningActions = "No"
+ filePath = "MusicConverter/helpers/MusicKITApi.swift"
+ startingColumnNumber = "9223372036854775807"
+ endingColumnNumber = "9223372036854775807"
+ startingLineNumber = "45"
+ endingLineNumber = "45"
+ landmarkName = "updateUserToken(completion:)"
+ landmarkType = "7">
+ <Locations>
+ <Location
+ uuid = "77B0C78E-C0E2-4D4C-80B6-C3FEC2BDBC70 - 241ec609cf574af2"
+ shouldBeEnabled = "Yes"
+ ignoreCount = "0"
+ continueAfterRunningActions = "No"
+ symbolName = "MusicConverter.AppleMusicApi.updateUserToken(completion: (Swift.Result&lt;Swift.String, Swift.Error&gt;) -&gt; ()) -&gt; ()"
+ moduleName = "MusicConverter"
+ usesParentBreakpointCondition = "Yes"
+ urlString = "file:///Users/kloenk/Developer/Xcode/MusicConverter/MusicConverter/helpers/MusicKITApi.swift"
+ startingColumnNumber = "9223372036854775807"
+ endingColumnNumber = "9223372036854775807"
+ startingLineNumber = "45"
+ endingLineNumber = "45"
+ offsetFromSymbolStart = "632">
+ </Location>
+ <Location
+ uuid = "77B0C78E-C0E2-4D4C-80B6-C3FEC2BDBC70 - f467be18a17b7198"
+ shouldBeEnabled = "Yes"
+ ignoreCount = "0"
+ continueAfterRunningActions = "No"
+ symbolName = "closure #2 (Swift.Optional&lt;Swift.String&gt;, Swift.Optional&lt;Swift.Error&gt;) -&gt; () in MusicConverter.AppleMusicApi.updateUserToken(completion: (Swift.Result&lt;Swift.String, Swift.Error&gt;) -&gt; ()) -&gt; ()"
+ moduleName = "MusicConverter"
+ usesParentBreakpointCondition = "Yes"
+ urlString = "file:///Users/kloenk/Developer/Xcode/MusicConverter/MusicConverter/helpers/MusicKITApi.swift"
+ startingColumnNumber = "9223372036854775807"
+ endingColumnNumber = "9223372036854775807"
+ startingLineNumber = "46"
+ endingLineNumber = "46"
+ offsetFromSymbolStart = "96">
+ </Location>
+ </Locations>
+ </BreakpointContent>
+ </BreakpointProxy>
+ </Breakpoints>
+</Bucket>
diff --git a/MusicConverter.xcodeproj/xcuserdata/kloenk.xcuserdatad/xcschemes/xcschememanagement.plist b/MusicConverter.xcodeproj/xcuserdata/kloenk.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 0000000..134a913
--- /dev/null
+++ b/MusicConverter.xcodeproj/xcuserdata/kloenk.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>SchemeUserState</key>
+ <dict>
+ <key>MusicConverter.xcscheme_^#shared#^_</key>
+ <dict>
+ <key>orderHint</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+</dict>
+</plist>
diff --git a/MusicConverter/ContentView.swift b/MusicConverter/ContentView.swift
deleted file mode 100644
index 9f65ec8..0000000
--- a/MusicConverter/ContentView.swift
+++ /dev/null
@@ -1,21 +0,0 @@
-//
-// ContentView.swift
-// MusicConverter
-//
-// Created by Finn Behrens on 02.04.21.
-//
-
-import SwiftUI
-
-struct ContentView: View {
- var body: some View {
- Text("Hello, world!")
- .padding()
- }
-}
-
-struct ContentView_Previews: PreviewProvider {
- static var previews: some View {
- ContentView()
- }
-}
diff --git a/MusicConverter/Info.plist b/MusicConverter/Info.plist
index efc211a..31fe65a 100644
--- a/MusicConverter/Info.plist
+++ b/MusicConverter/Info.plist
@@ -20,6 +20,8 @@
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
+ <key>NSAppleMusicUsageDescription</key>
+ <string>We have to access apple music to create a playlist</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
@@ -33,6 +35,8 @@
<array>
<string>armv7</string>
</array>
+ <key>MUSIC_KIT_API_KEY</key>
+ <string>$(MUSIC_KIT_API_KEY)</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
diff --git a/MusicConverter/Model/Napster.swift b/MusicConverter/Model/Napster.swift
new file mode 100644
index 0000000..21d8919
--- /dev/null
+++ b/MusicConverter/Model/Napster.swift
@@ -0,0 +1,216 @@
+//
+// Napster.swift
+// MusicConverter
+//
+// Created by Finn Behrens on 02.04.21.
+//
+
+import Foundation
+import MediaPlayer
+
+class Napster: ObservableObject {
+ var id: String
+ var api: AppleMusicApi
+ @Published var cachedNapsterSource: NANapsterPlaylist?
+ @Published var tracks: [NANapsterPlaylist.Track]?
+
+ var appleChoices: [AppleChoice] = []
+ var finischedConvertingChoices = false
+ @Published var applePlaylist: MPMediaPlaylist?
+ // ToDo: error handling
+ var error: Error?
+
+ var musicPlayer = MPMusicPlayerController.systemMusicPlayer
+
+ var name: String {
+ cachedNapsterSource?.name ?? id
+ }
+
+ init(id: String, api: AppleMusicApi) {
+ self.id = id
+ self.api = api
+
+ if self.id.isEmpty {
+ return
+ }
+ // TODO: apikey
+ }
+
+ init(demo: String, api: AppleMusicApi = AppleMusicApi()) {
+ self.id = demo
+ self.api = api
+ }
+
+ public func update(completion: @escaping (Error?) -> Void) {
+ self.cachedNapsterSource = NANapsterPlaylist(id: id)
+ self.cachedNapsterSource?.update { result in
+ switch result {
+ case .failure(let e):
+ self.error = e
+ case .success(_):
+ self.updateTracks() { result in
+ guard (try? result.get()) != nil else {
+ print("wtf? what did guard do?")
+ return
+ }
+
+ self.resolveTracks(completion: { e in
+ if e != nil {
+ self.error = e
+ }
+ completion(e)
+ self.finischedConvertingChoices = true
+ DispatchQueue.main.async {
+ self.objectWillChange.send()
+ }
+ })
+ }
+ }
+ DispatchQueue.main.async {
+ self.objectWillChange.send()
+ }
+ }
+ }
+
+ public func updateChoice(choice: Napster.AppleChoice, song: Int) {
+ self.appleChoices[choice.id].choice = song
+ DispatchQueue.main.async {
+ self.objectWillChange.send()
+ }
+ }
+
+ public func updateTracks(completion: @escaping (Result<[NANapsterPlaylist.Track], Error>) -> Void = {_ in}) {
+ self.cachedNapsterSource?.getTracks { result in
+ switch result {
+ case .failure(let e):
+ self.error = e
+ completion(.failure(e))
+ case .success(let d):
+ DispatchQueue.main.sync {
+ self.tracks = d;
+ }
+ completion(.success(d))
+ }
+ DispatchQueue.main.async {
+ self.objectWillChange.send()
+ }
+ }
+ }
+
+ public func resolveTracks(completion: @escaping (Error?) -> Void) {
+ guard self.tracks != nil else {
+ completion(NapsterError.TracksEmpty)
+ return
+ }
+ api.convertPlaylist(playlist: self.tracks!, updateCallback: { (track, songs) in
+ // FIXME: add unknown songs
+ guard songs.count > 0 else {
+ print("Did not find a song for \(track.name)")
+ return
+ }
+ do {
+ try self.appleChoices.append(AppleChoice(id: self.appleChoices.count, songs: songs.map({s in AppleMusicApi.SongInfo(song: s)})))
+ } catch {
+ print("error adding to choices: \(error)")
+ }
+ DispatchQueue.main.async {
+ self.objectWillChange.send()
+ }
+ }, completion: completion) // TODO: do I need the completion?
+ }
+
+ public func updateId(_ id: String) {
+ self.id = id
+ // Invalidate caches
+ }
+
+ public func createApplePlaylist(completion: @escaping (Result<MPMediaPlaylist, Error>) -> Void) {
+ /*self.musicPlayer.setQueue(with: self.appleChoices.map({s in
+ s.song.attributes.playParams.id
+ }))
+ self.musicPlayer.play()*/
+
+ // TODO: check input variables
+ guard self.cachedNapsterSource != nil else {
+ completion(.failure(NANapsterError.NotCachedError))
+ return
+ }
+
+ let playlistUUID = UUID()
+
+ let playlistCreationMetadata = MPMediaPlaylistCreationMetadata(name: self.name)
+ playlistCreationMetadata.descriptionText = self.cachedNapsterSource?.description ?? "" + "\n\nFrom napster"
+
+ MPMediaLibrary.default().getPlaylist(with: playlistUUID, creationMetadata: playlistCreationMetadata, completionHandler: { (playlist, error) in
+ guard error == nil else {
+ completion(.failure(error!))
+ return
+ }
+
+ self.applePlaylist = playlist
+ completion(.success(playlist!))
+ })
+ }
+
+ public func convertToApplePlaylist(completion: @escaping (Result<(), Error>) -> Void) {
+ guard let playlist = self.applePlaylist else {
+ completion(.failure(NapsterError.NoPlaylistCreated))
+ return
+ }
+
+ guard self.appleChoices.count > 0 else {
+ completion(.failure(NapsterError.NotEnoughSongs))
+ return
+ }
+
+ var pos = 0
+
+ // TODO: clear playlist
+
+ var callback: (Error?) -> Void = {_ in}
+ callback = { error in
+ guard error == nil else {
+ completion(.failure(error!))
+ return
+ }
+
+ if pos < self.appleChoices.count {
+ 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)
+ }
+
+ public struct AppleChoice: Identifiable, CustomStringConvertible {
+ init(id: Int, songs: [AppleMusicApi.SongInfo]) throws {
+ guard songs.count > 0 else {
+ throw NapsterError.NotEnoughSongs
+ }
+
+ self.id = id
+ self.songs = songs
+ }
+
+ var id: Int
+ var songs: [AppleMusicApi.SongInfo]
+ var choice: Int = 0
+
+ var song: AppleMusicApi.SongInfo {
+ songs[choice]
+ }
+
+ var description: String {
+ song.attributes.name
+ }
+ }
+}
+
+public enum NapsterError: Error {
+ case TracksEmpty
+ case NotEnoughSongs
+ case NoPlaylistCreated
+}
diff --git a/MusicConverter/MusicConverterApp.swift b/MusicConverter/MusicConverterApp.swift
index 276659e..068f8ae 100644
--- a/MusicConverter/MusicConverterApp.swift
+++ b/MusicConverter/MusicConverterApp.swift
@@ -10,8 +10,18 @@ import SwiftUI
@main
struct MusicConverterApp: App {
var body: some Scene {
- WindowGroup {
+ let api = AppleMusicApi();
+ api.updateUserToken(completion: {ret in
+ switch ret {
+ case .failure(let e):
+ print("error: \(e)")
+ case .success(_):
+ api.updateStoreFrontID(completion: {ret in print(ret)})
+ }
+ })
+ return WindowGroup {
ContentView()
+ .environmentObject(api)
}
}
}
diff --git a/MusicConverter/View/ContentView.swift b/MusicConverter/View/ContentView.swift
new file mode 100644
index 0000000..f6a9491
--- /dev/null
+++ b/MusicConverter/View/ContentView.swift
@@ -0,0 +1,48 @@
+//
+// ContentView.swift
+// MusicConverter
+//
+// Created by Finn Behrens on 02.04.21.
+//
+
+import SwiftUI
+import MediaPlayer
+
+// TODO: implement check for Apple Music from user
+struct ContentView: View {
+ //let myPlaylistQuery = MPMediaQuery.playlists()
+ @State private var selection: Tab = .New
+ enum Tab {
+ case New
+ case Update
+ }
+
+ var body: some View {
+ TabView(selection: $selection) {
+ NewView()
+ .tabItem {
+ Label("New", systemImage: "plus.circle")
+ }
+ .tag(Tab.New)
+ UpdateView()
+ .tabItem {
+ Label("Update", systemImage: "arrow.clockwise.circle")
+ }
+ .tag(Tab.Update)
+ // TODO: maybe account view with spotify and napster to get private playlists?
+ }
+ /*let playlists = myPlaylistQuery.collections!
+ VStack{
+ ForEach(playlists, id: \.self) { playlist in
+ Text("playlist: \(playlist.value(forProperty: MPMediaPlaylistPropertyName)! as! String)")
+ }
+ }
+ Text("foobar")*/
+ }
+}
+
+struct ContentView_Previews: PreviewProvider {
+ static var previews: some View {
+ ContentView()
+ }
+}
diff --git a/MusicConverter/View/Napster/NapsterNewView.swift b/MusicConverter/View/Napster/NapsterNewView.swift
new file mode 100644
index 0000000..8e00180
--- /dev/null
+++ b/MusicConverter/View/Napster/NapsterNewView.swift
@@ -0,0 +1,42 @@
+//
+// NapsterNewView.swift
+// MusicConverter
+//
+// Created by Finn Behrens on 02.04.21.
+//
+
+import SwiftUI
+
+struct NapsterNewView: View {
+ @EnvironmentObject var music: AppleMusicApi
+ @State var id: String = ""
+
+ var body: some View {
+ VStack {
+ HStack {
+ Text("Playlist id:")
+ Spacer(minLength: 20)
+ TextField("playlist id", text: $id)
+ }
+ .padding()
+ NavigationLink(
+ destination: NapsterPlaylistView(id: id, api: music),
+ label: {
+ Text("Ok")
+ })
+ .padding(2)
+ // replace with searchBar (https://www.appcoda.com/swiftui-search-bar/)
+ List {
+ Text("listItem")
+ }
+ }
+ }
+}
+
+struct NapsterNewView_Previews: PreviewProvider {
+ static var previews: some View {
+ Group {
+ NapsterNewView()
+ }
+ }
+}
diff --git a/MusicConverter/View/Napster/NapsterPlaylistView.swift b/MusicConverter/View/Napster/NapsterPlaylistView.swift
new file mode 100644
index 0000000..ef907fe
--- /dev/null
+++ b/MusicConverter/View/Napster/NapsterPlaylistView.swift
@@ -0,0 +1,124 @@
+//
+// NapsterPlaylist.swift
+// MusicConverter
+//
+// Created by Finn Behrens on 02.04.21.
+//
+
+import SwiftUI
+
+struct NapsterPlaylistView: View {
+ @ObservedObject var napsterPlaylist: Napster
+ @State private var showingSongs = false
+ @State private var updated = false
+
+
+ init(id: String, api: AppleMusicApi) {
+ self.napsterPlaylist = Napster(id: id, api: api)
+
+ self.napsterPlaylist.update(completion: {e in
+ if e == nil {
+ print("error updating: \(e!)")
+ return
+ }
+ })
+ }
+
+ init(demoData: String) {
+ self.napsterPlaylist = Napster(demo: demoData)
+ }
+
+ var body: some View {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 10) {
+ Text(self.napsterPlaylist.name)
+ .bold()
+ .font(.title)
+
+ Divider()
+ Button(action: { showingSongs.toggle() }) {
+ Text("Songs")
+ }
+ }
+ Text("trackCount: \(napsterPlaylist.tracks?.count ?? 0)")
+ Divider()
+ NapsterPlaylistView_CreatButton(napsterPlaylist: napsterPlaylist)
+ }
+ .sheet(isPresented: $showingSongs) {
+ NapsterSongListView(napsterPlaylist: napsterPlaylist)
+ }
+ .toolbar {
+ Button(action: {
+ updated = false
+ napsterPlaylist.update(completion: {error in
+ if error != nil {
+ print("failed to update")
+ // TODO: error handling onscreen
+ return
+ }
+ self.updated = true
+ })
+ }) {
+ Text("Update")
+ }
+ }
+ }
+
+
+}
+
+struct NapsterPlaylistView_CreatButton: View {
+ @ObservedObject var napsterPlaylist: Napster
+
+ @State private var state: CreationState = .none
+ private enum CreationState {
+ case none
+ case creating
+ case created
+ }
+
+ var body: some View {
+ 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")
+ }
+ }
+
+ func createPlaylist() {
+ self.state = .creating
+ self.napsterPlaylist.createApplePlaylist(completion: {result in
+ switch result {
+ case .failure(let e):
+ print("error creating: \(e)")
+ case .success(_):
+ self.napsterPlaylist.convertToApplePlaylist(completion: {result in
+ switch result {
+ case .failure(let e):
+ print("error converting: \(e)")
+ case .success(_):
+ print("created playlist!")
+ NotificationCenter.default.post(name: NSNotification.Name(rawValue: "test"), object: nil)
+ self.state = .created
+ }
+ })
+ }
+ })
+ }
+}
+
+struct NapsterPlaylist_Previews: PreviewProvider {
+ static var previews: some View {
+ NapsterPlaylistView(demoData: "mp.demo")
+ }
+}
diff --git a/MusicConverter/View/Napster/NapsterSongItemView.swift b/MusicConverter/View/Napster/NapsterSongItemView.swift
new file mode 100644
index 0000000..b3422c5
--- /dev/null
+++ b/MusicConverter/View/Napster/NapsterSongItemView.swift
@@ -0,0 +1,72 @@
+//
+// NapsterSongItemView.swift
+// MusicConverter
+//
+// Created by Finn Behrens on 02.04.21.
+//
+
+import SwiftUI
+
+struct NapsterSongItemView: View {
+ @Binding var selectedSong: String?
+ @ObservedObject var napsterPlaylist: Napster
+ @State var choice: Napster.AppleChoice
+ @State var image: Image = Image(systemName: "photo")
+ @State var subChoice: Int?
+
+
+ var body: some View {
+ List(selection: $subChoice) {
+ ForEach(0...choice.songs.count-1, id: \.self) { song in
+ NapsterSongItemView_Inner(choice: $choice, selectedSong: $selectedSong, song: song, napsterPlaylist: napsterPlaylist)
+ }
+ }
+ }
+}
+
+struct NapsterSongItemView_Inner: View {
+ @Binding var choice: Napster.AppleChoice
+ @Binding var selectedSong: String?
+ let song: Int
+ @ObservedObject var napsterPlaylist: Napster
+
+
+ var body: some View {
+ HStack {
+ Button(action: {
+ choice.choice = song
+ napsterPlaylist.updateChoice(choice: choice, song: song)
+ }, label: {
+ Circle()
+ .frame(width: 50, height: 50)
+ .foregroundColor(choice.choice == song ? .green : .gray)
+ })
+ Divider()
+ NavigationLink(
+ destination: Button(action: {
+ //self.napsterPlaylist.getApplePlaylist()
+ }, label: {
+ Text(choice.songs[song].name)
+ }),
+ label: {
+ VStack {
+ Text(choice.songs[song].name)
+ .bold()
+ HStack {
+ Text(choice.songs[song].albumName)
+ .font(.caption)
+ Text("\(choice.songs[song].attributes.playbackSeconds ?? 0)s")
+ }
+ }
+ }
+ )
+ }
+ }
+}
+
+struct NapsterSongItemView_Previews: PreviewProvider {
+ static var previews: some View {
+ Text("TODO")
+ //NapsterSongItemView()
+ }
+}
diff --git a/MusicConverter/View/Napster/NapsterSongListItemView.swift b/MusicConverter/View/Napster/NapsterSongListItemView.swift
new file mode 100644
index 0000000..8fbca75
--- /dev/null
+++ b/MusicConverter/View/Napster/NapsterSongListItemView.swift
@@ -0,0 +1,58 @@
+//
+// NapsterSongListItemView.swift
+// MusicConverter
+//
+// Created by Finn Behrens on 02.04.21.
+//
+
+import SwiftUI
+import MediaPlayer
+
+struct NapsterSongListItemView: View {
+ @ObservedObject var napsterPlaylist: Napster
+ @State var choice: Napster.AppleChoice
+ @State var image: Image
+
+ func getImage() -> some View {
+ /*print("mediaItem: \(self.choice.song.mediaItem == nil ? "no" : "yes")")
+ if let image = choice.song.mediaItem?.artwork?.image(at: CGSize(width: 50, height: 50)) {
+ return Image(uiImage: image)
+ } else {
+ return Image(systemName: "photo")
+ }*/
+
+ print("id: \(self.choice.song.attributes.playParams.id)")
+ let mpContentItem = MPContentItem(identifier: self.choice.song.attributes.playParams.id)
+ print("mpContentItem: \(mpContentItem)")
+ return Text(mpContentItem.title ?? "error")
+ }
+
+ var body: some View {
+ HStack {
+ getImage()
+ /*image
+ .resizable()
+ .frame(width: 50, height: 50)
+ .cornerRadius(5)*/
+ VStack(alignment: .leading) {
+ Text(choice.song.name)
+ .bold()
+ HStack {
+ Text(choice.song.albumName)
+ Text(" - ")
+ //.padding(.horizontal)
+ Text(choice.song.artistName)
+ .foregroundColor(.gray)
+ }
+ .font(.caption)
+ }
+ }
+ }
+}
+
+struct NapsterSongListItemView_Previews: PreviewProvider {
+ static var previews: some View {
+ Text("ToDo")
+ //NapsterSongListItemView()
+ }
+}
diff --git a/MusicConverter/View/Napster/NapsterSongListView.swift b/MusicConverter/View/Napster/NapsterSongListView.swift
new file mode 100644
index 0000000..da08085
--- /dev/null
+++ b/MusicConverter/View/Napster/NapsterSongListView.swift
@@ -0,0 +1,68 @@
+//
+// NapsterSongListView.swift
+// MusicConverter
+//
+// Created by Finn Behrens on 02.04.21.
+//
+
+import SwiftUI
+
+struct NapsterSongListView: View {
+ @ObservedObject var napsterPlaylist: Napster
+ @State private var selectedSong: String?
+ //@ObservedObject private var trigger = ImageLoader()
+ @State var image: Image = Image(systemName: "photo")
+
+ var body: some View {
+ return NavigationView {
+ List(selection: $selectedSong) {
+ ForEach(napsterPlaylist.appleChoices) { choice in
+ NavigationLink(
+ destination: NapsterSongItemView(selectedSong: $selectedSong, napsterPlaylist: napsterPlaylist, choice: choice),
+ label: {
+ NapsterSongListItemView(napsterPlaylist: napsterPlaylist, choice: choice, image: image)
+ })
+ .tag(choice.description)
+ }
+ }
+ }
+ }
+
+ // TODO: fix image somehow
+ /*func getLink(_ choice: Napster.AppleChoice) -> some View {
+ /*self.updateImage(choice, callback: {i in
+ image = i
+ DispatchQueue.main.async {
+ self.trigger.update()
+ }
+ })*/
+ return
+ }*/
+
+ /*func updateImage(_ choice: Napster.AppleChoice, callback: @escaping (Image) -> Void) {
+ napsterPlaylist.api.getPicture(song: choice.song, x: 50, y: 50, completion: {result in
+ switch result {
+ case .failure(let e):
+ print("failed to get picture: \(e)")
+ case .success(let d):
+ if d != nil {
+ callback(Image(uiImage: d!))
+ }
+ }
+ })
+
+ }*/
+
+ private class ImageLoader: ObservableObject {
+ func update() {
+ self.objectWillChange.send()
+ }
+ }
+}
+
+struct NapsterSongListView_Previews: PreviewProvider {
+ static var previews: some View {
+ //NapsterSongListView()
+ Text("Todo")
+ }
+}
diff --git a/MusicConverter/View/New/NewView.swift b/MusicConverter/View/New/NewView.swift
new file mode 100644
index 0000000..b41e899
--- /dev/null
+++ b/MusicConverter/View/New/NewView.swift
@@ -0,0 +1,49 @@
+//
+// NewView.swift
+// MusicConverter
+//
+// Created by Finn Behrens on 02.04.21.
+//
+
+import SwiftUI
+
+struct NewView: View {
+ @EnvironmentObject var music: AppleMusicApi
+ @State private var selectedProvider: ProviderRow.MusicProvider? = .Napster
+
+ var body: some View {
+ NavigationView {
+ List(selection: $selectedProvider){
+ // TODO: replace with filterdProviders
+ ForEach(ProviderRow.MusicProvider.allCases) { provider in
+ NavigationLink(
+ destination: NapsterNewView(),
+ label: {
+ ProviderRow(provider: provider)
+ })
+ .tag(provider)
+ }
+ Button("foo", action: {
+ music.fetchForIsrc(isrc: "USRC11301144", completion: { result in
+ switch result {
+ case .failure(let e):
+ print("error getting isrc: \(e)")
+ case .success(let d):
+ print("songs: \(d)")
+ print("number of songs: \(d.count)")
+ }
+ })
+ })
+ }
+ .navigationTitle("Providers")
+ .frame(minWidth: 300)
+ //.focusedValue(\.selectedProvider, <#T##value: Value##Value#>)
+ }
+ }
+}
+
+struct NewView_Previews: PreviewProvider {
+ static var previews: some View {
+ NewView()
+ }
+}
diff --git a/MusicConverter/View/ProviderRow.swift b/MusicConverter/View/ProviderRow.swift
new file mode 100644
index 0000000..3f6eb09
--- /dev/null
+++ b/MusicConverter/View/ProviderRow.swift
@@ -0,0 +1,39 @@
+//
+// ProviderRow.swift
+// MusicConverter
+//
+// Created by Finn Behrens on 02.04.21.
+//
+
+import SwiftUI
+
+struct ProviderRow: View {
+ var provider: MusicProvider
+ public enum MusicProvider: String, CaseIterable, Identifiable, Hashable {
+ case Napster = "Napster"
+
+ var id: MusicProvider { self }
+ }
+
+ var body: some View {
+ HStack {
+ // TODO: replace with real images (first make sure of copyright
+ Image(systemName: "photo")
+ .resizable()
+ .frame(width: 50, height: 50)
+ .cornerRadius(5)
+ VStack {
+ Text(provider.rawValue)
+ .bold()
+ }
+ Spacer()
+ }
+ }
+}
+
+struct ProviderRow_Previews: PreviewProvider {
+ static var previews: some View {
+ ProviderRow(provider: .Napster)
+ .previewLayout(.fixed(width: 300, height: 70))
+ }
+}
diff --git a/MusicConverter/View/UpdateView.swift b/MusicConverter/View/UpdateView.swift
new file mode 100644
index 0000000..2c1ab02
--- /dev/null
+++ b/MusicConverter/View/UpdateView.swift
@@ -0,0 +1,20 @@
+//
+// Update.swift
+// MusicConverter
+//
+// Created by Finn Behrens on 02.04.21.
+//
+
+import SwiftUI
+
+struct UpdateView: View {
+ var body: some View {
+ Text("UpdateView is not yet implemented")
+ }
+}
+
+struct Update_Previews: PreviewProvider {
+ static var previews: some View {
+ UpdateView()
+ }
+}
diff --git a/MusicConverter/helpers/MusicKITApi.swift b/MusicConverter/helpers/MusicKITApi.swift
new file mode 100644
index 0000000..0b394a3
--- /dev/null
+++ b/MusicConverter/helpers/MusicKITApi.swift
@@ -0,0 +1,348 @@
+//
+// MusicKITApi.swift
+// MusicConverter
+//
+// Created by Finn Behrens on 02.04.21.
+//
+
+import Foundation
+import StoreKit
+import MediaPlayer
+
+class AppleMusicApi: ObservableObject {
+ static let token = Bundle.main.infoDictionary?["MUSIC_KIT_API_KEY"] as! String
+ var userToken: String?
+ var storeFront: StoreFront?
+ var songLoader: URLSession = URLSession(configuration: .default)
+
+ func updateUserToken(completion: @escaping (Result<String, Error>) -> Void) {
+ var err: Error?
+ //let semaphore = DispatchSemaphore(value: 0)
+ SKCloudServiceController().requestCapabilities(completionHandler: { (cloudServiceCapability, error) in
+ guard error == nil else {
+ completion(.failure(error!))
+ err = error
+ return
+ }
+
+ if !cloudServiceCapability.contains(.addToCloudMusicLibrary) {
+ print("error: cannot add items to music library")
+ // TODO: error handling
+ }
+
+ if !cloudServiceCapability.contains(.musicCatalogPlayback) {
+ print("error: cannot play music from catalog")
+ }
+
+ print("got capabilitties")
+ //semaphore.signal()
+ })
+ //semaphore.wait()
+ /*guard err == nil else {
+ print("got error")
+ return
+ }*/
+ SKCloudServiceController().requestUserToken(forDeveloperToken: Self.token) { (recievedToken, error) in
+ guard error == nil else {
+ completion(.failure(error!))
+ return
+ }
+
+ self.userToken = recievedToken
+ completion(.success(recievedToken!))
+ }
+ }
+
+ func updateStoreFrontID(completion: @escaping(Result<String, Error>) -> Void) {
+ guard userToken != nil else {
+ completion(.failure(MusicKitError.NoUserTokenError))
+ return
+ }
+ let musicURL = URL(string: "https://api.music.apple.com/v1/me/storefront")!
+ var musicRequest = URLRequest(url: musicURL)
+ musicRequest.httpMethod = "GET"
+ musicRequest.addValue("Bearer \(Self.token)", forHTTPHeaderField: "Authorization")
+ musicRequest.addValue(self.userToken!, forHTTPHeaderField: "Music-User-Token")
+
+ URLSession.shared.dataTask(with: musicRequest) { (data, response, error) in
+ guard error == nil else {
+ completion(.failure(error!))
+ return
+ }
+
+ do {
+ let decodedData = try JSONDecoder().decode(musicData<StoreFront>.self, from: data!)
+ self.storeFront = decodedData.data.first
+ guard self.storeFront != nil else {
+ completion(.failure(MusicKitError.NoStoreFront))
+ return
+ }
+ completion(.success(self.storeFront!.id))
+ } catch {
+ print("data: \(String(decoding: data!, as: UTF8.self))")
+ completion(.failure(error))
+ return
+ }
+ }
+ .resume()
+ }
+
+ public func fetchForIsrc(isrc: String, completion: @escaping (Result<[Song], Error>) -> Void) {
+ guard userToken != nil else {
+ completion(.failure(MusicKitError.NoUserTokenError))
+ return
+ }
+ guard storeFront != nil else {
+ completion(.failure(MusicKitError.NoStoreFront))
+ return
+ }
+
+ guard let musicURL = URL(string: "https://api.music.apple.com/v1/catalog/\(self.storeFront!.id)/songs?filter[isrc]=\(isrc)") else {
+ completion(.failure(MusicKitError.InvalidUrl))
+ return
+ }
+
+ let musicRequest = self.buildRequest(url: musicURL)
+
+ self.songLoader.dataTask(with: musicRequest) { (data, response, error) in
+ guard error == nil else {
+ completion(.failure(error!))
+ return
+ }
+
+ do {
+ let decodedData = try JSONDecoder().decode(musicData<Song>.self, from: data!)
+ completion(.success(decodedData.data))
+ } catch {
+ print("data: \(String(decoding: data!, as: UTF8.self))")
+ completion(.failure(error))
+ return
+ }
+ }
+ .resume()
+ }
+
+ public func getPicture(song: Song, x: Int = 0, y: Int = 0, dataCompletion: @escaping (Result<Data, Error>) -> Void) {
+ let artwork = song.attributes.artwork
+ guard x < artwork.height && y < artwork.height else {
+ dataCompletion(.failure(MusicKitError.InvalidImageResulotion))
+ return
+ }
+ var x = x
+ var y = y
+ if x == 0 {
+ x = artwork.height
+ }
+ if y == 0 {
+ y = artwork.width
+ }
+
+ var url = artwork.url
+ url = url.replacingOccurrences(of: "{h}", with: x.description)
+ url = url.replacingOccurrences(of: "{w}", with: y.description)
+
+ guard let url = URL(string: url) else {
+ dataCompletion(.failure(MusicKitError.InvalidUrl))
+ return
+ }
+
+ let imageRequest = self.buildRequest(url: url)
+
+ URLSession.shared.dataTask(with: imageRequest) { (data, response, error) in
+ guard error == nil else {
+ dataCompletion(.failure(error!))
+ return
+ }
+
+ dataCompletion(.success(data!))
+ }
+ .resume()
+ }
+
+ public func getPicture(song: Song, x: Int = 0, y: Int = 0, completion: @escaping (Result<UIImage?, Error>) -> Void) {
+ getPicture(song: song, x: x, y: y, dataCompletion: {result in
+ switch result {
+ case .failure(let e):
+ completion(.failure(e))
+ case .success(let d):
+ completion(.success(UIImage(data: d)))
+ }
+ })
+ }
+
+
+ //private func buildRequest(urlString: String) -> URLRequest
+ private func buildRequest(url: URL) -> URLRequest {
+ var musicRequest = URLRequest(url: url)
+ musicRequest.httpMethod = "GET"
+ musicRequest.addValue("Bearer \(Self.token)", forHTTPHeaderField: "Authorization")
+ musicRequest.addValue(self.userToken!, forHTTPHeaderField: "Music-User-Token")
+
+ return musicRequest
+ }
+
+ public func convertPlaylist(playlist: [NATrack], updateCallback: @escaping (NATrack, [Song]) -> Void, completion: @escaping (Error?) -> Void) {
+ var pos = 0;
+
+ var callback: (Result<[Song], Error>) -> Void = {_ in}
+ callback = { result in
+ switch result {
+ case .failure(let e):
+ completion(e)
+ return
+ case .success(let d):
+ print("found \(d.count) songs for \(playlist[pos-1].name)")
+ updateCallback(playlist[pos-1], d)
+ if pos < playlist.count {
+ var curr = playlist[pos]
+ var isrc = curr.isrc
+ if isrc == nil {
+ print("\(curr.name): isrc empty, skipping...")
+ // TODO: more error handling?
+ while isrc == nil {
+ pos += 1;
+ isrc = playlist[pos].isrc
+ }
+ curr = playlist[pos]
+ }
+ pos += 1;
+ self.fetchForIsrc(isrc: isrc!, completion: callback)
+ }
+ }
+ }
+
+ var curr = playlist[pos]
+ var isrc = curr.isrc
+ if isrc == nil {
+ print("\(curr.name): isrc empty, skipping...")
+ // TODO: more error handling?
+ while isrc == nil {
+ pos += 1;
+ isrc = playlist[pos].isrc
+ }
+ curr = playlist[pos]
+ }
+ pos += 1;
+ self.fetchForIsrc(isrc: isrc!, completion: callback)
+ }
+
+ struct musicData<T: Decodable>: Decodable {
+ var data: [T]
+ }
+
+ public struct StoreFront: Decodable {
+ var attributes: Attributes
+ var href: String
+ var id: String
+ var type: String
+
+ public struct Attributes: Decodable {
+ var defaultLanguageTag: String
+ var name: String
+ var supportedLanguageTags: [String]
+ }
+ }
+
+ public class SongInfo {
+ var song: Song
+ var mediaItem: MPMediaItem?
+
+ init (song: Song) {
+ self.song = song
+ }
+
+ var attributes: Song.Attributes {
+ song.attributes
+ }
+
+ var name: String {
+ attributes.name
+ }
+
+ var isrc: String {
+ attributes.isrc
+ }
+
+ var artistName: String {
+ attributes.artistName
+ }
+
+ var albumName: String {
+ attributes.albumName
+ }
+ }
+
+ public struct Song: Decodable {
+ var attributes: Attributes
+
+
+
+ public struct Attributes: Decodable {
+ var albumName: String
+ var artistName: String
+ var artwork: Artwork
+ var composerName: String?
+ var contentRating: String?
+ var discNumber: Int
+ var durationInMillis: Int?
+ // var editorialNotes
+ var genreNames: [String]
+ var isrc: String
+ var name: String
+ var playParams: PlayParameters
+ // var previews: [Preview] (required)
+ var releaseDate: String
+ /// (Required) The number of the song in the album’s track list.
+ var trackNumber: Int
+ /// (Required) The URL for sharing a song in the iTunes Store
+ var url: String
+
+ var isExplicit: Bool? {
+ guard let contentRating = contentRating else {
+ return nil
+ }
+ return contentRating == "explicit"
+ }
+
+ var playbackSeconds: Int? {
+ guard let durationInMillis = durationInMillis else {
+ return nil
+ }
+ return durationInMillis / 1000
+ }
+
+ public struct Artwork: Decodable {
+ /// The average background color of the image.
+ var bgColor: String?
+ /// (Required) The maximum height available for the image.
+ var height: Int
+ /// (Required) The maximum width available for the image.
+ var width: Int
+ /// The primary text color that may be used if the background color is displayed.
+ var textColor1: String?
+ /// The secondary text color that may be used if the background color is displayed.
+ var textColor2: String?
+ /// The tertiary text color that may be used if the background color is displayed.
+ var textColor3: String?
+ /// The final post-tertiary text color that may be used if the background color is displayed.
+ var textColor4: String?
+ /// (Required) The URL to request the image asset. The image filename must be preceded by {w}x{h}, as placeholders for the width and height values as described above (for example, {w}x{h}bb.jpeg).
+ var url: String
+ }
+
+ public struct PlayParameters: Decodable, Identifiable {
+ /// (Required) The ID of the content to use for playback.
+ var id: String
+ /// (Required) The kind of the content to use for playback.
+ var kind: String
+ }
+ }
+ }
+}
+
+enum MusicKitError: Error {
+ case NoUserTokenError
+ case NoStoreFront
+ case InvalidUrl
+ case InvalidImageResulotion
+}
diff --git a/MusicConverter/helpers/NaappleHelper.swift b/MusicConverter/helpers/NaappleHelper.swift
new file mode 100644
index 0000000..913c7d2
--- /dev/null
+++ b/MusicConverter/helpers/NaappleHelper.swift
@@ -0,0 +1,13 @@
+import Foundation
+
+struct naapple {
+ var text = "Hello, World!"
+}
+
+
+protocol NATrack {
+ var name: String { get }
+ var artistName: String { get }
+ var albumName: String { get }
+ var isrc: String? { get }
+}
diff --git a/MusicConverter/helpers/NapsterPlaylistHelper.swift b/MusicConverter/helpers/NapsterPlaylistHelper.swift
new file mode 100644
index 0000000..00e53dc
--- /dev/null
+++ b/MusicConverter/helpers/NapsterPlaylistHelper.swift
@@ -0,0 +1,259 @@
+//
+// napster.swift
+//
+//
+// Created by Finn Behrens on 30.03.21.
+//
+
+import Foundation
+
+let NANapsterApiUrl: String = "https://api.napster.com/v2.2";
+let NANapsterToken: String = "YTkxZTRhNzAtODdlNy00ZjMzLTg0MWItOTc0NmZmNjU4Yzk4";
+
+
+protocol NANapsterSource {
+ var apikey: String { get }
+ var id: String { get }
+ var baseurl: String { get }
+}
+
+class NANapsterPlaylist: NANapsterSource {
+ public var apikey: String = NANapsterToken
+ public var id: String
+ public var baseurl: String {
+ NANapsterApiUrl + "/playlists/" + id
+ }
+ var cachedData: apiJSON?
+
+ var urlSession: URLSession;
+
+ public var name: String? {
+ return cachedData?.playlists.first?.name
+ }
+
+ public var trackCount: Int? {
+ return cachedData?.playlists.first?.trackCount
+ }
+
+ public var description: String? {
+ return cachedData?.playlists.first?.description
+ }
+
+ public var modified: String? {
+ return cachedData?.playlists.first?.modified
+ }
+
+
+ init(id: String, apikey: String = NANapsterToken) {
+ self.id = id
+ self.apikey = apikey
+
+ let config = URLSessionConfiguration.default;
+ config.httpAdditionalHeaders = [ "apikey" : apikey ];
+ self.urlSession = URLSession(configuration: config);
+ }
+
+ public func update(callback: @escaping (Result<(), Error>) -> Void) {
+ print("NSData: \(self.baseurl)")
+ loadJson(fromURLString: self.baseurl, completion: { result in
+ switch result {
+ case .success(let data):
+ self.parse(jsonData: data, callback: callback)
+ case .failure(let error):
+ print(error)
+ }
+ })
+ }
+
+ public func update() {
+ update(callback: {_ in})
+ }
+
+ /// This is the api binding, with a limit of 200
+ public func getTracksApiJson(offset: Int = 0, limit: Int = 200, completion: @escaping (Result<Data, Error>) -> Void) {
+ // check input
+ if cachedData == nil {
+ completion(.failure(NANapsterError.NotCachedError))
+ return
+ }
+ if limit > 200 || limit < 0 {
+ completion(.failure(NANapsterError.LimitError))
+ return
+ }
+ if offset > self.trackCount! || offset < 0 {
+ completion(.failure(NANapsterError.OffsetError))
+ return
+ }
+
+ let url = self.baseurl + "/tracks?offset=\(offset)&limit=\(limit)"
+ loadJson(fromURLString: url, completion: completion)
+ }
+
+ public func getTracks(offset: Int = 0, limit: Int = 0, completion: @escaping (Result<[Track], Error>) -> Void) {
+ guard self.trackCount != nil else {
+ completion(.failure(NANapsterError.NotCachedError))
+ return
+ }
+ if (limit != 0) {
+ print("limit not yet implemented")
+ }
+ // check input
+ if cachedData == nil {
+ completion(.failure(NANapsterError.NotCachedError))
+ return
+ }
+ let totalCount = limit != 0 ? min(self.trackCount! - offset, limit) : self.trackCount! - offset
+ var missingCount = totalCount
+ var list: [Track] = [];
+
+ var callback: (Result<TracksJSON, Error>) -> Void = {_ in}
+ callback = {result in
+ switch result {
+ case .failure(let e):
+ completion(.failure(e))
+ case .success(let d):
+ list += d.tracks
+ if missingCount >= 200 {
+ missingCount -= 200;
+ print("starting at \(offset + (totalCount - missingCount))")
+ self.getTracksApi(offset: offset + (totalCount - missingCount), completion: callback)
+ } else {
+ completion(.success(list))
+ }
+ }
+ }
+
+ // TODO: honour limit
+ print("first at \(offset + (totalCount - missingCount))")
+ getTracksApi(offset: offset, completion: callback)
+
+ }
+
+ /// This is the api binding, with a limit of 200
+ public func getTracksApi(offset: Int = 0, limit: Int = 200, completion: @escaping (Result<TracksJSON, Error>) -> Void) {
+
+ getTracksApiJson(offset: offset, limit: limit) { result in
+ switch result {
+ case .failure(let e):
+ completion(.failure(e))
+ return
+ case .success(let jsonData):
+ do {
+ let decodedData = try JSONDecoder().decode(TracksJSON.self, from: jsonData)
+ completion(.success(decodedData))
+ } catch {
+ completion(.failure(error))
+ }
+ }
+ }
+ }
+
+
+
+ private func parse(jsonData: Data, callback: @escaping (Result<(), Error>) -> Void) {
+ do {
+ let decodedData = try JSONDecoder().decode(apiJSON.self, from: jsonData)
+ self.cachedData = decodedData
+ callback(.success(()))
+ } catch {
+ print("error: \(error)")
+ callback(.failure(error))
+ }
+ }
+
+ private func loadJson(fromURLString urlString: String, completion: @escaping (Result<Data, Error>) -> Void) {
+ if let url = URL(string: urlString) {
+ let urlSession = self.urlSession.dataTask(with: url) { (data, response, error) in
+ if let error = error {
+ completion(.failure(error))
+ }
+
+ if let data = data {
+ completion(.success(data))
+ }
+
+ }
+ urlSession.resume();
+ }
+ }
+
+
+ struct apiJSON: Decodable {
+ var meta: Meta
+ var playlists: [Playlist]
+
+ struct Playlist: Decodable {
+ var type: String
+ var id: String
+ var name: String
+ var modified: String // TODO
+ var trackCount: Int
+ var privacy: String
+ // images
+ var description: String
+ // favoriteCount
+ var freePlayCompliant: Bool
+ // links
+ }
+
+ }
+
+ struct TracksJSON: Decodable {
+ var meta: Meta
+ var tracks: [Track]
+ }
+
+ struct Track: Decodable, NATrack {
+ var type: String
+ var id: String
+ var index: Int
+ var disc: Int
+ var href: String
+ var playbackSeconds: Int
+ var isExplicit: Bool
+ var isStreamable: Bool
+ var isAvailableInHiRes: Bool
+ var name: String
+ /// international standart record code
+ var isrc: String?
+ var shortcut: String
+ // blurbs
+ var artistId: String
+ var artistName: String
+ var albumName: String
+ var formats: [Format]
+ var losslessFormats: [Format]
+ var albumId: String
+ // contributors
+ // links
+ var previewURL: String?
+
+ struct Format: Decodable {
+ var type: String
+ var bitrate: Int
+ var name: String
+ var sampleBits: Int
+ var sampleRate: Int
+ }
+ }
+
+ struct Meta: Decodable {
+ var returnedCount: Int
+ var totalCount: Int?
+ var query: Query?
+
+ struct Query: Decodable {
+ var limit: Int?
+ var offset: Int?
+ var next: String?
+ var previos: String?
+ }
+ }
+}
+
+
+enum NANapsterError: Error {
+ case LimitError
+ case OffsetError
+ case NotCachedError
+}