#include "mainscreen.h" #include "html.hpp" #include #include #include #include #include #include #include #include #include MainScreen::MainScreen(std::string search_term, QObject *parent) : QObject{parent} { tags_file_path = std::filesystem::path( QStandardPaths::writableLocation (QStandardPaths::AppLocalDataLocation).toStdString()); if(!std::filesystem::exists(tags_file_path)) { qDebug() << "Created dir."; std::filesystem::create_directories(tags_file_path); } else { qDebug() << "It already exists."; } tags_file_path = std::filesystem::path(tags_file_path) / "tags.json"; qDebug() << "Using tags file location: " << tags_file_path; if(search_term == "") { renderHomePage(); } else { renderSearchPage(search_term); } } void MainScreen::resumeOldTorrentSession(SearchItem it) { qDebug() << "Resuming session: " << it.title; qDebug() << "Old download path: " << it.download_path.string(); TorrentSession *session = new TorrentSession(it, it.download_path); auto pid = session->start(); if(pid != 0) { session->deleteLater(); qDebug() << "Parent done."; } } void MainScreen::renderSearchPage(std::string search_term) { if(!manager) { manager = new QNetworkAccessManager(this); } disconnect(manager, nullptr, nullptr, nullptr); QNetworkRequest req; QUrl req_url; req_url.setUrl("https://nyaa.si/"); QUrlQuery Q{{"f","0"},{"c","0_0"},{"s", "seeders"},{"o","desc"}}; Q.addQueryItem("q", QString::fromStdString(search_term)); req_url.setQuery(Q); req.setUrl(req_url); connect(manager, &QNetworkAccessManager::finished, this, &MainScreen::renderSearchPage_); manager->get(req); } void MainScreen::renderHomePage() { qDebug() << "Render Home Page"; if(!socket) { socket = new Socket(); socket_thread = new QThread(this); socket->moveToThread(socket_thread); connect(this, &MainScreen::startSocket, socket, &Socket::start); connect(socket, &Socket::launchTorrentSession, this, &MainScreen::resumeOldTorrentSession); socket_thread->start(); emit startSocket(); } } std::vector MainScreen::parseSearchPage(std::string html_page) { html::parser p; auto root = p.parse(html_page); qDebug() << root->size() << " elements."; auto titles = root->select("div>table>tbody>tr"); qDebug() << "Found rows: " << titles.size(); std::vector results; for(auto i: titles) { /* * title -> name I see in the selection. * url -> url for it's nyaa page. * seeders * leachers * downloads * timestamp * magnet */ auto g = i->select("td>a[title]"); SearchItem result; for(auto j: g) { if(j->has_attr("class") || j->select("img").size() > 0) { continue; } result.title = QString::fromStdString(j->get_attr("title")); result.url = QUrl(QString::fromStdString(j->get_attr("href"))); } g = i->select("a[href*='magnet:?']"); if(g.size() == 0 || g.size() > 1) { continue; } result.magnet = QString::fromStdString(g[0]->get_attr("href")); // Asuming 4th column is size // 5th is timestamp // 6th is seeders // 7th is leachers // 8th is total downloads g = i->select("td[data-timestamp]"); if(g.size() > 0) result.timestamp = QString::fromStdString(g[0]->get_attr("data-timestamp")); g = i->select("td"); if(g.size() >= 8) { result.size = QString::fromStdString(g[3]->to_text()); result.seeders = QString::fromStdString(g[5]->to_text()); result.leechers = QString::fromStdString(g[6]->to_text()); result.downloads = QString::fromStdString(g[7]->to_text()); } results.push_back(result); } return results; } std::vector MainScreen::getStringEntries(std::vector res) { std::vector dat; for(auto &i: res) { dat.push_back(i.title.toStdString()); } return dat; } //SearchMenu is rendered here. int MainScreen::displaySearchMenu(std::vector res) { using namespace ftxui; std::vector entries; for (const auto& f : res) { entries.push_back(f.title.toStdString()); } int selected = 0; bool exit_flag = false; auto screen = ScreenInteractive::TerminalOutput(); MenuOption option; std::string status_message = "Use arrows to navigate. Press Enter to select, 'q' to quit."; option.on_enter = [&] { screen.Exit(); status_message = "You selected: " + res[selected].title.toStdString(); }; option.entries_option.transform = [&res](const EntryState& state) { const auto& file = res[state.index]; auto row = hbox({ text(file.title.toStdString()) | flex, text(file.size.toStdString()) | size(WIDTH, EQUAL, 10), text(file.timestamp.toStdString()) | size(WIDTH, EQUAL, 15), text(file.seeders.toStdString()) | size(WIDTH, EQUAL, 10), text(file.leechers.toStdString()) | size(WIDTH, EQUAL, 10), text(file.downloads.toStdString()) | size(WIDTH, EQUAL, 15), }); if (state.active) { row = row | inverted | bold; } return row; }; auto menu = Menu(&entries, &selected, option); auto renderer = Renderer(menu, [&] { Element header = hbox({ text(" TITLE") | flex, text("SIZE") | size(WIDTH, EQUAL, 12), text("DATE") | size(WIDTH, EQUAL, 12), text("SEEDERS") | size(WIDTH, EQUAL, 12), text("LEECHERS") | size(WIDTH, EQUAL, 12), text("DOWNLOADS") | size(WIDTH, EQUAL, 12), }) | underlined; return window(text(" Search Results "), vbox({ header, separatorEmpty(), menu->Render() }) ) | size(WIDTH, GREATER_THAN, 50); }); auto container = CatchEvent(renderer, [&](Event event) { // Checking for 'q', 'Q', or Escape if (event == Event::Character("q") || event == Event::Character("Q") || event == Event::Escape) { screen.Exit(); exit_flag = true; return true; } return false; }); screen.Loop(container); if(exit_flag) selected = -1; qDebug() << "Selected element = " << selected; return selected; } //TorrentSession is created here. int MainScreen::startDownloadIndex(std::vector res, int index) { qDebug() << "Enter Download Path: "; auto expected_path = runBasicShell(res[index]); if(!expected_path.has_value()) { return -1; } auto download_path = expected_path.value(); res[index].download_path = download_path; TorrentSession *session = new TorrentSession(res[index], download_path); auto pid = session->start(); if(pid != 0) { session->deleteLater(); qDebug() << "Parent done."; } return pid; } std::expected MainScreen::runBasicShell(SearchItem it) { qDebug() << "Running Basic Shell"; qDebug() << "Valid commands: ls, cd, select, pwd, rmdir, mkdir, set, lstags and exit."; qDebug() << "mkdir: Use $title to create a folder with name of title."; qDebug() << "select: Selects the present working dir" " as download directory if no arguments are provided." "Else uses the provided argument as a tag or a path."; qDebug() << "As a select argument the tag name be prefixed by '$' like '$TAG_NAME'."; qDebug() << "set: used for setting a tag. set [TAG_NAME] [FOLDER PATH]"; qDebug() << "lstags: Lists all tags stored."; namespace fs = std::filesystem; std::string extras = ""; while(true) { std::print("nsh|{}>{}",fs::current_path(), extras); extras = ""; std::string command; std::getline(std::cin, command); if(command == "ls") { system("ls"); } if(command.starts_with("cd")) { auto arg = stripForArgument(command, 1); auto abs = fs::absolute(arg); if(fs::is_directory(abs)) fs::current_path(abs); else { if(arg.starts_with('$')) { arg = arg.erase(0, 1); auto dir = Tags::getTag(tags_file_path, arg); if(fs::exists(fs::absolute(dir))) { fs::current_path(fs::absolute(dir)); continue; } else { qDebug() << "No such tag: " << arg; } } qDebug() << "No such directory: " << abs.string(); } } if(command == "pwd") { qDebug() << QString::fromStdString(fs::current_path()); } if(command == "exit") { break; } if(command.starts_with("select")) { if(command == "select") { Tags::setTag(tags_file_path, "last", fs::current_path().string()); return fs::current_path(); } auto arg = stripForArgument(command, 1); if(arg.starts_with("$")) { arg = arg.erase(0,1); auto string_path = Tags::getTag(tags_file_path, arg); if(string_path == "") { qDebug() << arg << " tag not found in tags file."; continue; } Tags::setTag(tags_file_path, "last", fs::current_path().string()); return fs::path(string_path); } else if(fs::is_directory(fs::absolute(arg))) { auto path = fs::absolute(arg); Tags::setTag(tags_file_path, "last", path.string()); return path; } else { qDebug() << arg << " is not a valid path or tag."; } } if(command.starts_with("set")) { auto arg1 = stripForArgument(command, 1); auto arg2 = stripForArgument(command, 2); if(arg1 == "" || arg2 == "") { qDebug() << "Invalid arguments for set."; continue; } if(!fs::is_directory(fs::canonical(arg2))) { qDebug() << arg2 << " is not a valid path."; continue; } if(arg1.contains(' ') || arg1.contains('\n') || arg1.contains('\t') || arg1.contains('\r') || arg1.contains('\v')) { qDebug() << arg1 << " can not have a whitespace char."; continue; } arg2 = fs::canonical(fs::path(arg2)); qDebug() << "Setting tag:" << arg1 << ":" << arg2; Tags::setTag(tags_file_path, arg1, arg2); } if(command == "lstags") { auto tags = Tags::getAllTags(tags_file_path); for(auto &[key, value]: tags) { qDebug() << key << ": " << value; } } if(command.starts_with("rmdir")) { auto arg = stripForArgument(command, 1); fs::path path_to_remove = fs::current_path() / fs::path(arg); std::string t; if(!fs::is_directory(path_to_remove)) { qDebug() << QString::fromStdString(path_to_remove) << " is not a directory."; continue; } qDebug() << "Are you sure you want to remove: " << QString::fromStdString(path_to_remove) << "(y/n)?"; std::cin >> t; std::cin.clear(); if(t.size() >= 1) { if(t[0] == 'y' || t[0] == 'Y') fs::remove(path_to_remove); } } if(command.starts_with("mkdir")) { auto arg = stripForArgument(command, 1); if(!arg.starts_with('$')) { fs::path path_to_create = fs::current_path() / fs::path(arg); bool t = fs::create_directory(path_to_create); if(!t) { qDebug() << "Failed to create path: " << QString::fromStdString(path_to_create); } } else if(arg == "$title") { extras = "mkdir " + it.title.toStdString(); continue; } } } return std::unexpected(-1); } std::string MainScreen::stripForArgument(std::string command, int index) { std::istringstream iss(command); std::string word; std::vector tokens; while (iss >> word) { tokens.push_back(word); } if (index > 0 && index < tokens.size()) { return tokens[index]; } return ""; } void MainScreen::statusFromSocket(Status st) { } void MainScreen::renderSearchPage_(QNetworkReply *rep) { QByteArray data = rep->readAll(); qDebug() << "Render Search Page"; std::string page = data.toStdString(); if(data.size() == 0) { qDebug() << "Invalid reply; Retrying..."; auto req = rep->request(); manager->get(req); rep->deleteLater(); return; } rep->deleteLater(); auto res = parseSearchPage(page); for(auto i: res) { i.printData(); qDebug() << "================================"; } qDebug() << "Total got from response: " << res.size(); int index = displaySearchMenu(res); if(index >= 0) { auto pid = startDownloadIndex(res, index); if(pid == 0) return; } renderHomePage(); } void SearchItem::printData() { qDebug() << "Title: " << title; qDebug() << "Url: " << url; qDebug() << "Size: " << size; qDebug() << "Seeders: " << seeders; qDebug() << "Leechers: " << leechers; qDebug() << "Downloads: " << downloads; qDebug() << "Timestamp: " << timestamp; qDebug() << "Magnet: " << magnet; qDebug() << "Download path: " << download_path.string(); } void SearchItem::writeData(std::string file_path) { std::fstream out(file_path, std::fstream::out); if(!out.is_open()) { qDebug() << "Failed to write to file: " << file_path; return; } qDebug() << "Writing title: " << title; qDebug() << "Writing url: " << url; qDebug() << "Writing magnet: " << magnet; qDebug() << "Writing download_path: " << download_path.string(); out << title.toStdString() << std::endl; out << url.toDisplayString().toStdString() << std::endl; out << magnet.toStdString() << std::endl; out << download_path.string() << std::endl; out.flush(); out.close(); } void SearchItem::readFromData(std::string file_path) { std::fstream in(file_path, std::fstream::in); if(!in.is_open()) { qDebug() << "Failed to read from file: " << file_path; return; } std::string stitle, surl, smagnet, spath; std::getline(in, stitle); std::getline(in, surl); std::getline(in, smagnet); std::getline(in, spath); in.close(); title = QString::fromStdString(stitle); url = QUrl(QString::fromStdString(surl)); magnet = QString::fromStdString(smagnet); download_path = std::filesystem::canonical(spath); in.close(); } std::string MainScreen::Tags::getTag(std::string file_name, std::string key) { QJsonDocument doc = QJsonDocument::fromJson(readFile(file_name)); QJsonObject obj = doc.object(); QString value = obj[QString::fromStdString(key)].toString(""); return value.toStdString(); } void MainScreen::Tags::setTag(std::string file_name, std::string key, std::string value) { QJsonDocument doc = QJsonDocument::fromJson(readFile(file_name)); QJsonObject obj = doc.object(); obj[QString::fromStdString(key)] = QString::fromStdString(value); doc.setObject(obj); bool write = writeFile(file_name, doc.toJson()); if(!write) qDebug() << "Failed to write to file: " << file_name; } QByteArray MainScreen::Tags::readFile(std::string file_name) { std::fstream fin(file_name, std::fstream::in); if(!fin.is_open()) return QByteArray(); std::string data, fdata; while(std::getline(fin, data)) { fdata += data; } fin.flush(); fin.close(); return QByteArray::fromStdString(fdata); } bool MainScreen::Tags::writeFile(std::string file_name, QByteArray data) { std::fstream fout(file_name, std::fstream::out); if(!fout.is_open()) return false; fout << data.toStdString(); fout.flush(); fout.close(); return true; } std::unordered_map MainScreen::Tags::getAllTags(std::string file_name) { QJsonDocument doc = QJsonDocument::fromJson(readFile(file_name)); QJsonObject obj = doc.object(); std::unordered_map t; for(auto &key: obj.keys()) { t[key.toStdString()] = obj[key].toString().toStdString(); } return t; } Socket::Socket(QObject *parent) { } //For each resume file found either start the libtorrent session if it was not running or //Ask each session their status. void Socket::start() { // Generate a list of all active libtorrent sessions. namespace fs = std::filesystem; auto temp_dir = QStandardPaths::writableLocation(QStandardPaths::TempLocation).toStdString(); auto temp_dir_path = fs::path(temp_dir); fs::directory_iterator itr(temp_dir_path); for(auto &file_folder: itr) { if(file_folder.is_regular_file() || file_folder.is_socket()) { auto file_name = file_folder.path().string(); if(file_name.contains("nekodl") && file_name.ends_with(".sock")) { QLocalSocket *socket = new QLocalSocket(this); connect(socket, &QLocalSocket::readyRead, this, &Socket::socketReadyRead); socket->connectToServer(QString::fromStdString(file_name)); qDebug() << "Opening file: " << file_name; if(socket->isOpen()) { qDebug() << "Open socket: " << file_name; QString data = ""; socket->write(data.toUtf8()); socket->flush(); active_sockets[file_name] = socket; } else { qDebug() << "Stale socket: " << file_name; fs::remove(file_name); disconnect(socket); socket->deleteLater(); } } } } qDebug() << "Checking temp dir."; if(!recheck) { if(!timer) { timer = new QTimer(this); timer->setInterval(5000); timer->setSingleShot(true); connect(timer, &QTimer::timeout, this, [&](){ startNotDownloadingSessions(); }); timer->start(); } if(checked_resume_files) { if(timer) { timer->deleteLater(); } } recheck = new QTimer(this); recheck->setInterval(3000); recheck->setSingleShot(false); connect(recheck, &QTimer::timeout, this, [this](){ start(); }); recheck->start(); } } // Start all sessions with a .resume_file but // are not in list of active libtorrent session. void Socket::startNotDownloadingSessions() { namespace fs = std::filesystem; fs::path resume_file_location = fs::path( QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation).toStdString() ); resume_file_location = resume_file_location / "resumes"; if(!fs::is_directory(resume_file_location)) { fs::create_directories(resume_file_location); } fs::directory_iterator itr(resume_file_location); for(auto &file_folder: itr) { qDebug() << "Checking: " << file_folder.path().string(); if(fs::is_regular_file(file_folder)) { if(file_folder.path().extension() == ".resume_file") { //Load resume file. qDebug() << "Resume file found: " << file_folder.path().string(); auto parent_path = file_folder.path().parent_path(); auto file_name = parent_path / file_folder.path().stem(); auto searchitem_file_path = file_name.string() + ".searchitem"; qDebug() << parent_path.string() << ": " << file_name.string(); qDebug() << "Search item path: " << searchitem_file_path; if(fs::exists(searchitem_file_path)) { SearchItem it; it.readFromData(searchitem_file_path); qDebug() << "Recovering from: " << it.title; if(!isTitleInActiveSession(it.title)) { qDebug() << "Title not in active session list."; it.printData(); emit launchTorrentSession(it); } } else { qDebug() << "Failed to recover: " << searchitem_file_path; } } } } checked_resume_files = true; } bool Socket::isTitleInActiveSession(QString title) { return active_sockets.find(title.toStdString()) != active_sockets.end(); } void Socket::socketReadyRead() { }