在Use MusicBrainz in iOS之后,由于MusicBrainz查不到专辑封面,只能转移到其它提供音乐信息搜索服务的网站,领导给出的就是GraceNote。(有压力。。。)
需求类似:通过一个音频文件的歌曲名,专辑名等信息查询到该文件专辑的封面。
本文介绍下如何在iOS中使用GraceNote给出的GNSDK(一个用C写的SDK),并通过序列化的GDO查询专辑封面。实际上,就是我对GNSDK文件中给出的samples的一些修正和调用而已。
首先前往GraceNote网站注册一个帐号,然后创建一个app(看得懂英语的都知道怎么做吧,我就略过了),完成后如下所示:
新建一个Demo工程,名字和上面的app名一致(不一致是否可行,我还没试过)。
下载GNSDK:点击打开链接
本文基于3.0.6版本。
下载完成后解压,前往lib_static/ios_armv7s-32,由于我的机子指令集是armv7s,所以使用该文件夹中的静态库文件。将所有.a文件复制到Demo工程中。
再将include目录下的所有.h文件和ios_armv7s_32文件夹复制到Demo工程中。
完成后工程目录如下:
打开Build Settings选项,设置Header Search Paths和Library Search Paths:
确保Build Phases全部静态库链接成功:
在工程中新建一个文本文件,例如my_licence.txt,内容为你所创建的App Details中的License String中的内容(注意不要有任何改动,空缺的信息无需补上),示例如下:
-- BEGIN LICENSE v1.0 1CBA6766 -- licensee: Gracenote, Inc. name: notes: Lic Gen 2.1 start_date: 0000-00-00 client_id: 4541440 musicid_file: enabled musicid_search: enabled musicid_stream: enabled musicid: enabled playlist: enabled -- SIGNATURE 1CBA6766 -- lAADAgAe5/qrtIoNKp6SWqcqU+a+R6l/SaJ3aW5YQ+93T7nPAB8Bm9BtwE5OsPgoKjHK14foIdX5dPugbz1mJnkfT6Kt -- END LICENSE 1CBA6766 --
新建一个Test.C,代码如下:
#define USE_LOCAL 0 #define GNSDK_LINK 1 #define GNSDK_STORAGE_SQLITE 1 #include "gnsdk.h" gnsdk_byte_t* response_data; gnsdk_size_t response_data_length; #include <stdio.h> #include <string.h> #include <stdlib.h> /********************************************** * Local Function Declarations **********************************************/ static int _init_gnsdk( const char* client_id, const char* client_id_tag, const char* client_app_version, const char* license_path, gnsdk_user_handle_t* p_user_handle ); static void _shutdown_gnsdk (gnsdk_user_handle_t user_handle); static void _query_for_album_images (gnsdk_user_handle_t user_handle); /****************************************************************** * * MAIN * ******************************************************************/ int coverart_main(int argc, const char* argv[]) { gnsdk_user_handle_t user_handle = GNSDK_NULL; const char* client_id = NULL; const char* client_id_tag = NULL; const char* client_app_version = "1"; /* increment with each version of your app */ const char* license_path = NULL; int rc = 0; /* Client ID, Client ID Tag and License file must be passed in */ if (argc == 4) { client_id = argv[1]; client_id_tag = argv[2]; license_path = argv[3]; /* Initialize GNSDK */ rc = _init_gnsdk( client_id, client_id_tag, client_app_version, license_path, &user_handle ); if (0 == rc) { /* Perform a sample cover art query */ _query_for_album_images(user_handle); /* Clean up and shutdown */ _shutdown_gnsdk(user_handle); } } else { printf("\nUsage:\n%s clientid clientidtag license\n", argv[0]); rc = -1; } return rc; } /* main() */ /****************************************************************** * * _DISPLAY_LAST_ERROR * * Echo the error and information. * *****************************************************************/ static void _display_last_error( int line_num ) { /* Get the last error information from the SDK */ const gnsdk_error_info_t* error_info = gnsdk_manager_error_info(); /* Error_info will never be GNSDK_NULL. * The SDK will always return a pointer to a populated error info structure. */ printf( "\nerror from: %s() [on line %d]\n\t0x%08x %s", error_info->error_api, line_num, error_info->error_code, error_info->error_description ); } /* display_last_error() */ /****************************************************************** * * _GET_USER_HANDLE * * Load existing user handle, or register new one. * * GNSDK requires a user handle instance to perform queries. * User handles encapsulate your Gracenote provided Client ID which is unique for your * application. User handles are registered once with Gracenote then must be saved by * your application and reused on future invocations. * *****************************************************************/ static int _get_user_handle( const char* client_id, const char* client_id_tag, const char* client_app_version, gnsdk_user_handle_t* p_user_handle ) { gnsdk_user_handle_t user_handle = GNSDK_NULL; gnsdk_str_t serialized_user = GNSDK_NULL; gnsdk_error_t error = GNSDK_SUCCESS; char* user_filename = NULL; size_t user_filename_len = 0; int rc = 0; FILE* file = NULL; // user_filename_len = strlen(client_id)+strlen("_user.txt")+1; // user_filename = malloc(user_filename_len); user_filename_len = strlen("/var/mobile/Applications/1B3B6648-8D50-430F-B7D2-21D99AA78B6F/tmp/4541440_user.txt") + 1; user_filename = malloc(user_filename_len); if (NULL != user_filename) { // strcpy(user_filename, client_id); // strcat(user_filename, "_user.txt"); strcpy(user_filename, "/var/mobile/Applications/1B3B6648-8D50-430F-B7D2-21D99AA78B6F/tmp/4541440_user.txt"); // /* Do we have a user saved locally? */ // file = fopen(user_filename, "r"); // if (NULL != file) // { // gnsdk_char_t serialized_user_string[1024] = {0}; // // if (NULL != (fgets(serialized_user_string, 1024, file))) // { // /* Create the user handle from the saved user */ // error = gnsdk_manager_user_create(serialized_user_string, client_id, client_id_tag, client_app_version, &user_handle); // if (GNSDK_SUCCESS != error) // { // _display_last_error(__LINE__); // rc = -1; // } // } // else // { // printf("Error reading user file into buffer.\n"); // rc = -1; // } // fclose(file); // } // else // { // printf("\nInfo: No stored user - this must be the app‘s first run.\n"); // } /* If not, create new one*/ if (GNSDK_NULL == user_handle) { error = gnsdk_manager_user_register(GNSDK_USER_REGISTER_MODE_ONLINE, client_id, client_id_tag, client_app_version, &serialized_user); if (GNSDK_SUCCESS != error) { _display_last_error(__LINE__); rc = -1; } else { /* save newly registered user for use next time */ file = fopen(user_filename, "w"); if (NULL != file) { if (0 > fputs(serialized_user, file)) { printf("Error writing user registration file from buffer.\n"); rc = -1; } fclose(file); } else { printf("\nError: Failed to open the user filename for use in saving the updated serialized user. (%s)\n", user_filename); } /* Create the user handle from the registered user */ error = gnsdk_manager_user_create(serialized_user, client_id, client_id_tag, client_app_version, &user_handle); if (GNSDK_SUCCESS != error) { _display_last_error(__LINE__); rc = -1; } gnsdk_manager_string_free(serialized_user); } } free(user_filename); } else { printf("Error allocating memory.\n"); rc = -1; } if (rc == 0) { *p_user_handle = user_handle; } return rc; } /* _get_user_handle() */ /****************************************************************** * * _ENABLE_LOGGING * * Enable logging for the SDK. Not used by Sample App. This helps * Gracenote debug your app, if necessary. * ******************************************************************/ static int _enable_logging(void) { gnsdk_error_t error = GNSDK_SUCCESS; int rc = 0; error = gnsdk_manager_logging_enable( "/var/mobile/Applications/1B3B6648-8D50-430F-B7D2-21D99AA78B6F/tmp/sample.log", /* Log file path */ GNSDK_LOG_PKG_ALL, /* Include entries for all packages and subsystems */ GNSDK_LOG_LEVEL_ERROR|GNSDK_LOG_LEVEL_WARNING, /* Include only error and warning entries */ GNSDK_LOG_OPTION_ALL, /* All logging options: timestamps, thread IDs, etc */ 0, /* Max size of log: 0 means a new log file will be created each run */ GNSDK_FALSE /* GNSDK_TRUE = old logs will be renamed and saved */ ); if (GNSDK_SUCCESS != error) { _display_last_error(__LINE__); rc = -1; } return rc; } /* _enable_logging() */ /* * Set the application Locale. */ static int _set_locale (gnsdk_user_handle_t user_handle) { gnsdk_locale_handle_t locale_handle = GNSDK_NULL; gnsdk_error_t error = GNSDK_SUCCESS; int rc = 0; error = gnsdk_manager_locale_load( GNSDK_LOCALE_GROUP_MUSIC, /* Locale group */ GNSDK_LANG_ENGLISH, /* Languae */ GNSDK_REGION_DEFAULT, /* Region */ GNSDK_DESCRIPTOR_SIMPLIFIED, /* Descriptor */ user_handle, /* User handle */ GNSDK_NULL, /* User callback function */ 0, /* Optional data for user callback function */ &locale_handle /* Return handle */ ); if (GNSDK_SUCCESS == error) { /* Setting the ‘locale‘ as default * If default not set, no locale-specific results would be available */ error = gnsdk_manager_locale_set_group_default(locale_handle); if (GNSDK_SUCCESS != error) { _display_last_error(__LINE__); rc = -1; } /* The manager will hold onto the locale when set as default * so it‘s ok to release our reference to it here */ gnsdk_manager_locale_release(locale_handle); } else { _display_last_error(__LINE__); rc = -1; } return rc; } /* _set_locale() */ /****************************************************************** * * _INIT_GNSDK * * Initializing the GNSDK is required before any other APIs can be called. * First step is to always initialize the Manager module, then use the returned * handle to initialize any modules to be used by the application. * * For this sample, we also load a locale which is used by GNSDK to provide * appropriate locale-sensitive metadata for certain metadata values. Loading of the * locale is done here for sample convenience but can be done at anytime in your * application. ******************************************************************/ static int _init_gnsdk( const char* client_id, const char* client_id_tag, const char* client_app_version, const char* license_path, gnsdk_user_handle_t* p_user_handle ) { gnsdk_manager_handle_t sdkmgr_handle = GNSDK_NULL; gnsdk_error_t error = GNSDK_SUCCESS; gnsdk_user_handle_t user_handle = GNSDK_NULL; int rc = 0; /* Initialize the GNSDK Manager */ error = gnsdk_manager_initialize( &sdkmgr_handle, license_path, GNSDK_MANAGER_LICENSEDATA_FILENAME ); if (GNSDK_SUCCESS != error) { _display_last_error(__LINE__); return -1; } /* Enable logging */ if (0 == rc) rc = _enable_logging(); /* Initialize the Storage SQLite Library */ if (0 == rc) { error = gnsdk_storage_sqlite_initialize(sdkmgr_handle); if (GNSDK_SUCCESS != error) { _display_last_error(__LINE__); rc = -1; } } /* For this library to successfully operate, you must first establish a valid storage folder path using the * GNSDK_SQLITE_OPTION_STORAGE_FOLDER option. */ error = gnsdk_storage_sqlite_option_set( GNSDK_STORAGE_SQLITE_OPTION_STORAGE_FOLDER, // option name : storage folder [required] "/var/mobile/Applications/1B3B6648-8D50-430F-B7D2-21D99AA78B6F/Documents" // option value : "." set it to current directory for this sample ); if( GNSDK_SUCCESS != error) { _display_last_error(__LINE__); return -1; } error = gnsdk_manager_storage_location_set(GNSDK_MANAGER_STORAGE_QUERYCACHE, "/var/mobile/Applications/1B3B6648-8D50-430F-B7D2-21D99AA78B6F/Documents/querycache"); if (GNSDK_SUCCESS != error) { _display_last_error(__LINE__); } /* Initialize the Link Content Library */ if (0 == rc) { error = gnsdk_link_initialize(sdkmgr_handle); if (GNSDK_SUCCESS != error) { _display_last_error(__LINE__); rc = -1; } } /* Get a user handle for our client ID. This will be passed in for all queries */ if (0 == rc) { rc = _get_user_handle( client_id, client_id_tag, client_app_version, &user_handle ); } /* Set the ‘locale‘ to return locale-specifc results values. This examples loads an English locale. */ if (0 == rc) rc = _set_locale(user_handle); if (0 != rc) { /* Clean up on failure. */ _shutdown_gnsdk(user_handle); } else { /* return the User handle for use at query time */ *p_user_handle = user_handle; } return rc; } /* _init_gnsdk() */ /****************************************************************** * * _SHUTDOWN_GNSDK * * Call shutdown all initialized GNSDK modules. * Release all existing handles before shutting down any of the modules. * Shutting down the Manager module should occur last, but the shutdown ordering of * all other modules does not matter. * *****************************************************************/ static void _shutdown_gnsdk (gnsdk_user_handle_t user_handle) { gnsdk_error_t error = GNSDK_SUCCESS; error = gnsdk_manager_user_release(user_handle); if (GNSDK_SUCCESS != error) { _display_last_error(__LINE__); } /* Shutdown the Manager to shutdown all libraries */ gnsdk_manager_shutdown(); } /*----------------------------------------------------------------------------- * _fetch_image */ static int _fetch_image( gnsdk_link_query_handle_t query_handle, gnsdk_link_content_type_t image_type, gnsdk_char_t* image_type_str ) { gnsdk_link_data_type_t data_type = gnsdk_link_data_unknown; gnsdk_byte_t* buffer = GNSDK_NULL; gnsdk_size_t buffer_size = 0; int rc = 0; gnsdk_error_t error = GNSDK_SUCCESS; error = gnsdk_link_query_content_retrieve( query_handle, image_type, 1, &data_type, &buffer, &buffer_size); if (GNSDK_SUCCESS == error) { /* data_type will always be == gnsdk_link_data_image_jpeg */ /* Do something with the image, e.g. display, save, etc. Here we just print the size. */ printf("\nRETRIEVED: %s image: %d byte JPEG\n", image_type_str, (gnsdk_uint32_t)buffer_size); response_data = buffer; response_data_length = buffer_size; // for (int i = 0; i < buffer_size; i++) { // printf("%x", *(buffer + i)); // } /* free the data when you are done with it */ error = gnsdk_link_query_content_free(buffer); if (GNSDK_SUCCESS != error) { _display_last_error(__LINE__); rc = -1; } } else { if (GNSDKERR_NotFound != GNSDKERR_ERROR_CODE(error)) { _display_last_error(__LINE__); rc = -1; } else { /* Do not return error code for not found. */ /* For image to be fetched, it must exist in the size specified and you must be entitled to fetch images. */ printf("\nNOT FOUND: %s image\n", image_type_str); } } return rc; } /*----------------------------------------------------------------------------- * _query_for_album_images */ static void _query_for_album_images (gnsdk_user_handle_t user_handle) { gnsdk_gdo_handle_t input_gdo = GNSDK_NULL; gnsdk_link_query_handle_t query_handle = GNSDK_NULL; gnsdk_cstr_t image_size = GNSDK_NULL; gnsdk_cstr_t preferred_image_size = GNSDK_NULL; gnsdk_error_t error = GNSDK_SUCCESS; printf("\n*****Sample Link Album Query*****\n"); /* Create the query handle. Do not include a callback or callback data (2nd & 3rd args are GNSDK_NULL). */ error = gnsdk_link_query_create(user_handle, GNSDK_NULL, GNSDK_NULL, &query_handle); if (GNSDK_SUCCESS != error) { _display_last_error(__LINE__); return; } /* Set the input GDO */ if (GNSDK_SUCCESS == error) { gnsdk_cstr_t serialized_gdo = "WEcxAbwX1+DYDXSI3nZZ/L9ntBr8EhRjYAYzNEwlFNYCWkbGGLvyitwgmBccgJtgIM/dkcbDgrOqBMIQJZMmvysjCkx10ppXc68ZcgU0SgLelyjfo1Tt7Ix/cn32BvcbeuPkAk0WwwReVdcSLuO8cYxAGcGQrEE+4s2H75HwxFG28r/yb2QX71pR"; /* Typically, the GDO passed in to a Link query will come from the output of a GNSDK query. * For an example of how to perform a query and get a GDO please refer to the documentation * or other sample applications. * The below serialized GDO was an 1-track album result from another GNSDK query. */ error = gnsdk_manager_gdo_deserialize(serialized_gdo, &input_gdo); if (GNSDK_SUCCESS == error) { error = gnsdk_link_query_set_gdo(query_handle, input_gdo); if (GNSDK_SUCCESS != error) { _display_last_error(__LINE__); } gnsdk_manager_gdo_release(input_gdo); } else { _display_last_error(__LINE__); } } /* Set preferred image size */ preferred_image_size = GNSDK_LINK_OPTION_VALUE_IMAGE_SIZE_170; /* Obtain image size available */ image_size = preferred_image_size; /* Specify the desired image size */ if (GNSDK_SUCCESS == error) { if (GNSDK_NULL != image_size) { error = gnsdk_link_query_option_set( query_handle, GNSDK_LINK_OPTION_KEY_IMAGE_SIZE, image_size ); if (GNSDK_SUCCESS != error) { _display_last_error(__LINE__); } else { /* Perform the image fetches */ _fetch_image(query_handle, gnsdk_link_content_cover_art, "cover art"); // _fetch_image(query_handle, gnsdk_link_content_image_artist, "artist"); } } } /* release the Link query handle */ gnsdk_link_query_release(query_handle); } /* _query_for_album_images() */
注意,为了便利(个人也非常的懒惰),代码中关于路径的代码直接使用了硬编码,例如:
"/var/mobile/Applications/1B3B6648-8D50-430F-B7D2-21D99AA78B6F/Documents/querycache"。
再写个Test.h开放接口出来:
#ifndef GNTest_iOS_Test_h #define GNTest_iOS_Test_h int coverart_main(int argc, const char* argv[]); #endif
最后回到一个普通的ViewController文件中,代码如下:
#import "ViewController.h" #import "Test.h" #import "ShareData.h" #include "gnsdk.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; NSString *licencePath = [[NSBundle mainBundle] pathForResource:@"my_licence" ofType:@"txt"]; const char* pathPtr = [licencePath UTF8String]; const char* argv[] = {NULL, "4541440", "79EFBF4E21724D084BA87FF9B242F0C9", pathPtr}; NSLog(@"%@", licencePath); coverart_main(4, argv); extern gnsdk_byte_t* response_data; extern gnsdk_size_t response_data_length; NSData *data = [NSData dataWithBytes:response_data length:response_data_length]; NSLog(@"%@", data); UIImage *image = [UIImage imageWithData:data]; [self.album_imageView setImage:image]; } @end
简单说下其运行过程,在sample中有一个所谓的序列化GDO(GraceNote Data Objects,是该网站自定的一种数据模型),该GDO由一列字符串标识,它对应的音乐专辑就是我们的查询目标。
gnsdk_cstr_t serialized_gdo = "WEcxAbwX1+DYDXSI3nZZ/L9ntBr8EhRjYAYzNEwlFNYCWkbGGLvyitwgmBccgJtgIM/dkcbDgrOqBMIQJZMmvysjCkx10ppXc68ZcgU0SgLelyjfo1Tt7Ix/cn32BvcbeuPkAk0WwwReVdcSLuO8cYxAGcGQrEE+4s2H75HwxFG28r/yb2QX71pR";
const char* pathPtr = [licencePath UTF8String]; const char* argv[] = {NULL, "4541440", "79EFBF4E21724D084BA87FF9B242F0C9", pathPtr}; NSLog(@"%@", licencePath); coverart_main(4, argv);
其中argv[]中的第二个参数是App Details中的client id,第三个参数是App Details中的client tag,第四个参数是上面的my_licence.txt文件的路径。
在查询完成后,网站返回的二进制数据保存在一个缓冲区中,我将其起始位置和缓冲区长度保存在以下全局变量中:
gnsdk_byte_t* response_data; gnsdk_size_t response_data_length;
最后解析缓冲区中的数据,并以UIImage形式展示出来:
extern gnsdk_byte_t* response_data; extern gnsdk_size_t response_data_length; NSData *data = [NSData dataWithBytes:response_data length:response_data_length]; NSLog(@"%@", data); UIImage *image = [UIImage imageWithData:data]; [self.album_imageView setImage:image];
接着我又偷了懒,打开PP助手,在app的Documents目录下创建一个querycache目录(用于存储查询结果到本地缓存中,这一步必不可少):
在armv7s 32位机子上的运行。
控制台部分输出如下:
2014-04-25 22:43:55.906 GNTest_iOS[784:60b] /var/mobile/Applications/1B3B6648-8D50-430F-B7D2-21D99AA78B6F/GNTest_iOS.app/my_licence.txt *****Sample Link Album Query***** RETRIEVED: cover art image: 11808 byte JPEG 2014-04-25 22:43:58.470 GNTest_iOS[784:60b] <ffd8ffe0 00104a46 49460001 01010060 00600000 ffdb0043 00080606 07060508 07070709 09080a0c 140d0c0b 0b0c1912 130f141d 1a1f1e1d 1a1c1c20 242e2720 222c231c 1c283729 2c303134 34341f27 393d3832 3c2e3334 32ffdb00 43010909 090c0b0c 180d0d18 32211c21 32323232 32323232 32323232 32323232 32323232 32323232 32323232 32323232 32323232 32323232 32323232 32323232 3232ffc0 00110800 aa00aa03 01220002 11010311 01ffc400 1f000001 05010101 01010100 00000000 00000001 02030405 06070809 0a0bffc4 00b51000 02010303 02040305 05040400 00017d01
运行结果如下,也就是上面的GDO对应的专辑封面图片:
本文比较乱,Demo中的代码还有很多地方要改善。等后面将关键的技术问题解决后再写篇博客好好梳理下,到时候我会给出无需任何手工操作的Demo。