使用GraceNote Web API发展Mac发现音乐信息的应用

好久没有写博客,最近各种忙,特别忙里忙,今晚难得清闲。写最近完成下一个博客任务的摘要:使用GraceNote的Web API开发一个查询的音乐信息的应用,事实上,并在这些功能的前GraceNote SDK鲍文是一样的,次不使用不论什么SDK。单纯的使用Web API,然后开发的平台从iOS转移到了Mac上。于是,我人生中第一个Mac App Demo就出来了。

GraceNote Web API的官方资料:点击打开链接

首先看下主要的查询和响应的数据格式:

能够看到交互的形式是XML。

其实。不论什么调用GraceNote的Web API的消息,都是向一个指定的URL POST XML消息。然后对返回的XML消息进行解析并从中提取出我们想要的信息。以下是程序的一些常数:

NSString * const kWebAPIURL = @"https://c10239232.web.cddbp.net/webapi/xml/1.0/"; // 调用网络接口的URL
NSString * const kClientID  = @"10239232"; // 你申请的应用的Client ID
NSString * const kClientTag = @"46B9ABAD30F0F5EB409C7BFAA13EB2EF"; // 你申请的应用的Client Tag

当中kWebAPIURL就是这个固定的发起请求的URL,注意将c后面的数字替换成你的App的Client ID。

kClient ID和kClient Tag能够从在站点中注冊的App中找到。

在使用GraceNote的Web API进行查询之前,首先要通过App的Client ID和Client Tag来注冊一个User ID,然后在全部兴许查询中都要使用这个User ID和之前的Client ID来进行认证,格式例如以下:

首先看看注冊的代码,在注冊成功后我们将其保存到本地的NSUserDefaults中:

// 向GraceNote站点注冊User ID
- (void)gn_registerUserID {
    NSString *registerString = [NSString stringWithFormat:@"                                <QUERIES>                                    <QUERY CMD=\"REGISTER\">                                        <CLIENT>%@-%@</CLIENT>                                    </QUERY>                                </QUERIES>",
                                kClientID, kClientTag]; // 要POST的字符串。CMD=REGISTER表示注冊动作
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:kWebAPIURL]];
    [request setHTTPMethod:@"POST"];
    NSData *data = [registerString dataUsingEncoding:NSUTF8StringEncoding];
    [request setHTTPBody:data];

    // 建立NSURLSessionDataTask
    NSURLSession *session = [NSURLSession sharedSession];
    __weak AppDelegate *weakSelf = self; // 防止self和block形成retain cycle
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        NSLog(@"*** Register ***");
        [self showResponseCode:response];

        if (data) {
            NSError *parseError = nil;
            // 这里使用第三方类库GDataXML解析XML数据,请确保已经安装GDataXML类库
            GDataXMLDocument *doc = [[GDataXMLDocument alloc] initWithData:data encoding:NSUTF8StringEncoding error:&parseError];
            if (parseError) {
                NSLog(@"Parse Error:%@", [parseError localizedDescription]);
                weakSelf.app_userID = nil;
            }
            else {
                /**
                 *  返回的XML数据演示样例:
                 <RESPONSES>
                    <RESPONSE STATUS="OK">
                        <USER>267493051066226693-31C70A189A61B89C0D45A782DCB7C072</USER>
                    </RESPONSE>
                 </RESPONSES>
                 */
                GDataXMLElement *rootElement = [doc rootElement];
                NSArray *responses = [rootElement elementsForName:kGNResponse];
                GDataXMLElement *resp = responses[0];
                if (![self gn_requestSucceed:resp]) {
                    return;
                }

                NSString *userID = [[resp elementsForName:kGNUser][0] stringValue];

                // 将获取到的user id保存起来
                weakSelf.app_userID = userID;

                // 将user id存储到User Defaults中
                NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
                [userDefaults setObject:userID forKey:kUserID];
                [userDefaults synchronize];

                NSLog(@"User ID = %@", userID);
            }
        }

        if (error) {
            NSLog(@"error : %@", [error localizedDescription]);
        }

        NSLog(@"--- Register Finished ---");
    }];

    // 最后一定要用resume方法启动任务
    [dataTask resume];
}

全部的任务都能够通过NSURLSessionDataTask来完毕。

然后依据艺术家名,专辑名。歌曲标题,搜索结果的返回范围来发起查询请求(album search):

