From b74e2d33f3296f274726587cf764735e7a54fb3d Mon Sep 17 00:00:00 2001 From: Justin Maggard Date: Wed, 25 Feb 2009 21:16:51 +0000 Subject: [PATCH] * Use internal music metadata functions intead of taglib. 1) Taglib does not support MP4 or WMA/ASF without hacking it in there. 2) Taglib is C++, so it's nice to remove that dependency. * Use embedded album art where available. --- INSTALL | 5 +- Makefile | 6 +- TODO | 7 +- albumart.c | 263 ++++++++++--- albumart.h | 2 +- log.c | 6 +- metadata.c | 178 ++++----- metadata.h | 30 ++ minidlna.c | 2 +- scanner.c | 45 +-- tagutils/misc.c | 118 ++++++ tagutils/misc.h | 52 +++ tagutils/tagutils-aac.c | 368 ++++++++++++++++++ tagutils/tagutils-aac.h | 27 ++ tagutils/tagutils-asf.c | 547 +++++++++++++++++++++++++++ tagutils/tagutils-asf.h | 352 +++++++++++++++++ tagutils/tagutils-flc.c | 90 +++++ tagutils/tagutils-flc.h | 24 ++ tagutils/tagutils-misc.c | 262 +++++++++++++ tagutils/tagutils-mp3.c | 776 ++++++++++++++++++++++++++++++++++++++ tagutils/tagutils-mp3.h | 64 ++++ tagutils/tagutils-ogg.c | 542 ++++++++++++++++++++++++++ tagutils/tagutils-ogg.h | 24 ++ tagutils/tagutils-plist.c | 140 +++++++ tagutils/tagutils.c | 289 ++++++++++++++ tagutils/tagutils.h | 120 ++++++ tagutils/textutils.c | 298 +++++++++++++++ tagutils/textutils.h | 29 ++ tivo_beacon.c | 48 ++- tivo_beacon.h | 1 + tivo_commands.c | 273 +++++++++++++- tivo_commands.h | 1 + tivo_utils.c | 1 + tivo_utils.h | 1 + upnpglobalvars.h | 9 +- upnphttp.c | 4 +- 36 files changed, 4766 insertions(+), 238 deletions(-) create mode 100644 tagutils/misc.c create mode 100644 tagutils/misc.h create mode 100644 tagutils/tagutils-aac.c create mode 100644 tagutils/tagutils-aac.h create mode 100644 tagutils/tagutils-asf.c create mode 100644 tagutils/tagutils-asf.h create mode 100644 tagutils/tagutils-flc.c create mode 100644 tagutils/tagutils-flc.h create mode 100644 tagutils/tagutils-misc.c create mode 100644 tagutils/tagutils-mp3.c create mode 100644 tagutils/tagutils-mp3.h create mode 100644 tagutils/tagutils-ogg.c create mode 100644 tagutils/tagutils-ogg.h create mode 100644 tagutils/tagutils-plist.c create mode 100644 tagutils/tagutils.c create mode 100644 tagutils/tagutils.h create mode 100644 tagutils/textutils.c create mode 100644 tagutils/textutils.h diff --git a/INSTALL b/INSTALL index 9bc05a2..78537d6 100644 --- a/INSTALL +++ b/INSTALL @@ -5,10 +5,11 @@ Homepage : http://sourceforge.net/projects/minidlna/ Prerequisites : -- taglib (including C bindings) - libexif -- libgd (either libgd-xpm or libgd-noxpm) +- libgd2 (either libgd2-xpm or libgd2-noxpm) - libid3tag +- libFLAC +- libvorbis - sqlite3 - libavformat (the ffmpeg libraries) - libuuid diff --git a/Makefile b/Makefile index 0dcb830..9821e94 100644 --- a/Makefile +++ b/Makefile @@ -30,11 +30,12 @@ BASEOBJS = minidlna.o upnphttp.o upnpdescgen.o upnpsoap.o \ options.o minissdp.o upnpevents.o \ sql.o utils.o metadata.o albumart.o scanner.o inotify.o \ tivo_utils.o tivo_beacon.o tivo_commands.o \ + tagutils/textutils.o tagutils/misc.o tagutils/tagutils.o \ log.o ALLOBJS = $(BASEOBJS) $(LNXOBJS) -LIBS = -lexif -ljpeg -ltag_c -lid3tag -lsqlite3 -lavformat -luuid -lgd +LIBS = -lexif -ljpeg -lsqlite3 -lavformat -lid3tag -lFLAC -luuid -lgd TESTUPNPDESCGENOBJS = testupnpdescgen.o upnpdescgen.o @@ -104,5 +105,8 @@ upnpdescgen.o: config.h upnpdescgen.h minidlnapath.h upnpglobalvars.h upnpdescgen.o: minidlnatypes.h upnpdescstrings.h scanner.o: upnpglobalvars.h metadata.h utils.h sql.h scanner.h log.h metadata.o: upnpglobalvars.h metadata.h albumart.h utils.h sql.h log.h +tagutils/misc.o: tagutils/misc.h +tagutils/textutils.o: tagutils/misc.h tagutils/textutils.h log.h +tagutils/tagutils.o: tagutils/tagutils-asf.c tagutils/tagutils-flc.c tagutils/tagutils-plist.c tagutils/tagutils-aac.c tagutils/tagutils-asf.h tagutils/tagutils-flc.h tagutils/tagutils-mp3.c tagutils/tagutils-ogg.c tagutils/tagutils-aac.h tagutils/tagutils.h tagutils/tagutils-mp3.h tagutils/tagutils-ogg.h log.h sql.o: sql.h log.o: log.h diff --git a/TODO b/TODO index 43f86e0..ba6c6da 100644 --- a/TODO +++ b/TODO @@ -2,9 +2,8 @@ Things left to do: * Persistent HTTP connection (Keep-Alive) support * PNG image support -* DLNA Profile Name (DLNA.ORG_PN) support for more audio files * SortCriteria support -* Completely redo the logging scheme. -* Update scan support (do not require removing the database) -* Inotify support * Upload support + +Wishlist: +* Transcoding diff --git a/albumart.c b/albumart.c index bbe7887..49e8946 100644 --- a/albumart.c +++ b/albumart.c @@ -15,7 +15,6 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -#undef HAVE_LIBID3TAG #include #include #include @@ -31,6 +30,7 @@ #include "upnpglobalvars.h" #include "sql.h" #include "utils.h" +#include "log.h" /* For libjpeg error handling */ jmp_buf setjmp_buffer; @@ -41,6 +41,7 @@ static void libjpeg_error_handler(j_common_ptr cinfo) return; } +#if 0 // Not needed currently int check_res(int width, int height, char * dlna_pn) { @@ -58,6 +59,131 @@ check_res(int width, int height, char * dlna_pn) return 0; return 1; } +#endif + +/* Use our own boxfilter resizer, because gdCopyImageResampled is slow, + * and gdCopyImageResized looks horrible when you downscale this much. */ +#define N_FRAC 8 +#define MASK_FRAC ((1 << N_FRAC) - 1) +#define ROUND2(v) (((v) + (1 << (N_FRAC - 1))) >> N_FRAC) +#define DIV(x, y) ( ((x) << (N_FRAC - 3)) / ((y) >> 3) ) +static void +boxfilter_resize(gdImagePtr dst, gdImagePtr src, + int dstX, int dstY, int srcX, int srcY, + int dstW, int dstH, int srcW, int srcH) +{ + int x, y; + int sy1, sy2, sx1, sx2; + + if(!dst->trueColor) + { + gdImageCopyResized(dst, src, dstX, dstY, srcX, srcY, dstW, dstH, + srcW, srcH); + return; + } + for(y = dstY; y < (dstY + dstH); y++) + { + sy1 = (((y - dstY) * srcH) << N_FRAC) / dstH; + sy2 = (((y - dstY + 1) * srcH) << N_FRAC) / dstH; + for(x = dstX; x < (dstX + dstW); x++) + { + int sx, sy; + int spixels = 0; + int red = 0, green = 0, blue = 0, alpha = 0; + sx1 = (((x - dstX) * srcW) << N_FRAC) / dstW; + sx2 = (((x - dstX + 1) * srcW) << N_FRAC) / dstW; + sy = sy1; + do { + int yportion; + if((sy >> N_FRAC) == (sy1 >> N_FRAC)) + { + yportion = (1 << N_FRAC) - (sy & MASK_FRAC); + if(yportion > sy2 - sy1) + { + yportion = sy2 - sy1; + } + sy = sy & ~MASK_FRAC; + } + else if(sy == (sy2 & ~MASK_FRAC)) + { + yportion = sy2 & MASK_FRAC; + } + else + { + yportion = (1 << N_FRAC); + } + sx = sx1; + do { + int xportion; + int pcontribution; + int p; + if((sx >> N_FRAC) == (sx1 >> N_FRAC)) + { + xportion = (1 << N_FRAC) - (sx & MASK_FRAC); + if(xportion > sx2 - sx1) + { + xportion = sx2 - sx1; + } + sx = sx & ~MASK_FRAC; + } + else if(sx == (sx2 & ~MASK_FRAC)) + { + xportion = sx2 & MASK_FRAC; + } + else + { + xportion = (1 << N_FRAC); + } + + if(xportion && yportion) + { + pcontribution = (xportion * yportion) >> N_FRAC; + p = gdImageGetTrueColorPixel(src, ROUND2(sx) + srcX, ROUND2(sy) + srcY); + if(pcontribution == (1 << N_FRAC)) + { + // optimization for down-scaler, which many pixel has pcontribution=1 + red += gdTrueColorGetRed(p) << N_FRAC; + green += gdTrueColorGetGreen(p) << N_FRAC; + blue += gdTrueColorGetBlue(p) << N_FRAC; + alpha += gdTrueColorGetAlpha(p) << N_FRAC; + spixels += (1 << N_FRAC); + } + else + { + red += gdTrueColorGetRed(p) * pcontribution; + green += gdTrueColorGetGreen(p) * pcontribution; + blue += gdTrueColorGetBlue(p) * pcontribution; + alpha += gdTrueColorGetAlpha(p) * pcontribution; + spixels += pcontribution; + } + } + sx += (1 << N_FRAC); + } + while(sx < sx2); + sy += (1 << N_FRAC); + } + while(sy < sy2); + if(spixels != 0) + { + red = DIV(red, spixels); + green = DIV(green, spixels); + blue = DIV(blue, spixels); + alpha = DIV(alpha, spixels); + } + /* Clamping to allow for rounding errors above */ + if(red > (255 << N_FRAC)) + red = (255 << N_FRAC); + if(green > (255 << N_FRAC)) + green = (255 << N_FRAC); + if(blue > (255 << N_FRAC)) + blue = (255 << N_FRAC); + if(alpha > (gdAlphaMax << N_FRAC)) + alpha = (gdAlphaMax << N_FRAC); + gdImageSetPixel(dst, x, y, + gdTrueColorAlpha(ROUND2(red), ROUND2(green), ROUND2(blue), ROUND2(alpha))); + } + } +} char * save_resized_album_art(void * ptr, const char * path, int srcw, int srch, int file, int size) @@ -104,11 +230,15 @@ save_resized_album_art(void * ptr, const char * path, int srcw, int srch, int fi fclose(dstfile); goto error; } + #if 0 // Try our box filter resizer for now #ifdef __sparc__ gdImageCopyResized(imdst, imsrc, 0, 0, 0, 0, dstw, dsth, imsrc->sx, imsrc->sy); #else gdImageCopyResampled(imdst, imsrc, 0, 0, 0, 0, dstw, dsth, imsrc->sx, imsrc->sy); #endif + #else + boxfilter_resize(imdst, imsrc, 0, 0, 0, 0, dstw, dsth, imsrc->sx, imsrc->sy); + #endif gdImageJpeg(imdst, dstfile, 96); fclose(dstfile); gdImageDestroy(imsrc); @@ -120,10 +250,6 @@ error: return NULL; } - -#ifdef HAVE_LIBID3TAG -#include - /* These next few functions are to allow loading JPEG data directly from memory for libjpeg. * The standard functions only allow you to read from a file. * This code comes from the JpgAlleg library, at http://wiki.allegro.cc/index.php?title=Libjpeg */ @@ -184,68 +310,99 @@ jpeg_memory_src(j_decompress_ptr cinfo, unsigned char const *buffer, size_t bufs src->pub.bytes_in_buffer = bufsize; } +/* Simple, efficient hash function from Daniel J. Bernstein */ +unsigned int DJBHash(const char* str, int len) +{ + unsigned int hash = 5381; + unsigned int i = 0; + + for(i = 0; i < len; str++, i++) + { + hash = ((hash << 5) + hash) + (*str); + } + + return hash; +} + /* And our main album art functions */ char * -check_embedded_art(const char * path, char * dlna_pn) +check_embedded_art(const char * path, const char * image_data, int image_size) { - struct id3_file *file; - struct id3_tag *pid3tag; - struct id3_frame *pid3frame; - id3_byte_t const *image; - id3_latin1_t const *mime; - id3_length_t length; - int index; struct jpeg_decompress_struct cinfo; struct jpeg_error_mgr jerr; int width = 0, height = 0; char * art_path = NULL; + char * cache_dir; + FILE * dstfile; + size_t nwritten; + static char last_path[PATH_MAX]; + static unsigned int last_hash = 0; + unsigned int hash; - file = id3_file_open(path, ID3_FILE_MODE_READONLY); - if( !file ) - return 0; - - pid3tag = id3_file_tag(file); - - for( index=0; (pid3frame = id3_tag_findframe(pid3tag, "", index)); index++ ) + /* If the embedded image matches the embedded image from the last file we + * checked, just make a hard link. No use in storing it on the disk twice. */ + hash = DJBHash(image_data, image_size); + if( hash == last_hash ) { - if( strcmp(pid3frame->id, "APIC") == 0 ) + asprintf(&art_path, DB_PATH "/art_cache%s", path); + if( link(last_path, art_path) == 0 ) { - mime = id3_field_getlatin1(&pid3frame->fields[1]); - if( strcmp((char*)mime, "image/jpeg") && strcmp((char*)mime, "jpeg") ) - continue; - image = id3_field_getbinarydata(&pid3frame->fields[4], &length); - - cinfo.err = jpeg_std_error(&jerr); - jerr.error_exit = libjpeg_error_handler; - jpeg_create_decompress(&cinfo); - if( setjmp(setjmp_buffer) ) - goto error; - jpeg_memory_src(&cinfo, image, length); - jpeg_read_header(&cinfo, TRUE); - jpeg_start_decompress(&cinfo); - width = cinfo.output_width; - height = cinfo.output_height; - error: - jpeg_destroy_decompress(&cinfo); - break; + return(art_path); + } + else + { + printf("link failed\n"); + free(art_path); + art_path = NULL; } } + + cinfo.err = jpeg_std_error(&jerr); + jerr.error_exit = libjpeg_error_handler; + jpeg_create_decompress(&cinfo); + if( setjmp(setjmp_buffer) ) + goto error; + jpeg_memory_src(&cinfo, (unsigned char *)image_data, image_size); + jpeg_read_header(&cinfo, TRUE); + jpeg_start_decompress(&cinfo); + width = cinfo.output_width; + height = cinfo.output_height; + error: + jpeg_destroy_decompress(&cinfo); + if( width > 160 || height > 160 ) { - art_path = save_resized_album_art((void *)image, path, width, height, 0, length); + art_path = save_resized_album_art((void *)image_data, path, width, height, 0, image_size); } else if( width > 0 && height > 0 ) { - art_path = path; + asprintf(&art_path, DB_PATH "/art_cache%s", path); + if( access(art_path, F_OK) == 0 ) + return art_path; + cache_dir = strdup(art_path); + make_dir(dirname(cache_dir), S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH); + free(cache_dir); + dstfile = fopen(art_path, "w"); + if( !dstfile ) + return NULL; + nwritten = fwrite((void *)image_data, image_size, 1, dstfile); + fclose(dstfile); + if( nwritten != image_size ) + { + free(art_path); + remove(art_path); + return NULL; + } } - id3_file_close(file); + DPRINTF(E_DEBUG, L_METADATA, "Found new embedded album art in %s\n", basename((char *)path)); + last_hash = hash; + strcpy(last_path, art_path); - return(art_file); + return(art_path); } -#endif // HAVE_LIBID3TAG char * -check_for_album_file(char * dir, char * dlna_pn) +check_for_album_file(char * dir) { char * file = malloc(PATH_MAX); struct album_art_name_s * album_art_name; @@ -290,7 +447,7 @@ check_for_album_file(char * dir, char * dlna_pn) } sqlite_int64 -find_album_art(const char * path, char * dlna_pn) +find_album_art(const char * path, char * dlna_pn, const char * image_data, int image_size) { char * album_art = NULL; char * sql; @@ -299,11 +456,8 @@ find_album_art(const char * path, char * dlna_pn) sqlite_int64 ret = 0; char * mypath = strdup(path); - #ifdef HAVE_LIBID3TAG - if( check_embedded_art(path, dlna_pn) || (album_art = check_for_album_file(dirname(mypath), dlna_pn)) ) - #else - if( (album_art = check_for_album_file(dirname(mypath), dlna_pn)) ) - #endif + if( (image_size && (album_art = check_embedded_art(path, image_data, image_size))) || + (album_art = check_for_album_file(dirname(mypath))) ) { strcpy(dlna_pn, "JPEG_TN"); sql = sqlite3_mprintf("SELECT ID from ALBUM_ART where PATH = '%q'", album_art ? album_art : path); @@ -314,12 +468,7 @@ find_album_art(const char * path, char * dlna_pn) else { sqlite3_free(sql); - sql = sqlite3_mprintf( "INSERT into ALBUM_ART" - " (PATH, EMBEDDED) " - "VALUES" - " ('%s', %d);", - (album_art ? album_art : path), - (album_art ? 0 : 1) ); + sql = sqlite3_mprintf("INSERT into ALBUM_ART (PATH) VALUES ('%q')", album_art); if( sql_exec(db, sql) == SQLITE_OK ) ret = sqlite3_last_insert_rowid(db); } diff --git a/albumart.h b/albumart.h index 6db6176..7711d59 100644 --- a/albumart.h +++ b/albumart.h @@ -11,6 +11,6 @@ #define __ALBUMART_H__ sqlite_int64 -find_album_art(const char * path, char * dlna_pn); +find_album_art(const char * path, char * dlna_pn, const char * image_data, int image_size); #endif diff --git a/log.c b/log.c index cca5320..fbc686e 100644 --- a/log.c +++ b/log.c @@ -104,7 +104,7 @@ log_init(const char *fname, const char *debug) log_level[i] = default_log_level; } - if (!fname) // use default i.e. stderr + if (!fname) // use default i.e. stdout return 0; if (!(fp = fopen(fname, "a"))) @@ -126,7 +126,7 @@ log_err(int level, enum _log_facility facility, char *fname, int lineno, char *f return; if (!log_fp) - log_fp = stderr; + log_fp = stdout; // user log va_start(ap, fmt); @@ -135,7 +135,7 @@ log_err(int level, enum _log_facility facility, char *fname, int lineno, char *f va_end(ap); // timestamp - t = time(0); + t = time(NULL); tm = localtime(&t); fprintf(log_fp, "[%04d/%02d/%02d %02d:%02d:%02d] ", tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, diff --git a/metadata.c b/metadata.c index 977ecf3..5732b51 100644 --- a/metadata.c +++ b/metadata.c @@ -26,14 +26,13 @@ #include #include -#include -#include #include #include #include #include #include #include +#include "tagutils/tagutils.h" #include "upnpglobalvars.h" #include "metadata.h" @@ -126,47 +125,65 @@ GetFolderMetadata(const char * name, const char * path, const char * artist, con sqlite_int64 GetAudioMetadata(const char * path, char * name) { - off_t size = 0; - char date[16], duration[16], dlna_pn[24], mime[16]; + char duration[16], mime[16], type[4]; struct stat file; - int seconds, minutes; sqlite_int64 ret; - TagLib_File *audio_file; - TagLib_Tag *tag; - const TagLib_AudioProperties *properties; char *sql; - char *title, *artist, *album, *genre, *comment; + char *title, *artist = NULL, *album = NULL, *genre = NULL, *comment = NULL, *date = NULL; int free_flags = 0; - sqlite_int64 album_art; + sqlite_int64 album_art = 0; char art_dlna_pn[9]; + struct song_metadata song; + char *dlna_pn = NULL; - if ( stat(path, &file) == 0 ) - size = file.st_size; - else + if ( stat(path, &file) != 0 ) return 0; strip_ext(name); - taglib_set_strings_unicode(1); - - audio_file = taglib_file_new(path); - if(audio_file == NULL) + if( ends_with(path, ".mp3") ) + { + strcpy(type, "mp3"); + strcpy(mime, "audio/mpeg"); + } + else if( ends_with(path, ".m4a") || ends_with(path, ".mp4") || + ends_with(path, ".aac") || ends_with(path, ".m4p") ) + { + strcpy(type, "aac"); + strcpy(mime, "audio/mp4"); + } + else if( ends_with(path, ".wma") || ends_with(path, ".asf") ) + { + strcpy(type, "asf"); + strcpy(mime, "audio/x-ms-wma"); + } + else if( ends_with(path, ".flac") || ends_with(path, ".fla") || ends_with(path, ".flc") ) + { + strcpy(type, "flc"); + strcpy(mime, "audio/x-flac"); + } + else + { + DPRINTF(E_WARN, L_GENERAL, "Unhandled file extension on %s\n", path); return 0; + } - tag = taglib_file_tag(audio_file); - properties = taglib_file_audioproperties(audio_file); - if( !properties ) + if( readtags((char *)path, &song, &file, NULL, type) != 0 ) + { + DPRINTF(E_WARN, L_GENERAL, "Cannot extract tags from %s\n", path); return 0; + } - seconds = taglib_audioproperties_length(properties) % 60; - minutes = (taglib_audioproperties_length(properties) - seconds) / 60; - - date[0] = '\0'; - if( taglib_tag_year(tag) ) - sprintf(date, "%04d-01-01", taglib_tag_year(tag)); - sprintf(duration, "%d:%02d:%02d.000", minutes/60, minutes, seconds); - - title = taglib_tag_title(tag); - if( strlen(title) ) + if( song.dlna_pn ) + asprintf(&dlna_pn, "%s;DLNA.ORG_OP=01", song.dlna_pn); + if( song.year ) + asprintf(&date, "%04d-01-01", song.year); + sprintf(duration, "%d:%02d:%02d.%03d", + (song.song_length/3600000), + (song.song_length/60000), + (song.song_length/1000%60), + (song.song_length%1000)); + title = song.title; + if( title ) { title = trim(title); if( index(title, '&') ) @@ -179,9 +196,9 @@ GetAudioMetadata(const char * path, char * name) { title = name; } - artist = taglib_tag_artist(tag); - if( strlen(artist) ) + if( song.contributor[ROLE_ARTIST] ) { + artist = song.contributor[ROLE_ARTIST]; artist = trim(artist); if( index(artist, '&') ) { @@ -189,13 +206,9 @@ GetAudioMetadata(const char * path, char * name) artist = modifyString(strdup(artist), "&", "&amp;", 0); } } - else - { - artist = NULL; - } - album = taglib_tag_album(tag); - if( strlen(album) ) + if( song.album ) { + album = song.album; album = trim(album); if( index(album, '&') ) { @@ -203,13 +216,9 @@ GetAudioMetadata(const char * path, char * name) album = modifyString(strdup(album), "&", "&amp;", 0); } } - else - { - album = NULL; - } - genre = taglib_tag_genre(tag); - if( strlen(genre) ) + if( song.genre ) { + genre = song.genre; genre = trim(genre); if( index(genre, '&') ) { @@ -217,13 +226,9 @@ GetAudioMetadata(const char * path, char * name) genre = modifyString(strdup(genre), "&", "&amp;", 0); } } - else - { - genre = NULL; - } - comment = taglib_tag_comment(tag); - if( strlen(comment) ) + if( song.comment ) { + comment = song.comment; comment = trim(comment); if( index(comment, '&') ) { @@ -231,50 +236,28 @@ GetAudioMetadata(const char * path, char * name) comment = modifyString(strdup(comment), "&", "&amp;", 0); } } - else - { - comment = NULL; - } - - /* Switch on audio file type */ - if( ends_with(path, ".mp3") ) - { - strcpy(dlna_pn, "MP3;DLNA.ORG_OP=01"); - strcpy(mime, "audio/mpeg"); - } - else if( ends_with(path, ".flac") || ends_with(path, ".fla") || ends_with(path, ".flc") ) - { - strcpy(mime, "audio/x-flac"); - } - else if( ends_with(path, ".m4a") || ends_with(path, ".aac") ) - { - //strcpy(dlna_pn, "MP3;DLNA.ORG_OP=01"); - strcpy(mime, "audio/mp4"); - } - - album_art = find_album_art(path, art_dlna_pn); + album_art = find_album_art(path, art_dlna_pn, song.image, song.image_size); sql = sqlite3_mprintf( "INSERT into DETAILS" " (PATH, SIZE, DURATION, CHANNELS, BITRATE, SAMPLERATE, DATE," " TITLE, CREATOR, ARTIST, ALBUM, GENRE, COMMENT, TRACK, DLNA_PN, MIME, ALBUM_ART, ART_DLNA_PN) " "VALUES" - " (%Q, %llu, '%s', %d, %d, %d, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %d, '%s', '%s', %lld, %Q);", - path, size, duration, taglib_audioproperties_channels(properties), - taglib_audioproperties_bitrate(properties)*1024, - taglib_audioproperties_samplerate(properties), - strlen(date) ? date : NULL, + " (%Q, %d, '%s', %d, %d, %d, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %d, %Q, '%s', %lld, %Q);", + path, song.file_size, duration, song.channels, song.bitrate, song.samplerate, date, title, artist, artist, album, genre, comment, - taglib_tag_track(tag), + song.track, dlna_pn, mime, album_art, album_art?art_dlna_pn:NULL ); - taglib_tag_free_strings(); - taglib_file_free(audio_file); - + freetags(&song); + if( dlna_pn ) + free(dlna_pn); + if( date ) + free(date); if( free_flags & FLAG_TITLE ) free(title); if( free_flags & FLAG_ARTIST ) @@ -297,6 +280,7 @@ GetAudioMetadata(const char * path, char * name) ret = sqlite3_last_insert_rowid(db); } sqlite3_free(sql); + return ret; } @@ -333,7 +317,7 @@ GetImageMetadata(const char * path, char * name) date[0] = '\0'; model[0] = '\0'; - DPRINTF(E_DEBUG, L_METADATA, "Parsing %s...\n", path); + //DEBUG DPRINTF(E_DEBUG, L_METADATA, "Parsing %s...\n", path); if ( stat(path, &file) == 0 ) size = file.st_size; else @@ -460,36 +444,6 @@ GetImageMetadata(const char * path, char * name) return ret; } -typedef enum { - AAC_INVALID = 0, - AAC_MAIN = 1, /* AAC Main */ - AAC_LC = 2, /* AAC Low complexity */ - AAC_SSR = 3, /* AAC SSR */ - AAC_LTP = 4, /* AAC Long term prediction */ - AAC_HE = 5, /* AAC High efficiency (SBR) */ - AAC_SCALE = 6, /* Scalable */ - AAC_TWINVQ = 7, /* TwinVQ */ - AAC_CELP = 8, /* CELP */ - AAC_HVXC = 9, /* HVXC */ - AAC_TTSI = 12, /* TTSI */ - AAC_MS = 13, /* Main synthetic */ - AAC_WAVE = 14, /* Wavetable synthesis */ - AAC_MIDI = 15, /* General MIDI */ - AAC_FX = 16, /* Algorithmic Synthesis and Audio FX */ - AAC_LC_ER = 17, /* AAC Low complexity with error recovery */ - AAC_LTP_ER = 19, /* AAC Long term prediction with error recovery */ - AAC_SCALE_ER = 20, /* AAC scalable with error recovery */ - AAC_TWINVQ_ER = 21, /* TwinVQ with error recovery */ - AAC_BSAC_ER = 22, /* BSAC with error recovery */ - AAC_LD_ER = 23, /* AAC LD with error recovery */ - AAC_CELP_ER = 24, /* CELP with error recovery */ - AAC_HXVC_ER = 25, /* HXVC with error recovery */ - AAC_HILN_ER = 26, /* HILN with error recovery */ - AAC_PARAM_ER = 27, /* Parametric with error recovery */ - AAC_SSC = 28, /* AAC SSC */ - AAC_HE_L3 = 31, /* Reserved : seems to be HeAAC L3 */ -} aac_object_type_t; - sqlite_int64 GetVideoMetadata(const char * path, char * name) { diff --git a/metadata.h b/metadata.h index e196f9e..f274d8a 100644 --- a/metadata.h +++ b/metadata.h @@ -31,6 +31,36 @@ typedef struct tsinfo_s { int packet_size; } tsinfo_t; +typedef enum { + AAC_INVALID = 0, + AAC_MAIN = 1, /* AAC Main */ + AAC_LC = 2, /* AAC Low complexity */ + AAC_SSR = 3, /* AAC SSR */ + AAC_LTP = 4, /* AAC Long term prediction */ + AAC_HE = 5, /* AAC High efficiency (SBR) */ + AAC_SCALE = 6, /* Scalable */ + AAC_TWINVQ = 7, /* TwinVQ */ + AAC_CELP = 8, /* CELP */ + AAC_HVXC = 9, /* HVXC */ + AAC_TTSI = 12, /* TTSI */ + AAC_MS = 13, /* Main synthetic */ + AAC_WAVE = 14, /* Wavetable synthesis */ + AAC_MIDI = 15, /* General MIDI */ + AAC_FX = 16, /* Algorithmic Synthesis and Audio FX */ + AAC_LC_ER = 17, /* AAC Low complexity with error recovery */ + AAC_LTP_ER = 19, /* AAC Long term prediction with error recovery */ + AAC_SCALE_ER = 20, /* AAC scalable with error recovery */ + AAC_TWINVQ_ER = 21, /* TwinVQ with error recovery */ + AAC_BSAC_ER = 22, /* BSAC with error recovery */ + AAC_LD_ER = 23, /* AAC LD with error recovery */ + AAC_CELP_ER = 24, /* CELP with error recovery */ + AAC_HXVC_ER = 25, /* HXVC with error recovery */ + AAC_HILN_ER = 26, /* HILN with error recovery */ + AAC_PARAM_ER = 27, /* Parametric with error recovery */ + AAC_SSC = 28, /* AAC SSC */ + AAC_HE_L3 = 31, /* Reserved : seems to be HeAAC L3 */ +} aac_object_type_t; + typedef enum { NONE, EMPTY, diff --git a/minidlna.c b/minidlna.c index bd96c9f..b1ab913 100644 --- a/minidlna.c +++ b/minidlna.c @@ -614,7 +614,7 @@ main(int argc, char * * argv) if( sql_get_table(db, "pragma user_version", &result, &rows, 0) == SQLITE_OK ) { if( atoi(result[1]) != DB_VERSION ) { - printf("Database version mismatch; need to recreate...\n"); + DPRINTF(E_WARN, L_GENERAL, "Database version mismatch; need to recreate...\n"); sqlite3_close(db); unlink(DB_PATH "/files.db"); system("rm -rf " DB_PATH "/art_cache"); diff --git a/scanner.c b/scanner.c index 50127d2..e3f1afe 100644 --- a/scanner.c +++ b/scanner.c @@ -55,8 +55,10 @@ int is_audio(const char * file) { return (ends_with(file, ".mp3") || ends_with(file, ".flac") || + ends_with(file, ".wma") || ends_with(file, ".asf") || ends_with(file, ".fla") || ends_with(file, ".flc") || - ends_with(file, ".m4a") || ends_with(file, ".aac")); + ends_with(file, ".m4a") || ends_with(file, ".aac") || + ends_with(file, ".mp4") || ends_with(file, ".m4p")); } int @@ -156,7 +158,7 @@ insert_containers(const char * name, const char *path, const char * refID, const if( strcmp(last_date.name, date_taken) == 0 ) { last_date.objectID++; - DPRINTF(E_DEBUG, L_SCANNER, "Using last date item: %s/%s/%X\n", last_date.name, last_date.parentID, last_date.objectID); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "Using last date item: %s/%s/%X\n", last_date.name, last_date.parentID, last_date.objectID); } else { @@ -164,7 +166,7 @@ insert_containers(const char * name, const char *path, const char * refID, const sprintf(last_date.parentID, "3$12$%llX", container>>32); last_date.objectID = (int)container; strcpy(last_date.name, date_taken); - DPRINTF(E_DEBUG, L_SCANNER, "Creating cached date item: %s/%s/%X\n", last_date.name, last_date.parentID, last_date.objectID); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "Creating cached date item: %s/%s/%X\n", last_date.name, last_date.parentID, last_date.objectID); } sql = sqlite3_mprintf( "INSERT into OBJECTS" " (OBJECT_ID, PARENT_ID, REF_ID, CLASS, DETAIL_ID, NAME) " @@ -186,7 +188,7 @@ insert_containers(const char * name, const char *path, const char * refID, const if( strcmp(last_camdate.name, date_taken) == 0 ) { last_camdate.objectID++; - DPRINTF(E_DEBUG, L_SCANNER, "Using last camdate item: %s/%s/%s/%X\n", cam, last_camdate.name, last_camdate.parentID, last_camdate.objectID); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "Using last camdate item: %s/%s/%s/%X\n", cam, last_camdate.name, last_camdate.parentID, last_camdate.objectID); } else { @@ -194,7 +196,7 @@ insert_containers(const char * name, const char *path, const char * refID, const sprintf(last_camdate.parentID, "%s$%llX", last_cam.parentID, container>>32); last_camdate.objectID = (int)container; strcpy(last_camdate.name, date_taken); - DPRINTF(E_DEBUG, L_SCANNER, "Creating cached camdate item: %s/%s/%s/%X\n", cam, last_camdate.name, last_camdate.parentID, last_camdate.objectID); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "Creating cached camdate item: %s/%s/%s/%X\n", cam, last_camdate.name, last_camdate.parentID, last_camdate.objectID); } sql = sqlite3_mprintf( "INSERT into OBJECTS" " (OBJECT_ID, PARENT_ID, REF_ID, CLASS, DETAIL_ID, NAME) " @@ -233,7 +235,7 @@ insert_containers(const char * name, const char *path, const char * refID, const if( strcmp(album, last_album.name) == 0 ) { last_album.objectID++; - DPRINTF(E_DEBUG, L_SCANNER, "Using last album item: %s/%s/%X\n", last_album.name, last_album.parentID, last_album.objectID); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "Using last album item: %s/%s/%X\n", last_album.name, last_album.parentID, last_album.objectID); } else { @@ -241,7 +243,7 @@ insert_containers(const char * name, const char *path, const char * refID, const container = insert_container(album, "1$7", NULL, "album.musicAlbum", artist, genre, album_art, art_dlna_pn); sprintf(last_album.parentID, "1$7$%llX", container>>32); last_album.objectID = (int)container; - DPRINTF(E_DEBUG, L_SCANNER, "Creating cached album item: %s/%s/%X\n", last_album.name, last_album.parentID, last_album.objectID); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "Creating cached album item: %s/%s/%X\n", last_album.name, last_album.parentID, last_album.objectID); } sql = sqlite3_mprintf( "INSERT into OBJECTS" " (OBJECT_ID, PARENT_ID, REF_ID, CLASS, DETAIL_ID, NAME) " @@ -263,7 +265,7 @@ insert_containers(const char * name, const char *path, const char * refID, const if( strcmp(album?album:"Unknown", last_artistalbum.name) == 0 ) { last_artistalbum.objectID++; - DPRINTF(E_DEBUG, L_SCANNER, "Using last artist/album item: %s/%s/%X\n", last_artist.name, last_artist.parentID, last_artist.objectID); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "Using last artist/album item: %s/%s/%X\n", last_artist.name, last_artist.parentID, last_artist.objectID); } else { @@ -271,7 +273,7 @@ insert_containers(const char * name, const char *path, const char * refID, const sprintf(last_artistalbum.parentID, "%s$%llX", last_artist.parentID, container>>32); last_artistalbum.objectID = (int)container; strcpy(last_artistalbum.name, album?album:"Unknown"); - DPRINTF(E_DEBUG, L_SCANNER, "Creating cached artist/album item: %s/%s/%X\n", last_artist.name, last_artist.parentID, last_artist.objectID); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "Creating cached artist/album item: %s/%s/%X\n", last_artist.name, last_artist.parentID, last_artist.objectID); } sql = sqlite3_mprintf( "INSERT into OBJECTS" " (OBJECT_ID, PARENT_ID, REF_ID, CLASS, DETAIL_ID, NAME) " @@ -286,7 +288,7 @@ insert_containers(const char * name, const char *path, const char * refID, const if( strcmp(genre, last_genre.name) == 0 ) { last_genre.objectID++; - DPRINTF(E_DEBUG, L_SCANNER, "Using last genre item: %s/%s/%X\n", last_genre.name, last_genre.parentID, last_genre.objectID); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "Using last genre item: %s/%s/%X\n", last_genre.name, last_genre.parentID, last_genre.objectID); } else { @@ -294,7 +296,7 @@ insert_containers(const char * name, const char *path, const char * refID, const container = insert_container(genre, "1$5", NULL, "genre.musicGenre", NULL, NULL, NULL, NULL); sprintf(last_genre.parentID, "1$5$%llX", container>>32); last_genre.objectID = (int)container; - DPRINTF(E_DEBUG, L_SCANNER, "Creating cached genre item: %s/%s/%X\n", last_genre.name, last_genre.parentID, last_genre.objectID); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "Creating cached genre item: %s/%s/%X\n", last_genre.name, last_genre.parentID, last_genre.objectID); } sql = sqlite3_mprintf( "INSERT into OBJECTS" " (OBJECT_ID, PARENT_ID, REF_ID, CLASS, DETAIL_ID, NAME) " @@ -410,7 +412,7 @@ insert_directory(const char * name, const char * path, const char * base, const "VALUES" " ('%s%s$%X', '%s%s', %Q, '%lld', '%s', '%q')", base, parentID, objectID, base, parentID, refID, detailID, class, name); - DPRINTF(E_DEBUG, L_SCANNER, "SQL: %s\n", sql); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "SQL: %s\n", sql); ret = sql_exec(db, sql); sqlite3_free(sql); if( refID ) @@ -443,15 +445,17 @@ insert_file(char * name, const char * path, const char * parentID, int object) strcpy(class, "item.audioItem.musicTrack"); detailID = GetAudioMetadata(path, name); } - else if( is_video(name) ) + if( !detailID && is_video(name) ) { strcpy(base, VIDEO_DIR_ID); strcpy(class, "item.videoItem"); detailID = GetVideoMetadata(path, name); } - DPRINTF(E_DEBUG, L_SCANNER, "Got DetailID %lu!\n", detailID); if( !detailID ) + { + DPRINTF(E_WARN, L_SCANNER, "Unsuccessful getting details for %s!\n", path); return -1; + } sprintf(objectID, "%s%s$%X", BROWSEDIR_ID, parentID, object); @@ -460,7 +464,7 @@ insert_file(char * name, const char * path, const char * parentID, int object) "VALUES" " ('%s', '%s%s', '%s', %lu, '%q')", objectID, BROWSEDIR_ID, parentID, class, detailID, name); - DPRINTF(E_DEBUG, L_SCANNER, "SQL: %s\n", sql); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "SQL: %s\n", sql); sql_exec(db, sql); sqlite3_free(sql); @@ -482,7 +486,7 @@ insert_file(char * name, const char * path, const char * parentID, int object) "VALUES" " ('%s%s$%X', '%s%s', '%s', '%s', %lu, '%q')", base, parentID, object, base, parentID, objectID, class, detailID, name); - DPRINTF(E_DEBUG, L_SCANNER, "SQL: %s\n", sql); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "SQL: %s\n", sql); sql_exec(db, sql); sqlite3_free(sql); @@ -556,19 +560,18 @@ CreateDatabase(void) "MIME TEXT, " "ALBUM_ART INTEGER DEFAULT 0, " "ART_DLNA_PN TEXT DEFAULT NULL" - ");"); + ")"); if( ret != SQLITE_OK ) goto sql_failed; ret = sql_exec(db, "CREATE TABLE ALBUM_ART ( " "ID INTEGER PRIMARY KEY AUTOINCREMENT, " - "PATH TEXT NOT NULL, " - "EMBEDDED BOOL DEFAULT 0" - ");"); + "PATH TEXT NOT NULL" + ")"); if( ret != SQLITE_OK ) goto sql_failed; ret = sql_exec(db, "CREATE TABLE SETTINGS (" "UPDATE_ID INTEGER PRIMARY KEY" - ");"); + ")"); if( ret != SQLITE_OK ) goto sql_failed; ret = sql_exec(db, "INSERT into SETTINGS values (0)"); diff --git a/tagutils/misc.c b/tagutils/misc.c new file mode 100644 index 0000000..4fd6220 --- /dev/null +++ b/tagutils/misc.c @@ -0,0 +1,118 @@ +//========================================================================= +// FILENAME : misc.c +// DESCRIPTION : Miscelleneous funcs +//========================================================================= +// Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved. +//========================================================================= + +/* This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include +#include +#include + +#include "misc.h" + +inline __u16 +le16_to_cpu(__u16 le16) +{ +#if __BYTE_ORDER == __LITTLE_ENDIAN + return le16; +#else + __u16 be16 = ((le16 << 8) & 0xff00) | ((le16 >> 8) & 0x00ff); + return be16; +#endif +} + +inline __u32 +le32_to_cpu(__u32 le32) +{ +#if __BYTE_ORDER == __LITTLE_ENDIAN + return le32; +#else + __u32 be32 = + ((le32 << 24) & 0xff000000) | + ((le32 << 8) & 0x00ff0000) | + ((le32 >> 8) & 0x0000ff00) | + ((le32 >> 24) & 0x000000ff); + return be32; +#endif +} + +inline __u64 +le64_to_cpu(__u64 le64) +{ +#if __BYTE_ORDER == __LITTLE_ENDIAN + return le64; +#else + __u64 be64; + __u8 *le64p = (__u8*)&le64; + __u8 *be64p = (__u8*)&be64; + be64p[0] = le64p[7]; + be64p[1] = le64p[6]; + be64p[2] = le64p[5]; + be64p[3] = le64p[4]; + be64p[4] = le64p[3]; + be64p[5] = le64p[2]; + be64p[6] = le64p[1]; + be64p[7] = le64p[0]; + return be64; +#endif +} + +inline __u8 +fget_byte(FILE *fp) +{ + __u8 d; + + (void)fread(&d, sizeof(d), 1, fp); + return d; +} + +inline __u16 +fget_le16(FILE *fp) +{ + __u16 d; + + (void)fread(&d, sizeof(d), 1, fp); + d = le16_to_cpu(d); + return d; +} + +inline __u32 +fget_le32(FILE *fp) +{ + __u32 d; + + (void)fread(&d, sizeof(d), 1, fp); + d = le32_to_cpu(d); + return d; +} + +inline __u32 +cpu_to_be32(__u32 cpu32) +{ +#if __BYTE_ORDER == __LITTLE_ENDIAN + __u32 be32 = + ((cpu32 << 24) & 0xff000000) | + ((cpu32 << 8) & 0x00ff0000) | + ((cpu32 >> 8) & 0x0000ff00) | + ((cpu32 >> 24) & 0x000000ff); + return be32; +#else + return cpu32; +#endif +} diff --git a/tagutils/misc.h b/tagutils/misc.h new file mode 100644 index 0000000..59704b6 --- /dev/null +++ b/tagutils/misc.h @@ -0,0 +1,52 @@ +//========================================================================= +// FILENAME : misc.h +// DESCRIPTION : Header for misc.c +//========================================================================= +// Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved. +//========================================================================= + +/* This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _SCANNER_MISC_H +#define _SCANNER_MISC_H + +typedef unsigned char __u8; +typedef signed char __s8; +typedef unsigned short __u16; +typedef signed short __s16; +typedef unsigned int __u32; +typedef signed int __s32; +#if __WORDSIZE == 64 +typedef unsigned long __u64; +typedef signed long __s64; +#else +typedef unsigned long long __u64; +typedef signed long long __s64; +#endif + + +inline __u16 le16_to_cpu(__u16 le16); +inline __u32 le32_to_cpu(__u32 le32); +inline __u64 le64_to_cpu(__u64 le64); +inline __u8 fget_byte(FILE *fp); +inline __u16 fget_le16(FILE *fp); +inline __u32 fget_le32(FILE *fp); + +inline __u32 cpu_to_be32(__u32 cpu32); + +extern char * sha1_hex(char *key); + +#endif diff --git a/tagutils/tagutils-aac.c b/tagutils/tagutils-aac.c new file mode 100644 index 0000000..1107454 --- /dev/null +++ b/tagutils/tagutils-aac.c @@ -0,0 +1,368 @@ +//========================================================================= +// FILENAME : tagutils-aac.c +// DESCRIPTION : AAC metadata reader +//========================================================================= +// Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved. +//========================================================================= + +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/* + * This file is derived from mt-daap project. + */ + +// _mac_to_unix_time +static time_t +_mac_to_unix_time(int t) +{ + struct timeval tv; + struct timezone tz; + + gettimeofday(&tv, &tz); + + return (t - (365L * 66L * 24L * 60L * 60L + 17L * 60L * 60L * 24L) + + (tz.tz_minuteswest * 60)); +} + + + +// _aac_findatom: +static long +_aac_findatom(FILE *fin, long max_offset, char *which_atom, int *atom_size) +{ + long current_offset = 0; + int size; + char atom[4]; + + while(current_offset < max_offset) + { + if(fread((void*)&size, 1, sizeof(int), fin) != sizeof(int)) + return -1; + + size = ntohl(size); + + if(size <= 7) + return -1; + + if(fread(atom, 1, 4, fin) != 4) + return -1; + + if(strncasecmp(atom, which_atom, 4) == 0) + { + *atom_size = size; + return current_offset; + } + + fseek(fin, size - 8, SEEK_CUR); + current_offset += size; + } + + return -1; +} + +// _get_aactags +static int +_get_aactags(char *file, struct song_metadata *psong) +{ + FILE *fin; + long atom_offset; + unsigned int atom_length; + + long current_offset = 0; + int current_size; + char current_atom[4]; + char *current_data; + int genre; + int len; + + if(!(fin = fopen(file, "rb"))) + { + DPRINTF(E_ERROR, L_SCANNER, "Cannot open file %s for reading\n", file); + return -1; + } + + fseek(fin, 0, SEEK_SET); + + atom_offset = _aac_lookforatom(fin, "moov:udta:meta:ilst", &atom_length); + if(atom_offset != -1) + { + while(current_offset < atom_length) + { + if(fread((void*)¤t_size, 1, sizeof(int), fin) != sizeof(int)) + break; + + current_size = ntohl(current_size); + + if(current_size <= 7) // something not right + break; + + if(fread(current_atom, 1, 4, fin) != 4) + break; + + len = current_size - 7; // too short + if(len < 22) + len = 22; + + current_data = (char*)malloc(len); // extra byte + memset(current_data, 0x00, len); + + if(fread(current_data, 1, current_size - 8, fin) != current_size - 8) + break; + + if(!memcmp(current_atom, "\xA9" "nam", 4)) + psong->title = strdup((char*)¤t_data[16]); + else if(!memcmp(current_atom, "\xA9" "ART", 4) || + !memcmp(current_atom, "\xA9" "art", 4)) + psong->contributor[ROLE_ARTIST] = strdup((char*)¤t_data[16]); + else if(!memcmp(current_atom, "\xA9" "alb", 4)) + psong->album = strdup((char*)¤t_data[16]); + else if(!memcmp(current_atom, "\xA9" "cmt", 4)) + psong->comment = strdup((char*)¤t_data[16]); + else if(!memcmp(current_atom, "\xA9" "dir", 4)) + psong->contributor[ROLE_CONDUCTOR] = strdup((char*)¤t_data[16]); + else if(!memcmp(current_atom, "\xA9" "wrt", 4)) + psong->contributor[ROLE_COMPOSER] = strdup((char*)¤t_data[16]); + else if(!memcmp(current_atom, "\xA9" "grp", 4)) + psong->grouping = strdup((char*)¤t_data[16]); + else if(!memcmp(current_atom, "\xA9" "gen", 4)) + psong->genre = strdup((char*)¤t_data[16]); + else if(!memcmp(current_atom, "\xA9" "day", 4)) + psong->year = atoi((char*)¤t_data[16]); + else if(!memcmp(current_atom, "tmpo", 4)) + psong->bpm = (current_data[16] << 8) | current_data[17]; + else if(!memcmp(current_atom, "trkn", 4)) + { + psong->track = (current_data[18] << 8) | current_data[19]; + psong->total_tracks = (current_data[20] << 8) | current_data[21]; + } + else if(!memcmp(current_atom, "disk", 4)) + { + psong->disc = (current_data[18] << 8) | current_data[19]; + psong->total_discs = (current_data[20] << 8) | current_data[21]; + } + else if(!memcmp(current_atom, "gnre", 4)) + { + genre = current_data[17] - 1; + if((genre < 0) || (genre > WINAMP_GENRE_UNKNOWN)) + genre = WINAMP_GENRE_UNKNOWN; + psong->genre = strdup(winamp_genre[genre]); + } + else if(!memcmp(current_atom, "cpil", 4)) + { + psong->compilation = current_data[16]; + } + + free(current_data); + current_offset += current_size; + } + } + + fclose(fin); + + if(atom_offset == -1) + return -1; + + return 0; +} + +// aac_lookforatom +static off_t +_aac_lookforatom(FILE *aac_fp, char *atom_path, unsigned int *atom_length) +{ + long atom_offset; + off_t file_size; + char *cur_p, *end_p; + char atom_name[5]; + + fseek(aac_fp, 0, SEEK_END); + file_size = ftell(aac_fp); + rewind(aac_fp); + + end_p = atom_path; + while(*end_p != '\0') + { + end_p++; + } + atom_name[4] = '\0'; + cur_p = atom_path; + + while(cur_p) + { + if((end_p - cur_p) < 4) + { + return -1; + } + strncpy(atom_name, cur_p, 4); + atom_offset = _aac_findatom(aac_fp, file_size, atom_name, (int*)atom_length); + if(atom_offset == -1) + { + return -1; + } + cur_p = strchr(cur_p, ':'); + if(cur_p != NULL) + { + cur_p++; + + if(!strcmp(atom_name, "meta")) + { + fseek(aac_fp, 4, SEEK_CUR); + } + else if(!strcmp(atom_name, "stsd")) + { + fseek(aac_fp, 8, SEEK_CUR); + } + else if(!strcmp(atom_name, "mp4a")) + { + fseek(aac_fp, 28, SEEK_CUR); + } + } + } + + // return position of 'size:atom' + return ftell(aac_fp) - 8; +} + +// _get_aacfileinfo +int +_get_aacfileinfo(char *file, struct song_metadata *psong) +{ + FILE *infile; + long atom_offset; + int atom_length; + int sample_size; + int samples; + unsigned int bitrate; + off_t file_size; + int ms; + unsigned char buffer[2]; + int time = 0; + aac_object_type_t profile_id = 0; + + psong->vbr_scale = -1; + + if(!(infile = fopen(file, "rb"))) + { + DPRINTF(E_ERROR, L_SCANNER, "Could not open %s for reading\n", file); + return -1; + } + + fseek(infile, 0, SEEK_END); + file_size = ftell(infile); + fseek(infile, 0, SEEK_SET); + + // move to 'mvhd' atom + atom_offset = _aac_lookforatom(infile, "moov:mvhd", (unsigned int*)&atom_length); + if(atom_offset != -1) + { + fseek(infile, 8, SEEK_CUR); + fread((void *)&time, sizeof(int), 1, infile); + time = ntohl(time); + // slimserver prefer to use filesystem time + //psong->time_modified = _mac_to_unix_time(time); + fread((void*)&sample_size, 1, sizeof(int), infile); + fread((void*)&samples, 1, sizeof(int), infile); + + sample_size = ntohl(sample_size); + samples = ntohl(samples); + + // avoid overflowing on large sample_sizes (90000) + ms = 1000; + while((ms > 9) && (!(sample_size % 10))) + { + sample_size /= 10; + ms /= 10; + } + + // unit = ms + psong->song_length = (int)((samples * ms) / sample_size); + } + + psong->bitrate = 0; + + // get samplerate from 'mp4a' (not from 'mdhd') + atom_offset = _aac_lookforatom(infile, "moov:trak:mdia:minf:stbl:stsd:mp4a", (unsigned int*)&atom_length); + if(atom_offset != -1) + { + fseek(infile, atom_offset + 32, SEEK_SET); + + fread(buffer, sizeof(unsigned char), 2, infile); + + psong->samplerate = (buffer[0] << 8) | (buffer[1]); + + fseek(infile, 2, SEEK_CUR); + + // get bitrate fomr 'esds' + atom_offset = _aac_findatom(infile, atom_length - (ftell(infile) - atom_offset), "esds", &atom_length); + + if(atom_offset != -1) + { + fseek(infile, atom_offset + 26, SEEK_CUR); // +22 is max bitrate, +26 is average bitrate + fread((void *)&bitrate, sizeof(unsigned int), 1, infile); + psong->bitrate = ntohl(bitrate); + + fseek(infile, 5, SEEK_CUR); // 5 bytes past bitrate is setup data + fread((void *)&buffer, 2, 1, infile); + profile_id = (buffer[0] >> 3); // first 5 bits of setup data is the Audo Profile ID + /* Frequency index: (((buffer[0] & 0x7) << 1) | (buffer[1] >> 7))) */ + samples = ((buffer[1] >> 3) & 0xF); + psong->channels = (samples == 7 ? 8 : samples); + } + } + + atom_offset = _aac_lookforatom(infile, "mdat", (unsigned int*)&atom_length); + psong->audio_size = atom_length - 8; + psong->audio_offset = atom_offset; + + if(!psong->bitrate) + { + DPRINTF(E_DEBUG, L_SCANNER, "No 'esds' atom. Guess bitrate.\n"); + if((atom_offset != -1) && (psong->song_length)) + { + psong->bitrate = atom_length * 1000 / psong->song_length / 128; + } + } + + //DPRINTF(E_DEBUG, L_METADATA, "Profile ID: %u\n", profile_id); + switch( profile_id ) + { + case AAC_LC: + case AAC_LC_ER: + if( psong->samplerate < 8000 || psong->samplerate > 48000 ) + { + DPRINTF(E_DEBUG, L_METADATA, "Unsupported AAC: sample rate is not 8000 < %d < 48000\n", + psong->samplerate); + break; + } + /* AAC @ Level 1/2 */ + if( psong->channels <= 2 && psong->bitrate <= 320000 ) + asprintf(&(psong->dlna_pn), "AAC_ISO_320"); + else if( psong->channels <= 2 && psong->bitrate <= 576000 ) + asprintf(&(psong->dlna_pn), "AAC_ISO"); + else if( psong->channels <= 6 && psong->bitrate <= 1440000 ) + asprintf(&(psong->dlna_pn), "AAC_MULT5_ISO"); + else + DPRINTF(E_DEBUG, L_METADATA, "Unhandled AAC: %d channels, %d bitrate\n", + psong->channels, psong->bitrate); + break; + default: + DPRINTF(E_DEBUG, L_METADATA, "Unhandled AAC type [%d]\n", profile_id); + break; + } + + fclose(infile); + return 0; +} diff --git a/tagutils/tagutils-aac.h b/tagutils/tagutils-aac.h new file mode 100644 index 0000000..1f6ff2b --- /dev/null +++ b/tagutils/tagutils-aac.h @@ -0,0 +1,27 @@ +//========================================================================= +// FILENAME : tagutils-aac.h +// DESCRIPTION : AAC metadata reader +//========================================================================= +// Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved. +//========================================================================= + +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +static int _get_aactags(char *file, struct song_metadata *psong); +static int _get_aacfileinfo(char *file, struct song_metadata *psong); +static off_t _aac_lookforatom(FILE *aac_fp, char *atom_path, unsigned int *atom_length); +static time_t _mac_to_unix_time(int t) __attribute__((unused)); diff --git a/tagutils/tagutils-asf.c b/tagutils/tagutils-asf.c new file mode 100644 index 0000000..5ec2896 --- /dev/null +++ b/tagutils/tagutils-asf.c @@ -0,0 +1,547 @@ +//========================================================================= +// FILENAME : tagutils-asf.c +// DESCRIPTION : ASF (wma/wmv) metadata reader +//========================================================================= +// Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved. +//========================================================================= + +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +static int +_asf_read_file_properties(FILE *fp, asf_file_properties_t *p, __u32 size) +{ + int len; + + len = sizeof(*p) - offsetof(asf_file_properties_t, FileID); + if(size < len) + return -1; + + memset(p, 0, sizeof(*p)); + p->ID = ASF_FileProperties; + p->Size = size; + + if(len != fread(&p->FileID, 1, len, fp)) + return -1; + + return 0; +} + +static int +_asf_read_audio_stream(FILE *fp, struct song_metadata *psong, int size) +{ + asf_audio_stream_t s; + int len; + + len = sizeof(s) - sizeof(s.Hdr); + if(len > size) + len = size; + + if(len != fread(&s.wfx, 1, len, fp)) + return -1; + + psong->channels = le16_to_cpu(s.wfx.nChannels); + psong->bitrate = le32_to_cpu(s.wfx.nAvgBytesPerSec) * 8; + psong->samplerate = le32_to_cpu(s.wfx.nSamplesPerSec); + /* DLNA Profile Name */ + switch( le16_to_cpu(s.wfx.wFormatTag) ) + { + case WMAV1: + case WMAV2: + if( psong->bitrate < 193000 ) + asprintf(&(psong->dlna_pn), "WMABASE"); + else + asprintf(&(psong->dlna_pn), "WMAFULL"); + break; + case WMAPRO: + asprintf(&(psong->dlna_pn), "WMAPRO"); + break; + default: + break; + } + + return 0; +} + +static int +_asf_read_media_stream(FILE *fp, struct song_metadata *psong, __u32 size) +{ + asf_media_stream_t s; + avi_audio_format_t wfx; + int len; + + len = sizeof(s) - sizeof(s.Hdr); + if(len > size) + len = size; + + if(len != fread(&s.MajorType, 1, len, fp)) + return -1; + + if(IsEqualGUID(&s.MajorType, &ASF_MediaTypeAudio) && + IsEqualGUID(&s.FormatType, &ASF_FormatTypeWave) && s.FormatSize >= sizeof(wfx)) + { + + if(sizeof(wfx) != fread(&wfx, 1, sizeof(wfx), fp)) + return -1; + + psong->channels = le16_to_cpu(wfx.nChannels); + psong->bitrate = le32_to_cpu(wfx.nAvgBytesPerSec) * 8; + psong->samplerate = le32_to_cpu(wfx.nSamplesPerSec); + /* DLNA Profile Name */ + switch( le16_to_cpu(wfx.wFormatTag) ) + { + case WMAV1: + case WMAV2: + printf("***** JM: bitrate: %d\n", psong->bitrate); + if( psong->bitrate < 193000 ) + asprintf(&(psong->dlna_pn), "WMABASE"); + else + asprintf(&(psong->dlna_pn), "WMAFULL"); + break; + case WMAPRO: + asprintf(&(psong->dlna_pn), "WMAPRO"); + break; + default: + break; + } + } + return 0; +} + +static int +_asf_read_stream_object(FILE *fp, struct song_metadata *psong, __u32 size) +{ + asf_stream_object_t s; + int len; + + len = sizeof(s) - sizeof(asf_object_t); + if(size < len) + return -1; + + if(len != fread(&s.StreamType, 1, len, fp)) + return -1; + + if(IsEqualGUID(&s.StreamType, &ASF_AudioStream)) + _asf_read_audio_stream(fp, psong, s.TypeSpecificSize); + else if(IsEqualGUID(&s.StreamType, &ASF_StreamBufferStream)) + _asf_read_media_stream(fp, psong, s.TypeSpecificSize); + else + { + DPRINTF(E_ERROR, L_SCANNER, "Unknown asf stream type.\n"); + } + + return 0; +} + +static int +_asf_read_extended_stream_object(FILE *fp, struct song_metadata *psong, __u32 size) +{ + int i, len; + long off; + asf_object_t tmp; + asf_extended_stream_object_t xs; + asf_stream_name_t nm; + asf_payload_extension_t pe; + + if(size < sizeof(asf_extended_stream_object_t)) + return -1; + + len = sizeof(xs) - offsetof(asf_extended_stream_object_t, StartTime); + if(len != fread(&xs.StartTime, 1, len, fp)) + return -1; + off = sizeof(xs); + + for(i = 0; i < xs.StreamNameCount; i++) + { + if(off + sizeof(nm) > size) + return -1; + if(sizeof(nm) != fread(&nm, 1, sizeof(nm), fp)) + return -1; + off += sizeof(nm); + if(off + nm.Length > sizeof(asf_extended_stream_object_t)) + return -1; + if(nm.Length > 0) + fseek(fp, nm.Length, SEEK_CUR); + off += nm.Length; + } + + for(i = 0; i < xs.PayloadExtensionSystemCount; i++) + { + if(off + sizeof(pe) > size) + return -1; + if(sizeof(pe) != fread(&pe, 1, sizeof(pe), fp)) + return -1; + off += sizeof(pe); + if(pe.InfoLength > 0) + fseek(fp, pe.InfoLength, SEEK_CUR); + off += pe.InfoLength; + } + + if(off < size) + { + if(sizeof(tmp) != fread(&tmp, 1, sizeof(tmp), fp)) + return -1; + if(IsEqualGUID(&tmp.ID, &ASF_StreamHeader)) + _asf_read_stream_object(fp, psong, tmp.Size); + } + + return 0; +} + +static int +_asf_read_header_extension(FILE *fp, struct song_metadata *psong, __u32 size) +{ + off_t pos; + long off; + asf_header_extension_t ext; + asf_object_t tmp; + + if(size < sizeof(asf_header_extension_t)) + return -1; + + fread(&ext.Reserved1, 1, sizeof(ext.Reserved1), fp); + ext.Reserved2 = fget_le16(fp); + ext.DataSize = fget_le32(fp); + + pos = ftell(fp); + off = 0; + while(off < ext.DataSize) + { + if(sizeof(asf_header_extension_t) + off > size) + break; + if(sizeof(tmp) != fread(&tmp, 1, sizeof(tmp), fp)) + break; + if(off + tmp.Size > ext.DataSize) + break; + if(IsEqualGUID(&tmp.ID, &ASF_ExtendedStreamPropertiesObject)) + _asf_read_extended_stream_object(fp, psong, tmp.Size); + + off += tmp.Size; + fseek(fp, pos + off, SEEK_SET); + } + + return 0; +} + +static int +_asf_load_string(FILE *fp, int type, int size, char *buf, int len) +{ + unsigned char data[2048]; + __u16 wc; + int i, j; + + i = 0; + if(size && (size <= sizeof(data)) && (size == fread(data, 1, size, fp))) + { + + switch(type) + { + case ASF_VT_UNICODE: + for(j = 0; j < size; j += 2) + { + wc = *(__s16*)&data[j]; + i += utf16le_to_utf8(&buf[i], len - i, wc); + } + break; + case ASF_VT_BYTEARRAY: + for(i = 0; i < size; i++) + { + if(i + 1 >= len) + break; + buf[i] = data[i]; + } + break; + case ASF_VT_BOOL: + case ASF_VT_DWORD: + if(size >= 4) + i = snprintf(buf, len, "%d", le32_to_cpu(*(__s32*)&data[0])); + break; + case ASF_VT_QWORD: + if(size >= 8) + { +#if __WORDSIZE == 64 + i = snprintf(buf, len, "%ld", le64_to_cpu(*(__s64*)&data[0])); +#else + i = snprintf(buf, len, "%lld", le64_to_cpu(*(__s64*)&data[0])); +#endif + } + break; + case ASF_VT_WORD: + if(size >= 2) + i = snprintf(buf, len, "%d", le16_to_cpu(*(__s16*)&data[0])); + break; + } + + size = 0; + } + else fseek(fp, size, SEEK_CUR); + + buf[i] = 0; + return i; +} + +static void +_asf_load_picture(FILE *fp, int size, void *bm, int *bm_size) +{ + int i; + char buf[256]; + char pic_type; + long pic_size; + + // + // Picture type $xx + // Data length $xx $xx $xx $xx + // MIME type $00 + // Description $00 + // Picture data + + pic_type = fget_byte(fp); size -= 1; + pic_size = fget_le32(fp); size -= 2; + + i = 0; + buf[i] = 0; + if(!strcasecmp(buf, "image/jpeg") || + !strcasecmp(buf, "image/jpg") || + !strcasecmp(buf, "image/peg")) + { + + while(0 != fget_le16(fp)) + size -= 2; + + if(size > 0) + { + if(!(bm = malloc(size))) + { + DPRINTF(E_ERROR, L_SCANNER, "Couldn't allocate %d bytes\n", size); + } + else + { + *bm_size = size; + if(size <= *bm_size) + { + fread(bm, 1, size, fp); + } + else + { + DPRINTF(E_ERROR, L_SCANNER, "Overrun %d bytes required\n", size); + free(bm); + bm = NULL; + } + } + } + else + { + DPRINTF(E_ERROR, L_SCANNER, "No binary data\n"); + size = 0; + bm = NULL; + } + } + else + { + DPRINTF(E_ERROR, L_SCANNER, "Invalid mime type %s\n", buf); + } + + *bm_size = size; +} + +static int +_get_asffileinfo(char *file, struct song_metadata *psong) +{ + FILE *fp; + asf_object_t hdr; + asf_object_t tmp; + unsigned long NumObjects; + unsigned short Reserved; + unsigned short TitleLength; + unsigned short AuthorLength; + unsigned short CopyrightLength; + unsigned short DescriptionLength; + unsigned short RatingLength; + unsigned short NumEntries; + unsigned short NameLength; + unsigned short ValueType; + unsigned short ValueLength; + off_t pos; + char buf[2048]; + int mask; + asf_file_properties_t FileProperties; + + psong->vbr_scale = -1; + + if(!(fp = fopen(file, "rb"))) + { + DPRINTF(E_ERROR, L_SCANNER, "Could not open %s for reading\n", file); + return -1; + } + + if(sizeof(hdr) != fread(&hdr, 1, sizeof(hdr), fp)) + { + DPRINTF(E_ERROR, L_SCANNER, "Error reading %s\n", file); + fclose(fp); + return -1; + } + hdr.Size = le64_to_cpu(hdr.Size); + + if(!IsEqualGUID(&hdr.ID, &ASF_HeaderObject)) + { + DPRINTF(E_ERROR, L_SCANNER, "Not a valid header\n"); + fclose(fp); + return -1; + } + NumObjects = fget_le32(fp); + Reserved = fget_le16(fp); + + pos = ftell(fp); + mask = 0; + while(NumObjects > 0) + { + if(sizeof(tmp) != fread(&tmp, 1, sizeof(tmp), fp)) + break; + tmp.Size = le64_to_cpu(tmp.Size); + + if(pos + tmp.Size > hdr.Size) + { + DPRINTF(E_ERROR, L_SCANNER, "Size overrun reading header object %I64x\n", tmp.Size); + break; + } + + if(IsEqualGUID(&tmp.ID, &ASF_FileProperties)) + { + _asf_read_file_properties(fp, &FileProperties, tmp.Size); + psong->song_length = le64_to_cpu(FileProperties.PlayDuration) / 10000; + psong->bitrate = le64_to_cpu(FileProperties.MaxBitrate); + } + else if(IsEqualGUID(&tmp.ID, &ASF_ContentDescription)) + { + TitleLength = fget_le16(fp); + AuthorLength = fget_le16(fp); + CopyrightLength = fget_le16(fp); + DescriptionLength = fget_le16(fp); + RatingLength = fget_le16(fp); + + if(_asf_load_string(fp, ASF_VT_UNICODE, TitleLength, buf, sizeof(buf))) + { + if(buf[0]) + psong->title = strdup(buf); + } + if(_asf_load_string(fp, ASF_VT_UNICODE, AuthorLength, buf, sizeof(buf))) + { + if(buf[0]) + psong->contributor[ROLE_TRACKARTIST] = strdup(buf); + } + if(CopyrightLength) + fseek(fp, CopyrightLength, SEEK_CUR); + if(DescriptionLength) + fseek(fp, DescriptionLength, SEEK_CUR); + if(RatingLength) + fseek(fp, RatingLength, SEEK_CUR); + } + else if(IsEqualGUID(&tmp.ID, &ASF_ExtendedContentDescription)) + { + NumEntries = fget_le16(fp); + while(NumEntries > 0) + { + NameLength = fget_le16(fp); + _asf_load_string(fp, ASF_VT_UNICODE, NameLength, buf, sizeof(buf)); + ValueType = fget_le16(fp); + ValueLength = fget_le16(fp); + + if(!strcasecmp(buf, "AlbumTitle") || !strcasecmp(buf, "WM/AlbumTitle")) + { + if(_asf_load_string(fp, ValueType, ValueLength, buf, sizeof(buf))) + if(buf[0]) + psong->album = strdup(buf); + } + else if(!strcasecmp(buf, "AlbumArtist") || !strcasecmp(buf, "WM/AlbumArtist")) + { + if(_asf_load_string(fp, ValueType, ValueLength, buf, sizeof(buf))) + { + if(buf[0]) + psong->contributor[ROLE_ALBUMARTIST] = strdup(buf); + } + } + else if(!strcasecmp(buf, "Description") || !strcasecmp(buf, "WM/Track")) + { + if(_asf_load_string(fp, ValueType, ValueLength, buf, sizeof(buf))) + if(buf[0]) + psong->track = atoi(buf); + } + else if(!strcasecmp(buf, "Genre") || !strcasecmp(buf, "WM/Genre")) + { + if(_asf_load_string(fp, ValueType, ValueLength, buf, sizeof(buf))) + if(buf[0]) + psong->genre = strdup(buf); + } + else if(!strcasecmp(buf, "Year") || !strcasecmp(buf, "WM/Year")) + { + if(_asf_load_string(fp, ValueType, ValueLength, buf, sizeof(buf))) + if(buf[0]) + psong->year = atoi(buf); + } + else if(!strcasecmp(buf, "WM/Director")) + { + if(_asf_load_string(fp, ValueType, ValueLength, buf, sizeof(buf))) + if(buf[0]) + psong->contributor[ROLE_CONDUCTOR] = strdup(buf); + } + else if(!strcasecmp(buf, "WM/Picture") && (ValueType == ASF_VT_BYTEARRAY)) + { + _asf_load_picture(fp, ValueLength, psong->image, &psong->image_size); + } + else if(!strcasecmp(buf, "TrackNumber") || !strcasecmp(buf, "WM/TrackNumber")) + { + if(_asf_load_string(fp, ValueType, ValueLength, buf, sizeof(buf))) + if(buf[0]) + psong->track = atoi(buf); + } + else if(!strcasecmp(buf, "isVBR")) + { + fseek(fp, ValueLength, SEEK_CUR); + psong->vbr_scale = 0; + } + else if(ValueLength) + { + fseek(fp, ValueLength, SEEK_CUR); + } + NumEntries--; + } + } + else if(IsEqualGUID(&tmp.ID, &ASF_StreamHeader)) + { + _asf_read_stream_object(fp, psong, tmp.Size); + } + else if(IsEqualGUID(&tmp.ID, &ASF_HeaderExtension)) + { + _asf_read_header_extension(fp, psong, tmp.Size); + } + pos += tmp.Size; + fseek(fp, pos, SEEK_SET); + NumObjects--; + } + +#if 0 + if(sizeof(hdr) == fread(&hdr, 1, sizeof(hdr), fp) && IsEqualGUID(&hdr.ID, &ASF_DataObject)) + { + if(psong->song_length) + { + psong->bitrate = (hdr.Size * 8000) / psong->song_length; + } + } +#endif + + fclose(fp); + return 0; +} diff --git a/tagutils/tagutils-asf.h b/tagutils/tagutils-asf.h new file mode 100644 index 0000000..254a5a3 --- /dev/null +++ b/tagutils/tagutils-asf.h @@ -0,0 +1,352 @@ +//========================================================================= +// FILENAME : tagutils-asf.h +// DESCRIPTION : ASF (wma/wmv) metadata reader +//========================================================================= +// Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved. +//========================================================================= + +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + + +#define __PACKED__ __attribute__((packed)) + +#include + +typedef struct _GUID { + __u32 l; + __u16 w[2]; + __u8 b[8]; +} __PACKED__ GUID; + +#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \ + GUID name = { l, { w1, w2 }, { b1, b2, b3, b4, b5, b6, b7, b8 } } +#define IsEqualGUID(rguid1, rguid2) (!memcmp(rguid1, rguid2, sizeof(GUID))) + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#define SWAP32(l) (l) +#define SWAP16(w) (w) +#else +#define SWAP32(l) ( (((l) >> 24) & 0x000000ff) | (((l) >> 8) & 0x0000ff00) | (((l) << 8) & 0x00ff0000) | (((l) << 24) & 0xff000000) ) +#define SWAP16(w) ( (((w) >> 8) & 0x00ff) | (((w) << 8) & 0xff00) ) +#endif + +DEFINE_GUID(ASF_StreamHeader, SWAP32(0xb7dc0791), SWAP16(0xa9b7), SWAP16(0x11cf), + 0x8e, 0xe6, 0x00, 0xc0, 0x0c, 0x20, 0x53, 0x65); + +DEFINE_GUID(ASF_VideoStream, SWAP32(0xbc19efc0), SWAP16(0x5b4d), SWAP16(0x11cf), + 0xa8, 0xfd, 0x00, 0x80, 0x5f, 0x5c, 0x44, 0x2b); + +DEFINE_GUID(ASF_AudioStream, SWAP32(0xf8699e40), SWAP16(0x5b4d), SWAP16(0x11cf), + 0xa8, 0xfd, 0x00, 0x80, 0x5f, 0x5c, 0x44, 0x2b); + +DEFINE_GUID(ASF_HeaderObject, SWAP32(0x75b22630), SWAP16(0x668e), SWAP16(0x11cf), + 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c); + +DEFINE_GUID(ASF_FileProperties, SWAP32(0x8cabdca1), SWAP16(0xa947), SWAP16(0x11cf), + 0x8e, 0xe4, 0x00, 0xc0, 0x0c, 0x20, 0x53, 0x65); + +DEFINE_GUID(ASF_ContentDescription, SWAP32(0x75b22633), SWAP16(0x668e), SWAP16(0x11cf), + 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c); + +DEFINE_GUID(ASF_ExtendedContentDescription, SWAP32(0xd2d0a440), SWAP16(0xe307), SWAP16(0x11d2), + 0x97, 0xf0, 0x00, 0xa0, 0xc9, 0x5e, 0xa8, 0x50); + +DEFINE_GUID(ASF_ClientGuid, SWAP32(0x8d262e32), SWAP16(0xfc28), SWAP16(0x11d7), + 0xa9, 0xea, 0x00, 0x04, 0x5a, 0x6b, 0x76, 0xc2); + +DEFINE_GUID(ASF_HeaderExtension, SWAP32(0x5fbf03b5), SWAP16(0xa92e), SWAP16(0x11cf), + 0x8e, 0xe3, 0x00, 0xc0, 0x0c, 0x20, 0x53, 0x65); + +DEFINE_GUID(ASF_CodecList, SWAP32(0x86d15240), SWAP16(0x311d), SWAP16(0x11d0), + 0xa3, 0xa4, 0x00, 0xa0, 0xc9, 0x03, 0x48, 0xf6); + +DEFINE_GUID(ASF_DataObject, SWAP32(0x75b22636), SWAP16(0x668e), SWAP16(0x11cf), + 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c); + +DEFINE_GUID(ASF_PaddingObject, SWAP32(0x1806d474), SWAP16(0xcadf), SWAP16(0x4509), + 0xa4, 0xba, 0x9a, 0xab, 0xcb, 0x96, 0xaa, 0xe8); + +DEFINE_GUID(ASF_SimpleIndexObject, SWAP32(0x33000890), SWAP16(0xe5b1), SWAP16(0x11cf), + 0x89, 0xf4, 0x00, 0xa0, 0xc9, 0x03, 0x49, 0xcb); + +DEFINE_GUID(ASF_NoErrorCorrection, SWAP32(0x20fb5700), SWAP16(0x5b55), SWAP16(0x11cf), + 0xa8, 0xfd, 0x00, 0x80, 0x5f, 0x5c, 0x44, 0x2b); + +DEFINE_GUID(ASF_AudioSpread, SWAP32(0xbfc3cd50), SWAP16(0x618f), SWAP16(0x11cf), + 0x8b, 0xb2, 0x00, 0xaa, 0x00, 0xb4, 0xe2, 0x20); + +DEFINE_GUID(ASF_Reserved1, SWAP32(0xabd3d211), SWAP16(0xa9ba), SWAP16(0x11cf), + 0x8e, 0xe6, 0x00, 0xc0, 0x0c, 0x20, 0x53, 0x65); + +DEFINE_GUID(ASF_Reserved2, SWAP32(0x86d15241), SWAP16(0x311d), SWAP16(0x11d0), + 0xa3, 0xa4, 0x00, 0xa0, 0xc9, 0x03, 0x48, 0xf6); + +DEFINE_GUID(ASF_ContentEncryptionObject, SWAP32(0x2211B3FB), SWAP16(0xBD23), SWAP16(0x11D2), + 0xB4, 0xB7, 0x00, 0xA0, 0xC9, 0x55, 0xFC, 0x6E); + +DEFINE_GUID(ASF_ExtendedContentEncryptionObject, SWAP32(0x298AE614), SWAP16(0x2622), SWAP16(0x4C17), + 0xB9, 0x35, 0xDA, 0xE0, 0x7E, 0xE9, 0x28, 0x9C); + +DEFINE_GUID(ASF_ExtendedStreamPropertiesObject, SWAP32(0x14E6A5CB), SWAP16(0xC672), SWAP16(0x4332), + 0x83, 0x99, 0xA9, 0x69, 0x52, 0x06, 0x5B, 0x5A); + +DEFINE_GUID(ASF_MediaTypeAudio, SWAP32(0x31178C9D), SWAP16(0x03E1), SWAP16(0x4528), + 0xB5, 0x82, 0x3D, 0xF9, 0xDB, 0x22, 0xF5, 0x03); + +DEFINE_GUID(ASF_FormatTypeWave, SWAP32(0xC4C4C4D1), SWAP16(0x0049), SWAP16(0x4E2B), + 0x98, 0xFB, 0x95, 0x37, 0xF6, 0xCE, 0x51, 0x6D); + +DEFINE_GUID(ASF_StreamBufferStream, SWAP32(0x3AFB65E2), SWAP16(0x47EF), SWAP16(0x40F2), + 0xAC, 0x2C, 0x70, 0xA9, 0x0D, 0x71, 0xD3, 0x43); + +typedef struct _BITMAPINFOHEADER { + __u32 biSize; + __s32 biWidth; + __s32 biHeight; + __u16 biPlanes; + __u16 biBitCount; + __u32 biCompression; + __u32 biSizeImage; + __s32 biXPelsPerMeter; + __s32 biYPelsPerMeter; + __u32 biClrUsed; + __u32 biClrImportant; +} __PACKED__ BITMAPINFOHEADER; + +typedef struct _WAVEFORMATEX { + __u16 wFormatTag; + __u16 nChannels; + __u32 nSamplesPerSec; + __u32 nAvgBytesPerSec; + __u16 nBlockAlign; + __u16 wBitsPerSample; + __u16 cbSize; +} __PACKED__ WAVEFORMATEX; + +typedef struct _asf_stream_object_t { + GUID ID; + __u64 Size; + GUID StreamType; + GUID ErrorCorrectionType; + __u64 TimeOffset; + __u32 TypeSpecificSize; + __u32 ErrorCorrectionSize; + __u16 StreamNumber; + __u32 Reserved; +} __PACKED__ asf_stream_object_t; + +typedef struct _asf_media_stream_t { + asf_stream_object_t Hdr; + GUID MajorType; + GUID SubType; + __u32 FixedSizeSamples; + __u32 TemporalCompression; + __u32 SampleSize; + GUID FormatType; + __u32 FormatSize; +} __PACKED__ asf_media_stream_t; + +typedef struct _avi_audio_format_t { + __u16 wFormatTag; + __u16 nChannels; + __u32 nSamplesPerSec; + __u32 nAvgBytesPerSec; + __u16 nBlockAlign; + __u16 wBitsPerSample; + __u16 cbSize; +} __PACKED__ avi_audio_format_t; + +typedef struct _asf_extended_stream_object_t { + GUID ID; + __u64 Size; + __u64 StartTime; + __u64 EndTime; + __u32 DataBitrate; + __u32 BufferSize; + __u32 InitialBufferFullness; + __u32 AltDataBitrate; + __u32 AltBufferSize; + __u32 AltInitialBufferFullness; + __u32 MaximumObjectSize; + __u32 Flags; + __u16 StreamNumber; + __u16 LanguageIDIndex; + __u64 AvgTimePerFrame; + __u16 StreamNameCount; + __u16 PayloadExtensionSystemCount; +} __PACKED__ asf_extended_stream_object_t; + +typedef struct _asf_stream_name_t { + __u16 ID; + __u16 Length; +} __PACKED__ asf_stream_name_t; + +typedef struct _asf_payload_extension_t { + GUID ID; + __u16 Size; + __u32 InfoLength; +} __PACKED__ asf_payload_extension_t; + + + +typedef struct _asf_object_t { + GUID ID; + __u64 Size; +} __PACKED__ asf_object_t; + +typedef struct _asf_codec_entry_t { + __u16 Type; + __u16 NameLen; + __u32 Name; + __u16 DescLen; + __u32 Desc; + __u16 InfoLen; + __u32 Info; +} __PACKED__ asf_codec_entry_t; + +typedef struct _asf_codec_list_t { + GUID ID; + __u64 Size; + GUID Reserved; + __u64 NumEntries; + asf_codec_entry_t Entries[2]; + asf_codec_entry_t VideoCodec; +} __PACKED__ asf_codec_list_t; + +typedef struct _asf_content_description_t { + GUID ID; + __u64 Size; + __u16 TitleLength; + __u16 AuthorLength; + __u16 CopyrightLength; + __u16 DescriptionLength; + __u16 RatingLength; + __u32 Title; + __u32 Author; + __u32 Copyright; + __u32 Description; + __u32 Rating; +} __PACKED__ asf_content_description_t; + +typedef struct _asf_file_properties_t { + GUID ID; + __u64 Size; + GUID FileID; + __u64 FileSize; + __u64 CreationTime; + __u64 TotalPackets; + __u64 PlayDuration; + __u64 SendDuration; + __u64 Preroll; + __u32 Flags; + __u32 MinPacketSize; + __u32 MaxPacketSize; + __u32 MaxBitrate; +} __PACKED__ asf_file_properties_t; + +typedef struct _asf_header_extension_t { + GUID ID; + __u64 Size; + GUID Reserved1; + __u16 Reserved2; + __u32 DataSize; +} __PACKED__ asf_header_extension_t; + +typedef struct _asf_video_stream_t { + asf_stream_object_t Hdr; + __u32 Width; + __u32 Height; + __u8 ReservedFlags; + __u16 FormatSize; + BITMAPINFOHEADER bmi; + __u8 ebih[1]; +} __PACKED__ asf_video_stream_t; + +typedef struct _asf_audio_stream_t { + asf_stream_object_t Hdr; + WAVEFORMATEX wfx; +} __PACKED__ asf_audio_stream_t; + +typedef struct _asf_payload_t { + __u8 StreamNumber; + __u8 MediaObjectNumber; + __u32 MediaObjectOffset; + __u8 ReplicatedDataLength; + __u32 ReplicatedData[2]; + __u32 PayloadLength; +} __PACKED__ asf_payload_t; + +typedef struct _asf_packet_t { + __u8 TypeFlags; + __u8 ECFlags; + __u8 ECType; + __u8 ECCycle; + __u8 PropertyFlags; + __u32 PacketLength; + __u32 Sequence; + __u32 PaddingLength; + __u32 SendTime; + __u16 Duration; + __u8 PayloadFlags; + asf_payload_t Payload; +} __PACKED__ asf_packet_t; + +typedef struct _asf_data_object_t { + GUID ID; + __u64 Size; + GUID FileID; + __u64 TotalPackets; + unsigned short Reserved; +} __PACKED__ asf_data_object_t; + +typedef struct _asf_padding_object_t { + GUID ID; + __u64 Size; +} __PACKED__ asf_padding_object_t; + +typedef struct _asf_simple_index_object_t { + GUID ID; + __u64 Size; + GUID FileID; + __u32 IndexEntryTimeInterval; + __u32 MaximumPacketCount; + __u32 IndexEntriesCount; +} __PACKED__ asf_simple_index_object_t; + +typedef struct _asf_header_object_t { + GUID ID; + __u64 Size; + __u32 NumObjects; + __u16 Reserved; + asf_header_extension_t HeaderExtension; + asf_content_description_t ContentDescription; + asf_file_properties_t FileProperties; + asf_video_stream_t * VideoStream; + asf_audio_stream_t * AudioStream; + asf_codec_list_t CodecList; + asf_padding_object_t PaddingObject; +} __PACKED__ asf_header_object_t; + + +#define ASF_VT_UNICODE (0) +#define ASF_VT_BYTEARRAY (1) +#define ASF_VT_BOOL (2) +#define ASF_VT_DWORD (3) +#define ASF_VT_QWORD (4) +#define ASF_VT_WORD (5) + +static int _get_asffileinfo(char *file, struct song_metadata *psong); diff --git a/tagutils/tagutils-flc.c b/tagutils/tagutils-flc.c new file mode 100644 index 0000000..d6b2dff --- /dev/null +++ b/tagutils/tagutils-flc.c @@ -0,0 +1,90 @@ +//========================================================================= +// FILENAME : tagutils-flc.c +// DESCRIPTION : FLAC metadata reader +//========================================================================= +// Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved. +//========================================================================= + +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +static int +_get_flctags(char *filename, struct song_metadata *psong) +{ + FLAC__Metadata_SimpleIterator *iterator = 0; + FLAC__StreamMetadata *block; + int block_number; + int i; + int err = 0; + + if(!(iterator = FLAC__metadata_simple_iterator_new())) + { + DPRINTF(E_FATAL, L_SCANNER, "Out of memory while FLAC__metadata_simple_iterator_new()\n"); + return -1; + } + + block_number = 0; + if(!FLAC__metadata_simple_iterator_init(iterator, filename, true, true)) + { + DPRINTF(E_ERROR, L_SCANNER, "Cannot extract tag from %s\n", filename); + return -1; + } + + do { + if(!(block = FLAC__metadata_simple_iterator_get_block(iterator))) + { + DPRINTF(E_ERROR, L_SCANNER, "Cannot extract tag from %s\n", filename); + err = -1; + goto _exit; + } + + switch(block->type) + { + case FLAC__METADATA_TYPE_STREAMINFO: + psong->samplerate = block->data.stream_info.sample_rate; + psong->channels = block->data.stream_info.channels; + break; + + case FLAC__METADATA_TYPE_VORBIS_COMMENT: + for(i = 0; i < block->data.vorbis_comment.num_comments; i++) + { + vc_scan(psong, + (char*)block->data.vorbis_comment.comments[i].entry, + block->data.vorbis_comment.comments[i].length); + } + break; + default: + break; + } + FLAC__metadata_object_delete(block); + } + while(FLAC__metadata_simple_iterator_next(iterator)); + + _exit: + if(iterator) + FLAC__metadata_simple_iterator_delete(iterator); + + return err; +} + +static int +_get_flcfileinfo(char *filename, struct song_metadata *psong) +{ + psong->lossless = 1; + psong->vbr_scale = 1; + + return 0; +} diff --git a/tagutils/tagutils-flc.h b/tagutils/tagutils-flc.h new file mode 100644 index 0000000..843442c --- /dev/null +++ b/tagutils/tagutils-flc.h @@ -0,0 +1,24 @@ +//========================================================================= +// FILENAME : tagutils-flc.h +// DESCRIPTION : FLAC metadata reader +//========================================================================= +// Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved. +//========================================================================= + +/* This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +static int _get_flcfileinfo(char *file, struct song_metadata *psong); +static int _get_flctags(char *file, struct song_metadata *psong); diff --git a/tagutils/tagutils-misc.c b/tagutils/tagutils-misc.c new file mode 100644 index 0000000..eb91ff6 --- /dev/null +++ b/tagutils/tagutils-misc.c @@ -0,0 +1,262 @@ +//========================================================================= +// FILENAME : tagutils-misc.c +// DESCRIPTION : Misc routines for supporting tagutils +//========================================================================= +// Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved. +//========================================================================= + +/* This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/************************************************************************** +* Language +**************************************************************************/ + +#define MAX_ICONV_BUF 1024 + +typedef enum { + ICONV_OK, + ICONV_TRYNEXT, + ICONV_FATAL +} iconv_result; + +static iconv_result +do_iconv(const char* to_ces, const char* from_ces, + char *inbuf, size_t inbytesleft, + char *outbuf_orig, size_t outbytesleft_orig) +{ + size_t rc; + iconv_result ret = ICONV_OK; + + size_t outbytesleft = outbytesleft_orig - 1; + char* outbuf = outbuf_orig; + + iconv_t cd = iconv_open(to_ces, from_ces); + + if(cd == (iconv_t)-1) + { + return ICONV_FATAL; + } + rc = iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft); + if(rc == (size_t)-1) + { + if(errno == E2BIG) + { + ret = ICONV_FATAL; + } + else + { + ret = ICONV_TRYNEXT; + memset(outbuf_orig, '\0', outbytesleft_orig); + } + } + iconv_close(cd); + + return ret; +} + +#define N_LANG_ALT 8 +struct { + char *lang; + char *cpnames[N_LANG_ALT]; +} iconv_map[] = { + { "JA", { "ISO-8859-1", "CP932", "ISO8859-1", "CP950", "CP936", 0 } }, + { "ZH_CN", { "ISO-8859-1", "CP936", "CP950", "CP932", 0 } }, + { "ZH_TW", { "ISO-8859-1", "CP950", "CP936", "CP932", 0 } }, + { 0, { 0 } } +}; +static int lang_index = -1; + +static int +_lang2cp(char *lang) +{ + int cp; + + if(!lang || lang[0] == '\0') + return -1; + for(cp = 0; iconv_map[cp].lang; cp++) + { + if(!strcasecmp(iconv_map[cp].lang, lang)) + return cp; + } + return -1; +} + +static unsigned char* +_get_utf8_text(const id3_ucs4_t* native_text) +{ + unsigned char *utf8_text = NULL; + char *in, *in8, *iconv_buf; + iconv_result rc; + int i, n; + + in = (char*)id3_ucs4_latin1duplicate(native_text); + if(!in) + { + goto out; + } + + in8 = (char*)id3_ucs4_utf8duplicate(native_text); + if(!in8) + { + free(in); + goto out; + } + + iconv_buf = (char*)calloc(MAX_ICONV_BUF, sizeof(char)); + if(!iconv_buf) + { + free(in); free(in8); + goto out; + } + + i = lang_index; + // (1) try utf8 -> default + rc = do_iconv(iconv_map[i].cpnames[0], "UTF-8", in8, strlen(in8), iconv_buf, MAX_ICONV_BUF); + if(rc == ICONV_OK) + { + utf8_text = (unsigned char*)in8; + free(iconv_buf); + } + else if(rc == ICONV_TRYNEXT) + { + // (2) try default -> utf8 + rc = do_iconv("UTF-8", iconv_map[i].cpnames[0], in, strlen(in), iconv_buf, MAX_ICONV_BUF); + if(rc == ICONV_OK) + { + utf8_text = (unsigned char*)iconv_buf; + } + else if(rc == ICONV_TRYNEXT) + { + // (3) try other encodes + for(n = 1; n < N_LANG_ALT && iconv_map[i].cpnames[n]; n++) + { + rc = do_iconv("UTF-8", iconv_map[i].cpnames[n], in, strlen(in), iconv_buf, MAX_ICONV_BUF); + if(rc == ICONV_OK) + { + utf8_text = (unsigned char*)iconv_buf; + break; + } + } + if(!utf8_text) + { + // cannot iconv + utf8_text = (unsigned char*)id3_ucs4_utf8duplicate(native_text); + free(iconv_buf); + } + } + free(in8); + } + free(in); + + out: + if(!utf8_text) + { + utf8_text = (unsigned char*)strdup("UNKNOWN"); + } + + return utf8_text; +} + + +static void +vc_scan(struct song_metadata *psong, const char *comment, const size_t length) +{ + char strbuf[1024]; + + if(length > (sizeof(strbuf) - 1)) + { + DPRINTF(E_ERROR, L_SCANNER, "Vorbis Comment too long\n"); + return; + } + strncpy(strbuf, comment, length); + strbuf[length] = '\0'; + + // ALBUM, ARTIST, PUBLISHER, COPYRIGHT, DISCNUMBER, ISRC, EAN/UPN, LABEL, LABELNO, + // LICENSE, OPUS, SOURCEMEDIA, TITLE, TRACKNUMBER, VERSION, ENCODED-BY, ENCODING, + // -- foollowing tags are muliples + // COMPOSER, ARRANGER, LYRICIST, AUTHOR, CONDUCTOR, PERFORMER, ENSEMBLE, PART + // PARTNUMBER, GENRE, DATE, LOCATION, COMMENT + if(!strncasecmp(strbuf, "ALBUM=", 6)) + { + psong->album = strdup(strbuf + 6); + } + else if(!strncasecmp(strbuf, "ARTIST=", 7)) + { + psong->contributor[ROLE_ARTIST] = strdup(strbuf + 7); + } + else if(!strncasecmp(strbuf, "ARTISTSORT=", 11)) + { + psong->contributor_sort[ROLE_ARTIST] = strdup(strbuf + 11); + } + else if(!strncasecmp(strbuf, "TITLE=", 6)) + { + psong->title = strdup(strbuf + 6); + } + else if(!strncasecmp(strbuf, "TRACKNUMBER=", 12)) + { + psong->track = atoi(strbuf + 12); + } + else if(!strncasecmp(strbuf, "DISCNUMBER=", 11)) + { + psong->disc = atoi(strbuf + 11); + } + else if(!strncasecmp(strbuf, "GENRE=", 6)) + { + psong->genre = strdup(strbuf + 6); + } + else if(!strncasecmp(strbuf, "DATE=", 5)) + { + if(length >= (5 + 10) && + isdigit(strbuf[5 + 0]) && isdigit(strbuf[5 + 1]) && ispunct(strbuf[5 + 2]) && + isdigit(strbuf[5 + 3]) && isdigit(strbuf[5 + 4]) && ispunct(strbuf[5 + 5]) && + isdigit(strbuf[5 + 6]) && isdigit(strbuf[5 + 7]) && isdigit(strbuf[5 + 8]) && isdigit(strbuf[5 + 9])) + { + // nn-nn-yyyy + strbuf[5 + 10] = '\0'; + psong->year = atoi(strbuf + 5 + 6); + } + else + { + // year first. year is at most 4 digit. + strbuf[5 + 4] = '\0'; + psong->year = atoi(strbuf + 5); + } + } + else if(!strncasecmp(strbuf, "COMMENT=", 8)) + { + psong->comment = strdup(strbuf + 8); + } + else if(!strncasecmp(strbuf, "MUSICBRAINZ_ALBUMID=", 20)) + { + psong->musicbrainz_albumid = strdup(strbuf + 20); + } + else if(!strncasecmp(strbuf, "MUSICBRAINZ_TRACKID=", 20)) + { + psong->musicbrainz_trackid = strdup(strbuf + 20); + } + else if(!strncasecmp(strbuf, "MUSICBRAINZ_TRACKID=", 20)) + { + psong->musicbrainz_trackid = strdup(strbuf + 20); + } + else if(!strncasecmp(strbuf, "MUSICBRAINZ_ARTISTID=", 21)) + { + psong->musicbrainz_artistid = strdup(strbuf + 21); + } + else if(!strncasecmp(strbuf, "MUSICBRAINZ_ALBUMARTISTID=", 26)) + { + psong->musicbrainz_albumartistid = strdup(strbuf + 26); + } +} diff --git a/tagutils/tagutils-mp3.c b/tagutils/tagutils-mp3.c new file mode 100644 index 0000000..f9c7f04 --- /dev/null +++ b/tagutils/tagutils-mp3.c @@ -0,0 +1,776 @@ +//========================================================================= +// FILENAME : tagutils-mp3.c +// DESCRIPTION : MP3 metadata reader +//========================================================================= +// Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved. +//========================================================================= + +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/* + * This file is derived from mt-daap project. + */ + +// _get_mp3tags +static int +_get_mp3tags(char *file, struct song_metadata *psong) +{ + struct id3_file *pid3file; + struct id3_tag *pid3tag; + struct id3_frame *pid3frame; + int err; + int index; + int used; + unsigned char *utf8_text; + int genre = WINAMP_GENRE_UNKNOWN; + int have_utf8; + int have_text; + id3_ucs4_t const *native_text; + char *tmp; + int got_numeric_genre; + id3_byte_t const *image; + id3_length_t image_size = 0; + + pid3file = id3_file_open(file, ID3_FILE_MODE_READONLY); + if(!pid3file) + { + DPRINTF(E_ERROR, L_SCANNER, "Cannot open %s\n", file); + return -1; + } + + pid3tag = id3_file_tag(pid3file); + + if(!pid3tag) + { + err = errno; + id3_file_close(pid3file); + errno = err; + DPRINTF(E_WARN, L_SCANNER, "Cannot get ID3 tag for %s\n", file); + return -1; + } + + index = 0; + while((pid3frame = id3_tag_findframe(pid3tag, "", index))) + { + used = 0; + utf8_text = NULL; + native_text = NULL; + have_utf8 = 0; + have_text = 0; + + if(!strcmp(pid3frame->id, "YTCP")) /* for id3v2.2 */ + { + psong->compilation = 1; + DPRINTF(E_DEBUG, L_SCANNER, "Compilation: %d\n", psong->compilation); + } + else if(!strcmp(pid3frame->id, "APIC") && !image_size) + { + if( (strcmp((char*)id3_field_getlatin1(&pid3frame->fields[1]), "image/jpeg") == 0) || + (strcmp((char*)id3_field_getlatin1(&pid3frame->fields[1]), "jpeg") == 0) ) + { + image = id3_field_getbinarydata(&pid3frame->fields[4], &image_size); + if( image_size ) + { + psong->image = malloc(image_size); + memcpy(psong->image, image, image_size); + psong->image_size = image_size; + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "Found thumbnail: %d\n", psong->image_size); + } + } + } + + if(((pid3frame->id[0] == 'T') || (strcmp(pid3frame->id, "COMM") == 0)) && + (id3_field_getnstrings(&pid3frame->fields[1]))) + have_text = 1; + + if(have_text) + { + native_text = id3_field_getstrings(&pid3frame->fields[1], 0); + + if(native_text) + { + have_utf8 = 1; + if(lang_index >= 0) + utf8_text = _get_utf8_text(native_text); // through iconv + else + utf8_text = (unsigned char*)id3_ucs4_utf8duplicate(native_text); + + if(!strcmp(pid3frame->id, "TIT2")) + { + used = 1; + psong->title = (char*)utf8_text; + } + else if(!strcmp(pid3frame->id, "TPE1")) + { + used = 1; + psong->contributor[ROLE_ARTIST] = (char*)utf8_text; + } + else if(!strcmp(pid3frame->id, "TALB")) + { + used = 1; + psong->album = (char*)utf8_text; + } + else if(!strcmp(pid3frame->id, "TCOM")) + { + used = 1; + psong->contributor[ROLE_COMPOSER] = (char*)utf8_text; + } + else if(!strcmp(pid3frame->id, "TIT1")) + { + used = 1; + psong->grouping = (char*)utf8_text; + } + else if(!strcmp(pid3frame->id, "TPE2")) + { + used = 1; + psong->contributor[ROLE_BAND] = (char*)utf8_text; + } + else if(!strcmp(pid3frame->id, "TPE3")) + { + used = 1; + psong->contributor[ROLE_CONDUCTOR] = (char*)utf8_text; + } + else if(!strcmp(pid3frame->id, "TCON")) + { + used = 1; + psong->genre = (char*)utf8_text; + got_numeric_genre = 0; + if(psong->genre) + { + if(!strlen(psong->genre)) + { + genre = WINAMP_GENRE_UNKNOWN; + got_numeric_genre = 1; + } + else if(isdigit(psong->genre[0])) + { + genre = atoi(psong->genre); + got_numeric_genre = 1; + } + else if((psong->genre[0] == '(') && (isdigit(psong->genre[1]))) + { + genre = atoi((char*)&psong->genre[1]); + got_numeric_genre = 1; + } + + if(got_numeric_genre) + { + if((genre < 0) || (genre > WINAMP_GENRE_UNKNOWN)) + genre = WINAMP_GENRE_UNKNOWN; + free(psong->genre); + psong->genre = strdup(winamp_genre[genre]); + } + } + } + else if(!strcmp(pid3frame->id, "COMM")) + { + used = 1; + psong->comment = (char*)utf8_text; + } + else if(!strcmp(pid3frame->id, "TPOS")) + { + tmp = (char*)utf8_text; + strsep(&tmp, "/"); + if(tmp) + { + psong->total_discs = atoi(tmp); + } + psong->disc = atoi((char*)utf8_text); + } + else if(!strcmp(pid3frame->id, "TRCK")) + { + tmp = (char*)utf8_text; + strsep(&tmp, "/"); + if(tmp) + { + psong->total_tracks = atoi(tmp); + } + psong->track = atoi((char*)utf8_text); + } + else if(!strcmp(pid3frame->id, "TDRC")) + { + psong->year = atoi((char*)utf8_text); + } + else if(!strcmp(pid3frame->id, "TLEN")) + { + psong->song_length = atoi((char*)utf8_text); + } + else if(!strcmp(pid3frame->id, "TBPM")) + { + psong->bpm = atoi((char*)utf8_text); + } + else if(!strcmp(pid3frame->id, "TCMP")) + { + psong->compilation = (char)atoi((char*)utf8_text); + } + } + } + + // check if text tag + if((!used) && (have_utf8) && (utf8_text)) + free(utf8_text); + + // v2 COMM + if((!strcmp(pid3frame->id, "COMM")) && (pid3frame->nfields == 4)) + { + native_text = id3_field_getstring(&pid3frame->fields[2]); + if(native_text) + { + utf8_text = (unsigned char*)id3_ucs4_utf8duplicate(native_text); + if((utf8_text) && (strncasecmp((char*)utf8_text, "iTun", 4) != 0)) + { + // read comment + if(utf8_text) + free(utf8_text); + + native_text = id3_field_getfullstring(&pid3frame->fields[3]); + if(native_text) + { + //if (psong->comment) + // free(psong->comment); + utf8_text = (unsigned char*)id3_ucs4_utf8duplicate(native_text); + if(utf8_text) + { + psong->comment = (char*)utf8_text; + } + } + } + else + { + if(utf8_text) + free(utf8_text); + } + } + } + + index++; + } + + id3_file_close(pid3file); + //DEBUG DPRINTF(E_INFO, L_SCANNER, "Got id3 tag successfully for file=%s\n", file); + return 0; +} + +// _decode_mp3_frame +static int +_decode_mp3_frame(unsigned char *frame, struct mp3_frameinfo *pfi) +{ + int ver; + int layer_index; + int sample_index; + int bitrate_index; + int samplerate_index; + + if((frame[0] != 0xFF) || (frame[1] < 224)) + { + pfi->is_valid = 0; + return -1; + } + + ver = (frame[1] & 0x18) >> 3; + pfi->layer = 4 - ((frame[1] & 0x6) >> 1); + + layer_index = sample_index = -1; + + switch(ver) + { + case 0: + pfi->mpeg_version = 0x25; // 2.5 + sample_index = 2; + if(pfi->layer == 1) + layer_index = 3; + if((pfi->layer == 2) || (pfi->layer == 3)) + layer_index = 4; + break; + case 2: + pfi->mpeg_version = 0x20; // 2.0 + sample_index = 1; + if(pfi->layer == 1) + layer_index = 3; + if((pfi->layer == 2) || (pfi->layer == 3)) + layer_index = 4; + break; + case 3: + pfi->mpeg_version = 0x10; // 1.0 + sample_index = 0; + if(pfi->layer == 1) + layer_index = 0; + if(pfi->layer == 2) + layer_index = 1; + if(pfi->layer == 3) + layer_index = 2; + break; + } + + if((layer_index < 0) || (layer_index > 4)) + { + pfi->is_valid = 0; + return -1; + } + + if((sample_index < 0) || (sample_index >= 2)) + { + pfi->is_valid = 0; + return -1; + } + + if(pfi->layer == 1) pfi->samples_per_frame = 384; + if(pfi->layer == 2) pfi->samples_per_frame = 1152; + if(pfi->layer == 3) + { + if(pfi->mpeg_version == 0x10) + pfi->samples_per_frame = 1152; + else + pfi->samples_per_frame = 576; + } + + bitrate_index = (frame[2] & 0xF0) >> 4; + samplerate_index = (frame[2] & 0x0C) >> 2; + + if((bitrate_index == 0xF) || (bitrate_index == 0x0)) + { + pfi->is_valid = 0; + return -1; + } + + if(samplerate_index == 3) + { + pfi->is_valid = 0; + return -1; + } + + + pfi->bitrate = bitrate_tbl[layer_index][bitrate_index]; + pfi->samplerate = sample_rate_tbl[sample_index][samplerate_index]; + + if((frame[3] & 0xC0 >> 6) == 3) + pfi->stereo = 0; + else + pfi->stereo = 1; + + if(frame[2] & 0x02) + pfi->padding = 1; + else + pfi->padding = 0; + + if(pfi->mpeg_version == 0x10) + { + if(pfi->stereo) + pfi->xing_offset = 32; + else + pfi->xing_offset = 17; + } + else + { + if(pfi->stereo) + pfi->xing_offset = 17; + else + pfi->xing_offset = 9; + } + + pfi->crc_protected = frame[1] & 0xFE; + + if(pfi->layer == 1) + pfi->frame_length = (12 * pfi->bitrate * 1000 / pfi->samplerate + pfi->padding) * 4; + else + pfi->frame_length = 144 * pfi->bitrate * 1000 / pfi->samplerate + pfi->padding; + + if((pfi->frame_length > 2880) || (pfi->frame_length <= 0)) + { + pfi->is_valid = 0; + return -1; + } + + pfi->is_valid = 1; + return 0; +} + +// _mp3_get_average_bitrate +// read from midle of file, and estimate +static void _mp3_get_average_bitrate(FILE *infile, struct mp3_frameinfo *pfi, const char *fname) +{ + off_t file_size; + unsigned char frame_buffer[2900]; + unsigned char header[4]; + int index = 0; + int found = 0; + off_t pos; + struct mp3_frameinfo fi; + int frame_count = 0; + int bitrate_total = 0; + + fseek(infile, 0, SEEK_END); + file_size = ftell(infile); + + pos = file_size >> 1; + + /* now, find the first frame */ + fseek(infile, pos, SEEK_SET); + if(fread(frame_buffer, 1, sizeof(frame_buffer), infile) != sizeof(frame_buffer)) + return; + + while(!found) + { + while((frame_buffer[index] != 0xFF) && (index < (sizeof(frame_buffer) - 4))) + index++; + + if(index >= (sizeof(frame_buffer) - 4)) // max mp3 framesize = 2880 + { + DPRINTF(E_DEBUG, L_SCANNER, "Could not find frame for %s\n", basename((char *)fname)); + return; + } + + if(!_decode_mp3_frame(&frame_buffer[index], &fi)) + { + /* see if next frame is valid */ + fseek(infile, pos + index + fi.frame_length, SEEK_SET); + if(fread(header, 1, sizeof(header), infile) != sizeof(header)) + { + DPRINTF(E_DEBUG, L_SCANNER, "Could not read frame header for %s\n", basename((char *)fname)); + return; + } + + if(!_decode_mp3_frame(header, &fi)) + found = 1; + } + + if(!found) + index++; + } + + pos += index; + + // got first frame + while(frame_count < 10) + { + fseek(infile, pos, SEEK_SET); + if(fread(header, 1, sizeof(header), infile) != sizeof(header)) + { + DPRINTF(E_DEBUG, L_SCANNER, "Could not read frame header for %s\n", basename((char *)fname)); + return; + } + if(_decode_mp3_frame(header, &fi)) + { + DPRINTF(E_DEBUG, L_SCANNER, "Invalid frame header while averaging %s\n", basename((char *)fname)); + return; + } + + bitrate_total += fi.bitrate; + frame_count++; + pos += fi.frame_length; + } + + pfi->bitrate = bitrate_total / frame_count; + + return; +} + +// _mp3_get_frame_count +// do brute scan +static void __attribute__((unused)) +_mp3_get_frame_count(FILE *infile, struct mp3_frameinfo *pfi) +{ + int pos; + int frames = 0; + unsigned char frame_buffer[4]; + struct mp3_frameinfo fi; + off_t file_size; + int err = 0; + int cbr = 1; + int last_bitrate = 0; + + fseek(infile, 0, SEEK_END); + file_size = ftell(infile); + + pos = pfi->frame_offset; + + while(1) + { + err = 1; + + fseek(infile, pos, SEEK_SET); + if(fread(frame_buffer, 1, sizeof(frame_buffer), infile) == sizeof(frame_buffer)) + { + // valid frame? + if(!_decode_mp3_frame(frame_buffer, &fi)) + { + frames++; + pos += fi.frame_length; + err = 0; + + if((last_bitrate) && (fi.bitrate != last_bitrate)) + cbr = 0; + last_bitrate = fi.bitrate; + + // no sense to scan cbr + if(cbr && (frames > 100)) + { + DPRINTF(E_DEBUG, L_SCANNER, "File appears to be CBR... quitting frame _mp3_get_frame_count()\n"); + return; + } + } + } + + if(err) + { + if(pos > (file_size - 4096)) + { + pfi->number_of_frames = frames; + return; + } + else + { + DPRINTF(E_ERROR, L_SCANNER, "Frame count aborted on error. Pos=%d, Count=%d\n", + pos, frames); + return; + } + } + } +} + +// _get_mp3fileinfo +static int +_get_mp3fileinfo(char *file, struct song_metadata *psong) +{ + FILE *infile; + struct id3header *pid3; + struct mp3_frameinfo fi; + unsigned int size = 0; + unsigned int n_read; + off_t fp_size = 0; + off_t file_size; + unsigned char buffer[1024]; + int index; + + int xing_flags; + int found; + + int first_check = 0; + char frame_buffer[4]; + + char id3v1taghdr[4]; + + if(!(infile = fopen(file, "rb"))) + { + DPRINTF(E_ERROR, L_SCANNER, "Could not open %s for reading\n", file); + return -1; + } + + memset((void*)&fi, 0, sizeof(fi)); + + fseek(infile, 0, SEEK_END); + file_size = ftell(infile); + fseek(infile, 0, SEEK_SET); + + if(fread(buffer, 1, sizeof(buffer), infile) != sizeof(buffer)) + { + if(ferror(infile)) + { + DPRINTF(E_ERROR, L_SCANNER, "Error reading: %s\n", strerror(errno)); + } + else + { + DPRINTF(E_ERROR, L_SCANNER, "File too small. Probably corrupted.\n"); + } + fclose(infile); + return -1; + } + + pid3 = (struct id3header*)buffer; + + found = 0; + fp_size = 0; + + if(strncmp((char*)pid3->id, "ID3", 3) == 0) + { + char tagversion[16]; + + /* found an ID3 header... */ + size = (pid3->size[0] << 21 | pid3->size[1] << 14 | + pid3->size[2] << 7 | pid3->size[3]); + fp_size = size + sizeof(struct id3header); + first_check = 1; + + snprintf(tagversion, sizeof(tagversion), "ID3v2.%d.%d", + pid3->version[0], pid3->version[1]); + psong->tagversion = strdup(tagversion); + } + + index = 0; + + /* Here we start the brute-force header seeking. Sure wish there + * weren't so many crappy mp3 files out there + */ + + while(!found) + { + fseek(infile, fp_size, SEEK_SET); + if((n_read = fread(buffer, 1, sizeof(buffer), infile)) < 4) // at least mp3 frame header size (i.e. 4 bytes) + { + fclose(infile); + return 0; + } + + index = 0; + while(!found) + { + while((buffer[index] != 0xFF) && (index < (n_read - 50))) + index++; + + if((first_check) && (index)) + { + fp_size = 0; + first_check = 0; + if(n_read < sizeof(buffer)) + { + fclose(infile); + return 0; + } + break; + } + + if(index > (n_read - 50)) + { + fp_size += index; + if(n_read < sizeof(buffer)) + { + fclose(infile); + return 0; + } + break; + } + + if(!_decode_mp3_frame(&buffer[index], &fi)) + { + if(!strncasecmp((char*)&buffer[index + fi.xing_offset + 4], "XING", 4)) + { + /* no need to check further... if there is a xing header there, + * this is definately a valid frame */ + found = 1; + fp_size += index; + } + else + { + /* No Xing... check for next frame to validate current fram is correct */ + fseek(infile, fp_size + index + fi.frame_length, SEEK_SET); + if(fread(frame_buffer, 1, sizeof(frame_buffer), infile) == sizeof(frame_buffer)) + { + if(!_decode_mp3_frame((unsigned char*)frame_buffer, &fi)) + { + found = 1; + fp_size += index; + } + } + else + { + DPRINTF(E_ERROR, L_SCANNER, "Could not read frame header: %s\n", file); + fclose(infile); + return 0; + } + + if(!found) + { + // cannot find second frame. Song may be too short. So assume first frame is valid. + found = 1; + fp_size += index; + } + } + } + + if(!found) + { + index++; + if(first_check) + { + DPRINTF(E_WARN, L_SCANNER, "Bad header... dropping back for full frame search\n"); + first_check = 0; + fp_size = 0; + break; + } + } + } + } + + fi.frame_offset = fp_size; + + psong->audio_offset = fp_size; + psong->audio_size = file_size - fp_size; + // check if last 128 bytes is ID3v1.0 ID3v1.1 tag + fseek(infile, file_size - 128, SEEK_SET); + if(fread(id3v1taghdr, 1, 4, infile) == 4) + { + if(id3v1taghdr[0] == 'T' && id3v1taghdr[1] == 'A' && id3v1taghdr[2] == 'G') + { + psong->audio_size -= 128; + } + } + + if(_decode_mp3_frame(&buffer[index], &fi)) + { + fclose(infile); + DPRINTF(E_ERROR, L_SCANNER, "Could not find sync frame: %s\n", file); + return 0; + } + + /* now check for an XING header */ + psong->vbr_scale = -1; + if(!strncasecmp((char*)&buffer[index + fi.xing_offset + 4], "XING", 4)) + { + xing_flags = *((int*)&buffer[index + fi.xing_offset + 4 + 4]); + xing_flags = ntohs(xing_flags); + psong->vbr_scale = 78; + + if(xing_flags & 0x1) + { + /* Frames field is valid... */ + fi.number_of_frames = *((int*)&buffer[index + fi.xing_offset + 4 + 8]); + fi.number_of_frames = ntohs(fi.number_of_frames); + } + } + + if((fi.number_of_frames == 0) && (!psong->song_length)) + { + _mp3_get_average_bitrate(infile, &fi, file); + } + + psong->bitrate = fi.bitrate * 1000; + psong->samplerate = fi.samplerate; + + if(!psong->song_length) + { + if(fi.number_of_frames) + { + psong->song_length = (int)((double)(fi.number_of_frames * fi.samples_per_frame * 1000.) / + (double)fi.samplerate); + psong->vbr_scale = 78; + } + else + { + psong->song_length = (int)((double)(file_size - fp_size) * 8. / + (double)fi.bitrate); + } + } + psong->channels = fi.stereo ? 2 : 1; + + fclose(infile); + //DEBUG DPRINTF(E_INFO, L_SCANNER, "Got fileinfo successfully for file=%s song_length=%d\n", file, psong->song_length); + + psong->blockalignment = 1; + asprintf(&(psong->dlna_pn), "MP3"); + + return 0; +} diff --git a/tagutils/tagutils-mp3.h b/tagutils/tagutils-mp3.h new file mode 100644 index 0000000..f6ad9ed --- /dev/null +++ b/tagutils/tagutils-mp3.h @@ -0,0 +1,64 @@ +//========================================================================= +// FILENAME : tagutils-mp3.h +// DESCRIPTION : MP3 metadata reader +//========================================================================= +// Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved. +//========================================================================= + +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + + +struct mp3_frameinfo { + int layer; // 1,2,3 + int bitrate; // unit=kbps + int samplerate; // samp/sec + int stereo; // flag + + int frame_length; // bytes + int crc_protected; // flag + int samples_per_frame; // calculated + int padding; // flag + int xing_offset; // for xing hdr + int number_of_frames; + + int frame_offset; + + short mpeg_version; + short id3_version; + + int is_valid; +}; + +static int _get_mp3tags(char *file, struct song_metadata *psong); +static int _get_mp3fileinfo(char *file, struct song_metadata *psong); +static int _decode_mp3_frame(unsigned char *frame, struct mp3_frameinfo *pfi); + +// bitrate_tbl[layer_index][bitrate_index] +static int bitrate_tbl[5][16] = { + { 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0 }, /* MPEG1, L1 */ + { 0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0 }, /* MPEG1, L2 */ + { 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0 }, /* MPEG1, L3 */ + { 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0 }, /* MPEG2/2.5, L1 */ + { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 } /* MPEG2/2.5, L2/L3 */ +}; + +// sample_rate[sample_index][samplerate_index] +static int sample_rate_tbl[3][4] = { + { 44100, 48000, 32000, 0 }, /* MPEG 1 */ + { 22050, 24000, 16000, 0 }, /* MPEG 2 */ + { 11025, 12000, 8000, 0 } /* MPEG 2.5 */ +}; diff --git a/tagutils/tagutils-ogg.c b/tagutils/tagutils-ogg.c new file mode 100644 index 0000000..b978b72 --- /dev/null +++ b/tagutils/tagutils-ogg.c @@ -0,0 +1,542 @@ +//========================================================================= +// FILENAME : tagutils-ogg.c +// DESCRIPTION : Ogg metadata reader +//========================================================================= +// Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved. +//========================================================================= + +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/* + * This file is derived from mt-daap project. + */ + +typedef struct _ogg_stream_processor { + void (*process_page)(struct _ogg_stream_processor *, ogg_page *, struct song_metadata *); + void (*process_end)(struct _ogg_stream_processor *, struct song_metadata *); + int isillegal; + int constraint_violated; + int shownillegal; + int isnew; + long seqno; + int lostseq; + + int start; + int end; + + int num; + char *type; + + ogg_uint32_t serial; + ogg_stream_state os; + void *data; +} ogg_stream_processor; + +typedef struct { + ogg_stream_processor *streams; + int allocated; + int used; + + int in_headers; +} ogg_stream_set; + +typedef struct { + vorbis_info vi; + vorbis_comment vc; + + ogg_int64_t bytes; + ogg_int64_t lastgranulepos; + ogg_int64_t firstgranulepos; + + int doneheaders; +} ogg_misc_vorbis_info; + +#define CONSTRAINT_PAGE_AFTER_EOS 1 +#define CONSTRAINT_MUXING_VIOLATED 2 + +static ogg_stream_set * +_ogg_create_stream_set(void) +{ + ogg_stream_set *set = calloc(1, sizeof(ogg_stream_set)); + + set->streams = calloc(5, sizeof(ogg_stream_processor)); + set->allocated = 5; + set->used = 0; + + return set; +} + +static void +_ogg_vorbis_process(ogg_stream_processor *stream, ogg_page *page, + struct song_metadata *psong) +{ + ogg_packet packet; + ogg_misc_vorbis_info *inf = stream->data; + int i, header = 0; + + ogg_stream_pagein(&stream->os, page); + if(inf->doneheaders < 3) + header = 1; + + while(ogg_stream_packetout(&stream->os, &packet) > 0) + { + if(inf->doneheaders < 3) + { + if(vorbis_synthesis_headerin(&inf->vi, &inf->vc, &packet) < 0) + { + DPRINTF(E_WARN, L_SCANNER, "Could not decode vorbis header " + "packet - invalid vorbis stream (%d)\n", stream->num); + continue; + } + inf->doneheaders++; + if(inf->doneheaders == 3) + { + if(ogg_page_granulepos(page) != 0 || ogg_stream_packetpeek(&stream->os, NULL) == 1) + DPRINTF(E_WARN, L_SCANNER, "No header in vorbis stream %d\n", stream->num); + DPRINTF(E_DEBUG, L_SCANNER, "Vorbis headers parsed for stream %d, " + "information follows...\n", stream->num); + DPRINTF(E_DEBUG, L_SCANNER, "Channels: %d\n", inf->vi.channels); + DPRINTF(E_DEBUG, L_SCANNER, "Rate: %ld\n\n", inf->vi.rate); + + psong->samplerate = inf->vi.rate; + psong->channels = inf->vi.channels; + + if(inf->vi.bitrate_nominal > 0) + { + DPRINTF(E_DEBUG, L_SCANNER, "Nominal bitrate: %f kb/s\n", + (double)inf->vi.bitrate_nominal / 1000.0); + psong->bitrate = inf->vi.bitrate_nominal / 1000; + } + else + { + int upper_rate, lower_rate; + + DPRINTF(E_DEBUG, L_SCANNER, "Nominal bitrate not set\n"); + + // + upper_rate = 0; + lower_rate = 0; + + if(inf->vi.bitrate_upper > 0) + { + DPRINTF(E_DEBUG, L_SCANNER, "Upper bitrate: %f kb/s\n", + (double)inf->vi.bitrate_upper / 1000.0); + upper_rate = inf->vi.bitrate_upper; + } + else + { + DPRINTF(E_DEBUG, L_SCANNER, "Upper bitrate not set\n"); + } + + if(inf->vi.bitrate_lower > 0) + { + DPRINTF(E_DEBUG, L_SCANNER, "Lower bitrate: %f kb/s\n", + (double)inf->vi.bitrate_lower / 1000.0); + lower_rate = inf->vi.bitrate_lower;; + } + else + { + DPRINTF(E_DEBUG, L_SCANNER, "Lower bitrate not set\n"); + } + + if(upper_rate && lower_rate) + { + psong->bitrate = (upper_rate + lower_rate) / 2; + } + else + { + psong->bitrate = upper_rate + lower_rate; + } + } + + if(inf->vc.comments > 0) + DPRINTF(E_DEBUG, L_SCANNER, + "User comments section follows...\n"); + + for(i = 0; i < inf->vc.comments; i++) + { + vc_scan(psong, inf->vc.user_comments[i], inf->vc.comment_lengths[i]); + } + } + } + } + + if(!header) + { + ogg_int64_t gp = ogg_page_granulepos(page); + if(gp > 0) + { + if(gp < inf->lastgranulepos) + DPRINTF(E_WARN, L_SCANNER, "granulepos in stream %d decreases from %lld to %lld", + stream->num, inf->lastgranulepos, gp); + inf->lastgranulepos = gp; + } + else + { + DPRINTF(E_WARN, L_SCANNER, "Malformed vorbis strem.\n"); + } + inf->bytes += page->header_len + page->body_len; + } +} + +static void +_ogg_vorbis_end(ogg_stream_processor *stream, struct song_metadata *psong) +{ + ogg_misc_vorbis_info *inf = stream->data; + long minutes, seconds; + double bitrate, time; + + time = (double)inf->lastgranulepos / inf->vi.rate; + bitrate = inf->bytes * 8 / time / 1000; + + if(psong != NULL) + { + if(psong->bitrate <= 0) + { + psong->bitrate = bitrate * 1000; + } + psong->song_length = time * 1000; + } + + minutes = (long)time / 60; + seconds = (long)time - minutes * 60; + + vorbis_comment_clear(&inf->vc); + vorbis_info_clear(&inf->vi); + + free(stream->data); +} + +static void +_ogg_process_null(ogg_stream_processor *stream, ogg_page *page, struct song_metadata *psong) +{ + // invalid stream +} + +static void +_ogg_process_other(ogg_stream_processor *stream, ogg_page *page, struct song_metadata *psong) +{ + ogg_stream_pagein(&stream->os, page); +} + +static void +_ogg_free_stream_set(ogg_stream_set *set) +{ + int i; + + for(i = 0; i < set->used; i++) + { + if(!set->streams[i].end) + { + // no EOS + if(set->streams[i].process_end) + set->streams[i].process_end(&set->streams[i], NULL); + } + ogg_stream_clear(&set->streams[i].os); + } + + free(set->streams); + free(set); +} + +static int +_ogg_streams_open(ogg_stream_set *set) +{ + int i; + int res = 0; + + for(i = 0; i < set->used; i++) + { + if(!set->streams[i].end) + res++; + } + + return res; +} + +static void +_ogg_null_start(ogg_stream_processor *stream) +{ + stream->process_end = NULL; + stream->type = "invalid"; + stream->process_page = _ogg_process_null; +} + +static void +_ogg_other_start(ogg_stream_processor *stream, char *type) +{ + if(type) + stream->type = type; + else + stream->type = "unknown"; + stream->process_page = _ogg_process_other; + stream->process_end = NULL; +} + +static void +_ogg_vorbis_start(ogg_stream_processor *stream) +{ + ogg_misc_vorbis_info *info; + + stream->type = "vorbis"; + stream->process_page = _ogg_vorbis_process; + stream->process_end = _ogg_vorbis_end; + + stream->data = calloc(1, sizeof(ogg_misc_vorbis_info)); + + info = stream->data; + + vorbis_comment_init(&info->vc); + vorbis_info_init(&info->vi); +} + +static ogg_stream_processor * +_ogg_find_stream_processor(ogg_stream_set *set, ogg_page *page) +{ + ogg_uint32_t serial = ogg_page_serialno(page); + int i, found = 0; + int invalid = 0; + int constraint = 0; + ogg_stream_processor *stream; + + for(i = 0; i < set->used; i++) + { + if(serial == set->streams[i].serial) + { + found = 1; + stream = &(set->streams[i]); + + set->in_headers = 0; + + if(stream->end) + { + stream->isillegal = 1; + stream->constraint_violated = CONSTRAINT_PAGE_AFTER_EOS; + return stream; + } + + stream->isnew = 0; + stream->start = ogg_page_bos(page); + stream->end = ogg_page_eos(page); + stream->serial = serial; + return stream; + } + } + if(_ogg_streams_open(set) && !set->in_headers) + { + constraint = CONSTRAINT_MUXING_VIOLATED; + invalid = 1; + } + + set->in_headers = 1; + + if(set->allocated < set->used) + stream = &set->streams[set->used]; + else + { + set->allocated += 5; + set->streams = realloc(set->streams, sizeof(ogg_stream_processor) * set->allocated); + stream = &set->streams[set->used]; + } + set->used++; + stream->num = set->used; // count from 1 + + stream->isnew = 1; + stream->isillegal = invalid; + stream->constraint_violated = constraint; + + { + int res; + ogg_packet packet; + + ogg_stream_init(&stream->os, serial); + ogg_stream_pagein(&stream->os, page); + res = ogg_stream_packetout(&stream->os, &packet); + if(res <= 0) + { + DPRINTF(E_WARN, L_SCANNER, "Invalid header page, no packet found\n"); + _ogg_null_start(stream); + } + else if(packet.bytes >= 7 && memcmp(packet.packet, "\001vorbis", 7) == 0) + _ogg_vorbis_start(stream); + else if(packet.bytes >= 8 && memcmp(packet.packet, "OggMIDI\0", 8) == 0) + _ogg_other_start(stream, "MIDI"); + else + _ogg_other_start(stream, NULL); + + res = ogg_stream_packetout(&stream->os, &packet); + if(res > 0) + { + DPRINTF(E_WARN, L_SCANNER, "Invalid header page in stream %d, " + "contains multiple packets\n", stream->num); + } + + /* re-init, ready for processing */ + ogg_stream_clear(&stream->os); + ogg_stream_init(&stream->os, serial); + } + + stream->start = ogg_page_bos(page); + stream->end = ogg_page_eos(page); + stream->serial = serial; + + return stream; +} + +static int +_ogg_get_next_page(FILE *f, ogg_sync_state *sync, ogg_page *page, + ogg_int64_t *written) +{ + int ret; + char *buffer; + int bytes; + + while((ret = ogg_sync_pageout(sync, page)) <= 0) + { + if(ret < 0) + DPRINTF(E_WARN, L_SCANNER, "Hole in data found at approximate offset %lld bytes. Corrupted ogg.\n", *written); + + buffer = ogg_sync_buffer(sync, 4500); // chunk=4500 + bytes = fread(buffer, 1, 4500, f); + if(bytes <= 0) + { + ogg_sync_wrote(sync, 0); + return 0; + } + ogg_sync_wrote(sync, bytes); + *written += bytes; + } + + return 1; +} + + +static int +_get_oggfileinfo(char *filename, struct song_metadata *psong) +{ + FILE *file = fopen(filename, "rb"); + ogg_sync_state sync; + ogg_page page; + ogg_stream_set *processors = _ogg_create_stream_set(); + int gotpage = 0; + ogg_int64_t written = 0; + + if(!file) + { + DPRINTF(E_FATAL, L_SCANNER, + "Error opening input file \"%s\": %s\n", filename, strerror(errno)); + _ogg_free_stream_set(processors); + return -1; + } + + DPRINTF(E_INFO, L_SCANNER, "Processing file \"%s\"...\n\n", filename); + + ogg_sync_init(&sync); + + while(_ogg_get_next_page(file, &sync, &page, &written)) + { + ogg_stream_processor *p = _ogg_find_stream_processor(processors, &page); + gotpage = 1; + + if(!p) + { + DPRINTF(E_FATAL, L_SCANNER, "Could not find a processor for stream, bailing\n"); + _ogg_free_stream_set(processors); + return -1; + } + + if(p->isillegal && !p->shownillegal) + { + char *constraint; + switch(p->constraint_violated) + { + case CONSTRAINT_PAGE_AFTER_EOS: + constraint = "Page found for stream after EOS flag"; + break; + case CONSTRAINT_MUXING_VIOLATED: + constraint = "Ogg muxing constraints violated, new " + "stream before EOS of all previous streams"; + break; + default: + constraint = "Error unknown."; + } + + DPRINTF(E_WARN, L_SCANNER, + "Warning: illegally placed page(s) for logical stream %d\n" + "This indicates a corrupt ogg file: %s.\n", + p->num, constraint); + p->shownillegal = 1; + + if(!p->isnew) + continue; + } + + if(p->isnew) + { + DPRINTF(E_DEBUG, L_SCANNER, "New logical stream (#%d, serial: %08x): type %s\n", + p->num, p->serial, p->type); + if(!p->start) + DPRINTF(E_WARN, L_SCANNER, + "stream start flag not set on stream %d\n", + p->num); + } + else if(p->start) + DPRINTF(E_WARN, L_SCANNER, "stream start flag found in mid-stream " + "on stream %d\n", p->num); + + if(p->seqno++ != ogg_page_pageno(&page)) + { + if(!p->lostseq) + DPRINTF(E_WARN, L_SCANNER, + "sequence number gap in stream %d. Got page %ld " + "when expecting page %ld. Indicates missing data.\n", + p->num, ogg_page_pageno(&page), p->seqno - 1); + p->seqno = ogg_page_pageno(&page); + p->lostseq = 1; + } + else + p->lostseq = 0; + + if(!p->isillegal) + { + p->process_page(p, &page, psong); + + if(p->end) + { + if(p->process_end) + p->process_end(p, psong); + DPRINTF(E_DEBUG, L_SCANNER, "Logical stream %d ended\n", p->num); + p->isillegal = 1; + p->constraint_violated = CONSTRAINT_PAGE_AFTER_EOS; + } + } + } + + _ogg_free_stream_set(processors); + + ogg_sync_clear(&sync); + + fclose(file); + + if(!gotpage) + { + DPRINTF(E_ERROR, L_SCANNER, "No ogg data found in file \"%s\".\n", filename); + return -1; + } + + return 0; +} diff --git a/tagutils/tagutils-ogg.h b/tagutils/tagutils-ogg.h new file mode 100644 index 0000000..2c78a09 --- /dev/null +++ b/tagutils/tagutils-ogg.h @@ -0,0 +1,24 @@ +//========================================================================= +// FILENAME : tagutils-ogg.h +// DESCRIPTION : Ogg metadata reader +//========================================================================= +// Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved. +//========================================================================= + +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +static int _get_oggfileinfo(char *filename, struct song_metadata *psong); diff --git a/tagutils/tagutils-plist.c b/tagutils/tagutils-plist.c new file mode 100644 index 0000000..1c4f1cf --- /dev/null +++ b/tagutils/tagutils-plist.c @@ -0,0 +1,140 @@ +//========================================================================= +// FILENAME : playlist.c +// DESCRIPTION : Playlist +//========================================================================= +// Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved. +//========================================================================= + +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include +#include +#include + +#include "misc.h" +#include "tagutils.h" +#include "textutils.h" +#include "log.h" + + +#define MAX_BUF 1024 + +static FILE *fp = 0; +static int _utf8bom = 0; + +static int (*_next_track)(struct song_metadata*, struct stat*, char*, char*); +static int _m3u_next_track(struct song_metadata*, struct stat*, char*, char*); + +int +start_plist(char *path, struct song_metadata *psong, struct stat *stat, char *lang, char *type) +{ + char *fname, *suffix; + + _next_track = 0; + _utf8bom = 0; + + if(!strcmp(type, "m3u")) + _next_track = _m3u_next_track; + + if(!_next_track) + { + DPRINTF(E_ERROR, L_SCAN_SCANNER, "Non supported type of playlist <%s>\n", type); + return -1; + } + + if(!(fp = fopen(path, "rb"))) + { + DPRINTF(E_ERROR, L_SCAN_SCANNER, "Cannot open %s\n", path); + return -1; + } + + // + memset((void*)psong, 0, sizeof(struct song_metadata)); + psong->is_plist = 1; + psong->path = strdup(path); + psong->type = type; + + fname = strrchr(psong->path, '/'); + psong->basename = fname ? fname + 1 : psong->path; + + psong->title = strdup(psong->basename); + suffix = strrchr(psong->title, '.'); + if(suffix) *suffix = '\0'; + + if(stat) + { + if(!psong->time_modified) + psong->time_modified = stat->st_mtime; + psong->file_size = stat->st_size; + } + + return 0; +} + +int +_m3u_next_track(struct song_metadata *psong, struct stat *stat, char *lang, char *type) +{ + char buf[MAX_BUF], *p; + int len; + + // + memset((void*)psong, 0, sizeof(struct song_metadata)); + + // read first line, check BOM + p = fgets(buf, MAX_BUF, fp); + if(!p) + { + fclose(fp); + return 1; + } + + if(!_utf8bom && p[0] == '\xef' && p[1] == '\xbb' && p[2] == '\xbf') + { + _utf8bom = 1; + p += 3; + } + + while(p) + { + while(isspace(*p)) p++; + if(*p && *p != '#') + { + // check dos format + len = strlen((char*)p); + + while(p[len - 1] == '\r' || p[len - 1] == '\n') + { + p[--len] = '\0'; + } + psong->path = strdup(p); + return 0; + } + p = fgets(buf, MAX_BUF, fp); + continue; + } + + fclose(fp); + return -1; +} + +int +next_plist_track(struct song_metadata *psong, struct stat *stat, char *lang, char *type) +{ + if(_next_track) + return _next_track(psong, stat, lang, type); + return -1; +} diff --git a/tagutils/tagutils.c b/tagutils/tagutils.c new file mode 100644 index 0000000..e5783b9 --- /dev/null +++ b/tagutils/tagutils.c @@ -0,0 +1,289 @@ +//========================================================================= +// FILENAME : tagutils.c +// DESCRIPTION : MP3/MP4/Ogg/FLAC metadata reader +//========================================================================= +// Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved. +//========================================================================= + +/* This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/* This file is derived from mt-daapd project */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include "tagutils.h" +#include "misc.h" +#include "textutils.h" +#include "../metadata.h" +#include "../log.h" + +struct id3header { + unsigned char id[3]; + unsigned char version[2]; + unsigned char flags; + unsigned char size[4]; +} __attribute((packed)); + +char *winamp_genre[] = { + /*00*/ "Blues", "Classic Rock", "Country", "Dance", + "Disco", "Funk", "Grunge", "Hip-Hop", + /*08*/ "Jazz", "Metal", "New Age", "Oldies", + "Other", "Pop", "R&B", "Rap", + /*10*/ "Reggae", "Rock", "Techno", "Industrial", + "Alternative", "Ska", "Death Metal", "Pranks", + /*18*/ "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", + "Vocal", "Jazz+Funk", "Fusion", "Trance", + /*20*/ "Classical", "Instrumental", "Acid", "House", + "Game", "Sound Clip", "Gospel", "Noise", + /*28*/ "AlternRock", "Bass", "Soul", "Punk", + "Space", "Meditative", "Instrumental Pop", "Instrumental Rock", + /*30*/ "Ethnic", "Gothic", "Darkwave", "Techno-Industrial", + "Electronic", "Pop-Folk", "Eurodance", "Dream", + /*38*/ "Southern Rock", "Comedy", "Cult", "Gangsta", + "Top 40", "Christian Rap", "Pop/Funk", "Jungle", + /*40*/ "Native American", "Cabaret", "New Wave", "Psychadelic", + "Rave", "Showtunes", "Trailer", "Lo-Fi", + /*48*/ "Tribal", "Acid Punk", "Acid Jazz", "Polka", + "Retro", "Musical", "Rock & Roll", "Hard Rock", + /*50*/ "Folk", "Folk/Rock", "National folk", "Swing", + "Fast-fusion", "Bebob", "Latin", "Revival", + /*58*/ "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", + "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock", + /*60*/ "Big Band", "Chorus", "Easy Listening", "Acoustic", + "Humour", "Speech", "Chanson", "Opera", + /*68*/ "Chamber Music", "Sonata", "Symphony", "Booty Bass", + "Primus", "Porn Groove", "Satire", "Slow Jam", + /*70*/ "Club", "Tango", "Samba", "Folklore", + "Ballad", "Powder Ballad", "Rhythmic Soul", "Freestyle", + /*78*/ "Duet", "Punk Rock", "Drum Solo", "A Capella", + "Euro-House", "Dance Hall", "Goa", "Drum & Bass", + /*80*/ "Club House", "Hardcore", "Terror", "Indie", + "BritPop", "NegerPunk", "Polsk Punk", "Beat", + /*88*/ "Christian Gangsta", "Heavy Metal", "Black Metal", "Crossover", + "Contemporary C", "Christian Rock", "Merengue", "Salsa", + /*90*/ "Thrash Metal", "Anime", "JPop", "SynthPop", + "Unknown" +}; + +#define WINAMP_GENRE_UNKNOWN ((sizeof(winamp_genre) / sizeof(winamp_genre[0])) - 1) + + +/* + * Prototype + */ +#include "tagutils-mp3.h" +#include "tagutils-aac.h" +#include "tagutils-ogg.h" +#include "tagutils-flc.h" +#include "tagutils-asf.h" + +static int _get_tags(char *file, struct song_metadata *psong); +static int _get_fileinfo(char *file, struct song_metadata *psong); + + +/* + * Typedefs + */ + +typedef struct { + char* type; + int (*get_tags)(char* file, struct song_metadata* psong); + int (*get_fileinfo)(char* file, struct song_metadata* psong); +} taghandler; + +static taghandler taghandlers[] = { + { "aac", _get_aactags, _get_aacfileinfo }, + { "mp3", _get_mp3tags, _get_mp3fileinfo }, + { "flc", _get_flctags, _get_flcfileinfo }, + { "ogg", 0, _get_oggfileinfo }, + { "asf", 0, _get_asffileinfo }, + { NULL, 0 } +}; + + + +//********************************************************************************* +#include "tagutils-misc.c" +#include "tagutils-mp3.c" +#include "tagutils-aac.c" +#include "tagutils-ogg.c" +#include "tagutils-flc.c" +#include "tagutils-asf.c" + +//********************************************************************************* +// freetags() +#define MAYBEFREE(a) { if((a)) free((a)); }; +void +freetags(struct song_metadata *psong) +{ + int role; + + MAYBEFREE(psong->path); + MAYBEFREE(psong->image); + MAYBEFREE(psong->title); + MAYBEFREE(psong->album); + MAYBEFREE(psong->genre); + MAYBEFREE(psong->comment); + for(role = ROLE_START; role <= ROLE_LAST; role++) + { + MAYBEFREE(psong->contributor[role]); + MAYBEFREE(psong->contributor_sort[role]); + } + MAYBEFREE(psong->grouping); + MAYBEFREE(psong->dlna_pn); + MAYBEFREE(psong->tagversion); + MAYBEFREE(psong->musicbrainz_albumid); + MAYBEFREE(psong->musicbrainz_trackid); + MAYBEFREE(psong->musicbrainz_artistid); + MAYBEFREE(psong->musicbrainz_albumartistid); +} + +// _get_fileinfo +static int +_get_fileinfo(char *file, struct song_metadata *psong) +{ + taghandler *hdl; + + // dispatch to appropriate tag handler + for(hdl = taghandlers; hdl->type; ++hdl) + if(!strcmp(hdl->type, psong->type)) + break; + + if(hdl->get_fileinfo) + return hdl->get_fileinfo(file, psong); + + return 0; +} + + +static void +_make_composite_tags(struct song_metadata *psong) +{ + int len; + + len = 0; + + if(!psong->contributor[ROLE_ARTIST] && + (psong->contributor[ROLE_BAND] || psong->contributor[ROLE_CONDUCTOR])) + { + if(psong->contributor[ROLE_BAND]) + len += strlen(psong->contributor[ROLE_BAND]); + if(psong->contributor[ROLE_CONDUCTOR]) + len += strlen(psong->contributor[ROLE_CONDUCTOR]); + + len += 3; + + psong->contributor[ROLE_ARTIST] = (char*)calloc(len, 1); + if(psong->contributor[ROLE_ARTIST]) + { + if(psong->contributor[ROLE_BAND]) + strcat(psong->contributor[ROLE_ARTIST], psong->contributor[ROLE_BAND]); + + if(psong->contributor[ROLE_BAND] && psong->contributor[ROLE_CONDUCTOR]) + strcat(psong->contributor[ROLE_ARTIST], " - "); + + if(psong->contributor[ROLE_CONDUCTOR]) + strcat(psong->contributor[ROLE_ARTIST], psong->contributor[ROLE_CONDUCTOR]); + } + } + + + if(!psong->title) + { + char *suffix; + psong->title = strdup(psong->basename); + suffix = strrchr(psong->title, '.'); + if(suffix) *suffix = '\0'; + } +} + + +/*****************************************************************************/ +// _get_tags +static int +_get_tags(char *file, struct song_metadata *psong) +{ + taghandler *hdl; + + // dispatch + for(hdl = taghandlers ; hdl->type ; ++hdl) + if(!strcasecmp(hdl->type, psong->type)) + break; + + if(hdl->get_tags) + { + return hdl->get_tags(file, psong); + } + + return 0; +} + +/*****************************************************************************/ +// readtags +int +readtags(char *path, struct song_metadata *psong, struct stat *stat, char *lang, char *type) +{ + char *fname; + int found = 0; + + if(lang_index == -1) + lang_index = _lang2cp(lang); + + memset((void*)psong, 0, sizeof(struct song_metadata)); + psong->path = strdup(path); + psong->type = type; + + fname = strrchr(psong->path, '/'); + psong->basename = fname ? fname + 1 : psong->path; + + // get tag + found |= _get_tags(path, psong); + + if(stat) + { + if(!psong->time_modified) + psong->time_modified = stat->st_mtime; + psong->file_size = stat->st_size; + } + + // get fileinfo + found |= _get_fileinfo(path, psong); + + // + if(!found) + { + _make_composite_tags(psong); + return 0; + } + else + { + return -1; + } +} diff --git a/tagutils/tagutils.h b/tagutils/tagutils.h new file mode 100644 index 0000000..7307601 --- /dev/null +++ b/tagutils/tagutils.h @@ -0,0 +1,120 @@ +//========================================================================= +// FILENAME : taguilts.h +// DESCRIPTION : Header for tagutils.c +//========================================================================= +// Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved. +//========================================================================= + +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/* + * This file is derived from mt-daap project. + */ + +#ifndef _TAG_H_ +#define _TAG_H_ + +#include +#include +#include + +#define ROLE_NOUSE 0 +#define ROLE_START 1 +#define ROLE_ARTIST 1 +#define ROLE_COMPOSER 2 +#define ROLE_CONDUCTOR 3 +#define ROLE_BAND 4 +#define ROLE_ALBUMARTIST 5 +#define ROLE_TRACKARTIST 6 +#define ROLE_LAST 6 +#define N_ROLE 7 + +struct song_metadata { + int file_size; + char *dirpath; + char *path; + char *basename; // basename is part of path + char *type; + int time_modified; + + char *image; // coverart + int image_size; + + char *title; // TIT2 + char *album; // TALB + char *genre; // TCON + char *comment; // COMM + + char *contributor[N_ROLE]; // TPE1 (artist) + // TCOM (composer) + // TPE3 (conductor) + // TPE2 (orchestra) + char *contributor_sort[N_ROLE]; + + + char *grouping; // TIT1 + int year; // TDRC + int track; // TRCK + int total_tracks; // TRCK + int disc; // TPOS + int total_discs; // TPOS + int bpm; // TBPM + char compilation; // YTCP + + int bitrate; + int samplerate; + int samplesize; + int channels; + int song_length; // TLEN + int audio_size; + int audio_offset; + int vbr_scale; + int lossless; + int blockalignment; + + char *dlna_pn; // DLNA Profile Name + + char *tagversion; + + unsigned long album_id; + unsigned long track_id; + unsigned long genre_id; + unsigned long contributor_id[N_ROLE]; + + char *musicbrainz_albumid; + char *musicbrainz_trackid; + char *musicbrainz_artistid; + char *musicbrainz_albumartistid; + + int is_plist; + int plist_position; + int plist_id; +}; + +#define WMAV1 0x161 +#define WMAV2 0x162 +#define WMAPRO 0x163 + +extern int scan_init(char *path); +extern void make_composite_tags(struct song_metadata *psong); +extern int readtags(char *path, struct song_metadata *psong, struct stat *stat, char *lang, char *type); +extern void freetags(struct song_metadata *psong); + +extern int start_plist(char *path, struct song_metadata *psong, struct stat *stat, char *lang, char *type); +extern int next_plist_track(struct song_metadata *psong, struct stat *stat, char *lang, char *type); + +#endif diff --git a/tagutils/textutils.c b/tagutils/textutils.c new file mode 100644 index 0000000..9e55181 --- /dev/null +++ b/tagutils/textutils.c @@ -0,0 +1,298 @@ +//========================================================================= +// FILENAME : textutils.c +// DESCRIPTION : Misc. text utilities +//========================================================================= +// Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved. +//========================================================================= + +/* This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + + +#include +#include +#include +#include + +#include "misc.h" +#include "textutils.h" +#include "../log.h" + +static unsigned int +_char_htoi(char h) +{ + if (h<'0') + return 0; + if (h<='9') + return h-'0'; + if (h<'A') + return 0; + if (h<='F') + return h-'A'+10; + if (h<'a') + return 0; + if (h<='f') + return h-'a'+10; + return 0; +} + +void +urldecode(char *src) +{ + char c, *s, *d; + + for (d=s=src; *s; s++, d++) { + c = *s; + if (c=='%') { + c = *++s; + if (c=='%') + c = '%'; + else { + c = _char_htoi(c)<<4 | _char_htoi(*++s); + } + *d = c; + } + else { + *d = c; + } + } + *d = '\0'; +} + +#if 0 +static int +is_ignoredword(const char *str) +{ + int i; + + if (!prefs.ignoredwords) + return 0; + + for (i=0; prefs.ignoredwords[i].n; i++) { + if (!(strncasecmp(prefs.ignoredwords[i].word, str, prefs.ignoredwords[i].n))) { + char next_char = str[prefs.ignoredwords[i].n]; + if (isalnum(next_char)) + continue; + return prefs.ignoredwords[i].n; + } + } + return 0; +} +#endif + +char * +skipspaces(const char *str) +{ + while (isspace(*str)) str++; + return (char*) str; +} + +/* +U+0040 (40): @ A B C D E F G H I J K L M N O +U+0050 (50): P Q R S T U V W X Y Z [ \ ] ^ _ +U+0060 (60): ` a b c d e f g h i j k l m n o +U+0070 (70): p q r s t u v w x y z { | } ~ + +U+00c0 (c3 80): À Á Â Ã Ä Å Æ Ç È É Ê Ë Ì Í Î Ï +U+00d0 (c3 90): Ð Ñ Ò Ó Ô Õ Ö × Ø Ù Ú Û Ü Ý Þ ß +U+00e0 (c3 a0): à á â ã ä å æ ç è é ê ë ì í î ï +U+00f0 (c3 b0): ð ñ ò ó ô õ ö ÷ ø ù ú û ü ý þ ÿ +U+0100 (c4 80): Ā ā Ă ă Ą ą Ć ć Ĉ ĉ Ċ ċ Č č Ď ď +U+0110 (c4 90): Đ đ Ē ē Ĕ ĕ Ė ė Ę ę Ě ě Ĝ ĝ Ğ ğ +U+0120 (c4 a0): Ġ ġ Ģ ģ Ĥ ĥ Ħ ħ Ĩ ĩ Ī ī Ĭ ĭ Į į +U+0130 (c4 b0): İ ı IJ ij Ĵ ĵ Ķ ķ ĸ Ĺ ĺ Ļ ļ Ľ ľ Ŀ +U+0140 (c5 80): ŀ Ł ł Ń ń Ņ ņ Ň ň ʼn Ŋ ŋ Ō ō Ŏ ŏ +U+0150 (c5 90): Ő ő Œ œ Ŕ ŕ Ŗ ŗ Ř ř Ś ś Ŝ ŝ Ş ş +U+0160 (c5 a0): Š š Ţ ţ Ť ť Ŧ ŧ Ũ ũ Ū ū Ŭ ŭ Ů ů +U+0170 (c5 b0): Ű ű Ų ų Ŵ ŵ Ŷ ŷ Ÿ Ź ź Ż ż Ž ž ſ + */ + +// conversion table for latin diacritical char to ascii one char or two chars. +unsigned short UtoAscii[] = { + // U+00c0 + 0x0041,0x0041,0x0041,0x0041, 0x0041,0x0041,0x4145,0x0043, 0x0045,0x0045,0x0045,0x0045, 0x0049,0x0049,0x0049,0x0049, + 0x0044,0x004e,0x004f,0x004f, 0x004f,0x004f,0x004f,0xc397, 0xc398,0x0055,0x0055,0x0055, 0x0055,0x0059,0x0050,0x5353, + // U+00e0 + 0x0041,0x0041,0x0041,0x0041, 0x0041,0x0041,0x4145,0x0043, 0x0045,0x0045,0x0045,0x0045, 0x0049,0x0049,0x0049,0x0049, + 0x0044,0x004e,0x004f,0x004f, 0x004f,0x004f,0x004f,0xc397, 0xc398,0x0055,0x0055,0x0055, 0x0055,0x0059,0x0050,0x5353, + // U+0100 + 0x0041,0x0041,0x0041,0x0041, 0x0041,0x0041,0x0043,0x0043, 0x0043,0x0043,0x0043,0x0043, 0x0043,0x0043,0x0044,0x0044, + 0x0044,0x0044,0x0045,0x0045, 0x0045,0x0045,0x0045,0x0045, 0x0045,0x0045,0x0045,0x0045, 0x0047,0x0047,0x0047,0x0047, + // U+0120 + 0x0047,0x0047,0x0047,0x0047, 0x0048,0x0048,0x0048,0x0048, 0x0049,0x0049,0x0049,0x0049, 0x0049,0x0049,0x0049,0x0049, + 0x0049,0x0049,0x494a,0x494a, 0x004a,0x004a,0x004b,0x004b, 0x004b,0x004c,0x004c,0x004c, 0x004c,0x004c,0x004c,0x004c, + // U+0140 + 0x004c,0x004c,0x004c,0x004e, 0x004e,0x004e,0x004e,0x004e, 0x004e,0x004e,0x004e,0x004e, 0x004f,0x004f,0x004f,0x004f, + 0x004f,0x004f,0x4f45,0x4f45, 0x0052,0x0052,0x0052,0x0052, 0x0052,0x0052,0x0053,0x0053, 0x0053,0x0053,0x0053,0x0053, + // U+0160 + 0x0053,0x0053,0x0054,0x0054, 0x0054,0x0054,0x0054,0x0054, 0x0055,0x0055,0x0055,0x0055, 0x0055,0x0055,0x0055,0x0055, + 0x0055,0x0055,0x0055,0x0055, 0x0057,0x0057,0x0059,0x0059, 0x0059,0x005a,0x005a,0x005a, 0x005a,0x005a,0x005a,0xc5bf +}; + +// conversion table for toupper() function for latin diacritical char +unsigned short UtoUpper[] = { + // U+00c0 + 0xc380,0xc381,0xc382,0xc383, 0xc384,0xc385,0xc386,0xc387, 0xc388,0xc389,0xc38a,0xc38b, 0xc38c,0xc38d,0xc38e,0xc38f, + 0xc390,0xc391,0xc392,0xc393, 0xc394,0xc395,0xc396,0xc397, 0xc398,0xc399,0xc39a,0xc39b, 0xc39c,0xc39d,0xc39e,0x5353, + // U+00e0 + 0xc380,0xc381,0xc382,0xc383, 0xc384,0xc385,0xc386,0xc387, 0xc388,0xc389,0xc38a,0xc38b, 0xc38c,0xc38d,0xc38e,0xc38f, + 0xc390,0xc391,0xc392,0xc393, 0xc394,0xc395,0xc396,0xc397, 0xc398,0xc399,0xc39a,0xc39b, 0xc39c,0xc39d,0xc39e,0xc39f, + // U+0100 + 0xc480,0xc480,0xc482,0xc482, 0xc484,0xc484,0xc486,0xc486, 0xc488,0xc488,0xc48a,0xc48a, 0xc48c,0xc48c,0xc48e,0xc48e, + 0xc490,0xc490,0xc492,0xc492, 0xc494,0xc494,0xc496,0xc496, 0xc498,0xc498,0xc49a,0xc49a, 0xc49c,0xc49c,0xc49e,0xc49e, + // U+0120 + 0xc4a0,0xc4a0,0xc4a2,0xc4a2, 0xc4a4,0xc4a4,0xc4a6,0xc4a6, 0xc4a8,0xc4a8,0xc4aa,0xc4aa, 0xc4ac,0xc4ac,0xc4ae,0xc4ae, + 0xc4b0,0xc4b0,0xc4b2,0xc4b2, 0xc4b4,0xc4b4,0xc4b6,0xc4b6, 0xc4b8,0xc4b9,0xc4b9,0xc4bb, 0xc4bb,0xc4bd,0xc4bd,0xc4bf, + // U+0140 + 0xc4bf,0xc581,0xc581,0xc583, 0xc583,0xc585,0xc585,0xc587, 0xc587,0xc589,0xc58a,0xc58a, 0xc58c,0xc58c,0xc58e,0xc58e, + 0xc590,0xc591,0xc592,0xc593, 0xc594,0xc595,0xc596,0xc597, 0xc598,0xc599,0xc59a,0xc59b, 0xc59c,0xc59d,0xc59e,0xc59f, + // U+0160 + 0xc5a0,0xc5a0,0xc5a2,0xc5a2, 0xc5a4,0xc5a4,0xc5a6,0xc5a6, 0xc5a8,0xc5a8,0xc5aa,0xc5aa, 0xc5ac,0xc5ac,0xc5ae,0xc5ae, + 0xc5b0,0xc5b1,0xc5b2,0xc5b3, 0xc5b4,0xc5b5,0xc5b6,0xc5b7, 0xc5b8,0xc5b9,0xc5b9,0xc5bb, 0xc5bc,0xc5bd,0xc5bd,0xc5bf, +}; + + +int +safe_atoi(char *s) +{ + if (!s) + return 0; + if ((s[0]>='0' && s[0]<='9') || s[0]=='-' || s[0]=='+') + return atoi(s); + return 0; +} + +// NOTE: support U+0000 ~ U+FFFF only. +int +utf16le_to_utf8(char *dst, int n, __u16 utf16le) +{ + __u16 wc = le16_to_cpu(utf16le); + if (wc < 0x80) { + if (n<1) return 0; + *dst++ = wc & 0xff; + return 1; + } + else if (wc < 0x800) { + if (n<2) return 0; + *dst++ = 0xc0 | (wc>>6); + *dst++ = 0x80 | (wc & 0x3f); + return 2; + } + else { + if (n<3) return 0; + *dst++ = 0xe0 | (wc>>12); + *dst++ = 0x80 | ((wc>>6) & 0x3f); + *dst++ = 0x80 | (wc & 0x3f); + return 3; + } +} + +void +fetch_string_txt(char *fname, char *lang, int n, ...) +{ + va_list args; + char **keys; + char ***strs; + char **defstr; + int i; + FILE *fp; + char buf[4096]; + int state; + char *p; + char *langid; + const char *lang_en = "EN"; + + if (!(keys = malloc(sizeof(keys) * n))) { + DPRINTF(E_FATAL, L_SCANNER, "Out of memory\n"); + } + if (!(strs = malloc(sizeof(strs) * n))) { + DPRINTF(E_FATAL, L_SCANNER, "Out of memory\n"); + } + if (!(defstr = malloc(sizeof(defstr) * n))) { + DPRINTF(E_FATAL, L_SCANNER, "Out of memory\n"); + } + + va_start(args, n); + for (i=0; i\n", fname); + goto _exit; + } + + state = -1; + while (fgets(buf, sizeof(buf), fp)) { + int len = strlen(buf); + + if (buf[len-1]=='\n') buf[len-1] = '\0'; + + if (state<0) { + if (isalpha(buf[0])) { + for (i=0; i + +extern void urldecode(char *src); +extern char * skipspaces(const char *src); +extern int safe_atoi(char *s); +extern int utf16le_to_utf8(char *dst, int n, __u16 utf16le); +extern void fetch_string_txt(char *fname, char *lang, int n, ...); diff --git a/tivo_beacon.c b/tivo_beacon.c index 3056025..804ead4 100644 --- a/tivo_beacon.c +++ b/tivo_beacon.c @@ -26,6 +26,7 @@ * * See the file "COPYING" for more details. */ +#include "config.h" #ifdef ENABLE_TIVO #include #include @@ -99,35 +100,29 @@ OpenAndConfTivoBeaconSocket() */ uint32_t getBcastAddress( void ) { - struct ifreq ifr; - struct sockaddr_in *sin; - int s, rval; - static int ifacePrinted = 0; + struct ifreq ifr; + struct sockaddr_in *sin; + int s, rval; - s = socket( AF_INET, SOCK_DGRAM, 0 ); - if ( s < 0 ) - { - return INADDR_BROADCAST; - } + s = socket( AF_INET, SOCK_DGRAM, 0 ); + if ( s < 0 ) + { + return INADDR_BROADCAST; + } - strcpy( ifr.ifr_name, "eth0" ); - rval = ioctl( s, SIOCGIFBRDADDR, &ifr ); - if ( rval < 0 ) - { - close(s); - return INADDR_BROADCAST; - } + strcpy( ifr.ifr_name, "eth0" ); + rval = ioctl( s, SIOCGIFBRDADDR, &ifr ); + if ( rval < 0 ) + { + close(s); + return INADDR_BROADCAST; + } - sin = (struct sockaddr_in *)&ifr.ifr_broadaddr; - if( !ifacePrinted ) - { - printf( "Interface: %s broadcast addr %s \n", "eth0", inet_ntoa(sin->sin_addr) ); - ifacePrinted = 1; - } + sin = (struct sockaddr_in *)&ifr.ifr_broadaddr; + close(s); + DPRINTF(E_DEBUG, L_TIVO, "Interface: %s broadcast addr %s \n", "eth0", inet_ntoa(sin->sin_addr) ); - close(s); - - return ntohl((uint32_t)(sin->sin_addr.s_addr)); + return ntohl((uint32_t)(sin->sin_addr.s_addr)); } /* @@ -139,7 +134,6 @@ sendBeaconMessage(int fd, struct sockaddr_in * client, int len, int broadcast) { char * mesg; int mesg_len; - time_t now = time(NULL); mesg_len = asprintf(&mesg, "TiVoConnect=1\n" "swversion=%s\n" @@ -151,7 +145,7 @@ sendBeaconMessage(int fd, struct sockaddr_in * client, int len, int broadcast) "1.0", broadcast ? "broadcast" : "connected", uuidvalue, friendly_name, runtime_vars.port); - printf("Sending beacon at %s%s", ctime(&now), mesg); + DPRINTF(E_DEBUG, L_TIVO, "Sending TiVo beacon\n"); sendto(fd, mesg, mesg_len, 0, (struct sockaddr*)client, len); free(mesg); } diff --git a/tivo_beacon.h b/tivo_beacon.h index 2819891..231984b 100644 --- a/tivo_beacon.h +++ b/tivo_beacon.h @@ -1,3 +1,4 @@ +#include "config.h" #ifdef ENABLE_TIVO /* * * A saved copy of a beacon from another tivo or another server diff --git a/tivo_commands.c b/tivo_commands.c index 37ebb7c..9e667e6 100644 --- a/tivo_commands.c +++ b/tivo_commands.c @@ -1,5 +1,7 @@ +#include "config.h" #ifdef ENABLE_TIVO #include +#include #include #include "tivo_utils.h" @@ -28,7 +30,248 @@ SendRootContainer(struct upnphttp * h) "
" "x-container/tivo-photos" "x-container/folder" - "Pictures" + "Pictures on %s" + "
" + "" + "" + "/TiVoConnect?Command=QueryContainer&Container=3/Pictures on %s" + "" + "" + "" + "" + "
" + "x-container/tivo-music" + "x-container/folder" + "Music on %s" + "
" + "" + "" + "/TiVoConnect?Command=QueryContainer&Container=1" + "" + "" + "
" + "", friendly_name, friendly_name, friendly_name, friendly_name); + BuildResp_upnphttp(h, resp, len); + SendResp_upnphttp(h); +} + +#if 0 +int callback(void *args, int argc, char **argv, char **azColName) +{ + struct Response *passed_args = (struct Response *)args; + char *id = argv[1], *parent = argv[2], *refID = argv[3], *class = argv[4], *detailID = argv[5], *size = argv[9], *title = argv[10], + *duration = argv[11], *bitrate = argv[12], *sampleFrequency = argv[13], *artist = argv[14], *album = argv[15], + *genre = argv[16], *comment = argv[17], *nrAudioChannels = argv[18], *track = argv[19], *date = argv[20], *resolution = argv[21], + *tn = argv[22], *creator = argv[23], *dlna_pn = argv[24], *mime = argv[25], *album_art = argv[26], *art_dlna_pn = argv[27]; + char dlna_buf[64]; + char str_buf[4096]; + char **result; + int ret; + + passed_args->total++; + + if( passed_args->requested && (passed_args->returned >= passed_args->requested) ) + return 0; + passed_args->returned++; + + if( dlna_pn ) + sprintf(dlna_buf, "DLNA.ORG_PN=%s", dlna_pn); + else + strcpy(dlna_buf, "*"); + + if( strncmp(class, "item", 4) == 0 ) + { + if( passed_args->client == EXbox ) + { + if( strcmp(mime, "video/divx") == 0 ) + { + mime[6] = 'a'; + mime[7] = 'v'; + mime[8] = 'i'; + mime[9] = '\0'; + } + } + sprintf(str_buf, "<item id=\"%s\" parentID=\"%s\" restricted=\"1\"", id, parent); + strcat(passed_args->resp, str_buf); + if( refID && (!passed_args->filter || strstr(passed_args->filter, "@refID")) ) { + sprintf(str_buf, " refID=\"%s\"", refID); + strcat(passed_args->resp, str_buf); + } + sprintf(str_buf, ">" + "<dc:title>%s</dc:title>" + "<upnp:class>object.%s</upnp:class>", + title, class); + strcat(passed_args->resp, str_buf); + if( comment && (!passed_args->filter || strstr(passed_args->filter, "dc:description")) ) { + sprintf(str_buf, "<dc:description>%s</dc:description>", comment); + strcat(passed_args->resp, str_buf); + } + if( creator && (!passed_args->filter || strstr(passed_args->filter, "dc:creator")) ) { + sprintf(str_buf, "<dc:creator>%s</dc:creator>", creator); + strcat(passed_args->resp, str_buf); + } + if( date && (!passed_args->filter || strstr(passed_args->filter, "dc:date")) ) { + sprintf(str_buf, "<dc:date>%s</dc:date>", date); + strcat(passed_args->resp, str_buf); + } + if( artist && (!passed_args->filter || strstr(passed_args->filter, "upnp:artist")) ) { + sprintf(str_buf, "<upnp:artist>%s</upnp:artist>", artist); + strcat(passed_args->resp, str_buf); + } + if( album && (!passed_args->filter || strstr(passed_args->filter, "upnp:album")) ) { + sprintf(str_buf, "<upnp:album>%s</upnp:album>", album); + strcat(passed_args->resp, str_buf); + } + if( genre && (!passed_args->filter || strstr(passed_args->filter, "upnp:genre")) ) { + sprintf(str_buf, "<upnp:genre>%s</upnp:genre>", genre); + strcat(passed_args->resp, str_buf); + } + if( track && atoi(track) && (!passed_args->filter || strstr(passed_args->filter, "upnp:originalTrackNumber")) ) { + sprintf(str_buf, "<upnp:originalTrackNumber>%s</upnp:originalTrackNumber>", track); + strcat(passed_args->resp, str_buf); + } + if( album_art && atoi(album_art) && (!passed_args->filter || strstr(passed_args->filter, "upnp:albumArtURI")) ) { + strcat(passed_args->resp, "<upnp:albumArtURI "); + if( !passed_args->filter || strstr(passed_args->filter, "upnp:albumArtURI@dlna:profileID") ) { + sprintf(str_buf, "dlna:profileID=\"%s\" xmlns:dlna=\"urn:schemas-dlnaorg:metadata-1-0/\"", art_dlna_pn); + strcat(passed_args->resp, str_buf); + } + sprintf(str_buf, ">http://%s:%d/AlbumArt/%s.jpg</upnp:albumArtURI>", + lan_addr[0].str, runtime_vars.port, album_art); + strcat(passed_args->resp, str_buf); + } + if( !passed_args->filter || strstr(passed_args->filter, "res") ) { + strcat(passed_args->resp, "<res "); + if( size && (!passed_args->filter || strstr(passed_args->filter, "res@size")) ) { + sprintf(str_buf, "size=\"%s\" ", size); + strcat(passed_args->resp, str_buf); + } + if( duration && (!passed_args->filter || strstr(passed_args->filter, "res@duration")) ) { + sprintf(str_buf, "duration=\"%s\" ", duration); + strcat(passed_args->resp, str_buf); + } + if( bitrate && (!passed_args->filter || strstr(passed_args->filter, "res@bitrate")) ) { + sprintf(str_buf, "bitrate=\"%s\" ", bitrate); + strcat(passed_args->resp, str_buf); + } + if( sampleFrequency && (!passed_args->filter || strstr(passed_args->filter, "res@sampleFrequency")) ) { + sprintf(str_buf, "sampleFrequency=\"%s\" ", sampleFrequency); + strcat(passed_args->resp, str_buf); + } + if( nrAudioChannels && (!passed_args->filter || strstr(passed_args->filter, "res@nrAudioChannels")) ) { + sprintf(str_buf, "nrAudioChannels=\"%s\" ", nrAudioChannels); + strcat(passed_args->resp, str_buf); + } + if( resolution && (!passed_args->filter || strstr(passed_args->filter, "res@resolution")) ) { + sprintf(str_buf, "resolution=\"%s\" ", resolution); + strcat(passed_args->resp, str_buf); + } + sprintf(str_buf, "protocolInfo=\"http-get:*:%s:%s\">" + "http://%s:%d/MediaItems/%s.dat" + "</res>", + mime, dlna_buf, lan_addr[0].str, runtime_vars.port, detailID); + #if 0 //JPEG_RESIZE + if( dlna_pn && (strncmp(dlna_pn, "JPEG_LRG", 8) == 0) ) { + strcat(passed_args->resp, str_buf); + sprintf(str_buf, "<res " + "protocolInfo=\"http-get:*:%s:%s\">" + "http://%s:%d/Resized/%s" + "</res>", + mime, "DLNA.ORG_PN=JPEG_SM", lan_addr[0].str, runtime_vars.port, id); + } + #endif + if( tn && atoi(tn) && dlna_pn ) { + strcat(passed_args->resp, str_buf); + strcat(passed_args->resp, "<res "); + sprintf(str_buf, "protocolInfo=\"http-get:*:%s:%s\">" + "http://%s:%d/Thumbnails/%s.dat" + "</res>", + mime, "DLNA.ORG_PN=JPEG_TN", lan_addr[0].str, runtime_vars.port, detailID); + } + strcat(passed_args->resp, str_buf); + } + strcpy(str_buf, "</item>"); + } + else if( strncmp(class, "container", 9) == 0 ) + { + sprintf(str_buf, "SELECT count(ID) from OBJECTS where PARENT_ID = '%s';", id); + ret = sql_get_table(db, str_buf, &result, NULL, NULL); + sprintf(str_buf, "<container id=\"%s\" parentID=\"%s\" restricted=\"1\" ", id, parent); + strcat(passed_args->resp, str_buf); + if( !passed_args->filter || strstr(passed_args->filter, "@childCount")) + { + sprintf(str_buf, "childCount=\"%s\"", result[1]); + strcat(passed_args->resp, str_buf); + } + /* If the client calls for BrowseMetadata on root, we have to include our "upnp:searchClass"'s, unless they're filtered out */ + if( (passed_args->requested == 1) && (strcmp(id, "0") == 0) ) + { + if( !passed_args->filter || strstr(passed_args->filter, "upnp:searchClass") ) + { + strcat(passed_args->resp, ">" + "<upnp:searchClass includeDerived=\"1\">object.item.audioItem</upnp:searchClass>" + "<upnp:searchClass includeDerived=\"1\">object.item.imageItem</upnp:searchClass>" + "<upnp:searchClass includeDerived=\"1\">object.item.videoItem</upnp:searchClass"); + } + } + sprintf(str_buf, ">" + "<dc:title>%s</dc:title>" + "<upnp:class>object.%s</upnp:class>", + title, class); + strcat(passed_args->resp, str_buf); + if( creator && (!passed_args->filter || strstr(passed_args->filter, "dc:creator")) ) { + sprintf(str_buf, "<dc:creator>%s</dc:creator>", creator); + strcat(passed_args->resp, str_buf); + } + if( genre && (!passed_args->filter || strstr(passed_args->filter, "upnp:genre")) ) { + sprintf(str_buf, "<upnp:genre>%s</upnp:genre>", genre); + strcat(passed_args->resp, str_buf); + } + if( artist && (!passed_args->filter || strstr(passed_args->filter, "upnp:artist")) ) { + sprintf(str_buf, "<upnp:artist>%s</upnp:artist>", artist); + strcat(passed_args->resp, str_buf); + } + if( album_art && atoi(album_art) && (!passed_args->filter || strstr(passed_args->filter, "upnp:albumArtURI")) ) { + strcat(passed_args->resp, "<upnp:albumArtURI "); + if( !passed_args->filter || strstr(passed_args->filter, "upnp:albumArtURI@dlna:profileID") ) { + sprintf(str_buf, "dlna:profileID=\"%s\" xmlns:dlna=\"urn:schemas-dlnaorg:metadata-1-0/\"", art_dlna_pn); + strcat(passed_args->resp, str_buf); + } + sprintf(str_buf, ">http://%s:%d/AlbumArt/%s.jpg</upnp:albumArtURI>", + lan_addr[0].str, runtime_vars.port, album_art); + strcat(passed_args->resp, str_buf); + } + sprintf(str_buf, "</container>"); + sqlite3_free_table(result); + } + strcat(passed_args->resp, str_buf); + + return 0; +} +#endif + +void +SendContainer(struct upnphttp * h, const char * objectID, const char * title, int itemStart, int itemCount) +{ + char * resp; + int len; + + len = asprintf(&resp, "\n" + "" + "
" + "x-container/tivo-%s" + "x-container/folder" + "0" + "2" + "%s" + "
" + "%d" + "%d" + "" + "
" + "x-container/tivo-photos" + "x-container/folder" + "Pictures on %s" "
" "" "" @@ -40,7 +283,7 @@ SendRootContainer(struct upnphttp * h) "
" "x-container/tivo-music" "x-container/folder" - "Music" + "Music on %s" "
" "" "" @@ -48,7 +291,9 @@ SendRootContainer(struct upnphttp * h) "" "" "
" - "
", friendly_name); + "", + (objectID[0]=='1') ? "music":"photos", + title, itemStart, itemCount, title, title); BuildResp_upnphttp(h, resp, len); SendResp_upnphttp(h); } @@ -60,6 +305,7 @@ ProcessTiVoCommand(struct upnphttp * h, const char * orig_path) char *key, *val; char *saveptr, *item; char *command = NULL, *container = NULL; + int itemStart=0, itemCount=0; path = decodeString(orig_path); DPRINTF(E_DEBUG, L_GENERAL, "Processing TiVo command %s\n", path); @@ -83,12 +329,29 @@ ProcessTiVoCommand(struct upnphttp * h, const char * orig_path) { container = val; } + else if( strcasecmp(key, "itemstart") == 0 ) + { + itemStart = atoi(val); + } + else if( strcasecmp(key, "itemcount") == 0 ) + { + itemCount = atoi(val); + } item = strtok_r( NULL, "&", &saveptr ); } - if( !container || (strcmp(container, "/") == 0) ) + if( strcmp(command, "QueryContainer") == 0 ) { - SendRootContainer(h); + if( !container || (strcmp(container, "/") == 0) ) + { + SendRootContainer(h); + } + else + { + val = container; + key = strsep(&val, "/"); + SendContainer(h, container, val, itemStart, itemCount); + } } CloseSocket_upnphttp(h); } diff --git a/tivo_commands.h b/tivo_commands.h index 67e4152..8130cfd 100644 --- a/tivo_commands.h +++ b/tivo_commands.h @@ -1,3 +1,4 @@ +#include "config.h" #ifdef ENABLE_TIVO void ProcessTiVoCommand(struct upnphttp * h, const char * orig_path); diff --git a/tivo_utils.c b/tivo_utils.c index 8e0546a..1d439b2 100644 --- a/tivo_utils.c +++ b/tivo_utils.c @@ -1,3 +1,4 @@ +#include "config.h" #ifdef ENABLE_TIVO #include #include diff --git a/tivo_utils.h b/tivo_utils.h index 06f680b..f4278ec 100644 --- a/tivo_utils.h +++ b/tivo_utils.h @@ -1,3 +1,4 @@ +#include "config.h" #ifdef ENABLE_TIVO char * decodeString(const char * string); diff --git a/upnpglobalvars.h b/upnpglobalvars.h index 681af97..76a45b3 100644 --- a/upnpglobalvars.h +++ b/upnpglobalvars.h @@ -30,9 +30,14 @@ "http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=AVC_TS_MP_HD_AC3_T;DLNA.ORG_OP=01;DLNA.ORG_CI=0," \ "http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_PRO;DLNA.ORG_OP=01;DLNA.ORG_CI=0," \ "http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01," \ - "http-get:*:audio/x-ms-wma:*," \ - "http-get:*:audio/wav:*," \ + "http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMABASE;DLNA.ORG_OP=01," \ + "http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMAFULL;DLNA.ORG_OP=01," \ + "http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMAPRO;DLNA.ORG_OP=01," \ + "http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO_320;DLNA.ORG_OP=01," \ + "http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO;DLNA.ORG_OP=01," \ + "http-get:*:audio/mp4:DLNA.ORG_PN=AAC_MULT5_ISO;DLNA.ORG_OP=01," \ "http-get:*:audio/mp4:*," \ + "http-get:*:audio/wav:*," \ "http-get:*:audio/x-aiff:*," \ "http-get:*:audio/x-flac:*," \ "http-get:*:application/ogg:*," \ diff --git a/upnphttp.c b/upnphttp.c index 8faf55b..42ee25d 100644 --- a/upnphttp.c +++ b/upnphttp.c @@ -478,7 +478,7 @@ static void ProcessHttpQuery_upnphttp(struct upnphttp * h) { char HttpCommand[16]; - char HttpUrl[128]; + char HttpUrl[256]; char * HttpVer; char * p; int i; @@ -496,7 +496,7 @@ ProcessHttpQuery_upnphttp(struct upnphttp * h) while(*p!='/') p++; } - for(i = 0; i<127 && *p != ' ' && *p != '\r'; i++) + for(i = 0; i<255 && *p != ' ' && *p != '\r'; i++) HttpUrl[i] = *(p++); HttpUrl[i] = '\0'; while(*p==' ')