├── .gitignore ├── DEVELOP.md ├── LICENSE ├── Menu Bar Ticker.xcodeproj └── project.pbxproj ├── MenubarTicker ├── AppDelegate.h ├── AppDelegate.m ├── Base.lproj │ └── MainMenu.xib ├── Menu Bar Ticker-Info.plist ├── Music.h ├── Spotify.h ├── en.lproj │ ├── Credits.rtf │ └── InfoPlist.strings └── main.m ├── README.md └── screenshots ├── dark-active.png ├── dark-idle.png ├── light-active.png └── light-idle.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS X 2 | .DS_Store 3 | 4 | # Vim 5 | .*.sw[a-z] 6 | 7 | # Emacs 8 | \#*# 9 | *~ 10 | 11 | # Xcode 4 12 | build/ 13 | xcuserdata/ 14 | *.xcworkspace/ 15 | 16 | # Other Hidden Files 17 | .* 18 | -------------------------------------------------------------------------------- /DEVELOP.md: -------------------------------------------------------------------------------- 1 | # Development Cheatsheet 2 | 3 | ## Generate a new CFBundleVersion 4 | 5 | ```fish 6 | date -u '+%Y%m%d%H%M%S' 7 | ``` 8 | 9 | ## Tag a release 10 | 11 | ```fish 12 | git tag --annotate --message='' vX.Y.Z 13 | ``` 14 | 15 | ## Make a build in Xcode 16 | 17 | 1. Product > Archive 18 | 1. Select the new archive in the Organizer. Click "Distribute App". 19 | 1. Select "Copy App". 20 | 1. Click "Export". 21 | 22 | ## Package the app bundle 23 | 24 | ```fish 25 | tar cvjf menubar-ticker-vX.Y.Z.tar.bz2 'Menu Bar Ticker.app' 26 | ``` 27 | 28 | ## Reset access approvals for Music and Spotify 29 | 30 | ```fish 31 | tccutil reset All org.serban.MenuBarTicker 32 | ``` 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Serban Giuroiu 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Menu Bar Ticker.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4CC719E2157C1FD7008976AD /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CC719E1157C1FD7008976AD /* Cocoa.framework */; }; 11 | 4CC719EC157C1FD7008976AD /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4CC719EA157C1FD7008976AD /* InfoPlist.strings */; }; 12 | 4CC719EE157C1FD7008976AD /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CC719ED157C1FD7008976AD /* main.m */; }; 13 | 4CC719F2157C1FD7008976AD /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 4CC719F0157C1FD7008976AD /* Credits.rtf */; }; 14 | 4CC719F5157C1FD7008976AD /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CC719F4157C1FD7008976AD /* AppDelegate.m */; }; 15 | 4CC719F8157C1FD7008976AD /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4CC719F6157C1FD7008976AD /* MainMenu.xib */; }; 16 | 4CC71A17157C2A59008976AD /* ScriptingBridge.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CC71A16157C2A59008976AD /* ScriptingBridge.framework */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 0005E3B923DE30AE002FC513 /* Music.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Music.h; sourceTree = ""; }; 21 | 4C51503225CB911200CCF4BE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 22 | 4CC719DD157C1FD7008976AD /* Menu Bar Ticker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Menu Bar Ticker.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | 4CC719E1157C1FD7008976AD /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 24 | 4CC719E4157C1FD7008976AD /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 25 | 4CC719E5157C1FD7008976AD /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 26 | 4CC719E6157C1FD7008976AD /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 27 | 4CC719E9157C1FD7008976AD /* Menu Bar Ticker-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Menu Bar Ticker-Info.plist"; sourceTree = ""; }; 28 | 4CC719EB157C1FD7008976AD /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 29 | 4CC719ED157C1FD7008976AD /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 30 | 4CC719F1157C1FD7008976AD /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = ""; }; 31 | 4CC719F3157C1FD7008976AD /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 32 | 4CC719F4157C1FD7008976AD /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 33 | 4CC71A16157C2A59008976AD /* ScriptingBridge.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ScriptingBridge.framework; path = System/Library/Frameworks/ScriptingBridge.framework; sourceTree = SDKROOT; }; 34 | 4CC71A18157C398E008976AD /* Spotify.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Spotify.h; sourceTree = ""; }; 35 | /* End PBXFileReference section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | 4CC719DA157C1FD7008976AD /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 2147483647; 41 | files = ( 42 | 4CC71A17157C2A59008976AD /* ScriptingBridge.framework in Frameworks */, 43 | 4CC719E2157C1FD7008976AD /* Cocoa.framework in Frameworks */, 44 | ); 45 | runOnlyForDeploymentPostprocessing = 0; 46 | }; 47 | /* End PBXFrameworksBuildPhase section */ 48 | 49 | /* Begin PBXGroup section */ 50 | 4CC719D2157C1FD7008976AD = { 51 | isa = PBXGroup; 52 | children = ( 53 | 4CC71A16157C2A59008976AD /* ScriptingBridge.framework */, 54 | 4CC719E7157C1FD7008976AD /* MenubarTicker */, 55 | 4CC719E0157C1FD7008976AD /* Frameworks */, 56 | 4CC719DE157C1FD7008976AD /* Products */, 57 | ); 58 | sourceTree = ""; 59 | }; 60 | 4CC719DE157C1FD7008976AD /* Products */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 4CC719DD157C1FD7008976AD /* Menu Bar Ticker.app */, 64 | ); 65 | name = Products; 66 | sourceTree = ""; 67 | }; 68 | 4CC719E0157C1FD7008976AD /* Frameworks */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | 4CC719E1157C1FD7008976AD /* Cocoa.framework */, 72 | 4CC719E3157C1FD7008976AD /* Other Frameworks */, 73 | ); 74 | name = Frameworks; 75 | sourceTree = ""; 76 | }; 77 | 4CC719E3157C1FD7008976AD /* Other Frameworks */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | 4CC719E4157C1FD7008976AD /* AppKit.framework */, 81 | 4CC719E5157C1FD7008976AD /* CoreData.framework */, 82 | 4CC719E6157C1FD7008976AD /* Foundation.framework */, 83 | ); 84 | name = "Other Frameworks"; 85 | sourceTree = ""; 86 | }; 87 | 4CC719E7157C1FD7008976AD /* MenubarTicker */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 4CC719F3157C1FD7008976AD /* AppDelegate.h */, 91 | 4CC719F4157C1FD7008976AD /* AppDelegate.m */, 92 | 0005E3B923DE30AE002FC513 /* Music.h */, 93 | 4CC71A18157C398E008976AD /* Spotify.h */, 94 | 4CC719F6157C1FD7008976AD /* MainMenu.xib */, 95 | 4CC719E8157C1FD7008976AD /* Supporting Files */, 96 | ); 97 | path = MenubarTicker; 98 | sourceTree = ""; 99 | }; 100 | 4CC719E8157C1FD7008976AD /* Supporting Files */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 4CC719E9157C1FD7008976AD /* Menu Bar Ticker-Info.plist */, 104 | 4CC719EA157C1FD7008976AD /* InfoPlist.strings */, 105 | 4CC719ED157C1FD7008976AD /* main.m */, 106 | 4CC719F0157C1FD7008976AD /* Credits.rtf */, 107 | ); 108 | name = "Supporting Files"; 109 | sourceTree = ""; 110 | }; 111 | /* End PBXGroup section */ 112 | 113 | /* Begin PBXNativeTarget section */ 114 | 4CC719DC157C1FD7008976AD /* Menu Bar Ticker */ = { 115 | isa = PBXNativeTarget; 116 | buildConfigurationList = 4CC71A0F157C1FD7008976AD /* Build configuration list for PBXNativeTarget "Menu Bar Ticker" */; 117 | buildPhases = ( 118 | 4CC719D9157C1FD7008976AD /* Sources */, 119 | 4CC719DA157C1FD7008976AD /* Frameworks */, 120 | 4CC719DB157C1FD7008976AD /* Resources */, 121 | ); 122 | buildRules = ( 123 | ); 124 | dependencies = ( 125 | ); 126 | name = "Menu Bar Ticker"; 127 | productName = MenubarTicker; 128 | productReference = 4CC719DD157C1FD7008976AD /* Menu Bar Ticker.app */; 129 | productType = "com.apple.product-type.application"; 130 | }; 131 | /* End PBXNativeTarget section */ 132 | 133 | /* Begin PBXProject section */ 134 | 4CC719D4157C1FD7008976AD /* Project object */ = { 135 | isa = PBXProject; 136 | attributes = { 137 | LastTestingUpgradeCheck = 0630; 138 | LastUpgradeCheck = 1240; 139 | }; 140 | buildConfigurationList = 4CC719D7157C1FD7008976AD /* Build configuration list for PBXProject "Menu Bar Ticker" */; 141 | compatibilityVersion = "Xcode 3.2"; 142 | developmentRegion = en; 143 | hasScannedForEncodings = 0; 144 | knownRegions = ( 145 | en, 146 | Base, 147 | ); 148 | mainGroup = 4CC719D2157C1FD7008976AD; 149 | productRefGroup = 4CC719DE157C1FD7008976AD /* Products */; 150 | projectDirPath = ""; 151 | projectRoot = ""; 152 | targets = ( 153 | 4CC719DC157C1FD7008976AD /* Menu Bar Ticker */, 154 | ); 155 | }; 156 | /* End PBXProject section */ 157 | 158 | /* Begin PBXResourcesBuildPhase section */ 159 | 4CC719DB157C1FD7008976AD /* Resources */ = { 160 | isa = PBXResourcesBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | 4CC719EC157C1FD7008976AD /* InfoPlist.strings in Resources */, 164 | 4CC719F2157C1FD7008976AD /* Credits.rtf in Resources */, 165 | 4CC719F8157C1FD7008976AD /* MainMenu.xib in Resources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXResourcesBuildPhase section */ 170 | 171 | /* Begin PBXSourcesBuildPhase section */ 172 | 4CC719D9157C1FD7008976AD /* Sources */ = { 173 | isa = PBXSourcesBuildPhase; 174 | buildActionMask = 2147483647; 175 | files = ( 176 | 4CC719EE157C1FD7008976AD /* main.m in Sources */, 177 | 4CC719F5157C1FD7008976AD /* AppDelegate.m in Sources */, 178 | ); 179 | runOnlyForDeploymentPostprocessing = 0; 180 | }; 181 | /* End PBXSourcesBuildPhase section */ 182 | 183 | /* Begin PBXVariantGroup section */ 184 | 4CC719EA157C1FD7008976AD /* InfoPlist.strings */ = { 185 | isa = PBXVariantGroup; 186 | children = ( 187 | 4CC719EB157C1FD7008976AD /* en */, 188 | ); 189 | name = InfoPlist.strings; 190 | sourceTree = ""; 191 | }; 192 | 4CC719F0157C1FD7008976AD /* Credits.rtf */ = { 193 | isa = PBXVariantGroup; 194 | children = ( 195 | 4CC719F1157C1FD7008976AD /* en */, 196 | ); 197 | name = Credits.rtf; 198 | sourceTree = ""; 199 | }; 200 | 4CC719F6157C1FD7008976AD /* MainMenu.xib */ = { 201 | isa = PBXVariantGroup; 202 | children = ( 203 | 4C51503225CB911200CCF4BE /* Base */, 204 | ); 205 | name = MainMenu.xib; 206 | sourceTree = ""; 207 | }; 208 | /* End PBXVariantGroup section */ 209 | 210 | /* Begin XCBuildConfiguration section */ 211 | 4CC71A0D157C1FD7008976AD /* Debug */ = { 212 | isa = XCBuildConfiguration; 213 | buildSettings = { 214 | ALWAYS_SEARCH_USER_PATHS = NO; 215 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 216 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 217 | CLANG_WARN_BOOL_CONVERSION = YES; 218 | CLANG_WARN_COMMA = YES; 219 | CLANG_WARN_CONSTANT_CONVERSION = YES; 220 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 221 | CLANG_WARN_EMPTY_BODY = YES; 222 | CLANG_WARN_ENUM_CONVERSION = YES; 223 | CLANG_WARN_INFINITE_RECURSION = YES; 224 | CLANG_WARN_INT_CONVERSION = YES; 225 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 226 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 227 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 228 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 229 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 230 | CLANG_WARN_STRICT_PROTOTYPES = YES; 231 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 232 | CLANG_WARN_UNREACHABLE_CODE = YES; 233 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 234 | COPY_PHASE_STRIP = NO; 235 | ENABLE_STRICT_OBJC_MSGSEND = YES; 236 | ENABLE_TESTABILITY = YES; 237 | GCC_C_LANGUAGE_STANDARD = gnu99; 238 | GCC_DYNAMIC_NO_PIC = NO; 239 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 240 | GCC_NO_COMMON_BLOCKS = YES; 241 | GCC_OPTIMIZATION_LEVEL = 0; 242 | GCC_PREPROCESSOR_DEFINITIONS = ( 243 | "DEBUG=1", 244 | "$(inherited)", 245 | ); 246 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 247 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 248 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 249 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 250 | GCC_WARN_UNDECLARED_SELECTOR = YES; 251 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 252 | GCC_WARN_UNUSED_FUNCTION = YES; 253 | GCC_WARN_UNUSED_VARIABLE = YES; 254 | MACOSX_DEPLOYMENT_TARGET = 10.15; 255 | ONLY_ACTIVE_ARCH = YES; 256 | PRODUCT_BUNDLE_IDENTIFIER = org.serban.MenuBarTicker; 257 | PRODUCT_NAME = "Menu Bar Ticker"; 258 | SDKROOT = macosx; 259 | }; 260 | name = Debug; 261 | }; 262 | 4CC71A0E157C1FD7008976AD /* Release */ = { 263 | isa = XCBuildConfiguration; 264 | buildSettings = { 265 | ALWAYS_SEARCH_USER_PATHS = NO; 266 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 267 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 268 | CLANG_WARN_BOOL_CONVERSION = YES; 269 | CLANG_WARN_COMMA = YES; 270 | CLANG_WARN_CONSTANT_CONVERSION = YES; 271 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 272 | CLANG_WARN_EMPTY_BODY = YES; 273 | CLANG_WARN_ENUM_CONVERSION = YES; 274 | CLANG_WARN_INFINITE_RECURSION = YES; 275 | CLANG_WARN_INT_CONVERSION = YES; 276 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 277 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 278 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 279 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 280 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 281 | CLANG_WARN_STRICT_PROTOTYPES = YES; 282 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 283 | CLANG_WARN_UNREACHABLE_CODE = YES; 284 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 285 | COPY_PHASE_STRIP = YES; 286 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 287 | ENABLE_STRICT_OBJC_MSGSEND = YES; 288 | GCC_C_LANGUAGE_STANDARD = gnu99; 289 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 290 | GCC_NO_COMMON_BLOCKS = YES; 291 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 292 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 293 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 294 | GCC_WARN_UNDECLARED_SELECTOR = YES; 295 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 296 | GCC_WARN_UNUSED_FUNCTION = YES; 297 | GCC_WARN_UNUSED_VARIABLE = YES; 298 | MACOSX_DEPLOYMENT_TARGET = 10.15; 299 | PRODUCT_BUNDLE_IDENTIFIER = org.serban.MenuBarTicker; 300 | PRODUCT_NAME = "Menu Bar Ticker"; 301 | SDKROOT = macosx; 302 | }; 303 | name = Release; 304 | }; 305 | 4CC71A10157C1FD7008976AD /* Debug */ = { 306 | isa = XCBuildConfiguration; 307 | buildSettings = { 308 | CLANG_ENABLE_OBJC_WEAK = YES; 309 | COMBINE_HIDPI_IMAGES = YES; 310 | GCC_PRECOMPILE_PREFIX_HEADER = NO; 311 | INFOPLIST_FILE = "MenubarTicker/Menu Bar Ticker-Info.plist"; 312 | PRODUCT_BUNDLE_IDENTIFIER = org.serban.MenuBarTicker; 313 | PRODUCT_NAME = "$(TARGET_NAME)"; 314 | WRAPPER_EXTENSION = app; 315 | }; 316 | name = Debug; 317 | }; 318 | 4CC71A11157C1FD7008976AD /* Release */ = { 319 | isa = XCBuildConfiguration; 320 | buildSettings = { 321 | CLANG_ENABLE_OBJC_WEAK = YES; 322 | COMBINE_HIDPI_IMAGES = YES; 323 | GCC_PRECOMPILE_PREFIX_HEADER = NO; 324 | INFOPLIST_FILE = "MenubarTicker/Menu Bar Ticker-Info.plist"; 325 | PRODUCT_BUNDLE_IDENTIFIER = org.serban.MenuBarTicker; 326 | PRODUCT_NAME = "$(TARGET_NAME)"; 327 | WRAPPER_EXTENSION = app; 328 | }; 329 | name = Release; 330 | }; 331 | /* End XCBuildConfiguration section */ 332 | 333 | /* Begin XCConfigurationList section */ 334 | 4CC719D7157C1FD7008976AD /* Build configuration list for PBXProject "Menu Bar Ticker" */ = { 335 | isa = XCConfigurationList; 336 | buildConfigurations = ( 337 | 4CC71A0D157C1FD7008976AD /* Debug */, 338 | 4CC71A0E157C1FD7008976AD /* Release */, 339 | ); 340 | defaultConfigurationIsVisible = 0; 341 | defaultConfigurationName = Release; 342 | }; 343 | 4CC71A0F157C1FD7008976AD /* Build configuration list for PBXNativeTarget "Menu Bar Ticker" */ = { 344 | isa = XCConfigurationList; 345 | buildConfigurations = ( 346 | 4CC71A10157C1FD7008976AD /* Debug */, 347 | 4CC71A11157C1FD7008976AD /* Release */, 348 | ); 349 | defaultConfigurationIsVisible = 0; 350 | defaultConfigurationName = Release; 351 | }; 352 | /* End XCConfigurationList section */ 353 | }; 354 | rootObject = 4CC719D4157C1FD7008976AD /* Project object */; 355 | } 356 | -------------------------------------------------------------------------------- /MenubarTicker/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface AppDelegate : NSObject 4 | 5 | @property (nonatomic, retain) IBOutlet NSMenu *statusMenu; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /MenubarTicker/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import "Music.h" 4 | #import "Spotify.h" 5 | 6 | const NSTimeInterval kPollingInterval = 10.0; 7 | 8 | 9 | @interface AppDelegate () 10 | 11 | @property (nonatomic, retain) MusicApplication *music; 12 | @property (nonatomic, retain) SpotifyApplication *spotify; 13 | 14 | @property (nonatomic, retain) NSStatusItem *statusItem; 15 | @property (nonatomic, retain) NSTimer *timer; 16 | 17 | @end 18 | 19 | 20 | @implementation AppDelegate 21 | 22 | @synthesize music; 23 | @synthesize spotify; 24 | 25 | @synthesize statusItem; 26 | @synthesize statusMenu; 27 | @synthesize timer; 28 | 29 | - (void)dealloc 30 | { 31 | [[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:nil object:nil]; 32 | 33 | self.music = nil; 34 | self.spotify = nil; 35 | 36 | self.statusItem = nil; 37 | self.statusMenu = nil; 38 | 39 | [self.timer invalidate]; 40 | self.timer = nil; 41 | 42 | [super dealloc]; 43 | } 44 | 45 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification 46 | { 47 | self.timer = [NSTimer scheduledTimerWithTimeInterval:kPollingInterval 48 | target:self 49 | selector:@selector(timerDidFire:) 50 | userInfo:nil 51 | repeats:YES]; 52 | 53 | // As of February 2021, notifications from Music.app are still coming in through 54 | // com.apple.iTunes.playerInfo and not com.apple.music.playerInfo. 55 | [[NSDistributedNotificationCenter defaultCenter] addObserver:self 56 | selector:@selector(didReceivePlayerNotification:) 57 | name:@"com.apple.iTunes.playerInfo" 58 | object:nil]; 59 | 60 | [[NSDistributedNotificationCenter defaultCenter] addObserver:self 61 | selector:@selector(didReceivePlayerNotification:) 62 | name:@"com.apple.music.playerInfo" 63 | object:nil]; 64 | 65 | [[NSDistributedNotificationCenter defaultCenter] addObserver:self 66 | selector:@selector(didReceivePlayerNotification:) 67 | name:@"com.spotify.client.PlaybackStateChanged" 68 | object:nil]; 69 | } 70 | 71 | - (void)awakeFromNib 72 | { 73 | self.music = [SBApplication applicationWithBundleIdentifier:@"com.apple.music"]; 74 | self.spotify = [SBApplication applicationWithBundleIdentifier:@"com.spotify.client"]; 75 | 76 | self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength]; 77 | self.statusItem.menu = self.statusMenu; 78 | self.statusItem.button.toolTip = @"Menu Bar Ticker"; 79 | 80 | [self updateTrackInfo]; 81 | } 82 | 83 | 84 | - (void)updateTrackInfo 85 | { 86 | id currentTrack = nil; 87 | 88 | if ([self.music isRunning] && [self.music playerState] == MusicEPlSPlaying) { 89 | currentTrack = [self.music currentTrack]; 90 | } else if ([self.spotify isRunning] && [self.spotify playerState] == SpotifyEPlSPlaying) { 91 | currentTrack = [self.spotify currentTrack]; 92 | } 93 | 94 | statusItem.button.title = currentTrack 95 | ? [NSString stringWithFormat:@"%@ - %@", [currentTrack artist], [currentTrack name]] 96 | : @"♫"; 97 | } 98 | 99 | - (void)timerDidFire:(NSTimer *)theTimer 100 | { 101 | [self updateTrackInfo]; 102 | } 103 | 104 | - (void)didReceivePlayerNotification:(NSNotification *)notification 105 | { 106 | [self updateTrackInfo]; 107 | } 108 | 109 | @end 110 | -------------------------------------------------------------------------------- /MenubarTicker/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | Default 523 | 524 | 525 | 526 | 527 | 528 | 529 | Left to Right 530 | 531 | 532 | 533 | 534 | 535 | 536 | Right to Left 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | Default 548 | 549 | 550 | 551 | 552 | 553 | 554 | Left to Right 555 | 556 | 557 | 558 | 559 | 560 | 561 | Right to Left 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | -------------------------------------------------------------------------------- /MenubarTicker/Menu Bar Ticker-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 2.0.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 20210214180000 25 | LSApplicationCategoryType 26 | public.app-category.utilities 27 | LSMinimumSystemVersion 28 | ${MACOSX_DEPLOYMENT_TARGET} 29 | LSUIElement 30 | 31 | NSAppleEventsUsageDescription 32 | Menu Bar Ticker needs permission to read the current song. 33 | NSHumanReadableCopyright 34 | 35 | NSMainNibFile 36 | MainMenu 37 | NSPrincipalClass 38 | NSApplication 39 | 40 | 41 | -------------------------------------------------------------------------------- /MenubarTicker/Music.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Music.h 3 | */ 4 | 5 | #import 6 | #import 7 | 8 | 9 | @class MusicApplication, MusicItem, MusicAirPlayDevice, MusicArtwork, MusicEncoder, MusicEQPreset, MusicPlaylist, MusicAudioCDPlaylist, MusicLibraryPlaylist, MusicRadioTunerPlaylist, MusicSource, MusicSubscriptionPlaylist, MusicTrack, MusicAudioCDTrack, MusicFileTrack, MusicSharedTrack, MusicURLTrack, MusicUserPlaylist, MusicFolderPlaylist, MusicVisual, MusicWindow, MusicBrowserWindow, MusicEQWindow, MusicMiniplayerWindow, MusicPlaylistWindow, MusicVideoWindow; 10 | 11 | enum MusicEKnd { 12 | MusicEKndTrackListing = 'kTrk' /* a basic listing of tracks within a playlist */, 13 | MusicEKndAlbumListing = 'kAlb' /* a listing of a playlist grouped by album */, 14 | MusicEKndCdInsert = 'kCDi' /* a printout of the playlist for jewel case inserts */ 15 | }; 16 | typedef enum MusicEKnd MusicEKnd; 17 | 18 | enum MusicEnum { 19 | MusicEnumStandard = 'lwst' /* Standard PostScript error handling */, 20 | MusicEnumDetailed = 'lwdt' /* print a detailed report of PostScript errors */ 21 | }; 22 | typedef enum MusicEnum MusicEnum; 23 | 24 | enum MusicEPlS { 25 | MusicEPlSStopped = 'kPSS', 26 | MusicEPlSPlaying = 'kPSP', 27 | MusicEPlSPaused = 'kPSp', 28 | MusicEPlSFastForwarding = 'kPSF', 29 | MusicEPlSRewinding = 'kPSR' 30 | }; 31 | typedef enum MusicEPlS MusicEPlS; 32 | 33 | enum MusicERpt { 34 | MusicERptOff = 'kRpO', 35 | MusicERptOne = 'kRp1', 36 | MusicERptAll = 'kAll' 37 | }; 38 | typedef enum MusicERpt MusicERpt; 39 | 40 | enum MusicEShM { 41 | MusicEShMSongs = 'kShS', 42 | MusicEShMAlbums = 'kShA', 43 | MusicEShMGroupings = 'kShG' 44 | }; 45 | typedef enum MusicEShM MusicEShM; 46 | 47 | enum MusicESrc { 48 | MusicESrcLibrary = 'kLib', 49 | MusicESrcAudioCD = 'kACD', 50 | MusicESrcMP3CD = 'kMCD', 51 | MusicESrcRadioTuner = 'kTun', 52 | MusicESrcSharedLibrary = 'kShd', 53 | MusicESrcITunesStore = 'kITS', 54 | MusicESrcUnknown = 'kUnk' 55 | }; 56 | typedef enum MusicESrc MusicESrc; 57 | 58 | enum MusicESrA { 59 | MusicESrAAlbums = 'kSrL' /* albums only */, 60 | MusicESrAAll = 'kAll' /* all text fields */, 61 | MusicESrAArtists = 'kSrR' /* artists only */, 62 | MusicESrAComposers = 'kSrC' /* composers only */, 63 | MusicESrADisplayed = 'kSrV' /* visible text fields */, 64 | MusicESrANames = 'kSrS' /* track names only */ 65 | }; 66 | typedef enum MusicESrA MusicESrA; 67 | 68 | enum MusicESpK { 69 | MusicESpKNone = 'kNon', 70 | MusicESpKFolder = 'kSpF', 71 | MusicESpKGenius = 'kSpG', 72 | MusicESpKLibrary = 'kSpL', 73 | MusicESpKMusic = 'kSpZ', 74 | MusicESpKPurchasedMusic = 'kSpM' 75 | }; 76 | typedef enum MusicESpK MusicESpK; 77 | 78 | enum MusicEMdK { 79 | MusicEMdKSong = 'kMdS' /* music track */, 80 | MusicEMdKMusicVideo = 'kVdV' /* music video track */, 81 | MusicEMdKUnknown = 'kUnk' 82 | }; 83 | typedef enum MusicEMdK MusicEMdK; 84 | 85 | enum MusicERtK { 86 | MusicERtKUser = 'kRtU' /* user-specified rating */, 87 | MusicERtKComputed = 'kRtC' /* computed rating */ 88 | }; 89 | typedef enum MusicERtK MusicERtK; 90 | 91 | enum MusicEAPD { 92 | MusicEAPDComputer = 'kAPC', 93 | MusicEAPDAirPortExpress = 'kAPX', 94 | MusicEAPDAppleTV = 'kAPT', 95 | MusicEAPDAirPlayDevice = 'kAPO', 96 | MusicEAPDBluetoothDevice = 'kAPB', 97 | MusicEAPDHomePod = 'kAPH', 98 | MusicEAPDUnknown = 'kAPU' 99 | }; 100 | typedef enum MusicEAPD MusicEAPD; 101 | 102 | enum MusicEClS { 103 | MusicEClSUnknown = 'kUnk', 104 | MusicEClSPurchased = 'kPur', 105 | MusicEClSMatched = 'kMat', 106 | MusicEClSUploaded = 'kUpl', 107 | MusicEClSIneligible = 'kRej', 108 | MusicEClSRemoved = 'kRem', 109 | MusicEClSError = 'kErr', 110 | MusicEClSDuplicate = 'kDup', 111 | MusicEClSSubscription = 'kSub', 112 | MusicEClSNoLongerAvailable = 'kRev', 113 | MusicEClSNotUploaded = 'kUpP' 114 | }; 115 | typedef enum MusicEClS MusicEClS; 116 | 117 | @protocol MusicGenericMethods 118 | 119 | - (void) printPrintDialog:(BOOL)printDialog withProperties:(NSDictionary *)withProperties kind:(MusicEKnd)kind theme:(NSString *)theme; // Print the specified object(s) 120 | - (void) close; // Close an object 121 | - (void) delete; // Delete an element from an object 122 | - (SBObject *) duplicateTo:(SBObject *)to; // Duplicate one or more object(s) 123 | - (BOOL) exists; // Verify if an object exists 124 | - (void) open; // Open the specified object(s) 125 | - (void) save; // Save the specified object(s) 126 | - (void) playOnce:(BOOL)once; // play the current track or the specified track or file. 127 | - (void) select; // select the specified object(s) 128 | 129 | @end 130 | 131 | 132 | 133 | /* 134 | * Music Suite 135 | */ 136 | 137 | // The application program 138 | @interface MusicApplication : SBApplication 139 | 140 | - (SBElementArray *) AirPlayDevices; 141 | - (SBElementArray *) browserWindows; 142 | - (SBElementArray *) encoders; 143 | - (SBElementArray *) EQPresets; 144 | - (SBElementArray *) EQWindows; 145 | - (SBElementArray *) miniplayerWindows; 146 | - (SBElementArray *) playlists; 147 | - (SBElementArray *) playlistWindows; 148 | - (SBElementArray *) sources; 149 | - (SBElementArray *) tracks; 150 | - (SBElementArray *) videoWindows; 151 | - (SBElementArray *) visuals; 152 | - (SBElementArray *) windows; 153 | 154 | @property (readonly) BOOL AirPlayEnabled; // is AirPlay currently enabled? 155 | @property (readonly) BOOL converting; // is a track currently being converted? 156 | @property (copy) NSArray *currentAirPlayDevices; // the currently selected AirPlay device(s) 157 | @property (copy) MusicEncoder *currentEncoder; // the currently selected encoder (MP3, AIFF, WAV, etc.) 158 | @property (copy) MusicEQPreset *currentEQPreset; // the currently selected equalizer preset 159 | @property (copy, readonly) MusicPlaylist *currentPlaylist; // the playlist containing the currently targeted track 160 | @property (copy, readonly) NSString *currentStreamTitle; // the name of the current track in the playing stream (provided by streaming server) 161 | @property (copy, readonly) NSString *currentStreamURL; // the URL of the playing stream or streaming web site (provided by streaming server) 162 | @property (copy, readonly) MusicTrack *currentTrack; // the current targeted track 163 | @property (copy) MusicVisual *currentVisual; // the currently selected visual plug-in 164 | @property BOOL EQEnabled; // is the equalizer enabled? 165 | @property BOOL fixedIndexing; // true if all AppleScript track indices should be independent of the play order of the owning playlist. 166 | @property BOOL frontmost; // is this the active application? 167 | @property BOOL fullScreen; // is the application using the entire screen? 168 | @property (copy, readonly) NSString *name; // the name of the application 169 | @property BOOL mute; // has the sound output been muted? 170 | @property double playerPosition; // the player’s position within the currently playing track in seconds. 171 | @property (readonly) MusicEPlS playerState; // is the player stopped, paused, or playing? 172 | @property (copy, readonly) SBObject *selection; // the selection visible to the user 173 | @property BOOL shuffleEnabled; // are songs played in random order? 174 | @property MusicEShM shuffleMode; // the playback shuffle mode 175 | @property MusicERpt songRepeat; // the playback repeat mode 176 | @property NSInteger soundVolume; // the sound output volume (0 = minimum, 100 = maximum) 177 | @property (copy, readonly) NSString *version; // the version of the application 178 | @property BOOL visualsEnabled; // are visuals currently being displayed? 179 | 180 | - (void) printPrintDialog:(BOOL)printDialog withProperties:(NSDictionary *)withProperties kind:(MusicEKnd)kind theme:(NSString *)theme; // Print the specified object(s) 181 | - (void) run; // Run the application 182 | - (void) quit; // Quit the application 183 | - (MusicTrack *) add:(NSArray *)x to:(SBObject *)to; // add one or more files to a playlist 184 | - (void) backTrack; // reposition to beginning of current track or go to previous track if already at start of current track 185 | - (MusicTrack *) convert:(NSArray *)x; // convert one or more files or tracks 186 | - (void) fastForward; // skip forward in a playing track 187 | - (void) nextTrack; // advance to the next track in the current playlist 188 | - (void) pause; // pause playback 189 | - (void) playOnce:(BOOL)once; // play the current track or the specified track or file. 190 | - (void) playpause; // toggle the playing/paused state of the current track 191 | - (void) previousTrack; // return to the previous track in the current playlist 192 | - (void) resume; // disable fast forward/rewind and resume playback, if playing. 193 | - (void) rewind; // skip backwards in a playing track 194 | - (void) stop; // stop playback 195 | - (void) openLocation:(NSString *)x; // Opens an iTunes Store or audio stream URL 196 | 197 | @end 198 | 199 | // an item 200 | @interface MusicItem : SBObject 201 | 202 | @property (copy, readonly) SBObject *container; // the container of the item 203 | - (NSInteger) id; // the id of the item 204 | @property (readonly) NSInteger index; // the index of the item in internal application order 205 | @property (copy) NSString *name; // the name of the item 206 | @property (copy, readonly) NSString *persistentID; // the id of the item as a hexadecimal string. This id does not change over time. 207 | @property (copy) NSDictionary *properties; // every property of the item 208 | 209 | - (void) download; // download a cloud track or playlist 210 | - (void) reveal; // reveal and select a track or playlist 211 | 212 | @end 213 | 214 | // an AirPlay device 215 | @interface MusicAirPlayDevice : MusicItem 216 | 217 | @property (readonly) BOOL active; // is the device currently being played to? 218 | @property (readonly) BOOL available; // is the device currently available? 219 | @property (readonly) MusicEAPD kind; // the kind of the device 220 | @property (copy, readonly) NSString *networkAddress; // the network (MAC) address of the device 221 | - (BOOL) protected; // is the device password- or passcode-protected? 222 | @property BOOL selected; // is the device currently selected? 223 | @property (readonly) BOOL supportsAudio; // does the device support audio playback? 224 | @property (readonly) BOOL supportsVideo; // does the device support video playback? 225 | @property NSInteger soundVolume; // the output volume for the device (0 = minimum, 100 = maximum) 226 | 227 | 228 | @end 229 | 230 | // a piece of art within a track or playlist 231 | @interface MusicArtwork : MusicItem 232 | 233 | @property (copy) NSImage *data; // data for this artwork, in the form of a picture 234 | @property (copy) NSString *objectDescription; // description of artwork as a string 235 | @property (readonly) BOOL downloaded; // was this artwork downloaded by Music? 236 | @property (copy, readonly) NSNumber *format; // the data format for this piece of artwork 237 | @property NSInteger kind; // kind or purpose of this piece of artwork 238 | @property (copy) id rawData; // data for this artwork, in original format 239 | 240 | 241 | @end 242 | 243 | // converts a track to a specific file format 244 | @interface MusicEncoder : MusicItem 245 | 246 | @property (copy, readonly) NSString *format; // the data format created by the encoder 247 | 248 | 249 | @end 250 | 251 | // equalizer preset configuration 252 | @interface MusicEQPreset : MusicItem 253 | 254 | @property double band1; // the equalizer 32 Hz band level (-12.0 dB to +12.0 dB) 255 | @property double band2; // the equalizer 64 Hz band level (-12.0 dB to +12.0 dB) 256 | @property double band3; // the equalizer 125 Hz band level (-12.0 dB to +12.0 dB) 257 | @property double band4; // the equalizer 250 Hz band level (-12.0 dB to +12.0 dB) 258 | @property double band5; // the equalizer 500 Hz band level (-12.0 dB to +12.0 dB) 259 | @property double band6; // the equalizer 1 kHz band level (-12.0 dB to +12.0 dB) 260 | @property double band7; // the equalizer 2 kHz band level (-12.0 dB to +12.0 dB) 261 | @property double band8; // the equalizer 4 kHz band level (-12.0 dB to +12.0 dB) 262 | @property double band9; // the equalizer 8 kHz band level (-12.0 dB to +12.0 dB) 263 | @property double band10; // the equalizer 16 kHz band level (-12.0 dB to +12.0 dB) 264 | @property (readonly) BOOL modifiable; // can this preset be modified? 265 | @property double preamp; // the equalizer preamp level (-12.0 dB to +12.0 dB) 266 | @property BOOL updateTracks; // should tracks which refer to this preset be updated when the preset is renamed or deleted? 267 | 268 | 269 | @end 270 | 271 | // a list of tracks/streams 272 | @interface MusicPlaylist : MusicItem 273 | 274 | - (SBElementArray *) tracks; 275 | - (SBElementArray *) artworks; 276 | 277 | @property (copy) NSString *objectDescription; // the description of the playlist 278 | @property BOOL disliked; // is this playlist disliked? 279 | @property (readonly) NSInteger duration; // the total length of all tracks (in seconds) 280 | @property (copy) NSString *name; // the name of the playlist 281 | @property BOOL loved; // is this playlist loved? 282 | @property (copy, readonly) MusicPlaylist *parent; // folder which contains this playlist (if any) 283 | @property (readonly) NSInteger size; // the total size of all tracks (in bytes) 284 | @property (readonly) MusicESpK specialKind; // special playlist kind 285 | @property (copy, readonly) NSString *time; // the length of all tracks in MM:SS format 286 | @property (readonly) BOOL visible; // is this playlist visible in the Source list? 287 | 288 | - (void) moveTo:(SBObject *)to; // Move playlist(s) to a new location 289 | - (MusicTrack *) searchFor:(NSString *)for_ only:(MusicESrA)only; // search a playlist for tracks matching the search string. Identical to entering search text in the Search field. 290 | 291 | @end 292 | 293 | // a playlist representing an audio CD 294 | @interface MusicAudioCDPlaylist : MusicPlaylist 295 | 296 | - (SBElementArray *) audioCDTracks; 297 | 298 | @property (copy) NSString *artist; // the artist of the CD 299 | @property BOOL compilation; // is this CD a compilation album? 300 | @property (copy) NSString *composer; // the composer of the CD 301 | @property NSInteger discCount; // the total number of discs in this CD’s album 302 | @property NSInteger discNumber; // the index of this CD disc in the source album 303 | @property (copy) NSString *genre; // the genre of the CD 304 | @property NSInteger year; // the year the album was recorded/released 305 | 306 | 307 | @end 308 | 309 | // the master library playlist 310 | @interface MusicLibraryPlaylist : MusicPlaylist 311 | 312 | - (SBElementArray *) fileTracks; 313 | - (SBElementArray *) URLTracks; 314 | - (SBElementArray *) sharedTracks; 315 | 316 | 317 | @end 318 | 319 | // the radio tuner playlist 320 | @interface MusicRadioTunerPlaylist : MusicPlaylist 321 | 322 | - (SBElementArray *) URLTracks; 323 | 324 | 325 | @end 326 | 327 | // a media source (library, CD, device, etc.) 328 | @interface MusicSource : MusicItem 329 | 330 | - (SBElementArray *) audioCDPlaylists; 331 | - (SBElementArray *) libraryPlaylists; 332 | - (SBElementArray *) playlists; 333 | - (SBElementArray *) radioTunerPlaylists; 334 | - (SBElementArray *) subscriptionPlaylists; 335 | - (SBElementArray *) userPlaylists; 336 | 337 | @property (readonly) long long capacity; // the total size of the source if it has a fixed size 338 | @property (readonly) long long freeSpace; // the free space on the source if it has a fixed size 339 | @property (readonly) MusicESrc kind; 340 | 341 | 342 | @end 343 | 344 | // a subscription playlist from Apple Music 345 | @interface MusicSubscriptionPlaylist : MusicPlaylist 346 | 347 | - (SBElementArray *) fileTracks; 348 | - (SBElementArray *) URLTracks; 349 | 350 | 351 | @end 352 | 353 | // playable audio source 354 | @interface MusicTrack : MusicItem 355 | 356 | - (SBElementArray *) artworks; 357 | 358 | @property (copy) NSString *album; // the album name of the track 359 | @property (copy) NSString *albumArtist; // the album artist of the track 360 | @property BOOL albumDisliked; // is the album for this track disliked? 361 | @property BOOL albumLoved; // is the album for this track loved? 362 | @property NSInteger albumRating; // the rating of the album for this track (0 to 100) 363 | @property (readonly) MusicERtK albumRatingKind; // the rating kind of the album rating for this track 364 | @property (copy) NSString *artist; // the artist/source of the track 365 | @property (readonly) NSInteger bitRate; // the bit rate of the track (in kbps) 366 | @property double bookmark; // the bookmark time of the track in seconds 367 | @property BOOL bookmarkable; // is the playback position for this track remembered? 368 | @property NSInteger bpm; // the tempo of this track in beats per minute 369 | @property (copy) NSString *category; // the category of the track 370 | @property (readonly) MusicEClS cloudStatus; // the iCloud status of the track 371 | @property (copy) NSString *comment; // freeform notes about the track 372 | @property BOOL compilation; // is this track from a compilation album? 373 | @property (copy) NSString *composer; // the composer of the track 374 | @property (readonly) NSInteger databaseID; // the common, unique ID for this track. If two tracks in different playlists have the same database ID, they are sharing the same data. 375 | @property (copy, readonly) NSDate *dateAdded; // the date the track was added to the playlist 376 | @property (copy) NSString *objectDescription; // the description of the track 377 | @property NSInteger discCount; // the total number of discs in the source album 378 | @property NSInteger discNumber; // the index of the disc containing this track on the source album 379 | @property BOOL disliked; // is this track disliked? 380 | @property (copy, readonly) NSString *downloaderAppleID; // the Apple ID of the person who downloaded this track 381 | @property (copy, readonly) NSString *downloaderName; // the name of the person who downloaded this track 382 | @property (readonly) double duration; // the length of the track in seconds 383 | @property BOOL enabled; // is this track checked for playback? 384 | @property (copy) NSString *episodeID; // the episode ID of the track 385 | @property NSInteger episodeNumber; // the episode number of the track 386 | @property (copy) NSString *EQ; // the name of the EQ preset of the track 387 | @property double finish; // the stop time of the track in seconds 388 | @property BOOL gapless; // is this track from a gapless album? 389 | @property (copy) NSString *genre; // the music/audio genre (category) of the track 390 | @property (copy) NSString *grouping; // the grouping (piece) of the track. Generally used to denote movements within a classical work. 391 | @property (copy, readonly) NSString *kind; // a text description of the track 392 | @property (copy) NSString *longDescription; // the long description of the track 393 | @property BOOL loved; // is this track loved? 394 | @property (copy) NSString *lyrics; // the lyrics of the track 395 | @property MusicEMdK mediaKind; // the media kind of the track 396 | @property (copy, readonly) NSDate *modificationDate; // the modification date of the content of this track 397 | @property (copy) NSString *movement; // the movement name of the track 398 | @property NSInteger movementCount; // the total number of movements in the work 399 | @property NSInteger movementNumber; // the index of the movement in the work 400 | @property NSInteger playedCount; // number of times this track has been played 401 | @property (copy) NSDate *playedDate; // the date and time this track was last played 402 | @property (copy, readonly) NSString *purchaserAppleID; // the Apple ID of the person who purchased this track 403 | @property (copy, readonly) NSString *purchaserName; // the name of the person who purchased this track 404 | @property NSInteger rating; // the rating of this track (0 to 100) 405 | @property (readonly) MusicERtK ratingKind; // the rating kind of this track 406 | @property (copy, readonly) NSDate *releaseDate; // the release date of this track 407 | @property (readonly) NSInteger sampleRate; // the sample rate of the track (in Hz) 408 | @property NSInteger seasonNumber; // the season number of the track 409 | @property BOOL shufflable; // is this track included when shuffling? 410 | @property NSInteger skippedCount; // number of times this track has been skipped 411 | @property (copy) NSDate *skippedDate; // the date and time this track was last skipped 412 | @property (copy) NSString *show; // the show name of the track 413 | @property (copy) NSString *sortAlbum; // override string to use for the track when sorting by album 414 | @property (copy) NSString *sortArtist; // override string to use for the track when sorting by artist 415 | @property (copy) NSString *sortAlbumArtist; // override string to use for the track when sorting by album artist 416 | @property (copy) NSString *sortName; // override string to use for the track when sorting by name 417 | @property (copy) NSString *sortComposer; // override string to use for the track when sorting by composer 418 | @property (copy) NSString *sortShow; // override string to use for the track when sorting by show name 419 | @property (readonly) long long size; // the size of the track (in bytes) 420 | @property double start; // the start time of the track in seconds 421 | @property (copy, readonly) NSString *time; // the length of the track in MM:SS format 422 | @property NSInteger trackCount; // the total number of tracks on the source album 423 | @property NSInteger trackNumber; // the index of the track on the source album 424 | @property BOOL unplayed; // is this track unplayed? 425 | @property NSInteger volumeAdjustment; // relative volume adjustment of the track (-100% to 100%) 426 | @property (copy) NSString *work; // the work name of the track 427 | @property NSInteger year; // the year the track was recorded/released 428 | 429 | 430 | @end 431 | 432 | // a track on an audio CD 433 | @interface MusicAudioCDTrack : MusicTrack 434 | 435 | @property (copy, readonly) NSURL *location; // the location of the file represented by this track 436 | 437 | 438 | @end 439 | 440 | // a track representing an audio file (MP3, AIFF, etc.) 441 | @interface MusicFileTrack : MusicTrack 442 | 443 | @property (copy) NSURL *location; // the location of the file represented by this track 444 | 445 | - (void) refresh; // update file track information from the current information in the track’s file 446 | 447 | @end 448 | 449 | // a track residing in a shared library 450 | @interface MusicSharedTrack : MusicTrack 451 | 452 | 453 | @end 454 | 455 | // a track representing a network stream 456 | @interface MusicURLTrack : MusicTrack 457 | 458 | @property (copy) NSString *address; // the URL for this track 459 | 460 | 461 | @end 462 | 463 | // custom playlists created by the user 464 | @interface MusicUserPlaylist : MusicPlaylist 465 | 466 | - (SBElementArray *) fileTracks; 467 | - (SBElementArray *) URLTracks; 468 | - (SBElementArray *) sharedTracks; 469 | 470 | @property BOOL shared; // is this playlist shared? 471 | @property (readonly) BOOL smart; // is this a Smart Playlist? 472 | @property (readonly) BOOL genius; // is this a Genius Playlist? 473 | 474 | 475 | @end 476 | 477 | // a folder that contains other playlists 478 | @interface MusicFolderPlaylist : MusicUserPlaylist 479 | 480 | 481 | @end 482 | 483 | // a visual plug-in 484 | @interface MusicVisual : MusicItem 485 | 486 | 487 | @end 488 | 489 | // any window 490 | @interface MusicWindow : MusicItem 491 | 492 | @property NSRect bounds; // the boundary rectangle for the window 493 | @property (readonly) BOOL closeable; // does the window have a close button? 494 | @property (readonly) BOOL collapseable; // does the window have a collapse button? 495 | @property BOOL collapsed; // is the window collapsed? 496 | @property BOOL fullScreen; // is the window full screen? 497 | @property NSPoint position; // the upper left position of the window 498 | @property (readonly) BOOL resizable; // is the window resizable? 499 | @property BOOL visible; // is the window visible? 500 | @property (readonly) BOOL zoomable; // is the window zoomable? 501 | @property BOOL zoomed; // is the window zoomed? 502 | 503 | 504 | @end 505 | 506 | // the main window 507 | @interface MusicBrowserWindow : MusicWindow 508 | 509 | @property (copy, readonly) SBObject *selection; // the selected tracks 510 | @property (copy) MusicPlaylist *view; // the playlist currently displayed in the window 511 | 512 | 513 | @end 514 | 515 | // the equalizer window 516 | @interface MusicEQWindow : MusicWindow 517 | 518 | 519 | @end 520 | 521 | // the miniplayer window 522 | @interface MusicMiniplayerWindow : MusicWindow 523 | 524 | 525 | @end 526 | 527 | // a sub-window showing a single playlist 528 | @interface MusicPlaylistWindow : MusicWindow 529 | 530 | @property (copy, readonly) SBObject *selection; // the selected tracks 531 | @property (copy, readonly) MusicPlaylist *view; // the playlist displayed in the window 532 | 533 | 534 | @end 535 | 536 | // the video window 537 | @interface MusicVideoWindow : MusicWindow 538 | 539 | 540 | @end 541 | 542 | -------------------------------------------------------------------------------- /MenubarTicker/Spotify.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Spotify.h 3 | */ 4 | 5 | #import 6 | #import 7 | 8 | 9 | @class SpotifyApplication, SpotifyTrack, SpotifyApplication; 10 | 11 | enum SpotifyEPlS { 12 | SpotifyEPlSStopped = 'kPSS', 13 | SpotifyEPlSPlaying = 'kPSP', 14 | SpotifyEPlSPaused = 'kPSp' 15 | }; 16 | typedef enum SpotifyEPlS SpotifyEPlS; 17 | 18 | 19 | 20 | /* 21 | * Spotify Suite 22 | */ 23 | 24 | // The Spotify application. 25 | @interface SpotifyApplication : SBApplication 26 | 27 | @property (copy, readonly) SpotifyTrack *currentTrack; // The current playing track. 28 | @property NSInteger soundVolume; // The sound output volume (0 = minimum, 100 = maximum) 29 | @property (readonly) SpotifyEPlS playerState; // Is Spotify stopped, paused, or playing? 30 | @property double playerPosition; // The player’s position within the currently playing track in seconds. 31 | @property (readonly) BOOL repeatingEnabled; // Is repeating enabled in the current playback context? 32 | @property BOOL repeating; // Is repeating on or off? 33 | @property (readonly) BOOL shufflingEnabled; // Is shuffling enabled in the current playback context? 34 | @property BOOL shuffling; // Is shuffling on or off? 35 | 36 | - (void) nextTrack; // Skip to the next track. 37 | - (void) previousTrack; // Skip to the previous track. 38 | - (void) playpause; // Toggle play/pause. 39 | - (void) pause; // Pause playback. 40 | - (void) play; // Resume playback. 41 | - (void) playTrack:(NSString *)x inContext:(NSString *)inContext; // Start playback of a track in the given context. 42 | 43 | @end 44 | 45 | // A Spotify track. 46 | @interface SpotifyTrack : SBObject 47 | 48 | @property (copy, readonly) NSString *artist; // The artist of the track. 49 | @property (copy, readonly) NSString *album; // The album of the track. 50 | @property (readonly) NSInteger discNumber; // The disc number of the track. 51 | @property (readonly) NSInteger duration; // The length of the track in seconds. 52 | @property (readonly) NSInteger playedCount; // The number of times this track has been played. 53 | @property (readonly) NSInteger trackNumber; // The index of the track in its album. 54 | @property (readonly) BOOL starred; // Is the track starred? 55 | @property (readonly) NSInteger popularity; // How popular is this track? 0-100 56 | - (NSString *) id; // The ID of the item. 57 | @property (copy, readonly) NSString *name; // The name of the track. 58 | @property (copy, readonly) NSImage *artwork; // The track's album cover. 59 | @property (copy, readonly) NSString *albumArtist; // That album artist of the track. 60 | @property (copy) NSString *spotifyUrl; // The URL of the track. 61 | 62 | 63 | @end 64 | 65 | 66 | 67 | /* 68 | * Standard Suite 69 | */ 70 | 71 | // The application's top level scripting object. 72 | @interface SpotifyApplication (StandardSuite) 73 | 74 | @property (copy, readonly) NSString *name; // The name of the application. 75 | @property (readonly) BOOL frontmost; // Is this the frontmost (active) application? 76 | @property (copy, readonly) NSString *version; // The version of the application. 77 | 78 | @end 79 | 80 | -------------------------------------------------------------------------------- /MenubarTicker/en.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1348\cocoasubrtf170 2 | {\fonttbl\f0\fswiss\fcharset0 Helvetica;} 3 | {\colortbl;\red255\green255\blue255;} 4 | \vieww9600\viewh8400\viewkind0 5 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 6 | 7 | \f0\fs24 \cf0 See {\field{\*\fldinst{HYPERLINK "https://github.com/serban/menubar-ticker"}}{\fldrslt https://github.com/serban/menubar-ticker}}.} -------------------------------------------------------------------------------- /MenubarTicker/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /MenubarTicker/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | int main(int argc, char *argv[]) 4 | { 5 | return NSApplicationMain(argc, (const char **)argv); 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # menubar-ticker - Show the current song 2 | 3 | Menu Bar Ticker is a simple macOS menu bar item that displays the current artist 4 | and song playing in Music (aka 5 | [iTunes](https://support.apple.com/en-us/HT210200)) or Spotify. 6 | 7 | There are no other fancy features. 8 | 9 | ### Idle 10 | ![Light - Idle](/screenshots/light-idle.png?raw=true) 11 | ![Dark - Idle](/screenshots/dark-idle.png?raw=true) 12 | 13 | ### Active 14 | ![Light - Active](/screenshots/light-active.png?raw=true) 15 | ![Dark - Active](/screenshots/dark-active.png?raw=true) 16 | 17 | 18 | ## Installation 19 | 20 | Download the 21 | [latest release](https://github.com/serban/menubar-ticker/releases/latest) and 22 | extract `Menu Bar Ticker.app` from the `menubar-ticker-vX.Y.Z.tar.bz2` archive. 23 | Move it into the `Applications` folder. Follow Apple's instructions to 24 | [start the app automatically](https://support.apple.com/kb/ph25590) 25 | when you log in. 26 | 27 | Since the app is 28 | [not](https://support.apple.com/en-us/HT202491) 29 | [signed](https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unidentified-developer-mh40616/mac), 30 | you will need to 31 | [right-click](https://support.apple.com/guide/mac-help/right-click-mh35853/mac) 32 | and select "Open" the first time you launch the app. A dialog will appear: 33 | 34 | > "Menu Bar Ticker.app" can't be opened because Apple cannot check it for 35 | > malicious software. 36 | 37 | Click "Open". If the app does not appear in the menu bar, right-click on the app 38 | again and select "Open". 39 | 40 | The first time you run Music or Spotify together with Menu Bar Ticker, a dialog 41 | will appear for each app: 42 | 43 | > "Menu Bar Ticker.app" wants access to control "Music.app". Allowing control 44 | > will provide access to documents and data in "Music.app", and to perform 45 | > actions within that app. 46 | > 47 | > Menu Bar Ticker needs permission to read the current song. 48 | 49 | Click "OK". 50 | 51 | ## Changelog 52 | 53 | This project lives at [GitHub](https://github.com/serban/menubar-ticker). 54 | Version numbers follow the [Semantic Versioning](https://semver.org) 55 | specification. 56 | 57 | ### v2.0.0 58 | 59 | * Add support for Apple Silicon 60 | * Drop support for pre-Catalina macOS (use v1 if you're running an older macOS) 61 | * Drop support for pre-Catalina iTunes.app 62 | * Drop support for Rdio (RIP) 63 | 64 | Verified to work with the following on February 14, 2021: 65 | 66 | * macOS Catalina (10.15.7) 67 | * macOS Big Sur (11.2.1) 68 | * Music 1.0.6.10 69 | * Music 1.1.3.3 70 | * Spotify 1.1.52.687.gf5565fe5 71 | 72 | ### v1.1.0 73 | 74 | * Add support for Music.app in macOS Catalina 75 | 76 | Verified to work with the following on January 28, 2020: 77 | 78 | * macOS High Sierra (10.13.6) 79 | * macOS Catalina (10.15.2) 80 | * iTunes 12.8.2.3 81 | * Music 1.0.2.37 82 | * Spotify 1.1.24.91.g4ca6d5eb 83 | 84 | ### v1.0.0 85 | 86 | * First release with support for iTunes, Rdio, and Spotify 87 | 88 | Verified to work with the following on July 19, 2019: 89 | 90 | * macOS High Sierra (10.13.6) 91 | * macOS Mojave (10.14.5) 92 | * iTunes 12.8.2.3 93 | * iTunes 12.9.5.5 94 | * Spotify 1.1.10.540.gfcf0430f 95 | 96 | 97 | ## Contributors 98 | 99 | * [Jonathan Anderson](https://github.com/trombonehero) 100 | * [Robin Macharg](https://github.com/robinmacharg) 101 | -------------------------------------------------------------------------------- /screenshots/dark-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serban/menubar-ticker/a69d738079cf1eb21d0e46f1f2e1458ed432c1f1/screenshots/dark-active.png -------------------------------------------------------------------------------- /screenshots/dark-idle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serban/menubar-ticker/a69d738079cf1eb21d0e46f1f2e1458ed432c1f1/screenshots/dark-idle.png -------------------------------------------------------------------------------- /screenshots/light-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serban/menubar-ticker/a69d738079cf1eb21d0e46f1f2e1458ed432c1f1/screenshots/light-active.png -------------------------------------------------------------------------------- /screenshots/light-idle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serban/menubar-ticker/a69d738079cf1eb21d0e46f1f2e1458ed432c1f1/screenshots/light-idle.png --------------------------------------------------------------------------------