// 以Artist,Album Title,Track Title为搜索keyword,发起搜索请求
- (void)gn_albumSearchWithArtist:(NSString *)anArtist
                      albumTitle:(NSString *)anAlbumTitle
                      trackTitle:(NSString *)aTrackTitle
                           start:(NSUInteger)startIndex
                             end:(NSUInteger)endIndex
{
    // 首先移除上次残留的查询结果
    [_gn_IDs removeAllObjects];

    if (startIndex <= 0 || endIndex <= 0 || startIndex > endIndex) {
        return;
    }

    // 设置查询字符串。本次请求属于ALBUM_SEARCH操作
    NSString *searchString = [NSString stringWithFormat:@"                              <QUERIES>                                <AUTH>                                    <CLIENT>%@-%@</CLIENT>                                    <USER>%@</USER>                                </AUTH>                                <QUERY CMD=\"ALBUM_SEARCH\">                                    <TEXT TYPE=\"ARTIST\">%@</TEXT>                                    <TEXT TYPE=\"ALBUM_TITLE\">%@</TEXT>                                    <TEXT TYPE=\"TRACK_TITLE\">%@</TEXT>                                    <RANGE>                                        <START>%ld</START>                                        <END>%ld</END>                                    </RANGE>                                </QUERY>                              </QUERIES>",
                              kClientID, kClientTag, _app_userID, anArtist, anAlbumTitle, aTrackTitle, startIndex, endIndex];

    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:kWebAPIURL]];
    [request setHTTPMethod:@"POST"];
    NSData *data = [searchString dataUsingEncoding:NSUTF8StringEncoding];
    [request setHTTPBody:data];

    // 建立NSURLSessionDataTask并用resume方法启动任务
    NSURLSession *session = [NSURLSession sharedSession];
    __weak AppDelegate *weakSelf = self;
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        NSLog(@"*** Album Search ***");
        [self showResponseCode:response];

        if (data) {
            NSError *parseError = nil;
            GDataXMLDocument *doc = [[GDataXMLDocument alloc] initWithData:data encoding:NSUTF8StringEncoding error:&parseError];
            if (parseError) {
                NSLog(@"Parse Error:%@", [parseError localizedDescription]);
            }
            else {
                /**
                 *  请求成功。返回XML结果演示样例:
                 <RESPONSES>
                    <RESPONSE STATUS="OK">
                        <RANGE>
                            <COUNT>2</COUNT>
                            <START>1</START>
                            <END>2</END>
                        </RANGE>
                        <ALBUM ORD="1">
                            <GN_ID>7552265-4E82AF73CE400EDC94DCDA49547C585F</GN_ID>
                            <ARTIST>The Carpenters</ARTIST>
                            <TITLE>Now & Then</TITLE>
                            <PKG_LANG>ENG</PKG_LANG>
                            <DATE>1973</DATE>
                            <GENRE NUM="61365" ID="25333">70's Rock</GENRE>
                            <MATCHED_TRACK_NUM>6</MATCHED_TRACK_NUM>
                            <TRACK_COUNT>15</TRACK_COUNT>
                            <TRACK>
                                <TRACK_NUM>6</TRACK_NUM>
                                <GN_ID>7552271-366ED2D1FEB61E8D720D4941009C91A9</GN_ID>
                                <TITLE>Yesterday Once More</TITLE>
                            </TRACK>
                        </ALBUM>
                        <ALBUM ORD="2">
                            <GN_ID>19546461-AA0668FE5972459884664A7C3FE9D9C2</GN_ID>
                            <ARTIST>The Carpenters</ARTIST>
                            <TITLE>Now And Then</TITLE>
                            <PKG_LANG>ENG</PKG_LANG>
                            <GENRE NUM="61365" ID="25333">70's Rock</GENRE>
                            <MATCHED_TRACK_NUM>6</MATCHED_TRACK_NUM>
                            <TRACK_COUNT>8</TRACK_COUNT>
                            <TRACK>
                                <TRACK_NUM>6</TRACK_NUM>
                                <GN_ID>19546467-560982E049BFF85016AB89C37513F474</GN_ID>
                                <TITLE>Yesterday Once More</TITLE>
                            </TRACK>
                        </ALBUM>
                    </RESPONSE>
                 </RESPONSES>
                 */
                GDataXMLElement *rootElement = [doc rootElement];
                NSArray *responses = [rootElement elementsForName:kGNResponse];
                if ([responses count]) {
                    GDataXMLElement *resp = [responses firstObject];
                    if (![self gn_requestSucceed:resp]) {
                        return;
                    }

                    GDataXMLElement *range = [resp elementsForName:kGNRange][0];
                    if (!range) { // 假设没有返回range元素。那么抓取数据失败
                        NSLog(@"Fail to search album");
                        return;
                    }
                    NSUInteger count = (NSUInteger)[[[range elementsForName:kGNCount][0] stringValue] integerValue];
                    NSUInteger start = (NSUInteger)[[[range elementsForName:kGNStart][0] stringValue] integerValue];

                    if (count <= 0) { // 没有搜索到结果,直接返回
                        [self showSearchResultsCountText:0];
                        return;
                    }

                    p_currentPage = start / 10 + 1;
                    p_allPages = count / 10;
                    NSUInteger i = (count - count / 10 * 10) ?

1 : 0;
                    p_allPages += i;
                    [self updatePagingText];
                    [self showSearchResultsCountText:count];

                    NSUInteger searchCount = 0;
                    if (endIndex >= count) {
                        searchCount = count - startIndex;
                    }
                    else {
                        searchCount = endIndex - startIndex;
                    }

                    NSArray *albums = [resp elementsForName:kGNAlbum];
                    for (NSUInteger i = 0; i <= searchCount; i++) {
                        GDataXMLElement *album = albums[i];
                        NSString *gn_id = [[album elementsForName:kGNID][0] stringValue];

                        // 将每一条搜索结果的GN_ID加入到数组gn_IDs中
                        [weakSelf.gn_IDs addObject:gn_id];
                    }

                    [_previousPage_button setEnabled:YES];
                    [_nextPage_button setEnabled:YES];

                    // 逐个抓取专辑的详细信息
                    [weakSelf albumFetch];
                }
            }
        }

        if (error) {
            NSLog(@"error : %@", [error localizedDescription]);
        }

        NSLog(@"--- Album Search Finished ---");
    }];

    [dataTask resume];
}

