root/src/library.cpp

#include "library.h"
#include "nwutils.h"
#include <QDebug>
#include "server.h"
#include "directoryscanner.h"
#include "tvshowscanner.h"
#include <QStandardPaths>
#include "videofile.h"
#include "config.h"

Library::Library(QString path, QObject *parent) :
    QObject(parent),
    directory(path),
    onlineSync(*this),
    didInit(false),
    mFilter(tvShows, directory),
    searchThread(NULL),
    fileSystemWatcher(NULL)
{
    connect(this, SIGNAL(initDone()), this, SLOT(onInitDone()));
}

Library::~Library() {
    // terminate
    foreach (WallpaperDownload::FetchThread* t, runningWallpaperDownloaders) {
        t->terminate();
    }
    if (searchThread) {
        searchThread->terminate();
    }
    onlineSync.terminate();

    // wait
    foreach (WallpaperDownload::FetchThread* t, runningWallpaperDownloaders) {
        t->wait();
    }
    if (searchThread) {
        searchThread->wait();
    }
    onlineSync.wait();
}

bool Library::initDirectory() const {
    if (!directory.exists() && !QDir::root().mkpath(directory.absolutePath())) {
        qDebug() << "could not create library dir" << directory.absolutePath();
        return false;
    }
    return true;
}

void Library::watchFilesystem() {
    fileSystemWatcher = new QFileSystemWatcher(this);
    connect(fileSystemWatcher, SIGNAL(fileChanged(QString)), this, SLOT(fileChangedInSearchDirectory(QString)));
    connect(fileSystemWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(fileChangedInSearchDirectory(QString)));
}

void Library::initWallpaperDownloaders() {
    addWallpaperDownloader(new Moebooru::Client(("http://konachan.com")));
    addWallpaperDownloader(new Moebooru::Client(("https://yande.re")));
    addWallpaperDownloader(new Gelbooru::Client());
}

void Library::initOnlineSync(const BaseConfig& config) {
    onlineSync.init(config);
    connect(&onlineSync, SIGNAL(allFinished()),
            this, SLOT(fetchingFinished()));
}

bool Library::handleApiRequest(QHttpRequest *req, QHttpResponse *resp) {
    if (req->path().startsWith("/api/library/filter")) {
        return filter().handleApiRequest(req, resp);
    } else if (req->path().startsWith("/api/library/tvshow/")) {
        const int prefixOffset = sizeof("/api/library/tvshow/")-1;
        const QString encodedPath = req->url().path();
        const int commandPathOffset = encodedPath.indexOf('/', prefixOffset);
        QString encodedShowName = encodedPath.mid(prefixOffset, commandPathOffset-prefixOffset);
        QString showName = QUrl::fromPercentEncoding(encodedShowName.toUtf8());


        TvShow* show = existingTvShow(showName);
        if (show) {
            QString commandPath = encodedPath.mid(commandPathOffset, encodedPath.length() - commandPathOffset);

            if (commandPath.startsWith("/dropUrl")) {
                const QUrl droppedUrl = req->url().query(QUrl::FullyDecoded);
                this->onlineSync.handleDropUrl(show, droppedUrl);
            } else {
                show->handleApiRequest(commandPath, req, resp);
            }
        } else {
            Server::simpleWrite(resp, 400, "{\"error\":\"invalid show name\"}");
        }
    } else if (req->path().startsWith("/api/library/toggleWatched")) {
        Episode* episode = filter().getEpisodeForPath(req->url().query(QUrl::FullyDecoded));
        if (episode) {
            episode->setWatched(!episode->getWatched());
            Server::simpleWrite(resp, 200, QString("{\"watched\":%1}").arg(episode->getWatched() ? "true" : "false"), mime::json);
        } else {
            Server::simpleWrite(resp, 400, QString("{\"error\":\"Episode not found\"}"), mime::json);
        }
    } else if (req->path().startsWith("/api/library/movieFileMetaData")) {
        QString path = req->url().query(QUrl::FullyDecoded);
        Episode* ep = filter().getEpisodeForPath(path);
        if (ep) {
            MetaData data = metaDataParser->parse(path);
            Server::simpleWrite(resp, 200, data.toJson(), mime::json);
        }
        Server::simpleWrite(resp, 404, "File for path does not exist, or is not in Library" "text/plain");
    } else if (req->path().startsWith("/api/library/removeFiles") &&
               req->method() == QHttpRequest::HTTP_DELETE) {

        RequestBodyListener* bodyListener = new RequestBodyListener(resp, this);
        connect(req, SIGNAL(data(QByteArray)), bodyListener, SLOT(onDataReceived(QByteArray)));
        connect(bodyListener, SIGNAL(bodyReceived(QHttpResponse*,const QByteArray&)), this, SLOT(onBodyForRemoveFiles(QHttpResponse*,QByteArray)));
    } else if (req->path().startsWith("/api/library/scan")) {
        this->startSearch();
        Server::simpleWrite(resp, 200, "{\"status\":\"new search running\"}");
    } else {
        return false;
    }
    return true;
}

