/* * $Id$ * Implementation file for mp3 scanner and monitor * * Ironically, this now scans file types other than mp3 files, * but the name is the same for historical purposes, not to mention * the fact that sf.net makes it virtually impossible to manage a cvs * root reasonably. Perhaps one day soon they will move to subversion. * * /me crosses his fingers * * 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 */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #define _POSIX_PTHREAD_SEMANTICS #include #include #include #include #include #include #include #include #ifndef WIN32 #include /* htons and friends */ #include /* why here? For osx 10.2, of course! */ #endif #include #include "conf.h" #include "daapd.h" #include "db-generic.h" #include "err.h" #include "mp3-scanner.h" #include "os.h" #include "restart.h" #include "ssc.h" /* * Typedefs */ typedef struct { char *suffix; int (*scanner)(char* file, MP3FILE* pmp3); char *type; /* daap.songformat */ char *codectype; /* song.codectype */ char *description; /* daap.songdescription */ } TAGHANDLER; #define MAYBEFREE(a) { if((a)) free((a)); }; /* * Forwards */ static int scan_path(char *path); static int scan_get_info(char *file, MP3FILE *pmp3); static int scan_freetags(MP3FILE *pmp3); static void scan_music_file(char *path, char *fname,struct stat *psb, int is_compdir); static TAGHANDLER *scan_gethandler(char *type); /* EXTERNAL SCANNERS */ #ifdef OGGVORBIS extern int scan_get_ogginfo(char *filename, MP3FILE *pmp3); #endif #ifdef FLAC extern int scan_get_flacinfo(char *filename, MP3FILE *pmp3); #endif #ifdef MUSEPACK extern int scan_get_mpcinfo(char *filename, MP3FILE *pmp3); #endif extern int scan_get_wmainfo(char *filename, MP3FILE *pmp3); extern int scan_get_aacinfo(char *filename, MP3FILE *pmp3); extern int scan_get_wavinfo(char *filename, MP3FILE *pmp3); extern int scan_get_urlinfo(char *filename, MP3FILE *pmp3); extern int scan_get_mp3info(char *filename, MP3FILE *pmp3); extern int scan_get_mp4info(char *filename, MP3FILE *pmp3); /* playlist scanners */ extern int scan_xml_playlist(char *filename); static int scan_static_playlist(char *path); /* For known types, I'm gong to use the "official" apple * daap.songformat, daap.songdescription, and daap.songcodecsubtype. * If I we don't have "official" ones, we can make them up the * way we currently are: using extension or whatver. * * This means that you can test to see if something is, say, an un-drmed * aac file by just testing for ->type "m4a", rather than checking every * different flavor of file extension. * * NOTE: Although they are represented here as strings, the codectype is * *really* an unsigned short. So when it gets serialized, it gets * serialized as a short int. If you put something other than 3 or 4 * characters as your codectype, you'll see strange results. * * FIXME: url != pls -- this method of dispatching handlers based on file type * is completely wrong. There needs to be a separate type that gets carried * around with it, at least outside the database that says where the info * CAME FROM. * * This system is broken, and won't work with something like a .cue file */ static TAGHANDLER taghandlers[] = { { "aac", scan_get_aacinfo, "m4a", "mp4a", "AAC audio file" }, { "mp4", scan_get_aacinfo, "m4a", "mp4a", "AAC audio file" }, { "m4a", scan_get_aacinfo, "m4a", "mp4a", "AAC audio file" }, { "m4p", scan_get_aacinfo, "m4p", "mp4a", "AAC audio file" }, { "mp3", scan_get_mp3info, "mp3", "mpeg", "MPEG audio file" }, { "wav", scan_get_wavinfo, "wav", "wav", "WAV audio file" }, { "wma", scan_get_wmainfo, "wma", "wma", "WMA audio file" }, { "url", scan_get_urlinfo, "pls", NULL, "Playlist URL" }, { "pls", scan_get_urlinfo, "pls", NULL, "Playlist URL" }, { "m4v", scan_get_mp4info, "m4v", "mp4v", "MPEG-4 video file" }, #ifdef OGGVORBIS { "ogg", scan_get_ogginfo, "ogg", "ogg", "Ogg Vorbis audio file" }, #endif #ifdef FLAC { "flac", scan_get_flacinfo, "flac","flac", "FLAC audio file" }, { "fla", scan_get_flacinfo, "flac","flac", "FLAC audio file" }, #endif #ifdef MUSEPACK { "mpc", scan_get_mpcinfo, "mpc", "mpc", "Musepack audio file" }, { "mpp", scan_get_mpcinfo, "mpc", "mpc", "Musepack audio file" }, { "mp+", scan_get_mpcinfo, "mpc", "mpc", "Musepack audio file" }, #endif { NULL, NULL, NULL, NULL, NULL } }; typedef struct tag_playlistlist { char *path; struct tag_playlistlist *next; } PLAYLISTLIST; static PLAYLISTLIST scan_playlistlist = { NULL, NULL }; /** * add a playlist to the playlistlist. The playlistlist is a * list of playlists that need to be processed once the current * scan is done. THIS IS NOT REENTRANT, and it meant to be * called only inside the rescan loop. * * \param path path of the playlist to add */ void scan_add_playlistlist(char *path) { PLAYLISTLIST *plist; DPRINTF(E_SPAM,L_SCAN,"Adding playlist %s\n",path); if(!conf_get_int("general","process_m3u",0)) { DPRINTF(E_DBG,L_SCAN,"Skipping playlist %s (process_m3u)\n",path); return; } DPRINTF(E_DBG,L_SCAN,"Adding %s for deferred processing.\n",path); plist=(PLAYLISTLIST*)malloc(sizeof(PLAYLISTLIST)); if(!plist) { DPRINTF(E_FATAL,L_SCAN,"Malloc error\n"); return; } plist->path=strdup(path); plist->next=scan_playlistlist.next; scan_playlistlist.next=plist; } /** * process the playlistlist * */ void scan_process_playlistlist(void) { PLAYLISTLIST *pnext; char *ext; DPRINTF(E_DBG,L_SCAN,"Starting playlist loop\n"); while(scan_playlistlist.next) { pnext=scan_playlistlist.next; DPRINTF(E_DBG,L_SCAN,"About to scan %S\n",pnext->path); ext=pnext->path; if(strrchr(pnext->path,'.')) { ext = strrchr(pnext->path,'.'); } if(strcasecmp(ext,".xml") == 0) { if(conf_get_int("scanning","process xml",1)) { DPRINTF(E_LOG,L_SCAN,"Scanning %s\n",pnext->path); scan_xml_playlist(pnext->path); } } else if(strcasecmp(ext,".m3u") == 0) { scan_static_playlist(pnext->path); } else { DPRINTF(E_LOG,L_SCAN,"Unknown playlist type: %s\n",ext); } free(pnext->path); scan_playlistlist.next=pnext->next; free(pnext); } DPRINTF(E_DBG,L_SCAN,"Finished playlist loop\n"); } /* * 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 **patharray) { int err=0; int index=0; DPRINTF(E_DBG,L_SCAN,"Starting scan_init\n"); if(db_start_scan()) { DPRINTF(E_DBG,L_SCAN,"Error in db_start_scan()\n"); return -1; } scan_playlistlist.next=NULL; while(patharray[index] != NULL) { DPRINTF(E_DBG,L_SCAN,"Scanning for MP3s in %s\n",patharray[index]); err=scan_path(patharray[index]); index++; } if(db_end_song_scan()) return -1; DPRINTF(E_DBG,L_SCAN,"Processing playlists\n"); scan_process_playlistlist(); if(db_end_scan()) return -1; return err; } /** * check to see if a particular path is a complation path * * @param path path to check * @returns TRUE if it is a compilation path, FALSE otherwise */ int scan_is_compdir(char *path) { int current=0; char **compdirs; if(!conf_get_array("general","compdirs",&compdirs)) return FALSE; while(compdirs[current]) { if(strcasestr(path,compdirs[current])) { conf_dispose_array(compdirs); return TRUE; } current++; } conf_dispose_array(compdirs); return FALSE; } /* * scan_path * * Do a brute force scan of a path, finding all the MP3 files there */ int scan_path(char *path) { DIR *current_dir; char de[sizeof(struct dirent) + MAXNAMLEN + 1]; /* extra for solaris */ struct dirent *pde; int err; char relative_path[PATH_MAX]; char mp3_path[PATH_MAX]; struct stat sb; char *extensions; int is_compdir; extensions = conf_alloc_string("general","extensions",".mp3,.m4a,.m4p"); if((current_dir=opendir(path)) == NULL) { DPRINTF(E_WARN,L_SCAN,"opendir: %s\n",strerror(errno)); free(extensions); return -1; } is_compdir=scan_is_compdir(path); while(1) { if(config.stop) { DPRINTF(E_WARN,L_SCAN,"Stop req. Aborting scan of %s.\n",path); closedir(current_dir); free(extensions); return 0; } pde=(struct dirent *)&de; err=readdir_r(current_dir,(struct dirent *)de,&pde); if(err == -1) { DPRINTF(E_DBG,L_SCAN,"Error on readdir_r: %s\n",strerror(errno)); err=errno; closedir(current_dir); free(extensions); errno=err; return -1; } if(!pde) break; if(!strcmp(pde->d_name,".") || !strcmp(pde->d_name,"..")) continue; snprintf(relative_path,PATH_MAX,"%s/%s",path,pde->d_name); mp3_path[0] = '\x0'; realpath(relative_path,mp3_path); DPRINTF(E_DBG,L_SCAN,"Found %s\n",relative_path); if(stat(mp3_path,&sb)) { DPRINTF(E_WARN,L_SCAN,"Error statting: %s\n",strerror(errno)); } else { if(sb.st_mode & S_IFDIR) { /* dir -- recurse */ if(conf_get_int("scanning","ignore_appledouble",1) && ((strcasecmp(pde->d_name,".AppleDouble") == 0) || (strcasecmp(pde->d_name,".AppleDesktop") == 0))) { DPRINTF(E_DBG,L_SCAN,"Skipping appledouble folder\n"); } else if(conf_get_int("scanning","ignore_dotfiles",0) && pde->d_name[0] == '.') { DPRINTF(E_DBG,L_SCAN,"Skipping dotfile\n"); } else { DPRINTF(E_DBG,L_SCAN,"Found %s.. recursing\n",pde->d_name); scan_path(mp3_path); } } else { scan_filename(mp3_path, is_compdir, extensions); } } } closedir(current_dir); free(extensions); return 0; } /** * Scan a file as a static playlist * * @param path path to playlist */ int scan_static_playlist(char *path) { char base_path[PATH_MAX]; char file_path[PATH_MAX]; char real_path[PATH_MAX]; char linebuffer[PATH_MAX]; int fd; int playlistid; M3UFILE *pm3u; MP3FILE *pmp3; struct stat sb; char *current; char *perr; DPRINTF(E_WARN,L_SCAN|L_PL,"Processing static playlist: %s\n",path); if(stat(path,&sb)) { DPRINTF(E_WARN,L_SCAN,"Error statting %s: %s\n",path,strerror(errno)); return FALSE; } if((current=strrchr(path,PATHSEP)) == NULL) { current = path; } else { current++; } /* temporarily use base_path for m3u name */ strcpy(base_path,current); if((current=strrchr(base_path,'.'))) { *current='\x0'; } pm3u = db_fetch_playlist(NULL,path,0); if(pm3u && (pm3u->db_timestamp > sb.st_mtime)) { /* already up-to-date */ DPRINTF(E_DBG,L_SCAN,"Playlist already up-to-date\n"); db_dispose_playlist(pm3u); return TRUE; } if(pm3u) { DPRINTF(E_DBG,L_SCAN,"Playlist needs updated\n"); /* welcome to texas, y'all */ db_delete_playlist(NULL,pm3u->id); } fd=open(path,O_RDONLY); if(fd != -1) { if(db_add_playlist(&perr,base_path,PL_STATICFILE,NULL,path, 0,&playlistid) != DB_E_SUCCESS) { DPRINTF(E_LOG,L_SCAN,"Error adding m3u %s: %s\n",path,perr); free(perr); db_dispose_playlist(pm3u); close(fd); return FALSE; } /* now get the *real* base_path */ strcpy(base_path,path); if((current=strrchr(base_path,PATHSEP))) { *(current+1) = '\x0'; } /* else something is fubar */ DPRINTF(E_INF,L_SCAN|L_PL,"Added playlist as id %d\n",playlistid); memset(linebuffer,0x00,sizeof(linebuffer)); while(readline(fd,linebuffer,sizeof(linebuffer)) > 0) { while((linebuffer[strlen(linebuffer)-1] == '\n') || (linebuffer[strlen(linebuffer)-1] == '\r')) linebuffer[strlen(linebuffer)-1] = '\0'; if((linebuffer[0] == ';') || (linebuffer[0] == '#')) continue; // FIXME - should chomp trailing comments // otherwise, assume it is a path if((linebuffer[0] == PATHSEP) || (linebuffer[1] == ':')) { strcpy(file_path,linebuffer); } else { snprintf(file_path,sizeof(file_path),"%s%s",base_path,linebuffer); } realpath(file_path,real_path); DPRINTF(E_DBG,L_SCAN|L_PL,"Checking %s\n",real_path); // might be valid, might not... if((pmp3=db_fetch_path(&perr,real_path,0))) { /* FIXME: better error handling */ db_add_playlist_item(NULL,playlistid,pmp3->id); db_dispose_item(pmp3); } else { DPRINTF(E_WARN,L_SCAN|L_PL,"Playlist entry %s bad: %s\n", path,perr); free(perr); } } close(fd); } db_dispose_playlist(pm3u); DPRINTF(E_WARN,L_SCAN|L_PL,"Done processing playlist\n"); return TRUE; } /** * here, we want to scan a file and add it (if necessary) * to the database. * * @param path path of file to scan * @param compdir whether or not this is a compdir: * should be SCAN_TEST_COMPDIR if called form outisde * mp3-scanner.c */ void scan_filename(char *path, int compdir, char *extensions) { int is_compdir=compdir; char mp3_path[PATH_MAX]; struct stat sb; char *fname; char *ext; char *all_ext = extensions; int mod_time; MP3FILE *pmp3; if(compdir == 2) { /* need to really figure it out */ is_compdir = scan_is_compdir(path); } if(!all_ext) { all_ext = conf_alloc_string("general","extensions",".mp3,.m4a,.m4p"); } realpath(path,mp3_path); fname = strrchr(mp3_path,PATHSEP); if(!fname) { fname = mp3_path; } else { fname++; } if(conf_get_int("scanning","ignore_dotfiles",0)) { if(fname[0] == '.') return; if(strncmp(fname,":2e",3) == 0) return; } if(conf_get_int("scanning","ignore_appledouble",1)) { if(strncmp(fname,"._",2) == 0) return; } if(stat(mp3_path,&sb)) { DPRINTF(E_WARN,L_SCAN,"Error statting: %s\n",strerror(errno)); } else { /* we assume this is regular file */ if(strlen(fname) > 4) { ext = strrchr(fname, '.'); if(ext) { if(strcasecmp(".m3u",ext) == 0) { scan_add_playlistlist(mp3_path); } else if(strcasecmp(".xml",ext) == 0) { scan_add_playlistlist(mp3_path); } else if(strcasestr(all_ext, ext)) { mod_time = (int)sb.st_mtime; pmp3 = db_fetch_path(NULL,mp3_path,0); if((!pmp3) || (pmp3->db_timestamp < mod_time) || (pmp3->force_update)) { scan_music_file(path,fname,&sb,is_compdir); } else { DPRINTF(E_DBG,L_SCAN,"Skipping file, not modified\n"); } db_dispose_item(pmp3); } } } } if((all_ext) && (!extensions)) free(all_ext); return; } /* * scan_music_file * * scan a particular file as a music file */ void scan_music_file(char *path, char *fname, struct stat *psb, int is_compdir) { MP3FILE mp3file; char *current=NULL; char *type; TAGHANDLER *ptaghandler; char fdescr[50]; /* we found an mp3 file */ DPRINTF(E_INF,L_SCAN,"Found music file: %s\n",fname); memset((void*)&mp3file,0,sizeof(mp3file)); mp3file.path=strdup(path); mp3file.fname=strdup(fname); if((fname) && (strlen(fname) > 1) && (fname[strlen(fname)-1] != '.')) { type = strrchr(fname, '.') + 1; if(type) { /* see if there is "official" format and info for it */ ptaghandler=scan_gethandler(type); if(ptaghandler) { /* yup, use the official format */ mp3file.type=strdup(ptaghandler->type); if(ptaghandler->description) mp3file.description=strdup(ptaghandler->description); if(ptaghandler->codectype) mp3file.codectype=strdup(ptaghandler->codectype); DPRINTF(E_DBG,L_SCAN,"Codec type: %s\n",mp3file.codectype); } else { /* just dummy up songformat, codectype and description */ mp3file.type=strdup(type); mp3file.codectype = strdup("unkn"); mp3file.song_length = 10 * 60 * 1000; /* 10 min */ /* upper-case types cause some problems */ current=mp3file.type; while(*current) { *current=tolower(*current); current++; } sprintf(fdescr,"%s audio file",mp3file.type); mp3file.description = strdup(fdescr); /* we'll just dodge the codectype */ } } } /* Do the tag lookup here */ if(scan_get_info(mp3file.path,&mp3file)) { if(is_compdir) mp3file.compilation = 1; make_composite_tags(&mp3file); /* fill in the time_added. I'm not sure of the logic in this. My thinking is to use time created, but what is that? Best guess would be earliest of st_mtime and st_ctime... */ mp3file.time_added=(int) psb->st_mtime; if(psb->st_ctime < mp3file.time_added) mp3file.time_added=(int) psb->st_ctime; mp3file.time_modified=(int) psb->st_mtime; DPRINTF(E_DBG,L_SCAN," Date Added: %d\n",mp3file.time_added); DPRINTF(E_DBG,L_SCAN," Codec: %s\n",mp3file.codectype); /* FIXME: error handling */ db_add(NULL,&mp3file,NULL); } else { DPRINTF(E_WARN,L_SCAN,"Skipping %s - scan failed\n",mp3file.path); } scan_freetags(&mp3file); } /** * fetch the taghandler for this file type */ TAGHANDLER *scan_gethandler(char *type) { TAGHANDLER *phdl = taghandlers; while((phdl->suffix) && (strcasecmp(phdl->suffix,type))) phdl++; if(phdl->suffix) return phdl; return NULL; } /* * scan_freetags * * Free up the tags that were dynamically allocated */ int scan_freetags(MP3FILE *pmp3) { MAYBEFREE(pmp3->path); MAYBEFREE(pmp3->fname); MAYBEFREE(pmp3->title); MAYBEFREE(pmp3->artist); MAYBEFREE(pmp3->album); MAYBEFREE(pmp3->genre); MAYBEFREE(pmp3->comment); MAYBEFREE(pmp3->type); MAYBEFREE(pmp3->composer); MAYBEFREE(pmp3->orchestra); MAYBEFREE(pmp3->conductor); MAYBEFREE(pmp3->grouping); MAYBEFREE(pmp3->description); MAYBEFREE(pmp3->codectype); return 0; } /** * Dispatch to actual file info handlers * * @param file file to read file metainfo for * @param pmp3 struct to stuff with info gleaned */ int scan_get_info(char *file, MP3FILE *pmp3) { FILE *infile; off_t file_size; TAGHANDLER *hdl; /* dispatch to appropriate tag handler */ hdl = scan_gethandler(pmp3->type); if(hdl && hdl->scanner) return hdl->scanner(file,pmp3); /* a file we don't know anything about... ogg or aiff maybe */ if(!(infile=fopen(file,"rb"))) { DPRINTF(E_WARN,L_SCAN,"Could not open %s for reading\n",file); return FALSE; } /* we can at least get this */ fseek(infile,0,SEEK_END); file_size=ftell(infile); fseek(infile,0,SEEK_SET); pmp3->file_size=file_size; fclose(infile); return TRUE; } /** * Manually build tags. Set artist to computer/orchestra * if there is already no artist. Perhaps this could be * done better, but I'm not sure what else to do here. * * @param song MP3FILE of the file to build composite tags for */ void make_composite_tags(MP3FILE *song) { int len; char *ptmp; char *sep = " - "; char *va_artist = "Various Artists"; if(!song->artist) { if (song->orchestra && song->conductor) { len = (int)strlen(song->orchestra) + (int)strlen(sep) + (int)strlen(song->conductor); ptmp = (char*)malloc(len + 1); if(ptmp) { sprintf(ptmp,"%s%s%s",song->orchestra, sep, song->conductor); song->artist = ptmp; } } else if(song->orchestra) { song->artist = strdup(song->orchestra); } else if (song->conductor) { song->artist = strdup(song->conductor); } } if(song->compilation && song->artist && song->title && (conf_get_int("scanning","concat compilations",0))) { len = (int)strlen(song->artist) + (int)strlen(sep) + (int)strlen(song->title); ptmp = (char*)malloc(len + 1); if(ptmp) { sprintf(ptmp,"%s%s%s",song->artist, sep, song->title); free(song->title); song->title = ptmp; if(va_artist) { ptmp = strdup(va_artist); if(ptmp) { free(song->artist); song->artist = ptmp; } } } } if(song->url) song->data_kind=1; else song->data_kind=0; if(!song->title) song->title = strdup(song->fname); song->item_kind = 2; /* music, I think. */ }