Compare commits

...

2 Commits

Author SHA1 Message Date
Storm Dragon
4cbe796774 Attempt to add opus support. 2025-04-05 02:03:06 -04:00
Storm Dragon
326b491fde Attempt to add opus support. 2025-04-05 02:02:50 -04:00
7 changed files with 661 additions and 524 deletions

View File

@ -15,7 +15,7 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA. # USA.
AM_CFLAGS = -Wall -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 AM_CFLAGS = -Wall -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 @libpng_CFLAGS@ @opus_CFLAGS@
SUBDIRS=po SUBDIRS=po
@ -55,14 +55,15 @@ endif
endif endif
minidlnad_LDADD = \ minidlnad_LDADD = \
@LIBJPEG_LIBS@ \ @LIBJPEG_LIBS@ \
@LIBID3TAG_LIBS@ \ @LIBID3TAG_LIBS@ \
@LIBSQLITE3_LIBS@ \ @LIBSQLITE3_LIBS@ \
@LIBAVFORMAT_LIBS@ \ @LIBAVFORMAT_LIBS@ \
@LIBEXIF_LIBS@ \ @LIBEXIF_LIBS@ \
@LIBINTL@ \ @LIBINTL@ \
@LIBICONV@ \ @LIBICONV@ \
-lFLAC $(flacogglibs) $(vorbislibs) $(avahilibs) -lFLAC $(flacogglibs) $(vorbislibs) $(avahilibs) \
@opus_LIBS@
testupnpdescgen_SOURCES = testupnpdescgen.c upnpdescgen.c testupnpdescgen_SOURCES = testupnpdescgen.c upnpdescgen.c
testupnpdescgen_LDADD = \ testupnpdescgen_LDADD = \

View File

