Browse Source

Initial version

Girish Ramakrishnan 8 years ago
commit
aa108236ef
12 changed files with 866 additions and 0 deletions
  1. 5 0
      DESIGN
  2. 233 0
      exifreader.cpp
  3. 60 0
      exifreader.h
  4. 20 0
      main.cpp
  5. 28 0
      mediamodel.cpp
  6. 24 0
      mediamodel.h
  7. 244 0
      mediascanner.cpp
  8. 43 0
      mediascanner.h
  9. 23 0
      mediascanner.pro
  10. 27 0
      schema.sql
  11. 119 0
      tagreader.cpp
  12. 40 0
      tagreader.h

+ 5 - 0
DESIGN

@@ -0,0 +1,5 @@
+MediaModel is the actual model that loads only what is needed
+from the database depending on what the view sees.
+
+MediaScanner scans directories and populates the database. All
+DB write operations reside here.

+ 233 - 0
exifreader.cpp

@@ -0,0 +1,233 @@
+#include "exifreader.h"
+
+#include <QFile>
+#include <QDebug>
+
+ExifReader::ExifReader(const QString &file) :
+	m_sizeOfRational(exif_format_get_size(EXIF_FORMAT_RATIONAL))
+{
+	m_data = exif_data_new_from_file(QFile::encodeName(file).constData());
+	if (m_data)
+		m_byteOrder = exif_data_get_byte_order(m_data);
+}
+
+ExifReader::~ExifReader() 
+{
+    if (m_data)
+	    exif_data_unref(m_data);
+}
+
+QString ExifReader::stringTag(ExifTag tag) const 
+{
+	if (!m_data)
+		return QString();
+
+	ExifEntry *entry = exif_data_get_entry(m_data, tag);
+	return stringValue(entry);
+}
+
+QString ExifReader::userComments() const 
+{
+	return stringTag(EXIF_TAG_USER_COMMENT);
+}
+
+QString ExifReader::imageDescription() const 
+{
+	return stringTag(EXIF_TAG_IMAGE_DESCRIPTION);
+}
+
+QDateTime ExifReader::creationTime() 
+{
+	QDateTime dateTime;
+	if (!m_data)
+		return dateTime;
+
+	const ExifTag TIME_TAGS[3] = {
+		EXIF_TAG_DATE_TIME,
+		EXIF_TAG_DATE_TIME_ORIGINAL,
+		EXIF_TAG_DATE_TIME_DIGITIZED
+	};
+	for (unsigned i = 0; i < sizeof(TIME_TAGS) / sizeof(ExifTag); i++) {
+		ExifEntry *entry = exif_data_get_entry(m_data, TIME_TAGS[i]);
+		if (!entry)
+			continue;
+		dateTime = QDateTime::fromString(stringValue(entry), Qt::ISODate);
+		if (dateTime.isValid())
+			return dateTime;
+	}
+
+	return dateTime;
+}
+
+QString ExifReader::cameraModel() const 
+{
+	return stringTag(EXIF_TAG_MODEL);
+}
+
+QString ExifReader::cameraMake() const 
+{
+	return stringTag(EXIF_TAG_MAKE);
+}
+
+// http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/GPS.html
+double ExifReader::latitude(bool *hasLatitude) const 
+{
+    if (hasLatitude)
+        *hasLatitude = false;
+
+	if (!m_data)
+		return 0;
+
+	ExifEntry *entry = exif_data_get_entry(m_data, (ExifTag) EXIF_TAG_GPS_LATITUDE_REF);
+	const QString ref = stringValue(entry).trimmed().toUpper();
+	if (ref != "N" && ref != "S")
+		return 0;
+	entry = exif_data_get_entry(m_data, (ExifTag) EXIF_TAG_GPS_LATITUDE);
+	if (!entry)
+		return 0;
+	ExifRational degrees = exif_get_rational(entry->data + 0 * m_sizeOfRational, m_byteOrder);
+	ExifRational minutes = exif_get_rational(entry->data + 1 * m_sizeOfRational, m_byteOrder);
+	ExifRational seconds = exif_get_rational(entry->data + 2 * m_sizeOfRational, m_byteOrder);
+	if (degrees.denominator == 0 || minutes.denominator == 0 || seconds.denominator == 0)
+		return 0;
+    
+    if (hasLatitude)
+        *hasLatitude = true;
+
+	return (ref == "N" ? +1 : -1)
+			* (degrees.numerator/degrees.denominator)
+				+ (minutes.numerator/(minutes.denominator*60.0))
+				+ (seconds.numerator/(seconds.denominator*3600.0));
+}
+
+double ExifReader::longitude(bool *hasLongitude) const 
+{
+    if (hasLongitude)
+        *hasLongitude = false;
+
+	if (!m_data)
+		return 0;
+
+	ExifEntry *entry = exif_data_get_entry(m_data, (ExifTag) EXIF_TAG_GPS_LONGITUDE_REF);
+	const QString ref = stringValue(entry).trimmed().toUpper();
+	if (ref != "E" && ref != "W")
+		return 0;
+	entry = exif_data_get_entry(m_data, (ExifTag) EXIF_TAG_GPS_LONGITUDE);
+	if (!entry)
+		return 0;
+	ExifRational degrees = exif_get_rational(entry->data + 0 * m_sizeOfRational, m_byteOrder);
+	ExifRational minutes = exif_get_rational(entry->data + 1 * m_sizeOfRational, m_byteOrder);
+	ExifRational seconds = exif_get_rational(entry->data + 2 * m_sizeOfRational, m_byteOrder);
+	if (degrees.denominator == 0 || minutes.denominator == 0 || seconds.denominator == 0)
+		return 0;
+
+    if (hasLongitude)
+        *hasLongitude = true;
+
+	return (ref == "E" ? +1 : -1)
+			* ((degrees.numerator/degrees.denominator)
+			+  (minutes.numerator/(minutes.denominator*60.0))
+			+  (seconds.numerator/(seconds.denominator*3600.0)));
+}
+
+double ExifReader::altitude(bool *hasAltitude) const 
+{
+    if (hasAltitude)
+        *hasAltitude = false;
+
+	if (!m_data)
+		return 0;
+
+	ExifEntry *entry = exif_data_get_entry(m_data, (ExifTag) EXIF_TAG_GPS_ALTITUDE_REF);
+	if (!entry)
+		return 0;
+	char ref;
+	exif_entry_get_value(entry, &ref, 1);
+	entry = exif_data_get_entry(m_data, (ExifTag) EXIF_TAG_GPS_ALTITUDE);
+	if (!entry)
+		return 0;
+	ExifRational r = exif_get_rational(entry->data, m_byteOrder);
+	if (r.denominator == 0)
+		return 0;
+
+    if (hasAltitude)
+        *hasAltitude = true;
+
+	return (ref == 0 ? +1 : -1)  * (r.numerator*1.0/r.denominator);
+}
+
+ExifReader::Orientation ExifReader::orientation() const 
+{
+	if (!m_data)
+		return Invalid;
+
+	ExifEntry *entry = exif_data_get_entry(m_data, (ExifTag) EXIF_TAG_ORIENTATION);
+	if (!entry)
+		return Invalid;
+	ExifByteOrder order = exif_data_get_byte_order(m_data);
+	return (Orientation) exif_get_short (entry->data, order);
+}
+
+QString ExifReader::stringValue(ExifEntry *entry) const 
+{
+	if (!entry)
+		return QString();
+
+	char value[1024] = { 0 };
+	exif_entry_get_value(entry, value, sizeof(value) - 1);
+	value[sizeof(value)-1] = 0;
+	return QString::fromAscii(value); // ###: fix encoding
+}
+
+QString ExifReader::aperture() const
+{
+    return stringTag(EXIF_TAG_APERTURE_VALUE);
+}
+
+QString ExifReader::focalLength() const
+{
+    return stringTag(EXIF_TAG_FOCAL_LENGTH);
+}
+
+QString ExifReader::exposureTime() const
+{
+    return stringTag(EXIF_TAG_EXPOSURE_TIME);
+}
+
+QString ExifReader::exposureMode() const
+{
+    return stringTag(EXIF_TAG_EXPOSURE_MODE);
+}
+
+QString ExifReader::whiteBalance() const
+{
+    return stringTag(EXIF_TAG_WHITE_BALANCE);
+}
+
+QString ExifReader::lightSource() const
+{
+    return stringTag(EXIF_TAG_LIGHT_SOURCE);
+}
+
+QString ExifReader::isoSpeed() const
+{
+    return stringTag(EXIF_TAG_ISO_SPEED_RATINGS);
+}
+
+QString ExifReader::digitalZoomRatio() const
+{
+    return stringTag(EXIF_TAG_DIGITAL_ZOOM_RATIO);
+}
+
+QString ExifReader::flashUsage() const
+{
+    return stringTag(EXIF_TAG_FLASH);
+}
+
+QString ExifReader::colorSpace() const
+{
+    return stringTag(EXIF_TAG_COLOR_SPACE);
+}
+
+
+

