From: APTX Date: Sat, 26 May 2012 01:51:36 +0000 (+0200) Subject: LocalMyList X-Git-Url: https://gitweb.tyo.aptx.org/?a=commitdiff_plain;h=fb0f70ed5800aeaffb49dd1eef264330ffc4a565;p=localmylist.git LocalMyList --- fb0f70ed5800aeaffb49dd1eef264330ffc4a565 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3dfe734 --- /dev/null +++ b/.gitignore @@ -0,0 +1,76 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +.qmake.cache +tags +.DS_Store +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp + +# qtcreator generated files +*.pro.user +*.pro.user.* +*.autosave + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.exp + +# MinGW generated files +*.Debug +*.Release + +# Directories to ignore +# --------------------- + +debug +release +lib/qtsingleapplication/lib +lib/qtsingleapplication/examples +lib/qtsingleapplication/doc +.tmp +qtc-gdbmacros + +# Binaries +# -------- +build/aniplayer +build/*.dll +build/*.lib +build/*.exe +build/*.so* + + diff --git a/import-mylist/import-mylist.pro b/import-mylist/import-mylist.pro new file mode 100644 index 0000000..8b68571 --- /dev/null +++ b/import-mylist/import-mylist.pro @@ -0,0 +1,13 @@ +QT += core +QT -= gui + +TARGET = import-mylist +DESTDIR = ../build +CONFIG += console +CONFIG -= app_bundle + +TEMPLATE = app + +SOURCES += main.cpp + +include(../localmylist.pri) diff --git a/import-mylist/main.cpp b/import-mylist/main.cpp new file mode 100644 index 0000000..5485df9 --- /dev/null +++ b/import-mylist/main.cpp @@ -0,0 +1,34 @@ +#include + +#include +#include + +#include "mylist.h" +#include "abstracttask.h" + +using namespace LocalMyList; + +int main(int argc, char *argv[]) +{ + QCoreApplication a(argc, argv); + QTextStream cout(stdout); + if (a.arguments().count() < 2) + { + cout << "Usage: " << a.arguments()[0] << " FILE"; + return 1; + } + + QFile f(a.arguments()[1]); + if (!f.open(QIODevice::ReadOnly)) + { + cout << "Failed open file for reading"; + return 1; + } + + AbstractTask *t = LocalMyList::instance()->importMyList(a.arguments()[1]); + QObject::connect(t, SIGNAL(finished()), &a, SLOT(quit())); + + return a.exec(); + + return 0; +} diff --git a/import-titles/import-titles.pro b/import-titles/import-titles.pro new file mode 100644 index 0000000..2ab825d --- /dev/null +++ b/import-titles/import-titles.pro @@ -0,0 +1,13 @@ +QT += core +QT -= gui + +TARGET = import-titles +DESTDIR = ../build +CONFIG += console +CONFIG -= app_bundle + +TEMPLATE = app + +SOURCES += main.cpp + +include(../localmylist.pri) diff --git a/import-titles/main.cpp b/import-titles/main.cpp new file mode 100644 index 0000000..2316911 --- /dev/null +++ b/import-titles/main.cpp @@ -0,0 +1,24 @@ +#include + +#include +#include +#include "mylist.h" +#include "abstracttask.h" + +using namespace LocalMyList; + +int main(int argc, char *argv[]) +{ + QCoreApplication a(argc, argv); + QTextStream cout(stdout); + if (a.arguments().count() < 2) + { + cout << "Usage: " << a.arguments()[0] << " FILE"; + return 1; + } + + AbstractTask *t = LocalMyList::instance()->importTitles(a.arguments()[1]); + QObject::connect(t, SIGNAL(finished()), &a, SLOT(quit())); + + return a.exec(); +} diff --git a/localmylist.pri b/localmylist.pri new file mode 100644 index 0000000..86b931b --- /dev/null +++ b/localmylist.pri @@ -0,0 +1,7 @@ +QT *= sql + +INCLUDEPATH += $$PWD/localmylist/include +INCLUDEPATH += $$PWD/localmylist +DEPENDPATH += $$PWD/localmylist +LIBS += -llocalmylist +LIBS += -L$$PWD/build diff --git a/localmylist.pro b/localmylist.pro new file mode 100644 index 0000000..208cee3 --- /dev/null +++ b/localmylist.pro @@ -0,0 +1,9 @@ +TEMPLATE = subdirs + +SUBDIRS += \ + localmylist \ + search-gui \ + import-titles \ + import-mylist \ + management-gui \ + play-next diff --git a/localmylist/abstracttask.cpp b/localmylist/abstracttask.cpp new file mode 100644 index 0000000..aa351bd --- /dev/null +++ b/localmylist/abstracttask.cpp @@ -0,0 +1,51 @@ +#include "abstracttask.h" + +namespace LocalMyList { + +AbstractTask::AbstractTask(Database *db, QObject *parent) : + QObject(parent) +{ + this->db = db; + connect(this, SIGNAL(nextWorkUnit()), this, SLOT(doNextWorkUnit()), Qt::QueuedConnection); +} + +AbstractTask::~AbstractTask() +{ +} + +QString AbstractTask::taskName() const +{ + return metaObject()->className(); +} + +QString AbstractTask::taskSubject() const +{ + return QString(); +} + +bool AbstractTask::canUseThreads() const +{ + return false; +} + +void AbstractTask::setDatabase(Database *db) +{ + this->db = db; +} + +void AbstractTask::start() +{ +} + + +void AbstractTask::doNextWorkUnit() +{ + workUnit(); +} + + +void AbstractTask::workUnit() +{ +} + +} // namespace LocalMyList diff --git a/localmylist/abstracttask.h b/localmylist/abstracttask.h new file mode 100644 index 0000000..4945917 --- /dev/null +++ b/localmylist/abstracttask.h @@ -0,0 +1,46 @@ +#ifndef ABSTRACTTASK_H +#define ABSTRACTTASK_H + +#include + +namespace LocalMyList { + +class Database; + +class AbstractTask : public QObject +{ + Q_OBJECT +public: + explicit AbstractTask(Database *db = 0, QObject *parent = 0); + virtual ~AbstractTask(); + + virtual QString taskName() const; + virtual QString taskSubject() const; + virtual bool canUseThreads() const; + + void setDatabase(Database *db); + +public slots: + virtual void start() = 0; + +signals: + void nextWorkUnit(); + void progress(int done, int currentTotal); + // TODO add some sort of error reporting + void finished(); + +protected: + virtual void workUnit(); + +private slots: + void doNextWorkUnit(); + +protected: + Database *db; + + static const int OPERATIONS_PER_UNIT = 100; +}; + +} // namespace LocalMyList + +#endif // ABSTRACTTASK_H diff --git a/localmylist/addfiletask.cpp b/localmylist/addfiletask.cpp new file mode 100644 index 0000000..6880bd0 --- /dev/null +++ b/localmylist/addfiletask.cpp @@ -0,0 +1,137 @@ +#include "addfiletask.h" + +#include "database.h" +#include "mylist.h" + +#include +#include +#include + +namespace LocalMyList { + +using namespace AniDBUdpClient; + +AddFileTask::AddFileTask(Database *db, QObject *parent) : + AbstractTask(db, parent), hashResult(0) +{ +} + +AddFileTask::~AddFileTask() +{ + if (hashResult) + hashResult->deleteLater(); +} + +QFileInfo AddFileTask::file() const +{ + return m_file; +} + +QString AddFileTask::taskSubject() const +{ + return m_file.fileName(); +} + +void AddFileTask::start() +{ + if (!m_file.exists()) + { + emit finished(); + return; + } + + if (hashResult) + { + hashingFinished(); + return; + } + hashResult = Hash::instance()->hashFile(HashRequest(m_file)); + connect(hashResult, SIGNAL(resultReady()), this, SLOT(hashingFinished())); +} + +void AddFileTask::setFile(const QFileInfo &file) +{ + m_file = file; +} + +void AddFileTask::hashingFinished() +{ + int fid = db->isKnownFile(hashResult->hash(), m_file.size()); + if (fid) + { + db->setFileLocation(fid, MyList::instance()->hostId(), m_file.canonicalFilePath()); + emit finished(); + return; + } + + UnknownFile f; + + f.ed2k = hashResult->hash(); + f.size = m_file.size(); + f.hostId = MyList::instance()->hostId(); + f.path = m_file.canonicalFilePath(); + + db->addUnknownFile(f); + + PendingRequest request; + request.ed2k = hashResult->hash(); + request.size = m_file.size(); + + db->addRequest(request); + + emit finished(); + +/* + static const FileFlags fileFlags = + FileFlag::Aid | FileFlag::Eid | FileFlag::Gid + | FileFlag::LengthInSeconds + | FileFlag::FileType + | FileFlag::Crc32 + | FileFlag::State + | FileFlag::Quality + | FileFlag::VideoResolution + | FileFlag::VideoCodec + | FileFlag::AudioCodec + | FileFlag::SubLanguage + | FileFlag::DubLanguage + | FileFlag::MyListViewDate; + + static const FileAnimeFlags fileAnimeFlags = + FileAnimeFlag::GroupName + | FileAnimeFlag::GroupShortName; + if (fileReply) + fileReply->deleteLater(); + fileReply = Client::instance()->send(FileCommand( + hashResult->hash(), m_file.size(), + fileFlags, fileAnimeFlags)); + connect(fileReply, SIGNAL(replyReady(bool)), this, SLOT(fileDataRecieved(bool))); +*/ +} +/* +void AddFileTask::fileDataRecieved(bool success) +{ + if (!success) + { + // Insert into unknown files + return; + } + File f; + + f.fid = fileReply->fid(); + f.aid = fileReply->value(FileFlag::Aid).toInt(); + f.eid = fileReply->value(FileFlag::Eid).toInt(); + f.gid = fileReply->value(FileFlag::Gid).toInt(); + f.length = fileReply->value(FileFlag::LengthInSeconds).toInt(); + f.extension = fileReply->value(FileFlag::FileType).toString(); + f.crc = fileReply->value(FileFlag::Crc32).toString(); + //f. = fileReply->value(FileFlag::State); + f.quality = fileReply->value(FileFlag::Quality).toString(); + f.resolution = fileReply->value(FileFlag::VideoResolution).toString(); + f.videoCodec = fileReply->value(FileFlag::VideoCodec).toString(); + f.audioCodec = fileReply->value(FileFlag::AudioCodec).toString(); + f.subtitleLanguage = fileReply->value(FileFlag::SubLanguage).toString(); + f.audioLanguage = fileReply->value(FileFlag::DubLanguage).toString(); + f.myWatched = QDateTime::fromTime_t(fileReply->value(FileFlag::MyListViewDate).toUInt()); +} +*/ +} // namespace LocalMyList diff --git a/localmylist/addfiletask.h b/localmylist/addfiletask.h new file mode 100644 index 0000000..5cc86f1 --- /dev/null +++ b/localmylist/addfiletask.h @@ -0,0 +1,55 @@ +#ifndef ADDFILETASK_H +#define ADDFILETASK_H + +#include "abstracttask.h" + +#include +#include + +namespace AniDBUdpClient { + class HashResult; + class FileReply; +} + +namespace LocalMyList { + +class Database; + +// TODO change DB schema to allow for duplicate file locations +// instead of only allowing one file location ever. +class AddFileTask : public AbstractTask +{ + Q_OBJECT + Q_PROPERTY(QFileInfo file READ file WRITE setFile) + +public: + explicit AddFileTask(Database *db = 0, QObject *parent = 0); + ~AddFileTask(); + + QFileInfo file() const; + + QString taskSubject() const; + + void start(); + +signals: + +public slots: + void setFile(const QFileInfo &file); + +private slots: + void hashingFinished(); + + // File data is handled via requests + // not just any task +// void fileDataRecieved(bool success); + + +private: + QFileInfo m_file; + AniDBUdpClient::HashResult *hashResult; +}; + +} // namespace LocalMyList + +#endif // ADDFILETASK_H diff --git a/localmylist/animetitleparsetask.cpp b/localmylist/animetitleparsetask.cpp new file mode 100644 index 0000000..3c86d8e --- /dev/null +++ b/localmylist/animetitleparsetask.cpp @@ -0,0 +1,91 @@ +#include "animetitleparsetask.h" + +#include +#include "database.h" + +#include + +namespace LocalMyList { + +AnimeTitleParseTask::AnimeTitleParseTask(QObject *parent) : + AbstractTask(0, parent) +{ +} + +QFileInfo AnimeTitleParseTask::file() const +{ + return m_file; +} + +QString AnimeTitleParseTask::taskSubject() const +{ + return QString("Anime Title Parse (from file: %1)").arg(m_file.fileName()); +} + +bool AnimeTitleParseTask::canUseThreads() const +{ + return true; +} + +void AnimeTitleParseTask::start() +{ + QFile f(m_file.absoluteFilePath()); + if (!f.open(QIODevice::ReadOnly)) + { + emit finished(); + qWarning("AnimeTitleParseTask failed to open file"); + return; + } + + parse(&f); + emit finished(); +} + +void AnimeTitleParseTask::setFile(const QFileInfo &file) +{ + m_file = file; +} + +void AnimeTitleParseTask::parse(QIODevice *device) +{ + db->transaction(); + db->truncateTitleData(); + + int titles = 0; + QByteArray buf; + while (!device->atEnd()) + { + buf = device->readLine(); + QString s = QString::fromUtf8(buf); + s = s.trimmed(); + if (s.startsWith(QChar('#'))) + continue; + + QStringList parts = s.split(QChar('|')); + if (parts.count() != 4) + continue; + + AnimeTitle title; + bool ok; + title.aid = parts[0].toInt(&ok); + if (!ok) + continue; + title.type = AnimeTitle::TitleType(parts[1].toInt(&ok)); + if (!ok) + continue; + title.language = parts[2]; + title.title = parts[3]; + + db->addTitle(title); + ++titles; + if (titles % 100 != 0) + continue; + qDebug() << "Read" << titles << "titles"; + emit progress(device->pos(), device->size()); + } + qDebug() << "Done. Read" << titles << "titles"; + + db->commit(); +} + +} // namespace LocalMyList diff --git a/localmylist/animetitleparsetask.h b/localmylist/animetitleparsetask.h new file mode 100644 index 0000000..7dbe08b --- /dev/null +++ b/localmylist/animetitleparsetask.h @@ -0,0 +1,33 @@ +#ifndef ANIMETITLEPARSETASK_H +#define ANIMETITLEPARSETASK_H + +#include "abstracttask.h" +#include + +namespace LocalMyList { + +class AnimeTitleParseTask : public AbstractTask +{ + Q_OBJECT +public: + explicit AnimeTitleParseTask(QObject *parent = 0); + + QFileInfo file() const; + QString taskSubject() const; + bool canUseThreads() const; + + void start(); + +public slots: + void setFile(const QFileInfo &file); + +private: + void parse(QIODevice *device); + + QFileInfo m_file; + +}; + +} // namespace LocalMyList + +#endif // ANIMETITLEPARSETASK_H diff --git a/localmylist/database.cpp b/localmylist/database.cpp new file mode 100644 index 0000000..a21e1e6 --- /dev/null +++ b/localmylist/database.cpp @@ -0,0 +1,915 @@ +#include "database.h" + +#include +#include +#include +#include +#include +#include + +namespace LocalMyList { + +AnimeTitle::AnimeTitle(int aid, TitleType type, const QString &language, const QString &title) +{ + this->aid = aid; + this->type = type; + this->language = language; + this->title = title; +} + +Anime::Anime() +{ + aid = 0; + rating = 0; + votes = 0; + tempRating = 0; + tempVotes = 0; + myVote = 0; + myTempVote = 0; + description = ""; +} + +Episode::Episode() +{ + eid = 0; + aid = 0; + epno = 0; + length = 0; + state = 0; + special = false; + recap = false; + opening = false; + ending = false; + rating = 0; + votes = 0; + myVote = 0; +} + +File::File() +{ + fid = 0; + eid = 0; + aid = 0; + gid = 0; + size = 0; + length = 0; + version = 1; + censored = false; + qualityId = 0; + myState = 0; + myFileState = 0; +} + +FileEpisodeRel::FileEpisodeRel() +{ + fid = 0; + eid = 0; + startPercent = 0; + endPercent = 0; +} + +UnknownFile::UnknownFile() +{ + size = 0; + hostId = 0; +} + + +PendingRequest::PendingRequest() +{ + aid = 0; + eid = 0; + fid = 0; + size = 0; +} + +HostInfo::HostInfo() +{ + id = 0; + isUdpHost = false; +} + +DatabaseConnectionSettings::DatabaseConnectionSettings() +{ + port = 0; +} + +struct DatabaseInternal +{ + QSqlDatabase db; + + QSqlQuery getHostInfoQuery; + QSqlQuery isKnownFileQuery; + + QSqlQuery getSettingsQuery; + QSqlQuery updateSettingQuery; + + QSqlQuery getAnimeQuery; + QSqlQuery getEpisodeQuery; + QSqlQuery getFileQuery; + + QSqlQuery setAnimeQuery; + QSqlQuery setEpisodeQuery; + QSqlQuery setFileQuery; + + QSqlQuery addLogQuery; + + QSqlQuery addTitleQuery; + QSqlQuery addAnimeQuery; + QSqlQuery addEpisodeQuery; + QSqlQuery addFileQuery; + QSqlQuery addFileEpisodeRelQuery; + + QSqlQuery addUnknownFileQuery; + + QSqlQuery addPendingRequestQuery; + QSqlQuery getRequestBatchQuery; + QSqlQuery clearRequestQuery; + + QThread *thread; +}; + +Database::Database(const QString &connectionName) : d(0) +{ + this->connectionName = connectionName; +} + +Database::~Database() +{ + if (d) + { + delete d; + d = 0; + QSqlDatabase::removeDatabase(connectionName); + emit disconnected(); + } +} + +bool Database::isConnected() const +{ + return d && d->db.isOpen(); +} + +void Database::setConnectionSettings(const DatabaseConnectionSettings &dbs) +{ + m_connectionSettings = dbs; +} + +DatabaseConnectionSettings Database::connectionSettings() const +{ + return m_connectionSettings; +} + +bool Database::transaction() +{ + Q_ASSERT_X(d->thread == QThread::currentThread(), "threads", "DB used from different thread"); + + bool success = d->db.transaction(); + if (success) + return true; + qDebug() << "Transaction Error:" << d->db.lastError(); + return false; +} + +bool Database::commit() +{ + Q_ASSERT_X(d->thread == QThread::currentThread(), "threads", "DB used from different thread"); + + bool success = d->db.commit(); + if (success) + return true; + qDebug() << "Commit Error:" << d->db.lastError(); + return false; +} + +bool Database::rollback() +{ + Q_ASSERT_X(d->thread == QThread::currentThread(), "threads", "DB used from different thread"); + + bool success = d->db.rollback(); + if (success) + return true; +qDebug() << "Commit Error:" << d->db.lastError(); + return false; +} + +HostInfo Database::getHostInfo(const QString &hostName) +{ + d->getHostInfoQuery.bindValue(":name", hostName); + + if (!exec(d->getHostInfoQuery)) + return HostInfo(); + + HostInfo hostInfo; + if (d->getHostInfoQuery.next()) + { + hostInfo.id = d->getHostInfoQuery.value(0).toInt(); + hostInfo.name = d->getHostInfoQuery.value(1).toString(); + hostInfo.isUdpHost = d->getHostInfoQuery.value(2).toBool(); + } + d->getHostInfoQuery.finish(); + + return hostInfo; +} + +QVariantMap Database::getConfig() +{ + if (!exec(d->getSettingsQuery)) + return QVariantMap(); + + QVariantMap settings; + while (d->getSettingsQuery.next()) + { + settings.insert(d->getSettingsQuery.value(0).toString(), d->getSettingsQuery.value(1)); + } + d->getSettingsQuery.finish(); + return settings; +} + +bool Database::setConfig(const QString &key, const QVariant &value) +{ + d->updateSettingQuery.bindValue(":key", key); + d->updateSettingQuery.bindValue(":value", value); + + return exec(d->updateSettingQuery); +} + +int Database::isKnownFile(const QByteArray &ed2k, qint64 size) +{ + d->isKnownFileQuery.bindValue(":ed2k", ed2k); + d->isKnownFileQuery.bindValue(":size", size); + + if (!exec(d->isKnownFileQuery)) + return 0; + + int fid = 0; + if (d->isKnownFileQuery.next()) + fid = d->isKnownFileQuery.value(0).toInt(); + d->isKnownFileQuery.finish(); + + return fid; +} + +bool Database::setFileLocation(int fid, int hostId, const QString &location) +{ + QSqlQuery q(d->db); + q.prepare("INSERT INTO file_location VALUES(:fid, :hostId, :path)"); + q.bindValue(":fid", fid); + q.bindValue(":hostId", hostId); + q.bindValue(":path", location); + + return exec(q); +} + +Anime Database::getAnime(int aid) +{ + Anime a; + + d->getAnimeQuery.bindValue(":aid", aid); + + if (!exec(d->getAnimeQuery)) + return a; + + if (!d->getAnimeQuery.next()) + { + d->getAnimeQuery.finish(); + return a; + } + + a.aid = d->getAnimeQuery.value(0).toInt(); + a.anidbUpdate = d->getAnimeQuery.value(1).toDateTime(); + a.entryUpdate = d->getAnimeQuery.value(2).toDateTime(); + a.myUpdate = d->getAnimeQuery.value(3).toDateTime(); + a.titleEnglish = d->getAnimeQuery.value(4).toString(); + a.titleRomaji = d->getAnimeQuery.value(5).toString(); + a.titleKanji = d->getAnimeQuery.value(6).toString(); + a.description = d->getAnimeQuery.value(7).toString(); + a.year = d->getAnimeQuery.value(8).toString(); + a.startDate = d->getAnimeQuery.value(9).toDateTime(); + a.endDate = d->getAnimeQuery.value(10).toDateTime(); + a.type = d->getAnimeQuery.value(11).toString(); + a.rating = d->getAnimeQuery.value(12).toDouble(); + a.votes = d->getAnimeQuery.value(13).toInt(); + a.tempRating = d->getAnimeQuery.value(14).toDouble(); + a.tempVotes = d->getAnimeQuery.value(15).toInt(); + a.myVote = d->getAnimeQuery.value(16).toDouble(); + a.myVoteDate = d->getAnimeQuery.value(17).toDateTime(); + a.myTempVote = d->getAnimeQuery.value(18).toDouble(); + a.myTempVoteDate = d->getAnimeQuery.value(19).toDateTime(); + + d->getAnimeQuery.finish(); + + return a; +} + +Episode Database::getEpisode(int eid) +{ + Episode e; + + d->getEpisodeQuery.bindValue(":eid", eid); + + if (!exec(d->getEpisodeQuery)) + return e; + + if (!d->getEpisodeQuery.next()) + { + d->getEpisodeQuery.finish(); + return e; + } + + e.eid = d->getEpisodeQuery.value(0).toInt(); + e.aid = d->getEpisodeQuery.value(1).toInt(); + e.anidbUpdate = d->getEpisodeQuery.value(2).toDateTime(); + e.entryUpdate = d->getEpisodeQuery.value(3).toDateTime(); + e.myUpdate = d->getEpisodeQuery.value(4).toDateTime(); + e.epno = d->getEpisodeQuery.value(5).toInt(); + e.titleEnglish = d->getEpisodeQuery.value(6).toString(); + e.titleRomaji = d->getEpisodeQuery.value(7).toString(); + e.titleKanji = d->getEpisodeQuery.value(8).toString(); + e.length = d->getEpisodeQuery.value(9).toInt(); + e.airdate = d->getEpisodeQuery.value(10).toDateTime(); + e.state = d->getEpisodeQuery.value(11).toInt(); + e.special = d->getEpisodeQuery.value(12).toBool(); + e.recap = d->getEpisodeQuery.value(13).toBool(); + e.opening = d->getEpisodeQuery.value(14).toBool(); + e.ending = d->getEpisodeQuery.value(15).toBool(); + e.rating = d->getEpisodeQuery.value(16).toDouble(); + e.votes = d->getEpisodeQuery.value(17).toInt(); + e.myVote = d->getEpisodeQuery.value(18).toDouble(); + e.myVoteDate = d->getEpisodeQuery.value(19).toDateTime(); + + d->getEpisodeQuery.finish(); + + return e; +} + +File Database::getFile(int fid) +{ + File f; + + d->getFileQuery.bindValue(":fid", fid); + + if (!exec(d->getFileQuery)) + return f; + + if (!d->getFileQuery.next()) + { + d->getFileQuery.finish(); + return f; + } + + f.fid = d->getFileQuery.value(0).toInt(); + f.eid = d->getFileQuery.value(1).toInt(); + f.aid = d->getFileQuery.value(2).toInt(); + f.gid = d->getFileQuery.value(3).toInt(); + f.anidbUpdate = d->getFileQuery.value(4).toDateTime(); + f.entryUpdate = d->getFileQuery.value(5).toDateTime(); + f.myUpdate = d->getFileQuery.value(6).toDateTime(); + f.ed2k = d->getFileQuery.value(7).toByteArray(); + f.size = d->getFileQuery.value(8).toLongLong(); + f.length = d->getFileQuery.value(9).toInt(); + f.extension = d->getFileQuery.value(10).toString(); + f.groupName = d->getFileQuery.value(11).toString(); + f.groupNameShort = d->getFileQuery.value(12).toString(); + f.crc = d->getFileQuery.value(13).toString(); + f.releaseDate = d->getFileQuery.value(14).toDateTime(); + f.version = d->getFileQuery.value(15).toInt(); + f.censored = d->getFileQuery.value(16).toBool(); + f.type = d->getFileQuery.value(17).toString(); + f.qualityId = d->getFileQuery.value(18).toInt(); + f.quality = d->getFileQuery.value(19).toString(); + f.resolution = d->getFileQuery.value(20).toString(); + f.videoCodec = d->getFileQuery.value(21).toString(); + f.audioCodec = d->getFileQuery.value(22).toString(); + f.audioLanguage = d->getFileQuery.value(23).toString(); + f.subtitleLanguage = d->getFileQuery.value(24).toString(); + f.aspectRatio = d->getFileQuery.value(25).toString(); + f.myWatched = d->getFileQuery.value(26).toDateTime(); + f.myState = d->getFileQuery.value(27).toInt(); + f.myFileState = d->getFileQuery.value(28).toInt(); + f.myStorage = d->getFileQuery.value(29).toString(); + f.mySource = d->getFileQuery.value(30).toString(); + f.myOther = d->getFileQuery.value(31).toString(); + + d->getFileQuery.finish(); + + return f; +} + +bool Database::setAnime(const Anime &anime) +{ + d->setAnimeQuery.bindValue(":aid", anime.aid); + d->setAnimeQuery.bindValue(":anidbUpdate", anime.anidbUpdate); + d->setAnimeQuery.bindValue(":entryUpdate", anime.entryUpdate); + d->setAnimeQuery.bindValue(":myUpdate", anime.myUpdate); + d->setAnimeQuery.bindValue(":titleEnglish", anime.titleEnglish); + d->setAnimeQuery.bindValue(":titleRomaji", anime.titleRomaji); + d->setAnimeQuery.bindValue(":titleKanji", anime.titleKanji); + d->setAnimeQuery.bindValue(":description", anime.description); + d->setAnimeQuery.bindValue(":year", anime.year); + d->setAnimeQuery.bindValue(":startDate", anime.startDate); + d->setAnimeQuery.bindValue(":endDate", anime.endDate); + d->setAnimeQuery.bindValue(":type", anime.type); + d->setAnimeQuery.bindValue(":rating", anime.rating); + d->setAnimeQuery.bindValue(":votes", anime.votes); + d->setAnimeQuery.bindValue(":tempRating", anime.tempRating); + d->setAnimeQuery.bindValue(":tempVotes", anime.tempVotes); + d->setAnimeQuery.bindValue(":myVote", anime.myVote); + d->setAnimeQuery.bindValue(":myVoteDate", anime.myVoteDate); + d->setAnimeQuery.bindValue(":myTempVote", anime.myTempVote); + d->setAnimeQuery.bindValue(":myTempVoteDate", anime.myTempVoteDate); + + return exec(d->setAnimeQuery); +} + +bool Database::setEpisode(const Episode &episode) +{ + d->setEpisodeQuery.bindValue(":eid", episode.eid); + d->setEpisodeQuery.bindValue(":aid", episode.aid); + d->setEpisodeQuery.bindValue(":anidbUpdate", episode.anidbUpdate); + d->setEpisodeQuery.bindValue(":entryUpdate", episode.entryUpdate); + d->setEpisodeQuery.bindValue(":myUpdate", episode.myUpdate); + d->setEpisodeQuery.bindValue(":epno", episode.epno); + d->setEpisodeQuery.bindValue(":titleEnglish", episode.titleEnglish); + d->setEpisodeQuery.bindValue(":titleRomaji", episode.titleRomaji); + d->setEpisodeQuery.bindValue(":titleKanji", episode.titleKanji); + d->setEpisodeQuery.bindValue(":length", episode.length); + d->setEpisodeQuery.bindValue(":airdate", episode.airdate); + d->setEpisodeQuery.bindValue(":state", episode.state); + d->setEpisodeQuery.bindValue(":special", episode.special); + d->setEpisodeQuery.bindValue(":recap", episode.recap); + d->setEpisodeQuery.bindValue(":opening", episode.opening); + d->setEpisodeQuery.bindValue(":ending", episode.ending); + d->setEpisodeQuery.bindValue(":rating", episode.rating); + d->setEpisodeQuery.bindValue(":votes", episode.votes); + d->setEpisodeQuery.bindValue(":myVote", episode.myVote); + d->setEpisodeQuery.bindValue(":myVoteDate", episode.myVoteDate); + + return exec(d->setEpisodeQuery); +} + +bool Database::setFile(const File &file) +{ + d->setFileQuery.bindValue(":fid", file.fid); + d->setFileQuery.bindValue(":eid", file.eid); + d->setFileQuery.bindValue(":aid", file.aid); + d->setFileQuery.bindValue(":gid", file.gid); + d->setFileQuery.bindValue(":anidbUpdate", file.anidbUpdate); + d->setFileQuery.bindValue(":entryUpdate", file.entryUpdate); + d->setFileQuery.bindValue(":myUpdate", file.myUpdate); + d->setFileQuery.bindValue(":ed2k", file.ed2k); + d->setFileQuery.bindValue(":size", file.size); + d->setFileQuery.bindValue(":length", file.length); + d->setFileQuery.bindValue(":extension", file.extension); + d->setFileQuery.bindValue(":groupName", file.groupName); + d->setFileQuery.bindValue(":groupNameShort", file.groupNameShort); + d->setFileQuery.bindValue(":crc", file.crc); + d->setFileQuery.bindValue(":releaseDate", file.releaseDate); + d->setFileQuery.bindValue(":version", file.version); + d->setFileQuery.bindValue(":censored", file.censored); + d->setFileQuery.bindValue(":type", file.type); + d->setFileQuery.bindValue(":qualityId", file.qualityId); + d->setFileQuery.bindValue(":quality", file.quality); + d->setFileQuery.bindValue(":resolution", file.resolution); + d->setFileQuery.bindValue(":videoCodec", file.videoCodec); + d->setFileQuery.bindValue(":audioCodec", file.audioCodec); + d->setFileQuery.bindValue(":audioLanguage", file.audioLanguage); + d->setFileQuery.bindValue(":subtitleLanguage", file.subtitleLanguage); + d->setFileQuery.bindValue(":aspectRatio", file.aspectRatio); + d->setFileQuery.bindValue(":myWatched", file.myWatched); + d->setFileQuery.bindValue(":myState", file.myState); + d->setFileQuery.bindValue(":myFileState", file.myFileState); + d->setFileQuery.bindValue(":myStorage", file.myStorage); + d->setFileQuery.bindValue(":mySource", file.mySource); + d->setFileQuery.bindValue(":myOther", file.myOther); + + return exec(d->setFileQuery); +} + +bool Database::addTitle(const AnimeTitle &title) +{ + d->addTitleQuery.bindValue(":aid", title.aid); + d->addTitleQuery.bindValue(":type", int(title.type)); + d->addTitleQuery.bindValue(":language", title.language); + d->addTitleQuery.bindValue(":title", title.title); + + return exec(d->addTitleQuery); +} + +bool Database::addAnime(const Anime &anime) +{ + d->addAnimeQuery.bindValue(":aid", anime.aid); + d->addAnimeQuery.bindValue(":anidbUpdate", anime.anidbUpdate); + d->addAnimeQuery.bindValue(":entryUpdate", anime.entryUpdate); + d->addAnimeQuery.bindValue(":myUpdate", anime.myUpdate); + d->addAnimeQuery.bindValue(":titleEnglish", anime.titleEnglish); + d->addAnimeQuery.bindValue(":titleRomaji", anime.titleRomaji); + d->addAnimeQuery.bindValue(":titleKanji", anime.titleKanji); + d->addAnimeQuery.bindValue(":description", anime.description); + d->addAnimeQuery.bindValue(":year", anime.year); + d->addAnimeQuery.bindValue(":startDate", anime.startDate); + d->addAnimeQuery.bindValue(":endDate", anime.endDate); + d->addAnimeQuery.bindValue(":type", anime.type); + d->addAnimeQuery.bindValue(":rating", anime.rating); + d->addAnimeQuery.bindValue(":votes", anime.votes); + d->addAnimeQuery.bindValue(":tempRating", anime.tempRating); + d->addAnimeQuery.bindValue(":tempVotes", anime.tempVotes); + d->addAnimeQuery.bindValue(":myVote", anime.myVote); + d->addAnimeQuery.bindValue(":myVoteDate", anime.myVoteDate); + d->addAnimeQuery.bindValue(":myTempVote", anime.myTempVote); + d->addAnimeQuery.bindValue(":myTempVoteDate", anime.myTempVoteDate); + + return exec(d->addAnimeQuery); +} + +bool Database::addEpisode(const Episode &episode) +{ + d->addEpisodeQuery.bindValue(":eid", episode.eid); + d->addEpisodeQuery.bindValue(":aid", episode.aid); + d->addEpisodeQuery.bindValue(":anidbUpdate", episode.anidbUpdate); + d->addEpisodeQuery.bindValue(":entryUpdate", episode.entryUpdate); + d->addEpisodeQuery.bindValue(":myUpdate", episode.myUpdate); + d->addEpisodeQuery.bindValue(":epno", episode.epno); + d->addEpisodeQuery.bindValue(":titleEnglish", episode.titleEnglish); + d->addEpisodeQuery.bindValue(":titleRomaji", episode.titleRomaji); + d->addEpisodeQuery.bindValue(":titleKanji", episode.titleKanji); + d->addEpisodeQuery.bindValue(":length", episode.length); + d->addEpisodeQuery.bindValue(":airdate", episode.airdate); + d->addEpisodeQuery.bindValue(":state", episode.state); + d->addEpisodeQuery.bindValue(":special", episode.special); + d->addEpisodeQuery.bindValue(":recap", episode.recap); + d->addEpisodeQuery.bindValue(":opening", episode.opening); + d->addEpisodeQuery.bindValue(":ending", episode.ending); + d->addEpisodeQuery.bindValue(":rating", episode.rating); + d->addEpisodeQuery.bindValue(":votes", episode.votes); + d->addEpisodeQuery.bindValue(":myVote", episode.myVote); + d->addEpisodeQuery.bindValue(":myVoteDate", episode.myVoteDate); + + return exec(d->addEpisodeQuery); +} + +bool Database::addFile(const File &file) +{ + d->addFileQuery.bindValue(":fid", file.fid); + d->addFileQuery.bindValue(":eid", file.eid); + d->addFileQuery.bindValue(":aid", file.aid); + d->addFileQuery.bindValue(":gid", file.gid); + d->addFileQuery.bindValue(":anidbUpdate", file.anidbUpdate); + d->addFileQuery.bindValue(":entryUpdate", file.entryUpdate); + d->addFileQuery.bindValue(":myUpdate", file.myUpdate); + d->addFileQuery.bindValue(":ed2k", file.ed2k); + d->addFileQuery.bindValue(":size", file.size); + d->addFileQuery.bindValue(":length", file.length); + d->addFileQuery.bindValue(":extension", file.extension); + d->addFileQuery.bindValue(":groupName", file.groupName); + d->addFileQuery.bindValue(":groupNameShort", file.groupNameShort); + d->addFileQuery.bindValue(":crc", file.crc); + d->addFileQuery.bindValue(":releaseDate", file.releaseDate); + d->addFileQuery.bindValue(":version", file.version); + d->addFileQuery.bindValue(":censored", file.censored); + d->addFileQuery.bindValue(":type", file.type); + d->addFileQuery.bindValue(":qualityId", file.qualityId); + d->addFileQuery.bindValue(":quality", file.quality); + d->addFileQuery.bindValue(":resolution", file.resolution); + d->addFileQuery.bindValue(":videoCodec", file.videoCodec); + d->addFileQuery.bindValue(":audioCodec", file.audioCodec); + d->addFileQuery.bindValue(":audioLanguage", file.audioLanguage); + d->addFileQuery.bindValue(":subtitleLanguage", file.subtitleLanguage); + d->addFileQuery.bindValue(":aspectRatio", file.aspectRatio); + d->addFileQuery.bindValue(":myWatched", file.myWatched); + d->addFileQuery.bindValue(":myState", file.myState); + d->addFileQuery.bindValue(":myFileState", file.myFileState); + d->addFileQuery.bindValue(":myStorage", file.myStorage); + d->addFileQuery.bindValue(":mySource", file.mySource); + d->addFileQuery.bindValue(":myOther", file.myOther); + + return exec(d->addFileQuery); +} + +bool Database::addFileEpisodeRel(const FileEpisodeRel &fileEpisodeRel) +{ + d->addFileEpisodeRelQuery.bindValue(":fid", fileEpisodeRel.fid); + d->addFileEpisodeRelQuery.bindValue(":eid", fileEpisodeRel.eid); + d->addFileEpisodeRelQuery.bindValue(":startPercent", fileEpisodeRel.startPercent); + d->addFileEpisodeRelQuery.bindValue(":endPercent", fileEpisodeRel.endPercent); + + return exec(d->addFileEpisodeRelQuery); +} + +bool Database::addUnknownFile(const UnknownFile &file) +{ + d->addUnknownFileQuery.bindValue(":ed2k", file.ed2k); + d->addUnknownFileQuery.bindValue(":size", file.size); + d->addUnknownFileQuery.bindValue(":hostId", file.hostId); + d->addUnknownFileQuery.bindValue(":path", file.path); + + return exec(d->addUnknownFileQuery); +} + +bool Database::addRequest(const PendingRequest &request) +{ + d->addPendingRequestQuery.bindValue(":aid", request.aid); + d->addPendingRequestQuery.bindValue(":eid", request.eid); + d->addPendingRequestQuery.bindValue(":fid", request.fid); + d->addPendingRequestQuery.bindValue(":ed2k", request.ed2k.isNull() ? QByteArray("") : request.ed2k); + d->addPendingRequestQuery.bindValue(":size", request.size); + + return exec(d->addPendingRequestQuery); +} + +QList Database::getRequestBatch(int limit) +{ + d->getRequestBatchQuery.bindValue(":limit", limit); + + QList ret; + + if (!exec(d->getRequestBatchQuery)) + return ret; + + while (d->getRequestBatchQuery.next()) + { + PendingRequest request; + request.aid = d->getRequestBatchQuery.value(0).toInt(); + request.eid = d->getRequestBatchQuery.value(1).toInt(); + request.fid = d->getRequestBatchQuery.value(2).toInt(); + request.ed2k = d->getRequestBatchQuery.value(3).toByteArray(); + request.size = d->getRequestBatchQuery.value(4).toInt(); + ret << request; + } + + d->getRequestBatchQuery.finish(); + + return ret; +} + +bool Database::clearRequest(const PendingRequest &request) +{ + d->clearRequestQuery.bindValue(":aid", request.aid); + d->clearRequestQuery.bindValue(":eid", request.eid); + d->clearRequestQuery.bindValue(":fid", request.fid); + d->clearRequestQuery.bindValue(":ed2k", request.ed2k.isNull() ? QByteArray("") : request.ed2k); + d->clearRequestQuery.bindValue(":size", request.size); + + bool ret = exec(d->clearRequestQuery); + qDebug() << "AFFECTED" << d->clearRequestQuery.numRowsAffected(); + + return ret; +} + +bool Database::truncateTitleData() +{ + return exec("TRUNCATE TABLE anime_title"); +} + +bool Database::truncateMyListData() +{ + return exec("TRUNCATE TABLE anime, episode, file, " + "file_episode_rel"); +} + +bool Database::truncateDatabase() +{ + return exec("TRUNCATE TABLE anime, anime_title, episode, file, " + "file_episode_rel, file_location, unknown_file"); +} + +bool Database::log(const QString &message, int type) +{ + d->addLogQuery.bindValue(":type", type); + d->addLogQuery.bindValue(":log", message); + + qDebug() << "LOG:" << message << "; TYPE:" << type; + + return exec(d->addLogQuery); +} + +QSqlDatabase Database::connection() const +{ + return d->db; +} + +bool Database::connect() +{ + if (!d) + { + d = new DatabaseInternal(); + d->db = QSqlDatabase::addDatabase("QPSQL", connectionName); + } + else if (d->db.isOpen()) + { + qDebug() << "Already Connected"; + return true; + } + + d->db.setHostName(m_connectionSettings.host); + if (m_connectionSettings.port) + d->db.setPort(m_connectionSettings.port); + d->db.setUserName(m_connectionSettings.user); + d->db.setPassword(m_connectionSettings.pass); + d->db.setDatabaseName(m_connectionSettings.database); + d->thread = QThread::currentThread(); + + bool success = d->db.open(); + if (!success) + { + qWarning() << "Failed opening database connection." << d->db.lastError(); + return success; + } + + QObject::connect(d->db.driver(), SIGNAL(notification(QString)), this, SLOT(handleNotification(QString))); + prepareQueries(); + emit connected(); + + return success; +} + +void Database::disconnect() +{ + if (!d) + return; + + if (!d->db.isOpen()) + { + qDebug() << "Already Connected"; + return; + } + d->db.close(); + emit disconnected(); +} + +void Database::prepareQueries() +{ + d->getHostInfoQuery = QSqlQuery(d->db); + d->getHostInfoQuery.prepare("SELECT host_id, name, is_udp_host FROM host" + "WHERE name = :name"); + d->isKnownFileQuery = QSqlQuery(d->db); + d->isKnownFileQuery.prepare("SELECT fid FROM file WHERE ed2k = :ed2k AND size = :size"); + + d->getSettingsQuery = QSqlQuery(d->db); + d->getSettingsQuery.prepare("SELECT key, value FROM config"); + + d->updateSettingQuery = QSqlQuery(d->db); + d->updateSettingQuery.prepare("UPDATE config SET value = :value WHERE key = :key"); + + d->getAnimeQuery = QSqlQuery(d->db); + d->getAnimeQuery.prepare("SELECT aid, anidb_update, entry_update, my_update, title_english, " + "title_romaji, title_kanji, description, year, start_date, end_date, " + "type, rating, votes, temp_rating, temp_votes, my_vote, my_vote_date, " + "my_temp_vote, my_temp_vote_date " + "FROM anime " + "WHERE aid = :aid"); + + d->getEpisodeQuery = QSqlQuery(d->db); + d->getEpisodeQuery.prepare("SELECT eid, aid, anidb_update, entry_update, my_update, epno, " + "title_english, title_romaji, title_kanji, length, airdate, state, " + "special, recap, opening, ending, rating, votes, my_vote, my_vote_date " + "FROM episode " + "WHERE eid = :eid"); + + d->getFileQuery = QSqlQuery(d->db); + d->getFileQuery.prepare("SELECT fid, eid, aid, gid, anidb_update, entry_update, my_update, " + "ed2k, size, length, extension, group_name, group_name_short, crc, " + "release_date, version, censored, type, quality_id, quality, resolution, " + "video_codec, audio_codec, audio_language, subtitle_language, aspect_ratio, " + "my_watched, my_state, my_file_state, my_storage, my_source, my_other " + "FROM file " + "WHERE fid = :fid"); + + d->setAnimeQuery = QSqlQuery(d->db); + d->setAnimeQuery.prepare("UPDATE anime SET " + "anidb_update = :anidbUpdate, entry_update = :entryUpdate, " + "my_update = :myUpdate, title_english = :titleEnglish, " + "title_romaji = :titleRomaji, title_kanji = :titleKanji, " + "description = :description, year = :year, start_date = :startDate, " + "end_date = :endDate, type = :type, rating = :rating, votes = :votes, " + "temp_rating = :tempRating, temp_votes = :tempVotes, my_vote = :myVote, " + "my_vote_date = :myVoteDate, my_temp_vote = :myTempVote, " + "my_temp_vote_date = :myTempVoteDate " + "WHERE aid = :aid"); + + d->setEpisodeQuery = QSqlQuery(d->db); + d->setEpisodeQuery.prepare("UPDATE episode SET " + "aid = :aid, anidb_update = :anidbUpdate, entry_update = :entryUpdate, " + "my_update = :myUpdate, epno = :epno, title_english = :titleEnglish, " + "title_romaji = :titleRomaji, title_kanji = :titleKanji, length = :length, " + "airdate = :airdate, state = :state, special = :special, recap = :recap, " + "opening = :opening, ending = :ending, rating = :rating, votes = :votes, " + "my_vote = :myVote, my_vote_date = :myVoteDate " + "WHERE eid = :eid"); + + d->setFileQuery = QSqlQuery(d->db); + d->setFileQuery.prepare("UPDATE file SET " + "eid = :eid, aid = :aid, gid = :gid, anidb_update = :anidbUpdate, " + "entry_update = :entryUpdate, my_update = :myUpdate, " + "ed2k = :ed2k, size = :size, length = :length, extension = :extension, " + "group_name = :groupName, group_name_short = :groupNameShort, crc = :crc, " + "release_date = :releaseDate, version = :version, censored = :censored, " + "type = :type, quality_id = :qualityId, quality = :quality, " + "resolution = :resolution, video_codec = :videoCodec, " + "audio_codec = :audioCodec, audio_language = :audioLanguage, " + "subtitle_language = :subtitleLanguage, aspect_ratio = :aspectRatio, " + "my_watched = :myWatched, my_state = :myState, my_file_state = :myFileState, " + "my_storage = :myStorage, my_source = :mySource, my_other = :myOther " + "WHERE fid = :fid"); + + d->addLogQuery = QSqlQuery(d->db); + d->addLogQuery.prepare("INSERT INTO log (type, log) VALUES (:type, :log)"); + + d->addTitleQuery = QSqlQuery(d->db); + d->addTitleQuery.prepare("INSERT INTO anime_title VALUES(:aid, :type, :language, :title)"); + + d->addAnimeQuery = QSqlQuery(d->db); + d->addAnimeQuery.prepare("INSERT INTO anime VALUES(:aid, :anidbUpdate, :entryUpdate, :myUpdate, :titleEnglish, " + ":titleRomaji, :titleKanji, :description, :year, :startDate, :endDate, :type, " + ":rating, :votes, :tempRating, :tempVotes, :myVote, :myVoteDate, " + ":myTempVote, :myTempVoteDate)"); + d->addEpisodeQuery = QSqlQuery(d->db); + d->addEpisodeQuery.prepare("INSERT INTO episode VALUES(:eid, :aid, :anidbUpdate, :entryUpdate, :myUpdate, :epno, " + ":titleEnglish, :titleRomaji, :titleKanji, :length, :airdate, " + ":state, :special, :recap, :openineg, :ending, :rating, " + ":votes, :myVote, :myVoteDate)"); + d->addFileQuery = QSqlQuery(d->db); + d->addFileQuery.prepare("INSERT INTO file VALUES(:fid, :eid, :aid, :gid, :anidbUpdate, :entryUpdate, :myUpdate, " + ":ed2k, :size, :length, :extension, :groupName, :groupNameShort, " + ":crc, :releaseDate, :version, :censored, :type, :qualityId, " + ":quality, :resolution, :vidoeCodec, :audioCodec, :audioLanguage, " + ":subtitleLanguage, :aspectRatio, :myWatched, :myState, " + ":myFileState, :myStorage, :mySource, :myOther)"); + d->addFileEpisodeRelQuery = QSqlQuery(d->db); + d->addFileEpisodeRelQuery.prepare("INSERT INTO file_episode_rel VALUES(:fid, :eid, " + ":startPercentage, :endPercentage)"); + + d->addUnknownFileQuery = QSqlQuery(d->db); + d->addUnknownFileQuery.prepare("INSERT INTO unknown_file VALUES(:ed2k, :size, :hostId, :path)"); + + d->addPendingRequestQuery = QSqlQuery(d->db); + d->addPendingRequestQuery.prepare("INSERT INTO pending_request VALUES(:aid, :eid, :fid, :ed2k, :size, DEFAULT, DEFAULT, DEFAULT)"); + + d->getRequestBatchQuery = QSqlQuery(d->db); + d->getRequestBatchQuery.prepare("UPDATE pending_request SET start = NOW() " + "WHERE (aid, eid, fid, ed2k, size) IN (SELECT aid, eid, fid, ed2k, size FROM pending_request " + "WHERE start IS NULL " + "ORDER BY priority DESC, added ASC " + "LIMIT :limit) " + "RETURNING aid, eid, fid, ed2k, size"); + + d->clearRequestQuery = QSqlQuery(d->db); + d->clearRequestQuery.prepare("DELETE FROM pending_request WHERE aid = :aid AND eid = :eid AND fid = :fid AND ed2k = :ed2k AND size = :size"); + + d->db.driver()->subscribeToNotification("new_pending_request"); +} + +bool Database::exec(QSqlQuery &query) +{ + Q_ASSERT_X(d->thread == QThread::currentThread(), "threads", "DB used from different thread"); + bool result = query.exec(); + if (result) + return result; + + qDebug() << "SQL error: " << query.lastError().type(); + qDebug() << "Message: " << query.lastError().text(); + qDebug() << "DB Message:" << query.lastError().databaseText(); + qDebug() << "Query: " << query.executedQuery(); + + if (query.lastError().type() == QSqlError::ConnectionError) + disconnect(); + return result; +} + +bool Database::exec(const QString &sql) +{ + Q_ASSERT_X(d->thread == QThread::currentThread(), "threads", "DB used from different thread"); + bool result = false; + bool connectionError = false; + + { + QSqlQuery query(d->db); + result = query.exec(sql); + + if (result) + return result; + + qDebug() << "SQL error: " << query.lastError().type(); + qDebug() << "Message: " << query.lastError().text(); + qDebug() << "DB Message:" << query.lastError().databaseText(); + qDebug() << "Query: " << query.executedQuery(); + + connectionError = query.lastError().type() == QSqlError::ConnectionError; + } + + if (connectionError) + disconnect(); + + return result; +} + +void Database::handleNotification(const QString ¬ification) +{ + qDebug() << "Recieved notification" << notification; + if (notification == "new_pending_request") + { + emit newPendingRequest(); + } +} + +} // namespace LocalMyList diff --git a/localmylist/database.h b/localmylist/database.h new file mode 100644 index 0000000..015d81f --- /dev/null +++ b/localmylist/database.h @@ -0,0 +1,249 @@ +#ifndef DATABASE_H +#define DATABASE_H + +#include "localmylist_global.h" +#include +#include +#include +#include + +namespace LocalMyList { + +struct LOCALMYLISTSHARED_EXPORT AnimeTitle +{ + enum TitleType { + PrimaryTitle = 1, + Synonym = 2, + ShortTitle = 3, + OfficialTitle = 4 + }; + + int aid; + TitleType type; + QString language; + QString title; + + AnimeTitle(int aid = 0, TitleType type = PrimaryTitle, const QString &language = QString(), const QString &title = QString()); +}; + +struct LOCALMYLISTSHARED_EXPORT Anime +{ + int aid; + QDateTime anidbUpdate; + QDateTime entryUpdate; + QDateTime myUpdate; + QString titleEnglish; + QString titleRomaji; + QString titleKanji; + QString description; + QString year; + QDateTime startDate; + QDateTime endDate; + QString type; + double rating; + int votes; + double tempRating; + int tempVotes; + double myVote; + QDateTime myVoteDate; + double myTempVote; + QDateTime myTempVoteDate; + + Anime(); +}; + +struct LOCALMYLISTSHARED_EXPORT Episode +{ + int eid; + int aid; + QDateTime anidbUpdate; + QDateTime entryUpdate; + QDateTime myUpdate; + int epno; + QString titleEnglish; + QString titleRomaji; + QString titleKanji; + int length; + QDateTime airdate; + int state; + bool special; + bool recap; + bool opening; + bool ending; + double rating; + int votes; + double myVote; + QDateTime myVoteDate; + + Episode(); +}; + +struct LOCALMYLISTSHARED_EXPORT File +{ + int fid; + int eid; + int aid; + int gid; + QDateTime anidbUpdate; + QDateTime entryUpdate; + QDateTime myUpdate; + QByteArray ed2k; + qint64 size; + int length; + QString extension; + QString groupName; + QString groupNameShort; + QString crc; + QDateTime releaseDate; + int version; + bool censored; + QString type; + int qualityId; + QString quality; + QString resolution; + QString videoCodec; + QString audioCodec; + QString audioLanguage; + QString subtitleLanguage; + QString aspectRatio; + QDateTime myWatched; + int myState; + int myFileState; + QString myStorage; + QString mySource; + QString myOther; + + File(); +}; + +struct LOCALMYLISTSHARED_EXPORT UnknownFile +{ + QByteArray ed2k; + qint64 size; + int hostId; + QString path; + + UnknownFile(); +}; + +struct LOCALMYLISTSHARED_EXPORT FileEpisodeRel +{ + int fid; + int eid; + int startPercent; + int endPercent; + + FileEpisodeRel(); +}; + +struct LOCALMYLISTSHARED_EXPORT PendingRequest +{ + int aid; + int eid; + int fid; + QByteArray ed2k; + qint64 size; + + PendingRequest(); +}; + +struct LOCALMYLISTSHARED_EXPORT HostInfo +{ + int id; + QString name; + bool isUdpHost; + + HostInfo(); +}; + +struct LOCALMYLISTSHARED_EXPORT DatabaseConnectionSettings +{ + QString host; + quint16 port; + QString user; + QString pass; + QString database; + + DatabaseConnectionSettings(); +}; + +struct DatabaseInternal; + +class LOCALMYLISTSHARED_EXPORT Database : public QObject +{ + Q_OBJECT +public: + Database(const QString &connectionName = "default"); + ~Database(); + + bool isConnected() const; + + void setConnectionSettings(const DatabaseConnectionSettings &dbs); + DatabaseConnectionSettings connectionSettings() const; + + bool transaction(); + bool commit(); + bool rollback(); + + HostInfo getHostInfo(const QString &hostName); + + QVariantMap getConfig(); + bool setConfig(const QString &key, const QVariant &value); + + int isKnownFile(const QByteArray &ed2k, qint64 size); + bool setFileLocation(int fid, int hostId, const QString &location); + + Anime getAnime(int aid); + Episode getEpisode(int eid); + File getFile(int fid); + + bool setAnime(const Anime &anime); + bool setEpisode(const Episode &episode); + bool setFile(const File &file); + + bool addTitle(const AnimeTitle &title); + bool addAnime(const Anime &anime); + bool addEpisode(const Episode &episode); + bool addFile(const File &file); + bool addFileEpisodeRel(const FileEpisodeRel &fileEpisodeRel); + bool addUnknownFile(const UnknownFile &file); + + bool addRequest(const PendingRequest &request); + QList getRequestBatch(int limit = 10); + bool clearRequest(const PendingRequest &request); + + bool truncateTitleData(); + bool truncateMyListData(); + bool truncateDatabase(); + + bool log(const QString &message, int type = 1); + + QSqlDatabase connection() const; + +public slots: + bool connect(); + void disconnect(); + +signals: + void connected(); + void disconnected(); + + void newPendingRequest(); + +private slots: + void handleNotification(const QString ¬ification); + +private: + bool exec(QSqlQuery &query); + bool exec(const QString &sql); + void prepareQueries(); + + DatabaseInternal *d; + DatabaseConnectionSettings m_connectionSettings; + + QString connectionName; +}; + +} // namespace LocalMyList + +#endif // DATABASE_H diff --git a/localmylist/directoryscantask.cpp b/localmylist/directoryscantask.cpp new file mode 100644 index 0000000..c9be688 --- /dev/null +++ b/localmylist/directoryscantask.cpp @@ -0,0 +1,92 @@ +#include "directoryscantask.h" + +#include "mylist.h" +#include "addfiletask.h" + +#include + +namespace LocalMyList { + +DirectoryScanTask::DirectoryScanTask(Database *db, QObject *parent) : + AbstractTask(db, parent) +{ +} + +QDir DirectoryScanTask::directory() const +{ + return m_directory; +} + +QString DirectoryScanTask::taskSubject() const +{ + return m_directory.canonicalPath(); +} + +bool DirectoryScanTask::canUseThreads() const +{ + return true; +} + + +void DirectoryScanTask::setDirectory(QDir directory) +{ + m_directory = directory; +} + +void DirectoryScanTask::start() +{ + stack.push(qMakePair(m_directory, 0)); + emit nextWorkUnit(); +} + +void DirectoryScanTask::workUnit() +{ + qDebug() << "Starting work unit"; + int operations = 0; + + while (!stack.isEmpty() && operations < OPERATIONS_PER_UNIT) + { + QDir currentDir = stack.top().first; + int startIndex = stack.top().second; + stack.pop(); + + QFileInfoList entries = currentDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files); + qDebug() << entries.count() << "entries in" << currentDir.absolutePath(); + int i; + for (i = startIndex; i < entries.count(); ++i) + { + if (operations >= OPERATIONS_PER_UNIT) + { + stack.push(qMakePair(currentDir, i)); + break; + } + ++operations; + + const QFileInfo &entry = entries[i]; + + if (entry.isDir()) + { + qDebug() << "Queueing DIR " << entry.absoluteFilePath(); + stack.push(qMakePair(QDir(entry.absoluteFilePath()), 0)); + } + else if (entry.isFile()) + { +// qDebug() << "Queueing FILE " << entry.fileName(); + QMetaObject::invokeMethod(MyList::instance(), "addFile", Qt::QueuedConnection, Q_ARG(QFileInfo, entry)); + } + } + + if (i < entries.count()) + { + qDebug() << "ReQueueing DIR " << currentDir.absolutePath() << "i = " << i; + qMakePair(currentDir, i); + } + } + + if (stack.isEmpty()) + emit finished(); + else + emit nextWorkUnit(); +} + +} // namespace LocalMyList diff --git a/localmylist/directoryscantask.h b/localmylist/directoryscantask.h new file mode 100644 index 0000000..fe9040e --- /dev/null +++ b/localmylist/directoryscantask.h @@ -0,0 +1,40 @@ +#ifndef DIRECTORYSCANTASK_H +#define DIRECTORYSCANTASK_H + +#include "abstracttask.h" + +#include +#include +#include + +namespace LocalMyList { + +class DirectoryScanTask : public AbstractTask +{ + Q_OBJECT + Q_PROPERTY(QDir directory READ directory WRITE setDirectory) + +public: + explicit DirectoryScanTask(Database *db = 0, QObject *parent = 0); + + QDir directory() const; + + QString taskSubject() const; + bool canUseThreads() const; + +public slots: + void start(); + + void setDirectory(QDir directory); + +protected: + void workUnit(); + + QDir m_directory; + + QStack > stack; +}; + +} // namespace LocalMyList + +#endif // DIRECTORYSCANTASK_H diff --git a/localmylist/include/LocalMyList/Database b/localmylist/include/LocalMyList/Database new file mode 100644 index 0000000..ff7ec1b --- /dev/null +++ b/localmylist/include/LocalMyList/Database @@ -0,0 +1 @@ +#include "" \ No newline at end of file diff --git a/localmylist/include/LocalMyList/LocalMyList b/localmylist/include/LocalMyList/LocalMyList new file mode 100644 index 0000000..ff7ec1b --- /dev/null +++ b/localmylist/include/LocalMyList/LocalMyList @@ -0,0 +1 @@ +#include "" \ No newline at end of file diff --git a/localmylist/localmylist.pro b/localmylist/localmylist.pro new file mode 100644 index 0000000..c4369fe --- /dev/null +++ b/localmylist/localmylist.pro @@ -0,0 +1,44 @@ +QT += network sql xml xmlpatterns +QT -= gui + +TARGET = localmylist +DESTDIR = ../build +TEMPLATE = lib + +DEFINES += LOCALMYLIST_LIBRARY + +SOURCES += \ + parser/animetitledatparser.cpp \ + database.cpp \ + parser/mylistexportxmlparser.cpp \ + workthread.cpp \ + addfiletask.cpp \ + abstracttask.cpp \ + directoryscantask.cpp \ + requesthandler.cpp \ + mylist.cpp \ + animetitleparsetask.cpp \ + mylistexportparsetask.cpp \ + settings.cpp + +HEADERS +=\ + localmylist_global.h \ + parser/animetitledatparser.h \ + database.h \ + parser/mylistexportxmlparser.h \ + workthread.h \ + addfiletask.h \ + abstracttask.h \ + directoryscantask.h \ + requesthandler.h \ + mylist.h \ + animetitleparsetask.h \ + mylistexportparsetask.h \ + settings.h + +unix { + target.path = /usr/lib + INSTALLS += target +} + +LIBS += -lanidbudpclient diff --git a/localmylist/localmylist_global.h b/localmylist/localmylist_global.h new file mode 100644 index 0000000..4f5754a --- /dev/null +++ b/localmylist/localmylist_global.h @@ -0,0 +1,15 @@ +#ifndef LOCALMYLIST_GLOBAL_H +#define LOCALMYLIST_GLOBAL_H + +#include + +#if defined(LOCALMYLIST_LIBRARY) +# define LOCALMYLISTSHARED_EXPORT Q_DECL_EXPORT +#else +# define LOCALMYLISTSHARED_EXPORT Q_DECL_IMPORT +#endif + +static const char *organizationName = "APTX"; +static const char *libraryName = "localmylist"; + +#endif // LOCALMYLIST_GLOBAL_H diff --git a/localmylist/mylist.cpp b/localmylist/mylist.cpp new file mode 100644 index 0000000..27fb181 --- /dev/null +++ b/localmylist/mylist.cpp @@ -0,0 +1,215 @@ +#include "mylist.h" + +#include +#include +#include + +#include "database.h" +#include "abstracttask.h" +#include "directoryscantask.h" +#include "addfiletask.h" +#include "animetitleparsetask.h" +#include "mylistexportparsetask.h" +#include "workthread.h" +#include "requesthandler.h" +#include + +namespace LocalMyList { + +MyList::MyList() +{ + init(); + loadLocalSettings(); + + m_requestHandler = 0; + + db = new Database("main"); + db->setConnectionSettings(dbs); + db->connect(); + m_settings = new Settings(db, this); + workThread = new WorkThread("workThread", dbs, this); + workThread->start(); + + setupUdpClient(); + setupRequestHandler(); +} + +MyList::~MyList() +{ + delete db; + delete workThread; +} + +QString MyList::hostName() const +{ + return hostInfo.name; +} + +int MyList::hostId() const +{ + return hostInfo.id; +} + +bool MyList::isUdpHost() const +{ + return hostInfo.isUdpHost; +} + +void MyList::setHostName(QString name) +{ + hostInfo.name = name; +} + +Database *MyList::database() const +{ + return db; +} + +Settings *MyList::settings() const +{ + return m_settings; +} + + +// ------- + +void MyList::setupUdpClient() +{ + using namespace ::AniDBUdpClient; + Client::instance()->setHost(m_settings->get("udpClientHost").toString()); + Client::instance()->setHostPort(m_settings->get("udpClientHostPort").toUInt()); + Client::instance()->setLocalPort(m_settings->get("udpClientLocalPort").toUInt()); + Client::instance()->setUser(m_settings->get("udpClientUser").toString()); + Client::instance()->setPass(m_settings->get("udpClientPass").toString()); + Client::instance()->setEncryptionEnabled(m_settings->get("udpClientEncryptionEnabled").toBool()); + Client::instance()->setApiKey(m_settings->get("udpClientApiKey").toString()); +} + +void MyList::setupRequestHandler() +{ + if(m_requestHandler) + return; + + m_requestHandler = new RequestHandler(db, this); + connect(db, SIGNAL(newPendingRequest()), m_requestHandler, SLOT(handleRequests())); +} + +void MyList::loadLocalSettings() +{ + QSettings s(QSettings::IniFormat, QSettings::UserScope, organizationName, libraryName); + s.beginGroup("general"); + hostInfo.name = s.value("hostName", QHostInfo::localHostName().toLower()).toString(); + s.endGroup(); + s.beginGroup("database"); + dbs.host = s.value("host", "localhost").toString(); + dbs.port = quint16(s.value("port", 0).toUInt()); + dbs.user = s.value("user", "localmylist").toString(); + dbs.pass = s.value("pass", "localmylist").toString(); + dbs.database = s.value("database", "localmylist").toString(); + s.endGroup(); +} + +void MyList::saveLocalSettings() +{ + QSettings s(QSettings::IniFormat, QSettings::UserScope, organizationName, libraryName); + s.beginGroup("general"); + s.setValue("hostName", hostInfo.name); + s.endGroup(); + s.beginGroup("database"); + s.setValue("host", dbs.host); + s.setValue("port", dbs.port); + s.setValue("user", dbs.user); + s.setValue("pass", dbs.pass); + s.setValue("database", dbs.database); + s.endGroup(); +} + +AbstractTask *MyList::addFile(const QFileInfo &file) +{ + AddFileTask *task = new AddFileTask(); + task->setFile(file); + executeTask(task); + return task; +} + +AbstractTask *MyList::addDirectory(const QDir &directory) +{ + DirectoryScanTask *task = new DirectoryScanTask(); + task->setDirectory(directory); + executeTask(task); + return task; +} + +AbstractTask *MyList::importTitles(const QFileInfo &file) +{ + AnimeTitleParseTask *task = new AnimeTitleParseTask(); + task->setFile(file); + executeTask(task); + return task; +} + +AbstractTask *MyList::importMyList(const QFileInfo &file) +{ + MyListExportParseTask *task = new MyListExportParseTask(); + task->setFile(file); + executeTask(task); + return task; +} + +void MyList::executeTask(AbstractTask *task) +{ + if (task->canUseThreads()) + { + task->setDatabase(workThread->database()); + task->moveToThread(workThread); + } + else + { + if (task->thread() != thread()) + task->moveToThread(thread()); + + task->setDatabase(database()); + } + + connect(task, SIGNAL(finished()), this, SLOT(taskFinished()), Qt::QueuedConnection); + tasks.insert(task); + db->log(tr("Starting task %1 on %2").arg(task->taskName(), task->taskSubject())); + + QMetaObject::invokeMethod(task, "start", Qt::QueuedConnection); +} + +void MyList::taskFinished() +{ + AbstractTask *task = qobject_cast(sender()); + Q_ASSERT(task); + tasks.remove(task); + db->log(tr("Task %1 on %2 finished").arg(task->taskName(), task->taskSubject())); + task->deleteLater(); +} + +MyList *MyList::instance() +{ + if (!m_instance) + m_instance = new MyList; + return m_instance; +} + +void MyList::init() +{ + static bool init = false; + if (init) return; + + if (!REGISTER_QT_TYPES) return; +// qRegisterMetaType("AbstractTaskPtr"); + qRegisterMetaType("QFileInfo"); +} + +bool MyList::REGISTER_QT_TYPES = true; +MyList *MyList::m_instance = 0; + +MyList *instance() +{ + return MyList::instance(); +} + +} // namespace LocalMyList diff --git a/localmylist/mylist.h b/localmylist/mylist.h new file mode 100644 index 0000000..bff9fc5 --- /dev/null +++ b/localmylist/mylist.h @@ -0,0 +1,82 @@ +#ifndef LOCALMYLIST_H +#define LOCALMYLIST_H + +#include "localmylist_global.h" +#include "database.h" +#include "settings.h" +#include + +#include +#include +#include + +namespace LocalMyList { + +class AbstractTask; +class WorkThread; +class RequestHandler; + +class LOCALMYLISTSHARED_EXPORT MyList : public QObject { + Q_OBJECT + Q_PROPERTY(QString hostName READ hostName WRITE setHostName) + Q_PROPERTY(int hostId READ hostId) + +public: + MyList(); + ~MyList(); + + Database *database() const; + Settings *settings() const; + + QString hostName() const; + int hostId() const; + bool isUdpHost() const; + +public slots: + void setHostName(QString name); + + + AbstractTask *addFile(const QFileInfo &file); + AbstractTask *addDirectory(const QDir &directory); + AbstractTask *importTitles(const QFileInfo &file); + AbstractTask *importMyList(const QFileInfo &file); + void executeTask(AbstractTask *task); + + void setupUdpClient(); + void setupRequestHandler(); + + void loadLocalSettings(); + void saveLocalSettings(); + +private slots: + void taskFinished(); + + +signals: + void requestAddDirectory(const QDir &directory); + +private: + DatabaseConnectionSettings dbs; + Database *db; + WorkThread *workThread; + RequestHandler *m_requestHandler; + Settings *m_settings; + + HostInfo hostInfo; + + QSet tasks; + +public: + static MyList *instance(); + static bool REGISTER_QT_TYPES; + +private: + static void init(); + static MyList *m_instance; +}; + +LOCALMYLISTSHARED_EXPORT MyList *instance(); + +} // namespace LocalMyList + +#endif // LOCALMYLIST_H diff --git a/localmylist/mylistexportparsetask.cpp b/localmylist/mylistexportparsetask.cpp new file mode 100644 index 0000000..28103b6 --- /dev/null +++ b/localmylist/mylistexportparsetask.cpp @@ -0,0 +1,679 @@ +#include "mylistexportparsetask.h" + +#include "database.h" + +#include + +namespace LocalMyList { + +MyListExportParseTask::MyListExportParseTask(QObject *parent) : + AbstractTask(0, parent), QXmlStreamReader() +{ + dateTimeFormat = "dd.MM.yyyy hh:mm"; + dateFormat = "dd.MM.yyyy"; +} + +QFileInfo MyListExportParseTask::file() const +{ + return m_file; +} + +QString MyListExportParseTask::taskSubject() const +{ + return QString("Anime Title Parse (from file: %1)").arg(m_file.fileName()); +} + +bool MyListExportParseTask::canUseThreads() const +{ + return true; +} + +void MyListExportParseTask::start() +{ + qDebug() << "Started"; + QFile f(m_file.absoluteFilePath()); + if (!f.open(QIODevice::ReadOnly)) + { + emit finished(); + qWarning("MyListExportParseTask failed to open file"); + return; + } + + parse(&f); + emit finished(); +} + +void MyListExportParseTask::setFile(const QFileInfo &file) +{ + m_file = file; +} + +bool MyListExportParseTask::parse(QIODevice *device) +{ + setDevice(device); + animeCount = episodeCount = fileCount = 0; + animeRead = episodesRead = filesRead = 0; + + while (!atEnd()) + { + readNext(); + if (isStartElement()) + { + if (name() == "my_anime_list") + { + readMyList(); + } + else + { + raiseError(QObject::tr("The file is not a \"mylist-xml-new\" file.")); + } + } + } + + return !error(); +} + +void MyListExportParseTask::readUnknownElement() +{ + Q_ASSERT(isStartElement()); + + int i = 1; + while (!atEnd()) + { + readNext(); + if (isStartElement()) + ++i; + + if (isEndElement()) + --i; + + if (!i) + break; + } +} + +void MyListExportParseTask::readMyList() +{ + Q_ASSERT(isStartElement()); + + while (!atEnd()) { + readNext(); + if (isEndElement()) + break; + + if (isStartElement()) { + if (name() == "anime") { + readAnime(); + reportProgress(); + } + else if (name() == "episode") { + readEpisode(); + reportProgress(); + } + else if (name() == "file") { + readFile(); + reportProgress(); + } + else if (name() == "file_episode") { + readFileEpisodeRel(); + reportProgress(); + } + else if (name() == "user_info") { + readUserInfo(); + qDebug() << "Entry count" << (animeCount + fileCount + episodeCount); + } else { + readUnknownElement(); + } + } + } +} + +void MyListExportParseTask::readUserInfo() +{ + while (!atEnd()) { + readNext(); + if (isEndElement()) + break; + + if (isStartElement()) { + if (name() == "ExportDate") { + exportDate = QDateTime::fromString(readElementText(), dateTimeFormat); + } + else if (name() == "MyAnimeCount") { + animeCount = readElementText().toInt(); + } + else if (name() == "MyEpisodeCount") { + episodeCount = readElementText().toInt(); + } + else if (name() == "MyFileCount") { + fileCount = readElementText().toInt(); + } else { + readUnknownElement(); + } + } + } +qDebug() << "Exported on" << exportDate; +} + +void MyListExportParseTask::readAnime() +{ + Q_ASSERT(isStartElement()); + + Anime a; + + while (!atEnd()) { + readNext(); + if (isEndElement()) + break; + + if (isStartElement()) { + if (name() == "AnimeID") { + a.aid = readElementText().toInt(); + } + else if (name() == "Update") { + a.anidbUpdate = QDateTime::fromTime_t(readElementText().toInt()); + } + else if (name() == "Name") { + a.titleRomaji = readElementText(); + } + else if (name() == "NameKanji") { + a.titleKanji = readElementText(); + } + else if (name() == "NameEnglish") { + a.titleEnglish = readElementText(); + } + else if (name() == "AnimeDescription") { + a.description = readElementText(); + } + else if (name() == "Year") { + a.year = readElementText(); + } + else if (name() == "StartDate") { + a.startDate = QDateTime::fromString(readElementText(), dateFormat); + } + else if (name() == "EndDate") { + a.endDate = QDateTime::fromString(readElementText(), dateFormat); + } + else if (name() == "Rating") { + a.rating = readElementText().toDouble(); + } + else if (name() == "Votes") { + a.votes = readElementText().toInt(); + } + else if (name() == "TempRating") { + a.tempRating = readElementText().toDouble(); + } + else if (name() == "TempVotes") { + a.tempVotes = readElementText().toInt(); + } + else if (name() == "MyVote") { + a.myVote = readElementText().toDouble(); + } + else if (name() == "MyVoteDate") { + a.myVoteDate = QDateTime::fromString(readElementText(), dateTimeFormat); + } + else if (name() == "MyTempVote") { + a.myTempVote = readElementText().toDouble(); + } + else if (name() == "MyTempVoteDate") { + a.myTempVoteDate = QDateTime::fromString(readElementText(), dateTimeFormat); + } + else if (name() == "TypeName") { + a.type = readElementText(); + } else { + readUnknownElement(); + } + } + } + + ++animeRead; + + db->transaction(); + Anime current = db->getAnime(a.aid); + + if (!current.aid) + { +qDebug() << "Adding new Anime"; + a.entryUpdate = QDateTime::currentDateTime(); + a.myUpdate = exportDate; + db->addAnime(a); + db->commit(); + return; + } + + if (current.anidbUpdate < a.anidbUpdate) + { +qDebug() << "Updating Anime" << current.aid; + if (a.anidbUpdate.isValid()) + current.anidbUpdate = a.anidbUpdate; + if (!a.titleRomaji.isEmpty()) + current.titleRomaji = a.titleRomaji; + if (!a.titleKanji.isEmpty()) + current.titleKanji = a.titleKanji; + if (!a.titleEnglish.isEmpty()) + current.titleEnglish = a.titleEnglish; + if (!a.description.isEmpty()) + current.description = a.description; + if (a.year.isEmpty()) + current.year = a.year; + if (a.startDate.isValid()) + current.startDate = a.startDate; + if (a.endDate.isValid()) + current.endDate = a.endDate; + if (!a.type.isEmpty()) + current.type = a.type; + }else{qDebug() << "No update required" << current.aid;} + + current.entryUpdate = QDateTime::currentDateTime(); + if (a.rating) + current.rating = a.rating; + if (a.votes) + current.votes = a.votes; + if (a.tempRating) + current.tempRating = a.tempRating; + if (a.tempVotes) + current.tempVotes = a.tempVotes; + + if (current.myUpdate < exportDate) + { + qDebug() << "My var update" << current.aid; + current.myUpdate = exportDate; + if (a.myVote) + current.myVote = a.myVote; + if (a.myVoteDate.isValid()) + current.myVoteDate = a.myVoteDate; + if (a.myTempVote) + current.myTempVote = a.myTempVote; + if (a.myTempVoteDate.isValid()) + current.myTempVoteDate = a.myTempVoteDate; + } + + db->setAnime(current); + db->commit(); +} + +void MyListExportParseTask::readEpisode() +{ + Q_ASSERT(isStartElement()); + + Episode e; + + while (!atEnd()) { + readNext(); + if (isEndElement()) + break; + + if (isStartElement()) { + if (name() == "EpID") { + e.eid = readElementText().toInt(); + } + else if (name() == "AnimeID") { + e.aid = readElementText().toInt(); + } + else if (name() == "EpUpdate") { + e.anidbUpdate = QDateTime::fromString(readElementText(), dateTimeFormat); + } + else if (name() == "EpNo") { + e.epno = readElementText().toInt(); + } + else if (name() == "EpNameRomaji") { + e.titleRomaji = readElementText(); + } + else if (name() == "EpNameKanji") { + e.titleKanji = readElementText(); + } + else if (name() == "EpName") { + e.titleEnglish = readElementText(); + } + else if (name() == "EpLength") { + e.length = readElementText().toInt(); + } + else if (name() == "EpAired") { + e.airdate = QDateTime::fromString(readElementText(), dateTimeFormat); + } + else if (name() == "EpState") { + e.state = readElementText().toInt(); + } + else if (name() == "EpStateSpecial") { + e.special = bool(readElementText().toInt()); + } + else if (name() == "EpStateRecap") { + e.recap = bool(readElementText().toInt()); + } + else if (name() == "EpStateOp") { + e.opening = bool(readElementText().toInt()); + } + else if (name() == "EpStateEnd") { + e.ending = bool(readElementText().toInt()); + } + else if (name() == "EpRating") { + e.rating = readElementText().toDouble(); + } + else if (name() == "EpVotes") { + e.votes = readElementText().toInt(); + } + else if (name() == "EpMyVote") { + e.myVote = readElementText().toDouble(); + } + else if (name() == "EpMyVoteDate") { + e.myVoteDate = QDateTime::fromString(readElementText(), dateTimeFormat); + } else { + readUnknownElement(); + } + } + } + + + ++episodesRead; + + db->transaction(); + Episode current = db->getEpisode(e.eid); + + if (!current.eid) + { +qDebug() << "Adding new Episode"; + e.entryUpdate = QDateTime::currentDateTime(); + e.myUpdate = exportDate; + db->addEpisode(e); + db->commit(); + return; + } + + if (current.anidbUpdate < e.anidbUpdate) + { +qDebug() << "Updating Episode" << current.eid; + if (e.aid) + current.aid = e.aid; + if (e.anidbUpdate.isValid()) + current.anidbUpdate = e.anidbUpdate; + if (e.epno) + current.epno = e.epno; + if (!e.titleRomaji.isEmpty()) + current.titleRomaji = e.titleRomaji; + if (!e.titleKanji.isEmpty()) + current.titleKanji = e.titleKanji; + if (!e.titleEnglish.isEmpty()) + current.titleEnglish = e.titleEnglish; + if (e.length) + current.length = e.length; + if (e.airdate.isValid()) + current.airdate = e.airdate; + current.state = e.state; + current.special = e.special; + current.recap = e.recap; + current.opening = e.opening; + current.ending = e.ending; + }else{qDebug() << "No update required" << current.eid;} + + current.entryUpdate = QDateTime::currentDateTime(); + if (e.rating) + current.rating = e.rating; + if (e.votes) + current.votes = e.votes; + + if (current.myUpdate < exportDate) + { +qDebug() << "My var update" << current.eid; + current.myUpdate = exportDate; + if (e.myVote) + current.myVote = e.myVote; + if (e.myVoteDate.isValid()) + current.myVoteDate = e.myVoteDate; + } + db->setEpisode(current); + db->commit(); +} + +void MyListExportParseTask::readFile() +{ + Q_ASSERT(isStartElement()); + + File f; + bool generic; + + while (!atEnd()) + { + readNext(); + if (isEndElement()) + break; + + if (isStartElement()) { + if (name() == "FID") { + f.fid = readElementText().toInt(); + } + else if (name() == "EpID") { + f.eid = readElementText().toInt(); + } + else if (name() == "AnimeID") { + f.aid = readElementText().toInt(); + } + else if (name() == "GID") { + f.gid = readElementText().toInt(); + } + else if (name() == "Update") { + f.anidbUpdate = QDateTime::fromString(readElementText(), dateTimeFormat); + } + else if (name() == "ed2kHash") { + f.ed2k = readElementText().toAscii(); + } + else if (name() == "Size") { + f.size = readElementText().toLongLong(); + } + else if (name() == "Length") { + f.length = readElementText().toInt(); + } + else if (name() == "FileType") { + QString text = readElementText(); + bool ok; + text.toInt(&ok); + if (!ok) + f.extension = text; + } + else if (name() == "GName") { + f.groupName = readElementText(); + } + else if (name() == "GShortName") { + f.groupNameShort = readElementText(); + } + else if (name() == "CRC") { + f.crc = readElementText(); + } + else if (name() == "ReleaseDate") { + f.releaseDate = QDateTime::fromString(readElementText(), dateTimeFormat); + } + else if (name() == "VersionName") { + f.version = readElementText().mid(1).toInt(); + } + // f.censored; + else if (name() == "TypeName") { + f.type = readElementText(); + } + else if (name() == "QualityID") { + f.qualityId = readElementText().toInt(); + } + else if (name() == "QualityName") { + f.quality = readElementText(); + } + else if (name() == "ResName") { + f.resolution = readElementText(); + } + else if (name() == "VCodecName") { + f.videoCodec = readElementText(); + } + else if (name() == "ACodecName") { + f.audioCodec = readElementText(); + } + else if (name() == "ALangName") { + f.audioLanguage = readElementText(); + } + else if (name() == "SubName") { + f.subtitleLanguage = readElementText(); + } + else if (name() == "VAspectRatioName") { + f.aspectRatio = readElementText(); + } + else if (name() == "ViewDate") { + f.myWatched = QDateTime::fromString(readElementText(), dateTimeFormat); + } + else if (name() == "MyState") { + f.myState = readElementText().toInt(); + } + else if (name() == "MyFileState") { + f.myFileState = readElementText().toInt(); + } +/* + else if (name() == "Storage") { + f.myStorage = readElementText(); + } + else if (name() == "Source") { + f.mySource = readElementText(); + } + else if (name() == "Other") { + f.myOther = readElementText(); + } +*/ + else if (name() == "Generic") { + generic = bool(readElementText().toInt()); + } else { + readUnknownElement(); + } + } + } + + ++filesRead; + + if (generic) + return; + + db->transaction(); + File current = db->getFile(f.fid); + + if (!current.fid) + { +qDebug() << "Adding new File"; + f.entryUpdate = QDateTime::currentDateTime(); + f.myUpdate = exportDate; + db->addFile(f); + db->commit(); + return; + } + + if (current.anidbUpdate < f.anidbUpdate) + { +qDebug() << "Updating File" << current.fid; + if (f.eid) + current.eid = f.eid; + if (f.aid) + current.aid = f.aid; + if (f.gid) + current.gid = f.gid; + if (f.anidbUpdate.isValid()) + current.anidbUpdate = f.anidbUpdate; + if (!f.ed2k.isEmpty()) + current.ed2k = f.ed2k; + if (f.size) + current.size = f.size; + if (f.length) + current.length = f.length; + if (!f.extension.isEmpty()) + current.extension = f.extension; + if (!f.groupName.isEmpty()) + current.groupName = f.groupName; + if (!f.groupNameShort.isEmpty()) + current.groupNameShort = f.groupNameShort; + if (!f.crc.isEmpty()) + current.crc = f.crc; + if (f.releaseDate.isValid()) + current.releaseDate = f.releaseDate; + if (f.version) + current.version = f.version; + if (f.censored) + current.censored = f.censored; + if (!f.type.isEmpty()) + current.type = f.type; + if (f.qualityId) + current.qualityId = f.qualityId; + if (!f.quality.isEmpty()) + current.quality = f.quality; + if (!f.resolution.isEmpty()) + current.resolution = f.resolution; + if (!f.videoCodec.isEmpty()) + current.videoCodec = f.videoCodec; + if (!f.audioCodec.isEmpty()) + current.audioCodec = f.audioCodec; + if (!f.audioLanguage.isEmpty()) + current.audioLanguage = f.audioLanguage; + if (!f.subtitleLanguage.isEmpty()) + current.subtitleLanguage = f.subtitleLanguage; + if (!f.aspectRatio.isEmpty()) + current.aspectRatio = f.aspectRatio; + }else{qDebug() << "No update required" << current.fid;} + + current.entryUpdate = QDateTime::currentDateTime(); + + if (current.myUpdate < exportDate) + { + qDebug() << "My var update" << current.fid; + current.myUpdate = exportDate; + if (f.myWatched.isValid()) + current.myWatched = f.myWatched; + if (f.myState) + current.myState = f.myState; + if (f.myFileState) + current.myFileState = f.myFileState; + if (!f.myStorage.isEmpty()) + current.myStorage = f.myStorage; + if (!f.mySource.isEmpty()) + current.mySource = f.mySource; + if (!f.myOther.isEmpty()) + current.myOther = f.myOther; + } + db->setFile(current); + db->commit(); +} + +void MyListExportParseTask::readFileEpisodeRel() +{ + Q_ASSERT(isStartElement()); + + FileEpisodeRel fe; + + while (!atEnd()) + { + readNext(); + if (isEndElement()) + break; + + if (isStartElement()) { + if (name() == "FID") { + fe.fid = readElementText().toInt(); + } + else if (name() == "EpID") { + fe.eid = readElementText().toInt(); + } + else if (name() == "StartPercent") { + fe.startPercent = readElementText().toInt(); + } + else if (name() == "EndPercent") { + fe.endPercent = readElementText().toInt(); + } else { + readUnknownElement(); + } + } + } + + db->addFileEpisodeRel(fe); +} + +void MyListExportParseTask::reportProgress() +{ + int read = animeRead + filesRead + episodesRead; + int total = animeCount + fileCount + episodeCount; + if (read % 200 != 0) + return; + emit progress(read, total); + qDebug() << "Read" << read << "entries"; + +} + +} // namespace LocalMyList diff --git a/localmylist/mylistexportparsetask.h b/localmylist/mylistexportparsetask.h new file mode 100644 index 0000000..e1d94d8 --- /dev/null +++ b/localmylist/mylistexportparsetask.h @@ -0,0 +1,58 @@ +#ifndef MYLISTEXPORTPARSETASK_H +#define MYLISTEXPORTPARSETASK_H + +#include "abstracttask.h" +#include +#include +#include + +namespace LocalMyList { + +class MyListExportParseTask : public AbstractTask, public QXmlStreamReader +{ + Q_OBJECT +public: + explicit MyListExportParseTask(QObject *parent = 0); + + QFileInfo file() const; + QString taskSubject() const; + bool canUseThreads() const; + + void start(); + +public slots: + void setFile(const QFileInfo &file); + +private: + bool parse(QIODevice *device); + + void readUnknownElement(); + + void readMyList(); + void readUserInfo(); + void readAnime(); + void readEpisode(); + void readFile(); + void readFileEpisodeRel(); + + void reportProgress(); + + int animeCount; + int episodeCount; + int fileCount; + + int animeRead; + int episodesRead; + int filesRead; + + QDateTime exportDate; + + QString dateTimeFormat; + QString dateFormat; + + QFileInfo m_file; +}; + +} // namespace LocalMyList + +#endif // MYLISTEXPORTPARSETASK_H diff --git a/localmylist/requesthandler.cpp b/localmylist/requesthandler.cpp new file mode 100644 index 0000000..9b1ba1a --- /dev/null +++ b/localmylist/requesthandler.cpp @@ -0,0 +1,415 @@ +#include "requesthandler.h" + +#include "database.h" + +#include +#include +#include +#include +#include +#include + +#include + +namespace LocalMyList { + +RequestHandler::RequestHandler(Database *db, QObject *parent) : + QObject(parent) +{ + this->db = db; + connect(this, SIGNAL(batchFinished()), this, SLOT(handleRequests()), Qt::QueuedConnection); +} + +void RequestHandler::handleRequests() +{ + using namespace ::AniDBUdpClient; + + qDebug() << "handleRequests"; + + static const AnimeFlags animeFlags = + AnimeFlag::Aid + | AnimeFlag::DateRecordUpdated + | AnimeFlag::EnglishName + | AnimeFlag::KanjiName + | AnimeFlag::RomajiName + | AnimeFlag::Year + | AnimeFlag::Type + | AnimeFlag::Rating + | AnimeFlag::VoteCount + | AnimeFlag::TempRating + | AnimeFlag::TempVoteCount; + + static const FileFlags fileFlags = + FileFlag::Aid | FileFlag::Eid | FileFlag::Gid | FileFlag::Lid + | FileFlag::Ed2k + | FileFlag::Size + | FileFlag::LengthInSeconds + | FileFlag::FileType + | FileFlag::Crc32 + | FileFlag::State + | FileFlag::Quality + | FileFlag::VideoResolution + | FileFlag::VideoCodec + | FileFlag::AudioCodec + | FileFlag::SubLanguage + | FileFlag::DubLanguage + | FileFlag::MyListViewDate; + + static const FileAnimeFlags fileAnimeFlags = + FileAnimeFlag::GroupName + | FileAnimeFlag::GroupShortName; + + db->transaction(); + QList requests = db->getRequestBatch(); + + qDebug() << "Got" << requests.count() << "requests"; + + if (!requests.count()) + { + db->commit(); + return; + } + + foreach (const PendingRequest &request, requests) + { + if (request.aid) + { + qDebug() << " `-> aid" << request.aid; + AnimeReply *reply = Client::instance()->send(AnimeCommand(request.aid, animeFlags)); + connect(reply, SIGNAL(replyReady(bool)), this, SLOT(animeRequestComplete(bool))); + } + else if (request.eid) + { + qDebug() << " `-> eid" << request.eid; + EpisodeReply *reply = Client::instance()->send(EpisodeCommand(request.eid)); + connect(reply, SIGNAL(replyReady(bool)), this, SLOT(episodeRequestComplete(bool))); + + // The vote command doesn't allow voting for an eid, but requires the aid and episode number + // This has to be done after recieving a reply. + } + else if (request.fid) + { + qDebug() << " `-> fid" << request.fid; + FileReply *reply = Client::instance()->send(FileCommand(request.fid, fileFlags, fileAnimeFlags)); + connect(reply, SIGNAL(replyReady(bool)), this, SLOT(fileRequestComplete(bool))); + } + else if (!request.ed2k.isEmpty() && request.size) + { + qDebug() << " `-> ed2k&size" << request.ed2k << request.size; + FileReply *reply = Client::instance()->send(FileCommand(request.ed2k, request.size, fileFlags, fileAnimeFlags)); + connect(reply, SIGNAL(replyReady(bool)), this, SLOT(fileRequestComplete(bool))); + } + else + { + Q_ASSERT_X(false, "requestHandler", "Unknown request"); + } + } + db->commit(); + + emit batchFinished(); +} + +void RequestHandler::animeRequestComplete(bool success) +{ + using namespace ::AniDBUdpClient; + + qDebug() << "animeRequestComplete"; + + AnimeReply *reply = qobject_cast(sender()); + + Q_ASSERT(reply); + reply->deleteLater(); + + if (!success) + return; + + // If entry exists we get to update fields we know. + // Entry might exist just with my values and aid from vote command + Anime next = db->getAnime(reply->command().aid()); + + bool isNew = !next.aid; + + next.aid = reply->aid(); + next.anidbUpdate = reply->dateRecordUpdated(); + next.entryUpdate = QDateTime::currentDateTime(); + // date goes here + next.titleEnglish = reply->englishName(); + next.titleRomaji = reply->romajiName(); + next.titleKanji = reply->kanjiName(); + // next.description This is obtained with a different command + next.year = reply->year(); // Format like in file command? + qDebug() << reply->year(); + // next.startDate + // next.endDate + next.type = reply->type(); + next.rating = reply->rating(); + next.votes = reply->voteCount(); + next.tempRating = reply->tempRating(); + next.tempVotes = reply->tempVoteCount(); + + if (isNew) + db->addAnime(next); + else + db->setAnime(next); + + // my values obtained with VoteCommand + + VoteReply *voteReply = Client::instance()->send(VoteCommand(VoteCommand::AnimeVote, next.aid)); + connect(voteReply, SIGNAL(replyReady(bool)), this, SLOT(voteRequestComplete(bool))); + idMap.insert(voteReply, next.aid); +} + +void RequestHandler::episodeRequestComplete(bool success) +{ + using namespace ::AniDBUdpClient; + + qDebug() << "episodeRequestComplete"; + + EpisodeReply *reply = qobject_cast(sender()); + + Q_ASSERT(reply); + reply->deleteLater(); + + if (!success) + return; + + Episode next = db->getEpisode(reply->command().eid()); + + bool isNew = !next.eid; + + next.eid = reply->eid(); + next.aid = reply->aid(); +// next.anidbUpdate = reply-> + next.entryUpdate = QDateTime::currentDateTime(); +// next.myUpdate = reply-> + next.epno = reply->epnoAsInt(); + next.titleEnglish = reply->titleEnglish(); + next.titleRomaji = reply->titleRomaji(); + next.titleKanji = reply->titleKanji(); + next.length = reply->length(); + next.airdate = reply->airDate(); + // next.state - State is a bitfield and will be split into parts + next.special = reply->type() == 'S'; + // I see no way of getting these via UDP api. + // next.recap + // next.opening + // next.ending + next.rating = reply->rating(); + next.votes = reply->votes(); + + if (isNew) + db->addEpisode(next); + else + db->setEpisode(next); + + // Obtain my values + VoteReply *voteReply = Client::instance()->send(VoteCommand(VoteCommand::AnimeVote, next.aid, VoteCommand::Retrieve, reply->epnoAsInt())); + connect(voteReply, SIGNAL(replyReady(bool)), this, SLOT(voteRequestComplete(bool))); + idMap.insert(voteReply, next.eid); +} + +void RequestHandler::fileRequestComplete(bool success) +{ + using namespace ::AniDBUdpClient; + + qDebug() << "fileRequestComplete"; + + FileReply *reply = qobject_cast(sender()); + + Q_ASSERT(reply); + reply->deleteLater(); + + if (!success) + return; + + // Fid might not be known in command + File next = db->getFile(reply->fid()); + + bool isNew = !next.fid; + + next.fid = reply->fid(); + next.aid = reply->aid(); + next.eid = reply->eid(); + next.gid = reply->gid(); + // next.anidbUpdate + next.entryUpdate = QDateTime::currentDateTime(); + // next.myUpdate + next.ed2k = reply->ed2k(); + next.size = reply->size(); + next.length = reply->lengthInSeconds(); + next.extension = reply->fileType(); + next.groupName = reply->groupName(); + next.groupNameShort = reply->groupShortName(); + next.crc = reply->crc32(); qDebug() << "crc" << reply->crc32(); + //next.releaseDate + next.version = reply->version(); + next.censored = reply->isCensored(); + next.type = reply->type(); + // next.qualityId - can map quality to qualityId + next.quality = reply->quality(); + next.resolution = reply->videoResolution(); + next.videoCodec = reply->videoCodec(); + next.audioCodec = reply->audioCodec(); + next.subtitleLanguage = reply->subLanguage(); + next.audioLanguage = reply->audioCodec(); + next.subtitleLanguage = reply->subLanguage(); + // next.aspectRatio + next.myWatched = reply->myListViewDate(); + next.myState = reply->myListState(); + next.myFileState = reply->myListFileState(); + next.myStorage = reply->myListStorage(); + next.mySource = reply->myListSource(); + next.myOther = reply->myListOther(); + + if (isNew) + db->addFile(next); + else + db->setFile(next); + + { + Episode ep = db->getEpisode(next.eid); + if (!ep.eid) + { + PendingRequest request; + request.eid = next.eid; + + db->addRequest(request); + } + } + + { + Anime anime = db->getAnime(next.aid); + if (!anime.aid) + { + PendingRequest request; + request.aid = next.aid; + + db->addRequest(request); + } + } + + // File is not in mylist + if (!reply->lid()) + { + MyListAddCommand cmd(next.fid); + cmd.setEd2k(reply->ed2k()); + cmd.setSize(reply->size()); + MyListAddReply *addReply = Client::instance()->send(cmd); + connect(addReply, SIGNAL(replyReady(bool)), this, SLOT(myListAddReplyRecieved(bool))); + return; + } + + qDebug() << "Clearing fid/ed2k&size" << next.fid << "/" << reply->command().ed2k() << "&" << reply->command().size(); + PendingRequest request; + request.fid = next.fid; + qDebug() << "Clearing fid" << request.fid; + db->clearRequest(request); + + PendingRequest ed2kRequest; + ed2kRequest.ed2k = reply->ed2k(); + ed2kRequest.size = reply->size(); + db->clearRequest(ed2kRequest); +} + +void RequestHandler::voteRequestComplete(bool) +{ + using namespace ::AniDBUdpClient; + + qDebug() << "voteRequestComplete"; + + VoteReply *reply = qobject_cast(sender()); + + Q_ASSERT(reply); + Q_ASSERT(idMap.contains(reply)); + + int id = idMap.take(reply); + reply->deleteLater(); + + + + // Anime vote + if (reply->command().voteType() == VoteCommand::AnimeVote && reply->command().epno() == 0) + { + qDebug() << " `-> anime vote"; + Anime anime = db->getAnime(id); + + if (!anime.aid) + return; + + anime.myUpdate = QDateTime::currentDateTime(); + anime.myVote = reply->vote(); + // vote dates not available via udp api? + + db->setAnime(anime); + + VoteReply *tmpVoteReply = Client::instance()->send(VoteCommand(VoteCommand::AnimeTempVote, id)); + idMap.insert(tmpVoteReply, id); + connect(tmpVoteReply, SIGNAL(replyReady(bool)), this, SLOT(voteRequestComplete(bool))); + } + // Episode vote + if (reply->command().voteType() == VoteCommand::AnimeVote && reply->command().epno() != 0) + { + qDebug() << " `-> episode vote"; + Episode ep = db->getEpisode(id); + + if (!ep.eid) + return; + + ep.myUpdate = QDateTime::currentDateTime(); + ep.myVote = reply->vote(); + + db->setEpisode(ep); + + qDebug() << "Clearing eid" << id; + PendingRequest request; + request.eid = id; + db->clearRequest(request); + } + if (reply->command().voteType() == VoteCommand::AnimeTempVote) + { + qDebug() << " `-> anime temp vote"; + Anime anime = db->getAnime(id); + + if (!anime.aid) + return; + + anime.myUpdate = QDateTime::currentDateTime(); + anime.myTempVote = reply->vote(); + + db->setAnime(anime); + + qDebug() << "Clearing aid" << id; + PendingRequest request; + request.aid = id; + db->clearRequest(request); + } +} + +void RequestHandler::myListAddReplyRecieved(bool success) +{ + using namespace ::AniDBUdpClient; + + qDebug() << "myListAddReplyRecieved"; + + MyListAddReply *reply = qobject_cast(sender()); + + Q_ASSERT(reply); + reply->deleteLater(); + + if (!success) + return; + + qDebug() << "Clearing fid/ed2k&size" << reply->command().fid() << "/" << reply->command().ed2k() << "&" << reply->command().size(); + PendingRequest request; + request.fid = reply->command().fid(); + db->clearRequest(request); + + PendingRequest ed2kRequest; + ed2kRequest.ed2k = reply->command().ed2k(); + ed2kRequest.size = reply->command().size(); + db->clearRequest(ed2kRequest); + + +} + +} // namespace LocalMyList diff --git a/localmylist/requesthandler.h b/localmylist/requesthandler.h new file mode 100644 index 0000000..a846591 --- /dev/null +++ b/localmylist/requesthandler.h @@ -0,0 +1,46 @@ +#ifndef REQUESTHANDLER_H +#define REQUESTHANDLER_H + +#include +#include + +namespace AniDBUdpClient +{ + class AnimeReply; + class EpisodeReply; + class FileReply; + class VoteReply; + class MyListAddReply; +} + +namespace LocalMyList { + +class Database; + +class RequestHandler : public QObject +{ + Q_OBJECT +public: + explicit RequestHandler(Database *db, QObject *parent = 0); + +signals: + void batchFinished(); + +public slots: + void handleRequests(); + + void animeRequestComplete(bool success); + void episodeRequestComplete(bool success); + void fileRequestComplete(bool success); + void voteRequestComplete(bool); + void myListAddReplyRecieved(bool success); + +private: + Database *db; + + QMap<::AniDBUdpClient::VoteReply *, int> idMap; +}; + +} // namespace LocalMyList + +#endif // REQUESTHANDLER_H diff --git a/localmylist/settings.cpp b/localmylist/settings.cpp new file mode 100644 index 0000000..74cd26c --- /dev/null +++ b/localmylist/settings.cpp @@ -0,0 +1,44 @@ +#include "settings.h" +#include "database.h" + +namespace LocalMyList { + +Settings::Settings(Database *db, QObject *parent) : + QObject(parent) +{ + this->db = db; +} + +Database *Settings::database() const +{ + return db; +} + +void Settings::setDatabase(Database *db) +{ + this->db = db; +} + +QVariant Settings::get(const QString &key) const +{ + if (!settings.contains(key)) readSettings(); + return settings.value(key); +} + +void Settings::set(const QString &key, const QVariant &value) +{ + settings[key] = value; + db->setConfig(key, value); +} + +void Settings::settingsChangedInDatabase() +{ + settings.clear(); +} + +void Settings::readSettings() const +{ + settings = db->getConfig(); +} + +} // namespace LocalMyList diff --git a/localmylist/settings.h b/localmylist/settings.h new file mode 100644 index 0000000..01bd3c0 --- /dev/null +++ b/localmylist/settings.h @@ -0,0 +1,39 @@ +#ifndef SETTINGS_H +#define SETTINGS_H + +#include +#include + +namespace LocalMyList { + +class Database; + +class Settings : public QObject +{ + Q_OBJECT +public: + explicit Settings(Database *db = 0, QObject *parent = 0); + Database *database() const; + void setDatabase(Database *db); + + template void get(const QString &key, T& value) const { value = get(key).value();} + +signals: + void settingsChanged(); + +public slots: + QVariant get(const QString &key) const; + void set(const QString &key, const QVariant &value); + + void settingsChangedInDatabase(); + +private: + void readSettings() const; + + Database *db; + mutable QVariantMap settings; +}; + +} // namespace LocalMyList + +#endif // SETTINGS_H diff --git a/localmylist/workthread.cpp b/localmylist/workthread.cpp new file mode 100644 index 0000000..58d882c --- /dev/null +++ b/localmylist/workthread.cpp @@ -0,0 +1,36 @@ +#include "workthread.h" +#include "database.h" + +namespace LocalMyList { + +WorkThread::WorkThread(const QString &threadName, const DatabaseConnectionSettings &dbs, QObject *parent) : + QThread(parent), m_threadName(threadName), db(0) +{ + db = new Database(m_threadName); + db->setConnectionSettings(dbs); + db->moveToThread(this); +} + +WorkThread::~WorkThread() +{ + if (db) + delete db; +} + +QString WorkThread::threadName() const +{ + return m_threadName; +} + +Database *WorkThread::database() const +{ + return db; +} + +void WorkThread::run() +{ + db->connect(); + exec(); +} + +} // namespace LocalMyList diff --git a/localmylist/workthread.h b/localmylist/workthread.h new file mode 100644 index 0000000..f00aeef --- /dev/null +++ b/localmylist/workthread.h @@ -0,0 +1,30 @@ +#ifndef WORKTHREAD_H +#define WORKTHREAD_H + +#include +#include "abstracttask.h" +#include "database.h" + +namespace LocalMyList { + +class WorkThread : public QThread +{ + Q_OBJECT +public: + explicit WorkThread(const QString &threadName, const DatabaseConnectionSettings &dbs, QObject *parent = 0); + ~WorkThread(); + + QString threadName() const; + Database *database() const; + +protected: + void run(); + +private: + QString m_threadName; + Database *db; +}; + +} // namespace LocalMyList + +#endif // WORKTHREAD_H diff --git a/management-gui/databaseconnectiondialog.cpp b/management-gui/databaseconnectiondialog.cpp new file mode 100644 index 0000000..efffcc8 --- /dev/null +++ b/management-gui/databaseconnectiondialog.cpp @@ -0,0 +1,14 @@ +#include "databaseconnectiondialog.h" +#include "ui_databaseconnectiondialog.h" + +DatabaseConnectionDialog::DatabaseConnectionDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::DatabaseConnectionDialog) +{ + ui->setupUi(this); +} + +DatabaseConnectionDialog::~DatabaseConnectionDialog() +{ + delete ui; +} diff --git a/management-gui/databaseconnectiondialog.h b/management-gui/databaseconnectiondialog.h new file mode 100644 index 0000000..b5a25f6 --- /dev/null +++ b/management-gui/databaseconnectiondialog.h @@ -0,0 +1,22 @@ +#ifndef DATABASECONNECTIONDIALOG_H +#define DATABASECONNECTIONDIALOG_H + +#include + +namespace Ui { +class DatabaseConnectionDialog; +} + +class DatabaseConnectionDialog : public QDialog +{ + Q_OBJECT + +public: + explicit DatabaseConnectionDialog(QWidget *parent = 0); + ~DatabaseConnectionDialog(); + +private: + Ui::DatabaseConnectionDialog *ui; +}; + +#endif // DATABASECONNECTIONDIALOG_H diff --git a/management-gui/databaseconnectiondialog.ui b/management-gui/databaseconnectiondialog.ui new file mode 100644 index 0000000..f02d333 --- /dev/null +++ b/management-gui/databaseconnectiondialog.ui @@ -0,0 +1,168 @@ + + + DatabaseConnectionDialog + + + + 0 + 0 + 400 + 300 + + + + Dialog + + + + + + Connection Details + + + + + + Host: + + + host + + + + + + + + + + Port: + + + port + + + + + + + + + + User: + + + user + + + + + + + + + + Pass: + + + pass + + + + + + + + + + + + + Client Details + + + + + + Name: + + + clientName + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + host + port + user + pass + clientName + buttonBox + + + + + buttonBox + accepted() + DatabaseConnectionDialog + accept() + + + 227 + 282 + + + 157 + 274 + + + + + buttonBox + rejected() + DatabaseConnectionDialog + reject() + + + 295 + 288 + + + 286 + 274 + + + + + diff --git a/management-gui/main.cpp b/management-gui/main.cpp new file mode 100644 index 0000000..24a397b --- /dev/null +++ b/management-gui/main.cpp @@ -0,0 +1,11 @@ +#include +#include "mainwindow.h" + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + MainWindow w; + w.show(); + + return a.exec(); +} diff --git a/management-gui/mainwindow.cpp b/management-gui/mainwindow.cpp new file mode 100644 index 0000000..18d928c --- /dev/null +++ b/management-gui/mainwindow.cpp @@ -0,0 +1,112 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" + +#include +#include +#include + +#include "mylist.h" +#include "database.h" + +#include + +using namespace LocalMyList; + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow) +{ + ui->setupUi(this); + + + connect(MyList::instance()->database(), SIGNAL(connected()), this, SLOT(dbConnected())); + connect(MyList::instance()->database(), SIGNAL(disconnected()), this, SLOT(dbDisconnected())); + connect(MyList::instance()->database(), SIGNAL(newPendingRequest()), this, SLOT(handleNotification())); + + databaseConnectionStatusIndicator = new QLabel(MyList::instance()->database()->isConnected() ? "Connected" : "Disconnected"); + ui->statusBar->addPermanentWidget(databaseConnectionStatusIndicator); + + animeModel = 0; +} + +MainWindow::~MainWindow() +{ + delete ui; +} + +void MainWindow::on_actionConnect_triggered() +{ + MyList::instance()->database()->connect(); +} + +void MainWindow::on_actionDisconnect_triggered() +{ + MyList::instance()->database()->disconnect(); +} + + +void MainWindow::dbConnected() +{ + databaseConnectionStatusIndicator->setText("Connected"); +// animeModel = new QSqlTableModel(this, LocalMyList::instance()->database()->connection()); +// animeModel->setTable("anime"); +// animeModel->select(); +// ui->tableView->setModel(animeModel); +} + +void MainWindow::dbDisconnected() +{ + databaseConnectionStatusIndicator->setText("Disconnected"); +// ui->tableView->setModel(0); +// delete animeModel; + // animeModel = 0; +} + +void MainWindow::handleNotification() +{ + ui->statusBar->showMessage("New Pending Request added!"); +} + +void MainWindow::on_actionScanDirectory_triggered() +{ + QString dir = QFileDialog::getExistingDirectory(this, tr("Scan Directory")); + if (dir.isEmpty()) + return; + MyList::instance()->addDirectory(QDir(dir)); +} + +void MainWindow::on_actionImportMyList_triggered() +{ + QString file = QFileDialog::getOpenFileName(this, tr("Import MyList")); + if (file.isEmpty()) + return; + MyList::instance()->importMyList(file); +} + +void MainWindow::on_actionImportTitles_triggered() +{ + QString file = QFileDialog::getOpenFileName(this, tr("Import Titles")); + if (file.isEmpty()) + return; + MyList::instance()->importTitles(file); +} + +void MainWindow::on_actionClearDatabase_triggered() +{ + MyList::instance()->database()->truncateDatabase(); +} + +void MainWindow::on_actionClearMyListData_triggered() +{ + MyList::instance()->database()->truncateMyListData(); +} + +void MainWindow::on_actionClearAnimeTitleData_triggered() +{ + MyList::instance()->database()->truncateTitleData(); +} + +void MainWindow::on_actionHandlePendingRequests_triggered() +{ + +} diff --git a/management-gui/mainwindow.h b/management-gui/mainwindow.h new file mode 100644 index 0000000..782694b --- /dev/null +++ b/management-gui/mainwindow.h @@ -0,0 +1,48 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include + +namespace Ui { +class MainWindow; +} + +class QSqlTableModel; +class QLabel; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = 0); + ~MainWindow(); + +private slots: + void on_actionConnect_triggered(); + + void dbConnected(); + void dbDisconnected(); + + void handleNotification(); + + void on_actionDisconnect_triggered(); + void on_actionScanDirectory_triggered(); + void on_actionImportTitles_triggered(); + void on_actionClearDatabase_triggered(); + void on_actionClearMyListData_triggered(); + void on_actionClearAnimeTitleData_triggered(); + + void on_actionImportMyList_triggered(); + + void on_actionHandlePendingRequests_triggered(); + +private: + Ui::MainWindow *ui; + + QLabel *databaseConnectionStatusIndicator; + + QSqlTableModel *animeModel; +}; + +#endif // MAINWINDOW_H diff --git a/management-gui/mainwindow.ui b/management-gui/mainwindow.ui new file mode 100644 index 0000000..782796a --- /dev/null +++ b/management-gui/mainwindow.ui @@ -0,0 +1,134 @@ + + + MainWindow + + + + 0 + 0 + 400 + 300 + + + + MainWindow + + + + + + + + + + + + 0 + 0 + 400 + 21 + + + + + Connection + + + + + + + + + Settings + + + + + + &Actions + + + + + + + + + + + + + + + + + + + TopToolBarArea + + + false + + + + + + Connect + + + + + Disconnect + + + + + Quit + + + + + Connection... + + + + + Scan Directory... + + + + + Clear Database + + + + + Clear MyList Data + + + + + Clear Anime Title Data + + + + + Import Titles... + + + + + Import MyList... + + + + + Reset Pending Requests + + + + + + + diff --git a/management-gui/management-gui.pro b/management-gui/management-gui.pro new file mode 100644 index 0000000..402d907 --- /dev/null +++ b/management-gui/management-gui.pro @@ -0,0 +1,24 @@ +#------------------------------------------------- +# +# Project created by QtCreator 2012-02-16T17:36:59 +# +#------------------------------------------------- + +QT += core gui + +TARGET = management-gui +DESTDIR = ../build +TEMPLATE = app + + +SOURCES += main.cpp\ + mainwindow.cpp \ + databaseconnectiondialog.cpp + +HEADERS += mainwindow.h \ + databaseconnectiondialog.h + +FORMS += mainwindow.ui \ + databaseconnectiondialog.ui + +include(../localmylist.pri) diff --git a/play-next/main.cpp b/play-next/main.cpp new file mode 100644 index 0000000..ea73e1f --- /dev/null +++ b/play-next/main.cpp @@ -0,0 +1,81 @@ +#include + +#include +#include +#include +#include + +#include "mylist.h" +#include "database.h" +#include +#include + +using namespace LocalMyList; + +int main(int argc, char *argv[]) +{ + QCoreApplication a(argc, argv); + QTextStream cout(stdout); + if (a.arguments().count() < 2) + { + cout << "Usage: " << a.arguments()[0] << " TITLE"; + return 1; + } + + QStringList args = a.arguments(); + args.removeFirst(); + + QString title = args.join(QChar(' ')); + + QString atitle, etitle; + QString path; + int epno; + { + QSqlQuery q(MyList::instance()->database()->connection()); + q.prepare( + "SELECT fl.path, e.epno, a.title_romaji, e.title FROM file f " + " LEFT JOIN anime a ON f.aid = a.aid " + " LEFT JOIN anime_title at ON f.aid = at.aid " + " LEFT JOIN episode e ON f.eid = e.eid " + " LEFT JOIN file_location fl ON fl.fid = f.fid " + " WHERE f.my_watched IS NULL " + " AND lower(at.title) = :title " + " AND fl.path IS NOT NULL " + "UNION " + "SELECT fl.path, e.epno, a.title_romaji, e.title FROM file f " + " LEFT JOIN anime a ON f.aid = a.aid " + " LEFT JOIN anime_title at ON f.aid = at.aid " + " LEFT JOIN episode e ON f.eid = e.eid " + " LEFT JOIN file_location fl ON fl.fid = f.fid " + " WHERE f.my_watched IS NULL " + " AND at.title ILIKE :title " + " AND fl.path IS NOT NULL " + "GROUP BY fl.path, a.title_romaji, e.epno, e.title " + "ORDER BY title_romaji ASC, epno ASC "); + q.bindValue(":title", title); + + if (!q.exec()) + { + qDebug() << q.lastError(); + return 1; + } + + if (!q.next()) + { + cout << "No file to watch."; + return 1; + } + path = q.value(0).toString(); + epno = q.value(1).toInt(); + atitle = q.value(2).toString(); + etitle = q.value(3).toString(); + } + + cout << "ANIME " << atitle << endl; + cout << "EPISODE " << epno << " - " << etitle << endl; + cout << "Starting player..." << endl; + QProcess::startDetached("D:/_C/aniplayer/build/aniplayer", QStringList() << path); + return 0; + + +} diff --git a/play-next/play-next.pro b/play-next/play-next.pro new file mode 100644 index 0000000..d2e8808 --- /dev/null +++ b/play-next/play-next.pro @@ -0,0 +1,23 @@ +#------------------------------------------------- +# +# Project created by QtCreator 2012-02-26T21:42:52 +# +#------------------------------------------------- + +QT += core + +QT -= gui + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +TARGET = play-next +DESTDIR = ../build +CONFIG += console +CONFIG -= app_bundle + +TEMPLATE = app + + +SOURCES += main.cpp + +include(../localmylist.pri) diff --git a/search-gui/main.cpp b/search-gui/main.cpp new file mode 100644 index 0000000..24a397b --- /dev/null +++ b/search-gui/main.cpp @@ -0,0 +1,11 @@ +#include +#include "mainwindow.h" + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + MainWindow w; + w.show(); + + return a.exec(); +} diff --git a/search-gui/mainwindow.cpp b/search-gui/mainwindow.cpp new file mode 100644 index 0000000..25d8740 --- /dev/null +++ b/search-gui/mainwindow.cpp @@ -0,0 +1,43 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" + +#include +#include +#include "database.h" +#include "mylist.h" + +#include + +using namespace LocalMyList; + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow) +{ + ui->setupUi(this); + MyList::instance(); + + model = new QSqlQueryModel(this); + + ui->resultView->setModel(model); + on_search_textChanged(""); +} + +MainWindow::~MainWindow() +{ + delete ui; +} + +void MainWindow::on_search_textChanged(const QString &text) +{ + + QSqlQuery q( + "SELECT a.aid, b.title AS MainTitle, a.title, a.language, a.type FROM anime_title a" + " LEFT JOIN anime_title b on b.aid = a.aid" + " WHERE lower(a.title) LIKE '%" + text + "%'" + " AND b.type = 1" + " ORDER BY a.title ASC, a.aid ASC", MyList::instance()->database()->connection()); +qDebug() << q.lastError(); + model->setQuery(q); + +} diff --git a/search-gui/mainwindow.h b/search-gui/mainwindow.h new file mode 100644 index 0000000..53b9ccd --- /dev/null +++ b/search-gui/mainwindow.h @@ -0,0 +1,32 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include + +namespace Ui { + class MainWindow; +} + +namespace LocalMyList { + class Database; +} + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = 0); + ~MainWindow(); + +private slots: + void on_search_textChanged(const QString &arg1); + +private: + Ui::MainWindow *ui; + QSqlQueryModel *model; + LocalMyList::Database *db; +}; + +#endif // MAINWINDOW_H diff --git a/search-gui/mainwindow.ui b/search-gui/mainwindow.ui new file mode 100644 index 0000000..b878805 --- /dev/null +++ b/search-gui/mainwindow.ui @@ -0,0 +1,49 @@ + + + MainWindow + + + + 0 + 0 + 400 + 300 + + + + MainWindow + + + + + + + + + + + + + + + 0 + 0 + 400 + 21 + + + + + + TopToolBarArea + + + false + + + + + + + + diff --git a/search-gui/search-gui.pro b/search-gui/search-gui.pro new file mode 100644 index 0000000..ed0ba85 --- /dev/null +++ b/search-gui/search-gui.pro @@ -0,0 +1,21 @@ +#------------------------------------------------- +# +# Project created by QtCreator 2012-02-13T02:45:41 +# +#------------------------------------------------- + +QT += core gui + +TARGET = search-gui +DESTDIR = ../build +TEMPLATE = app + + +SOURCES += main.cpp\ + mainwindow.cpp + +HEADERS += mainwindow.h + +FORMS += mainwindow.ui + +include(../localmylist.pri)