void Library::onBodyForRemoveFiles(QHttpResponse* resp, const QByteArray& body) {
    std::stringstream ss;
    ss << body.data();
    nw::JsonReader jr(ss);
    jr.describeValueArray("paths", -1);
    QStringList paths;
    NwUtils::describeValueArray(jr, "paths", paths);

    foreach (const QString& path, paths) {
        foreach (TvShow* show, this->tvShows) {
            if (show->episodeList().removeFileFromList(path)) {
                if(show->episodeList().hasNoFiles()) {
                    // TODO maybe also remove the directory and it's wallpapers from file system?
                    // right now this will only take it out of the library.json, but keep the $name directory
                    this->tvShows.removeOne(show); // Memory leak, TODO have manged pointers
                }
                break;
            }
        }
    }
    Server::simpleWrite(resp, 200, "removed" "text/plain");
}

TvShow& Library::tvShow(const QString name, bool setQParent) {
    TvShow* show = existingTvShow(name);
    if (show) {
        return *show;
    }
    show = new TvShow(name, setQParent ? this : NULL);
    this->tvShows.push_back(show);
    emit showAdded(show);
    connect(&show->episodeList(), SIGNAL(beforeWatchCountChanged(int,int)), this, SIGNAL(beforeWatchCountChanged(int,int)));
    return *show;
}

// TODO FIXME not actually const, since it returns a pointer to internal state,
// but c++ is a fucking piece of shit and I don't see how to do it without code duplication
TvShow* Library::existingTvShow(const QString name) const {
    foreach (TvShow* show, tvShows) {
        if (show->matchesNameOrSynonym(name)) {
            return show;
        }
    }
    return NULL;
}

const LibraryFilter& Library::filter() const {
    return mFilter;
}

void Library::importTvShowEpisode(QString episodePath) {
    const VideoFile* movieFile = new VideoFile(episodePath);
    if (!movieFile->showName.isEmpty()) {
        TvShow& show = this->tvShow(movieFile->showName);
        show.importVideoFile(movieFile); // takes ownage
    } else {
        qDebug() << "could not import (no show name parsed):" << movieFile->showName;
        delete movieFile;
    }
}

/*
void Library::xbmcLinkExport(QDir outputDir) {
    if (!outputDir.exists()) {
        outputDir.mkpath(".");
    }

    for (QList<TvShow*>::iterator it = tvShows.begin(); it != tvShows.end(); ++it) {
        (*it)->exportXbmcLinks((*it)->directory(outputDir));
    }
}
*/

void Library::fetchMetaData() {
    foreach (TvShow* show, tvShows) {
        onlineSync.addShowToFetch(show);
        onlineSync.addShowToUpdate(show);
    }
}

