mirror of
https://github.com/kyleneideck/BackgroundMusic
synced 2024-11-10 06:34:22 +00:00
Add music player: Google Play Music Desktop Player.
The code for GPMDP is a lot more complicated than the code for other music players. See BGMGooglePlayMusicDesktopPlayer.h for details. Adds a class, BGMAppWatcher, to hold the code that notifies listeners when a given app is launched or terminated. Resolves #161.
This commit is contained in:
parent
503d1a92ec
commit
e616718eab
24 changed files with 1548 additions and 164 deletions
|
@ -8,11 +8,11 @@
|
|||
|
||||
/* Begin PBXBuildFile section */
|
||||
19FE7071FF5280BC38F35E1D /* BGMVolumeChangeListener.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */; };
|
||||
19FE719951725A698A419CBA /* BGMVolumeChangeListener.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */; };
|
||||
19FE719951725A698A419CBA /* BGMVolumeChangeListener.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMVolumeChangeListener.cpp"; }; };
|
||||
19FE77608F6C80D0B1F595A7 /* BGMStatusBarItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */; };
|
||||
19FE7921FD1B6C037429ECA4 /* BGMStatusBarItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */; };
|
||||
19FE7DFF63F69E77C53BF95E /* BGMVolumeChangeListener.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */; };
|
||||
19FE7F77376562C179449013 /* BGMStatusBarItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */; };
|
||||
19FE7F77376562C179449013 /* BGMStatusBarItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMStatusBarItem.mm"; }; };
|
||||
1C0BD0A51BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAutoPauseMusicPrefs.mm"; }; };
|
||||
1C0BD0A81BF1B029004F4CF5 /* BGMPreferencesMenu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C0BD0A71BF1B029004F4CF5 /* BGMPreferencesMenu.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMPreferencesMenu.mm"; }; };
|
||||
1C1465B81BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C1465B71BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAutoPauseMusic.mm"; }; };
|
||||
|
@ -55,6 +55,9 @@
|
|||
1C533C801EF532CA00270802 /* _uninstall-non-interactive.sh in Resources */ = {isa = PBXBuildFile; fileRef = 1C533C7F1EF532CA00270802 /* _uninstall-non-interactive.sh */; };
|
||||
1C780FF21FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C780FF11FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMSystemSoundsVolume.mm"; }; };
|
||||
1C780FF31FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C780FF11FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm */; };
|
||||
1C8034D520B0347A004BC50C /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C8034D420B0347A004BC50C /* Security.framework */; };
|
||||
1C80DED320A6718600045BBE /* BGMAppWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C80DED220A6718600045BBE /* BGMAppWatcher.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAppWatcher.m"; }; };
|
||||
1C80DED420A6718600045BBE /* BGMAppWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C80DED220A6718600045BBE /* BGMAppWatcher.m */; };
|
||||
1C837DD81F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMOutputVolumeMenuItem.mm"; }; };
|
||||
1C837DD91F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */; };
|
||||
1C837DDA1F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */; };
|
||||
|
@ -64,6 +67,13 @@
|
|||
1C8D8304204238DB00A838F2 /* BGMSwinsian.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D8302204238DB00A838F2 /* BGMSwinsian.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMSwinsian.m"; }; };
|
||||
1C8D830520423E1C00A838F2 /* BGMSwinsian.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D8302204238DB00A838F2 /* BGMSwinsian.m */; };
|
||||
1C8D830620423E2400A838F2 /* BGMSwinsian.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D8302204238DB00A838F2 /* BGMSwinsian.m */; };
|
||||
1C8D830B2042DE9600A838F2 /* BGMGooglePlayMusicDesktopPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D830A2042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMGooglePlayMusicDesktopPlayer.m"; }; };
|
||||
1C8D830C2042DE9600A838F2 /* BGMGooglePlayMusicDesktopPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D830A2042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.m */; };
|
||||
1C8D830E2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js in Resources */ = {isa = PBXBuildFile; fileRef = 1C8D830D2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js */; };
|
||||
1C8D830F2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js in Resources */ = {isa = PBXBuildFile; fileRef = 1C8D830D2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js */; };
|
||||
1C9258472090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C9258462090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMGooglePlayMusicDesktopPlayerConnection.m"; }; };
|
||||
1C9258482090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C9258462090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m */; };
|
||||
1C9258492090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C9258462090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m */; };
|
||||
1CACCF391F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMBackgroundMusicDevice.cpp"; }; };
|
||||
1CACCF3A1F334447007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */; };
|
||||
1CACCF3B1F334450007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */; };
|
||||
|
@ -276,12 +286,20 @@
|
|||
1C780FF11FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMSystemSoundsVolume.mm; sourceTree = "<group>"; };
|
||||
1C8034C21BDAFD5700668E00 /* CAPThread.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CAPThread.cpp; path = PublicUtility/CAPThread.cpp; sourceTree = "<group>"; };
|
||||
1C8034C31BDAFD5700668E00 /* CAPThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAPThread.h; path = PublicUtility/CAPThread.h; sourceTree = "<group>"; };
|
||||
1C8034D420B0347A004BC50C /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
|
||||
1C80DED120A6718600045BBE /* BGMAppWatcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BGMAppWatcher.h; sourceTree = "<group>"; };
|
||||
1C80DED220A6718600045BBE /* BGMAppWatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BGMAppWatcher.m; sourceTree = "<group>"; };
|
||||
1C837DD61F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMOutputVolumeMenuItem.h; sourceTree = "<group>"; };
|
||||
1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMOutputVolumeMenuItem.mm; sourceTree = "<group>"; };
|
||||
1C8B0C69216205BF008C5679 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
|
||||
1C8D8301204238DB00A838F2 /* Swinsian.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Swinsian.h; path = "Music Players/Swinsian.h"; sourceTree = "<group>"; };
|
||||
1C8D8302204238DB00A838F2 /* BGMSwinsian.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMSwinsian.m; path = "Music Players/BGMSwinsian.m"; sourceTree = "<group>"; };
|
||||
1C8D8303204238DB00A838F2 /* BGMSwinsian.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMSwinsian.h; path = "Music Players/BGMSwinsian.h"; sourceTree = "<group>"; };
|
||||
1C8D83092042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMGooglePlayMusicDesktopPlayer.h; path = "Music Players/BGMGooglePlayMusicDesktopPlayer.h"; sourceTree = "<group>"; };
|
||||
1C8D830A2042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMGooglePlayMusicDesktopPlayer.m; path = "Music Players/BGMGooglePlayMusicDesktopPlayer.m"; sourceTree = "<group>"; };
|
||||
1C8D830D2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; name = GooglePlayMusicDesktopPlayer.js; path = "Music Players/GooglePlayMusicDesktopPlayer.js"; sourceTree = "<group>"; };
|
||||
1C9258452090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BGMGooglePlayMusicDesktopPlayerConnection.h; path = "Music Players/BGMGooglePlayMusicDesktopPlayerConnection.h"; sourceTree = "<group>"; };
|
||||
1C9258462090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BGMGooglePlayMusicDesktopPlayerConnection.m; path = "Music Players/BGMGooglePlayMusicDesktopPlayerConnection.m"; sourceTree = "<group>"; };
|
||||
1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGMBackgroundMusicDevice.cpp; sourceTree = "<group>"; };
|
||||
1CACCF381F3175AD007F86CA /* BGMBackgroundMusicDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMBackgroundMusicDevice.h; sourceTree = "<group>"; };
|
||||
1CB8B3361BBA75EF000E2DD1 /* Background Music.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Background Music.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -376,6 +394,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C8B0C6A216205BF008C5679 /* AVFoundation.framework in Frameworks */,
|
||||
1C8034D520B0347A004BC50C /* Security.framework in Frameworks */,
|
||||
270A84511E0044EF00F13C99 /* ScriptingBridge.framework in Frameworks */,
|
||||
1CD1FD301BDDEAF2004F7E1B /* AudioToolbox.framework in Frameworks */,
|
||||
1C1963031BCAC160008A4DF7 /* CoreAudio.framework in Frameworks */,
|
||||
|
@ -510,6 +529,10 @@
|
|||
1C2336D91BEAB6E7004C1C4E /* BGMMusicPlayer.m */,
|
||||
27F7D48E1D2483B100821C4B /* BGMDecibel.h */,
|
||||
27F7D48F1D2483B100821C4B /* BGMDecibel.m */,
|
||||
1C8D83092042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.h */,
|
||||
1C8D830A2042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.m */,
|
||||
1C9258452090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.h */,
|
||||
1C9258462090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m */,
|
||||
1C2336DB1BEAB73F004C1C4E /* BGMiTunes.h */,
|
||||
1C4699461BD5C0E400F78043 /* BGMiTunes.m */,
|
||||
1C2336DD1BEAE10C004C1C4E /* BGMSpotify.h */,
|
||||
|
@ -559,6 +582,8 @@
|
|||
children = (
|
||||
1CB8B33B1BBA75EF000E2DD1 /* BGMAppDelegate.h */,
|
||||
1CB8B33C1BBA75EF000E2DD1 /* BGMAppDelegate.mm */,
|
||||
1C80DED120A6718600045BBE /* BGMAppWatcher.h */,
|
||||
1C80DED220A6718600045BBE /* BGMAppWatcher.m */,
|
||||
1C837DD61F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.h */,
|
||||
1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */,
|
||||
1C780FF01FEF6C3B00497FAD /* BGMSystemSoundsVolume.h */,
|
||||
|
@ -692,6 +717,7 @@
|
|||
27F7D4911D2484A300821C4B /* Decibel.h */,
|
||||
279F48781DD6D94000768A85 /* Hermes.h */,
|
||||
27379B851C7C54870084A24C /* iTunes.h */,
|
||||
1C8D830D2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js */,
|
||||
27379B861C7C54870084A24C /* Spotify.h */,
|
||||
1C8D8301204238DB00A838F2 /* Swinsian.h */,
|
||||
27379B871C7C552A0084A24C /* VLC.h */,
|
||||
|
@ -717,6 +743,7 @@
|
|||
2743CA1B1D86DA9B0089613B /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1C8034D420B0347A004BC50C /* Security.framework */,
|
||||
1C8B0C69216205BF008C5679 /* AVFoundation.framework */,
|
||||
270A84501E0044EE00F13C99 /* ScriptingBridge.framework */,
|
||||
1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */,
|
||||
|
@ -871,6 +898,7 @@
|
|||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
English,
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
|
@ -895,6 +923,7 @@
|
|||
files = (
|
||||
274827951E11052500B31D8D /* MainMenu.xib in Resources */,
|
||||
1C533C7A1EED28B700270802 /* uninstall.sh in Resources */,
|
||||
1C8D830E2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js in Resources */,
|
||||
1C533C801EF532CA00270802 /* _uninstall-non-interactive.sh in Resources */,
|
||||
1CED61691C3081C2002CAFCF /* LICENSE in Resources */,
|
||||
1C2FC3041EB4D6E700A76592 /* BGMApp.sdef in Resources */,
|
||||
|
@ -922,6 +951,7 @@
|
|||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C8D830F2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -973,6 +1003,7 @@
|
|||
1C4699471BD5C0E400F78043 /* BGMiTunes.m in Sources */,
|
||||
1CD410D41F9EDDAD0070A094 /* BGMAppVolumesController.mm in Sources */,
|
||||
1C1962E41BC94E15008A4DF7 /* CARingBuffer.cpp in Sources */,
|
||||
1C8D830B2042DE9600A838F2 /* BGMGooglePlayMusicDesktopPlayer.m in Sources */,
|
||||
273F10DF1CC3D0B900C1C6DA /* BGMVOX.m in Sources */,
|
||||
1CC1DF811BE5068A00FB8FE4 /* CACFArray.cpp in Sources */,
|
||||
279F48771DD6D73A00768A85 /* BGMHermes.m in Sources */,
|
||||
|
@ -1003,9 +1034,11 @@
|
|||
1CED616C1C316E1A002CAFCF /* BGMAudioDeviceManager.mm in Sources */,
|
||||
2743C9F11D853FBB0089613B /* BGMUserDefaults.m in Sources */,
|
||||
1C1962FD1BCAC0C3008A4DF7 /* CADebugPrintf.cpp in Sources */,
|
||||
1C80DED320A6718600045BBE /* BGMAppWatcher.m in Sources */,
|
||||
2743C9EC1D852B360089613B /* BGMScriptingBridge.m in Sources */,
|
||||
1CC6593C1F91DEB400B0CCDC /* BGMTermination.mm in Sources */,
|
||||
1C837DD81F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */,
|
||||
1C9258472090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */,
|
||||
1C1963011BCAC0F6008A4DF7 /* CACFString.cpp in Sources */,
|
||||
1C1962E71BC94E91008A4DF7 /* BGMPlayThrough.cpp in Sources */,
|
||||
1C8D8304204238DB00A838F2 /* BGMSwinsian.m in Sources */,
|
||||
|
@ -1031,6 +1064,7 @@
|
|||
1CACCF3A1F334447007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */,
|
||||
1C780FF31FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm in Sources */,
|
||||
1CC6593D1F91DEB400B0CCDC /* BGMTermination.mm in Sources */,
|
||||
1C80DED420A6718600045BBE /* BGMAppWatcher.m in Sources */,
|
||||
1CD410D51F9EDDAD0070A094 /* BGMAppVolumesController.mm in Sources */,
|
||||
1CD989571ECFFD250014BBBF /* CAHostTimeBase.cpp in Sources */,
|
||||
1CD989581ECFFD250014BBBF /* CAMutex.cpp in Sources */,
|
||||
|
@ -1047,6 +1081,7 @@
|
|||
1CD989481ECFFCFC0014BBBF /* BGMMusicPlayer.m in Sources */,
|
||||
1CD989491ECFFCFC0014BBBF /* BGMDecibel.m in Sources */,
|
||||
1C3D36731ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */,
|
||||
1C9258482090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */,
|
||||
1CD9894A1ECFFCFC0014BBBF /* BGMiTunes.m in Sources */,
|
||||
1CD9894B1ECFFCFC0014BBBF /* BGMSpotify.m in Sources */,
|
||||
1CD9894C1ECFFCFC0014BBBF /* BGMHermes.m in Sources */,
|
||||
|
@ -1118,6 +1153,7 @@
|
|||
1C227C0B1FA4C48200A95B6D /* BGMAppVolumes.m in Sources */,
|
||||
1CEACF4D1F34793700FEC143 /* CAHALAudioDevice.cpp in Sources */,
|
||||
1CACCF3B1F334450007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */,
|
||||
1C8D830C2042DE9600A838F2 /* BGMGooglePlayMusicDesktopPlayer.m in Sources */,
|
||||
1C3D36741ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */,
|
||||
27FB8C301DE4758A0084DB9D /* BGMPlayThrough.cpp in Sources */,
|
||||
27FB8C311DE4758A0084DB9D /* BGM_Utils.cpp in Sources */,
|
||||
|
@ -1154,6 +1190,7 @@
|
|||
2743CA021D86D3CB0089613B /* BGMiTunes.m in Sources */,
|
||||
19FE77608F6C80D0B1F595A7 /* BGMStatusBarItem.mm in Sources */,
|
||||
19FE7071FF5280BC38F35E1D /* BGMVolumeChangeListener.cpp in Sources */,
|
||||
1C9258492090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
49
BGMApp/BGMApp/BGMAppWatcher.h
Normal file
49
BGMApp/BGMApp/BGMAppWatcher.h
Normal file
|
@ -0,0 +1,49 @@
|
|||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMAppWatcher.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2019 Kyle Neideck
|
||||
//
|
||||
// Calls callback functions when a given application is launched or terminated. Starts watching
|
||||
// after being initialised, stops after being destroyed.
|
||||
//
|
||||
|
||||
// System Includes
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMAppWatcher : NSObject
|
||||
|
||||
// appLaunched will be called when the application is launched and appTerminated will be called when
|
||||
// it's terminated. Background apps, status bar apps, etc. are ignored.
|
||||
- (instancetype) initWithBundleID:(NSString*)bundleID
|
||||
appLaunched:(void(^)(void))appLaunched
|
||||
appTerminated:(void(^)(void))appTerminated;
|
||||
|
||||
// With this constructor, when an application is launched or terminated, isMatchingBundleID will be
|
||||
// called first to decide whether or not the callback should be called.
|
||||
- (instancetype) initWithAppLaunched:(void(^)(void))appLaunched
|
||||
appTerminated:(void(^)(void))appTerminated
|
||||
isMatchingBundleID:(BOOL(^)(NSString* appBundleID))isMatchingBundleID;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
109
BGMApp/BGMApp/BGMAppWatcher.m
Normal file
109
BGMApp/BGMApp/BGMAppWatcher.m
Normal file
|
@ -0,0 +1,109 @@
|
|||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMAppWatcher.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMAppWatcher.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@implementation BGMAppWatcher {
|
||||
// Tokens for the notification observers so we can remove them in dealloc.
|
||||
id<NSObject> didLaunchToken;
|
||||
id<NSObject> didTerminateToken;
|
||||
}
|
||||
|
||||
- (instancetype) initWithBundleID:(NSString*)bundleID
|
||||
appLaunched:(void(^)(void))appLaunched
|
||||
appTerminated:(void(^)(void))appTerminated {
|
||||
return [self initWithAppLaunched:appLaunched
|
||||
appTerminated:appTerminated
|
||||
isMatchingBundleID:^BOOL(NSString* appBundleID) {
|
||||
return [bundleID isEqualToString:appBundleID];
|
||||
}];
|
||||
}
|
||||
|
||||
- (instancetype) initWithAppLaunched:(void(^)(void))appLaunched
|
||||
appTerminated:(void(^)(void))appTerminated
|
||||
isMatchingBundleID:(BOOL(^)(NSString*))isMatchingBundleID
|
||||
{
|
||||
if ((self = [super init])) {
|
||||
NSNotificationCenter* center = [NSWorkspace sharedWorkspace].notificationCenter;
|
||||
|
||||
didLaunchToken =
|
||||
[center addObserverForName:NSWorkspaceDidLaunchApplicationNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* notification) {
|
||||
if ([BGMAppWatcher shouldBeHandled:notification
|
||||
isMatchingBundleID:isMatchingBundleID]) {
|
||||
appLaunched();
|
||||
}
|
||||
}];
|
||||
|
||||
didTerminateToken =
|
||||
[center addObserverForName:NSWorkspaceDidTerminateApplicationNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* notification) {
|
||||
if ([BGMAppWatcher shouldBeHandled:notification
|
||||
isMatchingBundleID:isMatchingBundleID]) {
|
||||
appTerminated();
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// Returns YES if we should call the app launch/termination callback for this NSNotification.
|
||||
+ (BOOL) shouldBeHandled:(NSNotification*)notification
|
||||
isMatchingBundleID:(BOOL(^)(NSString*))isMatchingBundleID {
|
||||
NSString* __nullable notifiedBundleID =
|
||||
[notification.userInfo[NSWorkspaceApplicationKey] bundleIdentifier];
|
||||
|
||||
// Ignore the notification if the app doesn't have a bundle ID or isMatchingBundleID returns NO.
|
||||
return notifiedBundleID && isMatchingBundleID((NSString*)notifiedBundleID);
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
// Remove the application launch/termination observers.
|
||||
NSNotificationCenter* center = [NSWorkspace sharedWorkspace].notificationCenter;
|
||||
|
||||
if (didLaunchToken) {
|
||||
[center removeObserver:didLaunchToken];
|
||||
didLaunchToken = nil;
|
||||
}
|
||||
|
||||
if (didTerminateToken) {
|
||||
[center removeObserver:didTerminateToken];
|
||||
didTerminateToken = nil;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
// BGMAutoPauseMenuItem.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2019 Kyle Neideck
|
||||
// Copyright © 2016 Tanner Hoke
|
||||
//
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
|||
#import "BGMAutoPauseMenuItem.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGMMusicPlayer.h"
|
||||
#import "BGMAppWatcher.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
@ -41,7 +41,7 @@ static SInt64 const kMenuItemUpdateWaitTime = 1;
|
|||
NSMenuItem* menuItem;
|
||||
BGMAutoPauseMusic* autoPauseMusic;
|
||||
BGMMusicPlayers* musicPlayers;
|
||||
id<NSObject> didLaunchToken, didTerminateToken;
|
||||
BGMAppWatcher* appWatcher;
|
||||
}
|
||||
|
||||
- (instancetype) initWithMenuItem:(NSMenuItem*)item
|
||||
|
@ -66,53 +66,39 @@ static SInt64 const kMenuItemUpdateWaitTime = 1;
|
|||
// Toggle auto-pause when the menu item is clicked.
|
||||
menuItem.target = self;
|
||||
menuItem.action = @selector(toggleAutoPauseMusic);
|
||||
|
||||
[self updateMenuItemTitle];
|
||||
[self initMusicPlayerObservers];
|
||||
|
||||
[self initMenuItemTitle];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) initMusicPlayerObservers {
|
||||
// Add observers that enable/disable the Auto-pause Music menu item when the music player is launched/terminated.
|
||||
NSNotificationCenter* center = [[NSWorkspace sharedWorkspace] notificationCenter];
|
||||
|
||||
id<NSObject> (^addObserver)(NSString*) = ^(NSString* name) {
|
||||
return [center addObserverForName:name
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* note) {
|
||||
NSString* appBundleID = [note.userInfo[NSWorkspaceApplicationKey] bundleIdentifier];
|
||||
BOOL isAboutThisMusicPlayer = musicPlayers.selectedMusicPlayer.bundleID &&
|
||||
[appBundleID isEqualToString:(NSString*)musicPlayers.selectedMusicPlayer.bundleID];
|
||||
|
||||
if (isAboutThisMusicPlayer) {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
|
||||
kMenuItemUpdateWaitTime * NSEC_PER_SEC),
|
||||
dispatch_get_main_queue(),
|
||||
^{
|
||||
[self updateMenuItemTitle];
|
||||
});
|
||||
}
|
||||
}];
|
||||
};
|
||||
|
||||
didLaunchToken = addObserver(NSWorkspaceDidLaunchApplicationNotification);
|
||||
didTerminateToken = addObserver(NSWorkspaceDidTerminateApplicationNotification);
|
||||
}
|
||||
- (void) initMenuItemTitle {
|
||||
// Set the initial text, tool-tip, state, etc.
|
||||
[self updateMenuItemTitle];
|
||||
|
||||
- (void) dealloc {
|
||||
// Remove the application launch/termination observers.
|
||||
NSNotificationCenter* center = [[NSWorkspace sharedWorkspace] notificationCenter];
|
||||
|
||||
if (didLaunchToken) {
|
||||
[center removeObserver:didLaunchToken];
|
||||
}
|
||||
|
||||
if (didTerminateToken) {
|
||||
[center removeObserver:didTerminateToken];
|
||||
}
|
||||
// Avoid retain cycles in case we ever want to destroy instances of this class.
|
||||
BGMAutoPauseMenuItem* __weak weakSelf = self;
|
||||
|
||||
// Add a callback that enables/disables the Auto-pause Music menu item when the music player
|
||||
// is launched/terminated.
|
||||
void (^callback)(void) = ^{
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kMenuItemUpdateWaitTime * NSEC_PER_SEC),
|
||||
dispatch_get_main_queue(),
|
||||
^{
|
||||
BGMAutoPauseMenuItem* strongSelf = weakSelf;
|
||||
[strongSelf updateMenuItemTitle];
|
||||
});
|
||||
};
|
||||
|
||||
appWatcher = [[BGMAppWatcher alloc] initWithAppLaunched:callback
|
||||
appTerminated:callback
|
||||
isMatchingBundleID:^BOOL(NSString* appBundleID) {
|
||||
BGMAutoPauseMenuItem* strongSelf = weakSelf;
|
||||
NSString* __nullable playerBundleID =
|
||||
strongSelf->musicPlayers.selectedMusicPlayer.bundleID;
|
||||
return playerBundleID && [appBundleID isEqualToString:(NSString*)playerBundleID];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void) toggleAutoPauseMusic {
|
||||
|
@ -143,7 +129,7 @@ static SInt64 const kMenuItemUpdateWaitTime = 1;
|
|||
//
|
||||
// We don't actually disable it just in case the user decides to disable auto-pause and their music player isn't
|
||||
// running. E.g. someone who only recently installed Background Music and doesn't want to use auto-pause at all.
|
||||
if (musicPlayers.selectedMusicPlayer.isRunning) {
|
||||
if (musicPlayers.selectedMusicPlayer.running) {
|
||||
menuItem.attributedTitle = nil;
|
||||
menuItem.toolTip = nil;
|
||||
} else {
|
||||
|
|
|
@ -22,6 +22,8 @@
|
|||
// A simple wrapper around our use of NSUserDefaults. Used to store the preferences/state that only
|
||||
// apply to BGMApp. The others are stored by BGMDriver.
|
||||
//
|
||||
// Private data will be stored in the user's keychain instead of user defaults.
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMStatusBarItem.h"
|
||||
|
@ -51,6 +53,13 @@
|
|||
// BGMApp's main menu.)
|
||||
@property BGMStatusBarIcon statusBarIcon;
|
||||
|
||||
// The auth code we're required to send when connecting to GPMDP. Stored in the keychain. Reading
|
||||
// this property is thread-safe, but writing it isn't.
|
||||
//
|
||||
// Returns nil if no code is found or if reading fails. If writing fails, an error is logged, but no
|
||||
// exception is thrown.
|
||||
@property NSString* __nullable googlePlayMusicDesktopPlayerPermanentAuthCode;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
|
|
@ -30,10 +30,14 @@
|
|||
#pragma clang assume_nonnull begin
|
||||
|
||||
// Keys
|
||||
static NSString* const BGMDefaults_AutoPauseMusicEnabled = @"AutoPauseMusicEnabled";
|
||||
static NSString* const BGMDefaults_SelectedMusicPlayerID = @"SelectedMusicPlayerID";
|
||||
static NSString* const BGMDefaults_PreferredDeviceUIDs = @"PreferredDeviceUIDs";
|
||||
static NSString* const BGMDefaults_StatusBarIcon = @"StatusBarIcon";
|
||||
static NSString* const kDefaultKeyAutoPauseMusicEnabled = @"AutoPauseMusicEnabled";
|
||||
static NSString* const kDefaultKeySelectedMusicPlayerID = @"SelectedMusicPlayerID";
|
||||
static NSString* const kDefaultKeyPreferredDeviceUIDs = @"PreferredDeviceUIDs";
|
||||
static NSString* const kDefaultKeyStatusBarIcon = @"StatusBarIcon";
|
||||
|
||||
// Labels for Keychain Data
|
||||
static NSString* const kKeychainLabelGPMDPAuthCode =
|
||||
@"app.backgroundmusic: Google Play Music Desktop Player permanent auth code";
|
||||
|
||||
@implementation BGMUserDefaults {
|
||||
// The defaults object wrapped by this object.
|
||||
|
@ -49,11 +53,11 @@ static NSString* const BGMDefaults_StatusBarIcon = @"StatusBarIcon";
|
|||
|
||||
// Register the settings defaults.
|
||||
//
|
||||
// iTunes is the default music player, but we don't set BGMDefaults_SelectedMusicPlayerID
|
||||
// iTunes is the default music player, but we don't set kDefaultKeySelectedMusicPlayerID
|
||||
// here so we know when it's never been set. (If it hasn't, we try using BGMDevice's
|
||||
// kAudioDeviceCustomPropertyMusicPlayerBundleID property to tell which music player should
|
||||
// be selected. See BGMMusicPlayers.)
|
||||
NSDictionary* defaultsDict = @{ BGMDefaults_AutoPauseMusicEnabled: @YES };
|
||||
NSDictionary* defaultsDict = @{ kDefaultKeyAutoPauseMusicEnabled: @YES };
|
||||
|
||||
if (defaults) {
|
||||
[defaults registerDefaults:defaultsDict];
|
||||
|
@ -65,33 +69,37 @@ static NSString* const BGMDefaults_StatusBarIcon = @"StatusBarIcon";
|
|||
return self;
|
||||
}
|
||||
|
||||
#pragma mark Selected Music Player
|
||||
|
||||
- (NSString* __nullable) selectedMusicPlayerID {
|
||||
return [self get:BGMDefaults_SelectedMusicPlayerID];
|
||||
return [self get:kDefaultKeySelectedMusicPlayerID];
|
||||
}
|
||||
|
||||
- (void) setSelectedMusicPlayerID:(NSString* __nullable)selectedMusicPlayerID {
|
||||
[self set:BGMDefaults_SelectedMusicPlayerID to:selectedMusicPlayerID];
|
||||
[self set:kDefaultKeySelectedMusicPlayerID to:selectedMusicPlayerID];
|
||||
}
|
||||
|
||||
#pragma mark Auto-pause
|
||||
|
||||
- (BOOL) autoPauseMusicEnabled {
|
||||
return [self getBool:BGMDefaults_AutoPauseMusicEnabled];
|
||||
return [self getBool:kDefaultKeyAutoPauseMusicEnabled];
|
||||
}
|
||||
|
||||
- (void) setAutoPauseMusicEnabled:(BOOL)autoPauseMusicEnabled {
|
||||
[self setBool:BGMDefaults_AutoPauseMusicEnabled to:autoPauseMusicEnabled];
|
||||
[self setBool:kDefaultKeyAutoPauseMusicEnabled to:autoPauseMusicEnabled];
|
||||
}
|
||||
|
||||
- (NSArray<NSString*>*) preferredDeviceUIDs {
|
||||
NSArray<NSString*>* __nullable uids = [self get:BGMDefaults_PreferredDeviceUIDs];
|
||||
NSArray<NSString*>* __nullable uids = [self get:kDefaultKeyPreferredDeviceUIDs];
|
||||
return uids ? BGMNN(uids) : @[];
|
||||
}
|
||||
|
||||
- (void) setPreferredDeviceUIDs:(NSArray<NSString*>*)devices {
|
||||
[self set:BGMDefaults_PreferredDeviceUIDs to:devices];
|
||||
[self set:kDefaultKeyPreferredDeviceUIDs to:devices];
|
||||
}
|
||||
|
||||
- (BGMStatusBarIcon) statusBarIcon {
|
||||
NSInteger icon = [self getInt:BGMDefaults_StatusBarIcon or:kBGMStatusBarIconDefaultValue];
|
||||
NSInteger icon = [self getInt:kDefaultKeyStatusBarIcon or:kBGMStatusBarIconDefaultValue];
|
||||
|
||||
// Just in case we get an invalid value somehow.
|
||||
if ((icon < kBGMStatusBarIconMinValue) || (icon > kBGMStatusBarIconMaxValue)) {
|
||||
|
@ -103,10 +111,96 @@ static NSString* const BGMDefaults_StatusBarIcon = @"StatusBarIcon";
|
|||
}
|
||||
|
||||
- (void) setStatusBarIcon:(BGMStatusBarIcon)icon {
|
||||
[self setInt:BGMDefaults_StatusBarIcon to:icon];
|
||||
[self setInt:kDefaultKeyStatusBarIcon to:icon];
|
||||
}
|
||||
|
||||
#pragma mark Implementation
|
||||
#pragma mark Google Play Music Desktop Player
|
||||
|
||||
- (NSString* __nullable) googlePlayMusicDesktopPlayerPermanentAuthCode {
|
||||
// Try to read the permanent auth code from the user's keychain.
|
||||
NSDictionary<NSString*, NSObject*>* query = @{
|
||||
(__bridge NSString*)kSecClass: (__bridge NSString*)kSecClassGenericPassword,
|
||||
(__bridge NSString*)kSecAttrLabel: kKeychainLabelGPMDPAuthCode,
|
||||
(__bridge NSString*)kSecMatchLimit: (__bridge NSString*)kSecMatchLimitOne,
|
||||
(__bridge NSString*)kSecReturnData: @YES
|
||||
};
|
||||
|
||||
CFTypeRef result = nil;
|
||||
OSStatus err = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
|
||||
|
||||
NSString* __nullable authCode = nil;
|
||||
|
||||
// Check the return status, null check and check the type.
|
||||
if ((err == errSecSuccess) && result && (CFGetTypeID(result) == CFDataGetTypeID())) {
|
||||
// Convert it to a string.
|
||||
CFStringRef __nullable code =
|
||||
CFStringCreateFromExternalRepresentation(kCFAllocatorDefault,
|
||||
result,
|
||||
kCFStringEncodingUTF8);
|
||||
authCode = (__bridge_transfer NSString* __nullable)code;
|
||||
} else if (err != errSecItemNotFound) {
|
||||
NSString* __nullable errMsg =
|
||||
(__bridge_transfer NSString* __nullable)SecCopyErrorMessageString(err, nil);
|
||||
NSLog(@"Failed to read GPMDP auth code from keychain: %d, %@", err, errMsg);
|
||||
}
|
||||
|
||||
// Release the data we read.
|
||||
if (result) {
|
||||
CFRelease(result);
|
||||
}
|
||||
|
||||
return authCode;
|
||||
}
|
||||
|
||||
- (void) setGooglePlayMusicDesktopPlayerPermanentAuthCode:(NSString* __nullable)authCode {
|
||||
if (authCode) {
|
||||
// Convert it to an NSData so we can store it in the user's keychain.
|
||||
NSData* authCodeData = [authCode dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
// Delete the old code if necessary. (There's an update function, but this takes less code.)
|
||||
if (self.googlePlayMusicDesktopPlayerPermanentAuthCode) {
|
||||
[self deleteGPMDPPermanentAuthCode];
|
||||
}
|
||||
|
||||
// Store the code.
|
||||
[self addGPMDPPermanentAuthCode:authCodeData];
|
||||
} else {
|
||||
[self deleteGPMDPPermanentAuthCode];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) addGPMDPPermanentAuthCode:(NSData*)authCodeData {
|
||||
NSDictionary<NSString*, NSObject*>* attributes = @{
|
||||
(__bridge NSString*)kSecClass: (__bridge NSString*)kSecClassGenericPassword,
|
||||
(__bridge NSString*)kSecAttrLabel: kKeychainLabelGPMDPAuthCode,
|
||||
(__bridge NSString*)kSecValueData: authCodeData
|
||||
};
|
||||
|
||||
OSStatus err = SecItemAdd((__bridge CFDictionaryRef)attributes, nil);
|
||||
|
||||
// Just log an error if it failed.
|
||||
if (err != errSecSuccess) {
|
||||
NSString* errMsg = (__bridge_transfer NSString*)SecCopyErrorMessageString(err, nil);
|
||||
NSLog(@"Failed to store GPMDP auth code in keychain: %d, %@", err, errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
- (void) deleteGPMDPPermanentAuthCode {
|
||||
NSDictionary<NSString*, NSObject*>* query = @{
|
||||
(__bridge NSString*)kSecClass: (__bridge NSString*)kSecClassGenericPassword,
|
||||
(__bridge NSString*)kSecAttrLabel: kKeychainLabelGPMDPAuthCode
|
||||
};
|
||||
|
||||
OSStatus err = SecItemDelete((__bridge CFDictionaryRef)query);
|
||||
|
||||
// Just log an error if it failed.
|
||||
if (err != errSecSuccess) {
|
||||
NSString* errMsg = (__bridge_transfer NSString*)SecCopyErrorMessageString(err, nil);
|
||||
NSLog(@"Failed to delete GPMDP auth code from keychain: %d, %@", err, errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark General Accessors
|
||||
|
||||
- (id __nullable) get:(NSString*)key {
|
||||
return defaults ? [defaults objectForKey:key] : transientDefaults[key];
|
||||
|
|
|
@ -54,8 +54,8 @@
|
|||
return (DecibelApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMGooglePlayMusicDesktopPlayer.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2019 Kyle Neideck
|
||||
//
|
||||
// We have a lot more code for GPMDP than most music players largely because GPMDP has a WebSockets
|
||||
// API and because the user has to enter a code from GPMDP to allow BGMApp to control it.
|
||||
// Currently, the other music players all have AppleScript APIs, so for them the OS asks the user
|
||||
// for permission on our behalf automatically and handles the whole process for us.
|
||||
//
|
||||
// This class implements the usual BGMMusicPlayer methods and handles the UI for authenticating
|
||||
// with GPMDP. BGMGooglePlayMusicDesktopPlayerConnection manages the connection to GPMDP and hides
|
||||
// the details of its API.
|
||||
//
|
||||
|
||||
// Superclass/Protocol Import
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
API_AVAILABLE(macos(10.10))
|
||||
@interface BGMGooglePlayMusicDesktopPlayer : BGMMusicPlayerBase<BGMMusicPlayer>
|
||||
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstancesWithDefaults:(BGMUserDefaults*)userDefaults;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
342
BGMApp/BGMApp/Music Players/BGMGooglePlayMusicDesktopPlayer.m
Normal file
342
BGMApp/BGMApp/Music Players/BGMGooglePlayMusicDesktopPlayer.m
Normal file
|
@ -0,0 +1,342 @@
|
|||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMGooglePlayMusicDesktopPlayer.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMGooglePlayMusicDesktopPlayer.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Types.h"
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMAppWatcher.h"
|
||||
#import "BGMGooglePlayMusicDesktopPlayerConnection.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@implementation BGMGooglePlayMusicDesktopPlayer {
|
||||
BGMUserDefaults* userDefaults;
|
||||
BGMGooglePlayMusicDesktopPlayerConnection* connection;
|
||||
BGMAppWatcher* appWatcher;
|
||||
|
||||
// True while the auth code dialog is open. The user types in the four-digit auth code from
|
||||
// GPMDP when we connect to it for the first time.
|
||||
BOOL showingAuthCodeDialog;
|
||||
// True if the user has cancelled the auth code dialog. We only show the auth code dialog again
|
||||
// after the user has changed the music player and then changed it back to GPMDP (or restarted
|
||||
// BGMApp).
|
||||
BOOL authCancelled;
|
||||
}
|
||||
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstancesWithDefaults:(BGMUserDefaults*)userDefaults {
|
||||
return @[[[self alloc] initWithUserDefaults:userDefaults]];
|
||||
}
|
||||
|
||||
- (instancetype) initWithUserDefaults:(BGMUserDefaults*)defaults {
|
||||
// If you're copying this class, replace the ID string with a new one generated by uuidgen (the
|
||||
// command line tool).
|
||||
NSUUID* playerID = [BGMMusicPlayerBase makeID:@"FCDCC01F-4BF1-4AD2-BE3E-6B7659A90A3F"];
|
||||
if ((self = [super initWithMusicPlayerID:playerID
|
||||
name:@"GPMDP"
|
||||
toolTip:@"Google Play Music Desktop Player"
|
||||
bundleID:@"google-play-music-desktop-player"])) {
|
||||
userDefaults = defaults;
|
||||
showingAuthCodeDialog = NO;
|
||||
authCancelled = NO;
|
||||
|
||||
// We don't strictly need to use a weak ref (at least not yet), but it doesn't hurt.
|
||||
BGMGooglePlayMusicDesktopPlayer* __weak weakSelf = self;
|
||||
|
||||
connection = [[BGMGooglePlayMusicDesktopPlayerConnection alloc]
|
||||
initWithUserDefaults:userDefaults
|
||||
authRequiredHandler:^{
|
||||
BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf;
|
||||
return [strongSelf requestAuthCodeFromUser];
|
||||
}
|
||||
connectionErrorHandler:^{
|
||||
BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf;
|
||||
[strongSelf showConnectionErrorDialog];
|
||||
}
|
||||
apiVersionMismatchHandler:^(NSString* reportedAPIVersion) {
|
||||
BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf;
|
||||
[strongSelf showAPIVersionMismatchDialog:reportedAPIVersion];
|
||||
}];
|
||||
|
||||
// Set up callbacks that run when GPMDP is opened or closed.
|
||||
appWatcher = [[BGMAppWatcher alloc]
|
||||
initWithBundleID:BGMNN(self.bundleID)
|
||||
appLaunched:^{
|
||||
BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf;
|
||||
[strongSelf gpmdpWasLaunched];
|
||||
}
|
||||
appTerminated:^{
|
||||
BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf;
|
||||
[strongSelf gpmdpWasTerminated];
|
||||
}];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) gpmdpWasLaunched {
|
||||
if (self.selected) {
|
||||
// Reconnect so we can control GPMDP.
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayer::gpmdpWasLaunched: GPMDP launched. Connecting");
|
||||
|
||||
// Try up to 10 times because GPMDP won't start accepting connections until it's finished
|
||||
// starting up.
|
||||
//
|
||||
// TODO: If GPMDP shows an alert before it finishes launching, it doesn't start accepting
|
||||
// connections until the alert is dismissed, which can make this can timeout.
|
||||
// TODO: Is the error dialog still shown if the user closes GPMDP again while we're
|
||||
// retrying? It shouldn't be.
|
||||
[connection connectWithRetries:10];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) gpmdpWasTerminated {
|
||||
if (self.selected) {
|
||||
// Allow the connection to clean up and reset itself.
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayer::gpmdpWasTerminated: GPMDP has been closed.");
|
||||
[connection disconnect];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
|
||||
// Allow the auth code dialog to be shown again if we were hiding it because the user cancelled
|
||||
// it last time.
|
||||
authCancelled = NO;
|
||||
|
||||
if (self.running) {
|
||||
// Only retry once so the error message is shown fairly quickly if we fail to connect.
|
||||
[connection connectWithRetries:1];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) wasDeselected {
|
||||
[super wasDeselected];
|
||||
[connection disconnect];
|
||||
}
|
||||
|
||||
- (NSString* __nullable) requestAuthCodeFromUser {
|
||||
if (showingAuthCodeDialog) {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayer::requestAuthCodeFromUser: "
|
||||
"Already showing the auth code dialog");
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (authCancelled) {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayer::requestAuthCodeFromUser: "
|
||||
"Previously cancelled. Doing nothing.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
showingAuthCodeDialog = YES;
|
||||
|
||||
// Ask the user to read the auth code from GPMDP and type it in to BGMApp.
|
||||
NSString* __nullable authCode = [self showAuthCodeDialog];
|
||||
|
||||
showingAuthCodeDialog = NO;
|
||||
|
||||
return authCode;
|
||||
}
|
||||
|
||||
- (NSString* __nullable) showAuthCodeDialog {
|
||||
// When this isn't being called because the user just changed something in BGMApp (e.g. GPMDP
|
||||
// was closed, they selected it in BGMApp for the first time, then opened GPMDP later), we could
|
||||
// use notifications instead of an NSAlert. But it probably wouldn't happen often enough to be
|
||||
// worth the effort.
|
||||
NSAlert* alert = [NSAlert new];
|
||||
alert.messageText = @"Background Music needs permission to control GPMDP.";
|
||||
alert.informativeText = @"It should be displaying a four-digit code for you to enter.";
|
||||
[alert addButtonWithTitle:@"OK"];
|
||||
[alert addButtonWithTitle:@"Cancel"];
|
||||
|
||||
// The text field to type the auth code in.
|
||||
// TODO: Can we derive these dimensions from something instead of hardcoding them?
|
||||
NSTextField* input = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 50, 24)];
|
||||
[alert setAccessoryView:input];
|
||||
|
||||
// Focus the text field (so the user doesn't have to do it themselves).
|
||||
[alert.window setInitialFirstResponder:input];
|
||||
|
||||
// Bring GMPDP to the front, underneath our NSAlert, so the user can see the auth code.
|
||||
[self showGPMDPBehindAuthCodeDialog];
|
||||
|
||||
NSModalResponse buttonPressed = [alert runModal];
|
||||
|
||||
if (buttonPressed == NSAlertFirstButtonReturn) {
|
||||
// Set input's value to the text entered by the user so we can access it.
|
||||
[input validateEditing];
|
||||
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayer::showAuthCodeDialog: Got auth code: <private>");
|
||||
return input.stringValue;
|
||||
} else {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayer::showAuthCodeDialog: "
|
||||
"The user cancelled the auth code dialog");
|
||||
authCancelled = YES;
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) showGPMDPBehindAuthCodeDialog {
|
||||
// Dispatched because if we do this just before showing the auth code dialog, the user's current
|
||||
// active window will be deactivated, the auth code dialog will become the active window and
|
||||
// macOS will act as if the user activated it themselves. To avoid stealing key focus, it won't
|
||||
// activate GPMDP.
|
||||
//
|
||||
// We could pass NSApplicationActivateIgnoringOtherApps to activateWithOptions instead, but then
|
||||
// GPMDP would be activated even if the user really did activate a different application, which
|
||||
// would steal focus from it.
|
||||
//
|
||||
// 250 ms is a reasonable value on my system, but won't always be long enough. When it isn't,
|
||||
// GPMDP won't be activated, but that just means the user will have to do it themselves.
|
||||
const int64_t delay = 250 * NSEC_PER_MSEC;
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay),
|
||||
dispatch_get_main_queue(),
|
||||
^{
|
||||
// Make GMPDP the frontmost app.
|
||||
NSArray<NSRunningApplication*>* gpmdpApps =
|
||||
[NSRunningApplication
|
||||
runningApplicationsWithBundleIdentifier:BGMNN(self.bundleID)];
|
||||
|
||||
if (gpmdpApps.count > 0) {
|
||||
[gpmdpApps[0] activateWithOptions:0];
|
||||
}
|
||||
|
||||
// Focus the auth code dialog. It will already be in front of GPMDP because
|
||||
// it's modal. Dispatched for the same reason as above.
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay),
|
||||
dispatch_get_main_queue(),
|
||||
^{
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (void) showConnectionErrorDialog {
|
||||
NSString* errorMsg = @"Could not connect to Google Play Music Desktop Player";
|
||||
NSString* troubleshootingMsg =
|
||||
[NSString stringWithFormat:
|
||||
@"Make sure \"Enable JSON API\" and \"Enable Playback API\" are both checked in GPMDP's "
|
||||
"settings, then restart GPMDP.\n\n"
|
||||
"GPMDP should be listening on its default port, 5672.\n\n"
|
||||
"Consider filing a bug report at %s",
|
||||
kBGMIssueTrackerURL];
|
||||
|
||||
[self showErrorDialog:errorMsg troubleshootingMsg:troubleshootingMsg];
|
||||
}
|
||||
|
||||
- (void) showAPIVersionMismatchDialog:(NSString*)reportedAPIVersion {
|
||||
NSString* errorMsg = @"Google Play Music Desktop Player Version Not Supported";
|
||||
NSString* troubleshootingMsg =
|
||||
[NSString stringWithFormat:
|
||||
@"GPMDP reported its API version as \"%@\", which Background Music doesn't support "
|
||||
"yet. Background Music might not be able to control GPMDP properly.\n\n"
|
||||
"Feel free to open an issue about this at %s",
|
||||
reportedAPIVersion,
|
||||
kBGMIssueTrackerURL];
|
||||
|
||||
[self showErrorDialog:errorMsg troubleshootingMsg:troubleshootingMsg];
|
||||
}
|
||||
|
||||
- (void) showErrorDialog:(NSString*)errorMsg troubleshootingMsg:(NSString*)troubleshootingMsg {
|
||||
if (!self.running) {
|
||||
// GPMDP isn't running, so there's no need to inform the user. (The "Auto-pause GPMDP" menu
|
||||
// item will be greyed out, but that's handled elsewhere.)
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayer::showErrorDialog: Not running");
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"%@", errorMsg);
|
||||
|
||||
// Show the error in a UI dialog.
|
||||
NSAlert* alert = [NSAlert new];
|
||||
alert.messageText = errorMsg;
|
||||
alert.informativeText = troubleshootingMsg;
|
||||
// TODO: Show the suppression checkbox and save its value in user defaults.
|
||||
alert.showsSuppressionButton = NO;
|
||||
[alert addButtonWithTitle:@"OK"];
|
||||
|
||||
[alert runModal];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
// We have to check with NSRunningApplication instead of just setting a flag in appWatcher's
|
||||
// callbacks because BGMAutoPauseMenuItem calls this method when it's notified by its own
|
||||
// instance of BGMAppWatcher. If BGMAutoPauseMenuItem got notified first, the flag wouldn't be
|
||||
// updated in time.
|
||||
//
|
||||
// At some point we might want to try to avoid this by making the BGMMusicPlayers' running
|
||||
// properties observable.
|
||||
NSArray<NSRunningApplication*>* instances =
|
||||
[NSRunningApplication runningApplicationsWithBundleIdentifier:BGMNN(self.bundleID)];
|
||||
|
||||
return instances.count > 0;
|
||||
}
|
||||
|
||||
- (BOOL) isPlaying {
|
||||
return self.running && connection.playing;
|
||||
}
|
||||
|
||||
- (BOOL) isPaused {
|
||||
return self.running && connection.paused;
|
||||
}
|
||||
|
||||
- (BOOL) pause {
|
||||
// isPlaying checks isRunning, so we don't need to check it here.
|
||||
BOOL wasPlaying = self.playing;
|
||||
|
||||
if (wasPlaying) {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayer::pause: Pausing Google Play Music Desktop "
|
||||
"Player");
|
||||
// There's a race condition here and in unpause because, if the user paused GPMDP just
|
||||
// before we called playPause, GPMDP would play instead of pausing. I'm not sure there's
|
||||
// much we can/should do about it.
|
||||
[connection playPause];
|
||||
}
|
||||
|
||||
return wasPlaying;
|
||||
}
|
||||
|
||||
- (BOOL) unpause {
|
||||
// isPaused checks isRunning, so we don't need to check it here.
|
||||
BOOL wasPaused = self.paused;
|
||||
|
||||
if (wasPaused) {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayer::unpause: Unpausing Google Play Music Desktop "
|
||||
"Player");
|
||||
[connection playPause];
|
||||
}
|
||||
|
||||
return wasPaused;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMGooglePlayMusicDesktopPlayerConnection.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMUserDefaults.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <WebKit/WebKit.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
API_AVAILABLE(macos(10.10))
|
||||
@interface BGMGooglePlayMusicDesktopPlayerConnection : NSObject<WKScriptMessageHandler>
|
||||
|
||||
// authRequiredHandler: A UI callback that asks the user for the auth code GPMDP will display.
|
||||
// Returns the auth code they entered, or nil.
|
||||
// connectionErrorHandler: A UI callback that shows a connection error message.
|
||||
// apiVersionMismatchHandler: A UI callback that shows a warning dialog explaining that GPMDP
|
||||
// reported an API version that we don't support yet.
|
||||
- (instancetype) initWithUserDefaults:(BGMUserDefaults*)defaults
|
||||
authRequiredHandler:(NSString* __nullable (^)(void))authHandler
|
||||
connectionErrorHandler:(void (^)(void))errorHandler
|
||||
apiVersionMismatchHandler:(void (^)(NSString* reportedAPIVersion))apiVersionHandler;
|
||||
|
||||
// Returns before the connection has been fully established. The playing and paused properties will
|
||||
// remain false until the connection is complete, but playPause can be called at any time after
|
||||
// calling this method.
|
||||
//
|
||||
// If the connection fails, it will be retried after a one second delay, up to the number of times
|
||||
// given.
|
||||
- (void) connectWithRetries:(int)retries;
|
||||
- (void) disconnect;
|
||||
|
||||
// Tell GPMDP to play if it's paused or pause if it's playing.
|
||||
- (void) playPause;
|
||||
|
||||
@property (readonly) BOOL playing;
|
||||
@property (readonly) BOOL paused;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
|
@ -0,0 +1,444 @@
|
|||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMGooglePlayMusicDesktopPlayerConnection.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMGooglePlayMusicDesktopPlayerConnection.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Utils.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
// When GooglePlayMusicDesktopPlayer.js sends a message to this class, it sets the message handler
|
||||
// name to one of these, which tells us what type of message it is. (This is a macro because you
|
||||
// can't make a static const NSArray.)
|
||||
#define kScriptMessageHandlerNames (@[@"gpmdp", @"log", @"error"])
|
||||
|
||||
@implementation BGMGooglePlayMusicDesktopPlayerConnection {
|
||||
// GPMDP has a WebSocket API, so we use a WKWebView to access it using Javascript. Using a
|
||||
// proper library would make the code a bit cleaner and save a little memory, but I'm not sure
|
||||
// it would be worth adding an external dependency for that.
|
||||
WKWebView* webView;
|
||||
NSString* __nullable permanentAuthCode;
|
||||
BGMUserDefaults* userDefaults;
|
||||
// The number of times to retry if we fail to connect. For example, if GPMDP is still starting
|
||||
// up. Set to 0 when we aren't trying to connect.
|
||||
int connectionRetries;
|
||||
|
||||
// A UI callback that asks the user for the auth code GPMDP will display.
|
||||
NSString* __nullable (^authRequiredHandler)(void);
|
||||
// A UI callback that shows a connection error message.
|
||||
void (^connectionErrorHandler)(void);
|
||||
// A UI callback that shows a warning dialog explaining that GPMDP reported an API version that
|
||||
// we don't support yet.
|
||||
void (^apiVersionMismatchHandler)(NSString* reportedAPIVersion);
|
||||
}
|
||||
|
||||
- (instancetype) initWithUserDefaults:(BGMUserDefaults*)defaults
|
||||
authRequiredHandler:(NSString* __nullable (^)(void))authHandler
|
||||
connectionErrorHandler:(void (^)(void))errorHandler
|
||||
apiVersionMismatchHandler:(void (^)(NSString* reportedAPIVersion))apiVersionHandler {
|
||||
if((self = [super init])) {
|
||||
userDefaults = defaults;
|
||||
authRequiredHandler = authHandler;
|
||||
connectionErrorHandler = errorHandler;
|
||||
apiVersionMismatchHandler = apiVersionHandler;
|
||||
connectionRetries = 0;
|
||||
|
||||
// Lazily initialised.
|
||||
permanentAuthCode = nil;
|
||||
|
||||
// Report that GPMDP is stopped until we know otherwise.
|
||||
_playing = NO;
|
||||
_paused = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// Creates and initialises webView, a WKWebView we use to communicate with GPMDP over WebSockets.
|
||||
- (void) createWebView {
|
||||
// Read the Javascript we'll need for this.
|
||||
NSString* __nullable jsPath =
|
||||
[[NSBundle mainBundle] pathForResource:@"GooglePlayMusicDesktopPlayer.js"
|
||||
ofType:nil];
|
||||
NSError* err;
|
||||
NSString* __nullable jsStr =
|
||||
(!jsPath ? nil : [NSString stringWithContentsOfFile:BGMNN(jsPath)
|
||||
encoding:NSUTF8StringEncoding
|
||||
error:&err]);
|
||||
|
||||
if (err || !jsStr || [jsStr isEqualToString:@""]) {
|
||||
// TODO: Return an error so the caller can show an error dialog or something.
|
||||
NSLog(@"Error loading GPMDP Javascript file: %@", err);
|
||||
} else {
|
||||
webView = [WKWebView new];
|
||||
|
||||
// Register to receive messages from our Javascript. The messages are handled in
|
||||
// userContentController. We register several times using different names as a convenient
|
||||
// way to separate messages from GPMDP, messages to log and errors.
|
||||
for (NSString* name in kScriptMessageHandlerNames) {
|
||||
[webView.configuration.userContentController addScriptMessageHandler:self name:name];
|
||||
}
|
||||
|
||||
// Load our Javascript functions into webView so we can call them later.
|
||||
[self evaluateJavaScript:BGMNN(jsStr)];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) connectWithRetries:(int)retries {
|
||||
if (retries < 0) {
|
||||
BGMAssert(false, "retries < 0");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!permanentAuthCode) {
|
||||
// Read the API auth code from user defaults (actually the keychain), if there is one. If
|
||||
// the user hasn't authenticated before, it will be nil.
|
||||
//
|
||||
// We do this lazily because it can show a password dialog in debug/unsigned builds.
|
||||
permanentAuthCode = userDefaults.googlePlayMusicDesktopPlayerPermanentAuthCode;
|
||||
}
|
||||
|
||||
connectionRetries = retries;
|
||||
|
||||
// Create the WKWebView we'll use to connect to GPMDP with WebSockets. Using a WKWebView means
|
||||
// Background Music uses a bit more memory while connected to GPMDP, around 15 MB for me, but
|
||||
// saves us having to complicate the build process to add a dependency on a proper library.
|
||||
[self createWebView];
|
||||
|
||||
if (permanentAuthCode) {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::connectWithRetries: "
|
||||
"Connecting with auth code");
|
||||
|
||||
NSString* __nullable percentEncodedCode =
|
||||
[BGMGooglePlayMusicDesktopPlayerConnection
|
||||
toPercentEncoded:BGMNN(permanentAuthCode)];
|
||||
|
||||
[self evaluateJavaScript:[NSString stringWithFormat:@"connect('%@');", percentEncodedCode]];
|
||||
} else {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::connectWithRetries: "
|
||||
"Connecting without auth code");
|
||||
[self evaluateJavaScript:@"connect();"];
|
||||
}
|
||||
|
||||
// Check whether GPMDP is playing, paused or stopped.
|
||||
[self requestPlaybackState];
|
||||
}
|
||||
|
||||
- (void) disconnect {
|
||||
// Stop retrying if we're in the process of connecting.
|
||||
connectionRetries = 0;
|
||||
|
||||
// evaluateJavaScript is only safe to call on the main thread.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::disconnect: Disconnecting");
|
||||
|
||||
[webView evaluateJavaScript:@"disconnect();"
|
||||
completionHandler:^(id __nullable result, NSError* __nullable error) {
|
||||
#pragma unused (result)
|
||||
if (error) {
|
||||
NSLog(@"Error closing connection to GPMDP: %@", error);
|
||||
}
|
||||
|
||||
// Allow the WKWebView to be garbage collected.
|
||||
for (NSString* name in kScriptMessageHandlerNames) {
|
||||
[webView.configuration.userContentController
|
||||
removeScriptMessageHandlerForName:name];
|
||||
}
|
||||
webView = nil;
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
- (void) evaluateJavaScript:(NSString*)js {
|
||||
// evaluateJavaScript is only safe to call on the main thread.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[webView evaluateJavaScript:js
|
||||
completionHandler:^(id __nullable result, NSError* __nullable error) {
|
||||
#pragma unused (result)
|
||||
if (error) {
|
||||
// TODO: We should probably show an error dialog in some cases.
|
||||
NSLog(@"JS error: %@", error);
|
||||
}
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
- (void) playPause {
|
||||
[self evaluateJavaScript:@"playPause();"];
|
||||
}
|
||||
|
||||
- (void) sendAuthCode:(NSString*)authCode {
|
||||
// Don't log the code itself just in case it could be a security problem.
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::sendAuthCode: Sending GPMDP auth code");
|
||||
|
||||
// Percent-encode the user input just in case they entered something that could execute as
|
||||
// Javascript. We could limit the input to four digits instead, but this should be fine.
|
||||
NSString* __nullable percentEncodedCode =
|
||||
[BGMGooglePlayMusicDesktopPlayerConnection toPercentEncoded:authCode];
|
||||
|
||||
// We send the message to GPMDP even if percentEncodedCode is nil so it will reply with an error
|
||||
// and BGMApp will ask the user for the auth code again.
|
||||
NSString* js = [NSString stringWithFormat:@"window.sendAuthCode('%@');", percentEncodedCode];
|
||||
[self evaluateJavaScript:js];
|
||||
}
|
||||
|
||||
- (void) sendPermanentAuthCode {
|
||||
NSString* __nullable code = permanentAuthCode;
|
||||
|
||||
if (code) {
|
||||
// Don't log the code itself just in case it could be a security problem.
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::sendPermanentAuthCode: "
|
||||
"Sending GPMDP permanent auth code");
|
||||
|
||||
// Percent-encode it just in case something it includes could be executed as Javascript.
|
||||
NSString* __nullable percentEncodedCode =
|
||||
[BGMGooglePlayMusicDesktopPlayerConnection toPercentEncoded:BGMNN(code)];
|
||||
|
||||
// Pass the code to our WKWebView so it can send it to GPMDP.
|
||||
NSString* js =
|
||||
[NSString stringWithFormat:@"sendPermanentAuthCode('%@');", percentEncodedCode];
|
||||
[self evaluateJavaScript:js];
|
||||
} else {
|
||||
NSLog(@"BGMGooglePlayMusicDesktopPlayerConnection::sendPermanentAuthCode: No code to send");
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSString* __nullable)toPercentEncoded:(NSString*)rawString {
|
||||
// Just percent-encode every character (by passing an empty NSCharacterSet as the allowed
|
||||
// characters).
|
||||
NSString* __nullable percentEncoded = [rawString
|
||||
stringByAddingPercentEncodingWithAllowedCharacters:
|
||||
[NSCharacterSet characterSetWithCharactersInString:@""]];
|
||||
if (percentEncoded) {
|
||||
return percentEncoded;
|
||||
} else {
|
||||
// The docs say that stringByAddingPercentEncodingWithAllowedCharacters returns nil "if the
|
||||
// transformation is not possible", but don't explain when that could happen. According to
|
||||
// https://stackoverflow.com/a/33558934/1091063 it can be caused by the string containing
|
||||
// invalid unicode.
|
||||
NSLog(@"Could not encode");
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
// Ask GPMDP whether it's playing, paused or stopped. The response is handled asynchronously in
|
||||
// handleResultMessage.
|
||||
- (void) requestPlaybackState {
|
||||
[self evaluateJavaScript:@"requestPlaybackState();"];
|
||||
}
|
||||
|
||||
#pragma mark WKScriptMessageHandler Methods
|
||||
|
||||
- (void) userContentController:(WKUserContentController*)userContentController
|
||||
didReceiveScriptMessage:(WKScriptMessage*)message {
|
||||
#pragma unused (userContentController)
|
||||
|
||||
if ([@"log" isEqual:message.name]) {
|
||||
// The message body is always a string in this case.
|
||||
[self handleLogMessage:message.body];
|
||||
} else if ([@"error" isEqual:message.name]) {
|
||||
[self handleConnectionError];
|
||||
} else {
|
||||
BGMAssert([@"gpmdp" isEqual:message.name], "Unexpected message handler name");
|
||||
[self handleGPMDPMessage:message];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) handleLogMessage:(NSString*)message {
|
||||
(void)message;
|
||||
#if DEBUG
|
||||
if (permanentAuthCode) {
|
||||
// Avoid logging the auth code, which would be a minor security issue.
|
||||
message = [message stringByReplacingOccurrencesOfString:BGMNN(permanentAuthCode)
|
||||
withString:@"<private>"];
|
||||
}
|
||||
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::userContentController: %s",
|
||||
message.UTF8String);
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void) handleConnectionError {
|
||||
if (connectionRetries > 0) {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleConnectionError: "
|
||||
"Retrying in 1 second");
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),
|
||||
dispatch_get_main_queue(),
|
||||
^{
|
||||
// Check connectionRetries again because disconnect may have been called.
|
||||
if (connectionRetries > 0) {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::"
|
||||
"handleConnectionError: Retrying");
|
||||
[self connectWithRetries:(connectionRetries - 1)];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
NSLog(@"BGMGooglePlayMusicDesktopPlayerConnection::handleConnectionError: "
|
||||
"No retries left. Giving up.");
|
||||
connectionErrorHandler();
|
||||
}
|
||||
}
|
||||
|
||||
- (void) handleGPMDPMessage:(WKScriptMessage*)message {
|
||||
// See https://github.com/MarshallOfSound/Google-Play-Music-Desktop-Player-UNOFFICIAL-/blob/master/docs/PlaybackAPI_WebSocket.md
|
||||
|
||||
// Type check.
|
||||
if (![message.body isKindOfClass:[NSDictionary class]]) {
|
||||
NSLog(@"Unexpected message body type");
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary* body = message.body;
|
||||
NSString* messageType;
|
||||
|
||||
// The key for the message type is "channel", except when the message is a response, in which
|
||||
// case the key can be "namespace".
|
||||
if ([body[@"channel"] isKindOfClass:[NSString class]]) {
|
||||
messageType = body[@"channel"];
|
||||
} else if ([body[@"namespace"] isKindOfClass:[NSString class]]) {
|
||||
messageType = body[@"namespace"];
|
||||
} else {
|
||||
NSLog(@"No channel/namespace");
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the message depending on its type (or ignore it).
|
||||
if ([@"API_VERSION" isEqual:messageType]) {
|
||||
[self handleAPIVersionMessage:body];
|
||||
} else if ([@"connect" isEqual:messageType]) {
|
||||
[self handleConnectMessage:body];
|
||||
} else if ([@"playState" isEqual:messageType]) {
|
||||
[self handlePlayStateMessage:body];
|
||||
} else if ([@"result" isEqual:messageType]) {
|
||||
[self handleResultMessage:body];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) handleAPIVersionMessage:(NSDictionary*)body {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleAPIVersionMessage: Response: %s",
|
||||
[NSString stringWithFormat:@"%@", body].UTF8String);
|
||||
|
||||
// Type check.
|
||||
if (![body[@"payload"] isKindOfClass:[NSString class]]) {
|
||||
NSLog(@"Unexpected payload type");
|
||||
[self handleConnectionError];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString* apiVersion = body[@"payload"];
|
||||
// "1.0.0" -> ["1", "0", "0"]
|
||||
NSArray<NSString*>* versionParts = [apiVersion componentsSeparatedByString:@"."];
|
||||
|
||||
// Check the major version number is 1, which is the only major version we support.
|
||||
if (versionParts.count > 0) {
|
||||
NSInteger majorVersion = versionParts[0].integerValue;
|
||||
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleAPIVersionMessage: "
|
||||
"Major version: %lu", majorVersion);
|
||||
|
||||
if (majorVersion == 1) {
|
||||
// GPMDP uses SemVer, so as long as the major version number matches what we can handle,
|
||||
// it should work.
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleAPIVersionMessage: "
|
||||
"This API version is supported");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Show a warning dialog box to the user, but try to continue anyway. There's probably a
|
||||
// reasonable chance it'll still work.
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleAPIVersionMessage: "
|
||||
"Unsupported GPMDP API version");
|
||||
apiVersionMismatchHandler(apiVersion);
|
||||
}
|
||||
|
||||
- (void) handleConnectMessage:(NSDictionary*)body {
|
||||
// Don't log the response as it may contain the auth code.
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleConnectMessage: Received response");
|
||||
|
||||
// Type check.
|
||||
if (![body[@"payload"] isKindOfClass:[NSString class]]) {
|
||||
NSLog(@"Unexpected payload type");
|
||||
[self handleConnectionError];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString* payload = body[@"payload"];
|
||||
|
||||
if ([@"CODE_REQUIRED" isEqual:payload]) {
|
||||
// Ask the user for the auth code GPMDP is displaying and send it to GPMDP to finish
|
||||
// connecting.
|
||||
NSString* __nullable authCode = authRequiredHandler();
|
||||
|
||||
if (authCode) {
|
||||
[self sendAuthCode:BGMNN(authCode)];
|
||||
}
|
||||
} else {
|
||||
// The payload should be the permanent auth code.
|
||||
permanentAuthCode = payload;
|
||||
[self sendPermanentAuthCode];
|
||||
|
||||
// Save the code to the keychain so we can use it when connecting to GPMDP in future.
|
||||
userDefaults.googlePlayMusicDesktopPlayerPermanentAuthCode = permanentAuthCode;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) handlePlayStateMessage:(NSDictionary*)body {
|
||||
(void)body;
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handlePlayStateMessage: Response: %s",
|
||||
[NSString stringWithFormat:@"%@", body].UTF8String);
|
||||
|
||||
// This message tells us the playstate has changed, but doesn't differentiate between stopped
|
||||
// and paused. The response to this API request will. See handleResultMessage.
|
||||
// TODO: Can it transition from stopped to paused? Would that be a problem?
|
||||
[self requestPlaybackState];
|
||||
}
|
||||
|
||||
- (void) handleResultMessage:(NSDictionary*)body {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleResultMessage: Response: %s",
|
||||
[NSString stringWithFormat:@"%@", body].UTF8String);
|
||||
|
||||
// Type check.
|
||||
if (![body[@"value"] isKindOfClass:[NSNumber class]]) {
|
||||
NSLog(@"No value");
|
||||
return;
|
||||
}
|
||||
|
||||
// 0 - Playback is stopped
|
||||
// 1 - Track is paused
|
||||
// 2 - Track is playing
|
||||
int state = ((NSNumber*)body[@"value"]).intValue;
|
||||
_playing = (state == 2);
|
||||
_paused = (state == 1);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
|
@ -54,8 +54,8 @@
|
|||
return (HermesApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMMusicPlayer.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016, 2018 Kyle Neideck
|
||||
// Copyright © 2016, 2018, 2019 Kyle Neideck
|
||||
//
|
||||
// The base classes and protocol for objects that represent a music player app.
|
||||
//
|
||||
|
@ -41,6 +41,9 @@
|
|||
// BGMDriver will log the bundle ID to system.log when it becomes aware of the music player.
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMUserDefaults.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
@ -50,27 +53,26 @@
|
|||
@protocol BGMMusicPlayer <NSObject>
|
||||
|
||||
// Classes return an instance of themselves for each music player app they make available in
|
||||
// BGMApp. So far that's always been a single instance, and classes haven't needed to override
|
||||
// the default implementation of createInstances from BGMMusicPlayerBase. But that will probably
|
||||
// change eventually.
|
||||
// BGMApp. So far that's always been a single instance, but that will probably change eventually.
|
||||
// Most classes don't need to override the default implementation from BGMMusicPlayerBase.
|
||||
//
|
||||
// For example, a class for custom music players would probably return an instance for each
|
||||
// custom player the user has created. (Also note that it could return an empty array.) In that
|
||||
// case the class would probably restore some state from user defaults in its createInstances.
|
||||
// But, for example, a class for custom music players would probably return an instance for each
|
||||
// custom player the user has created. (Also note that it could return an empty array.)
|
||||
//
|
||||
// TODO: I think the return type should actually be NSArray<instancetype>*, but that doesn't seem
|
||||
// to work. There's a Clang bug about this: https://llvm.org/bugs/show_bug.cgi?id=27323
|
||||
// (though it hasn't been confirmed yet).
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstances;
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstancesWithDefaults:(BGMUserDefaults*)userDefaults;
|
||||
|
||||
// We need a unique ID for each music player to store in user defaults. In the most common case,
|
||||
// classes that provide a static (or at least bounded) number of music players, you can generate
|
||||
// IDs with uuidgen (the command line tool) and include them in your class as constants. Otherwise,
|
||||
// you'll probably want to store them in user defaults and retrieve them in your createInstances.
|
||||
// you'll probably want to store them in user defaults and load them in createInstancesWithDefaults.
|
||||
@property (readonly) NSUUID* musicPlayerID;
|
||||
|
||||
// The name and icon of the music player, to be used in the UI.
|
||||
// The name, tool-tip and icon of the music player, to be used in the UI.
|
||||
@property (readonly) NSString* name;
|
||||
@property (readonly) NSString* __nullable toolTip;
|
||||
@property (readonly) NSImage* __nullable icon;
|
||||
|
||||
@property (readonly) NSString* __nullable bundleID;
|
||||
|
@ -81,7 +83,7 @@
|
|||
// TODO: If we ever add a music player class that uses this property, it'll need a way to inform
|
||||
// BGMDevice of changes. It might be easiest to have BGMMusicPlayers to observe this property,
|
||||
// on the selected music player, with KVO and update BGMDevice when it changes. Or
|
||||
// BGMMusicPlayers could pass a pointer to itself to createInstances.
|
||||
// BGMMusicPlayers could pass a pointer to itself to createInstancesWithDefaults.
|
||||
@property NSNumber* __nullable pid;
|
||||
|
||||
// True if this is currently the selected music player.
|
||||
|
@ -101,9 +103,9 @@
|
|||
@property (readonly, getter=isPaused) BOOL paused;
|
||||
|
||||
// Called when the user selects this music player.
|
||||
- (void) onSelect;
|
||||
// Called when this is the selected music player and the user selects a different one.
|
||||
- (void) onDeselect;
|
||||
- (void) wasSelected;
|
||||
// Called when this was the selected music player and the user just selected a different one.
|
||||
- (void) wasDeselected;
|
||||
|
||||
// Pause the music player. Does nothing if the music player is already paused or isn't running.
|
||||
// Returns YES if the music player is paused now but wasn't before, returns NO otherwise.
|
||||
|
@ -123,6 +125,12 @@
|
|||
|
||||
- (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID
|
||||
name:(NSString*)name
|
||||
toolTip:(NSString*)toolTip
|
||||
bundleID:(NSString* __nullable)bundleID;
|
||||
|
||||
- (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID
|
||||
name:(NSString*)name
|
||||
toolTip:(NSString* __nullable)toolTip
|
||||
bundleID:(NSString* __nullable)bundleID
|
||||
pid:(NSNumber* __nullable)pid;
|
||||
|
||||
|
@ -131,15 +139,16 @@
|
|||
+ (NSUUID*) makeID:(NSString*)musicPlayerIDString;
|
||||
|
||||
// BGMMusicPlayer default implementations
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstances;
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstancesWithDefaults:(BGMUserDefaults*)userDefaults;
|
||||
@property (readonly) NSImage* __nullable icon;
|
||||
@property (readonly) NSUUID* musicPlayerID;
|
||||
@property (readonly) NSString* name;
|
||||
@property (readonly) NSString* __nullable toolTip;
|
||||
@property (readonly) NSString* __nullable bundleID;
|
||||
@property NSNumber* __nullable pid;
|
||||
@property (readonly) BOOL selected;
|
||||
- (void) onSelect;
|
||||
- (void) onDeselect;
|
||||
- (void) wasSelected;
|
||||
- (void) wasDeselected;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMMusicPlayer.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
// Copyright © 2016-2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
|
@ -33,6 +33,7 @@
|
|||
|
||||
@synthesize musicPlayerID = _musicPlayerID;
|
||||
@synthesize name = _name;
|
||||
@synthesize toolTip = _toolTip;
|
||||
@synthesize bundleID = _bundleID;
|
||||
@synthesize pid = _pid;
|
||||
@synthesize selected = _selected;
|
||||
|
@ -40,11 +41,27 @@
|
|||
- (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID
|
||||
name:(NSString*)name
|
||||
bundleID:(NSString* __nullable)bundleID {
|
||||
return [self initWithMusicPlayerID:musicPlayerID name:name bundleID:bundleID pid:nil];
|
||||
return [self initWithMusicPlayerID:musicPlayerID
|
||||
name:name
|
||||
toolTip:nil
|
||||
bundleID:bundleID
|
||||
pid:nil];
|
||||
}
|
||||
|
||||
- (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID
|
||||
name:(NSString*)name
|
||||
toolTip:(NSString*)toolTip
|
||||
bundleID:(NSString* __nullable)bundleID {
|
||||
return [self initWithMusicPlayerID:musicPlayerID
|
||||
name:name
|
||||
toolTip:toolTip
|
||||
bundleID:bundleID
|
||||
pid:nil];
|
||||
}
|
||||
|
||||
- (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID
|
||||
name:(NSString*)name
|
||||
toolTip:(NSString* __nullable)toolTip
|
||||
bundleID:(NSString* __nullable)bundleID
|
||||
pid:(NSNumber* __nullable)pid {
|
||||
if ((self = [super init])) {
|
||||
|
@ -55,6 +72,7 @@
|
|||
|
||||
_musicPlayerID = musicPlayerID;
|
||||
_name = name;
|
||||
_toolTip = toolTip;
|
||||
_bundleID = bundleID;
|
||||
_pid = pid;
|
||||
_selected = NO;
|
||||
|
@ -72,7 +90,8 @@
|
|||
|
||||
#pragma mark BGMMusicPlayer default implementations
|
||||
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstances {
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstancesWithDefaults:(BGMUserDefaults*)userDefaults {
|
||||
#pragma unused (userDefaults)
|
||||
return @[ [self new] ];
|
||||
}
|
||||
|
||||
|
@ -84,11 +103,11 @@
|
|||
return (!bundlePath ? nil : [[NSWorkspace sharedWorkspace] iconForFile:(NSString*)bundlePath]);
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
- (void) wasSelected {
|
||||
_selected = YES;
|
||||
}
|
||||
|
||||
- (void) onDeselect {
|
||||
- (void) wasDeselected {
|
||||
_selected = NO;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMMusicPlayers.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2019 Kyle Neideck
|
||||
//
|
||||
// Holds the music players (i.e. BGMMusicPlayer objects) available in BGMApp. Also keeps track of
|
||||
// which music player is currently selected by the user.
|
||||
|
@ -43,8 +43,8 @@
|
|||
// defaultMusicPlayerID is the musicPlayerID (see BGMMusicPlayer.h) of the music player that should be
|
||||
// selected by default.
|
||||
//
|
||||
// The createInstances method of each class in musicPlayerClasses will be called, and the results stored
|
||||
// in the musicPlayers property.
|
||||
// The createInstancesWithDefaults method of each class in musicPlayerClasses will be called and
|
||||
// the results will be stored in the musicPlayers property.
|
||||
- (instancetype) initWithAudioDevices:(BGMAudioDeviceManager*)devices
|
||||
defaultMusicPlayerID:(NSUUID*)defaultMusicPlayerID
|
||||
musicPlayerClasses:(NSArray<Class<BGMMusicPlayer>>*)musicPlayerClasses
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMMusicPlayers.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
// Copyright © 2016-2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self include
|
||||
|
@ -34,6 +34,7 @@
|
|||
#import "BGMDecibel.h"
|
||||
#import "BGMHermes.h"
|
||||
#import "BGMSwinsian.h"
|
||||
#import "BGMGooglePlayMusicDesktopPlayer.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
@ -47,16 +48,24 @@
|
|||
|
||||
- (instancetype) initWithAudioDevices:(BGMAudioDeviceManager*)devices
|
||||
userDefaults:(BGMUserDefaults*)defaults {
|
||||
// The classes handling each music player we support. If you write a new music player class, add
|
||||
// it to this array.
|
||||
NSArray<Class<BGMMusicPlayer>>* mpClasses = @[ [BGMVOX class],
|
||||
[BGMVLC class],
|
||||
[BGMSpotify class],
|
||||
[BGMiTunes class],
|
||||
[BGMDecibel class],
|
||||
[BGMHermes class],
|
||||
[BGMSwinsian class] ];
|
||||
|
||||
// We only support Google Play Music Desktop Player on macOS 10.10 and higher.
|
||||
if (@available(macOS 10.10, *)) {
|
||||
mpClasses = [mpClasses arrayByAddingObject:[BGMGooglePlayMusicDesktopPlayer class]];
|
||||
}
|
||||
|
||||
return [self initWithAudioDevices:devices
|
||||
defaultMusicPlayerID:[BGMiTunes sharedMusicPlayerID]
|
||||
// If you write a new music player class, add it to this array.
|
||||
musicPlayerClasses:@[ [BGMVOX class],
|
||||
[BGMVLC class],
|
||||
[BGMSpotify class],
|
||||
[BGMiTunes class],
|
||||
[BGMDecibel class],
|
||||
[BGMHermes class],
|
||||
[BGMSwinsian class] ]
|
||||
musicPlayerClasses:mpClasses
|
||||
userDefaults:defaults];
|
||||
}
|
||||
|
||||
|
@ -70,11 +79,14 @@
|
|||
|
||||
// Init _musicPlayers, an array containing one object for each music player in BGMApp.
|
||||
//
|
||||
// Each music player class has a factory method, createInstances, that returns all the instances of that
|
||||
// class BGMApp will use. (Though so far it's always just one instance.)
|
||||
// Each music player class has a factory method, createInstancesWithDefaults, that returns
|
||||
// all the instances of that class BGMApp will use. (Though so far it's always just one
|
||||
// instance.)
|
||||
NSMutableArray* musicPlayers = [NSMutableArray new];
|
||||
for (Class<BGMMusicPlayer> musicPlayerClass in musicPlayerClasses) {
|
||||
[musicPlayers addObjectsFromArray:[musicPlayerClass createInstances]];
|
||||
NSArray<id<BGMMusicPlayer>>* instances =
|
||||
[musicPlayerClass createInstancesWithDefaults:userDefaults];
|
||||
[musicPlayers addObjectsFromArray:instances];
|
||||
}
|
||||
|
||||
_musicPlayers = [NSArray arrayWithArray:musicPlayers];
|
||||
|
@ -215,7 +227,7 @@
|
|||
}
|
||||
|
||||
// Tell the current music player (object) a different player has been selected.
|
||||
[_selectedMusicPlayer onDeselect];
|
||||
[_selectedMusicPlayer wasDeselected];
|
||||
|
||||
_selectedMusicPlayer = newSelectedMusicPlayer;
|
||||
|
||||
|
@ -229,7 +241,7 @@
|
|||
userDefaults.selectedMusicPlayerID = _selectedMusicPlayer.musicPlayerID.UUIDString;
|
||||
|
||||
// Tell the music player (object) it's been selected.
|
||||
[_selectedMusicPlayer onSelect];
|
||||
[_selectedMusicPlayer wasSelected];
|
||||
}
|
||||
|
||||
- (void) updateBGMDeviceMusicPlayerProperties {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMScriptingBridge.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
// Copyright © 2016-2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
|
@ -25,6 +25,7 @@
|
|||
|
||||
// Local Includes
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMAppWatcher.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CADebugMacros.h"
|
||||
|
@ -34,8 +35,7 @@
|
|||
|
||||
@implementation BGMScriptingBridge {
|
||||
id<BGMMusicPlayer> __weak _musicPlayer;
|
||||
// Tokens for the notification observers. We need these to remove the observers in dealloc.
|
||||
id _didLaunchToken, _didTerminateToken;
|
||||
BGMAppWatcher* appWatcher;
|
||||
}
|
||||
|
||||
@synthesize application = _application;
|
||||
|
@ -57,17 +57,14 @@
|
|||
BGMScriptingBridge* __weak weakSelf = self;
|
||||
|
||||
void (^createSBApplication)(void) = ^{
|
||||
BGMScriptingBridge* __strong strongSelf = weakSelf;
|
||||
BGMScriptingBridge* strongSelf = weakSelf;
|
||||
strongSelf->_application = [SBApplication applicationWithBundleIdentifier:bundleID];
|
||||
// TODO: I think the SBApplication will still keep a strong ref to this object, so we might
|
||||
// have to make a separate delegate object.
|
||||
// TODO: The SBApplication will still keep a strong ref to this object, so we would have to
|
||||
// make a separate delegate object to avoid the retain cycle. Not currently a problem
|
||||
// because we only ever create instances that live forever.
|
||||
strongSelf->_application.delegate = strongSelf;
|
||||
};
|
||||
|
||||
BOOL (^isAboutThisMusicPlayer)(NSNotification*) = ^(NSNotification* note) {
|
||||
return [[note.userInfo[NSWorkspaceApplicationKey] bundleIdentifier] isEqualToString:bundleID];
|
||||
};
|
||||
|
||||
// Add observers that create/destroy the SBApplication when the music player is launched/terminated. We
|
||||
// only create the SBApplication when the music player is open. If it isn't open, creating the
|
||||
// SBApplication or sending it events could launch the music player. Whether or not it does depends on
|
||||
|
@ -76,31 +73,20 @@
|
|||
// From the docs for SBApplication's applicationWithBundleIdentifier method:
|
||||
// "For applications that declare themselves to have a dynamic scripting interface, this method will
|
||||
// launch the application if it is not already running."
|
||||
NSNotificationCenter* center = [[NSWorkspace sharedWorkspace] notificationCenter];
|
||||
_didLaunchToken = [center addObserverForName:NSWorkspaceDidLaunchApplicationNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* note)
|
||||
{
|
||||
if (isAboutThisMusicPlayer(note)) {
|
||||
DebugMsg("BGMScriptingBridge::initApplication: %s launched",
|
||||
bundleID.UTF8String);
|
||||
createSBApplication();
|
||||
[weakSelf ensurePermission];
|
||||
}
|
||||
}];
|
||||
_didTerminateToken = [center addObserverForName:NSWorkspaceDidTerminateApplicationNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* note)
|
||||
{
|
||||
if (isAboutThisMusicPlayer(note)) {
|
||||
DebugMsg("BGMScriptingBridge::initApplication: %s terminated",
|
||||
bundleID.UTF8String);
|
||||
BGMScriptingBridge* __strong strongSelf = weakSelf;
|
||||
strongSelf->_application = nil;
|
||||
}
|
||||
}];
|
||||
appWatcher =
|
||||
[[BGMAppWatcher alloc] initWithBundleID:bundleID
|
||||
appLaunched:^{
|
||||
DebugMsg("BGMScriptingBridge::initApplication: %s launched",
|
||||
bundleID.UTF8String);
|
||||
createSBApplication();
|
||||
[weakSelf ensurePermission];
|
||||
}
|
||||
appTerminated:^{
|
||||
BGMScriptingBridge* strongSelf = weakSelf;
|
||||
DebugMsg("BGMScriptingBridge::initApplication: %s terminated",
|
||||
bundleID.UTF8String);
|
||||
strongSelf->_application = nil;
|
||||
}];
|
||||
|
||||
// Create the SBApplication if the music player is already running.
|
||||
if ([NSRunningApplication runningApplicationsWithBundleIdentifier:bundleID].count > 0) {
|
||||
|
@ -108,19 +94,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
// Remove the application launch/termination observers.
|
||||
NSNotificationCenter* center = [NSWorkspace sharedWorkspace].notificationCenter;
|
||||
|
||||
if (_didLaunchToken) {
|
||||
[center removeObserver:_didLaunchToken];
|
||||
}
|
||||
|
||||
if (_didTerminateToken) {
|
||||
[center removeObserver:_didTerminateToken];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) ensurePermission {
|
||||
// Skip this check if running on a version of macOS before 10.14. In that case, we don't require
|
||||
// user permission to send Apple Events. Also skip it if compiling on an earlier version.
|
||||
|
|
|
@ -57,8 +57,8 @@
|
|||
return (SpotifyApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
|
|
|
@ -57,8 +57,8 @@
|
|||
return (SwinsianApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
|
|
|
@ -54,8 +54,8 @@
|
|||
return (VLCApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
|
|
|
@ -53,8 +53,8 @@
|
|||
return (VoxApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
|
|
|
@ -59,8 +59,8 @@
|
|||
return (iTunesApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
|
|
190
BGMApp/BGMApp/Music Players/GooglePlayMusicDesktopPlayer.js
Normal file
190
BGMApp/BGMApp/Music Players/GooglePlayMusicDesktopPlayer.js
Normal file
|
@ -0,0 +1,190 @@
|
|||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// GooglePlayMusicDesktopPlayer.js
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// The specification for GPMDP's API:
|
||||
// https://github.com/MarshallOfSound/Google-Play-Music-Desktop-Player-UNOFFICIAL-/blob/master/docs/PlaybackAPI_WebSocket.md
|
||||
|
||||
try {
|
||||
|
||||
window._log = msg => {
|
||||
window.webkit.messageHandlers.log.postMessage(msg);
|
||||
};
|
||||
|
||||
// Global JS error handler.
|
||||
window.onerror = (msg, url, line, col, error) => {
|
||||
let extra = !col ? '' : '\nColumn: ' + col;
|
||||
extra += !error ? '' : '\nError: ' + error;
|
||||
|
||||
// TODO: I'm not sure this log message is ever actually useful.
|
||||
window._log('Error: ' + msg + '\nURL: ' + url + '\nLine: ' + line + extra);
|
||||
|
||||
window.webkit.messageHandlers.error.postMessage(error);
|
||||
};
|
||||
|
||||
// Send a JSON message to GPMDP.
|
||||
//
|
||||
// If we're connecting, this function will return immediately and the message will be sent after we
|
||||
// finish connecting. Logs an error and returns if window.connect() hasn't been called yet.
|
||||
window._sendJSON = json => {
|
||||
if (window._wsPromise) {
|
||||
window._wsPromise.then(() => {
|
||||
window._sendJSONImmediate(json);
|
||||
}).catch(error => {
|
||||
// TODO: Is there anything else we can do? Retries?
|
||||
window._log('Error sending JSON: ' + JSON.stringify(error));
|
||||
});
|
||||
} else {
|
||||
window._log('Error: No WebSocket promise. Discarding JSON message: ' +
|
||||
JSON.stringify(json));
|
||||
}
|
||||
};
|
||||
|
||||
// Send a JSON message to GPMDP, but don't wait if we're in the process of connecting.
|
||||
//
|
||||
// Logs an error and returns if window.connect() hasn't been called yet. The authCode param is
|
||||
// optional and only used to hide the code in log messages.
|
||||
window._sendJSONImmediate = (json, authCode) => {
|
||||
let jsonStr = JSON.stringify(json);
|
||||
let jsonStrSanitized = authCode ? jsonStr.replace(authCode, "<private>") : jsonStr;
|
||||
|
||||
if (window._ws) {
|
||||
window._log('Sending JSON: ' + jsonStrSanitized);
|
||||
window._ws.send(jsonStr);
|
||||
} else {
|
||||
window._log('Error: No WebSocket. Discarding JSON message: ' + jsonStrSanitized);
|
||||
}
|
||||
};
|
||||
|
||||
// permanentAuthCode is optional. If this is the first time they've selected GPMDP, we won't have a
|
||||
// permanent code yet.
|
||||
window.connect = permanentAuthCode => {
|
||||
// Reset the connection state.
|
||||
window._requestID = 1;
|
||||
|
||||
// Close the existing connection if we're already connected.
|
||||
window.disconnect();
|
||||
|
||||
// Create the new connection.
|
||||
window._ws = new WebSocket('ws://localhost:5672');
|
||||
|
||||
window._ws.onmessage = event => {
|
||||
// Pass the message along to BGMGooglePlayMusicDesktopPlayerConnection.
|
||||
let reply = JSON.parse(event.data);
|
||||
window.webkit.messageHandlers.gpmdp.postMessage(reply);
|
||||
};
|
||||
|
||||
window._wsPromise = new Promise((resolve, reject) => {
|
||||
window._ws.onopen = () => {
|
||||
// Send GPMDP the initial connection message.
|
||||
if (permanentAuthCode) {
|
||||
window._log('Connecting with auth code');
|
||||
window.sendPermanentAuthCode(permanentAuthCode);
|
||||
} else {
|
||||
// Since we don't have an auth code, it will display a four-digit code and reply
|
||||
// telling us to ask the user to type it into Background Music.
|
||||
window._log('Connecting without auth code');
|
||||
|
||||
window._sendJSONImmediate({
|
||||
'namespace': 'connect',
|
||||
'method': 'connect',
|
||||
'arguments': ['Background Music']
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window._ws.onerror = error => {
|
||||
// Report the error to BGMGooglePlayMusicDesktopPlayerConnection.
|
||||
window.webkit.messageHandlers.error.postMessage(error);
|
||||
// Reject the connection promise.
|
||||
reject(error);
|
||||
};
|
||||
|
||||
// Store the function that resolves this promise. We resolve it after we finish
|
||||
// authenticating.
|
||||
window._resolveConnectionPromise = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
// Close the connection to GPMDP. Does nothing if we aren't connected.
|
||||
window.disconnect = () => {
|
||||
if (window._ws) {
|
||||
window._log('Closing WebSocket');
|
||||
window._ws.close();
|
||||
window._ws = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Send an authentication code to GPMDP. To send a four-digit code (i.e. one entered by the user),
|
||||
// call this directly. To send a permanent code received from GPMDP, use
|
||||
// window.sendPermanentAuthCode().
|
||||
//
|
||||
// authCode should be percent-encoded.
|
||||
window.sendAuthCode = authCode => {
|
||||
// Percent-decode the auth code string. We pass it percent-encoded just to make sure nothing in
|
||||
// it accidentally gets executed as Javascript.
|
||||
authCode = window.decodeURIComponent(authCode);
|
||||
|
||||
window._sendJSONImmediate({
|
||||
'namespace': 'connect',
|
||||
'method': 'connect',
|
||||
'arguments': ['Background Music', authCode]
|
||||
}, authCode);
|
||||
};
|
||||
|
||||
// Send a permanent authentication code, received from GPMDP previously, to GPMDP.
|
||||
window.sendPermanentAuthCode = permanentAuthCode => {
|
||||
window._log('Sending permanent auth code');
|
||||
window.sendAuthCode(permanentAuthCode);
|
||||
// TODO: If the code is rejected, GPMDP will send us a connect message and we'll show the auth
|
||||
// code dialog, but accepting the promise here means some messages we send might get
|
||||
// ignored.
|
||||
window._resolveConnectionPromise();
|
||||
};
|
||||
|
||||
// Ask GPMDP to send us its current playback state (playing, paused or stopped).
|
||||
window.requestPlaybackState = () => {
|
||||
window._sendJSON({
|
||||
'namespace': 'playback',
|
||||
'method': 'getPlaybackState',
|
||||
// We don't send any other types of request, so the ID we send only needs to be unique.
|
||||
'requestID': window._requestID++
|
||||
});
|
||||
};
|
||||
|
||||
// Tell GPMDP to toggle between playing and paused.
|
||||
window.playPause = () => {
|
||||
window._sendJSON({
|
||||
'namespace': 'playback',
|
||||
'method': 'playPause'
|
||||
});
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
window.webkit.messageHandlers.log.postMessage('Error: ' + JSON.stringify(error));
|
||||
window.webkit.messageHandlers.log.postMessage(JSON.stringify(error.stack));
|
||||
window.webkit.messageHandlers.error.postMessage(error);
|
||||
}
|
||||
|
||||
// Return an empty string as returning some types can cause an error when this Javascript is loaded
|
||||
// into the WKWebView.
|
||||
""
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
// BGMAutoPauseMusicPrefs.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Includes
|
||||
|
@ -69,6 +69,7 @@ static NSInteger const kPrefsMenuAutoPauseHeaderTag = 1;
|
|||
action:@selector(handleMusicPlayerChange:)
|
||||
keyEquivalent:@""
|
||||
atIndex:musicPlayerItemsIndex];
|
||||
menuItem.toolTip = musicPlayer.toolTip;
|
||||
|
||||
musicPlayerMenuItems = [musicPlayerMenuItems arrayByAddingObject:menuItem];
|
||||
|
||||
|
|
Loading…
Reference in a new issue