2 * MP3/MPlayer plugin to VDR (C++)
4 * (C) 2001-2009 Stefan Huelswitt <s.huelswitt@gmx.de>
6 * This code is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
11 * This code is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 * Or, point your browser to http://www.gnu.org/copyleft/gpl.html
28 #include <vdr/videodir.h>
34 #include "decoder-core.h"
35 #include "decoder-mp3.h"
36 #include "decoder-mp3-stream.h"
37 #include "decoder-snd.h"
38 #include "decoder-ogg.h"
39 #include "decoder-ogg-stream.h"
41 #define CACHEFILENAME "id3info.cache"
42 #define CACHESAVETIMEOUT 120 // secs
43 #define CACHEPURGETIMEOUT 120 // days
45 extern cFileSources MP3Sources;
50 int MakeHashBuff(const char *buff, int len)
53 while(len--) h=(h*13 + *buff++) & 0x7ff;
57 // --- cStrConv ----------------------------------------------------------------
59 class cStrConv : private cMutex {
63 cStrConv(void):toSys("UTF-8",cCharSetConv::SystemCharacterTable()) {}
64 char *ToSys(char *from);
67 static cStrConv *strconv;
69 char *cStrConv::ToSys(char *from)
73 const char *r=toSys.Convert(from);
86 // --- cSongInfo ---------------------------------------------------------------
88 cSongInfo::cSongInfo(void)
94 cSongInfo::~cSongInfo()
99 void cSongInfo::Clear(void)
101 Frames=0; Total=-1; DecoderID=0;
102 SampleFreq=Channels=Bitrate=MaxBitrate=ChMode=-1;
103 free(Title); Title=0;
104 free(Artist); Artist=0;
105 free(Album); Album=0;
108 infoDone=false; utf8clean=true;
111 void cSongInfo::Set(cSongInfo *si, bool update)
113 if(!update || si->Utf8Clean()) {
115 Title=si->Title ? strdup(si->Title):0;
116 Artist=si->Artist ? strdup(si->Artist):0;
117 Album=si->Album ? strdup(si->Album):0;
118 utf8clean=si->utf8clean;
122 SampleFreq=si->SampleFreq;
123 Channels=si->Channels;
125 MaxBitrate=si->MaxBitrate;
128 if(si->Level>0.0) { // preserve old level
132 DecoderID=si->DecoderID;
136 void cSongInfo::FakeTitle(const char *filename, const char *extention)
138 // if no title, try to build a reasonable from the filename
139 if(!Title && filename) {
140 const char *s=rindex(filename,'/');
144 strreplace(Title,'_',' ');
145 if(extention) { // strip given extention
146 int l=strlen(Title)-strlen(extention);
147 if(l>0 && !strcasecmp(Title+l,extention)) Title[l]=0;
149 else { // strip any extention
150 char *e=rindex(Title,'.');
151 if(e && *e=='.' && strlen(e)<=5) *e=0;
153 d(printf("mp3: faking title '%s' from filename '%s'\n",Title,filename))
158 void cSongInfo::ConvertToSys(void)
160 if(cCharSetConv::SystemCharacterTable()) {
161 Title=strconv->ToSys(Title);
162 Artist=strconv->ToSys(Artist);
163 Album=strconv->ToSys(Album);
168 // --- cFileInfo ---------------------------------------------------------------
170 cFileInfo::cFileInfo(void)
172 Filename=FsID=0; Clear();
175 cFileInfo::cFileInfo(const char *Name)
177 Filename=FsID=0; Clear();
178 Filename=strdup(Name);
181 cFileInfo::~cFileInfo()
186 void cFileInfo::Clear(void)
188 free(Filename); Filename=0;
190 Filesize=0; CTime=0; FsType=0; removable=-1;
194 bool cFileInfo::Removable(void)
196 if(removable<0 && Filename) {
197 cFileSource *src=MP3Sources.FindSource(Filename);
198 if(src) removable=src->NeedsMount();
201 return (removable!=0);
204 void cFileInfo::Set(cFileInfo *fi)
207 Filename=fi->Filename ? strdup(fi->Filename):0;
208 FsID=fi->FsID ? strdup(fi->FsID):0;
209 Filesize=fi->Filesize;
214 bool cFileInfo::FileInfo(bool log)
218 if(!stat64(Filename,&ds)) {
219 if(S_ISREG(ds.st_mode)) {
223 if(!statfs64(Filename,&sfs)) {
224 if(Removable()) FsID=aprintf("%llx:%llx",sfs.f_blocks,sfs.f_files);
227 else if(errno!=ENOSYS && log) { esyslog("ERROR: can't statfs %s: %s",Filename,strerror(errno)); }
231 if(FsType==CDFS_MAGIC) CTime=0; // CDFS returns mount time as ctime
236 else if(log) { esyslog("ERROR: %s is not a regular file",Filename); }
238 else if(log) { esyslog("ERROR: can't stat %s: %s",Filename,strerror(errno)); }
243 // --- cDecoders ---------------------------------------------------------------
245 cDecoder *cDecoders::FindDecoder(cFileObj *Obj)
247 const char *full=Obj->FullPath();
251 if(fi.FileInfo(false) && (dat=InfoCache.Search(&fi))) {
253 //d(printf("mp3: found DecoderID '%s' for %s from cache\n",cDecoders::ID2Str(dat->DecoderID),Filename))
254 switch(dat->DecoderID) {
255 case DEC_MP3: decoder=new cMP3Decoder(full); break;
256 case DEC_MP3S: decoder=new cMP3StreamDecoder(full); break;
258 case DEC_SND: decoder=new cSndDecoder(full); break;
260 #ifdef HAVE_VORBISFILE
261 case DEC_OGG: decoder=new cOggDecoder(full); break;
262 case DEC_OGGS: decoder=new cOggStreamDecoder(full); break;
264 default: esyslog("ERROR: bad DecoderID '%s' from info cache: %s",cDecoders::ID2Str(dat->DecoderID),full); break;
270 if(!decoder || !decoder->Valid()) {
271 // no decoder in cache or cached decoder doesn't matches.
272 // try to detect a decoder
274 delete decoder; decoder=0;
277 decoder=new cSndDecoder(full);
278 if(!decoder || !decoder->Valid()) { delete decoder; decoder=0; }
281 #ifdef HAVE_VORBISFILE
283 decoder=new cOggDecoder(full);
284 if(!decoder || !decoder->Valid()) { delete decoder; decoder=0; }
287 decoder=new cOggStreamDecoder(full);
288 if(!decoder || !decoder->Valid()) { delete decoder; decoder=0; }
292 decoder=new cMP3StreamDecoder(full);
293 if(!decoder || !decoder->Valid()) { delete decoder; decoder=0; }
296 decoder=new cMP3Decoder(full);
297 if(!decoder || !decoder->Valid()) { delete decoder; decoder=0; }
299 if(!decoder) esyslog("ERROR: no decoder found for %s",Obj->Name());
304 const char *cDecoders::ID2Str(int id)
307 case DEC_MP3: return DEC_MP3_STR;
308 case DEC_MP3S: return DEC_MP3S_STR;
309 case DEC_SND: return DEC_SND_STR;
310 case DEC_OGG: return DEC_OGG_STR;
311 case DEC_OGGS: return DEC_OGGS_STR;
316 int cDecoders::Str2ID(const char *str)
318 if (!strcmp(str,DEC_MP3_STR )) return DEC_MP3;
319 else if(!strcmp(str,DEC_MP3S_STR)) return DEC_MP3S;
320 else if(!strcmp(str,DEC_SND_STR )) return DEC_SND;
321 else if(!strcmp(str,DEC_OGG_STR )) return DEC_OGG;
322 else if(!strcmp(str,DEC_OGGS_STR)) return DEC_OGGS;
326 // --- cDecoder ----------------------------------------------------------------
328 cDecoder::cDecoder(const char *Filename)
330 filename=strdup(Filename);
331 locked=0; urgentLock=playing=false;
334 cDecoder::~cDecoder()
339 void cDecoder::Lock(bool urgent)
342 if(urgent && locked) urgentLock=true; // signal other locks to release quickly
344 locklock.Unlock(); // don't hold the "locklock" when locking "lock", may cause a deadlock
349 void cDecoder::Unlock(void)
357 bool cDecoder::TryLock(void)
361 if(!locked && !playing) {
369 // --- cCacheData -----------------------------------------------------
371 cCacheData::cCacheData(void)
376 void cCacheData::Touch(void)
381 #define SECS_PER_DAY (24*60*60)
383 bool cCacheData::Purge(void)
386 //XXX does this realy made sense?
387 //if(touch+CACHEPURGETIMEOUT*SECS_PER_DAY < now) {
388 // d(printf("cache: purged: timeout %s\n",Filename))
391 if(touch+CACHEPURGETIMEOUT*SECS_PER_DAY/10 < now) {
392 if(!Removable()) { // is this a permant source?
393 struct stat64 ds; // does the file exists? if not, purge
394 if(stat64(Filename,&ds) || !S_ISREG(ds.st_mode) || access(Filename,R_OK)) {
395 d(printf("cache: purged: file not found %s\n",Filename))
403 bool cCacheData::Check8bit(const char *str)
405 if(str) while(*str) if(*str++ & 0x80) return true;
409 bool cCacheData::Upgrade(void)
412 if(Check8bit(Title) || Check8bit(Artist) || Check8bit(Album))
413 return false; // Trash entries not 7bit clean
416 if(DecoderID==DEC_SND || (Title && startswith(Title,"track-")))
417 return false; // Trash older SND entries (incomplete)
420 if(!FsID) FsID=strdup("old"); // Dummy entry, will be replaced in InfoCache::Search()
422 else { free(FsID); FsID=0; }
425 Touch(); // Touch entry
427 if(version<3 && !Title) {
428 FakeTitle(Filename,".mp3"); // Fake title
430 if(version<2 && Bitrate<=0) {
431 return false; // Trash entry without bitrate
436 void cCacheData::Create(cFileInfo *fi, cSongInfo *si, bool update)
439 cSongInfo::Set(si,update);
440 hash=MakeHash(Filename);
444 bool cCacheData::Save(FILE *f)
446 fprintf(f,"##BEGIN\n"
462 Filename,Filesize,CTime,touch,CACHE_VERSION,Frames,Total,SampleFreq,Channels,Bitrate,MaxBitrate,ChMode,Year,Level,Peak);
463 if(Title) fprintf(f,"Title=%s\n" ,Title);
464 if(Artist) fprintf(f,"Artist=%s\n" ,Artist);
465 if(Album) fprintf(f,"Album=%s\n" ,Album);
466 if(DecoderID) fprintf(f,"DecoderID=%s\n",cDecoders::ID2Str(DecoderID));
467 if(FsID) fprintf(f,"FsID=%s\n" ,FsID);
468 fprintf(f,"##END\n");
472 bool cCacheData::Load(FILE *f)
474 static const char delimiters[] = { "=\n" };
479 while(fgets(buf,sizeof(buf),f)) {
481 char *name =strtok_r(buf ,delimiters,&ptrptr);
482 char *value=strtok_r(0,delimiters,&ptrptr);
484 if(!strcasecmp(name,"##END")) break;
486 if (!strcasecmp(name,"Filename")) Filename =strdup(value);
487 else if(!strcasecmp(name,"Filesize") ||
488 !strcasecmp(name,"Size")) Filesize =atoll(value);
489 else if(!strcasecmp(name,"FsID")) FsID =strdup(value);
490 else if(!strcasecmp(name,"Timestamp")) CTime =atol(value);
491 else if(!strcasecmp(name,"Touch")) touch =atol(value);
492 else if(!strcasecmp(name,"Version")) version =atoi(value);
493 else if(!strcasecmp(name,"DecoderID")) DecoderID =cDecoders::Str2ID(value);
494 else if(!strcasecmp(name,"Frames")) Frames =atoi(value);
495 else if(!strcasecmp(name,"Total")) Total =atoi(value);
496 else if(!strcasecmp(name,"SampleFreq")) SampleFreq=atoi(value);
497 else if(!strcasecmp(name,"Channels")) Channels =atoi(value);
498 else if(!strcasecmp(name,"Bitrate")) Bitrate =atoi(value);
499 else if(!strcasecmp(name,"MaxBitrate")) MaxBitrate=atoi(value);
500 else if(!strcasecmp(name,"ChMode")) ChMode =atoi(value);
501 else if(!strcasecmp(name,"Year")) Year =atoi(value);
502 else if(!strcasecmp(name,"Title")) Title =strdup(value);
503 else if(!strcasecmp(name,"Artist")) Artist =strdup(value);
504 else if(!strcasecmp(name,"Album")) Album =strdup(value);
505 else if(!strcasecmp(name,"Level")) Level =atof(value);
506 else if(!strcasecmp(name,"Peak")) Peak =atof(value);
507 else d(printf("cache: ignoring bad token '%s' from cache file\n",name))
512 if(ferror(f) || !Filename) return false;
513 hash=MakeHash(Filename);
517 // --- cInfoCache ----------------------------------------------------
519 cInfoCache::cInfoCache(void)
521 lasttime=0; modified=false;
522 lastpurge=time(0)-(50*60);
525 void cInfoCache::Shutdown(void)
531 void cInfoCache::Cache(cSongInfo *info, cFileInfo *file)
534 cCacheData *dat=Search(file);
536 dat->Create(file,info,true);
539 d(printf("cache: updating infos for %s\n",file->Filename))
543 dat->Create(file,info,false);
545 d(printf("cache: caching infos for %s\n",file->Filename))
550 cCacheData *cInfoCache::Search(cFileInfo *file)
552 int hash=MakeHash(file->Filename);
554 cCacheData *dat=FirstEntry(hash);
556 if(dat->hash==hash && !strcmp(dat->Filename,file->Filename) && dat->Filesize==file->Filesize) {
558 if(file->FsID && dat->FsID && !strcmp(dat->FsID,"old")) { // duplicate FsID for old entries
559 dat->FsID=strdup(file->FsID);
560 dat->Touch(); Modified();
561 //d(printf("adding FsID for %s\n",dat->Filename))
564 if((!file->FsID && !dat->FsID) || (file->FsID && dat->FsID && !strcmp(dat->FsID,file->FsID))) {
565 //d(printf("cache: found cache entry for %s\n",dat->Filename))
566 dat->Touch(); Modified();
567 if(dat->CTime!=file->CTime) {
568 d(printf("cache: ctime differs, removing from cache: %s\n",dat->Filename))
569 DelEntry(dat); dat=0;
575 dat=(cCacheData *)dat->Next();
581 void cInfoCache::AddEntry(cCacheData *dat)
583 lists[dat->hash%CACHELINES].Add(dat);
587 void cInfoCache::DelEntry(cCacheData *dat)
590 lists[dat->hash%CACHELINES].Del(dat);
594 cCacheData *cInfoCache::FirstEntry(int hash)
596 return lists[hash%CACHELINES].First();
599 bool cInfoCache::Purge(void)
602 if(now-lastpurge>(60*60)) {
603 isyslog("cleaning up id3 cache");
610 void cInfoCache::Action(void)
612 d(printf("cache: id3 cache purge thread started (pid=%d)\n",getpid()))
615 for(int i=0,n=0 ; i<CACHELINES && Running(); i++) {
616 cCacheData *dat=FirstEntry(i);
617 while(dat && Running()) {
618 cCacheData *ndat=(cCacheData *)dat->Next();
619 if(dat->Purge()) DelEntry(dat);
624 lock.Unlock(); lock.Lock(); // give concurrent thread an access chance
625 if(lastmod) dat=FirstEntry(i); // restart line, if cache changed meanwhile
630 d(printf("cache: id3 cache purge thread ended (pid=%d)\n",getpid()))
633 char *cInfoCache::CacheFile(void)
635 return AddPath(cachedir?cachedir:VideoDirectory,CACHEFILENAME);
638 void cInfoCache::Save(bool force)
640 if(modified && (force || (!Purge() && time(0)>lasttime))) {
641 char *name=CacheFile();
646 fprintf(f,"## This is a generated file. DO NOT EDIT!!\n"
647 "## This file will be OVERWRITTEN WITHOUT WARNING!!\n");
648 for(int i=0 ; i<CACHELINES ; i++) {
649 cCacheData *dat=FirstEntry(i);
651 if(!dat->Save(f)) { i=CACHELINES+1; break; }
652 dat=(cCacheData *)dat->Next();
657 modified=false; lasttime=time(0)+CACHESAVETIMEOUT;
658 d(printf("cache: saved cache to file\n"))
663 void cInfoCache::Load(void)
665 if(!strconv) strconv=new cStrConv;
667 char *name=CacheFile();
668 if(access(name,F_OK)==0) {
669 isyslog("loading id3 cache from %s",name);
670 FILE *f=fopen(name,"r");
675 for(int i=0 ; i<CACHELINES ; i++) lists[i].Clear();
676 while(fgets(buf,sizeof(buf),f)) {
677 if(!strcasecmp(buf,"##BEGIN\n")) {
678 cCacheData *dat=new cCacheData;
680 if(dat->version!=CACHE_VERSION) {
681 if(dat->Upgrade()) mod=true;
682 else { delete dat; continue; }
689 esyslog("ERROR: failed to load id3 cache");
697 modified=false; if(mod) Modified();
699 else LOG_ERROR_STR(name);