diff --git a/Makefile.am b/Makefile.am index fc56082..4260555 100644 --- a/Makefile.am +++ b/Makefile.am @@ -25,29 +25,43 @@ minidlnad_SOURCES = minidlna.c upnphttp.c upnpdescgen.c upnpsoap.c \ upnpreplyparse.c minixml.c clients.c \ getifaddr.c process.c upnpglobalvars.c \ options.c minissdp.c uuid.c upnpevents.c \ - sql.c utils.c metadata.c scanner.c inotify.c \ + sql.c utils.c metadata.c scanner.c monitor.c \ tivo_utils.c tivo_beacon.c tivo_commands.c \ - playlist.c image_utils.c albumart.c log.c \ - containers.c tagutils/tagutils.c + playlist.c image_utils.c albumart.c log.c video_thumb.c \ + containers.c avahi.c tagutils/tagutils.c -#if NEED_VORBIS -vorbisflag = -lvorbis -#endif +if HAVE_KQUEUE +minidlnad_SOURCES += kqueue.c monitor_kqueue.c +else +minidlnad_SOURCES += select.c +endif -#if NEED_OGG -flacoggflag = -logg -#endif +if HAVE_VORBISFILE +vorbislibs = -lvorbis -logg +else +if NEED_OGG +flacogglibs = -logg +endif +endif + +if TIVO_SUPPORT +if HAVE_AVAHI +avahilibs = -lavahi-client -lavahi-common +endif +endif minidlnad_LDADD = \ @LIBJPEG_LIBS@ \ @LIBID3TAG_LIBS@ \ @LIBSQLITE3_LIBS@ \ - @LIBAVFORMAT_LIBS@ \ @LIBAVUTIL_LIBS@ \ + @LIBAVCODEC_LIBS@ \ + @LIBAVFORMAT_LIBS@ \ + @LIBSWSCALE_LIBS@ \ @LIBEXIF_LIBS@ \ @LIBINTL@ \ @LIBICONV@ \ - -lpthread -lFLAC $(vorbisflag) $(flacoggflag) + -lFLAC $(flacogglibs) $(vorbislibs) $(avahilibs) minidlnad_LDFLAGS = @STATIC_LDFLAGS@ @@ -59,7 +73,7 @@ testupnpdescgen_LDADD = \ @LIBAVFORMAT_LIBS@ \ @LIBAVUTIL_LIBS@ \ @LIBEXIF_LIBS@ \ - -lFLAC $(flacoggflag) $(vorbisflag) + -lFLAC $(flacogglibs) $(vorbislibs) $(avahilibs) SUFFIXES = .tmpl . diff --git a/NEWS b/NEWS index e9744b2..823b5a4 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,23 @@ +1.2.1 - Released 24-Aug-2017 +-------------------------------- +- Added Movian client detection and subtitle support. +- Fixed an issue with discovery on non-Linux systems. +- Fixed Bonjour discovery compatibility with TiVo Bolt. +- Fixed NFO file parsing, and added change monitoring support for them. +- Added a workaround for video thumbnails on some Samsung clients. +- Added DoS protection for event subscriptions. +- Fixed content browsing issues with some Samsung TVs. +- Improved non-destructive update scan support. + +1.2.0 - Released 17-May-2017 +-------------------------------- +- Add libavformat > 57 compatibility. +- Add TiVo Bonjour discovery support using Avahi for Bolt. +- Improve Samsung BD-J5500 support. +- Add quirk to fix video playback on Hyundai TVs. +- Add non-destructive update rescan support. +- Enhance bookmark support to work with Kodi. + 1.1.6 - Released 16-June-2016 -------------------------------- - Add AllShare and Windows client detection. diff --git a/README b/README index 31c7ca3..9d59827 100644 --- a/README +++ b/README @@ -18,9 +18,6 @@ serves multimedia content to compatible clients on the network. See http://www.upnp.org/ for more details on UPnP and http://www.dlna.org/ for mode details on DLNA. -See the INSTALL file for instructions on compiling, installing, -and configuring minidlna. - Prerequisites ================== @@ -34,3 +31,8 @@ Prerequisites Justin Maggard + +Features +================== + +- add possibility to serve multiple-sized thumbnails based on client request \ No newline at end of file diff --git a/albumart.c b/albumart.c index 20ed14e..3b60b92 100644 --- a/albumart.c +++ b/albumart.c @@ -29,6 +29,7 @@ #include #include #include +#include #include @@ -37,57 +38,235 @@ #include "sql.h" #include "utils.h" #include "image_utils.h" +#include "libav.h" +#include "video_thumb.h" #include "log.h" -static int -art_cache_exists(const char *orig_path, char **cache_file) +const image_size_type_t image_size_types[] = { + { JPEG_TN, "thumbnail", 160, 160 }, + { JPEG_SM, "small", 640, 480 }, + { JPEG_MED, "medium", 1024, 768 }, + { JPEG_LRG, "large", 4096, 4096 }, + { JPEG_INV, "", 0, 0 }, +}; + +const image_size_type_t* +get_image_size_type(image_size_type_enum size_type) { - if( xasprintf(cache_file, "%s/art_cache%s", db_path, orig_path) < 0 ) - return 0; + if (size_type < JPEG_TN || size_type > JPEG_LRG) size_type = JPEG_INV; + return &image_size_types[size_type]; +} - strcpy(strchr(*cache_file, '\0')-4, ".jpg"); +int +art_cache_path(const image_size_type_t *image_size, const char* postfix, const char *orig_path, char **cache_file) +{ + unsigned int hash = DJBHash((uint8_t*)orig_path, strlen(orig_path)); + #ifdef DEBUG + const char *fname = strrchr(orig_path, '/'); + if(!fname) fname = orig_path; else ++fname; + #endif + if(xasprintf( + cache_file, + "%s/art_cache/%08x" + #ifdef DEBUG + " (%s)" + #endif + "%s%s", + db_path, + hash, + #ifdef DEBUG + fname, + #endif + image_size ? "/thumbnail" : "", // holds the image size, 10 chars max. I.e. '.thumbnail' + postfix + ) < 0 ) return 0; + if(image_size) + { + // '/' is not part of a valid filename, but we added it there. + // This is how we know what to replace, and why we can safely do so. + char* replace = strrchr(*cache_file, '/'); + sprintf(replace, ".%s%s", image_size->name, postfix); + } + + return 1; +} + +int +art_cache_exists(const image_size_type_t *image_size_type, const char* postfix, const char *orig_path, char **cache_file) +{ + if(!art_cache_path(image_size_type, postfix, orig_path, cache_file)) + return 0; return (!access(*cache_file, F_OK)); } -static char * -save_resized_album_art(image_s *imsrc, const char *path) +void +art_cache_cleanup(const char* path) { - int dstw, dsth; - image_s *imdst; - char *cache_file; - char cache_dir[MAXPATHLEN]; + char* cache_file = NULL; - if( !imsrc ) - return NULL; + const image_size_type_t* image_size = image_size_types; + do { - if( art_cache_exists(path, &cache_file) ) - return cache_file; + #ifdef ENABLE_VIDEO_THUMB + /* Remove video thumbnails */ + if(art_cache_exists(image_size, ".jpg", path, &cache_file)) + { + if(!remove(cache_file)) + DPRINTF(E_DEBUG, L_INOTIFY, "Removed video thumbnail (%s).\n", cache_file); + free(cache_file); + } + #endif - strncpyt(cache_dir, cache_file, sizeof(cache_dir)); - make_dir(dirname(cache_dir), S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH); + if(art_cache_exists(image_size, ".mta", path, &cache_file)) + { + sql_exec(db, "DELETE from MTA where PATH = '%q'", cache_file); + if(!remove(cache_file)) + DPRINTF(E_DEBUG, L_INOTIFY, "Removed MTA data (%s).\n", cache_file); + free(cache_file); + } - if( imsrc->width > imsrc->height ) + } while((++image_size)->type != JPEG_INV); +} + +const char* art_cache_rename_nftw_renamer_old_path; +int art_cache_rename_nftw_renamer( + const char *fpath, + const struct stat *sb, + int typeflag, + struct FTW *ftwbuf +) { + if( typeflag & FTW_DP ) { // directory + return 0; + } + + // If ftwbuf->level is 0, it means that we were passed file, instead of a + // directory. Therefor we don't need to 'construct' the old filename, it's + // already there. + // Note that in the case of a directory rename all file names remain the + // same (so we can construct the old name from the old path), but in a the + // case of renamed file, the file itself has a different name. + const char* old_fpath = NULL; + char buffer[PATH_MAX] = {}; + if(ftwbuf->level) { + const char* fname = fpath + ftwbuf->base; + snprintf(buffer, sizeof(buffer), "%s/%s", art_cache_rename_nftw_renamer_old_path, fname); + old_fpath = buffer; + } + else { + old_fpath = art_cache_rename_nftw_renamer_old_path; + } + + char* old_cache_file = NULL; + if(!art_cache_path(NULL, ".jpg", old_fpath, &old_cache_file)) { + return 0; + } + + char* new_cache_file = NULL; + if(!art_cache_path(NULL, ".jpg", fpath, &new_cache_file)) { + free(old_cache_file); + return 0; + } + + DPRINTF(E_DEBUG, L_GENERAL, "rename for\n '%s' ('%s') -->\n '%s' ('%s')\n", old_fpath, old_cache_file, fpath, new_cache_file); + if(rename(old_cache_file, new_cache_file)) { + DPRINTF(E_DEBUG, L_GENERAL, "rename failed: %s (%d)\n", strerror(errno), errno); + } + else { + // Update database, too + int ret = 0; + + ret = sql_exec(db, "UPDATE ALBUM_ART SET PATH = '%q' WHERE PATH = '%q'", new_cache_file, old_cache_file); + if(SQLITE_OK != ret) + { + DPRINTF(E_WARN, L_METADATA, "Error renaming ALBUM_ART date base entry for '%s': %d\n", old_cache_file, ret); + } + + ret = sql_exec(db, "UPDATE DETAILS SET PATH = '%q' WHERE PATH = '%q'", fpath, old_fpath); + if(SQLITE_OK != ret) + { + DPRINTF(E_WARN, L_METADATA, "Error renaming DETAILS date base entry for '%s': %d\n", old_fpath, ret); + } + } + + free(old_cache_file); + free(new_cache_file); + + return 0; +} + +int +art_cache_rename(const char * oldpath, const char * newpath) +{ + // i.e. not thread safe + art_cache_rename_nftw_renamer_old_path = oldpath; + // StackOverflow says 15 is a good number of file descriptors: + // http://stackoverflow.com/questions/8436841/how-to-recursively-list-directories-in-c-on-linux + return nftw(newpath, art_cache_rename_nftw_renamer, 15, 0); +} + +static int +save_resized_album_art_from_imsrc_to(const image_s *imsrc, const char *src_file, const char *dst_file, const image_size_type_t *image_size_type) +{ + int dstw, dsth; + char *result; + + if (!imsrc || !image_size_type) + return -1; + + // Scale, ensuring that neither direction exceeds the configured maximum + // image dimensions. + int src_ar = imsrc->width*image_size_type->height; + int tgt_ar = image_size_type->width*imsrc->height; + if( src_ar < tgt_ar ) + { // imsrc is too tall + dsth = image_size_type->height; + dstw = (imsrc->width << 8) / ((imsrc->height << 8) / dsth); + } + else if( src_ar > tgt_ar ) + { + dstw = image_size_type->width; + dsth = (imsrc->height << 8) / ((imsrc->width << 8) / dstw); + } + else { + dstw = image_size_type->width; + dsth = image_size_type->height; + } + + if (dstw > imsrc->width && dsth > imsrc->height) { - dstw = 160; - dsth = (imsrc->height<<8) / ((imsrc->width<<8)/160); + /* if requested dimensions are bigger than image, don't upsize but + * link file or save as-is if linking fails */ + int ret = link_file(src_file, dst_file); + result = (ret == 0) ? (char*)dst_file : image_save_to_jpeg_file(imsrc, dst_file); } else { - dstw = (imsrc->width<<8) / ((imsrc->height<<8)/160); - dsth = 160; + image_s *imdst = image_resize(imsrc, dstw, dsth); + result = image_save_to_jpeg_file(imdst, dst_file); + image_free(imdst); } - imdst = image_resize(imsrc, dstw, dsth); - if( !imdst ) + + if (result == NULL) { - free(cache_file); - return NULL; + DPRINTF(E_WARN, L_ARTWORK, "Failed to create albumart cache of '%s' to '%s' [%s]\n", src_file, dst_file, strerror(errno)); + return -1; } - cache_file = image_save_to_jpeg_file(imdst, cache_file); - image_free(imdst); - - return cache_file; + return 0; +} + +int +save_resized_album_art_from_file_to_file(const char *path, const char *dst_file, const image_size_type_t *image_size_type) +{ + image_s *imsrc = image_new_from_jpeg(path, 1, NULL, 0, 1, ROTATE_NONE); + if(!imsrc) { + DPRINTF(E_WARN, L_METADATA, "Failed to resize '%s' to '%s'\n", path, dst_file); + return -1; + } + int ret = save_resized_album_art_from_imsrc_to(imsrc, path, dst_file, image_size_type); + image_free(imsrc); + return ret; } /* And our main album art functions */ @@ -104,6 +283,7 @@ update_if_album_art(const char *path) DIR *dh; struct dirent *dp; enum file_types type = TYPE_UNKNOWN; + media_types dir_type; int64_t art_id = 0; int ret; @@ -122,6 +302,9 @@ update_if_album_art(const char *path) album_art = is_album_art(match); strncpyt(dpath, path, sizeof(dpath)); + dir_type = valid_media_types(dpath); + if (!(dir_type & (TYPE_VIDEO|TYPE_AUDIO))) + return; dir = dirname(dpath); dh = opendir(dir); if( !dh ) @@ -129,29 +312,28 @@ update_if_album_art(const char *path) while ((dp = readdir(dh)) != NULL) { if (is_reg(dp) == 1) - { type = TYPE_FILE; - } else if (is_dir(dp) == 1) - { type = TYPE_DIR; - } else { snprintf(file, sizeof(file), "%s/%s", dir, dp->d_name); - type = resolve_unknown_type(file, ALL_MEDIA); + type = resolve_unknown_type(file, dir_type); } - if( type != TYPE_FILE ) + + if (type != TYPE_FILE || dp->d_name[0] == '.') continue; - if( (dp->d_name[0] != '.') && - (is_video(dp->d_name) || is_audio(dp->d_name)) && + + if(((is_video(dp->d_name) && (dir_type & TYPE_VIDEO)) || + (is_audio(dp->d_name) && (dir_type & TYPE_AUDIO))) && (album_art || strncmp(dp->d_name, match, ncmp) == 0) ) { - DPRINTF(E_DEBUG, L_METADATA, "New file %s looks like cover art for %s\n", path, dp->d_name); snprintf(file, sizeof(file), "%s/%s", dir, dp->d_name); art_id = find_album_art(file, NULL, 0); - ret = sql_exec(db, "UPDATE DETAILS set ALBUM_ART = %lld where PATH = '%q'", (long long)art_id, file); - if( ret != SQLITE_OK ) + ret = sql_exec(db, "UPDATE DETAILS set ALBUM_ART = %lld where PATH = '%q' and ALBUM_ART != %lld", (long long)art_id, file, (long long)art_id); + if( ret == SQLITE_OK ) + DPRINTF(E_DEBUG, L_METADATA, "Updated cover art for %s to %s\n", dp->d_name, path); + else DPRINTF(E_WARN, L_METADATA, "Error setting %s as cover art for %s\n", match, dp->d_name); } } @@ -161,10 +343,7 @@ update_if_album_art(const char *path) char * check_embedded_art(const char *path, uint8_t *image_data, int image_size) { - int width = 0, height = 0; char *art_path = NULL; - char *cache_dir; - FILE *dstfile; image_s *imsrc; static char last_path[PATH_MAX]; static unsigned int last_hash = 0; @@ -175,34 +354,6 @@ check_embedded_art(const char *path, uint8_t *image_data, int image_size) { return NULL; } - /* If the embedded image matches the embedded image from the last file we - * checked, just make a hard link. Better than storing it on the disk twice. */ - hash = DJBHash(image_data, image_size); - if( hash == last_hash ) - { - if( !last_success ) - return NULL; - art_cache_exists(path, &art_path); - if( link(last_path, art_path) == 0 ) - { - return(art_path); - } - else - { - if( errno == ENOENT ) - { - cache_dir = strdup(art_path); - make_dir(dirname(cache_dir), S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH); - free(cache_dir); - if( link(last_path, art_path) == 0 ) - return(art_path); - } - DPRINTF(E_WARN, L_METADATA, "Linking %s to %s failed [%s]\n", art_path, last_path, strerror(errno)); - free(art_path); - art_path = NULL; - } - } - last_hash = hash; imsrc = image_new_from_jpeg(NULL, 0, image_data, image_size, 1, ROTATE_NONE); if( !imsrc ) @@ -210,49 +361,55 @@ check_embedded_art(const char *path, uint8_t *image_data, int image_size) last_success = 0; return NULL; } - width = imsrc->width; - height = imsrc->height; - if( width > 160 || height > 160 ) - { - art_path = save_resized_album_art(imsrc, path); - } - else if( width > 0 && height > 0 ) + /* If the embedded image matches the embedded image from the last file we + * checked, just make a link. Better than storing it on the disk twice. + * + * Daniel: + * Is this really worth the complexity? We don't seem to bother with this + * for resized images at all... + */ + hash = DJBHash(image_data, image_size); + if(hash == last_hash && last_success) { - size_t nwritten; - if( art_cache_exists(path, &art_path) ) - goto end_art; - 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 ) + if(art_cache_exists(NULL, ".jpg", path, &art_path)) { - free(art_path); - art_path = NULL; - goto end_art; + if(!link_file(last_path, art_path)) + { + // Linking failed, try to save a new copy instead (below) + free(art_path); + art_path = NULL; + } } - nwritten = fwrite((void *)image_data, 1, image_size, dstfile); - fclose(dstfile); - if( nwritten != image_size ) + else { - DPRINTF(E_WARN, L_METADATA, "Embedded art error: wrote %lu/%d bytes\n", - (unsigned long)nwritten, image_size); - remove(art_path); + // File did not exist yet, somehow, try again (below) free(art_path); art_path = NULL; - goto end_art; } } -end_art: + + // New file, save an original copy + // The webservice will generate other thumbs from this on the fly + if(!art_path) { + last_hash = hash; + if(!art_cache_path(NULL, ".jpg", path, &art_path)) + { + // This time it's fatal... + return NULL; + } + image_save_to_jpeg_file(imsrc, art_path); + } + image_free(imsrc); + if( !art_path ) { - DPRINTF(E_WARN, L_METADATA, "Invalid embedded album art in %s\n", basename((char *)path)); + DPRINTF(E_WARN, L_ARTWORK, "Invalid embedded album art in %s\n", path); last_success = 0; return NULL; } - DPRINTF(E_DEBUG, L_METADATA, "Found new embedded album art in %s\n", basename((char *)path)); + DPRINTF(E_DEBUG, L_ARTWORK, "Found new embedded album art in %s\n", path); last_success = 1; strcpy(last_path, art_path); @@ -265,9 +422,7 @@ check_for_album_file(const char *path) char file[MAXPATHLEN]; char mypath[MAXPATHLEN]; struct album_art_name_s *album_art_name; - image_s *imsrc = NULL; - int width=0, height=0; - char *art_file, *p; + char *p; const char *dir; struct stat st; int ret; @@ -306,62 +461,82 @@ check_for_album_file(const char *path) } } } - if( ret == 0 ) - { - if( art_cache_exists(file, &art_file) ) - goto existing_file; - free(art_file); - imsrc = image_new_from_jpeg(file, 1, NULL, 0, 1, ROTATE_NONE); - if( imsrc ) - goto found_file; - } + if (ret == 0) goto add_cached_image; + check_dir: /* Then fall back to possible generic cover art file names */ - for( album_art_name = album_art_names; album_art_name; album_art_name = album_art_name->next ) + for (album_art_name = album_art_names; album_art_name; album_art_name = album_art_name->next) { snprintf(file, sizeof(file), "%s/%s", dir, album_art_name->name); - if( access(file, R_OK) == 0 ) + if (access(file, R_OK) == 0) +add_cached_image: { - if( art_cache_exists(file, &art_file) ) - { -existing_file: - return art_file; - } - free(art_file); - imsrc = image_new_from_jpeg(file, 1, NULL, 0, 1, ROTATE_NONE); - if( !imsrc ) - continue; -found_file: - width = imsrc->width; - height = imsrc->height; - if( width > 160 || height > 160 ) - art_file = save_resized_album_art(imsrc, file); - else - art_file = strdup(file); - image_free(imsrc); - return(art_file); + char *cache_file; + + DPRINTF(E_DEBUG, L_ARTWORK, "Found album art in %s\n", file); + if (art_cache_exists(NULL, ".jpg", file, &cache_file)) + return cache_file; + + int ret = copy_file(file, cache_file); + return ret == 0 ? cache_file : NULL; } } + return NULL; } +#ifdef ENABLE_VIDEO_THUMB +char * +generate_thumbnail(const char * path) +{ + char *tfile = NULL; + char cache_dir[MAXPATHLEN]; + + if( art_cache_exists(NULL, ".jpg", path, &tfile) ) + return tfile; + + memset(&cache_dir, 0, sizeof(cache_dir)); + + if ( is_video(path) ) + { + strncpyt(cache_dir, tfile, sizeof(cache_dir)-1); + if ( !make_dir(dirname(cache_dir), S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) && + !video_thumb_generate_tofile(path, tfile, 20, runtime_vars.thumb_width)) + return tfile; + } + free(tfile); + + return 0; +} +#endif + int64_t find_album_art(const char *path, uint8_t *image_data, int image_size) { - char *album_art = NULL; - int64_t ret = 0; - if( (image_size && (album_art = check_embedded_art(path, image_data, image_size))) || - (album_art = check_for_album_file(path)) ) + char *album_art = check_embedded_art(path, image_data, image_size); + if (album_art == NULL) album_art = check_for_album_file(path); +#ifdef ENABLE_VIDEO_THUMB + if (album_art == NULL && GETFLAG(THUMB_MASK)) + album_art = generate_thumbnail(path); +#endif + if (album_art == NULL) return 0; + + + int64_t ret = sql_get_int_field(db, "SELECT ID from ALBUM_ART where PATH = '%q'", album_art); + if (ret == 0) { - ret = sql_get_int_field(db, "SELECT ID from ALBUM_ART where PATH = '%q'", album_art); - if( !ret ) + if (sql_exec(db, "INSERT into ALBUM_ART (PATH) VALUES ('%q')", album_art) == SQLITE_OK) + { + ret = sqlite3_last_insert_rowid(db); + } + else { - if( sql_exec(db, "INSERT into ALBUM_ART (PATH) VALUES ('%q')", album_art) == SQLITE_OK ) - ret = sqlite3_last_insert_rowid(db); + DPRINTF(E_WARN, L_METADATA, "Error setting %s as cover art for %s\n", album_art, path); + ret = 0; } } - free(album_art); + free(album_art); return ret; } diff --git a/albumart.h b/albumart.h index f3db00a..93f5dcc 100644 --- a/albumart.h +++ b/albumart.h @@ -24,7 +24,30 @@ #ifndef __ALBUMART_H__ #define __ALBUMART_H__ +typedef enum { + JPEG_TN = 0, + JPEG_SM, + JPEG_MED, + JPEG_LRG, + JPEG_INV, +} image_size_type_enum; + +typedef struct +{ + image_size_type_enum type; + const char* name; + int width; + int height; +} image_size_type_t; + void update_if_album_art(const char *path); int64_t find_album_art(const char *path, uint8_t *image_data, int image_size); +const image_size_type_t *get_image_size_type(image_size_type_enum size_type); +int art_cache_path(const image_size_type_t *image_size_type, const char* postfix, const char *orig_path, char **cache_file); +int art_cache_exists(const image_size_type_t *image_size_type, const char* postfix, const char *orig_path, char **cache_file); +int art_cache_rename(const char * oldpath, const char * newpath); +void art_cache_cleanup(const char* path); +char *save_resized_album_art_to(const char *src_file, const char *dst_file, const image_size_type_t *image_size_type); +int save_resized_album_art_from_file_to_file(const char *path, const char *dst, const image_size_type_t *image_size_type); #endif diff --git a/avahi.c b/avahi.c new file mode 100644 index 0000000..750542a --- /dev/null +++ b/avahi.c @@ -0,0 +1,248 @@ +/* MiniDLNA media server + * Copyright (C) 2017 NETGEAR + * + * This file is part of MiniDLNA. + * + * MiniDLNA is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * MiniDLNA 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 MiniDLNA. If not, see . + */ + +/* This file is loosely based on examples from zeroconf_avahi_demos: + * http://wiki.ros.org/zeroconf_avahi_demos + * with some ideas from Netatalk's Avahi implementation. + */ + +#include + +#if defined(TIVO_SUPPORT) && defined(HAVE_AVAHI) + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "upnpglobalvars.h" +#include "log.h" + +static struct context { + AvahiThreadedPoll *threaded_poll; + AvahiClient *client; + AvahiEntryGroup *group; +} ctx; + +/***************************************************************** + * Private functions + *****************************************************************/ + +/* Called when publishing of service data completes */ +static void publish_reply(AvahiEntryGroup *g, + AvahiEntryGroupState state, + AVAHI_GCC_UNUSED void *userdata) +{ + assert(ctx.group == NULL || g == ctx.group); + + switch (state) { + case AVAHI_ENTRY_GROUP_ESTABLISHED: + /* The entry group has been established successfully */ + DPRINTF(E_MAXDEBUG, L_SSDP, "publish_reply: AVAHI_ENTRY_GROUP_ESTABLISHED\n"); + break; + case AVAHI_ENTRY_GROUP_COLLISION: + /* With multiple names there's no way to know which one collided */ + DPRINTF(E_ERROR, L_SSDP, "publish_reply: AVAHI_ENTRY_GROUP_COLLISION: %s\n", + avahi_strerror(avahi_client_errno(ctx.client))); + avahi_threaded_poll_quit(ctx.threaded_poll); + break; + case AVAHI_ENTRY_GROUP_FAILURE: + DPRINTF(E_ERROR, L_SSDP, "Failed to register service: %s\n", + avahi_strerror(avahi_client_errno(ctx.client))); + avahi_threaded_poll_quit(ctx.threaded_poll); + break; + case AVAHI_ENTRY_GROUP_UNCOMMITED: + case AVAHI_ENTRY_GROUP_REGISTERING: + default: + break; + } +} + +static int +_add_svc(const char *cat, const char *srv, const char *id, const char *platform) +{ + char name[128+1]; + char path[64]; + int ret; + + snprintf(name, sizeof(name), "%s on %s", cat, friendly_name); + snprintf(path, sizeof(path), + "path=/TiVoConnect?Command=QueryContainer&Container=%s", id); + + DPRINTF(E_INFO, L_SSDP, "Registering '%s'\n", name); + ret = avahi_entry_group_add_service(ctx.group, AVAHI_IF_UNSPEC, AVAHI_PROTO_INET, + 0, name, srv, NULL, NULL, runtime_vars.port, + "protocol=http", path, platform, NULL); + if (ret < 0) + DPRINTF(E_ERROR, L_SSDP, "Failed to add %s: %s\n", + srv, avahi_strerror(avahi_client_errno(ctx.client))); + + return ret; +} + +/* Try to register the TiVo DNS SRV service type. */ +static void +register_stuff(void) +{ + assert(ctx.client); + + if (!ctx.group) + { + ctx.group = avahi_entry_group_new(ctx.client, publish_reply, NULL); + if (!ctx.group) + { + DPRINTF(E_ERROR, L_SSDP, "Failed to create entry group: %s\n", + avahi_strerror(avahi_client_errno(ctx.client))); + return; + } + } + + if (avahi_entry_group_is_empty(ctx.group)) + { + if (_add_svc("Music", "_tivo-music._tcp", "1", NULL) < 0) + return; + if (_add_svc("Photos", "_tivo-photos._tcp", "3", NULL) < 0) + return; + if (_add_svc("Videos", "_tivo-videostream._tcp", "2", "platform=pc/"SERVER_NAME) < 0) + return; + if (avahi_entry_group_commit(ctx.group) < 0) + { + DPRINTF(E_ERROR, L_SSDP, "Failed to commit entry group: %s\n", + avahi_strerror(avahi_client_errno(ctx.client))); + return; + } + } +} + +static void client_callback(AvahiClient *client, + AvahiClientState state, + void *userdata) +{ + ctx.client = client; + + switch (state) { + case AVAHI_CLIENT_S_RUNNING: + /* The server has started up successfully and registered its host + * name on the network, so it's time to create our services */ + register_stuff(); + break; + case AVAHI_CLIENT_S_COLLISION: + case AVAHI_CLIENT_S_REGISTERING: + if (ctx.group) + avahi_entry_group_reset(ctx.group); + break; + case AVAHI_CLIENT_FAILURE: + if (avahi_client_errno(client) == AVAHI_ERR_DISCONNECTED) + { + int error; + + avahi_client_free(ctx.client); + ctx.client = NULL; + ctx.group = NULL; + + /* Reconnect to the server */ + ctx.client = avahi_client_new( + avahi_threaded_poll_get(ctx.threaded_poll), + AVAHI_CLIENT_NO_FAIL, client_callback, + &ctx, &error); + if (!ctx.client) + { + DPRINTF(E_ERROR, L_SSDP, + "Failed to contact server: %s\n", + avahi_strerror(error)); + avahi_threaded_poll_quit(ctx.threaded_poll); + } + } + else + { + DPRINTF(E_ERROR, L_SSDP, "Client failure: %s\n", + avahi_strerror(avahi_client_errno(client))); + avahi_threaded_poll_quit(ctx.threaded_poll); + } + break; + case AVAHI_CLIENT_CONNECTING: + default: + break; + } +} + +/************************************************************************ + * Public funcions + ************************************************************************/ + +/* + * Tries to shutdown this loop impl. + * Call this function from inside this thread. + */ +void tivo_bonjour_unregister(void) +{ + DPRINTF(E_DEBUG, L_SSDP, "tivo_bonjour_unregister\n"); + + if (ctx.group) + { + avahi_entry_group_reset(ctx.group); + avahi_entry_group_free(ctx.group); + } + if (ctx.threaded_poll) + avahi_threaded_poll_stop(ctx.threaded_poll); + if (ctx.client) + avahi_client_free(ctx.client); + if (ctx.threaded_poll) + avahi_threaded_poll_free(ctx.threaded_poll); +} + +/* + * Tries to setup the Zeroconf thread and any + * neccessary config setting. + */ +void tivo_bonjour_register(void) +{ + int error; + + /* first of all we need to initialize our threading env */ + ctx.threaded_poll = avahi_threaded_poll_new(); + if (!ctx.threaded_poll) + return; + + /* now we need to acquire a client */ + ctx.client = avahi_client_new(avahi_threaded_poll_get(ctx.threaded_poll), + AVAHI_CLIENT_NO_FAIL, client_callback, NULL, &error); + if (!ctx.client) + { + DPRINTF(E_ERROR, L_SSDP, "Failed to create client object: %s\n", + avahi_strerror(error)); + tivo_bonjour_unregister(); + return; + } + + if (avahi_threaded_poll_start(ctx.threaded_poll) < 0) + { + DPRINTF(E_ERROR, L_SSDP, "Failed to create thread: %s\n", + avahi_strerror(avahi_client_errno(ctx.client))); + tivo_bonjour_unregister(); + } + else + DPRINTF(E_INFO, L_SSDP, "Successfully started avahi loop\n"); +} +#endif /* HAVE_AVAHI */ diff --git a/avahi.h b/avahi.h new file mode 100644 index 0000000..8939c60 --- /dev/null +++ b/avahi.h @@ -0,0 +1,9 @@ +#include "config.h" + +#if defined(TIVO_SUPPORT) && defined(HAVE_AVAHI) +void tivo_bonjour_register(void); +void tivo_bonjour_unregister(void); +#else +static inline void tivo_bonjour_register(void) {}; +static inline void tivo_bonjour_unregister(void) {}; +#endif diff --git a/clients.c b/clients.c index 5b9ba5d..42f0d1c 100644 --- a/clients.c +++ b/clients.c @@ -1,5 +1,5 @@ /* MiniDLNA media server - * Copyright (C) 2013 NETGEAR + * Copyright (C) 2013-2017 NETGEAR * * This file is part of MiniDLNA. * @@ -20,6 +20,7 @@ #include #include "clients.h" +#include "event.h" #include "getifaddr.h" #include "log.h" @@ -63,6 +64,13 @@ struct client_type_s client_types[] = EUserAgent }, + { ESamsungBDJ5500, + FLAG_SAMSUNG | FLAG_DLNA | FLAG_NO_RESIZE | FLAG_CAPTION_RES | FLAG_SKIP_DLNA_PN, + "Samsung BD J5500", + "[BD]J5500", + EUserAgent + }, + /* Samsung Series [CDE] BDPs and TVs must be separated, or some of our * advertised extra features trigger a folder browsing bug on BDPs. */ /* User-Agent: DLNADOC/1.50 SEC_HHP_BD-D5100/1.0 */ @@ -198,6 +206,13 @@ struct client_type_s client_types[] = EUserAgent }, + { EHyundaiTV, + FLAG_DLNA, + "Hyundai TV", + "HYUNDAITV", + EFriendlyName + }, + { ERokuSoundBridge, FLAG_MS_PFS | FLAG_AUDIO_ONLY | FLAG_MIME_WAV_WAV | FLAG_FORCE_SORT, "Roku SoundBridge", @@ -240,6 +255,20 @@ struct client_type_s client_types[] = EUserAgent }, + { EMovian, + FLAG_CAPTION_RES, + "Movian", + "Movian", + EUserAgent + }, + + { EKodi, + FLAG_DLNA | FLAG_MIME_AVI_AVI | FLAG_CAPTION_RES, + "Kodi", + "Kodi", + EUserAgent + }, + { 0, FLAG_DLNA | FLAG_MIME_AVI_AVI, "Windows", @@ -247,6 +276,13 @@ struct client_type_s client_types[] = EUserAgent }, + { 0, + 0, + "TiVo", + "TvHttpClient", + EUserAgent + }, + { EStandardDLNA150, FLAG_DLNA | FLAG_MIME_AVI_AVI, "Generic DLNA 1.5", @@ -324,4 +360,3 @@ AddClientCache(struct in_addr addr, int type) return NULL; } - diff --git a/clients.h b/clients.h index 35d819a..f982ea5 100644 --- a/clients.h +++ b/clients.h @@ -1,5 +1,5 @@ /* MiniDLNA media server - * Copyright (C) 2013 NETGEAR + * Copyright (C) 2013-2017 NETGEAR * * This file is part of MiniDLNA. * @@ -37,6 +37,7 @@ #define FLAG_AUDIO_ONLY 0x00000400 #define FLAG_FORCE_SORT 0x00000800 #define FLAG_CAPTION_RES 0x00001000 +#define FLAG_SKIP_DLNA_PN 0x00002000 /* during browsing */ /* Response-related flags */ #define FLAG_HAS_CAPTIONS 0x80000000 #define RESPONSE_FLAGS 0xF0000000 @@ -69,13 +70,17 @@ enum client_types { ESamsungSeriesB, ESamsungSeriesCDEBDP, ESamsungSeriesCDE, + ESamsungBDJ5500, ESonyBDP, ESonyBravia, ESonyInternetTV, EToshibaTV, + EHyundaiTV, EAsusOPlay, EBubbleUPnP, ENetFrontLivingConnect, + EKodi, + EMovian, EStandardDLNA150, EStandardUPnP }; diff --git a/configure.ac b/configure.ac index e4de777..756f350 100644 --- a/configure.ac +++ b/configure.ac @@ -68,6 +68,7 @@ AC_C_BIGENDIAN AC_FUNC_FORK AC_FUNC_LSTAT_FOLLOWS_SLASHED_SYMLINK AC_CHECK_FUNCS([gethostname getifaddrs gettimeofday inet_ntoa memmove memset mkdir realpath select sendfile setlocale socket strcasecmp strchr strdup strerror strncasecmp strpbrk strrchr strstr strtol strtoul]) +AC_CHECK_DECLS([SEEK_HOLE]) # # Check for struct ip_mreqn @@ -161,6 +162,24 @@ AC_CHECK_HEADERS([libavcodec/avcodec.h libavformat/avformat.h libavutil/avutil.h [AC_CHECK_HEADERS([ffmpeg/avcodec.h ffmpeg/avformat.h ffmpeg/avutil.h],, [AC_MSG_ERROR("FFmpeg headers are missing!")])]) +CPPFLAGS_SAVE="$CPPFLAGS" +for dir in "" /usr/local $INCLUDE_DIR; do + if test -n "$dir"; then + CPPFLAGS="$CPPFLAGS_SAVE -I$dir" + fi + AC_CHECK_HEADERS([libswscale/swscale.h ffmpeg/libswscale/swscale.h libav/libswscale/swscale.h swscale.h ffmpeg/swscale.h libav/swscale.h], [HAVE_LIBSWSCALE=1]) + if test -z "$HAVE_LIBSWSCALE"; then + unset ac_cv_header_swscale_h + unset ac_cv_header_libswscale_swscale_h + unset ac_cv_header_ffmpeg_swscale_h + unset ac_cv_header_ffmpeg_libswscale_swscale_h + unset ac_cv_header_libav_swscale_h + unset ac_cv_header_libav_libswscale_swscale_h + continue + fi + break +done + CPPFLAGS_SAVE="$CPPFLAGS" for dir in "" /usr/local $INCLUDE_DIR; do if test -n "$dir"; then @@ -392,12 +411,11 @@ AC_CHECK_LIB(pthread, pthread_create) # prior versions had ov_open_callbacks in libvorbis, test that, too. AC_CHECK_LIB(vorbisfile, ov_open_callbacks, [AC_CHECK_HEADERS([vorbis/vorbisfile.h], - AM_CONDITIONAL(HAVE_VORBISFILE, true) - AC_DEFINE(HAVE_VORBISFILE,1,[Have vorbisfile]), - AM_CONDITIONAL(HAVE_VORBISFILE, false) - AC_DEFINE(HAVE_VORBISFILE,0,[lacking vorbisfile]))], - AM_CONDITIONAL(HAVE_VORBISFILE, false), - -lvorbis -logg) + AM_CONDITIONAL(HAVE_VORBISFILE, true) + AC_DEFINE(HAVE_VORBISFILE,1,[Have vorbisfile]), + AM_CONDITIONAL(HAVE_VORBISFILE, false))], + AM_CONDITIONAL(HAVE_VORBISFILE, false), + -lvorbis -logg) AC_CHECK_LIB(FLAC, FLAC__stream_decoder_init_stream, [AC_CHECK_HEADERS([FLAC/all.h], AM_CONDITIONAL(HAVE_FLAC, true) @@ -416,6 +434,14 @@ AC_CHECK_LIB(vorbisfile, vorbis_comment_query, AM_CONDITIONAL(NEED_VORBIS, true), -logg) +AC_CHECK_LIB(avahi-client, avahi_threaded_poll_new, + [AC_CHECK_HEADERS([avahi-common/thread-watch.h], + AM_CONDITIONAL(HAVE_AVAHI, true) + AC_DEFINE(HAVE_AVAHI,1,[Have avahi]), + AM_CONDITIONAL(HAVE_AVAHI, false))], + AM_CONDITIONAL(HAVE_AVAHI, false), + -lavahi-client -lavahi-common) + ################################################################################################################ ### Header checks @@ -443,6 +469,8 @@ AC_CHECK_FUNCS(inotify_init, AC_DEFINE(HAVE_INOTIFY,1,[Whether kernel has inotif ]) ]) +AC_CHECK_FUNCS(kqueue, AM_CONDITIONAL(HAVE_KQUEUE, true), AM_CONDITIONAL(HAVE_KQUEUE, false)) + ################################################################################################################ ### Build Options @@ -457,6 +485,11 @@ AC_ARG_WITH(dbpath, [with_dbpath="$withval"],[with_dbpath="/var/cache/minidlna"]) AC_DEFINE_UNQUOTED([DEFAULT_DB_PATH],"${with_dbpath}",[DB path]) +AC_ARG_WITH(iconpath, + AS_HELP_STRING([--with-iconpath=PATH],[Default Icon path]), + [with_iconpath="$withval"],[with_iconpath="/usr/share/minidlna"]) +AC_DEFINE_UNQUOTED([DEFAULT_ICON_PATH],"${with_iconpath}",[Icon path]) + AC_ARG_WITH(osname, AS_HELP_STRING([--with-osname=NAME],[OS Name]), [with_osname="$withval"],[with_osname="$(uname -s)"]) @@ -478,11 +511,14 @@ AC_ARG_ENABLE(tivo, [ --enable-tivo enable TiVo support],[ if test "$enableval" = "yes"; then AC_DEFINE(TIVO_SUPPORT, 1, [Define to 1 if you want to enable TiVo support]) + AM_CONDITIONAL(TIVO_SUPPORT, true) AC_MSG_RESULT([yes]) else + AM_CONDITIONAL(TIVO_SUPPORT, false) AC_MSG_RESULT([no]) fi ],[ + AM_CONDITIONAL(TIVO_SUPPORT, false) AC_MSG_RESULT([no]) ] ) @@ -518,7 +554,23 @@ AC_ARG_ENABLE(readynas, AC_DEFINE_UNQUOTED([ROOTDEV_MANUFACTURER],"NETGEAR") AC_DEFINE_UNQUOTED([ROOTDEV_MODELNAME],"Windows Media Connect compatible (ReadyDLNA)") AC_DEFINE_UNQUOTED([ROOTDEV_MODELDESCRIPTION],"ReadyDLNA") + AM_CONDITIONAL(TIVO_SUPPORT, true) + AC_MSG_RESULT([yes]) + else + AC_MSG_RESULT([no]) + fi + ],[ + AC_MSG_RESULT([no]) + ] +) + +AC_MSG_CHECKING([whether to build a video thumbnail support]) +AC_ARG_ENABLE(videothumbnail, + [ --enable-videothumbnail enable video thumbnail generation using libavcodec and libswscale],[ + if test "$enableval" = "yes"; then + AC_DEFINE([ENABLE_VIDEO_THUMB],[1],[Define to 1 if you want to enable video thumbnail generation]) AC_MSG_RESULT([yes]) + ENABLE_THUMB=1 else AC_MSG_RESULT([no]) fi @@ -526,6 +578,18 @@ AC_ARG_ENABLE(readynas, AC_MSG_RESULT([no]) ] ) +if test x"$ENABLE_THUMB" = x"1"; then + if test -z "$HAVE_LIBSWSCALE"; then + AC_MSG_ERROR([videothumbnail requires libswscale headers]) + fi + + + AC_CHECK_LIB(swscale ,[sws_getCachedContext], [LIBSWSCALE_LIBS="-lswscale"], [unset ac_cv_lib_swscale_getCachedContext]) + AC_SUBST(LIBSWSCALE_LIBS) + + AC_CHECK_LIB(avcodec ,[avcodec_version], [LIBAVCODEC_LIBS="-lavcodec"], [AC_MSG_ERROR([videothumbnail requires libavcodec - could not find libavcodec - part of ffmpeg])]) + AC_SUBST(LIBAVCODEC_LIBS) +fi AC_MSG_CHECKING([whether to build a static binary executable]) AC_ARG_ENABLE(static, diff --git a/containers.c b/containers.c index e1a9c57..f5cece1 100644 --- a/containers.c +++ b/containers.c @@ -32,8 +32,10 @@ const char *music_artist_id = MUSIC_ARTIST_ID; const char *music_album_id = MUSIC_ALBUM_ID; const char *music_plist_id = MUSIC_PLIST_ID; const char *music_dir_id = MUSIC_DIR_ID; +const char *video_id = VIDEO_ID; const char *video_all_id = VIDEO_ALL_ID; const char *video_dir_id = VIDEO_DIR_ID; +const char *image_id = IMAGE_ID; const char *image_all_id = IMAGE_ALL_ID; const char *image_date_id = IMAGE_DATE_ID; const char *image_camera_id = IMAGE_CAMERA_ID; @@ -111,6 +113,11 @@ struct magic_container_s magic_containers[] = { NULL, "16", &image_dir_id, NULL, NULL, NULL, NULL, NULL, NULL, -1, FLAG_MS_PFS }, { NULL, "D2", &image_camera_id, NULL, NULL, NULL, NULL, NULL, NULL, -1, FLAG_MS_PFS }, + /* Samsung DCM10 containers for Series E(?) */ + { NULL, "I", &image_id, NULL, NULL, NULL, NULL, NULL, NULL, -1, FLAG_SAMSUNG_DCM10 }, + { NULL, "A", &music_id, NULL, NULL, NULL, NULL, NULL, NULL, -1, FLAG_SAMSUNG_DCM10 }, + { NULL, "V", &video_id, NULL, NULL, NULL, NULL, NULL, NULL, -1, FLAG_SAMSUNG_DCM10 }, + /* Jump straight to Music on audio-only devices */ { NULL, "0", &music_id, NULL, "0", NULL, NULL, NULL, NULL, -1, FLAG_AUDIO_ONLY }, @@ -133,9 +140,9 @@ in_magic_container(const char *id, int flags, const char **real_id) len = strlen(magic_containers[i].objectid_match); if (strncmp(id, magic_containers[i].objectid_match, len) == 0) { - if (*(id+len) == '$') + if (*(id+len) == '$') *real_id = id+len+1; - else if (*(id+len) == '\0') + else if (*(id+len) == '\0') *real_id = id; else continue; diff --git a/event.h b/event.h new file mode 100644 index 0000000..2e677f3 --- /dev/null +++ b/event.h @@ -0,0 +1,54 @@ +#include "config.h" + +#ifdef HAVE_KQUEUE +#include +#include +#endif + +struct event; + +typedef enum { +#ifdef HAVE_KQUEUE + EVENT_READ = EVFILT_READ, + EVENT_WRITE = EVFILT_WRITE, + EVENT_VNODE = EVFILT_VNODE, +#else + EVENT_READ, + EVENT_WRITE, +#endif +} event_t; + +#define EV_FLAG_CLOSING 0x00000001 + +typedef void event_process_t(struct event *); +#ifdef HAVE_KQUEUE +typedef void event_vnode_process_t(struct event *, u_int); +#endif + +struct event { + int fd; + int index; + event_t rdwr; + union { + event_process_t *process; +#ifdef HAVE_KQUEUE + event_vnode_process_t *process_vnode; +#endif + }; + void *data; +}; + +typedef int event_module_add_t(struct event *); +typedef int event_module_del_t(struct event *, int flags); +typedef int event_module_init_t(void); +typedef void event_module_fini_t(void); +typedef int event_module_process_t(u_long); +struct event_module { + event_module_add_t *add; + event_module_del_t *del; + event_module_process_t *process; + event_module_init_t *init; + event_module_fini_t *fini; +}; + +extern struct event_module event_module; diff --git a/getifaddr.c b/getifaddr.c index f0d3af3..61ad87d 100644 --- a/getifaddr.c +++ b/getifaddr.c @@ -43,6 +43,7 @@ #endif #include "config.h" +#include "event.h" #if HAVE_GETIFADDRS # include # ifdef __linux__ @@ -204,9 +205,13 @@ getsyshwaddr(char *buf, int len) continue; memcpy(mac, ifr.ifr_hwaddr.sa_data, 6); #else + if (p->ifa_addr->sa_family != AF_LINK) + continue; struct sockaddr_dl *sdl; sdl = (struct sockaddr_dl*)p->ifa_addr; - memcpy(mac, LLADDR(sdl), sdl->sdl_alen); + if (sdl->sdl_alen != 6) + continue; + memcpy(mac, LLADDR(sdl), 6); #endif if (MACADDR_IS_ZERO(mac)) continue; @@ -337,8 +342,9 @@ reload_ifaces(int force_notify) DPRINTF(E_INFO, L_GENERAL, "Enabling interface %s/%s\n", lan_addr[i].str, inet_ntoa(lan_addr[i].mask)); SendSSDPGoodbyes(lan_addr[i].snotify); - SendSSDPNotifies(lan_addr[i].snotify, lan_addr[i].str, - runtime_vars.port, runtime_vars.notify_interval); + char buf[LOCATION_URL_MAX_LEN] = {}; + const char* host = get_location_url_by_lan_addr(buf, i); + SendSSDPNotifies(lan_addr[i].snotify, runtime_vars.notify_interval, host); } } } @@ -377,9 +383,10 @@ OpenAndConfMonitorSocket(void) } void -ProcessMonitorEvent(int s) +ProcessMonitorEvent(struct event *ev) { #ifdef HAVE_NETLINK + int s = ev->fd; int len; char buf[4096]; struct nlmsghdr *nlh; diff --git a/getifaddr.h b/getifaddr.h index a2447f7..5631360 100644 --- a/getifaddr.h +++ b/getifaddr.h @@ -43,7 +43,7 @@ int get_remote_mac(struct in_addr ip_addr, unsigned char *mac); void reload_ifaces(int notify); int OpenAndConfMonitorSocket(); -void ProcessMonitorEvent(int s); +void ProcessMonitorEvent(struct event *); #endif diff --git a/icons.c b/icons.c index 0161a2c..033a4f6 100644 --- a/icons.c +++ b/icons.c @@ -2929,3 +2929,54 @@ jpeg_lrg[] = "\xff\xd8\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01\x01\x01\x00\x5a\x "\x1e\x88\x20\xa8\xd8\xba\xe2\x7e\xdc\x26\x9a\x69\x08\xf7\x22\x22\x02\x82\x28\x82\x29\xe8\x88\x9e" "\xda\x69\xa6\x90\x8f\xff\xd9"; #endif +/* Derived from large icons above for use with MiniDLNA by Shrimpkin@SourceForge.net. + * flavicon.ico 16x16 4 bit. */ +#ifdef NETGEAR +unsigned char +favicon[] = "\x00\x00\x01\x00\x01\x00\x10\x10\x10\x00\x01\x00\x04\x00\x28\x01\x00\x00\x16\x00\x00\x00\x28\x00" + "\x00\x00\x10\x00\x00\x00\x20\x00\x00\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8e\x2d\x65\x00\x90\x23\x97\x00\x66\x44" + "\x62\x00\x4d\x51\x5e\x00\x64\x27\xeb\x00\x83\x43\x90\x00\x6d\x70\x7f\x00\xeb\xb6\x35\x00\x15\x94" + "\xf7\x00\xa3\xa5\xb3\x00\x01\xb8\xfd\x00\x63\xc1\xe0\x00\xf5\xd2\x8d\x00\x17\xdc\xff\x00\xc5\xe1" + "\xec\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x51\x11\x11\x11\xff\xff\xff\xfc" + "\x66\x11\x11\x00\x4f\xff\xff\xcc\x66\x66\x00\x00\x4f\xff\xff\xcc\x66\xff\xff\x00\x44\xff\xff\xcc" + "\x6f\x3f\xff\x2f\x44\x4f\xfc\x77\xf3\x3f\xf3\xff\x94\x4f\x77\x77\xff\x3f\x33\x3f\x99\x94\xf7\x77" + "\xff\x33\xf3\xff\x99\x9b\xf7\x77\xff\x33\xf3\x3f\x9b\xaf\xf7\xc7\xff\xff\xf3\xff\xaa\xff\xff\x77" + "\xee\xff\xff\x88\xaa\xff\xff\xf7\xee\xee\xf8\x88\xaf\xff\xff\xf7\xee\xdd\xda\x88\xaf\xff\xff\xff" + "\xbd\xdd\xdd\xda\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xf0\x0f\x00\x00\xe0\x07" + "\x00\x00\xc0\x07\x00\x00\xc3\xc3\x00\x00\xc5\xd1\x00\x00\x89\xb1\x00\x00\x0d\x10\x00\x00\x8c\xb0" + "\x00\x00\x8c\x91\x00\x00\x8f\xb3\x00\x00\xc3\xc3\x00\x00\xe0\x87\x00\x00\xe0\x07\x00\x00\xf0\x0f" + "\x00\x00\xff\xff\x00\x00"; +#elif FreeBSD +unsigned char +favicon[] = "\x00\x00\x01\x00\x01\x00\x0e\x10\x10\x00\x01\x00\x04\x00\x28\x01\x00\x00\x16\x00\x00\x00\x28\x00" + "\x00\x00\x0e\x00\x00\x00\x20\x00\x00\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x13\x1f\x00\x0a\x0c\x5d\x00\x00\x00" + "\xb3\x00\x0f\x13\x98\x00\x00\x15\xd8\x00\x43\x49\x67\x00\x3f\x40\x96\x00\x02\x39\xed\x00\x2a\x6d" + "\x99\x00\x41\x7a\xe7\x00\x75\x7f\xed\x00\x8f\x93\xae\x00\x9d\xc2\xf5\x00\xbc\xc8\xef\x00\xef\xf4" + "\xfd\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff" + "\xf6\x38\x85\xff\xff\x00\xff\xfb\x24\x44\x79\x5f\xff\x00\xff\xb2\x24\x24\x47\x9b\xff\x00\xff\x34" + "\x44\x32\x22\x2c\x0f\x00\xfb\x44\x44\x33\x22\x19\xff\x00\x0b\x77\x44\x22\x22\x33\xef\x00\xfd\x99" + "\x74\x42\x33\x31\xff\x00\xff\xce\x94\x44\x23\x6b\xbf\x00\xf5\xee\xed\xcd\xda\x26\xff\x00\xf3\x6e" + "\xee\xee\xbe\xa2\x6f\x00\xf3\xad\xee\xee\xdd\xea\xff\x00\xff\x5f\xff\xbf\xbf\x0f\x1f\x00\xff\xff" + "\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\x00\xff\xfc\x00\x00\xff\xfc\x00\x00\xf8\x3c" + "\x00\x00\xe0\x1c\x00\x00\xc0\x0c\x00\x00\xc0\x04\x00\x00\x80\x0c\x00\x00\x00\x04\x00\x00\x80\x0c" + "\x00\x00\xc0\x04\x00\x00\x80\x0c\x00\x00\x80\x04\x00\x00\x80\x0c\x00\x00\xdd\x54\x00\x00\xff\xfc" + "\x00\x00\xff\xfc\x00\x00"; +#else +unsigned char +favicon[] = "\x00\x00\x01\x00\x01\x00\x0e\x10\x10\x00\x01\x00\x04\x00\x28\x01\x00\x00\x16\x00\x00\x00\x28\x00" + "\x00\x00\x0e\x00\x00\x00\x20\x00\x00\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x02\x00\x26\x27\x25\x00\x52\x52" + "\x51\x00\x1f\x58\x6f\x00\x00\x62\x8d\x00\x72\x70\x6b\x00\x00\x76\xa6\x00\x92\x91\x8e\x00\x00\x94" + "\xcb\x00\x6f\xab\xbf\x00\x00\xaf\xe4\x00\xaa\xba\xbe\x00\x22\xcf\xfd\x00\xda\xe0\xdf\x00\xfa\xfe" + "\xfc\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\x00\xff\x34\x86\x0f\x0f\x86\xff\x00\xfc\xcc" + "\xca\x22\x14\xcc\xcf\x00\xfc\xcc\xc9\xee\xe3\xcc\xcc\x00\xfc\xcc\x67\xee\xe9\x86\xff\x00\xff\xac" + "\xde\xde\xee\x30\x3f\x00\xff\xf0\xee\xde\xee\x50\x0f\x00\xff\xf0\xde\xee\xee\x20\x0f\x00\xff\xff" + "\x2e\xee\xed\x10\xff\x00\xff\xff\x0d\xee\xd7\x0f\xff\x00\xff\xff\xf5\xbd\xe1\xff\xff\x00\xff\xff" + "\xf3\xaa\xa1\x0f\xff\x00\xff\xff\xf2\x88\xb0\xff\xff\x00\xff\xff\xf2\x77\xb0\xff\xff\x00\xff\xff" + "\xf0\x00\x00\xff\xff\x00\xff\xff\xff\x00\x1f\xff\xff\x00\xff\xfc\x00\x00\xc1\x4c\x00\x00\x80\x04" + "\x00\x00\x80\x00\x00\x00\x80\x0c\x00\x00\xc0\x04\x00\x00\xe0\x04\x00\x00\xe0\x04\x00\x00\xf0\x0c" + "\x00\x00\xf0\x1c\x00\x00\xf8\x3c\x00\x00\xf8\x1c\x00\x00\xf8\x3c\x00\x00\xf8\x3c\x00\x00\xf8\x3c" + "\x00\x00\xfc\x7c\x00\x00"; +#endif diff --git a/image_utils.c b/image_utils.c index d90875a..ac234c9 100644 --- a/image_utils.c +++ b/image_utils.c @@ -208,7 +208,7 @@ image_free(image_s *pimage) } pix -get_pix(image_s *pimage, int32_t x, int32_t y) +get_pix(const image_s *pimage, int32_t x, int32_t y) { if (x < 0) x = 0; @@ -447,6 +447,7 @@ image_new_from_jpeg(const char *path, int is_file, const uint8_t *buf, int size, } if( setjmp(setjmp_buffer) ) { + DPRINTF(E_ERROR, L_METADATA, "jpeg handling failed on %s\n", path); jpeg_destroy_decompress(&cinfo); if( is_file && file ) fclose(file); @@ -574,7 +575,7 @@ image_new_from_jpeg(const char *path, int is_file, const uint8_t *buf, int size, } void -image_upsize(image_s * pdest, image_s * psrc, int32_t width, int32_t height) +image_upsize(image_s * pdest, const image_s * psrc, int32_t width, int32_t height) { int32_t vx, vy; #if !defined __i386__ && !defined __x86_64__ @@ -637,7 +638,7 @@ image_upsize(image_s * pdest, image_s * psrc, int32_t width, int32_t height) } void -image_downsize(image_s * pdest, image_s * psrc, int32_t width, int32_t height) +image_downsize(image_s * pdest, const image_s * psrc, int32_t width, int32_t height) { int32_t vx, vy; pix vcol; @@ -730,7 +731,7 @@ image_downsize(image_s * pdest, image_s * psrc, int32_t width, int32_t height) { vcol = get_pix(psrc, ((int32_t)rx)-half_square_width+i, ((int32_t)ry)-half_square_height+j); - + if(((j == 0) || (j == (half_square_height<<1)-1)) && ((i == 0) || (i == (half_square_width<<1)-1))) { @@ -762,12 +763,12 @@ image_downsize(image_s * pdest, image_s * psrc, int32_t width, int32_t height) } } } - + red /= width_scale*height_scale; green /= width_scale*height_scale; blue /= width_scale*height_scale; alpha /= width_scale*height_scale; - + /* on sature les valeurs */ red = (red > 255.0)? 255.0 : ((red < 0.0)? 0.0:red ); green = (green > 255.0)? 255.0 : ((green < 0.0)? 0.0:green); @@ -781,14 +782,18 @@ image_downsize(image_s * pdest, image_s * psrc, int32_t width, int32_t height) } image_s * -image_resize(image_s * src_image, int32_t width, int32_t height) +image_resize(const image_s *src_image, int32_t width, int32_t height) { image_s * dst_image; dst_image = image_new(width, height); if( !dst_image ) return NULL; - if( (src_image->width < width) || (src_image->height < height) ) + + + /**/ if( (src_image->width == width) && (src_image->height == height) ) + memcpy(dst_image->buf, src_image->buf, sizeof(pix) * width * height); + else if( (src_image->width < width) || (src_image->height < height) ) image_upsize(dst_image, src_image, width, height); else image_downsize(dst_image, src_image, width, height); @@ -798,7 +803,7 @@ image_resize(image_s * src_image, int32_t width, int32_t height) unsigned char * -image_save_to_jpeg_buf(image_s * pimage, int * size) +image_save_to_jpeg_buf(const image_s * pimage, int * size) { struct jpeg_compress_struct cinfo; struct jpeg_error_mgr jerr; @@ -848,7 +853,7 @@ image_save_to_jpeg_buf(image_s * pimage, int * size) } char * -image_save_to_jpeg_file(image_s * pimage, char * path) +image_save_to_jpeg_file(const image_s * pimage, const char * path) { int nwritten, size = 0; unsigned char * buf; @@ -857,7 +862,7 @@ image_save_to_jpeg_file(image_s * pimage, char * path) buf = image_save_to_jpeg_buf(pimage, &size); if( !buf ) return NULL; - dst_file = fopen(path, "w"); + dst_file = fopen(path, "w"); if( !dst_file ) { free(buf); @@ -867,5 +872,5 @@ image_save_to_jpeg_file(image_s * pimage, char * path) fclose(dst_file); free(buf); - return (nwritten == size) ? path : NULL; + return (nwritten == size) ? (char*)path : NULL; } diff --git a/image_utils.h b/image_utils.h index 7011c73..31a4f0b 100644 --- a/image_utils.h +++ b/image_utils.h @@ -49,10 +49,10 @@ image_s * image_new_from_jpeg(const char *path, int is_file, const uint8_t *ptr, int size, int scale, int resize); image_s * -image_resize(image_s * src_image, int32_t width, int32_t height); +image_resize(const image_s * src_image, int32_t width, int32_t height); unsigned char * -image_save_to_jpeg_buf(image_s * pimage, int * size); +image_save_to_jpeg_buf(const image_s * pimage, int * size); char * -image_save_to_jpeg_file(image_s * pimage, char * path); +image_save_to_jpeg_file(const image_s * pimage, const char * path); diff --git a/inotify.h b/inotify.h deleted file mode 100644 index 0f378c2..0000000 --- a/inotify.h +++ /dev/null @@ -1,7 +0,0 @@ -#ifdef HAVE_INOTIFY -int -inotify_remove_file(const char * path); - -void * -start_inotify(); -#endif diff --git a/kqueue.c b/kqueue.c new file mode 100644 index 0000000..67b2c86 --- /dev/null +++ b/kqueue.c @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2017 Gleb Smirnoff + * Copyright (c) 2002-2017 Igor Sysoev + * Copyright (c) 2011-2017 Nginx, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "event.h" +#include "log.h" + +static int kqueue_set(struct event *, short, u_short, u_int); + +static event_module_init_t kqueue_init; +static event_module_fini_t kqueue_fini; +static event_module_add_t kqueue_add; +static event_module_del_t kqueue_del; +static event_module_process_t kqueue_process; + +static int kq; +static struct kevent *change_list; +static struct kevent *event_list; +static u_int nchanges; + +#define MAXCHANGES 128 +#define MAXEVENTS 128 + +struct event_module event_module = { + .add = kqueue_add, + .del = kqueue_del, + .process = kqueue_process, + .init = kqueue_init, + .fini = kqueue_fini, +}; + +static int +kqueue_init(void) +{ + + kq = kqueue(); + if (kq == -1) + return (errno); + + change_list = calloc(MAXCHANGES, sizeof(struct kevent)); + event_list = calloc(MAXEVENTS, sizeof(struct kevent)); + if (change_list == NULL || event_list == NULL) + return (ENOMEM); + + nchanges = 0; + + return (0); +} + +static void +kqueue_fini() +{ + + (void )close(kq); + kq = -1; + + free(change_list); + free(event_list); + change_list = NULL; + event_list = NULL; + nchanges = 0; +} + +static int +kqueue_add(struct event *ev) +{ + u_int fflags; + u_short flags; + + if (ev->rdwr == EVFILT_VNODE) { + flags = EV_ADD | EV_ENABLE | EV_CLEAR; + fflags = NOTE_DELETE | NOTE_WRITE | NOTE_EXTEND; + } else { + flags = EV_ADD | EV_ENABLE; + fflags = 0; + } + + DPRINTF(E_DEBUG, L_GENERAL, "kqueue_add %d\n", ev->fd); + return (kqueue_set(ev, ev->rdwr, flags, fflags)); +} + +static int +kqueue_del(struct event *ev, int flags) +{ + + /* + * If the event is still not passed to a kernel, + * we will not pass it. + */ + assert(ev->fd >= 0); + if (ev->index < nchanges && + change_list[ev->index].udata == ev) { + if (ev->index < --nchanges) { + struct event *ev0; + + ev0 = (struct event *)change_list[nchanges].udata; + change_list[ev->index] = change_list[nchanges]; + ev0->index = ev->index; + } + return (0); + } + + /* + * when the file descriptor is closed the kqueue automatically deletes + * its filters so we do not need to delete explicitly the event + * before the closing the file descriptor. + */ + if (flags & EV_FLAG_CLOSING) + return (0); + + DPRINTF(E_DEBUG, L_GENERAL, "kqueue_del %d\n", ev->fd); + return (kqueue_set(ev, ev->rdwr, EV_DELETE, 0)); +} + +static int +kqueue_set(struct event *ev, short filter, u_short flags, u_int fflags) +{ + struct kevent *kev; + struct timespec ts; + + if (nchanges >= MAXCHANGES) { + DPRINTF(E_INFO, L_GENERAL, "kqueue change list is filled up\n"); + + ts.tv_sec = 0; + ts.tv_nsec = 0; + + if (kevent(kq, change_list, (int) nchanges, NULL, 0, &ts) == -1) { + DPRINTF(E_ERROR, L_GENERAL,"kevent() failed: %s\n", strerror(errno)); + return (errno); + } + nchanges = 0; + } + + kev = &change_list[nchanges]; + kev->ident = ev->fd; + kev->filter = filter; + kev->flags = flags; + kev->udata = ev; + kev->fflags = fflags; + kev->data = 0; + + ev->index = nchanges++; + + return (0); +} + +static int +kqueue_process(u_long timer) +{ + struct event *ev; + int events, n, i; + struct timespec ts, *tp; + + n = (int) nchanges; + nchanges = 0; + + if (timer == 0) { + tp = NULL; + } else { + ts.tv_sec = timer / 1000; + ts.tv_nsec = (timer % 1000) * 1000000; + tp = &ts; + } + + DPRINTF(E_DEBUG, L_GENERAL, "kevent timer: %lu, changes: %d\n", + timer, n); + + events = kevent(kq, change_list, n, event_list, MAXEVENTS, tp); + + if (events == -1) { + if (errno == EINTR) + return (errno); + DPRINTF(E_FATAL, L_GENERAL, "kevent(): %s. EXITING\n", strerror(errno)); + } + + DPRINTF(E_DEBUG, L_GENERAL, "kevent events: %d\n", events); + + if (events == 0) { + if (timer != 0) + return (0); + DPRINTF(E_FATAL, L_GENERAL, "kevent() returned no events. EXITING\n"); + } + + for (i = 0; i < events; i++) { + if (event_list[i].flags & EV_ERROR) { + DPRINTF(E_ERROR, L_GENERAL, + "kevent() error %d on %d filter:%d flags:0x%x\n", + (int)event_list[i].data, (int)event_list[i].ident, + event_list[i].filter, event_list[i].flags); + continue; + } + + ev = (struct event *)event_list[i].udata; + + switch (event_list[i].filter) { + case EVFILT_READ: + case EVFILT_WRITE: + ev->process(ev); + break; + case EVFILT_VNODE: + ev->process_vnode(ev, event_list[i].fflags); + break; + default: + DPRINTF(E_ERROR, L_GENERAL, + "unexpected kevent() filter %d", + event_list[i].filter); + continue; + } + } + + return (0); +} diff --git a/libav.h b/libav.h index 25511a1..51ef15c 100644 --- a/libav.h +++ b/libav.h @@ -57,6 +57,10 @@ #include #endif +#include "log.h" + +#define USE_CODECPAR LIBAVFORMAT_VERSION_INT >= ((57<<16)+(50<<8)+100) + #ifndef FF_PROFILE_H264_BASELINE #define FF_PROFILE_H264_BASELINE 66 #endif @@ -115,6 +119,12 @@ typedef AVMetadataTag AVDictionaryEntry; # endif #endif +#ifdef PIX_FMT_RGB32_1 +#define LAV_PIX_FMT_RGB32_1 PIX_FMT_RGB32_1 +#else +#define LAV_PIX_FMT_RGB32_1 AV_PIX_FMT_RGB32_1 +#endif + static inline int lav_open(AVFormatContext **ctx, const char *filename) { @@ -166,7 +176,7 @@ lav_get_interlaced(AVStream *s) #endif } -#if LIBAVCODEC_VERSION_MAJOR >= 57 +#if USE_CODECPAR #define lav_codec_id(s) s->codecpar->codec_id #define lav_codec_type(s) s->codecpar->codec_type #define lav_codec_tag(s) s->codecpar->codec_tag @@ -195,7 +205,7 @@ lav_get_interlaced(AVStream *s) static inline uint8_t * lav_codec_extradata(AVStream *s) { -#if LIBAVCODEC_VERSION_MAJOR >= 57 +#if USE_CODECPAR if (!s->codecpar->extradata_size) return NULL; return s->codecpar->extradata; @@ -222,3 +232,132 @@ lav_is_thumbnail_stream(AVStream *s, uint8_t **data, int *size) #endif return 0; } + +static inline AVFrame* +lav_frame_alloc(void) +{ +#if LIBAVCODEC_VERSION_INT >= ((55<<16)+(28<<8)+1) + return av_frame_alloc(); +#else + return avcodec_alloc_frame(); +#endif +} + +static inline void +lav_frame_unref(AVFrame *ptr) +{ +#if LIBAVCODEC_VERSION_INT >= ((55<<16)+(28<<8)+1) + return av_frame_unref(ptr); +#else + return avcodec_get_frame_defaults(ptr); +#endif +} + +static inline void +lav_free_packet(AVPacket *pkt) +{ +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 8, 0) + return av_packet_unref(pkt); +#else + return av_free_packet(pkt); +#endif +} + +static inline int +lav_avcodec_open(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options) +{ +#if LIBAVCODEC_VERSION_INT >= ((53<<16)+(6<<8)+0) + return avcodec_open2(avctx, codec, options); +#else + return avcodec_open(avctx, codec); +#endif +} + +static inline int +lav_avcodec_decode_video(AVCodecContext *avctx, AVFrame *picture, int *got_picture_ptr, AVPacket *avpkt) +{ +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 36, 100) + int error = 0; + if((error = avcodec_send_packet(avctx, avpkt))) return error; + error = avcodec_receive_frame(avctx, picture); + + // This is a bit tricky, I guess. The new interface does not map very well + // on the old interface. When we're done with the decoder, we may want to + // want to drain it (or not), depending on what we want to do with the + // decoder next. In the old interface this did not seem necessary, or + // perhaps it happened automatically? + // In either case, it seems like this function is only used from + // video_thumb.c::get_video_packet(), which is only interested in decoding + // a single frame. So for now we don't really need to do this (assuming it + // happens on close). + // + // Note: draining should only be done once a frame has been successfully + // received, otherwise no frame will ever be received. I.e. if we start + // emptying the codec before it has had enough data to produce a frame, we + // will not get any output. + //if(!error) { + // // drain (may fail, don't care) + // avcodec_send_packet(avctx, NULL); + // AVFrame *dummy = lav_frame_alloc(); + // //while(avcodec_receive_frame(avctx, dummy) != AVERROR_EOF); + // int res = 0; + // do { + // res = avcodec_receive_frame(avctx, dummy);// XXX unref? + // char buf[128]; + // av_strerror(res, buf, 128); + // fprintf(stderr, "lav_avcodec_decode_video: res: %d - %s\n", res, buf); + // fflush(stderr); + // } while(res != AVERROR_EOF); + // lav_frame_unref(dummy); + // avcodec_flush_buffers(avctx); + //} + + if(!error) *got_picture_ptr = 1; + return error; +#elif LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(52, 23, 0) + return avcodec_decode_video2(avctx, picture, got_picture_ptr, avpkt); +#else + return avcodec_decode_video(avctx, picture, got_picture_ptr, avpkt->data, avpkt->size); +#endif +} + +static inline int +lav_avpicture_deinterlace(AVFrame *dst, const AVFrame *src, enum AVPixelFormat pix_fmt, int width, int height) +{ +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(56, 0, 0) + // The interlaced_frame flag, which video_thumb.c uses to decide whether or + // not a frame requires deinterlacing does not seem to mean what one would + // think, or at least that is what is suggested here: + // http://libav-users.943685.n4.nabble.com/Libav-user-using-avpicture-deinterlace-td4660459.html + // + // I've tried a bunch of videos, and thusfar I've not yet found any video + // which had this flag set. This is not to say it cannot happen, but rather + // that at the moment I have no way to test it. If deinterlacing is indeed + // required, a somewhat complicated setup appears to be required, to set up + // a filter graph. Until I have a test case this will need to be postponed. + DPRINTF(E_WARN, L_ARTWORK, "deinterlacing of video frames is not implemented!\n"); + return 0; +#else + return avpicture_deinterlace((AVPicture*)dst, (AVPicture*)src, pix_fmt, width, height); +#endif +} + +// Taken from https://www.ffmpeg.org/doxygen/trunk/libavfilter_2fifo_8c_source.html#l00129 +// Did not find a public version (yet). +static inline int +calc_ptr_alignment(AVFrame *frame) +{ + int planes = av_sample_fmt_is_planar(frame->format) ? + frame->channels : 1; + int min_align = 128; + int p; + + for (p = 0; p < planes; p++) { + int cur_align = 128; + while ((intptr_t)frame->extended_data[p] % cur_align) + cur_align >>= 1; + if (cur_align < min_align) + min_align = cur_align; + } + return min_align; +} diff --git a/log.c b/log.c index f80e6d4..a989904 100644 --- a/log.c +++ b/log.c @@ -63,28 +63,46 @@ log_close(void) fclose(log_fp); } -int find_matching_name(const char* str, const char* names[]) { - if (str == NULL) return -1; +void +log_reopen(void) +{ + if (log_path[0] && log_fp) + { + char logfile[1048]; + snprintf(logfile, sizeof(logfile), "%s/" LOGFILE_NAME, log_path); + fclose(log_fp); + log_fp = fopen(logfile, "a"); + DPRINTF(E_INFO, L_GENERAL, "Reopened log file\n"); + } +} - const char* start = strpbrk(str, ",="); - int level, c = (start != NULL) ? start - str : strlen(str); +int find_matching_name(const char* str, const char* names[]) +{ + const char *start; + int level, c; + + if (!str) + return -1; + + start = strpbrk(str, ",="); + c = start ? start - str : strlen(str); for (level = 0; names[level] != 0; level++) { - if (!(strncasecmp(names[level], str, c))) + if (!strncasecmp(names[level], str, c)) return level; } return -1; } int -log_init(const char *fname, const char *debug) +log_init(const char *debug) { int i; - FILE *fp; + FILE *fp = NULL; int level = find_matching_name(debug, level_name); int default_log_level = (level == -1) ? _default_log_level : level; - for (i=0; i '%q.' and PATH <= '%q.z')" - " and MIME glob 'video/*' limit 1", file, file); + " and MIME glob 'video/*' limit 1", file, file); if (detailID <= 0) { //DPRINTF(E_MAXDEBUG, L_METADATA, "No file found for caption %s.\n", path); @@ -139,54 +138,57 @@ check_for_captions(const char *path, int64_t detailID) } } - strcpy(p, ".srt"); - ret = access(file, R_OK); - if (ret != 0) - { - strcpy(p, ".smi"); - ret = access(file, R_OK); - } - - if (ret == 0) - { - sql_exec(db, "INSERT OR REPLACE into CAPTIONS" - " (ID, PATH) " - "VALUES" - " (%lld, %Q)", detailID, file); - } + const char** subtitle_format = &subtitle_formats[0]; + do { + strcpy(p, *subtitle_format); + if(access(file, R_OK) == 0) { + sql_exec(db, "INSERT OR REPLACE into CAPTIONS" + " (ID, PATH) " + "VALUES" + " (%lld, %Q)", detailID, file); + break; + } + } while(*++subtitle_format); } -void +static void parse_nfo(const char *path, metadata_t *m) { FILE *nfo; - char buf[65536]; + char *buf; struct NameValueParserData xml; struct stat file; size_t nread; char *val, *val2; - if( stat(path, &file) != 0 || - file.st_size > 65536 ) + if (stat(path, &file) != 0 || + file.st_size > 65535) { DPRINTF(E_INFO, L_METADATA, "Not parsing very large .nfo file %s\n", path); return; } - DPRINTF(E_INFO, L_METADATA, "Parsing .nfo file: %s\n", path); + DPRINTF(E_DEBUG, L_METADATA, "Parsing .nfo file: %s\n", path); + buf = calloc(1, file.st_size + 1); + if (!buf) + return; nfo = fopen(path, "r"); - if( !nfo ) + if (!nfo) + { + free(buf); return; - nread = fread(&buf, 1, sizeof(buf), nfo); - + } + nread = fread(buf, 1, file.st_size, nfo); + fclose(nfo); + ParseNameValue(buf, nread, &xml, 0); //printf("\ttype: %s\n", GetValueFromNameValueList(&xml, "rootElement")); val = GetValueFromNameValueList(&xml, "title"); - if( val ) + if (val) { char *esc_tag, *title; val2 = GetValueFromNameValueList(&xml, "episodetitle"); - if( val2 ) + if (val2) xasprintf(&title, "%s - %s", val, val2); else title = strdup(val); @@ -197,39 +199,49 @@ parse_nfo(const char *path, metadata_t *m) } val = GetValueFromNameValueList(&xml, "plot"); - if( val ) { + if (val) + { char *esc_tag = unescape_tag(val, 1); m->comment = escape_tag(esc_tag, 1); free(esc_tag); } val = GetValueFromNameValueList(&xml, "capturedate"); - if( val ) { + if (val) + { char *esc_tag = unescape_tag(val, 1); m->date = escape_tag(esc_tag, 1); free(esc_tag); } val = GetValueFromNameValueList(&xml, "genre"); - if( val ) + if (val) { - free(m->genre); char *esc_tag = unescape_tag(val, 1); + free(m->genre); m->genre = escape_tag(esc_tag, 1); free(esc_tag); } val = GetValueFromNameValueList(&xml, "mime"); - if( val ) + if (val) { - free(m->mime); char *esc_tag = unescape_tag(val, 1); + free(m->mime); m->mime = escape_tag(esc_tag, 1); free(esc_tag); } + val = GetValueFromNameValueList(&xml, "season"); + if (val) + m->disc = atoi(val); + + val = GetValueFromNameValueList(&xml, "episode"); + if (val) + m->track = atoi(val); + ClearNameValueList(&xml); - fclose(nfo); + free(buf); } void @@ -278,7 +290,7 @@ GetFolderMetadata(const char *name, const char *path, const char *artist, const } int64_t -GetAudioMetadata(const char *path, char *name) +GetAudioMetadata(const char *path, const char *name) { char type[4]; static char lang[6] = { '\0' }; @@ -294,7 +306,6 @@ GetAudioMetadata(const char *path, char *name) if ( stat(path, &file) != 0 ) return 0; - strip_ext(name); if( ends_with(path, ".mp3") ) { @@ -337,6 +348,16 @@ GetAudioMetadata(const char *path, char *name) strcpy(type, "pcm"); m.mime = strdup("audio/L16"); } + else if( ends_with(path, ".dsf") ) + { + strcpy(type, "dsf"); + m.mime = strdup("audio/x-dsd"); + } + else if( ends_with(path, ".dff") ) + { + strcpy(type, "dff"); + m.mime = strdup("audio/x-dsd"); + } else { DPRINTF(E_WARN, L_METADATA, "Unhandled file extension on %s\n", path); @@ -354,7 +375,7 @@ GetAudioMetadata(const char *path, char *name) if( readtags((char *)path, &song, &file, lang, type) != 0 ) { DPRINTF(E_WARN, L_METADATA, "Cannot extract tags from %s!\n", path); - freetags(&song); + freetags(&song); free_metadata(&m, free_flags); return 0; } @@ -363,11 +384,7 @@ GetAudioMetadata(const char *path, char *name) m.dlna_pn = strdup(song.dlna_pn); if( song.year ) xasprintf(&m.date, "%04d-01-01", song.year); - xasprintf(&m.duration, "%d:%02d:%02d.%03d", - (song.song_length/3600000), - (song.song_length/60000%60), - (song.song_length/1000%60), - (song.song_length%1000)); + m.duration = duration_str(song.song_length); if( song.title && *song.title ) { m.title = trim(song.title); @@ -379,7 +396,9 @@ GetAudioMetadata(const char *path, char *name) } else { - m.title = name; + free_flags |= FLAG_TITLE; + m.title = strdup(name); + strip_ext(m.title); } for( i = ROLE_START; i < N_ROLE; i++ ) { @@ -409,7 +428,7 @@ GetAudioMetadata(const char *path, char *name) if( song.contributor[i] && *song.contributor[i] ) break; } - if( i <= ROLE_BAND ) + if( i <= ROLE_BAND ) { m.artist = trim(song.contributor[i]); if( strlen(m.artist) > 48 ) @@ -471,7 +490,7 @@ GetAudioMetadata(const char *path, char *name) { ret = sqlite3_last_insert_rowid(db); } - freetags(&song); + freetags(&song); free_metadata(&m, free_flags); return ret; @@ -488,7 +507,7 @@ libjpeg_error_handler(j_common_ptr cinfo) } int64_t -GetImageMetadata(const char *path, char *name) +GetImageMetadata(const char *path, const char *name) { ExifData *ed; ExifEntry *e = NULL; @@ -509,7 +528,6 @@ GetImageMetadata(const char *path, char *name) //DEBUG DPRINTF(E_DEBUG, L_METADATA, "Parsing %s...\n", path); if ( stat(path, &file) != 0 ) return 0; - strip_ext(name); //DEBUG DPRINTF(E_DEBUG, L_METADATA, " * size: %jd\n", file.st_size); /* MIME hard-coded to JPEG for now, until we add PNG support */ @@ -586,7 +604,7 @@ GetImageMetadata(const char *path, char *name) imsrc = image_new_from_jpeg(NULL, 0, ed->data, ed->size, 1, ROTATE_NONE); if( imsrc ) { - if( (imsrc->width <= 160) && (imsrc->height <= 160) ) + if( (imsrc->width <= 160) && (imsrc->height <= 160) ) thumb = 1; image_free(imsrc); } @@ -634,13 +652,15 @@ GetImageMetadata(const char *path, char *name) else if( (width <= 4096 && height <= 4096) || !GETFLAG(DLNA_STRICT_MASK) ) m.dlna_pn = strdup("JPEG_LRG"); xasprintf(&m.resolution, "%dx%d", width, height); + m.title = strdup(name); + strip_ext(m.title); ret = sql_exec(db, "INSERT into DETAILS" " (PATH, TITLE, SIZE, TIMESTAMP, DATE, RESOLUTION," " ROTATION, THUMBNAIL, CREATOR, DLNA_PN, MIME) " "VALUES" " (%Q, '%q', %lld, %lld, %Q, %Q, %u, %d, %Q, %Q, %Q);", - path, name, (long long)file.st_size, (long long)file.st_mtime, m.date, + path, m.title, (long long)file.st_size, (long long)file.st_mtime, m.date, m.resolution, m.rotation, thumb, m.creator, m.dlna_pn, m.mime); if( ret != SQLITE_OK ) { @@ -657,7 +677,7 @@ GetImageMetadata(const char *path, char *name) } int64_t -GetVideoMetadata(const char *path, char *name) +GetVideoMetadata(const char *path, const char *name) { struct stat file; int ret, i; @@ -680,7 +700,6 @@ GetVideoMetadata(const char *path, char *name) //DEBUG DPRINTF(E_DEBUG, L_METADATA, "Parsing video %s...\n", name); if ( stat(path, &file) != 0 ) return 0; - strip_ext(name); //DEBUG DPRINTF(E_DEBUG, L_METADATA, " * size: %jd\n", file.st_size); ret = lav_open(&ctx, path); @@ -806,20 +825,13 @@ GetVideoMetadata(const char *path, char *name) if( vstream ) { int off; - int duration, hours, min, sec, ms; ts_timestamp_t ts_timestamp = NONE; DPRINTF(E_DEBUG, L_METADATA, "Container: '%s' [%s]\n", ctx->iformat->name, basepath); xasprintf(&m.resolution, "%dx%d", lav_width(vstream), lav_height(vstream)); if( ctx->bit_rate > 8 ) m.bitrate = ctx->bit_rate / 8; - if( ctx->duration > 0 ) { - duration = (int)(ctx->duration / AV_TIME_BASE); - hours = (int)(duration / 3600); - min = (int)(duration / 60 % 60); - sec = (int)(duration % 60); - ms = (int)(ctx->duration / (AV_TIME_BASE/1000) % 1000); - xasprintf(&m.duration, "%d:%02d:%02d.%03d", hours, min, sec, ms); - } + if( ctx->duration > 0 ) + m.duration = duration_str(ctx->duration / (AV_TIME_BASE/1000)); /* NOTE: The DLNA spec only provides for ASF (WMV), TS, PS, and MP4 containers. * Skip DLNA parsing for everything else. */ @@ -1273,8 +1285,8 @@ GetVideoMetadata(const char *path, char *name) off += sprintf(m.dlna_pn+off, "3GPP_SP_L0B_AMR"); break; default: - DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for MPEG4-P2 3GP/0x%X file %s\n", - lav_codec_id(astream), basepath); + DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for MPEG4-P2 3GP/%d file %s\n", + audio_profile, basepath); free(m.dlna_pn); m.dlna_pn = NULL; break; @@ -1500,10 +1512,8 @@ GetVideoMetadata(const char *path, char *name) if( ext ) { strcpy(ext+1, "nfo"); - if( access(nfo, F_OK) == 0 ) - { + if( access(nfo, R_OK) == 0 ) parse_nfo(nfo, &m); - } } if( !m.mime ) @@ -1535,7 +1545,34 @@ GetVideoMetadata(const char *path, char *name) } if( !m.title ) + { m.title = strdup(name); + strip_ext(m.title); + } + + if (!m.disc && !m.track) + { + /* Search for Season and Episode in the filename */ + char *p = (char*)name, *s; + while ((s = strpbrk(p, "Ss"))) + { + unsigned season = strtoul(s+1, &p, 10); + unsigned episode = 0; + if (season > 0 && p) + { + while (isblank(*p) || ispunct(*p)) + p++; + if (*p == 'E' || *p == 'e') + episode = strtoul(p+1, NULL, 10); + } + if (season && episode) + { + m.disc = season; + m.track = episode; + } + p = s + 1; + } + } album_art = find_album_art(path, m.thumb_data, m.thumb_size); freetags(&video); @@ -1543,13 +1580,13 @@ GetVideoMetadata(const char *path, char *name) ret = sql_exec(db, "INSERT into DETAILS" " (PATH, SIZE, TIMESTAMP, DURATION, DATE, CHANNELS, BITRATE, SAMPLERATE, RESOLUTION," - " TITLE, CREATOR, ARTIST, GENRE, COMMENT, DLNA_PN, MIME, ALBUM_ART) " + " TITLE, CREATOR, ARTIST, GENRE, COMMENT, DLNA_PN, MIME, ALBUM_ART, DISC, TRACK) " "VALUES" - " (%Q, %lld, %lld, %Q, %Q, %u, %u, %u, %Q, '%q', %Q, %Q, %Q, %Q, %Q, '%q', %lld);", + " (%Q, %lld, %lld, %Q, %Q, %u, %u, %u, %Q, '%q', %Q, %Q, %Q, %Q, %Q, '%q', %lld, %u, %u);", path, (long long)file.st_size, (long long)file.st_mtime, m.duration, m.date, m.channels, m.bitrate, m.frequency, m.resolution, - m.title, m.creator, m.artist, m.genre, m.comment, m.dlna_pn, - m.mime, album_art); + m.title, m.creator, m.artist, m.genre, m.comment, m.dlna_pn, + m.mime, album_art, m.disc, m.track); if( ret != SQLITE_OK ) { DPRINTF(E_ERROR, L_METADATA, "Error inserting details for '%s'!\n", path); diff --git a/metadata.h b/metadata.h index a7ceee6..3add273 100644 --- a/metadata.h +++ b/metadata.h @@ -92,12 +92,12 @@ int64_t GetFolderMetadata(const char *name, const char *path, const char *artist, const char *genre, int64_t album_art); int64_t -GetAudioMetadata(const char *path, char *name); +GetAudioMetadata(const char *path, const char *name); int64_t -GetImageMetadata(const char *path, char *name); +GetImageMetadata(const char *path, const char *name); int64_t -GetVideoMetadata(const char *path, char *name); +GetVideoMetadata(const char *path, const char *name); #endif diff --git a/minidlna.c b/minidlna.c index 0236e06..1a3d07b 100644 --- a/minidlna.c +++ b/minidlna.c @@ -68,6 +68,7 @@ #include #include #include +#include #include "config.h" @@ -76,6 +77,7 @@ #include #endif +#include "event.h" #include "upnpglobalvars.h" #include "sql.h" #include "upnphttp.h" @@ -90,16 +92,20 @@ #include "process.h" #include "upnpevents.h" #include "scanner.h" -#include "inotify.h" +#include "monitor.h" +#include "libav.h" #include "log.h" #include "tivo_beacon.h" #include "tivo_utils.h" +#include "avahi.h" #if SQLITE_VERSION_NUMBER < 3005001 # warning "Your SQLite3 library appears to be too old! Please use 3.5.1 or newer." # define sqlite3_threadsafe() 0 #endif - + +static LIST_HEAD(httplisthead, upnphttp) upnphttphead; + /* OpenAndConfHTTPSocket() : * setup the socket used to handle incoming HTTP connections. */ static int @@ -134,7 +140,7 @@ OpenAndConfHTTPSocket(unsigned short port) return -1; } - if (listen(s, 6) < 0) + if (listen(s, 16) < 0) { DPRINTF(E_ERROR, L_GENERAL, "listen(http): %s\n", strerror(errno)); close(s); @@ -144,7 +150,47 @@ OpenAndConfHTTPSocket(unsigned short port) return s; } -/* Handler for the SIGTERM signal (kill) +/* ProcessListen() : + * accept incoming HTTP connection. */ +static void +ProcessListen(struct event *ev) +{ + int shttp; + socklen_t clientnamelen; + struct sockaddr_in clientname; + clientnamelen = sizeof(struct sockaddr_in); + + shttp = accept(ev->fd, (struct sockaddr *)&clientname, &clientnamelen); + if (shttp<0) + { + DPRINTF(E_ERROR, L_GENERAL, "accept(http): %s\n", strerror(errno)); + } + else + { + struct upnphttp * tmp = 0; + DPRINTF(E_DEBUG, L_GENERAL, "HTTP connection from %s:%d\n", + inet_ntoa(clientname.sin_addr), + ntohs(clientname.sin_port) ); + /*if (fcntl(shttp, F_SETFL, O_NONBLOCK) < 0) { + DPRINTF(E_ERROR, L_GENERAL, "fcntl F_SETFL, O_NONBLOCK\n"); + }*/ + /* Create a new upnphttp object and add it to + * the active upnphttp object list */ + tmp = New_upnphttp(shttp); + if (tmp) + { + tmp->clientaddr = clientname.sin_addr; + LIST_INSERT_HEAD(&upnphttphead, tmp, entries); + } + else + { + DPRINTF(E_ERROR, L_GENERAL, "New_upnphttp() failed\n"); + close(shttp); + } + } +} + +/* Handler for the SIGTERM signal (kill) * SIGINT is also handled */ static void sigterm(int sig) @@ -169,9 +215,10 @@ static void sighup(int sig) { signal(sig, sighup); - DPRINTF(E_WARN, L_GENERAL, "received signal %d, re-read\n", sig); + DPRINTF(E_WARN, L_GENERAL, "received signal %d, reloading\n", sig); reload_ifaces(1); + log_reopen(); } /* record the startup time */ @@ -244,8 +291,7 @@ getfriendlyname(char *buf, int len) #ifndef STATIC // Disable for static linking if (!logname) { - struct passwd * pwent; - pwent = getpwuid(getuid()); + struct passwd *pwent = getpwuid(geteuid()); if (pwent) logname = pwent->pw_name; } @@ -254,31 +300,6 @@ getfriendlyname(char *buf, int len) #endif } -static int -open_db(sqlite3 **sq3) -{ - char path[PATH_MAX]; - int new_db = 0; - - snprintf(path, sizeof(path), "%s/files.db", db_path); - if (access(path, F_OK) != 0) - { - new_db = 1; - make_dir(db_path, S_ISVTX|S_IRWXU|S_IRWXG|S_IRWXO); - } - if (sqlite3_open(path, &db) != SQLITE_OK) - DPRINTF(E_FATAL, L_GENERAL, "ERROR: Failed to open sqlite database! Exiting...\n"); - if (sq3) - *sq3 = db; - sqlite3_busy_timeout(db, 5000); - sql_exec(db, "pragma page_size = 4096"); - sql_exec(db, "pragma journal_mode = OFF"); - sql_exec(db, "pragma synchronous = OFF;"); - sql_exec(db, "pragma default_cache_size = 8192;"); - - return new_db; -} - static struct media_dir_s* ParseUPNPMediaDir(const char *media_option) { media_types type = ALL_MEDIA; @@ -343,7 +364,7 @@ ParseUPNPMediaDir(const char *media_option) { break; case 'P': case 'p': - type = TYPE_IMAGES; + type = TYPE_IMAGE; break; } } @@ -364,10 +385,22 @@ ParseUPNPMediaDir(const char *media_option) { } else { fprintf(stderr, "Media directory option is empty string!\n"); } - + return this_dir; } +static time_t +_get_dbtime(void) +{ + char path[PATH_MAX]; + struct stat st; + + snprintf(path, sizeof(path), "%s/files.db", db_path); + if (stat(path, &st) != 0) + return 0; + return st.st_mtime; +} + static void check_db(sqlite3 *db, int new_db, pid_t *scanner_pid) { @@ -383,7 +416,7 @@ check_db(sqlite3 *db, int new_db, pid_t *scanner_pid) media_path = media_dirs; while (media_path) { - ret = sql_get_int_field(db, "SELECT TIMESTAMP from DETAILS where PATH = %Q AND TIMESTAMP != '' ", media_path->path); + ret = sql_get_int_field(db, "SELECT TIMESTAMP as TYPE from DETAILS where PATH = %Q AND TIMESTAMP != '' ", media_path->path); if (ret != media_path->types) { ret = 1; @@ -416,14 +449,15 @@ check_db(sqlite3 *db, int new_db, pid_t *scanner_pid) if (ret != 0) { rescan: + CLEARFLAG(RESCAN_MASK); if (ret < 0) DPRINTF(E_WARN, L_GENERAL, "Creating new database at %s/files.db\n", db_path); else if (ret == 1) - DPRINTF(E_WARN, L_GENERAL, "New media_dir '%s' detected; rescanning...\n", media_path->path); + DPRINTF(E_WARN, L_GENERAL, "New media_dir detected; rebuilding...\n"); else if (ret == 2) - DPRINTF(E_WARN, L_GENERAL, "Removed media_dir detected; rescanning...\n"); + DPRINTF(E_WARN, L_GENERAL, "Removed media_dir detected; rebuilding...\n"); else - DPRINTF(E_WARN, L_GENERAL, "Database version mismatch (%d=>%d); need to recreate...\n", + DPRINTF(E_WARN, L_GENERAL, "Database version mismatch (%d => %d); need to recreate...\n", ret, DB_VERSION); sqlite3_close(db); @@ -434,27 +468,10 @@ check_db(sqlite3 *db, int new_db, pid_t *scanner_pid) open_db(&db); if (CreateDatabase() != 0) DPRINTF(E_FATAL, L_GENERAL, "ERROR: Failed to create sqlite database! Exiting...\n"); -#if USE_FORK - scanning = 1; - sqlite3_close(db); - *scanner_pid = fork(); - open_db(&db); - if (*scanner_pid == 0) /* child (scanner) process */ - { - start_scanner(); - sqlite3_close(db); - log_close(); - freeoptions(); - free(children); - exit(EXIT_SUCCESS); - } - else if (*scanner_pid < 0) - { - start_scanner(); - } -#else + } + if (ret || GETFLAG(RESCAN_MASK)) + { start_scanner(); -#endif } } @@ -496,7 +513,7 @@ writepidfile(const char *fname, int pid, uid_t uid) dir, strerror(errno)); } } - + pidfile = fopen(fname, "w"); if (!pidfile) { @@ -507,7 +524,7 @@ writepidfile(const char *fname, int pid, uid_t uid) if (fprintf(pidfile, "%d\n", pid) <= 0) { - DPRINTF(E_ERROR, L_GENERAL, + DPRINTF(E_ERROR, L_GENERAL, "Unable to write to pidfile %s: %s\n", fname, strerror(errno)); ret = -1; } @@ -552,6 +569,55 @@ static void init_nls(void) #endif } +void parse_location_url_overrides(const char* location_url_overrides) { + if(!location_url_overrides) return; + + char* list = strdup(location_url_overrides); + size_t ifaces = 0; + for(char *string = list, *word = NULL; (word = strtok(string, ",")); string = NULL) { + if(ifaces >= MAX_LAN_ADDR) { + DPRINTF(E_ERROR, L_GENERAL, "Too many interfaces in location override (max: %d), ignoring %s\n", MAX_LAN_ADDR, word); + break; + } + while(isspace(*word++)); + char* sep = strchr(--word, ':'); + if(!sep) { + DPRINTF(E_ERROR, L_GENERAL, "Invalid syntax in location override: '%s' is missing a ':'\n", word); + break; + } + *sep = 0; + char* ifname = word; + char* override = ++sep; + size_t override_len = strlen(override); + if(override_len == 0 ) { + DPRINTF(E_ERROR, L_GENERAL, "Invalid syntax in location override: empty override string for '%s'\n", ifname); + break; + } + while(override_len && override[override_len-1] == '/') { + // strip trailing '/', they are added back elsewhere in the code. + override[--override_len] = 0; + } + if(strcmp("http://", override) == 0) { + DPRINTF(E_WARN, L_GENERAL, "Note: location override '%s' does not start with 'http://'\n", override); + } + // locate the interface in runtime_vars.ifaces (lan_addrs has the same indexes) + size_t index = MAX_LAN_ADDR; + for(size_t i = 0; i < MAX_LAN_ADDR && runtime_vars.ifaces[i]; ++i) { + DPRINTF(E_DEBUG, L_GENERAL, "location override: runtime_vars.ifaces[%zu]='%s'\n", i, runtime_vars.ifaces[i]); + if(strcmp(runtime_vars.ifaces[i], ifname) == 0) { + index = i; + } + } + if(index == MAX_LAN_ADDR) { + DPRINTF(E_ERROR, L_GENERAL, "Could not locate interface '%s' for location override\n", ifname); + break; + } + DPRINTF(E_DEBUG, L_GENERAL, "Using location override '%s' for interface %zu ('%s')\n", override, index, ifname); + set_location_url_by_lan_addr(index, override); + } + free(list); +} + /* init phase : * 1) read configuration file * 2) read command line arguments @@ -570,6 +636,7 @@ init(int argc, char **argv) int options_flag = 0; struct sigaction sa; const char * presurl = NULL; + const char * location_url_overrides = NULL; const char * optionsfile = "/etc/minidlna.conf"; char mac_str[13]; char *string, *word; @@ -580,6 +647,8 @@ init(int argc, char **argv) char *log_level = NULL; int ifaces = 0; uid_t uid = 0; + gid_t gid = 0; + int error; /* first check if "-f" option is used */ for (i=2; inext = media_dirs; @@ -695,16 +781,15 @@ init(int argc, char **argv) make_dir(path, S_ISVTX|S_IRWXU|S_IRWXG|S_IRWXO); if (access(path, F_OK) != 0) DPRINTF(E_FATAL, L_GENERAL, "Database path not accessible! [%s]\n", path); - strncpyt(db_path, path, PATH_MAX); + strncpyt(db_path, path, sizeof(db_path)); break; case UPNPLOGDIR: path = realpath(ary_options[i].value, buf); if (!path) - path = (ary_options[i].value); - make_dir(path, S_ISVTX|S_IRWXU|S_IRWXG|S_IRWXO); - if (access(path, F_OK) != 0) - DPRINTF(E_FATAL, L_GENERAL, "Log path not accessible! [%s]\n", path); - strncpyt(log_path, path, PATH_MAX); + path = ary_options[i].value; + if (snprintf(log_path, sizeof(log_path), "%s", path) > sizeof(log_path)) + DPRINTF(E_FATAL, L_GENERAL, "Log path too long! [%s]\n", path); + make_dir(log_path, S_ISVTX|S_IRWXU|S_IRWXG|S_IRWXO); break; case UPNPLOGLEVEL: log_level = ary_options[i].value; @@ -765,10 +850,17 @@ init(int argc, char **argv) DPRINTF(E_FATAL, L_GENERAL, "Bad user '%s'.\n", ary_options[i].value); uid = entry->pw_uid; + if (!gid) + gid = entry->pw_gid; } break; case FORCE_SORT_CRITERIA: force_sort_criteria = ary_options[i].value; + if (force_sort_criteria[0] == '!') + { + SETFLAG(FORCE_ALPHASORT_MASK); + force_sort_criteria++; + } break; case MAX_CONNECTIONS: runtime_vars.max_connections = atoi(ary_options[i].value); @@ -781,20 +873,38 @@ init(int argc, char **argv) if (strtobool(ary_options[i].value)) SETFLAG(WIDE_LINKS_MASK); break; + case TIVO_DISCOVERY: + if (strcasecmp(ary_options[i].value, "beacon") == 0) + CLEARFLAG(TIVO_BONJOUR_MASK); + break; +#ifdef ENABLE_VIDEO_THUMB + case ENABLE_THUMB: + if( (strcmp(ary_options[i].value, "yes") == 0) || atoi(ary_options[i].value) ) + SETFLAG(THUMB_MASK); + break; + case THUMB_WIDTH: + runtime_vars.thumb_width = atoi(ary_options[i].value); + break; +#endif + case ENABLE_MTA: + runtime_vars.mta = atoi(ary_options[i].value); + break; + case ENABLE_SUBTITLES: + if (!strtobool(ary_options[i].value)) + CLEARFLAG(SUBTITLES_MASK); + break; default: DPRINTF(E_ERROR, L_GENERAL, "Unknown option in file %s\n", optionsfile); } } - if (log_path[0] == '\0') - { - if (db_path[0] == '\0') - strncpyt(log_path, DEFAULT_LOG_PATH, PATH_MAX); - else - strncpyt(log_path, db_path, PATH_MAX); - } - if (db_path[0] == '\0') - strncpyt(db_path, DEFAULT_DB_PATH, PATH_MAX); + + if (!log_path[0]) + strncpyt(log_path, DEFAULT_LOG_PATH, sizeof(log_path)); + if (!db_path[0]) + strncpyt(db_path, DEFAULT_DB_PATH, sizeof(db_path)); + if (!icon_path[0]) + strncpyt(icon_path, DEFAULT_ICON_PATH, sizeof(icon_path)); /* command line arguments processing */ for (i=1; ipw_uid; + if (!gid) + gid = entry->pw_gid; } } else DPRINTF(E_FATAL, L_GENERAL, "Option -%c takes one argument.\n", argv[i][1]); break; + case 'g': + if (i+1 != argc) + { + i++; + gid = strtoul(argv[i], &string, 0); + if (*string) + { + /* Symbolic group given, not GID. */ + struct group *grp = getgrnam(argv[i]); + if (!grp) + DPRINTF(E_FATAL, L_GENERAL, "Bad group '%s'.\n", argv[i]); + gid = grp->gr_gid; + } + } + else + DPRINTF(E_FATAL, L_GENERAL, "Option -%c takes one argument.\n", argv[i][1]); break; #ifdef __linux__ case 'S': @@ -922,13 +1058,13 @@ init(int argc, char **argv) { printf("Usage:\n\t" "%s [-d] [-v] [-f config_file] [-p port]\n" - "\t\t[-i network_interface] [-u uid_to_run_as]\n" + "\t\t[-i network_interface] [-u uid_to_run_as] [-g group_to_run_as]\n" "\t\t[-t notify_interval] [-P pid_filename]\n" "\t\t[-s serial] [-m model_number]\n" #ifdef __linux__ - "\t\t[-w url] [-R] [-L] [-S] [-V] [-h]\n" + "\t\t[-w url] [-l] [-r] [-R] [-L] [-S] [-V] [-h]\n" #else - "\t\t[-w url] [-R] [-L] [-V] [-h]\n" + "\t\t[-w url] [-l] [-r] [-R] [-L] [-V] [-h]\n" #endif "\nNotes:\n\tNotify interval is in seconds. Default is 895 seconds.\n" "\tDefault pid file is %s.\n" @@ -936,7 +1072,9 @@ init(int argc, char **argv) "\t-w sets the presentation url. Default is http address on port 80\n" "\t-v enables verbose output\n" "\t-h displays this text\n" - "\t-R forces a full rescan\n" + "\t-l configures ssdp-location overrides\n" + "\t-r forces a rescan\n" + "\t-R forces a rebuild\n" "\t-L do not create playlists\n" #ifdef __linux__ "\t-S changes behaviour for systemd\n" @@ -954,38 +1092,31 @@ init(int argc, char **argv) else if (!log_level) log_level = log_str; - /* Set the default log file path to NULL (stdout) */ - path = NULL; + /* Set the default log to stdout */ if (debug_flag) { pid = getpid(); strcpy(log_str+65, "maxdebug"); log_level = log_str; + log_path[0] = '\0'; } else if (GETFLAG(SYSTEMD_MASK)) { pid = getpid(); + log_path[0] = '\0'; } else { pid = process_daemonize(); - #ifdef READYNAS - unlink("/ramfs/.upnp-av_scan"); - path = "/var/log/upnp-av.log"; - #else if (access(db_path, F_OK) != 0) make_dir(db_path, S_ISVTX|S_IRWXU|S_IRWXG|S_IRWXO); - snprintf(buf, sizeof(buf), "%s/minidlna.log", log_path); - path = buf; - #endif } - log_init(path, log_level); + if (log_init(log_level) < 0) + DPRINTF(E_FATAL, L_GENERAL, "Failed to open log file '%s/" LOGFILE_NAME "': %s\n", + log_path, strerror(errno)); if (process_check_if_running(pidfilename) < 0) - { - DPRINTF(E_ERROR, L_GENERAL, SERVER_NAME " is already running. EXITING.\n"); - return 1; - } + DPRINTF(E_FATAL, L_GENERAL, SERVER_NAME " is already running. EXITING.\n"); set_startup_time(); @@ -995,6 +1126,13 @@ init(int argc, char **argv) else strcpy(presentationurl, "/"); + /** + * location overrides + * + * This is here because it depends on runtime_vars.ifaces[] being intialised. + */ + parse_location_url_overrides(location_url_overrides); + /* set signal handlers */ memset(&sa, 0, sizeof(struct sigaction)); sa.sa_handler = sigterm; @@ -1006,6 +1144,8 @@ init(int argc, char **argv) DPRINTF(E_FATAL, L_GENERAL, "Failed to set %s handler. EXITING.\n", "SIGPIPE"); if (signal(SIGHUP, &sighup) == SIG_ERR) DPRINTF(E_FATAL, L_GENERAL, "Failed to set %s handler. EXITING.\n", "SIGHUP"); + if (signal(SIGUSR2, SIG_IGN) == SIG_ERR) + DPRINTF(E_FATAL, L_GENERAL, "Failed to set %s handler. EXITING.\n", "SIGUSR2"); signal(SIGUSR1, &sigusr1); sa.sa_handler = process_handle_child_termination; if (sigaction(SIGCHLD, &sa, NULL)) @@ -1022,6 +1162,10 @@ init(int argc, char **argv) db_path, uid, strerror(errno)); } + if (gid > 0 && setgid(gid) == -1) + DPRINTF(E_FATAL, L_GENERAL, "Failed to switch to gid '%d'. [%s] EXITING.\n", + gid, strerror(errno)); + if (uid > 0 && setuid(uid) == -1) DPRINTF(E_FATAL, L_GENERAL, "Failed to switch to uid '%d'. [%s] EXITING.\n", uid, strerror(errno)); @@ -1033,6 +1177,10 @@ init(int argc, char **argv) return 1; } + if ((error = event_module.init()) != 0) + DPRINTF(E_FATAL, L_GENERAL, "Failed to init event module. " + "[%s] EXITING.\n", strerror(error)); + return 0; } @@ -1044,22 +1192,20 @@ main(int argc, char **argv) int ret, i; int shttpl = -1; int smonitor = -1; - LIST_HEAD(httplisthead, upnphttp) upnphttphead; struct upnphttp * e = 0; struct upnphttp * next; - fd_set readset; /* for select() */ - fd_set writeset; - struct timeval timeout, timeofday, lastnotifytime = {0, 0}; - time_t lastupdatetime = 0; - int max_fd = -1; + struct timeval tv, timeofday, lastnotifytime = {0, 0}; + time_t lastupdatetime = 0, lastdbtime = 0; + u_long timeout; /* in milliseconds */ int last_changecnt = 0; - pid_t scanner_pid = 0; pthread_t inotify_thread = 0; + struct event ssdpev, httpev, monev; #ifdef TIVO_SUPPORT uint8_t beacon_interval = 5; int sbeacon = -1; struct sockaddr_in tivo_bcast; struct timeval lastbeacontime = {0, 0}; + struct event beaconev; #endif for (i = 0; i < L_MAX; i++) @@ -1070,6 +1216,12 @@ main(int argc, char **argv) return 1; init_nls(); + // We always need to register with LibAV; we may end up as the one running + // the scanner, generating thumbnails, etc. if the scanner's fork() fails, + // USE_FORK is not set, or inotify/kqueue detect a change, etc. + av_register_all(); + av_log_set_level(AV_LOG_PANIC); + DPRINTF(E_WARN, L_GENERAL, "Starting " SERVER_NAME " version " MINIDLNA_VERSION ".\n"); if (sqlite3_libversion_number() < 3005001) { @@ -1086,6 +1238,7 @@ main(int argc, char **argv) ret = -1; } check_db(db, ret, &scanner_pid); + lastdbtime = _get_dbtime(); #ifdef HAVE_INOTIFY if( GETFLAG(INOTIFY_MASK) ) { @@ -1095,8 +1248,18 @@ main(int argc, char **argv) else if (pthread_create(&inotify_thread, NULL, start_inotify, NULL) != 0) DPRINTF(E_FATAL, L_GENERAL, "ERROR: pthread_create() failed for start_inotify. EXITING\n"); } -#endif +#endif /* HAVE_INOTIFY */ + +#ifdef HAVE_KQUEUE + kqueue_monitor_start(); +#endif /* HAVE_KQUEUE */ + smonitor = OpenAndConfMonitorSocket(); + if (smonitor > 0) + { + monev = (struct event ){ .fd = smonitor, .rdwr = EVENT_READ, .process = ProcessMonitorEvent }; + event_module.add(&monev); + } sssdp = OpenAndConfSSDPReceiveSocket(); if (sssdp < 0) @@ -1106,11 +1269,19 @@ main(int argc, char **argv) if (SubmitServicesToMiniSSDPD(lan_addr[0].str, runtime_vars.port) < 0) DPRINTF(E_FATAL, L_GENERAL, "Failed to connect to MiniSSDPd. EXITING"); } + else + { + ssdpev = (struct event ){ .fd = sssdp, .rdwr = EVENT_READ, .process = ProcessSSDPRequest }; + event_module.add(&ssdpev); + } + /* open socket for HTTP connections. */ shttpl = OpenAndConfHTTPSocket(runtime_vars.port); if (shttpl < 0) DPRINTF(E_FATAL, L_GENERAL, "Failed to open socket for HTTP. EXITING\n"); DPRINTF(E_WARN, L_GENERAL, "HTTP listening on port %d\n", runtime_vars.port); + httpev = (struct event ){ .fd = shttpl, .rdwr = EVENT_READ, .process = ProcessListen }; + event_module.add(&httpev); #ifdef TIVO_SUPPORT if (GETFLAG(TIVO_MASK)) @@ -1120,14 +1291,23 @@ main(int argc, char **argv) ret = sqlite3_create_function(db, "tivorandom", 1, SQLITE_UTF8, NULL, &TiVoRandomSeedFunc, NULL, NULL); if (ret != SQLITE_OK) DPRINTF(E_ERROR, L_TIVO, "ERROR: Failed to add sqlite randomize function for TiVo!\n"); - /* open socket for sending Tivo notifications */ - sbeacon = OpenAndConfTivoBeaconSocket(); - if(sbeacon < 0) - DPRINTF(E_FATAL, L_GENERAL, "Failed to open sockets for sending Tivo beacon notify " - "messages. EXITING\n"); - tivo_bcast.sin_family = AF_INET; - tivo_bcast.sin_addr.s_addr = htonl(getBcastAddress()); - tivo_bcast.sin_port = htons(2190); + if (GETFLAG(TIVO_BONJOUR_MASK)) + { + tivo_bonjour_register(); + } + else + { + /* open socket for sending Tivo notifications */ + sbeacon = OpenAndConfTivoBeaconSocket(); + if(sbeacon < 0) + DPRINTF(E_FATAL, L_GENERAL, "Failed to open sockets for sending Tivo beacon notify " + "messages. EXITING\n"); + beaconev = (struct event ){ .fd = sbeacon, .rdwr = EVENT_READ, .process = ProcessTiVoBeacon }; + event_module.add(&beaconev); + tivo_bcast.sin_family = AF_INET; + tivo_bcast.sin_addr.s_addr = htonl(getBcastAddress()); + tivo_bcast.sin_port = htons(2190); + } } #endif @@ -1137,145 +1317,102 @@ main(int argc, char **argv) /* main loop */ while (!quitting) { + if (gettimeofday(&timeofday, 0) < 0) + DPRINTF(E_FATAL, L_GENERAL, "gettimeofday(): %s\n", strerror(errno)); /* Check if we need to send SSDP NOTIFY messages and do it if * needed */ - if (gettimeofday(&timeofday, 0) < 0) + tv = lastnotifytime; + tv.tv_sec += runtime_vars.notify_interval; + if (timevalcmp(&timeofday, &tv, >=)) { - DPRINTF(E_ERROR, L_GENERAL, "gettimeofday(): %s\n", strerror(errno)); - timeout.tv_sec = runtime_vars.notify_interval; - timeout.tv_usec = 0; + DPRINTF(E_DEBUG, L_SSDP, "Sending SSDP notifies\n"); + for (i = 0; i < n_lan_addr; i++) + { + char buf[LOCATION_URL_MAX_LEN] = {}; + const char* host = get_location_url_by_lan_addr(buf, i); + SendSSDPNotifies(lan_addr[i].snotify, runtime_vars.notify_interval, host); + } + lastnotifytime = timeofday; + timeout = runtime_vars.notify_interval * 1000; } else { - /* the comparison is not very precise but who cares ? */ - if (timeofday.tv_sec >= (lastnotifytime.tv_sec + runtime_vars.notify_interval)) + timevalsub(&tv, &timeofday); + timeout = tv.tv_sec * 1000 + tv.tv_usec / 1000; + } +#ifdef TIVO_SUPPORT + if (sbeacon >= 0) + { + u_long beacontimeout; + + tv = lastbeacontime; + tv.tv_sec += beacon_interval; + if (timevalcmp(&timeofday, &tv, >=)) { - DPRINTF(E_DEBUG, L_SSDP, "Sending SSDP notifies\n"); - for (i = 0; i < n_lan_addr; i++) - { - SendSSDPNotifies(lan_addr[i].snotify, lan_addr[i].str, - runtime_vars.port, runtime_vars.notify_interval); - } - memcpy(&lastnotifytime, &timeofday, sizeof(struct timeval)); - timeout.tv_sec = runtime_vars.notify_interval; - timeout.tv_usec = 0; + sendBeaconMessage(sbeacon, &tivo_bcast, sizeof(struct sockaddr_in), 1); + lastbeacontime = timeofday; + beacontimeout = beacon_interval * 1000; + if (timeout > beacon_interval * 1000) + timeout = beacon_interval * 1000; + /* Beacons should be sent every 5 seconds or + * so for the first minute, then every minute + * or so thereafter. */ + if (beacon_interval == 5 && (timeofday.tv_sec - startup_time) > 60) + beacon_interval = 60; } else { - timeout.tv_sec = lastnotifytime.tv_sec + runtime_vars.notify_interval - - timeofday.tv_sec; - if (timeofday.tv_usec > lastnotifytime.tv_usec) - { - timeout.tv_usec = 1000000 + lastnotifytime.tv_usec - - timeofday.tv_usec; - timeout.tv_sec--; - } - else - timeout.tv_usec = lastnotifytime.tv_usec - timeofday.tv_usec; - } -#ifdef TIVO_SUPPORT - if (sbeacon >= 0) - { - if (timeofday.tv_sec >= (lastbeacontime.tv_sec + beacon_interval)) - { - sendBeaconMessage(sbeacon, &tivo_bcast, sizeof(struct sockaddr_in), 1); - memcpy(&lastbeacontime, &timeofday, sizeof(struct timeval)); - if (timeout.tv_sec > beacon_interval) - { - timeout.tv_sec = beacon_interval; - timeout.tv_usec = 0; - } - /* Beacons should be sent every 5 seconds or so for the first minute, - * then every minute or so thereafter. */ - if (beacon_interval == 5 && (timeofday.tv_sec - startup_time) > 60) - beacon_interval = 60; - } - else if (timeout.tv_sec > (lastbeacontime.tv_sec + beacon_interval + 1 - timeofday.tv_sec)) - timeout.tv_sec = lastbeacontime.tv_sec + beacon_interval - timeofday.tv_sec; + timevalsub(&tv, &timeofday); + beacontimeout = tv.tv_sec * 1000 + + tv.tv_usec / 1000; } -#endif + if (timeout > beacontimeout) + timeout = beacontimeout; } +#endif - if (scanning) - { - if (!scanner_pid || kill(scanner_pid, 0) != 0) - { - scanning = 0; - updateID++; + if (GETFLAG(SCANNING_MASK)) { + // If we fork()ed a scanner process, wait for it to finish. If we didn't + // fork(), we have already completed the scan (inline) at this point. + if(!scanner_pid || kill(scanner_pid, 0) != 0) { + // While scanning, the content database is in flux, and queries may + // fail (apparently). However, even the first query _after_ scanning + // has completed sometimes failed (first error 1, "SQL logic error or + // missing database", then if the same statement is re-stepped error 1, + // "database schema has changed"). By re-opening the database here, + // before marking scanning as completed, we force SQLite to refresh, + // preventing these errors. + sqlite3_close(db); + open_db(&db); + + // Mark scan complete + CLEARFLAG(SCANNING_MASK); + if (_get_dbtime() != lastdbtime) + updateID++; } } - /* select open sockets (SSDP, HTTP listen, and all HTTP soap sockets) */ - FD_ZERO(&readset); - - if (sssdp >= 0) - { - FD_SET(sssdp, &readset); - max_fd = MAX(max_fd, sssdp); - } - - if (shttpl >= 0) - { - FD_SET(shttpl, &readset); - max_fd = MAX(max_fd, shttpl); - } -#ifdef TIVO_SUPPORT - if (sbeacon >= 0) - { - FD_SET(sbeacon, &readset); - max_fd = MAX(max_fd, sbeacon); - } -#endif - if (smonitor >= 0) - { - FD_SET(smonitor, &readset); - max_fd = MAX(max_fd, smonitor); - } + event_module.process(timeout); + if (quitting) + goto shutdown; - i = 0; /* active HTTP connections count */ - for (e = upnphttphead.lh_first; e != NULL; e = e->entries.le_next) - { - if ((e->socket >= 0) && (e->state <= 2)) - { - FD_SET(e->socket, &readset); - max_fd = MAX(max_fd, e->socket); - i++; - } - } - FD_ZERO(&writeset); - upnpevents_selectfds(&readset, &writeset, &max_fd); + upnpevents_gc(); - ret = select(max_fd+1, &readset, &writeset, 0, &timeout); - if (ret < 0) - { - if(quitting) goto shutdown; - if(errno == EINTR) continue; - DPRINTF(E_ERROR, L_GENERAL, "select(all): %s\n", strerror(errno)); - DPRINTF(E_FATAL, L_GENERAL, "Failed to select open sockets. EXITING\n"); - } - upnpevents_processfds(&readset, &writeset); - /* process SSDP packets */ - if (sssdp >= 0 && FD_ISSET(sssdp, &readset)) - { - /*DPRINTF(E_DEBUG, L_GENERAL, "Received SSDP Packet\n");*/ - ProcessSSDPRequest(sssdp, (unsigned short)runtime_vars.port); - } -#ifdef TIVO_SUPPORT - if (sbeacon >= 0 && FD_ISSET(sbeacon, &readset)) - { - /*DPRINTF(E_DEBUG, L_GENERAL, "Received UDP Packet\n");*/ - ProcessTiVoBeacon(sbeacon); - } -#endif - if (smonitor >= 0 && FD_ISSET(smonitor, &readset)) - { - ProcessMonitorEvent(smonitor); - } /* increment SystemUpdateID if the content database has changed, * and if there is an active HTTP connection, at most once every 2 seconds */ - if (i && (timeofday.tv_sec >= (lastupdatetime + 2))) + if (!LIST_EMPTY(&upnphttphead) && + (timeofday.tv_sec >= (lastupdatetime + 2))) { - if (scanning || sqlite3_total_changes(db) != last_changecnt) + if (GETFLAG(SCANNING_MASK)) + { + time_t dbtime = _get_dbtime(); + if (dbtime != lastdbtime) + { + lastdbtime = dbtime; + last_changecnt = -1; + } + } + if (sqlite3_total_changes(db) != last_changecnt) { updateID++; last_changecnt = sqlite3_total_changes(db); @@ -1283,48 +1420,6 @@ main(int argc, char **argv) lastupdatetime = timeofday.tv_sec; } } - /* process active HTTP connections */ - for (e = upnphttphead.lh_first; e != NULL; e = e->entries.le_next) - { - if ((e->socket >= 0) && (e->state <= 2) && (FD_ISSET(e->socket, &readset))) - Process_upnphttp(e); - } - /* process incoming HTTP connections */ - if (shttpl >= 0 && FD_ISSET(shttpl, &readset)) - { - int shttp; - socklen_t clientnamelen; - struct sockaddr_in clientname; - clientnamelen = sizeof(struct sockaddr_in); - shttp = accept(shttpl, (struct sockaddr *)&clientname, &clientnamelen); - if (shttp<0) - { - DPRINTF(E_ERROR, L_GENERAL, "accept(http): %s\n", strerror(errno)); - } - else - { - struct upnphttp * tmp = 0; - DPRINTF(E_DEBUG, L_GENERAL, "HTTP connection from %s:%d\n", - inet_ntoa(clientname.sin_addr), - ntohs(clientname.sin_port) ); - /*if (fcntl(shttp, F_SETFL, O_NONBLOCK) < 0) { - DPRINTF(E_ERROR, L_GENERAL, "fcntl F_SETFL, O_NONBLOCK\n"); - }*/ - /* Create a new upnphttp object and add it to - * the active upnphttp object list */ - tmp = New_upnphttp(shttp); - if (tmp) - { - tmp->clientaddr = clientname.sin_addr; - LIST_INSERT_HEAD(&upnphttphead, tmp, entries); - } - else - { - DPRINTF(E_ERROR, L_GENERAL, "New_upnphttp() failed\n"); - close(shttp); - } - } - } /* delete finished HTTP connections */ for (e = upnphttphead.lh_first; e != NULL; e = next) { @@ -1339,7 +1434,7 @@ main(int argc, char **argv) shutdown: /* kill the scanner */ - if (scanning && scanner_pid) + if (GETFLAG(SCANNING_MASK) && scanner_pid) kill(scanner_pid, SIGKILL); /* close out open sockets */ @@ -1359,7 +1454,7 @@ main(int argc, char **argv) #endif if (smonitor >= 0) close(smonitor); - + for (i = 0; i < n_lan_addr; i++) { SendSSDPGoodbyes(lan_addr[i].snotify); @@ -1367,12 +1462,17 @@ main(int argc, char **argv) } if (inotify_thread) + { + pthread_kill(inotify_thread, SIGCHLD); pthread_join(inotify_thread, NULL); + } /* kill other child processes */ process_reap_children(); free(children); + event_module.fini(); + sql_exec(db, "UPDATE SETTINGS set VALUE = '%u' where KEY = 'UPDATE_ID'", updateID); sqlite3_close(db); @@ -1386,4 +1486,3 @@ main(int argc, char **argv) exit(EXIT_SUCCESS); } - diff --git a/minidlna.conf b/minidlna.conf index 446ed3e..76d592d 100644 --- a/minidlna.conf +++ b/minidlna.conf @@ -1,7 +1,7 @@ # port for HTTP (descriptions, SOAP, media transfer) traffic port=8200 -# network interfaces to serve, comma delimited +# network interfaces to serve, comma delimited (8 interfaces max) #network_interface=eth0 # specify the user account name or uid to run as @@ -45,6 +45,10 @@ inotify=yes # set this to yes to enable support for streaming .jpg and .mp3 files to a TiVo supporting HMO enable_tivo=no +# set this to beacon to use legacy broadcast discovery method +# defauts to bonjour if avahi is available +tivo_discovery=bonjour + # set this to strictly adhere to DLNA standards. # * This will allow server-side downscaling of very large JPEG images, # which may hurt JPEG serving performance on (at least) Sony DLNA products. @@ -76,6 +80,9 @@ model_number=1 #root_container=. # always force SortCriteria to this value, regardless of the SortCriteria passed by the client +# note: you can prepend the sort criteria with "!" to alter the titles of the objects so that they +# will be alphanumerically sorted in the order you specify here, to work around clients that do their +# own alphanumeric sorting. #force_sort_criteria=+upnp:class,+upnp:originalTrackNumber,+dc:title # maximum number of simultaneous connections @@ -84,3 +91,26 @@ model_number=1 # set this to yes to allow symlinks that point outside user-defined media_dirs. #wide_links=no + +# Suport to Movie Thumbnail generation. To use this option, thumbnail +# generation must be enabled at compile time. +#enable_thumbnail=no + +# The width of the thumbnail image. Large images takes more time to generate. +# To use this option, thumbnail generation must be enabled at compile time. +# Must be set to zero (full size) for best results with the multiple-sized +# thumbnails patch. +#thumbnail_width=160 + +# MTA file generation for Samsung TVs. The generation of a MTA file can be very time consuming. +# If you have an old and slow device and still want to use MTA, you should consider using the MTA with back images option. +# Values: +# 0: disable MTA Generation +# 1: enable MTA Generation but with black images (for slow devices or large video libraries). +# 2+: enable MTA Generation with movie thumbnail (if you have compiled video thumbnail support). The value tells that we only have to generate thumbnail +# for movies longer than the value in minutes (e.g 10 means that movies with less than 10 min will have a black MTA). +#enable_mta=1 + +# enable subtitle support by default on unknown clients. +# note: the default is yes +#enable_subtitles=yes diff --git a/minidlna.conf.5 b/minidlna.conf.5 index f5ecb97..defe493 100644 --- a/minidlna.conf.5 +++ b/minidlna.conf.5 @@ -19,6 +19,12 @@ set correctly for read access to media and write access to cache and log dirs. .IP "\fBfriendly_name\fP" The name you want your media server seen as, EG: friendly_name=Home Media Server +.IP "\fBicon_dir\fP" +.nf +Path to the directory containing the icons you wish to show the clients, +EG: icon_dir=/var/cache/minidlna/icons +.fi + .IP "\fBport\fP" .nf Port for HTTP (descriptions, SOAP, media transfer) traffic etc, defaults to 8200. @@ -26,7 +32,7 @@ There should be no need to change this. .fi .IP "\fBnetwork_interface\fP" -Network interfaces to serve, comma delimited. Defaults to all. +Network interfaces to serve, comma delimited. Maximum is 8 interfaces. Defaults to all. .IP "\fBstrict_dlna\fP" .nf @@ -106,7 +112,7 @@ EG: presentation_url=http://www.mediaserver.lan/index.php .fi .IP "\fBdb_dir\fP" -Where minidlna stores the data files, including Album caceh files, by default +Where minidlna stores the data files, including Album cache files, by default this is /var/cache/minidlna .IP "\fBlog_dir\fP" @@ -141,6 +147,10 @@ album_art_names=Cover.jpg/cover.jpg/AlbumArtSmall.jpg/albumartsmall.jpg/AlbumArt Set this to yes to enable support for streaming .jpg and .mp3 files to a TiVo supporting HMO, default is no. +.IP "\fBtivo_discovery\fP" +Set this to 'beacon' to use the legacy TiVo broadcast discovery method. Defaults to +using Bonjour if Avahi support is available. + .IP "\fBroot_container\fP" Use a different container as the root of the tree exposed to clients. .nf @@ -157,6 +167,10 @@ The possible values are: .IP "\fBforce_sort_criteria\fP" Always force SortCriteria to this value, regardless of the SortCriteria passed by the client. .nf +You may prepend the sort criteria with "!" to alter the titles of the objects so that they +will be alphanumerically sorted in the order you specify here, to work around clients that do +their own alphanumeric sorting. +.nf Example force_sort_criteria=+upnp:class,+upnp:originalTrackNumber,+dc:title @@ -167,10 +181,14 @@ force_sort_criteria=+upnp:class,+upnp:originalTrackNumber,+dc:title Set to 'yes' to allow symlinks that point outside user-defined media_dirs. By default, wide symlinks are not followed. +.IP "\fBenable_subtitles\fP" +Set to 'no' to disable subtitle support on unknown clients. +By default, subtitles are enabled for unknown or generic clients. + .SH VERSION -This manpage corresponds to minidlna version 1.0.25 +This manpage corresponds to minidlna version 1.3.0 .SH AUTHOR .nf diff --git a/minidlnatypes.h b/minidlnatypes.h index 1623137..62951b3 100644 --- a/minidlnatypes.h +++ b/minidlnatypes.h @@ -34,7 +34,7 @@ #include #include -#define MAX_LAN_ADDR 4 +#define MAX_LAN_ADDR 8 /* structure for storing lan addresses * with ascii representation and mask */ struct lan_addr_s { @@ -51,6 +51,10 @@ struct runtime_vars_s { int max_connections; /* max number of simultaneous conenctions */ const char *root_container; /* root ObjectID (instead of "0") */ const char *ifaces[MAX_LAN_ADDR]; /* list of configured network interfaces */ +#ifdef ENABLE_VIDEO_THUMB + int thumb_width; /* Video thumbnail width */ +#endif + int mta; }; struct string_s { @@ -60,11 +64,14 @@ struct string_s { }; typedef uint8_t media_types; -#define NO_MEDIA 0x00 -#define TYPE_AUDIO 0x01 -#define TYPE_VIDEO 0x02 -#define TYPE_IMAGES 0x04 -#define ALL_MEDIA TYPE_AUDIO|TYPE_VIDEO|TYPE_IMAGES +#define NO_MEDIA 0x00 +#define TYPE_AUDIO 0x01 +#define TYPE_VIDEO 0x02 +#define TYPE_IMAGE 0x04 +#define TYPE_PLAYLIST 0x09 +#define TYPE_CAPTION 0x10 +#define TYPE_NFO 0x20 +#define ALL_MEDIA TYPE_AUDIO|TYPE_VIDEO|TYPE_IMAGE enum file_types { TYPE_UNKNOWN, diff --git a/minissdp.c b/minissdp.c index 7789076..bb2b7d3 100644 --- a/minissdp.c +++ b/minissdp.c @@ -42,6 +42,7 @@ #include #include +#include "event.h" #include "minidlnapath.h" #include "upnphttp.h" #include "upnpglobalvars.h" @@ -113,11 +114,18 @@ OpenAndConfSSDPReceiveSocket(void) memset(&sockname, 0, sizeof(struct sockaddr_in)); sockname.sin_family = AF_INET; sockname.sin_port = htons(SSDP_PORT); +#ifdef __linux__ /* NOTE: Binding a socket to a UDP multicast address means, that we just want * to receive datagramms send to this multicast address. * To specify the local nics we want to use we have to use setsockopt, * see AddMulticastMembership(...). */ sockname.sin_addr.s_addr = inet_addr(SSDP_MCAST_ADDR); +#else + /* NOTE: Binding to SSDP_MCAST_ADDR on Darwin & *BSD causes NOTIFY replies are + * sent from SSDP_MCAST_ADDR what forces some clients to ignore subsequent + * unsolicited NOTIFY packets from the real interface address. */ + sockname.sin_addr.s_addr = htonl(INADDR_ANY); +#endif if (bind(s, (struct sockaddr *)&sockname, sizeof(struct sockaddr_in)) < 0) { @@ -136,7 +144,6 @@ OpenAndConfSSDPNotifySocket(struct lan_addr_s *iface) { int s; unsigned char loopchar = 0; - int bcast = 1; uint8_t ttl = 4; struct in_addr mc_if; struct sockaddr_in sockname; @@ -165,13 +172,6 @@ OpenAndConfSSDPNotifySocket(struct lan_addr_s *iface) } setsockopt(s, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl)); - - if (setsockopt(s, SOL_SOCKET, SO_BROADCAST, &bcast, sizeof(bcast)) < 0) - { - DPRINTF(E_ERROR, L_SSDP, "setsockopt(udp_notify, SO_BROADCAST): %s\n", strerror(errno)); - close(s); - return -1; - } memset(&sockname, 0, sizeof(struct sockaddr_in)); sockname.sin_family = AF_INET; @@ -205,9 +205,10 @@ static const char * const known_service_types[] = }; static void -_usleep(long usecs) +_usleep(long min, long max) { struct timespec sleep_time; + long usecs = min + rand() / (RAND_MAX / (max - min + 1) + 1); sleep_time.tv_sec = 0; sleep_time.tv_nsec = usecs * 1000; @@ -217,8 +218,7 @@ _usleep(long usecs) /* not really an SSDP "announce" as it is the response * to a SSDP "M-SEARCH" */ static void -SendSSDPResponse(int s, struct sockaddr_in sockname, int st_no, - const char *host, unsigned short port) +SendSSDPResponse(int s, struct sockaddr_in sockname, int st_no, const char *host, socklen_t len_r) { int l, n; char buf[512]; @@ -240,7 +240,7 @@ SendSSDPResponse(int s, struct sockaddr_in sockname, int st_no, "USN: %s%s%s%s\r\n" "EXT:\r\n" "SERVER: " MINIDLNA_SERVER_STRING "\r\n" - "LOCATION: http://%s:%u" ROOTDESC_PATH "\r\n" + "LOCATION: %s" ROOTDESC_PATH "\r\n" "Content-Length: 0\r\n" "\r\n", (runtime_vars.notify_interval<<1)+10, @@ -251,19 +251,18 @@ SendSSDPResponse(int s, struct sockaddr_in sockname, int st_no, (st_no > 0 ? "::" : ""), (st_no > 0 ? known_service_types[st_no] : ""), (st_no > 1 ? "1" : ""), - host, (unsigned int)port); + host); DPRINTF(E_DEBUG, L_SSDP, "Sending M-SEARCH response to %s:%d ST: %s\n", inet_ntoa(sockname.sin_addr), ntohs(sockname.sin_port), known_service_types[st_no]); n = sendto(s, buf, l, 0, - (struct sockaddr *)&sockname, sizeof(struct sockaddr_in) ); + (struct sockaddr *)&sockname, len_r); if (n < 0) DPRINTF(E_ERROR, L_SSDP, "sendto(udp): %s\n", strerror(errno)); } void -SendSSDPNotifies(int s, const char *host, unsigned short port, - unsigned int interval) +SendSSDPNotifies(int s, unsigned int interval, const char* host) { struct sockaddr_in sockname; int l, n, dup, i=0; @@ -279,23 +278,23 @@ SendSSDPNotifies(int s, const char *host, unsigned short port, for (dup = 0; dup < 2; dup++) { if (dup) - _usleep(200000); + _usleep(150000, 250000); i = 0; while (known_service_types[i]) { - l = snprintf(bufr, sizeof(bufr), + l = snprintf(bufr, sizeof(bufr), "NOTIFY * HTTP/1.1\r\n" - "HOST:%s:%d\r\n" - "CACHE-CONTROL:max-age=%u\r\n" - "LOCATION:http://%s:%d" ROOTDESC_PATH"\r\n" + "HOST: %s:%d\r\n" + "CACHE-CONTROL: max-age=%u\r\n" + "LOCATION: %s" ROOTDESC_PATH"\r\n" "SERVER: " MINIDLNA_SERVER_STRING "\r\n" - "NT:%s%s\r\n" - "USN:%s%s%s%s\r\n" - "NTS:ssdp:alive\r\n" + "NT: %s%s\r\n" + "USN: %s%s%s%s\r\n" + "NTS: ssdp:alive\r\n" "\r\n", SSDP_MCAST_ADDR, SSDP_PORT, lifetime, - host, port, + host, known_service_types[i], (i > 1 ? "1" : ""), uuidvalue, @@ -482,14 +481,16 @@ ParseUPnPClient(char *location) /* ProcessSSDPRequest() * process SSDP M-SEARCH requests and responds to them */ void -ProcessSSDPRequest(int s, unsigned short port) +ProcessSSDPRequest(struct event *ev) { + int s = ev->fd; int n; char bufr[1500]; struct sockaddr_in sendername; int i; char *st = NULL, *mx = NULL, *man = NULL, *mx_end = NULL; int man_len = 0; + socklen_t len_r = sizeof(struct sockaddr_in); #ifdef __linux__ char cmbuf[CMSG_SPACE(sizeof(struct in_pktinfo))]; struct iovec iovec = { @@ -507,10 +508,10 @@ ProcessSSDPRequest(int s, unsigned short port) n = recvmsg(s, &mh, 0); #else - socklen_t len_r = sizeof(struct sockaddr_in); n = recvfrom(s, bufr, sizeof(bufr)-1, 0, (struct sockaddr *)&sendername, &len_r); + len_r = MIN(len_r, sizeof(struct sockaddr_in)); #endif if (n < 0) { @@ -651,14 +652,14 @@ ProcessSSDPRequest(int s, unsigned short port) else if (st && (st_len > 0)) { int l; + char buf[LOCATION_URL_MAX_LEN] = "127.0.0.1"; + const char* host = buf; #ifdef __linux__ - char host[40] = "127.0.0.1"; struct cmsghdr *cmsg; /* find the interface we received the msg from */ for (cmsg = CMSG_FIRSTHDR(&mh); cmsg; cmsg = CMSG_NXTHDR(&mh, cmsg)) { - struct in_addr addr; struct in_pktinfo *pi; /* ignore the control headers that don't match what we want */ if (cmsg->cmsg_level != IPPROTO_IP || @@ -666,11 +667,11 @@ ProcessSSDPRequest(int s, unsigned short port) continue; pi = (struct in_pktinfo *)CMSG_DATA(cmsg); - addr = pi->ipi_spec_dst; - inet_ntop(AF_INET, &addr, host, sizeof(host)); + host = get_location_url_by_ifindex(buf, pi->ipi_ifindex); + if(host) // stop as soon as we find a match + break; } #else - const char *host; int iface = 0; /* find in which sub network the client is */ for (i = 0; i < n_lan_addr; i++) @@ -682,14 +683,15 @@ ProcessSSDPRequest(int s, unsigned short port) break; } } - if (n_lan_addr == i) + host = get_location_url_by_ifindex(buf, iface); +#endif + if (!host) { DPRINTF(E_DEBUG, L_SSDP, "Ignoring SSDP M-SEARCH on other interface [%s]\n", inet_ntoa(sendername.sin_addr)); return; } - host = lan_addr[iface].str; -#endif + DPRINTF(E_DEBUG, L_SSDP, "SSDP M-SEARCH from %s:%d ST: %.*s, MX: %.*s, MAN: %.*s\n", inet_ntoa(sendername.sin_addr), ntohs(sendername.sin_port), @@ -722,20 +724,19 @@ ProcessSSDPRequest(int s, unsigned short port) if (l != st_len) break; } - _usleep(random()>>20); - SendSSDPResponse(s, sendername, i, - host, port); + _usleep(13000, 20000); + SendSSDPResponse(s, sendername, i, host, len_r); return; } /* Responds to request with ST: ssdp:all */ /* strlen("ssdp:all") == 8 */ if ((st_len == 8) && (memcmp(st, "ssdp:all", 8) == 0)) { + _usleep(13000, 30000); for (i=0; known_service_types[i]; i++) { l = strlen(known_service_types[i]); - SendSSDPResponse(s, sendername, i, - host, port); + SendSSDPResponse(s, sendername, i, host, len_r); } } } @@ -852,8 +853,8 @@ SubmitServicesToMiniSSDPD(const char *host, unsigned short port) CODELENGTH(l, p); memcpy(p, MINIDLNA_SERVER_STRING, l); p += l; - l = snprintf(strbuf, sizeof(strbuf), "http://%s:%u" ROOTDESC_PATH, - host, (unsigned int)port); + l = snprintf(strbuf, sizeof(strbuf), "%s" ROOTDESC_PATH, + host); CODELENGTH(l, p); memcpy(p, strbuf, l); p += l; diff --git a/minissdp.h b/minissdp.h index f6a5c16..0d51d44 100644 --- a/minissdp.h +++ b/minissdp.h @@ -33,9 +33,9 @@ int OpenAndConfSSDPReceiveSocket(void); int OpenAndConfSSDPNotifySocket(struct lan_addr_s *iface); -void SendSSDPNotifies(int s, const char *host, unsigned short port, unsigned int lifetime); +void SendSSDPNotifies(int s, unsigned int lifetime, const char *host); -void ProcessSSDPRequest(int s, unsigned short port); +void ProcessSSDPRequest(struct event *ev); int SendSSDPGoodbyes(int s); diff --git a/minixml.c b/minixml.c index 2fed4a1..bf3fc0f 100644 --- a/minixml.c +++ b/minixml.c @@ -143,7 +143,7 @@ void parseelt(struct xmlparser * p) return; while( IS_WHITE_SPACE(*p->xml) ) { - p->xml++; + i++; p->xml++; if (p->xml >= p->xmlend) return; } diff --git a/inotify.c b/monitor.c similarity index 63% rename from inotify.c rename to monitor.c index 852db54..dae99d7 100644 --- a/inotify.c +++ b/monitor.c @@ -17,9 +17,9 @@ */ #include "config.h" -#ifdef HAVE_INOTIFY #include #include +#include #include #include #include @@ -30,6 +30,7 @@ #include #include #include +#ifdef HAVE_INOTIFY #include #include #ifdef HAVE_SYS_INOTIFY_H @@ -38,10 +39,11 @@ #include "linux/inotify.h" #include "linux/inotify-syscalls.h" #endif +#endif #include "libav.h" #include "upnpglobalvars.h" -#include "inotify.h" +#include "monitor.h" #include "utils.h" #include "sql.h" #include "scanner.h" @@ -50,6 +52,9 @@ #include "playlist.h" #include "log.h" +static time_t next_pl_fill = 0; + +#ifdef HAVE_INOTIFY #define EVENT_SIZE ( sizeof (struct inotify_event) ) #define BUF_LEN ( 1024 * ( EVENT_SIZE + 16 ) ) #define DESIRED_WATCH_LIMIT 65536 @@ -65,11 +70,11 @@ struct watch static struct watch *watches; static struct watch *lastwatch = NULL; -static time_t next_pl_fill = 0; static int IsMediaPath(const char * path); -char *get_path_from_wd(int wd) +static char * +get_path_from_wd(int wd) { struct watch *w = watches; @@ -83,6 +88,32 @@ char *get_path_from_wd(int wd) return NULL; } +static unsigned int +next_highest(unsigned int num) +{ + num |= num >> 1; + num |= num >> 2; + num |= num >> 4; + num |= num >> 8; + num |= num >> 16; + return ++num; +} + +static void +raise_watch_limit(unsigned int limit) +{ + FILE *max_watches = fopen("/proc/sys/fs/inotify/max_user_watches", "r+"); + if (!max_watches) + return; + if (!limit) + if(fscanf(max_watches, "%u", &limit)==1) + { + DPRINTF(E_INFO, L_INOTIFY, "raising watch limit: %d --> %d\n", limit, next_highest(limit)); + fprintf(max_watches, "%u", next_highest(limit)); + } + fclose(max_watches); +} + int add_watch(int fd, const char * path) { @@ -90,17 +121,22 @@ add_watch(int fd, const char * path) int wd; wd = inotify_add_watch(fd, path, IN_CREATE|IN_CLOSE_WRITE|IN_DELETE|IN_MOVE); + if( wd < 0 && errno == ENOSPC) + { + raise_watch_limit(0); + wd = inotify_add_watch(fd, path, IN_CREATE|IN_CLOSE_WRITE|IN_DELETE|IN_MOVE); + } if( wd < 0 ) { DPRINTF(E_ERROR, L_INOTIFY, "inotify_add_watch(%s) [%s]\n", path, strerror(errno)); - return -1; + return (errno); } nw = malloc(sizeof(struct watch)); if( nw == NULL ) { DPRINTF(E_ERROR, L_INOTIFY, "malloc() error\n"); - return -1; + return (ENOMEM); } nw->wd = wd; nw->next = NULL; @@ -117,10 +153,11 @@ add_watch(int fd, const char * path) } lastwatch = nw; - return wd; + DPRINTF(E_INFO, L_INOTIFY, "Added watch to %s [%d]\n", path, wd); + return (0); } -int +static int remove_watch(int fd, const char * path) { struct watch *w; @@ -134,18 +171,7 @@ remove_watch(int fd, const char * path) return 1; } -unsigned int -next_highest(unsigned int num) -{ - num |= num >> 1; - num |= num >> 2; - num |= num >> 4; - num |= num >> 8; - num |= num >> 16; - return(++num); -} - -int +static int inotify_create_watches(int fd) { FILE * max_watches; @@ -180,22 +206,20 @@ inotify_create_watches(int fd) fclose(max_watches); if( (watch_limit < DESIRED_WATCH_LIMIT) || (watch_limit < (num_watches*4/3)) ) { - max_watches = fopen("/proc/sys/fs/inotify/max_user_watches", "w"); - if( max_watches ) + if (access("/proc/sys/fs/inotify/max_user_watches", W_OK) == 0) { if( DESIRED_WATCH_LIMIT >= (num_watches*3/4) ) { - fprintf(max_watches, "%u", DESIRED_WATCH_LIMIT); + raise_watch_limit(8191U); } else if( next_highest(num_watches) >= (num_watches*3/4) ) { - fprintf(max_watches, "%u", next_highest(num_watches)); + raise_watch_limit(num_watches); } else { - fprintf(max_watches, "%u", next_highest(next_highest(num_watches))); + raise_watch_limit(next_highest(num_watches)); } - fclose(max_watches); } else { @@ -214,7 +238,7 @@ inotify_create_watches(int fd) return rows; } -int +static int inotify_remove_watches(int fd) { struct watch *w = watches; @@ -233,151 +257,172 @@ inotify_remove_watches(int fd) return rm_watches; } +#endif -int add_dir_watch(int fd, char * path, char * filename) +int +monitor_remove_file(const char * path) { - DIR *ds; - struct dirent *e; - char *dir; - char buf[PATH_MAX]; - int wd; - int i = 0; + char sql[128]; + char *id; + char *ptr; + char **result; + int64_t detailID; + int rows, playlist; - if( filename ) + if( is_caption(path) ) { - snprintf(buf, sizeof(buf), "%s/%s", path, filename); - dir = buf; + return sql_exec(db, "DELETE from CAPTIONS where PATH = '%q'", path); } - else - dir = path; - - wd = add_watch(fd, dir); - if( wd == -1 ) + /* Invalidate the scanner cache so we don't insert files into non-existent containers */ + valid_cache = 0; + playlist = is_playlist(path); + id = sql_get_text_field(db, "SELECT ID from %s where PATH = '%q'", playlist?"PLAYLISTS":"DETAILS", path); + if( !id ) + return 1; + detailID = strtoll(id, NULL, 10); + sqlite3_free(id); + if( playlist ) { - DPRINTF(E_ERROR, L_INOTIFY, "add_watch() [%s]\n", strerror(errno)); + sql_exec(db, "DELETE from PLAYLISTS where ID = %lld", detailID); + sql_exec(db, "DELETE from DETAILS where ID =" + " (SELECT DETAIL_ID from OBJECTS where OBJECT_ID = '%s$%llX')", + MUSIC_PLIST_ID, detailID); + sql_exec(db, "DELETE from OBJECTS where OBJECT_ID = '%s$%llX' or PARENT_ID = '%s$%llX'", + MUSIC_PLIST_ID, detailID, MUSIC_PLIST_ID, detailID); } else { - DPRINTF(E_INFO, L_INOTIFY, "Added watch to %s [%d]\n", dir, wd); - } - - ds = opendir(dir); - if( ds != NULL ) - { - while( (e = readdir(ds)) ) + /* Delete the parent containers if we are about to empty them. */ + snprintf(sql, sizeof(sql), "SELECT PARENT_ID from OBJECTS where DETAIL_ID = %lld" + " and PARENT_ID not like '64$%%'", + (long long int)detailID); + if( (sql_get_table(db, sql, &result, &rows, NULL) == SQLITE_OK) ) { - if( strcmp(e->d_name, ".") == 0 || - strcmp(e->d_name, "..") == 0 ) - continue; - if( (e->d_type == DT_DIR) || - (e->d_type == DT_UNKNOWN && resolve_unknown_type(dir, NO_MEDIA) == TYPE_DIR) ) - i += add_dir_watch(fd, dir, e->d_name); + int i, children; + for( i = 1; i <= rows; i++ ) + { + /* If it's a playlist item, adjust the item count of the playlist */ + if( strncmp(result[i], MUSIC_PLIST_ID, strlen(MUSIC_PLIST_ID)) == 0 ) + { + sql_exec(db, "UPDATE PLAYLISTS set FOUND = (FOUND-1) where ID = %d", + atoi(strrchr(result[i], '$') + 1)); + } + + children = sql_get_int_field(db, "SELECT count(*) from OBJECTS where PARENT_ID = '%s'", result[i]); + if( children < 0 ) + continue; + if( children < 2 ) + { + sql_exec(db, "DELETE from OBJECTS where OBJECT_ID = '%s'", result[i]); + + ptr = strrchr(result[i], '$'); + if( ptr ) + *ptr = '\0'; + if( sql_get_int_field(db, "SELECT count(*) from OBJECTS where PARENT_ID = '%s'", result[i]) == 0 ) + { + sql_exec(db, "DELETE from OBJECTS where OBJECT_ID = '%s'", result[i]); + } + } + } + sqlite3_free_table(result); } + /* Now delete the actual objects */ + sql_exec(db, "DELETE from DETAILS where ID = %lld", detailID); + sql_exec(db, "DELETE from OBJECTS where DETAIL_ID = %lld", detailID); } - else - { - DPRINTF(E_ERROR, L_INOTIFY, "Opendir error! [%s]\n", strerror(errno)); - } - closedir(ds); - i++; - return(i); + art_cache_cleanup(path); + + return 0; +} + +static char * +check_nfo(const char *path) +{ + char file[PATH_MAX]; + + strncpyt(file, path, sizeof(file)); + strip_ext(file); + + return sql_get_text_field(db, "SELECT PATH from DETAILS where (PATH > '%q.' and PATH <= '%q.z')" + " and MIME glob 'video/*' limit 1", file, file); } int -inotify_insert_file(char * name, const char * path) +monitor_insert_file(const char *name, const char *path) { int len; - char * last_dir; - char * path_buf; - char * base_name; - char * base_copy; - char * parent_buf = NULL; - char * id = NULL; + char *last_dir; + char *path_buf; + char *base_name; + char *base_copy; + char *parent_buf = NULL; + char *id = NULL; + char video[PATH_MAX]; + const char *tbl = "DETAILS"; int depth = 1; int ts; - media_types types = ALL_MEDIA; - struct media_dir_s * media_path = media_dirs; + media_types dir_types; + media_types mtype = get_media_type(path); struct stat st; + char dirpath[PATH_MAX]; + + strncpyt(dirpath, path, sizeof(dirpath)); + if ( has_ignore(dirname((char*)dirpath), 1) ) + return -1; /* Is it cover art for another file? */ - if( is_image(path) ) + if (mtype == TYPE_IMAGE) update_if_album_art(path); - else if( is_caption(path) ) + else if (mtype == TYPE_CAPTION) check_for_captions(path, 0); - - /* Check if we're supposed to be scanning for this file type in this directory */ - while( media_path ) + else if (mtype == TYPE_PLAYLIST) + tbl = "PLAYLISTS"; + else if (mtype == TYPE_NFO) { - if( strncmp(path, media_path->path, strlen(media_path->path)) == 0 ) - { - types = media_path->types; - break; - } - media_path = media_path->next; - } - switch( types ) - { - case ALL_MEDIA: - if( !is_image(path) && - !is_audio(path) && - !is_video(path) && - !is_playlist(path) ) - return -1; - break; - case TYPE_AUDIO: - if( !is_audio(path) && - !is_playlist(path) ) - return -1; - break; - case TYPE_AUDIO|TYPE_VIDEO: - if( !is_audio(path) && - !is_video(path) && - !is_playlist(path) ) - return -1; - break; - case TYPE_AUDIO|TYPE_IMAGES: - if( !is_image(path) && - !is_audio(path) && - !is_playlist(path) ) - return -1; - break; - case TYPE_VIDEO: - if( !is_video(path) ) - return -1; - break; - case TYPE_VIDEO|TYPE_IMAGES: - if( !is_image(path) && - !is_video(path) ) - return -1; - break; - case TYPE_IMAGES: - if( !is_image(path) ) - return -1; - break; - default: + char *vpath = check_nfo(path); + if (!vpath) + return -1; + strncpyt(video, vpath, sizeof(video)); + sqlite3_free(vpath); + DPRINTF(E_DEBUG, L_INOTIFY, "Found modified nfo %s\n", video); + monitor_remove_file(video); + name = strrchr(video, '/'); + if (!name) return -1; + name++; + path = video; + mtype = TYPE_VIDEO; } - + + /* Check if we're supposed to be scanning for this file type in this directory */ + dir_types = valid_media_types(path); + if (!(mtype & dir_types)) + return -1; + /* If it's already in the database, remove it before re-inserting. */ if( stat(path, &st) != 0 ) return -1; - ts = sql_get_int_field(db, "SELECT TIMESTAMP from DETAILS where PATH = '%q'", path); - if( !ts && is_playlist(path) && (sql_get_int_field(db, "SELECT ID from PLAYLISTS where PATH = '%q'", path) > 0) ) + ts = sql_get_int_field(db, "SELECT TIMESTAMP from %s where PATH = '%q'", tbl, path); + if( !ts ) + { + DPRINTF(E_DEBUG, L_INOTIFY, "Adding: %s\n", path); + } + else if( ts != st.st_mtime ) { - DPRINTF(E_DEBUG, L_INOTIFY, "Re-reading modified playlist (%s).\n", path); - inotify_remove_file(path); - next_pl_fill = 1; + DPRINTF(E_DEBUG, L_INOTIFY, "%s is %s than the last db entry.\n", + path, (ts > st.st_mtime) ? "older" : "newer"); + monitor_remove_file(path); } - else if( ts > 0 ) + else { - if( ts < st.st_mtime ) - DPRINTF(E_DEBUG, L_INOTIFY, "%s is newer than the last db entry.\n", path); - inotify_remove_file(path); + if( ts == st.st_mtime && !GETFLAG(RESCAN_MASK) ) + DPRINTF(E_DEBUG, L_INOTIFY, "%s already exists\n", path); + return 0; } - /* Find the parentID. If it's not found, create all necessary parents. */ + /* Find the parentID. If it's not found, create all necessary parents. */ len = strlen(path)+1; if( !(path_buf = malloc(len)) || !(last_dir = malloc(len)) || @@ -428,29 +473,54 @@ inotify_insert_file(char * name, const char * path) if( !depth ) { //DEBUG DPRINTF(E_DEBUG, L_INOTIFY, "Inserting %s\n", name); - insert_file(name, path, id+2, get_next_available_id("OBJECTS", id), types); - sqlite3_free(id); - if( (is_audio(path) || is_playlist(path)) && next_pl_fill != 1 ) + int ret = insert_file(name, path, id+2, get_next_available_id("OBJECTS", id), dir_types); + if (ret == 1 && (mtype & TYPE_PLAYLIST)) { next_pl_fill = time(NULL) + 120; // Schedule a playlist scan for 2 minutes from now. - //DEBUG DPRINTF(E_WARN, L_INOTIFY, "Playlist scan scheduled for %s", ctime(&next_pl_fill)); + //DEBUG DPRINTF(E_MAXDEBUG, L_INOTIFY, "Playlist scan scheduled for %s", ctime(&next_pl_fill)); } + + if( is_video(path)) + GenerateMTA(path); + + sqlite3_free(id); } return depth; } +static bool +check_notsparse(const char *path) +#if HAVE_DECL_SEEK_HOLE +{ + int fd; + bool rv; + + if ((fd = open(path, O_RDONLY)) == -1) + return (false); + if (lseek(fd, 0, SEEK_HOLE) == lseek(fd, 0, SEEK_END)) + rv = true; + else + rv = false; + close(fd); + return (rv); +} +#else +{ + struct stat st; + + return (stat(path, &st) == 0 && (st.st_blocks << 9 >= st.st_size)); +} +#endif + int -inotify_insert_directory(int fd, char *name, const char * path) +monitor_insert_directory(int fd, char *name, const char * path) { DIR * ds; struct dirent * e; char *id, *parent_buf, *esc_name; char path_buf[PATH_MAX]; - int wd; enum file_types type = TYPE_UNKNOWN; - media_types dir_types = ALL_MEDIA; - struct media_dir_s* media_path; - struct stat st; + media_types dir_types; if( access(path, R_OK|X_OK) != 0 ) { @@ -459,39 +529,28 @@ inotify_insert_directory(int fd, char *name, const char * path) } if( sql_get_int_field(db, "SELECT ID from DETAILS where PATH = '%q'", path) > 0 ) { - DPRINTF(E_DEBUG, L_INOTIFY, "%s already exists\n", path); - return 0; - } - - parent_buf = strdup(path); - id = sql_get_text_field(db, "SELECT OBJECT_ID from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)" - " where d.PATH = '%q' and REF_ID is NULL", dirname(parent_buf)); - if( !id ) - id = sqlite3_mprintf("%s", BROWSEDIR_ID); - insert_directory(name, path, BROWSEDIR_ID, id+2, get_next_available_id("OBJECTS", id)); - sqlite3_free(id); - free(parent_buf); - - wd = add_watch(fd, path); - if( wd == -1 ) - { - DPRINTF(E_ERROR, L_INOTIFY, "add_watch() failed\n"); + fd = 0; + if (!GETFLAG(RESCAN_MASK)) + DPRINTF(E_DEBUG, L_INOTIFY, "%s already exists\n", path); } else { - DPRINTF(E_INFO, L_INOTIFY, "Added watch to %s [%d]\n", path, wd); + parent_buf = strdup(path); + id = sql_get_text_field(db, "SELECT OBJECT_ID from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)" + " WHERE d.PATH = '%q' and REF_ID is NULL", dirname(parent_buf)); + if( !id ) + id = sqlite3_mprintf("%s", BROWSEDIR_ID); + insert_directory(name, path, BROWSEDIR_ID, id+2, get_next_available_id("OBJECTS", id)); + sqlite3_free(id); + free(parent_buf); } - media_path = media_dirs; - while( media_path ) - { - if( strncmp(path, media_path->path, strlen(media_path->path)) == 0 ) - { - dir_types = media_path->types; - break; - } - media_path = media_path->next; - } +#ifdef HAVE_WATCH + if( fd > 0 ) + add_watch(fd, path); +#endif + + dir_types = valid_media_types(path); ds = opendir(path); if( !ds ) @@ -499,7 +558,7 @@ inotify_insert_directory(int fd, char *name, const char * path) DPRINTF(E_ERROR, L_INOTIFY, "opendir failed! [%s]\n", strerror(errno)); return -1; } - while( (e = readdir(ds)) ) + while( !quitting && (e = readdir(ds)) ) { if( e->d_name[0] == '.' ) continue; @@ -517,14 +576,10 @@ inotify_insert_directory(int fd, char *name, const char * path) } if( type == TYPE_DIR ) { - inotify_insert_directory(fd, esc_name, path_buf); + monitor_insert_directory(fd, esc_name, path_buf); } - else if( type == TYPE_FILE ) - { - if( (stat(path_buf, &st) == 0) && (st.st_blocks<<9 >= st.st_size) ) - { - inotify_insert_file(esc_name, path_buf); - } + else if( type == TYPE_FILE && check_notsparse(path_buf)) { + monitor_insert_file(esc_name, path_buf); } free(esc_name); } @@ -548,109 +603,8 @@ IsMediaPath(const char * path) return 0; } - - -static void RemoveFromDB(const char * id) -{ - char * path = NULL; - path = sql_get_text_field(db, "SELECT PATH from DETAILS where ID = " - "(SELECT DETAIL_ID from OBJECTS WHERE OBJECT_ID = '%q')", id); - if(path) - { - DPRINTF(E_DEBUG, L_INOTIFY, "ParentPath: '%s'\n", path); - if(!IsMediaPath(path)) - { - DPRINTF(E_DEBUG, L_INOTIFY, "%s is a not virtual folder, removing\n", path); - sql_exec(db, "DELETE from DETAILS where ID =" - " (SELECT DETAIL_ID from OBJECTS where OBJECT_ID = '%s')", id); - sql_exec(db, "DELETE from OBJECTS where OBJECT_ID = '%s'", id); - } - - free(path); - } -} - -int -inotify_remove_file(const char * path) -{ - char sql[128]; - char art_cache[PATH_MAX]; - char *id; - char *ptr; - char **result; - int64_t detailID; - int rows, playlist; - - if( is_caption(path) ) - { - return sql_exec(db, "DELETE from CAPTIONS where PATH = '%q'", path); - } - /* Invalidate the scanner cache so we don't insert files into non-existent containers */ - valid_cache = 0; - playlist = is_playlist(path); - id = sql_get_text_field(db, "SELECT ID from %s where PATH = '%q'", playlist?"PLAYLISTS":"DETAILS", path); - if( !id ) - return 1; - detailID = strtoll(id, NULL, 10); - sqlite3_free(id); - if( playlist ) - { - sql_exec(db, "DELETE from PLAYLISTS where ID = %lld", detailID); - sql_exec(db, "DELETE from DETAILS where ID =" - " (SELECT DETAIL_ID from OBJECTS where OBJECT_ID = '%s$%llX')", - MUSIC_PLIST_ID, detailID); - sql_exec(db, "DELETE from OBJECTS where OBJECT_ID = '%s$%llX' or PARENT_ID = '%s$%llX'", - MUSIC_PLIST_ID, detailID, MUSIC_PLIST_ID, detailID); - } - else - { - /* Delete the parent containers if we are about to empty them. */ - snprintf(sql, sizeof(sql), "SELECT PARENT_ID from OBJECTS where DETAIL_ID = %lld" - " and PARENT_ID not like '64$%%'", - (long long int)detailID); - if( (sql_get_table(db, sql, &result, &rows, NULL) == SQLITE_OK) ) - { - int i, children; - for( i = 1; i <= rows; i++ ) - { - /* If it's a playlist item, adjust the item count of the playlist */ - if( strncmp(result[i], MUSIC_PLIST_ID, strlen(MUSIC_PLIST_ID)) == 0 ) - { - sql_exec(db, "UPDATE PLAYLISTS set FOUND = (FOUND-1) where ID = %d", - atoi(strrchr(result[i], '$') + 1)); - } - - children = sql_get_int_field(db, "SELECT count(*) from OBJECTS where PARENT_ID = '%s'", result[i]); - if( children < 0 ) - continue; - if( children < 2 ) - { - RemoveFromDB(result[i]); - // If the file/folder we just removed was the last file/folder in the resp. parent folder, - // then remove the parent folder as well - ptr = strrchr(result[i], '$'); - if( ptr ) - *ptr = '\0'; - if( sql_get_int_field(db, "SELECT count(*) from OBJECTS where PARENT_ID = '%s'", result[i]) == 0 ) - { - RemoveFromDB(result[i]); - } - } - } - sqlite3_free_table(result); - } - /* Now delete the actual objects */ - sql_exec(db, "DELETE from DETAILS where ID = %lld", detailID); - sql_exec(db, "DELETE from OBJECTS where DETAIL_ID = %lld", detailID); - } - snprintf(art_cache, sizeof(art_cache), "%s/art_cache%s", db_path, path); - remove(art_cache); - - return 0; -} - int -inotify_remove_directory(int fd, const char * path) +monitor_remove_directory(int fd, const char * path) { char * sql; char **result; @@ -659,52 +613,62 @@ inotify_remove_directory(int fd, const char * path) /* Invalidate the scanner cache so we don't insert files into non-existent containers */ valid_cache = 0; - remove_watch(fd, path); - sql = sqlite3_mprintf("SELECT ID from DETAILS where (PATH > '%q/' and PATH <= '%q/%c')" + #ifdef HAVE_INOTIFY + if( fd > 0 ) + { + remove_watch(fd, path); + } + #endif + sql = sqlite3_mprintf("SELECT ID, PATH" + " from DETAILS where (PATH > '%q/' and PATH <= '%q/%c')" " or PATH = '%q'", path, path, 0xFF, path); if( (sql_get_table(db, sql, &result, &rows, NULL) == SQLITE_OK) ) { - if( rows ) + for(i=2; i <= 2*rows; i+=2) // x2 since we've asked for 2 columns { - for( i=1; i <= rows; i++ ) - { - detailID = strtoll(result[i], NULL, 10); - sql_exec(db, "DELETE from DETAILS where ID = %lld", detailID); - sql_exec(db, "DELETE from OBJECTS where DETAIL_ID = %lld", detailID); - } - ret = 0; + detailID = strtoll(result[i], NULL, 10); + sql_exec(db, "DELETE from ALBUM_ART where ID = (SELECT ALBUM_ART from DETAILS where ID = %lld)", detailID); + sql_exec(db, "DELETE from DETAILS where ID = %lld", detailID); + sql_exec(db, "DELETE from OBJECTS where DETAIL_ID = %lld", detailID); + art_cache_cleanup(result[i+1]); } + ret = 0; sqlite3_free_table(result); } sqlite3_free(sql); /* Clean up any album art entries in the deleted directory */ - sql_exec(db, "DELETE from ALBUM_ART where (PATH > '%q/' and PATH <= '%q/%c')", path, path, 0xFF); + sql_exec(db, "DELETE from ALBUM_ART where (PATH > '%q/' and PATH <= '%q/%c' or PATH = '%q')", path, path, 0xFF, path); return ret; } +#ifdef HAVE_INOTIFY void * start_inotify(void) { struct pollfd pollfds[1]; - int timeout = 1000; char buffer[BUF_LEN]; char path_buf[PATH_MAX]; int length, i = 0; char * esc_name = NULL; struct stat st; +#ifdef ENABLE_VIDEO_THUMB + char renpath_buf[PATH_MAX] = {}; + int cookie = 0; +#endif sigset_t set; sigfillset(&set); + sigdelset(&set, SIGCHLD); pthread_sigmask(SIG_BLOCK, &set, NULL); - + pollfds[0].fd = inotify_init(); pollfds[0].events = POLLIN; if ( pollfds[0].fd < 0 ) DPRINTF(E_ERROR, L_INOTIFY, "inotify_init() failed!\n"); - while( scanning ) + while( GETFLAG(SCANNING_MASK) ) { if( quitting ) goto quitting; @@ -714,11 +678,19 @@ start_inotify(void) if (setpriority(PRIO_PROCESS, 0, 19) == -1) DPRINTF(E_WARN, L_INOTIFY, "Failed to reduce inotify thread priority\n"); sqlite3_release_memory(1<<31); - av_register_all(); - + while( !quitting ) { - length = poll(pollfds, 1, timeout); + int timeout = -1; + if (next_pl_fill) + { + time_t diff = next_pl_fill - time(NULL); + if (diff < 0) + timeout = 0; + else + timeout = diff * 1000; + } + length = poll(pollfds, 1, timeout); if( !length ) { if( next_pl_fill && (time(NULL) >= next_pl_fill) ) @@ -730,9 +702,9 @@ start_inotify(void) } else if( length < 0 ) { - if( (errno == EINTR) || (errno == EAGAIN) ) - continue; - else + if( (errno == EINTR) || (errno == EAGAIN) ) + continue; + else DPRINTF(E_ERROR, L_INOTIFY, "read failed!\n"); } else @@ -742,7 +714,7 @@ start_inotify(void) } i = 0; - while( i < length ) + while( !quitting && i < length ) { struct inotify_event * event = (struct inotify_event *) &buffer[i]; if( event->len ) @@ -754,11 +726,40 @@ start_inotify(void) } esc_name = modifyString(strdup(event->name), "&", "&amp;", 0); snprintf(path_buf, sizeof(path_buf), "%s/%s", get_path_from_wd(event->wd), event->name); +#ifdef ENABLE_VIDEO_THUMB + DPRINTF(E_DEBUG, L_INOTIFY, "%s '%s' was %s (%x).\n", + path_buf, + (event->mask & IN_ISDIR ) ? "directory" : "file", + (event->mask & IN_MOVED_TO ) ? "moved here" : + (event->mask & IN_MOVED_FROM) ? "moved away" : + (event->mask & IN_DELETE ) ? "deleted" : + (event->mask & IN_CREATE ) ? "created" : + (event->mask & IN_CLOSE ) ? "closed" : + "other", + event->mask + ); + /* We do not want to regenerate the thumbnails if renaming a directory. */ + if (event->cookie == cookie && event->mask & IN_MOVED_TO) + { + DPRINTF(E_DEBUG, L_INOTIFY, "Detected rename: '%s' -> '%s'\n", renpath_buf+1, path_buf); + art_cache_rename(renpath_buf+1, path_buf); + } + else if(renpath_buf[0]) { // check for delayed action + DPRINTF(E_DEBUG, L_INOTIFY, "Delayed delete for: '%s'\n", renpath_buf+1); + if( renpath_buf[0] == 'd' ) + { + monitor_remove_directory(pollfds[0].fd, renpath_buf+1); + } + else + monitor_remove_file(renpath_buf+1); + } + // Clear any delayed action (either it was a rename, or it has been + // executed just now) + renpath_buf[0] = 0; +#endif if ( event->mask & IN_ISDIR && (event->mask & (IN_CREATE|IN_MOVED_TO)) ) { - DPRINTF(E_DEBUG, L_INOTIFY, "The directory %s was %s.\n", - path_buf, (event->mask & IN_MOVED_TO ? "moved here" : "created")); - inotify_insert_directory(pollfds[0].fd, esc_name, path_buf); + monitor_insert_directory(pollfds[0].fd, esc_name, path_buf); } else if ( (event->mask & (IN_CLOSE_WRITE|IN_MOVED_TO|IN_CREATE)) && (lstat(path_buf, &st) == 0) ) @@ -769,9 +770,9 @@ start_inotify(void) (S_ISLNK(st.st_mode) ? "symbolic" : "hard"), path_buf, (event->mask & IN_MOVED_TO ? "moved here" : "created")); if( stat(path_buf, &st) == 0 && S_ISDIR(st.st_mode) ) - inotify_insert_directory(pollfds[0].fd, esc_name, path_buf); + monitor_insert_directory(pollfds[0].fd, esc_name, path_buf); else - inotify_insert_file(esc_name, path_buf); + monitor_insert_file(esc_name, path_buf); } else if( event->mask & (IN_CLOSE_WRITE|IN_MOVED_TO) && st.st_size > 0 ) { @@ -780,19 +781,33 @@ start_inotify(void) { DPRINTF(E_INFO, L_INOTIFY, "The file %s was %s.\n", path_buf, (event->mask & IN_MOVED_TO ? "moved here" : "changed")); - inotify_insert_file(esc_name, path_buf); + monitor_insert_file(esc_name, path_buf); } } } else if ( event->mask & (IN_DELETE|IN_MOVED_FROM) ) { - DPRINTF(E_INFO, L_INOTIFY, "The %s %s was %s.\n", - (event->mask & IN_ISDIR ? "directory" : "file"), - path_buf, (event->mask & IN_MOVED_FROM ? "moved away" : "deleted")); +#ifdef ENABLE_VIDEO_THUMB + if ( event->mask & IN_MOVED_FROM ) + { + // action will be taken on the next event + // this is to avoid deleting and having to regenerate thumbnails + // in case of just a rename. + strncpy(renpath_buf+1, path_buf, sizeof(renpath_buf)-1); + renpath_buf[0] = (event->mask & IN_ISDIR) ? 'd' : 'f'; + cookie = event->cookie; + } + else { +#endif if ( event->mask & IN_ISDIR ) - inotify_remove_directory(pollfds[0].fd, path_buf); + { + monitor_remove_directory(pollfds[0].fd, path_buf); + } else - inotify_remove_file(path_buf); + monitor_remove_file(path_buf); +#ifdef ENABLE_VIDEO_THUMB + } +#endif } free(esc_name); } @@ -800,7 +815,17 @@ start_inotify(void) } } inotify_remove_watches(pollfds[0].fd); + quitting: + if(renpath_buf[0]) { + DPRINTF(E_DEBUG, L_INOTIFY, "Delayed delete for: '%s'\n", renpath_buf+1); + if( renpath_buf[0] == 'd' ) + { + monitor_remove_directory(pollfds[0].fd, renpath_buf+1); + } + else + monitor_remove_file(renpath_buf+1); + } close(pollfds[0].fd); return 0; diff --git a/monitor.h b/monitor.h new file mode 100644 index 0000000..c5e7b99 --- /dev/null +++ b/monitor.h @@ -0,0 +1,18 @@ +int monitor_insert_file(const char *name, const char *path); +int monitor_insert_directory(int fd, char *name, const char * path); +int monitor_remove_file(const char * path); +int monitor_remove_directory(int fd, const char * path); + +#if defined(HAVE_INOTIFY) || defined(HAVE_KQUEUE) +#define HAVE_WATCH 1 +int add_watch(int, const char *); +#endif + +#ifdef HAVE_INOTIFY +void * +start_inotify(); +#endif + +#ifdef HAVE_KQUEUE +void kqueue_monitor_start(); +#endif diff --git a/monitor_kqueue.c b/monitor_kqueue.c new file mode 100644 index 0000000..34026d5 --- /dev/null +++ b/monitor_kqueue.c @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2017 Gleb Smirnoff + * Copyright (c) 2013 Bernard Spil + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "event.h" +#include "log.h" +#include "monitor.h" +#include "minidlnatypes.h" +#include "upnpglobalvars.h" +#include "sql.h" +#include "utils.h" + +struct watch { + struct event ev; + const char *path; + bool isdir; +}; + +static void +dir_vnode_process(struct event *ev, u_int fflags) +{ + struct watch *wt; + const char *path; + char *sql, **result, tmp_path[PATH_MAX], *esc_name; + int rows, result_path_len, i; + DIR* d; + struct dirent *entry; + bool found_flag; + + wt = (struct watch *)ev->data; + path = wt->path; + + if (fflags & NOTE_DELETE) { + DPRINTF(E_DEBUG, L_INOTIFY, "Path [%s] deleted.\n", path); + close(ev->fd); + free(wt); + monitor_remove_directory(0, path); + return; + } else if ((fflags & (NOTE_WRITE | NOTE_LINK)) == + (NOTE_WRITE | NOTE_LINK)) { + + DPRINTF(E_DEBUG, L_INOTIFY, "Directory [%s] content updated\n", + path); + sql = sqlite3_mprintf("SELECT PATH from DETAILS where " + "(PATH > '%q/' and PATH <= '%q/%c') and SIZE IS NULL", + path, path, 0xFF); + DPRINTF(E_DEBUG, L_INOTIFY, "SQL: %s\n", sql); + if ((sql_get_table(db, sql, &result, &rows, NULL) != + SQLITE_OK)) { + DPRINTF(E_WARN, L_INOTIFY, + "Read state [%s]: Query failed\n", path); + goto err1; + } + + for (i = 1; i <= rows; i++) { + DPRINTF(E_DEBUG, L_INOTIFY, + "Indexed content: %s\n", result[i]); + if (access(result[i], R_OK) == -1) + monitor_remove_directory(0, result[i]); + } + + if ((d = opendir(path)) == NULL) { + DPRINTF(E_ERROR, L_INOTIFY, "Can't list [%s] (%s)\n", + path, strerror(errno)); + goto err2; + } + + for (entry = readdir(d); entry != NULL; entry = readdir(d)) { + if ((entry->d_type != DT_DIR) || + (strcmp(entry->d_name, "..") == 0) || + (strcmp(entry->d_name, ".") == 0)) + continue; + + result_path_len = snprintf(tmp_path, PATH_MAX, + "%s/%s", path, entry->d_name); + if (result_path_len >= PATH_MAX) { + DPRINTF(E_ERROR, L_INOTIFY, + "File path too long for %s!", + entry->d_name); + continue; + } + + DPRINTF(E_DEBUG, L_INOTIFY, "Walking %s\n", tmp_path); + found_flag = false; + for (i = 1; i <= rows; i++) { + if (strcmp(result[i], tmp_path) == 0) { + found_flag = true; + break; + } + } + if (!found_flag) { + esc_name = strdup(entry->d_name); + if (esc_name == NULL) { + DPRINTF(E_ERROR, L_INOTIFY, + "strdup error"); + continue; + } + esc_name = modifyString(esc_name, "&", "&amp;", 0); + monitor_insert_directory(1, esc_name, tmp_path); + free(esc_name); + } + } + } else if (fflags & NOTE_WRITE) { + + DPRINTF(E_DEBUG, L_INOTIFY, "File [%s] content updated\n", + path); + sql = sqlite3_mprintf("SELECT PATH from DETAILS where " + "(PATH > '%q/' and PATH <= '%q/%c') and SIZE IS NOT NULL", + path, path, 0xFF); + if (sql_get_table(db, sql, &result, &rows, NULL) != SQLITE_OK) { + DPRINTF(E_WARN, L_INOTIFY, + "Read state [%s]: Query failed\n", path); + goto err1; + } + + for (i = 1; i <= rows; i++) { + DPRINTF(E_DEBUG, L_INOTIFY, + "Indexed content: %s\n", result[i]); + if (access(result[i], R_OK) == -1) + monitor_remove_file(result[i]); + } + + if ((d = opendir(path)) == NULL) { + DPRINTF(E_ERROR, L_INOTIFY, + "Can't list [%s] (%s)\n", path, strerror(errno)); + goto err2; + } + + for (entry = readdir(d); entry != NULL; entry = readdir(d)) { + if ((entry->d_type != DT_REG) && + (entry->d_type != DT_LNK)) + continue; + + result_path_len = snprintf(tmp_path, PATH_MAX, "%s/%s", + path, entry->d_name); + if (result_path_len >= PATH_MAX) { + DPRINTF(E_ERROR, L_INOTIFY, + "File path too long for %s!", + entry->d_name); + continue; + } + DPRINTF(E_DEBUG, L_INOTIFY, "Walking %s\n", tmp_path); + found_flag = false; + for (i = 1; i <= rows; i++) + if (strcmp(result[i], tmp_path) == 0) { + found_flag = true; + break; + } + if (!found_flag ) { + struct stat st; + + if (stat(tmp_path, &st) != 0) { + DPRINTF(E_ERROR, L_INOTIFY, + "stat(%s): %s\n", tmp_path, + strerror(errno)); + continue; + } + esc_name = strdup(entry->d_name); + if (esc_name == NULL) { + DPRINTF(E_ERROR, L_INOTIFY, + "strdup error"); + continue; + } + esc_name = modifyString(esc_name, "&", "&amp;", 0); + if (S_ISDIR(st.st_mode)) + monitor_insert_directory(1, esc_name, tmp_path); + else + monitor_insert_file(esc_name, tmp_path); + free(esc_name); + } + } + } else + return; + + closedir(d); +err2: + sqlite3_free_table(result); +err1: + sqlite3_free(sql); +} + +int +add_watch(int fd __unused, const char *path) +{ + struct watch *wt; + struct event *ev; + int wd; + + wd = open(path, O_RDONLY); + if (wd < 0) { + DPRINTF(E_ERROR, L_INOTIFY, "open(%s) [%s]\n", + path, strerror(errno)); + return (errno); + } + + if ((wt = malloc(sizeof(struct watch))) == NULL) { + DPRINTF(E_ERROR, L_INOTIFY, "malloc() error\n"); + close(wd); + return (ENOMEM); + } + if ((wt->path = strdup(path)) == NULL) { + DPRINTF(E_ERROR, L_INOTIFY, "strdup() error\n"); + close(wd); + free(wt); + return (ENOMEM); + } + wt->isdir = true; + ev = &wt->ev; + ev->data = wt; + ev->fd = wd; + ev->rdwr = EVENT_VNODE; + ev->process_vnode = dir_vnode_process; + + DPRINTF(E_DEBUG, L_INOTIFY, "kqueue add_watch [%s]\n", path); + event_module.add(ev); + + return (0); +} + +/* + * XXXGL: this function has too much copypaste of inotify_create_watches(). + * We need to split out inotify stuff from monitor.c into monitor_inotify.c, + * compile the latter on Linux and this file on FreeBSD, and keep monitor.c + * itself platform independent. + */ +void +kqueue_monitor_start() +{ + struct media_dir_s *media_path; + char **result; + int rows, i; + + DPRINTF(E_DEBUG, L_INOTIFY, "kqueue monitoring starting\n"); + for (media_path = media_dirs; media_path != NULL; + media_path = media_path->next) + add_watch(0, media_path->path); + sql_get_table(db, "SELECT PATH from DETAILS where MIME is NULL and PATH is not NULL", &result, &rows, NULL); + for (i = 1; i <= rows; i++ ) + add_watch(0, result[i]); + sqlite3_free_table(result); +} diff --git a/options.c b/options.c index 5f72a91..ce2609a 100644 --- a/options.c +++ b/options.c @@ -45,12 +45,14 @@ static const struct { { UPNPIFNAME, "network_interface" }, { UPNPPORT, "port" }, { UPNPPRESENTATIONURL, "presentation_url" }, + { UPNPLOCATIONURLOVERRIDE, "location_url" }, { UPNPNOTIFY_INTERVAL, "notify_interval" }, { UPNPUUID, "uuid"}, { UPNPSERIAL, "serial"}, { UPNPMODEL_NAME, "model_name"}, { UPNPMODEL_NUMBER, "model_number"}, { UPNPFRIENDLYNAME, "friendly_name"}, + { UPNPICONDIR, "icon_dir" }, { UPNPMEDIADIR, "media_dir"}, { UPNPALBUMART_NAMES, "album_art_names"}, { UPNPINOTIFY, "inotify" }, @@ -65,7 +67,14 @@ static const struct { { FORCE_SORT_CRITERIA, "force_sort_criteria" }, { MAX_CONNECTIONS, "max_connections" }, { MERGE_MEDIA_DIRS, "merge_media_dirs" }, - { WIDE_LINKS, "wide_links" } + { WIDE_LINKS, "wide_links" }, + { TIVO_DISCOVERY, "tivo_discovery" }, +#ifdef ENABLE_VIDEO_THUMB + { ENABLE_THUMB, "enable_thumbnail" }, + { THUMB_WIDTH, "thumbnail_width" }, +#endif + { ENABLE_MTA, "enable_mta" }, + { ENABLE_SUBTITLES, "enable_subtitles" }, }; int diff --git a/options.h b/options.h index e9ef921..f4c6453 100644 --- a/options.h +++ b/options.h @@ -38,12 +38,14 @@ enum upnpconfigoptions { UPNPIFNAME = 1, /* ext_ifname */ UPNPPORT, /* port */ UPNPPRESENTATIONURL, /* presentation_url */ + UPNPLOCATIONURLOVERRIDE, /* location url override */ UPNPNOTIFY_INTERVAL, /* notify_interval */ UPNPUUID, /* uuid */ UPNPSERIAL, /* serial */ UPNPMODEL_NAME, /* model_name */ UPNPMODEL_NUMBER, /* model_number */ UPNPFRIENDLYNAME, /* how the system should show up to DLNA clients */ + UPNPICONDIR, /* directory to search for icons to send to DLNA clients */ UPNPMEDIADIR, /* directory to search for UPnP-A/V content */ UPNPALBUMART_NAMES, /* list of '/'-delimited file names to check for album art */ UPNPINOTIFY, /* enable inotify on the media directories */ @@ -58,7 +60,14 @@ enum upnpconfigoptions { FORCE_SORT_CRITERIA, /* force sorting by a given sort criteria */ MAX_CONNECTIONS, /* maximum number of simultaneous connections */ MERGE_MEDIA_DIRS, /* don't add an extra directory level when there are multiple media dirs */ - WIDE_LINKS /* allow following symlinks outside the defined media_dirs */ + WIDE_LINKS, /* allow following symlinks outside the defined media_dirs */ + TIVO_DISCOVERY, /* TiVo discovery protocol: bonjour or beacon. Defaults to bonjour if supported */ +#ifdef ENABLE_VIDEO_THUMB + ENABLE_THUMB, /* enable thumbnail generation */ + THUMB_WIDTH, /* thunbnail image with */ +#endif + ENABLE_MTA, + ENABLE_SUBTITLES, /* Enable generic subtitle support for all clients by default */ }; /* readoptionsfile() diff --git a/playlist.c b/playlist.c index ae3a27e..4ac3b2c 100644 --- a/playlist.c +++ b/playlist.c @@ -37,13 +37,17 @@ #include "log.h" int -insert_playlist(const char * path, char * name) +insert_playlist(const char *path, const char *name) { struct song_metadata plist; struct stat file; int items = 0, matches, ret; + char *objname; char type[4]; + if (stat(path, &file) != 0) + return -1; + strncpyt(type, strrchr(name, '.')+1, 4); if( start_plist(path, NULL, &file, NULL, type) != 0 ) @@ -61,27 +65,27 @@ insert_playlist(const char * path, char * name) DPRINTF(E_WARN, L_SCANNER, "Bad playlist [%s]\n", path); return -1; } - strip_ext(name); - DPRINTF(E_DEBUG, L_SCANNER, "Playlist %s contains %d items\n", name, items); - - matches = sql_get_int_field(db, "SELECT count(*) from PLAYLISTS where NAME = '%q'", name); - if( matches > 0 ) - { - sql_exec(db, "INSERT into PLAYLISTS" - " (NAME, PATH, ITEMS) " - "VALUES" - " ('%q(%d)', '%q', %d)", - name, matches, path, items); - } - else + objname = strdup(name); + strip_ext(objname); + matches = sql_get_int_field(db, "SELECT count(*) from PLAYLISTS where NAME = '%q'", objname); + if (matches > 0) { - sql_exec(db, "INSERT into PLAYLISTS" - " (NAME, PATH, ITEMS) " - "VALUES" - " ('%q', '%q', %d)", - name, path, items); + char *newname; + xasprintf(&newname, "%s(%d)", objname, matches); + strip_ext(newname); + free(objname); + objname = newname; } + + DPRINTF(E_DEBUG, L_SCANNER, "Playlist %s contains %d items\n", objname, items); + + sql_exec(db, "INSERT into PLAYLISTS" + " (NAME, PATH, ITEMS, TIMESTAMP) " + "VALUES" + " ('%q', '%q', %d, %lld)", + objname, path, items, (long long)file.st_mtime); + free(objname); return 0; } @@ -122,6 +126,12 @@ fill_playlists(void) int64_t plID, detailID; char sql_buf[] = "SELECT ID, NAME, PATH from PLAYLISTS where ITEMS > FOUND"; + if( GETFLAG(NO_PLAYLIST_MASK) ) + { + DPRINTF(E_WARN, L_SCANNER, "Playlist creation disabled\n"); + return 0; + } + DPRINTF(E_WARN, L_SCANNER, "Parsing playlists...\n"); if( sql_get_table(db, sql_buf, &result, &rows, NULL) != SQLITE_OK ) @@ -165,7 +175,7 @@ fill_playlists(void) { //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "%d: already in database\n", plist.track); found++; - freetags(&plist); + freetags(&plist); continue; } if( last_dir ) @@ -245,7 +255,7 @@ fill_playlists(void) goto retry; } } - freetags(&plist); + freetags(&plist); } if( last_dir ) { diff --git a/playlist.h b/playlist.h index 8abcb17..478e504 100644 --- a/playlist.h +++ b/playlist.h @@ -24,10 +24,7 @@ #ifndef __PLAYLIST_H__ #define __PLAYLIST_H__ -int -insert_playlist(const char * path, char * name); - -int -fill_playlists(void); +int insert_playlist(const char *path, const char *name); +int fill_playlists(void); #endif // __PLAYLIST_H__ diff --git a/process.c b/process.c index ecb4922..197c59b 100644 --- a/process.c +++ b/process.c @@ -38,10 +38,12 @@ #include #include +#include "config.h" +#include "event.h" #include "upnpglobalvars.h" #include "process.h" -#include "config.h" #include "log.h" +#include "utils.h" struct child *children = NULL; int number_of_children = 0; @@ -64,7 +66,7 @@ add_process_info(pid_t pid, struct client_cache_s *client) } } -static inline void +static inline int remove_process_info(pid_t pid) { struct child *child; @@ -76,10 +78,12 @@ remove_process_info(pid_t pid) if (child->pid != pid) continue; child->pid = 0; - if (child->client) + if (child->client) { child->client->connections--; - break; + return 1; + } } + return 0; } pid_t @@ -100,7 +104,10 @@ process_fork(struct client_cache_s *client) client->connections++; add_process_info(pid, client); number_of_children++; - } + } else if (pid == 0) + event_module.fini(); + else + DPRINTF(E_FATAL, L_GENERAL, "Fork() failed: %s\n", strerror(errno)); return pid; } @@ -119,8 +126,12 @@ process_handle_child_termination(int signal) else break; } - number_of_children--; - remove_process_info(pid); + if(remove_process_info(pid)) { + // Only update the number of children if the process that just died was + // a registered child, i.e. an http connection. If it was something else, + // such as the background scanner, it doesn't count as a child. + number_of_children--; + } } } @@ -151,11 +162,11 @@ process_daemonize(void) for (i=getdtablesize();i>=0;--i) close(i); i = open("/dev/null",O_RDWR); /* open stdin */ - dup(i); /* stdout */ - dup(i); /* stderr */ + IGNORE_RETURN_VALUE(dup(i)); /* stdout */ + IGNORE_RETURN_VALUE(dup(i)); /* stderr */ umask(027); - chdir("/"); + IGNORE_RETURN_VALUE(chdir("/")); break; /* parent process */ diff --git a/scanner.c b/scanner.c index 29fec05..701588c 100644 --- a/scanner.c +++ b/scanner.c @@ -1,5 +1,5 @@ /* MiniDLNA media server - * Copyright (C) 2008-2009 Justin Maggard + * Copyright (C) 2008-2017 Justin Maggard * * This file is part of MiniDLNA. * @@ -45,7 +45,9 @@ #include "scanner.h" #include "albumart.h" #include "containers.h" +#include "video_thumb.h" #include "log.h" +#include "monitor.h" #if SCANDIR_CONST typedef const struct dirent scan_filter; @@ -94,12 +96,12 @@ insert_container(const char *item, const char *rootParent, const char *refID, co int ret = 0; result = sql_get_text_field(db, "SELECT OBJECT_ID from OBJECTS o " - "left join DETAILS d on (o.DETAIL_ID = d.ID)" - " where o.PARENT_ID = '%s'" - " and o.NAME like '%q'" - " and d.ARTIST %s %Q" - " and o.CLASS = 'container.%s' limit 1", - rootParent, item, artist?"like":"is", artist, class); + "left join DETAILS d on (o.DETAIL_ID = d.ID)" + " where o.PARENT_ID = '%s'" + " and o.NAME like '%q'" + " and d.ARTIST %s %Q" + " and o.CLASS = 'container.%s' limit 1", + rootParent, item, artist?"like":"is", artist, class); if( result ) { base = strrchr(result, '$'); @@ -175,7 +177,7 @@ insert_containers(const char *name, const char *path, const char *refID, const c else { insert_container(date_taken, IMAGE_DATE_ID, NULL, "album.photoAlbum", NULL, NULL, NULL, &objectID, &parentID); - sprintf(last_date.parentID, IMAGE_DATE_ID"$%llX", (unsigned long long)parentID); + snprintf(last_date.parentID, sizeof(last_date.parentID), IMAGE_DATE_ID"$%llX", (unsigned long long)parentID); last_date.objectID = objectID; strncpyt(last_date.name, date_taken, sizeof(last_date.name)); //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "Creating cached date item: %s/%s/%X\n", last_date.name, last_date.parentID, last_date.objectID); @@ -202,7 +204,7 @@ insert_containers(const char *name, const char *path, const char *refID, const c else { insert_container(date_taken, last_cam.parentID, NULL, "album.photoAlbum", NULL, NULL, NULL, &objectID, &parentID); - sprintf(last_camdate.parentID, "%s$%llX", last_cam.parentID, (long long)parentID); + snprintf(last_camdate.parentID, sizeof(last_camdate.parentID), "%s$%llX", last_cam.parentID, (long long)parentID); last_camdate.objectID = objectID; strncpyt(last_camdate.name, date_taken, sizeof(last_camdate.name)); //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "Creating cached camdate item: %s/%s/%s/%X\n", camera, last_camdate.name, last_camdate.parentID, last_camdate.objectID); @@ -270,14 +272,16 @@ insert_containers(const char *name, const char *path, const char *refID, const c { if( !valid_cache || strcmp(artist, last_artist.name) != 0 ) { - insert_container(artist, MUSIC_ARTIST_ID, NULL, "person.musicArtist", NULL, genre, NULL, &objectID, &parentID); + album_art = sql_get_text_field(db, "SELECT ALBUM_ART from DETAILS where PATH not NULL and TITLE like '%%q'", artist); + insert_container(artist, MUSIC_ARTIST_ID, NULL, "person.musicArtist", NULL, genre, album_art, &objectID, &parentID); sprintf(last_artist.parentID, MUSIC_ARTIST_ID"$%llX", (long long)parentID); strncpyt(last_artist.name, artist, sizeof(last_artist.name)); last_artistAlbum.name[0] = '\0'; /* Add this file to the "- All Albums -" container as well */ insert_container(_("- All Albums -"), last_artist.parentID, NULL, "album", artist, genre, NULL, &objectID, &parentID); - sprintf(last_artistAlbumAll.parentID, "%s$%llX", last_artist.parentID, (long long)parentID); + snprintf(last_artistAlbumAll.parentID, sizeof(last_artistAlbumAll.parentID), "%s$%llX", last_artist.parentID, (long long)parentID); last_artistAlbumAll.objectID = objectID; + sqlite3_free(album_art); } else { @@ -292,7 +296,7 @@ insert_containers(const char *name, const char *path, const char *refID, const c { insert_container(album?album:_("Unknown Album"), last_artist.parentID, album?last_album.parentID:NULL, "album.musicAlbum", artist, genre, album_art, &objectID, &parentID); - sprintf(last_artistAlbum.parentID, "%s$%llX", last_artist.parentID, (long long)parentID); + snprintf(last_artistAlbum.parentID, sizeof(last_artistAlbum.parentID), "%s$%llX", last_artist.parentID, (long long)parentID); last_artistAlbum.objectID = objectID; strncpyt(last_artistAlbum.name, album ? album : _("Unknown Album"), sizeof(last_artistAlbum.name)); //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "Creating cached artist/album item: %s/%s/%X\n", last_artist.name, last_artist.parentID, last_artist.objectID); @@ -317,7 +321,7 @@ insert_containers(const char *name, const char *path, const char *refID, const c strncpyt(last_genre.name, genre, sizeof(last_genre.name)); /* Add this file to the "- All Artists -" container as well */ insert_container(_("- All Artists -"), last_genre.parentID, NULL, "person", NULL, genre, NULL, &objectID, &parentID); - sprintf(last_genreArtistAll.parentID, "%s$%llX", last_genre.parentID, (long long)parentID); + snprintf(last_genreArtistAll.parentID, sizeof(last_genreArtistAll.parentID), "%s$%llX", last_genre.parentID, (long long)parentID); last_genreArtistAll.objectID = objectID; } else @@ -332,7 +336,7 @@ insert_containers(const char *name, const char *path, const char *refID, const c { insert_container(artist?artist:_("Unknown Artist"), last_genre.parentID, artist?last_artist.parentID:NULL, "person.musicArtist", NULL, genre, NULL, &objectID, &parentID); - sprintf(last_genreArtist.parentID, "%s$%llX", last_genre.parentID, (long long)parentID); + snprintf(last_genreArtist.parentID, sizeof(last_genreArtist.parentID), "%s$%llX", last_genre.parentID, (long long)parentID); last_genreArtist.objectID = objectID; strncpyt(last_genreArtist.name, artist ? artist : _("Unknown Artist"), sizeof(last_genreArtist.name)); //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "Creating cached genre/artist item: %s/%s/%X\n", last_genreArtist.name, last_genreArtist.parentID, last_genreArtist.objectID); @@ -397,7 +401,7 @@ insert_directory(const char *name, const char *path, const char *base, const cha char id_buf[64], parent_buf[64], refID[64]; char *dir_buf, *dir; - dir_buf = strdup(path); + dir_buf = strdup(path); dir = dirname(dir_buf); snprintf(refID, sizeof(refID), "%s%s$%X", BROWSEDIR_ID, parentID, objectID); snprintf(id_buf, sizeof(id_buf), "%s%s$%X", base, parentID, objectID); @@ -446,58 +450,59 @@ insert_directory(const char *name, const char *path, const char *base, const cha } int -insert_file(char *name, const char *path, const char *parentID, int object, media_types types) +insert_file(const char *name, const char *path, const char *parentID, int object, media_types types) { - char class[32]; + const char *class; char objectID[64]; int64_t detailID = 0; char base[8]; char *typedir_parentID; char *baseid; - char *orig_name = NULL; + char *objname; + media_types mtype = get_media_type(name); - if( (types & TYPE_IMAGES) && is_image(name) ) + if( mtype == TYPE_IMAGE && (types & TYPE_IMAGE) ) { if( is_album_art(name) ) return -1; strcpy(base, IMAGE_DIR_ID); - strcpy(class, "item.imageItem.photo"); + class = "item.imageItem.photo"; detailID = GetImageMetadata(path, name); } - else if( (types & TYPE_VIDEO) && is_video(name) ) + else if( mtype == TYPE_VIDEO && (types & TYPE_VIDEO) ) { - orig_name = strdup(name); strcpy(base, VIDEO_DIR_ID); - strcpy(class, "item.videoItem"); + class = "item.videoItem"; detailID = GetVideoMetadata(path, name); - if( !detailID ) - strcpy(name, orig_name); } - else if( is_playlist(name) ) + else if( mtype == TYPE_PLAYLIST && (types & TYPE_PLAYLIST) ) { if( insert_playlist(path, name) == 0 ) return 1; } - if( !detailID && (types & TYPE_AUDIO) && is_audio(name) ) + /* Some file extensions can be used for both audio and video. + ** Fall back to audio on these files if video parsing fails. */ + if (!detailID && (types & TYPE_AUDIO) && is_audio(name) ) { strcpy(base, MUSIC_DIR_ID); - strcpy(class, "item.audioItem.musicTrack"); + class = "item.audioItem.musicTrack"; detailID = GetAudioMetadata(path, name); } - free(orig_name); if( !detailID ) { - DPRINTF(E_WARN, L_SCANNER, "Unsuccessful getting details for %s!\n", path); + DPRINTF(E_WARN, L_SCANNER, "Unsuccessful getting details for %s\n", path); return -1; } sprintf(objectID, "%s%s$%X", BROWSEDIR_ID, parentID, object); + objname = strdup(name); + strip_ext(objname); sql_exec(db, "INSERT into OBJECTS" " (OBJECT_ID, PARENT_ID, CLASS, DETAIL_ID, NAME) " "VALUES" " ('%s', '%s%s', '%s', %lld, '%q')", - objectID, BROWSEDIR_ID, parentID, class, detailID, name); + objectID, BROWSEDIR_ID, parentID, class, detailID, objname); if( *parentID ) { @@ -509,16 +514,18 @@ insert_file(char *name, const char *path, const char *parentID, int object, medi typedir_objectID = strtol(baseid+1, NULL, 16); *baseid = '\0'; } - insert_directory(name, path, base, typedir_parentID, typedir_objectID); + insert_directory(objname, path, base, typedir_parentID, typedir_objectID); free(typedir_parentID); } sql_exec(db, "INSERT into OBJECTS" " (OBJECT_ID, PARENT_ID, REF_ID, CLASS, DETAIL_ID, NAME) " "VALUES" " ('%s%s$%X', '%s%s', '%s', '%s', %lld, '%q')", - base, parentID, object, base, parentID, objectID, class, detailID, name); + base, parentID, object, base, parentID, objectID, class, detailID, objname); + + insert_containers(objname, path, objectID, class, detailID); + free(objname); - insert_containers(name, path, objectID, class, detailID); return 0; } @@ -561,6 +568,9 @@ CreateDatabase(void) if( ret != SQLITE_OK ) goto sql_failed; ret = sql_exec(db, create_bookmarkTable_sqlite); + if( ret != SQLITE_OK ) + goto sql_failed; + ret = sql_exec(db, create_MTATable_sqlite); if( ret != SQLITE_OK ) goto sql_failed; ret = sql_exec(db, create_playlistTable_sqlite); @@ -608,6 +618,7 @@ CreateDatabase(void) sql_exec(db, "create INDEX IDX_DETAILS_PATH ON DETAILS(PATH);"); sql_exec(db, "create INDEX IDX_DETAILS_ID ON DETAILS(ID);"); sql_exec(db, "create INDEX IDX_ALBUM_ART ON ALBUM_ART(ID);"); + sql_exec(db, "create INDEX IDX_MTA ON MTA(ID);"); sql_exec(db, "create INDEX IDX_SCANNER_OPT ON OBJECTS(PARENT_ID, NAME, OBJECT_ID);"); sql_failed: @@ -627,9 +638,9 @@ filter_type(scan_filter *d) { #if HAVE_STRUCT_DIRENT_D_TYPE return ( (d->d_type == DT_DIR) || - (d->d_type == DT_LNK) || - (d->d_type == DT_UNKNOWN) - ); + (d->d_type == DT_LNK) || + (d->d_type == DT_UNKNOWN) + ); #else return 1; #endif @@ -639,79 +650,79 @@ static int filter_a(scan_filter *d) { return ( filter_hidden(d) && - (filter_type(d) || + (filter_type(d) || (is_reg(d) && (is_audio(d->d_name) || - is_playlist(d->d_name)))) - ); + is_playlist(d->d_name)))) + ); } static int filter_av(scan_filter *d) { return ( filter_hidden(d) && - (filter_type(d) || + (filter_type(d) || (is_reg(d) && (is_audio(d->d_name) || is_video(d->d_name) || - is_playlist(d->d_name)))) - ); + is_playlist(d->d_name)))) + ); } static int filter_ap(scan_filter *d) { return ( filter_hidden(d) && - (filter_type(d) || + (filter_type(d) || (is_reg(d) && (is_audio(d->d_name) || is_image(d->d_name) || - is_playlist(d->d_name)))) - ); + is_playlist(d->d_name)))) + ); } static int filter_v(scan_filter *d) { return ( filter_hidden(d) && - (filter_type(d) || + (filter_type(d) || (is_reg(d) && - is_video(d->d_name))) - ); + is_video(d->d_name))) + ); } static int filter_vp(scan_filter *d) { return ( filter_hidden(d) && - (filter_type(d) || + (filter_type(d) || (is_reg(d) && (is_video(d->d_name) || - is_image(d->d_name)))) - ); + is_image(d->d_name)))) + ); } static int filter_p(scan_filter *d) { return ( filter_hidden(d) && - (filter_type(d) || + (filter_type(d) || (is_reg(d) && is_image(d->d_name))) - ); + ); } static int filter_avp(scan_filter *d) { return ( filter_hidden(d) && - (filter_type(d) || + (filter_type(d) || (is_reg(d) && (is_audio(d->d_name) || is_image(d->d_name) || is_video(d->d_name) || - is_playlist(d->d_name)))) - ); + is_playlist(d->d_name)))) + ); } static void @@ -737,16 +748,16 @@ ScanDirectory(const char *dir, const char *parent, media_types dir_types) case TYPE_AUDIO|TYPE_VIDEO: n = scandir(dir, &namelist, filter_av, alphasort); break; - case TYPE_AUDIO|TYPE_IMAGES: + case TYPE_AUDIO|TYPE_IMAGE: n = scandir(dir, &namelist, filter_ap, alphasort); break; case TYPE_VIDEO: n = scandir(dir, &namelist, filter_v, alphasort); break; - case TYPE_VIDEO|TYPE_IMAGES: + case TYPE_VIDEO|TYPE_IMAGE: n = scandir(dir, &namelist, filter_vp, alphasort); break; - case TYPE_IMAGES: + case TYPE_IMAGE: n = scandir(dir, &namelist, filter_p, alphasort); break; default: @@ -797,12 +808,16 @@ ScanDirectory(const char *dir, const char *parent, media_types dir_types) if( (type == TYPE_DIR) && (access(full_path, R_OK|X_OK) == 0) ) { char *parent_id; + + if( has_ignore(full_path, 0) ) + continue; + insert_directory(name, full_path, BROWSEDIR_ID, THISORNUL(parent), i+startID); xasprintf(&parent_id, "%s$%X", THISORNUL(parent), i+startID); ScanDirectory(full_path, parent_id, dir_types); free(parent_id); } - else if( type == TYPE_FILE && (access(full_path, R_OK) == 0) ) + else if( !has_ignore(dir, 1) && type == TYPE_FILE && (access(full_path, R_OK) == 0) ) { if( insert_file(name, full_path, THISORNUL(parent), i+startID, dir_types) == 0 ) fileno++; @@ -825,49 +840,176 @@ char* GetParentID(struct media_dir_s * media_path) { if(media_path->vfolder != NULL) { startID = get_next_available_id("OBJECTS", BROWSEDIR_ID); insert_directory(media_path->vfolder, media_path->path, BROWSEDIR_ID, "", startID); - asprintf(&parent_id, "%s$%X", "", startID); - DPRINTF(E_DEBUG, L_SCANNER, _("[GetParentID] vfolder=%s, startID=%d, parent_id=%s\n"), - media_path->vfolder, startID, parent_id); + if(asprintf(&parent_id, "%s$%X", "", startID)) + { + DPRINTF(E_DEBUG, L_SCANNER, _("[GetParentID] vfolder=%s, startID=%d, parent_id=%s\n"), + media_path->vfolder, startID, parent_id); + } } return parent_id; } -static void -_notify_start(void) +void +GenerateMTA(const char *videopath) { -#ifdef READYNAS - FILE *flag = fopen("/ramfs/.upnp-av_scan", "w"); - if( flag ) - fclose(flag); -#endif + char *sql; + char **result; + int i, ret, cols, row; + char *id, *path, *duration, *mta_path; + int h,m,s; + char *dot; + int len; + int64_t mta_id; + int allblack, videolen; + long long unsigned int fileno = 0; + + if (!runtime_vars.mta || runtime_vars.mta < 0) + return; + + allblack = (runtime_vars.mta == 1); + + if (videopath) + { + sql = sqlite3_mprintf("SELECT ID, PATH, DURATION from DETAILS where MIME glob 'v*' AND MTA ='0' and PATH = '%q'", videopath); + if (!sql) + return; + } + else + { + sql = sqlite3_mprintf("SELECT ID, PATH, DURATION from DETAILS where MIME glob 'v*' AND MTA ='0'"); + if (!sql) + return; + } + + ret = sql_get_table(db, sql, &result, &row, &cols); + if( ret != SQLITE_OK || !row ) + goto mta_error; + + if (!videopath) + DPRINTF(E_WARN, L_SCANNER, _("Starting MTA files generation ... \n")); + + for (i = cols; i < (row + 1) * cols;) + { + id = result[i++]; + path = result[i++]; + duration = result[i++]; + + len = strnlen(duration, 15); + if (len < 11 || len == 15) + continue; + + dot = index(duration, ':'); + if (!dot || strnlen(dot, 11) != 10) + continue; + + h = atoi(duration); + m = atoi(dot+1); + s = atoi(dot+4); + videolen = 3600 * h + 60 * m + s; + + mta_path = video_thumb_generate_mta_file(path, videolen, + allblack ? 1: (runtime_vars.mta == 2 ? 0 : (videolen < 60 * runtime_vars.mta ? 1: 0 ))); + if (!mta_path) { + continue; + } + + if ( sql_exec(db, "INSERT into MTA (PATH) VALUES ('%q')", mta_path) == SQLITE_OK ) + { + mta_id = sqlite3_last_insert_rowid(db); + if( sql_exec(db, "UPDATE DETAILS set MTA = %lld where ID = '%q'", (long long)mta_id, id) != SQLITE_OK) + DPRINTF(E_WARN, L_SCANNER, "Error setting %s as MTA file for %s\n", mta_path, path); + } + free(mta_path); + fileno++; + + } + + if (!videopath) + DPRINTF(E_WARN, L_SCANNER, _("MTA files generation finished (%llu files)!\n"), fileno); + +mta_error: + sqlite3_free_table(result); + sqlite3_free(sql); } -static void -_notify_stop(void) +/* rescan functions added by shrimpkin@sourceforge.net */ +static int +cb_orphans(void *args, int argc, char **argv, char **azColName) { -#ifdef READYNAS - if( access("/ramfs/.rescan_done", F_OK) == 0 ) - system("/bin/sh /ramfs/.rescan_done"); - unlink("/ramfs/.upnp-av_scan"); -#endif + const char *path = argv[0]; + const char *mime = argv[1]; + + /* If we can't access the path, remove it */ + if (access(path, R_OK) != 0) + { + DPRINTF(E_DEBUG, L_SCANNER, "Removing %s [%s]\n", path, mime ? "file" : "dir"); + if (mime) + monitor_remove_file(path); + else + monitor_remove_directory(0, path); + } + + return 0; } void -start_scanner() +start_rescan(void) { struct media_dir_s *media_path; - char path[MAXPATHLEN]; - char *parent_id = NULL; + char *esc_name = NULL; + char *zErrMsg; + const char *sql_files = "SELECT path, mime FROM details WHERE path NOT NULL AND mime IS NOT NULL;"; + const char *sql_dir = "SELECT path, mime FROM details WHERE path NOT NULL AND mime IS NULL;"; + int changes = sqlite3_total_changes(db); + const char *summary; + int ret; - if (setpriority(PRIO_PROCESS, 0, 15) == -1) - DPRINTF(E_WARN, L_INOTIFY, "Failed to reduce scanner thread priority\n"); - _notify_start(); + DPRINTF(E_INFO, L_SCANNER, "Starting rescan\n"); - setlocale(LC_COLLATE, ""); + /* Find and remove any dead directory links */ + ret = sqlite3_exec(db, sql_dir, cb_orphans, NULL, &zErrMsg); + if (ret != SQLITE_OK) + { + DPRINTF(E_MAXDEBUG, L_SCANNER, "SQL error: %s\nBAD SQL: %s\n", zErrMsg, sql_dir); + sqlite3_free(zErrMsg); + } + + /* Find and remove any dead file links */ + ret = sqlite3_exec(db, sql_files, cb_orphans, NULL, &zErrMsg); + if (ret != SQLITE_OK) + { + DPRINTF(E_MAXDEBUG, L_SCANNER, "SQL error: %s\nBAD SQL: %s\n", zErrMsg, sql_files); + sqlite3_free(zErrMsg); + } + + /* Rescan media_paths for new and/or modified files */ + for (media_path = media_dirs; media_path != NULL; media_path = media_path->next) + { + char path[MAXPATHLEN], buf[MAXPATHLEN]; + strncpyt(path, media_path->path, sizeof(path)); + strncpyt(buf, media_path->path, sizeof(buf)); + esc_name = escape_tag(basename(buf), 1); + monitor_insert_directory(0, esc_name, path); + free(esc_name); + } + fill_playlists(); + + if (sqlite3_total_changes(db) != changes) + summary = "changes found"; + else + summary = "no changes"; + DPRINTF(E_INFO, L_SCANNER, "Rescan completed. (%s)\n", summary); +} +/* end rescan functions */ + +void +start_rebuild() +{ + struct media_dir_s *media_path; + char path[MAXPATHLEN]; + char *parent_id = NULL; - av_register_all(); - av_log_set_level(AV_LOG_PANIC); for( media_path = media_dirs; media_path != NULL; media_path = media_path->next ) { int64_t id; @@ -880,7 +1022,11 @@ start_scanner() { int startID = get_next_available_id("OBJECTS", BROWSEDIR_ID); id = insert_directory(bname, path, BROWSEDIR_ID, "", startID); - asprintf(&parent_id, "$%X", startID); + if(asprintf(&parent_id, "$%X", startID)<0) + { + DPRINTF(E_WARN, L_SCANNER, "Parent ID could not be generated\n"); + parent_id = NULL; + } } else id = GetFolderMetadata(bname, media_path->path, NULL, NULL, 0); @@ -895,22 +1041,55 @@ start_scanner() parent_id = NULL; } } - _notify_stop(); /* Create this index after scanning, so it doesn't slow down the scanning process. * This index is very useful for large libraries used with an XBox360 (or any * client that uses UPnPSearch on large containers). */ sql_exec(db, "create INDEX IDX_SEARCH_OPT ON OBJECTS(OBJECT_ID, CLASS, DETAIL_ID);"); - if( GETFLAG(NO_PLAYLIST_MASK) ) - { - DPRINTF(E_WARN, L_SCANNER, "Playlist creation disabled\n"); - } - else - { - fill_playlists(); - } + fill_playlists(); + + GenerateMTA(NULL); DPRINTF(E_DEBUG, L_SCANNER, "Initial file scan completed\n"); //JM: Set up a db version number, so we know if we need to rebuild due to a new structure. sql_exec(db, "pragma user_version = %d;", DB_VERSION); } + +void +start_scanner() +{ + DPRINTF(E_DEBUG, L_SCANNER, "Starting Media Scan\n"); +#if USE_FORK + SETFLAG(SCANNING_MASK); + sqlite3_close(db); + scanner_pid = fork(); + open_db(&db); + if(scanner_pid > 0) { // parent process (doesn't need to do anything) + return; + } + // The child process (or us, in case of an error) will continue below + // At the end of the function the child will also close the database again. +#endif + + + if (setpriority(PRIO_PROCESS, 0, 15) == -1) + DPRINTF(E_WARN, L_INOTIFY, "Failed to reduce scanner thread priority\n"); + + setlocale(LC_COLLATE, ""); + + if( GETFLAG(RESCAN_MASK) ) + { + start_rescan(); + } + else { + start_rebuild(); + } + +#if USE_FORK + if(scanner_pid == 0) { // child (scanner) process + sqlite3_close(db); + log_close(); + exit(EXIT_SUCCESS); + } +#endif +} diff --git a/scanner.h b/scanner.h index d220762..d43e80d 100644 --- a/scanner.h +++ b/scanner.h @@ -75,7 +75,7 @@ int64_t insert_directory(const char *name, const char *path, const char *base, const char *parentID, int objectID); int -insert_file(char *name, const char *path, const char *parentID, int object, media_types dir_types); +insert_file(const char *name, const char *path, const char *parentID, int object, media_types dir_types); int CreateDatabase(void); @@ -83,4 +83,7 @@ CreateDatabase(void); void start_scanner(); +void +GenerateMTA(const char *videopath); + #endif diff --git a/scanner_sqlite.h b/scanner_sqlite.h index 3a9aba1..c615ecd 100644 --- a/scanner_sqlite.h +++ b/scanner_sqlite.h @@ -5,7 +5,7 @@ * Author : Douglas Carmichael * * MiniDLNA media server - * Copyright (C) 2008-2009 Justin Maggard + * Copyright (C) 2008-2017 Justin Maggard * * This file is part of MiniDLNA. * @@ -29,7 +29,8 @@ char create_objectTable_sqlite[] = "CREATE TABLE OBJECTS (" "REF_ID TEXT DEFAULT NULL, " "CLASS TEXT NOT NULL, " "DETAIL_ID INTEGER DEFAULT NULL, " - "NAME TEXT DEFAULT NULL);"; + "NAME TEXT DEFAULT NULL" + ");"; char create_detailTable_sqlite[] = "CREATE TABLE DETAILS (" "ID INTEGER PRIMARY KEY AUTOINCREMENT, " @@ -52,14 +53,16 @@ char create_detailTable_sqlite[] = "CREATE TABLE DETAILS (" "RESOLUTION TEXT, " "THUMBNAIL BOOL DEFAULT 0, " "ALBUM_ART INTEGER DEFAULT 0, " + "MTA INTEGER DEFAULT 0, " "ROTATION INTEGER, " "DLNA_PN TEXT, " - "MIME TEXT);"; + "MIME TEXT" + ");"; char create_albumArtTable_sqlite[] = "CREATE TABLE ALBUM_ART (" "ID INTEGER PRIMARY KEY AUTOINCREMENT, " "PATH TEXT NOT NULL" - ");"; + ");"; char create_captionTable_sqlite[] = "CREATE TABLE CAPTIONS (" "ID INTEGER PRIMARY KEY, " @@ -68,7 +71,13 @@ char create_captionTable_sqlite[] = "CREATE TABLE CAPTIONS (" char create_bookmarkTable_sqlite[] = "CREATE TABLE BOOKMARKS (" "ID INTEGER PRIMARY KEY, " - "SEC INTEGER" + "SEC INTEGER, " + "WATCH_COUNT INTEGER" + ");"; + +char create_MTATable_sqlite[] = "CREATE TABLE MTA (" + "ID INTEGER PRIMARY KEY AUTOINCREMENT, " + "PATH TEXT NOT NULL" ");"; char create_playlistTable_sqlite[] = "CREATE TABLE PLAYLISTS (" @@ -76,12 +85,11 @@ char create_playlistTable_sqlite[] = "CREATE TABLE PLAYLISTS (" "NAME TEXT NOT NULL, " "PATH TEXT NOT NULL, " "ITEMS INTEGER DEFAULT 0, " - "FOUND INTEGER DEFAULT 0" + "FOUND INTEGER DEFAULT 0, " + "TIMESTAMP INTEGER DEFAULT 0" ");"; char create_settingsTable_sqlite[] = "CREATE TABLE SETTINGS (" "KEY TEXT NOT NULL, " "VALUE TEXT" ");"; - - diff --git a/select.c b/select.c new file mode 100644 index 0000000..dce7311 --- /dev/null +++ b/select.c @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2017 Gleb Smirnoff + * Copyright (c) 2002-2017 Igor Sysoev + * Copyright (c) 2011-2017 Nginx, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "event.h" +#include "log.h" + +static event_module_init_t select_init; +static event_module_fini_t select_fini; +static event_module_add_t select_add; +static event_module_del_t select_del; +static event_module_process_t select_process; + +static fd_set master_read_fd_set; +static fd_set master_write_fd_set; +static fd_set work_read_fd_set; +static fd_set work_write_fd_set; + +static struct event **events; +static int nevents; +static int max_fd; + +struct event_module event_module = { + .add = select_add, + .del = select_del, + .process = select_process, + .init = select_init, + .fini = select_fini, +}; + +static int +select_init(void) +{ + + events = calloc(FD_SETSIZE, sizeof(struct event *)); + if (events == NULL) + return (ENOMEM); + + FD_ZERO(&master_read_fd_set); + FD_ZERO(&master_write_fd_set); + max_fd = 0; + nevents = 0; + + return (0); +} + + +static void +select_fini(void) +{ + + free(events); + events = NULL; +} + +static int +select_add(struct event *ev) +{ + + assert(ev->fd < FD_SETSIZE); + + switch (ev->rdwr) { + case EVENT_READ: + FD_SET(ev->fd, &master_read_fd_set); + break; + case EVENT_WRITE: + FD_SET(ev->fd, &master_write_fd_set); + break; + } + + if (max_fd != -1 && max_fd < ev->fd) + max_fd = ev->fd; + + events[nevents] = ev; + ev->index = nevents++; + + assert(nevents < FD_SETSIZE); + + return (0); +} + +static int +select_del(struct event *ev, int flags) +{ + + assert(ev->fd < FD_SETSIZE); + + switch (ev->rdwr) { + case EVENT_READ: + FD_CLR(ev->fd, &master_read_fd_set); + break; + case EVENT_WRITE: + FD_CLR(ev->fd, &master_write_fd_set); + break; + } + + if (max_fd == ev->fd) + max_fd = -1; + + if (ev->index < --nevents) { + struct event *ev0; + + ev0 = events[nevents]; + events[ev->index] = ev0; + ev0->index = ev->index; + } + ev->index = -1; + + return (0); +} + +static int +select_process(u_long msec) +{ + struct timeval tv, *tp; + struct event *ev; + int ready, i; + + /* Need to rescan for max_fd. */ + if (max_fd == -1) + for (i = 0; i < nevents; i++) { + if (max_fd < events[i]->fd) + max_fd = events[i]->fd; + } + + tv.tv_sec = (long) (msec / 1000); + tv.tv_usec = (long) ((msec % 1000) * 1000); + tp = &tv; + + work_read_fd_set = master_read_fd_set; + work_write_fd_set = master_write_fd_set; + + ready = select(max_fd + 1, &work_read_fd_set, &work_write_fd_set, NULL, tp); + + if (ready == -1) { + if (errno == EINTR) + return (errno); + DPRINTF(E_FATAL, L_GENERAL, "select(): %s. EXITING\n", strerror(errno)); + } + + if (ready == 0) + return (0); + + for (i = 0; i < nevents; i++) { + ev = events[i]; + + switch (ev->rdwr) { + case EVENT_READ: + if (FD_ISSET(ev->fd, &work_read_fd_set)) + ev->process(ev); + break; + case EVENT_WRITE: + if (FD_ISSET(ev->fd, &work_write_fd_set)) + ev->process(ev); + break; + } + } + + return (0); +} diff --git a/sendfile.h b/sendfile.h index b4d1951..1ea50c5 100644 --- a/sendfile.h +++ b/sendfile.h @@ -51,7 +51,7 @@ int sys_sendfile(int sock, int sendfd, off_t *offset, off_t len) int ret; size_t nbytes = len; - ret = sendfile(sendfd, sock, *offset, nbytes, NULL, &len, SF_MNOWAIT); + ret = sendfile(sendfd, sock, *offset, nbytes, NULL, &len, 0); *offset += len; return ret; diff --git a/sql.c b/sql.c index b2ea238..37ea961 100644 --- a/sql.c +++ b/sql.c @@ -1,5 +1,5 @@ /* MiniDLNA media server - * Copyright (C) 2008-2009 Justin Maggard + * Copyright (C) 2008-2017 Justin Maggard * * This file is part of MiniDLNA. * @@ -18,10 +18,13 @@ #include #include #include +#include +#include #include "sql.h" #include "upnpglobalvars.h" #include "log.h" +#include "utils.h" int sql_exec(sqlite3 *db, const char *fmt, ...) @@ -93,10 +96,10 @@ sql_get_int_field(sqlite3 *db, const char *fmt, ...) for (counter = 0; ((result = sqlite3_step(stmt)) == SQLITE_BUSY || result == SQLITE_LOCKED) && counter < 2; counter++) { - /* While SQLITE_BUSY has a built in timeout, - SQLITE_LOCKED does not, so sleep */ - if (result == SQLITE_LOCKED) - sleep(1); + /* While SQLITE_BUSY has a built in timeout, + * SQLITE_LOCKED does not, so sleep */ + if (result == SQLITE_LOCKED) + sleep(1); } switch (result) @@ -114,10 +117,10 @@ sql_get_int_field(sqlite3 *db, const char *fmt, ...) ret = sqlite3_column_int(stmt, 0); break; default: - DPRINTF(E_WARN, L_DB_SQL, "%s: step failed: %s\n%s\n", __func__, sqlite3_errmsg(db), sql); + DPRINTF(E_WARN, L_DB_SQL, "%s: step failed: %d - %s\n%s\n", __func__, result, sqlite3_errmsg(db), sql); ret = -1; break; - } + } sqlite3_free(sql); sqlite3_finalize(stmt); @@ -152,10 +155,10 @@ sql_get_int64_field(sqlite3 *db, const char *fmt, ...) for (counter = 0; ((result = sqlite3_step(stmt)) == SQLITE_BUSY || result == SQLITE_LOCKED) && counter < 2; counter++) { - /* While SQLITE_BUSY has a built in timeout, - SQLITE_LOCKED does not, so sleep */ - if (result == SQLITE_LOCKED) - sleep(1); + /* While SQLITE_BUSY has a built in timeout, + * SQLITE_LOCKED does not, so sleep */ + if (result == SQLITE_LOCKED) + sleep(1); } switch (result) @@ -173,10 +176,10 @@ sql_get_int64_field(sqlite3 *db, const char *fmt, ...) ret = sqlite3_column_int64(stmt, 0); break; default: - DPRINTF(E_WARN, L_DB_SQL, "%s: step failed: %s\n%s\n", __func__, sqlite3_errmsg(db), sql); + DPRINTF(E_WARN, L_DB_SQL, "%s: step failed: %d - %s\n%s\n", __func__, result, sqlite3_errmsg(db), sql); ret = -1; break; - } + } sqlite3_free(sql); sqlite3_finalize(stmt); @@ -250,7 +253,7 @@ sql_get_text_field(sqlite3 *db, const char *fmt, ...) break; default: - DPRINTF(E_WARN, L_DB_SQL, "SQL step failed: %s\n", sqlite3_errmsg(db)); + DPRINTF(E_WARN, L_DB_SQL, "%s: step failed: %d - %s\n%s\n", __func__, result, sqlite3_errmsg(db), sql); str = NULL; break; } @@ -259,10 +262,36 @@ sql_get_text_field(sqlite3 *db, const char *fmt, ...) return str; } +int +open_db(sqlite3 **sq3) +{ + char path[PATH_MAX]; + int new_db = 0; + + snprintf(path, sizeof(path), "%s/files.db", db_path); + if (access(path, F_OK) != 0) + { + new_db = 1; + make_dir(db_path, S_ISVTX|S_IRWXU|S_IRWXG|S_IRWXO); + } + if (sqlite3_open(path, &db) != SQLITE_OK) + DPRINTF(E_FATAL, L_GENERAL, "ERROR: Failed to open sqlite database! Exiting...\n"); + if (sq3) + *sq3 = db; + sqlite3_busy_timeout(db, 5000); + sql_exec(db, "pragma page_size = 4096"); + sql_exec(db, "pragma journal_mode = OFF"); + sql_exec(db, "pragma synchronous = OFF;"); + sql_exec(db, "pragma default_cache_size = 8192;"); + + return new_db; +} + int db_upgrade(sqlite3 *db) { int db_vers; + int ret; db_vers = sql_get_int_field(db, "PRAGMA user_version"); @@ -274,7 +303,125 @@ db_upgrade(sqlite3 *db) return -1; if (db_vers < 9) return db_vers; + if (db_vers < 10) + { + DPRINTF(E_WARN, L_DB_SQL, "Updating DB version to v%d\n", 10); + ret = sql_exec(db, "ALTER TABLE BOOKMARKS ADD WATCH_COUNT INTEGER"); + if (ret != SQLITE_OK) + return 9; + } + if (db_vers < 11) + { + DPRINTF(E_WARN, L_DB_SQL, "Updating DB version to v%d\n", 11); + ret = sql_exec(db, "ALTER TABLE PLAYLISTS ADD TIMESTAMP INTEGER DEFAULT 1"); + if (ret != SQLITE_OK) + return 10; + } sql_exec(db, "PRAGMA user_version = %d", DB_VERSION); return 0; } + +int db_clear(sqlite3* db) { + if (db == NULL) { + DPRINTF(E_WARN, L_DB_SQL, "db is NULL\n"); + return 0; + } + + sqlite3_stmt* stmt = NULL; + int result = sqlite3_prepare_v2( + db, + "SELECT name FROM sqlite_master WHERE type='table';", + -1, + &stmt, + NULL + ); + if(SQLITE_OK != result) { + DPRINTF(E_ERROR, L_DB_SQL, "prepare failed: %s\n", sqlite3_errmsg(db)); + return 0; + } + + // Get the list of tables + char* tables[32] = {}; + int table_idx = 0; + for(;;) { + for( + int counter = 0; + (SQLITE_BUSY == (result = sqlite3_step(stmt)) || SQLITE_LOCKED == result) && counter < 2; + ++counter + ) { + /* While SQLITE_BUSY has a built in timeout, + * SQLITE_LOCKED does not, so sleep */ + if(SQLITE_LOCKED == result) + sleep(1); + } + switch (result) { + case SQLITE_ROW: { + char* table = NULL; + if(sqlite3_column_type(stmt, 0) != SQLITE_NULL) { + int len = sqlite3_column_bytes(stmt, 0); + if((table = sqlite3_malloc(len + 1)) == NULL) { + DPRINTF(E_ERROR, L_DB_SQL, "malloc failed\n"); + result = SQLITE_NULL; + break; + } + strncpy(table, (const char*)sqlite3_column_text(stmt, 0), len + 1); + } + + tables[table_idx++] = table; + if(table_idx >= sizeof(tables)) { + break; + } + }; continue; + + case SQLITE_DONE: { + /* no rows returned */ + }; break; + + default: { + DPRINTF(E_WARN, L_DB_SQL, "%s: step failed: %d - %s\n%s\n", __func__, result, sqlite3_errmsg(db), "(querying database tables)"); + }; break; + } + break; + } + sqlite3_finalize(stmt); + + // construct SQL statement for dropping all tables in one go + char buf[4096] = {}; + struct string_s sql = { + .data = buf, + .size = sizeof(buf), + .off = 0, + }; + strcatf(&sql, "BEGIN EXCLUSIVE TRANSACTION;"); + while(table_idx--) { + char* table = tables[table_idx]; + // The sqlite_sequence table is not ours, leave it alone + if(table && strcmp(table,"sqlite_sequence")) { + strcatf(&sql, "DROP TABLE IF EXISTS %s;", table); + } + sqlite3_free(table); + } + if(19 != strcatf(&sql, "COMMIT TRANSACTION;")) { + DPRINTF(E_ERROR, L_DB_SQL, "prepare failed, statement string too long!\n"); + return 0; + } + + // Drop all tables if there were no errors thus far. + if(SQLITE_DONE == result) { + DPRINTF(E_DEBUG, L_DB_SQL, "Dropping all database tables:\n%s\n", sql.data); + result = sqlite3_exec(db, sql.data, NULL, NULL, NULL); + switch (result) { + case SQLITE_OK: { + /* dropped ok */ + DPRINTF(E_INFO, L_DB_SQL, "Succesfully dropped all database tables\n"); + }; break; + + default: { + DPRINTF(E_WARN, L_DB_SQL, "Failed to drop tables: %d (%s)\n", result, sqlite3_errmsg(db)); + }; break; + } + } + + return SQLITE_DONE == result; +} diff --git a/sql.h b/sql.h index a62bc51..72871e2 100644 --- a/sql.h +++ b/sql.h @@ -39,6 +39,8 @@ int sql_get_table(sqlite3 *db, const char *zSql, char ***pazResult, int *pnRow, int sql_get_int_field(sqlite3 *db, const char *fmt, ...); int64_t sql_get_int64_field(sqlite3 *db, const char *fmt, ...); char * sql_get_text_field(sqlite3 *db, const char *fmt, ...); +int open_db(sqlite3 **sq3); int db_upgrade(sqlite3 *db); +int db_clear(sqlite3* db); #endif diff --git a/tagutils/tagutils-dff.c b/tagutils/tagutils-dff.c new file mode 100644 index 0000000..05a3b42 --- /dev/null +++ b/tagutils/tagutils-dff.c @@ -0,0 +1,399 @@ +//========================================================================= +// FILENAME : tagutils-dff.c +// DESCRIPTION : DFF metadata reader +//========================================================================= +// Copyright (c) 2014 Takeshich NAKAMURA +//========================================================================= + +/* + * 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, see . + */ +#define GET_DFF_INT64(p) ((((uint64_t)((p)[0])) << 56) | \ + (((uint64_t)((p)[1])) << 48) | \ + (((uint64_t)((p)[2])) << 40) | \ + (((uint64_t)((p)[3])) << 32) | \ + (((uint64_t)((p)[4])) << 24) | \ + (((uint64_t)((p)[5])) << 16) | \ + (((uint64_t)((p)[6])) << 8) | \ + (((uint64_t)((p)[7])))) + +#define GET_DFF_INT32(p) ((((uint32_t)((p)[0])) << 24) | \ + (((uint32_t)((p)[1])) << 16) | \ + (((uint32_t)((p)[2])) << 8) | \ + (((uint32_t)((p)[3])))) + +#define GET_DFF_INT16(p) ((((uint16_t)((p)[0])) << 8) | \ + (((uint16_t)((p)[1])))) + +static int +_get_dfffileinfo(char *file, struct song_metadata *psong) +{ + FILE *fp; + uint32_t len; + uint32_t rt; + unsigned char hdr[32] = { 0 }; + + uint64_t totalsize = 0; + uint64_t propckDataSize = 0; + uint64_t count = 0; + uint32_t samplerate = 0; + uint16_t channels = 0; + //DST + uint64_t dstickDataSize = 0; + uint32_t numFrames = 0; + uint16_t frameRate = 0; + unsigned char frteckData[18] = { 0 }; + unsigned char dstickData[12] = { 0 }; + uint64_t totalcount = 0; + unsigned char ckbuf[12] = { 0 }; + unsigned char compressionType[4] = { 0 }; + unsigned char dsdsdckData[12] = { 0 }; + uint64_t dsdsdckDataSize = 0; + uint64_t cmprckDataSize = 0; + uint64_t abssckDataSize = 0; + uint64_t lscockDataSize = 0; + uint64_t comtckDataSize = 0; + uint64_t diinckDataSize = 0; + uint64_t diarckDataSize = 0; + uint64_t ditickDataSize = 0; + uint64_t manfckDataSize = 0; + + //DPRINTF(E_DEBUG,L_SCANNER,"Getting DFF fileinfo =%s\n",file); + + if ((fp = fopen(file, "rb")) == NULL) + { + DPRINTF(E_WARN, L_SCANNER, "Could not create file handle\n"); + return -1; + } + + len = 32; + //Form DSD chunk + if (!(rt = fread(hdr, len, 1, fp))) + { + DPRINTF(E_WARN, L_SCANNER, "Could not read Form DSD chunk from %s\n", file); + fclose(fp); + return -1; + } + + if (strncmp((char*)hdr, "FRM8", 4)) + { + DPRINTF(E_WARN, L_SCANNER, "Invalid Form DSD chunk in %s\n", file); + fclose(fp); + return -1; + } + + totalsize = GET_DFF_INT64(hdr + 4); + + if (strncmp((char*)hdr + 12, "DSD ", 4)) + { + DPRINTF(E_WARN, L_SCANNER, "Invalid Form DSD chunk in %s\n", file); + fclose(fp); + return -1; + } + + //FVER chunk + if (strncmp((char*)hdr + 16, "FVER", 4)) + { + DPRINTF(E_WARN, L_SCANNER, "Invalid Format Version Chunk in %s\n", file); + fclose(fp); + return -1; + } + + totalsize -= 16; + while (totalcount < totalsize - 4) + { + if (!(rt = fread(ckbuf, sizeof(ckbuf), 1, fp))) + { + //DPRINTF(E_WARN, L_SCANNER, "Could not read chunk header from %s\n", file); + //fclose(fp); + //return -1; + break; + } + + //Property chunk + if (strncmp((char*)ckbuf, "PROP", 4) == 0) + { + propckDataSize = GET_DFF_INT64(ckbuf + 4); + totalcount += propckDataSize + 12; + + unsigned char propckData[propckDataSize]; + + if (!(rt = fread(propckData, propckDataSize, 1, fp))) + { + DPRINTF(E_WARN, L_SCANNER, "Could not read Property chunk from %s\n", file); + fclose(fp); + return -1; + } + + if (strncmp((char*)propckData, "SND ", 4)) + { + DPRINTF(E_WARN, L_SCANNER, "Invalid Property chunk in %s\n", file); + fclose(fp); + return -1; + } + + count += 4; + while (count < propckDataSize) + { + if (strncmp((char*)propckData + count, "FS ", 4) == 0) + { + //Sample Rate Chunk + count += 12; + samplerate = GET_DFF_INT32(propckData + count); + psong->samplerate = samplerate; + count += 4; + + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "Sample Rate is %d\n", psong->samplerate); + } + else if (strncmp((char*)propckData + count, "CHNL", 4) == 0) + { + //Channels Chunk + count += 12; + channels = GET_DFF_INT16(propckData + count); + psong->channels = channels; + count += channels * 4 + 2; + + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "channels is %d\n", channels); + } + else if (strncmp((char*)propckData + count, "CMPR", 4) == 0) + { + //Compression Type Chunk + count += 4; + cmprckDataSize = GET_DFF_INT64(propckData + count); + count += 8; + strncpy((char*)compressionType, (char*)propckData + count, 4); + count += cmprckDataSize; + } + else if (strncmp((char*)propckData + count, "ABSS", 4) == 0) + { + //Absolute Start Time Chunk + count += 4; + abssckDataSize = GET_DFF_INT64(propckData + count); + count += abssckDataSize + 8; + } + else if (strncmp((char*)propckData + count, "LSCO", 4) == 0) + { + //Loudsperaker Configuration Chunk + count += 4; + lscockDataSize = GET_DFF_INT64(propckData + count); + count += lscockDataSize + 8; + } + else + { + break; + } + } + + //bitrate bitpersample is 1bit + psong->bitrate = channels * samplerate * 1; + + //DSD/DST Sound Data Chunk + len = 12; + if (!(rt = fread(dsdsdckData, len, 1, fp))) + { + DPRINTF(E_WARN, L_SCANNER, "Could not read DSD/DST Sound Data chunk from %s\n", file); + fclose(fp); + return -1; + } + + if (strncmp((char*)compressionType, (char*)dsdsdckData, 4)) + { + DPRINTF(E_WARN, L_SCANNER, "Invalid DSD/DST Sound Data chunk in %s\n", file); + fclose(fp); + return -1; + } + + if (strncmp((char*)dsdsdckData, "DSD ", 4) == 0) + { + //DSD + dsdsdckDataSize = GET_DFF_INT64(dsdsdckData + 4); + totalcount += dsdsdckDataSize + 12; + psong->song_length = (int)((double)dsdsdckDataSize / (double)samplerate / (double)channels * 8 * 1000); + + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "songlength is %d\n", psong->song_length); + + fseeko(fp, dsdsdckDataSize, SEEK_CUR); + } + else if (strncmp((char*)dsdsdckData, "DST ", 4) == 0) + { + //DST + dsdsdckDataSize = GET_DFF_INT64(dsdsdckData + 4); + totalcount += dsdsdckDataSize + 12; + + //DST Frame Information chunk + if (!(rt = fread(frteckData, 18, 1, fp))) + { + DPRINTF(E_WARN, L_SCANNER, "Could not read DST Frame Information chunk from %s\n", file); + fclose(fp); + return -1; + } + + if (strncmp((char*)frteckData, "FRTE", 4) == 0) + { + //uint64_t frteckDataSize = GET_DFF_INT64(frteckData+4); + numFrames = GET_DFF_INT32((char*)frteckData + 12); + frameRate = GET_DFF_INT16((char*)frteckData + 16); + + psong->song_length = numFrames / frameRate * 1000; + + fseeko(fp, dsdsdckDataSize - 18, SEEK_CUR); + } + else + { + DPRINTF(E_WARN, L_SCANNER, "Invalid DST Frame Information chunk in %s\n", file); + fclose(fp); + return -1; + } + + //DST Sound Index Chunk + if (!(rt = fread(dstickData, 12, 1, fp))) + { + if (ferror(fp)) + { + DPRINTF(E_WARN, L_SCANNER, "Could not read DST Sound Index chunk from %s\n", file); + fclose(fp); + return -1; + } + else + { + //EOF + break; + } + } + + if (strncmp((char*)dstickData, "DSTI", 4) == 0) + { + dstickDataSize = GET_DFF_INT64(dstickData + 4); + totalcount += dstickDataSize + 12; + fseeko(fp, dstickDataSize, SEEK_CUR); + } + else + { + fseeko(fp, -12, SEEK_CUR); + } + } + else + { + DPRINTF(E_WARN, L_SCANNER, "Invalid DSD/DST Sound Data chunk in %s\n", file); + fclose(fp); + return -1; + } + } + else if (!strncmp((char*)ckbuf, "COMT", 4)) + { + //COMT Chunk + comtckDataSize = GET_DFF_INT64(ckbuf + 4); + totalcount += comtckDataSize + 12; + fseeko(fp, comtckDataSize, SEEK_CUR); + } + else if (!strncmp((char*)ckbuf, "DIIN", 4)) + { + //Edited Master Information chunk + diinckDataSize = GET_DFF_INT64(ckbuf + 4); + unsigned char diinckData[diinckDataSize]; + totalcount += diinckDataSize + 12; + + if (!(rt = fread(diinckData, diinckDataSize, 1, fp))) + { + DPRINTF(E_WARN, L_SCANNER, "Could not read Edited Master Information chunk from %s\n", file); + fclose(fp); + return -1; + } + + uint64_t icount = 0; + while (icount < diinckDataSize) + { + if (!strncmp((char*)diinckData + icount, "EMID", 4)) + { + //Edited Master ID chunk + icount += 4; + icount += GET_DFF_INT64(diinckData + icount) + 8; + } + else if (!strncmp((char*)diinckData + icount, "MARK", 4)) + { + //Master Chunk + icount += 4; + icount += GET_DFF_INT64(diinckData + icount) + 8; + } + else if (!strncmp((char*)diinckData + icount, "DIAR", 4)) + { + //Artist Chunk + icount += 4; + diarckDataSize = GET_DFF_INT64(diinckData + icount); + unsigned char arttext[diarckDataSize + 1 - 4]; + + icount += 12; + + memset(arttext, 0x00, sizeof(arttext)); + strncpy((char*)arttext, (char*)diinckData + icount, sizeof(arttext) - 1); + psong->contributor[ROLE_ARTIST] = strdup((char*)&arttext[0]); + + icount += diarckDataSize - 4; + } + else if (!strncmp((char*)diinckData + icount, "DITI", 4)) + { + //Title Chunk + icount += 4; + ditickDataSize = GET_DFF_INT64(diinckData + icount); + unsigned char titletext[ditickDataSize + 1 - 4]; + + icount += 12; + + memset(titletext, 0x00, sizeof(titletext)); + strncpy((char*)titletext, (char*)diinckData + icount, sizeof(titletext) - 1); + psong->title = strdup((char*)&titletext[0]); + icount += ditickDataSize - 4; + } + else + { + break; + } + } + } + else if (!strncmp((char*)ckbuf, "MANF", 4)) + { + //Manufacturer Specific Chunk + manfckDataSize = GET_DFF_INT64(ckbuf + 4); + totalcount += manfckDataSize + 12; + fseeko(fp, manfckDataSize, SEEK_CUR); + } + } + + fclose(fp); + + //DPRINTF(E_DEBUG, L_SCANNER, "totalsize is 0x%016lx\n", (long unsigned int)totalsize); + //DPRINTF(E_DEBUG, L_SCANNER, "propckDataSize is 0x%016lx\n", (long unsigned int)propckDataSize); + //DPRINTF(E_DEBUG, L_SCANNER, "cmprckDataSize is 0x%016lx\n", (long unsigned int)cmprckDataSize); + //DPRINTF(E_DEBUG, L_SCANNER, "abssckDataSize is 0x%016lx\n", (long unsigned int)abssckDataSize); + //DPRINTF(E_DEBUG, L_SCANNER, "lscockDataSize is 0x%016lx\n", (long unsigned int)lscockDataSize); + //DPRINTF(E_DEBUG, L_SCANNER, "dsdsdckDataSize is 0x%016lx\n", (long unsigned int)dsdsdckDataSize); + //DPRINTF(E_DEBUG, L_SCANNER, "dstickDataSize is 0x%016lx\n", (long unsigned int)dstickDataSize); + //DPRINTF(E_DEBUG, L_SCANNER, "comtckDataSize is 0x%016lx\n", (long unsigned int)comtckDataSize); + //DPRINTF(E_DEBUG, L_SCANNER, "diinckDataSize is 0x%016lx\n", (long unsigned int)diinckDataSize); + //DPRINTF(E_DEBUG, L_SCANNER, "diarckDataSize is 0x%016lx\n", (long unsigned int)diarckDataSize); + //DPRINTF(E_DEBUG, L_SCANNER, "ditickDataSize is 0x%016lx\n", (long unsigned int)ditickDataSize); + //DPRINTF(E_DEBUG, L_SCANNER, "manfckDataSize is 0x%016lx\n", (long unsigned int)manfckDataSize); + + + //DPRINTF(E_DEBUG, L_SCANNER, "Got dff fileinfo successfully=%s\n", file); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "TITLE is %s\n",psong->title ); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "ARTIST is %s\n",psong->contributor[ROLE_ARTIST]); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "samplerate is %d\n", psong->samplerate); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "song_length is %d\n", psong->song_length); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "channels are %d\n", psong->channels); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "bitrate is %d\n", psong->bitrate); + + xasprintf(&(psong->dlna_pn), "DFF"); + return 0; +} diff --git a/tagutils/tagutils-dff.h b/tagutils/tagutils-dff.h new file mode 100644 index 0000000..7f3e3a4 --- /dev/null +++ b/tagutils/tagutils-dff.h @@ -0,0 +1,22 @@ +//========================================================================= +// FILENAME : tagutils-dff.h +// DESCRIPTION : DFF metadata reader +//========================================================================= +// Copyright (c) 2014 Takeshich NAKAMURA +//========================================================================= + +/* + * 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, see . + */ +static int _get_dfffileinfo(char *file, struct song_metadata *psong); diff --git a/tagutils/tagutils-dsf.c b/tagutils/tagutils-dsf.c new file mode 100644 index 0000000..fe76ce6 --- /dev/null +++ b/tagutils/tagutils-dsf.c @@ -0,0 +1,426 @@ +//========================================================================= +// FILENAME : tagutils-dsf.c +// DESCRIPTION : DSF metadata reader +//========================================================================= +// Copyright (c) 2014 Takeshich NAKAMURA +// based on tagutils-mp3.c +//========================================================================= + +/* + * 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, see . + */ + +#define GET_DSF_INT64(p) ((((uint64_t)((p)[7])) << 56) | \ + (((uint64_t)((p)[6])) << 48) | \ + (((uint64_t)((p)[5])) << 40) | \ + (((uint64_t)((p)[4])) << 32) | \ + (((uint64_t)((p)[3])) << 24) | \ + (((uint64_t)((p)[2])) << 16) | \ + (((uint64_t)((p)[1])) << 8) | \ + (((uint64_t)((p)[0])))) + +#define GET_DSF_INT32(p) ((((uint8_t)((p)[3])) << 24) | \ + (((uint8_t)((p)[2])) << 16) | \ + (((uint8_t)((p)[1])) << 8) | \ + (((uint8_t)((p)[0])))) + +static int +_get_dsftags(char *file, struct song_metadata *psong) +{ + 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; + + FILE *fp; + struct id3header *pid3; + uint32_t len; + unsigned char hdr[28] = { 0 }; + uint64_t total_size = 0; + uint64_t pointer_to_metadata_chunk = 0; + uint64_t metadata_chunk_size = 0; + unsigned char *id3tagbuf = NULL; + + //DEBUG DPRINTF(E_DEBUG,L_SCANNER,"Getting DSF file info\n"); + + if ((fp = fopen(file, "rb")) == NULL) + { + DPRINTF(E_WARN, L_SCANNER, "Could not create file handle\n"); + return -1; + } + + len = 28; + if (!(len = fread(hdr, len, 1, fp))) + { + DPRINTF(E_WARN, L_SCANNER, "Could not read DSD Chunk from %s\n", file); + fclose(fp); + return -1; + } + + if (strncmp((char*)hdr, "DSD ", 4)) + { + DPRINTF(E_WARN, L_SCANNER, "Invalid DSD Chunk header in %s\n", file); + fclose(fp); + return -1; + } + + total_size = GET_DSF_INT64(hdr + 12); + pointer_to_metadata_chunk = GET_DSF_INT64(hdr + 20); + + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "%llu\n", total_size); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "%llu\n", pointer_to_metadata_chunk); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "%llu\n", metadata_chunk_size); + + //check invalid metadata + if (total_size == 0) + { + fclose(fp); + DPRINTF(E_INFO, L_SCANNER, "Invalid TotalDataSize in %s\n", file); + return 0; + } + + if (pointer_to_metadata_chunk == 0) + { + fclose(fp); + DPRINTF(E_INFO, L_SCANNER, "Metadata doesn't exist %s\n", file); + return 0; + } + + if (total_size > pointer_to_metadata_chunk) + { + metadata_chunk_size = total_size - pointer_to_metadata_chunk; + } + else + { + fclose(fp); + DPRINTF(E_INFO, L_SCANNER, "Invalid PointerToMetadata in %s\n", file); + return 0; + } + + fseeko(fp, pointer_to_metadata_chunk, SEEK_SET); + + id3tagbuf = (unsigned char*)malloc(sizeof(unsigned char) * metadata_chunk_size); + if (id3tagbuf == NULL) + { + fclose(fp); + DPRINTF(E_WARN, L_SCANNER, "Out of memory.Big MetadataSize in %s\n", file); + return -1; + } + memset(id3tagbuf, 0, sizeof(unsigned char) * metadata_chunk_size); + + if (!(len = fread(id3tagbuf, metadata_chunk_size, 1, fp))) + { + fclose(fp); + free(id3tagbuf); + DPRINTF(E_WARN, L_SCANNER, "Could not read Metadata Chunk from %s\n", file); + return -1; + } + + pid3tag = id3_tag_parse(id3tagbuf, metadata_chunk_size); + + if (!pid3tag) + { + free(id3tagbuf); + err = errno; + errno = err; + DPRINTF(E_WARN, L_SCANNER, "Cannot get ID3 tag for %s\n", file); + return -1; + } + + pid3 = (struct id3header*)id3tagbuf; + + if (strncmp((char*)pid3->id, "ID3", 3) == 0) + { + char tagversion[16]; + + /* found an ID3 header... */ + snprintf(tagversion, sizeof(tagversion), "ID3v2.%d.%d", + pid3->version[0], pid3->version[1]); + psong->tagversion = strdup(tagversion); + } + pid3 = NULL; + + 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 [%s]\n", psong->compilation, basename(file)); + } + 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]), "image/jpg") == 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 + free(utf8_text); + + native_text = id3_field_getfullstring(&pid3frame->fields[3]); + if (native_text) + { + utf8_text = (unsigned char*)id3_ucs4_utf8duplicate(native_text); + if (utf8_text) + { + free(psong->comment); + psong->comment = (char*)utf8_text; + } + } + } + else + { + free(utf8_text); + } + } + } + + index++; + } + + id3_tag_delete(pid3tag); + free(id3tagbuf); + fclose(fp); + //DPRINTF(E_DEBUG, L_SCANNER, "Got id3tag successfully for file=%s\n", file); + return 0; +} + +static int +_get_dsffileinfo(char *file, struct song_metadata *psong) +{ + FILE *fp; + int len = 80; + unsigned char hdr[len]; + uint32_t channelnum; + uint32_t samplingfrequency; + uint32_t bitpersample; + uint64_t samplecount; + + if ((fp = fopen(file, "rb")) == NULL) + { + DPRINTF(E_WARN, L_SCANNER, "Could not create file handle\n"); + return -1; + } + + if (!(len = fread(hdr, len, 1, fp))) + { + DPRINTF(E_WARN, L_SCANNER, "Could not read chunks from %s\n", file); + fclose(fp); + return -1; + } + + if (strncmp((char*)hdr, "DSD ", 4)) + { + DPRINTF(E_WARN, L_SCANNER, "Invalid DSD Chunk headerin %s\n", file); + fclose(fp); + return -1; + } + + if (strncmp((char*)hdr + 28, "fmt ", 4)) + { + DPRINTF(E_WARN, L_SCANNER, "Invalid fmt Chunk header in %s\n", file); + fclose(fp); + return -1; + } + + channelnum = GET_DSF_INT32(hdr + 52); + samplingfrequency = GET_DSF_INT32(hdr + 56); + bitpersample = GET_DSF_INT32(hdr + 60); + samplecount = GET_DSF_INT64(hdr + 64); + + psong->bitrate = channelnum * samplingfrequency * bitpersample; + psong->samplesize = bitpersample; + psong->samplerate = samplingfrequency; + psong->song_length = (samplecount / samplingfrequency) * 1000; + psong->channels = channelnum; + + DPRINTF(E_MAXDEBUG, L_SCANNER, "Got file info successfully for %s\n", file); + //DEBUG DPRINTF(E_MAXDEBUG, L_SCANNER, "bitrate is %d\n", psong->bitrate); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "samplesize is %d\n", psong->samplesize); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "samplerate is %d\n", psong->samplerate); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "song_length is %d\n", psong->song_length); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "channels are %d\n", psong->channels); + //DEBUG DPRINTF(E_DEBUG, L_SCANNER, "samplecount are %lld\n", samplecount); + fclose(fp); + + xasprintf(&(psong->dlna_pn), "DSF"); + return 0; +} diff --git a/tagutils/tagutils-dsf.h b/tagutils/tagutils-dsf.h new file mode 100644 index 0000000..9b7634e --- /dev/null +++ b/tagutils/tagutils-dsf.h @@ -0,0 +1,23 @@ +//========================================================================= +// FILENAME : tagutils-dsf.h +// DESCRIPTION : DSF metadata reader +//========================================================================= +// Copyright (c) 2014 Takeshich NAKAMURA +//========================================================================= + +/* + * 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, see . + */ +static int _get_dsffileinfo(char *file, struct song_metadata *psong); +static int _get_dsftags(char *file, struct song_metadata *psong); diff --git a/tagutils/tagutils-ogg.c b/tagutils/tagutils-ogg.c index 3e1417f..24243ad 100644 --- a/tagutils/tagutils-ogg.c +++ b/tagutils/tagutils-ogg.c @@ -435,7 +435,7 @@ _get_oggfileinfo(char *filename, struct song_metadata *psong) return -1; } - DPRINTF(E_MAXDEBUG, L_SCANNER, "Processing file \"%s\"...\n\n", filename); + DPRINTF(E_MAXDEBUG, L_SCANNER, "Processing file \"%s\"...\n", filename); ogg_sync_init(&sync); diff --git a/tagutils/tagutils.c b/tagutils/tagutils.c index 794e7b9..b38a8a5 100644 --- a/tagutils/tagutils.c +++ b/tagutils/tagutils.c @@ -21,6 +21,7 @@ /* This file is derived from mt-daapd project */ +#include "config.h" #include #include #include @@ -32,11 +33,12 @@ #include #include #include +#ifdef HAVE_VORBISFILE #include #include +#endif #include -#include "config.h" #ifdef HAVE_ICONV #include #endif @@ -102,11 +104,15 @@ char *winamp_genre[] = { */ #include "tagutils-mp3.h" #include "tagutils-aac.h" +#ifdef HAVE_VORBISFILE #include "tagutils-ogg.h" +#endif #include "tagutils-flc.h" #include "tagutils-asf.h" #include "tagutils-wav.h" #include "tagutils-pcm.h" +#include "tagutils-dsf.h" +#include "tagutils-dff.h" static int _get_tags(char *file, struct song_metadata *psong); static int _get_fileinfo(char *file, struct song_metadata *psong); @@ -123,14 +129,18 @@ typedef struct { } 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 }, - { "wav", _get_wavtags, _get_wavfileinfo }, - { "pcm", 0, _get_pcmfileinfo }, - { NULL, 0 } + { "aac", _get_aactags, _get_aacfileinfo }, + { "mp3", _get_mp3tags, _get_mp3fileinfo }, + { "flc", _get_flctags, _get_flcfileinfo }, +#ifdef HAVE_VORBISFILE + { "ogg", NULL, _get_oggfileinfo }, +#endif + { "asf", NULL, _get_asffileinfo }, + { "wav", _get_wavtags, _get_wavfileinfo }, + { "pcm", NULL, _get_pcmfileinfo }, + { "dsf", _get_dsftags, _get_dsffileinfo }, + { "dff", NULL, _get_dfffileinfo }, + { NULL, NULL, NULL } }; @@ -139,12 +149,16 @@ static taghandler taghandlers[] = { #include "tagutils-misc.c" #include "tagutils-mp3.c" #include "tagutils-aac.c" +#ifdef HAVE_VORBISFILE #include "tagutils-ogg.c" +#endif #include "tagutils-flc.c" #include "tagutils-asf.c" #include "tagutils-wav.c" #include "tagutils-pcm.c" #include "tagutils-plist.c" +#include "tagutils-dsf.c" +#include "tagutils-dff.c" //********************************************************************************* // freetags() diff --git a/tivo_beacon.c b/tivo_beacon.c index f2f80aa..bc8c976 100644 --- a/tivo_beacon.c +++ b/tivo_beacon.c @@ -47,6 +47,7 @@ #include #include +#include "event.h" #include "tivo_beacon.h" #include "upnpglobalvars.h" #include "log.h" @@ -288,15 +289,16 @@ rcvBeaconMessage(char *beacon) return 0; } -void ProcessTiVoBeacon(int s) +void ProcessTiVoBeacon(struct event *ev) { - int n; + int s, n; char *cp; struct sockaddr_in sendername; socklen_t len_r; char bufr[1500]; len_r = sizeof(struct sockaddr_in); + s = ev->fd; /* We only expect to see beacon msgs from TiVo's and possibly other tivo servers */ n = recvfrom(s, bufr, sizeof(bufr), 0, (struct sockaddr *)&sendername, &len_r); diff --git a/tivo_beacon.h b/tivo_beacon.h index 738042c..166fc53 100644 --- a/tivo_beacon.h +++ b/tivo_beacon.h @@ -46,5 +46,5 @@ void sendBeaconMessage(int fd, struct sockaddr_in * client, int len, int broadcast); void -ProcessTiVoBeacon(int fd); +ProcessTiVoBeacon(struct event *); #endif diff --git a/tivo_commands.c b/tivo_commands.c index 61e0a6e..d4ab157 100644 --- a/tivo_commands.c +++ b/tivo_commands.c @@ -24,6 +24,7 @@ #include #include +#include "event.h" #include "tivo_utils.h" #include "upnpglobalvars.h" #include "upnphttp.h" diff --git a/upnpdescgen.c b/upnpdescgen.c index 22c8217..3311cef 100644 --- a/upnpdescgen.c +++ b/upnpdescgen.c @@ -31,6 +31,7 @@ #include #include "config.h" +#include "event.h" #include "getifaddr.h" #include "upnpdescgen.h" #include "minidlnapath.h" @@ -192,24 +193,6 @@ static const struct argument GetProtocolInfoArgs[] = {NULL, 0, 0} }; -static const struct argument PrepareForConnectionArgs[] = -{ - {"RemoteProtocolInfo", 1, 6}, - {"PeerConnectionManager", 1, 4}, - {"PeerConnectionID", 1, 7}, - {"Direction", 1, 5}, - {"ConnectionID", 2, 7}, - {"AVTransportID", 2, 8}, - {"RcsID", 2, 9}, - {NULL, 0, 0} -}; - -static const struct argument ConnectionCompleteArgs[] = -{ - {"ConnectionID", 1, 7}, - {NULL, 0, 0} -}; - static const struct argument GetCurrentConnectionIDsArgs[] = { {"ConnectionIDs", 2, 2}, @@ -232,8 +215,6 @@ static const struct argument GetCurrentConnectionInfoArgs[] = static const struct action ConnectionManagerActions[] = { {"GetProtocolInfo", GetProtocolInfoArgs}, /* R */ - //OPTIONAL {"PrepareForConnection", PrepareForConnectionArgs}, /* R */ - //OPTIONAL {"ConnectionComplete", ConnectionCompleteArgs}, /* R */ {"GetCurrentConnectionIDs", GetCurrentConnectionIDsArgs}, /* R */ {"GetCurrentConnectionInfo", GetCurrentConnectionInfoArgs}, /* R */ {0, 0} @@ -256,19 +237,27 @@ static const struct stateVar ConnectionManagerVars[] = static const struct argument GetSearchCapabilitiesArgs[] = { - {"SearchCaps", 2, 10}, + {"SearchCaps", 2, 11}, {0, 0} }; static const struct argument GetSortCapabilitiesArgs[] = { - {"SortCaps", 2, 11}, + {"SortCaps", 2, 12}, {0, 0} }; static const struct argument GetSystemUpdateIDArgs[] = { - {"Id", 2, 12}, + {"Id", 2, 13}, + {0, 0} +}; + +static const struct argument UpdateObjectArgs[] = +{ + {"ObjectID", 1, 1}, + {"CurrentTagValue", 1, 10}, + {"NewTagValue", 1, 10}, {0, 0} }; @@ -309,10 +298,10 @@ static const struct action ContentDirectoryActions[] = {"GetSystemUpdateID", GetSystemUpdateIDArgs}, /* R */ {"Browse", BrowseArgs}, /* R */ {"Search", SearchArgs}, /* O */ + {"UpdateObject", UpdateObjectArgs}, /* O */ #if 0 // Not implementing optional features yet... {"CreateObject", CreateObjectArgs}, /* O */ {"DestroyObject", DestroyObjectArgs}, /* O */ - {"UpdateObject", UpdateObjectArgs}, /* O */ {"ImportResource", ImportResourceArgs}, /* O */ {"ExportResource", ExportResourceArgs}, /* O */ {"StopTransferResource", StopTransferResourceArgs}, /* O */ @@ -336,6 +325,7 @@ static const struct stateVar ContentDirectoryVars[] = {"A_ARG_TYPE_Index", 3, 0}, {"A_ARG_TYPE_Count", 3, 0}, {"A_ARG_TYPE_UpdateID", 3, 0}, + {"A_ARG_TYPE_TagValueList", 0, 0}, {"SearchCapabilities", 0, 0}, {"SortCapabilities", 0, 0}, {"SystemUpdateID", 3|EVENTED, 0, 0, 255}, diff --git a/upnpevents.c b/upnpevents.c index 06ec43a..4de6ce8 100644 --- a/upnpevents.c +++ b/upnpevents.c @@ -59,9 +59,11 @@ #include #include #include +#include #include #include +#include "event.h" #include "upnpevents.h" #include "minidlnapath.h" #include "upnpglobalvars.h" @@ -82,27 +84,26 @@ struct subscriber { }; struct upnp_event_notify { + struct event ev; LIST_ENTRY(upnp_event_notify) entries; - int s; /* socket */ - enum { ECreated=1, - EConnecting, + enum { EConnecting, ESending, EWaitingForResponse, EFinished, EError } state; - struct subscriber * sub; - char * buffer; - int buffersize; + struct subscriber * sub; + char * buffer; + int buffersize; int tosend; - int sent; + int sent; const char * path; char addrstr[16]; char portstr[8]; }; /* prototypes */ -static void -upnp_event_create_notify(struct subscriber * sub); +static void upnp_event_create_notify(struct subscriber * sub); +static void upnp_event_process_notify(struct event *ev); /* Subscriber list */ LIST_HEAD(listhead, subscriber) subscriberlist = { NULL }; @@ -110,6 +111,9 @@ LIST_HEAD(listhead, subscriber) subscriberlist = { NULL }; /* notify list */ LIST_HEAD(listheadnotif, upnp_event_notify) notifylist = { NULL }; +#define MAX_SUBSCRIBERS 500 +static uint16_t nsubscribers = 0; + /* create a new subscriber */ static struct subscriber * newSubscriber(const char * eventurl, const char * callback, int callbacklen) @@ -151,12 +155,15 @@ upnpevents_addSubscriber(const char * eventurl, struct subscriber * tmp; DPRINTF(E_DEBUG, L_HTTP, "addSubscriber(%s, %.*s, %d)\n", eventurl, callbacklen, callback, timeout); + if (nsubscribers >= MAX_SUBSCRIBERS) + return NULL; tmp = newSubscriber(eventurl, callback, callbacklen); if(!tmp) return NULL; if(timeout) tmp->timeout = time(NULL) + timeout; LIST_INSERT_HEAD(&subscriberlist, tmp, entries); + nsubscribers++; upnp_event_create_notify(tmp); return tmp->uuid; } @@ -189,6 +196,7 @@ upnpevents_removeSubscriber(const char * sid, int sidlen) sub->notify->sub = NULL; } LIST_REMOVE(sub, entries); + nsubscribers--; free(sub); return 0; } @@ -217,30 +225,35 @@ upnp_event_var_change_notify(enum subscriber_service_enum service) } } -/* create and add the notify object to the list */ +/* create and add the notify object to the list, start connecting */ static void -upnp_event_create_notify(struct subscriber * sub) +upnp_event_create_notify(struct subscriber *sub) { struct upnp_event_notify * obj; - int flags; + int flags, s, i; + const char *p; + unsigned short port; + struct sockaddr_in addr; + + assert(sub); + obj = calloc(1, sizeof(struct upnp_event_notify)); if(!obj) { DPRINTF(E_ERROR, L_HTTP, "%s: calloc(): %s\n", "upnp_event_create_notify", strerror(errno)); return; } obj->sub = sub; - obj->state = ECreated; - obj->s = socket(PF_INET, SOCK_STREAM, 0); - if(obj->s<0) { + s = socket(PF_INET, SOCK_STREAM, 0); + if(s < 0) { DPRINTF(E_ERROR, L_HTTP, "%s: socket(): %s\n", "upnp_event_create_notify", strerror(errno)); goto error; } - if((flags = fcntl(obj->s, F_GETFL, 0)) < 0) { + if((flags = fcntl(s, F_GETFL, 0)) < 0) { DPRINTF(E_ERROR, L_HTTP, "%s: fcntl(..F_GETFL..): %s\n", "upnp_event_create_notify", strerror(errno)); goto error; } - if(fcntl(obj->s, F_SETFL, flags | O_NONBLOCK) < 0) { + if(fcntl(s, F_SETFL, flags | O_NONBLOCK) < 0) { DPRINTF(E_ERROR, L_HTTP, "%s: fcntl(..F_SETFL..): %s\n", "upnp_event_create_notify", strerror(errno)); goto error; @@ -248,28 +261,9 @@ upnp_event_create_notify(struct subscriber * sub) if(sub) sub->notify = obj; LIST_INSERT_HEAD(¬ifylist, obj, entries); - return; -error: - if(obj->s >= 0) - close(obj->s); - free(obj); -} -static void -upnp_event_notify_connect(struct upnp_event_notify * obj) -{ - int i; - const char * p; - unsigned short port; - struct sockaddr_in addr; - if(!obj) - return; memset(&addr, 0, sizeof(addr)); i = 0; - if(obj->sub == NULL) { - obj->state = EError; - return; - } p = obj->sub->callback; p += 7; /* http:// */ while(*p != '/' && *p != ':' && i < (sizeof(obj->addrstr)-1)) @@ -299,12 +293,23 @@ upnp_event_notify_connect(struct upnp_event_notify * obj) DPRINTF(E_DEBUG, L_HTTP, "%s: '%s' %hu '%s'\n", "upnp_event_notify_connect", obj->addrstr, port, obj->path); obj->state = EConnecting; - if(connect(obj->s, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + if(connect(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) { if(errno != EINPROGRESS && errno != EWOULDBLOCK) { DPRINTF(E_ERROR, L_HTTP, "%s: connect(): %s\n", "upnp_event_notify_connect", strerror(errno)); obj->state = EError; } + } else { + obj->ev = (struct event ){ .fd = s, .rdwr = EVENT_WRITE, + .process = upnp_event_process_notify, .data = obj }; + event_module.add(&obj->ev); } + + return; + +error: + if(s >= 0) + close(s); + free(obj); } static void upnp_event_prepare(struct upnp_event_notify * obj) @@ -324,10 +329,9 @@ static void upnp_event_prepare(struct upnp_event_notify * obj) "%.*s\r\n"; char * xml; int l; - if(obj->sub == NULL) { - obj->state = EError; - return; - } + + assert(obj->sub); + switch(obj->sub->service) { case EContentDirectory: xml = getVarsContentDirectory(&l); @@ -357,30 +361,37 @@ static void upnp_event_send(struct upnp_event_notify * obj) int i; //DEBUG DPRINTF(E_DEBUG, L_HTTP, "Sending UPnP Event:\n%s", obj->buffer+obj->sent); while( obj->sent < obj->tosend ) { - i = send(obj->s, obj->buffer + obj->sent, obj->tosend - obj->sent, 0); + i = send(obj->ev.fd, obj->buffer + obj->sent, obj->tosend - obj->sent, 0); if(i<0) { DPRINTF(E_WARN, L_HTTP, "%s: send(): %s\n", "upnp_event_send", strerror(errno)); obj->state = EError; + event_module.del(&obj->ev, 0); return; } obj->sent += i; } - if(obj->sent == obj->tosend) + if(obj->sent == obj->tosend) { obj->state = EWaitingForResponse; + event_module.del(&obj->ev, 0); + obj->ev.rdwr = EVENT_READ; + event_module.add(&obj->ev); + } } static void upnp_event_recv(struct upnp_event_notify * obj) { int n; - n = recv(obj->s, obj->buffer, obj->buffersize, 0); + n = recv(obj->ev.fd, obj->buffer, obj->buffersize, 0); if(n<0) { DPRINTF(E_ERROR, L_HTTP, "%s: recv(): %s\n", "upnp_event_recv", strerror(errno)); obj->state = EError; + event_module.del(&obj->ev, 0); return; } DPRINTF(E_DEBUG, L_HTTP, "%s: (%dbytes) %.*s\n", "upnp_event_recv", n, n, obj->buffer); obj->state = EFinished; + event_module.del(&obj->ev, 0); if(obj->sub) { obj->sub->seq++; @@ -390,8 +401,10 @@ static void upnp_event_recv(struct upnp_event_notify * obj) } static void -upnp_event_process_notify(struct upnp_event_notify * obj) +upnp_event_process_notify(struct event *ev) { + struct upnp_event_notify *obj = ev->data; + switch(obj->state) { case EConnecting: /* now connected or failed to connect */ @@ -405,91 +418,52 @@ upnp_event_process_notify(struct upnp_event_notify * obj) upnp_event_recv(obj); break; case EFinished: - close(obj->s); - obj->s = -1; + close(obj->ev.fd); + obj->ev.fd = -1; break; default: DPRINTF(E_ERROR, L_HTTP, "upnp_event_process_notify: unknown state\n"); } } -void upnpevents_selectfds(fd_set *readset, fd_set *writeset, int * max_fd) -{ - struct upnp_event_notify * obj; - for(obj = notifylist.lh_first; obj != NULL; obj = obj->entries.le_next) { - DPRINTF(E_DEBUG, L_HTTP, "upnpevents_selectfds: %p %d %d\n", - obj, obj->state, obj->s); - if(obj->s >= 0) { - switch(obj->state) { - case ECreated: - upnp_event_notify_connect(obj); - if(obj->state != EConnecting) - break; - case EConnecting: - case ESending: - FD_SET(obj->s, writeset); - if(obj->s > *max_fd) - *max_fd = obj->s; - break; - case EWaitingForResponse: - FD_SET(obj->s, readset); - if(obj->s > *max_fd) - *max_fd = obj->s; - break; - default: - break; - } - } - } -} - -void upnpevents_processfds(fd_set *readset, fd_set *writeset) +void upnpevents_gc(void) { struct upnp_event_notify * obj; struct upnp_event_notify * next; struct subscriber * sub; struct subscriber * subnext; time_t curtime; - for(obj = notifylist.lh_first; obj != NULL; obj = obj->entries.le_next) { - DPRINTF(E_DEBUG, L_HTTP, "%s: %p %d %d %d %d\n", - "upnpevents_processfds", obj, obj->state, obj->s, - FD_ISSET(obj->s, readset), FD_ISSET(obj->s, writeset)); - if(obj->s >= 0) { - if(FD_ISSET(obj->s, readset) || FD_ISSET(obj->s, writeset)) - upnp_event_process_notify(obj); - } - } + obj = notifylist.lh_first; while(obj != NULL) { next = obj->entries.le_next; if(obj->state == EError || obj->state == EFinished) { - if(obj->s >= 0) { - close(obj->s); + if(obj->ev.fd >= 0) { + close(obj->ev.fd); } if(obj->sub) obj->sub->notify = NULL; -#if 0 /* Just let it time out instead of explicitly removing the subscriber */ /* remove also the subscriber from the list if there was an error */ if(obj->state == EError && obj->sub) { LIST_REMOVE(obj->sub, entries); + nsubscribers--; free(obj->sub); } -#endif free(obj->buffer); LIST_REMOVE(obj, entries); free(obj); } obj = next; } - /* remove timeouted subscribers */ + /* remove timed-out subscribers */ curtime = time(NULL); for(sub = subscriberlist.lh_first; sub != NULL; ) { subnext = sub->entries.le_next; if(sub->timeout && curtime > sub->timeout && sub->notify == NULL) { LIST_REMOVE(sub, entries); + nsubscribers--; free(sub); } sub = subnext; } } - diff --git a/upnpevents.h b/upnpevents.h index 5dcd0d9..fa6c104 100644 --- a/upnpevents.h +++ b/upnpevents.h @@ -63,12 +63,10 @@ upnpevents_addSubscriber(const char * eventurl, int upnpevents_removeSubscriber(const char * sid, int sidlen); void upnpevents_removeSubscribers(void); +void upnpevents_gc(void); int renewSubscription(const char * sid, int sidlen, int timeout); -void upnpevents_selectfds(fd_set *readset, fd_set *writeset, int * max_fd); -void upnpevents_processfds(fd_set *readset, fd_set *writeset); - #ifdef USE_MINIUPNPDCTL void write_events_details(int s); #endif diff --git a/upnpglobalvars.c b/upnpglobalvars.c index 6660ac8..f8f658d 100644 --- a/upnpglobalvars.c +++ b/upnpglobalvars.c @@ -46,10 +46,11 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ +#include +#include #include #include #include -#include #include "config.h" #include "upnpglobalvars.h" @@ -58,7 +59,7 @@ time_t startup_time = 0; struct runtime_vars_s runtime_vars; -uint32_t runtime_flags = INOTIFY_MASK; +uint32_t runtime_flags = INOTIFY_MASK | TIVO_BONJOUR_MASK | SUBTITLES_MASK; const char *pidfilename = "/var/run/minidlna/minidlna.pid"; @@ -83,9 +84,37 @@ sqlite3 *db; char friendly_name[FRIENDLYNAME_MAX_LEN]; char db_path[PATH_MAX] = {'\0'}; char log_path[PATH_MAX] = {'\0'}; +char icon_path[PATH_MAX] = {'\0'}; struct media_dir_s * media_dirs = NULL; struct album_art_name_s * album_art_names = NULL; -short int scanning = 0; +pid_t scanner_pid = 0; volatile short int quitting = 0; volatile uint32_t updateID = 0; const char *force_sort_criteria = NULL; + + +/* override the auto-detection of the 'LOCATION' key in UPNP responses + * (i.e. in the response to M-SEARCH, as well as when generating media links) */ +char location_url_override[MAX_LAN_ADDR][LOCATION_URL_MAX_LEN] = {}; +const char* get_location_url_by_lan_addr(char* buf, size_t addr) { + if(addr < n_lan_addr && location_url_override[addr][0]) { + return location_url_override[addr]; + } + else { + snprintf(buf, LOCATION_URL_MAX_LEN, "http://%s:%d", lan_addr[addr].str, runtime_vars.port); + return buf; + } +} + +const char* get_location_url_by_ifindex(char* buf, size_t ifindex) { + for(size_t i = 0; i < n_lan_addr; ++i) { + if(lan_addr[i].ifindex == ifindex) { + return get_location_url_by_lan_addr(buf, i); + } + } + return NULL; +} + +void set_location_url_by_lan_addr(size_t addr, char* url) { + strncpy(location_url_override[addr], url, LOCATION_URL_MAX_LEN); +} diff --git a/upnpglobalvars.h b/upnpglobalvars.h index 6021a2a..b1151ec 100644 --- a/upnpglobalvars.h +++ b/upnpglobalvars.h @@ -3,7 +3,7 @@ * http://sourceforge.net/projects/minidlna/ * * MiniDLNA media server - * Copyright (C) 2008-2009 Justin Maggard + * Copyright (C) 2008-2017 Justin Maggard * * This file is part of MiniDLNA. * @@ -49,6 +49,7 @@ #ifndef __UPNPGLOBALVARS_H__ #define __UPNPGLOBALVARS_H__ +#include #include #include "minidlnatypes.h" @@ -57,7 +58,7 @@ #include -#define MINIDLNA_VERSION "1.1.6" +#define MINIDLNA_VERSION "1.2.1" #ifdef NETGEAR # define SERVER_NAME "ReadyDLNA" @@ -66,7 +67,13 @@ #endif #define USE_FORK 1 -#define DB_VERSION 9 +#define DB_VERSION 11 + +#ifdef READYNAS +# define LOGFILE_NAME "upnp-av.log" +#else +# define LOGFILE_NAME "minidlna.log" +#endif #ifdef ENABLE_NLS #define _(string) gettext(string) @@ -165,6 +172,7 @@ "http-get:*:audio/mp4:*," \ "http-get:*:audio/x-wav:*," \ "http-get:*:audio/x-flac:*," \ + "http-get:*:audio/x-dsd:*," \ "http-get:*:application/ogg:*" #define DLNA_FLAG_DLNA_V1_5 0x00100000 @@ -188,6 +196,18 @@ extern uint32_t runtime_flags; #define SYSTEMD_MASK 0x0010 #define MERGE_MEDIA_DIRS_MASK 0x0020 #define WIDE_LINKS_MASK 0x0040 +#ifdef HAVE_AVAHI +#define TIVO_BONJOUR_MASK 0x0080 +#else +#define TIVO_BONJOUR_MASK 0x0000 +#endif +#ifdef ENABLE_VIDEO_THUMB +#define THUMB_MASK 0x0100 +#endif +#define SCANNING_MASK 0x0100 +#define RESCAN_MASK 0x0200 +#define SUBTITLES_MASK 0x0400 +#define FORCE_ALPHASORT_MASK 0x0800 #define SETFLAG(mask) runtime_flags |= mask #define GETFLAG(mask) (runtime_flags & mask) @@ -209,6 +229,11 @@ extern char serialnumber[]; #define PRESENTATIONURL_MAX_LEN 64 extern char presentationurl[]; +#define LOCATION_URL_MAX_LEN 128 +const char* get_location_url_by_lan_addr(char* buf, size_t addr); +const char* get_location_url_by_ifindex(char* buf, size_t ifindex); +void set_location_url_by_lan_addr(size_t addr, char* url); + /* lan addresses */ extern int n_lan_addr; extern struct lan_addr_s lan_addr[]; @@ -220,11 +245,12 @@ extern const char *minissdpdsocketpath; extern sqlite3 *db; #define FRIENDLYNAME_MAX_LEN 64 extern char friendly_name[]; -extern char db_path[]; -extern char log_path[]; +extern char db_path[PATH_MAX]; +extern char log_path[PATH_MAX]; +extern char icon_path[PATH_MAX]; extern struct media_dir_s *media_dirs; extern struct album_art_name_s *album_art_names; -extern short int scanning; +extern pid_t scanner_pid; extern volatile short int quitting; extern volatile uint32_t updateID; extern const char *force_sort_criteria; diff --git a/upnphttp.c b/upnphttp.c index ab463e9..771daef 100644 --- a/upnphttp.c +++ b/upnphttp.c @@ -64,12 +64,14 @@ #include #include "config.h" +#include "event.h" #include "upnpglobalvars.h" #include "upnphttp.h" #include "upnpdescgen.h" #include "minidlnapath.h" #include "upnpsoap.h" #include "upnpevents.h" +#include "albumart.h" #include "utils.h" #include "getifaddr.h" #include "image_utils.h" @@ -81,6 +83,7 @@ #include "clients.h" #include "process.h" #include "sendfile.h" +#include "scanner.h" #define MAX_BUFFER_SIZE 2147483647 #define MIN_BUFFER_SIZE 65536 @@ -97,10 +100,12 @@ enum event_type { static void SendResp_icon(struct upnphttp *, char * url); static void SendResp_albumArt(struct upnphttp *, char * url); +static void SendResp_mta(struct upnphttp *, char * url); static void SendResp_caption(struct upnphttp *, char * url); static void SendResp_resizedimg(struct upnphttp *, char * url); static void SendResp_thumbnail(struct upnphttp *, char * url); static void SendResp_dlnafile(struct upnphttp *, char * url); +static void Process_upnphttp(struct event *ev); struct upnphttp * New_upnphttp(int s) @@ -112,18 +117,21 @@ New_upnphttp(int s) if(ret == NULL) return NULL; memset(ret, 0, sizeof(struct upnphttp)); - ret->socket = s; + ret->ev = (struct event ){ .fd = s, .rdwr = EVENT_READ, .process = Process_upnphttp, .data = ret }; + event_module.add(&ret->ev); return ret; } void CloseSocket_upnphttp(struct upnphttp * h) { - if(close(h->socket) < 0) + + event_module.del(&h->ev, EV_FLAG_CLOSING); + if(close(h->ev.fd) < 0) { - DPRINTF(E_ERROR, L_HTTP, "CloseSocket_upnphttp: close(%d): %s\n", h->socket, strerror(errno)); + DPRINTF(E_ERROR, L_HTTP, "CloseSocket_upnphttp: close(%d): %s\n", h->ev.fd, strerror(errno)); } - h->socket = -1; + h->ev.fd = -1; h->state = 100; } @@ -132,7 +140,7 @@ Delete_upnphttp(struct upnphttp * h) { if(h) { - if(h->socket >= 0) + if(h->ev.fd >= 0) CloseSocket_upnphttp(h); free(h->req_buf); free(h->res_buf); @@ -293,7 +301,7 @@ ParseHttpHeaders(struct upnphttp * h) } DPRINTF(E_DEBUG, L_HTTP, "Range Start-End: %lld - %lld\n", - (long long)h->req_RangeStart, h->req_RangeEnd); + (long long)h->req_RangeStart, (long long)h->req_RangeEnd); } } else if(strncasecmp(line, "Host", 4)==0) @@ -303,14 +311,14 @@ ParseHttpHeaders(struct upnphttp * h) p = colon + 1; while(isspace(*p)) p++; - for(n = 0; niface = n; break; @@ -502,6 +510,7 @@ static void Send400(struct upnphttp * h) { static const char body400[] = + "" "400 Bad Request" "

Bad Request

The request is invalid" " for this HTTP version.\r\n"; @@ -517,6 +526,7 @@ static void Send403(struct upnphttp * h) { static const char body403[] = + "" "403 Forbidden" "

Forbidden

You don't have permission to access this resource." "\r\n"; @@ -532,6 +542,7 @@ static void Send404(struct upnphttp * h) { static const char body404[] = + "" "404 Not Found" "

Not Found

The requested URL was not found" " on this server.\r\n"; @@ -547,6 +558,7 @@ static void Send406(struct upnphttp * h) { static const char body406[] = + "" "406 Not Acceptable" "

Not Acceptable

An unsupported operation" " was requested.\r\n"; @@ -562,6 +574,7 @@ static void Send416(struct upnphttp * h) { static const char body416[] = + "" "416 Requested Range Not Satisfiable" "

Requested Range Not Satisfiable

The requested range" " was outside the file's size.\r\n"; @@ -577,6 +590,7 @@ void Send500(struct upnphttp * h) { static const char body500[] = + "" "500 Internal Server Error" "

Internal Server Error

Server encountered " "and Internal Error.\r\n"; @@ -592,6 +606,7 @@ void Send501(struct upnphttp * h) { static const char body501[] = + "" "501 Not Implemented" "

Not Implemented

The HTTP Method " "is not implemented by this server.\r\n"; @@ -653,31 +668,141 @@ SendResp_presentation(struct upnphttp * h) v = sql_get_int_field(db, "SELECT count(*) from DETAILS where MIME glob 'v*'"); p = sql_get_int_field(db, "SELECT count(*) from DETAILS where MIME glob 'i*'"); strcatf(&str, - "" SERVER_NAME " " MINIDLNA_VERSION "" - "
" + "" + "" + "" + ); + if(GETFLAG(SCANNING_MASK)) { // during the scan, refresh the page every 5sec + strcatf(&str, + "" + ); + } + strcatf(&str, + "" SERVER_NAME " " MINIDLNA_VERSION "" + // Technically, the browser should ask for a favicon on its own, but + // it seems that at least firefox sometimes has ideas of its own. It + // will aggressively cache favicons (and/or not ask for one). One + // trick that seems to somehow help is to append a question mark + // to the of the URL. For some unknown reason this seems to trigger + // firefox to always request the favicon, without even the need for + // a real 'cache buster'. + // C.f. http://stackoverflow.com/questions/8616016/favicon-not-displayed-by-firefox + // Note however that even though the icon is requested, firefox is + // *still* caching it. The only way to refresh it seems to be to + // just keep trying to refresh, stopping e.g. minidlna (so you get + // "Unable to connect", close the tab, reopen tab, etc, etc, until + // eventually the icon will update. Perhaps there is a timeout on + // refresh? + "" + "" + "" + "" + "
" + "
" "

" SERVER_NAME " status

"); strcatf(&str, "

Media library

" - "" - "" - "" - "" + "
Audio files%d
Video files%d
Image files%d
" + "" + "" "
Audio filesVideo filesImage files
%d%d%d
", a, v, p); - if (scanning) - strcatf(&str, - "
* Media scan in progress
"); + // Full rescan button + strcatf(&str, "
" + "
" + "
"); + + // Fast rescan button + strcatf(&str, "
" + "


"); strcatf(&str, "

Connected clients

" - "" - ""); + "
IDTypeIP AddressHW AddressConnections
" + ""); for (i = 0; i < CLIENT_CACHE_SLOTS; i++) { if (!clients[i].addr.s_addr) continue; - strcatf(&str, "", + strcatf(&str, "", i, clients[i].type->name, inet_ntoa(clients[i].addr), clients[i].mac[0], clients[i].mac[1], clients[i].mac[2], clients[i].mac[3], clients[i].mac[4], clients[i].mac[5], clients[i].connections); @@ -685,13 +810,49 @@ SendResp_presentation(struct upnphttp * h) strcatf(&str, "
IDTypeIP AddressHW AddressConnections
%d%s%s%02X:%02X:%02X:%02X:%02X:%02X%d
%d%s%s%02X:%02X:%02X:%02X:%02X:%02X%d
"); strcatf(&str, "
%d connection%s currently open
", number_of_children, (number_of_children == 1 ? "" : "s")); - strcatf(&str, "\r\n"); + strcatf(&str, "
\r\n"); BuildResp_upnphttp(h, str.data, str.off); SendResp_upnphttp(h); CloseSocket_upnphttp(h); } +static void +DoMediaScan(struct upnphttp * h, int rebuild_db) +{ + if(!GETFLAG(SCANNING_MASK)) { + if(rebuild_db) { + db_clear(db); + char cmd[PATH_MAX*2]; + snprintf(cmd, sizeof(cmd), "rm -rf %s/art_cache", db_path); + if (system(cmd) != 0) + DPRINTF(E_WARN, L_HTTP, "Failed to clean art cache!\n"); + if(CreateDatabase() != 0) { + DPRINTF(E_FATAL, L_HTTP, "ERROR: Failed to create sqlite database!\n"); + } + } + CLEARFLAG(RESCAN_MASK); + if(!rebuild_db) + SETFLAG(RESCAN_MASK); + start_scanner(); + } + // Here we're redirecting the user back to the 'presentation' page through a + // http-equiv refresh, rather than just directly showing the 'presentation' + // page. The reason for this is that this action is a POST request, and is + // processed from ProcessHTTPPOST_upnphttp(). From there it's (currently) + // hard to get back into the parsing of a GET request. + const char body[] = "" + "" + "" + "" + "" + ""; + h->respflags = FLAG_HTML; + BuildResp_upnphttp(h, body, sizeof(body) - 1); + SendResp_upnphttp(h); + CloseSocket_upnphttp(h); +} + /* ProcessHTTPPOST_upnphttp() * executes the SOAP query if it is possible */ static void @@ -707,10 +868,23 @@ ProcessHTTPPOST_upnphttp(struct upnphttp * h) h->req_soapAction, h->req_soapActionLen); } - else - { + else { + const char* query = strchr(h->req_buf, '?'); + const char* newline = strchr(h->req_buf, '\n'); + if(queryBad request"; + "" + "Bad request"; DPRINTF(E_WARN, L_HTTP, "No SOAPAction in HTTP headers\n"); h->respflags = FLAG_HTML; BuildResp2_upnphttp(h, 400, "Bad Request", @@ -736,7 +910,8 @@ check_event(struct upnphttp *h) if (h->req_SID || !h->req_NT) { BuildResp2_upnphttp(h, 400, "Bad Request", - "Bad request", 37); + "" + "Bad request", 37); type = E_INVALID; } else if (strncmp(h->req_Callback, "http://", 7) != 0 || @@ -757,7 +932,8 @@ check_event(struct upnphttp *h) if (h->req_NT) { BuildResp2_upnphttp(h, 400, "Bad Request", - "Bad request", 37); + "" + "Bad request", 37); type = E_INVALID; } else @@ -1004,6 +1180,10 @@ ProcessHttpQuery_upnphttp(struct upnphttp * h) { SendResp_albumArt(h, HttpUrl+10); } + else if(strncmp(HttpUrl, "/MTA/", 5) == 0) + { + SendResp_mta(h, HttpUrl+5); + } #ifdef TIVO_SUPPORT else if(strncmp(HttpUrl, "/TiVoConnect", 12) == 0) { @@ -1031,6 +1211,10 @@ ProcessHttpQuery_upnphttp(struct upnphttp * h) { SendResp_resizedimg(h, HttpUrl+9); } + else if(begins_with(HttpUrl, "/favicon.ico")) + { + SendResp_icon(h, "favicon.ico"); + } else if(strncmp(HttpUrl, "/icons/", 7) == 0) { SendResp_icon(h, HttpUrl+7); @@ -1074,18 +1258,17 @@ ProcessHttpQuery_upnphttp(struct upnphttp * h) } } - -void -Process_upnphttp(struct upnphttp * h) +static void +Process_upnphttp(struct event *ev) { char buf[2048]; + struct upnphttp *h = ev->data; int n; - if(!h) - return; + switch(h->state) { case 0: - n = recv(h->socket, buf, 2048, 0); + n = recv(h->ev.fd, buf, 2048, 0); if(n<0) { DPRINTF(E_ERROR, L_HTTP, "recv (state0): %s\n", strerror(errno)); @@ -1131,7 +1314,7 @@ Process_upnphttp(struct upnphttp * h) break; case 1: case 2: - n = recv(h->socket, buf, sizeof(buf), 0); + n = recv(h->ev.fd, buf, sizeof(buf), 0); if(n < 0) { DPRINTF(E_ERROR, L_HTTP, "recv (state%d): %s\n", h->state, strerror(errno)); @@ -1185,7 +1368,7 @@ BuildHeader_upnphttp(struct upnphttp * h, int respcode, { static const char httpresphead[] = "%s %d %s\r\n" - "Content-Type: %s\r\n" + "Content-Type: text/%s; charset=\"utf-8\"\r\n" "Connection: close\r\n" "Content-Length: %d\r\n" "Server: " MINIDLNA_SERVER_STRING "\r\n"; @@ -1204,7 +1387,7 @@ BuildHeader_upnphttp(struct upnphttp * h, int respcode, res.off = 0; strcatf(&res, httpresphead, "HTTP/1.1", respcode, respmsg, - (h->respflags&FLAG_HTML)?"text/html":"text/xml; charset=\"utf-8\"", + (h->respflags&FLAG_HTML)?"html":"xml", bodylen); /* Additional headers */ if(h->respflags & FLAG_TIMEOUT) { @@ -1258,7 +1441,7 @@ SendResp_upnphttp(struct upnphttp * h) { int n; DPRINTF(E_DEBUG, L_HTTP, "HTTP RESPONSE: %.*s\n", h->res_buflen, h->res_buf); - n = send(h->socket, h->res_buf, h->res_buflen, 0); + n = send(h->ev.fd, h->res_buf, h->res_buflen, 0); if(n<0) { DPRINTF(E_ERROR, L_HTTP, "send(res_buf): %s\n", strerror(errno)); @@ -1276,7 +1459,7 @@ send_data(struct upnphttp * h, char * header, size_t size, int flags) { int n; - n = send(h->socket, header, size, flags); + n = send(h->ev.fd, header, size, flags); if(n<0) { DPRINTF(E_ERROR, L_HTTP, "send(res_buf): %s\n", strerror(errno)); @@ -1310,7 +1493,7 @@ send_file(struct upnphttp * h, int sendfd, off_t offset, off_t end_offset) if( try_sendfile ) { send_size = ( ((end_offset - offset) < MAX_BUFFER_SIZE) ? (end_offset - offset + 1) : MAX_BUFFER_SIZE); - ret = sys_sendfile(h->socket, sendfd, &offset, send_size); + ret = sys_sendfile(h->ev.fd, sendfd, &offset, send_size); if( ret == -1 ) { DPRINTF(E_DEBUG, L_HTTP, "sendfile error :: error no. %d [%s]\n", errno, strerror(errno)); @@ -1340,7 +1523,7 @@ send_file(struct upnphttp * h, int sendfd, off_t offset, off_t end_offset) else break; } - ret = write(h->socket, buf, ret); + ret = write(h->ev.fd, buf, ret); if( ret == -1 ) { DPRINTF(E_DEBUG, L_HTTP, "write error :: error no. %d [%s]\n", errno, strerror(errno)); if( errno == EAGAIN ) @@ -1416,61 +1599,220 @@ static void SendResp_icon(struct upnphttp * h, char * icon) { char header[512]; - char mime[12] = "image/"; - char *data; - int size; + char mime[16] = "image/"; + char *data = NULL; + long size; + int fd; struct string_s str; - if( strcmp(icon, "sm.png") == 0 ) + char *path; + char buf[PATH_MAX]; + snprintf(buf, sizeof(buf), "%s/%s", icon_path, icon); + path = buf; + fd = open(path, O_RDONLY); + if( fd < 0 ) { + if( strcmp(icon, "sm.png") == 0 ) + { + DPRINTF(E_DEBUG, L_HTTP, "Sending small PNG icon\n"); + data = (char *)png_sm; + size = sizeof(png_sm)-1; + } + else if( strcmp(icon, "lrg.png") == 0 ) + { + DPRINTF(E_DEBUG, L_HTTP, "Sending large PNG icon\n"); + data = (char *)png_lrg; + size = sizeof(png_lrg)-1; + } + else if( strcmp(icon, "sm.jpg") == 0 ) + { + DPRINTF(E_DEBUG, L_HTTP, "Sending small JPEG icon\n"); + data = (char *)jpeg_sm; + size = sizeof(jpeg_sm)-1; + } + else if( strcmp(icon, "lrg.jpg") == 0 ) + { + DPRINTF(E_DEBUG, L_HTTP, "Sending large JPEG icon\n"); + data = (char *)jpeg_lrg; + size = sizeof(jpeg_lrg)-1; + } + else if( strcmp(icon, "favicon.ico") == 0 ) + { + DPRINTF(E_DEBUG, L_HTTP, "Sending favicon\n"); + data = (char *)favicon; + size = sizeof(favicon)-1; + } + else + { + DPRINTF(E_WARN, L_HTTP, "Invalid icon request: %s\n", icon); + Send404(h); + return; + } + } + else { - DPRINTF(E_DEBUG, L_HTTP, "Sending small PNG icon\n"); - data = (char *)png_sm; - size = sizeof(png_sm)-1; - strcpy(mime+6, "png"); + DPRINTF(E_DEBUG, L_HTTP, "Sending custom icon: '%s/%s'\n", icon_path, icon); + size = lseek(fd, 0, SEEK_END); + lseek(fd, 0, SEEK_SET); + } + if( ends_with(icon, ".jpg") ) + { + strcpy(mime+6, "jpeg"); } - else if( strcmp(icon, "lrg.png") == 0 ) + else if( ends_with(icon, ".png") ) { - DPRINTF(E_DEBUG, L_HTTP, "Sending large PNG icon\n"); - data = (char *)png_lrg; - size = sizeof(png_lrg)-1; strcpy(mime+6, "png"); } - else if( strcmp(icon, "sm.jpg") == 0 ) + else if( ends_with(icon, ".ico") ) { - DPRINTF(E_DEBUG, L_HTTP, "Sending small JPEG icon\n"); - data = (char *)jpeg_sm; - size = sizeof(jpeg_sm)-1; - strcpy(mime+6, "jpeg"); + strcpy(mime+6, "x-icon"); } - else if( strcmp(icon, "lrg.jpg") == 0 ) + + INIT_STR(str, header); + + start_dlna_header(&str, 200, "Interactive", mime); + strcatf(&str, "Content-Length: %ld\r\n\r\n", size); + + if( send_data(h, str.data, str.off, MSG_MORE) == 0 ) { - DPRINTF(E_DEBUG, L_HTTP, "Sending large JPEG icon\n"); - data = (char *)jpeg_lrg; - size = sizeof(jpeg_lrg)-1; - strcpy(mime+6, "jpeg"); + if( h->req_command != EHead ) + { + if(fd<0) + { + send_data(h, data, size, 0); + } + else + { + send_file(h, fd, 0, size-1); + } + } } - else + CloseSocket_upnphttp(h); + if(fd>=0) { + close(fd); + } +} + +static void +SendResp_albumArt(struct upnphttp * h, char * url) +{ + char header[512]; + char *path, *albumart_path; + char *tmode; + off_t size; + struct string_s str; + + if( h->reqflags & (FLAG_XFERSTREAMING|FLAG_RANGE) ) + { + DPRINTF(E_WARN, L_HTTP, "Client tried to specify transferMode as Streaming with an image!\n"); + Send406(h); + return; + } + + long long id = strtoll(url, NULL, 10); + const char *suffix = strrchr(url, '-'); + + path = sql_get_text_field(db, "SELECT PATH from DETAILS where ID = %lld", id); + if( !path || !suffix) { - DPRINTF(E_WARN, L_HTTP, "Invalid icon request: %s\n", icon); + DPRINTF(E_WARN, L_HTTP, "ALBUM_ART ID %s not found, responding ERROR 404\n", url); Send404(h); return; } +#if USE_FORK + pid_t newpid = -1; +#endif /* USE_FORK */ + + long long size_type = strtoll(suffix + 1, NULL, 10); + const image_size_type_t *image_size_type = get_image_size_type((image_size_type_enum)size_type); + if(image_size_type->type == JPEG_INV) + { + DPRINTF(E_ERROR, L_HTTP, "Invalid image size '%s' requested, responding ERROR 404\n", url); + Send404(h); + return; + } + art_cache_path(image_size_type, ".jpg", path, &albumart_path); + + int fd = _open_file(albumart_path); + if (fd < 0) { + if (fd == -403) { + Send403(h); + goto albumart_error; + } + DPRINTF(E_DEBUG, L_HTTP, "Album art doesn't exist in cache, adding new entry %s\n", albumart_path); +#if USE_FORK + // XXX: Don't we need to wait a bit here?? + newpid = process_fork(h->req_client); + if (newpid > 0) + { + CloseSocket_upnphttp(h); + goto albumart_error; + } +#endif + char *fullsize_albumart_path = NULL; + if(art_cache_path(NULL, ".jpg", path, &fullsize_albumart_path)) + { + int ret = save_resized_album_art_from_file_to_file(fullsize_albumart_path, albumart_path, image_size_type); + free(fullsize_albumart_path); + if (ret != 0) + { + DPRINTF(E_WARN, L_HTTP, "ALBUM_ART ID %s-%s not found, responding ERROR 404\n", url, image_size_type->name); + Send404(h); + goto albumart_error; + } + } + else + { + DPRINTF(E_WARN, L_HTTP, "ALBUM_ART ID %s-%s: Could not format path to full-size album art for '%s', responding ERROR 404\n", url, image_size_type->name, path); + Send404(h); + goto albumart_error; + } + + fd = open(albumart_path, O_RDONLY); + } + + if( fd < 0 ) { + DPRINTF(E_ERROR, L_HTTP, "Error opening %s\n", albumart_path); + Send404(h); + goto albumart_error; + } + + DPRINTF(E_INFO, L_HTTP, "Serving album art ID: %lld [%s]\n", id, albumart_path); + + size = lseek(fd, 0, SEEK_END); + lseek(fd, 0, SEEK_SET); + INIT_STR(str, header); - start_dlna_header(&str, 200, "Interactive", mime); - strcatf(&str, "Content-Length: %d\r\n\r\n", size); +#if USE_FORK + if (newpid == 0 && (h->reqflags & FLAG_XFERBACKGROUND) && (setpriority(PRIO_PROCESS, 0, 19) == 0)) + tmode = "Background"; + else +#endif + tmode = "Interactive"; + start_dlna_header(&str, 200, tmode, "image/jpeg"); + strcatf(&str, "Content-Length: %jd\r\n" + "contentFeatures.dlna.org: DLNA.ORG_PN=%s\r\n\r\n", + (intmax_t)size, image_size_type->name); if( send_data(h, str.data, str.off, MSG_MORE) == 0 ) { if( h->req_command != EHead ) - send_data(h, data, size, 0); + send_file(h, fd, 0, size-1); } - CloseSocket_upnphttp(h); + close(fd); + +albumart_error: + sqlite3_free(path); + free(albumart_path); +#if USE_FORK + if (newpid == 0) + _exit(0); +#endif } static void -SendResp_albumArt(struct upnphttp * h, char * object) +SendResp_mta(struct upnphttp * h, char * object) { char header[512]; char *path; @@ -1478,6 +1820,7 @@ SendResp_albumArt(struct upnphttp * h, char * object) long long id; int fd; struct string_s str; + const char *tmode; if( h->reqflags & (FLAG_XFERSTREAMING|FLAG_RANGE) ) { @@ -1488,42 +1831,61 @@ SendResp_albumArt(struct upnphttp * h, char * object) id = strtoll(object, NULL, 10); - path = sql_get_text_field(db, "SELECT PATH from ALBUM_ART where ID = '%lld'", id); + path = sql_get_text_field(db, "SELECT PATH from MTA where ID = '%lld'", id); if( !path ) { - DPRINTF(E_WARN, L_HTTP, "ALBUM_ART ID %s not found, responding ERROR 404\n", object); + DPRINTF(E_WARN, L_HTTP, "MTA ID %s not found, responding ERROR 404\n", object); Send404(h); return; } - DPRINTF(E_INFO, L_HTTP, "Serving album art ID: %lld [%s]\n", id, path); +#if USE_FORK + pid_t newpid = 0; + newpid = process_fork(h->req_client); + if( newpid > 0 ) + { + goto mta_error; + } +#endif + DPRINTF(E_INFO, L_HTTP, "Serving MTA file ID: %lld [%s]\n", id, path); - fd = _open_file(path); + fd = open(path, O_RDONLY); if( fd < 0 ) { + DPRINTF(E_ERROR, L_HTTP, "Error opening %s\n", path); sqlite3_free(path); - if (fd == -403) - Send403(h); - else - Send404(h); - return; + Send404(h); + goto mta_error; } + sqlite3_free(path); size = lseek(fd, 0, SEEK_END); lseek(fd, 0, SEEK_SET); INIT_STR(str, header); - start_dlna_header(&str, 200, "Interactive", "image/jpeg"); +#if USE_FORK + if( (h->reqflags & FLAG_XFERBACKGROUND) && (setpriority(PRIO_PROCESS, 0, 19) == 0) ) + tmode = "Background"; + else +#endif + tmode = "Interactive"; + + start_dlna_header(&str, 200, tmode, "image/jpeg"); strcatf(&str, "Content-Length: %jd\r\n" - "contentFeatures.dlna.org: DLNA.ORG_PN=JPEG_TN\r\n\r\n", - (intmax_t)size); + "contentFeatures.dlna.org: DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=%08X%024X\r\n\r\n", + (intmax_t)size, DLNA_FLAG_DLNA_V1_5|DLNA_FLAG_TM_B|DLNA_FLAG_TM_S, 0); if( send_data(h, str.data, str.off, MSG_MORE) == 0 ) - { if( h->req_command != EHead ) send_file(h, fd, 0, size-1); - } + close(fd); +mta_error: CloseSocket_upnphttp(h); +#if USE_FORK + if( newpid == 0 ) + _exit(0); +#endif + } static void @@ -2084,9 +2446,10 @@ SendResp_dlnafile(struct upnphttp *h, char *object) if( h->reqflags & FLAG_CAPTION ) { + char buf[LOCATION_URL_MAX_LEN] = {}; + const char* host = get_location_url_by_lan_addr(buf, h->iface); if( sql_get_int_field(db, "SELECT ID from CAPTIONS where ID = '%lld'", (long long)id) > 0 ) - strcatf(&str, "CaptionInfo.sec: http://%s:%d/Captions/%lld.srt\r\n", - lan_addr[h->iface].str, runtime_vars.port, (long long)id); + strcatf(&str, "CaptionInfo.sec: %s/Captions/%lld.srt\r\n", host, (long long)id); } strcatf(&str, "Accept-Ranges: bytes\r\n" diff --git a/upnphttp.h b/upnphttp.h index d708946..e28a943 100644 --- a/upnphttp.h +++ b/upnphttp.h @@ -75,7 +75,7 @@ enum httpCommands { }; struct upnphttp { - int socket; + struct event ev; struct in_addr clientaddr; /* client address */ int iface; int state; @@ -144,10 +144,6 @@ CloseSocket_upnphttp(struct upnphttp *); void Delete_upnphttp(struct upnphttp *); -/* Process_upnphttp() */ -void -Process_upnphttp(struct upnphttp *); - /* BuildHeader_upnphttp() * build the header for the HTTP Response * also allocate the buffer for body data */ diff --git a/upnpreplyparse.c b/upnpreplyparse.c index 2411f05..d3243d9 100644 --- a/upnpreplyparse.c +++ b/upnpreplyparse.c @@ -122,7 +122,7 @@ DisplayNameValueList(char * buffer, int bufsize) { struct NameValueParserData pdata; struct NameValue * nv; - ParseNameValue(buffer, bufsize, &pdata); + ParseNameValue(buffer, bufsize, &pdata, XML_STORE_EMPTY_FL); for(nv = pdata.head.lh_first; nv != NULL; nv = nv->entries.le_next) diff --git a/upnpsoap.c b/upnpsoap.c index 4015a80..79f4b14 100644 --- a/upnpsoap.c +++ b/upnpsoap.c @@ -3,7 +3,7 @@ * http://sourceforge.net/projects/minidlna/ * * MiniDLNA media server - * Copyright (C) 2008-2009 Justin Maggard + * Copyright (C) 2008-2017 Justin Maggard * * This file is part of MiniDLNA. * @@ -61,6 +61,7 @@ #include #include +#include "event.h" #include "upnpglobalvars.h" #include "utils.h" #include "upnphttp.h" @@ -77,6 +78,8 @@ #else # define __SORT_LIMIT #endif +#define NON_ZERO(x) (x && atoi(x)) +#define IS_ZERO(x) (!x || !atoi(x)) /* Standard Errors: * @@ -84,7 +87,7 @@ * -------- ---------------- ----------- * 401 Invalid Action No action by that name at this service. * 402 Invalid Args Could be any of the following: not enough in args, - * too many in args, no in arg by that name, + * too many in args, no in arg by that name, * one or more in args are of the wrong data type. * 403 Out of Sync Out of synchronization. * 501 Action Failed May be returned in current state of service @@ -93,13 +96,14 @@ * Technical Committee. * 700-799 TBD Action-specific errors for standard actions. * Defined by UPnP Forum working committee. - * 800-899 TBD Action-specific errors for non-standard actions. + * 800-899 TBD Action-specific errors for non-standard actions. * Defined by UPnP vendor. */ +#define SoapError(x,y,z) _SoapError(x,y,z,__func__) static void -SoapError(struct upnphttp * h, int errCode, const char * errDesc) +_SoapError(struct upnphttp * h, int errCode, const char * errDesc, const char *func) { - static const char resp[] = + static const char resp[] = "" @@ -120,7 +124,7 @@ SoapError(struct upnphttp * h, int errCode, const char * errDesc) char body[2048]; int bodylen; - DPRINTF(E_WARN, L_HTTP, "Returning UPnPError %d: %s\n", errCode, errDesc); + DPRINTF(E_WARN, L_HTTP, "%s Returning UPnPError %d: %s\n", func, errCode, errDesc); bodylen = snprintf(body, sizeof(body), resp, errCode, errDesc); BuildResp2_upnphttp(h, 500, "Internal Server Error", body, bodylen); SendResp_upnphttp(h); @@ -201,13 +205,13 @@ IsAuthorizedValidated(struct upnphttp * h, const char * action) int bodylen; bodylen = snprintf(body, sizeof(body), resp, action, "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1", - 1, action); + 1, action); BuildSendAndCloseSoapResp(h, body, bodylen); } else SoapError(h, 402, "Invalid Args"); - ClearNameValueList(&data); + ClearNameValueList(&data); } static void @@ -245,7 +249,7 @@ GetProtocolInfo(struct upnphttp * h, const char * action) bodylen = asprintf(&body, resp, action, "urn:schemas-upnp-org:service:ConnectionManager:1", - action); + action); BuildSendAndCloseSoapResp(h, body, bodylen); free(body); } @@ -261,6 +265,7 @@ GetSortCapabilities(struct upnphttp * h, const char * action) "dc:date," "upnp:class," "upnp:album," + "upnp:episodeNumber," "upnp:originalTrackNumber" "" ""; @@ -270,7 +275,7 @@ GetSortCapabilities(struct upnphttp * h, const char * action) bodylen = snprintf(body, sizeof(body), resp, action, "urn:schemas-upnp-org:service:ContentDirectory:1", - action); + action); BuildSendAndCloseSoapResp(h, body, bodylen); } @@ -299,7 +304,7 @@ GetSearchCapabilities(struct upnphttp * h, const char * action) bodylen = snprintf(body, sizeof(body), resp, action, "urn:schemas-upnp-org:service:ContentDirectory:1", - action); + action); BuildSendAndCloseSoapResp(h, body, bodylen); } @@ -318,7 +323,7 @@ GetCurrentConnectionIDs(struct upnphttp * h, const char * action) bodylen = snprintf(body, sizeof(body), resp, action, "urn:schemas-upnp-org:service:ConnectionManager:1", - action); + action); BuildSendAndCloseSoapResp(h, body, bodylen); } @@ -362,56 +367,68 @@ GetCurrentConnectionInfo(struct upnphttp * h, const char * action) int bodylen; bodylen = snprintf(body, sizeof(body), resp, action, "urn:schemas-upnp-org:service:ConnectionManager:1", - action); + action); BuildSendAndCloseSoapResp(h, body, bodylen); } - ClearNameValueList(&data); + ClearNameValueList(&data); } /* Standard DLNA/UPnP filter flags */ -#define FILTER_CHILDCOUNT 0x00000001 -#define FILTER_DC_CREATOR 0x00000002 -#define FILTER_DC_DATE 0x00000004 -#define FILTER_DC_DESCRIPTION 0x00000008 -#define FILTER_DLNA_NAMESPACE 0x00000010 -#define FILTER_REFID 0x00000020 -#define FILTER_RES 0x00000040 -#define FILTER_RES_BITRATE 0x00000080 -#define FILTER_RES_DURATION 0x00000100 -#define FILTER_RES_NRAUDIOCHANNELS 0x00000200 -#define FILTER_RES_RESOLUTION 0x00000400 -#define FILTER_RES_SAMPLEFREQUENCY 0x00000800 -#define FILTER_RES_SIZE 0x00001000 -#define FILTER_SEARCHABLE 0x00002000 -#define FILTER_UPNP_ACTOR 0x00004000 -#define FILTER_UPNP_ALBUM 0x00008000 -#define FILTER_UPNP_ALBUMARTURI 0x00010000 -#define FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID 0x00020000 -#define FILTER_UPNP_ARTIST 0x00040000 -#define FILTER_UPNP_GENRE 0x00080000 -#define FILTER_UPNP_ORIGINALTRACKNUMBER 0x00100000 -#define FILTER_UPNP_SEARCHCLASS 0x00200000 -#define FILTER_UPNP_STORAGEUSED 0x00400000 +#define FILTER_CHILDCOUNT 0x00000001 +#define FILTER_DC_CREATOR 0x00000002 +#define FILTER_DC_DATE 0x00000004 +#define FILTER_DC_DESCRIPTION 0x00000008 +#define FILTER_DLNA_NAMESPACE 0x00000010 +#define FILTER_REFID 0x00000020 +#define FILTER_RES 0x00000040 +#define FILTER_RES_BITRATE 0x00000080 +#define FILTER_RES_DURATION 0x00000100 +#define FILTER_RES_NRAUDIOCHANNELS 0x00000200 +#define FILTER_RES_RESOLUTION 0x00000400 +#define FILTER_RES_SAMPLEFREQUENCY 0x00000800 +#define FILTER_RES_SIZE 0x00001000 +#define FILTER_SEARCHABLE 0x00002000 +#define FILTER_UPNP_ACTOR 0x00004000 +#define FILTER_UPNP_ALBUM 0x00008000 +#define FILTER_UPNP_ALBUMARTURI 0x00010000 +#define FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID 0x00020000 +#define FILTER_UPNP_ARTIST 0x00040000 +#define FILTER_UPNP_EPISODENUMBER 0x00080000 +#define FILTER_UPNP_EPISODESEASON 0x00100000 +#define FILTER_UPNP_GENRE 0x00200000 +#define FILTER_UPNP_ORIGINALTRACKNUMBER 0x00400000 +#define FILTER_UPNP_SEARCHCLASS 0x00800000 +#define FILTER_UPNP_STORAGEUSED 0x01000000 +/* Not normally used, so leave out of the default filter */ +#define FILTER_UPNP_PLAYBACKCOUNT 0x02000000 +#define FILTER_UPNP_LASTPLAYBACKPOSITION 0x04000000 /* Vendor-specific filter flags */ -#define FILTER_SEC_CAPTION_INFO_EX 0x01000000 -#define FILTER_SEC_DCM_INFO 0x02000000 -#define FILTER_PV_SUBTITLE_FILE_TYPE 0x04000000 -#define FILTER_PV_SUBTITLE_FILE_URI 0x08000000 -#define FILTER_PV_SUBTITLE 0x0C000000 -#define FILTER_AV_MEDIA_CLASS 0x10000000 - -static uint32_t +#define FILTER_SEC_CAPTION_INFO_EX 0x08000000 +#define FILTER_SEC_DCM_INFO 0x10000000 +#define FILTER_SEC_META_FILE_INFO 0x20000000 +#define FILTER_SEC 0x38000000 +#define FILTER_PV_SUBTITLE_FILE_TYPE 0x40000000 +#define FILTER_PV_SUBTITLE_FILE_URI 0x80000000 +#define FILTER_PV_SUBTITLE 0xc0000000 +#define FILTER_AV_MEDIA_CLASS 0x100000000 +/* Masks */ +#define STANDARD_FILTER_MASK 0x01FFFFFF +#define FILTER_BOOKMARK_MASK (FILTER_UPNP_PLAYBACKCOUNT | \ + FILTER_UPNP_LASTPLAYBACKPOSITION | \ + FILTER_SEC_DCM_INFO) + +static uint64_t set_filter_flags(char *filter, struct upnphttp *h) { char *item, *saveptr = NULL; - uint32_t flags = 0; + uint64_t flags = 0; int samsung = h->req_client && (h->req_client->type->flags & FLAG_SAMSUNG); if( !filter || (strlen(filter) <= 1) ) { - /* Not the full 32 bits. Skip vendor-specific stuff by default. */ - flags = 0xFFFFFF; + /* Not the full 64 bits. Skip vendor-specific stuff by default. */ + flags = STANDARD_FILTER_MASK; if (samsung) - flags |= FILTER_SEC_CAPTION_INFO_EX | FILTER_SEC_DCM_INFO; + flags |= FILTER_SEC_CAPTION_INFO_EX | FILTER_SEC_DCM_INFO | FILTER_SEC_META_FILE_INFO; } if (flags) return flags; @@ -538,6 +555,14 @@ set_filter_flags(char *filter, struct upnphttp *h) flags |= FILTER_RES; flags |= FILTER_RES_SIZE; } + else if( strcmp(item, "upnp:playbackCount") == 0 ) + { + flags |= FILTER_UPNP_PLAYBACKCOUNT; + } + else if( strcmp(item, "upnp:lastPlaybackPosition") == 0 ) + { + flags |= FILTER_UPNP_LASTPLAYBACKPOSITION; + } else if( strcmp(item, "sec:CaptionInfoEx") == 0 ) { flags |= FILTER_SEC_CAPTION_INFO_EX; @@ -546,6 +571,10 @@ set_filter_flags(char *filter, struct upnphttp *h) { flags |= FILTER_SEC_DCM_INFO; } + else if( strcmp(item, "sec:MetaFileInfo") == 0 ) + { + flags |= FILTER_SEC_META_FILE_INFO; + } else if( strcmp(item, "res@pv:subtitleFileType") == 0 ) { flags |= FILTER_PV_SUBTITLE_FILE_TYPE; @@ -558,6 +587,14 @@ set_filter_flags(char *filter, struct upnphttp *h) { flags |= FILTER_AV_MEDIA_CLASS; } + else if( strcmp(item, "upnp:episodeNumber") == 0 ) + { + flags |= FILTER_UPNP_EPISODENUMBER; + } + else if( strcmp(item, "upnp:episodeSeason") == 0 ) + { + flags |= FILTER_UPNP_EPISODESEASON; + } item = strtok_r(NULL, ",", &saveptr); } @@ -618,9 +655,10 @@ parse_sort_criteria(char *sortCriteria, int *error) { strcatf(&str, "d.DATE"); } - else if( strcasecmp(item, "upnp:originalTrackNumber") == 0 ) + else if( strcasecmp(item, "upnp:originalTrackNumber") == 0 || + strcasecmp(item, "upnp:episodeNumber") == 0 ) { - strcatf(&str, "d.DISC, d.TRACK"); + strcatf(&str, "d.DISC%s, d.TRACK", reverse ? " DESC" : ""); } else if( strcasecmp(item, "upnp:album") == 0 ) { @@ -662,6 +700,35 @@ parse_sort_criteria(char *sortCriteria, int *error) return order; } +static void +_alphasort_alt_title(char **title, char **alt_title, int requested, int returned, const char *disc, const char *track) +{ + char *old_title = *alt_title ?: NULL; + char buf[8]; + int pad; + int ret; + + snprintf(buf, sizeof(buf), "%d", requested); + pad = strlen(buf); + + if (NON_ZERO(track) && !strstr(*title, track)) { + if (NON_ZERO(disc)) + ret = asprintf(alt_title, "%0*d %s.%s %s", + pad, returned, disc, track, *title); + else + ret = asprintf(alt_title, "%0*d %s %s", + pad, returned, track, *title); + } + else + ret = asprintf(alt_title, "%0*d %s", pad, returned, *title); + + if (ret > 0) + *title = *alt_title; + else + *alt_title = NULL; + free(old_title); +} + inline static void add_resized_res(int srcw, int srch, int reqw, int reqh, char *dlna_pn, char *detailID, struct Response *args) @@ -683,12 +750,15 @@ add_resized_res(int srcw, int srch, int reqw, int reqh, char *dlna_pn, } strcatf(args->str, "resolution=\"%dx%d\" ", dstw, dsth); } + + char buf[LOCATION_URL_MAX_LEN] = {}; + const char* host = get_location_url_by_lan_addr(buf, args->iface); strcatf(args->str, "protocolInfo=\"http-get:*:image/jpeg:" "DLNA.ORG_PN=%s;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=%08X%024X\">" - "http://%s:%d/Resized/%s.jpg?width=%d,height=%d" + "%s/Resized/%s.jpg?width=%d,height=%d" "</res>", dlna_pn, DLNA_FLAG_DLNA_V1_5|DLNA_FLAG_HTTP_STALLING|DLNA_FLAG_TM_B|DLNA_FLAG_TM_I, 0, - lan_addr[args->iface].str, runtime_vars.port, + host, detailID, dstw, dsth); } @@ -719,6 +789,10 @@ add_res(char *size, char *duration, char *bitrate, char *sampleFrequency, if( resolution && (args->filter & FILTER_RES_RESOLUTION) ) { strcatf(args->str, "resolution=\"%s\" ", resolution); } + + char buf[LOCATION_URL_MAX_LEN] = {}; + const char* host = get_location_url_by_lan_addr(buf, args->iface); + if( args->filter & FILTER_PV_SUBTITLE ) { if( args->flags & FLAG_HAS_CAPTIONS ) @@ -726,15 +800,13 @@ add_res(char *size, char *duration, char *bitrate, char *sampleFrequency, if( args->filter & FILTER_PV_SUBTITLE_FILE_TYPE ) strcatf(args->str, "pv:subtitleFileType=\"SRT\" "); if( args->filter & FILTER_PV_SUBTITLE_FILE_URI ) - strcatf(args->str, "pv:subtitleFileUri=\"http://%s:%d/Captions/%s.srt\" ", - lan_addr[args->iface].str, runtime_vars.port, detailID); + strcatf(args->str, "pv:subtitleFileUri=\"%s/Captions/%s.srt\" ", host, detailID); } } strcatf(args->str, "protocolInfo=\"http-get:*:%s:%s\">" - "http://%s:%d/MediaItems/%s.%s" + "%s/MediaItems/%s.%s" "</res>", - mime, dlna_pn, lan_addr[args->iface].str, - runtime_vars.port, detailID, ext); + mime, dlna_pn, host, detailID, ext); } static int @@ -764,11 +836,14 @@ object_exists(const char *object) #define COLUMNS "o.DETAIL_ID, o.CLASS," \ " d.SIZE, d.TITLE, d.DURATION, d.BITRATE, d.SAMPLERATE, d.ARTIST," \ " d.ALBUM, d.GENRE, d.COMMENT, d.CHANNELS, d.TRACK, d.DATE, d.RESOLUTION," \ - " d.THUMBNAIL, d.CREATOR, d.DLNA_PN, d.MIME, d.ALBUM_ART, d.ROTATION, d.DISC " + " d.THUMBNAIL, d.CREATOR, d.DLNA_PN, d.MIME, d.ALBUM_ART, d.ROTATION, d.MTA, d.DISC " #define SELECT_COLUMNS "SELECT o.OBJECT_ID, o.PARENT_ID, o.REF_ID, " COLUMNS -#define NON_ZERO(x) (x && atoi(x)) -#define IS_ZERO(x) (!x || !atoi(x)) +static int +append_with_attributes(struct string_s *str, const char *attribute, const char *value, const char *elementName) +{ + return strcatf(str, "<%s %.512s>%.512s</%s>", elementName, attribute, value, elementName); +} static int callback(void *args, int argc, char **argv, char **azColName) @@ -777,12 +852,15 @@ callback(void *args, int argc, char **argv, char **azColName) char *id = argv[0], *parent = argv[1], *refID = argv[2], *detailID = argv[3], *class = argv[4], *size = argv[5], *title = argv[6], *duration = argv[7], *bitrate = argv[8], *sampleFrequency = argv[9], *artist = argv[10], *album = argv[11], *genre = argv[12], *comment = argv[13], *nrAudioChannels = argv[14], *track = argv[15], *date = argv[16], *resolution = argv[17], - *tn = argv[18], *creator = argv[19], *dlna_pn = argv[20], *mime = argv[21], *album_art = argv[22], *rotate = argv[23]; + *tn = argv[18], *creator = argv[19], *dlna_pn = argv[20], *mime = argv[21], *album_art = argv[22], *rotate = argv[23], *mta = argv[24], *disc = argv[25]; char dlna_buf[128]; const char *ext; struct string_s *str = passed_args->str; int ret = 0; + char buf[LOCATION_URL_MAX_LEN] = {}; + const char* host = get_location_url_by_lan_addr(buf, passed_args->iface); + /* Make sure we have at least 8KB left of allocated memory to finish the response. */ if( str->off > (str->size - 8192) ) { @@ -823,6 +901,10 @@ callback(void *args, int argc, char **argv, char **azColName) { passed_args->flags &= ~FLAG_HAS_CAPTIONS; // clear the caption flag for each item dlna_flags |= DLNA_FLAG_TM_S; + if (GETFLAG(SUBTITLES_MASK) && + (passed_args->client >= EStandardDLNA150 || !passed_args->client)) + passed_args->flags |= FLAG_CAPTION_RES; + if( passed_args->flags & FLAG_MIME_AVI_DIVX ) { if( strcmp(mime, "video/x-msvideo") == 0 ) @@ -884,6 +966,16 @@ callback(void *args, int argc, char **argv, char **azColName) if( strlen(title) > 23 ) title[23] = '\0'; } + /* Hyundai hack: Only titles with a media extension get recognized. */ + else if( passed_args->client == EHyundaiTV ) + { + ext = mime_to_ext(mime); + ret = asprintf(&alt_title, "%s.%s", title, ext); + if( ret > 0 ) + title = alt_title; + else + alt_title = NULL; + } } else if( *mime == 'a' ) { @@ -905,6 +997,12 @@ callback(void *args, int argc, char **argv, char **azColName) } else dlna_flags |= DLNA_FLAG_TM_I; + /* Force an alphabetical sort, for clients that like to do their own sorting */ + if( GETFLAG(FORCE_ALPHASORT_MASK) ) + _alphasort_alt_title(&title, &alt_title, passed_args->requested, passed_args->returned, disc, track); + + if( passed_args->flags & FLAG_SKIP_DLNA_PN ) + dlna_pn = NULL; if( dlna_pn ) snprintf(dlna_buf, sizeof(dlna_buf), "DLNA.ORG_PN=%s;" @@ -937,10 +1035,30 @@ callback(void *args, int argc, char **argv, char **azColName) if( date && (passed_args->filter & FILTER_DC_DATE) ) { ret = strcatf(str, "<dc:date>%s</dc:date>", date); } - if( passed_args->filter & FILTER_SEC_DCM_INFO ) { + if( (passed_args->filter & FILTER_BOOKMARK_MASK) ) { /* Get bookmark */ - ret = strcatf(str, "<sec:dcmInfo>CREATIONDATE=0,FOLDER=%s,BM=%d</sec:dcmInfo>", - title, sql_get_int_field(db, "SELECT SEC from BOOKMARKS where ID = '%s'", detailID)); + int sec = sql_get_int_field(db, "SELECT SEC from BOOKMARKS where ID = '%s'", detailID); + if( sec > 0 ) { + /* This format is wrong according to the UPnP/AV spec. It should be in duration format, + ** so HH:MM:SS. But Kodi seems to be the only user of this tag, and it only works with a + ** raw seconds value. + ** If Kodi gets fixed, we can use duration_str(sec * 1000) here */ + if( passed_args->filter & FILTER_UPNP_LASTPLAYBACKPOSITION ) + ret = strcatf(str, "<upnp:lastPlaybackPosition>%d</upnp:lastPlaybackPosition>", + sec); + if( passed_args->filter & FILTER_SEC_DCM_INFO ) + ret = strcatf(str, "<sec:dcmInfo>CREATIONDATE=0,FOLDER=%s,BM=%d</sec:dcmInfo>", + title, sec); + } + if( passed_args->filter & FILTER_UPNP_PLAYBACKCOUNT ) { + ret = strcatf(str, "<upnp:playbackCount>%d</upnp:playbackCount>", + sql_get_int_field(db, "SELECT WATCH_COUNT from BOOKMARKS where ID = '%s'", detailID)); + } + } + free(alt_title); + if( (passed_args->filter & FILTER_SEC_META_FILE_INFO) && runtime_vars.mta > 0 && mta && *mta != '0' ) { + ret = strcatf(str, "<sec:MetaFileInfo sec:type="mta">http://%s:%d/MTA/%s.mta</sec:MetaFileInfo>", + lan_addr[passed_args->iface].str, runtime_vars.port, mta); } if( artist ) { if( (*mime == 'v') && (passed_args->filter & FILTER_UPNP_ACTOR) ) { @@ -959,8 +1077,15 @@ callback(void *args, int argc, char **argv, char **azColName) if( strncmp(id, MUSIC_PLIST_ID, strlen(MUSIC_PLIST_ID)) == 0 ) { track = strrchr(id, '$')+1; } - if( NON_ZERO(track) && (passed_args->filter & FILTER_UPNP_ORIGINALTRACKNUMBER) ) { - ret = strcatf(str, "<upnp:originalTrackNumber>%s</upnp:originalTrackNumber>", track); + if( NON_ZERO(track) ) { + if( *mime == 'a' && (passed_args->filter & FILTER_UPNP_ORIGINALTRACKNUMBER) ) { + ret = strcatf(str, "<upnp:originalTrackNumber>%s</upnp:originalTrackNumber>", track); + } else if( *mime == 'v' ) { + if( NON_ZERO(disc) && (passed_args->filter & FILTER_UPNP_EPISODESEASON) ) + ret = strcatf(str, "<upnp:episodeSeason>%s</upnp:episodeSeason>", disc); + if( passed_args->filter & FILTER_UPNP_EPISODENUMBER ) + ret = strcatf(str, "<upnp:episodeNumber>%s</upnp:episodeNumber>", track); + } } if( passed_args->filter & FILTER_RES ) { ext = mime_to_ext(mime); @@ -979,10 +1104,9 @@ callback(void *args, int argc, char **argv, char **azColName) } if( !(passed_args->flags & FLAG_RESIZE_THUMBS) && NON_ZERO(tn) && IS_ZERO(rotate) ) { ret = strcatf(str, "<res protocolInfo=\"http-get:*:%s:%s\">" - "http://%s:%d/Thumbnails/%s.jpg" + "%s/Thumbnails/%s.jpg" "</res>", - mime, "DLNA.ORG_PN=JPEG_TN;DLNA.ORG_CI=1", lan_addr[passed_args->iface].str, - runtime_vars.port, detailID); + mime, "DLNA.ORG_PN=JPEG_TN;DLNA.ORG_CI=1", host, detailID); } else add_resized_res(srcw, srch, 160, 160, "JPEG_TN", detailID, passed_args); @@ -1063,16 +1187,15 @@ callback(void *args, int argc, char **argv, char **azColName) { if( passed_args->flags & FLAG_CAPTION_RES ) ret = strcatf(str, "<res protocolInfo=\"http-get:*:text/srt:*\">" - "http://%s:%d/Captions/%s.srt" + "%s/Captions/%s.srt" "</res>", - lan_addr[passed_args->iface].str, runtime_vars.port, detailID); + host, detailID); if( passed_args->filter & FILTER_SEC_CAPTION_INFO_EX ) ret = strcatf(str, "<sec:CaptionInfoEx sec:type=\"srt\">" - "http://%s:%d/Captions/%s.srt" + "%s/Captions/%s.srt" "</sec:CaptionInfoEx>", - lan_addr[passed_args->iface].str, runtime_vars.port, detailID); + host, detailID); } - free(alt_title); break; } } @@ -1081,17 +1204,33 @@ callback(void *args, int argc, char **argv, char **azColName) { /* Video and audio album art is handled differently */ if( *mime == 'v' && (passed_args->filter & FILTER_RES) && !(passed_args->flags & FLAG_MS_PFS) ) { - ret = strcatf(str, "<res protocolInfo=\"http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN\">" - "http://%s:%d/AlbumArt/%s-%s.jpg" - "</res>", - lan_addr[passed_args->iface].str, runtime_vars.port, album_art, detailID); + snprintf(dlna_buf, sizeof(dlna_buf), "%s/AlbumArt/%s-%d.jpg", host, detailID, 3); + append_with_attributes(str, "protocolInfo=\"http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG\"", dlna_buf, "res"); + append_with_attributes(str, "dlna:profileID=\"JPEG_LRG\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"", dlna_buf, "upnp:albumArtURI"); + + snprintf(dlna_buf, sizeof(dlna_buf), "%s/AlbumArt/%s-%d.jpg", host, detailID, 2); + append_with_attributes(str, "protocolInfo=\"http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED\"", dlna_buf, "res"); + append_with_attributes(str, "dlna:profileID=\"JPEG_MED\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"", dlna_buf, "upnp:albumArtURI"); + + snprintf(dlna_buf, sizeof(dlna_buf), "%s/AlbumArt/%s-%d.jpg", host, detailID, 1); + append_with_attributes(str, "protocolInfo=\"http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM\"", dlna_buf, "res"); + append_with_attributes(str, "dlna:profileID=\"JPEG_SM\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"", dlna_buf, "upnp:albumArtURI"); + + snprintf(dlna_buf, sizeof(dlna_buf), "%s/AlbumArt/%s-%d.jpg", host, detailID, 0); + append_with_attributes(str, "protocolInfo=\"http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN\"", dlna_buf, "res"); + append_with_attributes(str, "dlna:profileID=\"JPEG_TN\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"", dlna_buf, "upnp:albumArtURI"); } else if( passed_args->filter & FILTER_UPNP_ALBUMARTURI ) { - ret = strcatf(str, "<upnp:albumArtURI"); - if( passed_args->filter & FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID ) { - ret = strcatf(str, " dlna:profileID=\"JPEG_TN\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\""); - } - ret = strcatf(str, ">http://%s:%d/AlbumArt/%s-%s.jpg</upnp:albumArtURI>", - lan_addr[passed_args->iface].str, runtime_vars.port, album_art, detailID); + snprintf(dlna_buf, sizeof(dlna_buf), "%s/AlbumArt/%s-%d.jpg", host, detailID, 3); + append_with_attributes(str, "dlna:profileID=\"JPEG_LRG\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"", dlna_buf, "upnp:albumArtURI"); + + snprintf(dlna_buf, sizeof(dlna_buf), "%s/AlbumArt/%s-%d.jpg", host, detailID, 2); + append_with_attributes(str, "dlna:profileID=\"JPEG_MED\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"", dlna_buf, "upnp:albumArtURI"); + + snprintf(dlna_buf, sizeof(dlna_buf), "%s/AlbumArt/%s-%d.jpg", host, detailID, 1); + append_with_attributes(str, "dlna:profileID=\"JPEG_SM\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"", dlna_buf, "upnp:albumArtURI"); + + snprintf(dlna_buf, sizeof(dlna_buf), "%s/AlbumArt/%s-%d.jpg", host, detailID, 0); + append_with_attributes(str, "dlna:profileID=\"JPEG_TN\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"", dlna_buf, "upnp:albumArtURI"); } } if( (passed_args->flags & FLAG_MS_PFS) && *mime == 'i' ) { @@ -1101,14 +1240,14 @@ callback(void *args, int argc, char **argv, char **azColName) /* EVA2000 doesn't seem to handle embedded thumbnails */ if( !(passed_args->flags & FLAG_RESIZE_THUMBS) && NON_ZERO(tn) && IS_ZERO(rotate) ) { ret = strcatf(str, "<upnp:albumArtURI>" - "http://%s:%d/Thumbnails/%s.jpg" + "%s/Thumbnails/%s.jpg" "</upnp:albumArtURI>", - lan_addr[passed_args->iface].str, runtime_vars.port, detailID); + host, detailID); } else { ret = strcatf(str, "<upnp:albumArtURI>" - "http://%s:%d/Resized/%s.jpg?width=160,height=160" + "%s/Resized/%s.jpg?width=160,height=160" "</upnp:albumArtURI>", - lan_addr[passed_args->iface].str, runtime_vars.port, detailID); + host, detailID); } } ret = strcatf(str, "</item>"); @@ -1146,13 +1285,22 @@ callback(void *args, int argc, char **argv, char **azColName) if( artist && (passed_args->filter & FILTER_UPNP_ARTIST) ) { ret = strcatf(str, "<upnp:artist>%s</upnp:artist>", artist); } - if( NON_ZERO(album_art) && (passed_args->filter & FILTER_UPNP_ALBUMARTURI) ) { - ret = strcatf(str, "<upnp:albumArtURI "); - if( passed_args->filter & FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID ) { - ret = strcatf(str, "dlna:profileID=\"JPEG_TN\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\""); - } - ret = strcatf(str, ">http://%s:%d/AlbumArt/%s-%s.jpg</upnp:albumArtURI>", - lan_addr[passed_args->iface].str, runtime_vars.port, album_art, detailID); + if( NON_ZERO(album_art) && (passed_args->filter & FILTER_UPNP_ALBUMARTURI)) { + char *attribute = (passed_args->filter & FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID) ? "dlna:profileID=\"JPEG_LRG\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"" : ""; + snprintf(dlna_buf, sizeof(dlna_buf), "%s/AlbumArt/%s-%d.jpg", host, detailID, 3); + append_with_attributes(str, attribute, dlna_buf, "upnp:albumArtURI"); + + attribute = (passed_args->filter & FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID) ? "dlna:profileID=\"JPEG_MED\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"" : ""; + snprintf(dlna_buf, sizeof(dlna_buf), "%s/AlbumArt/%s-%d.jpg", host, detailID, 2); + append_with_attributes(str, attribute, dlna_buf, "upnp:albumArtURI"); + + attribute = (passed_args->filter & FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID) ? "dlna:profileID=\"JPEG_SM\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"" : ""; + snprintf(dlna_buf, sizeof(dlna_buf), "%s/AlbumArt/%s-%d.jpg", host, detailID, 1); + append_with_attributes(str, attribute, dlna_buf, "upnp:albumArtURI"); + + attribute = (passed_args->filter & FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID) ? "dlna:profileID=\"JPEG_TN\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"" : ""; + snprintf(dlna_buf, sizeof(dlna_buf), "%s/AlbumArt/%s-%d.jpg", host, detailID, 0); + append_with_attributes(str, attribute, dlna_buf, "upnp:albumArtURI"); } if( passed_args->filter & FILTER_AV_MEDIA_CLASS ) { char class; @@ -1248,6 +1396,8 @@ BrowseContentDirectory(struct upnphttp * h, const char * action) ret = strcatf(&str, DLNA_NAMESPACE); if( args.filter & FILTER_PV_SUBTITLE ) ret = strcatf(&str, PV_NAMESPACE); + if( args.filter & FILTER_SEC ) + ret = strcatf(&str, SEC_NAMESPACE); strcatf(&str, ">\n"); args.returned = 0; @@ -1357,7 +1507,7 @@ BrowseContentDirectory(struct upnphttp * h, const char * action) } sql = sqlite3_mprintf("SELECT %s, %s, %s, " COLUMNS - "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)" + "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)" " where %s %s limit %d, %d;", objectid_sql, parentid_sql, refid_sql, where, THISORNUL(orderBy), StartingIndex, RequestedCount); @@ -1411,7 +1561,7 @@ parse_search_criteria(const char *str, char *sep) { struct string_s criteria; int len; - int literal = 0, like = 0; + int literal = 0, like = 0, class = 0; const char *s; if (!str) @@ -1461,13 +1611,17 @@ parse_search_criteria(const char *str, char *sep) } break; case 'o': - if (strncmp(s, "object.", 7) == 0) - s += 7; - else if (strncmp(s, "object\"", 7) == 0 || - strncmp(s, "object"", 12) == 0) + if (class) { - s += 6; - continue; + class = 0; + if (strncmp(s, "object.", 7) == 0) + s += 7; + else if (strncmp(s, "object\"", 7) == 0 || + strncmp(s, "object"", 12) == 0) + { + s += 6; + continue; + } } default: charcat(&criteria, *s); @@ -1609,11 +1763,29 @@ parse_search_criteria(const char *str, char *sep) else charcat(&criteria, *s); break; + case 'o': + if (class) + { + if (strncmp(s, "object.", 7) == 0) + { + s += 7; + charcat(&criteria, '"'); + while (*s && !isspace(*s)) + { + charcat(&criteria, *s); + s++; + } + charcat(&criteria, '"'); + } + class = 0; + continue; + } case 'u': if (strncmp(s, "upnp:class", 10) == 0) { strcatf(&criteria, "o.CLASS"); s += 10; + class = 1; continue; } else if (strncmp(s, "upnp:actor", 10) == 0) @@ -1832,10 +2004,10 @@ static void QueryStateVariable(struct upnphttp * h, const char * action) { static const char resp[] = - "" + "" "%s" - ""; + ""; char body[512]; struct NameValueParserData data; @@ -1853,10 +2025,10 @@ QueryStateVariable(struct upnphttp * h, const char * action) SoapError(h, 402, "Invalid Args"); } else if(strcmp(var_name, "ConnectionStatus") == 0) - { + { int bodylen; bodylen = snprintf(body, sizeof(body), resp, - action, "urn:schemas-upnp-org:control-1-0", + action, "urn:schemas-upnp-org:control-1-0", "Connected", action); BuildSendAndCloseSoapResp(h, body, bodylen); } @@ -1866,7 +2038,147 @@ QueryStateVariable(struct upnphttp * h, const char * action) SoapError(h, 404, "Invalid Var"); } - ClearNameValueList(&data); + ClearNameValueList(&data); +} + +static int _set_watch_count(long long id, const char *old, const char *new) +{ + int64_t rowid = sqlite3_last_insert_rowid(db); + int ret; + + ret = sql_exec(db, "INSERT or IGNORE into BOOKMARKS (ID, WATCH_COUNT)" + " VALUES (%lld, %Q)", id, new ?: "1"); + if (sqlite3_last_insert_rowid(db) != rowid) + return 0; + + if (!new) /* Increment */ + ret = sql_exec(db, "UPDATE BOOKMARKS set WATCH_COUNT =" + " ifnull(WATCH_COUNT,'0') + 1" + " where ID = %lld", id); + else if (old && old[0]) + ret = sql_exec(db, "UPDATE BOOKMARKS set WATCH_COUNT = %Q" + " where WATCH_COUNT = %Q and ID = %lld", + new, old, id); + else + ret = sql_exec(db, "UPDATE BOOKMARKS set WATCH_COUNT = %Q" + " where ID = %lld", + new, id); + return ret; +} + +/* For some reason, Kodi does URI encoding and appends a trailing slash */ +static void _kodi_decode(char *str) +{ + while (*str) + { + switch (*str) { + case '%': + { + if (isxdigit(str[1]) && isxdigit(str[2])) + { + char x[3] = { str[1], str[2], '\0' }; + *str++ = (char)strtol(x, NULL, 16); + memmove(str, str+2, strlen(str+1)); + } + break; + } + case '/': + if (!str[1]) + *str = '\0'; + default: + str++; + break; + } + } +} + +static int duration_sec(const char *str) +{ + int hr, min, sec; + + if (sscanf(str, "%d:%d:%d", &hr, &min, &sec) == 3) + return (hr * 3600) + (min * 60) + sec; + + return atoi(str); +} + +static void UpdateObject(struct upnphttp * h, const char * action) +{ + static const char resp[] = + "" + ""; + + struct NameValueParserData data; + + ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, 0); + + char *ObjectID = GetValueFromNameValueList(&data, "ObjectID"); + char *CurrentTagValue = GetValueFromNameValueList(&data, "CurrentTagValue"); + char *NewTagValue = GetValueFromNameValueList(&data, "NewTagValue"); + const char *rid = ObjectID; + char tag[32], current[32], new[32]; + char *item, *saveptr = NULL; + int64_t detailID; + int ret = 1; + + if (!ObjectID || !CurrentTagValue || !NewTagValue) + { + SoapError(h, 402, "Invalid Args"); + ClearNameValueList(&data); + return; + } + + _kodi_decode(ObjectID); + DPRINTF(E_DEBUG, L_HTTP, "UpdateObject %s: %s => %s\n", ObjectID, CurrentTagValue, NewTagValue); + + in_magic_container(ObjectID, 0, &rid); + detailID = sql_get_int64_field(db, "SELECT DETAIL_ID from OBJECTS where OBJECT_ID = '%q'", rid); + if (detailID <= 0) + { + SoapError(h, 701, "No such object"); + ClearNameValueList(&data); + return; + } + + for (item = strtok_r(CurrentTagValue, ",", &saveptr); item; item = strtok_r(NULL, ",", &saveptr)) + { + char *p; + if (sscanf(item, "<%31[^&]>%31[^&]", tag, current) != 2) + continue; + p = strstr(NewTagValue, tag); + if (!p || sscanf(p, "%*[^&]>%31[^&]", new) != 1) + continue; + + DPRINTF(E_DEBUG, L_HTTP, "Setting %s to %s\n", tag, new); + /* Kodi uses incorrect tag "upnp:playCount" instead of "upnp:playbackCount" */ + if (strcmp(tag, "upnp:playbackCount") == 0 || strcmp(tag, "upnp:playCount") == 0) + { + ret = _set_watch_count(detailID, current, new); + } + else if (strcmp(tag, "upnp:lastPlaybackPosition") == 0) + { + int sec = duration_sec(new); + if (sec < 30) + sec = 0; + else + sec -= 1; + ret = sql_exec(db, "INSERT OR IGNORE into BOOKMARKS (ID, SEC)" + " VALUES (%lld, %d)", (long long)detailID, sec); + ret = sql_exec(db, "UPDATE BOOKMARKS set SEC = %d" + " where SEC = %Q and ID = %lld", + sec, current, (long long)detailID); + } + else + DPRINTF(E_WARN, L_HTTP, "Tag %s unsupported for writing\n", tag); + } + + if (ret == SQLITE_OK) + BuildSendAndCloseSoapResp(h, resp, sizeof(resp)-1); + else + SoapError(h, 501, "Action Failed"); + + ClearNameValueList(&data); } static void @@ -1906,6 +2218,12 @@ SamsungGetFeatureList(struct upnphttp * h, const char * action) image = runtime_vars.root_container; } } + else if (h->req_client && (h->req_client->type->flags & FLAG_SAMSUNG_DCM10)) + { + audio = "A"; + video = "V"; + image = "I"; + } len = snprintf(body, sizeof(body), resp, audio, video, image); @@ -1927,18 +2245,23 @@ SamsungSetBookmark(struct upnphttp * h, const char * action) ObjectID = GetValueFromNameValueList(&data, "ObjectID"); PosSecond = GetValueFromNameValueList(&data, "PosSecond"); - if ( atoi(PosSecond) < 30 ) - PosSecond = "0"; - if( ObjectID && PosSecond ) { - int ret; const char *rid = ObjectID; + int64_t detailID; + int sec = atoi(PosSecond); + int ret; in_magic_container(ObjectID, 0, &rid); - ret = sql_exec(db, "INSERT OR REPLACE into BOOKMARKS" - " VALUES " - "((select DETAIL_ID from OBJECTS where OBJECT_ID = '%q'), %q)", rid, PosSecond); + detailID = sql_get_int64_field(db, "SELECT DETAIL_ID from OBJECTS where OBJECT_ID = '%q'", rid); + + if ( sec < 30 ) + sec = 0; + ret = sql_exec(db, "INSERT OR IGNORE into BOOKMARKS (ID, SEC)" + " VALUES (%lld, %d)", (long long)detailID, sec); + ret = sql_exec(db, "UPDATE BOOKMARKS set SEC = %d" + " where ID = %lld", + sec, (long long)detailID); if( ret != SQLITE_OK ) DPRINTF(E_WARN, L_METADATA, "Error setting bookmark %s on ObjectID='%s'\n", PosSecond, rid); BuildSendAndCloseSoapResp(h, resp, sizeof(resp)-1); @@ -1946,12 +2269,136 @@ SamsungSetBookmark(struct upnphttp * h, const char * action) else SoapError(h, 402, "Invalid Args"); - ClearNameValueList(&data); + ClearNameValueList(&data); +} + +static void +SamsungGetSemanticQueryCapabilities(struct upnphttp * h, const char * action) +{ + static const char resp[] = + "" + "" + "MACRO_RECENTLYADDED_ALL,MACRO_RECENTLYPLAYED_ALL" + "" + ""; + + BuildSendAndCloseSoapResp(h, resp, sizeof(resp)-1); } -static const struct +static void +SamsungGetSemanticList(struct upnphttp * h, const char * action) { - const char * methodName; + static const char resp[] = + "" + "" + "<DIDL-Lite" + CONTENT_DIRECTORY_SCHEMAS; + + struct NameValueParserData data; + char *SemanticQueryRequest; + int StartingIndex = 0, RequestedCount = 0; + char *ptr, *sql; + char *zErrMsg = NULL; + int totalMatches = 0; + struct Response args; + struct string_s str; + int ret; + + memset(&args, 0, sizeof(args)); + memset(&str, 0, sizeof(str)); + + ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, 0); + SemanticQueryRequest = GetValueFromNameValueList(&data, "SemanticQueryRequest"); + + if( strcmp(SemanticQueryRequest, "MACRO_RECENTLYADDED_ALL" ) ) + { + SoapError(h, 402, "Invalid Args"); + goto semantic_error; + } + + if( (ptr = GetValueFromNameValueList(&data, "StartingIndex")) ) + StartingIndex = atoi(ptr); + if( StartingIndex < 0 ) + { + SoapError(h, 402, "Invalid Args"); + goto semantic_error; + } + if( (ptr = GetValueFromNameValueList(&data, "RequestedCount")) ) + RequestedCount = atoi(ptr); + if( RequestedCount < 0 ) + { + SoapError(h, 402, "Invalid Args"); + goto semantic_error; + } + if( !RequestedCount ) + RequestedCount = -1; + + str.data = malloc(DEFAULT_RESP_SIZE); + str.size = DEFAULT_RESP_SIZE; + str.off = sprintf(str.data, "%s", resp); + args.iface = h->iface; + args.filter = set_filter_flags(NULL, h); + if( args.filter & FILTER_DLNA_NAMESPACE ) + ret = strcatf(&str, DLNA_NAMESPACE); + strcatf(&str, ">\n"); + + args.returned = 0; + + totalMatches = sql_get_int_field(db, "SELECT count(*) from OBJECTS o left join DETAILS d on (o.DETAIL_ID = d.ID)" + " where PARENT_ID in " + " ( '" VIDEO_ALL_ID "'," + " '" MUSIC_ALL_ID "'," + " '" IMAGE_ALL_ID "')" + " AND TIMESTAMP > strftime('%%s', 'now', '-90 day')"); + + if ( totalMatches > 0 ) + { + args.requested = RequestedCount; + args.client = h->req_client ? h->req_client->type->type : 0; + args.flags = h->req_client ? h->req_client->type->flags : 0; + args.str = &str; + + sql = sqlite3_mprintf( SELECT_COLUMNS + "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)" + " where PARENT_ID in" + " ( '" VIDEO_ALL_ID "'," + " '" MUSIC_ALL_ID "'," + " '" IMAGE_ALL_ID "')" + " AND TIMESTAMP > strftime('%%s', 'now', '-90 day')" + " ORDER BY TIMESTAMP DESC" + " limit %d, %d", + StartingIndex, RequestedCount); + DPRINTF(E_DEBUG, L_HTTP, "Search SQL: %s\n", sql); + ret = sqlite3_exec(db, sql, callback, (void *) &args, &zErrMsg); + if( (ret != SQLITE_OK) && (zErrMsg != NULL) ) + { + DPRINTF(E_WARN, L_HTTP, "SQL error: %s\nBAD SQL: %s\n", zErrMsg, sql); + sqlite3_free(zErrMsg); + totalMatches = 0; + args.returned = 0; + } + sqlite3_free(sql); + } + else + totalMatches = 0; + + ret = strcatf(&str, "</DIDL-Lite>\n" + "%u\n" + "%u\n" + "%u" + "", + args.returned, totalMatches, updateID); + BuildSendAndCloseSoapResp(h, str.data, str.off); + +semantic_error: + ClearNameValueList(&data); + free(str.data); +} + +static const struct +{ + const char * methodName; void (*methodImpl)(struct upnphttp *, const char *); } soapMethods[] = @@ -1968,8 +2415,11 @@ soapMethods[] = { "IsAuthorized", IsAuthorizedValidated}, { "IsValidated", IsAuthorizedValidated}, { "RegisterDevice", RegisterDevice}, + { "UpdateObject", UpdateObject}, { "X_GetFeatureList", SamsungGetFeatureList}, { "X_SetBookmark", SamsungSetBookmark}, + { "X_GetSemanticQueryCapabilities", SamsungGetSemanticQueryCapabilities}, + { "X_GetSemanticList", SamsungGetSemanticList}, { 0, 0 } }; @@ -2008,4 +2458,3 @@ ExecuteSoapAction(struct upnphttp * h, const char * action, int n) SoapError(h, 401, "Invalid Action"); } - diff --git a/upnpsoap.h b/upnpsoap.h index b3e94a0..a8e950a 100644 --- a/upnpsoap.h +++ b/upnpsoap.h @@ -32,6 +32,8 @@ " xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"" #define PV_NAMESPACE \ " xmlns:pv=\"http://www.pv.com/pvns/\"" +#define SEC_NAMESPACE \ + " xmlns:sec=\"http://www.sec.co.kr/dlna\"" struct Response { @@ -41,7 +43,7 @@ struct Response int requested; int iface; uint32_t filter; - uint32_t flags; + uint64_t flags; enum client_types client; }; @@ -51,4 +53,3 @@ void ExecuteSoapAction(struct upnphttp *, const char *, int); #endif - diff --git a/utils.c b/utils.c index 5a233dc..0bb4992 100644 --- a/utils.c +++ b/utils.c @@ -1,5 +1,5 @@ /* MiniDLNA media server - * Copyright (C) 2008-2009 Justin Maggard + * Copyright (C) 2008-2017 Justin Maggard * * This file is part of MiniDLNA. * @@ -27,6 +27,7 @@ #include #include #include +#include #include #include @@ -61,11 +62,23 @@ ends_with(const char * haystack, const char * needle) if( nlen > hlen ) return 0; - end = haystack + hlen - nlen; + end = haystack + hlen - nlen; return (strcasecmp(end, needle) ? 0 : 1); } +int +begins_with(const char * haystack, const char * needle) +{ + int nlen = strlen(needle); + int hlen = strlen(haystack); + + if( hlen < nlen ) + return 0; + + return (strncasecmp(haystack, needle, nlen) ? 0 : 1); +} + char * trim(char *str) { @@ -230,11 +243,27 @@ escape_tag(const char *tag, int force_alloc) return esc_tag; } +char * +duration_str(int msec) +{ + char *str; + + xasprintf(&str, "%d:%02d:%02d.%03d", + (msec / 3600000), + (msec / 60000 % 60), + (msec / 1000 % 60), + (msec % 1000)); + + return str; +} + char * strip_ext(char *name) { char *period; + if (!name) + return NULL; period = strrchr(name, '.'); if (period) *period = '\0'; @@ -282,7 +311,7 @@ make_dir(char * path, mode_t mode) return -1; } } - if (!c) + if (!c) return 0; /* Remove any inserted nul from the path. */ @@ -291,9 +320,61 @@ make_dir(char * path, mode_t mode) } while (1); } +int +copy_file(const char *src_file, const char *dst_file) +{ + char buf[MAXPATHLEN]; + size_t nread; + size_t nwritten = 0; + size_t size = 0; + + strncpyt(buf, dst_file, sizeof(buf)); + make_dir(dirname(buf), S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); + + FILE *fsrc = fopen(src_file, "rb"); + FILE *fdst = fopen(dst_file, "wb"); + + while ((nread = fread(buf, 1, sizeof(buf), fsrc)) > 0) + { + size += nread; + nwritten += fwrite(buf, 1, nread, fdst); + } + + fclose(fsrc); + fclose(fdst); + + if (nwritten != size) + { + DPRINTF(E_WARN, L_ARTWORK, "copying %s to %s failed [%s]\n", src_file, dst_file, strerror(errno)); + return -1; + } + return 0; +} + +int +link_file(const char *src_file, const char *dst_file) +{ + if (link(src_file, dst_file) == 0) + return 0; + + if (errno == ENOENT) + { + char *dir = strdup(dst_file); + make_dir(dirname(dir), S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); + free(dir); + if (link(src_file, dst_file) == 0) + return 0; + /* try a softlink if all else fails */ + if (symlink(src_file, dst_file) == 0) + return 0; + } + DPRINTF(E_INFO, L_GENERAL, "Linking %s to %s failed [%s]\n", src_file, dst_file, strerror(errno)); + return -1; +} + /* Simple, efficient hash function from Daniel J. Bernstein */ unsigned int -DJBHash(uint8_t *data, int len) +DJBHash(const uint8_t *data, int len) { unsigned int hash = 5381; unsigned int i = 0; @@ -331,6 +412,8 @@ mime_to_ext(const char * mime) return "3gp"; else if( strcmp(mime, "application/ogg") == 0 ) return "ogg"; + else if( strcmp(mime+6, "x-dsd") == 0 ) + return "dsd"; break; case 'v': if( strcmp(mime+6, "avi") == 0 ) @@ -398,7 +481,8 @@ is_audio(const char * file) ends_with(file, ".m4a") || ends_with(file, ".aac") || ends_with(file, ".mp4") || ends_with(file, ".m4p") || ends_with(file, ".wav") || ends_with(file, ".ogg") || - ends_with(file, ".pcm") || ends_with(file, ".3gp")); + ends_with(file, ".pcm") || ends_with(file, ".3gp") || + ends_with(file, ".dsf") || ends_with(file, ".dff")); } int @@ -413,10 +497,55 @@ is_playlist(const char * file) return (ends_with(file, ".m3u") || ends_with(file, ".pls")); } +// List of common subtitle formats, taken from https://en.wikipedia.org/wiki/Subtitle_(captioning) +const char* subtitle_formats[] = { + ".aqt", // AQTitle + ".ass", // Advanced SubStation Alpha + ".gsub", // Gloss Subtitle + ".jss", // JACOSub + ".pjs", // Phoenix Subtitle + ".psb", // PowerDivX + ".rt", // RealText + ".smi", // SAMI + ".srt", // SubRip + ".ssa", // SubStation Alpha + ".ssf", // Structured Subtitle Format + ".stl", // Spruce subtitle format[15] + ".sub", // MPSub, MicroDVD, SubViewer, VobSub (also needs .idx) + ".ttxt", // MPEG-4 Timed Text + ".usf", // Universal Subtitle Format +}; + int is_caption(const char * file) { - return (ends_with(file, ".srt") || ends_with(file, ".smi")); + const char** subtitle_format = &subtitle_formats[0]; + do { + if(ends_with(file, *subtitle_format)) + return 1; + } while(*++subtitle_format); + return 0; +} + +media_types +get_media_type(const char *file) +{ + const char *ext = strrchr(file, '.'); + if (!ext) + return NO_MEDIA; + if (is_image(ext)) + return TYPE_IMAGE; + if (is_video(ext)) + return TYPE_VIDEO; + if (is_audio(ext)) + return TYPE_AUDIO; + if (is_playlist(ext)) + return TYPE_PLAYLIST; + if (is_caption(ext)) + return TYPE_CAPTION; + if (is_nfo(ext)) + return TYPE_NFO; + return NO_MEDIA; } int @@ -442,11 +571,34 @@ is_album_art(const char * name) return (album_art_name ? 1 : 0); } +#define IGNORE_FILENAME ".mediaignore" +#define IGNOREALL_FILENAME ".mediaignoreall" +int +has_ignore(const char * dir, int checkboth) +{ + char path[PATH_MAX]; + int spfr; + int hignore = 0; + char *ignore_path = path; + + spfr = snprintf(ignore_path, PATH_MAX, "%s/" IGNOREALL_FILENAME, dir); + if( spfr > 0 && spfr < PATH_MAX) + hignore = !access(ignore_path, F_OK); + if( !hignore && checkboth) + { + spfr = snprintf(ignore_path, PATH_MAX, "%s/" IGNORE_FILENAME, dir); + if( spfr > 0 && spfr < PATH_MAX) + hignore = !access(ignore_path, F_OK); + } + + return hignore; +} + int resolve_unknown_type(const char * path, media_types dir_type) { struct stat entry; - unsigned char type = TYPE_UNKNOWN; + enum file_types type = TYPE_UNKNOWN; char str_buf[PATH_MAX]; ssize_t len; @@ -473,33 +625,123 @@ resolve_unknown_type(const char * path, media_types dir_type) } else if( S_ISREG(entry.st_mode) ) { - switch( dir_type ) - { - case ALL_MEDIA: - if( is_image(path) || - is_audio(path) || - is_video(path) || - is_playlist(path) ) - type = TYPE_FILE; - break; - case TYPE_AUDIO: - if( is_audio(path) || - is_playlist(path) ) - type = TYPE_FILE; - break; - case TYPE_VIDEO: - if( is_video(path) ) - type = TYPE_FILE; - break; - case TYPE_IMAGES: - if( is_image(path) ) - type = TYPE_FILE; - break; - default: - break; - } + media_types mtype = get_media_type(path); + if (dir_type & mtype) + type = TYPE_FILE; } } return type; } +char* +base64_encode(const unsigned char *data, size_t ilen, size_t *olen) +{ + static const char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + + unsigned char a,b,c; + unsigned int idx; + unsigned int i , j, s; + char *obuff; + size_t osize; + + if ( !data || !ilen ) + return 0; + + osize = 4 *((ilen + 2) / 3); + obuff = malloc(osize); + + if ( !obuff ) + return 0; + + s = ilen - (ilen % 3); + + for (i = 0, j = 0; i < s;) + { + a = data[i++]; + b = data[i++]; + c = data[i++]; + + idx = (a << 16) + (b << 8) + c; + obuff[j++] = table[(idx >> 3 * 6) & 0x3F]; + obuff[j++] = table[(idx >> 2 * 6) & 0x3F]; + obuff[j++] = table[(idx >> 6) & 0x3F]; + obuff[j++] = table[idx & 0x3F]; + } + if (s < ilen) + { + a = data[i++]; + b = i < ilen ? data[i++] : 0; + c = 0; + + idx = (a << 16) + (b << 8) + c; + obuff[j++] = table[(idx >> 3 * 6) & 0x3F]; + obuff[j++] = table[(idx >> 2 * 6) & 0x3F]; + obuff[j++] = table[(idx >> 6) & 0x3F]; + obuff[j++] = table[idx & 0x3F]; + + obuff[osize -1] = '='; + + if ( (ilen % 3) == 1) + obuff[osize - 2] = '='; + } + + *olen = osize; + + return obuff; +} + +media_types +valid_media_types(const char *path) +{ + struct media_dir_s *media_dir; + + for (media_dir = media_dirs; media_dir; media_dir = media_dir->next) + { + if (strncmp(path, media_dir->path, strlen(media_dir->path)) == 0) + return media_dir->types; + } + + return ALL_MEDIA; +} + +/* + * Add and subtract routines for timevals. + * N.B.: subtract routine doesn't deal with + * results which are before the beginning, + * it just gets very confused in this case. + * Caveat emptor. + */ +static void timevalfix(struct timeval *); +void +timevaladd(struct timeval *t1, const struct timeval *t2) +{ + + t1->tv_sec += t2->tv_sec; + t1->tv_usec += t2->tv_usec; + timevalfix(t1); +} + +void +timevalsub(struct timeval *t1, const struct timeval *t2) +{ + + t1->tv_sec -= t2->tv_sec; + t1->tv_usec -= t2->tv_usec; + timevalfix(t1); +} + +static void +timevalfix(struct timeval *t1) +{ + + if (t1->tv_usec < 0) { + t1->tv_sec--; + t1->tv_usec += 1000000; + } + if (t1->tv_usec >= 1000000) { + t1->tv_sec++; + t1->tv_usec -= 1000000; + } +} diff --git a/utils.h b/utils.h index 433179e..0610103 100644 --- a/utils.h +++ b/utils.h @@ -5,7 +5,7 @@ * Author : Justin Maggard * * MiniDLNA media server - * Copyright (C) 2008-2009 Justin Maggard + * Copyright (C) 2008-2017 Justin Maggard * * This file is part of MiniDLNA. * @@ -73,6 +73,7 @@ static inline int is_dir(const struct dirent *d) #endif } int xasprintf(char **strp, char *fmt, ...) __attribute__((__format__ (__printf__, 2, 3))); +int begins_with(const char * haystack, const char * needle); int ends_with(const char * haystack, const char * needle); char *trim(char *str); char *strstrc(const char *s, const char *p, const char t); @@ -80,6 +81,7 @@ char *strcasestrc(const char *s, const char *p, const char t); char *modifyString(char *string, const char *before, const char *after, int noalloc); char *escape_tag(const char *tag, int force_alloc); char *unescape_tag(const char *tag, int force_alloc); +char *duration_str(int msec); char *strip_ext(char *name); /* Metadata functions */ @@ -88,12 +90,30 @@ int is_audio(const char * file); int is_image(const char * file); int is_playlist(const char * file); int is_caption(const char * file); +#define is_nfo(file) ends_with(file, ".nfo") +media_types get_media_type(const char *file); +media_types valid_media_types(const char *path); + int is_album_art(const char * name); +int has_ignore(const char * dir, int checkboth); int resolve_unknown_type(const char * path, media_types dir_type); const char *mime_to_ext(const char * mime); /* Others */ int make_dir(char * path, mode_t mode); -unsigned int DJBHash(uint8_t *data, int len); +char *base64_encode(const unsigned char *data, size_t ilen, size_t *olen); +unsigned int DJBHash(const uint8_t *data, int len); +extern const char* subtitle_formats[]; +int copy_file(const char *src_file, const char *dst_file); +int link_file(const char *src_file, const char *dst_file); +#define IGNORE_RETURN_VALUE(...) if(__VA_ARGS__); + +/* Timeval manipulations */ +void timevaladd(struct timeval *t1, const struct timeval *t2); +void timevalsub(struct timeval *t1, const struct timeval *t2); +#define timevalcmp(tvp, uvp, cmp) \ + (((tvp)->tv_sec == (uvp)->tv_sec) ? \ + ((tvp)->tv_usec cmp (uvp)->tv_usec) : \ + ((tvp)->tv_sec cmp (uvp)->tv_sec)) #endif diff --git a/uuid.c b/uuid.c index 3cd6f5a..6e6a9ec 100644 --- a/uuid.c +++ b/uuid.c @@ -39,6 +39,7 @@ #include #endif +#include "event.h" #include "uuid.h" #include "getifaddr.h" #include "log.h" diff --git a/video_thumb.c b/video_thumb.c new file mode 100644 index 0000000..dfbaaa8 --- /dev/null +++ b/video_thumb.c @@ -0,0 +1,577 @@ +/* MiniDLNA project + * + * http://sourceforge.net/projects/minidlna/ + * + * MiniDLNA media server + * Copyright (C) 2008-2009 Justin Maggard + * + * This file is part of MiniDLNA. + * + * MiniDLNA is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * MiniDLNA 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 MiniDLNA. If not, see . + * + * Portions of the code from the MiniUPnP project: + * + * Copyright (c) 2006-2007, Thomas Bernard + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#ifdef ENABLE_VIDEO_THUMB +#if HAVE_FFMPEG_LIBSWSCALE_SWSCALE_H +#include +#elif HAVE_LIBAV_LIBSWSCALE_SWSCALE_H +#include +#elif HAVE_LIBSWSCALE_SWSCALE_H +#include +#elif HAVE_FFMPEG_SWSCALE_H +#include +#elif HAVE_LIBAV_SWSCALE_H +#include +#elif HAVE_SWSCALE_H +#include +#endif +#endif + +#include "upnpglobalvars.h" +#include "image_utils.h" +#include "libav.h" +#include "log.h" +#include "video_thumb.h" +#include "utils.h" +#include "albumart.h" + + +#ifdef ENABLE_VIDEO_THUMB +static int +get_video_packet(AVFormatContext *ctx, AVCodecContext *vctx, + AVPacket *pkt, AVFrame *frame, int vstream) +{ + int moreframes, decoded, finished; + int avret, i, j, l; + + lav_free_packet(pkt); + + finished = 0; + for (l = 0; !finished && l < 500; l++) + { + moreframes = 1; + decoded = 0; + for (i = 0; moreframes && ! decoded && i < 2000; i++) + { + avret = av_read_frame(ctx, pkt); + + if ((moreframes = (avret >= 0))) + { + if (!(decoded = (pkt->stream_index == vstream))) + { + lav_free_packet(pkt); + continue; + } + + for (j = 0; !finished && j < 50; j++) + { + lav_frame_unref(frame); + avret = lav_avcodec_decode_video(vctx, frame, &finished, pkt); + if (avret < 0) { + if(avret==AVERROR(EAGAIN)) { + break; + } + DPRINTF(E_WARN, L_METADATA, "video_thumb_generate_tofile: failed to decode video \n"); + return 0; + } + } + + + } + } + } + + return (finished > 0); +} + +static int +video_seek(int seconds, AVFormatContext *ctx, AVCodecContext *vctx, + AVPacket *pkt, AVFrame *frame, int vstream) +{ + int64_t tstamp = AV_TIME_BASE * (int64_t) seconds; + + if (tstamp < 0) + tstamp = 0; + + if ((av_seek_frame(ctx, -1, tstamp, 0) >=0)) { + avcodec_flush_buffers(vctx); + } + + return get_video_packet(ctx, vctx, pkt, frame, vstream); +} +#endif + +int +video_thumb_generate_tofile(const char *moviefname, const char *thumbfname, int seek, int width) +{ +#ifdef ENABLE_VIDEO_THUMB + image_s img; + int ret = 0; + clock_t start, end; + + start = clock(); + memset(&img, 0, sizeof(image_s)); + + if ((video_thumb_generate_tobuff(moviefname, &img, seek, width) < 0)) + { + DPRINTF(E_WARN, L_METADATA, "video_thumb_generate_tofile: unable to generate thumbnail to buffer for '%s'!\n", moviefname); + return -1; + } + + if (!image_save_to_jpeg_file(&img, (char*) thumbfname)) + { + DPRINTF(E_WARN, L_METADATA, "video_thumb_generate_tofile: unable to save jpeg file : %s \n", thumbfname); + ret = -1; + } + + free(img.buf); + + end = clock(); + DPRINTF(E_DEBUG, L_METADATA, "Generated thumbnail for (%s) in %ldms.\n", moviefname, (end-start) * 1000 / CLOCKS_PER_SEC); + + return ret; +#else + return -1; +#endif +} + +#ifdef ENABLE_VIDEO_THUMB +static AVFrame* +video_thumb_swscale_frame(AVFrame *frame, int width, enum AVPixelFormat pixfmt) +{ + AVFrame *scframe = NULL; + struct SwsContext *scctx = NULL; + int dwidth, dheight, avret; + + if(!frame) return NULL; + + dwidth = width; + dheight = (int) ((float) (width * frame->height) / frame->width ); + + scframe = lav_frame_alloc(); + if (!scframe) + { + DPRINTF(E_WARN, L_METADATA, "video_thumb_swscale_frame: malloc error!! Unable to alloc memory for the scaled frame \n"); + avret = -1; + goto rescale_error; + } + + scframe->format = pixfmt; + scframe->width = dwidth; + scframe->height = dheight; + avret = av_frame_get_buffer(scframe, calc_ptr_alignment(scframe)); + if (avret < 0) + { + DPRINTF(E_WARN, L_METADATA, "video_thumb_swscale_frame: malloc error!! Unable to alloc memory for the scaled frame buffer \n"); + goto rescale_error; + } + + scctx = sws_getCachedContext(scctx, + frame->width, frame->height, frame->format, + scframe->width, scframe->height, scframe->format, + SWS_BICUBIC, NULL, NULL, NULL); + if (!scctx) + { + DPRINTF(E_WARN, L_METADATA, "video_thumb_swscale_frame: Unable to get a scale context! \n"); + avret = -1; + goto rescale_error; + } + + avret = sws_scale(scctx, (const uint8_t * const*)frame->data, frame->linesize, 0, frame->height, + scframe->data, scframe->linesize); + + rescale_error: + sws_freeContext(scctx); + + if (avret <= 0) + { + DPRINTF(E_WARN, L_METADATA, "video_thumb_swscale_frame: Unable to scale thumbnail! \n"); + return NULL; + } + + return scframe; +} + + +static int +video_thumb_generate_ctx_tobuff(AVFormatContext *fctx, void* imgbuffer, int seek, int width) +{ + AVFrame *frame = NULL; + AVPacket packet; + AVCodecContext *vcctx = NULL; + AVCodec *vcodec = NULL; + AVDictionary *opts = NULL; + int avret, i, vs, ret = -1; + image_s* buffer = (image_s*) imgbuffer; + + if (!fctx) + return -1; + + if (!buffer) + return -1; + + if (seek < 0) + seek = 0; + else if (seek > 90) + seek = 90; + + av_init_packet(&packet); + + vs = -1; + for (i =0; inb_streams; i++) + { + if (fctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) + { + vs = i; + break; + } + } + + if( vs < 0) + { + DPRINTF(E_WARN, L_METADATA, "video_thumb_generate_tobuff: unable to find a video stream \n"); + goto thumb_generate_error; + } + + vcodec = avcodec_find_decoder(fctx->streams[vs]->codecpar->codec_id); + if (!vcodec) + { + DPRINTF(E_WARN, L_METADATA, "video_thumb_generate_tobuff: unable to find a video decoder \n"); + goto thumb_generate_error; + } + + vcctx = avcodec_alloc_context3(vcodec); + if( !vcctx ) + { + DPRINTF(E_WARN, L_METADATA, "video_thumb_generate_tobuff: unable to allocate a video context \n"); + goto thumb_generate_error; + } + + if(avcodec_parameters_to_context(vcctx, fctx->streams[i]->codecpar) < 0) + { + DPRINTF(E_WARN, L_METADATA, "video_thumb_generate_tobuff: unable to initialise video context \n"); + } + av_codec_set_pkt_timebase(vcctx, fctx->streams[vs]->time_base); + + av_dict_set(&opts, "refcounted_frames", "1", 0); + vcctx->workaround_bugs = 1; + + avret = lav_avcodec_open(vcctx, vcodec, &opts); + if(avret < 0) + { + DPRINTF(E_WARN, L_METADATA, "video_thumb_generate_tobuff: unable to open a decoder \n"); + goto thumb_generate_error; + } + + frame = lav_frame_alloc(); + if (!frame) + { + DPRINTF(E_WARN, L_METADATA, "video_thumb_generate_tobuff: malloc error!! Unable to alloc memory for a new frame \n"); + goto thumb_generate_error; + } + + if (!video_seek(seek*(fctx->duration/AV_TIME_BASE)/100, + fctx, vcctx, &packet, frame, vs)) + { + DPRINTF(E_WARN, L_METADATA, "video_thumb_generate_tobuff: unable to seek video for %d%% position \n", seek); + goto thumb_generate_error; + } + + if (frame->interlaced_frame) + { + DPRINTF(E_DEBUG, L_METADATA, "video_thumb_generate_tobuff: got an interlaced video \n"); + lav_avpicture_deinterlace(frame, frame, + vcctx->pix_fmt, vcctx->width, vcctx->height); + } + + // We always need to scale, even if just to convert the image format from + // YUV or other to the target pixfmt (RGBA32). We need to convert to RGBA32, + // because that is what this function returns. The data we return is expected + // to be in consecutive RGBA32, i.e. no planes or slices, because it is + // returned as an struct image_s. + if(!width) { + width = vcctx->width; + } + else if (width < 64) + width = 64; + else if (width > 480) + width = 480; + + AVFrame* scframe = video_thumb_swscale_frame(frame, width, LAV_PIX_FMT_RGB32_1); + if(!scframe) goto thumb_generate_error; + lav_frame_unref(frame); + frame = scframe; + + // Copy to output + free(buffer->buf); + buffer->width = frame->width; + buffer->height = frame->height; + buffer->buf = (pix*) malloc(sizeof(uint8_t) * 4 * buffer->width * buffer->height); + if(!buffer->buf) + { + DPRINTF(E_WARN, L_METADATA, "video_thumb_generate_tobuff: malloc error!! Unable to alloc memory to buffer the picture \n"); + goto thumb_generate_error; + } + + // check if we need to do any work to linearize. Often (because we target + // RGBA32) this check will be true. + if(frame->width == frame->linesize[0]) + { + memcpy(buffer->buf, frame->data[0], 4 * frame->width * buffer->height); + } + else + { + pix *p = buffer->buf; + for(int32_t i=0; i < buffer->height; ++i) + { + memcpy(p, frame->data[0] + i*frame->linesize[0], 4 * buffer->width); + p += buffer->width; + } + } + ret = 0; + +thumb_generate_error: + avcodec_free_context(&vcctx); + av_dict_free(&opts); + lav_free_packet(&packet); + lav_frame_unref(frame); + + return ret; +} +#endif + +int +video_thumb_generate_tobuff(const char *moviefname, void* imgbuffer, int seek, int width) +{ +#ifdef ENABLE_VIDEO_THUMB + AVFormatContext *fctx = NULL; + int ret = -1; + + if(!moviefname) + return -1; + + if (lav_open(&fctx, moviefname)) + { + DPRINTF(E_WARN, L_METADATA, "video_thumb_generate_tobuff: unable to open movie file (%s) \n", moviefname); + return -1; + } + + ret = video_thumb_generate_ctx_tobuff(fctx, imgbuffer, seek, width); + + lav_close(fctx); + return ret; +#else + return -1; +#endif +} + +#define MTA_WIDTH 96 + +char* +video_thumb_generate_mta_file(const char *moviefname, int duration, int allblack) +{ + static const char mta_header[] = + "\r\n" + "\r\n" + "1.0\r\n" + "\r\n" + "\r\n" + "file://samsung_content.con\r\n" + "\r\n" + "\r\n" + "\r\n" + "\r\n"; + + static const char mta_tail[] = + "\r\n" + "\r\n" + "\r\n"; + + static const char mta_chapter_header[] = + "\r\n" + "\r\n" + ""; + + static const char mta_chapter_tail[] = + "\r\n" + "\r\n" + "\r\n" + "\r\n" + "\r\n" + "\r\n"; + + /* 128x72 Black JPEG base 64 encoded */ + static const char bjpg64[] = + "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQECAgMCAgICAgQDAwIDBQQFBQUEBAQFB" + "gcGBQUHBgQEBgkGBwgICAgIBQYJCgkICgcICAj/2wBDAQEBAQICAgQCAgQIBQQFCAgICAgICAgICAgICAg" + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAj/wAARCABIAIADASIAAhEBAxEB/8QAHwAAA" + "QUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1F" + "hByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZ" + "WZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NX" + "W19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAt" + "REAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8Rc" + "YGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXm" + "JmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAM" + "BAAIRAxEAPwD/AD/6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKK" + "KKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKK" + "ACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP//Z"; + +#ifdef ENABLE_VIDEO_THUMB + AVFormatContext *fctx = NULL; + unsigned char *jpeg; + int jpegsize; +#endif + image_s img; + FILE *fp = NULL; + char *mta_path; + char *img64 = NULL; + size_t sizei64; + int i, res, ret = -1; + int per[] = { 15, 35, 50, 65, 85 }; + clock_t start, end; + + if (!moviefname) + return 0; + + if (duration <= 0) + return 0; + + if (!is_video(moviefname)) + return 0; + + memset(&img, 0, sizeof(image_s)); + + start = clock(); + + if(art_cache_exists(NULL, ".mta", moviefname, &mta_path)) + { + DPRINTF(E_INFO, L_METADATA, "The MTA file (%s) already exists.", mta_path); + return mta_path; + } + + if ( make_dir(dirname(mta_path), S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) ) + goto mta_error; + + if ( !(fp = fopen(mta_path, "w")) ) + goto mta_error; + + +#ifdef ENABLE_VIDEO_THUMB + if(!allblack) + lav_open(&fctx, moviefname); +#endif + + res = fwrite(mta_header, 1, sizeof(mta_header) -1, fp); + if(res != sizeof(mta_header) - 1) + goto mta_error; + + for (i = 0; i < sizeof(per)/sizeof(int); i++) + { + res = fwrite(mta_chapter_header, 1, sizeof(mta_chapter_header) - 1, fp); + if(res != sizeof(mta_chapter_header) - 1) + goto mta_error; + +#ifdef ENABLE_VIDEO_THUMB + if ( !allblack && fctx) + { + res = video_thumb_generate_ctx_tobuff(fctx, &img, per[i], MTA_WIDTH); + if (img.buf) + { + jpeg = image_save_to_jpeg_buf(&img, &jpegsize); + if(jpeg) + { + img64 = base64_encode(jpeg, jpegsize, &sizei64); + free(jpeg); + jpeg = NULL; + } + } + } +#endif + + if (!allblack && img64) + { + res = fwrite(img64, 1, sizei64, fp); + free(img64); + if(res != sizei64) + goto mta_error; + } + else + { + res = fwrite(bjpg64, 1, sizeof(bjpg64) -1, fp); + if(res != sizeof(bjpg64) -1) + goto mta_error; + } + fprintf(fp, mta_chapter_tail, (duration * per[i])/100); + } + res = fwrite(mta_tail, 1, sizeof(mta_tail) - 1, fp); + if( res != sizeof(mta_tail) -1 ) + goto mta_error; + + ret = 0; + + end = clock(); + DPRINTF(E_DEBUG, L_METADATA, "Generated MTA file for (%s) in %ldms.\n", moviefname, (end-start) * 1000 / CLOCKS_PER_SEC); + +mta_error: + free(img.buf); +#ifdef ENABLE_VIDEO_THUMB + if (fctx) + lav_close(fctx); +#endif + fclose(fp); + if (ret) + { + remove(mta_path); + free(mta_path); + mta_path = NULL; + } + + return mta_path; +} diff --git a/video_thumb.h b/video_thumb.h new file mode 100644 index 0000000..1a14421 --- /dev/null +++ b/video_thumb.h @@ -0,0 +1,36 @@ +/* Video thumbnail functions + * + * Project : minidlna + * Website : http://sourceforge.net/projects/minidlna/ + * + * MiniDLNA media server + * Copyright (C) 2009 Justin Maggard + * + * This file is part of MiniDLNA. + * + * MiniDLNA is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * MiniDLNA 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 MiniDLNA. If not, see . + */ + +#ifndef __VIDEOTHUMB_H__ +#define __VIDEOTHUMB_H__ + +int +video_thumb_generate_tofile(const char *moviefname, const char* thumbfname, int seek, int width); + +int +video_thumb_generate_tobuff(const char *moviefname, void* imgbuffer, int seek, int width); + +char* +video_thumb_generate_mta_file(const char *moviefname, int duration, int allblack); + +#endif