/* * $Id$ * Implementation file for mp3 scanner and monitor * * Copyright (C) 2003 Ron Pedde (ron@pedde.com) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #define _POSIX_PTHREAD_SEMANTICS #include #include #include #include #include #include #include #include #include #include #include #include #include "db-memory.h" #include "err.h" #include "mp3-scanner.h" #include "playlist.h" /* * Typedefs */ typedef struct tag_scan_id3header { unsigned char id[3]; unsigned char version[2]; unsigned char flags; unsigned char size[4]; } SCAN_ID3HEADER; #define MAYBEFREE(a) { if((a)) free((a)); }; /* * Globals */ int scan_br_table[] = { 0,32,40,48,56,64,80,96,112,128,160,192,224,256,320,0 }; char *scan_winamp_genre[] = { "Blues", // 0 "Classic Rock", "Country", "Dance", "Disco", "Funk", // 5 "Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", // 10 "Oldies", "Other", "Pop", "R&B", "Rap", // 15 "Reggae", "Rock", "Techno", "Industrial", "Alternative", // 20 "Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", // 25 "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", // 30 "Trance", "Classical", "Instrumental", "Acid", "House", // 35 "Game", "Sound Clip", "Gospel", "Noise", "AlternRock", // 40 "Bass", "Soul", "Punk", "Space", "Meditative", // 45 "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", // 50 "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", // 55 "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", // 60 "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret", // 65 "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", // 70 "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", // 75 "Retro", "Musical", "Rock & Roll", "Hard Rock", "Folk", // 80 "Folk/Rock", "National folk", "Swing", "Fast-fusion", "Bebob", // 85 "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", // 90 "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock", // 95 "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", // 100 "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", // 105 "Symphony", "Booty Bass", "Primus", "Porn Groove", "Satire", // 110 "Slow Jam", "Club", "Tango", "Samba", "Folklore", // 115 "Ballad", "Powder Ballad", "Rhythmic Soul", "Freestyle", "Duet", // 120 "Punk Rock", "Drum Solo", "A Capella", "Euro-House", "Dance Hall", // 125 "Goa", "Drum & Bass", "Club House", "Hardcore", "Terror", // 130 "Indie", "BritPop", "NegerPunk", "Polsk Punk", "Beat", // 135 "Christian Gangsta", "Heavy Metal", "Black Metal", "Crossover", "Contemporary C", // 140 "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", // 145 "JPop", "SynthPop", "Unknown" }; #define WINAMP_GENRE_UNKNOWN 148 /* * Forwards */ int scan_foreground(char *path); int scan_gettags(char *file, MP3FILE *pmp3); int scan_getfileinfo(char *file, MP3FILE *pmp3); int scan_freetags(MP3FILE *pmp3); /* * scan_init * * This assumes the database is already initialized. * * Ideally, this would check to see if the database is empty. * If it is, it sets the database into bulk-import mode, and scans * the MP3 directory. * * If not empty, it would start a background monitor thread * and update files on a file-by-file basis */ int scan_init(char *path) { int err; if(db_is_empty()) { if(db_start_initial_update()) return -1; DPRINTF(ERR_DEBUG,"Scanning for MP3s in %s\n",path); err=scan_foreground(path); if(db_end_initial_update()) return -1; } else { /* do deferred updating */ return ENOTSUP; } return err; } /* * scan_foreground * * Do a brute force scan of a path, finding all the MP3 files there */ int scan_foreground(char *path) { MP3FILE mp3file; DIR *current_dir; char de[sizeof(struct dirent) + MAXNAMLEN + 1]; /* overcommit for solaris */ struct dirent *pde; int err; char mp3_path[PATH_MAX]; char m3u_path[PATH_MAX]; char linebuffer[PATH_MAX]; int fd; int playlistid; struct stat sb; if((current_dir=opendir(path)) == NULL) { return -1; } while(1) { pde=(struct dirent *)&de; err=readdir_r(current_dir,(struct dirent *)de,&pde); if(err == -1) { DPRINTF(ERR_DEBUG,"Error on readdir_r: %s\n",strerror(errno)); err=errno; closedir(current_dir); errno=err; return -1; } if(!pde) break; if(pde->d_name[0] == '.') /* skip hidden and directories */ continue; sprintf(mp3_path,"%s/%s",path,pde->d_name); DPRINTF(ERR_DEBUG,"Found %s\n",mp3_path); if(stat(mp3_path,&sb)) { DPRINTF(ERR_WARN,"Error statting: %s\n",strerror(errno)); } if(sb.st_mode & S_IFDIR) { /* dir -- recurse */ DPRINTF(ERR_DEBUG,"Found dir %s... recursing\n",pde->d_name); scan_foreground(mp3_path); } else { DPRINTF(ERR_DEBUG,"Processing file\n"); /* process the file */ if(strlen(pde->d_name) > 4) { if(strcasecmp(".m3u",(char*)&pde->d_name[strlen(pde->d_name) - 4]) == 0) { /* we found an m3u file */ DPRINTF(ERR_DEBUG,"Found m3u: %s\n",pde->d_name); strcpy(m3u_path,pde->d_name); m3u_path[strlen(pde->d_name) - 4] = '\0'; playlistid=sb.st_ino; fd=open(mp3_path,O_RDONLY); if(fd != -1) { db_add_playlist(playlistid,m3u_path); while(readline(fd,linebuffer,sizeof(linebuffer)) > 0) { while(linebuffer[strlen(linebuffer)-1] == '\n') linebuffer[strlen(linebuffer)-1] = '\0'; if((linebuffer[0] == ';') || (linebuffer[0] == '#')) continue; /* otherwise, assume it is a path */ if(linebuffer[0] == '/') { strcpy(m3u_path,linebuffer); } else { snprintf(m3u_path,sizeof(m3u_path),"%s/%s",path,linebuffer); } DPRINTF(ERR_DEBUG,"Checking %s\n",m3u_path); /* might be valid, might not... */ if(!stat(m3u_path,&sb)) { /* FIXME: check to see if valid inode! */ db_add_playlist_song(playlistid,sb.st_ino); } else { DPRINTF(ERR_WARN,"Playlist entry %s bad: %s\n", m3u_path,strerror(errno)); } } close(fd); } } else if(strcasecmp(".mp3",(char*)&pde->d_name[strlen(pde->d_name) - 4]) == 0) { /* we found an mp3 file */ DPRINTF(ERR_DEBUG,"Found mp3: %s\n",pde->d_name); memset((void*)&mp3file,0,sizeof(mp3file)); mp3file.path=mp3_path; mp3file.fname=pde->d_name; #ifdef MAC /* wtf is this about? */ mp3file.mtime=sb.st_mtimespec.tv_sec; mp3file.atime=sb.st_atimespec.tv_sec; mp3file.ctime=sb.st_ctimespec.tv_sec; #else mp3file.mtime=sb.st_mtime; mp3file.atime=sb.st_atime; mp3file.ctime=sb.st_ctime; #endif /* FIXME; assumes that st_ino is a u_int_32 */ mp3file.id=sb.st_ino; /* Do the tag lookup here */ if(!scan_gettags(mp3file.path,&mp3file) && !scan_getfileinfo(mp3file.path,&mp3file)) { db_add(&mp3file); pl_eval(&mp3file); } else { DPRINTF(ERR_INFO,"Skipping %s\n",pde->d_name); } scan_freetags(&mp3file); } } } } closedir(current_dir); return 0; } /* * scan_gettags * * Scan an mp3 file for id3 tags using libid3tag */ int scan_gettags(char *file, MP3FILE *pmp3) { struct id3_file *pid3file; struct id3_tag *pid3tag; struct id3_frame *pid3frame; int err; int index; int used; unsigned char *utf8_text; int genre=WINAMP_GENRE_UNKNOWN; int have_utf8; int have_text; id3_ucs4_t const *native_text; char *tmp; pid3file=id3_file_open(file,ID3_FILE_MODE_READONLY); if(!pid3file) { DPRINTF(ERR_WARN,"Cannot open %s\n",file); return -1; } pid3tag=id3_file_tag(pid3file); if(!pid3tag) { err=errno; id3_file_close(pid3file); errno=err; DPRINTF(ERR_WARN,"Cannot get ID3 tag for %s\n",file); return -1; } index=0; while((pid3frame=id3_tag_findframe(pid3tag,"",index))) { used=0; utf8_text=NULL; native_text=NULL; have_utf8=0; have_text=0; if(((pid3frame->id[0] == 'T')||(strcmp(pid3frame->id,"COMM")==0)) && (id3_field_getnstrings(&pid3frame->fields[1]))) have_text=1; if(have_text) { native_text=id3_field_getstrings(&pid3frame->fields[1],0); if(native_text) { have_utf8=1; utf8_text=id3_ucs4_utf8duplicate(native_text); MEMNOTIFY(utf8_text); if(!strcmp(pid3frame->id,"TIT2")) { /* Title */ used=1; pmp3->title = utf8_text; DPRINTF(ERR_DEBUG," Title: %s\n",utf8_text); } else if(!strcmp(pid3frame->id,"TPE1")) { used=1; pmp3->artist = utf8_text; DPRINTF(ERR_DEBUG," Artist: %s\n",utf8_text); } else if(!strcmp(pid3frame->id,"TALB")) { used=1; pmp3->album = utf8_text; DPRINTF(ERR_DEBUG," Album: %s\n",utf8_text); } else if(!strcmp(pid3frame->id,"TCON")) { used=1; pmp3->genre = utf8_text; DPRINTF(ERR_DEBUG," Genre: %s\n",utf8_text); if(pmp3->genre) { if(!strlen(pmp3->genre)) { genre=WINAMP_GENRE_UNKNOWN; } else if (isdigit(pmp3->genre[0])) { genre=atoi(pmp3->genre); } else if ((pmp3->genre[0] == '(') && (isdigit(pmp3->genre[1]))) { genre=atoi((char*)&pmp3->genre[1]); } if((genre < 0) || (genre > WINAMP_GENRE_UNKNOWN)) genre=WINAMP_GENRE_UNKNOWN; free(pmp3->genre); pmp3->genre=strdup(scan_winamp_genre[genre]); } } else if(!strcmp(pid3frame->id,"COMM")) { used=1; pmp3->comment = utf8_text; DPRINTF(ERR_DEBUG," Comment: %s\n",pmp3->comment); } else if(!strcmp(pid3frame->id,"TRCK")) { tmp=(char*)utf8_text; strsep(&tmp,"/"); if(tmp) { pmp3->total_tracks=atoi(tmp); } pmp3->track=atoi((char*)utf8_text); DPRINTF(ERR_DEBUG," Track %d of %d\n",pmp3->track,pmp3->total_tracks); } else if(!strcmp(pid3frame->id,"TDRC")) { pmp3->year = atoi(utf8_text); DPRINTF(ERR_DEBUG," Year: %d\n",pmp3->year); } } } /* can check for non-text tags here */ if((!used) && (have_utf8) && (utf8_text)) free(utf8_text); index++; } pmp3->got_id3=1; id3_file_close(pid3file); DPRINTF(ERR_DEBUG,"Got id3 tag successfully\n"); return 0; } /* * scan_freetags * * Free up the tags that were dynamically allocated */ int scan_freetags(MP3FILE *pmp3) { if(!pmp3->got_id3) return 0; MAYBEFREE(pmp3->title); MAYBEFREE(pmp3->artist); MAYBEFREE(pmp3->album); MAYBEFREE(pmp3->genre); MAYBEFREE(pmp3->comment); return 0; } /* * scan_getfileinfo * * Get information from the file headers itself -- like * song length, bit rate, etc. */ int scan_getfileinfo(char *file, MP3FILE *pmp3) { FILE *infile; SCAN_ID3HEADER *pid3; unsigned int size=0; off_t fp_size=0; off_t file_size; unsigned char buffer[256]; int time_seconds; int ver=0; int layer=0; int bitrate=0; int samplerate=0; if(!(infile=fopen(file,"rb"))) { DPRINTF(ERR_WARN,"Could not open %s for reading\n",file); return -1; } fread(buffer,1,sizeof(buffer),infile); pid3=(SCAN_ID3HEADER*)buffer; if(strncmp(pid3->id,"ID3",3)==0) { /* found an ID3 header... */ DPRINTF(ERR_DEBUG,"Found ID3 header\n"); size = (pid3->size[0] << 21 | pid3->size[1] << 14 | pid3->size[2] << 7 | pid3->size[3]); fp_size=size + sizeof(SCAN_ID3HEADER); DPRINTF(ERR_DEBUG,"Header length: %d\n",size); } fseek(infile,0,SEEK_END); file_size=ftell(infile); file_size -= fp_size; fseek(infile,fp_size,SEEK_SET); fread(buffer,1,sizeof(buffer),infile); if((buffer[0] == 0xFF)&&(buffer[1] >= 224)) { ver=(buffer[1] & 0x18) >> 3; layer=(buffer[1] & 0x6) >> 1; if((ver==3) && (layer==1)) { /* MPEG1, Layer 3 */ bitrate=(buffer[2] & 0xF0) >> 4; bitrate=scan_br_table[bitrate]; samplerate=(buffer[2] & 0x0C) >> 2; switch(samplerate) { case 0: samplerate=44100; break; case 1: samplerate=48000; break; case 2: samplerate=32000; break; } pmp3->bitrate=bitrate; pmp3->samplerate=samplerate; } else { /* not an mp3... */ DPRINTF(ERR_DEBUG,"File is not a MPEG-1/Layer III\n"); return -1; } /* guesstimate the file length */ time_seconds = ((int)(file_size * 8)) / (bitrate * 1024); pmp3->song_length=time_seconds; pmp3->file_size=file_size; } else { /* should really scan forward to next sync frame */ fclose(infile); DPRINTF(ERR_DEBUG,"Could not find sync frame\n"); return -1; } fclose(infile); return 0; }