void Library::startWallpaperDownloaders() {
    if (getWallpaperDownloadRunning()) {
        foreach (WallpaperDownload::FetchThread* ft, runningWallpaperDownloaders) { 
            if (ft->isRunning()) {
                ft->append(filter().all());
            }
        }
        return;
    }
    for (int i=0; i < wallpaperDownloaders.size(); ++i) {
        WallpaperDownload::Client* downloader = wallpaperDownloaders[i];
        WallpaperDownload::FetchThread* ft = new WallpaperDownload::FetchThread(*downloader, filter().all(), directory, this);
        //connect(this, SIGNAL(destroyed()), ft, SLOT(terminate()));
        connect(ft, SIGNAL(finished()), this, SLOT(wallpaperDownloaderFinished()));
        connect(ft, SIGNAL(finished()), ft, SLOT(deleteLater()));
        runningWallpaperDownloaders.push_back(ft);
        ft->start(QThread::LowPriority);
    }
}

void Library::startSearch() {
    if (didInit) {
        startSearch(searchDirectories);
    }
}

void Library::startSearch(const QList<SearchDirectory> dirs) {
    if (this->searchThread) {
        if (this->searchThread->isFinished()) {
            QThread* t = this->searchThread;
            this->searchThread = NULL;
            delete t;
        } else {
            return;
        }
    }
    DirectoryScanner* scanner = new DirectoryScanner();
    scanner->addScanner(new TvShowScanner(*this));
    this->searchThread = new DirectoryScannerThread(scanner, dirs, this);
    connect(searchThread, SIGNAL(done()), this, SIGNAL(searchFinished()));
    connect(searchThread, SIGNAL(machingFile(QString)), this, SLOT(importTvShowEpisode(QString)));
    connect(searchThread, SIGNAL(done()), this, SLOT(startWallpaperDownloaders()));
    connect(searchThread, SIGNAL(done()), this, SLOT(fetchMetaData()));
    this->searchThread->start(QThread::HighPriority);
}

Library::searchStatus Library::getSearchStatus() const {
    if (this->searchThread) {
        return this->searchThread->isRunning() ? inProgress : done;
    }
    return notStarted;
}

bool Library::getWallpaperDownloadRunning() const {
    return !runningWallpaperDownloaders.empty();
}

const QList<SearchDirectory>& Library::getSearchDirectories() const {
    return searchDirectories;
}

bool Library::addSearchDirectory(SearchDirectory dir) {
    if (getSearchDirectory(dir.dir.absolutePath()) == NULL) {
        searchDirectories.append(dir);
        if (fileSystemWatcher) {
            fileSystemWatcher->addPath(dir.dir.absolutePath());
            qDebug() << fileSystemWatcher->directories();
        }
        return true;
    }
    return false;
}

void Library::addWallpaperDownloader(WallpaperDownload::Client* client) {
    wallpaperDownloaders.push_back(client);
    client->setParent(this);
    connect(client, SIGNAL(wallpaperDownloaded(QString)), this, SIGNAL(wallpaperDownloaded(QString)));
}

SearchDirectory *Library::getSearchDirectory(const QString path) {
    for (int i=0; i < searchDirectories.length(); ++i) {
        if (searchDirectories.at(i).dir.absolutePath() == QDir(path).absolutePath()) {
            return &searchDirectories[i];
        }
    }
    return NULL;
}

bool Library::removeSearchDirectory(QString path) {
    for (int i=0; i < searchDirectories.length(); ++i) {
        if (searchDirectories.at(i).dir.absolutePath() == QDir(path).absolutePath()) {
            searchDirectories.removeAt(i);
            return true;
        }
    }
    return false;
}

void Library::generateFrenchises() {
    /*
    foreach (TvShow* show, tvShows) {
        show->syncRelations(*this);
    }
    foreach (const TvShow* show, tvShows) {
        addToFrenchise(show);
    }
    foreach (Franchise* franchise, franchises) {
        franchise->generateName();
    }
    */
}

void Library::onInitDone() {
    didInit = true;
    this->startSearch();
}

