Logo Search packages:      
Sourcecode: madman version File versions  Download package

song.cpp

/*
madman - a music manager
Copyright (C) 2003  Andreas Kloeckner <ak@ixion.net>

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
*/




#include <algorithm>
#include <memory>
#include <iomanip>
#include <stdexcept>
#include <libgen.h>
#include <qregexp.h>
#include <qdatetime.h>
#include <qfileinfo.h>
#include <id3tag.h>
#include <vorbis/vorbisfile.h>
#include <glib.h>
#include <sys/stat.h>
#include "utility/vcedit.h"
#include "utility/mp3tech.h"
#include "database/database.h"
#include "song.h"

#include <qapplication.h>




// private helpers ------------------------------------------------------------
namespace 
{
  string sanitizeUtf8(string const &victim)
  {
    string result;
    string::const_iterator first = victim.begin(), last = victim.end();
    while (first != last)
    {
      if (*first < 0x20)
      {
      result += "?";
      first++;
      }
      else if (0x20 <= (unsigned char) *first <= 0x7f)
      {
      result += *first++;
      }
      else if (0x80 <= (unsigned char) *first <= 0xc1)
      {
      result += "?";
      first++;
      }
      else if (0xc2 <= (unsigned char) *first <= 0xdf)
      {
      // two byte sequences
      if (0x80 <= (unsigned char) first[1] <= 0xbf)
      {
        result += *first++;
        result += *first++;
      }
      else
      {
        first += 2;
        result += "?";
      }
      }
      else if (0xe0 <= (unsigned char) *first <= 0xef)
      {
      // three byte sequences
      bool valid = 
        (0x80 <= (unsigned char) first[1] <= 0xbf) &&
        (0x80 <= (unsigned char) first[2] <= 0xbf);

      if ((unsigned char) first[0] == 0xe0 && (unsigned char) first[1] < 0xa0)
        valid = false;
      if ((unsigned char) first[0] == 0xed && (unsigned char) first[1] > 0x9f)
        valid = false;
      if (valid)
      {
        result += *first++;
        result += *first++;
        result += *first++;
      }
      else
      {
        first += 3;
        result += "?";
      }
      }
      else if (0xf0 <= (unsigned char) *first <= 0xf4)
      {
      // four byte sequences
      bool valid = 
        (0x80 <= (unsigned char) first[1] <= 0xbf) &&
        (0x80 <= (unsigned char) first[2] <= 0xbf) &&
        (0x80 <= (unsigned char) first[3] <= 0xbf);

      if ((unsigned char) first[0] == 0xf0 && (unsigned char) first[1] < 0x90)
        valid = false;
      if ((unsigned char) first[0] == 0xf4 && (unsigned char) first[1] > 0x8f)
        valid = false;
      if (valid)
      {
        result += *first++;
        result += *first++;
        result += *first++;
        result += *first++;
      }
      else
      {
        first += 4;
        result += "?";
      }
      }
      else
      {
      first += 1;
      result += "?";
      }
    }
    return result;
  }

  string decodeFilename(const QString &str)
  {
    if (str.left(7) == "base64:")
      return decodeBase64(QString2string(str.mid(7)));
    else
      return QString2string(str);
  }

  template<class T, class T2>
  void assignAndCheckForModification(const tSong *song, tSongCollection *collection, T &value, const T2 &rvalue, tSongField field)
  { bool changed = value != rvalue;
    value = rvalue;
    if (changed && collection)
      collection->noticeSongModified(song, field);
  }
}




