Merge pull request #47 from fangfufu/id3

Id3
This commit is contained in:
Fufu Fang 2019-10-27 22:16:04 +00:00 committed by GitHub
commit ac3cea80d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 428 additions and 125 deletions

View File

@ -16,6 +16,8 @@ Jerome Charaoui
### Fixed
- Remove the erroneous error messages when the user supplies wrong command line
options.
- The same cache folder is used, irrespective whether the server root URL ends
with '/'
## [1.1.10] - 2019-09-10
### Added

View File

@ -1,6 +1,6 @@
VERSION=1.2.0
CFLAGS += -O2 -Wall -Wextra -Wshadow -rdynamic -D_GNU_SOURCE\
CFLAGS += -g -O2 -Wall -Wextra -Wshadow -rdynamic -D_GNU_SOURCE\
-D_FILE_OFFSET_BITS=64 -DVERSION=\"$(VERSION)\"\
`pkg-config --cflags-only-I gumbo libcurl fuse uuid expat`
LIBS = -pthread -lgumbo -lcurl -lfuse -lcrypto -luuid -lexpat

View File

@ -54,10 +54,14 @@ HTTPDirFS options:
--retry-wait Set delay in seconds before retrying an HTTP request
after encountering an error. (default: 5)
--user-agent Set user agent string (default: "HTTPDirFS")
--no-range-check Disable the build-in check for the server's support
for HTTP range requests
Subsonic options:
For mounting a Airsonic / Subsonic server:
--sonic-username The username for your Airsonic / Subsonic server
--sonic-password The username for your Airsonic / Subsonic server
--sonic-id3 Enable ID3 mode - this present the server content in
Artist/Album/Song layout
FUSE options:
@ -92,15 +96,32 @@ please refer to
## Airsonic / Subsonic server support
This is a new feature to 1.2.0. Now you can mount the music collection on your
Airsonic / Subsonic server, and browse them using your favourite file browser.
Airsonic / Subsonic server (*sonic), and browse them using your favourite file browser.
You simply have to supply both ``--sonic-username`` and ``--sonic-password`` to
trigger the Airsonic / Subsonic server mode. For example:
trigger the *sonic server mode. For example:
./httpdirfs -f --cache --sonic-username $USERNAME --sonic-password $PASSWORD $URL $MOUNT_POINT
You definitely want to enable the cache for this one, otherwise it is painfully
slow.
There are two ways of mounting your *sonic server
- the index mode
- and the ID3 mode.
In the index mode, the filesystem is presented based on the listing on the
``Index`` link in your *sonic's home page.
In ID3 mode, the filesystem is presented using the following hierarchy:
0. Root
1. Alphabetical indices of the Artists' name
2. The Arists' name
3. All of the albums by a single artist
4. All the songs in an album.
By default, *sonic server is mounted in the Index mode. If you want to mount in
ID3 mode, please use the ``--sonic-id3`` flag.
## Configuration file support
This program has basic support for using a configuration file. The configuration
file that the program reads is ``${XDG_CONFIG_HOME}/httpdirfs/config``, which by
@ -143,15 +164,24 @@ enable them, compile the program with the ``-DCACHE_LOCK_DEBUG``, the
make CPPFLAGS=-DCACHE_LOCK_DEBUG
## The Technical Details
This program downloads the HTML web pages/files using
[libcurl](https://curl.haxx.se/libcurl/), then parses the listing pages using
For the normal HTTP directories, this program downloads the HTML web pages/files
using [libcurl](https://curl.haxx.se/libcurl/), then parses the listing pages using
[Gumbo](https://github.com/google/gumbo-parser), and presents them using
[libfuse](https://github.com/libfuse/libfuse).
For *sonic servers, rather than using the Gumbo parser, this program parse
*sonic servers' XML responses using
[expat](https://github.com/libexpat/libexpat).
The cache system stores the metadata and the downloaded file into two
separate directories. It uses ``uint8_t`` arrays to record which segments of the
file had been downloaded.
Note that HTTPDirFS requires the server to support HTTP Range Request, some
servers support this features, but does not present ``"Accept-Ranges: bytes`` in
the header responses. HTTPDirFS by default checks for this header field. You can
disable this check by using the ``--no-range-check`` flag.
## Other projects which incorporate HTTPDirFS
- [Curious Container](https://www.curious-containers.cc/docs/red-connector-http#mount-dir)
has a Python wrapper for mounting HTTPDirFS.

View File

@ -585,7 +585,7 @@ void Cache_delete(const char *fn)
{
if (CONFIG.sonic_mode) {
Link *link = path_to_Link(fn);
fn = link->sonic_id_str;
fn = link->sonic_song_id_str;
}
char *metafn = path_append(META_DIR, fn);
@ -678,7 +678,7 @@ int Cache_create(const char *path)
fn = curl_easy_unescape(NULL, this_link->f_url + ROOT_LINK_OFFSET, 0,
NULL);
} else {
fn = this_link->sonic_id_str;
fn = this_link->sonic_song_id_str;
}
fprintf(stderr, "Cache_create(): Creating cache files for %s.\n", fn);
@ -774,7 +774,7 @@ Cache *Cache_open(const char *fn)
return NULL;
}
} else {
if (!Cache_exist(link->sonic_id_str)) {
if (!Cache_exist(link->sonic_song_id_str)) {
return NULL;
}
}
@ -788,7 +788,7 @@ Cache *Cache_open(const char *fn)
/* Set the path for the local cache file, if we are in sonic mode */
if (CONFIG.sonic_mode) {
fn = link->sonic_id_str;
fn = link->sonic_song_id_str;
}
cf->path = strndup(fn, MAX_PATH_LEN);

View File

@ -6,6 +6,7 @@
#include <gumbo.h>
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <stdlib.h>
@ -27,8 +28,15 @@ int ROOT_LINK_OFFSET = 0;
*/
static pthread_mutex_t link_lock;
LinkTable *LinkSystem_init(const char *url)
LinkTable *LinkSystem_init(const char *raw_url)
{
/* Remove excess '/' if it is there */
char *url = strdup(raw_url);
int url_len = strnlen(url, MAX_PATH_LEN) - 1;
if (url[url_len] == '/') {
url[url_len] = '\0';
}
if (pthread_mutex_init(&link_lock, NULL) != 0) {
fprintf(stderr,
"link_system_init(): link_lock initialisation failed!\n");
@ -37,17 +45,7 @@ LinkTable *LinkSystem_init(const char *url)
/* --------- Set the length of the root link ----------- */
/* This is where the '/' should be */
ROOT_LINK_OFFSET = strnlen(url, MAX_PATH_LEN) - 1;
if (url[ROOT_LINK_OFFSET] != '/') {
/*
* If '/' is not there, it is automatically added, so we need to skip 2
* characters
*/
ROOT_LINK_OFFSET += 2;
} else {
/* If '/' is there, we need to skip it */
ROOT_LINK_OFFSET += 1;
}
ROOT_LINK_OFFSET = strnlen(url, MAX_PATH_LEN) + 1;
/* --------------------- Enable cache system -------------------- /
*
@ -67,7 +65,11 @@ LinkTable *LinkSystem_init(const char *url)
ROOT_LINK_TBL = LinkTable_new(url);
} else {
sonic_config_init(url, CONFIG.sonic_username, CONFIG.sonic_password);
ROOT_LINK_TBL = sonic_LinkTable_new(0);
if (!CONFIG.sonic_id3) {
ROOT_LINK_TBL = sonic_LinkTable_new_index(0);
} else {
ROOT_LINK_TBL = sonic_LinkTable_new_id3(0, 0);
}
}
return ROOT_LINK_TBL;
}
@ -400,6 +402,7 @@ LinkTable *LinkTable_alloc(const char *url)
Link *head_link = Link_new("/", LINK_HEAD);
LinkTable_add(linktbl, head_link);
strncpy(head_link->f_url, url, MAX_PATH_LEN);
assert(linktbl->num == 1);
return linktbl;
}
@ -597,14 +600,21 @@ LinkTable *LinkTable_disk_open(const char *dirn)
LinkTable *path_to_Link_LinkTable_new(const char *path)
{
Link *link = path_to_Link(path);
if (!link->next_table) {
LinkTable *next_table = link->next_table;
if (!next_table) {
if (!CONFIG.sonic_mode) {
link->next_table = LinkTable_new(link->f_url);
next_table = LinkTable_new(link->f_url);
} else {
link->next_table = sonic_LinkTable_new(link->sonic_id);
if (!CONFIG.sonic_id3) {
next_table = sonic_LinkTable_new_index(link->sonic_id);
} else {
next_table = sonic_LinkTable_new_id3(link->sonic_depth,
link->sonic_id);
}
}
}
return link->next_table;
link->next_table = next_table;
return next_table;
}
static Link *path_to_Link_recursive(char *path, LinkTable *linktbl)
@ -645,17 +655,24 @@ static Link *path_to_Link_recursive(char *path, LinkTable *linktbl)
for (int i = 1; i < linktbl->num; i++) {
if (!strncmp(path, linktbl->links[i]->linkname, MAX_FILENAME_LEN)) {
/* The next sub-directory exists */
if (!linktbl->links[i]->next_table) {
LinkTable *next_table = linktbl->links[i]->next_table;
if (!next_table) {
if (!CONFIG.sonic_mode) {
linktbl->links[i]->next_table = LinkTable_new(
next_table = LinkTable_new(
linktbl->links[i]->f_url);
} else {
linktbl->links[i]->next_table = sonic_LinkTable_new(
linktbl->links[i]->sonic_id);
if (!CONFIG.sonic_id3) {
next_table = sonic_LinkTable_new_index(
linktbl->links[i]->sonic_id);
} else {
next_table = sonic_LinkTable_new_id3(
linktbl->links[i]->sonic_depth,
linktbl->links[i]->sonic_id);
}
}
}
return path_to_Link_recursive(
next_path, linktbl->links[i]->next_table);
linktbl->links[i]->next_table = next_table;
return path_to_Link_recursive(next_path, next_table);
}
}
}
@ -718,10 +735,12 @@ long path_download(const char *path, char *output_buf, size_t size,
transfer_blocking(curl);
/* Check for range seek support */
if (!strcasestr((header.data), "Accept-Ranges: bytes")) {
fprintf(stderr, "Error: This web server does not support HTTP \
if (!CONFIG.no_range_check) {
if (!strcasestr((header.data), "Accept-Ranges: bytes")) {
fprintf(stderr, "Error: This web server does not support HTTP \
range requests\n");
exit(EXIT_FAILURE);
exit(EXIT_FAILURE);
}
}
free(header.data);

View File

@ -69,14 +69,25 @@ struct Link {
/** \brief The pointer associated with the cache file */
Cache *cache_ptr;
/**
* \brief Sonic Music Directory ID
* \details We use linkname to store filename
* \brief Sonic id field
* \details This is used to store the followings:
* - Arist ID
* - Album ID
* - Song ID
* - Sub-directory ID (in the XML response, this is the ID on the "child"
* element)
*/
int sonic_id;
/**
* \brief Sonic Music Directory ID in string format
* \brief Sonic directory depth
* \details This is used exclusively in ID3 mode to store the depth of the
* current directory.
*/
char *sonic_id_str;
int sonic_depth;
/**
* \brief The sonic song's ID in character array format.
*/
char *sonic_song_id_str;
};
struct LinkTable {
@ -97,7 +108,7 @@ extern int ROOT_LINK_OFFSET;
/**
* \brief initialise link sub-system.
*/
LinkTable *LinkSystem_init(const char *url);
LinkTable *LinkSystem_init(const char *raw_url);
/**
* \brief Add a link to the curl multi bundle for querying stats

View File

@ -72,7 +72,7 @@ int main(int argc, char **argv)
CONFIG.sonic_mode = 1;
} else if (CONFIG.sonic_username || CONFIG.sonic_password) {
fprintf(stderr,
"Error: You have to supply both username and password to\
"Error: You have to supply both username and password to \
activate Sonic mode.\n");
exit(EXIT_FAILURE);
}
@ -151,6 +151,8 @@ parse_arg_list(int argc, char **argv, char ***fuse_argv, int *fuse_argc)
{"cache-location", required_argument, NULL, 'L'}, /* 14 */
{"sonic-username", required_argument, NULL, 'L'}, /* 15 */
{"sonic-password", required_argument, NULL, 'L'}, /* 16 */
{"sonic-id3", no_argument, NULL, 'L'}, /* 17 */
{"no-range-check", no_argument, NULL, 'L'}, /* 18 */
{0, 0, 0, 0}
};
while ((c =
@ -224,6 +226,12 @@ parse_arg_list(int argc, char **argv, char ***fuse_argv, int *fuse_argc)
case 16:
CONFIG.sonic_password = strdup(optarg);
break;
case 17:
CONFIG.sonic_id3 = 1;
break;
case 18:
CONFIG.no_range_check = 1;
break;
default:
fprintf(stderr, "see httpdirfs -h for usage\n");
return 1;
@ -296,9 +304,13 @@ HTTPDirFS options:\n\
--retry-wait Set delay in seconds before retrying an HTTP request\n\
after encountering an error. (default: 5)\n\
--user-agent Set user agent string (default: \"HTTPDirFS\")\n\
--no-range-check Disable the build-in check for the server's support\n\
for HTTP range requests\n\
\n\
For mounting a Airsonic / Subsonic server:\n\
--sonic-username The username for your Airsonic / Subsonic server\n\
--sonic-password The username for your Airsonic / Subsonic server\n\
--sonic-id3 Enable ID3 mode - this present the server content in\n\
Artist/Album/Song layout \n\
\n");
}

View File

@ -6,11 +6,11 @@
#include <expat.h>
#include <assert.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
typedef struct {
char *server;
char *username;
@ -90,6 +90,30 @@ static char *sonic_getMusicDirectory_link(const int id)
return url;
}
/**
* \brief generate a getArtist request URL
*/
static char *sonic_getArtist_link(const int 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=%d", first_part, id);
free(first_part);
return url;
}
/**
* \brief generate a getAlbum request URL
*/
static char *sonic_getAlbum_link(const int 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=%d", first_part, id);
free(first_part);
return url;
}
/**
* \brief generate a download request URL
*/
@ -98,13 +122,13 @@ static char *sonic_stream_link(const int 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&estimateContentLength=true&format=raw&id=%d", first_part, id);
"%s&format=raw&id=%d", first_part, id);
free(first_part);
return url;
}
/**
* \brief Process a single element output by the parser
* \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.
@ -117,18 +141,14 @@ static char *sonic_stream_link(const int id)
* parser terminates the strings properly, which is a fair assumption,
* considering how mature expat is.
*/
static void XMLCALL XML_process_single_element(void *data, const char *elem,
static void XMLCALL XML_parser_index(void *data, const char *elem,
const char **attr)
{
LinkTable *linktbl = (LinkTable *) data;
Link *link;
if (!strcmp(elem, "child")) {
/* Return from getMusicDirectory */
link = CALLOC(1, sizeof(Link));
link->type = LINK_INVALID;
} else if (!strcmp(elem, "artist")){
/* Return from getIndexes */
if (!strcmp(elem, "child") || !strcmp(elem, "artist")) {
link = CALLOC(1, sizeof(Link));
/* Initialise to LINK_DIR, as the LINK_FILE is set later. */
link->type = LINK_DIR;
} else {
/* The element does not contain directory structural information */
@ -141,27 +161,13 @@ static void XMLCALL XML_process_single_element(void *data, const char *elem,
for (int i = 0; attr[i]; i += 2) {
if (!strcmp("id", attr[i])) {
link->sonic_id = atoi(attr[i+1]);
link->sonic_id_str = calloc(MAX_FILENAME_LEN, sizeof(char));
snprintf(link->sonic_id_str, MAX_FILENAME_LEN, "%d",
link->sonic_song_id_str = calloc(MAX_FILENAME_LEN, sizeof(char));
snprintf(link->sonic_song_id_str, MAX_FILENAME_LEN, "%d",
link->sonic_id);
id_set = 1;
continue;
}
/*
* "title" is used for directory name,
* "name" is for top level directories
*/
if (!strcmp("title", attr[i]) || !strcmp("name", attr[i])) {
strncpy(link->linkname, attr[i+1], MAX_FILENAME_LEN);
linkname_set = 1;
continue;
}
/*
* Path always appears after title, it is used for filename.
* This is why it is safe to rewrite linkname
*/
if (!strcmp("path", attr[i])) {
memset(link->linkname, 0, MAX_FILENAME_LEN);
/* Skip to the last '/' if it exists */
@ -175,11 +181,25 @@ static void XMLCALL XML_process_single_element(void *data, const char *elem,
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("true", attr[i+1])) {
link->type = LINK_DIR;
} else if (!strcmp("false", attr[i+1])) {
if (!strcmp("false", attr[i+1])) {
link->type = LINK_FILE;
char *url = sonic_stream_link(link->sonic_id);
strncpy(link->f_url, url, MAX_PATH_LEN);
free(url);
}
continue;
}
@ -198,32 +218,8 @@ static void XMLCALL XML_process_single_element(void *data, const char *elem,
}
}
/* Clean up if linkname is not set */
if (!linkname_set) {
free(link);
return;
}
/* Clean up if id is not set */
if (!id_set) {
if (linkname_set) {
free(link->linkname);
}
free(link);
return;
}
if (link->type == LINK_DIR) {
char *url = sonic_getMusicDirectory_link(link->sonic_id);
strncpy(link->f_url, url, MAX_PATH_LEN);
free(url);
} else if (link->type == LINK_FILE) {
char *url = sonic_stream_link(link->sonic_id);
strncpy(link->f_url, url, MAX_PATH_LEN);
free(url);
} else {
/* Invalid link */
free(link->linkname);
/* Clean up if linkname or id is not set */
if (!linkname_set || !id_set) {
free(link);
return;
}
@ -234,32 +230,12 @@ static void XMLCALL XML_process_single_element(void *data, const char *elem,
/**
* \brief parse a XML string in order to fill in the LinkTable
*/
static void sonic_XML_to_LinkTable(DataStruct ds, LinkTable *linktbl)
static LinkTable *sonic_url_to_LinkTable(const char *url,
XML_StartElementHandler handler,
int depth)
{
XML_Parser parser = XML_ParserCreate(NULL);
XML_SetUserData(parser, linktbl);
XML_SetStartElementHandler(parser, XML_process_single_element);
if (XML_Parse(parser, ds.data, ds.size, 1) == XML_STATUS_ERROR) {
fprintf(stderr,
"sonic_XML_to_LinkTable(): Parse error at line %lu: %s\n",
XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser)));
}
XML_ParserFree(parser);
}
LinkTable *sonic_LinkTable_new(const int id)
{
char *url;
if (id > 0) {
url = sonic_getMusicDirectory_link(id);
} else {
url = sonic_gen_url_first_part("getIndexes");
}
printf("%s\n", url);
LinkTable *linktbl = LinkTable_alloc(url);
linktbl->links[0]->sonic_depth = depth;
/* start downloading the base URL */
DataStruct xml = Link_to_DataStruct(linktbl->links[0]);
@ -268,11 +244,241 @@ LinkTable *sonic_LinkTable_new(const int id)
return NULL;
}
sonic_XML_to_LinkTable(xml, linktbl);
XML_Parser parser = XML_ParserCreate(NULL);
XML_SetUserData(parser, linktbl);
XML_SetStartElementHandler(parser, handler);
if (XML_Parse(parser, xml.data, xml.size, 1) == XML_STATUS_ERROR) {
fprintf(stderr,
"sonic_XML_to_LinkTable(): Parse error at line %lu: %s\n",
XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser)));
}
XML_ParserFree(parser);
free(xml.data);
LinkTable_print(linktbl);
free(xml.data);
return linktbl;
}
LinkTable *sonic_LinkTable_new_index(const int id)
{
char *url;
if (id > 0) {
url = sonic_getMusicDirectory_link(id);
} else {
url = sonic_gen_url_first_part("getIndexes");
}
LinkTable *linktbl = sonic_url_to_LinkTable(url, XML_parser_index, 0);
free(url);
return linktbl;
}
static void XMLCALL XML_parser_id3_root(void *data, const char *elem,
const char **attr)
{
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;
/* This 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 = atoi(attr[i+1]);
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, this element does not contain directory structural
* information */
}
static void XMLCALL XML_parser_id3(void *data, const char *elem,
const char **attr)
{
LinkTable *linktbl = (LinkTable *) data;
Link *link;
/*
* Please refer to the documentation at the function prototype of
* sonic_LinkTable_new_id3()
*/
if (!strcmp(elem, "album") && linktbl->links[0]->sonic_depth == 3) {
link = CALLOC(1, sizeof(Link));
link->type = LINK_DIR;
/* This table should be a level 3 album 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 {
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 = atoi(attr[i+1]);
link->sonic_song_id_str = calloc(MAX_FILENAME_LEN, sizeof(char));
snprintf(link->sonic_song_id_str, MAX_FILENAME_LEN, "%d",
link->sonic_id);
id_set = 1;
continue;
}
if (!strcmp("size", attr[i])) {
link->content_length = atoll(attr[i+1]);
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;
}
/* This is used by the album table */
if (!strcmp("name", attr[i])) {
strncpy(link->linkname, attr[i+1], MAX_FILENAME_LEN);
linkname_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;
}
if (!strcmp("track", attr[i])) {
track = atoi(attr[i+1]);
}
if (!strcmp("title", attr[i])) {
title = (char *) attr[i+1];
}
if (!strcmp("suffix", attr[i])) {
suffix = (char *) attr[i+1];
}
}
if (!linkname_set && strlen(title) > 0 && strlen(suffix) > 0) {
snprintf(link->linkname, MAX_FILENAME_LEN, "%02d - %s.%s",
track, title, suffix);
linkname_set = 1;
}
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);
}
LinkTable *sonic_LinkTable_new_id3(int depth, int 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_id3, 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_id3, depth);
free(url);
break;
default:
/*
* We shouldn't reach here.
*/
fprintf(stderr, "sonic_LinkTable_new_id3(): case %d.\n", depth);
exit_failure();
break;
}
return linktbl;
}

View File

@ -14,7 +14,22 @@ void sonic_config_init(const char *server, const char *username,
const char *password);
/**
* \brief Create a new Sonic LinkTable
* \brief Create a new Sonic LinkTable in index mode
*/
LinkTable *sonic_LinkTable_new(const int id);
LinkTable *sonic_LinkTable_new_index(const int id);
/**
* \brief Create a new Sonic LinkTable in ID3 mode
* \details In this mode, the filesystem effectively has 5 levels of which are:
* 0. Root table
* 1. Index table
* 2. Artist table
* 3. Album table
* 4. Song table
* 5. Individual song
* \param[in] depth the level of the requested table
* \param[in] id the id of the requested table
*/
LinkTable *sonic_LinkTable_new_id3(int depth, int id);
#endif

View File

@ -72,6 +72,8 @@ void Config_init(void)
CONFIG.http_wait_sec = DEFAULT_HTTP_WAIT_SEC;
CONFIG.no_range_check = 0;
/*--------------- Cache related ---------------*/
CONFIG.cache_enabled = 0;
@ -87,6 +89,8 @@ void Config_init(void)
CONFIG.sonic_username = NULL;
CONFIG.sonic_password = NULL;
CONFIG.sonic_id3 = 0;
}
char *path_append(const char *path, const char *filename)

View File

@ -53,6 +53,8 @@ typedef struct {
char *user_agent;
/** \brief The waiting time after getting HTTP 429 (too many requests) */
int http_wait_sec;
/** \brief Disable check for the server's support of HTTP range request */
int no_range_check;
/** \brief Whether cache mode is enabled */
int cache_enabled;
@ -69,6 +71,8 @@ typedef struct {
char *sonic_username;
/** \brief The Sonic server password */
char *sonic_password;
/** \brief Whether we are using Sonic mode ID3 extension */
int sonic_id3;
} ConfigStruct;
/**