/* MiniDLNA media server * Copyright (C) 2008-2009 Justin Maggard * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include #include #include #include #include #include #include #include "image_utils.h" #include #include #include #include #include #include "tagutils/tagutils.h" #include "upnpglobalvars.h" #include "upnpreplyparse.h" #include "metadata.h" #include "albumart.h" #include "utils.h" #include "sql.h" #include "log.h" #define FLAG_TITLE 0x00000001 #define FLAG_ARTIST 0x00000002 #define FLAG_ALBUM 0x00000004 #define FLAG_GENRE 0x00000008 #define FLAG_COMMENT 0x00000010 #define FLAG_CREATOR 0x00000020 #define FLAG_DATE 0x00000040 #define FLAG_DLNA_PN 0x00000080 #define FLAG_MIME 0x00000100 #define FLAG_DURATION 0x00000200 #define FLAG_RESOLUTION 0x00000400 #define FLAG_BITRATE 0x00000800 #define FLAG_FREQUENCY 0x00001000 #define FLAG_BPS 0x00002000 #define FLAG_CHANNELS 0x00004000 /* Audio profile flags */ enum audio_profiles { PROFILE_AUDIO_UNKNOWN, PROFILE_AUDIO_MP3, PROFILE_AUDIO_AC3, PROFILE_AUDIO_WMA_BASE, PROFILE_AUDIO_WMA_FULL, PROFILE_AUDIO_WMA_PRO, PROFILE_AUDIO_MP2, PROFILE_AUDIO_PCM, PROFILE_AUDIO_AAC, PROFILE_AUDIO_AAC_MULT5, PROFILE_AUDIO_AMR }; /* This function shamelessly copied from libdlna */ #define MPEG_TS_SYNC_CODE 0x47 #define MPEG_TS_PACKET_LENGTH_DLNA 192 /* prepends 4 bytes to TS packet */ int dlna_timestamp_is_present(const char * filename) { unsigned char buffer[2*MPEG_TS_PACKET_LENGTH_DLNA+1]; int fd, i; /* read file header */ fd = open(filename, O_RDONLY); read(fd, buffer, MPEG_TS_PACKET_LENGTH_DLNA*2); close(fd); for( i=0; i < MPEG_TS_PACKET_LENGTH_DLNA; i++ ) { if( buffer[i] == MPEG_TS_SYNC_CODE ) { if (buffer[i + MPEG_TS_PACKET_LENGTH_DLNA] == MPEG_TS_SYNC_CODE) { if (buffer[i] == 0x00 && buffer [i+1] == 0x00 && buffer [i+2] == 0x00 && buffer [i+3] == 0x00) { break; } else { return 1; } } } } return 0; } #ifdef TIVO_SUPPORT int is_tivo_file(const char * path) { unsigned char buf[5]; unsigned char hdr[5] = { 'T','i','V','o','\0' }; int fd; /* read file header */ fd = open(path, O_RDONLY); read(fd, buf, 5); close(fd); return( !memcmp(buf, hdr, 5) ); } #endif /* This function taken from libavutil (ffmpeg), because it's not included with all versions of libavutil. */ int get_fourcc(const char *s) { return (s[0]) + (s[1]<<8) + (s[2]<<16) + (s[3]<<24); } void check_for_captions(const char * path, sqlite_int64 detailID) { char * sql; char * file = malloc(PATH_MAX); char **result; int ret, rows; sprintf(file, "%s", path); strip_ext(file); /* If we weren't given a detail ID, look for one. */ if( !detailID ) { sql = sqlite3_mprintf("SELECT ID from DETAILS where PATH glob '%q.*'" " and MIME glob 'video/*' limit 1", file); ret = sql_get_table(db, sql, &result, &rows, NULL); if( ret == SQLITE_OK ) { if( rows ) { detailID = strtoll(result[1], NULL, 10); //DEBUG DPRINTF(E_DEBUG, L_METADATA, "New file %s looks like a caption file.\n", path); } /*else { DPRINTF(E_DEBUG, L_METADATA, "No file found for caption %s.\n", path); }*/ sqlite3_free_table(result); } sqlite3_free(sql); if( !detailID ) goto no_source_video; } strcat(file, ".srt"); if( access(file, R_OK) == 0 ) { sql_exec(db, "INSERT into CAPTIONS" " (ID, PATH) " "VALUES" " (%lld, %Q)", detailID, file); } no_source_video: free(file); } void parse_nfo(const char * path, metadata_t * m) { FILE *nfo; char buf[65536]; struct NameValueParserData xml; struct stat file; size_t nread; char *val, *val2; if( stat(path, &file) != 0 || file.st_size > 65536 ) { DPRINTF(E_INFO, L_METADATA, "Not parsing very large .nfo file %s\n", path); return; } DPRINTF(E_DEBUG, L_METADATA, "Parsing .nfo file: %s\n", path); nfo = fopen(path, "r"); if( !nfo ) return; nread = fread(&buf, 1, sizeof(buf), nfo); ParseNameValue(buf, nread, &xml); //printf("\ttype: %s\n", GetValueFromNameValueList(&xml, "rootElement")); val = GetValueFromNameValueList(&xml, "title"); if( val ) { val2 = GetValueFromNameValueList(&xml, "episodetitle"); if( val2 ) asprintf(&m->title, "%s - %s", val, val2); else m->title = strdup(val); } val = GetValueFromNameValueList(&xml, "plot"); if( val ) m->comment = strdup(val); val = GetValueFromNameValueList(&xml, "capturedate"); if( val ) m->date = strdup(val); ClearNameValueList(&xml); fclose(nfo); } void free_metadata(metadata_t * m, uint32_t flags) { if( m->title && (flags & FLAG_TITLE) ) free(m->title); if( m->artist && (flags & FLAG_ARTIST) ) free(m->artist); if( m->album && (flags & FLAG_ALBUM) ) free(m->album); if( m->genre && (flags & FLAG_GENRE) ) free(m->genre); if( m->creator && (flags & FLAG_CREATOR) ) free(m->creator); if( m->date && (flags & FLAG_DATE) ) free(m->date); if( m->comment && (flags & FLAG_COMMENT) ) free(m->comment); if( m->dlna_pn && (flags & FLAG_DLNA_PN) ) free(m->dlna_pn); if( m->mime && (flags & FLAG_MIME) ) free(m->mime); if( m->duration && (flags & FLAG_DURATION) ) free(m->duration); if( m->resolution && (flags & FLAG_RESOLUTION) ) free(m->resolution); if( m->bitrate && (flags & FLAG_BITRATE) ) free(m->bitrate); if( m->frequency && (flags & FLAG_FREQUENCY) ) free(m->frequency); if( m->bps && (flags & FLAG_BPS) ) free(m->bps); if( m->channels && (flags & FLAG_CHANNELS) ) free(m->channels); } sqlite_int64 GetFolderMetadata(const char * name, const char * path, const char * artist, const char * genre, const char * album_art) { int ret; ret = sql_exec(db, "INSERT into DETAILS" " (TITLE, PATH, CREATOR, ARTIST, GENRE, ALBUM_ART) " "VALUES" " ('%q', %Q, %Q, %Q, %Q, %lld);", name, path, artist, artist, genre, album_art ? strtoll(album_art, NULL, 10) : 0); if( ret != SQLITE_OK ) ret = 0; else ret = sqlite3_last_insert_rowid(db); return ret; } sqlite_int64 GetAudioMetadata(const char * path, char * name) { char type[4]; static char lang[6] = { '\0' }; struct stat file; sqlite_int64 ret; char *esc_tag; int i; sqlite_int64 album_art = 0; struct song_metadata song; metadata_t m; uint32_t free_flags = FLAG_MIME|FLAG_DURATION|FLAG_DLNA_PN|FLAG_DATE; memset(&m, '\0', sizeof(metadata_t)); if ( stat(path, &file) != 0 ) return 0; strip_ext(name); if( ends_with(path, ".mp3") ) { strcpy(type, "mp3"); m.mime = strdup("audio/mpeg"); } else if( ends_with(path, ".m4a") || ends_with(path, ".mp4") || ends_with(path, ".aac") || ends_with(path, ".m4p") ) { strcpy(type, "aac"); m.mime = strdup("audio/mp4"); } else if( ends_with(path, ".3gp") ) { strcpy(type, "aac"); m.mime = strdup("audio/3gpp"); } else if( ends_with(path, ".wma") || ends_with(path, ".asf") ) { strcpy(type, "asf"); m.mime = strdup("audio/x-ms-wma"); } else if( ends_with(path, ".flac") || ends_with(path, ".fla") || ends_with(path, ".flc") ) { strcpy(type, "flc"); m.mime = strdup("audio/x-flac"); } else if( ends_with(path, ".wav") ) { strcpy(type, "wav"); m.mime = strdup("audio/x-wav"); } else if( ends_with(path, ".ogg") ) { strcpy(type, "ogg"); m.mime = strdup("application/ogg"); } else if( ends_with(path, ".pcm") ) { strcpy(type, "pcm"); m.mime = strdup("audio/L16"); } else { DPRINTF(E_WARN, L_GENERAL, "Unhandled file extension on %s\n", path); return 0; } if( !(*lang) ) { if( !getenv("LANG") ) strcpy(lang, "en_US"); else strncpy(lang, getenv("LANG"), 5); lang[5] = '\0'; } if( readtags((char *)path, &song, &file, lang, type) != 0 ) { DPRINTF(E_WARN, L_GENERAL, "Cannot extract tags from %s!\n", path); freetags(&song); free_metadata(&m, free_flags); return 0; } if( song.dlna_pn ) asprintf(&m.dlna_pn, "%s;DLNA.ORG_OP=01", song.dlna_pn); if( song.year ) asprintf(&m.date, "%04d-01-01", song.year); asprintf(&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.title = song.title; if( m.title && *m.title ) { m.title = trim(m.title); if( (esc_tag = escape_tag(m.title)) ) { free_flags |= FLAG_TITLE; m.title = esc_tag; } } else { m.title = name; } for( i=ROLE_START; i 48 ) { free_flags |= FLAG_ARTIST; m.artist = strdup("Various Artists"); } else if( (esc_tag = escape_tag(m.artist)) ) { free_flags |= FLAG_ARTIST; m.artist = esc_tag; } m.creator = m.artist; break; } } /* If there is a band associated with the album, use it for virtual containers. */ if( (i != ROLE_BAND) && song.contributor[ROLE_BAND] && *song.contributor[ROLE_BAND] ) { m.creator = trim(song.contributor[ROLE_BAND]); if( strlen(m.creator) > 48 ) { free_flags |= FLAG_CREATOR; m.creator = strdup("Various Artists"); } else if( (esc_tag = escape_tag(m.creator)) ) { free_flags |= FLAG_CREATOR; m.creator = esc_tag; } } if( song.album && *song.album ) { m.album = trim(song.album); if( (esc_tag = escape_tag(m.album)) ) { free_flags |= FLAG_ALBUM; m.album = esc_tag; } } if( song.genre && *song.genre ) { m.genre = trim(song.genre); if( (esc_tag = escape_tag(m.genre)) ) { free_flags |= FLAG_GENRE; m.genre = esc_tag; } } if( song.comment && *song.comment ) { m.comment = trim(song.comment); if( (esc_tag = escape_tag(m.comment)) ) { free_flags |= FLAG_COMMENT; m.comment = esc_tag; } } album_art = find_album_art(path, song.image, song.image_size); ret = sql_exec(db, "INSERT into DETAILS" " (PATH, SIZE, TIMESTAMP, DURATION, CHANNELS, BITRATE, SAMPLERATE, DATE," " TITLE, CREATOR, ARTIST, ALBUM, GENRE, COMMENT, DISC, TRACK, DLNA_PN, MIME, ALBUM_ART) " "VALUES" " (%Q, %lld, %ld, '%s', %d, %d, %d, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %d, %d, %Q, '%s', %lld);", path, file.st_size, file.st_mtime, m.duration, song.channels, song.bitrate, song.samplerate, m.date, m.title, m.creator, m.artist, m.album, m.genre, m.comment, song.disc, song.track, m.dlna_pn, song.mime?song.mime:m.mime, album_art); if( ret != SQLITE_OK ) { fprintf(stderr, "Error inserting details for '%s'!\n", path); ret = 0; } else { ret = sqlite3_last_insert_rowid(db); } freetags(&song); free_metadata(&m, free_flags); return ret; } /* For libjpeg error handling */ jmp_buf setjmp_buffer; static void libjpeg_error_handler(j_common_ptr cinfo) { cinfo->err->output_message (cinfo); longjmp(setjmp_buffer, 1); return; } sqlite_int64 GetImageMetadata(const char * path, char * name) { ExifData *ed; ExifEntry *e = NULL; ExifLoader *l; struct jpeg_decompress_struct cinfo; struct jpeg_error_mgr jerr; FILE *infile; int width=0, height=0, thumb=0; char make[32], model[64] = {'\0'}; char b[1024]; struct stat file; sqlite_int64 ret; image * imsrc; metadata_t m; uint32_t free_flags = 0xFFFFFFFF; memset(&m, '\0', sizeof(metadata_t)); //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 */ asprintf(&m.mime, "image/jpeg"); l = exif_loader_new(); exif_loader_write_file(l, path); ed = exif_loader_get_data(l); exif_loader_unref(l); if( !ed ) goto no_exifdata; e = exif_content_get_entry (ed->ifd[EXIF_IFD_EXIF], EXIF_TAG_DATE_TIME_ORIGINAL); if( e || (e = exif_content_get_entry(ed->ifd[EXIF_IFD_EXIF], EXIF_TAG_DATE_TIME_DIGITIZED)) ) { m.date = strdup(exif_entry_get_value(e, b, sizeof(b))); if( strlen(m.date) > 10 ) { m.date[4] = '-'; m.date[7] = '-'; m.date[10] = 'T'; } else { free(m.date); m.date = NULL; } } else { /* One last effort to get the date from XMP */ image_get_jpeg_date_xmp(path, &m.date); } //DEBUG DPRINTF(E_DEBUG, L_METADATA, " * date: %s\n", m.date); e = exif_content_get_entry (ed->ifd[EXIF_IFD_0], EXIF_TAG_MAKE); if( e ) { strncpy(make, exif_entry_get_value(e, b, sizeof(b)), sizeof(make)); e = exif_content_get_entry (ed->ifd[EXIF_IFD_0], EXIF_TAG_MODEL); if( e ) { strncpy(model, exif_entry_get_value(e, b, sizeof(b)), sizeof(model)); if( !strcasestr(model, make) ) snprintf(model, sizeof(model), "%s %s", make, exif_entry_get_value(e, b, sizeof(b))); m.creator = strdup(model); } } //DEBUG DPRINTF(E_DEBUG, L_METADATA, " * model: %s\n", model); if( ed->size ) { /* We might need to verify that the thumbnail is 160x160 or smaller */ if( ed->size > 12000 ) { imsrc = image_new_from_jpeg(NULL, 0, (char *)ed->data, ed->size, 1); if( imsrc ) { if( (imsrc->width <= 160) && (imsrc->height <= 160) ) thumb = 1; image_free(imsrc); } } else thumb = 1; } //DEBUG DPRINTF(E_DEBUG, L_METADATA, " * thumbnail: %d\n", thumb); exif_data_unref(ed); no_exifdata: /* If SOF parsing fails, then fall through to reading the JPEG data with libjpeg to get the resolution */ if( image_get_jpeg_resolution(path, &width, &height) != 0 || !width || !height ) { infile = fopen(path, "r"); cinfo.err = jpeg_std_error(&jerr); jerr.error_exit = libjpeg_error_handler; jpeg_create_decompress(&cinfo); if( setjmp(setjmp_buffer) ) goto error; jpeg_stdio_src(&cinfo, infile); jpeg_read_header(&cinfo, TRUE); jpeg_start_decompress(&cinfo); width = cinfo.output_width; height = cinfo.output_height; error: jpeg_destroy_decompress(&cinfo); fclose(infile); } //DEBUG DPRINTF(E_DEBUG, L_METADATA, " * resolution: %dx%d\n", width, height); if( !width || !height ) { free_metadata(&m, free_flags); return 0; } if( width <= 640 && height <= 480 ) asprintf(&m.dlna_pn, "JPEG_SM;%s", dlna_no_conv); else if( width <= 1024 && height <= 768 ) asprintf(&m.dlna_pn, "JPEG_MED;%s", dlna_no_conv); else if( (width <= 4096 && height <= 4096) || !(GETFLAG(DLNA_STRICT_MASK)) ) asprintf(&m.dlna_pn, "JPEG_LRG;%s", dlna_no_conv); asprintf(&m.resolution, "%dx%d", width, height); ret = sql_exec(db, "INSERT into DETAILS" " (PATH, TITLE, SIZE, TIMESTAMP, DATE, RESOLUTION, THUMBNAIL, CREATOR, DLNA_PN, MIME) " "VALUES" " (%Q, '%q', %lld, %ld, %Q, %Q, %d, %Q, %Q, %Q);", path, name, file.st_size, file.st_mtime, m.date, m.resolution, thumb, m.creator, m.dlna_pn, m.mime); if( ret != SQLITE_OK ) { fprintf(stderr, "Error inserting details for '%s'!\n", path); ret = 0; } else { ret = sqlite3_last_insert_rowid(db); } free_metadata(&m, free_flags); return ret; } sqlite_int64 GetVideoMetadata(const char * path, char * name) { struct stat file; int ret, i; struct tm *modtime; AVFormatContext *ctx; int audio_stream = -1, video_stream = -1; enum audio_profiles audio_profile = PROFILE_AUDIO_UNKNOWN; ts_timestamp_t ts_timestamp = NONE; int duration, hours, min, sec, ms; aac_object_type_t aac_type = AAC_INVALID; sqlite_int64 album_art = 0; char nfo[PATH_MAX], *ext; metadata_t m; uint32_t free_flags = 0xFFFFFFFF; memset(&m, '\0', sizeof(m)); // memset(&free_flags, 0xFF, sizeof(free_flags)); //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); av_register_all(); if( av_open_input_file(&ctx, path, NULL, 0, NULL) != 0 ) { DPRINTF(E_WARN, L_METADATA, "Opening %s failed!\n", path); return 0; } av_find_stream_info(ctx); //dump_format(ctx, 0, NULL, 0); for( i=0; inb_streams; i++) { if( audio_stream == -1 && ctx->streams[i]->codec->codec_type == CODEC_TYPE_AUDIO ) { audio_stream = i; continue; } else if( video_stream == -1 && ctx->streams[i]->codec->codec_type == CODEC_TYPE_VIDEO ) { video_stream = i; continue; } } /* This must not be a video file. */ if( video_stream == -1 ) { av_close_input_file(ctx); if( !is_audio(path) ) DPRINTF(E_DEBUG, L_METADATA, "File %s does not contain a video stream.\n", basename(path)); return 0; } strcpy(nfo, path); ext = strrchr(nfo, '.'); if( ext ) { strcpy(ext+1, "nfo"); if( access(nfo, F_OK) == 0 ) { parse_nfo(nfo, &m); } } if( !m.date ) { m.date = malloc(20); modtime = localtime(&file.st_mtime); strftime(m.date, 20, "%FT%T", modtime); } if( audio_stream >= 0 ) { switch( ctx->streams[audio_stream]->codec->codec_id ) { case CODEC_ID_MP3: audio_profile = PROFILE_AUDIO_MP3; break; case CODEC_ID_AAC: if( !ctx->streams[audio_stream]->codec->extradata_size || !ctx->streams[audio_stream]->codec->extradata ) { DPRINTF(E_DEBUG, L_METADATA, "No AAC type\n"); } else { uint8_t data; memcpy(&data, ctx->streams[audio_stream]->codec->extradata, 1); aac_type = data >> 3; } switch( aac_type ) { /* AAC Low Complexity variants */ case AAC_LC: case AAC_LC_ER: if( ctx->streams[audio_stream]->codec->sample_rate < 8000 || ctx->streams[audio_stream]->codec->sample_rate > 48000 ) { DPRINTF(E_DEBUG, L_METADATA, "Unsupported AAC: sample rate is not 8000 < %d < 48000\n", ctx->streams[audio_stream]->codec->sample_rate); break; } /* AAC @ Level 1/2 */ if( ctx->streams[audio_stream]->codec->channels <= 2 && ctx->streams[audio_stream]->codec->bit_rate <= 576000 ) audio_profile = PROFILE_AUDIO_AAC; else if( ctx->streams[audio_stream]->codec->channels <= 6 && ctx->streams[audio_stream]->codec->bit_rate <= 1440000 ) audio_profile = PROFILE_AUDIO_AAC_MULT5; else DPRINTF(E_DEBUG, L_METADATA, "Unhandled AAC: %d channels, %d bitrate\n", ctx->streams[audio_stream]->codec->channels, ctx->streams[audio_stream]->codec->bit_rate); break; default: DPRINTF(E_DEBUG, L_METADATA, "Unhandled AAC type [%d]\n", aac_type); break; } break; case CODEC_ID_AC3: case CODEC_ID_DTS: audio_profile = PROFILE_AUDIO_AC3; break; case CODEC_ID_WMAV1: case CODEC_ID_WMAV2: /* WMA Baseline: stereo, up to 48 KHz, up to 192,999 bps */ if ( ctx->streams[audio_stream]->codec->bit_rate <= 193000 ) audio_profile = PROFILE_AUDIO_WMA_BASE; /* WMA Full: stereo, up to 48 KHz, up to 385 Kbps */ else if ( ctx->streams[audio_stream]->codec->bit_rate <= 385000 ) audio_profile = PROFILE_AUDIO_WMA_FULL; break; #if LIBAVCODEC_VERSION_INT > ((51<<16)+(50<<8)+1) case CODEC_ID_WMAPRO: audio_profile = PROFILE_AUDIO_WMA_PRO; break; #endif case CODEC_ID_MP2: audio_profile = PROFILE_AUDIO_MP2; break; case CODEC_ID_AMR_NB: audio_profile = PROFILE_AUDIO_AMR; break; default: if( (ctx->streams[audio_stream]->codec->codec_id >= CODEC_ID_PCM_S16LE) && (ctx->streams[audio_stream]->codec->codec_id < CODEC_ID_ADPCM_IMA_QT) ) audio_profile = PROFILE_AUDIO_PCM; else DPRINTF(E_DEBUG, L_METADATA, "Unhandled audio codec [0x%X]\n", ctx->streams[audio_stream]->codec->codec_id); break; } asprintf(&m.frequency, "%u", ctx->streams[audio_stream]->codec->sample_rate); #if LIBAVCODEC_VERSION_INT < ((52<<16)+(0<<8)+0) asprintf(&m.bps, "%u", ctx->streams[audio_stream]->codec->bits_per_sample); #else asprintf(&m.bps, "%u", ctx->streams[audio_stream]->codec->bits_per_coded_sample); #endif asprintf(&m.channels, "%u", ctx->streams[audio_stream]->codec->channels); } if( video_stream >= 0 ) { DPRINTF(E_DEBUG, L_METADATA, "Container: '%s' [%s]\n", ctx->iformat->name, basename(path)); asprintf(&m.resolution, "%dx%d", ctx->streams[video_stream]->codec->width, ctx->streams[video_stream]->codec->height); if( ctx->bit_rate > 8 ) asprintf(&m.bitrate, "%u", 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); asprintf(&m.duration, "%d:%02d:%02d.%03d", hours, min, sec, ms); } /* NOTE: The DLNA spec only provides for ASF (WMV), TS, PS, and MP4 containers -- not AVI. */ switch( ctx->streams[video_stream]->codec->codec_id ) { case CODEC_ID_MPEG1VIDEO: if( strcmp(ctx->iformat->name, "mpeg") == 0 ) { if( (ctx->streams[video_stream]->codec->width == 352) && (ctx->streams[video_stream]->codec->height <= 288) ) { asprintf(&m.dlna_pn, "MPEG1;%s", dlna_no_conv); } asprintf(&m.mime, "video/mpeg"); } break; case CODEC_ID_MPEG2VIDEO: if( strcmp(ctx->iformat->name, "mpegts") == 0 ) { DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s is %s MPEG2 TS\n", video_stream, basename(path), m.resolution); char res; tsinfo_t * ts = ctx->priv_data; if( ts->packet_size == 192 ) { asprintf(&m.dlna_pn, "MPEG_TS_HD_NA;%s", dlna_no_conv); asprintf(&m.mime, "video/vnd.dlna.mpeg-tts"); } else if( ts->packet_size == 188 ) { if( (ctx->streams[video_stream]->codec->width >= 1280) && (ctx->streams[video_stream]->codec->height >= 720) ) res = 'H'; else res = 'S'; asprintf(&m.dlna_pn, "MPEG_TS_%cD_NA_ISO;%s", res, dlna_no_conv); asprintf(&m.mime, "video/mpeg"); } } else if( strcmp(ctx->iformat->name, "mpeg") == 0 ) { DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s is %s MPEG2 PS\n", video_stream, basename(path), m.resolution); char region[5]; if( (ctx->streams[video_stream]->codec->height == 576) || (ctx->streams[video_stream]->codec->height == 288) ) strcpy(region, "PAL"); else strcpy(region, "NTSC"); asprintf(&m.dlna_pn, "MPEG_PS_%s;%s", region, dlna_no_conv); asprintf(&m.mime, "video/mpeg"); } else { DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s [UNKNOWN CONTAINER] is %s MPEG2\n", video_stream, basename(path), m.resolution); } break; case CODEC_ID_H264: if( strcmp(ctx->iformat->name, "mpegts") == 0 ) { tsinfo_t * ts = ctx->priv_data; if( ts->packet_size == 192 ) { if( dlna_timestamp_is_present(path) ) ts_timestamp = VALID; else ts_timestamp = EMPTY; } char res = '\0'; if( ctx->streams[video_stream]->codec->width <= 720 && ctx->streams[video_stream]->codec->height <= 576 && ctx->streams[video_stream]->codec->bit_rate <= 10000000 ) res = 'S'; else if( ctx->streams[video_stream]->codec->width <= 1920 && ctx->streams[video_stream]->codec->height <= 1152 && ctx->streams[video_stream]->codec->bit_rate <= 20000000 ) res = 'H'; if( res ) { switch( audio_profile ) { case PROFILE_AUDIO_MP3: asprintf(&m.dlna_pn, "AVC_TS_MP_HD_MPEG1_L3%s;%s", ts_timestamp==NONE?"_ISO" : ts_timestamp==VALID?"_T":"", dlna_no_conv); break; case PROFILE_AUDIO_AC3: asprintf(&m.dlna_pn, "AVC_TS_MP_HD_AC3%s;%s", ts_timestamp==NONE?"_ISO" : ts_timestamp==VALID?"_T":"", dlna_no_conv); break; case PROFILE_AUDIO_AAC_MULT5: asprintf(&m.dlna_pn, "AVC_TS_MP_HD_AAC_MULT5%s;%s", ts_timestamp==NONE?"_ISO" : ts_timestamp==VALID?"_T":"", dlna_no_conv); break; default: DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for TS/AVC/%cD file %s\n", res, basename(path)); break; } if( m.dlna_pn && (ts_timestamp != NONE) ) asprintf(&m.mime, "video/vnd.dlna.mpeg-tts"); } else { DPRINTF(E_DEBUG, L_METADATA, "Unsupported h.264 video profile! [%dx%d, %dbps]\n", ctx->streams[video_stream]->codec->width, ctx->streams[video_stream]->codec->height, ctx->streams[video_stream]->codec->bit_rate); } } else if( strcmp(ctx->iformat->name, "mov,mp4,m4a,3gp,3g2,mj2") == 0 ) { /* AVC wrapped in MP4 only has SD profiles - 10 Mbps max */ if( ctx->streams[video_stream]->codec->width <= 720 && ctx->streams[video_stream]->codec->height <= 576 && ctx->streams[video_stream]->codec->bit_rate <= 10000000 && !ends_with(path, ".mov") ) { switch( audio_profile ) { case PROFILE_AUDIO_AC3: asprintf(&m.dlna_pn, "AVC_MP4_MP_SD_AC3;%s", dlna_no_conv); break; case PROFILE_AUDIO_AAC_MULT5: asprintf(&m.dlna_pn, "AVC_MP4_MP_SD_AAC_MULT5;%s", dlna_no_conv); break; default: DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for MP4/AVC/SD file %s\n", basename(path)); break; } } } DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s is h.264\n", video_stream, basename(path)); break; case CODEC_ID_MPEG4: if( ctx->streams[video_stream]->codec->codec_tag == get_fourcc("XVID") ) { DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s is %s XViD\n", video_stream, basename(path), m.resolution); asprintf(&m.artist, "DiVX"); } else if( ctx->streams[video_stream]->codec->codec_tag == get_fourcc("DX50") ) { DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s is %s DiVX5\n", video_stream, basename(path), m.resolution); asprintf(&m.artist, "DiVX"); } else if( ctx->streams[video_stream]->codec->codec_tag == get_fourcc("DIVX") ) { DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s is DiVX\n", video_stream, basename(path)); asprintf(&m.artist, "DiVX"); } else if( ends_with(path, ".3gp") && (strcmp(ctx->iformat->name, "mov,mp4,m4a,3gp,3g2,mj2") == 0) ) { asprintf(&m.mime, "video/3gpp"); switch( audio_profile ) { case PROFILE_AUDIO_AAC: asprintf(&m.dlna_pn, "MPEG4_P2_3GPP_SP_L0B_AAC;%s", dlna_no_conv); break; case PROFILE_AUDIO_AMR: asprintf(&m.dlna_pn, "MPEG4_P2_3GPP_SP_L0B_AMR;%s", dlna_no_conv); break; default: DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for MPEG4-P2 3GP/0x%X file %s\n", ctx->streams[audio_stream]->codec->codec_id, basename(path)); break; } } else { DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s is MPEG4 [%X]\n", video_stream, basename(path), ctx->streams[video_stream]->codec->codec_tag); } break; case CODEC_ID_WMV3: case CODEC_ID_VC1: DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s is VC1\n", video_stream, basename(path)); char profile[5]; profile[0] = '\0'; asprintf(&m.mime, "video/x-ms-wmv"); if( (ctx->streams[video_stream]->codec->width <= 352) && (ctx->streams[video_stream]->codec->height <= 288) && (ctx->bit_rate/8 <= 384000) ) { switch( audio_profile ) { case PROFILE_AUDIO_MP3: asprintf(&m.dlna_pn, "WMVSPML_MP3;%s", dlna_no_conv); break; case PROFILE_AUDIO_WMA_BASE: asprintf(&m.dlna_pn, "WMVSPML_BASE;%s", dlna_no_conv); break; default: DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for WMVSPML/0x%X file %s\n", audio_profile, basename(path)); break; } } else if( (ctx->streams[video_stream]->codec->width <= 720) && (ctx->streams[video_stream]->codec->height <= 576) && (ctx->bit_rate/8 <= 10000000) ) { switch( audio_profile ) { case PROFILE_AUDIO_WMA_PRO: asprintf(&m.dlna_pn, "WMVMED_PRO;%s", dlna_no_conv); break; case PROFILE_AUDIO_WMA_FULL: asprintf(&m.dlna_pn, "WMVMED_FULL;%s", dlna_no_conv); break; case PROFILE_AUDIO_WMA_BASE: asprintf(&m.dlna_pn, "WMVMED_BASE;%s", dlna_no_conv); break; default: DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for WMVMED/0x%X file %s\n", audio_profile, basename(path)); break; } } else if( (ctx->streams[video_stream]->codec->width <= 1920) && (ctx->streams[video_stream]->codec->height <= 1080) && (ctx->bit_rate/8 <= 20000000) ) { switch( audio_profile ) { case PROFILE_AUDIO_WMA_PRO: asprintf(&m.dlna_pn, "WMVHIGH_PRO;%s", dlna_no_conv); break; case PROFILE_AUDIO_WMA_FULL: asprintf(&m.dlna_pn, "WMVHIGH_FULL;%s", dlna_no_conv); break; default: DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for WMVHIGH/0x%X file %s\n", audio_profile, basename(path)); break; } } break; case CODEC_ID_MSMPEG4V3: asprintf(&m.mime, "video/x-msvideo"); default: DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s is %s [type %d]\n", video_stream, basename(path), m.resolution, ctx->streams[video_stream]->codec->codec_id); break; } } if( !m.mime ) { if( strcmp(ctx->iformat->name, "avi") == 0 ) asprintf(&m.mime, "video/x-msvideo"); else if( strcmp(ctx->iformat->name, "mpegts") == 0 ) asprintf(&m.mime, "video/mpeg"); else if( strcmp(ctx->iformat->name, "mpeg") == 0 ) asprintf(&m.mime, "video/mpeg"); else if( strcmp(ctx->iformat->name, "asf") == 0 ) asprintf(&m.mime, "video/x-ms-wmv"); else if( strcmp(ctx->iformat->name, "mov,mp4,m4a,3gp,3g2,mj2") == 0 ) if( ends_with(path, ".mov") ) asprintf(&m.mime, "video/quicktime"); else asprintf(&m.mime, "video/mp4"); else if( strcmp(ctx->iformat->name, "matroska") == 0 ) asprintf(&m.mime, "video/x-matroska"); else if( strcmp(ctx->iformat->name, "flv") == 0 ) asprintf(&m.mime, "video/x-flv"); else DPRINTF(E_WARN, L_METADATA, "%s: Unhandled format: %s\n", path, ctx->iformat->name); } av_close_input_file(ctx); #ifdef TIVO_SUPPORT if( ends_with(path, ".TiVo") && is_tivo_file(path) ) { free(m.mime); asprintf(&m.mime, "video/x-tivo-mpeg"); } #endif album_art = find_album_art(path, NULL, 0); ret = sql_exec(db, "INSERT into DETAILS" " (PATH, SIZE, TIMESTAMP, DURATION, DATE, CHANNELS, BITRATE, SAMPLERATE, RESOLUTION," " CREATOR, TITLE, COMMENT, DLNA_PN, MIME, ALBUM_ART) " "VALUES" " (%Q, %lld, %ld, %Q, %Q, %Q, %Q, %Q, %Q, %Q, '%q', %Q, %Q, '%q', %lld);", path, file.st_size, file.st_mtime, m.duration, m.date, m.channels, m.bitrate, m.frequency, m.resolution, m.artist, m.title ? m.title : name, m.comment, m.dlna_pn, m.mime, album_art); if( ret != SQLITE_OK ) { fprintf(stderr, "Error inserting details for '%s'!\n", path); ret = 0; } else { ret = sqlite3_last_insert_rowid(db); check_for_captions(path, ret); } free_metadata(&m, free_flags); return ret; }