// public helpers -------------------------------------------------------------
QString genreIdToString(int genre)
{
  switch (genre)
  {
    case 0:
      return "Blues";
    case 1:
      return "Classic Rock";
    case 2:
      return "Country";
    case 3:
      return "Dance";
    case 4:
      return "Disco";
    case 5:
      return "Funk";
    case 6:
      return "Grunge";
    case 7:
      return "Hip-Hop";
    case 8:
      return "Jazz";
    case 9:
      return "Metal";
    case 10:
      return "New Age";
    case 11:
      return "Oldies";
    case 12:
      return "Other";
    case 13:
      return "Pop";
    case 14:
      return "R&B";
    case 15:
      return "Rap";
    case 16:
      return "Reggae";
    case 17:
      return "Rock";
    case 18:
      return "Techno";
    case 19:
      return "Industrial";
    case 20:
      return "Alternative";
    case 21:
      return "Ska";
    case 22:
      return "Death Metal";
    case 23:
      return "Pranks";
    case 24:
      return "Soundtrack";
    case 25:
      return "Euro-Techno";
    case 26:
      return "Ambient";
    case 27:
      return "Trip-Hop";
    case 28:
      return "Vocal";
    case 29:
      return "Jazz+Funk";
    case 30:
      return "Fusion";
    case 31:
      return "Trance";
    case 32:
      return "Classical";
    case 33:
      return "Instrumental";
    case 34:
      return "Acid";
    case 35:
      return "House";
    case 36:
      return "Game";
    case 37:
      return "Sound Clip";
    case 38:
      return "Gospel";
    case 39:
      return "Noise";
    case 40:
      return "AlternRock";
    case 41:
      return "Bass";
    case 42:
      return "Soul";
    case 43:
      return "Punk";
    case 44:
      return "Space";
    case 45:
      return "Meditative";
    case 46:
      return "Instrumental Pop";
    case 47:
      return "Instrumental Rock";
    case 48:
      return "Ethnic";
    case 49:
      return "Gothic";
    case 50:
      return "Darkwave";
    case 51:
      return "Techno-Industrial";
    case 52:
      return "Electronic";
    case 53:
      return "Pop-Folk";
    case 54:
      return "Eurodance";
    case 55:
      return "Dream";
    case 56:
      return "Southern Rock";
    case 57:
      return "Comedy";
    case 58:
      return "Cult";
    case 59:
      return "Gangsta";
    case 60:
      return "Top 40";
    case 61:
      return "Christian Rap";
    case 62:
      return "Pop/Funk";
    case 63:
      return "Jungle";
    case 64:
      return "Native American";
    case 65:
      return "Cabaret";
    case 66:
      return "New Wave";
    case 67:
      return "Psychadelic";
    case 68:
      return "Rave";
    case 69:
      return "Showtunes";
    case 70:
      return "Trailer";
    case 71:
      return "Lo-Fi";
    case 72:
      return "Tribal";
    case 73:
      return "Acid Punk";
    case 74:
      return "Acid Jazz";
    case 75:
      return "Polka";
    case 76:
      return "Retro";
    case 77:
      return "Musical";
    case 78:
      return "Rock & Roll";
    case 79:
      return "Hard Rock";
    case 80:
      return "Folk";
    case 81:
      return "Folk-Rock";
    case 82:
      return "National Folk";
    case 83:
      return "Swing";
    case 84:
      return "Fast Fusion";
    case 85:
      return "Bebob";
    case 86:
      return "Latin";
    case 87:
      return "Revival";
    case 88:
      return "Celtic";
    case 89:
      return "Bluegrass";
    case 90:
      return "Avantgarde";
    case 91:
      return "Gothic Rock";
    case 92:
      return "Progressive Rock";
    case 93:
      return "Psychedelic Rock";
    case 94:
      return "Symphonic Rock";
    case 95:
      return "Slow Rock";
    case 96:
      return "Big Band";
    case 97:
      return "Chorus";
    case 98:
      return "Easy Listening";
    case 99:
      return "Acoustic";
    case 100:
      return "Humour";
    case 101:
      return "Speech";
    case 102:
      return "Chanson";
    case 103:
      return "Opera";
    case 104:
      return "Chamber Music";
    case 105:
      return "Sonata";
    case 106:
      return "Symphony";
    case 107:
      return "Booty Bass";
    case 108:
      return "Primus";
    case 109:
      return "Porn Groove";
    case 110:
      return "Satire";
    case 111:
      return "Slow Jam";
    case 112:
      return "Club";
    case 113:
      return "Tango";
    case 114:
      return "Samba";
    case 115:
      return "Folklore";
    case 116:
      return "Ballad";
    case 117:
      return "Power Ballad";
    case 118:
      return "Rhythmic Soul";
    case 119:
      return "Freestyle";
    case 120:
      return "Duet";
    case 121:
      return "Punk Rock";
    case 122:
      return "Drum Solo";
    case 123:
      return "A capella";
    case 124:
      return "Euro-House";
    case 125:
      return "Dance Hall";
    default:
      return "undefined";
  }
}




QString getFieldName(tSongField field)
{
  switch (field)
  {
    case FIELD_ARTIST: return qApp->translate("FieldDescriptions", "Artist");
    case FIELD_PERFORMER: return qApp->translate("FieldDescriptions", "Performer");
    case FIELD_TITLE: return qApp->translate("FieldDescriptions", "Title");
    case FIELD_ALBUM: return qApp->translate("FieldDescriptions", "Album");
    case FIELD_TRACKNUMBER: return qApp->translate("FieldDescriptions", "Track Number");
    case FIELD_DURATION: return qApp->translate("FieldDescriptions", "Duration");
    case FIELD_GENRE: return qApp->translate("FieldDescriptions", "Genre");
    case FIELD_FULLPLAYCOUNT: return qApp->translate("FieldDescriptions", "Play Count (full)");
    case FIELD_PARTIALPLAYCOUNT: return qApp->translate("FieldDescriptions", "Play Count (partial)");
    case FIELD_PLAYCOUNT: return qApp->translate("FieldDescriptions", "Play Count");
    case FIELD_LASTPLAYED: return qApp->translate("FieldDescriptions", "Last Played");
    case FIELD_RATING: return qApp->translate("FieldDescriptions", "Rating");
    case FIELD_YEAR: return qApp->translate("FieldDescriptions", "Year");
    case FIELD_FILE: return qApp->translate("FieldDescriptions", "File");
    case FIELD_PATH: return qApp->translate("FieldDescriptions", "Path");
    case FIELD_SIZE: return qApp->translate("FieldDescriptions", "File Size");
    case FIELD_EXISTSSINCE: return qApp->translate("FieldDescriptions", "Exists since");
    case FIELD_LASTMODIFIED: return qApp->translate("FieldDescriptions", "Last modified");
    case FIELD_MIMETYPE: return qApp->translate("FieldDescriptions", "MIME Type");
    case FIELD_UNIQUEID: return qApp->translate("FieldDescriptions", "Unique ID");
    default: return qApp->translate("FieldDescriptions", "<unknown>");
  }
}





QString getFieldIdentifier(tSongField field)
{
  switch (field)
  {
    case FIELD_ARTIST: return "artist";
    case FIELD_PERFORMER: return "performer";
    case FIELD_TITLE: return "title";
    case FIELD_ALBUM: return "album";
    case FIELD_TRACKNUMBER: return "tracknumber";
    case FIELD_DURATION: return "duration";
    case FIELD_GENRE: return "genre";
    case FIELD_FULLPLAYCOUNT: return "fullplaycount";
    case FIELD_PARTIALPLAYCOUNT: return "partialplaycount";
    case FIELD_PLAYCOUNT: return "playcount";
    case FIELD_LASTPLAYED: return "lastplayed";
    case FIELD_RATING: return "rating";
    case FIELD_YEAR: return "year";
    case FIELD_FILE: return "file";
    case FIELD_PATH: return "path";
    case FIELD_SIZE: return "size";
    case FIELD_EXISTSSINCE: return "existssince";
    case FIELD_LASTMODIFIED: return "lastmodified";
    case FIELD_MIMETYPE: return "mimetype";
    case FIELD_UNIQUEID: return "uniqueid";
    default: 
      throw tRuntimeError("Invalid field id in getFieldIdentifier");
  }

}




