/* daapd 0.2.4, a server for the DAA protocol (c) deleet 2003, Alexander Oberdoerster database (filesystem scan, id3 parsing) daapd 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. daapd 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 daapd; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "types.h" #include "dboutput.h" #include "parsemp3.h" #include "util.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef MPEG4_ENABLE #include #endif #ifdef __sgi__ #define TIMESTAMP st_mtim.tv_sec; #elif defined(__linux__) #define TIMESTAMP st_mtime; #elif defined(__sun__) #define TIMESTAMP st_mtime; #else #define TIMESTAMP st_mtimespec.tv_sec; #endif using namespace std; int Database::getId3TextFrame( id3_tag* tag, const char* frameId, std::string& out, bool allStrings = true ) { id3_frame *frame; id3_field *field; out = ""; frame = id3_tag_findframe( tag, frameId, 0 ); if( frame == 0 ) return -1; field = &frame->fields[1]; int nstrings = id3_field_getnstrings( field ); for( int j = 0; j < (allStrings ? nstrings : 1); ++j ) { id3_ucs4_t *ucs4 = (id3_ucs4_t *)id3_field_getstrings( field, j ); if( strcmp( frameId, ID3_FRAME_GENRE ) == 0) ucs4 = (id3_ucs4_t *)id3_genre_name( ucs4 ); id3_utf8_t *utf8 = id3_ucs4_utf8duplicate(ucs4); if( utf8 != 0 ) out += (char *)utf8; free(utf8); } return 0; } int Database::getId3Comment( id3_tag* tag, std::string& out) { out = ""; for (int i=0; ; ++i) { id3_frame *frame = id3_tag_findframe(tag,ID3_FRAME_COMMENT,i); if( frame == 0 ) return -1; // Skip iTunes-generated comments. id3_field *field = id3_frame_field(frame,2); id3_ucs4_t *ucs4 = (id3_ucs4_t *)id3_field_getstring(field); char *scomm = (char *)id3_ucs4_utf8duplicate(ucs4); if ( (strcmp(scomm,"iTunNORM") == 0) || (strcmp(scomm,"iTunes_CDDB_IDs") == 0)) { free( scomm ); continue; } free( scomm ); field = id3_frame_field(frame,3); ucs4 = (id3_ucs4_t *)id3_field_getfullstring(field); out = (char *)id3_ucs4_utf8duplicate(ucs4); return 0; } } void Database::addMp3( std::string& path, struct stat sb ) { /* still missing: (in order of difficulty) u8 userrating; std::string eqpreset; iTunes only saves this in the database, but in principle, there are id3 frames defined for this. u8 relativevolume; libid3 reads this frame (RVA/RVA2), but discards it. u8 compilation; Stored in the non-standard Frame 'TCP' which libid3tag understandably doesn't decode. So other than hacking libid3tag (which I won't do), there's no way to get it. u8 disabled; No way to get this from the mp3 file, no id3 frame defined for it. Only saved in the iTunes database. std::string description; u8 datakind; std::string dataurl; No clue what these are about. */ Song *song = new Song; std::string s; if ( verbose ) printf("\taddMp3 on '%s'\n", path.c_str()); song->present = true; song->path = path; song->format = "mp3"; song->size = sb.st_size; song->dateadded = time( NULL ); song->datemodified = sb.TIMESTAMP; id3_file *file = id3_file_open( path.c_str(), ID3_FILE_MODE_READONLY ); if( file != 0 ) { id3_tag *tag = id3_file_tag( file ); if( getId3TextFrame( tag, ID3_FRAME_TITLE, s ) == 0 ) song->name = s; if( getId3TextFrame( tag, ID3_FRAME_ALBUM, s ) == 0 ) song->album = s; if( getId3TextFrame( tag, ID3_FRAME_ARTIST, s ) == 0 ) song->artist = s; if( getId3TextFrame( tag, "TCOM", s ) == 0 ) song->composer = s; if( getId3TextFrame( tag, ID3_FRAME_GENRE, s, false ) == 0 ) song->genre = s; if (getId3Comment( tag, s) == 0 ) song->comment = s; if( getId3TextFrame( tag, "TLEN", s ) == 0 ) song->time = strtol( s.c_str(), NULL, 10 ); if( getId3TextFrame( tag, "TDRC", s ) == 0 ) song->year = strtol( s.c_str(), NULL, 10 ); if( getId3TextFrame( tag, "TBPM", s ) == 0 ) song->bpm = strtol( s.c_str(), NULL, 10 ); if( getId3TextFrame( tag, "TRCK", s ) == 0 ) { song->tracknumber = strtol( s.c_str(), NULL, 10 ); char *sp; if( ( sp = strchr( s.c_str(), '/' ) ) != 0 ) song->trackcount = strtol( sp+1, NULL, 10 ); } if( getId3TextFrame( tag, "TPOS", s ) == 0 ) { song->discnumber = strtol( s.c_str(), NULL, 10 ); char *sp; if( ( sp = strchr( s.c_str(), '/' ) ) != 0 ) song->disccount = strtol( sp+1, NULL, 10 ); } id3_file_close( file ); } if( song->time == 0 && timeScan >= 0 ) { Mp3Info mp3info; if( parseMp3( path.c_str(), (u32) timeScan, mp3info ) ) { song->time = (u32) mp3info.time; song->bitrate = (u16) mp3info.bitRate; song->samplerate = (u32) mp3info.sampleRate; } } if( song->name == "" ) { // no song title in id3 tags // get it from file name ComponentVect comp; comp = *tokenizeString( comp, path.c_str(), '/' ); song->name = comp[ comp.size() - 1 ]; } song->starttime = 0; song->stoptime = song->time; pthread_mutex_lock(&dbLock); songs.add( song, path ); pthread_mutex_unlock(&dbLock); } void Database::addTagless( std::string& path, struct stat sb, int fileType ) { Song *song = new Song; std::string s; if ( verbose ) printf("\taddTagless on '%s'\n", path.c_str()); song->path = path; // this is (sort of) the reverse of the if / else in getFileType switch(fileType) { case DAAP_AIFF: song->format = "aif"; break; case DAAP_WAV: song->format = "wav"; break; case DAAP_SD2: song->format = "sd2"; break; case DAAP_M4A: song->format = "m4a"; break; } song->size = sb.st_size; song->dateadded = time( NULL ); song->datemodified = sb.TIMESTAMP; ComponentVect comp; comp = *tokenizeString( comp, path.c_str(), '/' ); song->present = true; song->name = comp[ comp.size() - 1 ]; song->starttime = 0; song->stoptime = song->time; pthread_mutex_lock(&dbLock); songs.add( song, path ); pthread_mutex_unlock(&dbLock); } void Database::addURL( std::string& path, int fileType ) { Song *song = new Song; std::string s; if ( verbose ) printf("\taddURL on '%s'\n", path.c_str()); song->dataurl = path; song->datakind = 1; // URL // this is (sort of) the reverse of the if / else in getFileType switch(fileType) { case DAAP_MP3: song->format = "mp3"; break; case DAAP_AIFF: song->format = "aif"; break; case DAAP_WAV: song->format = "wav"; break; case DAAP_SD2: song->format = "sd2"; break; case DAAP_M4A: song->format = "m4a"; break; } song->dateadded = time( NULL ); ComponentVect comp; comp = *tokenizeString( comp, path.c_str(), '/' ); song->present = true; song->name = comp[ comp.size() - 1 ]; song->starttime = 0; song->stoptime = song->time; pthread_mutex_lock(&dbLock); songs.add( song, path ); pthread_mutex_unlock(&dbLock); } #ifdef MPEG4_ENABLE void Database::addM4a( std::string& path, struct stat sb ) { Song *song = new Song; u16 i, j; char *s; if ( verbose ) printf("\taddM4a on '%s'\n", path.c_str()); song->present = true; song->path = path; song->format = "m4a"; song->size = sb.st_size; song->dateadded = time( NULL ); song->datemodified = sb.TIMESTAMP; MP4FileHandle mp4file = MP4Read(path.c_str()); if (mp4file != MP4_INVALID_FILE_HANDLE) { u32 numTracks = MP4GetNumberOfTracks(mp4file); if (numTracks == 1) { MP4TrackId trackId = MP4FindTrackId(mp4file, 0); u32 timeScale = MP4GetTrackTimeScale(mp4file, trackId); MP4Duration trackDuration = MP4GetTrackDuration(mp4file, trackId); double msDuration = UINT64_TO_DOUBLE(MP4ConvertFromTrackDuration(mp4file, trackId, trackDuration, MP4_MSECS_TIME_SCALE)); u32 avgBitRate = MP4GetTrackBitRate(mp4file, trackId); song->time = (u32) (msDuration); song->bitrate = (u16) ((avgBitRate + 500) / 1000); song->samplerate = (u32) timeScale; } if( MP4GetMetadataName( mp4file, &s ) ) { song->name = s; free( s ); } if( MP4GetMetadataAlbum( mp4file, &s ) ) { song->album = s; free( s ); } if( MP4GetMetadataArtist( mp4file, &s ) ) { song->artist = s; free( s ); } if( MP4GetMetadataWriter( mp4file, &s ) ) { song->composer = s; free( s ); } if( MP4GetMetadataGenre( mp4file, &s ) ) { song->genre = s; free( s ); } if( MP4GetMetadataComment( mp4file, &s ) ) { song->comment = s; free( s ); } if( MP4GetMetadataYear( mp4file, &s ) ) song->year = strtol( s, NULL, 10 ); if( MP4GetMetadataTempo( mp4file, &i ) ) song->bpm = ( long )i; if( MP4GetMetadataTrack( mp4file, &i, &j ) ) { song->tracknumber = ( long )i; if( j ) { song->trackcount = ( long )j; } } if( MP4GetMetadataDisk( mp4file, &i, &j ) ) { song->discnumber = ( long )i; if( j ) { song->disccount = ( long )j; } } MP4Close(mp4file); } if( song->name == "" ) { // no song title in tags // get it from file name ComponentVect comp; comp = *tokenizeString( comp, path.c_str(), '/' ); song->name = comp[ comp.size() - 1 ]; } song->starttime = 0; song->stoptime = song->time; pthread_mutex_lock( &dbLock ); songs.add( song, path ); pthread_mutex_unlock( &dbLock ); } #else void Database::addM4a( std::string& path, struct stat sb ) { addTagless( path, sb, DAAP_M4A ); } #endif int Database::getTypeFromExtension( std::string& fileName ) { // we just trust the user that files with extension // .mp3 are really mp3 files etc. // metadata in the filesystem would be cool :( char *pointPos; if( ( pointPos = strrchr( fileName.c_str(), '.' ) ) != 0 ) { if( strlen( pointPos ) >= 5 ) { if( strncasecmp( pointPos, ".aiff", 5 ) == 0 ) { return( DAAP_AIFF ); } } else if( strlen( pointPos ) >= 4 ) { if( strncasecmp( pointPos, ".mp3", 4 ) == 0 ) { return( DAAP_MP3 ); } else if( strncasecmp( pointPos, ".aif", 4 ) == 0 ) { return( DAAP_AIFF ); } else if( strncasecmp( pointPos, ".wav", 4 ) == 0 ) { return( DAAP_WAV ); } else if( strncasecmp( pointPos, ".sd2", 4 ) == 0 ) { return( DAAP_SD2 ); } else if( strncasecmp( pointPos, ".aac", 4 ) == 0 || strncasecmp( pointPos, ".m4a", 4 ) == 0 || strncasecmp( pointPos, ".m4p", 4 ) == 0 ) { return( DAAP_M4A ); } else if( strncasecmp( pointPos, ".m3u", 4 ) == 0 ) { return( DAAP_M3U ); } else if( strncasecmp( pointPos, ".pls", 4 ) == 0 ) { return( DAAP_PLS ); } } } return DAAP_INVALID; } int Database::getFileType( std::string& fileName ) { int type = DAAP_INVALID; errno = 0; if( verbose ) printf("\t\tLooking at File '%s...'\n ", fileName.c_str()); struct stat sb; if( stat( fileName.c_str(), &sb ) < 0 ) { if( verbose ) perror( "cannot stat" ); return( DAAP_INVALID ); } if( verbose ) printf("\t\tst_mode is %o\n", sb.st_mode); if( verbose ) printf("\t\tst_ino is %o\n", (int) sb.st_ino); if( (sb.st_mode & S_IFDIR) == S_IFDIR ) { return( DAAP_DIRECTORY ); } if( !( ((sb.st_mode & S_IFLNK) == S_IFLNK) || ((sb.st_mode & S_IFREG) == S_IFREG) ) ) { // file is neither regular nor link (nor directory) return( DAAP_INVALID ); } int fDesc = open( fileName.c_str(), 0, O_RDONLY ); if( fDesc < 0 ) { close( fDesc ); return( DAAP_INVALID ); } if( ( type = getTypeFromExtension( fileName ) ) != DAAP_INVALID ) { close( fDesc ); return type; } unsigned char buffer[12]; bzero( buffer, 3 ); if( read( fDesc, buffer, 12 ) > 0 ) { if( ( buffer[0] == 0xFF ) && ( ( buffer[1] & 0xF0 ) == 0xF0 ) || strncasecmp( (const char *)buffer, "ID3", 3 ) == 0 ) { type = DAAP_MP3; // in fact, reliably detecting mp3 files is very expensive. // first of all, the 12-Bit sync word can be 0xFF0 or 0xFFF, // depending on which documentation you consult. // in addition to that, some encoders write something // entirely different into the the sync word. // to make matters worse, there are even mp3 files starting // in the middle of a frame, so the first frame header // is not at the file offset 0. } else if( strncmp( (const char *)buffer, "FORM", 4 ) == 0 ) { if( strncmp( (const char *)buffer+8, "AIFF", 4 ) == 0 || strncmp( (const char *)buffer+8, "AIFC", 4 ) == 0 ) type = DAAP_AIFF; } else if( strncmp( (const char *)buffer, "RIFF", 4 ) == 0 ) { if( strncmp( (const char *)buffer+8, "WAVE", 4 ) == 0 ) type = DAAP_WAV; } else if( strncmp( (const char *)buffer, "#EXTM3U", 7 ) == 0 ) { type = DAAP_M3U; } else if( strncmp( (const char *)buffer, "[playlist]", 10 ) == 0 ) { type = DAAP_PLS; } } close( fDesc ); return( type ); } void Database::parsePls( Container &cont, bool fillPlaylist = false ) { if ( verbose ) printf("\tparsePls on '%s'\n", cont.path.c_str()); char *playlistDir = dirname( strdup( cont.path.c_str() ) ); ifstream inputFile; inputFile.open( cont.path.c_str() ); if(inputFile) { // read complete file into string string file; getline( inputFile, file, (char)EOF ); inputFile.close(); // split file into lines vector lines; string delimiters = "\r\n"; tokenizeString( file, lines, delimiters ); // parse lines for( u32 i = 0; i < lines.size(); ++i ) { if( lines[i].compare( 0, 4, "File" ) == 0 ) { int pos = lines[i].find_first_of( "=" ); int listIndex = strtol( lines[i].substr( 4, pos-1 ).c_str(), 0, 10 ); pos = lines[i].find_first_not_of( "= \t", pos ); string filepath = lines[i].substr( pos, lines[i].size() ) ; Song *song; if( filepath.compare( 0, 7, "http://" ) == 0 ) { song = songs.find( filepath ); if( song == 0 ) { int fileType = getTypeFromExtension( filepath ); addURL( filepath, fileType ); song = songs.find( filepath ); } } else { canonicalizePathname( filepath, playlistDir ); song = songs.find( filepath ); if( song == 0 ) { int fileType = getFileType( filepath ); addRegularFile( filepath, fileType ); song = songs.find( filepath ); } } if( song != 0 ) { song->present = true; if( fillPlaylist ) { pthread_mutex_lock(&dbLock); cont.items[listIndex] = song->id; pthread_mutex_unlock(&dbLock); } } } } } } void Database::parseM3u( Container &cont, bool fillPlaylist = false ) { if ( verbose ) printf("\tparseM3u on '%s'\n", cont.path.c_str()); char *playlistDir = dirname( strdup( cont.path.c_str() ) ); ifstream inputFile; inputFile.open( cont.path.c_str() ); if(inputFile) { // read complete file into string string file; getline( inputFile, file, (char)EOF ); inputFile.close(); // split file into lines vector lines; string delimiters = "\r\n"; tokenizeString( file, lines, delimiters ); // parse lines for( u32 i = 0; i < lines.size(); ++i ) { if( lines[i][0] != '#' ) { string filepath = lines[i]; Song *song; if( filepath.compare( 0, 7, "http://" ) == 0 ) { song = songs.find( filepath ); if( song == 0 ) { int fileType = getTypeFromExtension( filepath ); addURL( filepath, fileType ); song = songs.find( filepath ); } } else { canonicalizePathname( filepath, playlistDir ); song = songs.find( filepath ); if( song == 0 ) { int fileType = getFileType( filepath ); addRegularFile( filepath, fileType ); song = songs.find( filepath ); } } if( song != 0 ) { song->present = true; if( fillPlaylist ) { pthread_mutex_lock(&dbLock); cont.items[i] = song->id; pthread_mutex_unlock(&dbLock); } } } } } } void Database::addRegularFile( std::string& path, int fileType ) { // check if we already have this file // and if it has been modified since we added it struct stat sb; u32 datemodified; if( stat( path.c_str(), &sb ) == 0 ) { datemodified = sb.TIMESTAMP; canonicalizePathname( path ); Song *song = songs.find( path ); if( song != NULL && song->datemodified >= datemodified ) { song->present = true; } else { if( song != NULL ) { pthread_mutex_lock(&dbLock); songs.erase( path ); pthread_mutex_unlock(&dbLock); } revision++; if (fileType == DAAP_MP3 || fileType == DAAP_AIFF) { addMp3( path, sb ); } else if( fileType == DAAP_M4A ) { addM4a( path, sb ); } else { addTagless( path, sb, fileType ); } } } else { if( verbose ) perror( "cannot stat" ); } } void Database::addPlaylist( std::string& path, int fileType ) { // check if we already have this playlist // and if it has been modified since we added it struct stat sb; u32 datemodified; bool fillPlaylist = false; if( stat( path.c_str(), &sb ) == 0 ) { // add playlist itself datemodified = sb.TIMESTAMP; canonicalizePathname( path ); Container *cont = containers.find( path ); if( cont != NULL && cont->datemodified >= datemodified ) { cont->present = true; } else { fillPlaylist = true; if( cont != NULL ) { pthread_mutex_lock(&dbLock); containers.erase( path ); pthread_mutex_unlock(&dbLock); } cont = new Container; cont->path = path; cont->datemodified = datemodified; ComponentVect comp; comp = *tokenizeString( comp, path.c_str(), '/' ); cont->present = true; cont->name = comp[ comp.size() - 1 ]; revision++; pthread_mutex_lock(&dbLock); containers.add( cont, path ); pthread_mutex_unlock(&dbLock); if ( verbose ) printf("\tPlaylist %s added\n", path.c_str() ); } // add files in playlists to library (and playlist) if( fileType == DAAP_M3U ) { parseM3u( *cont, fillPlaylist ); } else if( fileType == DAAP_PLS ) { parsePls( *cont, fillPlaylist ); } } else { if( verbose ) perror( "cannot stat" ); } } // solaris compatible directory scan, thanks to Ben Whaley void Database::addDirectory( std::string& path ) { if ( *(path.end() - 1) != '/') path += "/"; struct dirent *entrylist; DIR *dirp = opendir(path.c_str()); if( dirp != NULL ) { if( verbose ) printf("Scanning Directory '%s'\n", path.c_str()); while ( (entrylist = readdir(dirp)) != NULL) { if( verbose ) printf("\tLooking at entry '%s'\n", entrylist->d_name); // skip hidden files (as well as '.' and '..') if( entrylist->d_name[0] != '.' ) { std::string fileName = path + entrylist->d_name; int fileType = getFileType( fileName ); if( verbose ) printf("\t\ttype %d\n", fileType); if( fileType == DAAP_DIRECTORY ) { addDirectory( fileName ); } else if( fileType == DAAP_MP3 || fileType == DAAP_AIFF || fileType == DAAP_WAV || fileType == DAAP_SD2 || fileType == DAAP_M4A ) { addRegularFile( fileName, fileType ); } else if( fileType == DAAP_M3U || fileType == DAAP_PLS ) { addPlaylist( fileName, fileType ); } } } free( entrylist ); closedir( dirp ); } }