n1mda / sonos-objc

An Objective-C API for controlling Sonos Devices
MIT License
20 stars 24 forks source link

Issue: Sonos Play:1 Not found #6

Open loretoparisi opened 8 years ago

loretoparisi commented 8 years ago

I have a Sonos Play:1 on the same WiFi network. I registered the observer and waited for changes, but nothing happened:

Code snippet:

-(IBAction)discover:(id)sender {
    [[SonosManager sharedInstance] addObserver:self forKeyPath:@"allDevices" options:NSKeyValueObservingOptionNew context:NULL];
}

pragma Sonos Delegates

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    SonosController *controller = [[SonosManager sharedInstance] currentDevice];
    [controller trackInfo:^(NSString *artist, NSString *title, NSString *album, NSURL *albumArt, NSInteger time, NSInteger duration, NSInteger queueIndex, NSString *trackURI, NSString *protocol, NSError *error){

        NSLog(@"Artist: %@", artist);
        NSLog(@"Title: %@", title);
        NSLog(@"Album: %@", album);
        NSLog(@"Album Art: %@", albumArt);
        NSLog(@"Time: %d", (int)time);
        NSLog(@"Duration: %d", (int)duration);
        NSLog(@"Place in queue: %d", (int)queueIndex);
        NSLog(@"Track URI: %@", trackURI);
        NSLog(@"Protocol: %@", protocol);

    }];
}

I can see the Sonos PLAY:1 from the Sonos app and from DLNA compatible devices.

n1mda commented 8 years ago

Hi @loretoparisi

sonos-objc hasn't been updated in a while so some features might be broken. I haven't had time, but I'd love to get back to it.

IIRC the README is outdated, and you should discover devices using [SonosDiscover discoverControllers:^(NSArray *controllers, NSError *error) {}]

loretoparisi commented 8 years ago

Hello @n1mda ! Thanks for the hint.

So I did a simple fix to make it work again :)

As you suggested now you have to do like this

[SonosDiscover discoverControllers:^(NSArray *controllers, NSError *error) {

        if( [controllers count] > 0) {
            NSDictionary *controllerInfo = [controllers objectAtIndex:0];
            SonosController *controller  = [[SonosController alloc] initWithIP:[controllerInfo objectForKey:@"ip"] port:[[controllerInfo objectForKey:@"port"] intValue]];
            [controller trackInfo:^(NSString *artist, NSString *title, NSString *album, NSURL *albumArt, NSInteger time, NSInteger duration, NSInteger queueIndex, NSString *trackURI, NSString *protocol, NSError *error){

                NSLog(@"Artist: %@", artist);
                NSLog(@"Title: %@", title);
                NSLog(@"Album: %@", album);
                NSLog(@"Album Art: %@", albumArt);
                NSLog(@"Time: %d", (int)time);
                NSLog(@"Duration: %d", (int)duration);
                NSLog(@"Place in queue: %d", (int)queueIndex);
                NSLog(@"Track URI: %@", trackURI);
                NSLog(@"Protocol: %@", protocol);

            }];
        }

    }];

Then the fix is in the SonosDiscover class method

