/* * Copyright (c) Alex Allan * All rights reserved. * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ /** * tag.cpp * Implementation of tag.h. This implementation uses TagLib. */ #include #include #include #include #include #include // LibC TODO Replace with STL // POSIX #include // Non-POSIX #include // XPG4.2 #include #include #include #include #include "tracks.h" #include "cddb_query.h" // Implements: #include "tag.h" namespace TagLookup { // Utility functions: const TagLib::String::String toUTF8String(const std::string& s) { const TagLib::String::String new_string(s, TagLib::String::UTF8); return new_string; } // toUTF8String int static const frames_per_second = 75; // frames /** * Generates a set of PathFileRefs from file path strings. */ struct FileDisc::InstantiateTagLibFiles : public std::unary_function { std::vector * fs_; InstantiateTagLibFiles(std::vector &files) { fs_ = &files; } void operator() (const std::string & f) { PathFileRef pfr; pfr.path = f; pfr.file = TagLib::FileRef(f.c_str()); fs_->push_back(pfr); } }; /** * Generates a list of frame offsets suitable for use with CDDB queries */ struct FileDisc::OffsetsCounter : public std::unary_function { int current_offset; int length; // in seconds OffsetList ol; OffsetsCounter(const int &initial_offset) { current_offset = initial_offset; length = 0; } void operator() (const PathFileRef &pfr) { TagLib::AudioProperties *ap = NULL; int l; ap = pfr.file.audioProperties(); // if f is not null and does not have null properties if(!pfr.file.isNull() && ap != NULL) { l = ap->length(); length += l; ol.push_back(current_offset); current_offset += l * frames_per_second; } // if valid properties else throw TagException("File or audioProperties is NULL"); } // operator() (...) }; // struct generateOffsetListStruct typedef std::pair FileRefLengthPair; /** * Generates a vector of pairs of file references and lengths. */ struct FileDisc::DiscCopyWithLengths : public std::unary_function { std::vector fll; DiscCopyWithLengths() { } void operator() (const PathFileRef & f) { TagLib::AudioProperties *ap = NULL; unsigned int l; ap = f.file.audioProperties(); // if f is not null and does not have null properties if(!f.file.isNull() && ap != NULL) { l = ap->length(); fll.push_back(std::pair(f.file, l)); } // if valid properties else throw TagException("File or audioProperties is NULL"); } // operator() }; // struct DiscCopyWithLength /** * Binary function that returns true if the difference between f1 and x is * less than the difference between f2 and x. */ struct FileDisc::DiffFromX : public std::binary_function { unsigned int x_; DiffFromX(unsigned int x) : x_(x) { } bool operator() (const FileRefLengthPair &f1, const FileRefLengthPair &f2) { if( abs(f1.second - x_) < abs(f2.second - x_) ) return true; else return false; } }; // DiffFromX /** * Prepare a list of new file names based on template. */ struct FileDisc::PrepareNewNames : public std::unary_function { private: std::string templ_; // template void replace_with(std::string &s, const std::string &token, const std::string &replacement) { size_t pos = s.find(token, 0); if(pos != (size_t)(-1)) s.replace(pos, token.size(), replacement); return; } void replace_with(std::string &s, const std::string &token, const TagLib::String &replacement) { replace_with(s, token, replacement.to8Bit()); return; } public: std::vector new_names; PrepareNewNames(const std::string &templ) : templ_(templ), new_names() { } // This rename could easily throw void operator() (const PathFileRef &pfr) { std::string new_name(templ_); char temp[255]; // used for string representations of numbers. const TagLib::Tag *tag = pfr.file.tag(); // tag may well be NULL // Make sure sure you call checkFiles() before you call // RenameFile.operator() if(tag == NULL) throw TagException("Tag is NULL when trying to generate a name."); // replace each token with the approriate item from the cddb disc replace_with(new_name, ARTIST_TOKEN, tag->artist()); replace_with(new_name, GENRE_TOKEN, tag->genre()); replace_with(new_name, SONG_TITLE_TOKEN, tag->title()); replace_with(new_name, ALBUM_TITLE_TOKEN, tag->album()); // Number-based fields snprintf(temp, sizeof(temp), "%d", tag->year()); // TODO: replace with STL replace_with(new_name, YEAR_TOKEN, std::string(temp)); snprintf(temp, sizeof(temp), "%02d", tag->track()); // TODO: replace with STL replace_with(new_name, TRACK_NUMBER, std::string(temp)); OldNameNewName onnn; onnn.old_name = pfr.path; onnn.new_name = new_name; new_names.push_back(onnn); return; } }; /** * Renames files using an OldNameNewNam object. * This is required to work out the directory wrt the whole path. The file * is then renamed. Therefore */ struct FileDisc::RenameFiles : public std::unary_function { public: RenameFiles() { } void operator() (const OldNameNewName &onnn) { std::string new_base(basename(onnn.new_name.c_str())); std::string new_path(dirname(onnn.old_name.c_str())); new_path.append("/"); new_path.append(new_base); rename(onnn.old_name.c_str(), new_path.c_str()); return; } }; struct FileDisc::PrintFileDisc : public std::unary_function { private: std::ostream &os_; unsigned int i_; public: PrintFileDisc(std::ostream &os) : os_(os), i_(0) { } void operator() (const PathFileRef &pfr) { TagLib::AudioProperties *ap = pfr.file.audioProperties(); TagLib::Tag *tag = pfr.file.tag(); ++i_; if(!pfr.file.isNull() && ap != NULL && tag != NULL) { const unsigned int length = ap->length(); const unsigned int mins = length / 60; const unsigned int secs = length % 60; os_ << (i_ < 10 ? " " : "") << i_ << ". " << pfr.path << " (" << mins << ':' << (secs < 10 ? "0" : "") << secs << ')' << std::endl; os_ << " " << tag->artist() << " - " << tag->title() << " (" << tag->track() << ", " << tag->year() << ", " << tag->genre() << ")" << std::endl; } else throw TagException("File, audioProperties or Tag is NULL"); } }; struct FileDisc::CheckValidity : public std::unary_function { ProblemFiles problems; CheckValidity() { } void operator() (const PathFileRef &pfr) { struct stat file_stat; ProblemFile pf; ProblemType pt = TAGLOOKUP_NONE; TagLib::File *f = pfr.file.file(); if(f == NULL || pfr.path.empty() || stat(pfr.path.c_str(), &file_stat) != 0 ) { pt = TAGLOOKUP_INVALID_FILE; } else { if(!f->isValid()) pt = TAGLOOKUP_INVALID_FILE; if(f->readOnly()) { if(pt == TAGLOOKUP_INVALID_FILE) pt = TAGLOOKUP_READONLY_AND_INVALID_FILE; else pt = TAGLOOKUP_READONLY; } } // if stat() if(pt != TAGLOOKUP_NONE) { pf.type = pt; pf.path = pfr.path; problems.push_back(pf); } } // operator() }; // struct CheckValidity FileDisc::FileDisc(const std::vector & file_path_list) { InstantiateTagLibFiles fl = std::for_each( file_path_list.begin(), file_path_list.end(), InstantiateTagLibFiles(files_) ); } // Implementation for FileDisc class starts here const Offsets & FileDisc::generateOffsets(int inital_offset) { /** * Generates data suitable for use in a CDDB query. * Uses: _fl, the list of track files. * Takes: initial_offset, the inital offset for the query * Returns: an Offsets object. * See also: CDDBQuery class */ OffsetsCounter oc = for_each(files_.begin(), files_.end(), OffsetsCounter(inital_offset)); // This is exception-safe because all the dangerous exceptions should // have been raised in the for_each alg. of_.length = oc.length; of_.offset_list = oc.ol; return of_; } // generateOffsets(...) void FileDisc::tagByBestFit(const Disc& disc) { // First, make a new representation of the fl_ with the lengths included. DiscCopyWithLengths dcwl = for_each(files_.begin(), files_.end(), DiscCopyWithLengths()); std::vector fll(dcwl.fll); // copy // Then, we iterate through each 'track' in 'disc', finding the smallest // difference. std::vector::iterator smallest_diff; unsigned int j = 1; const Tracks::const_iterator te = disc.getTracks().end(); for(Tracks::const_iterator i = disc.getTracks().begin(); !fll.empty() && i != te; // TODO not sure about this! ++i) { smallest_diff = std::min_element(fll.begin(), fll.end(), DiffFromX(i->getLength())); TagLib::FileRef &sd_fr = smallest_diff->first; std::cout << j++ << ": File " << smallest_diff->first.file()->name() << " matches " << i->getName() << " [Tag Saved]" << std::endl; stripTag(sd_fr); setTag(disc, *i, sd_fr); sd_fr.save(); fll.erase(smallest_diff); } // for(...) } // tagByBestFit(...) void FileDisc::tagByOrder(const Disc& disc) { // fl_ may be smaller than disc or disc may be smaller than fl_ // we should look until either fl_ or disc runs out of tracks. const Tracks &tracks = disc.getTracks(); const Tracks::const_iterator te = tracks.end(); const std::vector::const_iterator fe = files_.end(); Tracks::const_iterator ti = tracks.begin(); std::vector::iterator fi = files_.begin(); int j = 1; while(ti != te && fi != fe) { stripTag(fi->file); setTag(disc, *ti, fi->file); fi->file.save(); std::cout << j++ << ": File " << fi->path << " [Tag Saved]" << std::endl; ++ti; ++fi; } } // tagByOrder void FileDisc::stripTag(const TagLib::FileRef &fr) { /** * If this file reference is strippable then strip it, otherwise * do nothing. * Side effect: may strip the tags from the file specified. */ TagLib::MPEG::File *mpeg; if( (mpeg = dynamic_cast(fr.file())) ) mpeg->strip(); return; } // end stripTag(...) void FileDisc::setTag(const Disc &disc, const Track &track, TagLib::FileRef &fr) { /** * Uses taglib to set the tag for the given file. * Takes: disc, uses this for the disc-wide details; track, uses this * for the track-specific details; fr, the file to tag. * Side effects: will change the contents of fr. */ TagLib::Tag* tag = fr.tag(); if(tag == NULL) throw TagException("Tag is NULL"); tag->setTitle(toUTF8String(track.getName())); tag->setArtist(toUTF8String(track.getArtist())); tag->setTrack(track.getTrackNo()); tag->setAlbum(toUTF8String(disc.getTitle())); tag->setGenre(toUTF8String(disc.getGenre())); tag->setYear(disc.getYear()); return; } // setDetails(...) void FileDisc::printDetails(std::ostream &os) { os << "Output format: Artist - Title (Track Number, Year, Genre)" << std::endl << std::endl; for_each(files_.begin(), files_.end(), PrintFileDisc(os)); } // printDetails /** * We are aiming for a conistant and 'atomic' update for the files in * this object. As you can't 'undo' saves in taglib, we need to check * to see if all files are valid and writiable. * * Check files will return a list of files that it sees as a problem. */ const ProblemFiles & FileDisc::checkFiles() { CheckValidity cv = for_each(files_.begin(), files_.end(), CheckValidity()); problems_ = cv.problems; return problems_; } /** * Renames the files in this FileDisc */ const std::vector & FileDisc::generateNewNames(const std::string& templ) { PrepareNewNames pnn = for_each(files_.begin(), files_.end(), PrepareNewNames(templ)); new_names_ = pnn.new_names; return new_names_; } void FileDisc::renameFiles(const std::vector &new_names) { for_each(new_names.begin(), new_names.end(), RenameFiles()); return; } const std::string FileDisc::ProblemTypeToString(const ProblemType & problem_type) { switch (problem_type) { case TAGLOOKUP_NONE: return "None"; case TAGLOOKUP_READONLY: return "File is read-only"; case TAGLOOKUP_INVALID_FILE: return "File is invalid"; case TAGLOOKUP_READONLY_AND_INVALID_FILE: return "File is invalid and read-only"; } return ""; } } // namespace TagLookup