#include "sonic.h" #include "config.h" #include "log.h" #include "link.h" #include "memcache.h" #include "util.h" #include #include #include #include #include typedef struct { char *server; char *username; char *password; char *client; char *api_version; } SonicConfigStruct; static SonicConfigStruct SONIC_CONFIG; /** * \brief initialise Sonic configuration struct */ void sonic_config_init(const char *server, const char *username, const char *password) { SONIC_CONFIG.server = strndup(server, MAX_PATH_LEN); /* * Correct for the extra '/' */ size_t server_url_len = strnlen(SONIC_CONFIG.server, MAX_PATH_LEN) - 1; if (SONIC_CONFIG.server[server_url_len] == '/') { SONIC_CONFIG.server[server_url_len] = '\0'; } SONIC_CONFIG.username = strndup(username, MAX_FILENAME_LEN); SONIC_CONFIG.password = strndup(password, MAX_FILENAME_LEN); SONIC_CONFIG.client = DEFAULT_USER_AGENT; if (!CONFIG.sonic_insecure) { /* * API 1.13.0 is the minimum version that supports * salt authentication scheme */ SONIC_CONFIG.api_version = "1.13.0"; } else { /* * API 1.8.0 is the minimum version that supports ID3 mode */ SONIC_CONFIG.api_version = "1.8.0"; } } /** * \brief generate authentication string */ static char *sonic_gen_auth_str(void) { if (!CONFIG.sonic_insecure) { char *salt = generate_salt(); size_t pwd_len = strnlen(SONIC_CONFIG.password, MAX_FILENAME_LEN); size_t pwd_salt_len = pwd_len + strnlen(salt, MAX_FILENAME_LEN); char *pwd_salt = CALLOC(pwd_salt_len + 1, sizeof(char)); strncat(pwd_salt, SONIC_CONFIG.password, MAX_FILENAME_LEN); strncat(pwd_salt + pwd_len, salt, MAX_FILENAME_LEN); char *token = generate_md5sum(pwd_salt); char *auth_str = CALLOC(MAX_PATH_LEN + 1, sizeof(char)); snprintf(auth_str, MAX_PATH_LEN, ".view?u=%s&t=%s&s=%s&v=%s&c=%s", SONIC_CONFIG.username, token, salt, SONIC_CONFIG.api_version, SONIC_CONFIG.client); FREE(salt); FREE(token); return auth_str; } else { char *pwd_hex = str_to_hex(SONIC_CONFIG.password); char *auth_str = CALLOC(MAX_PATH_LEN + 1, sizeof(char)); snprintf(auth_str, MAX_PATH_LEN, ".view?u=%s&p=enc:%s&v=%s&c=%s", SONIC_CONFIG.username, pwd_hex, SONIC_CONFIG.api_version, SONIC_CONFIG.client); FREE(pwd_hex); return auth_str; } } /** * \brief generate the first half of the request URL */ static char *sonic_gen_url_first_part(char *method) { char *auth_str = sonic_gen_auth_str(); char *url = CALLOC(MAX_PATH_LEN + 1, sizeof(char)); snprintf(url, MAX_PATH_LEN, "%s/rest/%s%s", SONIC_CONFIG.server, method, auth_str); FREE(auth_str); return url; } /** * \brief generate a getMusicDirectory request URL */ static char *sonic_getMusicDirectory_link(const char *id) { char *first_part = sonic_gen_url_first_part("getMusicDirectory"); char *url = CALLOC(MAX_PATH_LEN + 1, sizeof(char)); snprintf(url, MAX_PATH_LEN, "%s&id=%s", first_part, id); FREE(first_part); return url; } /** * \brief generate a getArtist request URL */ static char *sonic_getArtist_link(const char *id) { char *first_part = sonic_gen_url_first_part("getArtist"); char *url = CALLOC(MAX_PATH_LEN + 1, sizeof(char)); snprintf(url, MAX_PATH_LEN, "%s&id=%s", first_part, id); FREE(first_part); return url; } /** * \brief generate a getAlbum request URL */ static char *sonic_getAlbum_link(const char *id) { char *first_part = sonic_gen_url_first_part("getAlbum"); char *url = CALLOC(MAX_PATH_LEN + 1, sizeof(char)); snprintf(url, MAX_PATH_LEN, "%s&id=%s", first_part, id); FREE(first_part); return url; } /** * \brief generate a download request URL */ static char *sonic_stream_link(const char *id) { char *first_part = sonic_gen_url_first_part("stream"); char *url = CALLOC(MAX_PATH_LEN + 1, sizeof(char)); snprintf(url, MAX_PATH_LEN, "%s&format=raw&id=%s", first_part, id); FREE(first_part); return url; } /** * \brief The parser for Sonic index mode * \details This is the callback function called by the the XML parser. * \param[in] data user supplied data, in this case it is the pointer to the * LinkTable. * \param[in] elem the name of this element, it should be either "child" or * "artist" * \param[in] attr Each attribute seen in a start (or empty) tag occupies * 2 consecutive places in this vector: the attribute name followed by the * attribute value. These pairs are terminated by a null pointer. * \note we are using strcmp rather than strncmp, because we are assuming the * parser terminates the strings properly, which is a fair assumption, * considering how mature expat is. */ static void XMLCALL XML_parser_general(void *data, const char *elem, const char **attr) { /* * Error checking */ if (!strcmp(elem, "error")) { lprintf(error, "error:\n"); for (int i = 0; attr[i]; i += 2) { lprintf(error, "%s: %s\n", attr[i], attr[i + 1]); } } LinkTable *linktbl = (LinkTable *) data; Link *link; /* * Please refer to the documentation at the function prototype of * sonic_LinkTable_new_id3() */ if (!strcmp(elem, "child")) { link = CALLOC(1, sizeof(Link)); /* * Initialise to LINK_DIR, as the LINK_FILE is set later. */ link->type = LINK_DIR; } else if (!strcmp(elem, "artist") && linktbl->links[0]->sonic.depth != 3) { /* * We want to skip the first "artist" element in the album table */ link = CALLOC(1, sizeof(Link)); link->type = LINK_DIR; } else if (!strcmp(elem, "album") && linktbl->links[0]->sonic.depth == 3) { link = CALLOC(1, sizeof(Link)); link->type = LINK_DIR; /* * The new table should be a level 4 song table */ link->sonic.depth = 4; } else if (!strcmp(elem, "song") && linktbl->links[0]->sonic.depth == 4) { link = CALLOC(1, sizeof(Link)); link->type = LINK_FILE; } else { /* * The element does not contain directory structural information */ return; } int id_set = 0; int linkname_set = 0; int track = 0; char *title = ""; char *suffix = ""; for (int i = 0; attr[i]; i += 2) { if (!strcmp("id", attr[i])) { link->sonic.id = CALLOC(MAX_FILENAME_LEN + 1, sizeof(char)); strncpy(link->sonic.id, attr[i + 1], MAX_FILENAME_LEN); id_set = 1; continue; } if (!strcmp("path", attr[i])) { memset(link->linkname, 0, MAX_FILENAME_LEN); /* * Skip to the last '/' if it exists */ char *s = strrchr(attr[i + 1], '/'); if (s) { strncpy(link->linkname, s + 1, MAX_FILENAME_LEN); } else { strncpy(link->linkname, attr[i + 1], MAX_FILENAME_LEN); } linkname_set = 1; continue; } /* * "title" is used for directory name, * "name" is for top level directories * N.B. "path" attribute is given the preference */ if (!linkname_set) { if (!strcmp("title", attr[i]) || !strcmp("name", attr[i])) { strncpy(link->linkname, attr[i + 1], MAX_FILENAME_LEN); linkname_set = 1; continue; } } if (!strcmp("isDir", attr[i])) { if (!strcmp("false", attr[i + 1])) { link->type = LINK_FILE; } continue; } if (!strcmp("created", attr[i])) { struct tm *tm = CALLOC(1, sizeof(struct tm)); strptime(attr[i + 1], "%Y-%m-%dT%H:%M:%S.000Z", tm); link->time = mktime(tm); FREE(tm); continue; } if (!strcmp("size", attr[i])) { link->content_length = atoll(attr[i + 1]); continue; } if (!strcmp("track", attr[i])) { track = atoi(attr[i + 1]); continue; } if (!strcmp("title", attr[i])) { title = (char *) attr[i + 1]; continue; } if (!strcmp("suffix", attr[i])) { suffix = (char *) attr[i + 1]; continue; } } if (!linkname_set && strnlen(title, MAX_PATH_LEN) > 0 && strnlen(suffix, MAX_PATH_LEN) > 0) { snprintf(link->linkname, MAX_FILENAME_LEN, "%02d - %s.%s", track, title, suffix); linkname_set = 1; } /* * Clean up if linkname or id is not set */ if (!linkname_set || !id_set) { FREE(link); return; } if (link->type == LINK_FILE) { char *url = sonic_stream_link(link->sonic.id); strncpy(link->f_url, url, MAX_PATH_LEN); FREE(url); } LinkTable_add(linktbl, link); } static void sanitise_LinkTable(LinkTable *linktbl) { for (int i = 0; i < linktbl->num; i++) { if (!strcmp(linktbl->links[i]->linkname, ".")) { /* Note the super long sanitised name to avoid collision */ strcpy(linktbl->links[i]->linkname, "__DOT__"); } if (!strcmp(linktbl->links[i]->linkname, "/")) { /* Ditto */ strcpy(linktbl->links[i]->linkname, "__FORWARD-SLASH__"); } for (size_t j = 0; j < strlen(linktbl->links[i]->linkname); j++) { if (linktbl->links[i]->linkname[j] == '/') { linktbl->links[i]->linkname[j] = '-'; } } if (linktbl->links[i]->next_table != NULL) { sanitise_LinkTable(linktbl->links[i]->next_table); } } } /** * \brief parse a XML string in order to fill in the LinkTable */ static LinkTable *sonic_url_to_LinkTable(const char *url, XML_StartElementHandler handler, int depth) { LinkTable *linktbl = LinkTable_alloc(url); linktbl->links[0]->sonic.depth = depth; /* * start downloading the base URL */ TransferStruct xml = Link_download_full(linktbl->links[0]); if (xml.curr_size == 0) { LinkTable_free(linktbl); return NULL; } XML_Parser parser = XML_ParserCreate(NULL); XML_SetUserData(parser, linktbl); XML_SetStartElementHandler(parser, handler); if (XML_Parse(parser, xml.data, xml.curr_size, 1) == XML_STATUS_ERROR) { lprintf(error, "Parse error at line %lu: %s\n", XML_GetCurrentLineNumber(parser), XML_ErrorString(XML_GetErrorCode(parser))); } XML_ParserFree(parser); FREE(xml.data); LinkTable_print(linktbl); sanitise_LinkTable(linktbl); return linktbl; } LinkTable *sonic_LinkTable_new_index(const char *id) { char *url; if (strcmp(id, "0")) { url = sonic_getMusicDirectory_link(id); } else { url = sonic_gen_url_first_part("getIndexes"); } LinkTable *linktbl = sonic_url_to_LinkTable(url, XML_parser_general, 0); FREE(url); return linktbl; } static void XMLCALL XML_parser_id3_root(void *data, const char *elem, const char **attr) { if (!strcmp(elem, "error")) { lprintf(error, "\n"); for (int i = 0; attr[i]; i += 2) { lprintf(error, "%s: %s\n", attr[i], attr[i + 1]); } } LinkTable *root_linktbl = (LinkTable *) data; LinkTable *this_linktbl = NULL; /* * Set the current linktbl, if we have more than head link. */ if (root_linktbl->num > 1) { this_linktbl = root_linktbl->links[root_linktbl->num - 1]->next_table; } int id_set = 0; int linkname_set = 0; Link *link; if (!strcmp(elem, "index")) { /* * Add a subdirectory */ link = CALLOC(1, sizeof(Link)); link->type = LINK_DIR; for (int i = 0; attr[i]; i += 2) { if (!strcmp("name", attr[i])) { strncpy(link->linkname, attr[i + 1], MAX_FILENAME_LEN); linkname_set = 1; /* * Allocate a new LinkTable */ link->next_table = LinkTable_alloc("/"); } } /* * Make sure we don't add an empty directory */ if (linkname_set) { LinkTable_add(root_linktbl, link); } else { FREE(link); } return; } else if (!strcmp(elem, "artist")) { link = CALLOC(1, sizeof(Link)); link->type = LINK_DIR; /* * The new table should be a level 3 album table */ link->sonic.depth = 3; for (int i = 0; attr[i]; i += 2) { if (!strcmp("name", attr[i])) { strncpy(link->linkname, attr[i + 1], MAX_FILENAME_LEN); linkname_set = 1; continue; } if (!strcmp("id", attr[i])) { link->sonic.id = CALLOC(MAX_FILENAME_LEN + 1, sizeof(char)); strncpy(link->sonic.id, attr[i + 1], MAX_FILENAME_LEN); id_set = 1; continue; } } /* * Clean up if linkname is not set */ if (!linkname_set || !id_set) { FREE(link); return; } LinkTable_add(this_linktbl, link); } /* * If we reach here, then this element does not contain directory structural * information */ } LinkTable *sonic_LinkTable_new_id3(int depth, const char *id) { char *url; LinkTable *linktbl = ROOT_LINK_TBL; switch (depth) { /* * Root table */ case 0: url = sonic_gen_url_first_part("getArtists"); linktbl = sonic_url_to_LinkTable(url, XML_parser_id3_root, 0); FREE(url); break; /* * Album table - get all the albums of an artist */ case 3: url = sonic_getArtist_link(id); linktbl = sonic_url_to_LinkTable(url, XML_parser_general, depth); FREE(url); break; /* * Song table - get all the songs of an album */ case 4: url = sonic_getAlbum_link(id); linktbl = sonic_url_to_LinkTable(url, XML_parser_general, depth); FREE(url); break; default: /* * We shouldn't reach here. */ lprintf(fatal, "case %d.\n", depth); break; } return linktbl; }