916 lines
26 KiB
C
916 lines
26 KiB
C
/* 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 <stdio.h>
|
|
#include <ctype.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <sys/stat.h>
|
|
|
|
#include <unistd.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <fcntl.h>
|
|
|
|
#include <libexif/exif-loader.h>
|
|
#include <jpeglib.h>
|
|
#include <setjmp.h>
|
|
#include <avutil.h>
|
|
#include <avcodec.h>
|
|
#include <avformat.h>
|
|
#include "tagutils/tagutils.h"
|
|
|
|
#include "upnpglobalvars.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
|
|
|
|
/* Audio profile flags */
|
|
#define MP3 0x00000001
|
|
#define AC3 0x00000002
|
|
#define WMA_BASE 0x00000004
|
|
#define WMA_FULL 0x00000008
|
|
#define WMA_PRO 0x00000010
|
|
#define MP2 0x00000020
|
|
#define PCM 0x00000040
|
|
#define AAC 0x00000100
|
|
#define AAC_MULT5 0x00000200
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
char *
|
|
escape_tag(const char *tag)
|
|
{
|
|
char *esc_tag = NULL;
|
|
|
|
if( index(tag, '&') || index(tag, '<') || index(tag, '>') )
|
|
{
|
|
esc_tag = strdup(tag);
|
|
esc_tag = modifyString(esc_tag, "&", "&amp;", 0);
|
|
esc_tag = modifyString(esc_tag, "<", "&lt;", 0);
|
|
esc_tag = modifyString(esc_tag, ">", "&gt;", 0);
|
|
}
|
|
|
|
return esc_tag;
|
|
}
|
|
|
|
sqlite_int64
|
|
GetFolderMetadata(const char * name, const char * path, const char * artist, const char * genre, const char * album_art, const char * art_dlna_pn)
|
|
{
|
|
char * sql;
|
|
char * esc_name = NULL;
|
|
int ret;
|
|
|
|
esc_name = escape_tag(name);
|
|
sql = sqlite3_mprintf( "INSERT into DETAILS"
|
|
" (TITLE, PATH, CREATOR, ARTIST, GENRE, ALBUM_ART, ART_DLNA_PN) "
|
|
"VALUES"
|
|
" ('%q', %Q, %Q, %Q, %Q, %lld, %Q);",
|
|
esc_name ? esc_name : name,
|
|
path, artist, artist, genre,
|
|
album_art ? strtoll(album_art, NULL, 10) : 0,
|
|
art_dlna_pn);
|
|
if( sql_exec(db, sql) != SQLITE_OK )
|
|
ret = 0;
|
|
else
|
|
ret = sqlite3_last_insert_rowid(db);
|
|
if( esc_name )
|
|
free(esc_name);
|
|
sqlite3_free(sql);
|
|
|
|
return ret;
|
|
}
|
|
|
|
sqlite_int64
|
|
GetAudioMetadata(const char * path, char * name)
|
|
{
|
|
char duration[16], mime[16], type[4];
|
|
struct stat file;
|
|
sqlite_int64 ret;
|
|
char *sql;
|
|
char *title, *artist = NULL, *album = NULL, *genre = NULL, *comment = NULL, *date = NULL;
|
|
char *esc_tag;
|
|
int i, free_flags = 0;
|
|
sqlite_int64 album_art = 0;
|
|
char art_dlna_pn[9];
|
|
struct song_metadata song;
|
|
char *dlna_pn = NULL;
|
|
|
|
if ( stat(path, &file) != 0 )
|
|
return 0;
|
|
strip_ext(name);
|
|
|
|
if( ends_with(path, ".mp3") )
|
|
{
|
|
strcpy(type, "mp3");
|
|
strcpy(mime, "audio/mpeg");
|
|
}
|
|
else if( ends_with(path, ".m4a") || ends_with(path, ".mp4") ||
|
|
ends_with(path, ".aac") || ends_with(path, ".m4p") )
|
|
{
|
|
strcpy(type, "aac");
|
|
strcpy(mime, "audio/mp4");
|
|
}
|
|
else if( ends_with(path, ".wma") || ends_with(path, ".asf") )
|
|
{
|
|
strcpy(type, "asf");
|
|
strcpy(mime, "audio/x-ms-wma");
|
|
}
|
|
else if( ends_with(path, ".flac") || ends_with(path, ".fla") || ends_with(path, ".flc") )
|
|
{
|
|
strcpy(type, "flc");
|
|
strcpy(mime, "audio/x-flac");
|
|
}
|
|
else
|
|
{
|
|
DPRINTF(E_WARN, L_GENERAL, "Unhandled file extension on %s\n", path);
|
|
return 0;
|
|
}
|
|
|
|
if( readtags((char *)path, &song, &file, NULL, type) != 0 )
|
|
{
|
|
DPRINTF(E_WARN, L_GENERAL, "Cannot extract tags from %s!\n", path);
|
|
freetags(&song);
|
|
return 0;
|
|
}
|
|
|
|
if( song.dlna_pn )
|
|
asprintf(&dlna_pn, "%s;DLNA.ORG_OP=01", song.dlna_pn);
|
|
if( song.year )
|
|
asprintf(&date, "%04d-01-01", song.year);
|
|
sprintf(duration, "%d:%02d:%02d.%03d",
|
|
(song.song_length/3600000),
|
|
(song.song_length/60000),
|
|
(song.song_length/1000%60),
|
|
(song.song_length%1000));
|
|
title = song.title;
|
|
if( title )
|
|
{
|
|
title = trim(title);
|
|
if( (esc_tag = escape_tag(title)) )
|
|
{
|
|
free_flags |= FLAG_TITLE;
|
|
title = esc_tag;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
title = name;
|
|
}
|
|
for( i=ROLE_START; i<N_ROLE; i++ )
|
|
{
|
|
if( song.contributor[i] )
|
|
{
|
|
artist = trim(song.contributor[i]);
|
|
if( (esc_tag = escape_tag(artist)) )
|
|
{
|
|
free_flags |= FLAG_ARTIST;
|
|
artist = esc_tag;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if( song.album )
|
|
{
|
|
album = trim(song.album);
|
|
if( (esc_tag = escape_tag(album)) )
|
|
{
|
|
free_flags |= FLAG_ALBUM;
|
|
album = esc_tag;
|
|
}
|
|
}
|
|
if( song.genre )
|
|
{
|
|
genre = trim(song.genre);
|
|
if( (esc_tag = escape_tag(genre)) )
|
|
{
|
|
free_flags |= FLAG_GENRE;
|
|
genre = esc_tag;
|
|
}
|
|
}
|
|
if( song.comment )
|
|
{
|
|
comment = trim(song.comment);
|
|
if( (esc_tag = escape_tag(comment)) )
|
|
{
|
|
free_flags |= FLAG_COMMENT;
|
|
comment = esc_tag;
|
|
}
|
|
}
|
|
|
|
album_art = find_album_art(path, art_dlna_pn, song.image, song.image_size);
|
|
|
|
sql = sqlite3_mprintf( "INSERT into DETAILS"
|
|
" (PATH, SIZE, DURATION, CHANNELS, BITRATE, SAMPLERATE, DATE,"
|
|
" TITLE, CREATOR, ARTIST, ALBUM, GENRE, COMMENT, TRACK, DLNA_PN, MIME, ALBUM_ART, ART_DLNA_PN) "
|
|
"VALUES"
|
|
" (%Q, %d, '%s', %d, %d, %d, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %d, %Q, '%s', %lld, %Q);",
|
|
path, song.file_size, duration, song.channels, song.bitrate, song.samplerate, date,
|
|
title,
|
|
artist, artist,
|
|
album,
|
|
genre,
|
|
comment,
|
|
song.track,
|
|
dlna_pn, mime,
|
|
album_art, album_art?art_dlna_pn:NULL );
|
|
freetags(&song);
|
|
if( dlna_pn )
|
|
free(dlna_pn);
|
|
if( date )
|
|
free(date);
|
|
if( free_flags & FLAG_TITLE )
|
|
free(title);
|
|
if( free_flags & FLAG_ARTIST )
|
|
free(artist);
|
|
if( free_flags & FLAG_ALBUM )
|
|
free(album);
|
|
if( free_flags & FLAG_GENRE )
|
|
free(genre);
|
|
if( free_flags & FLAG_COMMENT )
|
|
free(comment);
|
|
|
|
//DEBUG DPRINTF(E_DEBUG, L_METADATA, "SQL: %s\n", sql);
|
|
if( sql_exec(db, sql) != SQLITE_OK )
|
|
{
|
|
fprintf(stderr, "Error inserting details for '%s'!\n", path);
|
|
ret = 0;
|
|
}
|
|
else
|
|
{
|
|
ret = sqlite3_last_insert_rowid(db);
|
|
}
|
|
sqlite3_free(sql);
|
|
|
|
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;
|
|
ExifTag tag;
|
|
struct jpeg_decompress_struct cinfo;
|
|
struct jpeg_error_mgr jerr;
|
|
FILE *infile;
|
|
int width=0, height=0, thumb=0;
|
|
off_t size;
|
|
char date[64], make[32], model[64];
|
|
char b[1024];
|
|
char *esc_name = NULL;
|
|
struct stat file;
|
|
sqlite_int64 ret;
|
|
char *sql;
|
|
metadata_t m;
|
|
memset(&m, '\0', sizeof(metadata_t));
|
|
|
|
date[0] = '\0';
|
|
model[0] = '\0';
|
|
|
|
//DEBUG DPRINTF(E_DEBUG, L_METADATA, "Parsing %s...\n", path);
|
|
if ( stat(path, &file) == 0 )
|
|
size = file.st_size;
|
|
else
|
|
return 0;
|
|
strip_ext(name);
|
|
esc_name = escape_tag(name);
|
|
//DEBUG DPRINTF(E_DEBUG, L_METADATA, " * size: %d\n", 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);
|
|
|
|
tag = EXIF_TAG_PIXEL_X_DIMENSION;
|
|
e = exif_content_get_entry(ed->ifd[EXIF_IFD_EXIF], tag);
|
|
if( e )
|
|
width = atoi( exif_entry_get_value(e, b, sizeof(b)) );
|
|
|
|
tag = EXIF_TAG_PIXEL_Y_DIMENSION;
|
|
e = exif_content_get_entry (ed->ifd[EXIF_IFD_EXIF], tag);
|
|
if( e )
|
|
height = atoi( exif_entry_get_value(e, b, sizeof(b)) );
|
|
//DEBUG DPRINTF(E_DEBUG, L_METADATA, " * resolution: %dx%d\n", width, height);
|
|
|
|
tag = EXIF_TAG_DATE_TIME_ORIGINAL;
|
|
e = exif_content_get_entry (ed->ifd[EXIF_IFD_EXIF], tag);
|
|
if( e || (e = exif_content_get_entry(ed->ifd[EXIF_IFD_0], EXIF_TAG_DATE_TIME)) ) {
|
|
strncpy(date, exif_entry_get_value(e, b, sizeof(b)), sizeof(date));
|
|
if( strlen(date) > 10 )
|
|
{
|
|
date[4] = '-';
|
|
date[7] = '-';
|
|
date[10] = 'T';
|
|
}
|
|
else {
|
|
strcpy(date, "0000-00-00");
|
|
}
|
|
}
|
|
else {
|
|
strcpy(date, "0000-00-00");
|
|
}
|
|
//DEBUG DPRINTF(E_DEBUG, L_METADATA, " * date: %s\n", date);
|
|
|
|
model[0] = '\0';
|
|
tag = EXIF_TAG_MAKE;
|
|
e = exif_content_get_entry (ed->ifd[EXIF_IFD_0], tag);
|
|
if( e )
|
|
{
|
|
strncpy(make, exif_entry_get_value(e, b, sizeof(b)), sizeof(make));
|
|
tag = EXIF_TAG_MODEL;
|
|
e = exif_content_get_entry (ed->ifd[EXIF_IFD_0], tag);
|
|
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)));
|
|
}
|
|
}
|
|
if( !strlen(model) )
|
|
strcpy(model, "Unknown");
|
|
//DEBUG DPRINTF(E_DEBUG, L_METADATA, " * model: %s\n", model);
|
|
|
|
if( ed->size )
|
|
thumb = 1;
|
|
else
|
|
thumb = 0;
|
|
//DEBUG DPRINTF(E_DEBUG, L_METADATA, " * thumbnail: %d\n", thumb);
|
|
|
|
exif_data_unref(ed);
|
|
|
|
/* If EXIF parsing fails, then fall through to reading the JPEG data with libjpeg to get the resolution */
|
|
if( !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);
|
|
}
|
|
|
|
if( !width || !height )
|
|
{
|
|
if( m.mime )
|
|
free(m.mime);
|
|
return 0;
|
|
}
|
|
if( width <= 640 && height <= 480 )
|
|
asprintf(&m.dlna_pn, "JPEG_SM;DLNA.ORG_OP=01;DLNA.ORG_CI=0");
|
|
else if( width <= 1024 && height <= 768 )
|
|
asprintf(&m.dlna_pn, "JPEG_MED;DLNA.ORG_OP=01;DLNA.ORG_CI=0");
|
|
else if( width <= 4096 && height <= 4096 )
|
|
asprintf(&m.dlna_pn, "JPEG_LRG;DLNA.ORG_OP=01;DLNA.ORG_CI=0");
|
|
else
|
|
asprintf(&m.dlna_pn, "JPEG_XL");
|
|
asprintf(&m.resolution, "%dx%d", width, height);
|
|
|
|
sql = sqlite3_mprintf( "INSERT into DETAILS"
|
|
" (PATH, TITLE, SIZE, DATE, RESOLUTION, THUMBNAIL, CREATOR, DLNA_PN, MIME) "
|
|
"VALUES"
|
|
" (%Q, '%q', %llu, '%s', %Q, %d, '%q', %Q, %Q);",
|
|
path, esc_name?esc_name:name, size, date, m.resolution, thumb, model, m.dlna_pn, m.mime);
|
|
//DEBUG DPRINTF(E_DEBUG, L_METADATA, "SQL: %s\n", sql);
|
|
if( sql_exec(db, sql) != SQLITE_OK )
|
|
{
|
|
fprintf(stderr, "Error inserting details for '%s'!\n", path);
|
|
ret = 0;
|
|
}
|
|
else
|
|
{
|
|
ret = sqlite3_last_insert_rowid(db);
|
|
}
|
|
sqlite3_free(sql);
|
|
if( m.resolution )
|
|
free(m.resolution);
|
|
if( m.dlna_pn )
|
|
free(m.dlna_pn);
|
|
if( m.mime )
|
|
free(m.mime);
|
|
if( esc_name )
|
|
free(esc_name);
|
|
return ret;
|
|
}
|
|
|
|
sqlite_int64
|
|
GetVideoMetadata(const char * path, char * name)
|
|
{
|
|
off_t size = 0;
|
|
struct stat file;
|
|
char *sql;
|
|
int ret, i;
|
|
struct tm *modtime;
|
|
char *esc_name = NULL;
|
|
char date[20];
|
|
AVFormatContext *ctx;
|
|
int audio_stream = -1, video_stream = -1;
|
|
int audio_profile = 0;
|
|
ts_timestamp_t ts_timestamp = NONE;
|
|
int duration, hours, min, sec, ms;
|
|
aac_object_type_t aac_type = AAC_INVALID;
|
|
metadata_t m;
|
|
memset(&m, '\0', sizeof(m));
|
|
date[0] = '\0';
|
|
|
|
DPRINTF(E_DEBUG, L_METADATA, "Parsing video %s...\n", path);
|
|
if ( stat(path, &file) == 0 )
|
|
{
|
|
modtime = localtime(&file.st_mtime);
|
|
strftime(date, sizeof(date), "%FT%T", modtime);
|
|
size = file.st_size;
|
|
}
|
|
strip_ext(name);
|
|
esc_name = escape_tag(name);
|
|
//DEBUG DPRINTF(E_DEBUG, L_METADATA, " * size: %d\n", 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; i<ctx->nb_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);
|
|
DPRINTF(E_WARN, L_METADATA, "File %s does not contain a video stream!\n", path);
|
|
return 0;
|
|
}
|
|
if( audio_stream >= 0 )
|
|
{
|
|
switch( ctx->streams[audio_stream]->codec->codec_id )
|
|
{
|
|
case CODEC_ID_MP3:
|
|
audio_profile = 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 = AAC;
|
|
else if( ctx->streams[audio_stream]->codec->channels <= 6 &&
|
|
ctx->streams[audio_stream]->codec->bit_rate <= 1440000 )
|
|
audio_profile = 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 = 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 = 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 = WMA_FULL;
|
|
break;
|
|
#if LIBAVCODEC_VERSION_INT > ((51<<16)+(50<<8)+1)
|
|
case CODEC_ID_WMAPRO:
|
|
audio_profile = WMA_PRO;
|
|
break;
|
|
#endif
|
|
case CODEC_ID_MP2:
|
|
audio_profile = MP2;
|
|
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 = 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, path);
|
|
asprintf(&m.resolution, "%dx%d", ctx->streams[video_stream]->codec->width, ctx->streams[video_stream]->codec->height);
|
|
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;DLNA.ORG_OP=01;DLNA.ORG_CI=0");
|
|
}
|
|
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, path, m.resolution);
|
|
char res;
|
|
tsinfo_t * ts = ctx->priv_data;
|
|
if( ts->packet_size == 192 )
|
|
{
|
|
asprintf(&m.dlna_pn, "MPEG_TS_HD_NA;DLNA.ORG_OP=01;DLNA.ORG_CI=0");
|
|
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;DLNA.ORG_OP=01;DLNA.ORG_CI=0", res);
|
|
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, 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;DLNA.ORG_OP=01;DLNA.ORG_CI=0", region);
|
|
asprintf(&m.mime, "video/mpeg");
|
|
}
|
|
else
|
|
{
|
|
DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s [UNKNOWN CONTAINER] is %s MPEG2\n", video_stream, 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 MP3:
|
|
asprintf(&m.dlna_pn, "AVC_TS_MP_HD_MPEG1_L3%s;DLNA.ORG_OP=01;DLNA.ORG_CI=0",
|
|
ts_timestamp==NONE?"_ISO" : ts_timestamp==VALID?"_T":"");
|
|
break;
|
|
case AC3:
|
|
asprintf(&m.dlna_pn, "AVC_TS_MP_HD_AC3%s;DLNA.ORG_OP=01;DLNA.ORG_CI=0",
|
|
ts_timestamp==NONE?"_ISO" : ts_timestamp==VALID?"_T":"");
|
|
break;
|
|
case AAC_MULT5:
|
|
asprintf(&m.dlna_pn, "AVC_TS_MP_HD_AAC_MULT5%s;DLNA.ORG_OP=01;DLNA.ORG_CI=0",
|
|
ts_timestamp==NONE?"_ISO" : ts_timestamp==VALID?"_T":"");
|
|
break;
|
|
default:
|
|
DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for TS/AVC/%cD file %s\n", res, 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 )
|
|
{
|
|
switch( audio_profile )
|
|
{
|
|
case AC3:
|
|
asprintf(&m.dlna_pn, "AVC_MP4_MP_SD_AC3;DLNA.ORG_OP=01;DLNA.ORG_CI=0");
|
|
break;
|
|
case AAC_MULT5:
|
|
asprintf(&m.dlna_pn, "AVC_MP4_MP_SD_AAC_MULT5;DLNA.ORG_OP=01;DLNA.ORG_CI=0");
|
|
break;
|
|
default:
|
|
DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for MP4/AVC/SD file %s\n", path);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s is h.264\n", video_stream, 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, path, m.resolution);
|
|
asprintf(&m.mime, "video/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, path, m.resolution);
|
|
asprintf(&m.mime, "video/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, path);
|
|
asprintf(&m.mime, "video/divx");
|
|
}
|
|
else
|
|
{
|
|
DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s is MPEG4 [%X]\n", video_stream, 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, 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 MP3:
|
|
asprintf(&m.dlna_pn, "WMVSPML_MP3;DLNA.ORG_OP=01;DLNA.ORG_CI=0");
|
|
break;
|
|
case WMA_BASE:
|
|
asprintf(&m.dlna_pn, "WMVSPML_BASE;DLNA.ORG_OP=01;DLNA.ORG_CI=0");
|
|
break;
|
|
default:
|
|
DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for WMVSPML/0x%X file %s\n", audio_profile, 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 WMA_PRO:
|
|
asprintf(&m.dlna_pn, "WMVMED_PRO;DLNA.ORG_OP=01;DLNA.ORG_CI=0");
|
|
break;
|
|
case WMA_FULL:
|
|
asprintf(&m.dlna_pn, "WMVMED_FULL;DLNA.ORG_OP=01;DLNA.ORG_CI=0");
|
|
break;
|
|
case WMA_BASE:
|
|
asprintf(&m.dlna_pn, "WMVMED_BASE;DLNA.ORG_OP=01;DLNA.ORG_CI=0");
|
|
break;
|
|
default:
|
|
DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for WMVMED/0x%X file %s\n", audio_profile, 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 WMA_PRO:
|
|
asprintf(&m.dlna_pn, "WMVHIGH_PRO;DLNA.ORG_OP=01;DLNA.ORG_CI=0");
|
|
break;
|
|
case WMA_FULL:
|
|
asprintf(&m.dlna_pn, "WMVHIGH_FULL;DLNA.ORG_OP=01;DLNA.ORG_CI=0");
|
|
break;
|
|
default:
|
|
DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for WMVHIGH/0x%X file %s\n", audio_profile, path);
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case CODEC_ID_MSMPEG4V3:
|
|
asprintf(&m.mime, "video/avi");
|
|
default:
|
|
DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s is %s [type %d]\n", video_stream, 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 )
|
|
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, "Unhandled format: %s\n", ctx->iformat->name);
|
|
}
|
|
av_close_input_file(ctx);
|
|
|
|
sql = sqlite3_mprintf( "INSERT into DETAILS"
|
|
" (PATH, SIZE, DURATION, DATE, CHANNELS, BITRATE, SAMPLERATE, RESOLUTION,"
|
|
" TITLE, DLNA_PN, MIME) "
|
|
"VALUES"
|
|
" (%Q, %lld, %Q, %Q, %Q, %Q, %Q, %Q, '%q', %Q, '%q');",
|
|
path, size, m.duration,
|
|
strlen(date) ? date : NULL,
|
|
m.channels, m.bitrate, m.frequency, m.resolution,
|
|
esc_name?esc_name:name,
|
|
m.dlna_pn, m.mime);
|
|
//DEBUG DPRINTF(E_DEBUG, L_METADATA, "SQL: %s\n", sql);
|
|
if( sql_exec(db, sql) != SQLITE_OK )
|
|
{
|
|
fprintf(stderr, "Error inserting details for '%s'!\n", path);
|
|
ret = 0;
|
|
}
|
|
else
|
|
{
|
|
ret = sqlite3_last_insert_rowid(db);
|
|
}
|
|
sqlite3_free(sql);
|
|
if( m.dlna_pn )
|
|
free(m.dlna_pn);
|
|
if( m.mime )
|
|
free(m.mime);
|
|
if( m.duration )
|
|
free(m.duration);
|
|
if( m.resolution )
|
|
free(m.resolution);
|
|
if( m.bitrate )
|
|
free(m.bitrate);
|
|
if( m.frequency )
|
|
free(m.frequency);
|
|
if( m.bps )
|
|
free(m.bps);
|
|
if( m.channels )
|
|
free(m.channels);
|
|
if( esc_name )
|
|
free(esc_name);
|
|
|
|
return ret;
|
|
}
|