void Library::fileChangedInSearchDirectory(QString path) {
    SearchDirectory* sd = getSearchDirectory(path);
    if (sd) {
        startSearch(QList<SearchDirectory>() << SearchDirectory(sd->dir));
    }
}

void Library::onVideoPlaybackEndedNormally(TvShow* show) {
    if (show) {
        this->onlineSync.addShowToUpdate(show);
    }
}

void Library::addToFrenchise(const TvShow* show) {
    bool added = false;
    foreach (Franchise* f, franchises) {
        if (f->hasRelationTo(show)) {
            f->addTvShow(show);
            added = true;
            break;
        }
    }
    if (!added) {
        Franchise* newFranchise = new Franchise(this);
        newFranchise->addTvShow(show);
        franchises.push_back(newFranchise);
    }
}

void Library::fetchingFinished() {
    qDebug() << "finished mal fetching, writing things now";
    generateFrenchises();
    this->write();
    qDebug() << "writing done!";
}

void Library::wallpaperDownloaderFinished() {
    runningWallpaperDownloaders.removeOne(dynamic_cast<WallpaperDownload::FetchThread*>(sender()));
    if (!getWallpaperDownloadRunning()) {
        emit wallpaperDownloadersFinished();
    }
}

bool Library::readAll(bool setQParent) {
    QString filepath = directory.absoluteFilePath("library.json");
    if (!QFile(filepath).exists()) {
        return false;
    }

    nw::JsonReader jr(filepath.toStdString());
    jr.describeArray("searchDirectories", "directory", searchDirectories.length());
    for (int i=0; jr.enterNextElement(i); ++i) {
        QString dirpath;
        bool enabled = false;
        NwUtils::describe(jr, "path", dirpath);
        NwUtils::describe(jr, "enabled", enabled);
        addSearchDirectory(SearchDirectory(dirpath, enabled));
    }

    jr.describeValueArray("tvShows", tvShows.length());
    for (int i=0; jr.enterNextElement(i); ++i) {
        std::string name;
        jr.describeValue(name);
        TvShow& show = tvShow(QString(name.data()), setQParent);

        QDir showDir = show.directory(directory);
        if (!showDir.exists()) {
            qDebug() << "show does not have a directory: " << show.name();
            break;
        }
        show.read(showDir);
    }
    jr.close();
    generateFrenchises();
    return true;
}

void Library::write() {
    if (didInit) {
        this->writeIndex();
        for (TvShow* show : tvShows) {
            this->writeShow(show);
        }
    }
}

bool Library::writeIndex() {
    if (!directory.exists()) {
        return false;
    }

    nw::JsonWriter jw(directory.absoluteFilePath("library.json").toStdString());
    jw.describeArray("searchDirectories", "directory", searchDirectories.length());
    for (int i=0; jw.enterNextElement(i); ++i) {
        QString dirpath = searchDirectories.at(i).dir.path();
        NwUtils::describe(jw, "path", dirpath);
        NwUtils::describe(jw, "enabled", searchDirectories[i].enabled);
    }
    jw.describeValueArray("tvShows", tvShows.length());
    for (int i=0; jw.enterNextElement(i); ++i) {
        TvShow* show = tvShows[i];
        std::string name = show->name().toStdString();
        jw.describeValue(name);
    }
    jw.close();
    return jw.getErrorMessage().empty();
}

bool Library::writeShow(TvShow* show) {
    QDir showDir = show->directory(directory);
    if (!showDir.exists() && !directory.mkdir(showDir.dirName())) {
        qDebug() << "error can not write Library item in: " << showDir.absolutePath();
        return false;
    }
    nw::JsonWriter jw(showDir.absoluteFilePath("tvShow.json").toStdString());
    show->write(jw);
    jw.close();
    return jw.getErrorMessage().empty();
}

MetaDataParser *Library::getMetaDataParser() const {
    return metaDataParser;
}

void Library::setMetaDataParser(MetaDataParser *value) {
    metaDataParser = value;
}

QDir Library::getDirectory() const {
    return directory;
}