diff --git a/configure.in b/configure.in index 5fd6d8fc..96079489 100644 --- a/configure.in +++ b/configure.in @@ -33,7 +33,6 @@ AC_ARG_ENABLE(new-howl,[ --enable-new-howl Use howl 0.9.2 or later], *) AC_MSG_ERROR(bad value ${enableval} for --enable-howl);; esac ],[rend_howl=false]) - AC_ARG_ENABLE(howl,[ --enable-howl Use the howl mDNS library], [ case "${enableval}" in yes) rend_howl=true; LDFLAGS="${LDFLAGS} -lrendezvous -lcorby -lsalt"; @@ -41,6 +40,21 @@ AC_ARG_ENABLE(howl,[ --enable-howl Use the howl mDNS library], no) rend_howl=false;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-howl);; esac ],[rend_howl=false]) +AC_ARG_ENABLE(browse,[ --enable-browse enable experimenal browse support], + [ case "${enableval}" in + yes) opt_browse=true; CPPFLAGS="${CPPFLAGS} -DOPT_BROWSE";; + no) opt_browse=false;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-browse);; + esac ],[opt_browse=false]) +AC_ARG_ENABLE(query,[ --enable-query enable experimenal query support], + [ case "${enableval}" in + yes) opt_query=true; CPPFLAGS="${CPPFLAGS} -DOPT_QUERY";; + no) opt_query=false;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-query);; + esac ],[opt_query=false]) + +AM_CONDITIONAL(OPT_BROWSE, test x$opt_browse = xtrue) +AM_CONDITIONAL(OPT_QUERY, test x$opt_query = xtrue) AM_CONDITIONAL(COND_REND_HOWL, test x$rend_howl = xtrue) AM_CONDITIONAL(COND_REND_POSIX, test x$rend_howl = xfalse) diff --git a/src/Makefile.am b/src/Makefile.am index 646bf5f0..fcaca756 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -18,19 +18,19 @@ if COND_REND_OSX ORENDSRC=rend-osx.c endif +if OPT_QUERY +QUERYSRC = query.c query.h +endif + mt_daapd_SOURCES = main.c daapd.h rend.h uici.c uici.h webserver.c \ webserver.h configfile.c configfile.h err.c err.h restart.c restart.h \ daap-proto.c daap-proto.h daap.c daap.h db-gdbm.c db-memory.h \ mp3-scanner.h mp3-scanner.c playlist.c playlist.h rend-unix.c \ rend-unix.h lexer.l parser.y strcasestr.c strcasestr.h strsep.c \ redblack.c redblack.h dynamic-art.c dynamic-art.h \ - $(PRENDSRC) $(ORENDSRC) $(HRENDSRC) + $(PRENDSRC) $(ORENDSRC) $(HRENDSRC) $(QUERYSRC) EXTRA_DIST = mDNS.c mDNSClientAPI.h mDNSDebug.h mDNSPosix.c \ mDNSUNP.c mDNSPlatformFunctions.h mDNSPosix.h mDNSUNP.h \ rend-howl.c rend-posix.c rend-osx.c strcasestr.c strsep.c db-memory.c \ db-gdbm.c strcasestr.h redblack.c redblack.h db-memory.c - - - - diff --git a/src/configfile.c b/src/configfile.c index 5dac2ae0..464d819e 100644 --- a/src/configfile.c +++ b/src/configfile.c @@ -167,9 +167,12 @@ int config_read(char *file) { config.runas=NULL; config.artfilename=NULL; config.logfile=NULL; - config.extensions=".mp3"; - config.servername="mt-daapd " VERSION; + /* DWB: use alloced space so it can be freed without errors */ + config.extensions=strdup(".mp3"); + + /* DWB: use alloced space so it can be freed without errors */ + config.servername=strdup("mt-daapd " VERSION); while(fgets(buffer,MAX_LINE,fin)) { if(*buffer != '#') { @@ -195,6 +198,9 @@ int config_read(char *file) { switch(pce->type) { case CONFIG_TYPE_STRING: + /* DWB: free space to prevent small leak */ + if(*((char **)(pce->var))) + free(*((char **)(pce->var))); *((char **)(pce->var)) = (void*)strdup(value); break; case CONFIG_TYPE_INT: diff --git a/src/daap-proto.c b/src/daap-proto.c index 9931156b..ac99c152 100644 --- a/src/daap-proto.c +++ b/src/daap-proto.c @@ -336,3 +336,20 @@ void daap_free(DAAP_BLOCK *root) { } return; } + +// search a block's children and change an integer value +int daap_set_int(DAAP_BLOCK* parent, char* tag, int value) +{ + DAAP_BLOCK* child = daap_find(parent, tag); + + if(0 == child || child->size != sizeof(int)) + return 0; + + child->svalue[0]=(value >> 24) & 0xFF; + child->svalue[1]=(value >> 16) & 0xFF; + child->svalue[2]=(value >> 8) & 0xFF; + child->svalue[3]=value & 0xFF; + + return 1; +} + diff --git a/src/daap-proto.h b/src/daap-proto.h index 0a60df75..516f5916 100644 --- a/src/daap-proto.h +++ b/src/daap-proto.h @@ -51,5 +51,8 @@ void daap_remove(DAAP_BLOCK* root); // search a block's direct children for a block with a given tag DAAP_BLOCK *daap_find(DAAP_BLOCK *parent, char* tag); +// search a block's children and change an integer value +int daap_set_int(DAAP_BLOCK* parent, char* tag, int value); + #endif diff --git a/src/daap.c b/src/daap.c index 3b217873..818c8164 100644 --- a/src/daap.c +++ b/src/daap.c @@ -40,6 +40,10 @@ #include "err.h" #include "daapd.h" +#ifdef OPT_QUERY +#include "query.h" +#endif + typedef struct tag_daap_items { int type; char *tag; @@ -134,6 +138,39 @@ DAAP_ITEMS taglist[] = { { 0x00, NULL, NULL } }; +#ifdef OPT_QUERY +#define OFFSET_OF(__type, __field) ((size_t) (&((__type*) 0)->__field)) + +static query_field_t song_fields[] = { + { qft_string, "dmap.itemname", OFFSET_OF(MP3FILE, title) }, + { qft_i32, "dmap.itemid", OFFSET_OF(MP3FILE, id) }, + { qft_string, "daap.songalbum", OFFSET_OF(MP3FILE, album) }, + { qft_string, "daap.songartist", OFFSET_OF(MP3FILE, artist) }, + { qft_i32, "daap.songbitrate", OFFSET_OF(MP3FILE, bitrate) }, + { qft_string, "daap.songcomment", OFFSET_OF(MP3FILE, comment) }, + // { qft_i32_const, "daap.songcompilation", 0 }, + { qft_string, "daap.songcomposer", OFFSET_OF(MP3FILE, composer) }, + // { qft_i32_const, "daap.songdatakind", 0 }, + // { qft_string, "daap.songdataurl", OFFSET_OF(MP3FILE, url) }, + { qft_i32, "daap.songdateadded", OFFSET_OF(MP3FILE, time_added) }, + { qft_i32, "daap.songdatemodified",OFFSET_OF(MP3FILE, time_modified) }, + { qft_string, "daap.songdescription", OFFSET_OF(MP3FILE, description) }, + { qft_i32, "daap.songdisccount", OFFSET_OF(MP3FILE, total_discs) }, + { qft_i32, "daap.songdiscnumber", OFFSET_OF(MP3FILE, disc) }, + { qft_string, "daap.songformat", OFFSET_OF(MP3FILE, type) }, + { qft_string, "daap.songgenre", OFFSET_OF(MP3FILE, genre) }, + { qft_i32, "daap.songsamplerate", OFFSET_OF(MP3FILE, samplerate) }, + { qft_i32, "daap.songsize", OFFSET_OF(MP3FILE, file_size) }, + // { qft_i32_const, "daap.songstarttime", 0 }, + { qft_i32, "daap.songstoptime", OFFSET_OF(MP3FILE, song_length) }, + { qft_i32, "daap.songtime", OFFSET_OF(MP3FILE, song_length) }, + { qft_i32, "daap.songtrackcount", OFFSET_OF(MP3FILE, total_tracks) }, + { qft_i32, "daap.songtracknumber", OFFSET_OF(MP3FILE, track) }, + { qft_i32, "daap.songyear", OFFSET_OF(MP3FILE, year) }, + { 0 } +}; +#endif + /* Forwards */ int daap_add_mdcl(DAAP_BLOCK *root, char *tag, char *name, short int number) { @@ -350,7 +387,7 @@ MetaField_t encodeMetaRequest(char* meta, MetaDataMap* map) break; if(m->tag) - bits |= (1 << m->bit); + bits |= (((MetaField_t) 1) << m->bit); else DPRINTF(ERR_WARN, "Unknown meta code: %*s\n", len, start); } @@ -362,10 +399,10 @@ MetaField_t encodeMetaRequest(char* meta, MetaDataMap* map) int wantsMeta(MetaField_t meta, MetaFieldName_t fieldNo) { - return 0 != (meta & (1ll << fieldNo)); + return 0 != (meta & (((MetaField_t) 1) << fieldNo)); } -DAAP_BLOCK *daap_response_songlist(char* metaStr) { +DAAP_BLOCK *daap_response_songlist(char* metaStr, char* query) { DAAP_BLOCK *root; int g=1; DAAP_BLOCK *mlcl; @@ -373,7 +410,11 @@ DAAP_BLOCK *daap_response_songlist(char* metaStr) { ENUMHANDLE henum; MP3FILE *current; MetaField_t meta; - +#ifdef OPT_QUERY + query_node_t* filter = 0; +#endif + int songs = 0; + // if the meta tag is specified, encode it, if it's not specified // we're given the latitude to select our own subset, for // simplicity we just include everything. @@ -382,6 +423,18 @@ DAAP_BLOCK *daap_response_songlist(char* metaStr) { else meta = encodeMetaRequest(metaStr, gSongMetaDataMap); +#ifdef OPT_QUERY + if(0 != query) + { + filter = query_build(query, song_fields); + if(err_debuglevel >= ERR_INFO) + { + fprintf(stderr, "query: %s\n", query); + query_dump(stderr, filter, 0); + } + } +#endif + DPRINTF(ERR_DEBUG,"Preparing to send db items\n"); henum=db_enum_begin(); @@ -394,22 +447,33 @@ DAAP_BLOCK *daap_response_songlist(char* metaStr) { if(root) { g = (int)daap_add_int(root,"mstt",200); g = g && daap_add_char(root,"muty",0); - g = g && daap_add_int(root,"mtco",db_get_song_count()); - g = g && daap_add_int(root,"mrco",db_get_song_count()); + g = g && daap_add_int(root,"mtco",0); + g = g && daap_add_int(root,"mrco",0); mlcl=daap_add_empty(root,"mlcl"); if(mlcl) { while(g && (current=db_enum(&henum))) { - DPRINTF(ERR_DEBUG,"Got entry for %s\n",current->fname); - // song entry generation extracted for usage with - // playlists as well - g = 0 != daap_add_song_entry(mlcl, current, meta); +#ifdef OPT_QUERY + if(filter == 0 || query_test(filter, current)) +#endif + { + DPRINTF(ERR_DEBUG,"Got entry for %s\n",current->fname); + // song entry generation extracted for usage with + // playlists as well + g = 0 != daap_add_song_entry(mlcl, current, meta); + songs++; + } } } else g=0; } - db_enum_end(); + db_enum_end(henum); + +#ifdef OPT_QUERY + if(filter != 0) + query_free(filter); +#endif if(!g) { DPRINTF(ERR_DEBUG,"Error enumerating database\n"); @@ -419,6 +483,9 @@ DAAP_BLOCK *daap_response_songlist(char* metaStr) { DPRINTF(ERR_DEBUG,"Successfully enumerated database\n"); + daap_set_int(root, "mtco", songs); + daap_set_int(root, "mrco", songs); + return root; } @@ -435,50 +502,14 @@ DAAP_BLOCK* daap_add_song_entry(DAAP_BLOCK* mlcl, MP3FILE* song, MetaField_t met mlit=daap_add_empty(mlcl,"mlit"); if(mlit) { if(wantsMeta(meta, metaItemKind)) - g = g && daap_add_char(mlit,"mikd",2); /* audio */ + g = g && daap_add_char(mlit,"mikd",song->item_kind); /* audio */ if(wantsMeta(meta, metaSongDataKind)) g = g && daap_add_char(mlit,"asdk",0); /* local file */ if(song->album && (wantsMeta(meta, metaSongAlbum))) g = g && daap_add_string(mlit,"asal",song->album); - if(wantsMeta(meta, metaSongArtist)) - { - char *artist; - int artist_len; - - artist=NULL; - artist_len=0; - if(song->orchestra || song->conductor) { - if(song->orchestra) - artist_len += strlen(song->orchestra); - if(song->conductor) - artist_len += strlen(song->conductor); - - artist_len += 3; - - artist=(char*)malloc(artist_len); - if(artist) { - memset(artist,0x0,artist_len); - - if(song->orchestra) - strcat(artist,song->orchestra); - - if(song->orchestra && song->conductor) - strcat(artist," - "); - - if(song->conductor) - strcat(artist,song->conductor); - - g = g && daap_add_string(mlit,"asar",artist); - - free(artist); - artist=NULL; - } else - g=1; - } else if(song->artist) { - g = g && daap_add_string(mlit,"asar",song->artist); - } - } + if(song->artist && wantsMeta(meta, metaSongArtist)) + g = g && daap_add_string(mlit,"asar",song->artist); // g = g && daap_add_short(mlit,"asbt",0); /* bpm */ if(song->bitrate && (wantsMeta(meta, metaSongBitRate))) @@ -521,25 +552,12 @@ DAAP_BLOCK* daap_add_song_entry(DAAP_BLOCK* mlcl, MP3FILE* song, MetaField_t met /* these quite go hand in hand */ if(wantsMeta(meta, metaSongFormat)) - g = g && daap_add_string(mlit,"asfm",(char*)&song->type[1]); /* song format */ + g = g && daap_add_string(mlit,"asfm",song->type); /* song format */ if(wantsMeta(meta, metaSongDescription)) - { - char fdescr[50]; - if(!strcasecmp(song->type,".ogg")) { - sprintf(fdescr,"QuickTime movie file"); - } else { - sprintf(fdescr,"%s audio file",song->type); - } - g = g && daap_add_string(mlit,"asdt",fdescr); /* descr */ - } + g = g && daap_add_string(mlit, "asdt",song->description); if(wantsMeta(meta, metaItemName)) - { - if(song->title) - g = g && daap_add_string(mlit,"minm",song->title); /* descr */ - else - g = g && daap_add_string(mlit,"minm",song->fname); - } + g = g && daap_add_string(mlit,"minm",song->title); /* descr */ // mper (long) // g = g && daap_add_char(mlit,"asdb",0); /* disabled */ @@ -553,10 +571,10 @@ DAAP_BLOCK* daap_add_song_entry(DAAP_BLOCK* mlcl, MP3FILE* song, MetaField_t met if(wantsMeta(meta, metaSongStartTime)) g = g && daap_add_int(mlit,"asst",0); /* song start time? */ if(wantsMeta(meta, metaSongStopTime)) - g = g && daap_add_int(mlit,"assp",0); /* songstoptime */ + g = g && daap_add_int(mlit,"assp",0); /* song stop time */ if(song->song_length && (wantsMeta(meta, metaSongTime))) - g = g && daap_add_int(mlit,"astm",song->song_length*1000); /* song time */ + g = g && daap_add_int(mlit,"astm",song->song_length); /* song time */ if(song->total_tracks && (wantsMeta(meta, metaSongTrackCount))) g = g && daap_add_short(mlit,"astc",song->total_tracks); /* track count */ @@ -680,7 +698,7 @@ DAAP_BLOCK *daap_response_playlists(char *name) { } g = g && mlit; } - db_playlist_enum_end(); + db_playlist_enum_end(henum); } } @@ -770,8 +788,12 @@ DAAP_BLOCK *daap_response_server_info(char *name, char *client_version) { g = g && daap_add_string(root,"minm",name); /* server name */ +#if 0 + /* DWB: login isn't actually required since the session id + isn't recorded, and isn't actually used for anything */ /* logon is always required, even if a password isn't */ g = g && daap_add_char(root,"mslr",1); +#endif /* authentication method is 0 for nothing, 1 for name and password, 2 for password only */ @@ -786,12 +808,17 @@ DAAP_BLOCK *daap_response_server_info(char *name, char *client_version) { supported */ g = g && daap_add_char(root,"msex",0); /* extensions */ g = g && daap_add_char(root,"msix",0); /* indexing? */ + +#ifdef OPT_BROWSE + g = g && daap_add_char(root,"msbr",0); /* browsing */ +#endif +#ifdef OPT_QUERY + g = g && daap_add_char(root,"msqy",0); /* queries */ +#endif #if 0 g = g && daap_add_char(root,"msal",0); /* autologout */ g = g && daap_add_char(root,"msup",0); /* update */ g = g && daap_add_char(root,"mspi",0); /* persistant ids */ - g = g && daap_add_char(root,"msbr",0); /* browsing */ - g = g && daap_add_char(root,"msqy",0); /* queries */ g = g && daap_add_char(root,"msrs",0); /* resolve? req. persist id */ #endif g = g && daap_add_int(root,"msdc",1); /* database count */ @@ -811,7 +838,7 @@ DAAP_BLOCK *daap_response_server_info(char *name, char *client_version) { * * given a playlist number, return the items on the playlist */ -DAAP_BLOCK *daap_response_playlist_items(unsigned int playlist, char* metaStr) { +DAAP_BLOCK *daap_response_playlist_items(unsigned int playlist, char* metaStr, char* query) { DAAP_BLOCK *root; DAAP_BLOCK *mlcl; DAAP_BLOCK *mlit; @@ -820,6 +847,10 @@ DAAP_BLOCK *daap_response_playlist_items(unsigned int playlist, char* metaStr) { int itemid; int g=1; unsigned long long meta; +#ifdef OPT_QUERY + query_node_t* filter = 0; +#endif + int songs = 0; // if no meta information is specifically requested, return only // the base play list information. iTunes only requests the base @@ -834,6 +865,18 @@ DAAP_BLOCK *daap_response_playlist_items(unsigned int playlist, char* metaStr) { else meta = encodeMetaRequest(metaStr, gSongMetaDataMap); +#ifdef OPT_QUERY + if(0 != query) + { + filter = query_build(query, song_fields); + if(err_debuglevel >= ERR_INFO) + { + fprintf(stderr, "query: %s\n", query); + query_dump(stderr, filter, 0); + } + } +#endif + DPRINTF(ERR_DEBUG,"Preparing to send playlist items for pl #%d\n",playlist); if(playlist == 1) { @@ -857,22 +900,34 @@ DAAP_BLOCK *daap_response_playlist_items(unsigned int playlist, char* metaStr) { if(mlcl) { if(playlist == 1) { while((current=db_enum(&henum))) { - mlit=daap_add_song_entry(mlcl, current, meta); - if(0 != mlit) { - if(wantsMeta(meta, metaContainerItemId)) - g = g && daap_add_int(mlit,"mcti",playlist); - } else g=0; +#ifdef OPT_QUERY + if(0 == filter || query_test(filter, current)) +#endif + { + songs++; + mlit=daap_add_song_entry(mlcl, current, meta); + if(0 != mlit) { + if(wantsMeta(meta, metaContainerItemId)) + g = g && daap_add_int(mlit,"mcti",playlist); + } else g=0; + } } } else { /* other playlist */ while((itemid=db_playlist_items_enum(&henum)) != -1) { current = db_find(itemid); if(0 != current) { - DPRINTF(ERR_DEBUG,"Adding itemid %d\n",itemid); - mlit=daap_add_song_entry(mlcl,current,meta); - if(0 != mlit) { - if(wantsMeta(meta, metaContainerItemId)) - g = g && daap_add_int(mlit,"mcti",playlist); - } else g = 0; +#ifdef OPT_QUERY + if(0 == filter || query_test(filter, current)) + { +#endif + songs++; + DPRINTF(ERR_DEBUG,"Adding itemid %d\n",itemid); + mlit=daap_add_song_entry(mlcl,current,meta); + if(0 != mlit) { + if(wantsMeta(meta, metaContainerItemId)) + g = g && daap_add_int(mlit,"mcti",playlist); + } else g = 0; + } } else g = 0; } } @@ -880,15 +935,23 @@ DAAP_BLOCK *daap_response_playlist_items(unsigned int playlist, char* metaStr) { } if(playlist == 1) - db_enum_end(); + db_enum_end(henum); else - db_playlist_items_enum_end(); + db_playlist_items_enum_end(henum); + +#ifdef OPT_QUERY + if(0 != filter) + query_free(filter); +#endif if(!g) { daap_free(root); return NULL; } + daap_set_int(root, "mtco", songs); + daap_set_int(root, "mrco", songs); + return root; } @@ -988,12 +1051,7 @@ void daap_handle_index(DAAP_BLOCK* block, const char* index) // update the returned record count entry, it's required, so // should have already be created - assert(0 != (item = daap_find(block, "mrco"))); - - item->svalue[0] = count >> 24; - item->svalue[1] = count >> 16; - item->svalue[2] = count >> 8; - item->svalue[3] = count; + daap_set_int(block, "mrco", count); DPRINTF(ERR_INFO, "index:%s first:%d count:%d\n", index, first, count); @@ -1001,7 +1059,7 @@ void daap_handle_index(DAAP_BLOCK* block, const char* index) for(back = &list->children ; *back && first ; ) if(!strncmp((**back).tag, "mlit", 4)) { - DPRINTF(ERR_INFO, "first:%d removing\n", first); + DPRINTF(ERR_DEBUG, "first:%d removing\n", first); daap_remove(*back); first--; } @@ -1012,7 +1070,7 @@ void daap_handle_index(DAAP_BLOCK* block, const char* index) for( ; *back && count ; back = &(**back).next) if(!strncmp((**back).tag, "mlit", 4)) { - DPRINTF(ERR_INFO, "count:%d keeping\n", count); + DPRINTF(ERR_DEBUG, "count:%d keeping\n", count); count--; } @@ -1021,10 +1079,182 @@ void daap_handle_index(DAAP_BLOCK* block, const char* index) { if(!strncmp((**back).tag, "mlit", 4)) { - DPRINTF(ERR_INFO, "removing spare\n"); + DPRINTF(ERR_DEBUG, "removing spare\n"); daap_remove(*back); } else back = &(**back).next; } } + +#ifdef OPT_BROWSE +typedef struct _browse_item browse_item; +struct _browse_item +{ + char* name; + browse_item* next; +}; + +static void add_browse_item(browse_item** root, char* name) +{ + browse_item* item; + + while(0 != (item = *root) && strcmp(item->name, name) < 0) + root = &item->next; + + if(item && strcmp(item->name, name) == 0) + return; + + item = calloc(1, sizeof(browse_item)); + item->name = strdup(name); + item->next = *root; + *root = item; +} + +static void free_browse_items(browse_item* root) +{ + while(0 != root) + { + browse_item* next = root->next; + + free(root->name); + free(root); + + root = next; + } +} + +static int count_browse_items(browse_item* root) +{ + int count = 0; + + while(0 != root) + { + root = root->next; + count++; + } + + return count; +} + +#ifdef OPT_QUERY +/* in theory each type of browse has a separate subset of these fields + which can be used for filtering, but it's just not worth the effort + and doesn't save anything */ +static query_field_t browse_fields[] = { + { qft_string, "daap.songartist", OFFSET_OF(MP3FILE, artist) }, + { qft_string, "daap.songalbum", OFFSET_OF(MP3FILE, album) }, + { qft_string, "daap.songgenre", OFFSET_OF(MP3FILE, genre) }, + { qft_string, "daap.songcomposer", OFFSET_OF(MP3FILE, composer) }, + { 0 } +}; +#endif + +DAAP_BLOCK* daap_response_browse(const char* name, const char* filter) +{ + MP3FILE* current; + ENUMHANDLE henum; + size_t field; + char* l_type; + browse_item* items = 0; + browse_item* item; + DAAP_BLOCK* root = 0; + query_node_t* query = 0; + + if(!strcmp(name, "artists")) + { + field = OFFSET_OF(MP3FILE, artist); + l_type = "abar"; + } + else if(!strcmp(name, "genres")) + { + field = OFFSET_OF(MP3FILE, genre); + l_type = "abgn"; + } + else if(!strcmp(name, "albums")) + { + field = OFFSET_OF(MP3FILE, album); + l_type = "abal"; + } + else if(!strcmp(name, "composers")) + { + field = OFFSET_OF(MP3FILE, composer); + l_type = "abcp"; + } + else + { + DPRINTF(ERR_WARN,"Invalid browse request: %s\n", name); + return NULL; + } + +#ifdef OPT_QUERY + if(0 != filter && + 0 == (query = query_build(filter, browse_fields))) + return NULL; + + if(query && err_debuglevel >= ERR_INFO) + { + fprintf(stderr, "query: %s\n", query); + query_dump(stderr, query, 0); + } +#endif + + if(0 == (henum = db_enum_begin())) + return NULL; + + while((current = db_enum(&henum))) + { +#ifdef OPT_QUERY + if(0 == query || query_test(query, current)) +#endif + { + char* name = * (char**) ((size_t) current + field); + + if(0 != name) + add_browse_item(&items, name); + } + } + + db_enum_end(henum); + + if(0 != (root = daap_add_empty(0, "abro"))) + { + int count = count_browse_items(items); + DAAP_BLOCK* mlcl; + + if(!daap_add_int(root, "mstt", 200) || + !daap_add_int(root, "mtco", count) || + !daap_add_int(root, "mrco", count) || + 0 == (mlcl = daap_add_empty(root, l_type))) + goto error; + + for(item = items ; item ; item = item->next) + { + if(!daap_add_string(mlcl, "mlit", item->name)) + goto error; + } + } + + free_browse_items(items); + +#ifdef OPT_QUERY + if(0 != query) + query_free(query); +#endif + + return root; + + error: + free_browse_items(items); + +#ifdef OPT_QUERY + if(0 != query) + query_free(query); +#endif + + if(root != 0) + daap_free(root); + + return NULL; +} +#endif diff --git a/src/daap.h b/src/daap.h index 404e31b3..152bd588 100644 --- a/src/daap.h +++ b/src/daap.h @@ -27,11 +27,14 @@ DAAP_BLOCK *daap_response_server_info(char *name, char *client_version); DAAP_BLOCK *daap_response_content_codes(void); DAAP_BLOCK *daap_response_login(char *hostname); DAAP_BLOCK *daap_response_update(int fd, int clientver); -DAAP_BLOCK *daap_response_songlist(char* metaInfo); +DAAP_BLOCK *daap_response_songlist(char* metaInfo, char* query); DAAP_BLOCK *daap_response_playlists(char *name); DAAP_BLOCK *daap_response_dbinfo(char *name); -DAAP_BLOCK *daap_response_playlist_items(unsigned int playlist, char* metaStr); +DAAP_BLOCK *daap_response_playlist_items(unsigned int playlist, char* metaStr, char* query); void daap_handle_index(DAAP_BLOCK* block, const char* index); DAAP_BLOCK* daap_add_song_entry(DAAP_BLOCK* mlcl, MP3FILE* song, unsigned long long meta); +#ifdef OPT_BROWSE +DAAP_BLOCK* daap_response_browse(const char* name, const char* filter); +#endif #endif /* _DAAP_H_ */ diff --git a/src/db-gdbm.c b/src/db-gdbm.c index 3a8e7b37..4da3f311 100644 --- a/src/db-gdbm.c +++ b/src/db-gdbm.c @@ -40,6 +40,7 @@ #define RB_FREE #include "redblack.h" +#include "db-memory.h" /* * Defines @@ -61,10 +62,11 @@ /* * Typedefs */ -typedef struct tag_mp3record { +typedef struct tag_mp3record MP3RECORD; +struct tag_mp3record { MP3FILE mp3file; - datum key; -} MP3RECORD; + MP3RECORD* next; +}; typedef struct tag_playlistentry { unsigned int id; @@ -116,6 +118,10 @@ typedef struct tag_mp3packed { char data[1]; } MP3PACKED; +typedef struct { + MP3RECORD* root; + MP3RECORD* next; +} MP3HELPER;; /* * Globals @@ -127,7 +133,6 @@ int db_playlist_count=0; DB_PLAYLIST db_playlists; pthread_rwlock_t db_rwlock; /* OSX doesn't have PTHREAD_RWLOCK_INITIALIZER */ pthread_once_t db_initlock=PTHREAD_ONCE_INIT; -MP3RECORD db_enum_helper; GDBM_FILE db_songs; struct rbtree *db_removed; @@ -138,7 +143,7 @@ struct rbtree *db_removed; int db_start_initial_update(void); int db_end_initial_update(void); int db_is_empty(void); -int db_open(char *parameters); +int db_open(char *parameters, int reload); int db_init(); int db_deinit(void); int db_version(void); @@ -149,18 +154,6 @@ int db_add_playlist_song(unsigned int playlistid, unsigned int itemid); int db_unpackrecord(datum *pdatum, MP3FILE *pmp3); datum *db_packrecord(MP3FILE *pmp3); -MP3RECORD *db_enum_begin(void); -MP3FILE *db_enum(MP3RECORD **current); -int db_enum_end(void); - -DB_PLAYLIST *db_playlist_enum_begin(void); -int db_playlist_enum(DB_PLAYLIST **current); -int db_playlist_enum_end(void); - -DB_PLAYLISTENTRY *db_playlist_items_enum_begin(int playlistid); -int db_playlist_items_enum(DB_PLAYLISTENTRY **current); -int db_playlist_items_enum_end(void); - int db_get_song_count(void); int db_get_playlist_count(void); int db_get_playlist_is_smart(int playlistid); @@ -199,14 +192,16 @@ void db_init_once(void) { * * Open the database, so we can drop privs */ -int db_open(char *parameters) { +int db_open(char *parameters, int reload) { char db_path[PATH_MAX + 1]; if(pthread_once(&db_initlock,db_init_once)) return -1; snprintf(db_path,sizeof(db_path),"%s/%s",parameters,"songs.gdb"); - db_songs=gdbm_open(db_path,0,GDBM_WRCREAT | GDBM_SYNC | GDBM_NOLOCK, + + reload = reload ? GDBM_NEWDB : GDBM_WRCREAT; + db_songs=gdbm_open(db_path, 0, reload | GDBM_SYNC | GDBM_NOLOCK, 0600,NULL); if(!db_songs) { DPRINTF(ERR_FATAL,"Could not open songs database (%s)\n", @@ -686,6 +681,8 @@ int db_unpackrecord(datum *pdatum, MP3FILE *pmp3) { pmp3->grouping=strdup(&ppacked->data[offset]); offset += ppacked->grouping_len; + make_composite_tags(pmp3); + return 0; } @@ -771,19 +768,45 @@ void db_freefile(MP3FILE *pmp3) { MAYBEFREE(pmp3->orchestra); MAYBEFREE(pmp3->conductor); MAYBEFREE(pmp3->grouping); + MAYBEFREE(pmp3->description); } /* * db_enum_begin * - * Begin to walk through an enum of - * the database. + * Begin to walk through an enum of the database. In order to retain + * some sanity in song ordering we scan all the songs and perform an + * insertion sort based on album, track, record id. + * + * Since the list is built entirely within this routine, it's also + * safe to release the lock once the list is built. + * + * Also we eliminate the static db_enum_helper which allows (the + * admittedly unnecessary) possibility of reentrance. * * this should be done quickly, as we'll be holding * a reader lock on the db */ -MP3RECORD *db_enum_begin(void) { +int compare(MP3RECORD* a, MP3RECORD* b) +{ + int comp; + + if((comp = strcmp(a->mp3file.album, b->mp3file.album)) != 0) + return comp; + + if((comp = (a->mp3file.disc - b->mp3file.disc)) != 0) + return comp; + + if((comp = (a->mp3file.track - b->mp3file.track)) != 0) + return comp; + + return a->mp3file.id - b->mp3file.id; +} + +ENUMHANDLE db_enum_begin(void) { int err; + MP3HELPER* helper; + datum key, next; if((err=pthread_rwlock_rdlock(&db_rwlock))) { DPRINTF(ERR_FATAL,"Cannot lock rwlock\n"); @@ -791,13 +814,39 @@ MP3RECORD *db_enum_begin(void) { return NULL; } - memset((void*)&db_enum_helper,0x00,sizeof(db_enum_helper)); - db_enum_helper.key=gdbm_firstkey(db_songs); - MEMNOTIFY(db_enum_helper.key.dptr); - if(!db_enum_helper.key.dptr) - return NULL; + helper = calloc(1, sizeof(MP3HELPER)); + + for(key = gdbm_firstkey(db_songs) ; key.dptr ; key = next) + { + MP3RECORD* entry = calloc(1, sizeof(MP3RECORD)); + MP3RECORD** root; + datum data; - return &db_enum_helper; + data = gdbm_fetch(db_songs, key); + if(!data.dptr) + DPRINTF(ERR_FATAL, "Cannot find item... corrupt database?\n"); + + if(db_unpackrecord(&data, &entry->mp3file)) + DPRINTF(ERR_FATAL, "Cannot unpack item... corrupt database?\n"); + + for(root = &helper->root ; *root ; root = &(**root).next) + { + if(compare(*root, entry) > 0) + break; + } + + entry->next = *root; + *root = entry; + + next = gdbm_nextkey(db_songs, key); + free(key.dptr); + } + + helper->next = helper->root; + + pthread_rwlock_unlock(&db_rwlock); + + return helper; } /* @@ -805,7 +854,7 @@ MP3RECORD *db_enum_begin(void) { * * Start enumerating playlists */ -DB_PLAYLIST *db_playlist_enum_begin(void) { +ENUMHANDLE db_playlist_enum_begin(void) { int err; DB_PLAYLIST *current; @@ -828,7 +877,7 @@ DB_PLAYLIST *db_playlist_enum_begin(void) { * * Start enumerating playlist items */ -DB_PLAYLISTENTRY *db_playlist_items_enum_begin(int playlistid) { +ENUMHANDLE db_playlist_items_enum_begin(int playlistid) { DB_PLAYLIST *current; int err; @@ -854,40 +903,16 @@ DB_PLAYLISTENTRY *db_playlist_items_enum_begin(int playlistid) { * * Walk to the next entry */ -MP3FILE *db_enum(MP3RECORD **current) { - datum nextkey; - datum data; +MP3FILE *db_enum(ENUMHANDLE *current) { + MP3HELPER* helper = *(MP3HELPER**) current; + MP3RECORD* record = helper->next; - if(db_enum_helper.key.dptr) { - db_freefile(&db_enum_helper.mp3file); + if(helper->next == 0) + return 0; - /* Got the key, let's fetch it */ - data=gdbm_fetch(db_songs,db_enum_helper.key); - MEMNOTIFY(data.dptr); - if(!data.dptr) { - DPRINTF(ERR_FATAL,"Inconsistant database.\n"); - } + helper->next = helper->next->next; - if(db_unpackrecord(&data,&db_enum_helper.mp3file)) { - DPRINTF(ERR_FATAL,"Cannot unpack item.. Corrupt database?\n"); - } - - if(data.dptr) - free(data.dptr); - - nextkey=gdbm_nextkey(db_songs,db_enum_helper.key); - MEMNOTIFY(nextkey.dptr); - - if(db_enum_helper.key.dptr) { - free(db_enum_helper.key.dptr); - db_enum_helper.key.dptr=NULL; - } - - db_enum_helper.key=nextkey; - return &db_enum_helper.mp3file; - } - - return NULL; + return &record->mp3file; } /* @@ -895,7 +920,8 @@ MP3FILE *db_enum(MP3RECORD **current) { * * walk to the next entry */ -int db_playlist_enum(DB_PLAYLIST **current) { +int db_playlist_enum(ENUMHANDLE* handle) { + DB_PLAYLIST** current = (DB_PLAYLIST**) handle; int retval; DB_PLAYLIST *p; @@ -917,7 +943,8 @@ int db_playlist_enum(DB_PLAYLIST **current) { * * walk to the next entry */ -int db_playlist_items_enum(DB_PLAYLISTENTRY **current) { +int db_playlist_items_enum(ENUMHANDLE* handle) { + DB_PLAYLISTENTRY **current = (DB_PLAYLISTENTRY**) handle; int retval; if(*current) { @@ -932,14 +959,25 @@ int db_playlist_items_enum(DB_PLAYLISTENTRY **current) { /* * db_enum_end * - * quit walking the database (and give up reader lock) + * dispose of the list we built up in db_enum_begin, the lock's + * already been released. */ -int db_enum_end(void) { - db_freefile(&db_enum_helper.mp3file); - if(db_enum_helper.key.dptr) - free(db_enum_helper.key.dptr); +int db_enum_end(ENUMHANDLE handle) { + MP3HELPER* helper = (MP3HELPER*) handle; + MP3RECORD* record; - return pthread_rwlock_unlock(&db_rwlock); + while(helper->root) + { + MP3RECORD* record = helper->root; + + helper->root = record->next; + + db_freefile(&record->mp3file); + } + + free(helper); + + return 0; } /* @@ -947,7 +985,7 @@ int db_enum_end(void) { * * quit walking the database */ -int db_playlist_enum_end(void) { +int db_playlist_enum_end(ENUMHANDLE handle) { return pthread_rwlock_unlock(&db_rwlock); } @@ -956,7 +994,7 @@ int db_playlist_enum_end(void) { * * Quit walking the database */ -int db_playlist_items_enum_end(void) { +int db_playlist_items_enum_end(ENUMHANDLE handle) { return pthread_rwlock_unlock(&db_rwlock); } diff --git a/src/db-memory.c b/src/db-memory.c index f10afbd1..299b8823 100644 --- a/src/db-memory.c +++ b/src/db-memory.c @@ -389,6 +389,8 @@ int db_add(MP3FILE *mp3file) { return -1; } + make_composite_tags(&pnew->mp3file); + pnew->next=db_root.next; db_root.next=pnew; @@ -421,6 +423,7 @@ void db_freerecord(MP3RECORD *mp3record) { MAYBEFREE(mp3record->mp3file.orchestra); MAYBEFREE(mp3record->mp3file.conductor); MAYBEFREE(mp3record->mp3file.grouping); + MAYBEFREE(mp3record->mp3file.description); free(mp3record); } diff --git a/src/db-memory.h b/src/db-memory.h index 3b9f5920..037fc96f 100644 --- a/src/db-memory.h +++ b/src/db-memory.h @@ -29,7 +29,7 @@ typedef void* ENUMHANDLE; extern int db_start_initial_update(void); extern int db_end_initial_update(void); extern int db_is_empty(void); -extern int db_open(char *parameters); +extern int db_open(char *parameters, int reload); extern int db_init(void); extern int db_deinit(void); extern int db_version(void); @@ -39,8 +39,8 @@ extern int db_add_playlist(unsigned int playlistid, char *name, int is_smart); extern int db_add_playlist_song(unsigned int playlistid, unsigned int itemid); extern ENUMHANDLE db_enum_begin(void); -extern MP3FILE *db_enum(ENUMHANDLE *current); -extern int db_enum_end(void); +extern MP3FILE *db_enum(ENUMHANDLE *handle); +extern int db_enum_end(ENUMHANDLE handle); extern MP3FILE *db_find(int id); extern int db_get_song_count(void); @@ -50,11 +50,11 @@ extern int db_get_playlist_is_smart(int playlistid); extern ENUMHANDLE db_playlist_enum_begin(void); extern int db_playlist_enum(ENUMHANDLE *current); -extern int db_playlist_enum_end(void); +extern int db_playlist_enum_end(ENUMHANDLE handle); extern ENUMHANDLE db_playlist_items_enum_begin(int playlistid); extern int db_playlist_items_enum(ENUMHANDLE *current); -extern int db_playlist_items_enum_end(void); +extern int db_playlist_items_enum_end(ENUMHANDLE handle); extern char *db_get_playlist_name(int playlistid); diff --git a/src/main.c b/src/main.c index e0841925..cb4573ab 100644 --- a/src/main.c +++ b/src/main.c @@ -51,7 +51,13 @@ #include "playlist.h" #include "dynamic-art.h" +#ifndef DEFAULT_CONFIGFILE #define DEFAULT_CONFIGFILE "/etc/mt-daapd.conf" +#endif + +#ifndef PIDFILE +#define PIDFILE "/var/run/mt-daapd.pid" +#endif #ifndef SIGCLD # define SIGCLD SIGCHLD @@ -67,6 +73,7 @@ CONFIG config; */ RETSIGTYPE sig_child(int signal); int daemon_start(int reap_children); +void write_pid_file(void); /* * daap_auth @@ -167,6 +174,7 @@ void daap_handler(WS_CONNINFO *pwsc) { * /databases/id/containers, which returns a container * /databases/id/containers/id/items, which returns playlist elements * /databases/id/items/id.mp3, to spool an mp3 + * /databases/id/browse/category */ uri = strdup(pwsc->uri); @@ -198,7 +206,9 @@ void daap_handler(WS_CONNINFO *pwsc) { /* songlist */ free(uri); // pass the meta field request for processing - root=daap_response_songlist(ws_getvar(pwsc,"meta")); + // pass the query request for processing + root=daap_response_songlist(ws_getvar(pwsc,"meta"), + ws_getvar(pwsc,"query")); config_set_status(pwsc,session_id,"Sending songlist"); } else if (strncasecmp(last,"containers/",11)==0) { /* playlist elements */ @@ -213,7 +223,8 @@ void daap_handler(WS_CONNINFO *pwsc) { playlist_index=atoi(first); // pass the meta list info for processing root=daap_response_playlist_items(playlist_index, - ws_getvar(pwsc,"meta")); + ws_getvar(pwsc,"meta"), + ws_getvar(pwsc,"query")); } free(uri); config_set_status(pwsc,session_id,"Sending playlist info"); @@ -222,6 +233,14 @@ void daap_handler(WS_CONNINFO *pwsc) { free(uri); root=daap_response_playlists(config.servername); config_set_status(pwsc,session_id,"Sending playlist info"); +#ifdef OPT_BROWSE + } else if (strncasecmp(last,"browse/",7)==0) { + config_set_status(pwsc,session_id,"Compiling browse info"); + root = daap_response_browse(last + 7, + ws_getvar(pwsc, "filter")); + config_set_status(pwsc,session_id,"Sending browse info"); + free(uri); +#endif } } @@ -291,7 +310,7 @@ void daap_handler(WS_CONNINFO *pwsc) { // content type (dmap tagged) should only be used on // dmap protocol requests, not the actually song data if(pmp3->type) - ws_addresponseheader(pwsc,"Content-Type","audio/%s",(pmp3->type)+1); + ws_addresponseheader(pwsc,"Content-Type","audio/%s",pmp3->type); ws_addresponseheader(pwsc,"Content-Length","%ld",(long)file_len); ws_addresponseheader(pwsc,"Connection","Close"); @@ -539,13 +558,14 @@ int main(int argc, char *argv[]) { WSHANDLE server; int parseonly=0; int foreground=0; + int reload=0; int start_time; int end_time; config.use_mdns=1; err_debuglevel=1; - while((option=getopt(argc,argv,"d:c:mpf")) != -1) { + while((option=getopt(argc,argv,"d:c:mpfr")) != -1) { switch(option) { case 'd': err_debuglevel=atoi(optarg); @@ -566,6 +586,10 @@ int main(int argc, char *argv[]) { parseonly=1; break; + case 'r': + reload=1; + break; + default: usage(argv[0]); exit(EXIT_FAILURE); @@ -599,7 +623,19 @@ int main(int argc, char *argv[]) { } } - if(db_open(config.dbdir)) { + /* DWB: we want to detach before we drop privs so the pid file can + be created with the original permissions. This has the + drawback that there's a bit less error checking done while + we're attached, but if is much better when being automatically + started as a system service. */ + if(!foreground) + { + daemon_start(1); + write_pid_file(); + } + + /* DWB: shouldn't this be done after dropping privs? */ + if(db_open(config.dbdir, reload)) { DPRINTF(ERR_FATAL,"Error in db_open: %s\n",strerror(errno)); } @@ -633,10 +669,6 @@ int main(int argc, char *argv[]) { DPRINTF(ERR_FATAL,"Error in db_init: %s\n",strerror(errno)); } - /* will want to detach before we start scanning mp3 files */ - if(!foreground) - daemon_start(1); - DPRINTF(ERR_LOG,"Starting mp3 scan\n"); if(scan_init(config.mp3dir)) { DPRINTF(ERR_FATAL,"Error scanning MP3 files: %s\n",strerror(errno)); @@ -724,3 +756,22 @@ int main(int argc, char *argv[]) { return EXIT_SUCCESS; } +void write_pid_file(void) +{ + FILE* fp; + int fd; + + /* use open/fdopen instead of fopen for more control over the file + permissions */ + if(-1 == (fd = open(PIDFILE, O_CREAT | O_WRONLY | O_TRUNC, 0644))) + return; + + if(0 == (fp = fdopen(fd, "w"))) + { + close(fd); + return; + } + + fprintf(fp, "%d\n", getpid()); + fclose(fp); +} diff --git a/src/mp3-scanner.c b/src/mp3-scanner.c index d4e9e468..592954a4 100644 --- a/src/mp3-scanner.c +++ b/src/mp3-scanner.c @@ -261,6 +261,8 @@ int scan_freetags(MP3FILE *pmp3); void scan_static_playlist(char *path, struct dirent *pde, struct stat *psb); void scan_music_file(char *path, struct dirent *pde, struct stat *psb); +void make_composite_tags(MP3FILE *pmp3); + /* * scan_init * @@ -452,14 +454,17 @@ void scan_music_file(char *path, struct dirent *pde, struct stat *psb) { mp3file.path=mp3_path; mp3file.fname=pde->d_name; if(strlen(pde->d_name) > 4) - mp3file.type=strdup((char*)&pde->d_name[strlen(pde->d_name) - 4]); + mp3file.type=strdup(strrchr(pde->d_name, '.') + 1); - /* FIXME; assumes that st_ino is a u_int_32 */ + /* FIXME; assumes that st_ino is a u_int_32 + DWB: also assumes that the library is contained entirely within + one file system */ mp3file.id=psb->st_ino; /* Do the tag lookup here */ if(!scan_gettags(mp3file.path,&mp3file) && !scan_get_fileinfo(mp3file.path,&mp3file)) { + make_composite_tags(&mp3file); db_add(&mp3file); pl_eval(&mp3file); /* FIXME: move to db_add? */ } else { @@ -630,11 +635,11 @@ int scan_gettags(char *file, MP3FILE *pmp3) { * in turn, just in case the extensions are wrong/lying */ - if(strcasestr(".aac.m4a.m4p.mp4",pmp3->type)) + if(strcasestr("aacm4a4pmp4",pmp3->type)) return scan_get_aactags(file,pmp3); /* should handle mp3 in the same way */ - if(!strcasecmp(pmp3->type,".mp3")) + if(!strcasecmp(pmp3->type,"mp3")) return scan_get_mp3tags(file,pmp3); /* maybe this is an extension that we've manually @@ -661,7 +666,7 @@ int scan_get_mp3tags(char *file, MP3FILE *pmp3) { char *tmp; int got_numeric_genre; - if(strcasecmp(pmp3->type,".mp3")) /* can't get tags for non-mp3 */ + if(strcasecmp(pmp3->type,"mp3")) /* can't get tags for non-mp3 */ return 0; pid3file=id3_file_open(file,ID3_FILE_MODE_READONLY); @@ -775,6 +780,9 @@ int scan_get_mp3tags(char *file, MP3FILE *pmp3) { } else if(!strcmp(pid3frame->id,"TDRC")) { pmp3->year = atoi(utf8_text); DPRINTF(ERR_DEBUG," Year: %d\n",pmp3->year); + } else if(!strcmp(pid3frame->id,"TLEN")) { + pmp3->song_length = atoi(utf8_text) / 1000; + DPRINTF(ERR_DEBUG, " Length: %d\n", pmp3->song_length); } } } @@ -807,6 +815,7 @@ int scan_freetags(MP3FILE *pmp3) { MAYBEFREE(pmp3->orchestra); MAYBEFREE(pmp3->conductor); MAYBEFREE(pmp3->grouping); + MAYBEFREE(pmp3->description); return 0; } @@ -821,10 +830,10 @@ int scan_get_fileinfo(char *file, MP3FILE *pmp3) { FILE *infile; off_t file_size; - if(strcasestr(".aac.m4a.m4p.mp4",pmp3->type)) + if(strcasestr("aacm4am4pmp4",pmp3->type)) return scan_get_aacfileinfo(file,pmp3); - if(!strcasecmp(pmp3->type,".mp3")) + if(!strcasecmp(pmp3->type,"mp3")) return scan_get_mp3fileinfo(file,pmp3); /* a file we don't know anything about... ogg or aiff maybe */ @@ -878,11 +887,13 @@ int scan_get_aacfileinfo(char *file, MP3FILE *pmp3) { fseek(infile,16,SEEK_CUR); fread((void*)&temp_int,1,sizeof(int),infile); temp_int=ntohl(temp_int); + /* DWB: use ms time instead of sec */ if(temp_int > 720000) - pmp3->song_length=temp_int / 90000; /* PlayFair? */ + pmp3->song_length=temp_int / 90; /* PlayFair? */ else - pmp3->song_length=temp_int/600; - DPRINTF(ERR_DEBUG,"Song length: %d seconds\n",temp_int/600); + pmp3->song_length=temp_int * 1000 / 600; + + DPRINTF(ERR_DEBUG,"Song length: %d seconds\n", pmp3->song_length / 1000); } } fclose(infile); @@ -903,7 +914,6 @@ int scan_get_mp3fileinfo(char *file, MP3FILE *pmp3) { off_t fp_size=0; off_t file_size; unsigned char buffer[1024]; - int time_seconds=0; int index; int layer_index; int sample_index; @@ -1019,11 +1029,17 @@ int scan_get_mp3fileinfo(char *file, MP3FILE *pmp3) { DPRINTF(ERR_DEBUG," Bit Rate: %d\n",bitrate); /* guesstimate the file length */ - if(bitrate) - time_seconds = ((int)(file_size * 8)) / (bitrate * 1024); - if(!pmp3->song_length) /* could have gotten it from the tag */ - pmp3->song_length=time_seconds; + { + /* DWB: use ms time instead of seconds, use doubles to + avoid overflow */ + if(bitrate) + { + pmp3->song_length = (int) ((double) file_size * 8000. / + (double) bitrate / + 1024.); + } + } } else { /* FIXME: should really scan forward to next sync frame */ fclose(infile); @@ -1035,3 +1051,48 @@ int scan_get_mp3fileinfo(char *file, MP3FILE *pmp3) { fclose(infile); return 0; } + +void make_composite_tags(MP3FILE *song) +{ + int len; + + len=0; + + if(!song->artist && (song->orchestra || song->conductor)) { + if(song->orchestra) + len += strlen(song->orchestra); + if(song->conductor) + len += strlen(song->conductor); + + len += 3; + + song->artist=(char*)calloc(len, 1); + if(song->artist) { + if(song->orchestra) + strcat(song->artist,song->orchestra); + + if(song->orchestra && song->conductor) + strcat(song->artist," - "); + + if(song->conductor) + strcat(song->artist,song->conductor); + } + } + + if(!strcasecmp(song->type,"ogg")) { + song->description = strdup("QuickTime movie file"); + } else { + char fdescr[50]; + + sprintf(fdescr,"%s audio file",song->type); + song->description = strdup(fdescr); + } + + if(!song->title) + song->title = strdup(song->fname); + + if(!strcmp(song->type, "ogg")) + song->item_kind = 4; + else + song->item_kind = 2; +} diff --git a/src/mp3-scanner.h b/src/mp3-scanner.h index c80725f6..aa18d806 100644 --- a/src/mp3-scanner.h +++ b/src/mp3-scanner.h @@ -55,8 +55,14 @@ typedef struct tag_mp3file { int got_id3; unsigned int id; + + /* generated fields */ + char* description; /* long file type */ + int item_kind; /* song or movie */ } MP3FILE; extern int scan_init(char *path); +extern void make_composite_tags(MP3FILE *song); + #endif /* _MP3_SCANNER_H_ */ diff --git a/src/query.c b/src/query.c new file mode 100644 index 00000000..d60168b4 --- /dev/null +++ b/src/query.c @@ -0,0 +1,700 @@ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#define DEBUG + +#include "err.h" +#include "query.h" + +static const query_field_t* find_field(const char* name, + const query_field_t* fields); +static int arith_query(query_node_t* query, void* target); +static int string_query(query_node_t* query, void* target); + +static query_node_t* match_specifier(const char* query, + const char** cursor, + const query_field_t* fields); +static query_node_t* group_match(const char* query, + const char** cursor, + const query_field_t* fields); +static query_node_t* single_match(const char* query, + const char** cursor, + const query_field_t* fields); +static int get_field_name(const char** pcursor, + const char* query, + char* name, + int len); +static int get_opcode(const char** pcursor, + const char* query, + char* name, + int len); +static query_node_t* match_number(const query_field_t* field, + char not, char opcode, + const char** pcursor, + const char* query); +static query_node_t* match_string(const query_field_t* field, + char not, char opcode, + const char** pcursor, + const char* query); +char* query_unescape(const char* query); + +query_node_t* query_build(const char* query, const query_field_t* fields) +{ + query_node_t* left = 0; + char* raw = query_unescape(query); + const char* cursor = raw; + query_node_t* right = 0; + query_type_t join; + + if(0 == (left = match_specifier(query, &cursor, fields))) + goto error; + + while(*cursor) + { + query_node_t* con; + + switch(*cursor) + { + case '+': + case ' ': join = qot_and; break; + case ',': join = qot_or; break; + default: + DPRINTF(ERR_LOG, "Illegal character '%c' (0%o) at index %d: %s\n", + *cursor, *cursor, cursor - raw, raw); + goto error; + } + + cursor++; + + if(0 == (right = match_specifier(raw, &cursor, fields))) + goto error; + + con = (query_node_t*) calloc(1, sizeof(*con)); + con->type = join; + con->left.node = left; + con->right.node = right; + + left = con; + } + + if(query != raw) + free(raw); + + return left; + + error: + if(left != 0) + query_free(left); + if(raw != query) + free(raw); + + return NULL; +} + +static query_node_t* match_specifier(const char* query, + const char** cursor, + const query_field_t* fields) +{ + switch(**cursor) + { + case '\'': return single_match(query, cursor, fields); + case '(': return group_match(query, cursor, fields); + } + + DPRINTF(ERR_LOG, "Illegal character '%c' (0%o) at index %d: %s\n", + **cursor, **cursor, *cursor - query, query); + return NULL; +} + +static query_node_t* group_match(const char* query, + const char** pcursor, + const query_field_t* fields) +{ + query_node_t* left = 0; + query_node_t* right = 0; + query_node_t* join = 0; + query_type_t opcode; + const char* cursor = *pcursor; + + /* skip the opening ')' */ + ++cursor; + + if(0 == (left = single_match(query, &cursor, fields))) + return NULL; + + switch(*cursor) + { + case '+': + case ' ': + opcode = qot_and; + break; + + case ',': + opcode = qot_or; + break; + + default: + DPRINTF(ERR_LOG, "Illegal character '%c' (0%o) at index %d: %s\n", + *cursor, *cursor, cursor - query, query); + goto error; + } + + if(0 == (right = single_match(query, &cursor, fields))) + goto error; + + if(*cursor != ')') + { + DPRINTF(ERR_LOG, "Illegal character '%c' (0%o) at index %d: %s\n", + *cursor, *cursor, cursor - query, query); + goto error; + } + + *pcursor = cursor + 1; + + join = (query_node_t*) calloc(1, sizeof(*join)); + join->type = opcode; + join->left.node = left; + join->right.node = right; + + return join; + + error: + if(0 != left) + query_free(left); + if(0 != right) + query_free(right); + + return 0; +} + +static query_node_t* single_match(const char* query, + const char** pcursor, + const query_field_t* fields) +{ + char fname[64]; + const query_field_t* field; + char not = 0; + char op = 0; + query_node_t* node = 0; + + /* skip opening ' */ + (*pcursor)++; + + /* collect the field name */ + if(!get_field_name(pcursor, query, fname, sizeof(fname))) + return NULL; + + if(**pcursor == '!') + { + not = '!'; + ++(*pcursor); + } + + if(strchr(":+-", **pcursor)) + { + op = **pcursor; + ++(*pcursor); + } + else + { + DPRINTF(ERR_LOG, "Illegal Operator: %c (0%o) at index %d: %s\n", + **pcursor, **pcursor, *pcursor - query, query); + return NULL; + } + + if(0 == (field = find_field(fname, fields))) + { + DPRINTF(ERR_LOG, "Unknown field: %s\n", fname); + return NULL; + } + + switch(field->type) + { + case qft_i32: + case qft_i64: + node = match_number(field, not, op, pcursor, query); + break; + + case qft_string: + node = match_string(field, not, op, pcursor, query); + break; + + default: + DPRINTF(ERR_LOG, "Invalid field type: %d\n", field->type); + break; + } + + if(**pcursor != '\'') + { + DPRINTF(ERR_LOG, "Illegal Character: %c (0%o) index %d: %s\n", + **pcursor, **pcursor, *pcursor - query, query); + query_free(node); + node = 0; + } + else + ++(*pcursor); + + return node; +} + +static int get_field_name(const char** pcursor, + const char* query, + char* name, + int len) +{ + const char* cursor = *pcursor; + + if(!isalpha(*cursor)) + return 0; + + while(isalpha(*cursor) || *cursor == '.') + { + if(--len <= 0) + { + DPRINTF(ERR_LOG, "token length exceeded at offset %d: %s\n", + cursor - query, query); + return 0; + } + + *name++ = *cursor++; + } + + *pcursor = cursor; + + *name = 0; + + return 1; +} + +static query_node_t* match_number(const query_field_t* field, + char not, char opcode, + const char** pcursor, + const char* query) +{ + query_node_t* node = (query_node_t*) calloc(1, sizeof(node)); + + switch(opcode) + { + case ':': + node->type = not ? qot_ne : qot_eq; + break; + case '+': + case ' ': + node->type = not ? qot_le : qot_gt; + break; + case '-': + node->type = not ? qot_ge : qot_lt; + break; + } + + node->left.field = field; + + switch(field->type) + { + case qft_i32: + node->right.i32 = strtol(*pcursor, (char**) pcursor, 10); + break; + case qft_i64: + node->right.i64 = strtoll(*pcursor, (char**) pcursor, 10); + break; + } + + if(**pcursor != '\'') + { + DPRINTF(ERR_LOG, "Illegal char in number '%c' (0%o) at index %d: %s\n", + **pcursor, **pcursor, *pcursor - query, query); + free(node); + return 0; + } + + return node; +} + +static query_node_t* match_string(const query_field_t* field, + char not, char opcode, + const char** pcursor, + const char* query) +{ + char match[64]; + char* dst = match; + int left = sizeof(match); + const char* cursor = *pcursor; + query_type_t op = qot_is; + query_node_t* node; + + if(opcode != ':') + { + DPRINTF(ERR_LOG, "Illegal operation on string: %c at index %d: %s\n", + opcode, cursor - query - 1); + return NULL; + } + + if(*cursor == '*') + { + op = qot_ends; + cursor++; + } + + while(*cursor && *cursor != '\'') + { + if(--left == 0) + { + DPRINTF(ERR_LOG, "string too long at index %d: %s\n", + cursor - query, query); + return NULL; + } + + if(*cursor == '\\') + { + switch(*++cursor) + { + case '*': + case '\'': + case '\\': + *dst++ = *cursor++; + break; + default: + DPRINTF(ERR_LOG, "Illegal escape: %c (0%o) at index %d: %s\n", + *cursor, *cursor, cursor - query, query); + return NULL; + } + } + else + *dst++ = *cursor++; + } + + if(dst[-1] == '*') + { + op = (op == qot_is) ? qot_begins : qot_contains; + dst--; + } + + *dst = 0; + + node = (query_node_t*) calloc(1, sizeof(*node)); + node->type = op; + node->left.field = field; + node->right.str = strdup(match); + + *pcursor = cursor; + + return node; +} + +int query_test(query_node_t* query, void* target) +{ + switch(query->type) + { + /* conjunction */ + case qot_and: + return (query_test(query->left.node, target) && + query_test(query->right.node, target)); + + case qot_or: + return (query_test(query->left.node, target) || + query_test(query->right.node, target)); + + /* negation */ + case qot_not: + return !query_test(query->left.node, target); + + /* arithmetic */ + case qot_eq: + case qot_ne: + case qot_le: + case qot_lt: + case qot_ge: + case qot_gt: + return arith_query(query, target); + + /* string */ + case qot_is: + case qot_begins: + case qot_ends: + case qot_contains: + return string_query(query, target); + break; + + /* constants */ + case qot_const: + return query->left.constant; + } +} + +void query_free(query_node_t* query) +{ + if(0 != query) + { + switch(query->type) + { + /* conjunction */ + case qot_and: + case qot_or: + query_free(query->left.node); + query_free(query->right.node); + break; + + /* negation */ + case qot_not: + query_free(query->left.node); + break; + + /* arithmetic */ + case qot_eq: + case qot_ne: + case qot_le: + case qot_lt: + case qot_ge: + case qot_gt: + break; + + /* string */ + case qot_is: + case qot_begins: + case qot_ends: + case qot_contains: + free(query->right.str); + break; + + /* constants */ + case qot_const: + break; + + default: + DPRINTF(ERR_LOG, "Illegal query type: %d\n", query->type); + break; + } + + free(query); + } +} + +static const query_field_t* find_field(const char* name, const query_field_t* fields) +{ + while(fields->name && strcasecmp(fields->name, name)) + fields++; + + if(fields->name == 0) + { + DPRINTF(ERR_LOG, "Illegal query field: %s\n", name); + return NULL; + } + + return fields; +} + +static int arith_query(query_node_t* query, void* target) +{ + const query_field_t* field = query->left.field; + + switch(field->type) + { + case qft_i32: + { + int tv = * (int*) ((size_t) target + field->offset); + + tv -= query->right.i32; + + switch(query->type) + { + case qot_eq: return tv == 0; + case qot_ne: return tv != 0; + case qot_le: return tv <= 0; + case qot_lt: return tv < 0; + case qot_ge: return tv >= 0; + case qot_gt: return tv > 0; + default: + DPRINTF(ERR_LOG, "illegal query type: %d\n", query->type); + break; + } + } + break; + + case qft_i64: + { + long long tv = * (long long*) ((size_t) target + field->offset); + + tv -= query->right.i32; + + switch(query->type) + { + case qot_eq: return tv == 0; + case qot_ne: return tv != 0; + case qot_le: return tv <= 0; + case qot_lt: return tv < 0; + case qot_ge: return tv >= 0; + case qot_gt: return tv > 0; + default: + DPRINTF(ERR_LOG, "illegal query type: %d\n", query->type); + break; + } + } + break; + + default: + DPRINTF(ERR_LOG, "illegal field type: %d\n", field->type); + break; + } + + return 0; +} + +static int string_query(query_node_t* query, void* target) +{ + const query_field_t* field = query->left.field; + const char* ts; + + if(field->type != qft_string) + { + DPRINTF(ERR_LOG, "illegal field type: %d\n", field->type); + return 0; + } + + ts = * (const char**) ((size_t) target + field->offset); + + if(0 == ts) + return strlen(query->right.str) == 0; + + switch(query->type) + { + case qot_is: + return !strcasecmp(query->right.str, ts); + + case qot_begins: + return !strncasecmp(query->right.str, ts, strlen(query->right.str)); + + case qot_ends: + { + int start = strlen(ts) - strlen(query->right.str); + + if(start < 0) + return 0; + + return !strcasecmp(query->right.str, ts + start); + } + + case qot_contains: + return !strcasestr(ts, query->right.str); + + default: + DPRINTF(ERR_LOG, "Illegal query type: %d\n", query->type); + break; + } + + return 0; +} + +void query_dump(FILE* fp, query_node_t* query, int depth) +{ + static const char* labels[] = { + "NOP", + "and", + "or", + "not", + "==", + "!=", + "<=", + "<", + ">=", + ">", + "eq", + "beginswith", + "endwith", + "contains", + "constant" + }; + + switch(query->type) + { + case qot_and: + case qot_or: + fprintf(fp, "%*s(%s\n", depth, "", labels[query->type]); + query_dump(fp, query->left.node, depth + 4); + query_dump(fp, query->right.node, depth + 4); + fprintf(fp, "%*s)\n", depth, ""); + break; + + case qot_not: + fprintf(fp, "%*s(not\n", depth, ""); + query_dump(fp, query->left.node, depth + 4); + fprintf(fp, "%*s)\n", depth, ""); + break; + + /* arithmetic */ + case qot_eq: + case qot_ne: + case qot_le: + case qot_lt: + case qot_ge: + case qot_gt: + if(query->left.field->type == qft_i32) + fprintf(fp, "%*s(%s %s %d)\n", + depth, "", labels[query->type], + query->left.field->name, query->right.i32); + else + fprintf(fp, "%*s(%s %s %q)\n", + depth, "", labels[query->type], + query->left.field->name, query->right.i64); + break; + + /* string */ + case qot_is: + case qot_begins: + case qot_ends: + case qot_contains: + fprintf(fp, "%*s(%s %s \"%s\")\n", + depth, "", labels[query->type], + query->left.field->name, query->right.str); + break; + + /* constants */ + case qot_const: + fprintf(fp, "%*s(%s)\n", depth, "", query->left.constant ? "true" : "false"); + break; + } + +} + +char* query_unescape(const char* src) +{ + char* copy = malloc(strlen(src) + 1); + char* dst = copy; + + while(*src) + { + if(*src == '%') + { + int val = 0; + + if(*++src) + { + if(isdigit(*src)) + val = val * 16 + *src - '0'; + else + val = val * 16 + tolower(*src) - 'a' + 10; + } + + if(*++src) + { + if(isdigit(*src)) + val = val * 16 + *src - '0'; + else + val = val * 16 + tolower(*src) - 'a' + 10; + } + + src++; + *dst++ = val; + } + else + *dst++ = *src++; + } + + *dst++ = 0; + + return copy; +} diff --git a/src/query.h b/src/query.h new file mode 100644 index 00000000..0d52bf6f --- /dev/null +++ b/src/query.h @@ -0,0 +1,71 @@ +#ifndef __query__ +#define __query__ + +typedef enum +{ + /* node opcodes */ + qot_empty, + + /* conjunctions */ + qot_and, + qot_or, + + /* negation */ + qot_not, + + /* arithmetic */ + qot_eq, + qot_ne, + qot_le, + qot_lt, + qot_ge, + qot_gt, + + /* string */ + qot_is, + qot_begins, + qot_ends, + qot_contains, + + /* constant opcode */ + qot_const, + + /* field types */ + qft_i32, + qft_i64, + qft_string + +} query_type_t; + +typedef struct query_field_ query_field_t; +struct query_field_ +{ + query_type_t type; + const char* name; + int offset; +}; + +typedef struct query_node_ query_node_t; +struct query_node_ +{ + query_type_t type; + union { + query_node_t* node; + const query_field_t* field; + int constant; + } left; + union { + query_node_t* node; + int i32; + long long i64; + char* str; + } right; +}; + +query_node_t* query_build(const char* query, + const query_field_t* fields); +int query_test(query_node_t* query, void* target); +void query_free(query_node_t* query); +void query_dump(FILE* fp, query_node_t* query, int depth); + +#endif diff --git a/src/webserver.c b/src/webserver.c index 587c166c..e09c9611 100644 --- a/src/webserver.c +++ b/src/webserver.c @@ -356,6 +356,10 @@ void *ws_mainthread(void *arg) { void ws_close(WS_CONNINFO *pwsc) { WS_PRIVATE *pwsp = (WS_PRIVATE *)pwsc->pwsp; + /* DWB: update the status so it doesn't fill up with no longer + relevant entries */ + config_set_status(pwsc, 0, NULL); + DPRINTF(ERR_DEBUG,"Thread %d: Terminating\n",pwsc->threadno); DPRINTF(ERR_DEBUG,"Thread %d: Freeing request headers\n",pwsc->threadno); ws_freearglist(&pwsc->request_headers); @@ -521,7 +525,7 @@ int ws_getheaders(WS_CONNINFO *pwsc) { DPRINTF(ERR_DEBUG,"Thread %d: Adding header *%s=%s*\n", pwsc->threadno,first,last); - if(ws_addarg(&pwsc->request_headers,first,last)) { + if(ws_addarg(&pwsc->request_headers,first,"%s",last)) { DPRINTF(ERR_FATAL,"Thread %d: Out of memory\n", pwsc->threadno); pwsc->error=ENOMEM; @@ -567,7 +571,7 @@ int ws_getgetvars(WS_CONNINFO *pwsc, char *string) { } else { DPRINTF(ERR_DEBUG,"Thread %d: Adding arg %s = %s\n", pwsc->threadno,first,middle); - ws_addarg(&pwsc->request_vars,first,middle); + ws_addarg(&pwsc->request_vars,first,"%s",middle); } if(!last) { @@ -613,7 +617,9 @@ void *ws_dispatcher(void *arg) { * and decide where to dispatch it */ - if((readlinetimed(pwsc->fd,buffer,sizeof(buffer),30.0)) < 1) { + /* DWB: set timeout to 30 minutes as advertised in the + server-info response. */ + if((readlinetimed(pwsc->fd,buffer,sizeof(buffer),1800.0)) < 1) { pwsc->error=errno; pwsc->close=1; DPRINTF(ERR_WARN,"Thread %d: could not read: %s\n", @@ -623,6 +629,7 @@ void *ws_dispatcher(void *arg) { } DPRINTF(ERR_DEBUG,"Thread %d: got request\n",pwsc->threadno); + DPRINTF(ERR_DEBUG - 1, "Request: %s", buffer); first=last=buffer; strsep(&last," "); @@ -1057,10 +1064,13 @@ char *ws_urldecode(char *string) { while(*src) { switch(*src) { +#if 0 + /* DWB - space gets converted to %20, not +, this definitely breaks compatibility with iTunes */ case '+': *dst++=' '; src++; break; +#endif case '%': /* this is hideous */ src++;