tSongField getFieldFromIdentifier(const QString &field)
{
  if (field == "artist") return FIELD_ARTIST;
  else if (field == "performer") return FIELD_PERFORMER;
  else if (field == "title") return FIELD_TITLE;
  else if (field == "album") return FIELD_ALBUM;
  else if (field == "tracknumber") return FIELD_TRACKNUMBER;
  else if (field == "duration") return FIELD_DURATION;
  else if (field == "genre") return FIELD_GENRE;
  else if (field == "fullplaycount") return FIELD_FULLPLAYCOUNT;
  else if (field == "partialplaycount") return FIELD_PARTIALPLAYCOUNT;
  else if (field == "playcount") return FIELD_PLAYCOUNT;
  else if (field == "lastplayed") return FIELD_LASTPLAYED;
  else if (field == "rating") return FIELD_RATING;
  else if (field == "year") return FIELD_YEAR;
  else if (field == "file") return FIELD_FILE;
  else if (field == "path") return FIELD_PATH;
  else if (field == "size") return FIELD_SIZE;
  else if (field == "existssince") return FIELD_EXISTSSINCE;
  else if (field == "lastmodified") return FIELD_LASTMODIFIED;
  else if (field == "mimetype") return FIELD_MIMETYPE;
  else if (field == "uniqueid") return FIELD_UNIQUEID;
  else 
  {
    throw tRuntimeError("Invalid field id in getFieldFromIdentifier");
  }
}




QString substituteSongFields(QString const &format, tSong *song, bool human_readable, bool shell_quote)
{
  QString result = format;

  for (int i = 0; i < FIELD_COUNT; i++)
  {
    tSongField f = (tSongField) i;
    QString replacement;
    if (human_readable)
      replacement = song->humanReadableFieldText(f);
    else
      replacement = song->fieldText(f);

    if (shell_quote)
      replacement = quoteString(replacement);

    result.replace("%" + getFieldIdentifier(f) + "%", replacement);
  }
  result.replace("%newline%", "\n");

  return result;
}




// tSong ----------------------------------------------------------------------
tSong::tDirectoryList tSong::DirectoryList;




tSong::tSong()
    : UniqueId(SONG_UID_INVALID),
    SongCollection(NULL),
    Duration(0),
    FileSize(0),
    ExistsSince(0),
    LastPlayed(-1),
    LastModified(time(NULL)),
    FullPlayCount(0),
    PartialPlayCount(0),
    Rating(-1),
    CacheValid(false)
{
  IndexIntoDirectoryList = 0;
  if (DirectoryList.size() == 0)
    DirectoryList.push_back("");
}




tSong::~tSong()
{
}




tFilename tSong::filename() const
{
  return pathname() + "/" + filenameOnly();
}




tFilename tSong::pathname() const
{
  return DirectoryList[ IndexIntoDirectoryList ];
}




tFilename tSong::filenameOnly() const
{
  return FilenameOnly;
}




tUniqueId tSong::uniqueId() const
{
  return UniqueId;
}




QString tSong::album() const
{
  ensureInfoIsThere();
  return Album;
}




QString tSong::artist() const
{
  ensureInfoIsThere();
  return Artist;
}




QString tSong::performer() const
{
  ensureInfoIsThere();
  return Performer;
}




QString tSong::title() const
{
  ensureInfoIsThere();
  return Title;
}




QString tSong::year() const
{
  ensureInfoIsThere();
  return Year;
}




QString tSong::genre() const
{
  ensureInfoIsThere();
  return Genre;
}




QString tSong::trackNumber() const
{
  ensureInfoIsThere();
  return TrackNumber;
}




float tSong::duration() const
{
  ensureInfoIsThere();
  return Duration;
}




time_t tSong::existsSince() const
{
  return ExistsSince;
}




time_t tSong::lastModified() const
{
  ensureInfoIsThere();
  return LastModified;
}




time_t tSong::lastPlayed() const
{
  return LastPlayed;
}




int tSong::playCount() const
{
  return FullPlayCount+PartialPlayCount;
}




int tSong::fullPlayCount() const
{
  return FullPlayCount;
}




int tSong::partialPlayCount() const
{
  return PartialPlayCount;
}




int tSong::rating() const
{
  return Rating;
}




int tSong::fileSize() const
{
  ensureInfoIsThere();
  return FileSize;
}




QDomNode tSong::serialize(QDomDocument &doc) const
{
  ensureInfoIsThere();

  QDomElement result = doc.createElement("song");
  result.setAttribute("filename", QString("base64:")+encodeBase64(filename()).c_str());
  result.setAttribute("uniqueid", UniqueId);
  result.setAttribute("album", Album);
  result.setAttribute("artist", Artist);
  result.setAttribute("performer", Performer);
  result.setAttribute("title", Title);
  result.setAttribute("year", Year);
  result.setAttribute("genre", Genre);
  result.setAttribute("tracknumber", TrackNumber);
  result.setAttribute("duration", Duration);

  result.setAttribute("existssince", (int) ExistsSince);
  result.setAttribute("lastplayed", (int) LastPlayed);
  result.setAttribute("lastmodified", (int) LastModified);
  result.setAttribute("fullplaycount", FullPlayCount);
  result.setAttribute("partialplaycount", PartialPlayCount);
  result.setAttribute("rating", Rating);
  result.setAttribute("filesize", (int) FileSize);
  return result;
}




void tSong::deserialize(const char **attributes)
{
  tFilename fn;
  fn = decodeFilename(lookupAttribute("filename", attributes));

  if (fn != filename())
  {
    throw tRuntimeError(qApp->translate("ErrorMessages", "deserializing to different filename"));
  }

  UniqueId = lookupAttribute("uniqueid", attributes).toUInt();
  Album = lookupAttribute("album", attributes);
  Artist = lookupAttribute("artist", attributes);
  try
  {
    Performer = lookupAttribute("performer", attributes);
  }
  catch (...)
  {
    Performer = qApp->translate("ErrorMessages", "<Unkown, reread tags>");
  }

  Title = lookupAttribute("title", attributes);
  Year = lookupAttribute("year", attributes);
  Genre = lookupAttribute("genre", attributes);
  TrackNumber = lookupAttribute("tracknumber", attributes);
  Duration = lookupAttribute("duration", attributes).toFloat();

  if (hasAttribute("existssince", attributes))
    ExistsSince = lookupAttribute("existssince", attributes).toUInt();

  if (hasAttribute("fullplaycount", attributes))
  {
    LastPlayed = lookupAttribute("lastplayed", attributes).toUInt();
    FullPlayCount = lookupAttribute("fullplaycount", attributes).toUInt();
    PartialPlayCount = lookupAttribute("partialplaycount", attributes).toUInt();
  }
  else
  {
    FullPlayCount = 0;
    PartialPlayCount = 0;
  }

  if (hasAttribute("lastmodified", attributes))
    LastModified = lookupAttribute("lastmodified", attributes).toUInt();
  else
  {
    // we'll believe that our mtime is way back, so we'll
    // update our tags.
    LastModified = 0;
  }

  if (hasAttribute("rating", attributes))
    Rating = lookupAttribute("rating", attributes).toInt();
  else
    Rating = -1;

  if (hasAttribute("filesize", attributes))
    FileSize = lookupAttribute("filesize", attributes).toInt();
  else
  {
    string fn = filename();
    struct stat statbuf;
    if (stat(fn.c_str(), &statbuf))
      throw runtime_error(("Unable to stat file "+filename()).c_str());
    FileSize = statbuf.st_size;
  }

  CacheValid = true;
}




void tSong::setUniqueId(tUniqueId id)
{
  UniqueId = id;
}




void tSong::setFilename(tFilename const &new_filename)
{
  if (SongCollection)
    SongCollection->noticeSongFilenameAboutToChange(this, filename(), new_filename);

  char *cstr_copy = strdup(new_filename.c_str());
  FilenameOnly = basename(cstr_copy);
  free(cstr_copy);
  cstr_copy = strdup(new_filename.c_str());
  tFilename pathname = dirname(cstr_copy);
  free(cstr_copy);

  bool found = false;
  IndexIntoDirectoryList = 0;
  FOREACH(first, DirectoryList, tDirectoryList)
  {
    if (pathname == *first)
    {
      found = true;
      break;
    }
    IndexIntoDirectoryList++;
  }
  if (!found)
  {
    IndexIntoDirectoryList = DirectoryList.size();
    DirectoryList.push_back(pathname);
  }

  CacheValid = false;

  if (SongCollection)
  {
    SongCollection->noticeSongModified(this, FIELD_FILE);
    SongCollection->noticeSongModified(this, FIELD_PATH);
  }
}




void tSong::invalidateCache() 
{
  CacheValid = false;

  // Cache invalidation does not entail song modification, so
  // don't call noticeSongModified here.
}




void tSong::setExistsSince(time_t value)
{
  LastModified = value;
  if (SongCollection)
    SongCollection->noticeSongModified(this, FIELD_EXISTSSINCE);
}




void tSong::setLastModified(time_t value)
{
  LastModified = value;
  if (SongCollection)
    SongCollection->noticeSongModified(this, FIELD_LASTMODIFIED);
}




void tSong::setLastPlayed(time_t value)
{
  LastPlayed = value;
  if (SongCollection)
    SongCollection->noticeSongModified(this, FIELD_LASTPLAYED);
}




void tSong::setFullPlayCount(unsigned value)
{
  FullPlayCount = value;
  if (SongCollection)
  {
    SongCollection->noticeSongModified(this, FIELD_FULLPLAYCOUNT);
    SongCollection->noticeSongModified(this, FIELD_PLAYCOUNT);
  }
}




void tSong::setPartialPlayCount(unsigned value)
{
  PartialPlayCount = value;
  if (SongCollection)
  {
    SongCollection->noticeSongModified(this, FIELD_PARTIALPLAYCOUNT);
    SongCollection->noticeSongModified(this, FIELD_PLAYCOUNT);
  }
}




void tSong::setRating(int value)
{
  Rating = value;
  if (SongCollection)
    SongCollection->noticeSongModified(this, FIELD_RATING);
}