将搜索到的gnID(在数据库中标识这个专辑的一个ID)保存进一个数组gn_IDs中。然后依据数组中的每一个gn_id发起进一步的抓取专辑完整数据的操作(album fetch):

// 逐个抓取专辑的详细信息
- (void)albumFetch {
    // 首先移除上次搜索的残留数据
    [_searchAlbums removeAllObjects];

    // 以gn_IDs中的每个gnID为搜索keyword。运行album fetch请求,抓取专辑的完整信息
    for (NSString *gnID in _gn_IDs) {
        [self gn_albumFetchWithGNID:gnID];
    }
}

// 以GN_ID为搜索keyword。运行album fetch请求,抓取专辑的完整信息
- (void)gn_albumFetchWithGNID:(NSString *)aID {
    // 设置要查询的字符串,本次操作为ALBUM_FETCH操作
    NSString *searchString = [NSString stringWithFormat:@"                              <QUERIES>                                <AUTH>                                    <CLIENT>%@-%@</CLIENT>                                    <USER>%@</USER>                                </AUTH>                                <QUERY CMD=\"ALBUM_FETCH\">                                    <MODE>SINGLE_BEST_COVER</MODE>                                    <GN_ID>%@</GN_ID>                                    <OPTION>                                        <PARAMETER>SELECT_EXTENDED</PARAMETER>                                        <VALUE>COVER,ARTIST_IMAGE</VALUE>                                    </OPTION>                                    <OPTION>                                        <PARAMETER>COVER_SIZE</PARAMETER>                                        <VALUE>THUMBNAIL</VALUE>                                    </OPTION>                                </QUERY>                              </QUERIES>",
                              kClientID, kClientTag, _app_userID, aID];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:kWebAPIURL]];
    [request setHTTPMethod:@"POST"];
    NSData *data = [searchString dataUsingEncoding:NSUTF8StringEncoding];
    [request setHTTPBody:data];

    // 建立NSURLSessionDataTask并用resume方法启动任务
    NSURLSession *session = [NSURLSession sharedSession];
    __weak AppDelegate *weakSelf = self;
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        NSLog(@"*** Album Fetch ***");
        [self showResponseCode:response];

        if (data) {
//            // 输出返回的xml内容
//            [self logoutXMLData:data];

            // 通过返回的xml二进制数据初始化MFAlbum对象
            MFAlbum *album = [[MFAlbum alloc] initWithXMLData:data];
            if (album) {
                // 将查询结果加入到searchAlbums数组中
                [weakSelf.searchAlbums addObject:album];
            }

            [weakSelf showResults];
        }

        if (error) {
            NSLog(@"error : %@", [error localizedDescription]);
        }

        NSLog(@"--- Album Fetch Finished ---");
    }];

    [dataTask resume];
}

最后在NSTableView中将数据load出来:

#pragma mark - NSTableViewDataSource

- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
    return [_searchAlbums count];
}

- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
    NSString *unknown = @"未知";
    MFAlbum *album = _searchAlbums[row];
    NSString *identifier = tableColumn.identifier;
    if ([identifier isEqualToString:@"coverArt"]) {
        NSURL *coverArtURL = [NSURL URLWithString:album.coverArtURLString];
        NSImage *image;
        if (coverArtURL) {
            image = [[NSImage alloc] initWithContentsOfURL:coverArtURL];
        }
        else {
            image = [NSImage imageNamed:@"NotFound"];
        }
        return image;
    }
    else if ([identifier isEqualToString:@"artistImage"]) {
        NSURL *artistImageURL = [NSURL URLWithString:album.artistImageURLString];
        NSImage *image;
        if (artistImageURL) {
            image = [[NSImage alloc] initWithContentsOfURL:artistImageURL];
        }
        else {
            image = [NSImage imageNamed:@"NotFound"];
        }

        return image;
    }
    else if ([identifier isEqualToString:@"trackCount"]) {
        return [NSString stringWithFormat:@"%ld", album.trackCount] ?

[NSString stringWithFormat:@"%ld", album.trackCount] : unknown;
    }
    else {
        NSString *info = [album valueForKey:identifier];
        return info ?

info : unknown;
    }
}

另外我将专辑元数据抽象成了一个MFAlbum类,能够通过返回的XML响应数据初始化(在这里使用了GDataXML类库进行XML解析),代码例如以下:

- (instancetype)initWithXMLData:(NSData *)xmlData {
    self = [super init];
    if (self) {
        NSError *parseError = nil;
        GDataXMLDocument *doc = [[GDataXMLDocument alloc] initWithData:xmlData encoding:NSUTF8StringEncoding error:&parseError];
        if (parseError) {
            NSLog(@"Parse Error:%@", [parseError localizedDescription]);
            return nil; // 转换出错。直接返回nil
        }

        // 逐个解析xml结点,获取专辑对象所须要的全部信息
        GDataXMLElement *rootElement = [doc rootElement];
        GDataXMLElement *response = [rootElement elementsForName:kGNResponse][0];
        if (![self gn_requestSucceed:response]) {
            return nil;
        }

        GDataXMLElement *album = [response elementsForName:kGNAlbum][0];
        _gn_id = [[album elementsForName:kGNID][0] stringValue];
        _artistName = [[album elementsForName:kGNArtist][0] stringValue];
        _albumTitle = [[album elementsForName:kGNTitle][0] stringValue];
        _language = [[album elementsForName:kGNLanguage][0] stringValue];
        _releaseDate = [[album elementsForName:kGNDate][0] stringValue];
        _genre = [[album elementsForName:kGNGenre][0] stringValue];
        _trackCount = (NSUInteger)[[[album elementsForName:kGNTrackCount][0] stringValue] integerValue];

        _allTracks = [NSMutableArray array];
        NSArray *tracks = [album elementsForName:kGNTrack];
        for (GDataXMLElement *trackElement in tracks) {
            NSString *title = [[trackElement elementsForName:kGNTitle][0] stringValue];
            [_allTracks addObject:title];
        }

        NSArray *urlElements = [album elementsForName:kGNURL];
        if (!urlElements) {
            return self;
        }
        for (GDataXMLElement *element in urlElements) {
            GDataXMLNode *node = [element attributeForName:kGNType];
            NSString *type = [node stringValue];
            if ([type isEqualToString:kGNCoverArt]) {
                _coverArtURLString = [element stringValue];
            }
            else if ([type isEqualToString:kGNArtistImage]) {
                _artistImageURLString = [element stringValue];
            }
        }
    }
    return self;
}

主界面部分(MainMenu.xib):

最后上执行结果:

实在好久没写博客。写作水平下降得厉害,加上自己又变懒惰了非常多,这篇文章实在写得太烂,仅仅能当做做个记号,证明我有完毕了GraceNote的音乐信息查询服务了吧。

版权声明:本文博客原创文章。博客,未经同意,不得转载。

时间: 2024-10-23 22:51:31

使用GraceNote Web API发展Mac发现音乐信息的应用的相关文章

使用GraceNote Web API开发Mac查询音乐信息应用

好久没写博客了,最近各种忙,大忙特忙,今晚难得有空,写个博客总结下最近完成的一个任务:使用GraceNote的Web API来开发一个查询音乐信息的应用,其实功能和前面的那些GraceNote SDK的博文是一样的,只是这一次不使用任何SDK,单纯的使用Web API,然后开发的平台从iOS转移到了Mac上,于是,我人生中第一个Mac App Demo就出来了. GraceNote Web API的官方资料:点击打开链接 首先看下基本的查询和响应的数据格式: 可以看到交互的形式是XML. 事实上

在Mac下创建ASP.NET Core Web API

