]> Some of my projects - localmylist-fs.git/commitdiff
LocalMyList FUSE filesystem.
authorAPTX <marek321@gmail.com>
Sun, 28 Sep 2014 12:30:38 +0000 (14:30 +0200)
committerAPTX <marek321@gmail.com>
Sun, 28 Sep 2014 14:07:58 +0000 (16:07 +0200)
Initial commit.

12 files changed:
conversions.cpp [new file with mode: 0644]
conversions.h [new file with mode: 0644]
lmlfs.cpp [new file with mode: 0644]
lmlfs.h [new file with mode: 0644]
localmylist-fs.pro [new file with mode: 0644]
main.c [new file with mode: 0644]
pathparser.cpp [new file with mode: 0644]
pathparser.h [new file with mode: 0644]
querybuilder.cpp [new file with mode: 0644]
querybuilder.h [new file with mode: 0644]
tabledata.cpp [new file with mode: 0644]
tabledata.h [new file with mode: 0644]

diff --git a/conversions.cpp b/conversions.cpp
new file mode 100644 (file)
index 0000000..8d320ba
--- /dev/null
@@ -0,0 +1,16 @@
+#include "conversions.h"
+
+static const int SLASH = 0x2044;
+
+QString encodeDirectory(QString s)
+{
+       s.replace(QChar('/'), QChar(SLASH));
+       return s;
+}
+
+QString decodeDirectory(QString s)
+{
+       s.replace(QChar(SLASH), QChar('/'));
+       return s;
+
+}
diff --git a/conversions.h b/conversions.h
new file mode 100644 (file)
index 0000000..c8aaea6
--- /dev/null
@@ -0,0 +1,9 @@
+#ifndef CONVERSIONS_H
+#define CONVERSIONS_H
+
+#include <QString>
+
+QString encodeDirectory(QString s);
+QString decodeDirectory(QString s);
+
+#endif // CONVERSIONS_H
diff --git a/lmlfs.cpp b/lmlfs.cpp
new file mode 100644 (file)
index 0000000..0ff0354
--- /dev/null
+++ b/lmlfs.cpp
@@ -0,0 +1,244 @@
+#include "lmlfs.h"
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+
+#include <QMutexLocker>
+#include <QThread>
+#include <LocalMyList/MyList>
+#include <LocalMyList/Database>
+#include <LocalMyList/RaiiMyList>
+
+#include "tabledata.h"
+#include "pathparser.h"
+#include "querybuilder.h"
+#include "conversions.h"
+
+#include <QDebug>
+
+static const QMap<PathParser::PartType, LocalMyList::OpenFileData (LocalMyList::Database::*)(int)> ofdMapping = []() {
+       QMap<PathParser::PartType, LocalMyList::OpenFileData(LocalMyList::Database::*)(int)> m;
+       m[PathParser::FirstUnwatchedPart] = &LocalMyList::Database::firstUnwatchedByAid;
+       m[PathParser::BestFilePart] = &LocalMyList::Database::openFileByEid;
+       m[PathParser::BestLocationPart] = &LocalMyList::Database::openFile;
+       return m;
+}();
+
+int lmlfs_getattr(const char *path, struct stat *stbuf)
+{
+       memset(stbuf, 0, sizeof(struct stat));
+
+       PathParser p;
+
+       if (!p.parse(path))
+               return -ENOENT;
+
+       switch (p.lastPart()) {
+               case PathParser::RootPart:
+               case PathParser::TablePart:
+               case PathParser::ColumnPart:
+               case PathParser::ValuePart:
+               case PathParser::EntriesPart:
+               case PathParser::AnimeEntry:
+               case PathParser::EpisodeEntry:
+               case PathParser::FileEntry:
+               case PathParser::MetadataPart:
+                       stbuf->st_mode = S_IFDIR | 0555;
+                       break;
+               case PathParser::MetadataEntryPart:
+                       stbuf->st_mode = S_IFREG | 0444;
+                       break;
+               case PathParser::FirstUnwatchedPart:
+               case PathParser::BestFilePart:
+               case PathParser::BestLocationPart:
+                       stbuf->st_mode = S_IFLNK | 0444;
+                       break;
+               default:
+                       return -ENOENT;
+       }
+       return 0;
+}
+
+int lmlfs_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi)
+{
+       (void)offset;
+       (void)fi;
+
+       PathParser p;
+
+       if (!p.parse(path))
+               return -ENOENT;
+
+       switch (p.lastPart()) {
+               case PathParser::RootPart:
+                       for (const QString &table : tables)
+                               filler(buf, table.toUtf8().data(), NULL, 0);
+                       break;
+               case PathParser::TablePart:
+                       for (const QString &column : table_columns[p.lastTable()])
+                               filler(buf, column.toUtf8().data(), NULL, 0);
+                       filler(buf, Token::Entries.latin1(), NULL, 0);
+                       break;
+               case PathParser::ColumnPart: {
+                       QueryBuilder qb;
+                       qb.buildQuery(p);
+
+                       if (!qb.isValid()) return -ENOENT;
+
+                       LocalMyList::RaiiMyList lml;
+                       if (!lml) return -ENOTCONN;
+                       if (!lml.connected()) return -ENOTCONN;
+
+                       QSqlQuery q = LocalMyList::instance()->database()->prepareOneShot(qb.query());
+
+                       if (!LocalMyList::instance()->database()->exec(q))
+                               return -ENOENT;
+
+                       while (q.next()) {
+                               QString value = q.value(0).toString().trimmed();
+                               if (value.isEmpty())
+                                       continue;
+                               filler(buf, encodeDirectory(value).toUtf8().data(), NULL, 0);
+                       }
+                       q.finish();
+               } break;
+               case PathParser::ValuePart:
+                       for (const QString &table : join_map[p.lastTable()].keys())
+                               filler(buf, table.toUtf8().data(), NULL, 0);
+                       for (const QString &table : table_columns[p.lastTable()])
+                               filler(buf, table.toUtf8().data(), NULL, 0);
+                       filler(buf, Token::Entries.latin1(), NULL, 0);
+                       break;
+               case PathParser::EntriesPart: {
+                       QueryBuilder qb;
+                       qb.buildQuery(p);
+
+                       if (!qb.isValid()) return -ENOENT;
+
+                       LocalMyList::RaiiMyList lml;
+                       if (!lml) return -ENOTCONN;
+                       if (!lml.connected()) return -ENOTCONN;
+
+                       QSqlQuery q = LocalMyList::instance()->database()->prepareOneShot(qb.query());
+
+                       if (!LocalMyList::instance()->database()->exec(q))
+                               return -ENOENT;
+
+                       while (q.next()) {
+                               QString value = q.value(0).toString().trimmed();
+                               if (value.isEmpty())
+                                       continue;
+                               filler(buf, encodeDirectory(value).toUtf8().data(), NULL, 0);
+                       }
+                       q.finish();
+               } break;
+               case PathParser::AnimeEntry:
+                       filler(buf, Token::FirstUnwatched.latin1(), NULL, 0);
+                       filler(buf, Token::Metadata.latin1(), NULL, 0);
+                       for (const QString &table : tables)
+                               filler(buf, table.toUtf8().data(), NULL, 0);
+                       break;
+               case PathParser::EpisodeEntry:
+                       filler(buf, Token::BestFile.latin1(), NULL, 0);
+                       filler(buf, Token::Metadata.latin1(), NULL, 0);
+                       for (const QString &table : join_map[p.lastTable()].keys())
+                               filler(buf, table.toUtf8().data(), NULL, 0);
+                       break;
+               case PathParser::FileEntry:
+                       filler(buf, Token::BestLocation.latin1(), NULL, 0);
+                       filler(buf, Token::Metadata.latin1(), NULL, 0);
+                       for (const QString &table : join_map[p.lastTable()].keys())
+                               filler(buf, table.toUtf8().data(), NULL, 0);
+                       break;
+               default:
+                       return -ENOENT;
+       }
+
+       filler(buf, ".", NULL, 0);
+       filler(buf, "..", NULL, 0);
+       return 0;
+}
+
+int lmlfs_readlink(const char *path, char *buf, size_t size)
+{
+       PathParser p;
+
+       if (!p.parse(path)) return -ENOENT;
+
+       LocalMyList::RaiiMyList lml;
+       if (!lml) return -ENOTCONN;
+
+       if (!lml.connected()) return -ENOTCONN;
+
+       QueryBuilder qb;
+       qb.buildQuery(p);
+
+       *buf = '\0';
+
+       switch (p.lastPart()) {
+               case PathParser::FirstUnwatchedPart:
+               case PathParser::BestFilePart:
+               case PathParser::BestLocationPart: {
+                       if (!qb.isValid()) return -ENOENT;
+                       QSqlQuery q = LocalMyList::instance()->database()->prepareOneShot(qb.query());
+
+                       if (!LocalMyList::instance()->database()->exec(q)) return -ENOENT;
+                       if (!q.next()) return -ENOENT;
+
+                       int aid = q.value(0).toInt();
+
+                       if (!aid) return -ENOENT;
+
+                       LocalMyList::OpenFileData ofd = (LocalMyList::instance()->database()->*ofdMapping[p.lastPart()])(aid);
+                       if (!ofd.fid) return 0;
+
+                       qstrncpy(buf, ofd.localPath.toUtf8().data(), size);
+                       return 0;
+               } break;
+               default:
+                       break;
+       }
+       return -ENOENT;
+}
+
+int lmlfs_open(const char *path, struct fuse_file_info *fi)
+{
+       // Currently opening files is not allowed
+       (void)path;
+       if ((fi->flags & 3) != O_RDONLY)
+               return -EACCES;
+       return -EACCES;
+}
+
+int lmlfs_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi)
+{
+       Q_UNUSED(path);
+       Q_UNUSED(buf);
+       Q_UNUSED(size);
+       Q_UNUSED(offset);
+       Q_UNUSED(fi);
+       return 0;
+}
+
+void dbg(const char *path)
+{
+       PathParser p;
+       p.parse(path);
+
+       qDebug() << "==========================================";
+       qDebug() << "valid path =" << p.isValid();
+       qDebug() << "lastPart =" << p.lastPart();
+       qDebug() << "properties =" << p.properties();
+
+       if (!p.isValid())
+               return;
+       QueryBuilder qb;
+
+       qb.buildQuery(p);
+
+       qDebug() << "valid query =" << qb.isValid();
+       if (!qb.isValid())
+               return;
+       qDebug() << "query =\n" << qb.query();
+}
diff --git a/lmlfs.h b/lmlfs.h
new file mode 100644 (file)
index 0000000..79d3a00
--- /dev/null
+++ b/lmlfs.h
@@ -0,0 +1,20 @@
+#ifndef LMLFS_H
+#define LMLFS_H
+#include <fuse.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+int lmlfs_getattr(const char *path, struct stat *stbuf);
+int lmlfs_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi);
+int lmlfs_readlink(const char *path, char *buf, size_t size);
+int lmlfs_open(const char *path, struct fuse_file_info *fi);
+int lmlfs_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi);
+
+void dbg(const char *path);
+
+#ifdef __cplusplus
+}
+#endif
+#endif // LMLFS_H
diff --git a/localmylist-fs.pro b/localmylist-fs.pro
new file mode 100644 (file)
index 0000000..b0dbdd5
--- /dev/null
@@ -0,0 +1,25 @@
+TEMPLATE = app
+CONFIG += console
+CONFIG -= app_bundle
+CONFIG += debug
+QT -= gui widgets
+QT *= sql
+QMAKE_CXXFLAGS += -std=c++11
+
+SOURCES += main.c \
+       lmlfs.cpp \
+       querybuilder.cpp \
+       tabledata.cpp \
+       pathparser.cpp \
+    conversions.cpp
+LIBS += -lfuse -llocalmylist
+TARGET = lmlfs
+
+DEFINES += _FILE_OFFSET_BITS=64
+
+HEADERS += \
+       lmlfs.h \
+       querybuilder.h \
+       tabledata.h \
+       pathparser.h \
+    conversions.h
diff --git a/main.c b/main.c
new file mode 100644 (file)
index 0000000..4e039a3
--- /dev/null
+++ b/main.c
@@ -0,0 +1,25 @@
+#define FUSE_USE_VERSION 30
+#include <fuse.h>
+#include <string.h>
+#include "lmlfs.h"
+
+static struct fuse_operations lmlfs_oper = {
+       .getattr = lmlfs_getattr,
+       .readdir = lmlfs_readdir,
+       .readlink = lmlfs_readlink,
+       .open = lmlfs_open,
+       .read = lmlfs_read,
+};
+
+int main(int argc, char *argv[])
+{
+       int i;
+       for (i = 0; i < argc; ++i) {
+               if (strcmp(argv[i], "--dbg") == 0) {
+                       dbg(argv[argc - 1]);
+                       return 1;
+               }
+       }
+
+       return fuse_main(argc, argv, &lmlfs_oper, NULL);
+}
diff --git a/pathparser.cpp b/pathparser.cpp
new file mode 100644 (file)
index 0000000..d0c2fba
--- /dev/null
@@ -0,0 +1,180 @@
+#include "pathparser.h"
+#include <QStringList>
+#include "tabledata.h"
+#include "conversions.h"
+
+#include <QDebug>
+
+PathParser::PathParser() : m_valid{false}
+{
+}
+
+bool PathParser::parse(const char *rawPath)
+{
+       m_path = QString::fromUtf8(rawPath);
+       QStringList parts = m_path.split(QChar('/'), QString::SkipEmptyParts);
+
+       m_lastPart = RootPart;
+       qDebug() << "parse " << parts;
+
+       for (int i = 0; i < parts.length(); ++i) {
+               const QString &part = parts[i];
+
+               qDebug() << "----------------------- Iteration" << i << "-----------------------";
+               qDebug() << "lastPart =" << m_lastPart;
+               qDebug() << "part(" << part.length() << ") =" << part;
+
+               switch (m_lastPart) {
+                       case RootPart:
+                               // only "table"
+                               if (!tables.contains(part)) {
+                                       m_valid = false;
+                                       return m_valid;
+                               }
+                               m_properties.insert(part, PropertyMap{});
+                               m_lastTable = part;
+                               m_lastPart = TablePart;
+                               break;
+                       case TablePart:
+                               // either "entries" or "column"
+                               if (part == Token::Entries) {
+                                       m_lastPart = EntriesPart;
+                               } else if (table_columns[m_lastTable].contains(part)) {
+                                       m_lastPart = ColumnPart;
+                                       m_lastColumn = part;
+                               } else {
+                                       m_valid = false;
+                                       return m_valid;
+                               }
+                               break;
+                       case ColumnPart:
+                               // only "value"
+                               m_properties[m_lastTable].insert(m_lastColumn, decodeDirectory(part));
+                               m_lastPart = ValuePart;
+                               break;
+                       case ValuePart:
+                               // either "entries", "table" or "column"
+                               if (part == Token::Entries) {
+                                       m_lastPart = EntriesPart;
+                               } else if (join_map[m_lastTable].keys().contains(part)) {
+                                       m_lastPart = TablePart;
+                                       m_lastTable = part;
+                               } else if (table_columns[m_lastTable].contains(part)) {
+                                       m_lastPart = ColumnPart;
+                                       m_lastColumn = part;
+                               } else {
+                                       m_valid = false;
+                                       return m_valid;
+                               }
+                               break;
+                       case EntriesPart:
+                               // Only an "entry" can be in an entry list, the type depends on the last table
+                               if (m_lastTable == Token::AnimeTable)
+                                       m_lastPart = AnimeEntry;
+                               else if (m_lastTable == Token::EpisodeTable)
+                                       m_lastPart = EpisodeEntry;
+                               else if (m_lastTable == Token::FileTable)
+                                       m_lastPart = FileEntry;
+
+                               // Since all entries in "entries" are unique all other conditions
+                               // from the last table can be replaced with this one
+                               // TODO this is not true with most of the main columns
+                               // m_properties[m_lastTable].clear();
+                               m_properties[m_lastTable].insert(table_main_column[m_lastTable], decodeDirectory(part));
+                               break;
+                       case AnimeEntry:
+                               // same as "root" part, but may also contain "metadata" or "first_unwatched"
+                               if (part == Token::Metadata) {
+                                       m_lastPart = MetadataPart;
+                               } else if (part == Token::FirstUnwatched) {
+                                       m_lastPart = FirstUnwatchedPart;
+                               } else if (join_map[m_lastTable].keys().contains(part)) {
+                                       m_lastPart = TablePart;
+                                       m_lastTable = part;
+                               } else {
+                                       m_valid = false;
+                                       return m_valid;
+                               }
+                               break;
+                       case EpisodeEntry:
+                               // same as "root" part, but may also contain "metadata" or "best_file"
+                               if (part == Token::Metadata) {
+                                       m_lastPart = MetadataPart;
+                               } else if (part == Token::BestFile) {
+                                       m_lastPart = BestFilePart;
+                               } else if (join_map[m_lastTable].keys().contains(part)) {
+                                       m_lastPart = TablePart;
+                                       m_lastTable = part;
+                               } else {
+                                       m_valid = false;
+                                       return m_valid;
+                               }
+                               break;
+                       case FileEntry:
+                               // same as "root" part, but may also contain "metadata" or "best_location"
+                               if (part == Token::Metadata) {
+                                       m_lastPart = MetadataPart;
+                               } else if (part == Token::BestLocation) {
+                                       m_lastPart = BestLocationPart;
+                               } else if (join_map[m_lastTable].keys().contains(part)) {
+                                       m_lastPart = TablePart;
+                                       m_lastTable = part;
+                               } else {
+                                       m_valid = false;
+                                       return m_valid;
+                               }
+                               break;
+                       case MetadataPart:
+                               // Only contains metadata entries (files with values of all columns of the last table)
+                               if (table_columns[m_lastTable].contains(part)) {
+                                       m_lastPart = MetadataEntryPart;
+                                       m_lastColumn = part;
+                               } else {
+                                       m_valid = false;
+                                       return m_valid;
+                               }
+                               break;
+                       // These are terminators, there can not be anything after those in a path
+                       case MetadataEntryPart:
+                       case FirstUnwatchedPart:
+                       case BestFilePart:
+                       case BestLocationPart:
+                       default:
+                               m_valid = false;
+                               return m_valid;
+                               break;
+               }
+       }
+       m_valid = true;
+       return m_valid;
+}
+
+bool PathParser::isValid() const
+{
+       return m_valid;
+}
+
+PathParser::PartType PathParser::lastPart() const
+{
+       return m_lastPart;
+}
+
+QString PathParser::lastTable() const
+{
+       return m_lastTable;
+}
+
+QString PathParser::lastColumn() const
+{
+       return m_lastColumn;
+}
+
+QString PathParser::path() const
+{
+       return m_path;
+}
+
+TablePropertyMap PathParser::properties() const
+{
+       return m_properties;
+}
diff --git a/pathparser.h b/pathparser.h
new file mode 100644 (file)
index 0000000..8d3f885
--- /dev/null
@@ -0,0 +1,50 @@
+#ifndef PATHPARSER_H
+#define PATHPARSER_H
+
+#include <QString>
+#include <QMap>
+
+typedef QMap<QString, QString> PropertyMap;
+typedef QMap<QString, QMap<QString, QString>> TablePropertyMap;
+
+class PathParser
+{
+public:
+       enum PartType {
+               RootPart,
+               TablePart,
+               ColumnPart,
+               ValuePart,
+               EntriesPart,
+               AnimeEntry,
+               EpisodeEntry,
+               FileEntry,
+               MetadataPart,
+               MetadataEntryPart,
+               FirstUnwatchedPart,
+               BestFilePart,
+               BestLocationPart
+       };
+
+       PathParser();
+
+       bool parse(const char *rawPath);
+
+       bool isValid() const;
+       PartType lastPart() const;
+       QString lastTable() const;
+       QString lastColumn() const;
+
+       QString path() const;
+       TablePropertyMap properties() const;
+
+private:
+       QString m_path;
+       TablePropertyMap m_properties;
+       PartType m_lastPart;
+       QString m_lastTable;
+       QString m_lastColumn;
+       bool m_valid;
+};
+
+#endif // PATHPARSER_H
diff --git a/querybuilder.cpp b/querybuilder.cpp
new file mode 100644 (file)
index 0000000..b77f7c2
--- /dev/null
@@ -0,0 +1,106 @@
+#include "querybuilder.h"
+
+#include <QRegExp>
+#include <QDebug>
+#include "tabledata.h"
+
+QueryBuilder::QueryBuilder() : m_valid{false}
+{
+}
+
+bool QueryBuilder::buildQuery(const PathParser &path)
+{
+       m_valid = true;
+       if (!path.isValid()) {
+               m_valid = false;
+               return m_valid;
+       }
+
+       const TablePropertyMap &properties = path.properties();
+       QString sql;
+
+       switch (path.lastPart()) {
+               // Get all possible values for a column
+               case PathParser::ColumnPart:
+                       sql = QString("SELECT DISTINCT %1.%2\n\tFROM %1\n").arg(path.lastTable()).arg(path.lastColumn());
+                       break;
+               case PathParser::EntriesPart:
+                       sql = QString("SELECT DISTINCT %1.%2\n\tFROM %1\n").arg(path.lastTable()).arg(table_main_column[path.lastTable()]);
+                       break;
+               case PathParser::FirstUnwatchedPart:
+                       sql = QString("SELECT DISTINCT %1.aid\n\tFROM %1\n").arg(path.lastTable());
+                       break;
+               case PathParser::BestFilePart:
+                       sql = QString("SELECT DISTINCT %1.eid\n\tFROM %1\n").arg(path.lastTable());
+                       break;
+               case PathParser::BestLocationPart:
+                       // This is currently dumb, but the idea is to move away from fid as the main display table
+                       sql = QString("SELECT DISTINCT %1.fid\n\tFROM %1\n").arg(path.lastTable());
+                       break;
+               default:
+                       m_valid = false;
+                       return m_valid;
+                       break;
+       }
+
+       for (const QString &table : properties.keys()) {
+               if (table == path.lastTable())
+                       continue;
+
+               sql += buildJoin(table, path.lastTable(), properties[table]);
+       }
+       if (properties.keys().contains(path.lastTable())) {
+               sql += buildWhere(path.lastTable(), properties[path.lastTable()]);
+       }
+
+       m_query = sql;
+       m_valid = true;
+       return m_valid;
+}
+
+bool QueryBuilder::isValid() const
+{
+       return m_valid;
+}
+
+QString QueryBuilder::query() const
+{
+       return m_query;
+}
+
+QString QueryBuilder::buildJoin(const QString &table, const QString &lastTable, const PropertyMap &properties) const
+{
+       QString sql = QString{"\tJOIN %1 ON (%2 AND true\n%3\t)\n"}.arg(table).arg(join_map[lastTable][table]).arg(buildCondition(table, properties));
+       return sql;
+}
+
+QString QueryBuilder::buildWhere(const QString &table, const PropertyMap &properties) const
+{
+       if (properties.isEmpty())
+               return QString();
+
+       QString sql = "\tWHERE true\n" + buildCondition(table, properties);
+       return sql;
+}
+
+QString QueryBuilder::buildCondition(const QString &table, const PropertyMap &properties) const
+{
+       if (properties.isEmpty())
+               return QString();
+
+       QString sql;
+       for (auto it = properties.constBegin(); it != properties.constEnd(); ++it) {
+               sql += QString("\t\tAND %1.%2 = %3\n").arg(table).arg(it.key()).arg(escape(it.value()));
+       }
+       return sql;
+}
+
+QString QueryBuilder::escape(QString value) const
+{
+       QRegExp rx{"^[0-9]+(\\.[0-9]*)$"};
+       if (value.contains(rx)) {
+               return value;
+       }
+       // TODO real escaping
+       return QChar{'\''} + value.replace(QChar{'\''}, "") + QChar{'\''};
+}
diff --git a/querybuilder.h b/querybuilder.h
new file mode 100644 (file)
index 0000000..1c24750
--- /dev/null
@@ -0,0 +1,27 @@
+#ifndef QUERYBUILDER_H
+#define QUERYBUILDER_H
+
+#include "pathparser.h"
+
+class QueryBuilder
+{
+public:
+       QueryBuilder();
+
+       bool buildQuery(const PathParser &path);
+
+       bool isValid() const;
+       QString query() const;
+
+private:
+       QString buildJoin(const QString &table, const QString &lastTable, const PropertyMap &properties) const;
+       QString buildWhere(const QString &table, const PropertyMap &properties) const;
+       QString buildCondition(const QString &table, const PropertyMap &properties) const;
+
+       QString escape(QString value) const;
+
+       QString m_query;
+       bool m_valid;
+};
+
+#endif // QUERYBUILDER_H
diff --git a/tabledata.cpp b/tabledata.cpp
new file mode 100644 (file)
index 0000000..9167246
--- /dev/null
@@ -0,0 +1,123 @@
+#include "tabledata.h"
+
+namespace Token {
+const QLatin1String AnimeTable{"anime"};
+const QLatin1String EpisodeTable{"episode"};
+const QLatin1String FileTable{"file"};
+const QLatin1String Entries{"entries"};
+const QLatin1String Metadata{"metadata"};
+const QLatin1String FirstUnwatched{"fisrt_unwatched"};
+const QLatin1String BestFile{"best_file"};
+const QLatin1String BestLocation{"best_location"};
+}
+
+// TODO all of these shouldbe generated.
+const QStringList tables = QStringList()
+// TODO anime_title is currently not supported
+//             << "anime_title"
+               << "anime"
+               << "episode"
+               << "file";
+
+const QMap<QString, QString> table_main_column = []() {
+       QMap<QString, QString> r;
+       r["anime"] = "title_romaji";
+       r["episode"] = "title_english";
+       r["file"] = "fid";
+       return r;
+}();
+
+const QMap<QString, QStringList> table_columns = []() {
+       QMap<QString, QStringList> r;
+       r["anime"] = QStringList()
+               << "aid"
+               << "entry_added"
+               << "anidb_update"
+               << "entry_update"
+               << "my_update"
+               << "title_english"
+               << "title_romaji"
+               << "title_kanji"
+               << "description"
+               << "year"
+               << "start_date"
+               << "end_date"
+               << "type"
+               << "total_episode_count"
+               << "highest_epno"
+               << "rating"
+               << "votes"
+               << "temp_rating"
+               << "temp_votes"
+               << "my_vote"
+               << "my_vote_date"
+               << "my_temp_vote"
+               << "my_temp_vote_date";
+       r["episode"] = QStringList()
+               << "eid"
+               << "aid"
+               << "entry_added"
+               << "anidb_update"
+               << "entry_update"
+               << "my_update"
+               << "epno"
+               << "title_english"
+               << "title_romaji"
+               << "title_kanji"
+               << "length"
+               << "airdate"
+               << "state"
+               << "type"
+               << "recap"
+               << "rating"
+               << "votes"
+               << "my_vote"
+               << "my_vote_date";
+       r["file"] = QStringList()
+               << "fid"
+               << "eid"
+               << "aid"
+               << "gid"
+               << "lid"
+               << "entry_added"
+               << "anidb_update"
+               << "entry_update"
+               << "my_update"
+               << "ed2k"
+               << "size"
+               << "length"
+               << "extension"
+               << "group_name"
+               << "group_name_short"
+               << "crc"
+               << "release_date"
+               << "version"
+               << "censored"
+               << "deprecated"
+               << "source"
+               << "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";
+       return r;
+}();
+
+const QMap<QString, QMap<QString, QString>> join_map = []() {
+       QMap<QString, QMap<QString, QString>> r;
+       r["anime"]["episode"] = "anime.aid = episode.aid";
+       r["anime"]["file"] = "anime.aid = file.aid";
+       r["episode"]["file"] = "anime.eid = episode.eid";
+       r["episode"]["anime"] = "episode.aid = anime.aid";
+       r["file"]["anime"] = "file.aid = anime.aid";
+       r["file"]["episode"] = "file.eid = episode.eid";
+       return r;
+}();
diff --git a/tabledata.h b/tabledata.h
new file mode 100644 (file)
index 0000000..d055d7b
--- /dev/null
@@ -0,0 +1,24 @@
+#ifndef TABLEDATA_H
+#define TABLEDATA_H
+
+#include <QMap>
+#include <QStringList>
+#include <QString>
+
+namespace Token {
+extern const QLatin1String AnimeTable;
+extern const QLatin1String EpisodeTable;
+extern const QLatin1String FileTable;
+extern const QLatin1String Entries;
+extern const QLatin1String Metadata;
+extern const QLatin1String FirstUnwatched;
+extern const QLatin1String BestFile;
+extern const QLatin1String BestLocation;
+}
+
+extern const QStringList tables;
+extern const QMap<QString, QStringList> table_columns;
+extern const QMap<QString, QString> table_main_column;
+extern const QMap<QString, QMap<QString, QString>> join_map;
+
+#endif // TABLEDATA_H