├── .gitignore
├── LICENSE
├── MusicKitPlayer.xcodeproj
├── MusicKit_Info.plist
├── project.pbxproj
└── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ ├── IDEWorkspaceChecks.plist
│ └── WorkspaceSettings.xcsettings
├── Package.swift
├── README.md
├── Sources
└── MusicKitPlayer
│ ├── API
│ ├── API.swift
│ ├── Library.swift
│ ├── MusicKitPlayer.swift
│ ├── MusicKitUtils.swift
│ ├── Player.swift
│ └── Queue.swift
│ ├── Info.plist
│ ├── Internal
│ ├── AuthorizationWindow.swift
│ ├── MKDecoder.swift
│ ├── MKWebController.swift
│ ├── MKWebpage.swift
│ └── URLRequestManager.swift
│ ├── Managers
│ ├── NowPlayingManager.swift
│ └── QueueManager.swift
│ ├── MusicKitPlayer.h
│ └── Utils
│ ├── ArrayUtil.swift
│ └── StringFormatUtil.swift
└── jwt.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | .DS_Store
6 |
7 | ## User settings
8 | xcuserdata/
9 |
10 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
11 | *.xcscmblueprint
12 | *.xccheckout
13 |
14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
15 | build/
16 | DerivedData/
17 | *.moved-aside
18 | *.pbxuser
19 | !default.pbxuser
20 | *.mode1v3
21 | !default.mode1v3
22 | *.mode2v3
23 | !default.mode2v3
24 | *.perspectivev3
25 | !default.perspectivev3
26 |
27 | ## Obj-C/Swift specific
28 | *.hmap
29 |
30 | ## App packaging
31 | *.ipa
32 | *.dSYM.zip
33 | *.dSYM
34 |
35 | ## Playgrounds
36 | timeline.xctimeline
37 | playground.xcworkspace
38 |
39 | # Swift Package Manager
40 | #
41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
42 | # Packages/
43 | # Package.pins
44 | # Package.resolved
45 | # *.xcodeproj
46 | #
47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
48 | # hence it is not needed unless you have added a package configuration file to your project
49 | # .swiftpm
50 |
51 | .build/
52 |
53 | # CocoaPods
54 | #
55 | # We recommend against adding the Pods directory to your .gitignore. However
56 | # you should judge for yourself, the pros and cons are mentioned at:
57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
58 | #
59 | # Pods/
60 | #
61 | # Add this line if you want to avoid checking in source code from the Xcode workspace
62 | # *.xcworkspace
63 |
64 | # Carthage
65 | #
66 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
67 | # Carthage/Checkouts
68 |
69 | Carthage/Build/
70 |
71 | # Accio dependency management
72 | Dependencies/
73 | .accio/
74 |
75 | # fastlane
76 | #
77 | # It is recommended to not store the screenshots in the git repo.
78 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
79 | # For more information about the recommended setup visit:
80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
81 |
82 | fastlane/report.xml
83 | fastlane/Preview.html
84 | fastlane/screenshots/**/*.png
85 | fastlane/test_output
86 |
87 | # Code Injection
88 | #
89 | # After new code Injection tools there's a generated folder /iOSInjectionProject
90 | # https://github.com/johnno1962/injectionforxcode
91 |
92 | iOSInjectionProject/
93 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Nate Thompson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MusicKitPlayer.xcodeproj/MusicKit_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/MusicKitPlayer.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 43ABBA8F24B599D30027CA77 /* URLRequestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43ABBA8E24B599D30027CA77 /* URLRequestManager.swift */; };
11 | 43CBD2F6242F2F550023F7B7 /* MKWebpage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CBD2F5242F2F550023F7B7 /* MKWebpage.swift */; };
12 | OBJ_37 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* API.swift */; };
13 | OBJ_38 /* Library.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* Library.swift */; };
14 | OBJ_39 /* MusicKitPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_14 /* MusicKitPlayer.swift */; };
15 | OBJ_40 /* MusicKitUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* MusicKitUtils.swift */; };
16 | OBJ_41 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_16 /* Player.swift */; };
17 | OBJ_42 /* Queue.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_17 /* Queue.swift */; };
18 | OBJ_43 /* AuthorizationWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_19 /* AuthorizationWindow.swift */; };
19 | OBJ_44 /* MKDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_20 /* MKDecoder.swift */; };
20 | OBJ_45 /* MKWebController.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_21 /* MKWebController.swift */; };
21 | OBJ_46 /* NowPlayingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_23 /* NowPlayingManager.swift */; };
22 | OBJ_47 /* QueueManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_24 /* QueueManager.swift */; };
23 | OBJ_48 /* ArrayUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_26 /* ArrayUtil.swift */; };
24 | OBJ_49 /* StringFormatUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_27 /* StringFormatUtil.swift */; };
25 | OBJ_56 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; };
26 | /* End PBXBuildFile section */
27 |
28 | /* Begin PBXCopyFilesBuildPhase section */
29 | 43CBD2F3242F07800023F7B7 /* CopyFiles */ = {
30 | isa = PBXCopyFilesBuildPhase;
31 | buildActionMask = 2147483647;
32 | dstPath = "";
33 | dstSubfolderSpec = 7;
34 | files = (
35 | );
36 | runOnlyForDeploymentPostprocessing = 0;
37 | };
38 | /* End PBXCopyFilesBuildPhase section */
39 |
40 | /* Begin PBXFileReference section */
41 | 43ABBA8E24B599D30027CA77 /* URLRequestManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestManager.swift; sourceTree = ""; };
42 | 43CBD2F5242F2F550023F7B7 /* MKWebpage.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = MKWebpage.swift; sourceTree = ""; tabWidth = 4; };
43 | "MusicKit::MusicKit::Product" /* MusicKitPlayer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MusicKitPlayer.framework; sourceTree = BUILT_PRODUCTS_DIR; };
44 | OBJ_10 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
45 | OBJ_12 /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; };
46 | OBJ_13 /* Library.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Library.swift; sourceTree = ""; };
47 | OBJ_14 /* MusicKitPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicKitPlayer.swift; sourceTree = ""; };
48 | OBJ_15 /* MusicKitUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicKitUtils.swift; sourceTree = ""; };
49 | OBJ_16 /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; };
50 | OBJ_17 /* Queue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Queue.swift; sourceTree = ""; };
51 | OBJ_19 /* AuthorizationWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationWindow.swift; sourceTree = ""; };
52 | OBJ_20 /* MKDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MKDecoder.swift; sourceTree = ""; };
53 | OBJ_21 /* MKWebController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MKWebController.swift; sourceTree = ""; };
54 | OBJ_23 /* NowPlayingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingManager.swift; sourceTree = ""; };
55 | OBJ_24 /* QueueManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueManager.swift; sourceTree = ""; };
56 | OBJ_26 /* ArrayUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayUtil.swift; sourceTree = ""; };
57 | OBJ_27 /* StringFormatUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringFormatUtil.swift; sourceTree = ""; };
58 | OBJ_31 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
59 | OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; };
60 | OBJ_9 /* MusicKitPlayer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MusicKitPlayer.h; sourceTree = ""; };
61 | /* End PBXFileReference section */
62 |
63 | /* Begin PBXFrameworksBuildPhase section */
64 | OBJ_50 /* Frameworks */ = {
65 | isa = PBXFrameworksBuildPhase;
66 | buildActionMask = 0;
67 | files = (
68 | );
69 | runOnlyForDeploymentPostprocessing = 0;
70 | };
71 | /* End PBXFrameworksBuildPhase section */
72 |
73 | /* Begin PBXGroup section */
74 | OBJ_11 /* API */ = {
75 | isa = PBXGroup;
76 | children = (
77 | OBJ_14 /* MusicKitPlayer.swift */,
78 | OBJ_16 /* Player.swift */,
79 | OBJ_17 /* Queue.swift */,
80 | OBJ_12 /* API.swift */,
81 | OBJ_13 /* Library.swift */,
82 | OBJ_15 /* MusicKitUtils.swift */,
83 | );
84 | path = API;
85 | sourceTree = "";
86 | };
87 | OBJ_18 /* Internal */ = {
88 | isa = PBXGroup;
89 | children = (
90 | 43CBD2F5242F2F550023F7B7 /* MKWebpage.swift */,
91 | OBJ_21 /* MKWebController.swift */,
92 | OBJ_20 /* MKDecoder.swift */,
93 | 43ABBA8E24B599D30027CA77 /* URLRequestManager.swift */,
94 | OBJ_19 /* AuthorizationWindow.swift */,
95 | );
96 | path = Internal;
97 | sourceTree = "";
98 | };
99 | OBJ_22 /* Managers */ = {
100 | isa = PBXGroup;
101 | children = (
102 | OBJ_23 /* NowPlayingManager.swift */,
103 | OBJ_24 /* QueueManager.swift */,
104 | );
105 | path = Managers;
106 | sourceTree = "";
107 | };
108 | OBJ_25 /* Utils */ = {
109 | isa = PBXGroup;
110 | children = (
111 | OBJ_26 /* ArrayUtil.swift */,
112 | OBJ_27 /* StringFormatUtil.swift */,
113 | );
114 | path = Utils;
115 | sourceTree = "";
116 | };
117 | OBJ_29 /* Products */ = {
118 | isa = PBXGroup;
119 | children = (
120 | "MusicKit::MusicKit::Product" /* MusicKitPlayer.framework */,
121 | );
122 | name = Products;
123 | sourceTree = BUILT_PRODUCTS_DIR;
124 | };
125 | OBJ_5 = {
126 | isa = PBXGroup;
127 | children = (
128 | OBJ_6 /* Package.swift */,
129 | OBJ_7 /* Sources */,
130 | OBJ_29 /* Products */,
131 | OBJ_31 /* README.md */,
132 | );
133 | sourceTree = "";
134 | };
135 | OBJ_7 /* Sources */ = {
136 | isa = PBXGroup;
137 | children = (
138 | OBJ_8 /* MusicKitPlayer */,
139 | );
140 | name = Sources;
141 | sourceTree = SOURCE_ROOT;
142 | };
143 | OBJ_8 /* MusicKitPlayer */ = {
144 | isa = PBXGroup;
145 | children = (
146 | OBJ_9 /* MusicKitPlayer.h */,
147 | OBJ_10 /* Info.plist */,
148 | OBJ_11 /* API */,
149 | OBJ_18 /* Internal */,
150 | OBJ_22 /* Managers */,
151 | OBJ_25 /* Utils */,
152 | );
153 | name = MusicKitPlayer;
154 | path = Sources/MusicKitPlayer;
155 | sourceTree = SOURCE_ROOT;
156 | };
157 | /* End PBXGroup section */
158 |
159 | /* Begin PBXNativeTarget section */
160 | "MusicKit::MusicKit" /* MusicKitPlayer */ = {
161 | isa = PBXNativeTarget;
162 | buildConfigurationList = OBJ_33 /* Build configuration list for PBXNativeTarget "MusicKitPlayer" */;
163 | buildPhases = (
164 | OBJ_36 /* Sources */,
165 | 43CBD2F3242F07800023F7B7 /* CopyFiles */,
166 | OBJ_50 /* Frameworks */,
167 | );
168 | buildRules = (
169 | );
170 | dependencies = (
171 | );
172 | name = MusicKitPlayer;
173 | productName = MusicKit;
174 | productReference = "MusicKit::MusicKit::Product" /* MusicKitPlayer.framework */;
175 | productType = "com.apple.product-type.framework";
176 | };
177 | "MusicKit::SwiftPMPackageDescription" /* MusicKitPlayerPackageDescription */ = {
178 | isa = PBXNativeTarget;
179 | buildConfigurationList = OBJ_52 /* Build configuration list for PBXNativeTarget "MusicKitPlayerPackageDescription" */;
180 | buildPhases = (
181 | OBJ_55 /* Sources */,
182 | );
183 | buildRules = (
184 | );
185 | dependencies = (
186 | );
187 | name = MusicKitPlayerPackageDescription;
188 | productName = MusicKitPackageDescription;
189 | productType = "com.apple.product-type.framework";
190 | };
191 | /* End PBXNativeTarget section */
192 |
193 | /* Begin PBXProject section */
194 | OBJ_1 /* Project object */ = {
195 | isa = PBXProject;
196 | attributes = {
197 | LastSwiftMigration = 9999;
198 | LastUpgradeCheck = 9999;
199 | };
200 | buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "MusicKitPlayer" */;
201 | compatibilityVersion = "Xcode 3.2";
202 | developmentRegion = en;
203 | hasScannedForEncodings = 0;
204 | knownRegions = (
205 | en,
206 | );
207 | mainGroup = OBJ_5;
208 | productRefGroup = OBJ_29 /* Products */;
209 | projectDirPath = "";
210 | projectRoot = "";
211 | targets = (
212 | "MusicKit::MusicKit" /* MusicKitPlayer */,
213 | "MusicKit::SwiftPMPackageDescription" /* MusicKitPlayerPackageDescription */,
214 | );
215 | };
216 | /* End PBXProject section */
217 |
218 | /* Begin PBXSourcesBuildPhase section */
219 | OBJ_36 /* Sources */ = {
220 | isa = PBXSourcesBuildPhase;
221 | buildActionMask = 0;
222 | files = (
223 | OBJ_37 /* API.swift in Sources */,
224 | OBJ_38 /* Library.swift in Sources */,
225 | OBJ_39 /* MusicKitPlayer.swift in Sources */,
226 | OBJ_40 /* MusicKitUtils.swift in Sources */,
227 | OBJ_41 /* Player.swift in Sources */,
228 | OBJ_42 /* Queue.swift in Sources */,
229 | OBJ_43 /* AuthorizationWindow.swift in Sources */,
230 | OBJ_44 /* MKDecoder.swift in Sources */,
231 | OBJ_45 /* MKWebController.swift in Sources */,
232 | 43CBD2F6242F2F550023F7B7 /* MKWebpage.swift in Sources */,
233 | OBJ_46 /* NowPlayingManager.swift in Sources */,
234 | OBJ_47 /* QueueManager.swift in Sources */,
235 | OBJ_48 /* ArrayUtil.swift in Sources */,
236 | 43ABBA8F24B599D30027CA77 /* URLRequestManager.swift in Sources */,
237 | OBJ_49 /* StringFormatUtil.swift in Sources */,
238 | );
239 | runOnlyForDeploymentPostprocessing = 0;
240 | };
241 | OBJ_55 /* Sources */ = {
242 | isa = PBXSourcesBuildPhase;
243 | buildActionMask = 0;
244 | files = (
245 | OBJ_56 /* Package.swift in Sources */,
246 | );
247 | runOnlyForDeploymentPostprocessing = 0;
248 | };
249 | /* End PBXSourcesBuildPhase section */
250 |
251 | /* Begin XCBuildConfiguration section */
252 | OBJ_3 /* Debug */ = {
253 | isa = XCBuildConfiguration;
254 | buildSettings = {
255 | CLANG_ENABLE_OBJC_ARC = YES;
256 | COMBINE_HIDPI_IMAGES = YES;
257 | COPY_PHASE_STRIP = NO;
258 | DEBUG_INFORMATION_FORMAT = dwarf;
259 | DYLIB_INSTALL_NAME_BASE = "@rpath";
260 | ENABLE_NS_ASSERTIONS = YES;
261 | GCC_OPTIMIZATION_LEVEL = 0;
262 | GCC_PREPROCESSOR_DEFINITIONS = (
263 | "$(inherited)",
264 | "SWIFT_PACKAGE=1",
265 | "DEBUG=1",
266 | );
267 | MACOSX_DEPLOYMENT_TARGET = 10.10;
268 | ONLY_ACTIVE_ARCH = YES;
269 | OTHER_SWIFT_FLAGS = "$(inherited) -DXcode";
270 | PRODUCT_NAME = "$(TARGET_NAME)";
271 | SDKROOT = macosx;
272 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator";
273 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE DEBUG";
274 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
275 | USE_HEADERMAP = NO;
276 | };
277 | name = Debug;
278 | };
279 | OBJ_34 /* Debug */ = {
280 | isa = XCBuildConfiguration;
281 | buildSettings = {
282 | ENABLE_TESTABILITY = YES;
283 | FRAMEWORK_SEARCH_PATHS = (
284 | "$(inherited)",
285 | "$(PLATFORM_DIR)/Developer/Library/Frameworks",
286 | );
287 | HEADER_SEARCH_PATHS = "$(inherited)";
288 | INFOPLIST_FILE = MusicKitPlayer.xcodeproj/MusicKit_Info.plist;
289 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx";
290 | MACOSX_DEPLOYMENT_TARGET = 10.14;
291 | MARKETING_VERSION = 0.1;
292 | OTHER_CFLAGS = "$(inherited)";
293 | OTHER_LDFLAGS = "$(inherited)";
294 | OTHER_SWIFT_FLAGS = "$(inherited)";
295 | PRODUCT_BUNDLE_IDENTIFIER = MusicKitPlayer;
296 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
297 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
298 | SKIP_INSTALL = YES;
299 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
300 | SWIFT_VERSION = 5.0;
301 | TARGET_NAME = MusicKitPlayer;
302 | };
303 | name = Debug;
304 | };
305 | OBJ_35 /* Release */ = {
306 | isa = XCBuildConfiguration;
307 | buildSettings = {
308 | ENABLE_TESTABILITY = YES;
309 | FRAMEWORK_SEARCH_PATHS = (
310 | "$(inherited)",
311 | "$(PLATFORM_DIR)/Developer/Library/Frameworks",
312 | );
313 | HEADER_SEARCH_PATHS = "$(inherited)";
314 | INFOPLIST_FILE = MusicKitPlayer.xcodeproj/MusicKit_Info.plist;
315 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx";
316 | MACOSX_DEPLOYMENT_TARGET = 10.14;
317 | MARKETING_VERSION = 0.1;
318 | OTHER_CFLAGS = "$(inherited)";
319 | OTHER_LDFLAGS = "$(inherited)";
320 | OTHER_SWIFT_FLAGS = "$(inherited)";
321 | PRODUCT_BUNDLE_IDENTIFIER = MusicKitPlayer;
322 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
323 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
324 | SKIP_INSTALL = YES;
325 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
326 | SWIFT_VERSION = 5.0;
327 | TARGET_NAME = MusicKitPlayer;
328 | };
329 | name = Release;
330 | };
331 | OBJ_4 /* Release */ = {
332 | isa = XCBuildConfiguration;
333 | buildSettings = {
334 | CLANG_ENABLE_OBJC_ARC = YES;
335 | COMBINE_HIDPI_IMAGES = YES;
336 | COPY_PHASE_STRIP = YES;
337 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
338 | DYLIB_INSTALL_NAME_BASE = "@rpath";
339 | GCC_OPTIMIZATION_LEVEL = s;
340 | GCC_PREPROCESSOR_DEFINITIONS = (
341 | "$(inherited)",
342 | "SWIFT_PACKAGE=1",
343 | );
344 | MACOSX_DEPLOYMENT_TARGET = 10.10;
345 | OTHER_SWIFT_FLAGS = "$(inherited) -DXcode";
346 | PRODUCT_NAME = "$(TARGET_NAME)";
347 | SDKROOT = macosx;
348 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator";
349 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE";
350 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
351 | USE_HEADERMAP = NO;
352 | };
353 | name = Release;
354 | };
355 | OBJ_53 /* Debug */ = {
356 | isa = XCBuildConfiguration;
357 | buildSettings = {
358 | LD = /usr/bin/true;
359 | OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -package-description-version 5.1";
360 | PRODUCT_NAME = MusicKitPlayerPackageDescription;
361 | SWIFT_VERSION = 5.0;
362 | };
363 | name = Debug;
364 | };
365 | OBJ_54 /* Release */ = {
366 | isa = XCBuildConfiguration;
367 | buildSettings = {
368 | LD = /usr/bin/true;
369 | OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -package-description-version 5.1";
370 | PRODUCT_NAME = MusicKitPlayerPackageDescription;
371 | SWIFT_VERSION = 5.0;
372 | };
373 | name = Release;
374 | };
375 | /* End XCBuildConfiguration section */
376 |
377 | /* Begin XCConfigurationList section */
378 | OBJ_2 /* Build configuration list for PBXProject "MusicKitPlayer" */ = {
379 | isa = XCConfigurationList;
380 | buildConfigurations = (
381 | OBJ_3 /* Debug */,
382 | OBJ_4 /* Release */,
383 | );
384 | defaultConfigurationIsVisible = 0;
385 | defaultConfigurationName = Release;
386 | };
387 | OBJ_33 /* Build configuration list for PBXNativeTarget "MusicKitPlayer" */ = {
388 | isa = XCConfigurationList;
389 | buildConfigurations = (
390 | OBJ_34 /* Debug */,
391 | OBJ_35 /* Release */,
392 | );
393 | defaultConfigurationIsVisible = 0;
394 | defaultConfigurationName = Release;
395 | };
396 | OBJ_52 /* Build configuration list for PBXNativeTarget "MusicKitPlayerPackageDescription" */ = {
397 | isa = XCConfigurationList;
398 | buildConfigurations = (
399 | OBJ_53 /* Debug */,
400 | OBJ_54 /* Release */,
401 | );
402 | defaultConfigurationIsVisible = 0;
403 | defaultConfigurationName = Release;
404 | };
405 | /* End XCConfigurationList section */
406 | };
407 | rootObject = OBJ_1 /* Project object */;
408 | }
409 |
--------------------------------------------------------------------------------
/MusicKitPlayer.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/MusicKitPlayer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/MusicKitPlayer.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "MusicKitPlayer",
8 | platforms: [
9 | .macOS(.v10_14),
10 | ],
11 | products: [
12 | // Products define the executables and libraries produced by a package, and make them visible to other packages.
13 | .library(
14 | name: "MusicKitPlayer",
15 | targets: ["MusicKitPlayer"]),
16 | ],
17 | dependencies: [
18 | // Dependencies declare other packages that this package depends on.
19 | // .package(url: /* package url */, from: "1.0.0"),
20 | ],
21 | targets: [
22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
24 | .target(
25 | name: "MusicKitPlayer",
26 | dependencies: []),
27 | ]
28 | )
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MusicKit-macOS
2 | A Swift framework that brings Apple Music to the Mac.
3 |
4 | This framework is essentially a Swift wrapper around Apple's [MusicKit JS](https://developer.apple.com/documentation/musickitjs) API, with a structure very similar to that of MusicKit JS.
5 |
6 | Internally, the JS framework runs in a web view and is interfaced with by sending it JavaScript strings and interpreting the response. Because of this, MusicKit for Mac is highly asynchronous, though a lot of care has been put into making the API as easy to use as possible.
7 |
8 | ## Configuration
9 | MusicKit needs to be authenticated with a developer token. You can follow [Apple's documentation](https://developer.apple.com/documentation/applemusicapi/getting_keys_and_creating_tokens) on creating and signing a token. [This JavaScript file](/jwt.js) may help with generating tokens. Keep in mind that these tokens expire after a maximum of 6 months.
10 |
11 | After your app launches, configure MusicKit with your developer token. When `onSuccess` is called, MusicKit is set up and the rest of the API can be used.
12 | ```swift
13 | MusicKit.shared.configure(
14 | withDeveloperToken: "...",
15 | appName: "My App",
16 | appBuild:"1.0",
17 | onSuccess: {
18 | // MusicKit is ready to use!
19 | }, onError: { error in
20 | // Error configuring or loading
21 | })
22 | ```
23 |
24 | ## Usage
25 | After configuration, users can sign in to Apple Music. The authorization UI is completely handled by MusicKit for Mac and can be invoked with this simple function call:
26 | ```swift
27 | MusicKit.shared.authorize(onSuccess: { _ in
28 | // Signed in!
29 | }, onError: { error in
30 | // Error signing in
31 | })
32 | ```
33 |
34 | Now we're ready to play some music. Use a playlist URL to set the playlist to the queue and play it:
35 | ```swift
36 | let rushPlaylistURL = "https://itunes.apple.com/us/playlist/rush-deep-cuts/pl.20e85b7fb46347479317bd6b0fb5f0d0"
37 | MusicKit.shared.setQueue(url: rushPlaylistURL, onSuccess: {
38 | MusicKit.shared.player.play()
39 | }, onError: { error in
40 | // Error setting queue to URL
41 | })
42 | ```
43 |
44 | To control playback, use the player object:
45 | ```swift
46 | MusicKit.shared.player.pause()
47 |
48 | MusicKit.shared.player.skipToNextItem()
49 |
50 | MusicKit.shared.player.seek(to: 30.0)
51 |
52 | MusicKit.shared.player.getCurrentPlaybackProgress { progress in
53 | print(progress)
54 | }
55 | ```
56 |
57 | MusicKit also has a bunch of event listeners. Here's an example that prints the name the currently playing media item when it changes:
58 | ```swift
59 | MusicKit.shared.addEventListener(event: .mediaItemDidChange) {
60 | MusicKit.shared.player.getNowPlayingItem { nowPlayingItem in
61 | if let item = nowPlayingItem {
62 | print(item.attributes.name)
63 | }
64 | }
65 | }
66 | ```
67 |
--------------------------------------------------------------------------------
/Sources/MusicKitPlayer/API/API.swift:
--------------------------------------------------------------------------------
1 | //
2 | // API.swift
3 | // MusicKitPlayer
4 | //
5 | // Created by Nate Thompson on 2/11/20.
6 | // Copyright © 2020 Nate Thompson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Represents the Apple Music API
12 | open class API {
13 | public var library: Library
14 |
15 | private var mkWebController: MKWebController
16 |
17 | init(webController: MKWebController) {
18 | mkWebController = webController
19 | library = Library(webController: webController)
20 | }
21 |
22 | public func getRecommendations(
23 | onSuccess: @escaping ([Recommendation]) -> Void,
24 | onError: @escaping (Error) -> Void)
25 | {
26 | mkWebController.evaluateJavaScriptWithPromise(
27 | "music.api.recommendations()",
28 | type: [Recommendation].self,
29 | decodingStrategy: .jsonSerialization,
30 | onSuccess: onSuccess,
31 | onError: onError)
32 | }
33 |
34 | /// Fetch the recently played resources for the user.
35 | public func getRecentPlayed(
36 | limit: Int = 10,
37 | offset: Int = 0,
38 | onSuccess: @escaping ([MediaCollection]) -> Void,
39 | onError: @escaping (Error) -> Void)
40 | {
41 | mkWebController.evaluateJavaScriptWithPromise(
42 | "music.api.recentPlayed({ limit: \(limit), offset: \(offset) })",
43 | type: [MediaCollection].self,
44 | decodingStrategy: .jsonSerialization,
45 | onSuccess: onSuccess,
46 | onError: onError)
47 | }
48 |
49 | /// Fetch the resources in heavy rotation for the user.
50 | public func getHeavyRotation(
51 | limit: Int = 10,
52 | offset: Int = 0,
53 | onSuccess: @escaping ([MediaCollection]) -> Void,
54 | onError: @escaping (Error) -> Void)
55 | {
56 | mkWebController.evaluateJavaScriptWithPromise(
57 | "music.api.historyHeavyRotation({ limit: \(limit), offset: \(offset) })",
58 | type: [MediaCollection].self,
59 | decodingStrategy: .jsonSerialization,
60 | onSuccess: onSuccess,
61 | onError: onError)
62 | }
63 |
64 | public func addToLibrary(
65 | songs: [MediaID],
66 | onSuccess: (() -> Void)? = nil,
67 | onError: @escaping (Error) -> Void)
68 | {
69 | addToLibrary(songs: songs, albums: nil, playlists: nil, onSuccess: onSuccess, onError: onError)
70 | }
71 |
72 | public func addToLibrary(
73 | albums: [MediaID],
74 | onSuccess: (() -> Void)? = nil,
75 | onError: @escaping (Error) -> Void)
76 | {
77 | addToLibrary(songs: nil, albums: albums, playlists: nil, onSuccess: onSuccess, onError: onError)
78 | }
79 |
80 | public func addToLibrary(
81 | playlists: [MediaID],
82 | onSuccess: (() -> Void)? = nil,
83 | onError: @escaping (Error) -> Void)
84 | {
85 | addToLibrary(songs: nil, albums: nil, playlists: playlists, onSuccess: onSuccess, onError: onError)
86 | }
87 |
88 | public func addToLibrary(
89 | songs: [MediaID]?,
90 | albums: [MediaID]?,
91 | playlists: [MediaID]?,
92 | onSuccess: (() -> Void)? = nil,
93 | onError: @escaping (Error) -> Void)
94 | {
95 | var params = [String]()
96 | if let songs = songs {
97 | params.append("songs: \(songs.description)")
98 | }
99 | if let albums = albums {
100 | params.append("albums: \(albums.description)")
101 | }
102 | if let playlists = playlists {
103 | params.append("playlists: \(playlists.description)")
104 | }
105 | let paramsString = params.joined(separator: ", ")
106 |
107 | mkWebController.evaluateJavaScriptWithPromise(
108 | "music.api.addToLibrary({ \(paramsString) })",
109 | onSuccess: onSuccess,
110 | onError: onError)
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/Sources/MusicKitPlayer/API/Library.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Library.swift
3 | // MusicKitPlayer
4 | //
5 | // Created by Nate Thompson on 2/11/20.
6 | // Copyright © 2020 Nate Thompson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Represents the user's cloud library
12 | open class Library {
13 | private var mkWebController: MKWebController
14 |
15 | init(webController: MKWebController) {
16 | mkWebController = webController
17 | }
18 |
19 | public func getSong(
20 | id: MediaID,
21 | onSuccess: @escaping (Song) -> Void,
22 | onError: @escaping (Error) -> Void)
23 | {
24 | URLRequestManager.shared.request(
25 | "https://api.music.apple.com/v1/me/library/songs/\(id.description)",
26 | requiresUserToken: false,
27 | type: [Song].self,
28 | decodingStrategy: .jsonSerialization,
29 | onSuccess: { songs in
30 | if !songs.isEmpty {
31 | onSuccess(songs[0])
32 | } else {
33 | onError(MKError.emptyResponse)
34 | }
35 | },
36 | onError: onError)
37 | }
38 |
39 |
40 | public func getSongs(
41 | ids: [MediaID],
42 | onSuccess: @escaping ([Song]) -> Void,
43 | onError: @escaping (Error) -> Void)
44 | {
45 | var idsFormatted = ""
46 | for id in ids {
47 | idsFormatted.append(id)
48 | idsFormatted.append(",")
49 | }
50 | URLRequestManager.shared.request(
51 | "https://api.music.apple.com/v1/me/library/songs?ids=\(idsFormatted)",
52 | requiresUserToken: true,
53 | type: [Song].self,
54 | decodingStrategy: .jsonSerialization,
55 | onSuccess: onSuccess,
56 | onError: onError)
57 | }
58 |
59 |
60 | public func getSongs(
61 | limit: Int = 25,
62 | offset: Int = 0,
63 | onSuccess: @escaping ([Song], Metadata?) -> Void,
64 | onError: @escaping (Error) -> Void)
65 | {
66 | URLRequestManager.shared.request(
67 | "https://api.music.apple.com/v1/me/library/songs?limit=\(limit.description)&offset=\(offset.description)",
68 | requiresUserToken: true,
69 | type: [Song].self,
70 | decodingStrategy: .jsonSerialization,
71 | onSuccess: onSuccess,
72 | onError: onError)
73 | }
74 |
75 |
76 | public func getAlbums(
77 | ids: [MediaID],
78 | onSuccess: @escaping ([Album]) -> Void,
79 | onError: @escaping (Error) -> Void)
80 | {
81 | let jsString = "music.api.library.albums(\(ids.count > 0 ? ids.description : "null"), null)"
82 | mkWebController.evaluateJavaScriptWithPromise(
83 | jsString,
84 | type: [Album].self,
85 | decodingStrategy: .jsonSerialization,
86 | onSuccess: onSuccess,
87 | onError: onError)
88 | }
89 |
90 | public func getAlbums(
91 | limit: Int,
92 | offset: Int = 0,
93 | onSuccess: @escaping ([Album]) -> Void,
94 | onError: @escaping (Error) -> Void)
95 | {
96 | let jsString = "music.api.library.albums(null, { limit: \(limit), offset: \(offset) })"
97 | mkWebController.evaluateJavaScriptWithPromise(
98 | jsString,
99 | type: [Album].self,
100 | decodingStrategy: .jsonSerialization,
101 | onSuccess: onSuccess,
102 | onError: onError)
103 | }
104 |
105 | public func getPlaylists(
106 | ids: [MediaID],
107 | onSuccess: @escaping ([LibraryPlaylist]) -> Void,
108 | onError: @escaping (Error) -> Void)
109 | {
110 | let jsString = "music.api.library.playlists(\(ids.count > 0 ? ids.description : "null"), null)"
111 | mkWebController.evaluateJavaScriptWithPromise(
112 | jsString,
113 | type: [LibraryPlaylist].self,
114 | decodingStrategy: .jsonSerialization,
115 | onSuccess: onSuccess,
116 | onError: onError)
117 | }
118 |
119 | public func getPlaylists(
120 | limit: Int,
121 | offset: Int = 0,
122 | onSuccess: @escaping ([LibraryPlaylist]) -> Void,
123 | onError: @escaping (Error) -> Void)
124 | {
125 | let jsString = "music.api.library.playlists(null, { limit: \(limit), offset: \(offset) })"
126 | mkWebController.evaluateJavaScriptWithPromise(
127 | jsString,
128 | type: [LibraryPlaylist].self,
129 | decodingStrategy: .jsonSerialization,
130 | onSuccess: onSuccess,
131 | onError: onError)
132 | }
133 |
134 | public func getSongs(
135 | inPlaylist id: MediaID,
136 | limit: Int = 10,
137 | offset: Int = 0,
138 | onSuccess: @escaping ([Song], Metadata?) -> Void,
139 | onError: @escaping (Error) -> Void)
140 | {
141 | URLRequestManager.shared.request(
142 | "https://api.music.apple.com/v1/me/library/playlists/\(id)/tracks?limit=\(limit)&offset=\(offset)",
143 | requiresUserToken: true,
144 | type: [Song].self,
145 | decodingStrategy: .jsonSerialization,
146 | onSuccess: onSuccess,
147 | onError: onError)
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/Sources/MusicKitPlayer/API/MusicKitPlayer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicKitPlayer.swift
3 | // MusicKitPlayer
4 | //
5 | // Created by Nate Thompson on 2/10/20.
6 | // Copyright © 2020 Nate Thompson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | open class MusicKitPlayer {
12 |
13 | static public var shared = MusicKitPlayer()
14 | private var mkWebController = MKWebController()
15 |
16 | public var player: Player
17 | public var api: API
18 |
19 | init() {
20 | player = Player(webController: mkWebController)
21 | api = API(webController: mkWebController)
22 | }
23 |
24 | /// Configures MusicKit. This must be called before MusicKit can be used.
25 | /// - Parameters:
26 | /// - developerToken: Your MusicKit developer token
27 | /// - appName: The name of your app
28 | /// - appBuild: Your app's build version
29 | /// - appURL: A URL that represents your app. This is surfaced in the Access Request dialog
30 | /// in the sign-in flow and in the name of the web process shown in Activity Monitor.
31 | /// - appIconURL: A URL that points to your app icon, surfaced in the Access Request dialog
32 | /// in the sign-in flow. The preferred size is 120x120.
33 | /// - onSuccess: Called when MusicKit is ready to use.
34 | /// - onError: Called if there is an error loading or configuring MusicKit.
35 | public func configure(withDeveloperToken developerToken: String,
36 | appName: String,
37 | appBuild: String,
38 | appURL: URL,
39 | appIconURL: URL?,
40 | onSuccess: (() -> Void)? = nil,
41 | onError: @escaping (Error) -> Void)
42 | {
43 | mkWebController.addEventListener(for: .musicKitDidLoad) {
44 | onSuccess?()
45 | }
46 |
47 | mkWebController.loadWebView(withDeveloperToken: developerToken,
48 | appName: appName,
49 | appBuild: appBuild,
50 | baseURL: appURL,
51 | appIconURL: appIconURL,
52 | onError: onError)
53 | }
54 |
55 | /// Returns a promise containing a music user token when a user has authenticated and authorized the app.
56 | public func authorize(onSuccess: ((String) -> Void)? = nil, onError: @escaping (Error) -> Void) {
57 | mkWebController.evaluateJavaScriptWithPromise(
58 | "music.authorize()",
59 | type: String.self,
60 | decodingStrategy: .typeCasting,
61 | onSuccess: onSuccess,
62 | onError: onError)
63 | }
64 |
65 | /// Unauthorizes the app for the current user.
66 | public func unauthorize(onSuccess: (() -> Void)? = nil) {
67 | mkWebController.evaluateJavaScriptWithPromise(
68 | "music.unauthorize()",
69 | onSuccess: onSuccess)
70 | }
71 |
72 | public func getIsAuthorized(onSuccess: @escaping (Bool) -> Void) {
73 | mkWebController.evaluateJavaScript(
74 | "music.isAuthorized",
75 | type: Bool.self,
76 | decodingStrategy: .typeCasting,
77 | onSuccess: onSuccess)
78 | }
79 |
80 | internal func getDeveloperToken(
81 | onSuccess: ((String) -> Void)?,
82 | onError: @escaping (Error) -> Void)
83 | {
84 | mkWebController.evaluateJavaScript(
85 | "music.developerToken",
86 | type: String.self,
87 | decodingStrategy: .typeCasting,
88 | onSuccess: onSuccess,
89 | onError: onError)
90 | }
91 |
92 |
93 | internal func getUserToken(
94 | onSuccess: ((String?) -> Void)?,
95 | onError: @escaping (Error) -> Void)
96 | {
97 | // Value for musicUserToken is undefined if no user is logged in.
98 | // We need JS to return null instead of undefined so we can decode it.
99 | mkWebController.evaluateJavaScript(
100 | "JSON.stringify(music.musicUserToken !== undefined ? music.musicUserToken : null)",
101 | type: String?.self,
102 | decodingStrategy: .jsonString,
103 | onSuccess: onSuccess,
104 | onError: onError)
105 | }
106 |
107 | /// Sets a music player's playback queue using a URL.
108 | public func setQueue(url: String, onSuccess: (() -> Void)? = nil, onError: @escaping (Error) -> Void) {
109 | mkWebController.evaluateJavaScriptWithPromise(
110 | "music.setQueue({ url: '\(url)' })",
111 | onSuccess: onSuccess,
112 | onError: onError)
113 | }
114 |
115 | /// Sets a music player's playback queue to a single Song.
116 | public func setQueue(song id: MediaID, onSuccess: (() -> Void)? = nil, onError: @escaping (Error) -> Void) {
117 | mkWebController.evaluateJavaScriptWithPromise(
118 | "music.setQueue({ song: '\(id)' })",
119 | onSuccess: onSuccess,
120 | onError: onError)
121 | }
122 |
123 | public func setQueue(songs ids: [MediaID], onSuccess: (() -> Void)? = nil, onError: @escaping (Error) -> Void) {
124 | mkWebController.evaluateJavaScriptWithPromise(
125 | "music.setQueue({ songs: \(ids.description) })",
126 | onSuccess: onSuccess,
127 | onError: onError)
128 | }
129 |
130 | /// Sets a music player's playback queue to a single Playlist.
131 | public func setQueue(playlist id: MediaID, onSuccess: (() -> Void)? = nil, onError: @escaping (Error) -> Void) {
132 | mkWebController.evaluateJavaScriptWithPromise(
133 | "music.setQueue({ playlist: '\(id)' })",
134 | onSuccess: onSuccess,
135 | onError: onError)
136 | }
137 |
138 | /// Sets a music player's playback queue to a single Album.
139 | public func setQueue(album id: MediaID, onSuccess: (() -> Void)? = nil, onError: @escaping (Error) -> Void) {
140 | mkWebController.evaluateJavaScriptWithPromise(
141 | "music.setQueue({ album: '\(id)' })",
142 | onSuccess: onSuccess,
143 | onError: onError)
144 | }
145 |
146 | public func addEventListener(for event: MKEvent, callback: @escaping () -> Void) {
147 | mkWebController.addEventListener(for: event, callback: callback)
148 | }
149 |
150 | /// Set to true to enable logging errors with additional information useful for debugging the API.
151 | public var enhancedErrorLogging = false
152 | }
153 |
154 |
--------------------------------------------------------------------------------
/Sources/MusicKitPlayer/API/MusicKitUtils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicKitUtils.swift
3 | // MusicKitPlayer
4 | //
5 | // Created by Nate Thompson on 2/10/20.
6 | // Copyright © 2020 Nate Thompson. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | public enum MKEvent: String {
12 | case authorizationStatusDidChange
13 | case authorizationStatusWillChange
14 | case eligibleForSubscribeView
15 | case loaded
16 | case mediaCanPlay
17 | case mediaItemDidChange
18 | case mediaItemWillChange
19 | case mediaPlaybackError
20 | case metadataDidChange
21 | case musicKitDidLoad
22 | case playbackBitrateDidChange
23 | case playbackDurationDidChange
24 | case playbackProgressDidChange
25 | case playbackStateDidChange
26 | case playbackStateWillChange
27 | case playbackTargetAvailableDidChange
28 | case playbackTimeDidChange
29 | case playbackVolumeDidChange
30 | case primaryPlayerDidChange
31 | case queueItemsDidChange
32 | case queuePositionDidChange
33 | case storefrontCountryCodeDidChange
34 | case storefrontIdentifierDidChange
35 | case userTokenDidChange
36 | }
37 |
38 |
39 | /// The playback bit rate of the music player
40 | public enum PlaybackBitrate: Int, Codable {
41 | /// The bit rate is 256 kbps.
42 | case high = 256
43 | /// The bit rate is 64 kbps.
44 | case standard = 64
45 | }
46 |
47 | /// The playback states of the music player
48 | public enum PlaybackStates: Int, Codable {
49 | /// The player has not attempted to start playback.
50 | case none = 0
51 | /// Loading of the media item has begun.
52 | case loading = 1
53 | /// The player is currently playing media.
54 | case playing = 2
55 | /// Playback has been paused.
56 | case paused = 3
57 | /// Plaback has been stopped.
58 | case stopped = 4
59 | /// Playback of the media item has ended.
60 | case ended = 5
61 | /// The player has started a seek operation.
62 | case seeking = 6
63 | /// Playback is delayed pending the completion of another operation.
64 | case waiting = 8
65 | /// The player is trying to fetch media data but cannot retrieve the data.
66 | case stalled = 9
67 | /// Playback of all media items in the queue has ended.
68 | case completed = 10
69 | }
70 |
71 | /// The repeat mode for the music player.
72 | public enum PlayerRepeatMode: Int, Codable, CaseIterable {
73 | /// No repeat mode specified.
74 | case none = 0
75 | /// The current media item will be repeated.
76 | case one = 1
77 | /// The current queue will be repeated.
78 | case all = 2
79 |
80 | }
81 |
82 | /// The shuffle mode for the music player.
83 | public enum PlayerShuffleMode: Int, Codable, CaseIterable {
84 | /// This value indicates that shuffle mode is off.
85 | case off = 0
86 | /// This value indicates that songs are being shuffled in the current queue.
87 | case shuffle = 1 // JS docs call this "songs" for some reason
88 | }
89 |
90 | /// A single audio or video item
91 | public struct MediaItem: Codable {
92 | public let assetURL: String?
93 | public let attributes: MediaItemAttributes
94 | public let flavor: String?
95 | public let id: String
96 | public let type: String
97 | }
98 |
99 | public struct MediaItemAttributes: Codable {
100 | public let albumName: String?
101 | public let artistName: String?
102 | public let artwork: MediaItemArtwork?
103 | public let composerName: String?
104 | public let contentRating: ContentRating?
105 | public let discNumber: Int?
106 | public let durationInMillis: Int?
107 | public let genreNames: [String] = []
108 | public let isrc: String?
109 | public let name: String?
110 | public let playParams: PlayParams
111 | public let releaseDate: String?
112 | public let trackNumber: Int?
113 | public let url: String?
114 |
115 | public var durationInSecs: Int? {
116 | guard let millis = durationInMillis else { return nil }
117 | return millis / 1000
118 | }
119 |
120 | public var isExplicit: Bool {
121 | switch contentRating {
122 | case .explicit:
123 | return true
124 | default:
125 | return false
126 | }
127 | }
128 | }
129 |
130 | public struct MediaItemArtwork: Codable {
131 | let url: String
132 |
133 | public func url(forImageOfSize size: NSSize) -> URL? {
134 | // For some reason the url seems to sometimes contain 2000x2000 instead of {w}x{h}.
135 | return URL(string: url
136 | .replacingOccurrences(of: "2000x2000", with: "\(Int(size.width))x\(Int(size.height))")
137 | .replacingOccurrences(of: "{w}x{h}", with: "\(Int(size.width))x\(Int(size.height))"))
138 | }
139 |
140 | public func nsImage(ofSize size: NSSize) -> NSImage? {
141 | guard let url = url(forImageOfSize: size) else { return nil }
142 | return NSImage(contentsOf: url)
143 | }
144 | }
145 |
146 | /// An object that represents play parameters for resources.
147 | ///
148 | /// [https://developer.apple.com/documentation/applemusicapi/playparameters](https://developer.apple.com/documentation/applemusicapi/playparameters)
149 | public struct PlayParams: Codable {
150 | /// The ID of the content to use for playback.
151 | public let id: String
152 | public let catalogId: String?
153 | public let purchasedId: String?
154 | public let isLibrary: Bool?
155 | /// The kind of the content to use for playback.
156 | public let kind: String
157 | public let reporting: Bool?
158 |
159 | public init(from decoder: Decoder) throws {
160 | let container = try decoder.container(keyedBy: PlayParamsKeys.self)
161 | // For some reason, id is sometimes a string and sometimes a number, try decoding both
162 | var idNumber: Int? = nil
163 | var idString: String? = nil
164 | do {
165 | idNumber = try container.decode(Int.self, forKey: .id)
166 | } catch {
167 | idString = try container.decode(String.self, forKey: .id)
168 | }
169 | id = idString ?? String(idNumber ?? -1)
170 | catalogId = try? container.decode(String.self, forKey: .catalogId)
171 | isLibrary = try? container.decode(Bool.self, forKey: .isLibrary)
172 | kind = try container.decode(String.self, forKey: .kind)
173 | reporting = try? container.decode(Bool.self, forKey: .reporting)
174 | purchasedId = try? container.decode(String.self, forKey: .purchasedId)
175 | }
176 |
177 | public enum PlayParamsKeys: String, CodingKey {
178 | case id
179 | case catalogId
180 | case isLibrary
181 | case kind
182 | case reporting
183 | case purchasedId
184 | }
185 | }
186 |
187 | public enum ContentRating: String, Codable {
188 | case clean
189 | case explicit
190 | }
191 |
192 |
193 | /// A Song identifier
194 | public typealias MediaID = String
195 |
196 | /// A Resource object that represents a song.
197 | ///
198 | /// [https://developer.apple.com/documentation/applemusicapi/song](https://developer.apple.com/documentation/applemusicapi/song)
199 | public struct Song: Codable {
200 | /// The attributes for the song.
201 | public let attributes: Attributes
202 | /// A URL subpath that fetches the resource as the primary object. This member is only present in responses.
203 | public let href: String
204 | /// Persistent identifier of the resource.
205 | public let id: MediaID
206 | /// The type of resource. This value will always be songs. Value: `songs`
207 | public let type: String
208 |
209 | /// The attributes for a song object.
210 | ///
211 | /// [https://developer.apple.com/documentation/applemusicapi/song/attributes](https://developer.apple.com/documentation/applemusicapi/song/attributes)
212 | public struct Attributes: Codable {
213 | /// The localized name of the song.
214 | public let name: String
215 | /// The name of the album the song appears on.
216 | public let albumName: String?
217 | /// The artist’s name.
218 | public let artistName: String?
219 | /// The number of the song in the album’s track list.
220 | public let trackNumber: Int?
221 | /// The duration of the song in milliseconds.
222 | public let durationInMillis: Int
223 | /// The release date of the song in `YYYY-MM-DD` format.
224 | public let releaseDate: String?
225 | /// The parameters to use to playback the song.
226 | public let playParams: PlayParams?
227 | /// The album artwork.
228 | public let artwork: Artwork?
229 | /// The genre names the song is associated with.
230 | public let genreNames: [String]
231 |
232 | /// The song’s composer.
233 | // composerName
234 | /// The Recording Industry Association of America (RIAA) rating of the content. The possible values for this rating are clean and explicit. No value means no rating.
235 | // contentRating
236 | /// The disc number the song appears on.
237 | // discNumber
238 | /// The notes about the song that appear in the iTunes Store.
239 | // editorialNotes
240 | /// The International Standard Recording Code (ISRC) for the song.
241 | // isrc
242 | /// The URL for sharing a song in the iTunes Store.
243 | // url
244 |
245 | public var trackTime: String {
246 | return String(milliseconds: durationInMillis)
247 | }
248 | }
249 | }
250 |
251 |
252 | public enum SongType {
253 | case song
254 | case librarySong
255 | }
256 |
257 |
258 | public struct LibraryAlbum: Codable {
259 | public let attributes: Attributes
260 | public let href: String
261 | public let id: MediaID
262 | public let type: String
263 |
264 | public struct Attributes: Codable {
265 | public let artistName: String?
266 | public let artwork: Artwork?
267 | public let dateAdded: String
268 | public let genreNames: [String]
269 | public let name: String
270 | public let playParams: PlayParams?
271 | public let releaseDate: String?
272 | public let trackCount: Int
273 | }
274 | }
275 |
276 |
277 | public struct Album: Codable {
278 | public let attributes: Attributes
279 | public let href: String
280 | public let id: MediaID
281 | public let type: String
282 |
283 | public struct Attributes: Codable {
284 | public let artistName: String
285 | public let artwork: Artwork
286 | public let copyright: String
287 | public let genreNames: [String]
288 | public let isCompilation: Bool
289 | public let isComplete: Bool
290 | public let isMasteredForItunes: Bool
291 | public let isSingle: Bool
292 | public let name: String
293 | public let playParams: PlayParams?
294 | public let releaseDate: String
295 | public let trackCount: Int
296 | public let url: String
297 | }
298 | }
299 |
300 |
301 | public struct LibraryPlaylist: Codable {
302 | public let attributes: Attributes
303 | public let href: String
304 | public let id: MediaID
305 | public let type: String
306 |
307 | public struct Attributes: Codable {
308 | public let artwork: Artwork?
309 | public let canEdit: Bool
310 | public let dateAdded: String
311 | public let description: Description?
312 | public let hasCatalog: Bool
313 | public let name: String
314 | public let playParams: PlayParams?
315 | }
316 | }
317 |
318 |
319 | public struct Playlist: Codable {
320 | public let attributes: Attributes
321 | public let href: String
322 | public let id: MediaID
323 | public let type: String
324 |
325 | public struct Attributes: Codable {
326 | public let artwork: Artwork
327 | public let curatorName: String?
328 | public let curatorSocialHandle: String?
329 | public let description: Description?
330 | public let isChart: Bool?
331 | public let lastModifiedDate: String
332 | public let name: String
333 | public let nextUpdateDate: String?
334 | public let playParams: PlayParams?
335 | public let playlistType: String
336 | public let url: String
337 | }
338 | }
339 |
340 |
341 | public struct Station: Codable {
342 | public let href: String
343 | public let id: MediaID
344 | public let type: String
345 | public let attributes: Attributes
346 |
347 | public struct Attributes: Codable {
348 | public let artwork: Artwork
349 | public let isLive: Bool
350 | public let name: String
351 | public let playParams: PlayParams
352 | public let url: String
353 | }
354 | }
355 |
356 |
357 | public enum MediaCollection: Decodable {
358 | case album(album: Album)
359 | case libraryAlbum(album: LibraryAlbum)
360 | case playlist(playlist: Playlist)
361 | case libraryPlaylist(playlist: LibraryPlaylist)
362 | case station(station: Station)
363 |
364 | public init(from decoder: Decoder) throws {
365 | let values = try decoder.container(keyedBy: CodingKeys.self)
366 | let typeString = try values.decode(String.self, forKey: .type)
367 |
368 | guard let type = CollectionType(rawValue: typeString) else {
369 | throw MKError.decodingFailed(
370 | underlyingError: DecodingError.unexpectedType(expected: typeString))
371 | }
372 |
373 | switch type {
374 | case .albums:
375 | self = .album(album: try Album(from: decoder))
376 | case .libraryAlbums:
377 | self = .libraryAlbum(album: try LibraryAlbum(from: decoder))
378 | case .playlists:
379 | self = .playlist(playlist: try Playlist(from: decoder))
380 | case .libraryPlaylists:
381 | self = .libraryPlaylist(playlist: try LibraryPlaylist(from: decoder))
382 | case .stations:
383 | self = .station(station: try Station(from: decoder))
384 | }
385 | }
386 |
387 | enum CodingKeys: String, CodingKey {
388 | case type
389 | }
390 |
391 | public enum CollectionType: String {
392 | case albums
393 | case libraryAlbums = "library-albums"
394 | case playlists
395 | case libraryPlaylists = "library-playlists"
396 | case stations
397 | }
398 | }
399 |
400 | public struct Recommendation: Decodable {
401 | public let attributes: Attributes
402 | public let href: String
403 | public let id: String
404 | public let relationships: Relationships
405 | public let type: String
406 |
407 | public struct Attributes: Decodable {
408 | public let isGroupRecommendation: Bool
409 | public let kind: String
410 | public let nextUpdateDate: String
411 | public let resourceTypes: [String]
412 | public let title: Title
413 |
414 | public struct Title: Decodable {
415 | public let stringForDisplay: String
416 | }
417 | }
418 |
419 | public struct Relationships: Decodable {
420 | public let contents: Contents
421 | public let href: String?
422 |
423 | public struct Contents: Decodable {
424 | public let data: [MediaCollection]
425 | }
426 | }
427 | }
428 |
429 | /// An object that represents artwork.
430 | ///
431 | /// [https://developer.apple.com/documentation/applemusicapi/artwork](https://developer.apple.com/documentation/applemusicapi/artwork)
432 | public struct Artwork: Codable {
433 | /// The maximum height available for the image.
434 | let height: Int?
435 | /// The maximum width available for the image.
436 | let width: Int?
437 | /// 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).
438 | let url: String?
439 |
440 | public init(height: Int?, width: Int?, url: String?) {
441 | self.height = height
442 | self.width = width
443 | self.url = url
444 | }
445 |
446 | var maxSize: NSSize? {
447 | guard let width = width,
448 | let height = height else { return nil }
449 | return NSSize(width: width, height: height)
450 | }
451 |
452 | public func url(forImageOfSize size: NSSize) -> URL? {
453 | guard let urlString = url else { return nil }
454 | return URL(string: urlString
455 | .replacingOccurrences(of: "{w}x{h}", with: "\(Int(size.width))x\(Int(size.height))"))
456 | }
457 |
458 | public func nsImage(ofSize size: NSSize) -> NSImage? {
459 | guard let url = url(forImageOfSize: size) else { return nil }
460 | return NSImage(contentsOf: url)
461 | }
462 | }
463 |
464 |
465 | public struct Description: Codable {
466 | public let standard: String
467 | public let short: String?
468 | }
469 |
470 |
471 |
472 | public enum MKError: Error, LocalizedError {
473 | case javaScriptError(underlyingError: Error)
474 | case promiseRejected(context: Error)
475 | case requestFailed(underlyingError: Error)
476 | case decodingFailed(underlyingError: Error)
477 | case navigationFailed(withError: Error)
478 | case loadingFailed(message: String)
479 | case timeoutError(timeout: Int)
480 | case emptyResponse
481 |
482 | public var errorDescription: String? {
483 | switch self {
484 | case .javaScriptError(let error):
485 | return "Error evaluating JavaScript \(String(describing: error))"
486 | case .promiseRejected(let context):
487 | return "MusicKit rejected promise: \(context)"
488 | case .requestFailed(let error):
489 | return "URL Request failed: \(error)"
490 | case .decodingFailed(let error):
491 | return "Error decoding JavaScript result: \(String(describing: error))"
492 | case .navigationFailed(let error):
493 | return "MusicKit webpage navigation failed \(String(describing: error))"
494 | case .loadingFailed(let message):
495 | return "MusicKit failed to load. \n\(message)"
496 | case .timeoutError(let timeout):
497 | return "MusicKit was not loaded after a timeout of \(timeout) seconds"
498 | case .emptyResponse:
499 | return "Response is empty"
500 | }
501 | }
502 |
503 | public var failureReason: String? {
504 | switch self {
505 | case .javaScriptError(let error):
506 | return "JavaScript evaluation returned an error: \(error)"
507 | case .promiseRejected(let context):
508 | return "MusicKit rejected promise: \(context)"
509 | case .requestFailed(let error):
510 | return "URL Request failed: \(error)"
511 | case .decodingFailed(let error):
512 | return "Error decoding JavaScript result: \(String(describing: error))"
513 | case .navigationFailed(let error):
514 | return "MusicKit webpage navigation failed \(String(describing: error))"
515 | case .loadingFailed(let message):
516 | return message
517 | case .timeoutError(let timeout):
518 | return "MusicKit was not loaded after a timeout of \(timeout) seconds"
519 | case .emptyResponse:
520 | return "Response is empty"
521 | }
522 | }
523 |
524 | public var underlyingError: Error? {
525 | switch self {
526 | case .javaScriptError(let error):
527 | return error
528 | case .requestFailed(let error):
529 | return error
530 | case .decodingFailed(let error):
531 | return error
532 | case .navigationFailed(let error):
533 | return error
534 | case .promiseRejected(let error):
535 | return error
536 | case .loadingFailed, .timeoutError, .emptyResponse:
537 | return nil
538 | }
539 | }
540 | }
541 |
542 | extension Dictionary: Error where Key == String, Value == String {}
543 | extension Array: Error where Element: Error {}
544 |
--------------------------------------------------------------------------------
/Sources/MusicKitPlayer/API/Player.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Player.swift
3 | // MusicKitPlayer
4 | //
5 | // Created by Nate Thompson on 2/11/20.
6 | // Copyright © 2020 Nate Thompson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | open class Player {
12 | public var queue: Queue
13 | private var mkWebController: MKWebController
14 |
15 | init(webController: MKWebController) {
16 | mkWebController = webController
17 | queue = Queue(webController: mkWebController)
18 | }
19 |
20 | // MARK: Properties
21 | /// The current playback duration.
22 | public func getCurrentPlaybackDuration(onSuccess: @escaping (Int) -> Void) {
23 | mkWebController.evaluateJavaScript(
24 | "music.player.currentPlaybackDuration",
25 | type: Int.self,
26 | decodingStrategy: .typeCasting,
27 | onSuccess: onSuccess)
28 | }
29 |
30 | /// The current playback progress.
31 | public func getCurrentPlaybackProgress(onSuccess: @escaping (Double) -> Void) {
32 | mkWebController.evaluateJavaScript(
33 | "music.player.currentPlaybackProgress",
34 | type: Double.self,
35 | decodingStrategy: .typeCasting,
36 | onSuccess: onSuccess)
37 | }
38 |
39 | /// The current position of the playhead.
40 | public func getCurrentPlaybackTime(onSuccess: @escaping (Int) -> Void) {
41 | mkWebController.evaluateJavaScript(
42 | "music.player.currentPlaybackTime",
43 | type: Int.self,
44 | decodingStrategy: .typeCasting,
45 | onSuccess: onSuccess)
46 | }
47 |
48 | public func getCurrentPlaybackTimeRemaining(onSuccess: @escaping (Int) -> Void) {
49 | mkWebController.evaluateJavaScript(
50 | "music.player.currentPlaybackTimeRemaining",
51 | type: Int.self,
52 | decodingStrategy: .typeCasting,
53 | onSuccess: onSuccess)
54 | }
55 |
56 | /// A Boolean value indicating whether the player is currently playing.
57 | public func getIsPlaying(onSuccess: @escaping (Bool) -> Void) {
58 | mkWebController.evaluateJavaScript(
59 | "music.player.isPlaying",
60 | type: Bool.self,
61 | decodingStrategy: .typeCasting,
62 | onSuccess: onSuccess)
63 | }
64 |
65 | /// The currently-playing media item, or the media item, within an queue, that you have designated to begin playback.
66 | public func getNowPlayingItem(onSuccess: @escaping (MediaItem?) -> Void) {
67 | mkWebController.evaluateJavaScript(
68 | "JSON.stringify(music.player.nowPlayingItem)",
69 | type: MediaItem?.self,
70 | decodingStrategy: .jsonString,
71 | onSuccess: onSuccess)
72 | }
73 |
74 | /// The index of the now playing item in the current playback queue. If there is no now playing item, the index is -1.
75 | public func getNowPlayingItemIndex(onSuccess: @escaping (Int) -> Void) {
76 | mkWebController.evaluateJavaScript(
77 | "music.player.nowPlayingItemIndex",
78 | type: Int.self,
79 | decodingStrategy: .typeCasting,
80 | onSuccess: onSuccess)
81 | }
82 |
83 | /// The current playback state of the music player.
84 | public func getPlaybackState(onSuccess: @escaping (PlaybackStates) -> Void) {
85 | mkWebController.evaluateJavaScript(
86 | "music.player.playbackState",
87 | type: PlaybackStates.self,
88 | decodingStrategy: .enumType,
89 | onSuccess: onSuccess)
90 | }
91 |
92 | /// The current playback rate for the player.
93 | public func getPlaybackBitrate(onSuccess: @escaping (PlaybackBitrate) -> Void) {
94 | mkWebController.evaluateJavaScript(
95 | "music.player.bitrate",
96 | type: PlaybackBitrate.self,
97 | decodingStrategy: .enumType,
98 | onSuccess: onSuccess)
99 | }
100 |
101 | /// The current repeat mode of the music player.
102 | public func getRepeatMode(onSuccess: @escaping (PlayerRepeatMode) -> Void) {
103 | mkWebController.evaluateJavaScript(
104 | "music.player.repeatMode",
105 | type: PlayerRepeatMode.self,
106 | decodingStrategy: .enumType,
107 | onSuccess: onSuccess)
108 | }
109 |
110 | /// The current shuffle mode of the music player.
111 | public func getShuffleMode(onSuccess: @escaping (PlayerShuffleMode) -> Void) {
112 | mkWebController.evaluateJavaScript(
113 | "music.player.shuffleMode",
114 | type: PlayerShuffleMode.self,
115 | decodingStrategy: .enumType,
116 | onSuccess: onSuccess)
117 | }
118 |
119 | /// A number indicating the current volume of the music player.
120 | public func getVolume(onSuccess: @escaping (Double) -> Void) {
121 | mkWebController.evaluateJavaScript(
122 | "music.player.volume",
123 | type: Double.self,
124 | decodingStrategy: .typeCasting,
125 | onSuccess: onSuccess)
126 | }
127 |
128 | // MARK: Methods
129 |
130 | /// Begins playing the media item at the specified index in the queue immediately.
131 | public func changeToMediaAtIndex(_ index: Int, onSuccess: (() -> Void)? = nil) {
132 | mkWebController.evaluateJavaScriptWithPromise(
133 | "music.player.changeToMediaAtIndex(\(index))",
134 | onSuccess: onSuccess)
135 | }
136 |
137 | /// Sets the volume to 0.
138 | public func mute() {
139 | mkWebController.evaluateJavaScript("music.player.mute()")
140 | }
141 |
142 | /// Pauses playback of the current item.
143 | public func pause() {
144 | mkWebController.evaluateJavaScript("music.player.pause()")
145 | }
146 |
147 | /// Initiates playback of the current item.
148 | public func play() {
149 | mkWebController.evaluateJavaScript("music.player.play()")
150 | }
151 |
152 | public func togglePlayPause() {
153 | getPlaybackState { playbackState in
154 | switch playbackState {
155 | case .playing:
156 | MusicKitPlayer.shared.player.pause()
157 | case .paused, .stopped, .ended, .none:
158 | MusicKitPlayer.shared.player.play()
159 | default:
160 | break
161 | }
162 | }
163 | }
164 |
165 | // /// Prepares a music player for playback.
166 | // public func prepareToPlay(mediaItem: MediaItem, onSuccess: (() -> Void)? = nil, onError: @escaping (Error) -> Void) {
167 | // mkWebController.evaluateJavaScriptWithPromise(
168 | // "music.player.prepareToPlay()",
169 | // onSuccess: onSuccess,
170 | // onError: onError)
171 | // }
172 |
173 | /// Sets the playback point to a specified time.
174 | public func seek(to time: Double, onSuccess: (() -> Void)? = nil) {
175 | mkWebController.evaluateJavaScriptWithPromise(
176 | "music.player.seekToTime(\(time))",
177 | onSuccess: onSuccess)
178 | }
179 |
180 | // /// Displays the playback target picker if a playback target is available.
181 | // public func showPlaybackTargetPicker() {
182 | // mkWebController.evaluateJavaScript("music.player.showPlaybackTargetPicker()")
183 | // }
184 |
185 | /// Starts playback of the next media item in the playback queue.
186 | /// Returns the current media item position.
187 | public func skipToNextItem(onSuccess: ((Int) -> Void)? = nil) {
188 | mkWebController.evaluateJavaScriptWithPromise(
189 | "music.player.skipToNextItem()",
190 | type: Int.self,
191 | decodingStrategy: .typeCasting,
192 | onSuccess: onSuccess)
193 | }
194 |
195 | /// Starts playback of the previous media item in the playback queue.
196 | /// Returns the current media position.
197 | public func skipToPreviousItem(onSuccess: ((Int) -> Void)? = nil) {
198 | mkWebController.evaluateJavaScriptWithPromise(
199 | "music.player.skipToPreviousItem()",
200 | type: Int.self,
201 | decodingStrategy: .typeCasting,
202 | onSuccess: onSuccess)
203 | }
204 |
205 | /// Stops the currently playing media item.
206 | public func stop() {
207 | mkWebController.evaluateJavaScript("music.player.stop()")
208 | }
209 |
210 | /// Sets the repeat mode of the player.
211 | /// - Parameters:
212 | /// - mode: The repeat mode.
213 | /// - onSuccess: Called when the action is completed successfully.
214 | public func setRepeatMode(_ mode: PlayerRepeatMode, onSuccess: (() -> Void)? = nil) {
215 | mkWebController.evaluateJavaScript(
216 | "music.player.repeatMode = \(mode.rawValue)",
217 | onSuccess: onSuccess)
218 | }
219 |
220 | /// Sets the shuffle mode of the player.
221 | /// - Parameters:
222 | /// - mode: The shuffle mode: shuffle or off.
223 | /// - onSuccess: Called when the action is completed successfully.
224 | public func setShuffleMode(_ mode: PlayerShuffleMode, onSuccess: (() -> Void)? = nil) {
225 | mkWebController.evaluateJavaScript(
226 | "music.player.shuffleMode = \(mode.rawValue)",
227 | onSuccess: onSuccess)
228 | }
229 |
230 | /// Sets the volume of the player
231 | /// - Parameters:
232 | /// - volume: The volume level. Must be between 0 and 1.
233 | /// - onSuccess: Called when the action is completed successfully.
234 | public func setVolume(_ volume: Double, onSuccess: (() -> Void)? = nil) {
235 | mkWebController.evaluateJavaScript(
236 | "music.player.volume = \(volume)",
237 | onSuccess: onSuccess)
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/Sources/MusicKitPlayer/API/Queue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Queue.swift
3 | // MusicKitPlayer
4 | //
5 | // Created by Nate Thompson on 2/11/20.
6 | // Copyright © 2020 Nate Thompson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | /// The current playback queue of the music player.
13 | open class Queue {
14 | private var mkWebController: MKWebController
15 |
16 | init(webController: MKWebController) {
17 | mkWebController = webController
18 | }
19 |
20 | // MARK: Properties
21 |
22 | /// A Boolean value indicating whether the queue has no items.
23 | public func getIsEmpty(onSuccess: @escaping (Bool) -> Void) {
24 | mkWebController.evaluateJavaScript(
25 | "music.player.queue.isEmpty",
26 | type: Bool.self,
27 | decodingStrategy: .typeCasting,
28 | onSuccess: onSuccess)
29 | }
30 |
31 | /// An array of all the media items in the queue.
32 | public func getItems(onSuccess: @escaping ([MediaItem]) -> Void) {
33 | mkWebController.evaluateJavaScript(
34 | "JSON.stringify(music.player.queue.items)",
35 | type: [MediaItem].self,
36 | decodingStrategy: .jsonString,
37 | onSuccess: onSuccess)
38 | }
39 |
40 | /// The number of items in the queue.
41 | public func getLength(onSuccess: @escaping (Int) -> Void) {
42 | mkWebController.evaluateJavaScript(
43 | "music.player.queue.length",
44 | type: Int.self,
45 | decodingStrategy: .typeCasting,
46 | onSuccess: onSuccess)
47 | }
48 |
49 | // /// The next playable media item in the queue.
50 | // public func getNextPlayableItem(onSuccess: @escaping (MediaItem?) -> Void) {
51 | //
52 | // }
53 | //
54 | // /// The previous playable media item in the queue.
55 | // public func getPreviousPlayableItem(onSuccess: @escaping (Bool?) -> Void) {
56 | //
57 | // }
58 |
59 | /// The current queue position.
60 | public func getPosition(onSuccess: @escaping (Int) -> Void) {
61 | mkWebController.evaluateJavaScript(
62 | "music.player.queue.position",
63 | type: Int.self,
64 | decodingStrategy: .typeCasting,
65 | onSuccess: onSuccess)
66 | }
67 |
68 | // MARK: Methods
69 |
70 | /// Inserts the song defined by the given ID into the current queue immediately after the currently playing media item.
71 | public func prepend(
72 | song: MediaID,
73 | type: SongType = .song,
74 | onSuccess: (() -> Void)? = nil)
75 | {
76 | prepend(songs: [song], type: type, onSuccess: onSuccess)
77 | }
78 |
79 | /// Inserts the songs defined by the given IDs into the current queue immediately after the currently playing media item.
80 | public func prepend(
81 | songs: [MediaID],
82 | type: SongType = .song,
83 | onSuccess: (() -> Void )? = nil)
84 | {
85 | var jsString: String
86 | switch type {
87 | case .song:
88 | jsString = """
89 | music.api.songs(\(songs.description), null).then(function(songs) {
90 | music.player.queue.prepend(songs);
91 | })
92 | """
93 | case .librarySong:
94 | jsString = """
95 | music.api.library.songs(\(songs.description), null).then(function(songs) {
96 | music.player.queue.prepend(songs);
97 | })
98 | """
99 | }
100 | mkWebController.evaluateJavaScript(jsString, onSuccess: onSuccess)
101 | }
102 |
103 | /// Inserts the song defined by the given ID after the last media item in the current queue.
104 | public func append(
105 | song: MediaID,
106 | type: SongType = .song,
107 | onSuccess: (() -> Void)? = nil)
108 | {
109 | append(songs: [song], type: type, onSuccess: onSuccess)
110 | }
111 |
112 | /// Inserts the songs defined by the given IDs after the last media item in the current queue.
113 | public func append(
114 | songs: [MediaID],
115 | type: SongType = .song,
116 | onSuccess: (() -> Void)? = nil)
117 | {
118 | var jsString: String
119 | switch type {
120 | case .song:
121 | jsString = """
122 | music.api.songs(\(songs.description), null).then(function(songs) {
123 | music.player.queue.append(songs);
124 | })
125 | """
126 | case .librarySong:
127 | jsString = """
128 | music.api.library.songs(\(songs.description), null).then(function(songs) {
129 | music.player.queue.append(songs);
130 | })
131 | """
132 | }
133 | mkWebController.evaluateJavaScript(jsString, onSuccess: onSuccess)
134 | }
135 |
136 | public func remove(index: Int, onSuccess: (() -> Void)? = nil) {
137 | mkWebController.evaluateJavaScript(
138 | "music.player.queue.remove(\(index))",
139 | onSuccess: onSuccess)
140 | }
141 |
142 | public func remove(indexes: IndexSet, onSuccess: (() -> Void)? = nil) {
143 | mkWebController.evaluateJavaScript("""
144 | \(Array(indexes).description).reverse().forEach(function(element) {
145 | music.player.queue.remove(element);
146 | })
147 | """,
148 | onSuccess: onSuccess)
149 | }
150 |
151 | // /// Returns the index in the playback queue for a media item descriptor.
152 | // public func indexForItem(descriptor: Descriptor) -> Int{
153 | // return 0
154 | // }
155 | }
156 |
--------------------------------------------------------------------------------
/Sources/MusicKitPlayer/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSHumanReadableCopyright
22 | Copyright © 2020 Nate Thompson. All rights reserved.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Sources/MusicKitPlayer/Internal/AuthorizationWindow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AuthorizationWindow.swift
3 | // MusicKitPlayer
4 | //
5 | // Created by Nate Thompson on 1/4/19.
6 | // Copyright © 2019 Nate Thompson. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import WebKit
11 |
12 | class AuthorizeWindowController: NSWindowController {
13 | init(webView: WKWebView) {
14 | let viewController = AuthorizeViewController(webView: webView)
15 | let window = NSWindow(contentViewController: viewController)
16 | window.title = "Sign In"
17 | window.styleMask = [.titled, .closable]
18 |
19 | super.init(window: window)
20 | }
21 |
22 | required init?(coder: NSCoder) {
23 | fatalError("init(coder:) has not been implemented")
24 | }
25 | }
26 |
27 |
28 |
29 |
30 |
31 | class AuthorizeViewController: NSViewController {
32 | var webView: WKWebView!
33 |
34 | init(webView: WKWebView) {
35 | super.init(nibName: nil, bundle: nil)
36 | self.webView = webView
37 | self.webView.uiDelegate = self
38 | self.webView.navigationDelegate = self
39 | }
40 |
41 | required init?(coder: NSCoder) {
42 | fatalError("init(coder:) has not been implemented")
43 | }
44 |
45 | override func loadView() {
46 | view = webView
47 | view.frame.size = NSSize(width: 500.0, height: 630.0)
48 | webView.autoresizingMask = [.width, .height]
49 | }
50 | }
51 |
52 |
53 |
54 |
55 |
56 | extension AuthorizeViewController: WKUIDelegate, WKNavigationDelegate {
57 | func webViewDidClose(_ webView: WKWebView) {
58 | view.window?.close()
59 | }
60 |
61 | func webView(
62 | _ webView: WKWebView,
63 | decidePolicyFor navigationAction: WKNavigationAction,
64 | decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)
65 | {
66 | if navigationAction.navigationType == .linkActivated,
67 | let url = navigationAction.request.url,
68 | url.host != "idmsa.apple.com"
69 | {
70 | NSWorkspace.shared.open(url)
71 | }
72 | decisionHandler(.allow)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Sources/MusicKitPlayer/Internal/MKDecoder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MKDecoder.swift
3 | // MusicKitPlayer
4 | //
5 | // Created by Nate Thompson on 3/23/20.
6 | // Copyright © 2020 Nate Thompson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class MKDecoder {
12 | /// The strategy used to decode a JavaScript response.
13 | enum Strategy {
14 | /// Works with top-level objects of NSArray or NSDictionary.
15 | case jsonSerialization
16 | /// Decodes a JSON string. For use with JSON.stringify() in the JavaScript code.
17 | case jsonString
18 | /// Works on JS primitive types (e.g. string, boolean, number).
19 | case typeCasting
20 | /// For enums with associated RawValue types of Int or String.
21 | case enumType
22 | }
23 |
24 |
25 | /// Decodes the response to the given type using the given strategy.
26 | /// - Parameters:
27 | /// - response: The result from JavaScript.
28 | /// - type: The type to try to decode the response to.
29 | /// - strategy: The strategy used to decode the response. Different types need to be decoded in different ways,
30 | /// and this function attempts to decode the response using the strategy given. For more information, see MKDecoder.Strategy.
31 | func decodeJSResponse(
32 | _ response: Any,
33 | to type: T.Type,
34 | withStrategy strategy: Strategy) throws -> T
35 | {
36 | switch strategy {
37 | case .jsonSerialization:
38 | if JSONSerialization.isValidJSONObject(response) {
39 | // Top level object is NSArray or NSDictionary
40 | do {
41 | let responseData = try JSONSerialization.data(withJSONObject: response, options: [])
42 | return try JSONDecoder().decode(type.self, from: responseData)
43 | } catch {
44 | throw MKError.decodingFailed(underlyingError: error)
45 | }
46 | } else {
47 | throw MKError.decodingFailed(underlyingError: DecodingError.invalidJSONObject)
48 | }
49 |
50 | case .jsonString:
51 | // For use with JSON.stringify() in the JS code
52 | guard let jsonString = response as? String else {
53 | throw MKError.decodingFailed(underlyingError:
54 | DecodingError.unexpectedType(expected: "String"))
55 | }
56 |
57 | let responseData = jsonString.data(using: .utf8)!
58 |
59 | do {
60 | return try JSONDecoder().decode(type.self, from: responseData)
61 | } catch {
62 | throw MKError.decodingFailed(underlyingError: error)
63 | }
64 |
65 | case .typeCasting:
66 | // Works on raw JSON types (e.g. string, boolean, number)
67 | // JSONDecoder doesn't work with fragments https://bugs.swift.org/browse/SR-6163
68 | if let castResponse = response as? T {
69 | return castResponse
70 | } else {
71 | throw MKError.decodingFailed(underlyingError:
72 | DecodingError.typeCastingFailed(type: String(describing: type)))
73 | }
74 |
75 | case .enumType:
76 | // Synthesizes an integer or string into a case of the specified Swift enum
77 | if let _ = response as? Int {
78 | // RawValue type should be Int
79 | do {
80 | let fragmentArray = "[\(response)]".data(using: .utf8)!
81 | let decodedFragment = try JSONDecoder().decode([T].self, from: fragmentArray)
82 | return decodedFragment[0]
83 | } catch {
84 | throw MKError.decodingFailed(underlyingError: error)
85 | }
86 |
87 | } else if let _ = response as? String {
88 | // RawValue type should be String
89 | do {
90 | let fragmentArray = "[\"\(response)\"]".data(using: .utf8)!
91 | let decodedFragment = try JSONDecoder().decode([T].self, from: fragmentArray)
92 | return decodedFragment[0]
93 | } catch {
94 | throw MKError.decodingFailed(underlyingError: error)
95 | }
96 | } else {
97 | throw MKError.decodingFailed(underlyingError:
98 | DecodingError.unexpectedType(expected: "Int or String"))
99 | }
100 | }
101 | }
102 | }
103 |
104 |
105 | // MARK: Errors
106 |
107 | enum DecodingError: Error, CustomStringConvertible {
108 | case invalidJSONObject
109 | case unexpectedType(expected: String)
110 | case typeCastingFailed(type: String)
111 |
112 | var description: String {
113 | switch self {
114 | case .invalidJSONObject:
115 | return "Failed to decode invalid JSON object"
116 | case .unexpectedType(let expected):
117 | return "Unexpected Type, was expecting \(expected)"
118 | case .typeCastingFailed(let type):
119 | return "Failed to decode by type casting to \(type)"
120 | }
121 | }
122 | }
123 |
124 |
125 | /// A wrapper for decoding errors to bundle additional information.
126 | struct EnhancedDecodingError: Error, CustomStringConvertible {
127 | let underlyingError: Error
128 | let jsString: String
129 | let response: Any
130 | let decodingStrategy: MKDecoder.Strategy
131 |
132 | var description: String {
133 | return """
134 | Error decoding JavaScript result:
135 | Underlying error: \(String(describing: underlyingError))
136 | JavaScript input: \(jsString)
137 | JavaScript response: \(String(describing: response))
138 | Decoding strategy: \(String(describing: decodingStrategy))
139 | """
140 | }
141 |
142 | func logIfNeeded() {
143 | if MusicKitPlayer.shared.enhancedErrorLogging {
144 | NSLog(self.description)
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/Sources/MusicKitPlayer/Internal/MKWebController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MKWebController.swift
3 | // MusicKitPlayer
4 | //
5 | // Created by Nate Thompson on 1/5/19.
6 | // Copyright © 2019 Nate Thompson. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import WebKit
11 |
12 |
13 | class MKWebController: NSWindowController {
14 | private var webView: WKWebView!
15 | private var contentController = WKUserContentController()
16 | private var decoder = MKDecoder()
17 |
18 | /// A dictionary containing responses to pending promises. Keyed by the UUID assigned to the
19 | /// promise, beginning with an underscore.
20 | private var promiseDict = [String: PromiseResponse]()
21 |
22 | /// A dictionary containing listeners to events, keyed by the event.
23 | private var eventListenerDict = [MKEvent: [() -> Void]]()
24 |
25 | private var isMusicKitLoaded = false {
26 | didSet {
27 | if isMusicKitLoaded {
28 | musicKitDidLoad()
29 | }
30 | }
31 | }
32 |
33 | /// For setting up the framework. Guaranteed to be the called first before public
34 | /// musicKitDidLoad event listeners.
35 | private var musicKitDidLoad: () -> Void = {
36 | RemoteCommandController.setup()
37 | NowPlayingInfoManager.setup()
38 | QueueManager.setup()
39 | }
40 |
41 | /// Holds the error handler of the configure function while loading.
42 | private var loadErrorHandler: ((Error) -> Void)? = nil
43 |
44 | private static var defaultErrorHandler: (Error) -> Void {
45 | return { error in
46 | #if DEBUG
47 | print(error)
48 | #else
49 | NSLog(String(describing: error))
50 | #endif
51 | }
52 | }
53 |
54 | init() {
55 | let preferences = WKPreferences()
56 |
57 | let configuration = WKWebViewConfiguration()
58 | configuration.preferences = preferences
59 | configuration.userContentController = contentController
60 |
61 | webView = WKWebView(frame: .zero, configuration: configuration)
62 |
63 | let viewController = NSViewController(nibName: nil, bundle: nil)
64 | viewController.view = webView
65 | let window = NSWindow(contentViewController: viewController)
66 | super.init(window: window)
67 |
68 | // Clear WKWebView cache so we get an error if the MusicKit script fails to load
69 | let websiteDataTypes = NSSet(array: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache])
70 | let date = Date(timeIntervalSince1970: 0)
71 | WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes as! Set, modifiedSince: date, completionHandler:{ })
72 |
73 | // Adds message handler used in WKScriptMessageHandler extension
74 | contentController.add(self, name: "musicKitLoaded")
75 | contentController.add(self, name: "eventListenerCallback")
76 | contentController.add(self, name: "log")
77 | contentController.add(self, name: "throwLoadingError")
78 |
79 | webView.uiDelegate = self
80 | webView.navigationDelegate = self
81 | }
82 |
83 |
84 | required init?(coder: NSCoder) {
85 | fatalError("init(coder:) has not been implemented")
86 | }
87 |
88 | override func showWindow(_ sender: Any?) {
89 | fatalError("Don't do this")
90 | }
91 |
92 |
93 | // MARK: Loading Webpage
94 |
95 | func loadWebView(
96 | withDeveloperToken developerToken: String,
97 | appName: String,
98 | appBuild: String,
99 | baseURL: URL,
100 | appIconURL: URL?,
101 | onError: @escaping (Error) -> Void)
102 | {
103 | loadErrorHandler = onError
104 |
105 | let htmlString = MKWebpage.html(
106 | withDeveloperToken: developerToken,
107 | appName: appName,
108 | appBuild: appBuild,
109 | appIconURL: appIconURL)
110 |
111 | if baseURL.host == nil {
112 | onError(MKError.loadingFailed(message: "Invalid appURL"))
113 | return
114 | }
115 |
116 | webView.loadHTMLString(htmlString, baseURL: baseURL)
117 |
118 | // Ensure that MusicKit has loaded after a few seconds
119 | DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
120 | self.evaluateJavaScript("MusicKit.getInstance()", onError: { error in
121 | // only throw error if loadWebView's onError hasn't already been fired
122 | if self.loadErrorHandler != nil {
123 | self.throwLoadingError(.timeoutError(timeout: 10))
124 | }
125 | })
126 | }
127 | }
128 |
129 |
130 | private func throwLoadingError(_ error: MKError) {
131 | if let loadError = loadErrorHandler {
132 | loadErrorHandler = nil
133 | loadError(error)
134 | } else {
135 | MKWebController.defaultErrorHandler(error)
136 | }
137 | }
138 |
139 |
140 |
141 | // MARK: Event Listeners
142 |
143 | func addEventListener(for event: MKEvent, callback: @escaping () -> Void) {
144 | if eventListenerDict[event] != nil {
145 | eventListenerDict[event]!.append(callback)
146 | } else {
147 | eventListenerDict[event] = [callback]
148 | }
149 |
150 | // If MusicKit isn't loaded, we have to wait to add the event listeners
151 | if isMusicKitLoaded {
152 | addEventListenerToMKJS(for: event)
153 | }
154 | }
155 |
156 | /// MKEvents that currently have added event listeners
157 | private var currentListeningEvents = Set()
158 |
159 | private func addEventListenerToMKJS(for event: MKEvent) {
160 | // We only need to add one JS listener for each event. When the
161 | // JS event listener is called, all of the matching event listeners
162 | // in eventListenerDict will be called.
163 | if !currentListeningEvents.contains(event) {
164 | let eventName = event.rawValue
165 | evaluateJavaScript("""
166 | music.addEventListener('\(eventName)', function() {
167 | \(postCallback(named: eventName))
168 | })
169 | """)
170 | }
171 | currentListeningEvents.insert(event)
172 | }
173 |
174 |
175 |
176 | // MARK: Evaluate JavaScript
177 |
178 | /// Evaluates JavaScript String.
179 | func evaluateJavaScript(
180 | _ javaScriptString: String,
181 | onSuccess: (() -> Void)? = nil,
182 | onError: @escaping (Error) -> Void = defaultErrorHandler)
183 | {
184 | DispatchQueue.main.async {
185 | self.webView.evaluateJavaScript(javaScriptString) { (response, error) in
186 | if let underlyingError = error as? WKError,
187 | underlyingError.code == WKError.javaScriptResultTypeIsUnsupported
188 | {
189 | // This error occurs when JavaScript returns a promise,
190 | // which should mean the JS evaluation was successful.
191 | onSuccess?()
192 | } else if let error = error {
193 | onError(error)
194 | EnhancedJSError(underlyingError: error, jsString: javaScriptString).logIfNeeded()
195 | } else {
196 | onSuccess?()
197 | }
198 | }
199 | }
200 | }
201 |
202 |
203 |
204 | /// Evaluates JavaScript and passes decoded return value to completionHandler.
205 | func evaluateJavaScript(
206 | _ javaScriptString: String,
207 | type: T.Type,
208 | decodingStrategy strategy: MKDecoder.Strategy,
209 | onSuccess: ((T) -> Void)?,
210 | onError: @escaping (Error) -> Void = defaultErrorHandler)
211 | {
212 | guard let onSuccess = onSuccess else {
213 | evaluateJavaScript(javaScriptString, onError: onError)
214 | return
215 | }
216 |
217 | DispatchQueue.main.async {
218 | self.webView.evaluateJavaScript(javaScriptString) { (response, error) in
219 | self.handleResponse(
220 | response,
221 | to: javaScriptString,
222 | withError: error,
223 | decodeTo: type,
224 | withStrategy: strategy,
225 | onSuccess: onSuccess,
226 | onError: onError)
227 | }
228 | }
229 | }
230 |
231 |
232 |
233 | /// Evaluates JavaScript for void Promise and calls onSuccess handler when Promise is fulfilled.
234 | func evaluateJavaScriptWithPromise(
235 | _ javaScriptString: String,
236 | onSuccess: (() -> Void)?,
237 | onError: @escaping (Error) -> Void = defaultErrorHandler)
238 | {
239 | guard let onSuccess = onSuccess else {
240 | evaluateJavaScript(javaScriptString, onError: onError)
241 | return
242 | }
243 |
244 | evaluateJavaScriptWithPromise(
245 | javaScriptString,
246 | returnsValue: false,
247 | onSuccess: { response in
248 | onSuccess()
249 | }, onError: onError)
250 | }
251 |
252 | /// Evaluates JavaScript for Promise and passes decoded response to onSuccess handler.
253 | func evaluateJavaScriptWithPromise(
254 | _ javaScriptString: String,
255 | type: T.Type,
256 | decodingStrategy strategy: MKDecoder.Strategy,
257 | onSuccess: ((T) -> Void)?,
258 | onError: @escaping (Error) -> Void = defaultErrorHandler)
259 | {
260 | guard let onSuccess = onSuccess else {
261 | evaluateJavaScript(javaScriptString, onError: onError)
262 | return
263 | }
264 |
265 | evaluateJavaScriptWithPromise(
266 | javaScriptString,
267 | returnsValue: true,
268 | onSuccess: { response in
269 | self.handleResponse(
270 | response,
271 | to: javaScriptString,
272 | withError: nil,
273 | decodeTo: type,
274 | withStrategy: strategy,
275 | onSuccess: onSuccess,
276 | onError: onError)
277 | }, onError: onError)
278 | }
279 |
280 |
281 | /// Evaluates JavaScript for Promise and passes response to onSuccess handler.
282 | private func evaluateJavaScriptWithPromise(
283 | _ javaScriptString: String,
284 | returnsValue: Bool,
285 | onSuccess: @escaping (Any) -> Void,
286 | onError: @escaping (Error) -> Void)
287 | {
288 | // Create a PromiseResponse struct, which encapsulates both the success and error handlers.
289 | let promiseResponse = PromiseResponse(
290 | onSuccess: { response in
291 | // handle promise response
292 | onSuccess(response)
293 | }, onError: { error in
294 | if let errorDict = try? self.decoder.decodeJSResponse(
295 | error, to: [String: String].self, withStrategy: .jsonString)
296 | {
297 | onError(MKError.promiseRejected(context: errorDict))
298 | } else {
299 | onError(MKError.promiseRejected(context: ["unknown": String(describing: error)]))
300 | }
301 | })
302 |
303 | // Add PromiseResponse to dictionary to run after the message handler is called
304 | promiseDict[promiseResponse.id] = promiseResponse
305 |
306 | DispatchQueue.main.async {
307 | self.contentController.add(self, name: promiseResponse.successID)
308 | self.contentController.add(self, name: promiseResponse.errorID)
309 |
310 | let promiseString = self.promise(successID: promiseResponse.successID,
311 | errorID: promiseResponse.errorID,
312 | returnsValue: returnsValue)
313 | self.evaluateJavaScript(javaScriptString + promiseString, onError: onError)
314 | }
315 | }
316 |
317 |
318 |
319 | // MARK: Handle JS Response
320 |
321 | private func handleResponse(
322 | _ response: Any?,
323 | to jsString: String,
324 | withError error: Error?,
325 | decodeTo type: T.Type,
326 | withStrategy strategy: MKDecoder.Strategy,
327 | onSuccess: (T) -> Void,
328 | onError: (Error) -> Void)
329 | {
330 | if let error = error {
331 | onError(MKError.javaScriptError(underlyingError: error))
332 | EnhancedJSError(underlyingError: error, jsString: jsString).logIfNeeded()
333 | } else {
334 | do {
335 | let decodedResponse = try self.decoder.decodeJSResponse(
336 | response!, to: type, withStrategy: strategy)
337 | onSuccess(decodedResponse)
338 | } catch {
339 | onError(error)
340 |
341 | EnhancedDecodingError(underlyingError: error,
342 | jsString: jsString,
343 | response: response!,
344 | decodingStrategy: strategy).logIfNeeded()
345 | }
346 | }
347 | }
348 |
349 |
350 |
351 | // MARK: Boilerplate JavaScript
352 |
353 | private func postCallback(named name: String) -> String {
354 | return """
355 | try {
356 | webkit.messageHandlers.eventListenerCallback.postMessage('\(name)');
357 | } catch(err) {
358 | log(err);
359 | }
360 | """
361 | }
362 |
363 |
364 |
365 | /// Generates a JS string that will call message handler when promise is run.
366 | /// promiseName must be valid JS variable name.
367 | private func promise(successID: String, errorID: String, returnsValue: Bool) -> String {
368 | let promise = """
369 | .then(function(response) {
370 | try {
371 | webkit.messageHandlers.\(successID).postMessage(response);
372 | } catch(err) {
373 | log(err);
374 | }
375 | })
376 | """
377 | let voidPromise = """
378 | .then(function() {
379 | try {
380 | webkit.messageHandlers.\(successID).postMessage('');
381 | } catch(err) {
382 | log(err);
383 | }
384 | })
385 | """
386 | let catchError = """
387 | .catch(function(error) {
388 | let errorString = JSON.stringify(error);
389 | console.log(error);
390 | try {
391 | webkit.messageHandlers.\(errorID).postMessage(errorString);
392 | } catch(err) {
393 | log(err);
394 | }
395 | });
396 | """
397 | return (returnsValue ? promise : voidPromise) + catchError
398 | }
399 | }
400 |
401 |
402 |
403 | // MARK: Script Message Handler
404 |
405 | extension MKWebController: WKScriptMessageHandler {
406 | func userContentController(
407 | _ userContentController: WKUserContentController,
408 | didReceive message: WKScriptMessage)
409 | {
410 | // Remember to add message handler for each message in init()
411 | if message.name == "musicKitLoaded" {
412 | isMusicKitLoaded = true
413 |
414 | // Add registered event listeners to MKJS and call musicKitDidLoad listeners
415 | for eventListeners in eventListenerDict {
416 | if eventListeners.key == .musicKitDidLoad {
417 | for eventListener in eventListeners.value {
418 | eventListener()
419 | }
420 | } else {
421 | addEventListenerToMKJS(for: eventListeners.key)
422 | }
423 | }
424 |
425 | } else if message.name == "eventListenerCallback" {
426 | guard let eventName = message.body as? String,
427 | let event = MKEvent(rawValue: eventName),
428 | let callbacks = eventListenerDict[event] else
429 | {
430 | NSLog("Error: no callback function for event listener \(String(describing: message.body))")
431 | return
432 | }
433 | for callback in callbacks {
434 | callback()
435 | }
436 |
437 | } else if message.name == "log" {
438 | NSLog(String(describing: message.body))
439 |
440 | } else if message.name == "throwLoadingError" {
441 | let errorMessage = (message.body as? String ?? "Error loading webpage")
442 | throwLoadingError(.loadingFailed(message: errorMessage))
443 |
444 | // For promise response, message name should contain a UUID, unique to the pair
445 | // of success and error handlers, in the format "success_" and "error_".
446 | // promiseDict is keyed by "_".
447 | } else if let promiseResponse =
448 | promiseDict[removingPrefixBefore("_", in: message.name)]
449 | {
450 | if message.name.hasPrefix("success") {
451 | promiseResponse.onSuccess(message.body)
452 | promiseDict.removeValue(forKey: message.name)
453 | } else if message.name.hasPrefix("error") {
454 | promiseResponse.onError(message.body)
455 | promiseDict.removeValue(forKey: message.name)
456 | }
457 |
458 | } else {
459 | NSLog("Unhandled script message: \(message.name) - \(message.body)")
460 | }
461 | }
462 |
463 | private func removingPrefixBefore(
464 | _ character: String.Element,
465 | in string: String) -> String
466 | {
467 | var newString = string
468 | if let i = newString.firstIndex(of: character) {
469 | let start = newString.startIndex
470 | let end = newString.index(before: i)
471 | newString.removeSubrange(start...end)
472 | return newString
473 | } else {
474 | return string
475 | }
476 | }
477 | }
478 |
479 |
480 |
481 | // MARK: UI Delegate
482 | extension MKWebController: WKUIDelegate {
483 | func webView(
484 | _ webView: WKWebView,
485 | createWebViewWith configuration: WKWebViewConfiguration,
486 | for navigationAction: WKNavigationAction,
487 | windowFeatures: WKWindowFeatures) -> WKWebView?
488 | {
489 | let authWebView = WKWebView(frame: .zero, configuration: configuration)
490 | let authWindow = AuthorizeWindowController(webView: authWebView)
491 | authWindow.showWindow(nil)
492 |
493 | return authWebView
494 | }
495 | }
496 |
497 | // MARK: Navigation Delegate
498 | extension MKWebController: WKNavigationDelegate {
499 | func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
500 | throwLoadingError(.navigationFailed(withError: error))
501 | }
502 |
503 | func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
504 | throwLoadingError(.navigationFailed(withError: error))
505 | }
506 | }
507 |
508 |
509 |
510 |
511 | // MARK: Models
512 | extension MKWebController {
513 | /// Encapsulates success and error handlers for promise and
514 | private struct PromiseResponse {
515 | let onSuccess: (Any) -> Void
516 | let onError: (Any) -> Void
517 |
518 | let id: String
519 | let successID: String
520 | let errorID: String
521 |
522 | init(onSuccess: @escaping (Any) -> Void,
523 | onError: @escaping (Any) -> Void)
524 | {
525 | self.onSuccess = onSuccess
526 | self.onError = onError
527 |
528 | // Create unique UUID for promise response, and variants for success and error.
529 | self.id = "_\(UUID().uuidString.replacingOccurrences(of: "-", with: ""))"
530 | self.successID = "success\(id)"
531 | self.errorID = "error\(id)"
532 | }
533 | }
534 |
535 |
536 | struct EnhancedJSError: Error, CustomStringConvertible {
537 | let underlyingError: Error
538 | let jsString: String
539 |
540 | var description: String {
541 | return """
542 | Evaluation of JavaScript string produced an error:
543 | \(String(describing: underlyingError))
544 | JavaScript string: \(jsString)
545 | """
546 | }
547 |
548 | func logIfNeeded() {
549 | if MusicKitPlayer.shared.enhancedErrorLogging {
550 | if let underlyingError = underlyingError as? WKError,
551 | underlyingError.code == WKError.javaScriptResultTypeIsUnsupported
552 | {
553 | // Filter out unsupported type errors, which are common when
554 | // JavaScript evaluation returns a promise.
555 | } else {
556 | NSLog(self.description)
557 | }
558 | }
559 | }
560 | }
561 | }
562 |
--------------------------------------------------------------------------------
/Sources/MusicKitPlayer/Internal/MKWebpage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MKWebpage.swift
3 | // MusicKitPlayer
4 | //
5 | // Created by Nate Thompson on 3/28/20.
6 | //
7 |
8 | import Foundation
9 |
10 | enum MKWebpage {
11 | static func html(
12 | withDeveloperToken developerToken: String,
13 | appName: String,
14 | appBuild: String,
15 | appIconURL: URL?) -> String
16 | {
17 | var iconLinkHTML = ""
18 | if let appIconURL = appIconURL {
19 | iconLinkHTML = ""
20 | }
21 |
22 | return """
23 |
24 |
25 |
26 | MusicKit
27 | \(iconLinkHTML)
28 |
29 |
73 |
74 |
77 |
78 |
79 |
80 |
81 | """
82 | }
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/Sources/MusicKitPlayer/Internal/URLRequestManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLRequestManager.swift
3 | // MusicKitPlayer
4 | //
5 | // Created by Nate Thompson on 7/8/20.
6 | //
7 |
8 | import Foundation
9 |
10 | class URLRequestManager {
11 | static var shared = URLRequestManager()
12 |
13 | func request(
14 | _ endpoint: String,
15 | requiresUserToken: Bool,
16 | type: T.Type,
17 | decodingStrategy: MKDecoder.Strategy,
18 | onSuccess: @escaping (T, Metadata?) -> Void,
19 | onError: @escaping (Error) -> Void)
20 | {
21 | MusicKitPlayer.shared.getDeveloperToken(onSuccess: { developerToken in
22 | MusicKitPlayer.shared.getUserToken(onSuccess: { userToken in
23 |
24 | if requiresUserToken && userToken == nil {
25 | onError(MKError.requestFailed(underlyingError: URLRequestError.requiresUserToken))
26 | return
27 | }
28 |
29 | self.request(
30 | endpoint,
31 | developerToken: developerToken,
32 | userToken: userToken,
33 | type: type,
34 | decodingStrategy: decodingStrategy,
35 | onSuccess: onSuccess,
36 | onError: onError)
37 |
38 | }, onError: onError)
39 | }, onError: onError)
40 | }
41 |
42 |
43 | func request(
44 | _ endpoint: String,
45 | requiresUserToken: Bool,
46 | type: T.Type,
47 | decodingStrategy: MKDecoder.Strategy,
48 | onSuccess: @escaping (T) -> Void,
49 | onError: @escaping (Error) -> Void)
50 | {
51 | self.request(
52 | endpoint,
53 | requiresUserToken: requiresUserToken,
54 | type: type,
55 | decodingStrategy: decodingStrategy,
56 | onSuccess: { (data, _) in
57 | onSuccess(data)
58 | }, onError: onError)
59 | }
60 |
61 |
62 | private func request(
63 | _ endpoint: String,
64 | developerToken: String,
65 | userToken: String?,
66 | type: T.Type,
67 | decodingStrategy: MKDecoder.Strategy,
68 | onSuccess: @escaping (T, Metadata?) -> Void,
69 | onError: @escaping (Error) -> Void)
70 | {
71 | guard let endpointURL = URL(string: endpoint) else {
72 | onError(URLRequestError.invalidURL)
73 | return
74 | }
75 |
76 | var request = URLRequest(url: endpointURL)
77 | request.setValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization")
78 | request.setValue(userToken, forHTTPHeaderField: "Music-User-Token")
79 |
80 | let task = URLSession.shared.dataTask(with: request) { data, response, error in
81 | if let error = error {
82 | onError(MKError.requestFailed(underlyingError: error))
83 | return
84 | }
85 |
86 | do {
87 | let decodedResponse = try JSONDecoder().decode(Response.self, from: data!)
88 | if let decodedData = decodedResponse.data {
89 | onSuccess(decodedData, decodedResponse.meta)
90 | } else if let errors = decodedResponse.errors {
91 | onError(MKError.requestFailed(underlyingError: errors))
92 | } else {
93 | onError(MKError.decodingFailed(underlyingError:
94 | DecodingError.unexpectedType(expected: "values for keys named \"data\" or \"errors\"")))
95 | }
96 | } catch {
97 | if let statusCode = (response as? HTTPURLResponse)?.statusCode, statusCode >= 400 {
98 | onError(MKError.requestFailed(underlyingError: URLRequestError.failed(withStatusCode: statusCode)))
99 | } else {
100 | onError(MKError.decodingFailed(underlyingError: error))
101 | }
102 | }
103 | }
104 | task.resume()
105 | }
106 |
107 |
108 | struct Response: Decodable {
109 | let data: T?
110 | let meta: Metadata?
111 | let errors: [[String: String]]?
112 | }
113 |
114 |
115 | enum URLRequestError: Error, CustomStringConvertible {
116 | case failed(withStatusCode: Int)
117 | case requiresUserToken
118 | case invalidURL
119 |
120 | var description: String {
121 | switch self {
122 | case .failed(let statusCode):
123 | return "URL request failed with status code \(statusCode)"
124 | case .requiresUserToken:
125 | return "This endpoint requires that a user is signed in"
126 | case .invalidURL:
127 | return "The URL supplied for the endpoint is invalid"
128 | }
129 | }
130 | }
131 | }
132 |
133 |
134 | /// Metadata returned from the MusicKit API
135 | public struct Metadata: Decodable {
136 | public let total: Int
137 | }
138 |
--------------------------------------------------------------------------------
/Sources/MusicKitPlayer/Managers/NowPlayingManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NowPlayingManager.swift
3 | // MusicKitPlayer
4 | //
5 | // Created by Nate Thompson on 2/16/19.
6 | // Copyright © 2019 Nate Thompson. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import MediaPlayer
11 |
12 | enum RemoteCommandController {
13 | private static let remoteCommandCenter = MPRemoteCommandCenter.shared()
14 |
15 | static func setup() {
16 | remoteCommandCenter.playCommand.addTarget { _ -> MPRemoteCommandHandlerStatus in
17 | MusicKitPlayer.shared.player.play()
18 | return .success
19 | }
20 |
21 | remoteCommandCenter.pauseCommand.addTarget { _ -> MPRemoteCommandHandlerStatus in
22 | MusicKitPlayer.shared.player.pause()
23 | return .success
24 | }
25 |
26 | remoteCommandCenter.togglePlayPauseCommand.addTarget { _ -> MPRemoteCommandHandlerStatus in
27 | MusicKitPlayer.shared.player.getIsPlaying { isPlaying in
28 | if isPlaying {
29 | MusicKitPlayer.shared.player.pause()
30 | } else {
31 | MusicKitPlayer.shared.player.play()
32 | }
33 | }
34 | return .success
35 | }
36 |
37 | remoteCommandCenter.previousTrackCommand.addTarget { _ -> MPRemoteCommandHandlerStatus in
38 | MusicKitPlayer.shared.player.getCurrentPlaybackTime { playbackTime in
39 | if playbackTime < 2 {
40 | MusicKitPlayer.shared.player.skipToPreviousItem()
41 | } else {
42 | MusicKitPlayer.shared.player.seek(to: 0)
43 | }
44 | }
45 | return .success
46 | }
47 |
48 | remoteCommandCenter.nextTrackCommand.addTarget { _ -> MPRemoteCommandHandlerStatus in
49 | MusicKitPlayer.shared.player.skipToNextItem()
50 | return .success
51 | }
52 |
53 | remoteCommandCenter.changePlaybackPositionCommand.addTarget { event -> MPRemoteCommandHandlerStatus in
54 | let event = event as! MPChangePlaybackPositionCommandEvent
55 | MusicKitPlayer.shared.player.seek(to: event.positionTime)
56 | return .success
57 | }
58 | }
59 | }
60 |
61 |
62 | enum NowPlayingInfoManager {
63 | private static let infoCenter = MPNowPlayingInfoCenter.default()
64 |
65 | static func setup() {
66 | MusicKitPlayer.shared.addEventListener(for: .playbackStateDidChange) {
67 | MusicKitPlayer.shared.player.getPlaybackState(onSuccess: { state in
68 | switch state {
69 | case .playing:
70 | updateInfo()
71 | // Dirty hack: steal focus back as current now playing app from WKWebView process.
72 | // WebKit gives info to MPNowPlayingInfoCenter, but its implementation is incomplete.
73 | updateState(.paused)
74 | updateState(.playing)
75 | case .paused:
76 | updateState(.paused)
77 | case .stopped, .ended:
78 | updateState(.stopped)
79 | case .loading:
80 | updateInfo()
81 | default:
82 | break
83 | }
84 |
85 | })
86 | }
87 | }
88 |
89 | private static func updateState(_ state: MPNowPlayingPlaybackState) {
90 | infoCenter.playbackState = state
91 | }
92 |
93 | private static func updateInfo() {
94 | var nowPlayingInfo = [String: Any]()
95 |
96 | MusicKitPlayer.shared.player.getNowPlayingItem { nowPlayingItem in
97 | MusicKitPlayer.shared.player.getCurrentPlaybackDuration { duration in
98 | MusicKitPlayer.shared.player.getCurrentPlaybackTime { playbackTime in
99 |
100 | nowPlayingInfo[MPMediaItemPropertyMediaType] = MPNowPlayingInfoMediaType.audio.rawValue
101 |
102 | nowPlayingInfo[MPMediaItemPropertyTitle] = nowPlayingItem?.attributes.name ?? "Unknown"
103 | nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = nowPlayingItem?.attributes.albumName
104 | nowPlayingInfo[MPMediaItemPropertyArtist] = nowPlayingItem?.attributes.artistName
105 | nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
106 | nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = NSNumber(value: playbackTime)
107 |
108 | nowPlayingInfo[MPMediaItemPropertyComposer] = nowPlayingItem?.attributes.composerName
109 | nowPlayingInfo[MPMediaItemPropertyGenre] = nowPlayingItem?.attributes.genreNames.first
110 | nowPlayingInfo[MPMediaItemPropertyReleaseDate] = nowPlayingItem?.attributes.releaseDate
111 | nowPlayingInfo[MPMediaItemPropertyAlbumTrackNumber] = nowPlayingItem?.attributes.trackNumber
112 | nowPlayingInfo[MPMediaItemPropertyDiscNumber] = nowPlayingItem?.attributes.discNumber
113 |
114 | if let contentRating = nowPlayingItem?.attributes.contentRating {
115 | nowPlayingInfo[MPMediaItemPropertyIsExplicit] = contentRating == .explicit
116 | }
117 |
118 | if let artwork = nowPlayingItem?.attributes.artwork {
119 | let mediaItemArtwork = MPMediaItemArtwork(
120 | boundsSize: CGSize(width: 2000, height: 2000),
121 | requestHandler: { size -> NSImage in
122 | return artwork.nsImage(ofSize: size) ?? NSImage()
123 | })
124 | nowPlayingInfo[MPMediaItemPropertyArtwork] = mediaItemArtwork
125 | }
126 |
127 | infoCenter.nowPlayingInfo = nowPlayingInfo
128 | }
129 | }
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/Sources/MusicKitPlayer/Managers/QueueManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QueueManager.swift
3 | // MusicKitPlayer
4 | //
5 | // Created by Nate Thompson on 3/11/19.
6 | // Copyright © 2019 Nate Thompson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum QueueManager {
12 | public static var queue: [MediaItem] = []
13 |
14 | private static var currentQueuePosition = -1
15 | private static var previousQueuePosition = -1
16 |
17 | private static var currentlyUpdatingQueue = false
18 | private static var didUpdateQueue = false
19 |
20 | /// Specifies how queue should be reordered on next queue update.
21 | private static var queueMoveInstruction: (from: IndexSet, to: Int)? = nil
22 |
23 | private static var queueDidUpdateListeners: [(QueueDidUpdateEvent) -> Void] = []
24 |
25 | static func setup() {
26 | loadQueue(with: .setupQueue)
27 |
28 | MusicKitPlayer.shared.addEventListener(for: .queueItemsDidChange) {
29 | // Keep table view from reloading all data if queue was just uploaded
30 | // as the result of a user action
31 | if didUpdateQueue && !currentlyUpdatingQueue {
32 | self.loadQueue(with: .userModified)
33 | didUpdateQueue = false
34 | } else {
35 | self.loadQueue(with: .queueItemsDidChange)
36 | }
37 | }
38 |
39 | MusicKitPlayer.shared.addEventListener(for: .queuePositionDidChange) {
40 | self.loadQueue(with: .queuePositionDidChange)
41 | }
42 | }
43 |
44 | public static func addEventListener(_ listener: @escaping (QueueDidUpdateEvent) -> Void) {
45 | queueDidUpdateListeners.append(listener)
46 | }
47 |
48 | enum LoadQueueEvent: Equatable {
49 | case setupQueue
50 | case queueItemsDidChange
51 | case queuePositionDidChange
52 | case userModified
53 | }
54 |
55 | public enum QueueDidUpdateEvent: Equatable {
56 | case setupQueue
57 | case queueItemsDidChange
58 | case queuePositionDidChange(by: Int)
59 | case userModified
60 | case error
61 |
62 | init(_ event: LoadQueueEvent, positionChange: Int = 0) {
63 | if positionChange != 0 && event != .queuePositionDidChange && event != .queueItemsDidChange {
64 | // Queue position shouldn't have changed if the event isn't queuePositionDidChange
65 | assertionFailure("Queue Position change is nonzero and LoadQueueEvent is \(event)")
66 | }
67 |
68 | switch event {
69 | case .setupQueue:
70 | self = .setupQueue
71 | case .queueItemsDidChange:
72 | self = .queueItemsDidChange
73 | case .queuePositionDidChange:
74 | self = .queuePositionDidChange(by: positionChange)
75 | case .userModified:
76 | self = .userModified
77 | }
78 | }
79 | }
80 |
81 | private static func queueDidUpdate(with event: QueueDidUpdateEvent) {
82 | for listener in queueDidUpdateListeners {
83 | listener(event)
84 | }
85 | }
86 |
87 | private static func loadQueue(with event: LoadQueueEvent) {
88 | guard !currentlyUpdatingQueue else { return }
89 |
90 | MusicKitPlayer.shared.player.queue.getPosition { position in
91 | MusicKitPlayer.shared.player.queue.getItems { items in
92 | let queueSizeChange = items.count - self.queue.count
93 |
94 | self.queue = items
95 | self.previousQueuePosition = self.currentQueuePosition
96 | self.currentQueuePosition = position
97 |
98 | // Reorder queue if needed after append (i.e. inserted in middle of queue)
99 | if let move = self.queueMoveInstruction {
100 | // Ensure that append was was successful.
101 | // Append can fail if too many items are added at once.
102 | if move.from.count == queueSizeChange {
103 | self.queue.move(with: move.from, to: move.to)
104 | self.updateQueue()
105 | } else {
106 | NSLog("Error reordering queue - move instruction: \(move)")
107 | self.queueMoveInstruction = nil
108 | queueDidUpdate(with: .error)
109 | return
110 | }
111 | self.queueMoveInstruction = nil
112 | }
113 |
114 | let change = self.currentQueuePosition - self.previousQueuePosition
115 | queueDidUpdate(with: QueueDidUpdateEvent(event, positionChange: change))
116 | }
117 | }
118 | }
119 |
120 | /// Updates MusicKit JS queue with contents of local queue
121 | private static func updateQueue() {
122 | currentlyUpdatingQueue = true
123 |
124 | MusicKitPlayer.shared.player.queue.getLength { queueLength in
125 | MusicKitPlayer.shared.player.queue.getPosition { currentPosition in
126 | let afterNowPlaying = (currentPosition + 1).. Void)? = nil) {
199 | let queueIndex = convertToQueueIndex(upNextIndex: index)
200 | MusicKitPlayer.shared.player.changeToMediaAtIndex(queueIndex,
201 | onSuccess: onSuccess)
202 | }
203 |
204 | /// Calculates indexe of queue array from indexe of array slice starting after now playing item
205 | public static func convertToQueueIndex(upNextIndex index: Int) -> Int {
206 | return index + currentQueuePosition + 1
207 | }
208 |
209 | /// Calculates indexes of queue array from indexes of array slice starting after now playing item
210 | static func convertToQueueIndexes(upNextIndexes indexes: IndexSet) -> IndexSet {
211 | return IndexSet(indexes.map{ $0 + currentQueuePosition + 1 })
212 | }
213 |
214 | /// The number of songs after the now playing item
215 | public static var upNextCount: Int {
216 | return queue.count - currentQueuePosition - 1
217 | }
218 |
219 | /// Moves array element from one index to another.
220 | /// Arguments are based on indexes starting after now playing item.
221 | public static func move(from src: Int, to dest: Int) {
222 | let from = convertToQueueIndex(upNextIndex: src)
223 | let to = convertToQueueIndex(upNextIndex: dest)
224 | queue.move(from: from, to: to)
225 | updateQueue()
226 | }
227 |
228 | /// Moves array elements to a specified location in the array.
229 | /// Arguments are based on indexes starting after now playing item.
230 | public static func move(with src: IndexSet, to dest: Int) {
231 | let from = convertToQueueIndexes(upNextIndexes: src)
232 | let to = convertToQueueIndex(upNextIndex: dest)
233 | queue.move(with: from, to: to)
234 | updateQueue()
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/Sources/MusicKitPlayer/MusicKitPlayer.h:
--------------------------------------------------------------------------------
1 | //
2 | // MusicKitPlayer.h
3 | // MusicKitPlayer
4 | //
5 | // Created by Nate Thompson on 2/10/20.
6 | // Copyright © 2020 Nate Thompson. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for MusicKitPlayer.
12 | FOUNDATION_EXPORT double MusicKitPlayerVersionNumber;
13 |
14 | //! Project version string for MusicKitPlayer.
15 | FOUNDATION_EXPORT const unsigned char MusicKitPlayerVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Sources/MusicKitPlayer/Utils/ArrayUtil.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArrayUtil.swift
3 | // MusicKitPlayer
4 | //
5 | // Created by Nate Thompson on 3/9/19.
6 | // Copyright © 2019 Nate Thompson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension Array {
12 | // These move functions from sooop on GitHub
13 | // https://gist.github.com/sooop/3c964900d429516ba48bd75050d0de0a
14 | mutating func move(from start: Index, to end: Index) {
15 | guard (0..= 500 {
31 | seconds += 1
32 | }
33 |
34 | let minutes = seconds / 60
35 | let hours = minutes / 60
36 |
37 | if hours == 0 {
38 | self = String(format: "%d:%02d", minutes, seconds % 60)
39 | } else {
40 | self = String(format: "%d:%02d:%02d", hours, minutes % 60, seconds % 60)
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/jwt.js:
--------------------------------------------------------------------------------
1 | // This is a demo of generating a developer token from your private key
2 |
3 | const jwt = require('jsonwebtoken');
4 | const fs = require('fs');
5 |
6 | const privateKey = fs.readFileSync('YOUR_PRIVATE_KEY.p8').toString();
7 |
8 | const now = Math.floor(Date.now() / 1000);
9 | const sixMonths = now + 15777000;
10 |
11 | var token = jwt.sign({
12 | iss: 'YOUR_TEAM_ID',
13 | iat: now,
14 | exp: sixMonths
15 | }, privateKey, { algorithm: 'ES256', keyid: 'YOUR_KEY_ID' });
16 |
17 | console.log('Token expires on ' + new Date(sixMonths * 1000).toUTCString());
18 | console.log(token);
19 |
--------------------------------------------------------------------------------