QString tSong::fieldText(tSongField field) const
{
  try
  {
    switch (field)
    {
      case FIELD_ARTIST:
      return artist();
      case FIELD_PERFORMER:
      return performer();
      case FIELD_TITLE:
      return title();
      case FIELD_ALBUM:
      return album();
      case FIELD_TRACKNUMBER:
      return trackNumber();
      case FIELD_DURATION:
      return QString::number(duration());
      case FIELD_GENRE:
      return genre();
      case FIELD_FULLPLAYCOUNT:
      return QString::number(fullPlayCount());
      case FIELD_PARTIALPLAYCOUNT:
      return QString::number(partialPlayCount());
      case FIELD_PLAYCOUNT:
      return QString::number(playCount());

      case FIELD_LASTPLAYED:
      if (playCount())
        return QString::number(lastPlayed());
      else
        return "";

      case FIELD_RATING:
      return QString::number(rating());

      case FIELD_YEAR:
      return year();
      case FIELD_FILE:
      return QString::fromUtf8(filenameOnly().c_str());
      case FIELD_PATH:
      return QString::fromUtf8(pathname().c_str());
      case FIELD_SIZE:
      return QString::number(fileSize());

      case FIELD_EXISTSSINCE:
      return QString::number(existsSince());
      case FIELD_LASTMODIFIED:
      return QString::number(lastModified());
      case FIELD_MIMETYPE:
      return mimeType();
      case FIELD_UNIQUEID:
      return QString::number(uniqueId());
      default:
      throw tRuntimeError("Invalid field in fieldText");
    }
  }
  catch (exception &ex)
  {
    return "<ERROR>";
  }
}




QString tSong::humanReadableFieldText(tSongField field) const
{
  switch (field)
  {
    case FIELD_DURATION:
      {
      int total_seconds = (int) duration();
      int seconds = total_seconds % 60;
      int minutes = total_seconds / 60;

      QString duration;
      duration.sprintf("%d:%02d", minutes, seconds);
      return duration;
      }
    case FIELD_LASTPLAYED:
      {
      if (playCount())
      {
        QDateTime datetime;
        datetime.setTime_t(lastPlayed());
        return datetime.toString("MMM d h:mm");
      }
      else
        return "";
      }
    case FIELD_EXISTSSINCE:
      {
      QDateTime datetime;
      datetime.setTime_t(existsSince());
      return datetime.toString("MMM d h:mm");
      }
    case FIELD_LASTMODIFIED:
      {
      QDateTime datetime;
      datetime.setTime_t(lastModified());
      return datetime.toString("MMM d h:mm");
      }
    case FIELD_RATING:
      {
      if (rating() == 0)
      {
        return "-";
      }
      else if (rating() > 0)
      {
        QString rating_str;
        rating_str.fill('*', rating());
        return rating_str;
      }
      else
        return "";
      }
      break;
    case FIELD_SIZE:
      return QString("%1 MB").arg(double(fileSize()) / (1024 * 1024), 0, 'f', 2);
    default:
      return fieldText(field);
  } 
}




void tSong::setFieldText(tSongField field, const QString &new_text)
{
  switch (field)
  {
    case FIELD_ARTIST:
      setArtist(new_text);
      break;
    case FIELD_PERFORMER:
      setPerformer(new_text);
      break;
    case FIELD_TITLE:
      setTitle(new_text);
      break;
    case FIELD_ALBUM:
      setAlbum(new_text);
      break;
    case FIELD_GENRE:
      setGenre(new_text);
      break;
    case FIELD_YEAR:
      setYear(new_text);
      break;
    case FIELD_TRACKNUMBER:
      setTrackNumber(new_text);
      break;
    case FIELD_RATING:
      {
      bool ok;
      int rating = new_text.toInt(&ok);
      if (!ok)
        throw tRuntimeError(QString("Invalid rating numeral '%1' in setFieldText").arg(new_text));

      setRating(rating);
      break;
      }
    default:
      throw tRuntimeError(QString("Cannot set field %1").arg(getFieldName(field)));
  }
}




void tSong::stripTag()
{
  stripTagInternal();
  readInfo();
}




void tSong::rewriteTag()
{
  QString my_album = album();
  QString my_artist = artist();
  QString my_performer = performer();
  QString my_title = title();
  QString my_year = year();
  QString my_genre = genre();
  QString my_track_number = trackNumber();

  stripTagInternal();

  setAlbum(my_album);
  setArtist(my_artist);
  setPerformer(my_performer);
  setTitle(my_title);
  setYear(my_year);
  setGenre(my_genre);
  setTrackNumber(my_track_number);
}




void tSong::ensureInfoIsThere() const
{
  if (!CacheValid)
    readInfo();
}





void tSong::readInfo() const
{
  struct stat statbuf;
  string fn = filename();
  if (stat(fn.c_str(), &statbuf))
    throw runtime_error(("Unable to stat file "+filename()).c_str());

  const_cast<tSong *>(this)->FileSize = statbuf.st_size;
  const_cast<tSong *>(this)->LastModified = statbuf.st_mtime;
  if (ExistsSince == 0)
    const_cast<tSong *>(this)->ExistsSince = statbuf.st_ctime;
}




void tSong::played(time_t when, bool full)
{
  if (full)
    setFullPlayCount(fullPlayCount() + 1);
  else
    setPartialPlayCount(fullPlayCount() + 1);

  setLastPlayed(when);
}




void tSong::resetStatistics()
{
  setFullPlayCount(0);
  setPartialPlayCount(0);
  setLastPlayed(-1);
}




void tSong::setCollection(tSongCollection *collection)
{
  SongCollection = collection;
}




// tMP3Song -------------------------------------------------------------------
class tMP3Song : public tSong
{
  public:
    QString mimeType() const
    { return "audio/mpeg"; }

    void readInfo() const;

    void setAlbum(QString const &value);
    void setArtist(QString const &value);
    void setPerformer(QString const &value);
    void setTitle(QString const &value);
    void setYear(QString const &value);
    void setGenre(QString const &value);
    void setTrackNumber(QString const &value);

    void stripTagInternal();
};




