Search code examples
objective-citunesitunes-sdkitunes-search-api

How to combine entity type searches in apple itunes search api


In the itunes search api doc there is an example of searching for an artist called maroon and the url is like so:

https://itunes.apple.com/search?term=maroon&entity=allArtist&attribute=allArtistTerm

This returns over 50 results that start like this:

{
    "resultCount": 50,
    "results": [
        {
            "wrapperType": "artist",
            "artistType": "Artist",
            "artistName": "Maroon 5",
            "artistLinkUrl": "https://itunes.apple.com/us/artist/maroon-5/id1798556?uo=4",
            "artistId": 1798556,
            "amgArtistId": 529962,
            "primaryGenreName": "Pop",
            "primaryGenreId": 14,
            "radioStationUrl": "https://itunes.apple.com/station/idra.1798556"
        },
        {
            "wrapperType": "artist",
            "artistType": "Software Artist",
            "artistName": "MaroonEntertainment",
            "artistLinkUrl": "https://itunes.apple.com/us/artist/maroonentertainment/id537029262?uo=4",
            "artistId": 537029262,
            "radioStationUrl": "https://itunes.apple.com/station/idra.537029262"
        },

Which is nice. However here is my problem: I would like to create a search query that is as specific as possible by combining the search for both an artist and a song name and an album name..

So for example I got this song:

  • song: Across the Great Divide
  • album: Great Divide
  • Artist: Semisonic

I can search for the artist name only:

https://itunes.apple.com/search?term=Semisonic&entity=allArtist&attribute=allArtistTerm

I can search for the song term only:

https://itunes.apple.com/search?term=Across the Great Divide&entity=song&attribute=songTerm

I can search for the album name only:

https://itunes.apple.com/search?term=Great Divide&entity=album&attribute=albumTerm

However none of these guys give me the result i want (i can find the result i'm looking for amongst maybe 50 others.. but i just want the search query to be specific enough to avoid any client side filtering kind of thing).

How can I combine these searches? if I simply add two searches together (in this example i'm searching for both song and artist):

https://itunes.apple.com/search?term=Across the Great Divide&entity=song&attribute=songTerm&term=Semisonic&entity=allArtist&attribute=allArtistTerm

then apple will simply ignore the first search type (ie song) and return the results for artist only).

ideas?


Solution

  • Well this is more of a "workaround" answer.. but it's the solution I'm using.. so might as well spread the love eh?

    This is a 100% client side solution (ie the entire database of itunes music can be downloaded into my own server.. then I can create all sots of search wrappers around it.. but that's a project in itself).

    this is what i got:

    // this is just a wrapper around the apple search api.. it makes your 
    // average joe http get request
    [[AppleServer shared] searchForSongWithTitle:track.title andAlbumName:track.albumName completion:^(NSArray *results, NSError *error){
        if ([results count] >0) {
            NSLog(@"[%d] unfiltered songs retrieved from apple search api", [results count]);
            NSDictionary *filteredResult = [[self class] filterResults:results ToMatchTrack:track];
            if (!filteredResult) {
                NSLog(@"Filtering may be too strict, we got [%d] results from apple search api but none past our filter", [results count]);
                return;
            }
    
            .. process results
    
    
    + (NSDictionary *)filterResults:(NSArray *)results ToMatchTrack:(VBSong *)track
    {
    
        NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(NSDictionary *evaluatedTrack, NSDictionary *bindings){    
            BOOL result =
             ([track.title isLooselyEqualToString:evaluatedTrack[@"trackName"]] &&
              [track.artistName isLooselyEqualToString:evaluatedTrack[@"artistName"]] &&
              [track.albumName isLooselyEqualToString:evaluatedTrack[@"collectionName"]]);
    
            NSLog(@"match?[%d]", result);
    
            return result;
        }];
    
        return [[results filteredArrayUsingPredicate:predicate] firstObject];
    }
    

    the key method here is isLooselyEqualToString.. it's defined in an NSString category like so:

    /**
     * Tests if one string equals another substring, relaxing the following contraints
     *   - one string can be a substring of another
     *   - it's a case insensitive comparison
     *   - all special characters are removed from both strings
     *
     *     ie this should return true for this comparison:
     *     - comparing self:"Circus One (Presented By Doctor P and Flux Pavilion)" 
                    and str:"Circus One presented by Doctor P"
     *
     * @param str string to compare self against
     * @return if self is the same as str, relaxing the contraints described above
     */
    - (BOOL)isLooselyEqualToString:(NSString *)str
    {
        return [[self removeSpecialCharacters] containSubstringBothDirections:[str removeSpecialCharacters]];
    }
    
    /**
     * Tests if one string is a substring of another
     *     ie this should return true for both these comparisons:
     *     - comparing self:"Doctor P & Flux Pavilion" and substring:"Flux Pavilion"
     *     - comparing self:"Flux Pavilion" and substring:"Doctor P & Flux Pavilion"
     *
     * @param substring to compare self against
     * @return if self is a substring of substring
     */
    -(BOOL)containSubstringBothDirections:(NSString*)substring
    {
        if (substring == nil) return self.length == 0;
    
        if ([self rangeOfString:substring options:NSCaseInsensitiveSearch].location == NSNotFound) {
            if ([substring rangeOfString:self options:NSCaseInsensitiveSearch].location == NSNotFound) {
                return NO;
            } else {
                return YES;
            }
        } else {
            return YES;
        }
    }
    
    - (NSString *)removeSpecialCharacters
    {
        NSMutableCharacterSet *specialCharsSet = [[NSCharacterSet letterCharacterSet] mutableCopy];
        [specialCharsSet formUnionWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]];
        return [[self componentsSeparatedByCharactersInSet:[specialCharsSet invertedSet]] componentsJoinedByString:@""];
    }
    

    Bonus This is the solution we are currently using.. I'm fully aware that some terms may come up that break this algorithm.. so we have a unit test for this that we incrementally add terms to ensure we keep on improving our algorithm while not causing regression bugs.. i'll post it if i get enough votes on this answer heh.