+ 60 - 0
exifreader.h

@@ -0,0 +1,60 @@
+#ifndef EXIFREADER_H
+#define EXIFREADER_H
+
+#include <libexif/exif-data.h>
+#include <libexif/exif-utils.h>
+#include <libexif/exif-loader.h>
+
+#include <QDateTime>
+#include <QString>
+
+class ExifReader 
+{
+public:
+    ExifReader(const QString &file);
+    ~ExifReader();
+
+    QString stringTag(ExifTag tag) const;
+    QString userComments() const;
+    QString imageDescription() const;
+    QDateTime creationTime();
+    QString cameraModel() const;
+    QString cameraMake() const;
+
+    double latitude(bool *ok = 0) const;
+    double longitude(bool *ok = 0) const;
+    double altitude(bool *ok = 0) const;
+
+    // http://sylvana.net/jpegcrop/exif_orientation.html
+    enum Orientation {
+        Invalid,
+        NoOrientation = 1,
+        FlipHorizontal,
+        Rotate180,
+        FlipVertical,
+        Transpose,
+        Rotate90,
+        Transverse,
+        Rotate270
+    };
+    Orientation orientation() const;
+
+    QString aperture() const;
+    QString focalLength() const;
+    QString exposureTime() const;
+    QString exposureMode() const;
+    QString whiteBalance() const;
+    QString lightSource() const;
+    QString isoSpeed() const;
+    QString digitalZoomRatio() const;
+    QString flashUsage() const;
+    QString colorSpace() const;
+
+private:
+    QString stringValue(ExifEntry *entry) const;
+    ExifByteOrder m_byteOrder;
+    const int m_sizeOfRational;
+    ExifData *m_data;
+};
+
+#endif // EXIFREADER_H