static QString getID3FieldString(id3_tag *my_tag, const string &frame_id)
{
  id3_frame *frame = id3_tag_findframe(my_tag, frame_id.c_str(), 0);
  if (frame == NULL)
    return "";

  unsigned int index = 0;
  id3_field *field = id3_frame_field(frame, 0);
  while ((field = id3_frame_field(frame, index)))
  {
    if (field == NULL)
      return "";
    if (id3_field_type(field) == ID3_FIELD_TYPE_STRING || 
        id3_field_type(field) == ID3_FIELD_TYPE_STRINGLIST)
      break;
    index++;
  }

  const id3_ucs4_t *value;
  if (id3_field_type(field) == ID3_FIELD_TYPE_STRING)
    value = id3_field_getstring(field);
  else
  {
    if (id3_field_getnstrings(field) == 0)
      return "";
    value = id3_field_getstrings(field, 0);
  }

  if (frame_id == "TCON")
    value = id3_genre_name(value);

  id3_utf8_t *utf8_value = id3_ucs4_utf8duplicate(value);
  string utf8_managed = reinterpret_cast<const char *>(utf8_value);
  QString result = QString::fromUtf8(sanitizeUtf8(utf8_managed).c_str());
  free(utf8_value);

  return result;
}




static void setID3FieldString(const tFilename &filename, const string &frame_id, QString const &value)
{
  id3_file *my_file = id3_file_open(filename.c_str(), ID3_FILE_MODE_READWRITE);
  if (my_file == NULL)
    throw runtime_error("Failed to open MP3 file for writing");

  id3_tag *my_tag = id3_file_tag(my_file); // doesn't fail.

  id3_frame *frame = NULL;
 
  while ((frame = id3_tag_findframe(my_tag, frame_id.c_str(), 0)))
  {
    if (id3_tag_detachframe(my_tag, frame))
    {
      id3_file_close(my_file);
      throw runtime_error("Failed to detach frame from ID3 tag");
    }

    id3_frame_delete(frame);
  }
 
  frame = id3_frame_new(frame_id.c_str());
  if (frame == NULL)
  {
    id3_file_close(my_file);
    throw runtime_error("Failed to make new ID3 frame");
  }

  unsigned int index = 0;
  id3_field *field;
  while ((field = id3_frame_field(frame, index)))
  {
    if (field == NULL)
    {
      id3_frame_delete(frame);
      id3_file_close(my_file);
      throw runtime_error("ID3 write: Failed to find settable field");
    }

    if (id3_field_type(field) == ID3_FIELD_TYPE_STRING || 
        id3_field_type(field) == ID3_FIELD_TYPE_STRINGLIST)
      break;
    index++;
  }
  if (field == NULL)
  {
    id3_frame_delete(frame);
    id3_file_close(my_file);
    throw runtime_error("ID3 write: Failed to find settable field");
  }

  id3_ucs4_t *ucs4_value = id3_utf8_ucs4duplicate(
      reinterpret_cast<const id3_utf8_t *>((const char *)value.utf8()));
  if (id3_field_type(field) == ID3_FIELD_TYPE_STRINGLIST)
  {
    if (id3_field_setstrings(field, 1, &ucs4_value))
    {
      free(ucs4_value);
      id3_frame_delete(frame);
      id3_file_close(my_file);
      throw runtime_error("Failed to set ID3 tag to new value");
    }
  }
  else
  {
    if (id3_field_setstring(field, ucs4_value))
    {
      free(ucs4_value);
      id3_frame_delete(frame);
      id3_file_close(my_file);
      throw runtime_error("Failed to set ID3 tag to new value");
    }
  }
  free(ucs4_value);

  if (id3_tag_attachframe(my_tag, frame))
  {
    id3_frame_delete(frame);
    id3_file_close(my_file);
    throw runtime_error("Failed to attach new frame to ID3 tag");
  }

  if (id3_file_update(my_file))
  {
    id3_file_close(my_file);
    throw runtime_error("Failed to update ID3 tag");
  }

  id3_file_close(my_file);
}




void tMP3Song::readInfo() const
{
  id3_file *my_file = id3_file_open(filename().c_str(), ID3_FILE_MODE_READONLY);
  if (my_file == NULL)
    throw runtime_error("Failed to open MP3 file for reading");

  try
  {
    id3_tag *tag = id3_file_tag(my_file); // doesn't fail.

    CacheValid = true;

    assignAndCheckForModification(this, SongCollection,
        const_cast<tMP3Song *>(this)->Album, 
        getID3FieldString(tag, "TALB"), FIELD_ALBUM);
    assignAndCheckForModification(this, SongCollection,
        const_cast<tMP3Song *>(this)->Artist, 
        getID3FieldString(tag, "TPE1"), FIELD_ARTIST);
    assignAndCheckForModification(this, SongCollection,
        const_cast<tMP3Song *>(this)->Performer, 
        getID3FieldString(tag, "TPE4"), FIELD_PERFORMER); // FIXME
    assignAndCheckForModification(this, SongCollection,
        const_cast<tMP3Song *>(this)->Title, 
        getID3FieldString(tag, "TIT2"), FIELD_TITLE);
    assignAndCheckForModification(this, SongCollection,
        const_cast<tMP3Song *>(this)->Year, 
        getID3FieldString(tag, "TDRC"), FIELD_YEAR);
    assignAndCheckForModification(this, SongCollection,
        const_cast<tMP3Song *>(this)->TrackNumber, 
        getID3FieldString(tag, "TRCK"), FIELD_TRACKNUMBER);
    assignAndCheckForModification(this, SongCollection,
        const_cast<tMP3Song *>(this)->Genre, 
        getID3FieldString(tag, "TCON"), FIELD_GENRE);
    id3_file_close(my_file);

    mp3info info;
    info.filename = const_cast<char *>(filename().c_str());
    info.file = fopen(info.filename, "r");
    if (info.file)
    {
      if (get_mp3_info(&info, SCAN_QUICK, 0) && info.seconds > 0)
        assignAndCheckForModification(this, SongCollection,
            const_cast<tMP3Song *>(this)->Duration, 
            0, FIELD_DURATION);
      else
        assignAndCheckForModification(this, SongCollection,
            const_cast<tMP3Song *>(this)->Duration, 
            info.seconds, FIELD_DURATION);
      fclose(info.file);
    }
    else
      assignAndCheckForModification(this, SongCollection,
          const_cast<tMP3Song *>(this)->Duration, 
          0, FIELD_DURATION);

    tSong::readInfo();
  }
  catch (...)
  {
    id3_file_close(my_file);
    throw;
  }
}




