Skip to content

Commit

Permalink
Added LastFm scrobbling. Updated README for details.
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesFator committed Feb 25, 2014
1 parent 7322231 commit 118330d
Show file tree
Hide file tree
Showing 28 changed files with 7,288 additions and 30 deletions.
146 changes: 144 additions & 2 deletions Google Music.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions GoogleMusic/AppDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,15 @@
@property (assign) IBOutlet NSWindow *window;
@property (nonatomic, retain) IBOutlet CustomWebView *webView;
@property (assign) NSUserDefaults *defaults;
@property (weak) IBOutlet NSButton *loginButton;
@property (weak) IBOutlet NSTextField *usernameField;
@property (weak) IBOutlet NSSecureTextField *passwordField;

- (void) playPause;
- (void) forwardAction;
- (void) backAction;

// Preferences
- (IBAction)initLastFM:(id)sender;

@end
158 changes: 150 additions & 8 deletions GoogleMusic/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
//

#import "AppDelegate.h"
#import "LastFm/LastFm.h"
#import "SSKeychain.h"

#define SESSION_KEY @"lastfm.session"
#define USERNAME_KEY @"lastfm.username"
#define NOTIF_ENABLED_KEY @"notifications.enabled"
#define LASTFM_ENABLED_KEY @"lastfm.enabled"

static NSString *kServiceName = @"GoogleMusicMac";

@implementation AppDelegate

Expand Down Expand Up @@ -63,6 +72,9 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
NSURL *url = [NSURL URLWithString:@"https://play.google.com/music"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[[webView mainFrame] loadRequest:request];

// Initialize LastFm
[self initLastFM:self];
}