+ 20 - 0
main.cpp

@@ -0,0 +1,20 @@
+#include <QtGui>
+#include <QtSql>
+
+#include "mediamodel.h"
+
+int main(int argc, char *argv[])
+{
+    QApplication app(argc, argv);
+
+    if (!QSqlDatabase::isDriverAvailable("QSQLITE")) {
+        qFatal("The SQLITE driver is unavailable");
+        return 1;
+    }
+
+    MediaModel model;
+    model.addDirectory("/media/TOSHIBA EXT/music/");
+
+    return app.exec();
+}
+

+ 28 - 0
mediamodel.cpp

@@ -0,0 +1,28 @@
+#include "mediamodel.h"
+#include "mediascanner.h"
+#include <QtSql>
+
+MediaModel::MediaModel(QObject *parent)
+    : QObject(parent)
+{
+    m_scanner = new MediaScanner;
+
+    m_scannerThread = new QThread(this);
+    m_scanner->moveToThread(m_scannerThread);
+    m_scannerThread->start();
+
+    QMetaObject::invokeMethod(m_scanner, "initialize", Qt::QueuedConnection);
+    QMetaObject::invokeMethod(m_scanner, "refresh", Qt::QueuedConnection);
+}
+
+MediaModel::~MediaModel()
+{
+    m_scannerThread->quit();
+    // m_scanner leaks...
+}
+
+void MediaModel::addDirectory(const QString &path)
+{
+    QMetaObject::invokeMethod(m_scanner, "addDirectory", Qt::QueuedConnection, Q_ARG(QString, path));
+}
+