@ -462,6 +462,12 @@ AC_CHECK_LIB(vorbisfile, ov_open_callbacks,
AM_CONDITIONAL(HAVE_VORBISFILE, false))], AM_CONDITIONAL(HAVE_VORBISFILE, false))],
AM_CONDITIONAL(HAVE_VORBISFILE, false), AM_CONDITIONAL(HAVE_VORBISFILE, false),
-lvorbis -logg) -lvorbis -logg)
#test if we have opus/opusfile
PKG_CHECK_MODULES([opus], [opus opusfile],
AM_CONDITIONAL(HAVE_OPUS, true)
AC_DEFINE(HAVE_OPUS,1,[Define to 1 if you have opus/opusfile]),
AM_CONDITIONAL(HAVE_OPUS, false))
AC_CHECK_LIB(FLAC, FLAC__stream_decoder_init_stream, AC_CHECK_LIB(FLAC, FLAC__stream_decoder_init_stream,
[AC_CHECK_HEADERS([FLAC/all.h], [AC_CHECK_HEADERS([FLAC/all.h],
AM_CONDITIONAL(HAVE_FLAC, true) AM_CONDITIONAL(HAVE_FLAC, true)

View File

@ -343,11 +343,55 @@ GetAudioMetadata(const char *path, const char *name)
strcpy(type, "wav"); strcpy(type, "wav");
m.mime = strdup("audio/x-wav"); m.mime = strdup("audio/x-wav");
} }
else if( ends_with(path, ".ogg") || ends_with(path, ".oga") ) else if( ends_with(path,".oga") || ends_with(path,".ogg"))
{ {
strcpy(type, "ogg"); /* The .ogg/.oga file extensions present something of a problem.
m.mime = strdup("audio/ogg"); * ".ogg" has been deprecated in favor of ".oga" for some time, but
} * many applications still only recognize ".ogg".
*
* This examines the file and causes .ogg to be presented for any naked
* Vorbis file (MIME type audio/ogg; codecs=vorbis) and .oga
* (audio/ogg) to be used for everything else. This is in line with
* the official ogg naming conventions and, hopefully, makes for a
* resonable compromise.
*/
uint8_t oggtestbuf[35];
FILE *oggfile = fopen (path, "rb");
if (oggfile == (FILE *)NULL)
{
DPRINTF(E_ERROR, L_METADATA, "Error opening %s\n", path);
return 0;
}
if (fread (oggtestbuf, 1, 35, oggfile) != 35)
{
DPRINTF(E_WARN, L_METADATA, "Premature EOF on %s\n", path);
fclose (oggfile);
return 0;
}
fclose (oggfile);
if (memcmp (&oggtestbuf[28], "\x01vorbis", 7))
m.mime = strdup ("audio/ogg");
else
m.mime = strdup ("audio/ogg; codecs=vorbis");
strcpy(type, "ogg");
}
else if ( ends_with(path, ".opus") )
{
strcpy(type,"ops");
m.mime = strdup("audio/ogg; codecs=opus");
}
#if 0
/* Not supported yet, and probably won't be. */
else if( ends_with(path, ".ogx") )
{
strcpy(type, "ogx");
m.mime = strdup("application/ogg");
}
#endif
else if( ends_with(path, ".pcm") ) else if( ends_with(path, ".pcm") )
{ {
strcpy(type, "pcm"); strcpy(type, "pcm");

59
tagutils/opus.c Normal file
View File

@ -0,0 +1,59 @@
//=========================================================================
// FILENAME : tagutils-opus.c
// DESCRIPTION : Opus metadata reader
//=========================================================================
static int
_get_opusfileinfo(char *filename, struct song_metadata *psong)
{
OggOpusFile *opusfile;
const OpusTags *tags;
char **comment;
int *commentlen;
int j, e;
opusfile = op_open_file (filename, &e);
if(!opusfile)
{
DPRINTF(E_WARN, L_SCANNER,
"Error opening input file \"%s\": %s\n", filename, opus_strerror(e));
return -1;
}
DPRINTF(E_MAXDEBUG, L_SCANNER, "Processing file \"%s\"...\n", filename);
psong->song_length = op_pcm_total (opusfile, -1);
if (psong->song_length < 0)
{
DPRINTF(E_WARN, L_SCANNER,
"Unable to obtain length of %s\n", filename);
psong->song_length = 0;
} else
/* Sample rate is always 48k, so length in ms is just samples/48 */
psong->song_length /= 48;
/* Note that this gives only the first link's channel count. */
psong->channels = op_channel_count (opusfile, -1);
psong->samplerate = 48000;
psong->bitrate = op_bitrate (opusfile, -1);
tags = op_tags (opusfile, -1);
if (!tags)
{
DPRINTF(E_WARN, L_SCANNER, "Unable to obtain tags from %s\n",
filename);
return -1;
}
comment = tags->user_comments;
commentlen = tags->comment_lengths;
for (j = 0; j < tags->comments; j++)
vc_scan (psong, *(comment++), *(commentlen++));
op_free (opusfile);
return 0;
}

6
tagutils/opus.h Normal file
View File

@ -0,0 +1,6 @@
//=========================================================================
// FILENAME : tagutils-opus.h
// DESCRIPTION : Opus metadata reader
//=========================================================================
static int _get_opusfileinfo(char *filename, struct song_metadata *psong);

View File

@ -1,6 +1,6 @@
//========================================================================= //=========================================================================
// FILENAME : tagutils.c // FILENAME : tagutils.c
// DESCRIPTION : MP3/MP4/Ogg/FLAC metadata reader // DESCRIPTION : MP3/MP4/Ogg/FLAC metadata reader
//========================================================================= //=========================================================================
// Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved. // Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved.
//========================================================================= //=========================================================================
@ -42,6 +42,10 @@
#ifdef HAVE_ICONV #ifdef HAVE_ICONV
#include <iconv.h> #include <iconv.h>
#endif #endif
/* ADD THIS INCLUDE FOR OPUS */
#ifdef HAVE_OPUS
#include <opusfile.h>
#endif
#include <sqlite3.h> #include <sqlite3.h>
#include "tagutils.h" #include "tagutils.h"
#include "../metadata.h" #include "../metadata.h"
@ -49,51 +53,51 @@
#include "../log.h" #include "../log.h"
struct id3header { struct id3header {
unsigned char id[3]; unsigned char id[3];
unsigned char version[2]; unsigned char version[2];
unsigned char flags; unsigned char flags;
unsigned char size[4]; unsigned char size[4];
} __attribute((packed)); } __attribute((packed));
char *winamp_genre[] = { char *winamp_genre[] = {
/*00*/ "Blues", "Classic Rock", "Country", "Dance", /*00*/ "Blues", "Classic Rock", "Country", "Dance",
"Disco", "Funk", "Grunge", "Hip-Hop", "Disco", "Funk", "Grunge", "Hip-Hop",
/*08*/ "Jazz", "Metal", "New Age", "Oldies", /*08*/ "Jazz", "Metal", "New Age", "Oldies",
"Other", "Pop", "R&B", "Rap", "Other", "Pop", "R&B", "Rap",
/*10*/ "Reggae", "Rock", "Techno", "Industrial", /*10*/ "Reggae", "Rock", "Techno", "Industrial",
"Alternative", "Ska", "Death Metal", "Pranks", "Alternative", "Ska", "Death Metal", "Pranks",
/*18*/ "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", /*18*/ "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop",
"Vocal", "Jazz+Funk", "Fusion", "Trance", "Vocal", "Jazz+Funk", "Fusion", "Trance",
/*20*/ "Classical", "Instrumental", "Acid", "House", /*20*/ "Classical", "Instrumental", "Acid", "House",
"Game", "Sound Clip", "Gospel", "Noise", "Game", "Sound Clip", "Gospel", "Noise",
/*28*/ "AlternRock", "Bass", "Soul", "Punk", /*28*/ "AlternRock", "Bass", "Soul", "Punk",
"Space", "Meditative", "Instrumental Pop", "Instrumental Rock", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock",
/*30*/ "Ethnic", "Gothic", "Darkwave", "Techno-Industrial", /*30*/ "Ethnic", "Gothic", "Darkwave", "Techno-Industrial",
"Electronic", "Pop-Folk", "Eurodance", "Dream", "Electronic", "Pop-Folk", "Eurodance", "Dream",
/*38*/ "Southern Rock", "Comedy", "Cult", "Gangsta", /*38*/ "Southern Rock", "Comedy", "Cult", "Gangsta",
"Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Top 40", "Christian Rap", "Pop/Funk", "Jungle",
/*40*/ "Native American", "Cabaret", "New Wave", "Psychedelic", /*40*/ "Native American", "Cabaret", "New Wave", "Psychedelic",
"Rave", "Showtunes", "Trailer", "Lo-Fi", "Rave", "Showtunes", "Trailer", "Lo-Fi",
/*48*/ "Tribal", "Acid Punk", "Acid Jazz", "Polka", /*48*/ "Tribal", "Acid Punk", "Acid Jazz", "Polka",
"Retro", "Musical", "Rock & Roll", "Hard Rock", "Retro", "Musical", "Rock & Roll", "Hard Rock",
/*50*/ "Folk", "Folk/Rock", "National folk", "Swing", /*50*/ "Folk", "Folk/Rock", "National folk", "Swing",
"Fast-fusion", "Bebob", "Latin", "Revival", "Fast-fusion", "Bebob", "Latin", "Revival",
/*58*/ "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", /*58*/ "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock",
"Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock",
/*60*/ "Big Band", "Chorus", "Easy Listening", "Acoustic", /*60*/ "Big Band", "Chorus", "Easy Listening", "Acoustic",
"Humour", "Speech", "Chanson", "Opera", "Humour", "Speech", "Chanson", "Opera",
/*68*/ "Chamber Music", "Sonata", "Symphony", "Booty Bass", /*68*/ "Chamber Music", "Sonata", "Symphony", "Booty Bass",
"Primus", "Porn Groove", "Satire", "Slow Jam", "Primus", "Porn Groove", "Satire", "Slow Jam",
/*70*/ "Club", "Tango", "Samba", "Folklore", /*70*/ "Club", "Tango", "Samba", "Folklore",
"Ballad", "Powder Ballad", "Rhythmic Soul", "Freestyle", "Ballad", "Powder Ballad", "Rhythmic Soul", "Freestyle",
/*78*/ "Duet", "Punk Rock", "Drum Solo", "A Capella", /*78*/ "Duet", "Punk Rock", "Drum Solo", "A Capella",
"Euro-House", "Dance Hall", "Goa", "Drum & Bass", "Euro-House", "Dance Hall", "Goa", "Drum & Bass",
/*80*/ "Club House", "Hardcore", "Terror", "Indie", /*80*/ "Club House", "Hardcore", "Terror", "Indie",
"BritPop", "NegerPunk", "Polsk Punk", "Beat", "BritPop", "NegerPunk", "Polsk Punk", "Beat",
/*88*/ "Christian Gangsta", "Heavy Metal", "Black Metal", "Crossover", /*88*/ "Christian Gangsta", "Heavy Metal", "Black Metal", "Crossover",
"Contemporary C", "Christian Rock", "Merengue", "Salsa", "Contemporary C", "Christian Rock", "Merengue", "Salsa",
/*90*/ "Thrash Metal", "Anime", "JPop", "SynthPop", /*90*/ "Thrash Metal", "Anime", "JPop", "SynthPop",
"Unknown" "Unknown"
}; };
#define WINAMP_GENRE_UNKNOWN ((sizeof(winamp_genre) / sizeof(winamp_genre[0])) - 1) #define WINAMP_GENRE_UNKNOWN ((sizeof(winamp_genre) / sizeof(winamp_genre[0])) - 1)
@ -104,8 +108,9 @@ char *winamp_genre[] = {
*/ */
#include "tagutils-mp3.h" #include "tagutils-mp3.h"
#include "tagutils-aac.h" #include "tagutils-aac.h"
#ifdef HAVE_VORBISFILE
#include "tagutils-ogg.h" #include "tagutils-ogg.h"
#ifdef HAVE_OPUS
#include "tagutils-opus.h"
#endif #endif
#include "tagutils-flc.h" #include "tagutils-flc.h"
#include "tagutils-asf.h" #include "tagutils-asf.h"
@ -123,24 +128,25 @@ static int _get_fileinfo(char *file, struct song_metadata *psong);
*/ */
typedef struct { typedef struct {
char* type; char* type;
int (*get_tags)(char* file, struct song_metadata* psong); int (*get_tags)(char* file, struct song_metadata* psong);
int (*get_fileinfo)(char* file, struct song_metadata* psong); int (*get_fileinfo)(char* file, struct song_metadata* psong);
} taghandler; } taghandler;
static taghandler taghandlers[] = { static taghandler taghandlers[] = {
{ "aac", _get_aactags, _get_aacfileinfo }, { "aac", _get_aactags, _get_aacfileinfo },
{ "mp3", _get_mp3tags, _get_mp3fileinfo }, { "mp3", _get_mp3tags, _get_mp3fileinfo },
{ "flc", _get_flctags, _get_flcfileinfo }, { "flc", _get_flctags, _get_flcfileinfo },
#ifdef HAVE_VORBISFILE { "ogg", 0, _get_oggfileinfo },
{ "ogg", NULL, _get_oggfileinfo }, #ifdef HAVE_OPUS
{ "ops", 0, _get_opusfileinfo },
#endif #endif
{ "asf", NULL, _get_asffileinfo }, { "asf", 0, _get_asffileinfo },
{ "wav", _get_wavtags, _get_wavfileinfo }, { "wav", _get_wavtags, _get_wavfileinfo },
{ "pcm", NULL, _get_pcmfileinfo }, { "pcm", 0, _get_pcmfileinfo },
{ "dsf", _get_dsftags, _get_dsffileinfo }, { "dsf", _get_dsftags, _get_dsffileinfo },
{ "dff", NULL, _get_dfffileinfo }, { "dff", 0, _get_dfffileinfo },
{ NULL, NULL, NULL } { NULL, NULL, NULL }
}; };
@ -149,8 +155,9 @@ static taghandler taghandlers[] = {
#include "tagutils-misc.c" #include "tagutils-misc.c"
#include "tagutils-mp3.c" #include "tagutils-mp3.c"
#include "tagutils-aac.c" #include "tagutils-aac.c"
#ifdef HAVE_VORBISFILE
#include "tagutils-ogg.c" #include "tagutils-ogg.c"
#ifdef HAVE_OPUS
#include "tagutils-opus.c"
#endif #endif
#include "tagutils-flc.c" #include "tagutils-flc.c"
#include "tagutils-asf.c" #include "tagutils-asf.c"
@ -166,86 +173,86 @@ static taghandler taghandlers[] = {
void void
freetags(struct song_metadata *psong) freetags(struct song_metadata *psong)
{ {
int role; int role;
MAYBEFREE(psong->path); MAYBEFREE(psong->path);
MAYBEFREE(psong->image); MAYBEFREE(psong->image);
MAYBEFREE(psong->title); MAYBEFREE(psong->title);
MAYBEFREE(psong->album); MAYBEFREE(psong->album);
MAYBEFREE(psong->genre); MAYBEFREE(psong->genre);
MAYBEFREE(psong->comment); MAYBEFREE(psong->comment);
for(role = ROLE_START; role <= ROLE_LAST; role++) for(role = ROLE_START; role <= ROLE_LAST; role++)
{ {
MAYBEFREE(psong->contributor[role]); MAYBEFREE(psong->contributor[role]);
MAYBEFREE(psong->contributor_sort[role]); MAYBEFREE(psong->contributor_sort[role]);
} }
MAYBEFREE(psong->grouping); MAYBEFREE(psong->grouping);
MAYBEFREE(psong->mime); MAYBEFREE(psong->mime);
MAYBEFREE(psong->dlna_pn); MAYBEFREE(psong->dlna_pn);
MAYBEFREE(psong->tagversion); MAYBEFREE(psong->tagversion);
MAYBEFREE(psong->musicbrainz_albumid); MAYBEFREE(psong->musicbrainz_albumid);
MAYBEFREE(psong->musicbrainz_trackid); MAYBEFREE(psong->musicbrainz_trackid);
MAYBEFREE(psong->musicbrainz_artistid); MAYBEFREE(psong->musicbrainz_artistid);
MAYBEFREE(psong->musicbrainz_albumartistid); MAYBEFREE(psong->musicbrainz_albumartistid);
} }
// _get_fileinfo // _get_fileinfo
static int static int
_get_fileinfo(char *file, struct song_metadata *psong) _get_fileinfo(char *file, struct song_metadata *psong)
{ {
taghandler *hdl; taghandler *hdl;
// dispatch to appropriate tag handler // dispatch to appropriate tag handler
for(hdl = taghandlers; hdl->type; ++hdl) for(hdl = taghandlers; hdl->type; ++hdl)
if(!strcmp(hdl->type, psong->type)) if(!strcmp(hdl->type, psong->type))
break; break;
if(hdl->get_fileinfo) if(hdl->get_fileinfo)
return hdl->get_fileinfo(file, psong); return hdl->get_fileinfo(file, psong);
return 0; return 0;
} }
static void static void
_make_composite_tags(struct song_metadata *psong) _make_composite_tags(struct song_metadata *psong)
{ {
int len; int len;
len = 1; len = 1;
if(!psong->contributor[ROLE_ARTIST] && if(!psong->contributor[ROLE_ARTIST] &&
(psong->contributor[ROLE_BAND] || psong->contributor[ROLE_CONDUCTOR])) (psong->contributor[ROLE_BAND] || psong->contributor[ROLE_CONDUCTOR]))
{ {
if(psong->contributor[ROLE_BAND]) if(psong->contributor[ROLE_BAND])
len += strlen(psong->contributor[ROLE_BAND]); len += strlen(psong->contributor[ROLE_BAND]);
if(psong->contributor[ROLE_CONDUCTOR]) if(psong->contributor[ROLE_CONDUCTOR])
len += strlen(psong->contributor[ROLE_CONDUCTOR]); len += strlen(psong->contributor[ROLE_CONDUCTOR]);
len += 3; len += 3;
psong->contributor[ROLE_ARTIST] = (char*)calloc(len, 1); psong->contributor[ROLE_ARTIST] = (char*)calloc(len, 1);
if(psong->contributor[ROLE_ARTIST]) if(psong->contributor[ROLE_ARTIST])
{ {
if(psong->contributor[ROLE_BAND]) if(psong->contributor[ROLE_BAND])
strcat(psong->contributor[ROLE_ARTIST], psong->contributor[ROLE_BAND]); strcat(psong->contributor[ROLE_ARTIST], psong->contributor[ROLE_BAND]);
if(psong->contributor[ROLE_BAND] && psong->contributor[ROLE_CONDUCTOR]) if(psong->contributor[ROLE_BAND] && psong->contributor[ROLE_CONDUCTOR])
strcat(psong->contributor[ROLE_ARTIST], " - "); strcat(psong->contributor[ROLE_ARTIST], " - ");
if(psong->contributor[ROLE_CONDUCTOR]) if(psong->contributor[ROLE_CONDUCTOR])
strcat(psong->contributor[ROLE_ARTIST], psong->contributor[ROLE_CONDUCTOR]); strcat(psong->contributor[ROLE_ARTIST], psong->contributor[ROLE_CONDUCTOR]);
} }
} }
#if 0 // already taken care of by scanner.c #if 0 // already taken care of by scanner.c
if(!psong->title) if(!psong->title)
{ {
char *suffix; char *suffix;
psong->title = strdup(psong->basename); psong->title = strdup(psong->basename);
suffix = strrchr(psong->title, '.'); suffix = strrchr(psong->title, '.');
if(suffix) *suffix = '\0'; if(suffix) *suffix = '\0';
} }
#endif #endif
} }
@ -255,19 +262,19 @@ _make_composite_tags(struct song_metadata *psong)
static int static int
_get_tags(char *file, struct song_metadata *psong) _get_tags(char *file, struct song_metadata *psong)
{ {
taghandler *hdl; taghandler *hdl;
// dispatch // dispatch
for(hdl = taghandlers ; hdl->type ; ++hdl) for(hdl = taghandlers ; hdl->type ; ++hdl)
if(!strcasecmp(hdl->type, psong->type)) if(!strcasecmp(hdl->type, psong->type))
break; break;
if(hdl->get_tags) if(hdl->get_tags)
{ {
return hdl->get_tags(file, psong); return hdl->get_tags(file, psong);
} }
return 0; return 0;
} }
/*****************************************************************************/ /*****************************************************************************/
@ -275,31 +282,31 @@ _get_tags(char *file, struct song_metadata *psong)
int int
readtags(char *path, struct song_metadata *psong, struct stat *stat, char *lang, char *type) readtags(char *path, struct song_metadata *psong, struct stat *stat, char *lang, char *type)
{ {
char *fname; char *fname;
if(lang_index == -1) if(lang_index == -1)
lang_index = _lang2cp(lang); lang_index = _lang2cp(lang);
memset((void*)psong, 0, sizeof(struct song_metadata)); memset((void*)psong, 0, sizeof(struct song_metadata));
psong->path = strdup(path); psong->path = strdup(path);
psong->type = type; psong->type = type;
fname = strrchr(psong->path, '/'); fname = strrchr(psong->path, '/');
psong->basename = fname ? fname + 1 : psong->path; psong->basename = fname ? fname + 1 : psong->path;
if(stat) if(stat)
{ {
if(!psong->time_modified) if(!psong->time_modified)
psong->time_modified = stat->st_mtime; psong->time_modified = stat->st_mtime;
psong->file_size = stat->st_size; psong->file_size = stat->st_size;
} }
// get tag // get tag
if( _get_tags(path, psong) == 0 ) if( _get_tags(path, psong) == 0 )
{ {
_make_composite_tags(psong); _make_composite_tags(psong);
} }
// get fileinfo // get fileinfo
return _get_fileinfo(path, psong); return _get_fileinfo(path, psong);
} }

738
utils.c
View File

@ -38,503 +38,517 @@
int int
xasprintf(char **strp, char *fmt, ...) xasprintf(char **strp, char *fmt, ...)
{ {
va_list args; va_list args;
int ret; int ret;
va_start(args, fmt); va_start(args, fmt);
ret = vasprintf(strp, fmt, args); ret = vasprintf(strp, fmt, args);
va_end(args); va_end(args);
if( ret < 0 ) if( ret < 0 )
{ {
DPRINTF(E_WARN, L_GENERAL, "xasprintf: allocation failed\n"); DPRINTF(E_WARN, L_GENERAL, "xasprintf: allocation failed\n");
*strp = NULL; *strp = NULL;
} }
return ret; return ret;
} }
int int
ends_with(const char * haystack, const char * needle) ends_with(const char * haystack, const char * needle)
{ {
const char * end; const char * end;
int nlen = strlen(needle); int nlen = strlen(needle);
int hlen = strlen(haystack); int hlen = strlen(haystack);
if( nlen > hlen ) if( nlen > hlen )
return 0; return 0;
end = haystack + hlen - nlen; end = haystack + hlen - nlen;
return (strcasecmp(end, needle) ? 0 : 1); return (strcasecmp(end, needle) ? 0 : 1);
} }
char * char *
trim(char *str) trim(char *str)
{ {
int i; int i;
int len; int len;
if (!str) if (!str)
return(NULL); return(NULL);
len = strlen(str); len = strlen(str);
for (i=len-1; i >= 0 && isspace(str[i]); i--) for (i=len-1; i >= 0 && isspace(str[i]); i--)
{ {
str[i] = '\0'; str[i] = '\0';
len--; len--;
} }
while (isspace(*str)) while (isspace(*str))
{ {
str++; str++;
len--; len--;
} }
if (str[0] == '"' && str[len-1] == '"') if (str[0] == '"' && str[len-1] == '"')
{ {
str[0] = '\0'; str[0] = '\0';
str[len-1] = '\0'; str[len-1] = '\0';
str++; str++;
} }
return str; return str;
} }
/* Find the first occurrence of p in s, where s is terminated by t */ /* Find the first occurrence of p in s, where s is terminated by t */
char * char *
strstrc(const char *s, const char *p, const char t) strstrc(const char *s, const char *p, const char t)
{ {
char *endptr; char *endptr;
size_t slen, plen; size_t slen, plen;
endptr = strchr(s, t); endptr = strchr(s, t);
if (!endptr) if (!endptr)
return strstr(s, p); return strstr(s, p);
plen = strlen(p); plen = strlen(p);
slen = endptr - s; slen = endptr - s;
while (slen >= plen) while (slen >= plen)
{ {
if (*s == *p && strncmp(s+1, p+1, plen-1) == 0) if (*s == *p && strncmp(s+1, p+1, plen-1) == 0)
return (char*)s; return (char*)s;
s++; s++;
slen--; slen--;
} }
return NULL; return NULL;
} }
char * char *
strcasestrc(const char *s, const char *p, const char t) strcasestrc(const char *s, const char *p, const char t)
{ {
char *endptr; char *endptr;
size_t slen, plen; size_t slen, plen;
endptr = strchr(s, t); endptr = strchr(s, t);
if (!endptr) if (!endptr)
return strcasestr(s, p); return strcasestr(s, p);
plen = strlen(p); plen = strlen(p);
slen = endptr - s; slen = endptr - s;
while (slen >= plen) while (slen >= plen)
{ {
if (*s == *p && strncasecmp(s+1, p+1, plen-1) == 0) if (*s == *p && strncasecmp(s+1, p+1, plen-1) == 0)
return (char*)s; return (char*)s;
s++; s++;
slen--; slen--;
} }
return NULL; return NULL;
} }
char * char *
modifyString(char *string, const char *before, const char *after, int noalloc) modifyString(char *string, const char *before, const char *after, int noalloc)
{ {
int oldlen, newlen, chgcnt = 0; int oldlen, newlen, chgcnt = 0;
char *s, *p; char *s, *p;
/* If there is no match, just return */ /* If there is no match, just return */
s = strstr(string, before); s = strstr(string, before);
if (!s) if (!s)
return string; return string;
oldlen = strlen(before); oldlen = strlen(before);
newlen = strlen(after); newlen = strlen(after);
if (newlen > oldlen) if (newlen > oldlen)
{ {
if (noalloc) if (noalloc)
return string; return string;
while ((p = strstr(s, before))) while ((p = strstr(s, before)))
{ {
chgcnt++; chgcnt++;
s = p + oldlen; s = p + oldlen;
} }
s = realloc(string, strlen(string)+((newlen-oldlen)*chgcnt)+1); s = realloc(string, strlen(string)+((newlen-oldlen)*chgcnt)+1);
/* If we failed to realloc, return the original alloc'd string */ /* If we failed to realloc, return the original alloc'd string */
if( s ) if( s )
string = s; string = s;
else else
return string; return string;
} }
s = string; s = string;
while (s) while (s)
{ {
p = strstr(s, before); p = strstr(s, before);
if (!p) if (!p)
return string; return string;
memmove(p + newlen, p + oldlen, strlen(p + oldlen) + 1); memmove(p + newlen, p + oldlen, strlen(p + oldlen) + 1);
memcpy(p, after, newlen); memcpy(p, after, newlen);
s = p + newlen; s = p + newlen;
} }
return string; return string;
} }
char * char *
unescape_tag(const char *tag, int force_alloc) unescape_tag(const char *tag, int force_alloc)
{ {
char *esc_tag = NULL; char *esc_tag = NULL;
if (strchr(tag, '&') && if (strchr(tag, '&') &&
(strstr(tag, "&amp;") || strstr(tag, "&lt;") || strstr(tag, "&gt;") || (strstr(tag, "&amp;") || strstr(tag, "&lt;") || strstr(tag, "&gt;") ||
strstr(tag, "&quot;") || strstr(tag, "&apos;"))) strstr(tag, "&quot;") || strstr(tag, "&apos;")))
{ {
esc_tag = strdup(tag); esc_tag = strdup(tag);
esc_tag = modifyString(esc_tag, "&amp;", "&", 1); esc_tag = modifyString(esc_tag, "&amp;", "&", 1);
esc_tag = modifyString(esc_tag, "&lt;", "<", 1); esc_tag = modifyString(esc_tag, "&lt;", "<", 1);
esc_tag = modifyString(esc_tag, "&gt;", ">", 1); esc_tag = modifyString(esc_tag, "&gt;", ">", 1);
esc_tag = modifyString(esc_tag, "&quot;", "\"", 1); esc_tag = modifyString(esc_tag, "&quot;", "\"", 1);
esc_tag = modifyString(esc_tag, "&apos;", "'", 1); esc_tag = modifyString(esc_tag, "&apos;", "'", 1);
} }
else if( force_alloc ) else if( force_alloc )
esc_tag = strdup(tag); esc_tag = strdup(tag);
return esc_tag; return esc_tag;
} }
char * char *
escape_tag(const char *tag, int force_alloc) escape_tag(const char *tag, int force_alloc)
{ {
char *esc_tag = NULL; char *esc_tag = NULL;
if( strchr(tag, '&') || strchr(tag, '<') || strchr(tag, '>') || strchr(tag, '"') ) if( strchr(tag, '&') || strchr(tag, '<') || strchr(tag, '>') || strchr(tag, '"') )
{ {
esc_tag = strdup(tag); esc_tag = strdup(tag);
esc_tag = modifyString(esc_tag, "&", "&amp;amp;", 0); esc_tag = modifyString(esc_tag, "&", "&amp;amp;", 0);
esc_tag = modifyString(esc_tag, "<", "&amp;lt;", 0); esc_tag = modifyString(esc_tag, "<", "&amp;lt;", 0);
esc_tag = modifyString(esc_tag, ">", "&amp;gt;", 0); esc_tag = modifyString(esc_tag, ">", "&amp;gt;", 0);
esc_tag = modifyString(esc_tag, "\"", "&amp;quot;", 0); esc_tag = modifyString(esc_tag, "\"", "&amp;quot;", 0);
} }
else if( force_alloc ) else if( force_alloc )
esc_tag = strdup(tag); esc_tag = strdup(tag);
return esc_tag; return esc_tag;
} }
char * char *
duration_str(int msec) duration_str(int msec)
{ {
char *str; char *str;
xasprintf(&str, "%d:%02d:%02d.%03d", xasprintf(&str, "%d:%02d:%02d.%03d",
(msec / 3600000), (msec / 3600000),
(msec / 60000 % 60), (msec / 60000 % 60),
(msec / 1000 % 60), (msec / 1000 % 60),
(msec % 1000)); (msec % 1000));
return str; return str;
} }
char * char *
strip_ext(char *name) strip_ext(char *name)
{ {
char *period; char *period;
if (!name) if (!name)
return NULL; return NULL;
period = strrchr(name, '.'); period = strrchr(name, '.');
if (period) if (period)
*period = '\0'; *period = '\0';
return period; return period;
} }
/* Code basically stolen from busybox */ /* Code basically stolen from busybox */
int int
make_dir(char * path, mode_t mode) make_dir(char * path, mode_t mode)
{ {
char * s = path; char * s = path;
char c; char c;
struct stat st; struct stat st;
do { do {
c = '\0'; c = '\0';
/* Before we do anything, skip leading /'s, so we don't bother /* Before we do anything, skip leading /'s, so we don't bother
* trying to create /. */ * trying to create /. */
while (*s == '/') while (*s == '/')
++s; ++s;
/* Bypass leading non-'/'s and then subsequent '/'s. */ /* Bypass leading non-'/'s and then subsequent '/'s. */
while (*s) { while (*s) {
if (*s == '/') { if (*s == '/') {
do { do {
++s; ++s;
} while (*s == '/'); } while (*s == '/');
c = *s; /* Save the current char */ c = *s; /* Save the current char */
*s = '\0'; /* and replace it with nul. */ *s = '\0'; /* and replace it with nul. */
break; break;
} }
++s; ++s;
} }
if (mkdir(path, mode) < 0) { if (mkdir(path, mode) < 0) {
/* If we failed for any other reason than the directory /* If we failed for any other reason than the directory
* already exists, output a diagnostic and return -1.*/ * already exists, output a diagnostic and return -1.*/
if ((errno != EEXIST && errno != EISDIR) if ((errno != EEXIST && errno != EISDIR)
|| (stat(path, &st) < 0 || !S_ISDIR(st.st_mode))) { || (stat(path, &st) < 0 || !S_ISDIR(st.st_mode))) {
DPRINTF(E_WARN, L_GENERAL, "make_dir: cannot create directory '%s'\n", path); DPRINTF(E_WARN, L_GENERAL, "make_dir: cannot create directory '%s'\n", path);
if (c) if (c)
*s = c; *s = c;
return -1; return -1;
} }
} }
if (!c) if (!c)
return 0; return 0;
/* Remove any inserted nul from the path. */ /* Remove any inserted nul from the path. */
*s = c; *s = c;
} while (1); } while (1);
} }
/* Simple, efficient hash function from Daniel J. Bernstein */ /* Simple, efficient hash function from Daniel J. Bernstein */
unsigned int unsigned int
DJBHash(uint8_t *data, int len) DJBHash(uint8_t *data, int len)
{ {
unsigned int hash = 5381; unsigned int hash = 5381;
unsigned int i = 0; unsigned int i = 0;
for(i = 0; i < len; data++, i++) for(i = 0; i < len; data++, i++)
{ {
hash = ((hash << 5) + hash) + (*data); hash = ((hash << 5) + hash) + (*data);
} }
return hash; return hash;
} }
const char * const char *
mime_to_ext(const char * mime) mime_to_ext(const char * mime)
{ {
switch( *mime ) switch( *mime )
{ {
/* Audio extensions */ /* Audio extensions */
case 'a': case 'a':
if( strcmp(mime+6, "mpeg") == 0 ) if( strcmp(mime+6, "mpeg") == 0 )
return "mp3"; return "mp3";
else if( strcmp(mime+6, "mp4") == 0 ) else if( strcmp(mime+6, "mp4") == 0 )
return "m4a"; return "m4a";
else if( strcmp(mime+6, "x-ms-wma") == 0 ) else if( strcmp(mime+6, "x-ms-wma") == 0 )
return "wma"; return "wma";
else if( strcmp(mime+6, "x-flac") == 0 ) else if( strcmp(mime+6, "x-flac") == 0 )
return "flac"; return "flac";
else if( strcmp(mime+6, "flac") == 0 ) else if( strcmp(mime+6, "flac") == 0 )
return "flac"; return "flac";
else if( strcmp(mime+6, "x-wav") == 0 ) else if( strcmp(mime+6, "x-wav") == 0 )
return "wav"; return "wav";
else if( strncmp(mime+6, "L16", 3) == 0 ) else if( strncmp(mime+6, "L16", 3) == 0 )
return "pcm"; return "pcm";
else if( strcmp(mime+6, "3gpp") == 0 ) else if( strcmp(mime+6, "3gpp") == 0 )
return "3gp"; return "3gp";
else if( strcmp(mime, "application/ogg") == 0 ) else if( strncmp(mime+6, "ogg", 3) == 0 )
return "ogg"; {
else if( strcmp(mime+6, "x-dsd") == 0 ) if( strstr(mime+9, "opus" ) != (char *)NULL )
return "dsd"; return "opus";
break; else if( strstr (mime+9, "vorbis" ) != (char *)NULL )
case 'v': return "ogg";
if( strcmp(mime+6, "avi") == 0 )
return "avi"; return "oga";
else if( strcmp(mime+6, "divx") == 0 ) }
return "avi"; else if( strcmp(mime+6, "x-dsd") == 0 )
else if( strcmp(mime+6, "x-msvideo") == 0 ) return "dsd";
return "avi"; break;
else if( strcmp(mime+6, "mpeg") == 0 ) case 'v':
return "mpg"; if( strcmp(mime+6, "avi") == 0 )
else if( strcmp(mime+6, "mp4") == 0 ) return "avi";
return "mp4"; else if( strcmp(mime+6, "divx") == 0 )
else if( strcmp(mime+6, "x-ms-wmv") == 0 ) return "avi";
return "wmv"; else if( strcmp(mime+6, "x-msvideo") == 0 )
else if( strcmp(mime+6, "x-matroska") == 0 ) return "avi";
return "mkv"; else if( strcmp(mime+6, "mpeg") == 0 )
else if( strcmp(mime+6, "x-mkv") == 0 ) return "mpg";
return "mkv"; else if( strcmp(mime+6, "mp4") == 0 )
else if( strcmp(mime+6, "x-flv") == 0 ) return "mp4";
return "flv"; else if( strcmp(mime+6, "x-ms-wmv") == 0 )
else if( strcmp(mime+6, "vnd.dlna.mpeg-tts") == 0 ) return "wmv";
return "mpg"; else if( strcmp(mime+6, "x-matroska") == 0 )
else if( strcmp(mime+6, "quicktime") == 0 ) return "mkv";
return "mov"; else if( strcmp(mime+6, "x-mkv") == 0 )
else if( strcmp(mime+6, "3gpp") == 0 ) return "mkv";
return "3gp"; else if( strcmp(mime+6, "x-flv") == 0 )
else if( strncmp(mime+6, "x-tivo-mpeg", 11) == 0 ) return "flv";
return "TiVo"; else if( strcmp(mime+6, "vnd.dlna.mpeg-tts") == 0 )
break; return "mpg";
case 'i': else if( strcmp(mime+6, "quicktime") == 0 )
if( strcmp(mime+6, "jpeg") == 0 ) return "mov";
return "jpg"; else if( strcmp(mime+6, "3gpp") == 0 )
else if( strcmp(mime+6, "png") == 0 ) return "3gp";
return "png"; else if( strncmp(mime+6, "x-tivo-mpeg", 11) == 0 )
break; return "TiVo";
default: else if ( strcmp(mime+6, "ogg") == 0 )
break; return "ogv";
} break;
return "dat"; case 'i':
if( strcmp(mime+6, "jpeg") == 0 )
return "jpg";
else if( strcmp(mime+6, "png") == 0 )
return "png";
break;
default:
break;
}
return "dat";
} }
int int
is_video(const char * file) is_video(const char * file)
{ {
return (ends_with(file, ".mpg") || ends_with(file, ".mpeg") || return (ends_with(file, ".mpg") || ends_with(file, ".mpeg") ||
ends_with(file, ".avi") || ends_with(file, ".divx") || ends_with(file, ".avi") || ends_with(file, ".divx") ||
ends_with(file, ".asf") || ends_with(file, ".wmv") || ends_with(file, ".asf") || ends_with(file, ".wmv") ||
ends_with(file, ".mp4") || ends_with(file, ".m4v") || ends_with(file, ".mp4") || ends_with(file, ".m4v") ||
ends_with(file, ".mts") || ends_with(file, ".m2ts") || ends_with(file, ".mts") || ends_with(file, ".m2ts") ||
ends_with(file, ".m2t") || ends_with(file, ".mkv") || ends_with(file, ".m2t") || ends_with(file, ".mkv") ||
ends_with(file, ".vob") || ends_with(file, ".ts") || ends_with(file, ".vob") || ends_with(file, ".ts") ||
ends_with(file, ".flv") || ends_with(file, ".xvid") || ends_with(file, ".flv") || ends_with(file, ".xvid") ||
ends_with(file, ".ogv") ||
#ifdef TIVO_SUPPORT #ifdef TIVO_SUPPORT
ends_with(file, ".TiVo") || ends_with(file, ".TiVo") ||
#endif #endif
ends_with(file, ".mov") || ends_with(file, ".3gp") || ends_with(file, ".mov") || ends_with(file, ".3gp") ||
ends_with(file, ".rm") || ends_with(file, ".rmvb") || ends_with(file, ".rm") || ends_with(file, ".rmvb") ||
ends_with(file, ".webm")); ends_with(file, ".webm"));
} }
int int
is_audio(const char * file) is_audio(const char * file)
{ {
return (ends_with(file, ".mp3") || ends_with(file, ".flac") || return (ends_with(file, ".mp3") || ends_with(file, ".flac") ||
ends_with(file, ".wma") || ends_with(file, ".asf") || ends_with(file, ".wma") || ends_with(file, ".asf") ||
ends_with(file, ".fla") || ends_with(file, ".flc") || ends_with(file, ".fla") || ends_with(file, ".flc") ||
ends_with(file, ".m4a") || ends_with(file, ".aac") || ends_with(file, ".m4a") || ends_with(file, ".aac") ||
ends_with(file, ".mp4") || ends_with(file, ".m4p") || ends_with(file, ".mp4") || ends_with(file, ".m4p") ||
ends_with(file, ".wav") || ends_with(file, ".ogg") || ends_with(file, ".wav") || ends_with(file, ".ogg") ||
ends_with(file, ".pcm") || ends_with(file, ".3gp") || ends_with(file, ".oga") ||
ends_with(file, ".dsf") || ends_with(file, ".dff")); #ifdef HAVE_OPUS
ends_with(file, ".opus") ||
#endif
ends_with(file, ".pcm") || ends_with(file, ".3gp") ||
ends_with(file, ".dsf") || ends_with(file, ".dff"));
} }
int int
is_image(const char * file) is_image(const char * file)
{ {
return (ends_with(file, ".jpg") || ends_with(file, ".jpeg")); return (ends_with(file, ".jpg") || ends_with(file, ".jpeg"));
} }
int int
is_playlist(const char * file) is_playlist(const char * file)
{ {
return (ends_with(file, ".m3u") || ends_with(file, ".pls")); return (ends_with(file, ".m3u") || ends_with(file, ".pls"));
} }
int int
is_caption(const char * file) is_caption(const char * file)
{ {
return (ends_with(file, ".srt") || ends_with(file, ".smi")); return (ends_with(file, ".srt") || ends_with(file, ".smi"));
} }
media_types media_types
get_media_type(const char *file) get_media_type(const char *file)
{ {
const char *ext = strrchr(file, '.'); const char *ext = strrchr(file, '.');
if (!ext) if (!ext)
return NO_MEDIA; return NO_MEDIA;
if (is_image(ext)) if (is_image(ext))
return TYPE_IMAGE; return TYPE_IMAGE;
if (is_video(ext)) if (is_video(ext))
return TYPE_VIDEO; return TYPE_VIDEO;
if (is_audio(ext)) if (is_audio(ext))
return TYPE_AUDIO; return TYPE_AUDIO;
if (is_playlist(ext)) if (is_playlist(ext))
return TYPE_PLAYLIST; return TYPE_PLAYLIST;
if (is_caption(ext)) if (is_caption(ext))
return TYPE_CAPTION; return TYPE_CAPTION;
if (is_nfo(ext)) if (is_nfo(ext))
return TYPE_NFO; return TYPE_NFO;
return NO_MEDIA; return NO_MEDIA;
} }
int int
is_album_art(const char * name) is_album_art(const char * name)
{ {
struct album_art_name_s * album_art_name; struct album_art_name_s * album_art_name;
/* Check if this file name matches one of the default album art names */ /* Check if this file name matches one of the default album art names */
for( album_art_name = album_art_names; album_art_name; album_art_name = album_art_name->next ) for( album_art_name = album_art_names; album_art_name; album_art_name = album_art_name->next )
{ {
if( album_art_name->wildcard ) if( album_art_name->wildcard )
{ {
if( strncmp(album_art_name->name, name, strlen(album_art_name->name)) == 0 ) if( strncmp(album_art_name->name, name, strlen(album_art_name->name)) == 0 )
break; break;
} }
else else
{ {
if( strcmp(album_art_name->name, name) == 0 ) if( strcmp(album_art_name->name, name) == 0 )
break; break;
} }
} }
return (album_art_name ? 1 : 0); return (album_art_name ? 1 : 0);
} }
int int
resolve_unknown_type(const char * path, media_types dir_type) resolve_unknown_type(const char * path, media_types dir_type)
{ {
struct stat entry; struct stat entry;
enum file_types type = TYPE_UNKNOWN; enum file_types type = TYPE_UNKNOWN;
char str_buf[PATH_MAX]; char str_buf[PATH_MAX];
ssize_t len; ssize_t len;
if( lstat(path, &entry) == 0 ) if( lstat(path, &entry) == 0 )
{ {
if( S_ISLNK(entry.st_mode) ) if( S_ISLNK(entry.st_mode) )
{ {
if( (len = readlink(path, str_buf, PATH_MAX-1)) > 0 ) if( (len = readlink(path, str_buf, PATH_MAX-1)) > 0 )
{ {
str_buf[len] = '\0'; str_buf[len] = '\0';
//DEBUG DPRINTF(E_DEBUG, L_GENERAL, "Checking for recursive symbolic link: %s (%s)\n", path, str_buf); //DEBUG DPRINTF(E_DEBUG, L_GENERAL, "Checking for recursive symbolic link: %s (%s)\n", path, str_buf);
if( strncmp(path, str_buf, strlen(str_buf)) == 0 ) if( strncmp(path, str_buf, strlen(str_buf)) == 0 )
{ {
DPRINTF(E_DEBUG, L_GENERAL, "Ignoring recursive symbolic link: %s (%s)\n", path, str_buf); DPRINTF(E_DEBUG, L_GENERAL, "Ignoring recursive symbolic link: %s (%s)\n", path, str_buf);
return type; return type;
} }
} }
stat(path, &entry); stat(path, &entry);
} }
if( S_ISDIR(entry.st_mode) ) if( S_ISDIR(entry.st_mode) )
{ {
type = TYPE_DIR; type = TYPE_DIR;
} }
else if( S_ISREG(entry.st_mode) ) else if( S_ISREG(entry.st_mode) )
{ {
media_types mtype = get_media_type(path); media_types mtype = get_media_type(path);
if (dir_type & mtype) if (dir_type & mtype)
type = TYPE_FILE; type = TYPE_FILE;
} }
} }
return type; return type;
} }
media_types media_types
valid_media_types(const char *path) valid_media_types(const char *path)
{ {
struct media_dir_s *media_dir; struct media_dir_s *media_dir;
for (media_dir = media_dirs; media_dir; media_dir = media_dir->next) for (media_dir = media_dirs; media_dir; media_dir = media_dir->next)
{ {
if (strncmp(path, media_dir->path, strlen(media_dir->path)) == 0) if (strncmp(path, media_dir->path, strlen(media_dir->path)) == 0)
return media_dir->types; return media_dir->types;
} }
return ALL_MEDIA; return ALL_MEDIA;
} }
/* /*
@ -544,35 +558,35 @@ valid_media_types(const char *path)
* it just gets very confused in this case. * it just gets very confused in this case.
* Caveat emptor. * Caveat emptor.
*/ */
static void timevalfix(struct timeval *); static void timevalfix(struct timeval *);
void void
timevaladd(struct timeval *t1, const struct timeval *t2) timevaladd(struct timeval *t1, const struct timeval *t2)
{ {
t1->tv_sec += t2->tv_sec; t1->tv_sec += t2->tv_sec;
t1->tv_usec += t2->tv_usec; t1->tv_usec += t2->tv_usec;
timevalfix(t1); timevalfix(t1);
} }
void void
timevalsub(struct timeval *t1, const struct timeval *t2) timevalsub(struct timeval *t1, const struct timeval *t2)
{ {
t1->tv_sec -= t2->tv_sec; t1->tv_sec -= t2->tv_sec;
t1->tv_usec -= t2->tv_usec; t1->tv_usec -= t2->tv_usec;
timevalfix(t1); timevalfix(t1);
} }
static void static void
timevalfix(struct timeval *t1) timevalfix(struct timeval *t1)
{ {
if (t1->tv_usec < 0) { if (t1->tv_usec < 0) {
t1->tv_sec--; t1->tv_sec--;
t1->tv_usec += 1000000; t1->tv_usec += 1000000;
} }
if (t1->tv_usec >= 1000000) { if (t1->tv_usec >= 1000000) {
t1->tv_sec++; t1->tv_sec++;
t1->tv_usec -= 1000000; t1->tv_usec -= 1000000;
} }
} }