#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::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"; } 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(); 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() << "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; while(true) { std::print("nsh|{}>",fs::current_path()); 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); 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); } } } 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::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; } 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; } out << title.toStdString() << std::endl; out << url.toDisplayString().toStdString() << std::endl; out << magnet.toStdString() << 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; in >> stitle; in >> surl; in >> smagnet; in.close(); title = QString::fromStdString(stitle); url = QUrl(QString::fromStdString(surl)); magnet = QString::fromStdString(smagnet); 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; }