configure.in:
- Added support for optional Ogg/Vorbis support via the --enable-oggvorbis flag. Makefile.am: - Added conditional compilation of ogg.c dependant on whether Ogg/Vorbis support is enabled. mp3-scanner.c: - Added hook for Ogg/Vorbis support. - Commented out code which set the "item_kind" of ogg files differently. It now sets it the same as other audio files. They show up in iTunes now but they will not stream. ogg.c: - Initial check-in. - Code adapted from 'ogginfo' program. Does parsing of Ogg/Vorbis tags.
This commit is contained in:
parent
5d4f43eb23
commit
7aa6aa0062
33
configure.in
33
configure.in
|
@ -47,8 +47,14 @@ AC_ARG_ENABLE(howl,[ --enable-howl Use howl 0.9.2 or later],
|
||||||
*) AC_MSG_ERROR(bad value ${enableval} for --enable-howl);;
|
*) AC_MSG_ERROR(bad value ${enableval} for --enable-howl);;
|
||||||
esac ])
|
esac ])
|
||||||
|
|
||||||
|
AC_ARG_ENABLE(oggvorbis,[ --enable-oggvorbis Enable Ogg/Vorbis support],
|
||||||
|
use_oggvorbis=true;
|
||||||
|
# LDFLAGS="${LDFLAGS} -logg -lvorbis";
|
||||||
|
CPPFLAGS="${CPPFLAGS} -DOGGVORBIS")
|
||||||
|
|
||||||
AM_CONDITIONAL(COND_REND_HOWL, test x$rend_howl = xtrue)
|
AM_CONDITIONAL(COND_REND_HOWL, test x$rend_howl = xtrue)
|
||||||
AM_CONDITIONAL(COND_REND_POSIX, test x$rend_posix = xtrue)
|
AM_CONDITIONAL(COND_REND_POSIX, test x$rend_posix = xtrue)
|
||||||
|
AM_CONDITIONAL(COND_OGGVORBIS, test x$use_oggvorbis = xtrue)
|
||||||
|
|
||||||
AM_CONDITIONAL(COND_NEED_STRCASESTR,false)
|
AM_CONDITIONAL(COND_NEED_STRCASESTR,false)
|
||||||
AM_CONDITIONAL(COND_NEED_STRSEP,false)
|
AM_CONDITIONAL(COND_NEED_STRSEP,false)
|
||||||
|
@ -157,6 +163,33 @@ else
|
||||||
echo "Adding dynamic libid3tag"
|
echo "Adding dynamic libid3tag"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if test x"$use_oggvorbis" == x"true"; then
|
||||||
|
AC_CHECK_HEADERS(ogg/ogg.h,, [
|
||||||
|
AC_MSG_ERROR([ogg/ogg.h not found... Must have libogg installed for Ogg/Vorbis support])])
|
||||||
|
AC_CHECK_LIB(ogg,ogg_sync_init,echo "Have ogg",echo "Must have libogg for Ogg/Vorbis support";exit)
|
||||||
|
|
||||||
|
if test x"$STATIC_LIBS" != x"no"; then
|
||||||
|
LDFLAGS="${LDFLAGS} ${STATIC_LIBS}/libogg.a"
|
||||||
|
echo "Adding static libogg"
|
||||||
|
else
|
||||||
|
LDFLAGS="${LDFLAGS} -logg"
|
||||||
|
echo "Adding dynamic libogg"
|
||||||
|
fi
|
||||||
|
|
||||||
|
AC_CHECK_HEADERS(vorbis/codec.h,, [
|
||||||
|
AC_MSG_ERROR([vorbis/codec.h not found... Must have libvorbis installed for Ogg/Vorbis support])])
|
||||||
|
AC_CHECK_LIB(vorbis,vorbis_info_init,echo "Have vorbis",echo "Must have libvorbis for Ogg/Vorbis support";exit)
|
||||||
|
|
||||||
|
if test x"$STATIC_LIBS" != x"no"; then
|
||||||
|
LDFLAGS="${LDFLAGS} ${STATIC_LIBS}/libvorbis.a"
|
||||||
|
echo "Adding static libvorbis"
|
||||||
|
else
|
||||||
|
LDFLAGS="${LDFLAGS} -lvorbis"
|
||||||
|
echo "Adding dynamic libvorbis"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
AC_REPLACE_FUNCS(strcasestr)
|
AC_REPLACE_FUNCS(strcasestr)
|
||||||
AC_REPLACE_FUNCS(strsep)
|
AC_REPLACE_FUNCS(strsep)
|
||||||
|
|
||||||
|
|
|
@ -18,16 +18,20 @@ if COND_REND_OSX
|
||||||
ORENDSRC=rend-osx.c rend-unix.c
|
ORENDSRC=rend-osx.c rend-unix.c
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
if COND_OGGVORBIS
|
||||||
|
OGGVORBISSRC=ogg.c
|
||||||
|
endif
|
||||||
|
|
||||||
mt_daapd_SOURCES = main.c daapd.h rend.h uici.c uici.h webserver.c \
|
mt_daapd_SOURCES = main.c daapd.h rend.h uici.c uici.h webserver.c \
|
||||||
webserver.h configfile.c configfile.h err.c err.h restart.c restart.h \
|
webserver.h configfile.c configfile.h err.c err.h restart.c restart.h \
|
||||||
daap-proto.c daap-proto.h daap.c daap.h db-gdbm.c db-memory.h \
|
daap-proto.c daap-proto.h daap.c daap.h db-gdbm.c db-memory.h \
|
||||||
mp3-scanner.h mp3-scanner.c playlist.c playlist.h \
|
mp3-scanner.h mp3-scanner.c playlist.c playlist.h \
|
||||||
rend-unix.h lexer.l parser.y strcasestr.c strcasestr.h strsep.c \
|
rend-unix.h lexer.l parser.y strcasestr.c strcasestr.h strsep.c \
|
||||||
redblack.c redblack.h dynamic-art.c dynamic-art.h query.c query.h \
|
redblack.c redblack.h dynamic-art.c dynamic-art.h query.c query.h \
|
||||||
$(PRENDSRC) $(ORENDSRC) $(HRENDSRC)
|
$(PRENDSRC) $(ORENDSRC) $(HRENDSRC) $(OGGVORBISSRC)
|
||||||
|
|
||||||
EXTRA_DIST = mDNS.c mDNSClientAPI.h mDNSDebug.h mDNSPosix.c \
|
EXTRA_DIST = mDNS.c mDNSClientAPI.h mDNSDebug.h mDNSPosix.c \
|
||||||
mDNSUNP.c mDNSPlatformFunctions.h mDNSPosix.h mDNSUNP.h \
|
mDNSUNP.c mDNSPlatformFunctions.h mDNSPosix.h mDNSUNP.h \
|
||||||
rend-howl.c rend-posix.c rend-osx.c strcasestr.c strsep.c db-memory.c \
|
rend-howl.c rend-posix.c rend-osx.c strcasestr.c strsep.c db-memory.c \
|
||||||
db-gdbm.c strcasestr.h redblack.c redblack.h db-memory.c \
|
db-gdbm.c strcasestr.h redblack.c redblack.h db-memory.c \
|
||||||
parser.h
|
parser.h ogg.c
|
||||||
|
|
|
@ -290,6 +290,9 @@ static void scan_music_file(char *path, struct dirent *pde, struct stat *psb);
|
||||||
static int scan_decode_mp3_frame(unsigned char *frame, SCAN_FRAMEINFO *pfi);
|
static int scan_decode_mp3_frame(unsigned char *frame, SCAN_FRAMEINFO *pfi);
|
||||||
static time_t mac_to_unix_time(int t);
|
static time_t mac_to_unix_time(int t);
|
||||||
|
|
||||||
|
#ifdef OGGVORBIS
|
||||||
|
extern int scan_get_oggfileinfo(char *filename, MP3FILE *pmp3);
|
||||||
|
#endif
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Typedefs
|
* Typedefs
|
||||||
|
@ -308,6 +311,9 @@ static taghandler taghandlers[] = {
|
||||||
{ "m4p", scan_get_aactags, scan_get_aacfileinfo },
|
{ "m4p", scan_get_aactags, scan_get_aacfileinfo },
|
||||||
{ "mp3", scan_get_mp3tags, scan_get_mp3fileinfo },
|
{ "mp3", scan_get_mp3tags, scan_get_mp3fileinfo },
|
||||||
{ "url", scan_get_nultags, scan_get_urlfileinfo },
|
{ "url", scan_get_nultags, scan_get_urlfileinfo },
|
||||||
|
#ifdef OGGVORBIS
|
||||||
|
{ "ogg", scan_get_nultags, scan_get_oggfileinfo },
|
||||||
|
#endif
|
||||||
{ NULL, 0 }
|
{ NULL, 0 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1708,7 +1714,7 @@ void make_composite_tags(MP3FILE *song)
|
||||||
if(!strcasecmp(song->type,"ogg")) {
|
if(!strcasecmp(song->type,"ogg")) {
|
||||||
song->description = strdup("QuickTime movie file");
|
song->description = strdup("QuickTime movie file");
|
||||||
} else {
|
} else {
|
||||||
char fdescr[50];
|
char fdescr[50];
|
||||||
|
|
||||||
sprintf(fdescr,"%s audio file",song->type);
|
sprintf(fdescr,"%s audio file",song->type);
|
||||||
song->description = strdup(fdescr);
|
song->description = strdup(fdescr);
|
||||||
|
@ -1728,8 +1734,10 @@ void make_composite_tags(MP3FILE *song)
|
||||||
if(!song->title)
|
if(!song->title)
|
||||||
song->title = strdup(song->fname);
|
song->title = strdup(song->fname);
|
||||||
|
|
||||||
if(!strcmp(song->type, "ogg"))
|
/* PENDING: treat ogg just like any audio file? Doing so allows it
|
||||||
|
to show up in iTunes but it still can't be streamed. */
|
||||||
|
/*if(!strcmp(song->type, "ogg"))
|
||||||
song->item_kind = 4;
|
song->item_kind = 4;
|
||||||
else
|
else*/
|
||||||
song->item_kind = 2;
|
song->item_kind = 2;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,792 @@
|
||||||
|
/*
|
||||||
|
* $Id$
|
||||||
|
* Ogg parsing routines.
|
||||||
|
*
|
||||||
|
* This file has been modified from the 'ogginfo' code in the vorbistools
|
||||||
|
* distribution. The original copyright appears below:
|
||||||
|
*
|
||||||
|
* Ogginfo
|
||||||
|
*
|
||||||
|
* A tool to describe ogg file contents and metadata.
|
||||||
|
*
|
||||||
|
* Copyright 2002 Michael Smith <msmith@xiph.org>
|
||||||
|
* Licensed under the GNU GPL, distributed with this program.
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <ogg/ogg.h>
|
||||||
|
#include <vorbis/codec.h>
|
||||||
|
/*
|
||||||
|
#include <locale.h>
|
||||||
|
#include "utf8.h"
|
||||||
|
#include "i18n.h"
|
||||||
|
*/
|
||||||
|
#include "err.h"
|
||||||
|
#include "mp3-scanner.h"
|
||||||
|
|
||||||
|
#define CHUNK 4500
|
||||||
|
|
||||||
|
struct vorbis_release {
|
||||||
|
char *vendor_string;
|
||||||
|
char *desc;
|
||||||
|
} releases[] = {
|
||||||
|
{"Xiphophorus libVorbis I 20000508", "1.0 beta 1 or beta 2"},
|
||||||
|
{"Xiphophorus libVorbis I 20001031", "1.0 beta 3"},
|
||||||
|
{"Xiphophorus libVorbis I 20010225", "1.0 beta 4"},
|
||||||
|
{"Xiphophorus libVorbis I 20010615", "1.0 rc1"},
|
||||||
|
{"Xiphophorus libVorbis I 20010813", "1.0 rc2"},
|
||||||
|
{"Xiphophorus libVorbis I 20011217", "1.0 rc3"},
|
||||||
|
{"Xiphophorus libVorbis I 20011231", "1.0 rc3"},
|
||||||
|
{"Xiph.Org libVorbis I 20020717", "1.0"},
|
||||||
|
{"Xiph.Org libVorbis I 20030909", "1.0.1"},
|
||||||
|
{NULL, NULL},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/* TODO:
|
||||||
|
*
|
||||||
|
* - detect violations of muxing constraints
|
||||||
|
* - detect granulepos 'gaps' (possibly vorbis-specific). (seperate from
|
||||||
|
* serial-number gaps)
|
||||||
|
*/
|
||||||
|
|
||||||
|
typedef struct _stream_processor {
|
||||||
|
void (*process_page)(struct _stream_processor *, ogg_page *, MP3FILE *);
|
||||||
|
void (*process_end)(struct _stream_processor *, MP3FILE *);
|
||||||
|
int isillegal;
|
||||||
|
int constraint_violated;
|
||||||
|
int shownillegal;
|
||||||
|
int isnew;
|
||||||
|
long seqno;
|
||||||
|
int lostseq;
|
||||||
|
|
||||||
|
int start;
|
||||||
|
int end;
|
||||||
|
|
||||||
|
int num;
|
||||||
|
char *type;
|
||||||
|
|
||||||
|
ogg_uint32_t serial; /* must be 32 bit unsigned */
|
||||||
|
ogg_stream_state os;
|
||||||
|
void *data;
|
||||||
|
} stream_processor;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
stream_processor *streams;
|
||||||
|
int allocated;
|
||||||
|
int used;
|
||||||
|
|
||||||
|
int in_headers;
|
||||||
|
} stream_set;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
vorbis_info vi;
|
||||||
|
vorbis_comment vc;
|
||||||
|
|
||||||
|
ogg_int64_t bytes;
|
||||||
|
ogg_int64_t lastgranulepos;
|
||||||
|
ogg_int64_t firstgranulepos;
|
||||||
|
|
||||||
|
int doneheaders;
|
||||||
|
} misc_vorbis_info;
|
||||||
|
|
||||||
|
#define CONSTRAINT_PAGE_AFTER_EOS 1
|
||||||
|
#define CONSTRAINT_MUXING_VIOLATED 2
|
||||||
|
|
||||||
|
static stream_set *create_stream_set(void) {
|
||||||
|
stream_set *set = calloc(1, sizeof(stream_set));
|
||||||
|
|
||||||
|
set->streams = calloc(5, sizeof(stream_processor));
|
||||||
|
set->allocated = 5;
|
||||||
|
set->used = 0;
|
||||||
|
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void vorbis_process(stream_processor *stream, ogg_page *page,
|
||||||
|
MP3FILE *pmp3)
|
||||||
|
{
|
||||||
|
ogg_packet packet;
|
||||||
|
misc_vorbis_info *inf = stream->data;
|
||||||
|
int i, header=0;
|
||||||
|
int k;
|
||||||
|
|
||||||
|
ogg_stream_pagein(&stream->os, page);
|
||||||
|
if(inf->doneheaders < 3)
|
||||||
|
header = 1;
|
||||||
|
|
||||||
|
while(ogg_stream_packetout(&stream->os, &packet) > 0) {
|
||||||
|
if(inf->doneheaders < 3) {
|
||||||
|
if(vorbis_synthesis_headerin(&inf->vi, &inf->vc, &packet) < 0) {
|
||||||
|
DPRINTF(E_WARN, L_SCAN, "Could not decode vorbis header "
|
||||||
|
"packet - invalid vorbis stream (%d)\n", stream->num);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
inf->doneheaders++;
|
||||||
|
if(inf->doneheaders == 3) {
|
||||||
|
if(ogg_page_granulepos(page) != 0 || ogg_stream_packetpeek(&stream->os, NULL) == 1)
|
||||||
|
DPRINTF(E_WARN, L_SCAN, "Vorbis stream %d does not have headers "
|
||||||
|
"correctly framed. Terminal header page contains "
|
||||||
|
"additional packets or has non-zero granulepos\n",
|
||||||
|
stream->num);
|
||||||
|
DPRINTF(E_DBG, L_SCAN, "Vorbis headers parsed for stream %d, "
|
||||||
|
"information follows...\n", stream->num);
|
||||||
|
/*
|
||||||
|
DPRINTF(E_INF, L_SCAN, "Version: %d\n", inf->vi.version);
|
||||||
|
k = 0;
|
||||||
|
while(releases[k].vendor_string) {
|
||||||
|
if(!strcmp(inf->vc.vendor, releases[k].vendor_string)) {
|
||||||
|
//PENDING:
|
||||||
|
DPRINTF(E_INF, L_SCAN, "Vendor: %s (%s)\n",
|
||||||
|
inf->vc.vendor, releases[k].desc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!releases[k].vendor_string)
|
||||||
|
DPRINTF(E_INF, L_SCAN, "Vendor: %s\n", inf->vc.vendor);*/
|
||||||
|
|
||||||
|
DPRINTF(E_DBG, L_SCAN, "Channels: %d\n", inf->vi.channels);
|
||||||
|
DPRINTF(E_DBG, L_SCAN, "Rate: %ld\n\n", inf->vi.rate);
|
||||||
|
|
||||||
|
pmp3->samplerate = inf->vi.rate;
|
||||||
|
|
||||||
|
if(inf->vi.bitrate_nominal > 0) {
|
||||||
|
DPRINTF(E_DBG, L_SCAN, "Nominal bitrate: %f kb/s\n",
|
||||||
|
(double)inf->vi.bitrate_nominal / 1000.0);
|
||||||
|
pmp3->bitrate = inf->vi.bitrate_nominal / 1000;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
int upper_rate, lower_rate;
|
||||||
|
|
||||||
|
DPRINTF(E_DBG, L_SCAN, "Nominal bitrate not set\n");
|
||||||
|
|
||||||
|
/* Average the lower and upper bitrates if set */
|
||||||
|
|
||||||
|
upper_rate = 0;
|
||||||
|
lower_rate = 0;
|
||||||
|
|
||||||
|
if(inf->vi.bitrate_upper > 0) {
|
||||||
|
DPRINTF(E_DBG, L_SCAN, "Upper bitrate: %f kb/s\n",
|
||||||
|
(double)inf->vi.bitrate_upper / 1000.0);
|
||||||
|
upper_rate = inf->vi.bitrate_upper;
|
||||||
|
} else {
|
||||||
|
DPRINTF(E_DBG, L_SCAN, "Upper bitrate not set\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(inf->vi.bitrate_lower > 0) {
|
||||||
|
DPRINTF(E_DBG, L_SCAN,"Lower bitrate: %f kb/s\n",
|
||||||
|
(double)inf->vi.bitrate_lower / 1000.0);
|
||||||
|
lower_rate = inf->vi.bitrate_lower;;
|
||||||
|
} else {
|
||||||
|
DPRINTF(E_DBG, L_SCAN, "Lower bitrate not set\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upper_rate && lower_rate) {
|
||||||
|
pmp3->bitrate = (upper_rate + lower_rate) / 2;
|
||||||
|
} else {
|
||||||
|
pmp3->bitrate = upper_rate + lower_rate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(inf->vc.comments > 0)
|
||||||
|
DPRINTF(E_DBG, L_SCAN,
|
||||||
|
"User comments section follows...\n");
|
||||||
|
|
||||||
|
for(i=0; i < inf->vc.comments; i++) {
|
||||||
|
char *sep = strchr(inf->vc.user_comments[i], '=');
|
||||||
|
char *decoded;
|
||||||
|
int j;
|
||||||
|
int broken = 0;
|
||||||
|
unsigned char *val;
|
||||||
|
int bytes;
|
||||||
|
int remaining;
|
||||||
|
|
||||||
|
if(sep == NULL) {
|
||||||
|
DPRINTF(E_WARN, L_SCAN,
|
||||||
|
"Comment %d in stream %d is invalidly "
|
||||||
|
"formatted, does not contain '=': \"%s\"\n",
|
||||||
|
i, stream->num, inf->vc.user_comments[i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(j=0; j < sep-inf->vc.user_comments[i]; j++) {
|
||||||
|
if(inf->vc.user_comments[i][j] < 0x20 ||
|
||||||
|
inf->vc.user_comments[i][j] > 0x7D) {
|
||||||
|
DPRINTF(E_WARN, L_SCAN,
|
||||||
|
"Warning: Invalid comment fieldname in "
|
||||||
|
"comment %d (stream %d): \"%s\"\n",
|
||||||
|
i, stream->num, inf->vc.user_comments[i]);
|
||||||
|
broken = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(broken)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
val = inf->vc.user_comments[i];
|
||||||
|
|
||||||
|
j = sep-inf->vc.user_comments[i]+1;
|
||||||
|
while(j < inf->vc.comment_lengths[i])
|
||||||
|
{
|
||||||
|
remaining = inf->vc.comment_lengths[i] - j;
|
||||||
|
if((val[j] & 0x80) == 0)
|
||||||
|
bytes = 1;
|
||||||
|
else if((val[j] & 0x40) == 0x40) {
|
||||||
|
if((val[j] & 0x20) == 0)
|
||||||
|
bytes = 2;
|
||||||
|
else if((val[j] & 0x10) == 0)
|
||||||
|
bytes = 3;
|
||||||
|
else if((val[j] & 0x08) == 0)
|
||||||
|
bytes = 4;
|
||||||
|
else if((val[j] & 0x04) == 0)
|
||||||
|
bytes = 5;
|
||||||
|
else if((val[j] & 0x02) == 0)
|
||||||
|
bytes = 6;
|
||||||
|
else {
|
||||||
|
DPRINTF(E_WARN, L_SCAN,
|
||||||
|
"Illegal UTF-8 sequence in "
|
||||||
|
"comment %d (stream %d): length "
|
||||||
|
"marker wrong\n",
|
||||||
|
i, stream->num);
|
||||||
|
broken = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
DPRINTF(E_WARN, L_SCAN,
|
||||||
|
"Illegal UTF-8 sequence in comment "
|
||||||
|
"%d (stream %d): length marker wrong\n",
|
||||||
|
i, stream->num);
|
||||||
|
broken = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(bytes > remaining) {
|
||||||
|
DPRINTF(E_WARN, L_SCAN,
|
||||||
|
"Illegal UTF-8 sequence in comment "
|
||||||
|
"%d (stream %d): too few bytes\n",
|
||||||
|
i, stream->num);
|
||||||
|
broken = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(bytes) {
|
||||||
|
case 1:
|
||||||
|
/* No more checks needed */
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
if((val[j+1] & 0xC0) != 0x80)
|
||||||
|
broken = 1;
|
||||||
|
if((val[j] & 0xFE) == 0xC0)
|
||||||
|
broken = 1;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
if(!((val[j] == 0xE0 && val[j+1] >= 0xA0 &&
|
||||||
|
val[j+1] <= 0xBF &&
|
||||||
|
(val[j+2] & 0xC0) == 0x80) ||
|
||||||
|
(val[j] >= 0xE1 && val[j] <= 0xEC &&
|
||||||
|
(val[j+1] & 0xC0) == 0x80 &&
|
||||||
|
(val[j+2] & 0xC0) == 0x80) ||
|
||||||
|
(val[j] == 0xED && val[j+1] >= 0x80 &&
|
||||||
|
val[j+1] <= 0x9F &&
|
||||||
|
(val[j+2] & 0xC0) == 0x80) ||
|
||||||
|
(val[j] >= 0xEE && val[j] <= 0xEF &&
|
||||||
|
(val[j+1] & 0xC0) == 0x80 &&
|
||||||
|
(val[j+2] & 0xC0) == 0x80)))
|
||||||
|
broken = 1;
|
||||||
|
if(val[j] == 0xE0 && (val[j+1] & 0xE0) == 0x80)
|
||||||
|
broken = 1;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
if(!((val[j] == 0xF0 && val[j+1] >= 0x90 &&
|
||||||
|
val[j+1] <= 0xBF &&
|
||||||
|
(val[j+2] & 0xC0) == 0x80 &&
|
||||||
|
(val[j+3] & 0xC0) == 0x80) ||
|
||||||
|
(val[j] >= 0xF1 && val[j] <= 0xF3 &&
|
||||||
|
(val[j+1] & 0xC0) == 0x80 &&
|
||||||
|
(val[j+2] & 0xC0) == 0x80 &&
|
||||||
|
(val[j+3] & 0xC0) == 0x80) ||
|
||||||
|
(val[j] == 0xF4 && val[j+1] >= 0x80 &&
|
||||||
|
val[j+1] <= 0x8F &&
|
||||||
|
(val[j+2] & 0xC0) == 0x80 &&
|
||||||
|
(val[j+3] & 0xC0) == 0x80)))
|
||||||
|
broken = 1;
|
||||||
|
if(val[j] == 0xF0 && (val[j+1] & 0xF0) == 0x80)
|
||||||
|
broken = 1;
|
||||||
|
break;
|
||||||
|
/* 5 and 6 aren't actually allowed at this point*/
|
||||||
|
case 5:
|
||||||
|
broken = 1;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
broken = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(broken) {
|
||||||
|
DPRINTF(E_WARN, L_SCAN,
|
||||||
|
"Illegal UTF-8 sequence in comment "
|
||||||
|
"%d (stream %d): invalid sequence\n",
|
||||||
|
i, stream->num);
|
||||||
|
broken = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
j += bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!broken) {
|
||||||
|
/* if(utf8_decode(sep+1, &decoded) < 0) {
|
||||||
|
DPRINTF(E_WARN, L_SCAN,
|
||||||
|
"Failure in utf8 decoder. This "
|
||||||
|
"should be impossible\n");
|
||||||
|
continue;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
DPRINTF(E_DBG, L_SCAN,
|
||||||
|
"\t%s\n", inf->vc.user_comments[i]);
|
||||||
|
|
||||||
|
if (!strncasecmp(inf->vc.user_comments[i],"TITLE",5)) {
|
||||||
|
pmp3->title = strdup(sep + 1);
|
||||||
|
DPRINTF(E_DBG, L_SCAN, "Mapping %s to title.\n",
|
||||||
|
sep + 1);
|
||||||
|
} else if (!strncasecmp(inf->vc.user_comments[i], "ARTIST", 6)) {
|
||||||
|
pmp3->artist = strdup(sep + 1);
|
||||||
|
DPRINTF(E_DBG, L_SCAN, "Mapping %s to artist.\n",
|
||||||
|
sep + 1);
|
||||||
|
} else if (!strncasecmp(inf->vc.user_comments[i], "ALBUM", 5)) {
|
||||||
|
pmp3->album = strdup(sep + 1);
|
||||||
|
DPRINTF(E_DBG, L_SCAN, "Mapping %s to album.\n",
|
||||||
|
sep + 1);
|
||||||
|
} else if (!strncasecmp(inf->vc.user_comments[i],
|
||||||
|
"TRACKNUMBER", 11)) {
|
||||||
|
pmp3->track = atoi(sep + 1);
|
||||||
|
DPRINTF(E_INF, L_SCAN, "Mapping %s to track.\n",
|
||||||
|
sep + 1);
|
||||||
|
} else if (!strncasecmp(inf->vc.user_comments[i], "GENRE", 5)) {
|
||||||
|
pmp3->genre = strdup(sep + 1);
|
||||||
|
DPRINTF(E_INF, L_SCAN, "Mapping %s to genre.\n",
|
||||||
|
sep + 1);
|
||||||
|
} else if (!strncasecmp(inf->vc.user_comments[i], "DATE", 4)) {
|
||||||
|
//PENDING: Should only parse first 4 characters
|
||||||
|
pmp3->year = atoi(sep + 1);
|
||||||
|
DPRINTF(E_INF, L_SCAN, "Mapping %s to year.\n",
|
||||||
|
sep + 1);
|
||||||
|
} else if (!strncasecmp(inf->vc.user_comments[i], "COMMENT", 7)) {
|
||||||
|
pmp3->comment = strdup(sep + 1);
|
||||||
|
DPRINTF(E_INF, L_SCAN, "Mapping %s to comment.\n",
|
||||||
|
sep + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
*sep = 0;
|
||||||
|
/* free(decoded);*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!header) {
|
||||||
|
ogg_int64_t gp = ogg_page_granulepos(page);
|
||||||
|
if(gp > 0) {
|
||||||
|
if(gp < inf->lastgranulepos)
|
||||||
|
#ifdef _WIN32
|
||||||
|
DPRINTF(E_WARN, L_SCAN, "granulepos in stream %d decreases from %I64d to %I64d",
|
||||||
|
stream->num, inf->lastgranulepos, gp);
|
||||||
|
#else
|
||||||
|
DPRINTF(E_WARN, L_SCAN, "granulepos in stream %d decreases from %lld to %lld",
|
||||||
|
stream->num, inf->lastgranulepos, gp);
|
||||||
|
#endif
|
||||||
|
inf->lastgranulepos = gp;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
DPRINTF(E_WARN, L_SCAN, "Negative granulepos on vorbis stream outside of headers. This file was created by a buggy encoder\n");
|
||||||
|
}
|
||||||
|
if(inf->firstgranulepos < 0) { /* Not set yet */
|
||||||
|
}
|
||||||
|
inf->bytes += page->header_len + page->body_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void vorbis_end(stream_processor *stream, MP3FILE *pmp3)
|
||||||
|
{
|
||||||
|
misc_vorbis_info *inf = stream->data;
|
||||||
|
long minutes, seconds;
|
||||||
|
double bitrate, time;
|
||||||
|
|
||||||
|
/* This should be lastgranulepos - startgranulepos, or something like that*/
|
||||||
|
time = (double)inf->lastgranulepos / inf->vi.rate;
|
||||||
|
bitrate = inf->bytes*8 / time / 1000.0;
|
||||||
|
|
||||||
|
if (pmp3 != NULL) {
|
||||||
|
if (pmp3->bitrate <= 0) {
|
||||||
|
pmp3->bitrate = bitrate;
|
||||||
|
}
|
||||||
|
pmp3->song_length = time * 1000;
|
||||||
|
pmp3->file_size = inf->bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
minutes = (long)time / 60;
|
||||||
|
seconds = (long)time - minutes*60;
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
DPRINTF(E_DBG, L_SCAN, "Vorbis stream %d:\n"
|
||||||
|
"\tTotal data length: %I64d bytes\n"
|
||||||
|
"\tPlayback length: %ldm:%02lds\n"
|
||||||
|
"\tAverage bitrate: %f kbps\n",
|
||||||
|
stream->num,inf->bytes, minutes, seconds, bitrate);
|
||||||
|
#else
|
||||||
|
DPRINTF(E_DBG, L_SCAN, "Vorbis stream %d:\n"
|
||||||
|
"\tTotal data length: %lld bytes\n"
|
||||||
|
"\tPlayback length: %ldm:%02lds\n"
|
||||||
|
"\tAverage bitrate: %f kbps\n",
|
||||||
|
stream->num,inf->bytes, minutes, seconds, bitrate);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
vorbis_comment_clear(&inf->vc);
|
||||||
|
vorbis_info_clear(&inf->vi);
|
||||||
|
|
||||||
|
free(stream->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void process_null(stream_processor *stream, ogg_page *page, MP3FILE *pmp)
|
||||||
|
{
|
||||||
|
/* This is for invalid streams. */
|
||||||
|
}
|
||||||
|
|
||||||
|
static void process_other(stream_processor *stream, ogg_page *page, MP3FILE *pmp)
|
||||||
|
{
|
||||||
|
ogg_packet packet;
|
||||||
|
|
||||||
|
ogg_stream_pagein(&stream->os, page);
|
||||||
|
|
||||||
|
while(ogg_stream_packetout(&stream->os, &packet) > 0) {
|
||||||
|
/* Should we do anything here? Currently, we don't */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void free_stream_set(stream_set *set)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
for(i=0; i < set->used; i++) {
|
||||||
|
if(!set->streams[i].end) {
|
||||||
|
DPRINTF(E_WARN, L_SCAN, "Warning: EOS not set on stream %d\n",
|
||||||
|
set->streams[i].num);
|
||||||
|
//PENDING:
|
||||||
|
if(set->streams[i].process_end)
|
||||||
|
set->streams[i].process_end(&set->streams[i], NULL);
|
||||||
|
}
|
||||||
|
ogg_stream_clear(&set->streams[i].os);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(set->streams);
|
||||||
|
free(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int streams_open(stream_set *set)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
int res=0;
|
||||||
|
for(i=0; i < set->used; i++) {
|
||||||
|
if(!set->streams[i].end)
|
||||||
|
res++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void null_start(stream_processor *stream)
|
||||||
|
{
|
||||||
|
stream->process_end = NULL;
|
||||||
|
stream->type = "invalid";
|
||||||
|
stream->process_page = process_null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void other_start(stream_processor *stream, char *type)
|
||||||
|
{
|
||||||
|
if(type)
|
||||||
|
stream->type = type;
|
||||||
|
else
|
||||||
|
stream->type = "unknown";
|
||||||
|
stream->process_page = process_other;
|
||||||
|
stream->process_end = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void vorbis_start(stream_processor *stream)
|
||||||
|
{
|
||||||
|
misc_vorbis_info *info;
|
||||||
|
|
||||||
|
stream->type = "vorbis";
|
||||||
|
stream->process_page = vorbis_process;
|
||||||
|
stream->process_end = vorbis_end;
|
||||||
|
|
||||||
|
stream->data = calloc(1, sizeof(misc_vorbis_info));
|
||||||
|
|
||||||
|
info = stream->data;
|
||||||
|
|
||||||
|
vorbis_comment_init(&info->vc);
|
||||||
|
vorbis_info_init(&info->vi);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static stream_processor *find_stream_processor(stream_set *set, ogg_page *page)
|
||||||
|
{
|
||||||
|
ogg_uint32_t serial = ogg_page_serialno(page);
|
||||||
|
int i, found = 0;
|
||||||
|
int invalid = 0;
|
||||||
|
int constraint = 0;
|
||||||
|
stream_processor *stream;
|
||||||
|
|
||||||
|
for(i=0; i < set->used; i++) {
|
||||||
|
if(serial == set->streams[i].serial) {
|
||||||
|
/* We have a match! */
|
||||||
|
found = 1;
|
||||||
|
stream = &(set->streams[i]);
|
||||||
|
|
||||||
|
set->in_headers = 0;
|
||||||
|
/* if we have detected EOS, then this can't occur here. */
|
||||||
|
if(stream->end) {
|
||||||
|
stream->isillegal = 1;
|
||||||
|
stream->constraint_violated = CONSTRAINT_PAGE_AFTER_EOS;
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream->isnew = 0;
|
||||||
|
stream->start = ogg_page_bos(page);
|
||||||
|
stream->end = ogg_page_eos(page);
|
||||||
|
stream->serial = serial;
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If there are streams open, and we've reached the end of the
|
||||||
|
* headers, then we can't be starting a new stream.
|
||||||
|
* XXX: might this sometimes catch ok streams if EOS flag is missing,
|
||||||
|
* but the stream is otherwise ok?
|
||||||
|
*/
|
||||||
|
if(streams_open(set) && !set->in_headers) {
|
||||||
|
constraint = CONSTRAINT_MUXING_VIOLATED;
|
||||||
|
invalid = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
set->in_headers = 1;
|
||||||
|
|
||||||
|
if(set->allocated < set->used)
|
||||||
|
stream = &set->streams[set->used];
|
||||||
|
else {
|
||||||
|
set->allocated += 5;
|
||||||
|
set->streams = realloc(set->streams, sizeof(stream_processor)*
|
||||||
|
set->allocated);
|
||||||
|
stream = &set->streams[set->used];
|
||||||
|
}
|
||||||
|
set->used++;
|
||||||
|
stream->num = set->used; /* We count from 1 */
|
||||||
|
|
||||||
|
stream->isnew = 1;
|
||||||
|
stream->isillegal = invalid;
|
||||||
|
stream->constraint_violated = constraint;
|
||||||
|
|
||||||
|
{
|
||||||
|
int res;
|
||||||
|
ogg_packet packet;
|
||||||
|
|
||||||
|
/* We end up processing the header page twice, but that's ok. */
|
||||||
|
ogg_stream_init(&stream->os, serial);
|
||||||
|
ogg_stream_pagein(&stream->os, page);
|
||||||
|
res = ogg_stream_packetout(&stream->os, &packet);
|
||||||
|
if(res <= 0) {
|
||||||
|
DPRINTF(E_WARN, L_SCAN, "Invalid header page, no packet found\n");
|
||||||
|
null_start(stream);
|
||||||
|
}
|
||||||
|
else if(packet.bytes >= 7 && memcmp(packet.packet, "\001vorbis", 7)==0)
|
||||||
|
vorbis_start(stream);
|
||||||
|
else if(packet.bytes >= 8 && memcmp(packet.packet, "OggMIDI\0", 8)==0)
|
||||||
|
other_start(stream, "MIDI");
|
||||||
|
else
|
||||||
|
other_start(stream, NULL);
|
||||||
|
|
||||||
|
res = ogg_stream_packetout(&stream->os, &packet);
|
||||||
|
if(res > 0) {
|
||||||
|
DPRINTF(E_WARN, L_SCAN, "Invalid header page in stream %d, "
|
||||||
|
"contains multiple packets\n", stream->num);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* re-init, ready for processing */
|
||||||
|
ogg_stream_clear(&stream->os);
|
||||||
|
ogg_stream_init(&stream->os, serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream->start = ogg_page_bos(page);
|
||||||
|
stream->end = ogg_page_eos(page);
|
||||||
|
stream->serial = serial;
|
||||||
|
|
||||||
|
/* if(stream->serial == 0 || stream->serial == -1) {
|
||||||
|
info(_("Note: Stream %d has serial number %d, which is legal but may "
|
||||||
|
"cause problems with some tools."), stream->num, stream->serial);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int get_next_page(FILE *f, ogg_sync_state *sync, ogg_page *page,
|
||||||
|
ogg_int64_t *written)
|
||||||
|
{
|
||||||
|
int ret;
|
||||||
|
char *buffer;
|
||||||
|
int bytes;
|
||||||
|
|
||||||
|
while((ret = ogg_sync_pageout(sync, page)) <= 0) {
|
||||||
|
if(ret < 0)
|
||||||
|
#ifdef _WIN32
|
||||||
|
DPRINTF(E_WARN, L_SCAN, "Hole in data found at approximate offset %I64d bytes. Corrupted ogg.\n", *written);
|
||||||
|
#else
|
||||||
|
DPRINTF(E_WARN, L_SCAN, "Hole in data found at approximate offset %lld bytes. Corrupted ogg.\n", *written);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
buffer = ogg_sync_buffer(sync, CHUNK);
|
||||||
|
bytes = fread(buffer, 1, CHUNK, f);
|
||||||
|
if(bytes <= 0) {
|
||||||
|
ogg_sync_wrote(sync, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
ogg_sync_wrote(sync, bytes);
|
||||||
|
*written += bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int scan_get_oggfileinfo(char *filename, MP3FILE *pmp3) {
|
||||||
|
FILE *file = fopen(filename, "rb");
|
||||||
|
ogg_sync_state sync;
|
||||||
|
ogg_page page;
|
||||||
|
stream_set *processors = create_stream_set();
|
||||||
|
int gotpage = 0;
|
||||||
|
ogg_int64_t written = 0;
|
||||||
|
struct stat psb;
|
||||||
|
|
||||||
|
if(!file) {
|
||||||
|
DPRINTF(E_FATAL, L_SCAN,
|
||||||
|
"Error opening input file \"%s\": %s\n", filename,
|
||||||
|
strerror(errno));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
DPRINTF(E_INF, L_SCAN, "Processing file \"%s\"...\n\n", filename);
|
||||||
|
|
||||||
|
if (!stat(filename, &psb)) {
|
||||||
|
pmp3->time_added = psb.st_mtime;
|
||||||
|
if (psb.st_ctime < pmp3->time_added) {
|
||||||
|
pmp3->time_added = psb.st_ctime;
|
||||||
|
}
|
||||||
|
pmp3->time_modified = psb.st_mtime;
|
||||||
|
} else {
|
||||||
|
DPRINTF(E_WARN, L_SCAN, "Error statting: %s\n", strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
|
ogg_sync_init(&sync);
|
||||||
|
|
||||||
|
while(get_next_page(file, &sync, &page, &written)) {
|
||||||
|
stream_processor *p = find_stream_processor(processors, &page);
|
||||||
|
gotpage = 1;
|
||||||
|
|
||||||
|
if(!p) {
|
||||||
|
DPRINTF(E_FATAL, L_SCAN, "Could not find a processor for stream, bailing\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(p->isillegal && !p->shownillegal) {
|
||||||
|
char *constraint;
|
||||||
|
switch(p->constraint_violated) {
|
||||||
|
case CONSTRAINT_PAGE_AFTER_EOS:
|
||||||
|
constraint = "Page found for stream after EOS flag";
|
||||||
|
break;
|
||||||
|
case CONSTRAINT_MUXING_VIOLATED:
|
||||||
|
constraint = "Ogg muxing constraints violated, new "
|
||||||
|
"stream before EOS of all previous streams";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
constraint = "Error unknown.";
|
||||||
|
}
|
||||||
|
|
||||||
|
DPRINTF(E_WARN, L_SCAN,
|
||||||
|
"Warning: illegally placed page(s) for logical stream %d\n"
|
||||||
|
"This indicates a corrupt ogg file: %s.\n",
|
||||||
|
p->num, constraint);
|
||||||
|
p->shownillegal = 1;
|
||||||
|
/* If it's a new stream, we want to continue processing this page
|
||||||
|
* anyway to suppress additional spurious errors
|
||||||
|
*/
|
||||||
|
if(!p->isnew)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(p->isnew) {
|
||||||
|
DPRINTF(E_DBG, L_SCAN, "New logical stream (#%d, serial: %08x): type %s\n",
|
||||||
|
p->num, p->serial, p->type);
|
||||||
|
if(!p->start)
|
||||||
|
DPRINTF(E_WARN, L_SCAN,
|
||||||
|
"stream start flag not set on stream %d\n",
|
||||||
|
p->num);
|
||||||
|
}
|
||||||
|
else if(p->start)
|
||||||
|
DPRINTF(E_WARN, L_SCAN, "stream start flag found in mid-stream "
|
||||||
|
"on stream %d\n", p->num);
|
||||||
|
|
||||||
|
if(p->seqno++ != ogg_page_pageno(&page)) {
|
||||||
|
if(!p->lostseq)
|
||||||
|
DPRINTF(E_WARN, L_SCAN,
|
||||||
|
"sequence number gap in stream %d. Got page %ld "
|
||||||
|
"when expecting page %ld. Indicates missing data.\n",
|
||||||
|
p->num, ogg_page_pageno(&page), p->seqno - 1);
|
||||||
|
p->seqno = ogg_page_pageno(&page);
|
||||||
|
p->lostseq = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
p->lostseq = 0;
|
||||||
|
|
||||||
|
if(!p->isillegal) {
|
||||||
|
p->process_page(p, &page, pmp3);
|
||||||
|
|
||||||
|
if(p->end) {
|
||||||
|
if(p->process_end)
|
||||||
|
p->process_end(p, pmp3);
|
||||||
|
DPRINTF(E_DBG, L_SCAN, "Logical stream %d ended\n", p->num);
|
||||||
|
p->isillegal = 1;
|
||||||
|
p->constraint_violated = CONSTRAINT_PAGE_AFTER_EOS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free_stream_set(processors);
|
||||||
|
|
||||||
|
ogg_sync_clear(&sync);
|
||||||
|
|
||||||
|
fclose(file);
|
||||||
|
|
||||||
|
if(!gotpage) {
|
||||||
|
DPRINTF(E_FATAL, L_SCAN, "No ogg data found in file \"%s\".\n"
|
||||||
|
"Input probably not ogg.\n", filename);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*int main(int argc, char **argv) {
|
||||||
|
int f, ret;
|
||||||
|
|
||||||
|
setlocale(LC_ALL, "");
|
||||||
|
bindtextdomain(PACKAGE, LOCALEDIR);
|
||||||
|
textdomain(PACKAGE);
|
||||||
|
|
||||||
|
process_file(argv[f]);
|
||||||
|
}*/
|
||||||
|
|
Loading…
Reference in New Issue