Initial commit.
--- /dev/null
+#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;
+
+}
--- /dev/null
+#ifndef CONVERSIONS_H
+#define CONVERSIONS_H
+
+#include <QString>
+
+QString encodeDirectory(QString s);
+QString decodeDirectory(QString s);
+
+#endif // CONVERSIONS_H
--- /dev/null
+#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();
+}
--- /dev/null
+#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
--- /dev/null
+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
--- /dev/null
+#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);
+}
--- /dev/null
+#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;
+}
--- /dev/null
+#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
--- /dev/null
+#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{'\''};
+}
--- /dev/null
+#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
--- /dev/null
+#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;
+}();
--- /dev/null
+#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