void tMP3Song::setAlbum(QString const &value)
{
  setID3FieldString(filename(), "TALB", value);
  readInfo();
}




void tMP3Song::setArtist(QString const &value)
{
  setID3FieldString(filename(), "TPE1", value);
  readInfo();
}




void tMP3Song::setPerformer(QString const &value)
{
  setID3FieldString(filename(), "TPE4", value);
  readInfo();
}




void tMP3Song::setTitle(QString const &value)
{
  setID3FieldString(filename(), "TIT2", value);
  readInfo();
}




void tMP3Song::setYear(QString const &value)
{
  setID3FieldString(filename(), "TDRC", value);
  readInfo();
}




void tMP3Song::setGenre(QString const &value)
{
  setID3FieldString(filename(), "TCON", value);
  readInfo();
}




void tMP3Song::setTrackNumber(QString const &value)
{
  setID3FieldString(filename(), "TRCK", value);
  readInfo();
}




void tMP3Song::stripTagInternal()
{
  /*
  ID3_Tag tag(filename().latin1());
  tag.Strip();
  */
  // FIXME: unimplemented
}




// tOggSong -------------------------------------------------------------------
class tOggSong : public tSong
{
  public:
    QString mimeType() const
    { return "audio/x-ogg"; }

    void readInfo() const;

    void setAlbum(QString const &value);
    void setArtist(QString const &value);
    void setPerformer(QString const &value);
    void setTitle(QString const &value);
    void setYear(QString const &value);
    void setGenre(QString const &value);
    void setTrackNumber(QString const &value);

    void stripTagInternal();
};




static QString getOggFieldString(vorbis_comment *vc, const char *tag)
{
  int count = vorbis_comment_query_count(vc, const_cast<char *>(tag));
  if (count > 0)
  {
    string comment = vorbis_comment_query(vc, const_cast<char *>(tag), 0);
    QString result = QString::fromUtf8(sanitizeUtf8(comment).c_str());
    return result;
  }
  else
    return "";
}




// ----------------------------------------------------------------------------
// begin code shamelessly pulled from xmms, with minimal changes
// ----------------------------------------------------------------------------
static char** get_comment_list(vorbis_comment *vc)
{
  int i;
  char **strv;

  strv = g_new0(char*, vc->comments + 1);
  for (i = 0; i < vc->comments; i++)
  {
    strv[i] = g_strdup(vc->user_comments[i]);
  }

  return strv;
}

static char** add_tag(char **list, const char *label, const char *in_tag)
{
  char **ptr = list, *reallabel = g_strconcat(label, "=", NULL);

  char *tag = g_strdup(in_tag);
  g_strstrip(tag);
  if (strlen(tag) == 0)
    tag = NULL;
  /*
   * There can be several tags with the same label.  We clear
   * them all.
   */
  while (*ptr != NULL)
  {
    if (!g_strncasecmp(reallabel, *ptr, strlen(reallabel)))
    {
      g_free(*ptr);
      if (tag != NULL)
      {
      *ptr = g_strconcat(reallabel, tag, NULL);
      g_free(tag);
      tag = NULL;
      ptr++;
      }
      else
      {
      char **str;
      for (str = ptr; *str; str++)
        *str = *(str + 1);
      }

    }
    else
      ptr++;
  }
  if (tag)
  {
    int i = 0;
    for (ptr = list; *ptr; ptr++)
      i++;
    list = g_renew(char*, list, i + 2);
    list[i] = g_strconcat(reallabel, tag, NULL);
    list[i + 1] = NULL;
    g_free(tag);
  }
  g_free(reallabel);

  return list;
}

static void add_list(vorbis_comment *vc, char **comments)
{
  while (*comments)
    vorbis_comment_add(vc, *comments++);
}
// ----------------------------------------------------------------------------
// end code shamelessly pulled from xmms
// ----------------------------------------------------------------------------




