commit
ac3cea80d4
|
@ -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
|
||||
|
|
2
Makefile
2
Makefile
|
@ -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
|
||||
|
|
40
README.md
40
README.md
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
71
src/link.c
71
src/link.c
|
@ -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);
|
||||
|
|
21
src/link.h
21
src/link.h
|
@ -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
|
||||
|
|
14
src/main.c
14
src/main.c
|
@ -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");
|
||||
}
|
||||
|
|
368
src/sonic.c
368
src/sonic.c
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
19
src/sonic.h
19
src/sonic.h
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue