├── .gitignore ├── .vscode └── launch.json ├── Readme.md ├── app.json ├── crawllers ├── 1337x │ ├── details.js │ └── search.js ├── limetorrent │ ├── details.js │ └── search.js └── piratebay │ ├── details.js │ └── search.js ├── downloads └── sample.txt ├── index.js ├── lib ├── bot.js └── torrent.js ├── logs.txt ├── package-lock.json ├── package.json ├── routes ├── details.js ├── search.js └── torrent.js ├── utils ├── diskinfo.js ├── gdrive.js ├── humanTime.js ├── keepalive.js ├── logger.js ├── mkfile.js ├── prettyBytes.js ├── status.js └── ziper.js └── web ├── .gitignore ├── package-lock.json ├── package.json ├── public └── index.html └── src ├── App.js ├── assets └── css │ ├── helpers.css │ ├── index.css │ └── navbar.css ├── components ├── DownloadItem.js ├── DriveItem.js ├── Input.js ├── Navbar.js ├── NightModeToggle.js ├── Picker.js ├── SearchItem.js └── TopNav.js ├── index.js └── screens ├── Downloads.js ├── Drive.js ├── DriveHelp.js ├── Home.js └── Search.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next/ 3 | config.js 4 | test.js 5 | logs.txt 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}/index.js", 12 | "skipFiles": ["/**"] 13 | }, 14 | { 15 | "type": "node", 16 | "request": "attach", 17 | "name": "Attach", 18 | "processId": "${command:PickProcess}", 19 | "skipFiles": ["/**"] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # UnOfficial Clone Of [Torrent-AIO-Bot](https://github.com/patheticGeek/torrent-aio-bot) 2 | This is an unofficial clone of **[Torrent-AIO-Bot](https://github.com/patheticGeek/torrent-aio-bot)**. 3 | 4 | 5 | ## Deploy 👀 6 | 7 | If you like to make your own version of [this mod](https://github.com/Itz-fork/Torrent-Aio-Bot-Duplicate), just click on "Deploy to Heroku" button 👇: 8 | 9 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/Itz-fork/Torrent-Aio-Bot-Duplicate) 10 | 11 | 12 | ## Configs 📓 13 | 14 | #### Fill Before deploy, 15 | 16 | - `SITE` - Your Heroku app url 17 | - `SEARCH_SITE` - Your Heroku app url (You can also set this to "https://torrent-aio-bot.herokuapp.com/". [Not my site tho]) 18 | - `TELEGRAM_TOKEN` - Your telegram bot token (Get it from [@BotFather](https://t.me/BotFather)) 19 | - `GDRIVE_PARENT_FOLDER` - Your Google Folder Id (Team drive id supported) 20 | 21 | #### Fill after deploy 22 | 23 | - `CLIENT_ID` ⭐ - Your Google Drive Drive API Client ID (Defaults to: `202264815644.apps.googleusercontent.com` [rcone](https://rclone.org/)'s client id) 24 | - `CLIENT_SECRET` ⭐ - Your Google Drive Drive API Client Secret (Defaults to: `X4Z3ca8xfWDb1Voo-F9a7ZxJ` [rcone](https://rclone.org/)'s client secret) 25 | - `TOKEN` - Get this token from https://.herokuapp.com/drivehelp ( is your heroku app name) 26 | - `AUTH_CODE` - While Obtaining the "TOKEN" your website (heroku app) will ask you to login to your account. After a successful login you'll get this 27 | 28 | ⭐ - If you don't have those just don't fill them **but** if you have remember to use your own Google Dirve credentials 29 | 30 | #### Other configs 31 | 32 | - `DISABLE_WEB` - Set this to "true" if you don't wanna use the website 33 | - `O337X_SITE` - If you want to change search site for "1337x" 34 | - `LIMETORRENT_SITE` - If you want to change search site for "limetorrent" 35 | - `PIRATEBAY_SITE` - If you want to change search site for "piratebay" 36 | 37 | > Note ⚠️ : Change the search site urls only if the search feature is broken (Not working). 38 | > 39 | 40 | 41 | ## API Endpoints 💻 42 | 43 | **Prefix:** https://.herokuapp.com/api/v1 44 | 45 | #### For downloading: 46 | 47 | | Endpoint | Params | Return | 48 | | :---------------- | :----------: | --------------------------------------------------------------------: | 49 | | /torrent/download | link: string | { error: bool, link: string, infohash: string errorMessage?: string } | 50 | | /torrent/list | none | {error: bool, torrents: [ torrent, torrent, ... ]} | 51 | | /torrent/remove | link: string | { error: bool, errorMessage?: string } | 52 | | /torrent/status | link: string | {error: bool, status: torrent, errorMessage?: string} | 53 | 54 | link is magnet url of the torrent 55 | 56 | ``` 57 | torrent: { 58 | magnetURI: string, 59 | speed: string, 60 | downloaded: string, 61 | total: string, 62 | progress: number, 63 | timeRemaining: number, 64 | redableTimeRemaining: string, 65 | downloadLink: string, 66 | status: string, 67 | done: bool 68 | } 69 | ``` 70 | 71 | #### For searching: 72 | 73 | | Endpoint | Params | Return | 74 | | :-------------- | :--------------------------: | --------------------------------------------------------------: | 75 | | /search/{site} | query: string, site?: string | {error: bool, results: [ result, ... ], totalResults: number, } | 76 | | /details/{site} | query: string | {error: bool, torrent } | 77 | 78 | query is what you want to search for or the link of the torrent page 79 | site is the link to homepage of proxy to use must have a trailing '/' 80 | 81 | ``` 82 | result: { 83 | name: string, 84 | link: string, 85 | seeds: number, 86 | details: string 87 | } 88 | 89 | torrent: { 90 | title: string, 91 | info: string, 92 | downloadLink: string, 93 | details: [ { infoTitle: string, infoText: string } ] 94 | } 95 | ``` 96 | 97 | sites available: "piratebay", "1337x", "limetorrent" 98 | 99 | **Written by:** [patheticGeek](https://github.com/patheticGeek) for [the original project](https://github.com/patheticGeek/torrent-aio-bot) 100 | 101 | 102 | ## Issues (or Wiki?) 📨 103 | 104 | #### Custom client id and client secrets for Google drive upload: 105 | 106 | **Written by:** [patheticGeek](https://github.com/patheticGeek) for [the original project](https://github.com/patheticGeek/torrent-aio-bot) 107 | 108 | 1. Go to https://developers.google.com/drive/api/v3/quickstart/nodejs and click on Enable the Drive API. Copy client id and set an enviorment variable in heroku with name CLIENT_ID then copy client secret and set another env named CLIENT_SECRET. 109 | 2. Goto https://\.herokuapp.com/drivehelp and paste your client id and secret and click "Get auth code", it will redirect you to login and you'll get a auth code after login paste that auth code in the auth code feild and click "Generate token" it'll give you a token. now set these as env variable CLIENT_ID, CLIENT_SECRET, AUTH_CODE and TOKEN. 110 | 3. By default files are uploaded in the root of drive if you dont want to upload in root folder make a folder copy its id and set a env var GDRIVE_PARENT_FOLDER and value id of desired folder. The folder id will be the last part of the url such as in url "https://drive.google.com/drive/folders/1rpk7tGWs_lv_kZ_W4EPaKj8brfFVLOH-" the folder id is "1rpk7tGWs_lv_kZ_W4EPaKj8brfFVLOH-". 111 | 4. If you want team drive support open your teamdrive and copy the folder id from url eg. https://drive.google.com/drive/u/0/folders/0ABZHZpfYfdVCUk9PVA this is link of a team drive copy the last part "0ABZHZpfYfdVCUk9PVA" this will be your GDRIVE_PARENT_FOLDER. If you want them in a folder in teamdrive open the folder and use that folder's id instead. 112 | 5. You're good to go. The gdrive status will be shown in "gdrive.txt" file when you click Open on the website downloads page. Bot wil automatically send you drive link when its uploaded. 113 | 114 | > Use this torrent for testing or when downloading to setup drive it is well seeded and downloads in ~10s 115 | > 116 | > magnet:?xt=urn:btih:dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c&dn=Big+Buck+Bunny 117 | 118 | #### Changing the sites used for searching 119 | 120 | **Written by:** [patheticGeek](https://github.com/patheticGeek) for [the original project](https://github.com/patheticGeek/torrent-aio-bot) 121 | 122 | To change the pirate bay site, visit the site you would like to use search something there, copy the url eg. https://thepiratebay.org/search/whatisearched and replace the search with {term} so the url looks like https://thepiratebay.org/search/{term} ans set this to env var `PIRATEBAY_SITE` 123 | 124 | Same, if you want to change the limetorrents site visit the site you want to use and search for something, then replace the thing you searched for with {term} so final url looks like https://limetorrents.at/search?search={term} and set this value to env var `LIMETORRENT_SITE` 125 | 126 | Simillarly the enviorment variable for 1337x is `O337X_SITE` 127 | 128 | #### Search feature isn't working: 129 | 130 | **Written by:** [Itz-fork](https://github.com/Itz-fork) for [the unofficial clone](https://github.com/Itz-fork/Torrent-Aio-Bot-Duplicate) 131 | 132 | Follow these steps 👇, 133 | - Fork this repo and connect it to your heroku app 134 | - Add the following buildpack to your heroku app (click to copy) 135 | - ``` 136 | https://github.com/jontewks/puppeteer-heroku-buildpack.git 137 | ``` 138 | - Redeploy your heroku app 139 | 140 | If the search feature still isn't working try checking your heroku apps [Configs](#configs-) 141 | 142 | > Note ⚠️ : The search feature is currently unmaintained due to the inconsistency between proxy sites and their uptimes 143 | > 144 | 145 | 146 | ## Credits & Thanks To ❤️, 147 | 148 | - **[PathetikGeek](https://github.com/patheticGeek/torrent-aio-bot)** - This hardwork is done by this person! 149 | - **[rony-alt-ac](https://github.com/rony-alt-ac)** - For Search Fix! 150 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Torrent AIO Bot [Clone]", 3 | "description": "All in one bot for torrenting", 4 | "repository": "https://github.com/Itz-fork/Torrent-Aio-Bot-Duplicate", 5 | "keywords": [ 6 | "node", 7 | "express", 8 | "bot", 9 | "torrent", 10 | "automation" 11 | ], 12 | "website": "https://t.me/NexaBotsUpdates", 13 | "repository": "https://github.com/Itz-fork/Torrent-Aio-Bot-Duplicate", 14 | "success_url": "https://t.me/NexaBotsUpdates", 15 | "env": { 16 | "AUTH_CODE": { 17 | "description": "Your Auth Code. You will get this after Login To Google Account.", 18 | "required": false 19 | }, 20 | "CLIENT_ID": { 21 | "description": "Your Google Drive Client Id. Read ReadMe for more info.", 22 | "value": "202264815644.apps.googleusercontent.com", 23 | "required": false 24 | }, 25 | "CLIENT_SECRET": { 26 | "description": "Your Google Drive Client Secret. Read ReadMe for more info.", 27 | "value": "X4Z3ca8xfWDb1Voo-F9a7ZxJ", 28 | "required": false 29 | }, 30 | "GDRIVE_PARENT_FOLDER": { 31 | "description": "Your Google Drive Folder Id. Team Drive Supported.", 32 | "required": false 33 | }, 34 | "SITE": { 35 | "description": "Your Heroku App Url", 36 | "value": "https://.herokuapp.com/", 37 | "required": true 38 | }, 39 | "SEARCH_SITE": { 40 | "description": "Your Heroku App Url", 41 | "value": "https://.herokuapp.com/", 42 | "required": true 43 | }, 44 | "TELEGRAM_TOKEN": { 45 | "description": "Your Bot Token From @BotFather", 46 | "required": true 47 | }, 48 | "TOKEN": { 49 | "description": "Your Token. Get it from https://.herokuapp.com/drivehelp", 50 | "required": false 51 | } 52 | }, 53 | "builpacks": [ 54 | { 55 | "url": "heroku/nodejs" 56 | }, 57 | { 58 | "url": "https://github.com/jontewks/puppeteer-heroku-buildpack.git" 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /crawllers/1337x/details.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require("puppeteer"); 2 | 3 | async function details(link) { 4 | try { 5 | var browser = await puppeteer.launch({ 6 | headless: true, 7 | args: ["--no-sandbox"] 8 | }); 9 | var page = await browser.newPage(); 10 | await page.goto(link); 11 | 12 | var torrentDetails = await page.evaluate(async () => { 13 | var detailsFrame = document.querySelector("div.torrent-detail-page"); 14 | var title = detailsFrame.querySelector("div.box-info-heading>h1").innerText; 15 | var downloadLink = detailsFrame.querySelector("div:nth-of-type(2)>div:nth-of-type(1)>ul>li>a").href; 16 | var info = ""; 17 | 18 | var infoTitles = detailsFrame.querySelectorAll("ul.list > li > strong"); 19 | var infoTexts = detailsFrame.querySelectorAll("ul.list > li > span"); 20 | var i = 0; 21 | var details = []; 22 | 23 | infoTitles.forEach(text => { 24 | details.push({ 25 | infoTitle: text.innerText, 26 | infoText: infoTexts[i].innerText 27 | }); 28 | i += 1; 29 | }); 30 | 31 | return { error: false, torrent: { title, info, downloadLink, details } }; 32 | }); 33 | 34 | await page.close(); 35 | await browser.close(); 36 | 37 | return torrentDetails; 38 | } catch (err) { 39 | console.log(err); 40 | return { error: true, errorMessage: "Runtime error occured" }; 41 | } 42 | } 43 | 44 | module.exports = details; 45 | -------------------------------------------------------------------------------- /crawllers/1337x/search.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require("puppeteer"); 2 | const O337X_SITE = process.env.O337X_SITE || "https://www.1337xx.to/search/{term}/1/"; 3 | 4 | async function search(search, site = O337X_SITE) { 5 | try { 6 | var browser = await puppeteer.launch({ 7 | headless: true, 8 | args: ["--no-sandbox"] 9 | }); 10 | var page = await browser.newPage(); 11 | await page.goto(site.replace("{term}", search)); 12 | 13 | var searchResults = await page.evaluate(async () => { 14 | var searchResults = document.querySelector("tbody"); 15 | if (!searchResults) { 16 | return { error: true, errorMessage: "No results found" }; 17 | } 18 | var tableRows = searchResults.querySelectorAll("tr"); 19 | var results = []; 20 | 21 | tableRows.forEach(item => { 22 | var details = 23 | "Uploaded: " + 24 | item.querySelectorAll("td")[3].innerText + 25 | ", Size: " + 26 | item.querySelectorAll("td")[4].innerText + 27 | ", By: " + 28 | item.querySelectorAll("td")[5].innerText; 29 | results.push({ 30 | name: item.querySelectorAll("td")[0].querySelector("a:last-of-type").innerText, 31 | link: item.querySelectorAll("td")[0].querySelector("a:last-of-type").href, 32 | seeds: item.querySelectorAll("td")[1].innerText, 33 | details 34 | }); 35 | }); 36 | 37 | return { 38 | error: false, 39 | results, 40 | errorMessage: "" 41 | }; 42 | }); 43 | 44 | await page.close(); 45 | await browser.close(); 46 | 47 | return searchResults; 48 | } catch (err) { 49 | console.log(err); 50 | return { error: true, errorMessage: "Runtime error occured" }; 51 | } 52 | } 53 | 54 | module.exports = search; 55 | -------------------------------------------------------------------------------- /crawllers/limetorrent/details.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require("puppeteer"); 2 | 3 | async function details(link) { 4 | try { 5 | var browser = await puppeteer.launch({ 6 | headless: true, 7 | args: ["--no-sandbox"] 8 | }); 9 | await browser.userAgent( 10 | "Mozilla/5.0 (Linux; U; Android 4.4.2; zh-cn; GT-I9500 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko)Version/4.0 MQQBrowser/5.0 QQ-URL-Manager Mobile Safari/537.36" 11 | ); 12 | var page = await browser.newPage(); 13 | await page.goto(link); 14 | 15 | var torrentDetails = await page.evaluate(async () => { 16 | var detailsFrame = document.querySelector("div#maincontentrouter > div#content"); 17 | var title = detailsFrame.querySelector("h1").innerText; 18 | var downloadLink = detailsFrame.querySelectorAll("a.csprite_dltorrent")[1].href; 19 | var info = ""; 20 | 21 | var infoTitles = detailsFrame.querySelectorAll("div.torrentinfo > table > tbody > tr > td:nth-of-type(1)"); 22 | var infoTexts = detailsFrame.querySelectorAll("div.torrentinfo > table > tbody > tr > td:nth-of-type(2)"); 23 | var i = 0; 24 | var details = []; 25 | 26 | details.push({ 27 | infoTitle: "Seeders", 28 | infoText: detailsFrame.querySelector("#content > span.greenish").innerText.replace("Seeders : ", "") 29 | }); 30 | infoTitles.forEach(text => { 31 | details.push({ 32 | infoTitle: text.innerText.replace(" :", ""), 33 | infoText: infoTexts[i].innerText 34 | }); 35 | i += 1; 36 | }); 37 | 38 | return { error: false, torrent: { title, info, downloadLink, details } }; 39 | }); 40 | 41 | await page.close(); 42 | await browser.close(); 43 | 44 | return torrentDetails; 45 | } catch (err) { 46 | console.log(err); 47 | return { error: true, errorMessage: "Runtime error occured" }; 48 | } 49 | } 50 | 51 | module.exports = details; 52 | -------------------------------------------------------------------------------- /crawllers/limetorrent/search.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require("puppeteer"); 2 | const LIMETORRENT_SITE = process.env.LIMETORRENT_SITE || "https://www.limetorrents.pro/search/all/{term}/"; 3 | 4 | async function search(search, site = LIMETORRENT_SITE) { 5 | try { 6 | var browser = await puppeteer.launch({ 7 | headless: true, 8 | args: ["--no-sandbox"] 9 | }); 10 | await browser.userAgent( 11 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36" 12 | ); 13 | var page = await browser.newPage(); 14 | await page.goto(site.replace("{term}", search)); 15 | 16 | var searchResults = await page.evaluate(async () => { 17 | var searchResults = document.querySelector("table.table2 > tbody"); 18 | if (!searchResults) { 19 | return { error: true, errorMessage: "No results found" }; 20 | } 21 | var tableRows = searchResults.querySelectorAll("tr"); 22 | var results = []; 23 | 24 | tableRows.forEach(item => { 25 | if (!item.querySelector("th")) { 26 | var details = 27 | "Added: " + 28 | item.querySelector("td:nth-of-type(2)").innerText + 29 | ", Size: " + 30 | item.querySelector("td:nth-of-type(3)").innerText; 31 | results.push({ 32 | name: item.querySelector("td:nth-of-type(1) > div:nth-of-type(1) > a:nth-of-type(2)").innerText, 33 | link: item.querySelector("td:nth-of-type(1) > div:nth-of-type(1) > a:nth-of-type(2)").href, 34 | seeds: item.querySelector("td:nth-of-type(4)").innerText, 35 | details 36 | }); 37 | } 38 | }); 39 | 40 | return { 41 | error: false, 42 | results, 43 | errorMessage: "" 44 | }; 45 | }); 46 | 47 | await page.close(); 48 | await browser.close(); 49 | 50 | return searchResults; 51 | } catch (err) { 52 | console.log(err); 53 | return { error: true, errorMessage: "Runtime error occured" }; 54 | } 55 | } 56 | 57 | module.exports = search; 58 | -------------------------------------------------------------------------------- /crawllers/piratebay/details.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require("puppeteer"); 2 | 3 | async function details(link) { 4 | try { 5 | var browser = await puppeteer.launch({ 6 | headless: true, 7 | args: ["--no-sandbox"] 8 | }); 9 | var page = await browser.newPage(); 10 | await page.goto(link); 11 | 12 | var torrentDetails = await page.evaluate(async () => { 13 | var detailsFrame = document.querySelector("div#detailsframe"); 14 | var title = detailsFrame.querySelector("div#title").innerText; 15 | var downloadLink = detailsFrame.querySelector("div.download > a").href; 16 | var info = detailsFrame.querySelector("div.nfo > pre").innerText; 17 | 18 | var infoTitle = document.querySelectorAll("dt"); 19 | var infoText = document.querySelectorAll("dd"); 20 | var i = 0; 21 | var details = []; 22 | infoTitle.forEach(text => { 23 | if (text.innerText !== "Info Hash:" && text.innerText !== "Comments") { 24 | details.push({ 25 | infoTitle: text.innerText, 26 | infoText: infoText[i].innerText 27 | }); 28 | } 29 | i += 1; 30 | }); 31 | 32 | return { error: false, torrent: { title, info, downloadLink, details } }; 33 | }); 34 | 35 | await page.close(); 36 | await browser.close(); 37 | 38 | return torrentDetails; 39 | } catch (err) { 40 | console.log(err); 41 | return { error: true, errorMessage: "Runtime error occured" }; 42 | } 43 | } 44 | 45 | module.exports = details; 46 | -------------------------------------------------------------------------------- /crawllers/piratebay/search.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require("puppeteer"); 2 | const PIRATEBAY_SITE = 3 | process.env.PIRATEBAY_SITE || "https://thepiratebay.org/search.php?q={term}"; 4 | 5 | async function search(search, site = PIRATEBAY_SITE) { 6 | try { 7 | var browser = await puppeteer.launch({ 8 | headless: true, 9 | args: ["--no-sandbox"] 10 | }); 11 | var page = await browser.newPage(); 12 | await page.goto(site.replace("{term}", search)); 13 | 14 | var searchResults = await page.evaluate(async () => { 15 | var searchResults = document.querySelector("div#SearchResults"); 16 | if (!searchResults) { 17 | return { error: true, errorMessage: "No results found" }; 18 | } 19 | var tableRows = searchResults.querySelectorAll("tr"); 20 | var results = []; 21 | 22 | tableRows.forEach(item => { 23 | if (item.classList.value === "header") { 24 | } else { 25 | results.push({ 26 | name: item.querySelectorAll("td")[1].querySelector("a").innerText, 27 | link: item.querySelectorAll("td")[1].querySelector("a").href, 28 | seeds: item.querySelectorAll("td")[2].innerText, 29 | details: item 30 | .querySelectorAll("td")[1] 31 | .querySelector("font.detDesc").innerText 32 | }); 33 | } 34 | }); 35 | return { 36 | error: false, 37 | results, 38 | errorMessage: "" 39 | }; 40 | }); 41 | 42 | await page.close(); 43 | await browser.close(); 44 | 45 | return searchResults; 46 | } catch (err) { 47 | console.log(err); 48 | return { error: true, errorMessage: "Runtime error occured" }; 49 | } 50 | } 51 | 52 | module.exports = search; 53 | -------------------------------------------------------------------------------- /downloads/sample.txt: -------------------------------------------------------------------------------- 1 | Sample file. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const compression = require("compression"); 3 | const bodyParser = require("body-parser"); 4 | const serveIndex = require("serve-index"); 5 | 6 | const humanTime = require("./utils/humanTime"); 7 | const keepalive = require("./utils/keepalive"); 8 | const diskinfo = require("./utils/diskinfo"); 9 | const status = require("./utils/status"); 10 | const { getFiles, sendFileStream, getAuthURL, getAuthToken } = require("./utils/gdrive"); 11 | 12 | const search = require("./routes/search"); 13 | const details = require("./routes/details"); 14 | const torrent = require("./routes/torrent"); 15 | 16 | const dev = process.env.NODE_ENV !== "production"; 17 | const allowWeb = !process.env.DISABLE_WEB; 18 | const PORT = parseInt(process.env.PORT, 10) || 3000; 19 | 20 | const server = express(); 21 | 22 | keepalive(); 23 | 24 | server.use(compression()); 25 | server.use(bodyParser.json()); 26 | server.use((req, res, next) => { 27 | res.setHeader("Access-Control-Allow-Origin", "*"); 28 | res.setHeader("Access-Control-Allow-Headers", "*"); 29 | next(); 30 | }); 31 | 32 | server.get("/ping", (req, res) => res.send("pong")); 33 | 34 | server.get("/logs", (req, res) => res.sendFile("logs.txt", { root: __dirname })); 35 | 36 | server.use("/downloads", express.static("downloads"), serveIndex("downloads", { icons: true })); 37 | 38 | server.use("/api/v1/drive/folder", async (req, res) => { 39 | const folderId = req.query.id; 40 | res.send(await getFiles(folderId)); 41 | }); 42 | 43 | server.use("/api/v1/drive/file/:id", sendFileStream); 44 | 45 | server.use("/api/v1/drive/getAuthURL", (req, res) => { 46 | const CLIENT_ID = req.query.clientId; 47 | const CLIENT_SECRET = req.query.clientSecret; 48 | 49 | if (!CLIENT_ID || !CLIENT_SECRET) { 50 | res.send(JSON.stringify({ error: "Client Id and secret are required" })); 51 | } else { 52 | const authURL = getAuthURL(CLIENT_ID, CLIENT_SECRET); 53 | res.send(JSON.stringify({ error: "", authURL })); 54 | } 55 | }); 56 | 57 | server.use("/api/v1/drive/getAuthToken", async (req, res) => { 58 | const CLIENT_ID = req.query.clientId; 59 | const CLIENT_SECRET = req.query.clientSecret; 60 | const AUTH_CODE = req.query.authCode; 61 | 62 | if (!CLIENT_ID || !CLIENT_SECRET || !AUTH_CODE) { 63 | res.send(JSON.stringify({ error: "Client Id and secret and auth code are required" })); 64 | } else { 65 | const token = await getAuthToken(CLIENT_ID, CLIENT_SECRET, AUTH_CODE); 66 | res.send(JSON.stringify({ token, error: "" })); 67 | } 68 | }); 69 | 70 | server.use("/api/v1/torrent", torrent); 71 | server.use("/api/v1/search", search); 72 | server.use("/api/v1/details", details); 73 | 74 | server.get("/api/v1/uptime", async (req, res) => { 75 | res.send({ uptime: humanTime(process.uptime() * 1000) }); 76 | }); 77 | 78 | server.get("/api/v1/diskinfo", async (req, res) => { 79 | const path = req.query.path; 80 | const info = await diskinfo(path); 81 | res.send(info); 82 | }); 83 | 84 | server.get("/api/v1/status", async (req, res) => { 85 | const currStatus = await status(); 86 | res.send(currStatus); 87 | }); 88 | 89 | if (allowWeb) { 90 | console.log("web allowed"); 91 | server.use("/static", express.static("web/build/static")); 92 | server.all("*", (req, res) => res.sendFile("web/build/index.html", { root: __dirname })); 93 | } else { 94 | console.log("web disabled"); 95 | } 96 | 97 | server.listen(PORT, () => { 98 | console.log(`> Running on http://localhost:${PORT}`); 99 | }); 100 | -------------------------------------------------------------------------------- /lib/bot.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | 3 | const status = require("../utils/status"); 4 | const diskinfo = require("../utils/diskinfo"); 5 | const humanTime = require("../utils/humanTime"); 6 | const { uploadFileStream } = require("../utils/gdrive"); 7 | 8 | const api = process.env.SEARCH_SITE || "https://torrent-aio-bot.herokuapp.com/"; 9 | console.log("Using api: ", api); 10 | 11 | const searchRegex = /\/search (piratebay|limetorrent|1337x) (.+)/; 12 | //const detailsRegex = /\/details (piratebay|limetorrent|1337x) (.+)/; 13 | const downloadRegex = /\/download (.+)/; 14 | const statusRegex = /\/status (.+)/; 15 | const removeRegex = /\/cancel (.+)/; 16 | 17 | 18 | var msg_buttons = { 19 | reply_markup: { 20 | inline_keyboard: [ 21 | [{ text: "🔰 Updates Channel 🔰", url: "https://t.me/NexaBotsUpdates" }], 22 | [{ text: "⚜️ Support Group ⚜️", url: "https://t.me/Nexa_bots" }] 23 | ] 24 | }, 25 | parse_mode : "HTML" 26 | }; 27 | 28 | function bot(torrent, bot) { 29 | bot.onText(/\/start/, async msg => { 30 | // start message 31 | const startMessage = ` 32 | Hi ${msg.from.first_name} 😉️, 33 | 34 | I'm Torrent Nexa Bot 😇️, 35 | 36 | My Features 👇️, 37 | 38 | - Search Across Torrent Sites For Your Keyword 39 | - Mirror Magnet Links to Our Team Drive 40 | - Upload Telegram Files to Our Team Drive 41 | - Get Status of a Magnet Link 42 | 43 | Interfaces, 44 | - Telegram Bot 45 | - Web Interface 46 | `; 47 | bot.sendMessage(msg.chat.id, startMessage, msg_buttons); 48 | }), 49 | bot.onText(/\/help/, async msg => { 50 | // help message 51 | const helpMessage = ` 52 | Commands, 53 | 54 | /start - Start Me 🙂️ 55 | /download - To Download A Torrent 56 | /cancel - To Remove Downloading Torrent 57 | /status - To Check If Your Torrent is Downloading or Not 58 | /help - To Get This Help Message! 59 | 60 | Made With ❤️ By @NexaBotsUpdates 61 | `; 62 | bot.sendMessage(msg.chat.id, helpMessage, msg_buttons); 63 | }); 64 | 65 | bot.on("message", async msg => { 66 | if (!msg.document) return; 67 | const chatId = msg.chat.id; 68 | const fileSize = msg.document.file_size; 69 | const mimeType = msg.document.mimeType; 70 | const fileName = msg.document.file_name; 71 | const fileId = msg.document.file_id; 72 | // if (size > 20000000) { 73 | // bot.sendMessage(chatId, `Ooops, File size is too big!`) return; 74 | // } 75 | try { 76 | if (fileSize > 20000000) { 77 | bot.sendMessage(chatId, `Ooops, File size is too big!`); 78 | } else if (20000000 > fileSize) { 79 | const statusMessge = bot.sendMessage(chatId, "📤 Uploading Your File to Google Drive...", {parse_mode : "HTML"}); 80 | const uploadedFile = await uploadFileStream(fileName, bot.getFileStream(fileId)); 81 | const driveId = uploadedFile.data.id; 82 | const driveLink = `https://drive.google.com/file/d/${driveId}/view?usp=sharing`; 83 | const publicLink = `${process.env.SITE}api/v1/drive/file/${fileName}?id=${driveId}`; 84 | const statusMessgeId = statusMessge.message_id 85 | bot.deleteMessage(chatId, statusMessgeId) 86 | bot.sendMessage(chatId, `💾 Uploaded to Google Drive \n\nName 🏷️: ${fileName} \n\nGoogle Drive Link 🔗: ${driveLink} \nGoogle Drive Link 🔗: ${publicLink} \n\nJoin @NexaBotsUpdates ❤️`, {parse_mode : "HTML"}); 87 | } 88 | } catch (e) { 89 | bot.sendMessage(chatId, e.message || "An error occured 🥺"); 90 | } 91 | }); 92 | 93 | // bot.onText(/\/server diskinfo (.+)/, async (msg, match) => { 94 | // const from = msg.chat.id; 95 | // const path = match[1]; 96 | // const info = await diskinfo(path); 97 | // bot.sendMessage(from, info); 98 | // }); 99 | 100 | // bot.onText(/\/server uptime/, async msg => { 101 | // const from = msg.chat.id; 102 | // bot.sendMessage(from, humanTime(process.uptime() * 1000)); 103 | // }); 104 | 105 | bot.onText(/\/server_status/, async msg => { 106 | const from = msg.chat.id; 107 | const currStatus = await status(); 108 | bot.sendMessage(from, currStatus, {parse_mode : "HTML"}); 109 | }); 110 | 111 | bot.onText(searchRegex, async (msg, match) => { 112 | var from = msg.from.id; 113 | var site = match[1]; 114 | var query = match[2]; 115 | 116 | bot.sendMessage(from, "Searching For You Keywords 🔍...", {parse_mode : "HTML"}); 117 | 118 | const data = await axios(`${api}api/v1/search/${site}?query=${query}`).then(({ data }) => data); 119 | 120 | if (!data || data.error) { 121 | bot.sendMessage(from, "An error occured on server!"); 122 | } else if (!data.results || data.results.length === 0) { 123 | bot.sendMessage(from, "No results found."); 124 | } else if (data.results.length > 0) { 125 | let results1 = ""; 126 | let results2 = ""; 127 | let results3 = ""; 128 | 129 | data.results.forEach((result, i) => { 130 | if (i <= 2) { 131 | results1 += `Name 🏷️: ${result.name} \nSeeds 💰: ${result.seeds} \nDetails 📝: ${result.details} \nLink 🖇️: ${result.link} \n\n`; 132 | } else if (2 < i && i <= 5) { 133 | results2 += `Name 🏷️: ${result.name} \nSeeds 💰: ${result.seeds} \nDetails 📝: ${result.details} \nLink 🖇️: ${result.link} \n\n`; 134 | } else if (5 < i && i <= 8) { 135 | results3 += `Name 🏷️: ${result.name} \nSeeds 💰: ${result.seeds} \nDetails 📝: ${result.details} \nLink 🖇️: ${result.link} \n\n`; 136 | } 137 | }); 138 | 139 | bot.sendMessage(from, results1, {parse_mode : "HTML"}); 140 | bot.sendMessage(from, results2, {parse_mode : "HTML"}); 141 | bot.sendMessage(from, results3, {parse_mode : "HTML"}); 142 | } 143 | }); 144 | 145 | // bot.onText(detailsRegex, async (msg, match) => { 146 | // var from = msg.from.id; 147 | // var site = match[1]; 148 | // var query = match[2]; 149 | 150 | // bot.sendMessage(from, "🕰️ Loading... Wait and Join @NexaBotsUpdates🕰️"); 151 | 152 | // const data = await axios(`${api}/details/${site}?query=${query}`).then(({ data }) => data); 153 | // if (!data || data.error) { 154 | // bot.sendMessage(from, "An error occured 🥺"); 155 | // } else if (data.torrent) { 156 | // const torrent = data.torrent; 157 | // let result1 = ""; 158 | // let result2 = ""; 159 | 160 | // result1 += `Title 🏷️: ${torrent.title} \n\nInfo: ${torrent.info}`; 161 | // torrent.details.forEach(item => { 162 | // result2 += `${item.infoTitle} ${item.infoText} \n\n`; 163 | // }); 164 | // result2 += "Magnet Link 🧲:"; 165 | 166 | // await bot.sendMessage(from, result1); 167 | // await bot.sendMessage(from, result2); 168 | // await bot.sendMessage(from, torrent.downloadLink); 169 | // } 170 | // }); 171 | 172 | bot.onText(downloadRegex, (msg, match) => { 173 | var from = msg.from.id; 174 | var link = match[1]; 175 | let messageObj = null; 176 | let torrInterv = null; 177 | 178 | const reply = async torr => { 179 | let mess1 = ""; 180 | mess1 += `Name 🏷️: ${torr.name}\n\n`; 181 | mess1 += `Status 📱: ${torr.status}\n\n`; 182 | mess1 += `Size 📏: ${torr.total}\n\n`; 183 | if (!torr.done) { 184 | mess1 += `Downloaded ✅: ${torr.downloaded}\n\n`; 185 | mess1 += `Speed 🚀: ${torr.speed}\n\n`; 186 | mess1 += `Progress 📥: ${torr.progress}%\n\n`; 187 | mess1 += `Time Remaining ⏳: ${torr.redableTimeRemaining}\n\n`; 188 | } else { 189 | mess1 += `Download Link 🔗: ${torr.downloadLink}\n\n`; 190 | clearInterval(torrInterv); 191 | torrInterv = null; 192 | } 193 | mess1 += `Magnet URI 🧲: ${torr.magnetURI}`; 194 | try { 195 | if (messageObj) { 196 | if (messageObj.text !== mess1) bot.editMessageText(mess1, { chat_id: messageObj.chat.id, message_id: messageObj.message_id, parse_mode : "HTML" }); 197 | } else messageObj = await bot.sendMessage(from, mess1, {parse_mode : "HTML"}); 198 | } catch (e) { 199 | console.log(e.message); 200 | } 201 | }; 202 | 203 | const onDriveUpload = (torr, url) => bot.sendMessage(from, `💾 Uploaded to Google Drive \n\nName 🏷️: ${torr.name} \nGoogle Drive Link 🔗: ${url} \n\nJoin @NexaBotsUpdates ❤️`, {parse_mode : "HTML"}); 204 | const onDriveUploadStart = torr => bot.sendMessage(from, `📤 Uploading ${torr.name} to Google Drive...`, {parse_mode : "HTML"}); 205 | 206 | if (link.indexOf("magnet:") !== 0) { 207 | bot.sendMessage(from, "Hey! Link is not a magnet link 😒️"); 208 | } else { 209 | bot.sendMessage(from, "📩️ Starting to download 📩️... \n\nJoin @NexaBotsUpdates ❤️", {parse_mode : "HTML"}); 210 | try { 211 | const torren = torrent.download( 212 | link, 213 | torr => reply(torr), 214 | torr => reply(torr), 215 | onDriveUpload, 216 | onDriveUploadStart 217 | ); 218 | torrInterv = setInterval(() => reply(torrent.statusLoader(torren)), 5000); 219 | } catch (e) { 220 | bot.sendMessage(from, "An error occured 🤷‍♀️️\n" + e.message); 221 | } 222 | } 223 | }); 224 | 225 | bot.onText(statusRegex, (msg, match) => { 226 | var from = msg.from.id; 227 | var link = match[1]; 228 | 229 | const torr = torrent.get(link); 230 | if (link.indexOf("magnet:") !== 0) { 231 | bot.sendMessage(from, "Hey! Link is not a magnet link 😒️"); 232 | } else if (!torr) { 233 | bot.sendMessage(from, "Not downloading please add 😌️"); 234 | } else { 235 | let mess1 = ""; 236 | mess1 += `Name 🏷️: ${torr.name}\n\n`; 237 | mess1 += `Status 📱: ${torr.status}\n\n`; 238 | mess1 += `Size 📏: ${torr.total}\n\n`; 239 | if (!torr.done) { 240 | mess1 += `Downloaded ✅: ${torr.downloaded}\n\n`; 241 | mess1 += `Speed 🚀: ${torr.speed}\n\n`; 242 | mess1 += `Progress 📥: ${torr.progress}\n\n`; 243 | mess1 += `Time Remaining ⏳: ${torr.redableTimeRemaining}\n\n`; 244 | } else { 245 | mess1 += `Download Link 🔗: ${torr.downloadLink}\n\n`; 246 | } 247 | mess1 += `Magnet URI 🧲: ${torr.magnetURI}`; 248 | bot.sendMessage(from, mess1, {parse_mode : "HTML"}); 249 | } 250 | }); 251 | 252 | bot.onText(removeRegex, (msg, match) => { 253 | var from = msg.from.id; 254 | var link = match[1]; 255 | 256 | try { 257 | torrent.remove(link); 258 | bot.sendMessage(from, "Cancelled That Torrent 😏️"); 259 | } catch (e) { 260 | bot.sendMessage(from, `${e.message}`); 261 | } 262 | }); 263 | } 264 | 265 | module.exports = bot; 266 | -------------------------------------------------------------------------------- /lib/torrent.js: -------------------------------------------------------------------------------- 1 | const WebTorrent = require("webtorrent"); 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | const prettyBytes = require("../utils/prettyBytes"); 5 | const humanTime = require("../utils/humanTime"); 6 | const mkfile = require("../utils/mkfile"); 7 | const ziper = require("../utils/ziper"); 8 | const { uploadWithLog } = require("../utils/gdrive"); 9 | const dev = process.env.NODE_ENV !== "production"; 10 | const site = (dev ? require("../config").site : process.env.SITE) || "SET SITE ENVIORMENT VARIABLE. READ DOCS"; 11 | 12 | if (!site) console.log("SET SITE ENVIORMENT VARIABLE. READ DOCS\n"); 13 | 14 | class Torrent { 15 | constructor() { 16 | this.downloads = []; 17 | this.client = new WebTorrent(); 18 | setInterval(() => { 19 | this.downloads = this.client.torrents.map(torrent => this.get(torrent.magnetURI)); 20 | }, 3000); 21 | } 22 | 23 | statusLoader = torrent => { 24 | return { 25 | status: torrent.done ? "Downloaded" : torrent.name ? "Downloading" : "Getting metadata", 26 | magnetURI: torrent.magnetURI, 27 | name: torrent.name, 28 | speed: `${prettyBytes(torrent.downloadSpeed)}/s`, 29 | downloaded: prettyBytes(torrent.downloaded), 30 | total: prettyBytes(torrent.length), 31 | progress: parseInt(torrent.progress * 100), 32 | timeRemaining: parseInt(torrent.timeRemaining), 33 | redableTimeRemaining: humanTime(torrent.timeRemaining), 34 | downloadLink: `${site}downloads/${torrent.infoHash}`, 35 | done: torrent.done 36 | }; 37 | }; 38 | 39 | download = (magnetURI, onStart, onDone, onDriveUpload, onDriveUploadStart) => { 40 | if (!this.client.get(magnetURI)) { 41 | const torrent = this.client.add(magnetURI); 42 | 43 | torrent.once("metadata", () => { 44 | if (onStart) onStart(this.get(torrent.magnetURI)); 45 | }); 46 | 47 | torrent.once("done", () => { 48 | this.saveFiles(torrent, onDriveUpload, onDriveUploadStart); 49 | if (onDone) onDone(this.get(torrent.magnetURI)); 50 | }); 51 | 52 | return torrent; 53 | } else if (!this.client.get(magnetURI).done) { 54 | const torrent = this.client.get(magnetURI); 55 | return torrent; 56 | } else { 57 | const torrent = this.client.get(magnetURI); 58 | if (onDone) onDone(this.get(this.client.get(magnetURI).magnetURI)); 59 | return torrent; 60 | } 61 | }; 62 | 63 | remove = magnetURI => { 64 | this.client.get(magnetURI) ? this.client.remove(magnetURI) : undefined; 65 | return null; 66 | }; 67 | 68 | list = () => this.downloads; 69 | 70 | get = magnetURI => { 71 | const torr = this.client.get(magnetURI); 72 | return torr ? this.statusLoader(torr) : null; 73 | }; 74 | 75 | saveFiles = async (torrent, onDriveUpload, onDriveUploadStart) => { 76 | torrent.files.forEach((file, i) => { 77 | let filePath; 78 | if (torrent.files.length === 1) filePath = `./downloads/${torrent.infoHash}/${file.path}/${file.path}`; 79 | else filePath = `./downloads/${torrent.infoHash}/${file.path}`; 80 | //mkfile(filePath); 81 | fs.mkdirSync(path.dirname(filePath), {recursive: true}); 82 | let toFile = fs.createWriteStream(filePath); 83 | let torrentFile = file.createReadStream(); 84 | torrentFile.pipe(toFile); 85 | }); 86 | try { 87 | ziper(`./downloads/${torrent.infoHash}/${torrent.name}`); 88 | const torr = this.statusLoader(torrent); 89 | if (onDriveUploadStart) onDriveUploadStart(torr); 90 | const url = await uploadWithLog(`./downloads/${torrent.infoHash}/${torrent.name}`); 91 | if (onDriveUpload) onDriveUpload(torr, url); 92 | } catch (e) { 93 | console.log(e); 94 | } 95 | }; 96 | } 97 | 98 | module.exports = Torrent; 99 | -------------------------------------------------------------------------------- /logs.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "torrent-bot", 3 | "version": "1.0.0", 4 | "engines": { 5 | "node": "14.x" 6 | }, 7 | "description": "Torrent bot for downloading and searching torrents", 8 | "main": "index.js", 9 | "scripts": { 10 | "dev": "nodemon index.js", 11 | "build": "cd web && npm install && npm run build", 12 | "start": "NODE_ENV=production node index.js" 13 | }, 14 | "keywords": [ 15 | "torrent", 16 | "bot", 17 | "download", 18 | "search", 19 | "seed" 20 | ], 21 | "author": "pathetic_geek", 22 | "license": "ISC", 23 | "nodemonConfig": { 24 | "ignore": [ 25 | ".next", 26 | "node_modules", 27 | "web" 28 | ] 29 | }, 30 | "prettier": { 31 | "printWidth": 140, 32 | "tabWidth": 2, 33 | "useTabs": false, 34 | "semi": true, 35 | "singleQuote": false, 36 | "trailingComma": "none", 37 | "arrowParens": "avoid" 38 | }, 39 | "dependencies": { 40 | "axios": "^0.21.1", 41 | "body-parser": "^1.19.0", 42 | "compression": "^1.7.4", 43 | "diskusage": "^1.1.3", 44 | "express": "^4.17.1", 45 | "googleapis": "39", 46 | "http-proxy": "^1.18.0", 47 | "node-telegram-bot-api": "^0.40.0", 48 | "nodemon": "^2.0.2", 49 | "prop-types": "^15.7.2", 50 | "puppeteer": "^2.0.0", 51 | "readline": "^1.3.0", 52 | "serve-index": "^1.9.1", 53 | "webtorrent": "^0.107.17", 54 | "zip-a-folder": "^0.0.12" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /routes/details.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const piratebayDetails = require("../crawllers/piratebay/details"); 3 | const o337xDetails = require("../crawllers/1337x/details"); 4 | const limetorrentDetails = require("../crawllers/limetorrent/details"); 5 | 6 | const router = express.Router(); 7 | 8 | router.get("/piratebay", async (req, res) => { 9 | let query = req.query.query; 10 | 11 | if (query === "" || !query) { 12 | res.send({ error: true, errorMessage: "Search term cannot be empty" }); 13 | } else { 14 | const data = await piratebayDetails(query); 15 | res.send(data); 16 | } 17 | }); 18 | 19 | router.get("/1337x", async (req, res) => { 20 | let query = req.query.query; 21 | 22 | if (query === "" || !query) { 23 | res.send({ error: true, errorMessage: "Search term cannot be empty" }); 24 | } else { 25 | const data = await o337xDetails(query); 26 | res.send(data); 27 | } 28 | }); 29 | 30 | router.get("/limetorrent", async (req, res) => { 31 | let query = req.query.query; 32 | 33 | if (query === "" || !query) { 34 | res.send({ error: true, errorMessage: "Search term cannot be empty" }); 35 | } else { 36 | const data = await limetorrentDetails(query); 37 | res.send(data); 38 | } 39 | }); 40 | 41 | module.exports = router; 42 | -------------------------------------------------------------------------------- /routes/search.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const puppeteer = require("puppeteer"); 3 | 4 | const piratebaySearch = require("../crawllers/piratebay/search"); 5 | const o337xSearch = require("../crawllers/1337x/search"); 6 | const limetorrentSearch = require("../crawllers/limetorrent/search"); 7 | 8 | const router = express.Router(); 9 | 10 | router.get("/test", async (req, res) => { 11 | try { 12 | const browser = await puppeteer.launch(); 13 | 14 | await browser.close(); 15 | res.send({ error: false }); 16 | } catch (e) { 17 | console.log("Puppeteer error"); 18 | res.json({ error: true, errorMessage: e.message }); 19 | } 20 | }); 21 | 22 | router.get("/piratebay", async (req, res) => { 23 | let query = req.query.query; 24 | 25 | if (query === "" || !query) { 26 | res.send({ error: true, errorMessage: "Search term cannot be empty" }); 27 | } else { 28 | const data = await piratebaySearch(query); 29 | res.send(data); 30 | } 31 | }); 32 | 33 | router.get("/1337x", async (req, res) => { 34 | let query = req.query.query; 35 | 36 | if (query === "" || !query) { 37 | res.send({ error: true, errorMessage: "Search term cannot be empty" }); 38 | } else { 39 | const data = await o337xSearch(query); 40 | res.send(data); 41 | } 42 | }); 43 | 44 | router.get("/limetorrent", async (req, res) => { 45 | let query = req.query.query; 46 | 47 | if (query === "" || !query) { 48 | res.send({ error: true, errorMessage: "Search term cannot be empty" }); 49 | } else { 50 | const data = await limetorrentSearch(query); 51 | res.send(data); 52 | } 53 | }); 54 | 55 | module.exports = router; 56 | -------------------------------------------------------------------------------- /routes/torrent.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const telegram = require("node-telegram-bot-api"); 3 | const Torrent = require("../lib/torrent"); 4 | const botInit = require("../lib/bot"); 5 | const torrent = new Torrent(); 6 | 7 | const dev = process.env.NODE_ENV !== "production"; 8 | const site = dev ? require("../config").site : process.env.SITE; 9 | const token = dev ? require("../config").telegramToken : process.env.TELEGRAM_TOKEN; 10 | 11 | const router = express.Router(); 12 | 13 | if (!token) 14 | console.log( 15 | "Set telegram token env var to start telegram bot. Read docs at https://github.com/patheticGeek/torrent-aio-bot" 16 | ); 17 | 18 | if (site && token) { 19 | const botOptions = dev ? { polling: true } : {}; 20 | const bot = new telegram(token, botOptions); 21 | if (!dev) { 22 | router.post("/bot", (req, res) => { 23 | bot.processUpdate(req.body); 24 | res.sendStatus(200); 25 | }); 26 | bot.setWebHook(`${site}api/v1/torrent/bot`); 27 | } else { 28 | router.post("/bot", (req, res) => { 29 | res.sendStatus(200); 30 | }); 31 | } 32 | botInit(torrent, bot); 33 | console.log("Bot ready"); 34 | } 35 | 36 | router.get("/download", (req, res) => { 37 | const link = req.query.link; 38 | 39 | if (!link) { 40 | res.send({ error: true, errorMessage: "No link provided" }); 41 | } else if (link.indexOf("magnet:") !== 0) { 42 | res.send({ error: true, errorMessage: "Link is not a magnet link" }); 43 | } else { 44 | torrent.download(link, torr => res.send({ error: false, magnetURI: torr.magnetURI })); 45 | } 46 | }); 47 | 48 | router.get("/status", (req, res) => { 49 | const link = req.query.link; 50 | 51 | if (!link) { 52 | res.send({ error: true, errorMessage: "No link provided" }); 53 | } else { 54 | try { 55 | res.send({ error: false, status: torrent.get(link) }); 56 | } catch (e) { 57 | res.send({ error: false, errorMessage: e.message }); 58 | } 59 | } 60 | }); 61 | 62 | router.get("/remove", (req, res) => { 63 | const link = req.query.link; 64 | 65 | if (!link) { 66 | res.send({ error: true, errorMessage: "No link provided" }); 67 | } else { 68 | try { 69 | torrent.remove(link); 70 | res.send({ error: false }); 71 | } catch (e) { 72 | res.send({ error: true, errorMessage: e.message }); 73 | } 74 | } 75 | }); 76 | 77 | router.get("/list", (req, res) => { 78 | try { 79 | res.json({ 80 | error: false, 81 | torrents: torrent.list() 82 | }); 83 | } catch (e) { 84 | res.json({ error: true, errorMessage: e.message }); 85 | } 86 | }); 87 | 88 | module.exports = router; 89 | -------------------------------------------------------------------------------- /utils/diskinfo.js: -------------------------------------------------------------------------------- 1 | const disk = require("diskusage"); 2 | const prettyBytes = require("../utils/prettyBytes"); 3 | 4 | async function diskinfo(path = "/") { 5 | try { 6 | const { available, free, total } = await disk.check(path); 7 | return { 8 | path, 9 | available: prettyBytes(available), 10 | free: prettyBytes(free), 11 | total: prettyBytes(total), 12 | // Total and Available as bytes. needed to check used space 13 | totalInBytes: total, 14 | freeInBytes: free 15 | }; 16 | } catch (e) { 17 | console.log(e); 18 | return e.message; 19 | } 20 | } 21 | 22 | module.exports = diskinfo; 23 | -------------------------------------------------------------------------------- /utils/gdrive.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { google } = require("googleapis"); 3 | const logger = require("./logger"); 4 | const dev = process.env.NODE_ENV !== "production"; 5 | const { CLIENT_ID, CLIENT_SECRET, TOKEN, AUTH_CODE, GDRIVE_PARENT_FOLDER } = dev ? require("../config").creds : process.env; 6 | let parsedToken = null; 7 | if (TOKEN) { 8 | try { 9 | parsedToken = JSON.parse(TOKEN); 10 | } catch (e) { 11 | logger("TOKEN env not correct\nTOKEN set to:", TOKEN); 12 | } 13 | } 14 | 15 | const SCOPES = ["https://www.googleapis.com/auth/drive.metadata.readonly", "https://www.googleapis.com/auth/drive.file"]; 16 | 17 | if (!CLIENT_ID) { 18 | logger("CLIENT_ID env not set. Not uploading to gdrive."); 19 | } 20 | if (!CLIENT_SECRET) { 21 | logger("CLIENT_SECRET env not set. Not uploading to gdrive."); 22 | } 23 | if (!AUTH_CODE) { 24 | logger("AUTH_CODE env not set."); 25 | } 26 | if (!TOKEN) { 27 | logger("TOKEN env not set."); 28 | } 29 | if (GDRIVE_PARENT_FOLDER) { 30 | logger(`GDRIVE_PARENT_FOLDER set to ${GDRIVE_PARENT_FOLDER}`); 31 | } 32 | 33 | let auth = null; 34 | let drive = null; 35 | 36 | if (CLIENT_ID && CLIENT_SECRET) { 37 | authorize().then(a => { 38 | if (!a) return; 39 | auth = a; 40 | drive = google.drive({ version: "v3", auth }); 41 | logger("Gdrive client up"); 42 | }); 43 | } 44 | 45 | async function authorize() { 46 | const oAuth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, "urn:ietf:wg:oauth:2.0:oob"); 47 | 48 | if (!AUTH_CODE) { 49 | const authUrl = oAuth2Client.generateAuthUrl({ 50 | access_type: "offline", 51 | scope: SCOPES 52 | }); 53 | logger(`Get AUTH_CODE env by visiting this url: \n${authUrl}\n`); 54 | return null; 55 | } else if (AUTH_CODE && !TOKEN) { 56 | return oAuth2Client.getToken(AUTH_CODE, (err, token) => { 57 | if (err) { 58 | console.error("Error retrieving access token\n", err); 59 | return null; 60 | } 61 | oAuth2Client.setCredentials(token); 62 | if (!TOKEN) logger("Set TOKEN env to:\n", JSON.stringify(token)); 63 | else logger("Gdrive config OK."); 64 | return oAuth2Client; 65 | }); 66 | } else if (AUTH_CODE && TOKEN) { 67 | oAuth2Client.setCredentials(parsedToken); 68 | return oAuth2Client; 69 | } else { 70 | logger("AUTH_CODE:", !!AUTH_CODE); 71 | logger("TOKEN:", !!TOKEN); 72 | } 73 | } 74 | 75 | function getAuthURL(CLIENT_ID, CLIENT_SECRET) { 76 | const oAuth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, "urn:ietf:wg:oauth:2.0:oob"); 77 | const authUrl = oAuth2Client.generateAuthUrl({ 78 | access_type: "offline", 79 | scope: SCOPES 80 | }); 81 | return authUrl; 82 | } 83 | 84 | function getAuthToken(CLIENT_ID, CLIENT_SECRET, AUTH_CODE) { 85 | return new Promise((resolve, reject) => { 86 | const oAuth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, "urn:ietf:wg:oauth:2.0:oob"); 87 | oAuth2Client.getToken(AUTH_CODE, (err, token) => { 88 | err ? reject(err) : resolve(token); 89 | }); 90 | }); 91 | } 92 | 93 | function createFolder(name, parentId) { 94 | return new Promise((resolve, reject) => { 95 | var fileMetadata = { 96 | name, mimeType: "application/vnd.google-apps.folder" , parents: parentId ? [parentId] : null 97 | }; // prettier-ignore 98 | drive.files.create( 99 | { supportsTeamDrives: true, resource: fileMetadata, fields: "id" }, 100 | (err, file) => (err ? reject(err) : resolve(file)) 101 | ); // prettier-ignore 102 | }); 103 | } 104 | 105 | function uploadFileStream(name, stream, parentId = GDRIVE_PARENT_FOLDER) { 106 | return new Promise((resolve, reject) => { 107 | var media = { body: stream }; 108 | drive.files.create( 109 | { supportsTeamDrives: true, resource: { name, parents: parentId ? [parentId] : null }, media: media, fields: "id" }, 110 | (err, file) => (err ? reject(err) : resolve(file)) 111 | ); // prettier-ignore 112 | }); 113 | } 114 | 115 | function uploadFile(name, path, parentId = GDRIVE_PARENT_FOLDER) { 116 | return new Promise((resolve, reject) => { 117 | var media = { body: fs.createReadStream(path) }; 118 | drive.files.create( 119 | { supportsTeamDrives: true, resource: { name, parents: parentId ? [parentId] : null }, media: media, fields: "id" }, 120 | (err, file) => (err ? reject(err) : resolve(file)) 121 | ); // prettier-ignore 122 | }); 123 | } 124 | 125 | async function uploadFolder(path, parentId) { 126 | const intr = path.split("/"); 127 | const name = intr[intr.length - 1]; 128 | 129 | try { 130 | const stat = fs.lstatSync(path); 131 | 132 | if (stat.isDirectory()) { 133 | // make a folder in gdrive 134 | const folder = await createFolder(name, parentId || GDRIVE_PARENT_FOLDER); 135 | const folderId = folder.data.id; 136 | 137 | // get list of folders contents 138 | const contents = fs.readdirSync(path, { withFileTypes: true }); 139 | const uploads = contents.map(val => { 140 | const name = val.name; 141 | const isDir = val.isDirectory(); 142 | const isFile = val.isFile(); 143 | 144 | // if dir upload dir recursively 145 | // else file upload the file 146 | if (isDir) { 147 | return uploadFolder(`${path}/${name}`, folderId); 148 | } else if (isFile) { 149 | return uploadFile(name, `${path}/${name}`, folderId); 150 | } else { 151 | return null; 152 | } 153 | }); 154 | 155 | // await all uploads 156 | await Promise.all(uploads); 157 | 158 | // return the gdrive link 159 | return `https://drive.google.com/drive/folders/${folderId}`; 160 | } else if (stat.isFile()) { 161 | // make a folder in gdrive 162 | const folder = await createFolder(name, parentId || GDRIVE_PARENT_FOLDER); 163 | const folderId = folder.data.id; 164 | 165 | // upload the file to drive 166 | await uploadFile(name, `${path}`, folderId); 167 | 168 | // return the gdrive link 169 | return `https://drive.google.com/drive/folders/${folderId}`; 170 | } 171 | } catch (e) { 172 | console.log("error", e.message); 173 | return null; 174 | } 175 | } 176 | 177 | async function uploadWithLog(path, parentId) { 178 | const intr = path.split("/"); 179 | intr[intr.length - 1] = "gdrive.txt"; 180 | const gdriveText = intr.join("/"); 181 | fs.writeFileSync(gdriveText, "Upload started\n"); 182 | const url = await uploadFolder(path, parentId); 183 | if (url) { 184 | fs.appendFileSync(gdriveText, `Gdrive url: ${url}`); 185 | return url; 186 | } else { 187 | fs.appendFileSync(gdriveText, `An error occured. GDRIVE_PARENT_FOLDER: ${GDRIVE_PARENT_FOLDER}`); 188 | return null; 189 | } 190 | } 191 | 192 | function getFiles(folderId) { 193 | let query; 194 | const parent = folderId || GDRIVE_PARENT_FOLDER; 195 | if (parent) query = `'${parent}' in parents and trashed = false`; 196 | else query = "trashed = false"; 197 | return new Promise((resolve, reject) => { 198 | drive.files.list( 199 | { 200 | q: query, 201 | pageSize: 1000, 202 | supportsAllDrives: true, 203 | includeItemsFromAllDrives: true, 204 | fields: "nextPageToken, files(id, name, modifiedTime, iconLink, mimeType)" 205 | }, 206 | (err, res) => (err ? reject(err) : resolve(res.data.files)) 207 | ); 208 | }); 209 | } 210 | 211 | function sendFileStream(req, res) { 212 | const fileId = req.query.id || req.params.id; 213 | if (!fileId) res.sendStatus(404); 214 | drive.files.get( 215 | { 216 | fileId, 217 | alt: "media" 218 | }, 219 | { responseType: "stream", ...req.headers }, 220 | (err, resp) => { 221 | if (!err) { 222 | Object.keys(resp.headers).forEach(val => { 223 | res.setHeader(val, resp.headers[val]); 224 | }); 225 | resp.data 226 | .on("end", () => {}) 227 | .on("error", () => {}) 228 | .pipe(res); 229 | } else { 230 | console.log("error ", err); 231 | res.end(); 232 | } 233 | } 234 | ); 235 | } 236 | 237 | module.exports = { uploadFolder, uploadFileStream, uploadFile, uploadWithLog, getFiles, sendFileStream, getAuthURL, getAuthToken }; 238 | -------------------------------------------------------------------------------- /utils/humanTime.js: -------------------------------------------------------------------------------- 1 | function humanTime(ms) { 2 | let seconds = ms / 1000; 3 | let result = ""; 4 | const days = Math.floor((seconds % 31536000) / 86400); 5 | if (days > 0) result += `${days}d `; 6 | const hours = Math.floor(((seconds % 31536000) % 86400) / 3600); 7 | if (hours > 0) result += `${hours}h `; 8 | const minutes = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60); 9 | if (minutes > 0) result += `${minutes}m `; 10 | seconds = ((((seconds % 31536000) % 86400) % 3600) % 60).toFixed(0); 11 | if (seconds > 0) result += `${seconds}s`; 12 | if (result === "") result += "0s"; 13 | return result; 14 | } 15 | 16 | module.exports = humanTime; 17 | -------------------------------------------------------------------------------- /utils/keepalive.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const dev = process.env.NODE_ENV !== "production"; 3 | const site = dev ? require("../config").site : process.env.SITE; 4 | 5 | function keepalive() { 6 | if (site) { 7 | setInterval(async () => { 8 | const data = await axios(`https://ping-pong-sn.herokuapp.com/pingback?link=${site}`); 9 | console.log("keep alive triggred, status: ", data.status); 10 | }, 1560000); 11 | } else { 12 | console.warn("Set site env var. Read docs at https://github.com/patheticGeek/torrent-aio-bot"); 13 | } 14 | } 15 | 16 | module.exports = keepalive; 17 | -------------------------------------------------------------------------------- /utils/logger.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | const logFile = "./logs.txt"; 4 | 5 | fs.writeFileSync(logFile, ""); 6 | 7 | function logger(...args) { 8 | let toWrite = "\n"; 9 | args.forEach(arg => { 10 | if (typeof arg === "string" || typeof arg === "number") { 11 | toWrite += arg; 12 | } else { 13 | try { 14 | const stringified = JSON.stringify(arg); 15 | toWrite += stringified; 16 | } catch (e) { 17 | toWrite += arg; 18 | } 19 | } 20 | toWrite += " "; 21 | }); 22 | console.log(toWrite); 23 | fs.appendFileSync(logFile, toWrite); 24 | } 25 | 26 | module.exports = logger; 27 | -------------------------------------------------------------------------------- /utils/mkfile.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | function mkfile(file) { 4 | file = file.split("/"); 5 | const fileName = file.pop(); 6 | 7 | file.forEach((val, i) => { 8 | if (val !== ".") { 9 | const currPath = file.slice(0, i + 1).join("/"); 10 | if (!fs.existsSync(currPath)) { 11 | fs.mkdirSync(currPath); 12 | } 13 | } 14 | }); 15 | 16 | file = file.join("/") + "/" + fileName; 17 | fs.writeFileSync(file); 18 | } 19 | 20 | module.exports = mkfile; 21 | -------------------------------------------------------------------------------- /utils/prettyBytes.js: -------------------------------------------------------------------------------- 1 | function prettyBytes(num) { 2 | var exponent, 3 | unit, 4 | neg = num < 0, 5 | units = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; 6 | if (neg) num = -num; 7 | if (num < 1) return (neg ? "-" : "") + num + " B"; 8 | exponent = Math.min(Math.floor(Math.log(num) / Math.log(1000)), units.length - 1); 9 | num = Number((num / Math.pow(1000, exponent)).toFixed(2)); 10 | unit = units[exponent]; 11 | return (neg ? "-" : "") + num + " " + unit; 12 | } 13 | 14 | module.exports = prettyBytes; 15 | -------------------------------------------------------------------------------- /utils/status.js: -------------------------------------------------------------------------------- 1 | const diskinfo = require("./diskinfo"); 2 | const humanTime = require("../utils/humanTime"); 3 | const prettyBytes = require("./prettyBytes"); 4 | 5 | async function status(path = "/app") { 6 | let info = ""; 7 | 8 | try { 9 | let dinfo = await diskinfo(path); 10 | const memory = process.memoryUsage(); 11 | 12 | if (typeof dinfo === "string") throw Error(dinfo); 13 | 14 | info += `✯ Disk, \n`; 15 | info += ` Total: ${dinfo.total} \n`; 16 | //info += ` Available: ${dinfo.available} \n`; 17 | info += ` Used: ${prettyBytes(dinfo.totalInBytes - dinfo.freeInBytes)} \n`; 18 | info += ` Free: ${dinfo.free} \n\n`; 19 | info += `✯ Memory, \n`; 20 | info += ` Total: ${prettyBytes(memory.external)} \n`; 21 | info += ` Rss: ${prettyBytes(memory.rss)} \n\n`; 22 | info += `✯ Heap, \n`; 23 | info += ` Total: ${prettyBytes(memory.heapTotal)} \n`; 24 | info += ` Used: ${prettyBytes(memory.heapUsed)} \n\n`; 25 | info += `✯ Uptime: ${humanTime(process.uptime() * 1000)} \n`; 26 | 27 | return info; 28 | } catch (e) { 29 | console.log(e); 30 | info = e.message; 31 | return info; 32 | } 33 | } 34 | 35 | module.exports = status; 36 | -------------------------------------------------------------------------------- /utils/ziper.js: -------------------------------------------------------------------------------- 1 | const { zip } = require("zip-a-folder"); 2 | 3 | async function ziper(folderPath, savePath) { 4 | try { 5 | if (!savePath) { 6 | var a = folderPath.split("/"); 7 | var name = a.pop(); 8 | savePath = a.join("/") + `/${name}.zip`; 9 | } 10 | await zip(folderPath, savePath); 11 | } catch (e) { 12 | console.log(e.message); 13 | } 14 | } 15 | 16 | module.exports = ziper; 17 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "nprogress": "^0.2.0", 10 | "react": "^16.13.1", 11 | "react-dom": "^16.13.1", 12 | "react-router": "^5.2.0", 13 | "react-router-dom": "^5.2.0", 14 | "react-scripts": "3.4.1", 15 | "swr": "^0.2.2" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": "react-app" 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Nexa Bots 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /web/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; 3 | 4 | import Home from "./screens/Home"; 5 | import DriveHelp from "./screens/DriveHelp"; 6 | 7 | function App() { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /web/src/assets/css/helpers.css: -------------------------------------------------------------------------------- 1 | .ion { 2 | margin-bottom: -1px; 3 | } 4 | 5 | .no-border { 6 | border: 0px !important; 7 | } 8 | 9 | .cursor-pointer { 10 | cursor: pointer; 11 | } 12 | 13 | .overflow-hidden { 14 | overflow: hidden; 15 | } 16 | 17 | .text-oneline { 18 | white-space: nowrap; 19 | overflow: hidden; 20 | text-overflow: ellipsis; 21 | } 22 | .text-twoline { 23 | max-lines: 2; 24 | text-overflow: ellipsis; 25 | } 26 | 27 | .text-400 { 28 | font-weight: 400; 29 | } 30 | .text-300 { 31 | font-weight: 300; 32 | } 33 | .text-600 { 34 | font-weight: 600; 35 | } 36 | .text-700 { 37 | font-weight: 700; 38 | } 39 | .text-center { 40 | text-align: center; 41 | } 42 | .text-muted { 43 | color: var(--muted) !important; 44 | } 45 | .text-color { 46 | color: var(--color) !important; 47 | } 48 | .text-primary { 49 | color: var(--primary) !important; 50 | } 51 | .text-warning { 52 | color: var(--warning) !important; 53 | } 54 | .text-danger { 55 | color: var(--danger) !important; 56 | } 57 | .text-success { 58 | color: var(--success) !important; 59 | } 60 | .text-muted__hover:hover { 61 | color: var(--muted) !important; 62 | } 63 | .text-color__hover:hover { 64 | color: var(--color) !important; 65 | } 66 | .text-primary__hover:hover { 67 | color: var(--primary) !important; 68 | } 69 | .text-warning__hover:hover { 70 | color: var(--warning) !important; 71 | } 72 | .text-danger__hover:hover { 73 | color: var(--danger) !important; 74 | } 75 | .text-success__hover:hover { 76 | color: var(--success) !important; 77 | } 78 | 79 | .main-bg-color { 80 | background-color: var(--main-background-color); 81 | } 82 | .bg-color { 83 | background-color: var(--background-color); 84 | } 85 | .bg-white { 86 | background-color: #fff !important; 87 | } 88 | .bg-default { 89 | background-color: #172b4d !important; 90 | } 91 | .bg-primary { 92 | background-color: var(--primary) !important; 93 | } 94 | .bg-gradient-default { 95 | background: linear-gradient(87deg, #172b4d 0, #1a174d 100%) !important; 96 | } 97 | .bg-gradient-red { 98 | background: linear-gradient(87deg, #f5365c 0, #f56036 100%) !important; 99 | } 100 | .bg-gradient-green { 101 | background: linear-gradient(87deg, #2dce89 0, #2dcecc 100%) !important; 102 | } 103 | .bg-gradient-orange { 104 | background: linear-gradient(87deg, #fb6340 0, #fbb140 100%) !important; 105 | } 106 | .bg-gradient-primary { 107 | background: linear-gradient(87deg, #5e72e4 0, #825ee4 100%) !important; 108 | } 109 | .bg-gradient-success { 110 | background: linear-gradient(87deg, #2dce89 0, #2dcecc 100%) !important; 111 | } 112 | .bg-gradient-info { 113 | background: linear-gradient(87deg, #11cdef 0, #1171ef 100%) !important; 114 | } 115 | .bg-gradient-warning { 116 | background: linear-gradient(87deg, #fb6340 0, #fbb140 100%) !important; 117 | } 118 | .bg-gradient-danger { 119 | background: linear-gradient(87deg, #f5365c 0, #f56036 100%) !important; 120 | } 121 | 122 | .border-1 { 123 | border: 1px solid var(--color); 124 | } 125 | .border-bottom-1 { 126 | border-bottom: 1px solid var(--color); 127 | } 128 | .border-top-1 { 129 | border-top: 1px solid var(--color); 130 | } 131 | .border-left-1 { 132 | border-left: 1px solid var(--color); 133 | } 134 | .border-right-1 { 135 | border-right: 1px solid var(--color); 136 | } 137 | 138 | .border-bottom-2 { 139 | border-bottom: 2px solid var(--color); 140 | } 141 | .border-top-2 { 142 | border-top: 2px solid var(--color); 143 | } 144 | .border-left-2 { 145 | border-left: 2px solid var(--color); 146 | } 147 | .border-right-2 { 148 | border-right: 2px solid var(--color); 149 | } 150 | 151 | .border-primary { 152 | border-color: var(--primary) !important; 153 | } 154 | .border-color { 155 | border-color: var(--border-color) !important; 156 | } 157 | 158 | .min-height-100 { 159 | min-height: 100% !important; 160 | } 161 | .max-width-100 { 162 | max-width: 100% !important; 163 | } 164 | .width-100 { 165 | width: 100% !important; 166 | } 167 | .height-100 { 168 | height: 100% !important; 169 | } 170 | .flex-wrap { 171 | flex-wrap: wrap; 172 | } 173 | .align-items-left { 174 | align-items: flex-end !important; 175 | } 176 | .align-items-center { 177 | align-items: center !important; 178 | } 179 | .d-none { 180 | display: none !important; 181 | } 182 | .d-block { 183 | display: block !important; 184 | } 185 | .d-flex { 186 | display: flex !important; 187 | } 188 | .d-flex-column { 189 | display: flex !important; 190 | flex-direction: column; 191 | } 192 | .flex-wrap { 193 | flex-wrap: wrap; 194 | } 195 | .flex-1 { 196 | flex: 1; 197 | } 198 | .space-between { 199 | justify-content: space-between !important; 200 | } 201 | .space-around { 202 | justify-content: space-around !important; 203 | } 204 | .space-evenly { 205 | justify-content: space-evenly !important; 206 | } 207 | .justify-center { 208 | justify-content: center !important; 209 | } 210 | 211 | .row { 212 | display: flex; 213 | justify-content: center; 214 | } 215 | .row.mg { 216 | margin: 7px 0px; 217 | } 218 | .row > * { 219 | padding-right: 14px; 220 | display: inline; 221 | } 222 | .row > *:last-child { 223 | padding-right: 0px; 224 | } 225 | .col { 226 | max-width: 100%; 227 | flex-basis: 0; 228 | flex-grow: 1; 229 | } 230 | .col-sm { 231 | max-width: 540px; 232 | } 233 | .col-md { 234 | max-width: 720px; 235 | } 236 | .col-lg { 237 | max-width: 960px; 238 | } 239 | .col-xl { 240 | max-width: 1140px; 241 | } 242 | .col-100 { 243 | width: 100%; 244 | } 245 | .col-75 { 246 | width: 75%; 247 | } 248 | .col-66 { 249 | width: 66.66%; 250 | } 251 | .col-50 { 252 | width: 50%; 253 | } 254 | .col-33 { 255 | width: 33.33%; 256 | } 257 | .col-25 { 258 | width: 25%; 259 | } 260 | .col-auto { 261 | width: auto; 262 | } 263 | 264 | .m-0 { 265 | margin: 0px !important; 266 | } 267 | .m-auto { 268 | margin: auto !important; 269 | } 270 | .mh-auto { 271 | margin-left: auto !important; 272 | margin-right: auto !important; 273 | } 274 | .mv-auto { 275 | margin-top: auto !important; 276 | margin-bottom: auto !important; 277 | } 278 | 279 | .mh-1 { 280 | margin-left: 1em !important; 281 | margin-right: 1em !important; 282 | } 283 | .mv-1 { 284 | margin-top: 1em !important; 285 | margin-bottom: 1em !important; 286 | } 287 | 288 | .mh-05 { 289 | margin-left: 0.5em !important; 290 | margin-right: 0.5em !important; 291 | } 292 | .mv-05 { 293 | margin-top: 0.5em !important; 294 | margin-bottom: 0.5em !important; 295 | } 296 | 297 | .mt-0 { 298 | margin-top: 0px !important; 299 | } 300 | 301 | .mt-2 { 302 | margin-top: 2em !important; 303 | } 304 | .mb-2 { 305 | margin-bottom: 2em !important; 306 | } 307 | .mr-2 { 308 | margin-right: 2em !important; 309 | } 310 | .ml-2 { 311 | margin-left: 2em !important; 312 | } 313 | 314 | .mt-1 { 315 | margin-top: 1em !important; 316 | } 317 | .mb-1 { 318 | margin-bottom: 1em !important; 319 | } 320 | .mr-1 { 321 | margin-right: 1em !important; 322 | } 323 | .ml-1 { 324 | margin-left: 1em !important; 325 | } 326 | 327 | .mt-05 { 328 | margin-top: 0.5em !important; 329 | } 330 | .mb-05 { 331 | margin-bottom: 0.5em !important; 332 | } 333 | .mr-05 { 334 | margin-right: 0.5em !important; 335 | } 336 | .ml-05 { 337 | margin-left: 0.5em !important; 338 | } 339 | 340 | .p-0 { 341 | padding: 0px !important; 342 | } 343 | .pt-0 { 344 | padding-top: 0px !important; 345 | } 346 | 347 | .ph-2 { 348 | padding-left: 2em !important; 349 | padding-right: 2em !important; 350 | } 351 | .pv-2 { 352 | padding-top: 2em !important; 353 | padding-bottom: 2em !important; 354 | } 355 | 356 | .ph-1 { 357 | padding-left: 1em !important; 358 | padding-right: 1em !important; 359 | } 360 | .pv-1 { 361 | padding-top: 1em !important; 362 | padding-bottom: 1em !important; 363 | } 364 | 365 | .ph-05 { 366 | padding-left: 0.5em !important; 367 | padding-right: 0.5em !important; 368 | } 369 | .pv-05 { 370 | padding-top: 0.5em !important; 371 | padding-bottom: 0.5em !important; 372 | } 373 | 374 | .pt-1 { 375 | padding-top: 1em !important; 376 | } 377 | .pb-1 { 378 | padding-bottom: 1em !important; 379 | } 380 | 381 | .pt-2 { 382 | padding-top: 2em !important; 383 | } 384 | .pb-2 { 385 | padding-bottom: 2em !important; 386 | } 387 | 388 | .pt-05 { 389 | padding-top: 0.5em !important; 390 | } 391 | .pb-05 { 392 | padding-bottom: 0.5em !important; 393 | } 394 | 395 | .mw-512 { 396 | max-width: 512px; 397 | } 398 | 399 | .mw-768 { 400 | max-width: 768px; 401 | } 402 | 403 | .mobile-only { 404 | display: none; 405 | } 406 | 407 | @media screen and (max-width: 768px) { 408 | .row { 409 | flex-wrap: wrap; 410 | } 411 | .row > * { 412 | padding: 0px; 413 | } 414 | .col-33, 415 | .col-25, 416 | .col-100, 417 | .col-75, 418 | .col-66, 419 | .col-50 { 420 | width: 100%; 421 | } 422 | .col-33:not(:first-child), 423 | .col-25:not(:first-child), 424 | .col-100:not(:first-child), 425 | .col-75:not(:first-child), 426 | .col-66:not(:first-child), 427 | .col-50:not(:first-child) { 428 | margin-top: 14px; 429 | } 430 | .col { 431 | padding: 0px 14px; 432 | } 433 | .col:first-child { 434 | padding-left: 0px; 435 | } 436 | .col:last-child { 437 | padding-right: 0px; 438 | } 439 | .mobile-only { 440 | display: block; 441 | } 442 | .mobile-width-100 { 443 | width: 100%; 444 | padding-left: 0px; 445 | padding-right: 0px; 446 | } 447 | .desktop-only { 448 | display: none; 449 | } 450 | } 451 | 452 | @media screen and (max-width: 425px) { 453 | .tablet-desktop-only { 454 | display: none; 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /web/src/assets/css/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0px; 4 | padding: 0px; 5 | font-size: 1rem; 6 | font-weight: 300; 7 | line-height: 1.5; 8 | font-family: "IBM Plex Sans", sans-serif; 9 | z-index: 0; 10 | --color: #1e1e1e; 11 | --primary: #5e72e4; 12 | --secondary: #f7fafc; 13 | --success: #2dce89; 14 | --warning: #fb6340; 15 | --danger: #f5365c; 16 | --muted: #8898aa; 17 | --input-bg: #f7f7f7; 18 | --navbar-background: #ffffff; 19 | --navbar-color: #5e5e5e; 20 | --navbar-color-hover: #161616; 21 | --main-background-color: #f8f9fe; 22 | --background-color: #fff; 23 | --pagination-color: #dee2e6; 24 | --pagination-text: #8898aa; 25 | --border-color: #eeeeee; 26 | } 27 | body.dark { 28 | --color: #f2f2f2; 29 | --primary: #3461af; 30 | --success: #2f825e; 31 | --danger: #c12f4c; 32 | --warning: #cc593e; 33 | --muted: #616161; 34 | --input-bg: #111111; 35 | --background-color: #080808; 36 | --navbar-background: #151515; 37 | --navbar-color: #c9c9c9; 38 | --navbar-color-hover: #fafafa; 39 | --main-background-color: #1a1c1d; 40 | --pagination-color: #343c45; 41 | --pagination-text: #b6b1a6; 42 | --border-color: #3d3d3d; 43 | } 44 | 45 | #nprogress { 46 | pointer-events: none; 47 | } 48 | #nprogress .bar { 49 | background: var(--primary); 50 | position: fixed; 51 | z-index: 25; 52 | top: 0; 53 | left: 0; 54 | width: 100%; 55 | height: 3px; 56 | } 57 | #nprogress .peg { 58 | display: block; 59 | position: absolute; 60 | right: 0; 61 | width: 100px; 62 | height: 100%; 63 | box-shadow: 0 0 10px var(--primary), 0 0 5px var(--primary); 64 | opacity: 1; 65 | -webkit-transform: rotate(3deg) translate(0px, -4px); 66 | -ms-transform: rotate(3deg) translate(0px, -4px); 67 | transform: rotate(3deg) translate(0px, -4px); 68 | } 69 | .nprogress-custom-parent { 70 | overflow: hidden; 71 | position: relative; 72 | } 73 | .nprogress-custom-parent #nprogress .bar { 74 | position: absolute; 75 | } 76 | 77 | div#root { 78 | min-height: 100vh; 79 | display: flex; 80 | flex-direction: column; 81 | color: var(--color); 82 | background-color: var(--main-background-color); 83 | } 84 | 85 | main, 86 | div.main { 87 | flex: 1; 88 | display: flex; 89 | flex-direction: column; 90 | padding: 12px 16px; 91 | width: calc(100% - 32px); 92 | } 93 | main > .content, 94 | div.main > .content { 95 | width: 100%; 96 | max-width: 1140px; 97 | margin: 0px auto; 98 | } 99 | main > .content.large, 100 | div.main > .content.large { 101 | max-width: 1500px; 102 | } 103 | 104 | h1, 105 | h2, 106 | h3, 107 | h4, 108 | .h1, 109 | .h2, 110 | .h3, 111 | .h4 { 112 | margin: 0px; 113 | margin-bottom: 0.25rem; 114 | font-weight: 600; 115 | } 116 | 117 | .h1, 118 | h1 { 119 | font-size: 1.625rem; 120 | } 121 | .h2, 122 | h2 { 123 | font-size: 1.25rem; 124 | } 125 | .h3, 126 | h3 { 127 | font-size: 1.0625rem; 128 | } 129 | .h4, 130 | h4 { 131 | font-size: 0.9375rem; 132 | } 133 | 134 | .dp-1, 135 | .dp-2, 136 | .dp-3 { 137 | font-weight: 600; 138 | line-height: 1.5; 139 | } 140 | 141 | .dp-1 { 142 | font-size: 3.3rem; 143 | } 144 | .dp-2 { 145 | font-size: 2.75rem; 146 | } 147 | .dp-3 { 148 | font-size: 2.1875rem; 149 | } 150 | 151 | .small, 152 | small { 153 | font-size: 80%; 154 | font-weight: 400; 155 | } 156 | 157 | a, 158 | .a { 159 | margin: 0px 8px; 160 | color: #5e72e4; 161 | font-weight: 400; 162 | text-decoration: none; 163 | transition: 0.2s color ease-in; 164 | } 165 | a:hover, 166 | .a:hover { 167 | color: #5e96e4; 168 | } 169 | a:first-child, 170 | .a:first-child { 171 | margin-left: 0px; 172 | } 173 | a:last-child, 174 | .a:last-child { 175 | margin-right: 0px; 176 | } 177 | 178 | p { 179 | font-size: 1rem; 180 | font-weight: 300; 181 | line-height: 1.7; 182 | } 183 | .lead { 184 | font-size: 1.25rem; 185 | font-weight: 300; 186 | line-height: 1.7; 187 | margin-top: 1.5rem; 188 | } 189 | 190 | .mark, 191 | mark { 192 | padding: 0.2em; 193 | background-color: #fcf8e3; 194 | } 195 | 196 | .badge { 197 | font-size: 66%; 198 | font-weight: 600; 199 | line-height: 1; 200 | display: inline-block; 201 | padding: 0.35rem 0.375rem; 202 | text-align: center; 203 | vertical-align: baseline; 204 | white-space: nowrap; 205 | padding-right: 0.875em; 206 | padding-left: 0.875em; 207 | border-radius: 10rem; 208 | text-transform: uppercase; 209 | background-color: rgb(52, 97, 175); 210 | color: #fff; 211 | margin: 0.5rem; 212 | margin-left: 0px; 213 | } 214 | .badge.neutral { 215 | background-color: #fff; 216 | color: #212121; 217 | } 218 | .badge.success { 219 | color: var(--success); 220 | background-color: rgb(147, 231, 195); 221 | } 222 | .badge.danger { 223 | color: var(--danger); 224 | background-color: rgb(251, 175, 190); 225 | } 226 | .badge.warning { 227 | color: var(--warning); 228 | background-color: rgb(254, 201, 189); 229 | } 230 | 231 | button, 232 | .btn { 233 | color: #fff; 234 | margin: 0.5rem; 235 | font-weight: 600; 236 | line-height: 1.5; 237 | user-select: none; 238 | position: relative; 239 | font-size: 0.875rem; 240 | text-transform: none; 241 | display: inline-block; 242 | will-change: transform; 243 | letter-spacing: 0.025em; 244 | transition: all 0.15s ease; 245 | padding: 0.5rem 1.25rem; 246 | text-align: center; 247 | vertical-align: middle; 248 | white-space: nowrap; 249 | border: 0px solid transparent; 250 | border-radius: 0.375rem; 251 | border-color: 0px solid var(--primary); 252 | background-color: var(--primary); 253 | box-shadow: 0 4px 6px rgba(50, 50, 50, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08); 254 | } 255 | button.sm, 256 | .btn.sm { 257 | line-height: 1.5; 258 | font-size: 0.75rem; 259 | padding: 0.25rem 0.5rem; 260 | border-radius: 0.375rem; 261 | } 262 | button.neutral, 263 | .btn.neutral { 264 | border-color: #fff; 265 | background-color: #fff; 266 | color: #1e1e1e; 267 | } 268 | button.success, 269 | .btn.success { 270 | border-color: var(--success); 271 | background-color: var(--success); 272 | } 273 | button.danger, 274 | .btn.danger { 275 | border-color: var(--danger); 276 | background-color: var(--danger); 277 | } 278 | button.warning, 279 | .btn.warning { 280 | border-color: var(--warning); 281 | background-color: var(--warning); 282 | } 283 | button.icon-only, 284 | .btn.icon-only { 285 | width: 2.375rem; 286 | height: 2.375rem; 287 | padding: 0; 288 | } 289 | button > .btn-text:not(:first-child), 290 | .btn > .btn-text:not(:first-child) { 291 | margin-left: 0.75em; 292 | } 293 | button > .btn-icon, 294 | .btn > .btn-icon { 295 | font-size: 140%; 296 | margin-top: 2px; 297 | } 298 | button.loading, 299 | .btn.loading { 300 | color: transparent; 301 | } 302 | button.loading:before, 303 | .btn.loading:before { 304 | content: ""; 305 | width: 20px; 306 | height: 20px; 307 | position: absolute; 308 | border-radius: 50%; 309 | top: calc(50% - 12px); 310 | left: calc(50% - 12px); 311 | transform-origin: center; 312 | border: 2px solid transparent; 313 | animation: loader 0.8s linear infinite; 314 | border-left-color: #fff; 315 | } 316 | button.sm.loading:before, 317 | .btn.sm.loading:before { 318 | width: 14px; 319 | height: 14px; 320 | top: calc(50% - 9px); 321 | left: calc(50% - 9px); 322 | } 323 | button.full-width, 324 | .btn.full-width { 325 | width: calc(100% - 3rem); 326 | } 327 | button:hover, 328 | .btn:hover { 329 | cursor: pointer; 330 | transform: translateY(-1px); 331 | box-shadow: 0 7px 14px rgba(50, 50, 50, 0.1), 0 3px 6px rgba(0, 0, 0, 0.08); 332 | } 333 | button:focus, 334 | .btn:focus { 335 | outline: 0px; 336 | } 337 | button.minimal, 338 | .btn.minimal { 339 | box-shadow: none; 340 | color: var(--color); 341 | border-color: transparent; 342 | background-color: transparent; 343 | } 344 | button.minimal:hover, 345 | .btn.minimal:hover { 346 | transform: translateY(0px); 347 | } 348 | button:first-child, 349 | .btn:first-child { 350 | margin-left: 0px; 351 | } 352 | button:last-child, 353 | .btn:last-child { 354 | margin-right: 0px; 355 | } 356 | button:active, 357 | .btn:active { 358 | transform: translateY(0px); 359 | } 360 | .btn:disabled, 361 | button[disabled], 362 | .btn.disabled { 363 | opacity: 0.65; 364 | box-shadow: none; 365 | transform: translateY(0px); 366 | cursor: default; 367 | } 368 | 369 | .card { 370 | display: flex; 371 | min-width: 300px; 372 | margin-bottom: 30px; 373 | word-wrap: break-word; 374 | flex-direction: column; 375 | background-size: cover; 376 | border-radius: 0.25rem; 377 | background-color: #ffffff; 378 | background-position: center; 379 | background-repeat: no-repeat; 380 | transition: all 0.15s ease; 381 | box-shadow: 1px 1px 5px 0px rgba(136, 152, 170, 0.15); 382 | } 383 | body.dark .card { 384 | background-color: #151515; 385 | box-shadow: none; 386 | } 387 | .card .border-card { 388 | border: 1px solid var(--border-color); 389 | box-shadow: none !important; 390 | } 391 | .card .card-image-top { 392 | width: 100%; 393 | height: auto; 394 | border-style: none; 395 | vertical-align: middle; 396 | object-fit: cover; 397 | object-position: center; 398 | border-top-left-radius: 0.25rem; 399 | border-top-right-radius: 0.25rem; 400 | } 401 | .card .card-header { 402 | margin-bottom: 0; 403 | padding: 1rem 1.25rem; 404 | border-top-left-radius: 0.25rem; 405 | border-top-right-radius: 0.25rem; 406 | border-bottom: 1px solid var(--border-color); 407 | } 408 | .card .card-header > * { 409 | margin: 0px; 410 | } 411 | .card .card-header.compact { 412 | padding: 0.825rem 1rem; 413 | } 414 | .card .card-title { 415 | margin-bottom: 0.725rem; 416 | } 417 | .card .card-subtitle { 418 | margin-bottom: 0.725rem; 419 | } 420 | .card .card-body { 421 | padding: 1.25rem 1.5rem; 422 | display: flex; 423 | flex-direction: column; 424 | } 425 | .card .card-body.compact { 426 | padding: 0.825rem 1rem; 427 | } 428 | .card p:last-child { 429 | margin-bottom: 0; 430 | } 431 | .card .card-footer { 432 | padding: 1.25rem 1.5rem; 433 | border-top: 1px solid var(--border-color); 434 | } 435 | .card-footer:last-child { 436 | border-radius: 0 0 calc(0.375rem - 1px) calc(0.375rem - 1px); 437 | } 438 | .card.lift-hover:hover { 439 | transform: translateY(-20px); 440 | } 441 | 442 | .list-group { 443 | width: 100%; 444 | display: flex; 445 | flex-direction: column; 446 | overflow-x: auto; 447 | } 448 | .list-group:first-child .list-item:first-of-type { 449 | border-top-left-radius: 0.375rem; 450 | border-top-right-radius: 0.375rem; 451 | } 452 | .list-group .list-item { 453 | padding: 1rem; 454 | font-weight: 400; 455 | border-bottom: 1px solid var(--pagination-color); 456 | } 457 | 458 | .form-group { 459 | display: flex; 460 | flex-wrap: wrap; 461 | margin-bottom: 0.5rem; 462 | } 463 | .form-group > * { 464 | margin: auto 5px; 465 | } 466 | 467 | .form-group > input[type="checkbox"] { 468 | display: none; 469 | } 470 | input.custom { 471 | display: none; 472 | } 473 | .form-group > label.custom-checkbox { 474 | font-size: 0.875rem; 475 | padding-left: 28px; 476 | user-select: none; 477 | cursor: pointer; 478 | } 479 | .form-group > label.custom-checkbox:before { 480 | content: ""; 481 | width: 1.25rem; 482 | height: 1.25rem; 483 | margin-left: -28px; 484 | position: absolute; 485 | border-radius: 4px; 486 | transform-origin: center; 487 | background-size: 50% 50%; 488 | background-repeat: no-repeat; 489 | background-color: var(--input-bg); 490 | background-position: center center; 491 | transition: all 0.2s cubic-bezier(0.68, -0.55, 0.265, 1.55); 492 | box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08); 493 | } 494 | .form-group > input[type="checkbox"]:disabled ~ label.custom-checkbox { 495 | color: var(--muted); 496 | cursor: default; 497 | } 498 | .form-group > input[type="checkbox"]:disabled ~ label.custom-checkbox:before { 499 | opacity: 0.65; 500 | } 501 | .form-group > input[type="checkbox"]:checked ~ label.custom-checkbox:before { 502 | background-color: var(--primary); 503 | background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E"); 504 | } 505 | 506 | .form-group > input[type="radio"] { 507 | display: none; 508 | } 509 | .form-group > label.custom-radio { 510 | font-size: 0.875rem; 511 | padding-left: 28px; 512 | cursor: pointer; 513 | width: 1.25rem; 514 | user-select: none; 515 | transition: all 0.2s cubic-bezier(0.68, -0.55, 0.265, 1.55); 516 | } 517 | .form-group > label.custom-radio:before { 518 | content: ""; 519 | width: 1.25rem; 520 | height: 1.25rem; 521 | margin-top: 3px; 522 | position: absolute; 523 | border-radius: 50%; 524 | margin-left: -28px; 525 | background-size: 50%; 526 | transform-origin: center; 527 | background-position: center; 528 | background-repeat: no-repeat; 529 | background-color: var(--input-bg); 530 | transition: all 0.2s cubic-bezier(0.68, -0.55, 0.265, 1.55); 531 | box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08); 532 | } 533 | .form-group > input[type="radio"]:disabled ~ label.custom-radio { 534 | color: var(--muted); 535 | cursor: default; 536 | } 537 | .form-group > input[type="radio"]:disabled ~ label.custom-radio:before { 538 | opacity: 0.65; 539 | } 540 | .form-group > input[type="radio"]:checked ~ label.custom-radio:before { 541 | background-color: var(--primary); 542 | background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E"); 543 | } 544 | 545 | label.custom-toggle { 546 | position: relative; 547 | display: inline-flex; 548 | width: 58px; 549 | height: 1.5rem; 550 | margin: 0; 551 | } 552 | .custom-toggle-slider { 553 | width: 56px; 554 | height: 1.5rem; 555 | cursor: pointer; 556 | display: inline-block; 557 | background-color: transparent; 558 | border: 1px solid var(--muted); 559 | border-radius: 34px !important; 560 | } 561 | .custom-toggle-slider:before { 562 | content: ""; 563 | margin-left: 2px; 564 | border-radius: 50%; 565 | position: absolute; 566 | margin-top: 0.15rem; 567 | width: calc(1.5rem - 4px); 568 | height: calc(1.5rem - 4px); 569 | background-color: var(--muted); 570 | transition: all 0.2s cubic-bezier(0.68, -0.55, 0.265, 1.55); 571 | } 572 | input[type="checkbox"]:checked ~ .custom-toggle-slider { 573 | border-color: var(--primary); 574 | } 575 | input[type="checkbox"]:checked ~ .custom-toggle-slider:before { 576 | margin-left: 34px; 577 | background-color: var(--primary); 578 | } 579 | .custom-toggle-slider:after { 580 | position: absolute; 581 | color: var(--muted); 582 | content: attr(data-off); 583 | font-size: 0.75rem; 584 | font-weight: 600; 585 | line-height: 24px; 586 | display: block; 587 | overflow: hidden; 588 | min-width: 1.66667rem; 589 | margin: 0 0.21667rem; 590 | left: auto; 591 | right: 0; 592 | text-align: center; 593 | transition: all 0.2s cubic-bezier(0.68, -0.55, 0.265, 1.55); 594 | } 595 | input[type="checkbox"]:checked ~ .custom-toggle-slider:after { 596 | color: var(--primary); 597 | content: attr(data-on); 598 | left: 0; 599 | right: auto; 600 | } 601 | 602 | .form-control { 603 | border: 0; 604 | margin-top: 0.5rem; 605 | padding: 0.625rem 0.75rem; 606 | font-family: inherit; 607 | margin-bottom: 0.5rem; 608 | border-radius: 0.375rem; 609 | background-color: var(--input-bg); 610 | color: var(--color); 611 | background-clip: padding-box; 612 | font-size: 0.875rem; 613 | line-height: 1.5; 614 | display: block; 615 | background-repeat: no-repeat; 616 | transition: box-shadow 0.15s ease; 617 | width: calc(100% - 0.75rem - 0.75rem); 618 | box-shadow: 0 1px 3px rgba(50, 50, 93, 0.15), 0 1px 0 rgba(0, 0, 0, 0.02); 619 | } 620 | .form-control:focus { 621 | outline: 0px; 622 | box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08); 623 | } 624 | input.form-control.disabled, 625 | input.form-control:disabled { 626 | opacity: 0.65; 627 | } 628 | .form-control.is-valid, 629 | .form-control:valid { 630 | border-color: var(--success); 631 | } 632 | .form-control.is-invalid, 633 | .form-control:invalid { 634 | border-color: var(--warning); 635 | } 636 | .form-control.icon-left { 637 | width: calc(100% - 40px - 0.75rem); 638 | background-position: 10px center; 639 | background-size: 20px 20px; 640 | padding-left: 40px; 641 | } 642 | .form-control.icon-right { 643 | background-position: calc(100% - 10px) center; 644 | width: calc(100% - 40px - 0.75rem); 645 | background-size: 20px 20px; 646 | padding-right: 40px; 647 | } 648 | 649 | .pagination { 650 | display: flex; 651 | padding-left: 0; 652 | list-style: none; 653 | margin: 0.5rem 0px; 654 | border-radius: 0.375rem; 655 | } 656 | .pagination > li > * { 657 | width: 2rem; 658 | height: 2rem; 659 | margin-right: 6px; 660 | border-radius: 50%; 661 | font-size: 0.95rem; 662 | align-items: center; 663 | display: inline-flex; 664 | justify-content: center; 665 | color: var(--pagination-text); 666 | background-color: var(--input-bg); 667 | border: 1px solid var(--pagination-color); 668 | transition: all 0.2s cubic-bezier(0.68, -0.55, 0.265, 1.55); 669 | } 670 | .pagination > li > *:hover { 671 | background-color: var(--pagination-color); 672 | } 673 | .pagination > li.active > * { 674 | color: #fff; 675 | border-color: var(--primary); 676 | background-color: var(--primary); 677 | box-shadow: 0 7px 14px rgba(50, 50, 93, 0.1), 0 3px 6px rgba(0, 0, 0, 0.08); 678 | } 679 | .pagination > li.disabled > * { 680 | opacity: 0.65; 681 | cursor: default; 682 | background-color: var(--input-bg); 683 | } 684 | 685 | blockquote, 686 | .blockquote { 687 | font-style: italic; 688 | font-size: 1.25rem; 689 | margin: 0.5rem 0px; 690 | padding-left: 1em; 691 | border-left: 3px solid var(--muted); 692 | } 693 | blockquote > footer, 694 | .blockquote-footer { 695 | font-size: 80%; 696 | display: block; 697 | color: var(--muted); 698 | } 699 | blockquote > footer:before, 700 | .blockquote-footer:before { 701 | content: "\2014\A0"; 702 | } 703 | blockquote p { 704 | margin: 0px; 705 | padding: 0px; 706 | } 707 | 708 | ul, 709 | .list-unstyled { 710 | padding-left: 0; 711 | list-style: none; 712 | } 713 | ul > li { 714 | padding: 0.5rem 0; 715 | } 716 | 717 | .avatar { 718 | font-size: 1rem; 719 | display: inline-flex; 720 | width: 48px; 721 | height: 48px; 722 | color: #fff; 723 | border-radius: 0.375rem; 724 | background-color: #adb5bd; 725 | align-items: center; 726 | justify-content: center; 727 | } 728 | .avatar-sm { 729 | font-size: 0.875rem; 730 | width: 36px; 731 | height: 36px; 732 | } 733 | .avatar-lg { 734 | font-size: 0.875rem; 735 | width: 58px; 736 | height: 58px; 737 | } 738 | .avatar-xl { 739 | width: 78px; 740 | height: 78px; 741 | } 742 | 743 | .icon { 744 | width: 3rem; 745 | height: 3rem; 746 | font-size: 1.5rem; 747 | user-select: none; 748 | display: inline-flex; 749 | flex-direction: column; 750 | align-items: center; 751 | justify-content: center; 752 | } 753 | .icon-sm { 754 | width: 1.5rem; 755 | height: 1.5rem; 756 | font-size: 0.725rem; 757 | } 758 | .icon-xs { 759 | width: 0.825rem; 760 | height: 0.825rem; 761 | } 762 | 763 | img { 764 | vertical-align: middle; 765 | border-style: none; 766 | } 767 | .img-fluid { 768 | max-width: 100%; 769 | height: auto; 770 | } 771 | .rounded { 772 | border-radius: 0.375rem; 773 | } 774 | .rounded-circle { 775 | border-radius: 50%; 776 | } 777 | 778 | .avatar-update > .custom-file-input { 779 | display: flex; 780 | justify-content: flex-end; 781 | margin-top: -28px; 782 | } 783 | .avatar-update > .custom-file-input > label { 784 | border-radius: 50%; 785 | margin: 0px; 786 | width: 16px; 787 | height: 24px; 788 | } 789 | 790 | .custom-file-input > label { 791 | cursor: pointer; 792 | } 793 | .custom-file-input > label > input { 794 | display: none; 795 | } 796 | 797 | .loading-div { 798 | width: 30px; 799 | height: 30px; 800 | border-top: 2px solid var(--primary); 801 | border-radius: 15px; 802 | animation: loader infinite 1.25s; 803 | margin: auto; 804 | } 805 | 806 | @-webkit-keyframes loader { 807 | from { 808 | transform: rotate(0deg); 809 | } 810 | to { 811 | transform: rotate(360deg); 812 | } 813 | } 814 | @-o-keyframes loader { 815 | from { 816 | transform: rotate(0deg); 817 | } 818 | to { 819 | transform: rotate(360deg); 820 | } 821 | } 822 | @keyframes loader { 823 | from { 824 | transform: rotate(0deg); 825 | } 826 | to { 827 | transform: rotate(360deg); 828 | } 829 | } 830 | -------------------------------------------------------------------------------- /web/src/assets/css/navbar.css: -------------------------------------------------------------------------------- 1 | .nav { 2 | background-color: var(--navbar-background); 3 | border-bottom: 1px solid var(--border-color); 4 | user-select: none; 5 | opacity: 0.95; 6 | z-index: 20; 7 | } 8 | .nav.nav-horiz { 9 | border-bottom: 0px; 10 | } 11 | .nav > .content { 12 | display: flex; 13 | padding: 0px 16px; 14 | height: 52px; 15 | } 16 | 17 | .nav .nav-logo { 18 | display: flex; 19 | align-items: center; 20 | } 21 | .nav .nav-logo > img { 22 | height: 40px; 23 | width: 40px; 24 | margin: auto 6px auto 0px; 25 | } 26 | .nav .nav-logo > h1 { 27 | margin: auto 0px; 28 | } 29 | 30 | .nav .nav-links { 31 | margin-left: auto; 32 | display: flex; 33 | flex-direction: column; 34 | } 35 | 36 | .nav a { 37 | color: var(--navbar-color); 38 | transition: 0.2s color ease-in-out; 39 | } 40 | .nav a:hover { 41 | color: var(--navbar-color-hover); 42 | } 43 | -------------------------------------------------------------------------------- /web/src/components/DownloadItem.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | function DownloadItem({ torrent }) { 4 | const [stopping, setStopping] = useState(false); 5 | 6 | const stop = () => { 7 | setStopping(true); 8 | 9 | try { 10 | fetch(`/api/v1/torrent/remove?link=${torrent.magnetURI}`); 11 | } catch (e) { 12 | console.log(e); 13 | } 14 | }; 15 | 16 | return ( 17 |
18 |
19 |

{torrent.name}

20 |
{torrent.done ? "Done" : torrent.redableTimeRemaining}
21 |
22 | {torrent.progress !== 100 && ( 23 |
30 | )} 31 |
32 |
33 |
Status:
34 |
{torrent.status}
35 |
36 |
37 |
Size:
38 |
{torrent.total}
39 |
40 |
41 |
Downloaded:
42 |
{torrent.downloaded}
43 |
44 |
45 |
Speed:
46 |
{torrent.speed}
47 |
48 | {!torrent.done && ( 49 | 52 | )} 53 | {torrent.done && ( 54 | 55 | Open 56 | 57 | )} 58 |
59 |
60 | ); 61 | } 62 | 63 | export default DownloadItem; 64 | -------------------------------------------------------------------------------- /web/src/components/DriveItem.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | export default function DriveItem({ item: { id, name, modifiedTime, iconLink, mimeType } }) { 5 | const isFolder = mimeType === "application/vnd.google-apps.folder"; 6 | 7 | return ( 8 |
9 |
10 |

11 | {mimeType} 16 | {name} 17 |

18 | Modified: {modifiedTime} 19 | {isFolder ? ( 20 | 21 | Open folder 22 | 23 | ) : ( 24 | 25 | Download 26 | 27 | )} 28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /web/src/components/Input.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | function Input({ label, labelProps, wrapperClass, onChange, id, ...props }) { 5 | return ( 6 |
7 | 8 | onChange(e.target.value)} {...props} /> 9 |
10 | ); 11 | } 12 | 13 | Input.defaultProps = { 14 | type: "text", 15 | onChange: text => console.log("Value: ", text) 16 | }; 17 | 18 | Input.propTypes = { 19 | id: PropTypes.string.isRequired, 20 | name: PropTypes.string, 21 | label: PropTypes.string.isRequired, 22 | labelProps: PropTypes.object, 23 | wrapperClass: PropTypes.string, 24 | type: PropTypes.oneOf(["text", "password", "number"]), 25 | placeholder: PropTypes.string, 26 | onChange: PropTypes.func, 27 | readonly: PropTypes.bool, 28 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 29 | defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 30 | required: PropTypes.bool 31 | }; 32 | 33 | export default Input; 34 | -------------------------------------------------------------------------------- /web/src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NightModeToggle from "./NightModeToggle"; 3 | 4 | function Navbar() { 5 | return ( 6 |
7 |
8 |
9 |

Nexa Bots

10 |
11 |
12 | 13 |
14 |
15 |
16 | ); 17 | } 18 | 19 | export default Navbar; 20 | -------------------------------------------------------------------------------- /web/src/components/NightModeToggle.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | export default function NightModeToggle() { 4 | let localStored = false; 5 | if (process.browser) { 6 | localStored = localStorage.getItem("nightMode") === "true"; 7 | } 8 | 9 | const [nightMode, setNightMode] = useState(localStored); 10 | 11 | if (process.browser) { 12 | if (nightMode) { 13 | document.body.classList.add("dark"); 14 | localStorage.setItem("nightMode", true); 15 | } else { 16 | document.body.classList.remove("dark"); 17 | localStorage.setItem("nightMode", false); 18 | } 19 | } 20 | 21 | return ( 22 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /web/src/components/Picker.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | function Picker({ label, labelProps, wrapperClass, onChange, id, options, ...props }) { 5 | return ( 6 |
7 | 10 | 19 |
20 | ); 21 | } 22 | 23 | Picker.defaultProps = { 24 | onChange: text => console.log("Value: ", text) 25 | }; 26 | 27 | Picker.propTypes = { 28 | id: PropTypes.string.isRequired, 29 | name: PropTypes.string, 30 | label: PropTypes.string.isRequired, 31 | labelProps: PropTypes.object, 32 | wrapperClass: PropTypes.string, 33 | onChange: PropTypes.func, 34 | readonly: PropTypes.bool, 35 | value: PropTypes.string, 36 | defaultValue: PropTypes.string, 37 | required: PropTypes.bool, 38 | options: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.string.isRequired, name: PropTypes.string.isRequired })) 39 | .isRequired 40 | }; 41 | 42 | export default Picker; 43 | -------------------------------------------------------------------------------- /web/src/components/SearchItem.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | function SearchItem({ result, site, api }) { 4 | const [loading, setLoading] = useState(false); 5 | const [response, setResponse] = useState({}); 6 | 7 | const loadDetails = async () => { 8 | setLoading(true); 9 | 10 | const res = await fetch(api + "api/v1/details/" + site + "?query=" + result.link); 11 | if (res.status !== 200) { 12 | setResponse({ error: true, errorMessage: "Cannot connect to site" }); 13 | } else { 14 | const response = await res.json(); 15 | setResponse({ ...response }); 16 | } 17 | 18 | setLoading(false); 19 | }; 20 | 21 | const copyToClipboard = () => { 22 | const str = response.torrent.downloadLink; 23 | const el = document.createElement("textarea"); 24 | el.value = str; 25 | el.setAttribute("readonly", ""); 26 | el.style.position = "absolute"; 27 | el.style.left = "-9999px"; 28 | document.body.appendChild(el); 29 | el.select(); 30 | document.execCommand("copy"); 31 | document.body.removeChild(el); 32 | }; 33 | 34 | return ( 35 |
36 |
37 |

{result.name}

38 |
Seeds: {result.seeds}
39 |
{result.details}
40 | {!response.torrent && ( 41 | 44 | )} 45 | {response.error &&
{response.errorMessage}
} 46 | {response.torrent && ( 47 |
48 | {response.torrent.details.map(({ infoText, infoTitle }, i) => ( 49 |
50 |
{infoTitle}
51 |
{infoText}
52 |
53 | ))} 54 | 55 | Download 56 | 57 | 60 |
61 | )} 62 |
63 |
64 | ); 65 | } 66 | 67 | export default SearchItem; 68 | -------------------------------------------------------------------------------- /web/src/components/TopNav.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | export default function TopNav({ nav }) { 5 | return ( 6 |
7 |
8 |
    9 |
  • 10 | 11 | 12 | 13 | 14 | Search 15 | 16 |
  • 17 |
  • 18 | 19 | 20 | 21 | 22 | Downloads 23 | 24 |
  • 25 |
  • 26 | 27 | 28 | 29 | 30 | Drive 31 | 32 |
  • 33 |
34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /web/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import Navbar from "./components/Navbar"; 4 | import App from "./App"; 5 | import "./assets/css/index.css"; 6 | import "./assets/css/helpers.css"; 7 | import "./assets/css/navbar.css"; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | 13 | , 14 | document.getElementById("root") 15 | ); 16 | -------------------------------------------------------------------------------- /web/src/screens/Downloads.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import useSWR from "swr"; 3 | import Input from "../components/Input"; 4 | import DownloadItem from "../components/DownloadItem"; 5 | 6 | function Downloads() { 7 | const fetcher = (...args) => fetch(...args).then(res => res.json()); 8 | const { data, error } = useSWR("/api/v1/torrent/list", fetcher, { refreshInterval: 3500 }); 9 | const [link, setLink] = useState(""); 10 | const [adding, setAdding] = useState(false); 11 | const [addingError, setAddingError] = useState(""); 12 | 13 | const add = async e => { 14 | if (e) e.preventDefault(); 15 | setAdding(true); 16 | 17 | if (link.indexOf("magnet:") !== 0) { 18 | setAddingError("Link is not a magnet link"); 19 | } else { 20 | setAddingError(""); 21 | const resp = await fetch(`/api/v1/torrent/download?link=${link}`); 22 | 23 | if (resp.status === 200) { 24 | setLink(""); 25 | } else { 26 | setAddingError("An error occured"); 27 | } 28 | } 29 | 30 | setAdding(false); 31 | }; 32 | 33 | return ( 34 | <> 35 |

Downloads

36 | 37 |
38 | 47 | {addingError !== "" &&
{addingError}
} 48 | 51 |
52 | {error &&
An error occured. Check your internet.
} 53 | {data && ( 54 |
55 | {data.torrents.map(torrent => ( 56 | 57 | ))} 58 |
59 | )} 60 | 61 | ); 62 | } 63 | 64 | export default Downloads; 65 | -------------------------------------------------------------------------------- /web/src/screens/Drive.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useParams, Link } from "react-router-dom"; 3 | import DriveItem from "../components/DriveItem"; 4 | import useSWR from "swr"; 5 | 6 | export default function Drive() { 7 | const { folderId } = useParams(); 8 | const { data, error } = useSWR(`/api/v1/drive/folder?id=${folderId || ""}`, url => fetch(url).then(res => res.json())); 9 | 10 | return ( 11 | <> 12 |

13 | {folderId && ( 14 | 15 | 16 | 17 | 18 | 19 | )} 20 | Drive Index 21 |

22 | {!data && !error &&
} 23 | {!!error &&
{`${error}`}
} 24 | {data && data.map(item => )} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /web/src/screens/DriveHelp.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Input from "../components/Input"; 3 | 4 | export default function DriveHelp() { 5 | const [clientId, setClientId] = useState(""); 6 | const [clientSecret, setClientSecret] = useState(""); 7 | const [authCode, setAuthCode] = useState(""); 8 | const [showAuthCodeInput, setShowAuthCodeInput] = useState(false); 9 | const [token, setToken] = useState(""); 10 | const [loading, setLoading] = useState(false); 11 | const [error, setError] = useState(""); 12 | 13 | const copyToClipboard = () => { 14 | const str = token; 15 | const el = document.createElement("textarea"); 16 | el.value = str; 17 | el.setAttribute("readonly", ""); 18 | el.style.position = "absolute"; 19 | el.style.left = "-9999px"; 20 | document.body.appendChild(el); 21 | el.select(); 22 | document.execCommand("copy"); 23 | document.body.removeChild(el); 24 | }; 25 | 26 | const onSubmit = async e => { 27 | if (e) e.preventDefault(); 28 | setLoading(true); 29 | if (clientId && clientSecret && !authCode) { 30 | const resp = await fetch(`/api/v1/drive/getAuthURL?clientId=${clientId}&clientSecret=${clientSecret}`).then(res => res.json()); 31 | if (!resp || resp.error) { 32 | setError(resp.error || "An error occured"); 33 | } else { 34 | window.open(resp.authURL); 35 | setShowAuthCodeInput(true); 36 | } 37 | } else if (clientId && clientSecret && authCode) { 38 | const resp = await fetch( 39 | `/api/v1/drive/getAuthToken?clientId=${clientId}&clientSecret=${clientSecret}&authCode=${authCode}` 40 | ).then(res => res.json()); 41 | if (!resp || resp.error) { 42 | setError(resp.error || "An error occured"); 43 | } else { 44 | setToken(JSON.stringify(resp.token)); 45 | } 46 | } 47 | setLoading(false); 48 | }; 49 | 50 | return ( 51 |
52 |
53 |

Gdrive token generator

54 |
55 | 56 | 57 | {showAuthCodeInput && } 58 | {error &&
{error}
} 59 | 62 |
63 | {token && ( 64 | <> 65 |
{token}
66 | 69 | 70 | )} 71 |
72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /web/src/screens/Home.js: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense } from "react"; 2 | import TopNav from "../components/TopNav"; 3 | 4 | const Search = lazy(() => import("../screens/Search")); 5 | const Downloads = lazy(() => import("../screens/Downloads")); 6 | const Drive = lazy(() => import("../screens/Drive")); 7 | 8 | export default function Home({ tab }) { 9 | const nav = tab || "search"; 10 | 11 | return ( 12 | <> 13 | 14 |
15 |
16 | }> 17 | {nav === "search" && } 18 | {nav === "downloads" && } 19 | {nav === "drive" && } 20 | 21 |
22 |
23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /web/src/screens/Search.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Input from "../components/Input"; 3 | import Picker from "../components/Picker"; 4 | import SearchItem from "../components/SearchItem"; 5 | 6 | const site = process.env.SEARCH_SITE; 7 | 8 | function Search({ api }) { 9 | const [term, setTerm] = useState(""); 10 | const [site, setSite] = useState(""); 11 | const [loading, setLoading] = useState(false); 12 | const [response, setResponse] = useState({}); 13 | 14 | const search = async e => { 15 | if (e) e.preventDefault(); 16 | setLoading(true); 17 | 18 | if (term !== "") { 19 | const res = await fetch(api + "api/v1/search/" + site + "?query=" + term); 20 | if (res.status !== 200) { 21 | setResponse({ 22 | error: true, 23 | errorMessage: "Cannot connect to site" 24 | }); 25 | } else { 26 | const response = await res.json(); 27 | setResponse(response); 28 | } 29 | } 30 | 31 | setLoading(false); 32 | }; 33 | 34 | return ( 35 | <> 36 |

Search

37 |
38 | 51 | 60 | 63 | 64 |
65 | {response.error &&
{response.errorMessage}
} 66 | {response.results && 67 | response.results.length > 0 && 68 | response.results.map(result => )} 69 |
70 | 71 | ); 72 | } 73 | 74 | Search.defaultProps = { api: String(site) }; 75 | 76 | export default Search; 77 | --------------------------------------------------------------------------------