在Mac下创建ASP.NET Core Web API 在Mac下创建ASP.NET Core Web API 这系列文章是参考了.NET Core文档和源码,可能有人要问,直接看官方的英文文档不就可以了吗,为什么还要写这些文章呢? 原因如下: 官方文档涉及的内容相当全面,属于那种大而全的知识仓库,不太适合初学者,很容易让人失去重要,让人掉入到具体的细节之中. 对于大多数人来讲开发语言只是工具,程序员都有一个通病,就是死磕工具,把工具学深.我认为在工具上没有必要投入太多时间,以能高效地完成日常的

Gitlab CI 自动部署 asp.net core web api 到Docker容器

为什么要写这个? 在一个系统长大的过程中会经历不断重构升级来满足商业的需求,而一个严谨的商业系统需要高效.稳定.可扩展,有时候还不得不考虑成本的问题.我希望能找到比较完整的开源解决方案来解决持续集成.监控报警.以及扩容和高可用性的问题.是学习和探索的过程分享给大家,也欢迎同行的人交流. 先来一个三步曲,我们将完成通过GitLab CI 自动部署 net core web api 到Docker 容器的一个示例.这是第一步,通过此文您将了解如何将net core web api 运行在Docker

Asp.Net Web API 2第六课——Web API路由和动作选择

Asp.Net Web API 导航 Asp.Net Web API第一课——入门http://www.cnblogs.com/aehyok/p/3432158.html Asp.Net Web API第二课——CRUD操作http://www.cnblogs.com/aehyok/p/3434578.html Asp.Net Web API第三课——.NET客户端调用Web API http://www.cnblogs.com/aehyok/p/3439698.html Asp.Net Web

【Web API系列教程】2.2 — ASP.NET Web API中的路由和动作选择机制

这篇文章描述了ASP.NET Web API如何将HTTP请求路由到控制器上的特定动作. 备注:想要了解关于路由的高层次概述,请查看Routing in ASP.NET Web API. 这篇文章侧重于路由过程的细节.如果你创建了一个Web API项目并且发现一些请求并没有按你预期得到相应的路由,希望这篇文章有所帮助. 路由有以下三个主要阶段: 将URI匹配到路由模板 选择一个控制器 选择一个动作 你可以用自己的习惯行为来替换其中一些过程.在本文中,我会描述默认行为.在结尾,我会指出你可以自定义

web api中的RouteHandler

ASP.NET MVC4中引入的Web API可以说是进行REST软件开发的利器(个人意见),但是最近在web form中混入web api时,发现一个问题:由于以前的web form项目中,使用到了session(包括那些复杂的底层逻辑),所以为了最小改动,必须保证web api能支持session.而web api默认情况下,是不支持session的. 问题重现 重现这个不支持session的问题,其实很简单.只需要新建一个web api项目,然后修改ValuesController的Get

ASP.NET Web API 帮助(help)页面上没有 Test API按钮的解决方法

参与一个web API项目时发现它的help页面特别好用,不仅可以根据webapi的方法和注释自动生成帮助文档以方便查阅,还可以在这个页面上测试webapi方法.于是在自己新建项目时也打算将这个help页面用起来.在实际操作中,发现新建Web API项目时会自动为你生成一个帮助页面,如下: 点开api后,到达如下页面,右下角并没有Test API按钮,不能对webAPI进行测试. 经过多方查资料,终于找到了解决方法.简单来说就是需要通过NuGet引用Web API Test Client. 在右

Web API路由和动作选择

前言 本文描述ASP.NET Web API如何把一个HTTP请求路由到控制器的一个特定的Action上.关于路由的总体概述可以参见上一篇教程 http://www.cnblogs.com/aehyok/p/3442051.html.这篇文章主要来学习路由过程的细节.如果你创建了一个Web API项目,发现有一些请求没有按照你期望的方式被路由,希望这篇文章将对你有所帮助. 本文主要分为三个阶段: 1.匹配URI到一个Route Template. 2.选择一个Controller. 3.选择一个

用Owin Host实现脱离IIS跑Web API单元测试

开发笔记:用Owin Host实现脱离IIS跑Web API单元测试 今天在开发一个ASP.NET Web API项目写单元测试时,实在无法忍受之前的笨方法,决定改过自新. 之前Web API的单元测试需要进行以下的操作: 初始配置: 1)在IIS中创建一个站点指定Web API项目 2)在hosts加上该站点的IP地址解析 每次修改代码: 3)修改代码之后按F6编译 4)用TestDriven.Net运行单元测试 一看就知道这个方法好土.好笨.好受罪.理想的方式应该是:无需任何初始配置,修改代