+ (void)discoverControllers:(void (^)(NSArray *, NSError *))completion {

is like this very hot hot workaround

 NSDictionary *responseDictionary = [XMLReader dictionaryForXMLData:data error:&error];
//  NSArray *inputDictionaryArray = responseDictionary[@"ZPSupportInfo"][@"ZonePlayers"][@"ZonePlayer"];

 NSDictionary *dictionary = responseDictionary[@"ZPSupportInfo"][@"ZonePlayers"][@"ZonePlayer"];

                        //for (NSDictionary *dictionary in inputDictionaryArray){
                            NSString *name = dictionary[@"text"];
                            NSString *coordinator = dictionary[@"coordinator"];
                            NSString *uuid = dictionary[@"uuid"];
                            NSString *group = dictionary[@"group"];
                            NSString *ip = [[dictionary[@"location"] stringByReplacingOccurrencesOfString:@"http://" withString:@""] stringByReplacingOccurrencesOfString:@"/xml/device_description.xml" withString:@""];
                            NSArray *location = [ip componentsSeparatedByString:@":"];
                            SonosController *controllerObject = [[SonosController alloc] initWithIP:[location objectAtIndex:0] port:[[location objectAtIndex:1] intValue]];

                            [devices addObject:@{@"ip": [location objectAtIndex:0], @"port" : [location objectAtIndex:1], @"name": name, @"coordinator": [NSNumber numberWithBool:[coordinator isEqualToString:@"true"] ? YES : NO], @"uuid": uuid, @"group": group, @"controller": controllerObject}];

                        //}

since it crashes on the

NSArray *inputDictionaryArray = responseDictionary[@"ZPSupportInfo"][@"ZonePlayers"][@"ZonePlayer"];

Of course this could mean that when you get the

NSDictionary *dictionary = responseDictionary[@"ZPSupportInfo"][@"ZonePlayers"][@"ZonePlayer"];

it could be an array as well when there is more than one Sonos (sorry I have got just one), so you should do a NSArray / NSDictionary check right there. Any ways then it works:

2015-07-09 16:36:38.417 Musixmatch[5927:570986] Artist: Coldplay
2015-07-09 16:36:38.418 Musixmatch[5927:570986] Title: Paradise
2015-07-09 16:36:38.419 Musixmatch[5927:570986] Album: Paradise
2015-07-09 16:36:38.419 Musixmatch[5927:570986] Album Art: http://192.168.2.150:1400http://192.168.2.218:3401/music/image?id=4B00233EB6CC816A
2015-07-09 16:36:38.420 Musixmatch[5927:570986] Time: 0
2015-07-09 16:36:38.420 Musixmatch[5927:570986] Duration: 278
2015-07-09 16:36:38.420 Musixmatch[5927:570986] Place in queue: 1
2015-07-09 16:36:38.421 Musixmatch[5927:570986] Track URI: http://mobile-iPhone-4877ACE2-7AB1-4DD1-93E0-E98BF97F086F.x-udn/music/track.adts?id=4B00233EB6CC816A
2015-07-09 16:36:38.421 Musixmatch[5927:570986] Protocol: http-get:*:audio/mp4:*

and radio protocols as well

2015-07-09 16:44:22.126 Musixmatch[5927:570986] Artist: (null)
2015-07-09 16:44:22.127 Musixmatch[5927:570986] Title: 216.119.144.221:8008
2015-07-09 16:44:22.128 Musixmatch[5927:570986] Album: (null)
2015-07-09 16:44:22.128 Musixmatch[5927:570986] Album Art: http://192.168.2.150:1400(null)
2015-07-09 16:44:22.128 Musixmatch[5927:570986] Time: 6
2015-07-09 16:44:22.129 Musixmatch[5927:570986] Duration: 0
2015-07-09 16:44:22.129 Musixmatch[5927:570986] Place in queue: 1
2015-07-09 16:44:22.130 Musixmatch[5927:570986] Track URI: x-rincon-mp3radio://216.119.144.221:8008
2015-07-09 16:44:22.130 Musixmatch[5927:570986] Protocol: x-rincon-mp3radio:*:*:*

I'm going to check the other features later on!

loretoparisi commented 8 years ago

More info:

(lldb) po responseDictionary[@"ZPSupportInfo"][@"ZonePlayers"]
{
    ZonePlayer =     {
        behindwifiext = 0;
        bootseq = 3;
        channelfreq = 2437;
        coordinator = true;
        group = "RINCON_B8E937E150E601400:1";
        legacycompatibleversion = "24.0-0000";
        location = "http://192.168.2.150:1400/xml/device_description.xml";
        mincompatibleversion = "27.0-00000";
        text = PostazioneLoreto;
        uuid = "RINCON_B8E937E150E601400";
        version = "28.1-86173";
        wifienabled = 1;
        wirelessmode = 0;
    };
}

Not sure, but when you have more than one ZonePlayer, it could become an array at the root level.

loretoparisi commented 8 years ago

So this was my last solution, better than the first, but quick and dirt as well:

NSDictionary *responseDictionary = [XMLReader dictionaryForXMLData:data error:&error];

                        NSObject *zonePlayers = responseDictionary[@"ZPSupportInfo"][@"ZonePlayers"];
                        if( [zonePlayers isKindOfClass:[NSDictionary class]] ) { // one player
                            NSDictionary *dictionary = responseDictionary[@"ZPSupportInfo"][@"ZonePlayers"][@"ZonePlayer"];
                            NSString *name = dictionary[@"text"];
                            NSString *coordinator = dictionary[@"coordinator"];
                            NSString *uuid = dictionary[@"uuid"];
                            NSString *group = dictionary[@"group"];
                            NSString *ip = [[dictionary[@"location"] stringByReplacingOccurrencesOfString:@"http://" withString:@""] stringByReplacingOccurrencesOfString:@"/xml/device_description.xml" withString:@""];
                            NSArray *location = [ip componentsSeparatedByString:@":"];
                            SonosController *controllerObject = [[SonosController alloc] initWithIP:[location objectAtIndex:0] port:[[location objectAtIndex:1] intValue]];

                            [devices addObject:@{@"ip": [location objectAtIndex:0], @"port" : [location objectAtIndex:1], @"name": name, @"coordinator": [NSNumber numberWithBool:[coordinator isEqualToString:@"true"] ? YES : NO], @"uuid": uuid, @"group": group, @"controller": controllerObject}];

                        }
                        else if( [zonePlayers isKindOfClass:[NSArray class]] ) { // list of players
                            NSArray *inputDictionaryArray = responseDictionary[@"ZPSupportInfo"][@"ZonePlayers"][@"ZonePlayer"];
                            for (NSDictionary *dictionary in inputDictionaryArray){
                                NSString *name = dictionary[@"text"];
                                NSString *coordinator = dictionary[@"coordinator"];
                                NSString *uuid = dictionary[@"uuid"];
                                NSString *group = dictionary[@"group"];
                                NSString *ip = [[dictionary[@"location"] stringByReplacingOccurrencesOfString:@"http://" withString:@""] stringByReplacingOccurrencesOfString:@"/xml/device_description.xml" withString:@""];
                                NSArray *location = [ip componentsSeparatedByString:@":"];
                                SonosController *controllerObject = [[SonosController alloc] initWithIP:[location objectAtIndex:0] port:[[location objectAtIndex:1] intValue]];

                                [devices addObject:@{@"ip": [location objectAtIndex:0], @"port" : [location objectAtIndex:1], @"name": name, @"coordinator": [NSNumber numberWithBool:[coordinator isEqualToString:@"true"] ? YES : NO], @"uuid": uuid, @"group": group, @"controller": controllerObject}];

                            }
                        }
loretoparisi commented 8 years ago

I have found another crash here

- (void)trackInfo:(void (^)(NSString *artist, NSString *title, NSString *album, NSURL *albumArt, NSInteger time, NSInteger duration, NSInteger queueIndex, NSString *trackURI, NSString *protocol, NSError *error))block {

due to the

albumArt Property parsing:

NSURL *albumArt = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%d%@", self.ip, self.port, trackMetaData[@"DIDL-Lite"][@"item"][@"upnp:albumArtURI"][@"text"]]];

since now it can be an array of objects having the DLNA profile for image types:

(lldb) po trackMetaData[@"DIDL-Lite"][@"item"][@"upnp:albumArtURI"]
<__NSArrayM 0x1702460f0>(
{
    text = "http://192.168.2.200:10246/MDEServer/C2B26D33-1801-465B-AD1D-B0CF56E689F7/1000.jpg?albumArt=0";
},
{
    "dlna:profileID" = "JPEG_TN";
    text = "http://192.168.2.200:10246/MDEServer/C2B26D33-1801-465B-AD1D-B0CF56E689F7/1000.jpg?albumArt=0,formatID=00000023-A9AF-4584-84E2-55BFEF0A7D7E,width=160,height=159";
}
)

then we have to take care of this

A solution (that works for sure with my DLNA supported device! ) could be like this

NSURL *albumArt = nil;
         NSObject * albumArtObject = trackMetaData[@"DIDL-Lite"][@"item"][@"upnp:albumArtURI"];
         if( [albumArtObject isKindOfClass:[NSArray class]] ) {
             for( NSDictionary *albumArtElement in (NSArray*)albumArtObject) {
                 if ( albumArtElement[@"text"] ) {
                    albumArt = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%d%@", self.ip, self.port, albumArtElement[@"text"]]];
                 }
             }
         } else if( [albumArtObject isKindOfClass:[NSDictionary class]] ) {
             if ( ((NSDictionary*)albumArtObject)[@"text"] ) {
                 albumArt = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%d%@", self.ip, self.port, ((NSDictionary*)albumArtObject)[@"text"]]];
             }
         }

CRASH 2

NSString *protocol = trackMetaData[@"DIDL-Lite"][@"item"][@"res"][@"protocolInfo"];

since I can have

(lldb) po trackMetaData[@"DIDL-Lite"][@"item"][@"res"]
<__NSArrayM 0x174459860>(
{
    bitrate = 23302;
    bitsPerSample = 16;
    duration = "0:03:26.576";
    "microsoft:codec" = "{00000055-0000-0010-8000-00AA00389B71}";
    nrAudioChannels = 2;
    protocolInfo = "http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01700000000000000000000000000000";
    sampleFrequency = 44100;
    size = 4871876;
    text = "http://192.168.2.200:10246/MDEServer/C2B26D33-1801-465B-AD1D-B0CF56E689F7/1000.mp3";
},
{
    bitrate = 8000;
    duration = "0:03:26.576";
    "microsoft:codec" = "{00000161-0000-0010-8000-00AA00389B71}";
    nrAudioChannels = 2;
    protocolInfo = "http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMABASE;DLNA.ORG_OP=10;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=01700000000000000000000000000000";
    sampleFrequency = 44100;
    text = "http://192.168.2.200:10246/MDEServer/C2B26D33-1801-465B-AD1D-B0CF56E689F7/1000.wma?formatID=00000086-A9AF-4584-84E2-55BFEF0A7D7E";
},
{
    bitrate = 23302;
    bitsPerSample = 16;
    duration = "0:03:26.576";
    "microsoft:codec" = "{00000055-0000-0010-8000-00AA00389B71}";
    nrAudioChannels = 2;
    protocolInfo = "http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=10;DLNA.ORG_FLAGS=01700000000000000000000000000000";
    sampleFrequency = 44100;
    text = "http://192.168.2.200:10246/MDEServer/C2B26D33-1801-465B-AD1D-B0CF56E689F7/1000.mp3?formatID=00000008-A9AF-4584-84E2-55BFEF0A7D7E";
}
)

and a fix is

NSString *trackURI = positionInfoResponse[@"TrackURI"][@"text"];
         NSObject *itemRes = trackMetaData[@"DIDL-Lite"][@"item"][@"res"];
         NSString *protocol = nil;
         if ([itemRes isKindOfClass:[NSArray class]]) {
             for(NSDictionary *protocolInfo in (NSArray*)itemRes) {
                 protocol = protocolInfo[@"protocolInfo"];
             }
         } else if( [itemRes isKindOfClass:[NSDictionary class]] ) {
             protocol = ((NSDictionary*)itemRes)[@"protocolInfo"];
         }

Sorry no time to merge request / etc. I will update here if I find other issues/crashes...

2015-07-09 19:02:57.316 Musixmatch[6257:601936] Artist: Alanis Morissette
2015-07-09 19:02:57.316 Musixmatch[6257:601936] Title: Perfect
2015-07-09 19:02:57.317 Musixmatch[6257:601936] Album: Jagged Little Pill Acoustic
2015-07-09 19:02:57.317 Musixmatch[6257:601936] Album Art: http://192.168.2.150:1400http://192.168.2.200:10246/MDEServer/C2B26D33-1801-465B-AD1D-B0CF56E689F7/1000.jpg?albumArt=0,formatID=00000023-A9AF-4584-84E2-55BFEF0A7D7E,width=160,height=159
2015-07-09 19:02:57.318 Musixmatch[6257:601936] Time: 9
2015-07-09 19:02:57.318 Musixmatch[6257:601936] Duration: 207
2015-07-09 19:02:57.319 Musixmatch[6257:601936] Place in queue: 1
2015-07-09 19:02:57.319 Musixmatch[6257:601936] Track URI: http://192.168.2.200:10246/MDEServer/C2B26D33-1801-465B-AD1D-B0CF56E689F7/1000.mp3
2015-07-09 19:02:57.320 Musixmatch[6257:601936] Protocol: http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=10;DLNA.ORG_FLAGS=01700000000000000000000000000000
larsblumberg commented 8 years ago

I'm maintaining this project now here: https://github.com/getSenic/sonos-objc/

Here's the fix that tests the players response for an array or for a single dictionary: https://github.com/getSenic/sonos-objc/commit/d73c2290120daa35c697c2537aa6d0736d1fa157

Pull requests welcome!

loretoparisi commented 8 years ago

@larsblumberg thanks for the help!

larsblumberg commented 8 years ago

@loretoparisi You're welcome. @n1mda Thank you for your great work on this library.