mirror of
https://git.code.sf.net/p/minidlna/git
synced 2025-03-30 04:08:05 +00:00
1618 lines
45 KiB
C
1618 lines
45 KiB
C
/* MiniDLNA media server
|
|
* Copyright (C) 2008-2017 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "config.h"
|
|
|
|
#include <stdio.h>
|
|
#include <ctype.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <libgen.h>
|
|
#include <unistd.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/param.h>
|
|
#include <fcntl.h>
|
|
|
|
#include <libexif/exif-loader.h>
|
|
#include <jpeglib.h>
|
|
#include <setjmp.h>
|
|
#include "libav.h"
|
|
|
|
#include "upnpglobalvars.h"
|
|
#include "tagutils/tagutils.h"
|
|
#include "image_utils.h"
|
|
#include "upnpreplyparse.h"
|
|
#include "tivo_utils.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
|
|
|
|
/* 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 188
|
|
#define MPEG_TS_PACKET_LENGTH_DLNA 192 /* prepends 4 bytes to TS packet */
|
|
int
|
|
dlna_timestamp_is_present(const char *filename, int *raw_packet_size)
|
|
{
|
|
unsigned char buffer[3*MPEG_TS_PACKET_LENGTH_DLNA];
|
|
int fd, i;
|
|
|
|
/* read file header */
|
|
fd = open(filename, O_RDONLY);
|
|
if( fd < 0 )
|
|
return 0;
|
|
i = read(fd, buffer, MPEG_TS_PACKET_LENGTH_DLNA*3);
|
|
close(fd);
|
|
if( i < 0 )
|
|
return 0;
|
|
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 &&
|
|
buffer[i + MPEG_TS_PACKET_LENGTH_DLNA*2] == MPEG_TS_SYNC_CODE)
|
|
{
|
|
*raw_packet_size = MPEG_TS_PACKET_LENGTH_DLNA;
|
|
if (buffer[i+MPEG_TS_PACKET_LENGTH] == 0x00 &&
|
|
buffer[i+MPEG_TS_PACKET_LENGTH+1] == 0x00 &&
|
|
buffer[i+MPEG_TS_PACKET_LENGTH+2] == 0x00 &&
|
|
buffer[i+MPEG_TS_PACKET_LENGTH+3] == 0x00)
|
|
return 0;
|
|
else
|
|
return 1;
|
|
} else if (buffer[i + MPEG_TS_PACKET_LENGTH] == MPEG_TS_SYNC_CODE &&
|
|
buffer[i + MPEG_TS_PACKET_LENGTH*2] == MPEG_TS_SYNC_CODE) {
|
|
*raw_packet_size = MPEG_TS_PACKET_LENGTH;
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
*raw_packet_size = 0;
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
check_for_captions(const char *path, int64_t detailID)
|
|
{
|
|
char file[MAXPATHLEN];
|
|
char *p;
|
|
int ret;
|
|
|
|
strncpyt(file, path, sizeof(file));
|
|
p = strip_ext(file);
|
|
if (!p)
|
|
return;
|
|
|
|
/* If we weren't given a detail ID, look for one. */
|
|
if (!detailID)
|
|
{
|
|
detailID = sql_get_int64_field(db, "SELECT ID from DETAILS where (PATH > '%q.' and PATH <= '%q.z')"
|
|
" and MIME glob 'video/*' limit 1", file, file);
|
|
if (detailID <= 0)
|
|
{
|
|
//DPRINTF(E_MAXDEBUG, L_METADATA, "No file found for caption %s.\n", path);
|
|
return;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
static void
|
|
parse_nfo(const char *path, metadata_t *m)
|
|
{
|
|
FILE *nfo;
|
|
char *buf;
|
|
struct NameValueParserData xml;
|
|
struct stat file;
|
|
size_t nread;
|
|
char *val, *val2;
|
|
|
|
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_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)
|
|
{
|
|
free(buf);
|
|
return;
|
|
}
|
|
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)
|
|
{
|
|
char *esc_tag, *title;
|
|
val2 = GetValueFromNameValueList(&xml, "episodetitle");
|
|
if (val2)
|
|
xasprintf(&title, "%s - %s", val, val2);
|
|
else
|
|
title = strdup(val);
|
|
esc_tag = unescape_tag(title, 1);
|
|
m->title = escape_tag(esc_tag, 1);
|
|
free(esc_tag);
|
|
free(title);
|
|
}
|
|
|
|
val = GetValueFromNameValueList(&xml, "plot");
|
|
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)
|
|
{
|
|
char *esc_tag = unescape_tag(val, 1);
|
|
m->date = escape_tag(esc_tag, 1);
|
|
free(esc_tag);
|
|
}
|
|
|
|
val = GetValueFromNameValueList(&xml, "genre");
|
|
if (val)
|
|
{
|
|
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)
|
|
{
|
|
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);
|
|
free(buf);
|
|
}
|
|
|
|
void
|
|
free_metadata(metadata_t *m, uint32_t flags)
|
|
{
|
|
if( flags & FLAG_TITLE )
|
|
free(m->title);
|
|
if( flags & FLAG_ARTIST )
|
|
free(m->artist);
|
|
if( flags & FLAG_ALBUM )
|
|
free(m->album);
|
|
if( flags & FLAG_GENRE )
|
|
free(m->genre);
|
|
if( flags & FLAG_CREATOR )
|
|
free(m->creator);
|
|
if( flags & FLAG_DATE )
|
|
free(m->date);
|
|
if( flags & FLAG_COMMENT )
|
|
free(m->comment);
|
|
if( flags & FLAG_DLNA_PN )
|
|
free(m->dlna_pn);
|
|
if( flags & FLAG_MIME )
|
|
free(m->mime);
|
|
if( flags & FLAG_DURATION )
|
|
free(m->duration);
|
|
if( flags & FLAG_RESOLUTION )
|
|
free(m->resolution);
|
|
}
|
|
|
|
int64_t
|
|
GetFolderMetadata(const char *name, const char *path, const char *artist, const char *genre, int64_t 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);
|
|
if( ret != SQLITE_OK )
|
|
ret = 0;
|
|
else
|
|
ret = sqlite3_last_insert_rowid(db);
|
|
|
|
return ret;
|
|
}
|
|
|
|
int64_t
|
|
GetAudioMetadata(const char *path, const char *name)
|
|
{
|
|
char type[4];
|
|
static char lang[6] = { '\0' };
|
|
struct stat file;
|
|
int64_t ret;
|
|
char *esc_tag;
|
|
int i;
|
|
int64_t 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;
|
|
|
|
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") || ends_with(path, ".oga") )
|
|
{
|
|
strcpy(type, "ogg");
|
|
m.mime = strdup("audio/ogg");
|
|
}
|
|
else if( ends_with(path, ".pcm") )
|
|
{
|
|
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);
|
|
return 0;
|
|
}
|
|
|
|
if( !(*lang) )
|
|
{
|
|
if( !getenv("LANG") )
|
|
strcpy(lang, "en_US");
|
|
else
|
|
strncpyt(lang, getenv("LANG"), sizeof(lang));
|
|
}
|
|
|
|
if( readtags((char *)path, &song, &file, lang, type) != 0 )
|
|
{
|
|
DPRINTF(E_WARN, L_METADATA, "Cannot extract tags from %s!\n", path);
|
|
freetags(&song);
|
|
free_metadata(&m, free_flags);
|
|
return 0;
|
|
}
|
|
|
|
if( song.dlna_pn )
|
|
m.dlna_pn = strdup(song.dlna_pn);
|
|
if( song.year )
|
|
xasprintf(&m.date, "%04d-01-01", song.year);
|
|
m.duration = duration_str(song.song_length);
|
|
if( song.title && *song.title )
|
|
{
|
|
m.title = trim(song.title);
|
|
if( (esc_tag = escape_tag(m.title, 0)) )
|
|
{
|
|
free_flags |= FLAG_TITLE;
|
|
m.title = esc_tag;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
free_flags |= FLAG_TITLE;
|
|
m.title = strdup(name);
|
|
strip_ext(m.title);
|
|
}
|
|
for( i = ROLE_START; i < N_ROLE; i++ )
|
|
{
|
|
if( song.contributor[i] && *song.contributor[i] )
|
|
{
|
|
m.creator = trim(song.contributor[i]);
|
|
if( strlen(m.creator) > 48 )
|
|
{
|
|
m.creator = strdup("Various Artists");
|
|
free_flags |= FLAG_CREATOR;
|
|
}
|
|
else if( (esc_tag = escape_tag(m.creator, 0)) )
|
|
{
|
|
m.creator = esc_tag;
|
|
free_flags |= FLAG_CREATOR;
|
|
}
|
|
m.artist = m.creator;
|
|
break;
|
|
}
|
|
}
|
|
/* If there is a album artist or band associated with the album,
|
|
use it for virtual containers. */
|
|
if( i < ROLE_ALBUMARTIST )
|
|
{
|
|
for( i = ROLE_ALBUMARTIST; i <= ROLE_BAND; i++ )
|
|
{
|
|
if( song.contributor[i] && *song.contributor[i] )
|
|
break;
|
|
}
|
|
if( i <= ROLE_BAND )
|
|
{
|
|
m.artist = trim(song.contributor[i]);
|
|
if( strlen(m.artist) > 48 )
|
|
{
|
|
m.artist = strdup("Various Artists");
|
|
free_flags |= FLAG_ARTIST;
|
|
}
|
|
else if( (esc_tag = escape_tag(m.artist, 0)) )
|
|
{
|
|
m.artist = esc_tag;
|
|
free_flags |= FLAG_ARTIST;
|
|
}
|
|
}
|
|
}
|
|
if( song.album && *song.album )
|
|
{
|
|
m.album = trim(song.album);
|
|
if( (esc_tag = escape_tag(m.album, 0)) )
|
|
{
|
|
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, 0)) )
|
|
{
|
|
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, 0)) )
|
|
{
|
|
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, %lld, '%s', %d, %d, %d, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %d, %d, %Q, '%s', %lld);",
|
|
path, (long long)file.st_size, (long long)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 )
|
|
{
|
|
DPRINTF(E_ERROR, L_METADATA, "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 */
|
|
static jmp_buf setjmp_buffer;
|
|
static void
|
|
libjpeg_error_handler(j_common_ptr cinfo)
|
|
{
|
|
cinfo->err->output_message (cinfo);
|
|
longjmp(setjmp_buffer, 1);
|
|
return;
|
|
}
|
|
|
|
int64_t
|
|
GetImageMetadata(const char *path, const 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;
|
|
int64_t ret;
|
|
image_s *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;
|
|
//DEBUG DPRINTF(E_DEBUG, L_METADATA, " * size: %jd\n", file.st_size);
|
|
|
|
/* MIME hard-coded to JPEG for now, until we add PNG support */
|
|
m.mime = strdup("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 )
|
|
{
|
|
strncpyt(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 )
|
|
{
|
|
strncpyt(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 = escape_tag(trim(model), 1);
|
|
}
|
|
}
|
|
//DEBUG DPRINTF(E_DEBUG, L_METADATA, " * model: %s\n", model);
|
|
|
|
e = exif_content_get_entry(ed->ifd[EXIF_IFD_0], EXIF_TAG_ORIENTATION);
|
|
if( e )
|
|
{
|
|
switch( exif_get_short(e->data, exif_data_get_byte_order(ed)) )
|
|
{
|
|
case 3:
|
|
m.rotation = 180;
|
|
break;
|
|
case 6:
|
|
m.rotation = 90;
|
|
break;
|
|
case 8:
|
|
m.rotation = 270;
|
|
break;
|
|
default:
|
|
m.rotation = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
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, ed->data, ed->size, 1, ROTATE_NONE);
|
|
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");
|
|
if( infile )
|
|
{
|
|
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 )
|
|
m.dlna_pn = strdup("JPEG_SM");
|
|
else if( width <= 1024 && height <= 768 )
|
|
m.dlna_pn = strdup("JPEG_MED");
|
|
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, 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 )
|
|
{
|
|
DPRINTF(E_ERROR, L_METADATA, "Error inserting details for '%s'!\n", path);
|
|
ret = 0;
|
|
}
|
|
else
|
|
{
|
|
ret = sqlite3_last_insert_rowid(db);
|
|
}
|
|
free_metadata(&m, free_flags);
|
|
|
|
return ret;
|
|
}
|
|
|
|
int64_t
|
|
GetVideoMetadata(const char *path, const char *name)
|
|
{
|
|
struct stat file;
|
|
int ret, i;
|
|
struct tm *modtime;
|
|
AVFormatContext *ctx = NULL;
|
|
AVStream *astream = NULL, *vstream = NULL;
|
|
int audio_stream = -1, video_stream = -1;
|
|
enum audio_profiles audio_profile = PROFILE_AUDIO_UNKNOWN;
|
|
char fourcc[4];
|
|
int64_t album_art = 0;
|
|
char nfo[MAXPATHLEN], *ext;
|
|
struct song_metadata video;
|
|
metadata_t m;
|
|
uint32_t free_flags = 0xFFFFFFFF;
|
|
char *path_cpy, *basepath;
|
|
|
|
memset(&m, '\0', sizeof(m));
|
|
memset(&video, '\0', sizeof(video));
|
|
|
|
//DEBUG DPRINTF(E_DEBUG, L_METADATA, "Parsing video %s...\n", name);
|
|
if ( stat(path, &file) != 0 )
|
|
return 0;
|
|
//DEBUG DPRINTF(E_DEBUG, L_METADATA, " * size: %jd\n", file.st_size);
|
|
|
|
ret = lav_open(&ctx, path);
|
|
if( ret != 0 )
|
|
{
|
|
char err[128];
|
|
av_strerror(ret, err, sizeof(err));
|
|
DPRINTF(E_WARN, L_METADATA, "Opening %s failed! [%s]\n", path, err);
|
|
return 0;
|
|
}
|
|
//dump_format(ctx, 0, NULL, 0);
|
|
for( i=0; i < ctx->nb_streams; i++)
|
|
{
|
|
if( lav_codec_type(ctx->streams[i]) == AVMEDIA_TYPE_AUDIO &&
|
|
audio_stream == -1 )
|
|
{
|
|
audio_stream = i;
|
|
astream = ctx->streams[audio_stream];
|
|
continue;
|
|
}
|
|
else if( lav_codec_type(ctx->streams[i]) == AVMEDIA_TYPE_VIDEO &&
|
|
!lav_is_thumbnail_stream(ctx->streams[i], &m.thumb_data, &m.thumb_size) &&
|
|
video_stream == -1 )
|
|
{
|
|
video_stream = i;
|
|
vstream = ctx->streams[video_stream];
|
|
continue;
|
|
}
|
|
}
|
|
path_cpy = strdup(path);
|
|
basepath = basename(path_cpy);
|
|
if( !vstream )
|
|
{
|
|
/* This must not be a video file. */
|
|
lav_close(ctx);
|
|
if( !is_audio(path) )
|
|
DPRINTF(E_DEBUG, L_METADATA, "File %s does not contain a video stream.\n", basepath);
|
|
free(path_cpy);
|
|
return 0;
|
|
}
|
|
|
|
if( astream )
|
|
{
|
|
aac_object_type_t aac_type = AAC_INVALID;
|
|
switch( lav_codec_id(astream) )
|
|
{
|
|
case AV_CODEC_ID_MP3:
|
|
audio_profile = PROFILE_AUDIO_MP3;
|
|
break;
|
|
case AV_CODEC_ID_AAC:
|
|
if( !lav_codec_extradata(astream) )
|
|
{
|
|
DPRINTF(E_DEBUG, L_METADATA, "No AAC type\n");
|
|
}
|
|
else
|
|
{
|
|
uint8_t data;
|
|
memcpy(&data, lav_codec_extradata(astream), 1);
|
|
aac_type = data >> 3;
|
|
}
|
|
switch( aac_type )
|
|
{
|
|
/* AAC Low Complexity variants */
|
|
case AAC_LC:
|
|
case AAC_LC_ER:
|
|
if( lav_sample_rate(astream) < 8000 ||
|
|
lav_sample_rate(astream) > 48000 )
|
|
{
|
|
DPRINTF(E_DEBUG, L_METADATA, "Unsupported AAC: sample rate is not 8000 < %d < 48000\n",
|
|
lav_sample_rate(astream));
|
|
break;
|
|
}
|
|
/* AAC @ Level 1/2 */
|
|
if( lav_channels(astream) <= 2 &&
|
|
lav_bit_rate(astream) <= 576000 )
|
|
audio_profile = PROFILE_AUDIO_AAC;
|
|
else if( lav_channels(astream) <= 6 &&
|
|
lav_bit_rate(astream) <= 1440000 )
|
|
audio_profile = PROFILE_AUDIO_AAC_MULT5;
|
|
else
|
|
DPRINTF(E_DEBUG, L_METADATA, "Unhandled AAC: %lld channels, %lld bitrate\n",
|
|
(long long)lav_channels(astream),
|
|
(long long)lav_bit_rate(astream));
|
|
break;
|
|
default:
|
|
DPRINTF(E_DEBUG, L_METADATA, "Unhandled AAC type [%d]\n", aac_type);
|
|
break;
|
|
}
|
|
break;
|
|
case AV_CODEC_ID_AC3:
|
|
case AV_CODEC_ID_DTS:
|
|
audio_profile = PROFILE_AUDIO_AC3;
|
|
break;
|
|
case AV_CODEC_ID_WMAV1:
|
|
case AV_CODEC_ID_WMAV2:
|
|
/* WMA Baseline: stereo, up to 48 KHz, up to 192,999 bps */
|
|
if ( lav_bit_rate(astream) <= 193000 )
|
|
audio_profile = PROFILE_AUDIO_WMA_BASE;
|
|
/* WMA Full: stereo, up to 48 KHz, up to 385 Kbps */
|
|
else if ( lav_bit_rate(astream) <= 385000 )
|
|
audio_profile = PROFILE_AUDIO_WMA_FULL;
|
|
break;
|
|
case AV_CODEC_ID_WMAPRO:
|
|
audio_profile = PROFILE_AUDIO_WMA_PRO;
|
|
break;
|
|
case AV_CODEC_ID_MP2:
|
|
audio_profile = PROFILE_AUDIO_MP2;
|
|
break;
|
|
case AV_CODEC_ID_AMR_NB:
|
|
audio_profile = PROFILE_AUDIO_AMR;
|
|
break;
|
|
default:
|
|
if( (lav_codec_id(astream) >= AV_CODEC_ID_PCM_S16LE) &&
|
|
(lav_codec_id(astream) < AV_CODEC_ID_ADPCM_IMA_QT) )
|
|
audio_profile = PROFILE_AUDIO_PCM;
|
|
else
|
|
DPRINTF(E_DEBUG, L_METADATA, "Unhandled audio codec [0x%X]\n", lav_codec_id(astream));
|
|
break;
|
|
}
|
|
m.frequency = lav_sample_rate(astream);
|
|
m.channels = lav_channels(astream);
|
|
}
|
|
if( vstream )
|
|
{
|
|
int off;
|
|
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 )
|
|
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. */
|
|
if( strcmp(ctx->iformat->name, "avi") == 0 )
|
|
{
|
|
xasprintf(&m.mime, "video/x-msvideo");
|
|
if( lav_codec_id(vstream) == AV_CODEC_ID_MPEG4 )
|
|
{
|
|
fourcc[0] = lav_codec_tag(vstream) & 0xff;
|
|
fourcc[1] = lav_codec_tag(vstream)>>8 & 0xff;
|
|
fourcc[2] = lav_codec_tag(vstream)>>16 & 0xff;
|
|
fourcc[3] = lav_codec_tag(vstream)>>24 & 0xff;
|
|
if( memcmp(fourcc, "XVID", 4) == 0 ||
|
|
memcmp(fourcc, "DX50", 4) == 0 ||
|
|
memcmp(fourcc, "DIVX", 4) == 0 )
|
|
xasprintf(&m.creator, "DiVX");
|
|
}
|
|
}
|
|
else if( strcmp(ctx->iformat->name, "mov,mp4,m4a,3gp,3g2,mj2") == 0 &&
|
|
ends_with(path, ".mov") )
|
|
xasprintf(&m.mime, "video/quicktime");
|
|
else if( strncmp(ctx->iformat->name, "matroska", 8) == 0 )
|
|
xasprintf(&m.mime, "video/x-matroska");
|
|
else if( strcmp(ctx->iformat->name, "flv") == 0 )
|
|
xasprintf(&m.mime, "video/x-flv");
|
|
else if( strcmp(ctx->iformat->name, "rm") == 0 )
|
|
xasprintf(&m.mime, "application/vnd.rn-realmedia");
|
|
else if( strcmp(ctx->iformat->name, "rmvb") == 0 )
|
|
xasprintf(&m.mime, "application/vnd.rn-realmedia-vbr");
|
|
if( m.mime )
|
|
goto video_no_dlna;
|
|
|
|
switch( lav_codec_id(vstream) )
|
|
{
|
|
case AV_CODEC_ID_MPEG1VIDEO:
|
|
if( strcmp(ctx->iformat->name, "mpeg") == 0 )
|
|
{
|
|
if( (lav_width(vstream) == 352) &&
|
|
(lav_height(vstream) <= 288) )
|
|
{
|
|
m.dlna_pn = strdup("MPEG1");
|
|
}
|
|
xasprintf(&m.mime, "video/mpeg");
|
|
}
|
|
break;
|
|
case AV_CODEC_ID_MPEG2VIDEO:
|
|
m.dlna_pn = malloc(64);
|
|
off = sprintf(m.dlna_pn, "MPEG_");
|
|
if( strcmp(ctx->iformat->name, "mpegts") == 0 )
|
|
{
|
|
int raw_packet_size;
|
|
int dlna_ts_present = dlna_timestamp_is_present(path, &raw_packet_size);
|
|
DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s is %s MPEG2 TS packet size %d\n",
|
|
video_stream, basepath, m.resolution, raw_packet_size);
|
|
off += sprintf(m.dlna_pn+off, "TS_");
|
|
if( (lav_width(vstream) >= 1280) &&
|
|
(lav_height(vstream) >= 720) )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "HD_NA");
|
|
}
|
|
else
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "SD_");
|
|
if( (lav_height(vstream) == 576) ||
|
|
(lav_height(vstream) == 288) )
|
|
off += sprintf(m.dlna_pn+off, "EU");
|
|
else
|
|
off += sprintf(m.dlna_pn+off, "NA");
|
|
}
|
|
if( raw_packet_size == MPEG_TS_PACKET_LENGTH_DLNA )
|
|
{
|
|
if (dlna_ts_present)
|
|
ts_timestamp = VALID;
|
|
else
|
|
ts_timestamp = EMPTY;
|
|
}
|
|
else if( raw_packet_size != MPEG_TS_PACKET_LENGTH )
|
|
{
|
|
DPRINTF(E_DEBUG, L_METADATA, "Unsupported DLNA TS packet size [%d] (%s)\n",
|
|
raw_packet_size, basepath);
|
|
free(m.dlna_pn);
|
|
m.dlna_pn = NULL;
|
|
}
|
|
switch( ts_timestamp )
|
|
{
|
|
case NONE:
|
|
xasprintf(&m.mime, "video/mpeg");
|
|
if( m.dlna_pn )
|
|
off += sprintf(m.dlna_pn+off, "_ISO");
|
|
break;
|
|
case VALID:
|
|
off += sprintf(m.dlna_pn+off, "_T");
|
|
case EMPTY:
|
|
xasprintf(&m.mime, "video/vnd.dlna.mpeg-tts");
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
else if( strcmp(ctx->iformat->name, "mpeg") == 0 )
|
|
{
|
|
DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s is %s MPEG2 PS\n",
|
|
video_stream, basepath, m.resolution);
|
|
off += sprintf(m.dlna_pn+off, "PS_");
|
|
if( (lav_height(vstream) == 576) ||
|
|
(lav_height(vstream) == 288) )
|
|
off += sprintf(m.dlna_pn+off, "PAL");
|
|
else
|
|
off += sprintf(m.dlna_pn+off, "NTSC");
|
|
xasprintf(&m.mime, "video/mpeg");
|
|
}
|
|
else
|
|
{
|
|
DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s [%s] is %s non-DLNA MPEG2\n",
|
|
video_stream, basepath, ctx->iformat->name, m.resolution);
|
|
free(m.dlna_pn);
|
|
m.dlna_pn = NULL;
|
|
}
|
|
break;
|
|
case AV_CODEC_ID_H264:
|
|
m.dlna_pn = malloc(128);
|
|
off = sprintf(m.dlna_pn, "AVC_");
|
|
|
|
if( strcmp(ctx->iformat->name, "mpegts") == 0 )
|
|
{
|
|
AVRational display_aspect_ratio;
|
|
int fps, interlaced;
|
|
int raw_packet_size;
|
|
int dlna_ts_present = dlna_timestamp_is_present(path, &raw_packet_size);
|
|
|
|
off += sprintf(m.dlna_pn+off, "TS_");
|
|
if (lav_sample_aspect_ratio(vstream).num) {
|
|
av_reduce(&display_aspect_ratio.num, &display_aspect_ratio.den,
|
|
lav_width(vstream) * lav_sample_aspect_ratio(vstream).num,
|
|
lav_height(vstream) * lav_sample_aspect_ratio(vstream).den,
|
|
1024*1024);
|
|
}
|
|
fps = lav_get_fps(vstream);
|
|
interlaced = lav_get_interlaced(vstream);
|
|
if( ((((lav_width(vstream) == 1920 || lav_width(vstream) == 1440) && lav_height(vstream) == 1080) ||
|
|
(lav_width(vstream) == 720 && lav_height(vstream) == 480)) && fps == 59 && interlaced) ||
|
|
((lav_width(vstream) == 1280 && lav_height(vstream) == 720) && fps == 59 && !interlaced) )
|
|
{
|
|
if( (lav_profile(vstream) == FF_PROFILE_H264_MAIN || lav_profile(vstream) == FF_PROFILE_H264_HIGH) &&
|
|
audio_profile == PROFILE_AUDIO_AC3 )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "HD_60_");
|
|
lav_profile(vstream) = FF_PROFILE_SKIP;
|
|
}
|
|
}
|
|
else if( ((lav_width(vstream) == 1920 && lav_height(vstream) == 1080) ||
|
|
(lav_width(vstream) == 1440 && lav_height(vstream) == 1080) ||
|
|
(lav_width(vstream) == 1280 && lav_height(vstream) == 720) ||
|
|
(lav_width(vstream) == 720 && lav_height(vstream) == 576)) &&
|
|
interlaced && fps == 50 )
|
|
{
|
|
if( (lav_profile(vstream) == FF_PROFILE_H264_MAIN || lav_profile(vstream) == FF_PROFILE_H264_HIGH) &&
|
|
audio_profile == PROFILE_AUDIO_AC3 )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "HD_50_");
|
|
lav_profile(vstream) = FF_PROFILE_SKIP;
|
|
}
|
|
}
|
|
switch( lav_profile(vstream) )
|
|
{
|
|
case FF_PROFILE_H264_BASELINE:
|
|
case FF_PROFILE_H264_CONSTRAINED_BASELINE:
|
|
off += sprintf(m.dlna_pn+off, "BL_");
|
|
if( lav_width(vstream) <= 352 &&
|
|
lav_height(vstream) <= 288 &&
|
|
lav_bit_rate(vstream) <= 384000 )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "CIF15_");
|
|
break;
|
|
}
|
|
else if( lav_width(vstream) <= 352 &&
|
|
lav_height(vstream) <= 288 &&
|
|
lav_bit_rate(vstream) <= 3000000 )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "CIF30_");
|
|
break;
|
|
}
|
|
/* Fall back to Main Profile if it doesn't match a Baseline DLNA profile. */
|
|
else
|
|
off -= 3;
|
|
default:
|
|
case FF_PROFILE_H264_MAIN:
|
|
off += sprintf(m.dlna_pn+off, "MP_");
|
|
if( lav_profile(vstream) != FF_PROFILE_H264_BASELINE &&
|
|
lav_profile(vstream) != FF_PROFILE_H264_CONSTRAINED_BASELINE &&
|
|
lav_profile(vstream) != FF_PROFILE_H264_MAIN )
|
|
{
|
|
DPRINTF(E_DEBUG, L_METADATA, "Unknown AVC profile %d; assuming MP. [%s]\n",
|
|
lav_profile(vstream), basepath);
|
|
}
|
|
if( lav_width(vstream) <= 720 &&
|
|
lav_height(vstream) <= 576 &&
|
|
lav_bit_rate(vstream) <= 10000000 )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "SD_");
|
|
}
|
|
else if( lav_width(vstream) <= 1920 &&
|
|
lav_height(vstream) <= 1152 &&
|
|
lav_bit_rate(vstream) <= 20000000 )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "HD_");
|
|
}
|
|
else
|
|
{
|
|
DPRINTF(E_DEBUG, L_METADATA, "Unsupported h.264 video profile! [%s, %dx%d, %lldbps : %s]\n",
|
|
m.dlna_pn, lav_width(vstream), lav_height(vstream),
|
|
(long long)lav_bit_rate(vstream), basepath);
|
|
free(m.dlna_pn);
|
|
m.dlna_pn = NULL;
|
|
}
|
|
break;
|
|
case FF_PROFILE_H264_HIGH:
|
|
off += sprintf(m.dlna_pn+off, "HP_");
|
|
if( lav_width(vstream) <= 1920 &&
|
|
lav_height(vstream) <= 1152 &&
|
|
lav_bit_rate(vstream) <= 30000000 &&
|
|
audio_profile == PROFILE_AUDIO_AC3 )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "HD_");
|
|
}
|
|
else
|
|
{
|
|
DPRINTF(E_DEBUG, L_METADATA, "Unsupported h.264 HP video profile! [%lldbps, %d audio : %s]\n",
|
|
(long long)lav_bit_rate(vstream), audio_profile, basepath);
|
|
free(m.dlna_pn);
|
|
m.dlna_pn = NULL;
|
|
}
|
|
break;
|
|
case FF_PROFILE_SKIP:
|
|
break;
|
|
}
|
|
if( !m.dlna_pn )
|
|
break;
|
|
switch( audio_profile )
|
|
{
|
|
case PROFILE_AUDIO_MP3:
|
|
off += sprintf(m.dlna_pn+off, "MPEG1_L3");
|
|
break;
|
|
case PROFILE_AUDIO_AC3:
|
|
off += sprintf(m.dlna_pn+off, "AC3");
|
|
break;
|
|
case PROFILE_AUDIO_AAC:
|
|
case PROFILE_AUDIO_AAC_MULT5:
|
|
off += sprintf(m.dlna_pn+off, "AAC_MULT5");
|
|
break;
|
|
default:
|
|
DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for %s file [%s]\n",
|
|
m.dlna_pn, basepath);
|
|
free(m.dlna_pn);
|
|
m.dlna_pn = NULL;
|
|
break;
|
|
}
|
|
if( !m.dlna_pn )
|
|
break;
|
|
if( raw_packet_size == MPEG_TS_PACKET_LENGTH_DLNA )
|
|
{
|
|
if( lav_profile(vstream) == FF_PROFILE_H264_HIGH ||
|
|
dlna_ts_present )
|
|
ts_timestamp = VALID;
|
|
else
|
|
ts_timestamp = EMPTY;
|
|
}
|
|
else if( raw_packet_size != MPEG_TS_PACKET_LENGTH )
|
|
{
|
|
DPRINTF(E_DEBUG, L_METADATA, "Unsupported DLNA TS packet size [%d] (%s)\n",
|
|
raw_packet_size, basepath);
|
|
free(m.dlna_pn);
|
|
m.dlna_pn = NULL;
|
|
}
|
|
switch( ts_timestamp )
|
|
{
|
|
case NONE:
|
|
if( m.dlna_pn )
|
|
off += sprintf(m.dlna_pn+off, "_ISO");
|
|
break;
|
|
case VALID:
|
|
off += sprintf(m.dlna_pn+off, "_T");
|
|
case EMPTY:
|
|
xasprintf(&m.mime, "video/vnd.dlna.mpeg-tts");
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
else if( strcmp(ctx->iformat->name, "mov,mp4,m4a,3gp,3g2,mj2") == 0 )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "MP4_");
|
|
|
|
switch( lav_profile(vstream) ) {
|
|
case FF_PROFILE_H264_BASELINE:
|
|
case FF_PROFILE_H264_CONSTRAINED_BASELINE:
|
|
if( lav_width(vstream) <= 352 &&
|
|
lav_height(vstream) <= 288 )
|
|
{
|
|
if( ctx->bit_rate < 600000 )
|
|
off += sprintf(m.dlna_pn+off, "BL_CIF15_");
|
|
else if( ctx->bit_rate < 5000000 )
|
|
off += sprintf(m.dlna_pn+off, "BL_CIF30_");
|
|
else
|
|
goto mp4_mp_fallback;
|
|
|
|
if( audio_profile == PROFILE_AUDIO_AMR )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "AMR");
|
|
}
|
|
else if( audio_profile == PROFILE_AUDIO_AAC )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "AAC_");
|
|
if( ctx->bit_rate < 520000 )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "520");
|
|
}
|
|
else if( ctx->bit_rate < 940000 )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "940");
|
|
}
|
|
else
|
|
{
|
|
off -= 13;
|
|
goto mp4_mp_fallback;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
off -= 9;
|
|
goto mp4_mp_fallback;
|
|
}
|
|
}
|
|
else if( lav_width(vstream) <= 720 &&
|
|
lav_height(vstream) <= 576 )
|
|
{
|
|
if( lav_level(vstream) == 30 &&
|
|
audio_profile == PROFILE_AUDIO_AAC &&
|
|
ctx->bit_rate <= 5000000 )
|
|
off += sprintf(m.dlna_pn+off, "BL_L3L_SD_AAC");
|
|
else if( lav_level(vstream) <= 31 &&
|
|
audio_profile == PROFILE_AUDIO_AAC &&
|
|
ctx->bit_rate <= 15000000 )
|
|
off += sprintf(m.dlna_pn+off, "BL_L31_HD_AAC");
|
|
else
|
|
goto mp4_mp_fallback;
|
|
}
|
|
else if( lav_width(vstream) <= 1280 &&
|
|
lav_height(vstream) <= 720 )
|
|
{
|
|
if( lav_level(vstream) <= 31 &&
|
|
audio_profile == PROFILE_AUDIO_AAC &&
|
|
ctx->bit_rate <= 15000000 )
|
|
off += sprintf(m.dlna_pn+off, "BL_L31_HD_AAC");
|
|
else if( lav_level(vstream) <= 32 &&
|
|
audio_profile == PROFILE_AUDIO_AAC &&
|
|
ctx->bit_rate <= 21000000 )
|
|
off += sprintf(m.dlna_pn+off, "BL_L32_HD_AAC");
|
|
else
|
|
goto mp4_mp_fallback;
|
|
}
|
|
else
|
|
goto mp4_mp_fallback;
|
|
break;
|
|
case FF_PROFILE_H264_MAIN:
|
|
mp4_mp_fallback:
|
|
off += sprintf(m.dlna_pn+off, "MP_");
|
|
/* AVC MP4 SD profiles - 10 Mbps max */
|
|
if( lav_width(vstream) <= 720 &&
|
|
lav_height(vstream) <= 576 &&
|
|
lav_bit_rate(vstream) <= 10000000 )
|
|
{
|
|
sprintf(m.dlna_pn+off, "SD_");
|
|
if( audio_profile == PROFILE_AUDIO_AC3 )
|
|
off += sprintf(m.dlna_pn+off, "AC3");
|
|
else if( audio_profile == PROFILE_AUDIO_AAC ||
|
|
audio_profile == PROFILE_AUDIO_AAC_MULT5 )
|
|
off += sprintf(m.dlna_pn+off, "AAC_MULT5");
|
|
else if( audio_profile == PROFILE_AUDIO_MP3 )
|
|
off += sprintf(m.dlna_pn+off, "MPEG1_L3");
|
|
else
|
|
m.dlna_pn[10] = '\0';
|
|
}
|
|
else if( lav_width(vstream) <= 1280 &&
|
|
lav_height(vstream) <= 720 &&
|
|
lav_bit_rate(vstream) <= 15000000 &&
|
|
audio_profile == PROFILE_AUDIO_AAC )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "HD_720p_AAC");
|
|
}
|
|
else if( lav_width(vstream) <= 1920 &&
|
|
lav_height(vstream) <= 1080 &&
|
|
lav_bit_rate(vstream) <= 21000000 &&
|
|
audio_profile == PROFILE_AUDIO_AAC )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "HD_1080i_AAC");
|
|
}
|
|
if( strlen(m.dlna_pn) <= 11 )
|
|
{
|
|
DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for %s file %s\n",
|
|
m.dlna_pn, basepath);
|
|
free(m.dlna_pn);
|
|
m.dlna_pn = NULL;
|
|
}
|
|
break;
|
|
case FF_PROFILE_H264_HIGH:
|
|
if( lav_width(vstream) <= 1920 &&
|
|
lav_height(vstream) <= 1080 &&
|
|
lav_bit_rate(vstream) <= 25000000 &&
|
|
audio_profile == PROFILE_AUDIO_AAC )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "HP_HD_AAC");
|
|
}
|
|
break;
|
|
default:
|
|
DPRINTF(E_DEBUG, L_METADATA, "AVC profile [%d] not recognized for file %s\n",
|
|
lav_profile(vstream), basepath);
|
|
free(m.dlna_pn);
|
|
m.dlna_pn = NULL;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
free(m.dlna_pn);
|
|
m.dlna_pn = NULL;
|
|
}
|
|
DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s is h.264\n", video_stream, basepath);
|
|
break;
|
|
case AV_CODEC_ID_MPEG4:
|
|
fourcc[0] = lav_codec_tag(vstream) & 0xff;
|
|
fourcc[1] = lav_codec_tag(vstream)>>8 & 0xff;
|
|
fourcc[2] = lav_codec_tag(vstream)>>16 & 0xff;
|
|
fourcc[3] = lav_codec_tag(vstream)>>24 & 0xff;
|
|
DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s is MPEG4 [%c%c%c%c/0x%X]\n",
|
|
video_stream, basepath,
|
|
isprint(fourcc[0]) ? fourcc[0] : '_',
|
|
isprint(fourcc[1]) ? fourcc[1] : '_',
|
|
isprint(fourcc[2]) ? fourcc[2] : '_',
|
|
isprint(fourcc[3]) ? fourcc[3] : '_',
|
|
lav_codec_tag(vstream));
|
|
|
|
if( strcmp(ctx->iformat->name, "mov,mp4,m4a,3gp,3g2,mj2") == 0 )
|
|
{
|
|
m.dlna_pn = malloc(128);
|
|
off = sprintf(m.dlna_pn, "MPEG4_P2_");
|
|
|
|
if( ends_with(path, ".3gp") )
|
|
{
|
|
xasprintf(&m.mime, "video/3gpp");
|
|
switch( audio_profile )
|
|
{
|
|
case PROFILE_AUDIO_AAC:
|
|
off += sprintf(m.dlna_pn+off, "3GPP_SP_L0B_AAC");
|
|
break;
|
|
case PROFILE_AUDIO_AMR:
|
|
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/%d file %s\n",
|
|
audio_profile, basepath);
|
|
free(m.dlna_pn);
|
|
m.dlna_pn = NULL;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if( ctx->bit_rate <= 1000000 &&
|
|
audio_profile == PROFILE_AUDIO_AAC )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "MP4_ASP_AAC");
|
|
}
|
|
else if( ctx->bit_rate <= 4000000 &&
|
|
lav_width(vstream) <= 640 &&
|
|
lav_height(vstream) <= 480 &&
|
|
audio_profile == PROFILE_AUDIO_AAC )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "MP4_SP_VGA_AAC");
|
|
}
|
|
else
|
|
{
|
|
DPRINTF(E_DEBUG, L_METADATA, "Unsupported h.264 video profile! [%dx%d, %lldbps]\n",
|
|
lav_width(vstream),
|
|
lav_height(vstream),
|
|
(long long)ctx->bit_rate);
|
|
free(m.dlna_pn);
|
|
m.dlna_pn = NULL;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case AV_CODEC_ID_WMV3:
|
|
/* I'm not 100% sure this is correct, but it works on everything I could get my hands on */
|
|
if( lav_codec_extradata(vstream) )
|
|
{
|
|
if( !((lav_codec_extradata(vstream)[0] >> 3) & 1) )
|
|
lav_level(vstream) = 0;
|
|
if( !((lav_codec_extradata(vstream)[0] >> 6) & 1) )
|
|
lav_profile(vstream) = 0;
|
|
}
|
|
case AV_CODEC_ID_VC1:
|
|
if( strcmp(ctx->iformat->name, "asf") != 0 )
|
|
{
|
|
DPRINTF(E_DEBUG, L_METADATA, "Skipping DLNA parsing for non-ASF VC1 file %s\n", path);
|
|
break;
|
|
}
|
|
m.dlna_pn = malloc(64);
|
|
off = sprintf(m.dlna_pn, "WMV");
|
|
DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s is VC1\n", video_stream, basepath);
|
|
xasprintf(&m.mime, "video/x-ms-wmv");
|
|
if( (lav_width(vstream) <= 176) &&
|
|
(lav_height(vstream) <= 144) &&
|
|
(lav_level(vstream) == 0) )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "SPLL_");
|
|
switch( audio_profile )
|
|
{
|
|
case PROFILE_AUDIO_MP3:
|
|
off += sprintf(m.dlna_pn+off, "MP3");
|
|
break;
|
|
case PROFILE_AUDIO_WMA_BASE:
|
|
off += sprintf(m.dlna_pn+off, "BASE");
|
|
break;
|
|
default:
|
|
DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for WMVSPLL/0x%X file %s\n",
|
|
audio_profile, basepath);
|
|
free(m.dlna_pn);
|
|
m.dlna_pn = NULL;
|
|
break;
|
|
}
|
|
}
|
|
else if( (lav_width(vstream) <= 352) &&
|
|
(lav_height(vstream) <= 288) &&
|
|
(lav_profile(vstream) == 0) &&
|
|
(ctx->bit_rate/8 <= 384000) )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "SPML_");
|
|
switch( audio_profile )
|
|
{
|
|
case PROFILE_AUDIO_MP3:
|
|
off += sprintf(m.dlna_pn+off, "MP3");
|
|
break;
|
|
case PROFILE_AUDIO_WMA_BASE:
|
|
off += sprintf(m.dlna_pn+off, "BASE");
|
|
break;
|
|
default:
|
|
DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for WMVSPML/0x%X file %s\n",
|
|
audio_profile, basepath);
|
|
free(m.dlna_pn);
|
|
m.dlna_pn = NULL;
|
|
break;
|
|
}
|
|
}
|
|
else if( (lav_width(vstream) <= 720) &&
|
|
(lav_height(vstream) <= 576) &&
|
|
(ctx->bit_rate/8 <= 10000000) )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "MED_");
|
|
switch( audio_profile )
|
|
{
|
|
case PROFILE_AUDIO_WMA_PRO:
|
|
off += sprintf(m.dlna_pn+off, "PRO");
|
|
break;
|
|
case PROFILE_AUDIO_WMA_FULL:
|
|
off += sprintf(m.dlna_pn+off, "FULL");
|
|
break;
|
|
case PROFILE_AUDIO_WMA_BASE:
|
|
off += sprintf(m.dlna_pn+off, "BASE");
|
|
break;
|
|
default:
|
|
DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for WMVMED/0x%X file %s\n",
|
|
audio_profile, basepath);
|
|
free(m.dlna_pn);
|
|
m.dlna_pn = NULL;
|
|
break;
|
|
}
|
|
}
|
|
else if( (lav_width(vstream) <= 1920) &&
|
|
(lav_height(vstream) <= 1080) &&
|
|
(ctx->bit_rate/8 <= 20000000) )
|
|
{
|
|
off += sprintf(m.dlna_pn+off, "HIGH_");
|
|
switch( audio_profile )
|
|
{
|
|
case PROFILE_AUDIO_WMA_PRO:
|
|
off += sprintf(m.dlna_pn+off, "PRO");
|
|
break;
|
|
case PROFILE_AUDIO_WMA_FULL:
|
|
off += sprintf(m.dlna_pn+off, "FULL");
|
|
break;
|
|
default:
|
|
DPRINTF(E_DEBUG, L_METADATA, "No DLNA profile found for WMVHIGH/0x%X file %s\n",
|
|
audio_profile, basepath);
|
|
free(m.dlna_pn);
|
|
m.dlna_pn = NULL;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case AV_CODEC_ID_MSMPEG4V3:
|
|
xasprintf(&m.mime, "video/x-msvideo");
|
|
default:
|
|
DPRINTF(E_DEBUG, L_METADATA, "Stream %d of %s is %s [type %d]\n",
|
|
video_stream, basepath, m.resolution, lav_codec_id(vstream));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( strcmp(ctx->iformat->name, "asf") == 0 )
|
|
{
|
|
if( readtags((char *)path, &video, &file, "en_US", "asf") == 0 )
|
|
{
|
|
if( video.title && *video.title )
|
|
{
|
|
m.title = escape_tag(trim(video.title), 1);
|
|
}
|
|
if( video.genre && *video.genre )
|
|
{
|
|
m.genre = escape_tag(trim(video.genre), 1);
|
|
}
|
|
if( video.contributor[ROLE_TRACKARTIST] && *video.contributor[ROLE_TRACKARTIST] )
|
|
{
|
|
m.artist = escape_tag(trim(video.contributor[ROLE_TRACKARTIST]), 1);
|
|
}
|
|
if( video.contributor[ROLE_ALBUMARTIST] && *video.contributor[ROLE_ALBUMARTIST] )
|
|
{
|
|
m.creator = escape_tag(trim(video.contributor[ROLE_ALBUMARTIST]), 1);
|
|
}
|
|
else
|
|
{
|
|
m.creator = m.artist;
|
|
free_flags &= ~FLAG_CREATOR;
|
|
}
|
|
if (!m.thumb_data)
|
|
{
|
|
m.thumb_data = video.image;
|
|
m.thumb_size = video.image_size;
|
|
}
|
|
}
|
|
}
|
|
#ifndef NETGEAR
|
|
#if LIBAVFORMAT_VERSION_INT >= ((52<<16)+(31<<8)+0)
|
|
else if( strcmp(ctx->iformat->name, "mov,mp4,m4a,3gp,3g2,mj2") == 0 )
|
|
{
|
|
if( ctx->metadata )
|
|
{
|
|
AVDictionaryEntry *tag = NULL;
|
|
|
|
//DEBUG DPRINTF(E_DEBUG, L_METADATA, "Metadata:\n");
|
|
while( (tag = av_dict_get(ctx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX)) )
|
|
{
|
|
//DEBUG DPRINTF(E_DEBUG, L_METADATA, " %-16s: %s\n", tag->key, tag->value);
|
|
if( strcmp(tag->key, "title") == 0 )
|
|
m.title = escape_tag(trim(tag->value), 1);
|
|
else if( strcmp(tag->key, "genre") == 0 )
|
|
m.genre = escape_tag(trim(tag->value), 1);
|
|
else if( strcmp(tag->key, "artist") == 0 )
|
|
m.artist = escape_tag(trim(tag->value), 1);
|
|
else if( strcmp(tag->key, "comment") == 0 )
|
|
m.comment = escape_tag(trim(tag->value), 1);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
#endif
|
|
video_no_dlna:
|
|
|
|
#ifdef TIVO_SUPPORT
|
|
if( ends_with(path, ".TiVo") && is_tivo_file(path) )
|
|
{
|
|
if( m.dlna_pn )
|
|
{
|
|
free(m.dlna_pn);
|
|
m.dlna_pn = NULL;
|
|
}
|
|
m.mime = realloc(m.mime, 21);
|
|
strcpy(m.mime, "video/x-tivo-mpeg");
|
|
}
|
|
#endif
|
|
|
|
strcpy(nfo, path);
|
|
ext = strrchr(nfo, '.');
|
|
if( ext )
|
|
{
|
|
strcpy(ext+1, "nfo");
|
|
if( access(nfo, R_OK) == 0 )
|
|
parse_nfo(nfo, &m);
|
|
}
|
|
|
|
if( !m.mime )
|
|
{
|
|
if( strcmp(ctx->iformat->name, "avi") == 0 )
|
|
xasprintf(&m.mime, "video/x-msvideo");
|
|
else if( strncmp(ctx->iformat->name, "mpeg", 4) == 0 )
|
|
xasprintf(&m.mime, "video/mpeg");
|
|
else if( strcmp(ctx->iformat->name, "asf") == 0 )
|
|
xasprintf(&m.mime, "video/x-ms-wmv");
|
|
else if( strcmp(ctx->iformat->name, "mov,mp4,m4a,3gp,3g2,mj2") == 0 )
|
|
if( ends_with(path, ".mov") )
|
|
xasprintf(&m.mime, "video/quicktime");
|
|
else
|
|
xasprintf(&m.mime, "video/mp4");
|
|
else if( strncmp(ctx->iformat->name, "matroska", 8) == 0 )
|
|
xasprintf(&m.mime, "video/x-matroska");
|
|
else if( strcmp(ctx->iformat->name, "flv") == 0 )
|
|
xasprintf(&m.mime, "video/x-flv");
|
|
else if( strcmp(ctx->iformat->name, "rm") == 0 )
|
|
xasprintf(&m.mime, "application/vnd.rn-realmedia");
|
|
else if( strcmp(ctx->iformat->name, "rmvb") == 0 )
|
|
xasprintf(&m.mime, "application/vnd.rn-realmedia-vbr");
|
|
else
|
|
DPRINTF(E_WARN, L_METADATA, "%s: Unhandled format: %s\n", path, ctx->iformat->name);
|
|
}
|
|
|
|
if( !m.date )
|
|
{
|
|
m.date = malloc(20);
|
|
modtime = localtime(&file.st_mtime);
|
|
strftime(m.date, 20, "%FT%T", modtime);
|
|
}
|
|
|
|
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);
|
|
lav_close(ctx);
|
|
|
|
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, DISC, TRACK) "
|
|
"VALUES"
|
|
" (%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.disc, m.track);
|
|
if( ret != SQLITE_OK )
|
|
{
|
|
DPRINTF(E_ERROR, L_METADATA, "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);
|
|
free(path_cpy);
|
|
|
|
return ret;
|
|
}
|