├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Documentation ├── Contributing.md ├── FAQ.md ├── KeyboardShortcuts.md └── ReleaseEngineering.md ├── Frameworks ├── Growl.framework │ ├── Growl │ ├── Headers │ ├── Resources │ └── Versions │ │ ├── A │ │ ├── Growl │ │ ├── Headers │ │ │ ├── Growl.h │ │ │ ├── GrowlApplicationBridge.h │ │ │ └── GrowlDefines.h │ │ ├── Resources │ │ │ └── Info.plist │ │ └── _CodeSignature │ │ │ └── CodeResources │ │ └── Current └── Sparkle.framework │ ├── Headers │ ├── Modules │ ├── PrivateHeaders │ ├── Resources │ ├── Sparkle │ └── Versions │ ├── A │ ├── Headers │ │ ├── SUAppcast.h │ │ ├── SUAppcastItem.h │ │ ├── SUErrors.h │ │ ├── SUExport.h │ │ ├── SUStandardVersionComparator.h │ │ ├── SUUpdater.h │ │ ├── SUVersionComparisonProtocol.h │ │ ├── SUVersionDisplayProtocol.h │ │ └── Sparkle.h │ ├── Modules │ │ └── module.modulemap │ ├── PrivateHeaders │ │ └── SUUnarchiver.h │ ├── Resources │ │ ├── Autoupdate.app │ │ │ └── Contents │ │ │ │ ├── Info.plist │ │ │ │ ├── MacOS │ │ │ │ └── Autoupdate │ │ │ │ ├── PkgInfo │ │ │ │ └── Resources │ │ │ │ ├── AppIcon.icns │ │ │ │ ├── SUStatus.nib │ │ │ │ └── en.lproj │ │ │ │ └── Sparkle.strings │ │ ├── Info.plist │ │ ├── SUModelTranslation.plist │ │ ├── SUStatus.nib │ │ └── en.lproj │ │ │ ├── SUAutomaticUpdateAlert.nib │ │ │ ├── SUUpdateAlert.nib │ │ │ ├── SUUpdatePermissionPrompt.nib │ │ │ └── Sparkle.strings │ └── Sparkle │ └── Current ├── Hermes.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── Archive Hermes.xcscheme │ ├── Hermes (Release — media keys enabled).xcscheme │ ├── Hermes.xcscheme │ └── Upload Hermes Release.xcscheme ├── ImportedSources ├── FMEngine │ ├── FMEngine.h │ ├── FMEngine.m │ ├── LICENSE │ ├── NSString+FMEngine.h │ └── NSString+FMEngine.m ├── SPMediaKeyTap │ ├── LICENSE │ ├── NSObject+SPInvocationGrabbing.h │ ├── NSObject+SPInvocationGrabbing.m │ ├── SPMediaKeyTap.h │ └── SPMediaKeyTap.m └── blowfish │ ├── blowfish.c │ └── blowfish.h ├── LICENSE ├── Makefile ├── README.md ├── RELEASING.md ├── Resources ├── Credits.rtf ├── English.lproj │ ├── InfoPlist.strings │ └── MainMenu.xib ├── Hermes-Info.plist ├── Hermes.sdef ├── Icons │ ├── LICENSE │ ├── Pandora-Menu-Dark-Pause.pdf │ ├── Pandora-Menu-Dark-Play.pdf │ ├── cd.png │ ├── delete.png │ ├── error_icon.png │ ├── fast_forward.png │ ├── history.png │ ├── missing-album.png │ ├── music-note.png │ ├── pause.png │ ├── play.png │ ├── radio.png │ ├── thumbdown.png │ ├── thumbup.png │ ├── volume_down.icns │ ├── volume_up.png │ └── zzs.png ├── dsa_pub.pem └── pandora.icns ├── Scripts ├── archive.sh ├── releaselib.sh ├── sign_sparkle_release.sh ├── upload_release.sh └── verify_sparkle_signature.sh └── Sources ├── AudioStreamer ├── ASPlaylist.h ├── ASPlaylist.m ├── AudioStreamer.h └── AudioStreamer.m ├── Controllers ├── AuthController.h ├── AuthController.m ├── HistoryController.h ├── HistoryController.m ├── PlaybackController.h ├── PlaybackController.m ├── PreferencesController.h ├── PreferencesController.m ├── StationController.h ├── StationController.m ├── StationsController.h └── StationsController.m ├── FileReader.h ├── FileReader.m ├── HermesApp.h ├── HermesApp.m ├── HermesAppDelegate.h ├── HermesAppDelegate.m ├── Hermes_Prefix.pch ├── Integration ├── AppleScript.h ├── AppleScript.m ├── Growler.h ├── Growler.m ├── Keychain.h ├── Keychain.m ├── Scrobbler.h └── Scrobbler.m ├── Models ├── HistoryItem.h ├── HistoryItem.m ├── ImageLoader.h └── ImageLoader.m ├── NetworkConnection.h ├── NetworkConnection.m ├── Notifications.h ├── Notifications.m ├── Pandora ├── Crypt.h ├── Crypt.m ├── Pandora.h ├── Pandora.m ├── PandoraDevice.h ├── PandoraDevice.m ├── Song.h ├── Song.m ├── Station.h └── Station.m ├── URLConnection.h ├── URLConnection.m ├── Views ├── HermesBackgroundView.h ├── HermesBackgroundView.m ├── HermesMainWindow.h ├── HermesMainWindow.m ├── HermesVolumeSliderCell.h ├── HermesVolumeSliderCell.m ├── HistoryCollectionView.h ├── HistoryCollectionView.m ├── HistoryView.h ├── HistoryView.m ├── LabelHoverShowField.h ├── LabelHoverShowField.m ├── LabelHoverShowFieldCell.h ├── LabelHoverShowFieldCell.m ├── MusicProgressSliderCell.h ├── MusicProgressSliderCell.m ├── NSDrawerWindow-HermesFirstResponderWorkaround.m ├── StationsTableView.h └── StationsTableView.m └── main.m /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .DS_Store 3 | .LSOverride 4 | *.xcodeproj/*.mode* 5 | *.xcodeproj/*.pbxuser 6 | *.xcodeproj/xcuserdata 7 | *.xcodeproj/*.xcworkspace 8 | Hermes.dmg 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode8.2 2 | language: objective-c 3 | before_install: 4 | - gem install xcpretty-travis-formatter 5 | script: make travis 6 | -------------------------------------------------------------------------------- /Documentation/Contributing.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Style 5 | ----- 6 | 7 | Follow the general style of Apple's official work. Some important points: 8 | 9 | * We use two-space indentation. Worry not, XCode knows! 10 | * Don't introduce unnecessary whitespace in message passing or function 11 | declarations. 12 | * Add space after control statements (`if (aCondition)`, not 13 | `if(aCondition)`). 14 | * Always include braces with control flow statements. 15 | * Prefer English readable variables to short single letters names — even for 16 | throw away variables. 17 | * Do not use parenthesis in the `return` statement (`return playbackStatus;`, 18 | not `return (playbackStatus);`). 19 | * Pointer operators have a space on their left, and the variable name on their 20 | right (`NSString *userName`, not `NSString* userName`). 21 | 22 | When in doubt, do what Apple does. 23 | -------------------------------------------------------------------------------- /Documentation/FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## Help! Hermes crashes on startup or behaves badly! 4 | 5 | Try resetting all saved state, Preferences, and anything else Hermes related: 6 | 7 | 1. Exit Hermes 8 | 2. Remove `~/Library/Application Support/Hermes/` 9 | 3. Remove `~/Library/Preferences/com.alexcrichton.Hermes.plist` 10 | 4. Remove `~/Library/Caches/com.alexcrichton.Hermes/` 11 | 5. Remove Hermes from keychain 12 | 1. Open Keychain Access 13 | 2. Search for "Hermes" 14 | 3. Select all items named "Hermes" 15 | 4. Click on Edit → Delete 16 | 17 | The following shell script should do all the above: 18 | 19 | ```sh 20 | rm -rf ~/Library/Application\ Support/Hermes 21 | rm -f ~/Library/Preferences/com.alexcrichton.Hermes.plist 22 | rm -rf ~/Library/Caches/com.alexcrichton.Hermes 23 | while [ $? -eq 0 ]; do security delete-generic-password -l Hermes >/dev/null 2>&1; done 24 | ``` 25 | 26 | If you had to use these steps, chances are there is a bug that should be reported. 27 | All bug reports are welcome on the [issue tracker](https://github.com/HermesApp/Hermes/issues). 28 | 29 | ## Can I enable logging in Hermes? 30 | 31 | Logging is enabled at startup: hold down ⌥ (Option key) after launching Hermes. 32 | Look for the  (ladybug emoji) in the window title. 33 | 34 | Once you have logging enabled, open Console.app `/Applications/Utilities/Console.app` and navigate to the Hermes log folder: 35 | 36 | 1. Under "FILES" on the sidebar, 37 | 2. Expand `~/Library/Logs` 38 | 3. Expand `Hermes` 39 | 4. Click the latest log created by Hermes 40 | 41 | You can then copy the log text and send it in a bug report. 42 | -------------------------------------------------------------------------------- /Documentation/KeyboardShortcuts.md: -------------------------------------------------------------------------------- 1 | # Keyboard Shortcuts 2 | | Keystroke | Action | 3 | |-----------|--------------------------------------| 4 | | ⌘, | Preferences | 5 | | ⌘H | Hide Hermes | 6 | | ⌘Q | Quit Hermes | 7 | | ⌘W | Close the frontmost window | 8 | | ⇧⌘S | Hide/show drawer | 9 | | ⇧⌘H | Show song history in drawer | 10 | | ⇧⌘T | Show station list in drawer | 11 | | ⌥⌘T | Hide/show toolbar | 12 | | ⌘N | Create new station | 13 | | ⇧⌘D | Edit selected station | 14 | | ⌘R | Reload stations list | 15 | | ⇧⌘L | Sign out | 16 | | Space | Pause playback | 17 | | ⌘L | Like current song | 18 | | ⌘D | Dislike current song | 19 | | ⌘E | Skip to next song | 20 | | ⌘T | Tired of song (don't play for a month) | 21 | | ⌘↑ | Increase volume | 22 | | ⌘↓ | Decrease volume | 23 | | ⌘M | Minimize window | 24 | | ⇧⌘? | Focus menubar search | 25 | -------------------------------------------------------------------------------- /Documentation/ReleaseEngineering.md: -------------------------------------------------------------------------------- 1 | Release Engineering 2 | =================== 3 | 4 | Requirements 5 | ------------ 6 | 7 | - The DSA private key to generate a signature. This signature is used by 8 | Sparkle to prove the integrity and authenticity of the release archive (zip 9 | file). The DSA private key should be accessible (can be a symlink) as 10 | `~/Documents/hermes.key`. 11 | - S3 credentials to upload the release archive to Alex's S3 bucket. 12 | - Copies of HermesApp/Hermes (the app repo) and the 13 | HermesApp/HermesApp.github.io (the GitHub pages repo) in the **same** 14 | directory. The GitHub pages repo **must** be accessible as `hermes-pages`. 15 | 16 | The Process 17 | ----------- 18 | 19 | 1. Edit `Hermes/Resources/Info.plist`: 20 | 1. Increment "Bundle versions string, short" 21 | (`CFBundleShortVersionString`). 22 | 2. Increment "Bundle version" (`CFBundleVersion`). 23 | 24 | 2. Edit `Hermes/CHANGELOG.md` ensuring all significant improvements are noted, 25 | and change the link to "Full Changelog" to the differences between last 26 | release and the release that is about to be pushed to GitHub. 27 | 28 | 3. Test everything, ensuring it's all working as expected. Once satisfied, 29 | commit the changes made in the last two steps. Once committed, tag the 30 | commit with the release version. For example: 31 | 32 | git tag v1.2.0 750f2de 33 | 34 | 35 | 4. Compile, archive, upload the new version, and edit the `hermes-pages` site: 36 | 37 | make upload-release 38 | 39 | 5. Ensure the GitHub pages repository was edited correctly: 40 | 1. Inspect `hermes-pages/_data/urls.yml` ensuring the URL for key 41 | `hermes_download` is valid. 42 | 2. Ensure `hermes-pages/versions.xml` is valid XML. 43 | 3. Make sure `hermes-pages/CHANGELOG.md` looks like `Hermes/CHANGELOG.md`. 44 | 45 | 6. Commit changes in `hermes-pages` and push to GitHub: 46 | 47 | cd hermes-pages 48 | git add . 49 | git commit -m v1.2.0 50 | git push origin master 51 | 52 | 7. Try updating from an old version of Hermes. From textual menus: 53 | **Hermes → Check for Updates**; or from the statusbar icon: click on the 54 | icon, then hold down option and click **Check for Updates**. If the update 55 | results in an error, open `~/Library/Logs/SparkleUpdateLog.log` and find 56 | out what went wrong. If necessary start over, preferrably removing the bad 57 | commit in `github-pages` with command `git reset --hard HEAD^`. 58 | 59 | 8. Push *both* Hermes/HermesApp's master branch and the newly made git tag: 60 | 61 | cd Hermes 62 | git push origin master 63 | git push origin v1.2.0 64 | -------------------------------------------------------------------------------- /Frameworks/Growl.framework/Growl: -------------------------------------------------------------------------------- 1 | Versions/Current/Growl -------------------------------------------------------------------------------- /Frameworks/Growl.framework/Headers: -------------------------------------------------------------------------------- 1 | Versions/Current/Headers -------------------------------------------------------------------------------- /Frameworks/Growl.framework/Resources: -------------------------------------------------------------------------------- 1 | Versions/Current/Resources -------------------------------------------------------------------------------- /Frameworks/Growl.framework/Versions/A/Growl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Frameworks/Growl.framework/Versions/A/Growl -------------------------------------------------------------------------------- /Frameworks/Growl.framework/Versions/A/Headers/Growl.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #ifdef __OBJC__ 4 | # include 5 | #endif 6 | -------------------------------------------------------------------------------- /Frameworks/Growl.framework/Versions/A/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 11C74 7 | CFBundleDevelopmentRegion 8 | English 9 | CFBundleExecutable 10 | Growl 11 | CFBundleIdentifier 12 | com.growl.growlframework 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.3.1 19 | CFBundleSignature 20 | GRRR 21 | CFBundleVersion 22 | 1.3.1 23 | DTCompiler 24 | com.apple.compilers.llvm.clang.1_0 25 | DTPlatformBuild 26 | 4D199 27 | DTPlatformVersion 28 | GM 29 | DTSDKBuild 30 | 11C63 31 | DTSDKName 32 | macosx10.7 33 | DTXcode 34 | 0420 35 | DTXcodeBuild 36 | 4D199 37 | NSPrincipalClass 38 | GrowlApplicationBridge 39 | 40 | 41 | -------------------------------------------------------------------------------- /Frameworks/Growl.framework/Versions/A/_CodeSignature/CodeResources: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | files 6 | 7 | Resources/Info.plist 8 | 9 | SwzGt9RQsuVafBBrfBalB75dCwU= 10 | 11 | 12 | rules 13 | 14 | ^Resources/ 15 | 16 | ^Resources/.*\.lproj/ 17 | 18 | optional 19 | 20 | weight 21 | 1000 22 | 23 | ^Resources/.*\.lproj/locversion.plist$ 24 | 25 | omit 26 | 27 | weight 28 | 1100 29 | 30 | ^version.plist$ 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Frameworks/Growl.framework/Versions/Current: -------------------------------------------------------------------------------- 1 | A -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Headers: -------------------------------------------------------------------------------- 1 | Versions/Current/Headers -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Modules: -------------------------------------------------------------------------------- 1 | Versions/Current/Modules -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/PrivateHeaders: -------------------------------------------------------------------------------- 1 | Versions/Current/PrivateHeaders -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Resources: -------------------------------------------------------------------------------- 1 | Versions/Current/Resources -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Sparkle: -------------------------------------------------------------------------------- 1 | Versions/Current/Sparkle -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Headers/SUAppcast.h: -------------------------------------------------------------------------------- 1 | // 2 | // SUAppcast.h 3 | // Sparkle 4 | // 5 | // Created by Andy Matuschak on 3/12/06. 6 | // Copyright 2006 Andy Matuschak. All rights reserved. 7 | // 8 | 9 | #ifndef SUAPPCAST_H 10 | #define SUAPPCAST_H 11 | 12 | #import 13 | #import "SUExport.h" 14 | 15 | @class SUAppcastItem; 16 | SU_EXPORT @interface SUAppcast : NSObject 17 | 18 | @property (copy) NSString *userAgentString; 19 | @property (copy) NSDictionary *httpHeaders; 20 | 21 | - (void)fetchAppcastFromURL:(NSURL *)url completionBlock:(void (^)(NSError *))err; 22 | 23 | @property (readonly, copy) NSArray *items; 24 | @end 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Headers/SUAppcastItem.h: -------------------------------------------------------------------------------- 1 | // 2 | // SUAppcastItem.h 3 | // Sparkle 4 | // 5 | // Created by Andy Matuschak on 3/12/06. 6 | // Copyright 2006 Andy Matuschak. All rights reserved. 7 | // 8 | 9 | #ifndef SUAPPCASTITEM_H 10 | #define SUAPPCASTITEM_H 11 | 12 | #import 13 | #import "SUExport.h" 14 | 15 | SU_EXPORT @interface SUAppcastItem : NSObject 16 | @property (copy, readonly) NSString *title; 17 | @property (copy, readonly) NSDate *date; 18 | @property (copy, readonly) NSString *itemDescription; 19 | @property (strong, readonly) NSURL *releaseNotesURL; 20 | @property (copy, readonly) NSString *DSASignature; 21 | @property (copy, readonly) NSString *minimumSystemVersion; 22 | @property (copy, readonly) NSString *maximumSystemVersion; 23 | @property (strong, readonly) NSURL *fileURL; 24 | @property (copy, readonly) NSString *versionString; 25 | @property (copy, readonly) NSString *displayVersionString; 26 | @property (copy, readonly) NSDictionary *deltaUpdates; 27 | @property (strong, readonly) NSURL *infoURL; 28 | 29 | // Initializes with data from a dictionary provided by the RSS class. 30 | - (instancetype)initWithDictionary:(NSDictionary *)dict; 31 | - (instancetype)initWithDictionary:(NSDictionary *)dict failureReason:(NSString **)error; 32 | 33 | @property (getter=isDeltaUpdate, readonly) BOOL deltaUpdate; 34 | @property (getter=isCriticalUpdate, readonly) BOOL criticalUpdate; 35 | @property (getter=isInformationOnlyUpdate, readonly) BOOL informationOnlyUpdate; 36 | 37 | // Returns the dictionary provided in initWithDictionary; this might be useful later for extensions. 38 | @property (readonly, copy) NSDictionary *propertiesDictionary; 39 | 40 | - (NSURL *)infoURL; 41 | 42 | @end 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Headers/SUErrors.h: -------------------------------------------------------------------------------- 1 | // 2 | // SUErrors.h 3 | // Sparkle 4 | // 5 | // Created by C.W. Betts on 10/13/14. 6 | // Copyright (c) 2014 Sparkle Project. All rights reserved. 7 | // 8 | 9 | #ifndef SUERRORS_H 10 | #define SUERRORS_H 11 | 12 | #import 13 | #import "SUExport.h" 14 | 15 | /** 16 | * Error domain used by Sparkle 17 | */ 18 | SU_EXPORT extern NSString *const SUSparkleErrorDomain; 19 | 20 | typedef NS_ENUM(OSStatus, SUError) { 21 | // Appcast phase errors. 22 | SUAppcastParseError = 1000, 23 | SUNoUpdateError = 1001, 24 | SUAppcastError = 1002, 25 | SURunningFromDiskImageError = 1003, 26 | 27 | // Downlaod phase errors. 28 | SUTemporaryDirectoryError = 2000, 29 | 30 | // Extraction phase errors. 31 | SUUnarchivingError = 3000, 32 | SUSignatureError = 3001, 33 | 34 | // Installation phase errors. 35 | SUFileCopyFailure = 4000, 36 | SUAuthenticationFailure = 4001, 37 | SUMissingUpdateError = 4002, 38 | SUMissingInstallerToolError = 4003, 39 | SURelaunchError = 4004, 40 | SUInstallationError = 4005, 41 | SUDowngradeError = 4006, 42 | 43 | // System phase errors 44 | SUSystemPowerOffError = 5000 45 | }; 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Headers/SUExport.h: -------------------------------------------------------------------------------- 1 | // 2 | // SUExport.h 3 | // Sparkle 4 | // 5 | // Created by Jake Petroules on 2014-08-23. 6 | // Copyright (c) 2014 Sparkle Project. All rights reserved. 7 | // 8 | 9 | #ifndef SUEXPORT_H 10 | #define SUEXPORT_H 11 | 12 | #ifdef BUILDING_SPARKLE 13 | #define SU_EXPORT __attribute__((visibility("default"))) 14 | #else 15 | #define SU_EXPORT 16 | #endif 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Headers/SUStandardVersionComparator.h: -------------------------------------------------------------------------------- 1 | // 2 | // SUStandardVersionComparator.h 3 | // Sparkle 4 | // 5 | // Created by Andy Matuschak on 12/21/07. 6 | // Copyright 2007 Andy Matuschak. All rights reserved. 7 | // 8 | 9 | #ifndef SUSTANDARDVERSIONCOMPARATOR_H 10 | #define SUSTANDARDVERSIONCOMPARATOR_H 11 | 12 | #import 13 | #import "SUExport.h" 14 | #import "SUVersionComparisonProtocol.h" 15 | 16 | /*! 17 | Sparkle's default version comparator. 18 | 19 | This comparator is adapted from MacPAD, by Kevin Ballard. 20 | It's "dumb" in that it does essentially string comparison, 21 | in components split by character type. 22 | */ 23 | SU_EXPORT @interface SUStandardVersionComparator : NSObject 24 | 25 | /*! 26 | Returns a singleton instance of the comparator. 27 | */ 28 | + (SUStandardVersionComparator *)defaultComparator; 29 | 30 | /*! 31 | Compares version strings through textual analysis. 32 | 33 | See the implementation for more details. 34 | */ 35 | - (NSComparisonResult)compareVersion:(NSString *)versionA toVersion:(NSString *)versionB; 36 | @end 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Headers/SUVersionComparisonProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // SUVersionComparisonProtocol.h 3 | // Sparkle 4 | // 5 | // Created by Andy Matuschak on 12/21/07. 6 | // Copyright 2007 Andy Matuschak. All rights reserved. 7 | // 8 | 9 | #ifndef SUVERSIONCOMPARISONPROTOCOL_H 10 | #define SUVERSIONCOMPARISONPROTOCOL_H 11 | 12 | #import 13 | #import "SUExport.h" 14 | 15 | /*! 16 | Provides version comparison facilities for Sparkle. 17 | */ 18 | @protocol SUVersionComparison 19 | 20 | /*! 21 | An abstract method to compare two version strings. 22 | 23 | Should return NSOrderedAscending if b > a, NSOrderedDescending if b < a, 24 | and NSOrderedSame if they are equivalent. 25 | */ 26 | - (NSComparisonResult)compareVersion:(NSString *)versionA toVersion:(NSString *)versionB; // *** MAY BE CALLED ON NON-MAIN THREAD! 27 | 28 | @end 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Headers/SUVersionDisplayProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // SUVersionDisplayProtocol.h 3 | // EyeTV 4 | // 5 | // Created by Uli Kusterer on 08.12.09. 6 | // Copyright 2009 Elgato Systems GmbH. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "SUExport.h" 11 | 12 | /*! 13 | Applies special display formatting to version numbers. 14 | */ 15 | @protocol SUVersionDisplay 16 | 17 | /*! 18 | Formats two version strings. 19 | 20 | Both versions are provided so that important distinguishing information 21 | can be displayed while also leaving out unnecessary/confusing parts. 22 | */ 23 | - (void)formatVersion:(NSString **)inOutVersionA andVersion:(NSString **)inOutVersionB; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Headers/Sparkle.h: -------------------------------------------------------------------------------- 1 | // 2 | // Sparkle.h 3 | // Sparkle 4 | // 5 | // Created by Andy Matuschak on 3/16/06. (Modified by CDHW on 23/12/07) 6 | // Copyright 2006 Andy Matuschak. All rights reserved. 7 | // 8 | 9 | #ifndef SPARKLE_H 10 | #define SPARKLE_H 11 | 12 | #import 13 | 14 | // This list should include the shared headers. It doesn't matter if some of them aren't shared (unless 15 | // there are name-space collisions) so we can list all of them to start with: 16 | 17 | #import "SUAppcast.h" 18 | #import "SUAppcastItem.h" 19 | #import "SUStandardVersionComparator.h" 20 | #import "SUUpdater.h" 21 | #import "SUVersionComparisonProtocol.h" 22 | #import "SUVersionDisplayProtocol.h" 23 | #import "SUErrors.h" 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module Sparkle { 2 | umbrella header "Sparkle.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/PrivateHeaders/SUUnarchiver.h: -------------------------------------------------------------------------------- 1 | // 2 | // SUUnarchiver.h 3 | // Sparkle 4 | // 5 | // Created by Andy Matuschak on 3/16/06. 6 | // Copyright 2006 Andy Matuschak. All rights reserved. 7 | // 8 | 9 | #ifndef SUUNARCHIVER_H 10 | #define SUUNARCHIVER_H 11 | 12 | #import 13 | 14 | @class SUHost; 15 | @protocol SUUnarchiverDelegate; 16 | 17 | @interface SUUnarchiver : NSObject 18 | 19 | @property (copy, readonly) NSString *archivePath; 20 | @property (copy, readonly) NSString *updateHostBundlePath; 21 | @property (weak) id delegate; 22 | 23 | + (SUUnarchiver *)unarchiverForPath:(NSString *)path updatingHostBundlePath:(NSString *)host; 24 | 25 | - (void)start; 26 | @end 27 | 28 | @protocol SUUnarchiverDelegate 29 | - (void)unarchiverDidFinish:(SUUnarchiver *)unarchiver; 30 | - (void)unarchiverDidFail:(SUUnarchiver *)unarchiver; 31 | @optional 32 | - (void)unarchiver:(SUUnarchiver *)unarchiver extractedProgress:(double)progress; 33 | @end 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 15E27e 7 | CFBundleDevelopmentRegion 8 | English 9 | CFBundleExecutable 10 | Autoupdate 11 | CFBundleIconFile 12 | AppIcon 13 | CFBundleIdentifier 14 | org.sparkle-project.Sparkle.Autoupdate 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.13.1 git-2afc553 21 | CFBundleSignature 22 | ???? 23 | CFBundleSupportedPlatforms 24 | 25 | MacOSX 26 | 27 | CFBundleVersion 28 | 1.13.1 29 | DTCompiler 30 | com.apple.compilers.llvm.clang.1_0 31 | DTPlatformBuild 32 | 7C68 33 | DTPlatformVersion 34 | GM 35 | DTSDKBuild 36 | 15C43 37 | DTSDKName 38 | macosx10.11 39 | DTXcode 40 | 0720 41 | DTXcodeBuild 42 | 7C68 43 | LSBackgroundOnly 44 | 1 45 | LSMinimumSystemVersion 46 | 10.7 47 | LSUIElement 48 | 1 49 | NSMainNibFile 50 | MainMenu 51 | NSPrincipalClass 52 | NSApplication 53 | 54 | 55 | -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/MacOS/Autoupdate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Frameworks/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/MacOS/Autoupdate -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/PkgInfo: -------------------------------------------------------------------------------- 1 | APPL???? -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/AppIcon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Frameworks/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/AppIcon.icns -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/SUStatus.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Frameworks/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/SUStatus.nib -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/en.lproj/Sparkle.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Frameworks/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/en.lproj/Sparkle.strings -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 15E27e 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | Sparkle 11 | CFBundleIdentifier 12 | org.sparkle-project.Sparkle 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | Sparkle 17 | CFBundlePackageType 18 | FMWK 19 | CFBundleShortVersionString 20 | 1.13.1 git-2afc553 21 | CFBundleSignature 22 | ???? 23 | CFBundleSupportedPlatforms 24 | 25 | MacOSX 26 | 27 | CFBundleVersion 28 | 1.13.1 29 | DTCompiler 30 | com.apple.compilers.llvm.clang.1_0 31 | DTPlatformBuild 32 | 7C68 33 | DTPlatformVersion 34 | GM 35 | DTSDKBuild 36 | 15C43 37 | DTSDKName 38 | macosx10.11 39 | DTXcode 40 | 0720 41 | DTXcodeBuild 42 | 7C68 43 | 44 | 45 | -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Resources/SUStatus.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Frameworks/Sparkle.framework/Versions/A/Resources/SUStatus.nib -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Frameworks/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Frameworks/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Frameworks/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Resources/en.lproj/Sparkle.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Frameworks/Sparkle.framework/Versions/A/Resources/en.lproj/Sparkle.strings -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/A/Sparkle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Frameworks/Sparkle.framework/Versions/A/Sparkle -------------------------------------------------------------------------------- /Frameworks/Sparkle.framework/Versions/Current: -------------------------------------------------------------------------------- 1 | A -------------------------------------------------------------------------------- /Hermes.xcodeproj/xcshareddata/xcschemes/Archive Hermes.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 66 | 67 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /Hermes.xcodeproj/xcshareddata/xcschemes/Hermes (Release — media keys enabled).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Hermes.xcodeproj/xcshareddata/xcschemes/Hermes.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Hermes.xcodeproj/xcshareddata/xcschemes/Upload Hermes Release.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 66 | 67 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /ImportedSources/FMEngine/FMEngine.h: -------------------------------------------------------------------------------- 1 | // 2 | // FMEngine.h 3 | // LastFMAPI 4 | // 5 | // Created by Nicolas Haunold on 4/26/09. 6 | // Copyright 2009 Tapolicious Software. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "NSString+FMEngine.h" 11 | 12 | #define _LASTFM_API_KEY_ @"31fc44bcd6e21954afb404d179a09e9a" 13 | #define _LASTFM_SECRETK_ @"a146429ed54f25b8bf9d5ca3cc423260" 14 | #define _LASTFM_BASEURL_ @"https://ws.audioscrobbler.com/2.0/" 15 | 16 | // Comment the next line to use XML 17 | #define _USE_JSON_ 1 18 | 19 | #define POST_TYPE @"POST" 20 | #define GET_TYPE @"GET" 21 | 22 | typedef void(^FMCallback)(NSData*, NSError*); 23 | 24 | @class FMEngine; 25 | 26 | @interface FMEngine : NSObject { 27 | NSMutableData *receivedData; 28 | } 29 | 30 | - (NSString *)generateAuthTokenFromUsername:(NSString *)username password:(NSString *)password; 31 | - (NSString *)generateSignatureFromDictionary:(NSDictionary *)dict; 32 | - (NSString *)generatePOSTBodyFromDictionary:(NSDictionary *)dict; 33 | - (NSURL *)generateURLFromDictionary:(NSDictionary *)dict; 34 | 35 | - (void)performMethod:(NSString *)method withCallback:(FMCallback)cb withParameters:(NSDictionary *)params useSignature:(BOOL)useSig httpMethod:(NSString *)httpMethod; 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /ImportedSources/FMEngine/FMEngine.m: -------------------------------------------------------------------------------- 1 | // 2 | // FMEngine.m 3 | // LastFMAPI 4 | // 5 | // Created by Nicolas Haunold on 4/26/09. 6 | // Copyright 2009 Tapolicious Software. All rights reserved. 7 | // 8 | 9 | #import "FMEngine.h" 10 | #import "URLConnection.h" 11 | 12 | @implementation FMEngine 13 | 14 | static NSInteger sortAlpha(NSString *n1, NSString *n2, void *context) { 15 | return [n1 caseInsensitiveCompare:n2]; 16 | } 17 | 18 | - (NSString *)generateAuthTokenFromUsername:(NSString *)username password:(NSString *)password { 19 | NSString *unencryptedToken = [NSString stringWithFormat:@"%@%@", username, [password md5sum]]; 20 | return [unencryptedToken md5sum]; 21 | } 22 | 23 | - (void) performMethod:(NSString *)method 24 | withCallback:(FMCallback)callback 25 | withParameters:(NSDictionary *)params 26 | useSignature:(BOOL)useSig 27 | httpMethod:(NSString *)httpMethod { 28 | NSString *dataSig; 29 | NSMutableURLRequest *request; 30 | NSMutableDictionary *tempDict = [[NSMutableDictionary alloc] initWithDictionary:params]; 31 | 32 | tempDict[@"method"] = method; 33 | if(useSig == TRUE) { 34 | dataSig = [self generateSignatureFromDictionary:tempDict]; 35 | 36 | tempDict[@"api_sig"] = dataSig; 37 | NSLogd(@"%@", tempDict); 38 | } 39 | 40 | #ifdef _USE_JSON_ 41 | if(![httpMethod isEqualToString:@"POST"]) { 42 | tempDict[@"format"] = @"json"; 43 | } 44 | #endif 45 | 46 | params = [NSDictionary dictionaryWithDictionary:tempDict]; 47 | 48 | if(![httpMethod isEqualToString:@"POST"]) { 49 | NSURL *dataURL = [self generateURLFromDictionary:params]; 50 | request = [NSMutableURLRequest requestWithURL:dataURL]; 51 | } else { 52 | #ifdef _USE_JSON_ 53 | request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:_LASTFM_BASEURL_ @"?format=json"]]; 54 | #else 55 | request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:_LASTFM_BASEURL_]]; 56 | #endif 57 | [request setHTTPMethod:httpMethod]; 58 | [request setHTTPBody:[[self generatePOSTBodyFromDictionary:params] dataUsingEncoding:NSUTF8StringEncoding]]; 59 | [request addValue:@"application/x-www-form-urlencoded" 60 | forHTTPHeaderField:@"Content-Type"]; 61 | } 62 | 63 | URLConnection *connection = [URLConnection connectionForRequest:request 64 | completionHandler:callback]; 65 | [connection start]; 66 | } 67 | 68 | - (NSString *)generatePOSTBodyFromDictionary:(NSDictionary *)dict { 69 | NSMutableString *rawBody = [[NSMutableString alloc] init]; 70 | NSMutableArray *aMutableArray = [[NSMutableArray alloc] initWithArray:[dict allKeys]]; 71 | [aMutableArray sortUsingFunction:sortAlpha context:NULL]; 72 | 73 | for(NSString *key in aMutableArray) { 74 | NSString *val = [NSString stringWithFormat:@"%@", dict[key]]; 75 | [rawBody appendString:[NSString stringWithFormat:@"&%@=%@", key, [val urlEncoded]]]; 76 | } 77 | 78 | NSString *body = [NSString stringWithString:rawBody]; 79 | 80 | return body; 81 | } 82 | 83 | - (NSURL *)generateURLFromDictionary:(NSDictionary *)dict { 84 | NSMutableArray *aMutableArray = [[NSMutableArray alloc] initWithArray:[dict allKeys]]; 85 | NSMutableString *rawURL = [NSMutableString stringWithString:_LASTFM_BASEURL_]; 86 | [aMutableArray sortUsingFunction:sortAlpha context:NULL]; 87 | 88 | for(unsigned int i = 0; i < [aMutableArray count]; i++) { 89 | NSString *key = aMutableArray[i]; 90 | NSString *val = [NSString stringWithFormat:@"%@", dict[key]]; 91 | 92 | if(i == 0) { 93 | [rawURL appendString:[NSString stringWithFormat:@"?%@=%@", key, [val urlEncoded]]]; 94 | } else { 95 | [rawURL appendString:[NSString stringWithFormat:@"&%@=%@", key, [val urlEncoded]]]; 96 | } 97 | } 98 | 99 | NSString *encodedURL = [rawURL stringByReplacingOccurrencesOfString:@" " withString:@"%20"]; 100 | NSURL *url = [NSURL URLWithString:encodedURL]; 101 | 102 | return url; 103 | } 104 | 105 | - (NSString *)generateSignatureFromDictionary:(NSDictionary *)dict { 106 | NSMutableArray *aMutableArray = [[NSMutableArray alloc] initWithArray:[dict allKeys]]; 107 | NSMutableString *rawSignature = [[NSMutableString alloc] init]; 108 | [aMutableArray sortUsingFunction:sortAlpha context:NULL]; 109 | 110 | for(NSString *key in aMutableArray) { 111 | [rawSignature appendString:[NSString stringWithFormat:@"%@%@", key, dict[key]]]; 112 | } 113 | 114 | [rawSignature appendString:_LASTFM_SECRETK_]; 115 | 116 | NSString *signature = [rawSignature md5sum]; 117 | return signature; 118 | } 119 | 120 | @end 121 | -------------------------------------------------------------------------------- /ImportedSources/FMEngine/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Nicolas Haunold 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. -------------------------------------------------------------------------------- /ImportedSources/FMEngine/NSString+FMEngine.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+UUID.h 3 | // LastFMAPI 4 | // 5 | // Created by Nicolas Haunold on 4/26/09. 6 | // Copyright 2009 Tapolicious Software. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSString (UUID) 12 | + (NSString *)stringWithNewUUID; 13 | - (NSString *)md5sum; 14 | - (NSString*) urlEncoded; 15 | @end 16 | -------------------------------------------------------------------------------- /ImportedSources/FMEngine/NSString+FMEngine.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+UUID.m 3 | // LastFMAPI 4 | // 5 | // Created by Nicolas Haunold on 4/26/09. 6 | // Copyright 2009 Tapolicious Software. All rights reserved. 7 | // 8 | 9 | // Thanks to Sam Steele / c99koder for -[NSString md5sum]; 10 | 11 | #import "NSString+FMEngine.h" 12 | #import 13 | 14 | @implementation NSString (FMEngineAdditions) 15 | 16 | + (NSString *)stringWithNewUUID { 17 | CFUUIDRef uuidObj = CFUUIDCreate(nil); 18 | 19 | NSString *newUUID = (__bridge_transfer NSString*)CFUUIDCreateString(nil, uuidObj); 20 | CFRelease(uuidObj); 21 | return newUUID; 22 | } 23 | 24 | - (NSString*) urlEncoded { 25 | NSString *encoded = (__bridge_transfer NSString*) CFURLCreateStringByAddingPercentEscapes(NULL, 26 | (__bridge CFStringRef) self, NULL, 27 | (CFStringRef) @"!*'();:@&=+$,/?%#[]", 28 | kCFStringEncodingUTF8 ); 29 | return encoded; 30 | } 31 | 32 | - (NSString *)md5sum { 33 | unsigned char digest[CC_MD5_DIGEST_LENGTH], i; 34 | CC_MD5([self UTF8String], (uint32_t)[self lengthOfBytesUsingEncoding:NSUTF8StringEncoding], digest); 35 | NSMutableString *ms = [NSMutableString string]; 36 | for (i=0;i 2 | 3 | @interface SPInvocationGrabber : NSObject { 4 | id _object; 5 | NSInvocation *_invocation; 6 | int frameCount; 7 | char **frameStrings; 8 | BOOL backgroundAfterForward; 9 | BOOL onMainAfterForward; 10 | BOOL waitUntilDone; 11 | } 12 | -(id)initWithObject:(id)obj; 13 | -(id)initWithObject:(id)obj stacktraceSaving:(BOOL)saveStack; 14 | @property (readonly, retain, nonatomic) id object; 15 | @property (readonly, retain, nonatomic) NSInvocation *invocation; 16 | @property BOOL backgroundAfterForward; 17 | @property BOOL onMainAfterForward; 18 | @property BOOL waitUntilDone; 19 | -(void)invoke; // will release object and invocation 20 | -(void)printBacktrace; 21 | -(void)saveBacktrace; 22 | @end 23 | 24 | @interface NSObject (SPInvocationGrabbing) 25 | -(id)grab; 26 | -(id)invokeAfter:(NSTimeInterval)delta; 27 | -(id)nextRunloop; 28 | -(id)inBackground; 29 | -(id)onMainAsync:(BOOL)async; 30 | @end 31 | -------------------------------------------------------------------------------- /ImportedSources/SPMediaKeyTap/NSObject+SPInvocationGrabbing.m: -------------------------------------------------------------------------------- 1 | #import "NSObject+SPInvocationGrabbing.h" 2 | #import 3 | 4 | #pragma mark Invocation grabbing 5 | @interface SPInvocationGrabber () 6 | @property (readwrite, retain, nonatomic) id object; 7 | @property (readwrite, retain, nonatomic) NSInvocation *invocation; 8 | 9 | @end 10 | 11 | @implementation SPInvocationGrabber 12 | - (id)initWithObject:(id)obj 13 | { 14 | return [self initWithObject:obj stacktraceSaving:YES]; 15 | } 16 | 17 | -(id)initWithObject:(id)obj stacktraceSaving:(BOOL)saveStack 18 | { 19 | self.object = obj; 20 | 21 | if(saveStack) 22 | [self saveBacktrace]; 23 | 24 | return self; 25 | } 26 | -(void)dealloc 27 | { 28 | free(frameStrings); 29 | self.object = nil; 30 | self.invocation = nil; 31 | [super dealloc]; 32 | } 33 | @synthesize invocation = _invocation, object = _object; 34 | 35 | @synthesize backgroundAfterForward, onMainAfterForward, waitUntilDone; 36 | - (void)runInBackground 37 | { 38 | NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 39 | @try { 40 | [self invoke]; 41 | } 42 | @finally { 43 | [pool drain]; 44 | } 45 | } 46 | 47 | 48 | - (void)forwardInvocation:(NSInvocation *)anInvocation { 49 | [anInvocation retainArguments]; 50 | anInvocation.target = _object; 51 | self.invocation = anInvocation; 52 | 53 | if(backgroundAfterForward) 54 | [NSThread detachNewThreadSelector:@selector(runInBackground) toTarget:self withObject:nil]; 55 | else if(onMainAfterForward) 56 | [self performSelectorOnMainThread:@selector(invoke) withObject:nil waitUntilDone:waitUntilDone]; 57 | } 58 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)inSelector { 59 | NSMethodSignature *signature = [super methodSignatureForSelector:inSelector]; 60 | if (signature == NULL) 61 | signature = [_object methodSignatureForSelector:inSelector]; 62 | 63 | return signature; 64 | } 65 | 66 | - (void)invoke 67 | { 68 | 69 | @try { 70 | [_invocation invoke]; 71 | } 72 | @catch (NSException * e) { 73 | NSLog(@"SPInvocationGrabber's target raised %@:\n\t%@\nInvocation was originally scheduled at:", e.name, e); 74 | [self printBacktrace]; 75 | printf("\n"); 76 | [e raise]; 77 | } 78 | 79 | self.invocation = nil; 80 | self.object = nil; 81 | } 82 | 83 | -(void)saveBacktrace 84 | { 85 | void *backtraceFrames[128]; 86 | frameCount = backtrace(&backtraceFrames[0], 128); 87 | frameStrings = backtrace_symbols(&backtraceFrames[0], frameCount); 88 | } 89 | -(void)printBacktrace 90 | { 91 | for(int x = 3; x < frameCount; x++) { 92 | if(frameStrings[x] == NULL) { break; } 93 | printf("%s\n", frameStrings[x]); 94 | } 95 | } 96 | @end 97 | 98 | @implementation NSObject (SPInvocationGrabbing) 99 | -(id)grab 100 | { 101 | return [[[SPInvocationGrabber alloc] initWithObject:self] autorelease]; 102 | } 103 | -(id)invokeAfter:(NSTimeInterval)delta 104 | { 105 | id grabber = [self grab]; 106 | [NSTimer scheduledTimerWithTimeInterval:delta target:grabber selector:@selector(invoke) userInfo:nil repeats:NO]; 107 | return grabber; 108 | } 109 | - (id)nextRunloop 110 | { 111 | return [self invokeAfter:0]; 112 | } 113 | -(id)inBackground 114 | { 115 | SPInvocationGrabber *grabber = [self grab]; 116 | grabber.backgroundAfterForward = YES; 117 | return grabber; 118 | } 119 | -(id)onMainAsync:(BOOL)async 120 | { 121 | SPInvocationGrabber *grabber = [self grab]; 122 | grabber.onMainAfterForward = YES; 123 | grabber.waitUntilDone = !async; 124 | return grabber; 125 | } 126 | 127 | @end 128 | -------------------------------------------------------------------------------- /ImportedSources/SPMediaKeyTap/SPMediaKeyTap.h: -------------------------------------------------------------------------------- 1 | #include 2 | #import 3 | #import 4 | 5 | // http://overooped.com/post/2593597587/mediakeys 6 | 7 | #define SPSystemDefinedEventMediaKeys 8 8 | 9 | @interface SPMediaKeyTap : NSObject { 10 | EventHandlerRef _app_switching_ref; 11 | EventHandlerRef _app_terminating_ref; 12 | CFMachPortRef _eventPort; 13 | CFRunLoopSourceRef _eventPortSource; 14 | CFRunLoopRef _tapThreadRL; 15 | BOOL _shouldInterceptMediaKeyEvents; 16 | id _delegate; 17 | // The app that is frontmost in this list owns media keys 18 | NSMutableArray *_mediaKeyAppList; 19 | } 20 | + (NSArray*)defaultMediaKeyUserBundleIdentifiers; 21 | 22 | -(id)initWithDelegate:(id)delegate; 23 | 24 | +(BOOL)usesGlobalMediaKeyTap; 25 | -(void)startWatchingMediaKeys; 26 | -(void)stopWatchingMediaKeys; 27 | -(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event; 28 | @end 29 | 30 | @interface NSObject (SPMediaKeyTapDelegate) 31 | -(void)mediaKeyTap:(SPMediaKeyTap*)keyTap receivedMediaKeyEvent:(NSEvent*)event; 32 | @end 33 | 34 | #ifdef __cplusplus 35 | extern "C" { 36 | #endif 37 | 38 | extern NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey; 39 | extern NSString *kIgnoreMediaKeysDefaultsKey; 40 | 41 | #ifdef __cplusplus 42 | } 43 | #endif -------------------------------------------------------------------------------- /ImportedSources/blowfish/blowfish.h: -------------------------------------------------------------------------------- 1 | /* === The header === */ 2 | 3 | /* Put this in blowfish.h if you don't like having everything in one 4 | * big file. */ 5 | 6 | #ifndef _DMADORE_BLOWFISH_H 7 | #define _DMADORE_BLOWFISH_H 8 | 9 | /* --- Basic blowfish routines --- */ 10 | 11 | #define NBROUNDS 16 12 | 13 | struct blf_ctx { 14 | /* The subkeys used by the blowfish cipher */ 15 | unsigned long P[NBROUNDS+2], S[4][256]; 16 | }; 17 | 18 | /* Encipher one 64-bit quantity (divided in two 32-bit quantities) 19 | * using the precalculated subkeys). */ 20 | void Blowfish_encipher (const struct blf_ctx *c, 21 | unsigned long *xl, unsigned long *xr); 22 | 23 | /* Decipher one 64-bit quantity (divided in two 32-bit quantities) 24 | * using the precalculated subkeys). */ 25 | void Blowfish_decipher (const struct blf_ctx *c, 26 | unsigned long *xl, unsigned long *xr); 27 | 28 | /* Initialize the cipher by calculating the subkeys from the key. */ 29 | void Blowfish_initialize (struct blf_ctx *c, 30 | const unsigned char key[], unsigned long key_bytes); 31 | 32 | /* --- Blowfish used in Electronic Code Book (ECB) mode --- */ 33 | 34 | struct blf_ecb_ctx { 35 | /* Whether we are encrypting (rather than decrypting) */ 36 | char encrypt; 37 | /* The blowfish subkeys */ 38 | struct blf_ctx c; 39 | /* The 64-bits of data being written */ 40 | unsigned long dl, dr; 41 | /* Our position within the 64 bits (always between 0 and 7) */ 42 | int b; 43 | /* The callback function to be called with every byte produced */ 44 | void (* callback) (unsigned char byte, void *user_data); 45 | /* The user data to pass the the callback function */ 46 | void *user_data; 47 | }; 48 | 49 | /* Start an ECB Blowfish cipher session: specify whether we are 50 | * encrypting or decrypting, what key is to be used, and what callback 51 | * should be called for every byte produced. */ 52 | void Blowfish_ecb_start (struct blf_ecb_ctx *c, char encrypt, 53 | const unsigned char key[], unsigned long key_bytes, 54 | void (* callback) (unsigned char byte, 55 | void *user_data), 56 | void *user_data); 57 | 58 | /* Feed one byte to an ECB Blowfish cipher session. */ 59 | void Blowfish_ecb_feed (struct blf_ecb_ctx *c, unsigned char inb); 60 | 61 | /* Stop an ECB Blowfish session (i.e. flush the remaining bytes). */ 62 | void Blowfish_ecb_stop (struct blf_ecb_ctx *c); 63 | 64 | #endif /* not defined _DMADORE_BLOWFISH_H */ 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2017 Alex Crichton, Nicholas Riley, and Winston Weinert 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | 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, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Do not mind me. I'm just a nice wrapper around xcodebuild(1). 2 | 3 | XCB = xcodebuild 4 | XCPIPE = 5 | CONFIGURATION = Debug 6 | SCHEME = Hermes 7 | HERMES = ./build/$(CONFIGURATION)/Hermes.app/Contents/MacOS/Hermes 8 | DEBUGGER = lldb 9 | 10 | # For some reason the project's SYMROOT setting is ignored when we specify an 11 | # explicit -project option. The -project option is required when using xctool. 12 | COMMON_OPTS = -project Hermes.xcodeproj SYMROOT=build 13 | 14 | all: hermes 15 | 16 | hermes: 17 | $(XCB) $(COMMON_OPTS) -configuration $(CONFIGURATION) -scheme $(SCHEME) $(XCPIPE) 18 | 19 | travis: COMMON_OPTS += CODE_SIGN_IDENTITY= CODE_SIGNING_REQUIRED=NO 20 | travis: XCPIPE = | xcpretty -f `xcpretty-travis-formatter` 21 | travis: hermes 22 | 23 | run: hermes 24 | $(HERMES) 25 | 26 | dbg: hermes 27 | $(DEBUGGER) $(HERMES) 28 | 29 | install: 30 | $(XCB) $(COMMON_OPTS) -configuration Release -scheme Hermes 31 | rm -rf /Applications/Hermes.app 32 | cp -a ./build/Release/Hermes.app /Applications/ 33 | 34 | # Create an archive to share (for beta testing purposes). 35 | archive: CONFIGURATION = Release 36 | archive: SCHEME = 'Archive Hermes' 37 | archive: hermes 38 | 39 | # Used to be called 'archive'. Upload Hermes and update the website. 40 | upload-release: CONFIGURATION = Release 41 | upload-release: SCHEME = 'Upload Hermes Release' 42 | upload-release: hermes 43 | 44 | clean: 45 | $(XCB) $(COMMON_OPTS) -scheme $(SCHEME) clean 46 | rm -rf build 47 | 48 | .PHONY: all hermes travis run dbg archive clean install archive upload-release 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Hermes 2 | ====== 3 | 4 | 5 | [![Build Status](https://travis-ci.org/HermesApp/Hermes.svg?branch=master)](https://travis-ci.org/HermesApp/Hermes) 6 | 7 | A [Pandora](http://www.pandora.com/) client for macOS. 8 | 9 | ### THIS PROJECT IS UNMAINTAINED 10 | 11 | This means that bugs will not be fixed and features will not be added unless someone else does so. Unfortunately, the former maintainers no longer have the time and/or resources to work on Hermes further. 12 | 13 | If you're interested in fixing up Hermes, please reply to this [GitHub issue (237)](https://github.com/HermesApp/Hermes/issues/237). 14 | 15 | ### Download Hermes 16 | 17 | - Click Download at [hermesapp.org](http://hermesapp.org/). 18 | - Or install using [Homebrew](http://brew.sh)/[Caskroom](https://caskroom.github.io): `brew cask install hermes`. 19 | 20 | If you would like to compile Hermes, continue reading. 21 | 22 | ### Develop against Hermes 23 | 24 | Thanks to the suggestions by [blalor](https://github.com/blalor), there's a few 25 | ways you can develop against Hermes if you really want to. 26 | 27 | 1. `NSDistributedNotificationCenter` - Every time a new song plays, a 28 | notification is posted with the name `hermes.song` under the object `hermes` 29 | with `userInfo` as a dictionary representing the song being played. See 30 | [Song.m](https://github.com/HermesApp/Hermes/blob/master/Sources/Pandora/Song.m#L29) 31 | for the keys available to you. 32 | 33 | 2. AppleScript - here's an example script: 34 | 35 | tell application "Hermes" 36 | play -- resumes playback, does nothing if playing 37 | pause -- pauses playback, does nothing if not playing 38 | playpause -- toggles playback between pause/play 39 | next song -- goes to the next song 40 | get playback state 41 | set playback state to playing 42 | 43 | thumbs up -- likes the current song 44 | thumbs down -- dislikes the current song, going to another one 45 | tired of song -- sets the current song as being "tired of" 46 | 47 | raise volume -- raises the volume partially 48 | lower volume -- lowers the volume partially 49 | full volume -- raises volume to max 50 | mute -- mutes the volume 51 | unmute -- unmutes the volume to the last state from mute 52 | 53 | -- integer 0 to 100 for the volume 54 | get playback volume 55 | set playback volume to 92 56 | 57 | -- Working with the current station 58 | set stationName to the current station's name 59 | set stationId to station 2's stationId 60 | set the current station to station 4 61 | 62 | -- Getting information from the current song 63 | set title to the current song's title 64 | set artist to the current song's artist 65 | set album to the current song's album 66 | ... etc 67 | end tell 68 | 69 | ### Want something new/fixed? 70 | 71 | 1. [Open a ticket](https://github.com/HermesApp/Hermes/issues)! We'll get 72 | around to it soon, especially if it sounds appealing to us. We take all 73 | suggestions/feedback! 74 | 75 | 2. Take a stab at it yourself if you're brave. Just send us a pull request if 76 | you've got something fixed. Here's some common things to do at the command 77 | line: 78 | 79 | make # build everything 80 | make run # build and run the application (logging to stdout) 81 | make dbg # build and run inside LLDB 82 | 83 | # Build with the 'Release' configuration instead of 'Debug' 84 | make CONFIGURATION=Release [run|dbg] 85 | 86 | Please note that Media Key shortcuts 87 | [will not work](https://github.com/nevyn/SPMediaKeyTap/blob/master/SPMediaKeyTap.m#L108) 88 | if compiled with `CONFIGURATION=Debug` (the default). 89 | 90 | ## License 91 | 92 | Code is available under the [MIT 93 | License](https://github.com/HermesApp/Hermes/blob/master/LICENSE). 94 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing Hermes 2 | 3 | You can make the following types of releases: 4 | 5 | * Private releases, generally only to one or two people, which don't get distributed through the Web site 6 | * Public beta releases, which do get distributed through the Web site 7 | * Final releases 8 | 9 | The work involved in each release type generally encompasses those above it. 10 | 11 | ## If you need to make a public release 12 | 13 | Assuming your account can commit to the Hermes repository, to make a public beta or final release, you will also need an access token for the GitHub API, which you can generate [here](https://github.com/settings/tokens/) — click **Generate new token**, then set the scope to **repo**. Make sure to save the key locally as it only gets shown to you at the time of generation. 14 | 15 | Before making a final release, you will need the following structure on disk: 16 | 17 | - `Hermes/` — `git clone git@github.com:HermesApp/Hermes` 18 | - `hermes-pages/` — `git clone git@github.com:HermesApp/hermesapp.github.io hermes-pages` 19 | - `hermes.key` — DSA private key for Sparkle updates (obtain from a project administrator) 20 | 21 | Also for final releases, you also need to install the `redcarpet` Ruby gem in order to process the Markdown-formatted changelog into HTML. Just `sudo gem install redcarpet`, or do something more sophisticated if you're better at Ruby than I am. 22 | 23 | ## About version numbers 24 | 25 | Hermes uses Apple generic versioning, maintained by `agvtool`. Each release of Hermes has two versions. 26 | 27 | The **project version** is a floating-point number which increases with each release. It is `$CURRENT_PROJECT_VERSION` during the build process and `CFBundleVersion` in `Info.plist`. 28 | 29 | The **marketing version** is formatted like `x.y.z`, possibly with a letter-number suffix for prerelease versions, and is `CFBundleShortVersionString` in `Info.plist`. It is what most users consider to be the version. 30 | 31 | Both of these show up in Hermes’ About box, for example "Version 1.2.7 (2040)". 32 | 33 | ## Always bump before you release 34 | 35 | Every time you distribute a new version of Hermes beyond the confines of your computer(s), first `bump` the project version as follows: 36 | 37 | % cd Hermes/ 38 | % agvtool bump -all 39 | Setting version of project Hermes to: 40 | 2041. 41 | 42 | Also setting CFBundleVersion key (assuming it exists) 43 | 44 | Updating CFBundleVersion in Info.plist(s)... 45 | 46 | Updated CFBundleVersion in "Hermes.xcodeproj/../Resources/Hermes-Info.plist" to 2041 47 | 48 | ## Making a private release 49 | 50 | % cd Hermes/ 51 | % make archive 52 | [...] 53 | >>> INFO: Building archive [...]/Hermes/build/Release/Hermes-1.2.7.zip 54 | 55 | ** BUILD SUCCEEDED ** 56 | 57 | The Hermes zip file is then ready for distribution. 58 | 59 | ## Making a beta release 60 | 61 | First, make sure you've committed and pushed all your intended changes to GitHub. 62 | 63 | Create an archive (`make archive` as above) and *test it* before proceeding. Fix, commit, push and repeat until you're satisfied. 64 | 65 | Pick a marketing version for your beta release and use `agvtool` to set it (in addition to `bump`ing the project version as discussed above). 66 | 67 | % cd Hermes/ 68 | % agvtool mvers -terse1 69 | 1.2.7 70 | % agvtool new-marketing-version 1.2.8b1 71 | Setting CFBundleShortVersionString of project Hermes to: 72 | 1.2.8b1. 73 | 74 | Updating CFBundleShortVersionString in Info.plist(s)... 75 | 76 | Updated CFBundleShortVersionString in "Hermes.xcodeproj/../Resources/Hermes-Info.plist" to 1.2.8b1 77 | 78 | Next, make sure the [changelog](https://github.com/HermesApp/Hermes/blob/master/CHANGELOG.md) is up to date. Create a section for the next non-beta version if needed, include "unreleased" for the release date, link to the Git history between the last `v` tag and `HEAD` and document the meaningful changes (see an example [here](https://raw.githubusercontent.com/HermesApp/Hermes/308d81f7b16540742e6398371cde38e46b14755f/CHANGELOG.md)). 79 | 80 | Now you're ready to create and upload the release. You'll need your GitHub access token. 81 | 82 | % make upload-release GITHUB_ACCESS_TOKEN=[...] 83 | [...] 84 | >>> INFO: Building archive [...]/Hermes/build/Release/Hermes-1.2.8b1.zip 85 | >>> INFO: Not signing for Sparkle distribution for prerelease version 86 | >>> INFO: Not building versions.xml fragment for prerelease version 87 | >>> INFO: Not updating website for prerelease version 88 | >>> INFO: Creating release for 1.2.8b1 89 | [...] 90 | >>> INFO: Uploading Hermes-1.2.8b1.zip to GitHub 91 | [...] 92 | 93 | ** BUILD SUCCEEDED ** 94 | 95 | Your Web browser will open to a draft release (unpublished, with no corresponding Git tag yet). Try out the download, make sure it's working and so forth. If it doesn't work, click **Delete**, make changes and commit as usual. 96 | 97 | Once you're convinced the download works, make and push a commit to mark the release. 98 | 99 | % git commit -am v$(agvtool mvers -terse1) 100 | [master 0f6078f] v1.2.8b1 101 | [...] 102 | % git push 103 | [...] 104 | 105 | At this point, `master` on GitHub should contain the bits you want to release. Now, back in your browser, click **Edit draft** then **Publish release**. This will create a Git tag pointing at `master` named the same as the release — `v` followed by the marketing version. 106 | 107 | Finally, pull your newly created tag from GitHub with `git pull -t`. 108 | 109 | ## Making a final release 110 | 111 | Then follow the instructions above under **Making a beta release** to commit and push your changes, make and test an archive, use `agvtool` to bump the project version and set the marketing version, and upload your release. When editing the [changelog](https://github.com/HermesApp/Hermes/blob/master/CHANGELOG.md), create a section for the version you're about to release if it doesn't already exist, link to the Git history between `v` tags, and add today's date. 112 | 113 | % make upload-release GITHUB_ACCESS_TOKEN=[...] 114 | [...] 115 | >>> INFO: Building archive [...]/Hermes/build/Release/Hermes-1.2.8.zip 116 | >>> INFO: Signing for Sparkle distribution 117 | >>> INFO: Building versions.xml fragment 118 | >>> INFO: Updating website in [...]/hermes-pages 119 | + ruby [...]/hermes-pages/_config/release.rb 1.2.8 [...]/Hermes/build/Release/versions.xml [...]/Hermes/CHANGELOG.md 10.10 120 | ==>>> INFO: Updating Hermes release information in release.yml to 1.2.8 121 | ==>>> INFO: Injecting new xml fragment ([...]/Hermes/build/Release/versions.xml) into [...]/hermes-pages/versions.xml 122 | ==>>> INFO: Rendering changelog ([...]/Hermes/CHANGELOG.md -> [...]/hermes-pages/changelog.html) 123 | + set +x 124 | >>> INFO: Creating release for 1.2.8 125 | [...] 126 | >>> INFO: Uploading Hermes-1.2.8.zip to GitHub 127 | [...] 128 | 129 | As you see above, this will also update your local copy of the Sparkle appcast and the public Web site, but not make any changes yet. 130 | 131 | Finally, `cd` into the `hermes-pages` repository, commit and push to update the Web site. 132 | -------------------------------------------------------------------------------- /Resources/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1348\cocoasubrtf170 2 | \cocoascreenfonts1{\fonttbl\f0\fnil\fcharset0 LucidaGrande;} 3 | {\colortbl;\red255\green255\blue255;} 4 | \margl1440\margr1440\vieww9000\viewh8400\viewkind0 5 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 6 | 7 | \f0\b\fs22 \cf0 Written by 8 | \b0 \ 9 | {\field{\*\fldinst{HYPERLINK "https://github.com/alexcrichton"}}{\fldrslt Alex Crichton}}\ 10 | {\field{\*\fldinst{HYPERLINK "https://github.com/nriley"}}{\fldrslt Nicholas Riley}}\ 11 | {\field{\*\fldinst{HYPERLINK "https://github.com/winny-"}}{\fldrslt Winston Weinert}}\ 12 | and other {\field{\*\fldinst{HYPERLINK "https://github.com/HermesApp/Hermes/graphs/contributors"}}{\fldrslt contributors}}\ 13 | \ 14 | {\field{\*\fldinst{HYPERLINK "http://hermesapp.org/"}}{\fldrslt hermesapp.org}}} -------------------------------------------------------------------------------- /Resources/English.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Resources/Hermes-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleDisplayName 8 | Hermes 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIconFile 12 | pandora.icns 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | ${PRODUCT_NAME} 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 1.3.2d1 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | 2059 27 | LSApplicationCategoryType 28 | public.app-category.music 29 | LSMinimumSystemVersion 30 | ${MACOSX_DEPLOYMENT_TARGET} 31 | LSUIElement 32 | 33 | NSAppTransportSecurity 34 | 35 | NSAllowsArbitraryLoads 36 | 37 | 38 | NSAppleScriptEnabled 39 | 40 | NSHumanReadableCopyright 41 | © 2011–2017 Hermes contributors 42 | NSMainNibFile 43 | MainMenu 44 | NSPrincipalClass 45 | HermesApp 46 | NSSupportsAutomaticGraphicsSwitching 47 | 48 | OSAScriptingDefinition 49 | Hermes.sdef 50 | SUAllowsAutomaticUpdates 51 | 52 | SUEnableAutomaticChecks 53 | 54 | SUFeedURL 55 | https://raw.githubusercontent.com/HermesApp/HermesApp.github.io/master/versions.xml 56 | SUPublicDSAKeyFile 57 | dsa_pub.pem 58 | SUShowReleaseNotes 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /Resources/Hermes.sdef: -------------------------------------------------------------------------------- 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 | 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 | -------------------------------------------------------------------------------- /Resources/Icons/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Blake Stephens 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Resources/Icons/Pandora-Menu-Dark-Pause.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Resources/Icons/Pandora-Menu-Dark-Pause.pdf -------------------------------------------------------------------------------- /Resources/Icons/Pandora-Menu-Dark-Play.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Resources/Icons/Pandora-Menu-Dark-Play.pdf -------------------------------------------------------------------------------- /Resources/Icons/cd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Resources/Icons/cd.png -------------------------------------------------------------------------------- /Resources/Icons/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Resources/Icons/delete.png -------------------------------------------------------------------------------- /Resources/Icons/error_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Resources/Icons/error_icon.png -------------------------------------------------------------------------------- /Resources/Icons/fast_forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Resources/Icons/fast_forward.png -------------------------------------------------------------------------------- /Resources/Icons/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Resources/Icons/history.png -------------------------------------------------------------------------------- /Resources/Icons/missing-album.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Resources/Icons/missing-album.png -------------------------------------------------------------------------------- /Resources/Icons/music-note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Resources/Icons/music-note.png -------------------------------------------------------------------------------- /Resources/Icons/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Resources/Icons/pause.png -------------------------------------------------------------------------------- /Resources/Icons/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Resources/Icons/play.png -------------------------------------------------------------------------------- /Resources/Icons/radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Resources/Icons/radio.png -------------------------------------------------------------------------------- /Resources/Icons/thumbdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Resources/Icons/thumbdown.png -------------------------------------------------------------------------------- /Resources/Icons/thumbup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Resources/Icons/thumbup.png -------------------------------------------------------------------------------- /Resources/Icons/volume_down.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Resources/Icons/volume_down.icns -------------------------------------------------------------------------------- /Resources/Icons/volume_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Resources/Icons/volume_up.png -------------------------------------------------------------------------------- /Resources/Icons/zzs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Resources/Icons/zzs.png -------------------------------------------------------------------------------- /Resources/dsa_pub.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIDOzCCAi0GByqGSM44BAEwggIgAoIBAQCuqjewVLrVCTzE26R2ZuyW1eNmCh+O 3 | FshknH01pa0WCPMwLc/IrtaAZUjDcuNXDXjKYJCxEGHEa8+gMCgwhS3O2scCE9jJ 4 | zZzEKjvTHvQOP7VTigHNFJqBlBkW26iJcQ+majDRG43VryLZccaLwdqkyzgEUtRI 5 | 4wTecaZ54+A7TVCUlrFozZQtLT2dhCyiwSVQkH/QtlfoN1eYfY+XfMeAPHu3etNj 6 | ldZTzxhA5/brp1/san8mGnLvWRksYTRTzvQ8ei39aQ8SVQoPQPITaSq6lw4iN9zz 7 | VGq+olhVr7OFU3Ck0MbxWZevbutMHwxZHfmbNpNhWTM7tsbxXkd7iCgZAhUAsmCG 8 | vDf3I40fT7cDUHdEW1ThjTcCggEAGvpA1+BGiAzFlrYuGwkbsod/hqogHxfx496V 9 | 1AkHbrW3k+l1SOQ3H7zeJs7TX5ZZCVH+9jZFrzokjYbVnN7sAfaA55nAxbOuHMQd 10 | wugW8ZctR6s125unE4PMYQfHEN4KJusoz4ggC10s6NrkpXbJ1NPQxLmvGcUTH7vL 11 | aCeCIjJWDZRrVXYWCBorCAgIv1lnxMW/vSeCEqyf/89+CHIa34JGq3zDx9EUSVoT 12 | MUDf3PGTlbrvidNxrxUl5bHpbuTDlim0IJO+6sCladk4G6suc0Z6oorX25yfqOU+ 13 | 02WvTAwg8jrhZxKjQH1fbApvSJUeqqlRTGoAyDjDKcOo/PoiaQOCAQYAAoIBAQCH 14 | g56eaMmxjpQD0J9JxWv10HvQYrN+LNF0U8J+yKoBzq74Y13CQzF7/nzSGT1U7nxk 15 | 6ExsEGlTAZlRpqF2qLwEsKYEB1vshb99/brK2BlAH7W40krGtaAN3YdC+HvD+I5w 16 | pnIYBOzR5Zmpr+rxC2z0B3zkUzY9A4ct5Q8ithiVi12yy+QqefSWNXJv6iCpLVtN 17 | jtQmnOZ5rKlAfuGj5ZZgR9WIS4Vaj+UeP7qPncp6EFiUBsE/xAnWfRmlZL+9hN5Y 18 | hyZan9f6yirTr9t0rT2ZXnDej6IcHyLpuinoLZ9OoA10K9ZsCRWjmjePjl+NXQb/ 19 | X04Vkel1JToEpP3d8UM4 20 | -----END PUBLIC KEY----- 21 | -------------------------------------------------------------------------------- /Resources/pandora.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HermesApp/Hermes/78f8bf498e09411299b31a994f3c9fbb3bb4d3b4/Resources/pandora.icns -------------------------------------------------------------------------------- /Scripts/archive.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | . "$(dirname "$0")/releaselib.sh" 6 | 7 | check_environment 8 | set_environment 9 | 10 | build_archive 11 | -------------------------------------------------------------------------------- /Scripts/releaselib.sh: -------------------------------------------------------------------------------- 1 | # releaselib.sh 2 | # 3 | # Tools used to archive and release Hermes. 4 | 5 | mytput() { 6 | if tput colors >/dev/null 2>&1 && [ "$(tput colors)" -ge 8 ]; then 7 | tput "$@" 8 | fi 9 | } 10 | 11 | information() { 12 | mytput setaf 2 13 | printf '>>> INFO: ' 14 | mytput sgr0 15 | printf '%s\n' "$*" 16 | } 17 | 18 | error() { 19 | mytput setaf 1 20 | printf '>>> ERROR: ' 21 | mytput sgr0 22 | printf '%s\n' "$*" 23 | exit 1 24 | } 25 | 26 | # Ensure build target is called correctly. 27 | check_environment() { 28 | if [ "$ACTION" = "clean" ]; then 29 | exit 0 30 | fi 31 | if [ "$CONFIGURATION" != "Release" ]; then 32 | error Distribution target requires "'Release'" build style 33 | fi 34 | } 35 | 36 | # Set up environment for the rest of the functions 37 | set_environment() { 38 | SCRIPTS_DIR="$PROJECT_DIR/Scripts" 39 | APPLICATION="$BUILT_PRODUCTS_DIR/$PROJECT_NAME.app" 40 | 41 | VERSION=$(cd "$PROJECT_DIR"; agvtool mvers -terse1) 42 | VERSION_IS_PRERELEASE=$([[ $VERSION =~ [^0-9.] ]] && echo true || echo false) 43 | INT_VERSION=$(cd "$PROJECT_DIR"; agvtool vers -terse) 44 | ARCHIVE_FILENAME="$PROJECT_NAME-$VERSION.zip" 45 | 46 | HERMES_PAGES="$(dirname $SOURCE_ROOT)/hermes-pages" 47 | DOWNLOAD_URL="https://github.com/HermesApp/Hermes/releases/download/v${VERSION}/${ARCHIVE_FILENAME}" 48 | RELEASENOTES_URL="http://hermesapp.org/changelog.html" 49 | } 50 | 51 | build_archive() { 52 | information "Building archive $BUILT_PRODUCTS_DIR/$ARCHIVE_FILENAME" 53 | cd "$BUILT_PRODUCTS_DIR" 54 | rm -f "$PROJECT_NAME"*.zip 55 | ditto -ck --keepParent "$BUILT_PRODUCTS_DIR/$PROJECT_NAME.app" "$ARCHIVE_FILENAME" 56 | SIZE=$(stat -f %z "$ARCHIVE_FILENAME") 57 | PUBDATE=$(LC_TIME=en_US date +"%a, %d %b %G %T %z") 58 | } 59 | 60 | # This also verifies the signature. 61 | sign_and_verify() { 62 | if [[ $VERSION_IS_PRERELEASE == true ]]; then 63 | information "Not signing for Sparkle distribution for prerelease version" 64 | return 65 | fi 66 | information "Signing for Sparkle distribution" 67 | cd "$BUILT_PRODUCTS_DIR" 68 | SIGNATURE=$("$SCRIPTS_DIR/sign_sparkle_release.sh" "$PROJECT_DIR/../hermes.key" "$ARCHIVE_FILENAME") 69 | if [ "$SIGNATURE" = '' ]; then 70 | error 'Signing failed. Aborting.' 71 | fi 72 | if ! "$SCRIPTS_DIR/verify_sparkle_signature.sh" "$PROJECT_DIR/Resources/dsa_pub.pem" \ 73 | "$SIGNATURE" "$ARCHIVE_FILENAME" >/dev/null 74 | then 75 | 76 | error 'Sparkle DSA signature verification FAILED. Aborting.' 77 | fi 78 | } 79 | 80 | check_code_signature() { 81 | codesign --verify --verbose=4 "$APPLICATION" || error 'codesign failed. Aborting.' 82 | spctl -vv --assess "$APPLICATION" || error 'spctl failed. Aborting.' 83 | } 84 | 85 | build_versions_fragment() { 86 | if [[ $VERSION_IS_PRERELEASE == true ]]; then 87 | information "Not building versions.xml fragment for prerelease version" 88 | return 89 | fi 90 | information 'Building versions.xml fragment' 91 | cd "$BUILT_PRODUCTS_DIR" 92 | cat > versions.xml < 95 | Version $VERSION 96 | $RELEASENOTES_URL 97 | $MACOSX_DEPLOYMENT_TARGET 98 | $PUBDATE 99 | 102 | 103 | EOF 104 | } 105 | 106 | update_website() { 107 | if [[ $VERSION_IS_PRERELEASE == true ]]; then 108 | information "Not updating website for prerelease version" 109 | return 110 | fi 111 | information "Updating website in $HERMES_PAGES" 112 | cd "$BUILT_PRODUCTS_DIR" 113 | # Log the command 114 | set -x 115 | ruby $HERMES_PAGES/_config/release.rb \ 116 | "$VERSION" \ 117 | "$PWD/versions.xml" \ 118 | "$PROJECT_DIR/CHANGELOG.md" \ 119 | "$MACOSX_DEPLOYMENT_TARGET" 120 | # Stop logging 121 | set +x 122 | } 123 | 124 | upload_release() { 125 | local releases_url release_json html_url upload_url 126 | information "Creating release for $VERSION" 127 | releases_url='https://api.github.com/repos/HermesApp/Hermes/releases?access_token='$GITHUB_ACCESS_TOKEN 128 | release_json=$(curl --data @- "$releases_url" < "$DECODED_SIGNATURE_FILE" 34 | "$OPENSSL" dgst -sha1 -binary < "$ZIPFILE" > "$ZIPFILE_SHA1_FILE" 35 | "$OPENSSL" dgst -dss1 -verify "$DSA_PUBKEY" -signature "$DECODED_SIGNATURE_FILE" "$ZIPFILE_SHA1_FILE" 36 | 37 | rm -f "$DECODED_SIGNATURE_FILE" "$ZIPFILE_SHA1_FILE" 38 | -------------------------------------------------------------------------------- /Sources/AudioStreamer/ASPlaylist.h: -------------------------------------------------------------------------------- 1 | // 2 | // ASPlaylist.h 3 | // AudioStreamer 4 | // 5 | // Created by Alex Crichton on 8/21/12. 6 | // 7 | // 8 | 9 | #import "AudioStreamer.h" 10 | 11 | extern NSString * const ASNewSongPlaying; 12 | extern NSString * const ASNoSongsLeft; 13 | extern NSString * const ASRunningOutOfSongs; 14 | extern NSString * const ASCreatedNewStream; 15 | extern NSString * const ASStreamError; 16 | extern NSString * const ASAttemptingNewSong; 17 | extern NSString * const ASBitrateReadyNotification; 18 | 19 | /** 20 | * The ASPlaylist class is intended to be a wrapper around the AudioStreamer 21 | * class for a more robust interface if one is desired. It also manages a queue 22 | * of songs to play and automatically switches from one song to the next when 23 | * playback finishes. 24 | */ 25 | @interface ASPlaylist : NSObject { 26 | BOOL retrying; /* Are we retrying the current url? */ 27 | BOOL nexting; /* Are we in the middle of nexting? */ 28 | BOOL stopping; /* Are we in the middle of stopping? */ 29 | BOOL volumeSet; /* TRUE if the volume has been set on the stream */ 30 | double lastKnownSeekTime; /* time to seek to */ 31 | double volume; /* volume for all streams on this playlist */ 32 | 33 | NSInteger tries; /* # of retry attempts */ 34 | NSMutableArray *urls; /* list of URLs to play */ 35 | AudioStreamer *stream; /* stream that is playing */ 36 | } 37 | 38 | /** 39 | * The currently playing URL. 40 | * 41 | * This is nil of no url has ever been playing. 42 | */ 43 | @property NSURL *playing; 44 | 45 | /** @name Managing the playlist */ 46 | 47 | /** 48 | * Start playing songs on the playlist, or resume playback. 49 | * 50 | * This will send out notifications for more songs if we're running low on songs 51 | * or are out of songs completely to play. 52 | */ 53 | - (void) play; 54 | 55 | /** 56 | * Pause playback on the playlist. 57 | * 58 | * This has no effect if the playlist is already paused or wasn't playing a song 59 | */ 60 | - (void) pause; 61 | 62 | /** 63 | * Stops playing the current song and forgets about it. 64 | * 65 | * The song is stopped and internally all state about the song is thrown away 66 | */ 67 | - (void) stop; 68 | 69 | /** 70 | * Goes to the next song in the playlist 71 | * 72 | * This can trigger notifications about songs running low or associated events. 73 | */ 74 | - (void) next; 75 | 76 | /** @name Interface to AudioStreamer */ 77 | - (BOOL) isPaused; 78 | - (BOOL) isPlaying; 79 | - (BOOL) isIdle; 80 | - (BOOL) isError; 81 | - (void) setVolume:(double)volume; 82 | - (BOOL) duration:(double *)ret; 83 | - (BOOL) progress:(double *)ret; 84 | 85 | /** @name Miscellaneous */ 86 | 87 | /** 88 | * If the stream has stopped for a network error, this retries playing the 89 | * stream 90 | */ 91 | - (void) retry; 92 | 93 | /** 94 | * Removes all songs from the internal list of songs. This does not trigger 95 | * notifications about songs running low. 96 | */ 97 | - (void) clearSongList; 98 | 99 | /** 100 | * Adds a new song to the playlist, optionally starting playback. 101 | */ 102 | - (void) addSong:(NSURL*)url play:(BOOL)play; 103 | 104 | @end 105 | -------------------------------------------------------------------------------- /Sources/AudioStreamer/ASPlaylist.m: -------------------------------------------------------------------------------- 1 | // 2 | // ASPlaylist.m 3 | // AudioStreamer 4 | // 5 | // Created by Alex Crichton on 8/21/12. 6 | // 7 | // 8 | 9 | #import "ASPlaylist.h" 10 | 11 | NSString * const ASCreatedNewStream = @"ASCreatedNewStream"; 12 | NSString * const ASNewSongPlaying = @"ASNewSongPlaying"; 13 | NSString * const ASNoSongsLeft = @"ASNoSongsLeft"; 14 | NSString * const ASRunningOutOfSongs = @"ASRunningOutOfSongs"; 15 | NSString * const ASStreamError = @"ASStreamError"; 16 | NSString * const ASAttemptingNewSong = @"ASAttemptingNewSong"; 17 | 18 | @implementation ASPlaylist 19 | 20 | - (id)init { 21 | if (!(self = [super init])) return nil; 22 | urls = [NSMutableArray arrayWithCapacity:10]; 23 | return self; 24 | } 25 | 26 | - (void)dealloc { 27 | [self stop]; 28 | } 29 | 30 | - (void)clearSongList { 31 | [urls removeAllObjects]; 32 | } 33 | 34 | - (void)addSong:(NSURL *)url play:(BOOL)play { 35 | [urls addObject:url]; 36 | 37 | if (play && ![stream isPlaying]) { 38 | [self play]; 39 | } 40 | } 41 | 42 | - (void)setAudioStream { 43 | if (stream != nil) { 44 | [[NSNotificationCenter defaultCenter] 45 | removeObserver:self 46 | name:nil 47 | object:stream]; 48 | [stream stop]; 49 | } 50 | stream = [AudioStreamer streamWithURL: _playing]; 51 | [[NSNotificationCenter defaultCenter] 52 | postNotificationName:ASCreatedNewStream 53 | object:self 54 | userInfo:@{@"stream": stream}]; 55 | volumeSet = [stream setVolume:volume]; 56 | 57 | /* Watch for error notifications */ 58 | [[NSNotificationCenter defaultCenter] 59 | addObserver:self 60 | selector:@selector(playbackStateChanged:) 61 | name:ASStatusChangedNotification 62 | object:stream]; 63 | [[NSNotificationCenter defaultCenter] 64 | addObserver:self 65 | selector:@selector(bitrateReady:) 66 | name:ASBitrateReadyNotification 67 | object:stream]; 68 | } 69 | 70 | - (void)bitrateReady: (NSNotification*)notification { 71 | NSAssert([notification object] == stream, 72 | @"Should only receive notifications for the current stream"); 73 | [[NSNotificationCenter defaultCenter] 74 | postNotificationName:ASNewSongPlaying 75 | object:self 76 | userInfo:@{@"url": _playing}]; 77 | NSLogd(@"%@", stream); 78 | if (lastKnownSeekTime == 0) 79 | return; 80 | if (![stream seekToTime:lastKnownSeekTime]) 81 | return; 82 | retrying = NO; 83 | lastKnownSeekTime = 0; 84 | } 85 | 86 | - (void)playbackStateChanged: (NSNotification *)notification { 87 | NSAssert([notification object] == stream, 88 | @"Should only receive notifications for the current stream"); 89 | if (!volumeSet) { 90 | volumeSet = [stream setVolume:volume]; 91 | } 92 | 93 | int code = [stream errorCode]; 94 | if (stopping) { 95 | return; 96 | } else if (code != 0) { 97 | /* If we've hit an error, then we want to record out current progress into 98 | the song. Only do this if we're not in the process of retrying to 99 | establish a connection, so that way we don't blow away the original 100 | progress from when the error first happened */ 101 | if (!retrying) { 102 | if (![stream progress:&lastKnownSeekTime]) { 103 | lastKnownSeekTime = 0; 104 | } 105 | } 106 | 107 | /* If the network connection just outright failed, then we shouldn't be 108 | retrying with a new auth token because it will never work for that 109 | reason. Most likely this is some network trouble and we should have the 110 | opportunity to hit a button to retry this specific connection so we can 111 | at least hope to regain our current place in the song */ 112 | if (code == AS_NETWORK_CONNECTION_FAILED || code == AS_TIMED_OUT) { 113 | [[NSNotificationCenter defaultCenter] 114 | postNotificationName:ASStreamError 115 | object:self]; 116 | 117 | /* Otherwise, this might be because our authentication token is invalid, but 118 | just in case, retry the current song automatically a few times before we 119 | finally give up and clear our cache of urls (see below) */ 120 | } else { 121 | [self performSelector:@selector(retry) withObject:nil afterDelay:0]; 122 | } 123 | 124 | /* When the stream has finished, move on to the next song */ 125 | } else if ([stream isDone]) { 126 | [self performSelectorOnMainThread:@selector(next) 127 | withObject:nil 128 | waitUntilDone:NO]; 129 | } 130 | } 131 | 132 | - (void)retry { 133 | if (tries > 2) { 134 | /* too many retries means just skip to the next song */ 135 | [self clearSongList]; 136 | [self next]; 137 | return; 138 | } 139 | tries++; 140 | retrying = YES; 141 | [self setAudioStream]; 142 | [stream start]; 143 | } 144 | 145 | - (void)play { 146 | if (stream) { 147 | [stream play]; 148 | return; 149 | } 150 | 151 | if ([urls count] == 0) { 152 | [[NSNotificationCenter defaultCenter] 153 | postNotificationName:ASNoSongsLeft 154 | object:self]; 155 | return; 156 | } 157 | 158 | _playing = urls[0]; 159 | [urls removeObjectAtIndex:0]; 160 | [self setAudioStream]; 161 | tries = 0; 162 | [[NSNotificationCenter defaultCenter] 163 | postNotificationName:ASAttemptingNewSong 164 | object:self]; 165 | [stream start]; 166 | 167 | if ([urls count] < 2) { 168 | [[NSNotificationCenter defaultCenter] 169 | postNotificationName:ASRunningOutOfSongs 170 | object:self]; 171 | } 172 | } 173 | 174 | - (void)pause { 175 | [stream pause]; 176 | } 177 | 178 | - (BOOL)isPaused { 179 | return [stream isPaused]; 180 | } 181 | 182 | - (BOOL)isPlaying { 183 | return [stream isPlaying]; 184 | } 185 | 186 | - (BOOL)isIdle { 187 | return [stream isDone]; 188 | } 189 | 190 | - (BOOL)isError { 191 | return [stream errorCode] != AS_NO_ERROR; 192 | } 193 | 194 | - (BOOL)progress:(double *)ret { 195 | return [stream progress:ret]; 196 | } 197 | 198 | - (BOOL)duration:(double *)ret { 199 | return [stream duration:ret]; 200 | } 201 | 202 | - (void)next { 203 | assert(!nexting); 204 | nexting = YES; 205 | lastKnownSeekTime = 0; 206 | retrying = FALSE; 207 | [self stop]; 208 | [self play]; 209 | nexting = NO; 210 | } 211 | 212 | - (void)stop { 213 | assert(!stopping); 214 | stopping = YES; 215 | [stream stop]; 216 | if (stream != nil) { 217 | [[NSNotificationCenter defaultCenter] 218 | removeObserver:self 219 | name:nil 220 | object:stream]; 221 | } 222 | stream = nil; 223 | _playing = nil; 224 | stopping = NO; 225 | } 226 | 227 | - (void)setVolume:(double)vol { 228 | volumeSet = [stream setVolume:vol]; 229 | self->volume = vol; 230 | } 231 | 232 | @end 233 | -------------------------------------------------------------------------------- /Sources/Controllers/AuthController.h: -------------------------------------------------------------------------------- 1 | @interface AuthController : NSObject { 2 | IBOutlet NSView *view; 3 | 4 | // Fields of the authentication view 5 | IBOutlet NSButton *login; 6 | IBOutlet NSProgressIndicator *spinner; 7 | IBOutlet NSImageView *error; 8 | IBOutlet NSTextField *username; 9 | IBOutlet NSSecureTextField *password; 10 | IBOutlet NSTextField *errorText; 11 | } 12 | 13 | - (IBAction) authenticate: (id)sender; 14 | - (IBAction) logout: (id) sender; 15 | - (void) authenticationFailed:(NSNotification*) notification 16 | error:(NSString*)err; 17 | - (void) show; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /Sources/Controllers/AuthController.m: -------------------------------------------------------------------------------- 1 | #import "AuthController.h" 2 | #import "PlaybackController.h" 3 | #import "StationsController.h" 4 | #import "Notifications.h" 5 | 6 | #define ROUGH_EMAIL_REGEX @"[^\\s@]+@[^\\s@]+\\.[^\\s@]+" 7 | 8 | @implementation AuthController 9 | 10 | - (id) init { 11 | NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; 12 | 13 | [notificationCenter 14 | addObserver:self 15 | selector:@selector(authenticationSucceeded:) 16 | name:PandoraDidAuthenticateNotification 17 | object:nil]; 18 | 19 | [notificationCenter 20 | addObserver:self 21 | selector:@selector(controlTextDidChange:) 22 | name:NSControlTextDidChangeNotification 23 | object:username]; 24 | 25 | [notificationCenter 26 | addObserver:self 27 | selector:@selector(controlTextDidChange:) 28 | name:NSControlTextDidChangeNotification 29 | object:password]; 30 | 31 | return self; 32 | } 33 | 34 | - (void) authenticationFailed: (NSNotification*) notification 35 | error: (NSString*) err { 36 | [spinner setHidden:YES]; 37 | [spinner stopAnimation:nil]; 38 | [self show]; 39 | [error setHidden:NO]; 40 | [errorText setHidden:NO]; 41 | [errorText setStringValue:err]; 42 | if ([username stringValue] == nil || [[username stringValue] isEqual:@""]) { 43 | [username becomeFirstResponder]; 44 | } else { 45 | [password becomeFirstResponder]; 46 | } 47 | NSNotification *emptyNotification; 48 | [self controlTextDidChange:emptyNotification]; 49 | } 50 | 51 | - (void) authenticationSucceeded: (NSNotification*) notification { 52 | [spinner setHidden:YES]; 53 | [spinner stopAnimation:nil]; 54 | 55 | HermesAppDelegate *delegate = HMSAppDelegate; 56 | if (![[username stringValue] isEqualToString:@""]) { 57 | [delegate saveUsername:[username stringValue] password:[password stringValue]]; 58 | } 59 | 60 | [[delegate stations] show]; 61 | [PlaybackController setPlayOnStart:YES]; 62 | } 63 | 64 | /* Login button in sheet hit, should authenticate */ 65 | - (IBAction) authenticate: (id) sender { 66 | [error setHidden: YES]; 67 | [errorText setHidden: YES]; 68 | [spinner setHidden:NO]; 69 | [spinner startAnimation: sender]; 70 | 71 | [[HMSAppDelegate pandora] authenticate:[username stringValue] 72 | password:[password stringValue] 73 | request:nil]; 74 | [login setEnabled:NO]; 75 | } 76 | 77 | /* Show the authentication view */ 78 | - (void) show { 79 | [HMSAppDelegate setCurrentView:view]; 80 | [username becomeFirstResponder]; 81 | 82 | NSNotification *emptyNotification; 83 | [self controlTextDidChange:emptyNotification]; 84 | } 85 | 86 | /* Log out the current session */ 87 | - (IBAction) logout: (id) sender { 88 | [password setStringValue:@""]; 89 | HermesAppDelegate *delegate = HMSAppDelegate; 90 | [[delegate pandora] logout]; 91 | } 92 | 93 | - (void)controlTextDidChange:(NSNotification *)obj { 94 | NSPredicate *emailTest = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", ROUGH_EMAIL_REGEX]; 95 | 96 | [login setEnabled: 97 | [spinner isHidden] && 98 | [emailTest evaluateWithObject:[username stringValue]] && 99 | ![[password stringValue] isEqualToString:@""]]; 100 | } 101 | 102 | - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { 103 | HermesAppDelegate *delegate = HMSAppDelegate; 104 | 105 | if (![[delegate pandora] isAuthenticated]) { 106 | return NO; 107 | } 108 | 109 | return YES; 110 | } 111 | 112 | @end 113 | -------------------------------------------------------------------------------- /Sources/Controllers/HistoryController.h: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryController.h 3 | // Hermes 4 | // 5 | // Created by Alex Crichton on 10/9/11. 6 | // 7 | 8 | @class FileReader; 9 | @class Song; 10 | 11 | @interface HistoryController : NSObject { 12 | IBOutlet NSCollectionView *collection; 13 | FileReader *reader; 14 | 15 | IBOutlet NSButton *pandoraSong; 16 | IBOutlet NSButton *pandoraArtist; 17 | IBOutlet NSButton *pandoraAlbum; 18 | IBOutlet NSButton *lyrics; 19 | IBOutlet NSButton *like; 20 | IBOutlet NSButton *dislike; 21 | IBOutlet NSDrawer *drawer; 22 | IBOutlet NSProgressIndicator *spinner; 23 | } 24 | 25 | @property IBOutlet NSMutableArray *songs; 26 | @property IBOutlet NSArrayController *controller; 27 | 28 | - (void) showDrawer; 29 | - (void) hideDrawer; 30 | - (void) focus; 31 | 32 | - (void) addSong: (Song*) song; 33 | - (BOOL) saveSongs; 34 | 35 | - (void) insertObject:(Song *)s inSongsAtIndex:(NSUInteger)index; 36 | - (void) removeObjectFromSongsAtIndex:(NSUInteger)index; 37 | 38 | - (Song*) selectedItem; 39 | - (void) updateUI; 40 | 41 | - (IBAction) likeSelected:(id)sender; 42 | - (IBAction) dislikeSelected:(id)sender; 43 | - (IBAction) gotoArtist:(id)sender; 44 | - (IBAction) gotoSong:(id)sender; 45 | - (IBAction) gotoAlbum:(id)sender; 46 | - (IBAction) showLyrics:(id)sender; 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /Sources/Controllers/HistoryController.m: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryController.m 3 | // Hermes 4 | // 5 | // Created by Alex Crichton on 10/9/11. 6 | // 7 | 8 | #import "HistoryController.h" 9 | #import "FileReader.h" 10 | #import "FMEngine/NSString+FMEngine.h" 11 | #import "PlaybackController.h" 12 | #import "PreferencesController.h" 13 | #import "URLConnection.h" 14 | #import "Notifications.h" 15 | 16 | #define HISTORY_LIMIT 20 17 | 18 | @implementation HistoryController 19 | 20 | @synthesize songs, controller; 21 | 22 | - (void) awakeFromNib { 23 | [super awakeFromNib]; 24 | drawer.contentView.window.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; 25 | } 26 | 27 | - (void) loadSavedSongs { 28 | NSLogd(@"loading saved songs"); 29 | NSString *historySaveStatePath = [HMSAppDelegate stateDirectory:@"history.savestate"]; 30 | if (historySaveStatePath == nil) return; 31 | 32 | reader = [FileReader readerForFile:historySaveStatePath completionHandler:^(NSData *data, NSError *err) { 33 | if (err) return; 34 | assert(data != nil); 35 | 36 | NSArray *s = [NSKeyedUnarchiver unarchiveObjectWithData:data]; 37 | for (Song *song in s) { 38 | if ([self->songs indexOfObject:song] == NSNotFound) 39 | [self->controller addObject:song]; 40 | } 41 | 42 | self->reader = nil; 43 | }]; 44 | [reader start]; 45 | } 46 | 47 | - (void) insertObject:(Song *)s inSongsAtIndex:(NSUInteger)index { 48 | [songs insertObject:s atIndex:index]; 49 | } 50 | 51 | - (void) removeObjectFromSongsAtIndex:(NSUInteger)index { 52 | [songs removeObjectAtIndex:index]; 53 | } 54 | 55 | - (void) addSong:(Song *)song { 56 | if (songs == nil) { 57 | [self loadSavedSongs]; 58 | songs = [NSMutableArray array]; 59 | } 60 | [self insertObject:song inSongsAtIndex:0]; 61 | 62 | [[NSDistributedNotificationCenter defaultCenter] 63 | postNotificationName:HistoryControllerDidPlaySongDistributedNotification 64 | object:@"hermes" 65 | userInfo:[song toDictionary] 66 | deliverImmediately: YES]; 67 | 68 | while ([songs count] > HISTORY_LIMIT) { 69 | [self removeObjectFromSongsAtIndex:HISTORY_LIMIT]; 70 | } 71 | } 72 | 73 | - (BOOL) saveSongs { 74 | NSString *path = [HMSAppDelegate stateDirectory:@"history.savestate"]; 75 | if (path == nil) { 76 | return NO; 77 | } 78 | 79 | return [NSKeyedArchiver archiveRootObject:songs toFile:path]; 80 | } 81 | 82 | - (Song*) selectedItem { 83 | NSUInteger selection = [controller selectionIndex]; 84 | if (selection == NSNotFound) { 85 | return nil; 86 | } 87 | return songs[selection]; 88 | } 89 | 90 | - (Pandora*) pandora { 91 | return [HMSAppDelegate pandora]; 92 | } 93 | 94 | - (void) setEnabledState:(BOOL)enabled allowRating:(BOOL)ratingEnabled { 95 | [pandoraSong setEnabled:enabled]; 96 | [pandoraArtist setEnabled:enabled]; 97 | [pandoraAlbum setEnabled:enabled]; 98 | [lyrics setEnabled:enabled]; 99 | [like setEnabled:ratingEnabled]; 100 | [dislike setEnabled:ratingEnabled]; 101 | } 102 | 103 | - (void) updateUI { 104 | Song *song = [self selectedItem]; 105 | int rating = 0; 106 | if (song && [[song station] shared]) { 107 | [self setEnabledState:YES allowRating:NO]; 108 | } 109 | else if (song) { 110 | [self setEnabledState:YES allowRating:YES]; 111 | rating = [[song nrating] intValue]; 112 | } 113 | else { 114 | [self setEnabledState:NO allowRating:NO]; 115 | } 116 | 117 | if (rating == -1) { 118 | [like setState:NSOffState]; 119 | [dislike setState:NSOnState]; 120 | } 121 | else if (rating == 0) { 122 | [like setState:NSOffState]; 123 | [dislike setState:NSOffState]; 124 | } 125 | else if (rating == 1) { 126 | [like setState:NSOnState]; 127 | [dislike setState:NSOffState]; 128 | } 129 | } 130 | 131 | - (IBAction) dislikeSelected:(id)sender { 132 | Song* song = [self selectedItem]; 133 | if (!song) return; 134 | [[HMSAppDelegate playback] rate:song as:NO]; 135 | } 136 | 137 | - (IBAction) likeSelected:(id)sender { 138 | Song* song = [self selectedItem]; 139 | if (!song) return; 140 | [[HMSAppDelegate playback] rate:song as:YES]; 141 | } 142 | 143 | - (IBAction)gotoSong:(id)sender { 144 | Song* s = [self selectedItem]; 145 | if (s == nil) return; 146 | NSURL *url = [NSURL URLWithString:[s titleUrl]]; 147 | [[NSWorkspace sharedWorkspace] openURL:url]; 148 | } 149 | 150 | - (IBAction)gotoArtist:(id)sender { 151 | Song* s = [self selectedItem]; 152 | if (s == nil) return; 153 | NSURL *url = [NSURL URLWithString:[s artistUrl]]; 154 | [[NSWorkspace sharedWorkspace] openURL:url]; 155 | } 156 | 157 | - (IBAction)gotoAlbum:(id)sender { 158 | Song* s = [self selectedItem]; 159 | if (s == nil) return; 160 | NSURL *url = [NSURL URLWithString:[s albumUrl]]; 161 | [[NSWorkspace sharedWorkspace] openURL:url]; 162 | } 163 | 164 | - (NSSize) drawerWillResizeContents:(NSDrawer*) drawer toSize:(NSSize) size { 165 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 166 | [defaults setInteger:size.width forKey:HIST_DRAWER_WIDTH]; 167 | return size; 168 | } 169 | 170 | - (void)drawerWillClose:(NSNotification *)notification { 171 | PREF_KEY_SET_INT(OPEN_DRAWER, DRAWER_NONE_HIST); 172 | } 173 | 174 | - (void) showDrawer { 175 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 176 | NSSize s; 177 | s.height = 100; 178 | s.width = [defaults integerForKey:HIST_DRAWER_WIDTH]; 179 | 180 | [drawer open]; 181 | [drawer setContentSize:s]; 182 | [collection setMaxItemSize:NSMakeSize(227, 41)]; 183 | [collection setMinItemSize:NSMakeSize(40, 41)]; 184 | [self focus]; 185 | } 186 | 187 | - (void) hideDrawer { 188 | [drawer close]; 189 | } 190 | 191 | - (void) focus { 192 | [[drawer parentWindow] makeFirstResponder:collection]; 193 | } 194 | 195 | - (IBAction) showLyrics:(id)sender { 196 | Song* s = [self selectedItem]; 197 | if (s == nil) return; 198 | NSString *surl = 199 | [NSString 200 | stringWithFormat:@"http://lyrics.wikia.com/api.php?action=lyrics&artist=%@&song=%@&fmt=realjson", 201 | [[s artist] urlEncoded], [[s title] urlEncoded]]; 202 | NSURL *url = [NSURL URLWithString:surl]; 203 | NSURLRequest *req = [NSURLRequest requestWithURL:url]; 204 | NSLogd(@"Fetch: %@", surl); 205 | URLConnection *conn = [URLConnection connectionForRequest:req 206 | completionHandler:^(NSData *d, NSError *err) { 207 | if (err == nil) { 208 | NSDictionary *object = [NSJSONSerialization JSONObjectWithData:d options:0 error:&err]; 209 | if (err == nil) { 210 | NSString *url = object[@"url"]; 211 | [self->spinner setHidden:YES]; 212 | [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]]; 213 | return; 214 | } 215 | } 216 | NSAlert *alert = [NSAlert alertWithError:err]; 217 | alert.messageText = @"Couldn't open lyrics"; 218 | alert.informativeText = [err localizedDescription]; 219 | [alert beginSheetModalForWindow:[HMSAppDelegate window] completionHandler:nil]; 220 | }]; 221 | 222 | [conn setHermesProxy]; 223 | [conn start]; 224 | [spinner setHidden:NO]; 225 | } 226 | 227 | @end 228 | -------------------------------------------------------------------------------- /Sources/Controllers/PlaybackController.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import "Pandora/Station.h" 5 | #import "Integration/Scrobbler.h" 6 | 7 | @class Song; 8 | @class MPRemoteCommandCenter; 9 | 10 | // XXX macOS 10.12.2 exposes media keys; 10.12.3 doesn't 11 | #define MPREMOTECOMMANDCENTER_MEDIA_KEYS_BROKEN 1 12 | 13 | @interface PlaybackController : NSObject { 14 | IBOutlet NSProgressIndicator *songLoadingProgress; 15 | 16 | IBOutlet NSView *playbackView; 17 | 18 | // Song view items 19 | IBOutlet NSTextField *songLabel; 20 | IBOutlet NSTextField *artistLabel; 21 | IBOutlet NSTextField *albumLabel; 22 | IBOutlet NSTextField *progressLabel; 23 | IBOutlet NSButton *art; 24 | IBOutlet NSSlider *playbackProgress; 25 | IBOutlet NSProgressIndicator *artLoading; 26 | 27 | // Playback related items 28 | IBOutlet NSToolbarItem *like; 29 | IBOutlet NSToolbarItem *dislike; 30 | IBOutlet NSToolbarItem *playpause; 31 | IBOutlet NSToolbarItem *nextSong; 32 | IBOutlet NSToolbarItem *tiredOfSong; 33 | IBOutlet NSSlider *volume; 34 | IBOutlet NSToolbar *toolbar; 35 | 36 | NSTimer *progressUpdateTimer; 37 | BOOL scrobbleSent; 38 | NSString *lastImgSrc; 39 | NSData *lastImg; 40 | } 41 | 42 | @property (readonly) Station *playing; 43 | @property (readonly) NSData *lastImg; 44 | @property (nonatomic, retain) NSImage *artImage; 45 | @property BOOL pausedByScreensaver; 46 | @property BOOL pausedByScreenLock; 47 | 48 | @property (readonly) MPRemoteCommandCenter *remoteCommandCenter; 49 | @property (readonly) SPMediaKeyTap *mediaKeyTap; 50 | 51 | + (void) setPlayOnStart: (BOOL)play; 52 | + (BOOL) playOnStart; 53 | 54 | //- (void) applicationOpened; 55 | 56 | - (void) reset; 57 | - (void) playStation: (Station*) station; 58 | - (BOOL) saveState; 59 | - (void) show; 60 | - (void) prepareFirst; 61 | 62 | - (BOOL) play; 63 | - (BOOL) pause; 64 | - (void) stop; 65 | - (void) setIntegerVolume: (NSInteger) volume; 66 | - (NSInteger) integerVolume; 67 | - (void) pauseOnScreensaverStart: (NSNotification *) aNotification; 68 | - (void) playOnScreensaverStop: (NSNotification *) aNotification; 69 | - (void) pauseOnScreenLock: (NSNotification *) aNotification; 70 | - (void) playOnScreenUnlock: (NSNotification *) aNotification; 71 | 72 | - (void) rate:(Song *)song as:(BOOL)liked; 73 | 74 | - (IBAction)playpause: (id) sender; 75 | - (IBAction)next: (id) sender; 76 | - (IBAction)like: (id) sender; 77 | - (IBAction)dislike: (id) sender; 78 | - (IBAction)tired: (id) sender; 79 | - (IBAction)loadMore: (id)sender; 80 | - (IBAction)songURL: (id)sender; 81 | - (IBAction)artistURL: (id)sender; 82 | - (IBAction)albumURL: (id)sender; 83 | - (IBAction)volumeChanged: (id)sender; 84 | - (IBAction)increaseVolume:(id)sender; 85 | - (IBAction)decreaseVolume:(id)sender; 86 | - (IBAction)quickLookArt:(id)sender; 87 | 88 | @end 89 | -------------------------------------------------------------------------------- /Sources/Controllers/PreferencesController.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file PreferencesController.h 3 | * @brief Headers for the PreferencesController class and preferences keys 4 | * which are set from it 5 | */ 6 | 7 | /* If these are changed, then the xib also needs to be updated */ 8 | #define PLEASE_BIND_MEDIA @"pleaseBindMedia" 9 | #define PLEASE_SCROBBLE @"pleaseScrobble" 10 | #define PLEASE_SCROBBLE_LIKES @"pleaseScrobbleLikes" 11 | #define ONLY_SCROBBLE_LIKED @"onlyScrobbleLiked" 12 | #define PLEASE_GROWL @"pleaseGrowl" 13 | #define PLEASE_GROWL_NEW @"pleaseGrowlNew" 14 | #define PLEASE_GROWL_PLAY @"pleaseGrowlPlay" 15 | #define PLEASE_CLOSE_DRAWER @"pleaseCloseDrawer" 16 | #define DRAWER_WIDTH @"drawerWidth" 17 | #define HIST_DRAWER_WIDTH @"histDrawerWidth" 18 | #define DESIRED_QUALITY @"audioQuality" 19 | #define LAST_PREF_PANE @"lastPrefPane" 20 | #define ENABLED_PROXY @"enabledProxy" 21 | #define PROXY_HTTP_HOST @"httpProxyHost" 22 | #define PROXY_HTTP_PORT @"httpProxyPort" 23 | #define PROXY_SOCKS_HOST @"socksProxyHost" 24 | #define PROXY_SOCKS_PORT @"socksProxyPort" 25 | #define PROXY_AUDIO @"proxyAudio" 26 | #define OPEN_DRAWER @"openDrawer" 27 | #define SORT_STATIONS @"sortStations" 28 | #define GROWL_TYPE @"notificationType" 29 | #define DOCK_ICON_ALBUM_ART @"dockIconAlbumArt" 30 | #define ALBUM_ART_PLAY_PAUSE @"albumArtPlayPause" 31 | #define STATUS_BAR_ICON @"statusBarIcon" 32 | #define STATUS_BAR_ICON_BW @"statusBarIconBlackWhite" 33 | #define STATUS_BAR_ICON_ALBUM @"statusBarIconAlbumArt" 34 | #define STATUS_BAR_SHOW_SONG @"statusBarShowSongTitle" 35 | #define ALWAYS_ON_TOP @"alwaysOnTop" 36 | #define PAUSE_ON_SCREENSAVER_START @"pauseOnScreensaverStart" 37 | #define PLAY_ON_SCREENSAVER_STOP @"playOnScreensaverStop" 38 | #define PAUSE_ON_SCREEN_LOCK @"pauseOnScreenLock" 39 | #define PLAY_ON_SCREEN_UNLOCK @"playOnScreenUnlock" 40 | 41 | /* If observing a value, then the method which is implemented is: 42 | observeValueForKeyPath:(NSString*) ofObject:(id) change:(NSDictionary*) 43 | context:(void*) */ 44 | #define PREFERENCES [NSUserDefaults standardUserDefaults] 45 | #define PREF_KEY_VALUE(x) [PREFERENCES valueForKey:(x)] 46 | #define PREF_KEY_BOOL(x) [PREFERENCES boolForKey:(x)] 47 | #define PREF_KEY_INT(x) [PREFERENCES integerForKey:(x)] 48 | #define PREF_KEY_SET_BOOL(x, y) [PREFERENCES setBool:y forKey:x] 49 | #define PREF_KEY_SET_INT(x, y) [PREFERENCES setInteger:y forKey:x] 50 | 51 | #define QUALITY_HIGH 0 52 | #define QUALITY_MED 1 53 | #define QUALITY_LOW 2 54 | 55 | #define PROXY_SYSTEM 0 56 | #define PROXY_HTTP 1 57 | #define PROXY_SOCKS 2 58 | 59 | #define SORT_DATE_ASC 0 60 | #define SORT_DATE_DSC 1 61 | #define SORT_NAME_ASC 2 62 | #define SORT_NAME_DSC 3 63 | 64 | #define GROWL_TYPE_GROWL 0 65 | #define GROWL_TYPE_OSX 1 66 | 67 | @interface PreferencesController : NSObject { 68 | IBOutlet NSWindow *window; 69 | IBOutlet NSToolbar *toolbar; 70 | IBOutlet NSView *general; 71 | IBOutlet NSView *playback; 72 | IBOutlet NSView *network; 73 | 74 | // General 75 | IBOutlet NSButton *mediaKeysCheckbox; 76 | IBOutlet NSTextField *mediaKeysLabel; 77 | 78 | IBOutlet NSButton *statusItemShowColorIcon; 79 | IBOutlet NSButton *statusItemShowBlackAndWhiteIcon; 80 | IBOutlet NSButton *statusItemShowAlbumArt; 81 | 82 | // Playback 83 | IBOutlet NSButton *notificationEnabled; 84 | IBOutlet NSPopUpButton *notificationType; 85 | 86 | // Network 87 | IBOutlet NSTextField *proxyServerErrorMessage; 88 | 89 | NSArray *itemIdentifiers; 90 | } 91 | 92 | /* Selecting views */ 93 | - (IBAction) showGeneral: (id) sender; 94 | - (IBAction) showPlayback: (id) sender; 95 | - (IBAction) showNetwork: (id) sender; 96 | 97 | - (IBAction) statusItemIconChanged:(id)sender; 98 | - (IBAction) bindMediaChanged: (id) sender; 99 | - (IBAction) show: (id) sender; 100 | 101 | @end 102 | -------------------------------------------------------------------------------- /Sources/Controllers/PreferencesController.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "PlaybackController.h" 4 | #import "PreferencesController.h" 5 | #import "URLConnection.h" 6 | 7 | @implementation PreferencesController 8 | 9 | - (void)awakeFromNib { 10 | [super awakeFromNib]; 11 | 12 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(proxyServerValidityChanged:) name:URLConnectionProxyValidityChangedNotification object:nil]; 13 | } 14 | 15 | - (void)windowDidBecomeMain:(NSNotification *)notification { 16 | /* See HermesAppDelegate#updateStatusBarIcon */ 17 | [window setCanHide:NO]; 18 | 19 | if (PREF_KEY_BOOL(STATUS_BAR_ICON_BW)) 20 | statusItemShowBlackAndWhiteIcon.state = NSOnState; 21 | else if (PREF_KEY_BOOL(STATUS_BAR_ICON_ALBUM)) 22 | statusItemShowAlbumArt.state = NSOnState; 23 | else 24 | statusItemShowColorIcon.state = NSOnState; 25 | 26 | NSString *last = PREF_KEY_VALUE(LAST_PREF_PANE); 27 | if (NSClassFromString(@"NSUserNotification") != nil) { 28 | [notificationEnabled setTitle:@""]; 29 | [notificationType setHidden:NO]; 30 | } 31 | 32 | if (itemIdentifiers == nil) { 33 | itemIdentifiers = [[toolbar items] valueForKey:@"itemIdentifier"]; 34 | } 35 | 36 | if ([last isEqual:@"playback"]) { 37 | [toolbar setSelectedItemIdentifier:@"playback"]; 38 | [self setPreferenceView:playback as:@"playback"]; 39 | } else if ([last isEqual:@"network"]) { 40 | [toolbar setSelectedItemIdentifier:@"network"]; 41 | [self setPreferenceView:network as:@"network"]; 42 | } else { 43 | [toolbar setSelectedItemIdentifier:@"general"]; 44 | [self setPreferenceView:general as:@"general"]; 45 | } 46 | } 47 | 48 | - (void) setPreferenceView:(NSView*) view as:(NSString*)name { 49 | NSView *container = [window contentView]; 50 | if ([[container subviews] count] > 0) { 51 | NSView *prev_view = [container subviews][0]; 52 | if (prev_view == view) { 53 | return; 54 | } 55 | [prev_view removeFromSuperviewWithoutNeedingDisplay]; 56 | } 57 | 58 | NSRect frame = [view bounds]; 59 | frame.origin.y = NSHeight([container frame]) - NSHeight([view bounds]); 60 | [view setFrame:frame]; 61 | [view setAutoresizingMask:NSViewMinYMargin | NSViewWidthSizable]; 62 | [container addSubview:view]; 63 | [window setInitialFirstResponder:view]; 64 | 65 | NSRect windowFrame = [window frame]; 66 | NSRect contentRect = [window contentRectForFrameRect:windowFrame]; 67 | windowFrame.size.height = NSHeight(frame) + NSHeight(windowFrame) - NSHeight(contentRect); 68 | windowFrame.size.width = NSWidth(frame); 69 | windowFrame.origin.y = NSMaxY([window frame]) - NSHeight(windowFrame); 70 | [window setFrame:windowFrame display:YES animate:YES]; 71 | 72 | NSUInteger toolbarItemIndex = [itemIdentifiers indexOfObject:name]; 73 | NSString *title = @"Preferences"; 74 | if (toolbarItemIndex != NSNotFound) { 75 | title = [[toolbar items][toolbarItemIndex] label]; 76 | } 77 | [window setTitle:title]; 78 | 79 | if ([HMSAppDelegate playback].mediaKeyTap == nil) { 80 | mediaKeysCheckbox.enabled = NO; 81 | #ifndef MPREMOTECOMMANDCENTER_MEDIA_KEYS_BROKEN 82 | if ([HMSAppDelegate playback].remoteCommandCenter != nil) { 83 | mediaKeysCheckbox.integerValue = YES; 84 | mediaKeysLabel.stringValue = @"Play/pause and next track keys are always enabled in macOS 10.12.2 and later."; 85 | } else { 86 | #endif 87 | #if DEBUG 88 | mediaKeysLabel.stringValue = @"Media keys are not available because this version of Hermes is compiled in debug mode."; 89 | #else 90 | mediaKeysLabel.stringValue = @"Media keys are unavailable for an unknown reason."; 91 | #endif 92 | #ifndef MPREMOTECOMMANDCENTER_MEDIA_KEYS_BROKEN 93 | } 94 | #endif 95 | } 96 | 97 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 98 | [defaults setObject:name forKey:LAST_PREF_PANE]; 99 | } 100 | 101 | - (IBAction) showGeneral: (id) sender { 102 | [self setPreferenceView:general as:@"general"]; 103 | } 104 | 105 | - (IBAction) showPlayback: (id) sender { 106 | [self setPreferenceView:playback as:@"playback"]; 107 | } 108 | 109 | - (IBAction) showNetwork: (id) sender { 110 | [self setPreferenceView:network as:@"network"]; 111 | } 112 | 113 | - (IBAction) statusItemIconChanged:(id)sender { 114 | if (sender == statusItemShowColorIcon) { 115 | PREF_KEY_SET_BOOL(STATUS_BAR_ICON_BW, NO); 116 | PREF_KEY_SET_BOOL(STATUS_BAR_ICON_ALBUM, NO); 117 | } else if (sender == statusItemShowBlackAndWhiteIcon) { 118 | PREF_KEY_SET_BOOL(STATUS_BAR_ICON_BW, YES); 119 | PREF_KEY_SET_BOOL(STATUS_BAR_ICON_ALBUM, NO); 120 | } else if (sender == statusItemShowAlbumArt) { 121 | PREF_KEY_SET_BOOL(STATUS_BAR_ICON_BW, NO); 122 | PREF_KEY_SET_BOOL(STATUS_BAR_ICON_ALBUM, YES); 123 | } 124 | [HMSAppDelegate updateStatusItem:sender]; 125 | } 126 | 127 | - (IBAction) bindMediaChanged: (id) sender { 128 | SPMediaKeyTap *mediaKeyTap = [HMSAppDelegate playback].mediaKeyTap; 129 | if (!mediaKeyTap) 130 | return; 131 | 132 | if (PREF_KEY_BOOL(PLEASE_BIND_MEDIA)) { 133 | [mediaKeyTap startWatchingMediaKeys]; 134 | } else { 135 | [mediaKeyTap stopWatchingMediaKeys]; 136 | } 137 | } 138 | 139 | - (IBAction) show: (id) sender { 140 | [NSApp activateIgnoringOtherApps:YES]; 141 | [window makeKeyAndOrderFront:sender]; 142 | } 143 | 144 | - (IBAction)proxySettingsChanged:(id)sender { 145 | BOOL proxyValid = NO; 146 | NSString *proxyHost; 147 | NSInteger proxyPort; 148 | 149 | switch (PREF_KEY_INT(ENABLED_PROXY)) { 150 | case PROXY_SYSTEM: 151 | proxyValid = YES; 152 | break; 153 | case PROXY_HTTP: 154 | proxyHost = PREF_KEY_VALUE(PROXY_HTTP_HOST); 155 | proxyPort = PREF_KEY_INT(PROXY_HTTP_PORT); 156 | break; 157 | case PROXY_SOCKS: 158 | proxyHost = PREF_KEY_VALUE(PROXY_SOCKS_HOST); 159 | proxyPort = PREF_KEY_INT(PROXY_SOCKS_PORT); 160 | } 161 | if (!proxyValid) { 162 | proxyValid = [URLConnection validProxyHost:&proxyHost port:proxyPort]; 163 | } 164 | proxyServerErrorMessage.hidden = proxyValid; 165 | } 166 | 167 | - (void)proxyServerValidityChanged:(NSNotification *)notification { 168 | BOOL proxyServerValid = [notification.userInfo[@"isValid"] boolValue]; 169 | proxyServerErrorMessage.hidden = proxyServerValid; 170 | if (!proxyServerValid) { 171 | [self showNetwork:nil]; 172 | [window orderFront:nil]; 173 | } 174 | } 175 | 176 | @end 177 | -------------------------------------------------------------------------------- /Sources/Controllers/StationController.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file StationController.h 3 | * @brief Headers for editing stations 4 | */ 5 | 6 | @class Station; 7 | 8 | @interface StationController : NSObject { 9 | IBOutlet NSWindow *window; 10 | 11 | /* Metadata */ 12 | IBOutlet NSImageView *art; 13 | IBOutlet NSTextField *stationName; 14 | IBOutlet NSTextField *stationCreated; 15 | IBOutlet NSTextField *stationGenres; 16 | IBOutlet NSProgressIndicator *progress; 17 | IBOutlet NSButton *gotoStation; 18 | 19 | /* Seeds */ 20 | IBOutlet NSTextField *seedSearch; 21 | IBOutlet NSOutlineView *seedsResults; 22 | IBOutlet NSOutlineView *seedsCurrent; 23 | NSMutableDictionary *seeds; 24 | NSDictionary *lastResults; 25 | IBOutlet NSButton *seedAdd; 26 | IBOutlet NSButton *seedDel; 27 | 28 | /* Likes/Dislikes */ 29 | IBOutlet NSTableView *likes; 30 | IBOutlet NSTableView *dislikes; 31 | NSArray *alikes; 32 | NSArray *adislikes; 33 | IBOutlet NSButton *deleteFeedback; 34 | 35 | Station *cur_station; 36 | NSString *station_url; 37 | } 38 | 39 | - (void) editStation: (Station*) station; 40 | - (IBAction) renameStation:(id)sender; 41 | - (IBAction) gotoPandora:(id)sender; 42 | 43 | - (IBAction) searchSeeds:(id)sender; 44 | - (IBAction) addSeed:(id)sender; 45 | - (IBAction) deleteSeed:(id)sender; 46 | - (void) seedFailedDeletion:(NSNotification*) not; 47 | 48 | - (IBAction) deleteFeedback:(id)sender; 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /Sources/Controllers/StationsController.h: -------------------------------------------------------------------------------- 1 | #define LAST_STATION_KEY @"hermes.last-station" 2 | 3 | @class FileReader; 4 | @class Station; 5 | 6 | @interface StationsController : NSObject { 7 | 8 | IBOutlet NSView *chooseStationView; 9 | 10 | IBOutlet NSDrawer *stations; 11 | IBOutlet NSTableView *stationsTable; 12 | IBOutlet NSProgressIndicator *stationsRefreshing; 13 | 14 | IBOutlet NSButton *playStationButton; 15 | IBOutlet NSButton *deleteStationButton; 16 | IBOutlet NSButton *editStationButton; 17 | 18 | /* New station by searching */ 19 | IBOutlet NSTextField *search; 20 | IBOutlet NSOutlineView *results; 21 | IBOutlet NSProgressIndicator *searchSpinner; 22 | IBOutlet NSImageView *errorIndicator; 23 | 24 | /* New station by genres */ 25 | IBOutlet NSOutlineView *genres; 26 | IBOutlet NSProgressIndicator *genreSpinner; 27 | 28 | /* Last known results */ 29 | NSDictionary *lastResults; 30 | NSArray *genreResults; 31 | 32 | /* Sorting the station list */ 33 | IBOutlet NSSegmentedControl *sort; 34 | 35 | FileReader *reader; 36 | } 37 | 38 | - (void) showDrawer; 39 | - (void) hideDrawer; 40 | - (void) show; 41 | - (void) reset; 42 | - (void) focus; 43 | 44 | // Buttons at bottom of drawer 45 | - (IBAction)deleteSelected: (id)sender; 46 | - (IBAction)playSelected: (id)sender; 47 | - (IBAction)editSelected: (id)sender; 48 | - (IBAction)refreshList: (id)sender; 49 | - (IBAction)addStation: (id)sender; 50 | 51 | // Actions from new station sheet 52 | - (IBAction)search: (id)sender; 53 | - (IBAction)cancelCreateStation: (id)sender; 54 | - (IBAction)createStation: (id)sender; 55 | - (IBAction)createStationGenre: (id)sender; 56 | 57 | - (int) stationIndex: (Station*) station; 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /Sources/FileReader.h: -------------------------------------------------------------------------------- 1 | // 2 | // FileReader.h 3 | // Hermes 4 | // 5 | // Created by Alex Crichton on 6/29/12. 6 | // 7 | 8 | typedef void(^FileReadCallback)(NSData*, NSError*); 9 | 10 | @interface FileReader : NSObject { 11 | NSInputStream *stream; 12 | FileReadCallback cb; 13 | NSMutableData *bytes; 14 | } 15 | 16 | + (FileReader*) readerForFile:(NSString*)path 17 | completionHandler:(FileReadCallback) cb; 18 | 19 | - (void) start; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Sources/FileReader.m: -------------------------------------------------------------------------------- 1 | // 2 | // FileReader.m 3 | // Hermes 4 | // 5 | // Created by Alex Crichton on 6/29/12. 6 | // 7 | 8 | #import "FileReader.h" 9 | 10 | @implementation FileReader 11 | 12 | + (FileReader*) readerForFile:(NSString*)path 13 | completionHandler:(FileReadCallback) cb { 14 | FileReader *reader = [[FileReader alloc] init]; 15 | reader->stream = [NSInputStream inputStreamWithFileAtPath:path]; 16 | reader->cb = [cb copy]; 17 | reader->bytes = [NSMutableData data]; 18 | return reader; 19 | } 20 | 21 | - (void)stream:(NSStream *)s handleEvent:(NSStreamEvent)eventCode { 22 | NSError *error = nil; 23 | uint8_t buffer[1024]; 24 | switch (eventCode) { 25 | case NSStreamEventHasBytesAvailable: { 26 | NSUInteger len = [stream read:buffer maxLength:1024]; 27 | if (len) 28 | [bytes appendBytes:buffer length:len]; 29 | return; 30 | } 31 | case NSStreamEventEndEncountered: 32 | break; 33 | case NSStreamEventErrorOccurred: 34 | bytes = nil; 35 | error = [stream streamError]; 36 | break; 37 | default: 38 | return; 39 | } 40 | NSLogd(@"notifying"); 41 | cb(bytes, error); 42 | [s close]; 43 | [s removeFromRunLoop:[NSRunLoop currentRunLoop] 44 | forMode:NSDefaultRunLoopMode]; 45 | } 46 | 47 | - (void) start { 48 | [stream setDelegate:self]; 49 | [stream scheduleInRunLoop:[NSRunLoop currentRunLoop] 50 | forMode:NSDefaultRunLoopMode]; 51 | [stream open]; 52 | } 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /Sources/HermesApp.h: -------------------------------------------------------------------------------- 1 | // 2 | // HermesApp.h 3 | // Hermes 4 | // 5 | // Created by Nicholas Riley on 4/1/14. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface HermesApp : NSApplication 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Sources/HermesApp.m: -------------------------------------------------------------------------------- 1 | // 2 | // HermesApp.m 3 | // Hermes 4 | // 5 | // Created by Nicholas Riley on 4/1/14. 6 | // 7 | // 8 | 9 | #import "HermesApp.h" 10 | 11 | @implementation HermesApp 12 | 13 | - (void)orderFrontStandardAboutPanelWithOptions:(NSDictionary *)optionsDictionary { 14 | // XXX work around bug in OS X 10.7–10.12 where the Credits text is not centered (r. 14829080) 15 | NSSet *windowsBefore = [NSSet setWithArray:[NSApp windows]]; 16 | 17 | // change credits font to current system font 18 | NSData *creditsData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"Credits" ofType:@"rtf"]]; 19 | NSMutableAttributedString *credits = [[NSMutableAttributedString alloc] initWithRTF:creditsData documentAttributes:nil]; 20 | NSString *systemFontFamily = [[NSFont systemFontOfSize:[NSFont labelFontSize]].fontDescriptor objectForKey:NSFontFamilyAttribute]; 21 | 22 | NSFontManager *fontManager = [NSFontManager sharedFontManager]; 23 | NSRange effectiveRange = {0, 0}; 24 | NSUInteger length = credits.length; 25 | while (NSMaxRange(effectiveRange) < length) { 26 | NSFont *font = [credits attribute:NSFontAttributeName atIndex:NSMaxRange(effectiveRange) effectiveRange:&effectiveRange]; 27 | font = [fontManager convertFont:font toFamily:systemFontFamily]; 28 | [credits addAttribute:NSFontAttributeName value:font range:effectiveRange]; 29 | } 30 | 31 | NSMutableDictionary *optionsWithCredits = optionsDictionary == nil ? [[NSMutableDictionary alloc] initWithCapacity:1] : [optionsDictionary mutableCopy]; 32 | optionsWithCredits[@"Credits"] = credits; 33 | 34 | [super orderFrontStandardAboutPanelWithOptions:optionsWithCredits]; 35 | 36 | for (NSWindow *window in [NSApp windows]) { 37 | if ([windowsBefore containsObject:window]) 38 | continue; 39 | 40 | for (NSView *view in [[window contentView] subviews]) { 41 | if (![view isKindOfClass:[NSScrollView class]]) 42 | continue; 43 | 44 | NSClipView *clipView = [(NSScrollView *)view contentView]; 45 | NSRect clipViewFrame = [clipView frame]; 46 | NSView *documentView = [clipView documentView]; 47 | NSRect documentViewFrame = [documentView frame]; 48 | 49 | if (clipViewFrame.size.height != documentViewFrame.size.height) 50 | continue; // don't mess with a scrollable view 51 | 52 | if (clipViewFrame.size.width != documentViewFrame.size.width) { 53 | documentViewFrame.size.width = clipViewFrame.size.width; 54 | [documentView setFrame:documentViewFrame]; 55 | break; 56 | } 57 | } 58 | break; 59 | } 60 | 61 | [NSApp activateIgnoringOtherApps:YES]; 62 | } 63 | 64 | @end 65 | -------------------------------------------------------------------------------- /Sources/HermesAppDelegate.h: -------------------------------------------------------------------------------- 1 | #import "Pandora.h" 2 | 3 | #define USERNAME_KEY @"pandora.username" 4 | 5 | #define DRAWER_STATIONS 0 6 | #define DRAWER_HISTORY 1 7 | #define DRAWER_NONE_HIST 2 8 | #define DRAWER_NONE_STA 3 9 | 10 | @class StationsController; 11 | @class AuthController; 12 | @class PlaybackController; 13 | @class HistoryController; 14 | @class StationController; 15 | @class PandoraRequest; 16 | @class Growler; 17 | @class Scrobbler; 18 | @class SPMediaKeyTap; 19 | @class NetworkConnection; 20 | @class PreferencesController; 21 | 22 | @interface HermesAppDelegate : NSObject { 23 | /* Generic loading view */ 24 | IBOutlet NSView *loadingView; 25 | IBOutlet NSProgressIndicator *loadingIcon; 26 | 27 | /* Pandora error view */ 28 | IBOutlet NSView *errorView; 29 | IBOutlet NSTextField *errorLabel; 30 | IBOutlet NSButton *errorButton; 31 | PandoraRequest *lastRequest; 32 | Station *lastStationErr; 33 | NSTimer *autoRetry; 34 | 35 | IBOutlet NSWindow *newStationSheet; 36 | IBOutlet NSToolbarItem *drawerToggle; 37 | IBOutlet NSMenu *statusBarMenu; 38 | 39 | /* Status bar menu */ 40 | NSStatusItem *statusItem; 41 | IBOutlet NSMenuItem *nowPlaying; 42 | IBOutlet NSMenuItem *currentSong; 43 | IBOutlet NSMenuItem *currentArtist; 44 | IBOutlet NSMenuItem *playbackState; 45 | } 46 | 47 | @property (readonly) Pandora *pandora; 48 | @property (readonly) IBOutlet NSWindow *window; 49 | @property (readonly) IBOutlet StationsController *stations; 50 | @property (readonly) IBOutlet HistoryController *history; 51 | @property (readonly) IBOutlet AuthController *auth; 52 | @property (readonly) IBOutlet PlaybackController *playback; 53 | @property (readonly) IBOutlet StationController *station; 54 | @property (readonly) IBOutlet Growler *growler; 55 | @property (readonly) IBOutlet Scrobbler *scrobbler; 56 | @property (readonly) IBOutlet NetworkConnection *networkManager; 57 | @property (readonly) IBOutlet PreferencesController *preferences; 58 | @property (readonly) BOOL debugMode; 59 | 60 | - (void) closeNewStationSheet; 61 | - (void) showNewStationSheet; 62 | - (void) saveUsername: (NSString*) username password: (NSString*) password; 63 | - (void) setCurrentView: (NSView*) view; 64 | - (void) showLoader; 65 | - (NSString*) stateDirectory: (NSString*) file; 66 | 67 | - (NSString*) getSavedUsername; 68 | - (NSString*) getSavedPassword; 69 | - (NSImage*) buildPlayPauseAlbumArtImage:(NSSize)size; 70 | 71 | - (void) tryRetry; 72 | - (void) handleDrawer; 73 | 74 | - (IBAction) changelog:(id)sender; 75 | - (IBAction) hermesOnGitHub:(id)sender; 76 | - (IBAction) reportAnIssue:(id)sender; 77 | - (IBAction) hermesHomepage:(id)sender; 78 | - (IBAction) retry:(id)sender; 79 | - (IBAction) toggleDrawerContent:(id)sender; 80 | - (IBAction) toggleDrawerVisible:(id)sender; 81 | - (IBAction) showStationsDrawer:(id)sender; 82 | - (IBAction) showHistoryDrawer:(id)sender; 83 | - (IBAction) activate:(id)sender; 84 | - (IBAction) updateStatusItemVisibility:(id)sender; 85 | - (IBAction) updateStatusItem:(id)sender; 86 | - (IBAction) updateAlwaysOnTop:(id)sender; 87 | 88 | /** 89 | * Log message to Hermes-specific logging facility. 90 | * 91 | * @param message the NSString to log 92 | */ 93 | - (void)logMessage:(NSString *)message; 94 | 95 | @end 96 | -------------------------------------------------------------------------------- /Sources/Hermes_Prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #import "HermesAppDelegate.h" 4 | #include 5 | 6 | #define NSLogd(fmt, args...) [(HermesAppDelegate *)HMSAppDelegate logMessage:[NSString stringWithFormat:@"%s:%d %s " fmt, basename(__FILE__), __LINE__, __PRETTY_FUNCTION__, ##args]]; 7 | #define HMSLog NSLogd 8 | 9 | #define HMSAssert(expression, ...) \ 10 | do { \ 11 | if (!(expression)) { \ 12 | HMSLog(@"Assertion failure: %s in %s on line %s:%d. %@", #expression, __func__, __FILE__, __LINE__, [NSString stringWithFormat: @"" __VA_ARGS__]); \ 13 | abort(); \ 14 | } \ 15 | } while (0) 16 | #endif 17 | 18 | #define HMSAppDelegate ((HermesAppDelegate *)[NSApp delegate]) 19 | -------------------------------------------------------------------------------- /Sources/Integration/AppleScript.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppleScript.h 3 | // Hermes 4 | // 5 | // Created by Alex Crichton on 11/19/11. 6 | // 7 | 8 | #import "Pandora/Station.h" 9 | 10 | // These match iTunes (4-char codes) 11 | typedef enum { 12 | PlaybackStateStopped = 'stop', 13 | PlaybackStatePlaying = 'play', 14 | PlaybackStatePaused = 'paus' 15 | } PlaybackStates; 16 | 17 | @interface PlayCommand : NSScriptCommand {} @end 18 | @interface PauseCommand : NSScriptCommand {} @end 19 | @interface PlayPauseCommand : NSScriptCommand {} @end 20 | @interface SkipCommand : NSScriptCommand {} @end 21 | @interface ThumbsUpCommand : NSScriptCommand {} @end 22 | @interface ThumbsDownCommand : NSScriptCommand {} @end 23 | @interface RaiseVolumeCommand : NSScriptCommand {} @end 24 | @interface LowerVolumeCommand : NSScriptCommand {} @end 25 | @interface FullVolumeCommand : NSScriptCommand {} @end 26 | @interface MuteCommand : NSScriptCommand {} @end 27 | @interface UnmuteCommand : NSScriptCommand {} @end 28 | @interface TiredCommand : NSScriptCommand {} @end 29 | 30 | @interface NSApplication (HermesScripting) 31 | - (NSNumber*) volume; 32 | - (void) setVolume: (NSNumber*) volume; 33 | - (int) playbackState; 34 | - (NSNumber*) playbackPosition; 35 | - (NSNumber*) currentSongDuration; 36 | - (Station*) currentStation; 37 | - (void) setCurrentStation: (Station*) station; 38 | - (void) setPlaybackState: (int) state; 39 | - (NSArray*) stations; 40 | - (Song*) currentSong; 41 | @end 42 | -------------------------------------------------------------------------------- /Sources/Integration/AppleScript.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppleScript.m 3 | // Hermes 4 | // 5 | // Created by Alex Crichton on 11/19/11. 6 | // 7 | 8 | #import "AppleScript.h" 9 | #import "PlaybackController.h" 10 | #import "StationsController.h" 11 | 12 | NSInteger savedVolume = 0; 13 | 14 | @implementation PlayCommand 15 | - (id) performDefaultImplementation { 16 | PlaybackController *playback = [HMSAppDelegate playback]; 17 | return @([playback play]); 18 | } 19 | @end 20 | 21 | @implementation PauseCommand 22 | - (id) performDefaultImplementation { 23 | PlaybackController *playback = [HMSAppDelegate playback]; 24 | return @([playback pause]); 25 | } 26 | @end 27 | 28 | @implementation PlayPauseCommand 29 | - (id) performDefaultImplementation { 30 | PlaybackController *playback = [HMSAppDelegate playback]; 31 | [playback playpause:self]; 32 | return self; 33 | } 34 | @end 35 | 36 | @implementation SkipCommand 37 | - (id) performDefaultImplementation { 38 | PlaybackController *playback = [HMSAppDelegate playback]; 39 | [playback next:self]; 40 | return self; 41 | } 42 | @end 43 | @implementation ThumbsUpCommand 44 | - (id) performDefaultImplementation { 45 | PlaybackController *playback = [HMSAppDelegate playback]; 46 | [playback like:self]; 47 | return self; 48 | } 49 | @end 50 | @implementation ThumbsDownCommand 51 | - (id) performDefaultImplementation { 52 | PlaybackController *playback = [HMSAppDelegate playback]; 53 | [playback dislike:self]; 54 | return self; 55 | } 56 | @end 57 | @implementation RaiseVolumeCommand 58 | - (id) performDefaultImplementation { 59 | PlaybackController *playback = [HMSAppDelegate playback]; 60 | NSInteger volume = [playback integerVolume]; 61 | [playback setIntegerVolume:volume + 7]; 62 | NSLogd(@"Raised volume to: %ld", (long)[playback integerVolume]); 63 | return self; 64 | } 65 | @end 66 | @implementation LowerVolumeCommand 67 | - (id) performDefaultImplementation { 68 | PlaybackController *playback = [HMSAppDelegate playback]; 69 | NSInteger volume = [playback integerVolume]; 70 | [playback setIntegerVolume:volume - 7]; 71 | NSLogd(@"Lowered volume to: %ld", (long)[playback integerVolume]); 72 | return self; 73 | } 74 | @end 75 | @implementation FullVolumeCommand 76 | - (id) performDefaultImplementation { 77 | PlaybackController *playback = [HMSAppDelegate playback]; 78 | [playback setIntegerVolume:100]; 79 | NSLogd(@"Changed volume to: %ld", (long)[playback integerVolume]); 80 | return self; 81 | } 82 | @end 83 | @implementation MuteCommand 84 | - (id) performDefaultImplementation { 85 | PlaybackController *playback = [HMSAppDelegate playback]; 86 | savedVolume = [playback integerVolume]; 87 | [playback setIntegerVolume:0]; 88 | NSLogd(@"Changed volume to: %ld", (long)[playback integerVolume]); 89 | return self; 90 | } 91 | @end 92 | @implementation UnmuteCommand 93 | - (id) performDefaultImplementation { 94 | PlaybackController *playback = [HMSAppDelegate playback]; 95 | [playback setIntegerVolume:savedVolume]; 96 | NSLogd(@"Changed volume to: %ld", (long)[playback integerVolume]); 97 | return self; 98 | } 99 | @end 100 | @implementation TiredCommand 101 | - (id) performDefaultImplementation { 102 | PlaybackController *playback = [HMSAppDelegate playback]; 103 | [playback tired:self]; 104 | return self; 105 | } 106 | @end 107 | 108 | @implementation NSApplication (HermesScripting) 109 | 110 | - (NSNumber*) volume { 111 | PlaybackController *playback = [HMSAppDelegate playback]; 112 | return @([playback integerVolume]); 113 | } 114 | 115 | - (void) setVolume: (NSNumber*) vol { 116 | PlaybackController *playback = [HMSAppDelegate playback]; 117 | [playback setIntegerVolume:[vol intValue]]; 118 | } 119 | 120 | - (int) playbackState { 121 | PlaybackController *playback = [HMSAppDelegate playback]; 122 | Station *playing = [playback playing]; 123 | if (playing == nil) { 124 | return PlaybackStateStopped; 125 | } else if ([playing isPaused]) { 126 | return PlaybackStatePaused; 127 | } 128 | return PlaybackStatePlaying; 129 | } 130 | 131 | - (NSNumber *) playbackPosition { 132 | double progress; 133 | PlaybackController *playback = [HMSAppDelegate playback]; 134 | [[playback playing] progress:&progress]; 135 | return @(progress); 136 | } 137 | 138 | - (NSNumber *) currentSongDuration { 139 | double duration; 140 | PlaybackController *playback = [HMSAppDelegate playback]; 141 | [[playback playing] duration:&duration]; 142 | return @(duration); 143 | } 144 | 145 | - (void) setPlaybackState: (int) state { 146 | PlaybackController *playback = [HMSAppDelegate playback]; 147 | switch (state) { 148 | case PlaybackStateStopped: 149 | case PlaybackStatePaused: 150 | [playback pause]; 151 | break; 152 | 153 | case PlaybackStatePlaying: 154 | [playback play]; 155 | break; 156 | 157 | default: 158 | NSLog(@"Invalid playback state: %d", state); 159 | } 160 | } 161 | 162 | - (Station*) currentStation { 163 | PlaybackController *playback = [HMSAppDelegate playback]; 164 | return [playback playing]; 165 | } 166 | 167 | - (void) setCurrentStation:(Station *)station { 168 | HermesAppDelegate *delegate = HMSAppDelegate; 169 | PlaybackController *playback = [delegate playback]; 170 | [playback playStation:station]; 171 | StationsController *stations = [delegate stations]; 172 | [stations refreshList:self]; 173 | } 174 | 175 | - (NSArray*) stations { 176 | HermesAppDelegate *delegate = HMSAppDelegate; 177 | return [[delegate pandora] stations]; 178 | } 179 | 180 | - (Song*) currentSong { 181 | PlaybackController *playback = [HMSAppDelegate playback]; 182 | return [[playback playing] playingSong]; 183 | } 184 | 185 | @end 186 | -------------------------------------------------------------------------------- /Sources/Integration/Growler.h: -------------------------------------------------------------------------------- 1 | // 2 | // Growler.h 3 | // Hermes 4 | // 5 | 6 | #import 7 | 8 | @class Song; 9 | 10 | #define GROWLER [HMSAppDelegate growler] 11 | 12 | @interface Growler : NSObject 14 | 15 | - (void) growl:(Song*)song withImage:(NSData*)image isNew:(BOOL) n; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Sources/Integration/Growler.m: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Growler.h 3 | * @brief Growl integration for the rest of Hermes 4 | * 5 | * Provides unified access to displaying notifications for different kinds 6 | * of events without having to deal with Growl directly. 7 | */ 8 | 9 | #import 10 | 11 | #import "Growler.h" 12 | #import "PreferencesController.h" 13 | #import "PlaybackController.h" 14 | 15 | @implementation Growler 16 | 17 | - (id) init { 18 | [GrowlApplicationBridge setGrowlDelegate:self]; 19 | [[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:self]; 20 | return self; 21 | } 22 | 23 | - (void) growl:(Song*)song withImage:(NSData*)image isNew:(BOOL)n { 24 | // Unconditionally remove all notifications from notification center to behave like iTunes 25 | // notifications and does not fill the notification center with old song details. 26 | [[NSUserNotificationCenter defaultUserNotificationCenter] removeAllDeliveredNotifications]; 27 | 28 | if (!PREF_KEY_BOOL(PLEASE_GROWL) || 29 | (n && !PREF_KEY_BOOL(PLEASE_GROWL_NEW)) || 30 | (!n && !PREF_KEY_BOOL(PLEASE_GROWL_PLAY))) { 31 | return; 32 | } 33 | 34 | NSString *title = [song title]; 35 | if ([[song nrating] intValue] == 1) { 36 | title = [NSString stringWithFormat:@"👍 %@", title]; 37 | } 38 | NSString *description = [NSString stringWithFormat:@"%@\n%@", [song artist], 39 | [song album]]; 40 | 41 | if (PREF_KEY_INT(GROWL_TYPE) == GROWL_TYPE_OSX) { 42 | NSUserNotification *not = [[NSUserNotification alloc] init]; 43 | [not setTitle:title]; 44 | [not setInformativeText:description]; 45 | [not setHasActionButton:YES]; 46 | [not setActionButtonTitle: @"Skip"]; 47 | 48 | // Make skip button visible for banner notifications (like in iTunes) 49 | // - Undocumented API. Will only work if Apple keeps in NSUserNotification 50 | // class. Otherwise, skip button will only appear if 'Alert' style 51 | // notifications are used. 52 | // - see: https://github.com/indragiek/NSUserNotificationPrivate 53 | @try { 54 | [not setValue:@YES forKey:@"_showsButtons"]; 55 | } @catch (NSException *e) { 56 | if ([e name] != NSUndefinedKeyException) @throw e; 57 | } 58 | 59 | // Skip action 60 | NSUserNotificationAction *skipAction = 61 | [NSUserNotificationAction actionWithIdentifier:@"next" title:@"Skip"]; 62 | 63 | // Like/Dislike actions 64 | NSString *likeActionTitle = 65 | ([[song nrating] intValue] == 1) ? @"Remove Like" : @"Like"; 66 | 67 | NSUserNotificationAction *likeAction = 68 | [NSUserNotificationAction actionWithIdentifier:@"like" title:likeActionTitle]; 69 | NSUserNotificationAction *dislikeAction = 70 | [NSUserNotificationAction actionWithIdentifier:@"dislike" title:@"Dislike"]; 71 | 72 | [not setAdditionalActions: @[skipAction,likeAction,dislikeAction]]; 73 | 74 | if ([not respondsToSelector:@selector(setContentImage:)]) { 75 | // Set album art where app icon is (like in iTunes) 76 | // - Undocumented API. Will only work if Apple keeps in NSUserNotification 77 | // class. Otherwise, skip button will only appear if 'Alert' style 78 | // notifications are used. 79 | // - see: https://github.com/indragiek/NSUserNotificationPrivate 80 | @try { 81 | [not setValue:[[NSImage alloc] initWithData:image] forKey:@"_identityImage"]; 82 | } @catch (NSException *e) { 83 | if ([e name] != NSUndefinedKeyException) @throw e; 84 | [not setContentImage:[[NSImage alloc] initWithData:image]]; 85 | } 86 | } 87 | 88 | NSUserNotificationCenter *center = 89 | [NSUserNotificationCenter defaultUserNotificationCenter]; 90 | [not setDeliveryDate:[NSDate date]]; 91 | [center scheduleNotification:not]; 92 | return; 93 | } 94 | 95 | /* To deliver the event that a notification was clicked, the click context 96 | must be serializable and all that whatnot. Right now, we don't need any 97 | state to pass between these two methods, so just make sure that there's 98 | something that's plist-encodable */ 99 | [GrowlApplicationBridge notifyWithTitle:title 100 | description:description 101 | notificationName:n ? @"hermes-song" : @"hermes-play" 102 | iconData:image 103 | priority:0 104 | isSticky:NO 105 | clickContext:@YES 106 | identifier:@"Hermes"]; 107 | } 108 | 109 | /****************************************************************************** 110 | * Implementation of GrowlApplicationDelegate 111 | ******************************************************************************/ 112 | 113 | - (NSDictionary*) registrationDictionaryForGrowl { 114 | NSArray *notifications = @[@"hermes-song", @"hermes-play"]; 115 | NSDictionary *human_names = @{ 116 | @"hermes-song": @"New Songs", 117 | @"hermes-play": @"Play/pause Events" 118 | }; 119 | return @{ 120 | GROWL_NOTIFICATIONS_ALL: notifications, 121 | GROWL_NOTIFICATIONS_DEFAULT: notifications, 122 | GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES: human_names 123 | }; 124 | } 125 | 126 | - (void) growlNotificationWasClicked:(id)clickContext { 127 | [[HMSAppDelegate window] orderFront:nil]; 128 | [NSApp activateIgnoringOtherApps:YES]; 129 | } 130 | 131 | /****************************************************************************** 132 | * Implementation of NSUserNotificationCenterDelegate 133 | ******************************************************************************/ 134 | 135 | - (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center 136 | shouldPresentNotification:(NSUserNotification *)notification { 137 | /* always show notifications, even if the application is active */ 138 | return YES; 139 | } 140 | 141 | - (void)userNotificationCenter:(NSUserNotificationCenter *)center 142 | didActivateNotification:(NSUserNotification *)notification { 143 | 144 | PlaybackController *playback = [HMSAppDelegate playback]; 145 | NSString *actionID = [[notification additionalActivationAction] identifier]; 146 | 147 | switch([notification activationType]) { 148 | case NSUserNotificationActivationTypeActionButtonClicked: 149 | 150 | // Skip button pressed 151 | [playback next:self]; 152 | break; 153 | 154 | case NSUserNotificationActivationTypeAdditionalActionClicked: 155 | 156 | // One of the drop down buttons was pressed 157 | if ([actionID isEqualToString:@"like"]) { 158 | [playback like:self]; 159 | } else if ([actionID isEqualToString:@"dislike"]) { 160 | [playback dislike:self]; 161 | } else if ([actionID isEqualToString:@"next"]) { 162 | [playback next:self]; 163 | } 164 | break; 165 | 166 | case NSUserNotificationActivationTypeContentsClicked: 167 | // Banner was clicked, so bring up and focus main UI 168 | [[HMSAppDelegate window] orderFront:nil]; 169 | [NSApp activateIgnoringOtherApps:YES]; 170 | break; 171 | 172 | default: 173 | // Any other action 174 | break; 175 | 176 | } 177 | // Only way to get this notification to be removed from center 178 | [center removeAllDeliveredNotifications]; 179 | } 180 | 181 | 182 | 183 | @end 184 | -------------------------------------------------------------------------------- /Sources/Integration/Keychain.h: -------------------------------------------------------------------------------- 1 | // 2 | // Keychain.h 3 | // Hermes 4 | // 5 | // Created by Alex Crichton on 11/19/11. 6 | // 7 | 8 | #pragma once 9 | 10 | #define KEYCHAIN_SERVICE_NAME "Hermes" 11 | 12 | /** 13 | * Set a password in the Login Keychain 14 | * 15 | * @param username the Keychain entry name to use. 16 | * @param password the password to store in the Keychain entry. 17 | * @return YES if Keychain entry was set, NO otherwise. 18 | */ 19 | BOOL KeychainSetItem(NSString* username, NSString *password); 20 | 21 | /** 22 | * Get a password from the Login Keychain 23 | * 24 | * @param username the Keychain entry's name 25 | * @return the password stored in the entry 26 | */ 27 | NSString* KeychainGetPassword(NSString *username); 28 | -------------------------------------------------------------------------------- /Sources/Integration/Keychain.m: -------------------------------------------------------------------------------- 1 | // 2 | // Keychain.h 3 | // Hermes 4 | // 5 | // Created by Alex Crichton on 11/19/11. 6 | // 7 | 8 | #import "Keychain.h" 9 | 10 | BOOL KeychainSetItem(NSString* username, NSString* password) { 11 | SecKeychainItemRef item = nil; 12 | OSStatus result = SecKeychainFindGenericPassword( 13 | NULL, 14 | strlen(KEYCHAIN_SERVICE_NAME), 15 | KEYCHAIN_SERVICE_NAME, 16 | (UInt32)[username length], 17 | [username UTF8String], 18 | NULL, 19 | NULL, 20 | &item); 21 | 22 | if (result == noErr) { 23 | result = SecKeychainItemModifyContent(item, NULL, (UInt32)[password length], 24 | [password UTF8String]); 25 | } else { 26 | result = SecKeychainAddGenericPassword( 27 | NULL, 28 | strlen(KEYCHAIN_SERVICE_NAME), 29 | KEYCHAIN_SERVICE_NAME, 30 | (UInt32)[username length], 31 | [username UTF8String], 32 | (UInt32)[password length], 33 | [password UTF8String], 34 | NULL); 35 | } 36 | 37 | if (item) { 38 | CFRelease(item); 39 | } 40 | return result == noErr; 41 | } 42 | 43 | NSString *KeychainGetPassword(NSString* username) { 44 | void *passwordData = NULL; 45 | UInt32 length; 46 | OSStatus result = SecKeychainFindGenericPassword( 47 | NULL, 48 | strlen(KEYCHAIN_SERVICE_NAME), 49 | KEYCHAIN_SERVICE_NAME, 50 | (UInt32)[username length], 51 | [username UTF8String], 52 | &length, 53 | &passwordData, 54 | NULL); 55 | 56 | if (result != noErr) { 57 | return nil; 58 | } 59 | 60 | NSString *password = [[NSString alloc] initWithBytes:passwordData 61 | length:length 62 | encoding:NSUTF8StringEncoding]; 63 | SecKeychainItemFreeContent(NULL, passwordData); 64 | 65 | return password; 66 | } 67 | -------------------------------------------------------------------------------- /Sources/Integration/Scrobbler.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Scrobbler.h 3 | * 4 | * @brief Interface for talking to last.fm's api and updating what's currently 5 | * being listened to and such. 6 | */ 7 | 8 | #import "FMEngine/FMEngine.h" 9 | #import "Pandora/Song.h" 10 | 11 | typedef enum { 12 | NewSong, 13 | NowPlaying, 14 | FinalStatus 15 | } ScrobbleState; 16 | 17 | #define SCROBBLER [HMSAppDelegate scrobbler] 18 | 19 | @interface Scrobbler : NSObject { 20 | FMEngine *engine; 21 | NSString *requestToken; 22 | NSString *sessionToken; 23 | BOOL inAuthorization; 24 | } 25 | 26 | - (void) setPreference: (Song*)song loved:(BOOL)loved; 27 | - (void) scrobble: (Song*) song state: (ScrobbleState) status; 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Sources/Models/HistoryItem.h: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryItem.h 3 | // Hermes 4 | // 5 | // Created by Alex Crichton on 10/10/11. 6 | // 7 | 8 | @interface HistoryItem : NSCollectionViewItem { 9 | NSButton *art; 10 | } 11 | @end 12 | -------------------------------------------------------------------------------- /Sources/Models/HistoryItem.m: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryItem.m 3 | // Hermes 4 | // 5 | // Created by Alex Crichton on 10/10/11. 6 | // 7 | 8 | #import "HistoryController.h" 9 | #import "HistoryItem.h" 10 | #import "HistoryView.h" 11 | #import "ImageLoader.h" 12 | 13 | @implementation HistoryItem 14 | 15 | - (void) dealloc { 16 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 17 | } 18 | 19 | - (void) setSelected:(BOOL)selected { 20 | [super setSelected:selected]; 21 | HistoryView *view = (HistoryView*) [self view]; 22 | [view setSelected:selected]; 23 | [view setNeedsDisplay:YES]; 24 | HistoryController *hc = [[self collectionView] delegate]; 25 | [hc updateUI]; 26 | } 27 | 28 | - (void) updateUI { 29 | if (art == nil || [self representedObject] == nil) { 30 | return; 31 | } 32 | 33 | Song *s = [self representedObject]; 34 | NSString *a = [s art]; 35 | if (a && ![a isEqual:@""]) { 36 | [[ImageLoader loader] loadImageURL:a callback:^(NSData* data) { 37 | NSImage *image = [[NSImage alloc] initWithData: data]; 38 | [self->art setImage:image]; 39 | }]; 40 | } 41 | } 42 | 43 | - (void) trySetFromView { 44 | NSView *view = [self view]; 45 | if (view == nil) { 46 | return; 47 | } 48 | 49 | art = nil; 50 | for (NSView *view in [[self view] subviews]) { 51 | if ([view isKindOfClass:[NSButton class]]) { 52 | NSButton *button = (NSButton*) view; 53 | if ([[button alternateTitle] isEqual:@"Album"]) { 54 | art = button; 55 | } 56 | } 57 | } 58 | assert(art); 59 | } 60 | 61 | - (void) setView:(NSView *)view { 62 | [super setView:view]; 63 | [self trySetFromView]; 64 | } 65 | 66 | - (void) setRepresentedObject:(id)representedObject { 67 | [super setRepresentedObject:representedObject]; 68 | [self trySetFromView]; 69 | [self updateUI]; 70 | } 71 | 72 | @end 73 | -------------------------------------------------------------------------------- /Sources/Models/ImageLoader.h: -------------------------------------------------------------------------------- 1 | @class URLConnection; 2 | 3 | typedef void(^ImageCallback)(NSData*); 4 | 5 | @interface ImageLoader : NSObject { 6 | NSMutableArray *queue; 7 | NSMutableArray *cbqueue; 8 | URLConnection *cur; 9 | NSString *curURL; 10 | } 11 | 12 | + (ImageLoader*) loader; 13 | 14 | - (void) loadImageURL:(NSString*)url callback:(ImageCallback)cb; 15 | - (void) cancel: (NSString*)url; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Sources/Models/ImageLoader.m: -------------------------------------------------------------------------------- 1 | #import "ImageLoader.h" 2 | #import "URLConnection.h" 3 | 4 | @implementation ImageLoader 5 | 6 | + (ImageLoader*) loader { 7 | static ImageLoader *l = nil; 8 | if (l == nil) { 9 | l = [[ImageLoader alloc] init]; 10 | } 11 | return l; 12 | } 13 | 14 | - (id) init { 15 | cur = nil; 16 | queue = [NSMutableArray array]; 17 | cbqueue = [NSMutableArray array]; 18 | return self; 19 | } 20 | 21 | - (void) loadImageURL:(NSString*)url callback:(ImageCallback)cb { 22 | cb = [cb copy]; 23 | if (cur != nil) { 24 | [queue addObject:url]; 25 | [cbqueue addObject:cb]; 26 | return; 27 | } 28 | 29 | [self fetch:url cb:cb]; 30 | } 31 | 32 | - (void) fetch:(NSString*)url cb:(ImageCallback)cb { 33 | NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; 34 | cur = [URLConnection connectionForRequest:req 35 | completionHandler:^(NSData *d, NSError *error) { 36 | NSLogd(@"fetching: %@", url); 37 | cb(d); 38 | self->cur = nil; 39 | self->curURL = nil; 40 | 41 | /* If any pending requests are to this url, also satisfy them */ 42 | NSUInteger idx; 43 | while ((idx = [self->queue indexOfObject:url]) != NSNotFound) { 44 | NSLogd(@"cached: %@", url); 45 | [self->queue removeObjectAtIndex:idx]; 46 | ImageCallback cb = self->cbqueue[idx]; 47 | cb(d); 48 | [self->cbqueue removeObjectAtIndex:idx]; 49 | } 50 | 51 | [self tryFetch]; 52 | }]; 53 | curURL = url; 54 | [cur start]; 55 | } 56 | 57 | - (void) tryFetch { 58 | if ([queue count] == 0) return; 59 | NSString *url = queue[0]; 60 | ImageCallback cb = cbqueue[0]; 61 | [queue removeObjectAtIndex:0]; 62 | [cbqueue removeObjectAtIndex:0]; 63 | [self fetch:url cb:cb]; 64 | } 65 | 66 | - (void) cancel:(NSString*)url { 67 | NSUInteger idx = [queue indexOfObject:url]; 68 | if (idx == NSNotFound) { 69 | if ([url isEqualToString:curURL]) { 70 | cur = nil; 71 | curURL = nil; 72 | [self tryFetch]; 73 | } 74 | } else { 75 | [queue removeObjectAtIndex:idx]; 76 | [cbqueue removeObjectAtIndex:idx]; 77 | } 78 | } 79 | 80 | @end 81 | -------------------------------------------------------------------------------- /Sources/NetworkConnection.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface NetworkConnection : NSObject { 4 | SCNetworkReachabilityRef reachability; 5 | } 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /Sources/NetworkConnection.m: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NetworkConnection.m 3 | * @brief Tester for network connectivity and notifies the main application when 4 | * this becomes true 5 | * 6 | * This class is meant to have one instance of itself during runtime. 7 | */ 8 | #include 9 | 10 | #import "NetworkConnection.h" 11 | 12 | @implementation NetworkConnection 13 | 14 | /** 15 | * @brief Callback invoked when the network changes 16 | */ 17 | void NetworkCallback(SCNetworkReachabilityRef target, 18 | SCNetworkReachabilityFlags flags, 19 | void *info) { 20 | /* If the address 0.0.0.0 is considered 'local', then we've successfully 21 | connected to some network with an IP, and we're a candidate for retrying a 22 | currently pending request. This doesn't mean that we're guaranteed the 23 | request will succeed, but it's at least remotely possible that it can. */ 24 | if (flags & kSCNetworkReachabilityFlagsIsLocalAddress) { 25 | [HMSAppDelegate tryRetry]; 26 | } 27 | } 28 | 29 | - (id) init { 30 | /* We'll be testing against 0.0.0.0 */ 31 | struct sockaddr_in address; 32 | memset(&address, 0, sizeof(address)); 33 | address.sin_len = sizeof(address); 34 | address.sin_family = AF_INET; 35 | reachability = SCNetworkReachabilityCreateWithAddress(NULL, 36 | (struct sockaddr*) &address); 37 | 38 | /* Asynchronously notify us of network reachability changes */ 39 | BOOL success = SCNetworkReachabilityScheduleWithRunLoop( 40 | reachability, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); 41 | assert(success); 42 | success = SCNetworkReachabilitySetCallback( 43 | reachability, NetworkCallback, NULL); 44 | assert(success); 45 | 46 | return self; 47 | } 48 | 49 | - (void) dealloc { 50 | /* Removes ourselves from the run loop and move on */ 51 | SCNetworkReachabilityUnscheduleFromRunLoop( 52 | reachability, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); 53 | CFRelease(reachability); 54 | } 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /Sources/Notifications.h: -------------------------------------------------------------------------------- 1 | // 2 | // Notifications.h 3 | // Hermes 4 | // 5 | // Created by Winston Weinert on 4/15/14. 6 | // 7 | // 8 | 9 | #ifndef Hermes_Notifications_h 10 | #define Hermes_Notifications_h 11 | 12 | #pragma mark Distributed Notifications 13 | 14 | extern NSString * const HistoryControllerDidPlaySongDistributedNotification; 15 | 16 | extern NSString * const AppleScreensaverDidStartDistributedNotification; 17 | extern NSString * const AppleScreensaverDidStopDistributedNotification; 18 | extern NSString * const AppleScreenIsLockedDistributedNotification; 19 | extern NSString * const AppleScreenIsUnlockedDistributedNotification; 20 | 21 | #pragma mark Internal Notifications 22 | 23 | extern NSString * const PandoraDidErrorNotification; // userInfo: error 24 | extern NSString * const PandoraDidAuthenticateNotification; 25 | extern NSString * const PandoraDidLogOutNotification; 26 | extern NSString * const PandoraDidRateSongNotification; // object: song 27 | extern NSString * const PandoraDidTireSongNotification; // object: song 28 | extern NSString * const PandoraDidLoadStationsNotification; 29 | extern NSString * const PandoraDidCreateStationNotification; // userInfo: result 30 | extern NSString * const PandoraDidDeleteStationNotification; // object: Station 31 | extern NSString * const PandoraDidRenameStationNotification; 32 | extern NSString * const PandoraDidLoadStationInfoNotification; // userInfo: info 33 | extern NSString * const PandoraDidAddSeedNotification; // userInfo: result 34 | extern NSString * const PandoraDidDeleteSeedNotification; 35 | extern NSString * const PandoraDidDeleteFeedbackNotification; // object: feedbackId string 36 | extern NSString * const PandoraDidLoadSearchResultsNotification; // object: search string; userInfo: result 37 | extern NSString * const PandoraDidLoadGenreStationsNotification; // userInfo: result 38 | 39 | extern NSString * const StationDidPlaySongNotification; 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /Sources/Notifications.m: -------------------------------------------------------------------------------- 1 | // 2 | // Notifications.m 3 | // Hermes 4 | // 5 | // Created by Winston Weinert on 4/15/14. 6 | // 7 | // 8 | 9 | #import "Notifications.h" 10 | 11 | #pragma mark - Distributed Notifications 12 | 13 | NSString * const HistoryControllerDidPlaySongDistributedNotification = @"hermes.song"; 14 | 15 | NSString * const AppleScreensaverDidStartDistributedNotification = @"com.apple.screensaver.didstart"; 16 | NSString * const AppleScreensaverDidStopDistributedNotification = @"com.apple.screensaver.didstop"; 17 | NSString * const AppleScreenIsLockedDistributedNotification = @"com.apple.screenIsLocked"; 18 | NSString * const AppleScreenIsUnlockedDistributedNotification = @"com.apple.screenIsUnlocked"; 19 | 20 | #pragma mark - Internal Notifications 21 | 22 | NSString * const PandoraDidErrorNotification = @"PandoraDidErrorNotification"; 23 | NSString * const PandoraDidAuthenticateNotification = @"PandoraDidAuthenticateNotification"; 24 | NSString * const PandoraDidLogOutNotification = @"PandoraDidLogOutNotification"; 25 | NSString * const PandoraDidRateSongNotification = @"PandoraDidRateSongNotification"; 26 | NSString * const PandoraDidTireSongNotification = @"PandoraDidTireSongNotification"; 27 | NSString * const PandoraDidLoadStationsNotification = @"PandoraDidLoadStationsNotification"; 28 | NSString * const PandoraDidCreateStationNotification = @"PandoraDidCreateStationNotification"; 29 | NSString * const PandoraDidDeleteStationNotification = @"PandoraDidDeleteStationNotification"; 30 | NSString * const PandoraDidRenameStationNotification = @"PandoraDidRenameStationNotification"; 31 | NSString * const PandoraDidLoadStationInfoNotification = @"PandoraDidLoadStationInfoNotification"; 32 | NSString * const PandoraDidAddSeedNotification = @"PandoraDidAddSeedNotification"; 33 | NSString * const PandoraDidDeleteSeedNotification = @"PandoraDidDeleteSeedNotification"; 34 | NSString * const PandoraDidDeleteFeedbackNotification = @"PandoraDidDeleteFeedbackNotification"; 35 | NSString * const PandoraDidLoadSearchResultsNotification = @"PandoraDidLoadSearchResultsNotification"; 36 | NSString * const PandoraDidLoadGenreStationsNotification = @"PandoraDidLoadGenreStationsNotification"; 37 | 38 | NSString * const StationDidPlaySongNotification = @"StationDidPlaySongNotification"; 39 | -------------------------------------------------------------------------------- /Sources/Pandora/Crypt.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Pandora/Crypt.h 3 | * @brief Implementation of the encryption/decryption of requests to/from 4 | * Pandora 5 | * 6 | * The encryption algorithm used is Blowfish in ECB mode. 7 | */ 8 | 9 | #ifndef CRYPT_H 10 | #define CRYPT_H 11 | 12 | /** 13 | * @brief Encrypt some data for Pandora 14 | * 15 | * @param data the data to encrypt 16 | * @param encryptionKey the encryption key to use 17 | * @return the encrypted data, hex encoded 18 | */ 19 | NSData* PandoraEncryptData(NSData* string, NSString *encryptionKey); 20 | 21 | /** 22 | * @brief Decrypt some data received from Pandora 23 | * 24 | * @param string the hex-encoded string to be decrypted. 25 | * @param decryptionKey the decryption key to use 26 | * @return the decrypted data 27 | */ 28 | NSData* PandoraDecryptString(NSString* string, NSString *decryptionKey); 29 | 30 | #endif /* CRYPT_H */ 31 | -------------------------------------------------------------------------------- /Sources/Pandora/Crypt.m: -------------------------------------------------------------------------------- 1 | #include "blowfish/blowfish.h" 2 | 3 | #import "Crypt.h" 4 | 5 | /* Conversion from hex to int and int to hex */ 6 | static char i2h[16] = "0123456789abcdef"; 7 | static char h2i[256] = { 8 | ['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, ['4'] = 4, ['5'] = 5, ['6'] = 6, 9 | ['7'] = 7, ['8'] = 8, ['9'] = 9, ['a'] = 10, ['b'] = 11, ['c'] = 12, 10 | ['d'] = 13, ['e'] = 14, ['f'] = 15 11 | }; 12 | 13 | static void appendByte(unsigned char byte, void *_data) { 14 | NSMutableData *data = (__bridge NSMutableData *)_data; 15 | [data appendBytes:&byte length:1]; 16 | } 17 | 18 | static void appendHex(unsigned char byte, void *_data) { 19 | NSMutableData *data = (__bridge NSMutableData *)_data; 20 | char bytes[2]; 21 | bytes[1] = i2h[byte % 16]; 22 | bytes[0] = i2h[byte / 16]; 23 | [data appendBytes:bytes length:2]; 24 | } 25 | 26 | NSData* PandoraDecryptString(NSString *string, NSString *decryptionKey) { 27 | struct blf_ecb_ctx ctx; 28 | NSMutableData *mut = [[NSMutableData alloc] init]; 29 | const char *key = decryptionKey.UTF8String; 30 | 31 | Blowfish_ecb_start(&ctx, FALSE, (const unsigned char *)key, 32 | strlen(key), appendByte, 33 | (__bridge void *)mut); 34 | 35 | const char *bytes = [string cStringUsingEncoding:NSASCIIStringEncoding]; 36 | NSUInteger len = [string lengthOfBytesUsingEncoding:NSASCIIStringEncoding]; 37 | for (NSUInteger i = 0; i < len; i += 2) { 38 | Blowfish_ecb_feed(&ctx, h2i[(int) bytes[i]] * 16 + h2i[(int) bytes[i + 1]]); 39 | } 40 | Blowfish_ecb_stop(&ctx); 41 | 42 | return mut; 43 | } 44 | 45 | NSData* PandoraEncryptData(NSData *data, NSString *encryptionKey) { 46 | struct blf_ecb_ctx ctx; 47 | NSMutableData *mut = [[NSMutableData alloc] init]; 48 | const char *key = encryptionKey.UTF8String; 49 | 50 | Blowfish_ecb_start(&ctx, TRUE, (const unsigned char *)key, 51 | strlen(key), appendHex, 52 | (__bridge void*)mut); 53 | 54 | const char *bytes = [data bytes]; 55 | NSUInteger len = [data length]; 56 | for (NSUInteger i = 0; i < len; i++) { 57 | Blowfish_ecb_feed(&ctx, bytes[i]); 58 | } 59 | Blowfish_ecb_stop(&ctx); 60 | 61 | return mut; 62 | } 63 | -------------------------------------------------------------------------------- /Sources/Pandora/PandoraDevice.h: -------------------------------------------------------------------------------- 1 | // 2 | // PandoraDevice.h 3 | // Hermes 4 | // 5 | // Created by Winston Weinert on 4/18/14. 6 | // 7 | // 8 | 9 | 10 | extern NSString * const kPandoraDeviceUsername; 11 | extern NSString * const kPandoraDevicePassword; 12 | extern NSString * const kPandoraDeviceDeviceID; 13 | extern NSString * const kPandoraDeviceEncrypt; 14 | extern NSString * const kPandoraDeviceDecrypt; 15 | extern NSString * const kPandoraDeviceAPIHost; 16 | 17 | 18 | @interface PandoraDevice : NSObject 19 | 20 | + (NSDictionary *)iPhone; 21 | + (NSDictionary *)android; 22 | + (NSDictionary *)desktop; 23 | 24 | @end -------------------------------------------------------------------------------- /Sources/Pandora/PandoraDevice.m: -------------------------------------------------------------------------------- 1 | // 2 | // PandoraDevice.m 3 | // Hermes 4 | // 5 | // Created by Winston Weinert on 4/18/14. 6 | // 7 | // 8 | 9 | #import "PandoraDevice.h" 10 | 11 | 12 | NSString * const kPandoraDeviceUsername = @"username"; 13 | NSString * const kPandoraDevicePassword = @"password"; 14 | NSString * const kPandoraDeviceDeviceID = @"deviceid"; 15 | NSString * const kPandoraDeviceEncrypt = @"encrypt"; 16 | NSString * const kPandoraDeviceDecrypt = @"decrypt"; 17 | NSString * const kPandoraDeviceAPIHost = @"apihost"; 18 | 19 | 20 | @implementation PandoraDevice : NSObject 21 | 22 | + (NSDictionary *)iPhone { 23 | return @{ 24 | kPandoraDeviceUsername: @"iphone", 25 | kPandoraDevicePassword: @"P2E4FC0EAD3*878N92B2CDp34I0B1@388137C", 26 | kPandoraDeviceDeviceID: @"IP01", 27 | kPandoraDeviceEncrypt: @"721^26xE22776", 28 | kPandoraDeviceDecrypt: @"20zE1E47BE57$51", 29 | kPandoraDeviceAPIHost: @"tuner.pandora.com" 30 | }; 31 | } 32 | 33 | + (NSDictionary *)android { 34 | return @{ 35 | kPandoraDeviceUsername: @"android", 36 | kPandoraDevicePassword: @"AC7IBG09A3DTSYM4R41UJWL07VLN8JI7", 37 | kPandoraDeviceDeviceID: @"android-generic", 38 | kPandoraDeviceEncrypt: @"6#26FRL$ZWD", 39 | kPandoraDeviceDecrypt: @"R=U!LH$O2B#", 40 | kPandoraDeviceAPIHost: @"tuner.pandora.com" 41 | }; 42 | } 43 | 44 | + (NSDictionary *)desktop { 45 | return @{ 46 | kPandoraDeviceUsername: @"pandora one", 47 | kPandoraDevicePassword: @"TVCKIBGS9AO9TSYLNNFUML0743LH82D", 48 | kPandoraDeviceDeviceID: @"D01", 49 | kPandoraDeviceEncrypt: @"2%3WCL*JU$MP]4", 50 | kPandoraDeviceDecrypt: @"U#IO$RZPAB%VX2", 51 | kPandoraDeviceAPIHost: @"internal-tuner.pandora.com" 52 | }; 53 | } 54 | 55 | @end 56 | -------------------------------------------------------------------------------- /Sources/Pandora/Song.h: -------------------------------------------------------------------------------- 1 | @class Station; 2 | 3 | @interface Song : NSObject 4 | 5 | @property(nonatomic, retain) NSString *artist; 6 | @property(nonatomic, retain) NSString *title; 7 | @property(nonatomic, retain) NSString *album; 8 | @property(nonatomic, retain) NSString *art; 9 | @property(nonatomic, retain) NSString *stationId; 10 | @property(nonatomic, retain) NSNumber *nrating; 11 | @property(nonatomic, retain) NSString *albumUrl; 12 | @property(nonatomic, retain) NSString *artistUrl; 13 | @property(nonatomic, retain) NSString *titleUrl; 14 | @property(nonatomic, retain) NSString *token; 15 | 16 | @property(nonatomic, retain) NSString *highUrl; 17 | @property(nonatomic, retain) NSString *medUrl; 18 | @property(nonatomic, retain) NSString *lowUrl; 19 | 20 | @property(nonatomic, retain) NSDate *playDate; 21 | @property(readonly) NSString *playDateString; 22 | 23 | - (NSDictionary*) toDictionary; 24 | - (BOOL) isEqual:(id)other; 25 | - (Station*) station; 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /Sources/Pandora/Song.m: -------------------------------------------------------------------------------- 1 | 2 | #import "Station.h" 3 | 4 | @implementation Song 5 | 6 | @synthesize artist, title, album, highUrl, stationId, nrating, 7 | albumUrl, artistUrl, titleUrl, art, token, medUrl, lowUrl, playDate; 8 | 9 | #pragma mark - NSObject 10 | 11 | - (BOOL) isEqual:(id)object { 12 | return [token isEqual:[object token]]; 13 | } 14 | 15 | - (NSString *)description { 16 | return [NSString stringWithFormat:@"<%@ %p %@ - %@>", NSStringFromClass(self.class), self, self.artist, self.title]; 17 | } 18 | 19 | #pragma mark - NSCoding 20 | 21 | - (id) initWithCoder: (NSCoder *)coder { 22 | if ((self = [super init])) { 23 | [self setArtist:[coder decodeObjectForKey:@"artist"]]; 24 | [self setTitle:[coder decodeObjectForKey:@"title"]]; 25 | [self setAlbum:[coder decodeObjectForKey:@"album"]]; 26 | [self setArt:[coder decodeObjectForKey:@"art"]]; 27 | [self setHighUrl:[coder decodeObjectForKey:@"highUrl"]]; 28 | [self setMedUrl:[coder decodeObjectForKey:@"medUrl"]]; 29 | [self setLowUrl:[coder decodeObjectForKey:@"lowUrl"]]; 30 | [self setStationId:[coder decodeObjectForKey:@"stationId"]]; 31 | [self setNrating:[coder decodeObjectForKey:@"nrating"]]; 32 | [self setAlbumUrl:[coder decodeObjectForKey:@"albumUrl"]]; 33 | [self setArtistUrl:[coder decodeObjectForKey:@"artistUrl"]]; 34 | [self setTitleUrl:[coder decodeObjectForKey:@"titleUrl"]]; 35 | [self setToken:[coder decodeObjectForKey:@"token"]]; 36 | [self setPlayDate:[coder decodeObjectForKey:@"playDate"]]; 37 | } 38 | return self; 39 | } 40 | 41 | - (void) encodeWithCoder: (NSCoder *)coder { 42 | NSDictionary *info = [self toDictionary]; 43 | for(id key in info) { 44 | [coder encodeObject:info[key] forKey:key]; 45 | } 46 | } 47 | 48 | #pragma mark - NSDistributedNotification user info 49 | 50 | - (NSDictionary*) toDictionary { 51 | NSMutableDictionary *info = [NSMutableDictionary dictionary]; 52 | [info setValue:artist forKey:@"artist"]; 53 | [info setValue:title forKey:@"title"]; 54 | [info setValue:album forKey:@"album"]; 55 | [info setValue:art forKey:@"art"]; 56 | [info setValue:lowUrl forKey:@"lowUrl"]; 57 | [info setValue:medUrl forKey:@"medUrl"]; 58 | [info setValue:highUrl forKey:@"highUrl"]; 59 | [info setValue:stationId forKey:@"stationId"]; 60 | [info setValue:nrating forKey:@"nrating"]; 61 | [info setValue:albumUrl forKey:@"albumUrl"]; 62 | [info setValue:artistUrl forKey:@"artistUrl"]; 63 | [info setValue:titleUrl forKey:@"titleUrl"]; 64 | [info setValue:token forKey:@"token"]; 65 | [info setValue:playDate forKey:@"playDate"]; 66 | return info; 67 | } 68 | 69 | #pragma mark - Object Specifier 70 | 71 | - (NSScriptObjectSpecifier *) objectSpecifier { 72 | NSScriptClassDescription *appDesc = 73 | [NSScriptClassDescription classDescriptionForClass:[NSApp class]]; 74 | 75 | // currently, the only way to get a reference to a song 76 | // - if the playback history gets exposed, then publish it differently 77 | return [[NSPropertySpecifier alloc] 78 | initWithContainerClassDescription:appDesc containerSpecifier:nil key:@"currentSong"]; 79 | } 80 | 81 | #pragma mark - Reference to station 82 | 83 | - (Station*) station { 84 | return [Station stationForToken:[self stationId]]; 85 | } 86 | 87 | #pragma mark - Formatted play date 88 | 89 | - (NSString *)playDateString { 90 | if (self.playDate == nil) 91 | return nil; 92 | 93 | static NSDateFormatter *songDateFormatter = nil; 94 | if (songDateFormatter == nil) { 95 | songDateFormatter = [[NSDateFormatter alloc] init]; 96 | songDateFormatter.dateStyle = NSDateFormatterShortStyle; 97 | songDateFormatter.timeStyle = NSDateFormatterShortStyle; 98 | songDateFormatter.doesRelativeDateFormatting = YES; 99 | } 100 | 101 | return [songDateFormatter stringFromDate:playDate]; 102 | } 103 | 104 | @end 105 | -------------------------------------------------------------------------------- /Sources/Pandora/Station.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @class Pandora; 4 | @class Song; 5 | 6 | @interface Station : ASPlaylist { 7 | BOOL shouldPlaySongOnFetch; 8 | 9 | NSMutableArray *songs; 10 | Pandora *radio; 11 | } 12 | 13 | @property NSString *name; 14 | @property NSString *token; 15 | @property NSString *stationId; 16 | @property unsigned long long created; 17 | @property Song *playingSong; 18 | @property BOOL shared; 19 | @property BOOL allowRename; 20 | @property BOOL allowAddMusic; // seems that (with the exception of QuickMix, which is excluded from editing elsewhere) that this is not actually a limitation any more; it's possible to add seeds to genre stations (#267). 21 | @property BOOL isQuickMix; 22 | 23 | - (void) setRadio:(Pandora*)radio; 24 | - (NSString*) streamNetworkError; 25 | 26 | + (Station*) stationForToken:(NSString*)token; 27 | + (void) addStation:(Station*)s; 28 | + (void) removeStation:(Station*)s; 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /Sources/Pandora/Station.m: -------------------------------------------------------------------------------- 1 | 2 | #import "Pandora/Station.h" 3 | #import "PreferencesController.h" 4 | #import "StationsController.h" 5 | #import "Notifications.h" 6 | 7 | @implementation Station 8 | 9 | - (id) init { 10 | if (!(self = [super init])) return nil; 11 | 12 | songs = [NSMutableArray arrayWithCapacity:10]; 13 | 14 | [[NSNotificationCenter defaultCenter] 15 | addObserver:self 16 | selector:@selector(fetchMoreSongs:) 17 | name:ASRunningOutOfSongs 18 | object:self]; 19 | [[NSNotificationCenter defaultCenter] 20 | addObserver:self 21 | selector:@selector(fetchMoreSongs:) 22 | name:ASNoSongsLeft 23 | object:self]; 24 | 25 | [[NSNotificationCenter defaultCenter] 26 | addObserver:self 27 | selector:@selector(configureNewStream:) 28 | name:ASCreatedNewStream 29 | object:self]; 30 | 31 | [[NSNotificationCenter defaultCenter] 32 | addObserver:self 33 | selector:@selector(newSongPlaying:) 34 | name:ASNewSongPlaying 35 | object:self]; 36 | 37 | [[NSNotificationCenter defaultCenter] 38 | addObserver:self 39 | selector:@selector(attemptingNewSong:) 40 | name:ASAttemptingNewSong 41 | object:self]; 42 | 43 | return self; 44 | } 45 | 46 | - (id) initWithCoder:(NSCoder *)aDecoder { 47 | if ((self = [self init])) { 48 | [self setStationId:[aDecoder decodeObjectForKey:@"stationId"]]; 49 | [self setName:[aDecoder decodeObjectForKey:@"name"]]; 50 | [self setVolume:[aDecoder decodeFloatForKey:@"volume"]]; 51 | [self setCreated:[aDecoder decodeInt32ForKey:@"created"]]; 52 | [self setToken:[aDecoder decodeObjectForKey:@"token"]]; 53 | [self setShared:[aDecoder decodeBoolForKey:@"shared"]]; 54 | [self setAllowAddMusic:[aDecoder decodeBoolForKey:@"allowAddMusic"]]; 55 | [self setAllowRename:[aDecoder decodeBoolForKey:@"allowRename"]]; 56 | lastKnownSeekTime = [aDecoder decodeFloatForKey:@"lastKnownSeekTime"]; 57 | [songs addObject:[aDecoder decodeObjectForKey:@"playing"]]; 58 | [songs addObjectsFromArray:[aDecoder decodeObjectForKey:@"songs"]]; 59 | [urls addObject:[aDecoder decodeObjectForKey:@"playingURL"]]; 60 | [urls addObjectsFromArray:[aDecoder decodeObjectForKey:@"urls"]]; 61 | if ([songs count] != [urls count]) { 62 | [songs removeAllObjects]; 63 | [urls removeAllObjects]; 64 | } 65 | [Station addStation:self]; 66 | } 67 | return self; 68 | } 69 | 70 | - (void) encodeWithCoder:(NSCoder *)aCoder { 71 | [aCoder encodeObject:_stationId forKey:@"stationId"]; 72 | [aCoder encodeObject:_name forKey:@"name"]; 73 | [aCoder encodeObject:_playingSong forKey:@"playing"]; 74 | double seek = -1; 75 | if (_playingSong) { 76 | [stream progress:&seek]; 77 | } 78 | [aCoder encodeFloat:seek forKey:@"lastKnownSeekTime"]; 79 | [aCoder encodeFloat:volume forKey:@"volume"]; 80 | [aCoder encodeInt32:(int32_t)_created forKey:@"created"]; // XXX truncated? 81 | [aCoder encodeObject:songs forKey:@"songs"]; 82 | [aCoder encodeObject:urls forKey:@"urls"]; 83 | [aCoder encodeObject:[self playing] forKey:@"playingURL"]; 84 | [aCoder encodeObject:_token forKey:@"token"]; 85 | [aCoder encodeBool:_shared forKey:@"shared"]; 86 | [aCoder encodeBool:_allowAddMusic forKey:@"allowAddMusic"]; 87 | [aCoder encodeBool:_allowRename forKey:@"allowRename"]; 88 | } 89 | 90 | - (void) dealloc { 91 | [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:nil]; 92 | } 93 | 94 | - (BOOL) isEqual:(id)object { 95 | return [_stationId isEqual:[object stationId]]; 96 | } 97 | 98 | - (void) attemptingNewSong:(NSNotification*) notification { 99 | _playingSong = songs[0]; 100 | [songs removeObjectAtIndex:0]; 101 | } 102 | 103 | - (void) fetchMoreSongs:(NSNotification*) notification { 104 | shouldPlaySongOnFetch = YES; 105 | [radio fetchPlaylistForStation:self]; 106 | } 107 | 108 | - (void) setRadio:(Pandora *)pandora { 109 | @synchronized(radio) { 110 | if (radio != nil) { 111 | [[NSNotificationCenter defaultCenter] removeObserver:self 112 | name:nil 113 | object:radio]; 114 | } 115 | radio = pandora; 116 | 117 | NSString *n = [NSString stringWithFormat:@"hermes.fragment-fetched.%@", _token]; 118 | 119 | [[NSNotificationCenter defaultCenter] addObserver:self 120 | selector:@selector(songsLoaded:) 121 | name:n 122 | object:nil]; 123 | } 124 | } 125 | 126 | - (void) songsLoaded: (NSNotification*)not { 127 | NSArray *more = [not userInfo][@"songs"]; 128 | NSMutableArray *qualities = [[NSMutableArray alloc] init]; 129 | if (more == nil) return; 130 | 131 | for (Song *s in more) { 132 | NSURL *url = nil; 133 | switch (PREF_KEY_INT(DESIRED_QUALITY)) { 134 | case QUALITY_HIGH: 135 | [qualities addObject:@"high"]; 136 | url = [NSURL URLWithString:[s highUrl]]; 137 | break; 138 | case QUALITY_LOW: 139 | [qualities addObject:@"low"]; 140 | url = [NSURL URLWithString:[s lowUrl]]; 141 | break; 142 | 143 | case QUALITY_MED: 144 | default: 145 | [qualities addObject:@"med"]; 146 | url = [NSURL URLWithString:[s medUrl]]; 147 | break; 148 | } 149 | [urls addObject:url]; 150 | [songs addObject:s]; 151 | } 152 | if (shouldPlaySongOnFetch) { 153 | [self play]; 154 | } 155 | shouldPlaySongOnFetch = NO; 156 | NSLogd(@"Received %@ from %@ with qualities: %@", not.name, not.object, [qualities componentsJoinedByString:@" "]); 157 | } 158 | 159 | - (void) configureNewStream:(NSNotification*) notification { 160 | assert(stream == [notification userInfo][@"stream"]); 161 | [stream setBufferInfinite:TRUE]; 162 | [stream setTimeoutInterval:15]; 163 | 164 | if (PREF_KEY_BOOL(PROXY_AUDIO)) { 165 | switch ([PREF_KEY_VALUE(ENABLED_PROXY) intValue]) { 166 | case PROXY_HTTP: 167 | [stream setHTTPProxy:PREF_KEY_VALUE(PROXY_HTTP_HOST) 168 | port:[PREF_KEY_VALUE(PROXY_HTTP_PORT) intValue]]; 169 | break; 170 | case PROXY_SOCKS: 171 | [stream setSOCKSProxy:PREF_KEY_VALUE(PROXY_SOCKS_HOST) 172 | port:[PREF_KEY_VALUE(PROXY_SOCKS_PORT) intValue]]; 173 | break; 174 | default: 175 | break; 176 | } 177 | } 178 | } 179 | 180 | - (void) newSongPlaying:(NSNotification*) notification { 181 | assert([songs count] == [urls count]); 182 | [[NSNotificationCenter defaultCenter] 183 | postNotificationName:StationDidPlaySongNotification 184 | object:self 185 | userInfo:nil]; 186 | } 187 | 188 | - (NSString*) streamNetworkError { 189 | if ([stream errorCode] == AS_NETWORK_CONNECTION_FAILED) { 190 | return [[stream networkError] localizedDescription]; 191 | } 192 | return [AudioStreamer stringForErrorCode:[stream errorCode]]; 193 | } 194 | 195 | - (NSScriptObjectSpecifier *) objectSpecifier { 196 | HermesAppDelegate *delegate = HMSAppDelegate; 197 | StationsController *stationsc = [delegate stations]; 198 | int index = [stationsc stationIndex:self]; 199 | 200 | NSScriptClassDescription *containerClassDesc = 201 | [NSScriptClassDescription classDescriptionForClass:[NSApp class]]; 202 | 203 | return [[NSIndexSpecifier alloc] 204 | initWithContainerClassDescription:containerClassDesc 205 | containerSpecifier:nil key:@"stations" index:index]; 206 | } 207 | 208 | - (void) clearSongList { 209 | [songs removeAllObjects]; 210 | [super clearSongList]; 211 | } 212 | 213 | static NSMutableDictionary *stations = nil; 214 | 215 | + (Station*) stationForToken:(NSString*)stationId{ 216 | if (stations == nil) 217 | return nil; 218 | return stations[stationId]; 219 | } 220 | 221 | + (void) addStation:(Station*) s { 222 | if (stations == nil) { 223 | stations = [NSMutableDictionary dictionary]; 224 | } 225 | stations[[s stationId]] = s; 226 | } 227 | 228 | + (void) removeStation:(Station*) s { 229 | if (stations == nil) 230 | return; 231 | [stations removeObjectForKey:[s stationId]]; 232 | } 233 | 234 | @end 235 | -------------------------------------------------------------------------------- /Sources/URLConnection.h: -------------------------------------------------------------------------------- 1 | typedef void(^URLConnectionCallback)(NSData*, NSError*); 2 | 3 | extern NSString * const URLConnectionProxyValidityChangedNotification; 4 | 5 | @interface URLConnection : NSObject { 6 | CFReadStreamRef stream; 7 | URLConnectionCallback cb; 8 | NSMutableData *bytes; 9 | NSTimer *timeout; 10 | int events; 11 | } 12 | 13 | + (URLConnection*) connectionForRequest:(NSURLRequest*)request 14 | completionHandler:(URLConnectionCallback) cb; 15 | + (void) setHermesProxy: (CFReadStreamRef) stream; 16 | + (BOOL) validProxyHost:(NSString **)host port:(NSInteger)port; 17 | 18 | - (void) start; 19 | - (void) setHermesProxy; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Sources/Views/HermesBackgroundView.h: -------------------------------------------------------------------------------- 1 | // 2 | // HermesBackgroundView.h 3 | // Hermes 4 | // 5 | // Created by Nicholas Riley on 9/9/16. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface HermesBackgroundView : NSView 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Sources/Views/HermesBackgroundView.m: -------------------------------------------------------------------------------- 1 | // 2 | // HermesBackgroundView.m 3 | // Hermes 4 | // 5 | // Created by Nicholas Riley on 9/9/16. 6 | // 7 | // 8 | 9 | #import "HermesBackgroundView.h" 10 | 11 | @implementation HermesBackgroundView 12 | 13 | - (void)drawRect:(NSRect)dirtyRect { 14 | // work around ugly drawer background on 10.10 15 | // - doesn't seem to match any of the standard Apple system colors... 16 | if (NSAppKitVersionNumber <= NSAppKitVersionNumber10_10_Max) { 17 | [[NSColor colorWithGenericGamma22White:241/255. alpha:1] setFill]; 18 | NSRectFill(dirtyRect); 19 | } 20 | } 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /Sources/Views/HermesMainWindow.h: -------------------------------------------------------------------------------- 1 | // 2 | // HermesMainWindow.h 3 | // Hermes 4 | // 5 | // Created by Nicholas Riley on 9/10/16. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface HermesMainWindow : NSWindow 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Sources/Views/HermesMainWindow.m: -------------------------------------------------------------------------------- 1 | // 2 | // HermesMainWindow.m 3 | // Hermes 4 | // 5 | // Created by Nicholas Riley on 9/10/16. 6 | // 7 | // 8 | 9 | #import "HermesMainWindow.h" 10 | 11 | @implementation HermesMainWindow 12 | 13 | - (void)sendEvent:(NSEvent *)theEvent { 14 | if ([theEvent type] == NSKeyDown) { 15 | 16 | // don't ever let space bar get through to the field editor so it can be used for play/pause 17 | if ([[theEvent characters] isEqualToString:@" "] && ([theEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask) == 0) { 18 | [[NSApp mainMenu] performKeyEquivalent:theEvent]; 19 | return; 20 | } 21 | } 22 | [super sendEvent:theEvent]; 23 | } 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /Sources/Views/HermesVolumeSliderCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // HermesVolumeSliderCell.h 3 | // Hermes 4 | // 5 | // Created by Nicholas Riley on 9/9/16. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface HermesVolumeSliderCell : NSSliderCell 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Sources/Views/HermesVolumeSliderCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // HermesVolumeSliderCell.m 3 | // Hermes 4 | // 5 | // Created by Nicholas Riley on 9/9/16. 6 | // 7 | // 8 | 9 | #import "HermesVolumeSliderCell.h" 10 | 11 | @implementation HermesVolumeSliderCell 12 | 13 | // based upon http://stackoverflow.com/a/29828476/6372 14 | - (void)drawBarInside:(NSRect)rect flipped:(BOOL)flipped { 15 | rect = NSInsetRect(rect, 0, 1); 16 | 17 | CGFloat radius = rect.size.height / 2; 18 | CGFloat proportion = (self.doubleValue - self.minValue) / (self.maxValue - self.minValue); 19 | 20 | CGFloat leftWidth = proportion * ([[self controlView] frame].size.width - 8); 21 | 22 | NSRect leftRect = rect; 23 | leftRect.size.width = leftWidth; 24 | 25 | NSBezierPath *bar = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:radius yRadius:radius]; 26 | [[NSColor colorWithGenericGamma22White:171/255. alpha:1] setFill]; 27 | [bar fill]; 28 | 29 | NSBezierPath *barLeft = [NSBezierPath bezierPathWithRoundedRect: leftRect xRadius:radius yRadius:radius]; 30 | [[NSColor colorWithGenericGamma22White:103/255. alpha:1] setFill]; 31 | [barLeft fill]; 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /Sources/Views/HistoryCollectionView.h: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryCollectionView.h 3 | // Hermes 4 | // 5 | // Created by Winston Weinert on 1/1/14. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface HistoryCollectionView : NSCollectionView 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Sources/Views/HistoryCollectionView.m: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryCollectionView.m 3 | // Hermes 4 | // 5 | // Created by Winston Weinert on 1/1/14. 6 | // 7 | // 8 | 9 | #import "HistoryCollectionView.h" 10 | #import "PlaybackController.h" 11 | 12 | @implementation HistoryCollectionView 13 | 14 | - (void)keyDown:(NSEvent *)theEvent { 15 | if ([[theEvent characters] isEqualToString:@" "]) { 16 | [[HMSAppDelegate playback] playpause:nil]; 17 | } else { 18 | [super keyDown:theEvent]; 19 | } 20 | } 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /Sources/Views/HistoryView.h: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryView.h 3 | // Hermes 4 | // 5 | // Created by Alex Crichton on 6/29/12. 6 | // 7 | 8 | @interface HistoryView : NSView 9 | 10 | @property BOOL selected; 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /Sources/Views/HistoryView.m: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryView.m 3 | // Hermes 4 | // 5 | // Created by Alex Crichton on 6/29/12. 6 | // 7 | 8 | #import "HistoryView.h" 9 | 10 | @implementation HistoryView 11 | 12 | @synthesize selected; 13 | 14 | - (NSView *)hitTest:(NSPoint)aPoint { 15 | // don't allow any mouse clicks for subviews 16 | return nil; 17 | } 18 | 19 | - (void)setTextColor:(NSColor *)color { 20 | for (NSView *view in [self subviews]) { 21 | if ([view respondsToSelector:@selector(setTextColor:)]) { 22 | [(id)view setTextColor:color]; 23 | } 24 | } 25 | } 26 | 27 | - (void)drawRect:(NSRect)dirtyRect { 28 | // Don't allow partial redraws (e.g. when switching drawer): it produces artifacts 29 | if (!NSEqualPoints(dirtyRect.origin, NSZeroPoint)) { 30 | [self setNeedsDisplay:YES]; 31 | } 32 | if (selected) { 33 | if ([[self window] firstResponder] != [self superview]) { 34 | [[NSColor secondarySelectedControlColor] set]; 35 | [self setTextColor:[NSColor controlTextColor]]; 36 | } else { 37 | [[NSColor alternateSelectedControlColor] set]; 38 | [self setTextColor:[NSColor alternateSelectedControlTextColor]]; 39 | } 40 | NSRectFill([self bounds]); 41 | } else { 42 | [self setTextColor:[NSColor controlTextColor]]; 43 | } 44 | } 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /Sources/Views/LabelHoverShowField.h: -------------------------------------------------------------------------------- 1 | // 2 | // LabelHoverShowField.h 3 | // Hermes 4 | // 5 | // Created by Nicholas Riley on 5/22/16. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface LabelHoverShowField : NSTextField 12 | { 13 | NSTrackingArea *_labelTrackingArea; 14 | } 15 | 16 | @property (nonatomic) IBOutlet NSView *hoverView; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /Sources/Views/LabelHoverShowField.m: -------------------------------------------------------------------------------- 1 | // 2 | // LabelHoverShowField.m 3 | // Hermes 4 | // 5 | // Created by Nicholas Riley on 5/22/16. 6 | // 7 | // 8 | 9 | #import "LabelHoverShowField.h" 10 | #import "LabelHoverShowFieldCell.h" 11 | 12 | @implementation LabelHoverShowField 13 | 14 | + (void)initialize { 15 | [self setCellClass:[LabelHoverShowFieldCell class]]; 16 | } 17 | 18 | - (void)mouseEntered:(NSEvent *)theEvent { 19 | [super mouseEntered:theEvent]; 20 | [_hoverView setHidden:NO]; 21 | } 22 | 23 | - (void)mouseExited:(NSEvent *)theEvent { 24 | [super mouseExited:theEvent]; 25 | [_hoverView setHidden:YES]; 26 | } 27 | 28 | - (void)resetCursorRects { 29 | if (_hoverView != nil) { 30 | NSRect hoverViewRect = [self convertRect:_hoverView.bounds fromView:_hoverView]; 31 | [self addCursorRect:hoverViewRect cursor:[NSCursor arrowCursor]]; 32 | NSRect boundsRect = self.bounds; 33 | NSRect intersectionRect = NSIntersectionRect(boundsRect, hoverViewRect); 34 | if (!NSIsEmptyRect(intersectionRect)) { 35 | if (intersectionRect.origin.x == 0) 36 | boundsRect.origin.x += intersectionRect.size.width; 37 | boundsRect.size.width -= intersectionRect.size.width; 38 | } 39 | [self addCursorRect:boundsRect cursor:[NSCursor IBeamCursor]]; 40 | } else { 41 | [super resetCursorRects]; 42 | } 43 | } 44 | 45 | - (void)updateTrackingAreas { 46 | if (_labelTrackingArea != nil) [self removeTrackingArea:_labelTrackingArea]; 47 | 48 | _labelTrackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] 49 | options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways 50 | owner:self 51 | userInfo:nil]; 52 | [self addTrackingArea:_labelTrackingArea]; 53 | } 54 | 55 | @end 56 | -------------------------------------------------------------------------------- /Sources/Views/LabelHoverShowFieldCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // LabelHoverShowFieldCell.h 3 | // Hermes 4 | // 5 | // Created by Nicholas Riley on 5/22/16. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface LabelHoverShowFieldCell : NSTextFieldCell 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Sources/Views/LabelHoverShowFieldCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // LabelHoverShowFieldCell.m 3 | // Hermes 4 | // 5 | // Created by Nicholas Riley on 5/22/16. 6 | // 7 | // 8 | 9 | #import "LabelHoverShowFieldCell.h" 10 | #import "LabelHoverShowField.h" 11 | 12 | @implementation LabelHoverShowFieldCell 13 | 14 | - (NSRect)drawingRectForBounds:(NSRect)theRect { 15 | NSRect drawingRect = [super drawingRectForBounds:theRect]; 16 | 17 | NSView *hoverView = ((LabelHoverShowField *)self.controlView).hoverView; 18 | if (hoverView != nil) { 19 | CGFloat hoverViewWidth = hoverView.frame.size.width; 20 | drawingRect.origin.x += hoverViewWidth; 21 | drawingRect.size.width -= 2 * hoverViewWidth; 22 | } 23 | 24 | return drawingRect; 25 | } 26 | 27 | - (void)editWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(nullable id)anObject event:(NSEvent *)theEvent { 28 | [[self controlView] setNeedsDisplay:YES]; 29 | aRect = NSInsetRect([self drawingRectForBounds:controlView.bounds], 3, 0); 30 | // despite passing smaller rect to super, it ends up too wide the first time unless we set it explicitly 31 | NSDisableScreenUpdates(); // to prevent flashing of wider rect 32 | [super editWithFrame:aRect inView:controlView editor:textObj delegate:anObject event:theEvent]; 33 | [textObj setFrameSize:aRect.size]; 34 | NSEnableScreenUpdates(); 35 | } 36 | 37 | - (void)selectWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(nullable id)anObject start:(NSInteger)selStart length:(NSInteger)selLength { 38 | aRect = NSInsetRect([self drawingRectForBounds:controlView.bounds], 3, 0); 39 | // despite passing smaller rect to super, it ends up too wide the first time unless we set it explicitly 40 | NSDisableScreenUpdates(); // to prevent flashing of wider rect 41 | [super selectWithFrame:aRect inView:controlView editor:textObj delegate:anObject start:selStart length:selLength]; 42 | [textObj setFrameSize:aRect.size]; 43 | NSEnableScreenUpdates(); 44 | } 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /Sources/Views/MusicProgressSliderCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // MusicProgressSliderCell.h 3 | // Hermes 4 | // 5 | // Created by Xinhong LIU on 19/4/15. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface MusicProgressSliderCell : NSSliderCell 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Sources/Views/MusicProgressSliderCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // MusicProgressSliderCell.m 3 | // Hermes 4 | // 5 | // Created by Xinhong LIU on 19/4/15. 6 | // 7 | // 8 | 9 | #import "MusicProgressSliderCell.h" 10 | 11 | @implementation MusicProgressSliderCell 12 | 13 | - (NSRect)knobRectFlipped:(BOOL)flipped { 14 | CGPoint sliderOrigin = [self barRectFlipped:false].origin; 15 | CGSize knobSize = CGSizeMake(2.0, 8.0); // this value is measured in iTunes App 16 | CGSize sliderSize = [self barRectFlipped:false].size; 17 | 18 | CGPoint knobOrigin; 19 | // truncf is important to make knob's border clear and sharp. 20 | knobOrigin.x = truncf(sliderOrigin.x + [self progressInPercentage] 21 | * (sliderSize.width - knobSize.width)); 22 | knobOrigin.y = sliderOrigin.y + sliderSize.height - knobSize.height; 23 | 24 | NSRect knobRect = NSMakeRect(knobOrigin.x, knobOrigin.y, knobSize.width, knobSize.height); 25 | return knobRect; 26 | } 27 | 28 | - (CGFloat)progressInPercentage { 29 | return (self.doubleValue - self.minValue) / (self.maxValue - self.minValue); 30 | } 31 | 32 | - (void)drawKnob:(NSRect)knobRect { 33 | // Don't draw knob if it can't be dragged. 34 | // If we do support seeking at some point, would be better to ape the miniplayer, with symmetric knob 35 | #if 0 36 | [[self knobColor] setFill]; 37 | NSRectFill(knobRect); */ 38 | #endif 39 | } 40 | 41 | - (NSRect)barRectFlipped:(BOOL)flipped { 42 | NSRect barRect = [super barRectFlipped:flipped]; 43 | barRect = NSInsetRect(barRect, -1, 0); 44 | 45 | CGFloat barHeight = 4.0; // this value is measured in iTunes App 46 | barRect.origin.y += (barRect.size.height - barHeight); 47 | barRect.size.height = barHeight; 48 | 49 | return barRect; 50 | } 51 | 52 | - (void)drawBarInside:(NSRect)barRect flipped:(BOOL)flipped { 53 | NSRect knobRect = [self knobRectFlipped:false]; 54 | 55 | NSRect leftBarRect = barRect; 56 | leftBarRect.size.width = knobRect.origin.x - barRect.origin.x; 57 | [[self leftBarColor] setFill]; 58 | NSRectFill(leftBarRect); 59 | 60 | NSRect rightBarRect = barRect; 61 | rightBarRect.origin.x = knobRect.origin.x; 62 | rightBarRect.size.width = barRect.origin.x + barRect.size.width - knobRect.origin.x; 63 | [[self rightBarColor] setFill]; 64 | NSRectFill(rightBarRect); 65 | } 66 | 67 | // colors from iTunes 12.4 68 | - (NSColor *)knobColor { return [NSColor blackColor]; } 69 | - (NSColor *)leftBarColor { return [NSColor colorWithGenericGamma22White:112/255. alpha:1]; } 70 | - (NSColor *)rightBarColor { return [NSColor colorWithGenericGamma22White:188/255. alpha:1]; } 71 | 72 | @end 73 | -------------------------------------------------------------------------------- /Sources/Views/NSDrawerWindow-HermesFirstResponderWorkaround.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSDrawerWindow-HermesFirstResponderWorkaround.m 3 | // Hermes 4 | // 5 | // Created by Nicholas Riley on 1/14/17. 6 | // 7 | // 8 | 9 | #import 10 | 11 | // Work around regression in macOS 10.12 related to setting first responders in drawers. 12 | // Based on code from . 13 | 14 | @interface NSWindow (HermesFirstResponderWorkaround) 15 | - (void)_setFirstResponder:(NSResponder *)responder; 16 | @end 17 | 18 | @interface NSDrawerWindow : NSWindow 19 | @end 20 | 21 | @implementation NSDrawerWindow (HermesFirstResponderWorkaround) 22 | - (void)_setFirstResponder:(NSResponder *)responder { 23 | if (![responder isKindOfClass:[NSView class]] || [(NSView *)responder window] == self) 24 | [super _setFirstResponder:responder]; 25 | } 26 | @end 27 | -------------------------------------------------------------------------------- /Sources/Views/StationsTableView.h: -------------------------------------------------------------------------------- 1 | // 2 | // StationsTableView.h 3 | // Hermes 4 | // 5 | // Created by Nicholas Riley on 9/14/13. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface StationsTableView : NSTableView 12 | { 13 | IBOutlet NSButton *playButton; 14 | } 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Sources/Views/StationsTableView.m: -------------------------------------------------------------------------------- 1 | // 2 | // StationsTableView.m 3 | // Hermes 4 | // 5 | // Created by Nicholas Riley on 9/14/13. 6 | // 7 | // 8 | 9 | #import "StationsTableView.h" 10 | 11 | @implementation StationsTableView 12 | 13 | - (void)keyDown:(NSEvent *)theEvent { 14 | if ([[theEvent characters] isEqualToString:@"\r"]) { 15 | [playButton performClick:self]; 16 | } else { 17 | [super keyDown:theEvent]; 18 | } 19 | } 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Sources/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Pithos 4 | // 5 | // Created by Alex Crichton on 3/11/11. 6 | // 7 | 8 | int main(int argc, char *argv[]) { 9 | return NSApplicationMain(argc, (const char **) argv); 10 | } 11 | --------------------------------------------------------------------------------