+ 24 - 0
mediamodel.h

@@ -0,0 +1,24 @@
+#ifndef MEDIAMODEL_H
+#define MEDIAMODEL_H
+
+#include <QObject>
+
+class QThread;
+class MediaScanner;
+
+class MediaModel : public QObject
+{
+    Q_OBJECT
+public:
+    MediaModel(QObject *parent = 0);
+    ~MediaModel();
+
+    void addDirectory(const QString &path);
+
+private:
+    MediaScanner *m_scanner;
+    QThread *m_scannerThread;
+};
+
+#endif // MEDIAMODEL_H
+

+ 244 - 0
mediascanner.cpp

@@ -0,0 +1,244 @@
+#include "mediascanner.h"
+#include <QtSql>
+#include "mediainfo.h"
+#include "tagreader.h"
+
+#define DEBUG qDebug() << __PRETTY_FUNCTION__
+
+static const QString DATABASE_NAME = "media.db";
+static const QString CONNECTION_NAME = "MediaDatabase";
+
+class ScopedTransaction
+{
+public:
+    ScopedTransaction(QSqlDatabase &db) : m_db(db), m_query(db), m_errored(false) { 
+        m_db.transaction(); 
+    }
+    ~ScopedTransaction() { 
+        if (!m_errored)
+            m_db.commit(); 
+    }
+    
+    bool exec(const QString &query) {
+        if (m_errored) {
+            qDebug() << query << " skipped, transaction already errored";
+            return false;
+        }
+
+        if (!m_query.exec(query)) {
+            qDebug() << query << " failed with error : " << m_query.lastError();
+            m_db.rollback();
+            m_errored = true;
+            return false;
+        }
+        return true;
+    }
+
+    bool execFile(const QString &fileName) {
+        if (m_errored) {
+            qDebug() << fileName << " skipped, transaction already errored";
+            return false;
+        }
+
+        QFile file(fileName);
+        if (!file.open(QFile::ReadOnly)) {
+            qDebug() << fileName << " could not be opened for exec";
+            return false;
+        }
+
+        // Need to split the queries by hand - QTBUG-8689
+        QString fileContents = file.readAll();
+        QStringList queries = fileContents.split(";\n\n", QString::SkipEmptyParts);
+        foreach(const QString &query, queries) {
+            if (!m_query.exec(query)) {
+                qDebug() << fileName << " failed to exec " << query << " . Error : " << m_query.lastError();
+                m_db.rollback();
+                m_errored = true;
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+private:
+    QSqlDatabase &m_db;
+    QSqlQuery m_query;
+    bool m_errored;
+};
+
+MediaScanner::MediaScanner(QObject *parent)
+    : QObject(parent)
+{
+}
+
+MediaScanner::~MediaScanner()
+{
+    QSqlDatabase::removeDatabase(CONNECTION_NAME);
+}
+
+bool MediaScanner::initialize()
+{
+    if (!QSqlDatabase::isDriverAvailable("QSQLITE")) {
+        m_errorString = "The SQLITE driver is unavailable";
+        return false;
+    }
+
+    m_db = QSqlDatabase::addDatabase("QSQLITE", CONNECTION_NAME);
+    m_db.setDatabaseName(DATABASE_NAME);
+    if (!m_db.open()) {
+        m_errorString = "Failed to open SQLITE database";
+        return false;
+    }
+
+    if (!m_db.tables().isEmpty()) {
+        DEBUG << "Database already exists";
+        return true;
+    }
+
+    // create tables
+    DEBUG << "Creating database";
+    ScopedTransaction transaction(m_db);
+    return transaction.execFile("schema.sql");
+}
+
+void MediaScanner::addDirectory(const QString &_path)
+{
+    QString path = QFileInfo(_path).absoluteFilePath();
+    if (path.endsWith('/'))
+        path.chop(1);
+
+    QSqlQuery query(m_db);
+    query.prepare("INSERT INTO directories (path) VALUES (:path)");
+    query.bindValue(":path", path);
+    if (!query.exec()) {
+        m_errorString = query.lastError().text();
+        DEBUG << m_errorString;
+        return;
+    }
+
+    scan(path);
+}
+
+QHash<QString, MediaScanner::FileInfo> MediaScanner::findFilesByPath(const QString &path)
+{
+    QHash<QString, MediaScanner::FileInfo> hash;
+    QSqlQuery query(m_db);
+    query.prepare("SELECT filepath, mtime, ctime, filesize FROM music WHERE directory=:path");
+    query.bindValue(":path", path);
+    if (!query.exec()) {
+        m_errorString = query.lastError().text();
+        DEBUG << m_errorString;
+        return hash;
+    }
+
+    while (query.next()) {
+        FileInfo fi;
+        fi.name = query.value(0).toString();
+        fi.mtime = query.value(1).toLongLong();
+        fi.ctime = query.value(2).toLongLong();
+        fi.size = query.value(3).toLongLong();
+
+        hash.insert(fi.name, fi);
+    }
+
+    return hash;
+}
+
+// ## See if DELETE+INSERT is the best approach. Sqlite3 supports INSERT OR IGNORE which could aslo be used
+// ## Also check other upsert methods
+void MediaScanner::updateMediaInfo(const QFileInfo &fi)
+{
+    TagReader reader(fi.absoluteFilePath());
+
+    ScopedTransaction transaction(m_db);
+    QSqlQuery query(m_db);
+    query.prepare("DELETE FROM music WHERE filepath=:filepath");
+    query.bindValue(":filepath", fi.absoluteFilePath());
+    if (!query.exec())
+        DEBUG << query.lastError().text();
+
+    if (!query.prepare("INSERT INTO music (filepath, title, album, artist, track, year, genre, comment, length, bitrate, samplerate, directory, mtime, ctime, filesize) "
+                       " VALUES (:filepath, :title, :album, :artist, :track, :year, :genre, :comment, :length, :bitrate, :samplerate, :directory, :mtime, :ctime, :filesize)")) {
+        DEBUG << query.lastError().text();
+        return;
+    }
+
+    query.bindValue(":filepath", fi.absoluteFilePath());
+
+    query.bindValue(":title", reader.title());
+    query.bindValue(":album", reader.album());
+    query.bindValue(":artist", reader.artist());
+    query.bindValue(":track", reader.track());
+    query.bindValue(":year", reader.year());
+    query.bindValue(":genre", reader.genre());
+    query.bindValue(":comment", reader.comment());
+    query.bindValue(":length", reader.length());
+    query.bindValue(":bitrate", reader.bitrate());
+    query.bindValue(":samplerate", reader.sampleRate());
+
+    query.bindValue(":directory", fi.absolutePath());
+    query.bindValue(":mtime", fi.lastModified().toTime_t());
+    query.bindValue(":ctime", fi.created().toTime_t());
+    query.bindValue(":filesize", fi.size());
+
+    if (!query.exec())
+        DEBUG << query.lastError().text();
+}
+
+static bool isMediaFile(const QFileInfo &info)
+{
+    return info.suffix() == "mp3";
+}
+
+void MediaScanner::scan(const QString &path)
+{
+    QQueue<QString> dirQ;
+    dirQ.enqueue(path);
+
+    while (!dirQ.isEmpty()) {
+        QString curdir = dirQ.dequeue();
+        QFileInfoList fileInfosInDisk = QDir(curdir).entryInfoList(QDir::Files|QDir::Dirs|QDir::NoDotAndDotDot|QDir::NoSymLinks);
+        QHash<QString, FileInfo> fileInfosInDb = findFilesByPath(curdir);
+
+        DEBUG << "Scanning " << curdir << fileInfosInDisk.count() << " files in disk and " << fileInfosInDb.count() << "in database";
+
+        foreach(const QFileInfo &diskFileInfo, fileInfosInDisk) {
+            FileInfo dbFileInfo = fileInfosInDb.take(diskFileInfo.absoluteFilePath());
+
+            if (diskFileInfo.isFile()) {
+                if (!isMediaFile(diskFileInfo))
+                    continue;
+
+                if (diskFileInfo.lastModified().toTime_t() == dbFileInfo.mtime
+                    && diskFileInfo.created().toTime_t() == dbFileInfo.ctime
+                    && diskFileInfo.size() == dbFileInfo.size) {
+                    DEBUG << diskFileInfo.absoluteFilePath() << " : no change";
+                    continue;
+                }
+
+                updateMediaInfo(diskFileInfo);
+                DEBUG << diskFileInfo.absoluteFilePath() << " : added";
+            } else if (diskFileInfo.isDir()) {
+                dirQ.enqueue(diskFileInfo.absoluteFilePath());
+            }
+        }
+
+        // ## remove the files from the db in the fileInfosInDb hash now?
+    }
+}
+
+void MediaScanner::refresh()
+{
+    QSqlQuery query(m_db);
+    query.exec("SELECT path FROM directories");
+
+    QStringList dirs;
+    while (query.next()) {
+        dirs << query.value(0).toString();
+    }
+
+    for (int i = 0; i < dirs.count(); i++)
+        scan(dirs[i]);
+}
+

+ 43 - 0
mediascanner.h

@@ -0,0 +1,43 @@
+#ifndef MEDIASCANNER_H
+#define MEDIASCANNER_H
+
+#include <QObject>
+#include <QFileInfo>
+#include <QSqlDatabase>
+#include <QHash>
+
+#include "mediainfo.h"
+
+class MediaScanner : public QObject
+{
+    Q_OBJECT
+public:
+    MediaScanner(QObject *parent = 0);
+    ~MediaScanner();
+
+    struct FileInfo {
+        QString name;
+        quint32 mtime;
+        quint32 ctime;
+        qint64 size;
+
+        bool valid() const { return !name.isEmpty(); }
+    };
+
+    QHash<QString, FileInfo> findFilesByPath(const QString &path);
+
+public slots:
+    bool initialize();
+    void addDirectory(const QString &path);
+    void refresh();
+    
+private:
+    void scan(const QString &path);
+    void updateMediaInfo(const QFileInfo &fi);
+
+    QSqlDatabase m_db;
+    QString m_errorString;
+};
+
+#endif // MEDIASCANNER_H
+

+ 23 - 0
mediascanner.pro

@@ -0,0 +1,23 @@
+TEMPLATE = app
+TARGET = 
+DEPENDPATH += .
+INCLUDEPATH += .
+
+QT += sql
+
+CONFIG += link_pkgconfig
+
+PKGCONFIG += taglib libexif
+
+# Input
+SOURCES += main.cpp \
+           mediamodel.cpp \
+           mediascanner.cpp \
+           exifreader.cpp \
+           tagreader.cpp
+
+HEADERS += mediamodel.h \
+           mediascanner.h \
+           exifreader.h \
+           tagreader.h
+

+ 27 - 0
schema.sql

@@ -0,0 +1,27 @@
+CREATE TABLE directories (
+    path TEXT NOT NULL PRIMARY KEY
+);
+
+CREATE TABLE music (
+    filepath TEXT NOT NULL PRIMARY KEY,
+
+    /* taglib */
+    title TEXT,
+    album TEXT,
+    artist TEXT,
+    track INTEGER,
+    year INTEGER,
+    genre TEXT,
+    comment TEXT,
+
+    length INTEGER,
+    bitrate INTEGER,
+    samplerate INTEGER,
+
+    /* stat information */
+    directory TEXT NOT NULL,
+    mtime INTEGER NOT NULL,
+    ctime INTEGER NOT NULL,
+    filesize INTEGER NOT NULL
+);
+

+ 119 - 0
tagreader.cpp

@@ -0,0 +1,119 @@
+#include "tagreader.h"
+#include <QFile>
+
+TagReader::TagReader(const QString &file)
+    : m_tag(0), m_audioProperties(0)
+{
+    QByteArray fileName = QFile::encodeName(file);
+
+    m_fileRef = TagLib::FileRef (fileName.constData());
+    if (!m_fileRef.isNull()) {
+        TagLib::File *file = m_fileRef.file();
+        m_tag = file->tag();
+        m_audioProperties = file->audioProperties();
+    }
+}
+
+TagReader::~TagReader()
+{
+}
+
+static inline QString fromTagString(const TagLib::String &string)
+{
+    return QString::fromStdWString(string.toWString());
+}
+
+QString TagReader::title() const
+{
+    return m_tag ? fromTagString(m_tag->title()) : QString();
+}
+
+QString TagReader::album() const
+{
+    return m_tag ? fromTagString(m_tag->album()) : QString();
+}
+
+QString TagReader::artist() const
+{
+    return m_tag ? fromTagString(m_tag->artist()) : QString();
+}
+
+int TagReader::track() const
+{
+    return m_tag ? m_tag->track() : -1;
+}
+
+int TagReader::year() const
+{
+    return m_tag ? m_tag->year() : -1;
+}
+
+QString TagReader::genre() const
+{
+    return m_tag ? fromTagString(m_tag->genre()) : QString();
+}
+
+QString TagReader::comment() const
+{
+    return m_tag ? fromTagString(m_tag->comment()) : QString();
+}
+
+int TagReader::length() const
+{
+    return m_audioProperties ? m_audioProperties->length() : -1;
+}
+
+int TagReader::bitrate() const
+{
+    return m_audioProperties ? m_audioProperties->bitrate() : -1;
+}
+
+int TagReader::sampleRate() const
+{
+    return m_audioProperties ? m_audioProperties->sampleRate() : -1;
+}
+
+static QImage readFrontCover(TagLib::ID3v2::Tag *id3v2Tag)
+{
+    TagLib::ID3v2::FrameList frames = id3v2Tag->frameListMap()["APIC"];
+    if (frames.isEmpty()) {
+        //qDebug() << "No front cover";
+        return QImage();
+    }
+
+    TagLib::ID3v2::AttachedPictureFrame *selectedFrame = 0;
+    if (frames.size() != 1) {
+        TagLib::ID3v2::FrameList::Iterator it = frames.begin();
+        for (; it != frames.end(); ++it) {
+            TagLib::ID3v2::AttachedPictureFrame *frame = dynamic_cast<TagLib::ID3v2::AttachedPictureFrame *>(*it);
+            if (frame && frame->type() != TagLib::ID3v2::AttachedPictureFrame::FrontCover) // BackCover, LeafletPage
+                continue;
+            selectedFrame = frame;
+            break;
+        }
+    }
+    if (!selectedFrame)
+        selectedFrame = dynamic_cast<TagLib::ID3v2::AttachedPictureFrame *>(frames.front());
+    if (!selectedFrame)
+        return QImage();
+
+    QByteArray imageData(selectedFrame->picture().data(), selectedFrame->picture().size());
+    QImage attachedImage = QImage::fromData(imageData);
+    // ## scale as necessary
+    return attachedImage;
+}
+
+QImage TagReader::thumbnail() const
+{
+    TagLib::File *file = m_fileRef.file();
+    TagLib::MPEG::File *mpegFile = dynamic_cast<TagLib::MPEG::File *>(file);
+    if (!mpegFile)
+        return QImage();
+
+    TagLib::ID3v2::Tag *id3v2Tag = mpegFile->ID3v2Tag(false);
+    if (!id3v2Tag)
+        return QImage();
+
+    return readFrontCover(id3v2Tag);
+}
+

+ 40 - 0
tagreader.h

@@ -0,0 +1,40 @@
+#ifndef TAGREADER_H
+#define TAGREADER_H
+
+#include <taglib/fileref.h>
+#include <taglib/tag.h>
+#include <taglib/mpegfile.h>
+#include <taglib/id3v2tag.h>
+#include <taglib/attachedpictureframe.h>
+
+#include <QString>
+#include <QImage>
+
+class TagReader
+{
+public:
+    TagReader(const QString &file);
+    ~TagReader();
+
+    QString title() const;
+    QString album() const;
+    QString artist() const;
+    int track() const;
+    int year() const;
+    QString genre() const;
+    QString comment() const;
+
+    int length() const;
+    int bitrate() const;
+    int sampleRate() const;
+
+    QImage thumbnail() const;
+
+private:
+    TagLib::FileRef m_fileRef;
+    TagLib::Tag *m_tag;
+    TagLib::AudioProperties *m_audioProperties;
+};
+
+#endif // TAGREADER_H
+