#pragma mark - Event tap methods
Expand Down Expand Up @@ -103,9 +115,8 @@ static CGEventRef event_tap_callback(CGEventTapProxy proxy,
int keyFlags = ([keyEvent data1] & 0x0000FFFF);
int keyState = (((keyFlags & 0xFF00) >> 8)) == 0xA;

OSStatus err = noErr;
ProcessSerialNumber psn;
err = GetProcessForPID([[NSProcessInfo processInfo] processIdentifier], &psn);
GetProcessForPID([[NSProcessInfo processInfo] processIdentifier], &psn);

switch( keyCode )
{
Expand Down Expand Up @@ -182,7 +193,7 @@ - (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame
/**
* evaluateJavaScriptFile will load the JS file and execute it in the webView.
*/
- (void)evaluateJavaScriptFile:(NSString *)name
- (void)evaluateJavaScriptFile:(NSString*)name
{
NSString *file = [NSString stringWithFormat:@"js/%@", name];
NSString *path = [[NSBundle mainBundle] pathForResource:file ofType:@"js"];
Expand All @@ -195,10 +206,11 @@ - (void)evaluateJavaScriptFile:(NSString *)name
* notifySong is called when the song is updated. We use this to either
* bring up a standard OSX notification or a 3rd party notification.
*/
- (void)notifySong:(NSString *)title withArtist:(NSString *)artist
album:(NSString *)album art:(NSString *)art
- (void)notifySong:(NSString*)title withArtist:(NSString*)artist
album:(NSString*)album art:(NSString*)art time:(NSString*)time
{
if ([_defaults boolForKey:@"notifications.enabled"]) {
// Notification Center
if ([_defaults boolForKey:NOTIF_ENABLED_KEY]) {
NSUserNotification *notif = [[NSUserNotification alloc] init];
notif.title = title;
notif.informativeText = [NSString stringWithFormat:@"%@%@", artist, album];
Expand All @@ -217,14 +229,36 @@ - (void)notifySong:(NSString *)title withArtist:(NSString *)artist
// Deliver the notification.
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notif];
}
// Last.fm
if ([_defaults boolForKey:LASTFM_ENABLED_KEY]) {
NSTimeInterval duration = 0;
if (![time isEqualToString:@"Unknown"]) {
// If we have a time, convert to time interval
NSArray *timeSplit = [time componentsSeparatedByString:@":"];
@try {
duration += [timeSplit[0] intValue] * 60; // 60 seconds in minute
duration += [timeSplit[1] intValue]; // seconds
}
@catch (NSException *exception) {}
}
[[LastFm sharedInstance] sendScrobbledTrack:title byArtist:artist
onAlbum:album withDuration:duration
atTimestamp:(int)[[NSDate date] timeIntervalSince1970]
successHandler:^(NSDictionary *result)
{
// NSLog(@"result: %@", result);
} failureHandler:^(NSError *error) {
// NSLog(@"error: %@", error);
}];
}
}

/**
* webScriptNameForSelector will help the JS components access the notify selector.
*/
+ (NSString*)webScriptNameForSelector:(SEL)sel
{
if (sel == @selector(notifySong:withArtist:album:art:))
if (sel == @selector(notifySong:withArtist:album:art:time:))
return @"notifySong";

return nil;
Expand All @@ -235,10 +269,118 @@ + (NSString*)webScriptNameForSelector:(SEL)sel
*/
+ (BOOL)isSelectorExcludedFromWebScript:(SEL)sel
{
if (sel == @selector(notifySong:withArtist:album:art:))
if (sel == @selector(notifySong:withArtist:album:art:time:))
return NO;

return YES;
}

# pragma mark - Last.fm

/**
* initLastFM will start and stop the LastFm sharedInstance and log in if we can.
*/
- (IBAction)initLastFM:(id)sender
{
if ([_defaults boolForKey:LASTFM_ENABLED_KEY]) {
// Set the Last.fm session info
// THESE NEED TO BE REPLACED WITH DEVELOPER API CREDENTIALS
[LastFm sharedInstance].apiKey = @"xxx";
[LastFm sharedInstance].apiSecret = @"xxx";

[_loginButton setEnabled:YES];
[[LastFm sharedInstance] getSessionInfoWithSuccessHandler:^(NSDictionary *result) {
// Log in if we're already logged in
[_loginButton setTitle:[NSString stringWithFormat:@"Logout %@", result[@"name"]]];
[_loginButton setAction:@selector(logout)];
[self enableLoginForm:NO];
} failureHandler:^(NSError *error) {
// No, show login form
[self enableLoginForm:YES];
[_loginButton setTitle:@"Login"];
[_loginButton setAction:@selector(login)];
[self login];
}];
} else {
[self enableLoginForm:NO];
[_loginButton setTitle:@"Login"];
[_loginButton setAction:@selector(login)];
[_loginButton setEnabled:NO];
// Remove the securely stored password
NSString *username = [[NSUserDefaults standardUserDefaults] objectForKey:USERNAME_KEY];
[SSKeychain deletePasswordForService:kServiceName account:username];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:USERNAME_KEY];
}
}

/**
* enableLoginForm will enable/disable the username/password fields.
* @param hide - whether or not we want to enable the fields
*/
- (void)enableLoginForm:(BOOL)hide
{
[_usernameField setEnabled:hide];
[_passwordField setEnabled:hide];
}

/**
* login will sign the user into LastFm scrobbling.
*/
- (void)login
{
NSString *username = _usernameField.stringValue;
NSString *password = _passwordField.stringValue;
if ([_passwordField.stringValue length] == 0) {
// Test if saved securely
username = [[NSUserDefaults standardUserDefaults] objectForKey:USERNAME_KEY];
if (username) {
_usernameField.stringValue = username;
password = [SSKeychain passwordForService:kServiceName account:username];
} else {
username = @"";
}
}
[[LastFm sharedInstance] getSessionForUser:username
password:password
successHandler:^(NSDictionary *result)
{
// Save the session into NSUserDefaults. It is loaded on app start up in AppDelegate.
[[NSUserDefaults standardUserDefaults] setObject:result[@"key"] forKey:SESSION_KEY];
[[NSUserDefaults standardUserDefaults] setObject:result[@"name"] forKey:USERNAME_KEY];
[[NSUserDefaults standardUserDefaults] synchronize];

// Also set the session of the LastFm object
[LastFm sharedInstance].session = result[@"key"];
[LastFm sharedInstance].username = result[@"name"];

// Show the logout button
[self.loginButton setTitle:[NSString stringWithFormat:@"Logout %@", result[@"name"]]];
[self.loginButton setAction:@selector(logout)];
[self enableLoginForm:NO];

// Store the credentials in Keychain securely
[SSKeychain setPassword:password forService:kServiceName account:username];
} failureHandler:^(NSError *error) {
// TODO: Error message
}];
// Clear the password
[_passwordField setStringValue:@""];
}

/**
* logout will sign the user out of LastFm scrobbling.
*/
- (void)logout
{
[self enableLoginForm:YES];
[_loginButton setTitle:@"Login"];
[_loginButton setAction:@selector(login)];
[[LastFm sharedInstance] logout];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:SESSION_KEY];
// Remove the securely stored password
NSString *username = [[NSUserDefaults standardUserDefaults] objectForKey:USERNAME_KEY];
[SSKeychain deletePasswordForService:kServiceName account:username];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:USERNAME_KEY];
}

@end
24 changes: 24 additions & 0 deletions GoogleMusic/KissXML/Additions/DDXMLElementAdditions.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#import <Foundation/Foundation.h>
#import "DDXML.h"

// These methods are not part of the standard NSXML API.
// But any developer working extensively with XML will likely appreciate them.

@interface DDXMLElement (DDAdditions)

+ (DDXMLElement *)elementWithName:(NSString *)name xmlns:(NSString *)ns;

- (DDXMLElement *)elementForName:(NSString *)name;
- (DDXMLElement *)elementForName:(NSString *)name xmlns:(NSString *)xmlns;

- (NSString *)xmlns;
- (void)setXmlns:(NSString *)ns;

- (NSString *)prettyXMLString;
- (NSString *)compactXMLString;

- (void)addAttributeWithName:(NSString *)name stringValue:(NSString *)string;

- (NSDictionary *)attributesAsDictionary;

@end
131 changes: 131 additions & 0 deletions GoogleMusic/KissXML/Additions/DDXMLElementAdditions.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#import "DDXMLElementAdditions.h"

@implementation DDXMLElement (DDAdditions)

/**
* Quick method to create an element
**/
+ (DDXMLElement *)elementWithName:(NSString *)name xmlns:(NSString *)ns
{
DDXMLElement *element = [DDXMLElement elementWithName:name];
[element setXmlns:ns];
return element;
}

/**
* This method returns the first child element for the given name.
* If no child element exists for the given name, returns nil.
**/
- (DDXMLElement *)elementForName:(NSString *)name
{
NSArray *elements = [self elementsForName:name];
if([elements count] > 0)
{
return [elements objectAtIndex:0];
}
else
{
// Note: If you port this code to work with Apple's NSXML, beware of the following:
//
// There is a bug in the NSXMLElement elementsForName: method.
// Consider the following XML fragment:
//
// <query xmlns="jabber:iq:private">
// <x xmlns="some:other:namespace"></x>
// </query>
//
// Calling [query elementsForName:@"x"] results in an empty array!
//
// However, it will work properly if you use the following:
// [query elementsForLocalName:@"x" URI:@"some:other:namespace"]
//
// The trouble with this is that we may not always know the xmlns in advance,
// so in this particular case there is no way to access the element without looping through the children.
//
// This bug was submitted to apple on June 1st, 2007 and was classified as "serious".
//
// --!!-- This bug does NOT exist in DDXML --!!--

return nil;
}
}

/**
* This method returns the first child element for the given name and given xmlns.
* If no child elements exist for the given name and given xmlns, returns nil.
**/
- (DDXMLElement *)elementForName:(NSString *)name xmlns:(NSString *)xmlns
{
NSArray *elements = [self elementsForLocalName:name URI:xmlns];
if([elements count] > 0)
{
return [elements objectAtIndex:0];
}
else
{
return nil;
}
}

/**
* Returns the common xmlns "attribute", which is only accessible via the namespace methods.
* The xmlns value is often used in jabber elements.
**/
- (NSString *)xmlns
{
return [[self namespaceForPrefix:@""] stringValue];
}

- (void)setXmlns:(NSString *)ns
{
// If you use setURI: then the xmlns won't be displayed in the XMLString.
// Adding the namespace this way works properly.
//
// This applies to both Apple's NSXML and DDXML.

[self addNamespace:[DDXMLNode namespaceWithName:@"" stringValue:ns]];
}

/**
* Shortcut to get a pretty (formatted) string representation of the element.
**/
- (NSString *)prettyXMLString
{
return [self XMLStringWithOptions:(DDXMLNodePrettyPrint | DDXMLNodeCompactEmptyElement)];
}

/**
* Shortcut to get a compact string representation of the element.
**/
- (NSString *)compactXMLString
{
return [self XMLStringWithOptions:DDXMLNodeCompactEmptyElement];
}

/**
* Shortcut to avoid having to manually create a DDXMLNode everytime.
**/
- (void)addAttributeWithName:(NSString *)name stringValue:(NSString *)string
{
[self addAttribute:[DDXMLNode attributeWithName:name stringValue:string]];
}

/**
* Returns all the attributes as a dictionary.
**/
- (NSDictionary *)attributesAsDictionary
{
NSArray *attributes = [self attributes];
NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:[attributes count]];

uint i;
for(i = 0; i < [attributes count]; i++)
{
DDXMLNode *node = [attributes objectAtIndex:i];

[result setObject:[node stringValue] forKey:[node name]];
}
return result;
}

@end
Loading

0 comments on commit 118330d

Please sign in to comment.