static void setOggFieldString(const tFilename &filename, QString const &key, QString const &value)
{
  vcedit_state *state = vcedit_new_state();

  FILE *ogg_song = fopen(filename.c_str(), "rb");
  if (ogg_song == NULL)
    throw tRuntimeError(
        qApp->translate("ErrorMessages", "Trouble reading from %1").
        arg(string2QString(filename)));

  if (vcedit_open(state, ogg_song))
  {
    fclose(ogg_song);
    throw tRuntimeError(
        qApp->translate("ErrorMessages", "Trouble reading tags from %1")
        .arg(string2QString(filename)));
  }
  
  vorbis_comment *vc = vcedit_comments(state);
  char **comment_list = get_comment_list(vc);
  comment_list = add_tag(comment_list, key.utf8(), value.utf8());
  vorbis_comment_clear(vc);
  add_list(vc, comment_list);

  tFilename new_ogg_filename = filename + ".madman_retag";
  FILE *new_ogg_song = fopen(new_ogg_filename.c_str(), "wb");
  if (new_ogg_song == NULL)
  {
    fclose(ogg_song);
    throw tRuntimeError(qApp->translate("ErrorMessages", "Trouble writing retagged file"));
  }

  if (vcedit_write(state, new_ogg_song))
  {
    fclose(ogg_song);
    fclose(new_ogg_song);
    remove(new_ogg_filename.c_str());
    throw tRuntimeError(qApp->translate("ErrorMessages", "Trouble writing retagged file"));
  }

  fclose(new_ogg_song);
  fclose(ogg_song);
  if (rename(new_ogg_filename.c_str(), filename.c_str()))
    remove(new_ogg_filename.c_str());

  vcedit_clear(state);
}







void tOggSong::readInfo() const
{
  tFilename fn = filename();

  FILE *ogg_song = fopen(fn.c_str(), "rb");
  if (ogg_song == NULL)
  {
    throw tRuntimeError(
        qApp->translate("ErrorMessages", "Trouble reading tags from %1")
        .arg(string2QString(filename())));
  }
  OggVorbis_File vf;
  if (ov_open(ogg_song, &vf, NULL, 0))
  {
    fclose(ogg_song);
    throw tRuntimeError(
        qApp->translate("ErrorMessages", "Trouble reading tags from %1")
        .arg(string2QString(filename())));
  }

  // ov_open has taken possession of ogg_song, so no need to fclose 
  // otherwise.

  vorbis_comment *vc = ov_comment(&vf, -1);

  CacheValid = true;

  assignAndCheckForModification(this, SongCollection,
      const_cast<tOggSong *>(this)->Album, 
      getOggFieldString(vc, "ALBUM"), FIELD_ALBUM);
  assignAndCheckForModification(this, SongCollection,
      const_cast<tOggSong *>(this)->Artist, 
      getOggFieldString(vc, "ARTIST"), FIELD_ARTIST);
  assignAndCheckForModification(this, SongCollection,
      const_cast<tOggSong *>(this)->Performer, 
      getOggFieldString(vc, "PERFORMER"), FIELD_PERFORMER);
  assignAndCheckForModification(this, SongCollection,
      const_cast<tOggSong *>(this)->Title, 
      getOggFieldString(vc, "TITLE"), FIELD_TITLE);
  assignAndCheckForModification(this, SongCollection,
      const_cast<tOggSong *>(this)->Year, 
      getOggFieldString(vc, "YEAR"), FIELD_YEAR);
  assignAndCheckForModification(this, SongCollection,
      const_cast<tOggSong *>(this)->Genre, 
      getOggFieldString(vc, "GENRE"), FIELD_GENRE);
  assignAndCheckForModification(this, SongCollection,
      const_cast<tOggSong *>(this)->TrackNumber, 
      getOggFieldString(vc, "TRACKNUMBER"), FIELD_TRACKNUMBER);
  assignAndCheckForModification(this, SongCollection,
      const_cast<tOggSong *>(this)->Duration, 
      ov_time_total(&vf, -1), FIELD_DURATION);

  ov_clear(&vf);

  tSong::readInfo();
}




void tOggSong::setAlbum(QString const &value)
{
  setOggFieldString(filename(), "ALBUM", value);
  readInfo();
}




void tOggSong::setArtist(QString const &value)
{
  setOggFieldString(filename(), "ARTIST", value);
  readInfo();
}




void tOggSong::setPerformer(QString const &value)
{
  setOggFieldString(filename(), "PERFORMER", value);
  readInfo();
}




void tOggSong::setTitle(QString const &value)
{
  setOggFieldString(filename(), "TITLE", value);
  readInfo();
}




void tOggSong::setYear(QString const &value)
{
  setOggFieldString(filename(), "YEAR", value);
  readInfo();
}




void tOggSong::setGenre(QString const &value)
{
  setOggFieldString(filename(), "GENRE", value);
  readInfo();
}




void tOggSong::setTrackNumber(QString const &value)
{
  setOggFieldString(filename(), "TRACKNUMBER", value);
  readInfo();
}




void tOggSong::stripTagInternal()
{
  // *** FIXME: NYI
}




// public ---------------------------------------------------------------------
tSong *makeSong(tFilename const &filename)
{
  if (filename.size() < 3) 
    throw tRuntimeError(
        qApp->translate("ErrorMessages", "Unknown file type: %1")
        .arg(string2QString(filename)));

  tFilename ext = filename.substr(filename.size() - 3);
  if (string2QString(ext).lower() == "mp3")
  {
    tSong *song = new tMP3Song;
    song->setFilename(filename);
    return song;
  }
  if (string2QString(ext).lower() == "ogg")
  {
    tSong *song = new tOggSong;
    song->setFilename(filename);
    return song;
  }
  throw tRuntimeError(
      qApp->translate("ErrorMessages", "Unknown file type: %1")
      .arg(string2QString(filename)));
}





tSong *deserializeSong(const char **attributes)
{
  tFilename filename;
  filename = decodeFilename(lookupAttribute("filename", attributes));

  tSong *song = makeSong(filename);
  song->deserialize(attributes);
  return song;
}



  
//
// Local Variables:
// mode: C++
// eval: (c-set-style "stroustrup")
// eval: (c-set-offset 'access-label -2)
// eval: (c-set-offset 'inclass '++)
// c-basic-offset: 2
// tab-width: 8
// End:

Generated by  Doxygen 1.6.0   Back to index