├── .dockerignore ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .gitlab-ci.yml ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── app.js ├── asset ├── AddedTorrent.png ├── Category.png ├── CategoryIndexer.png ├── CategoryIndexerOpen.png ├── CategoryLabel.png ├── CategoryOpen.png ├── CategoryPath.png ├── CategoryTag.png ├── CreateCategory.gif ├── Custom_Search.png ├── DownloadTorrentFile.png ├── Downloading.png ├── FitGirl_Research.png ├── General.png ├── Home.png ├── Magnet.png ├── Reproduce.png ├── ReproduceStreaming.mp4 ├── Share.png ├── ShareIcon.png ├── Theme.png └── logo-nobackground.png ├── bin ├── www └── www-prod ├── package.json ├── public └── electron.js ├── routes ├── category.js ├── classes │ ├── ConfigStorage.js │ ├── SearxFetcher.js │ ├── indexers.js │ ├── type.js │ └── utility.js ├── config.js ├── files.js ├── index.js ├── indexer.js ├── stream.js └── torrent.js ├── start.js ├── swagger-output.json ├── swagger.js ├── views ├── error.pug ├── index.pug └── layout.pug ├── website └── crawfish-official │ ├── .env │ ├── .env.production │ ├── .gitignore │ ├── LICENSE │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── MobxContext │ ├── AppContext.js │ └── appStore.js │ ├── asset │ ├── default-nomargin.svg │ └── logo-nobackground.png │ ├── index.css │ ├── index.js │ ├── library │ ├── WebTorrentGuiV2.js │ ├── WebTorrentHelper.js │ ├── components │ │ ├── AddTorrent.js │ │ ├── FileElement.js │ │ ├── FilesTable.js │ │ ├── LinearProgressWithLabel.js │ │ ├── Menu.js │ │ ├── SettingsPage.js │ │ ├── SettingsSections │ │ │ ├── Category.js │ │ │ └── General.js │ │ ├── SpeedMeter.js │ │ ├── SupportComponent │ │ │ └── ConfirmationButton.js │ │ ├── TorrentClientTable.js │ │ └── TorrentTableRow.js │ ├── types.js │ └── utils.js │ ├── logo.svg │ ├── reportWebVitals.js │ ├── screen │ ├── ErrorBoundary.js │ └── TorrentManager.js │ └── setupTests.js └── websocket ├── server.js └── wss-conf.js /.dockerignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store* 3 | Icon? 4 | ._* 5 | 6 | # Windows 7 | Thumbs.db 8 | ehthumbs.db 9 | Desktop.ini 10 | 11 | # Linux 12 | .directory 13 | *~ 14 | 15 | 16 | # npm 17 | node_modules 18 | package-lock.json 19 | *.log 20 | *.gz 21 | 22 | 23 | # Coveralls 24 | coverage 25 | 26 | # Benchmarking 27 | benchmarks/graphs 28 | 29 | 30 | .idea 31 | .git 32 | /Downloads 33 | /build 34 | /dist 35 | config.json 36 | 37 | node_modules 38 | npm-debug.log 39 | Dockerfile 40 | .dockerignore 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store* 3 | Icon? 4 | ._* 5 | 6 | # Windows 7 | Thumbs.db 8 | ehthumbs.db 9 | Desktop.ini 10 | 11 | # Linux 12 | .directory 13 | *~ 14 | 15 | 16 | # npm 17 | node_modules 18 | package-lock.json 19 | *.log 20 | *.gz 21 | 22 | 23 | # Coveralls 24 | coverage 25 | 26 | # Benchmarking 27 | benchmarks/graphs 28 | 29 | 30 | .idea 31 | /swagger-output.json 32 | /Downloads 33 | /torrent 34 | config.json 35 | /Build 36 | /dist 37 | /public/crawfish-official/asset-manifest.json 38 | /public/crawfish-official/favicon.ico 39 | /public/crawfish-official/index.html 40 | /public/crawfish-official/logo192.png 41 | /public/crawfish-official/logo512.png 42 | /public/crawfish-official/manifest.json 43 | /public/crawfish-official/robots.txt 44 | /public/crawfish-official/static/ 45 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | IMAGE_VERSION: "1.7.19" 3 | GIT_DEPTH: '3' 4 | SIMPLECOV: 'true' 5 | RUST_BACKTRACE: '1' 6 | RUSTFLAGS: '' 7 | CARGOFLAGS: '' 8 | 9 | stages: 10 | - build 11 | 12 | 13 | cache: 14 | key: '${CI_COMMIT_BRANCH}' 15 | paths: 16 | - node_modules/ 17 | 18 | express-alpha: 19 | image: docker:latest 20 | stage: build 21 | services: 22 | - docker:dind 23 | script: 24 | - docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" docker.io 25 | - docker build --progress=plain --tag="mauromazzocchetti/crawfish:$IMAGE_VERSION" ./ 26 | - docker push mauromazzocchetti/crawfish:$IMAGE_VERSION 27 | - docker logout 28 | except: 29 | - main 30 | 31 | express: 32 | image: docker:latest 33 | stage: build 34 | services: 35 | - docker:dind 36 | script: 37 | - docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" docker.io 38 | - docker build --progress=plain --tag="mauromazzocchetti/crawfish:$IMAGE_VERSION" --tag="mauromazzocchetti/crawfish:latest" ./ 39 | - docker push mauromazzocchetti/crawfish:$IMAGE_VERSION 40 | - docker push mauromazzocchetti/crawfish:latest 41 | - docker logout 42 | only: 43 | - main 44 | 45 | 46 | linux-build: 47 | stage: build 48 | image: node:16 49 | script: 50 | - npm i @mapbox/node-pre-gyp -g 51 | - npm install 52 | - npm run bundle-linux 53 | artifacts: 54 | expire_in: 1 week 55 | paths: 56 | - 'dist/*.AppImage' 57 | - 'dist/*.tar.xz' 58 | - 'dist/*.snap' 59 | - 'dist/*.deb' 60 | when: manual 61 | 62 | #osx-build: 63 | # stage: build 64 | # script: 65 | # - npm install 66 | # - electron-builder --linux 67 | # tags: 68 | # - darwin-shell 69 | # artifacts: 70 | # expire_in: 1 week 71 | # paths: 72 | # - 'packages/fether-electron/dist/*.dmg' 73 | # - 'packages/fether-electron/dist/*.zip' 74 | # only: 75 | # - main 76 | # 77 | win-build: 78 | stage: build 79 | image: electronuserland/builder:16-wine 80 | script: 81 | - npm i @mapbox/node-pre-gyp -g 82 | - npm install 83 | - npm run bundle-win 84 | artifacts: 85 | expire_in: 1 week 86 | paths: 87 | - 'dist/*.exe' 88 | when: manual 89 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | Drakonkat#4077, adamo.mazzocchetti@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | #APP 3 | WORKDIR /app 4 | COPY ./ ./ 5 | RUN ls website/crawfish-official 6 | RUN npm i --prefix website/crawfish-official --legacy-peer-deps 7 | RUN npm run build --prefix website/crawfish-official 8 | RUN npm install -g @mapbox/node-pre-gyp 9 | RUN npm install 10 | 11 | EXPOSE 3000 12 | CMD [ "npm", "run", "prod" ] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 TND 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Crawfish](asset/logo-nobackground.png) 2 | 3 | **Crawfish** is an innovative and free torrent client where anyone can stream/download torrents and share it with a 4 | simple link. 5 | 6 | Available features are: 7 | 8 | - Basic torrent feature (Download/Seed) 9 | - Torrent seeded by this client can be streamed or downloaded from webtorrent browser client like QPlayer. 10 | Example: [QPlayer with magnet](https://tndsite.gitlab.io/quix-player/?magnet=magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent) 11 | - Search torrent direct in the client [Based on **[Searx](https://searx.me/)** service fetch data from **nyaa**, 12 | **1337**, **rarbg**, **FitGirl** and more will be added in the future] 13 | - Possibility to share a link to download file in the browser 14 | - File can be opened even if the download is not finished yet 15 | 16 | It uses **[WebTorrent-hybrid](https://github.com/webtorrent/webtorrent-hybrid)** - the first torrent client that works 17 | in the browser. **WebTorrent** uses **[WebRTC](https://webrtc.org/)** for true peer-to-peer transport. No browser 18 | plugin, extension, or installation is required. 19 | 20 | ### How to get CrawFish 21 | 22 | Download the latest release here. [Download CrawFish](https://github.com/drakonkat/webtorrent-express-api/releases) 23 | 24 | --- 25 | 26 | ### How to use CrawFish 27 | 28 | **CrawFish** is easy to use! 29 | 30 | ![Home](asset/Home.png) 31 | 32 | After you installed the client you are ready to Search and Download/Seed lot of contents! But also there are some other 33 | features you can use in CrawFish 34 | 35 | **Streaming** 36 | 37 | ![Reproduce icon](asset/Reproduce.png) 38 | 39 | When downloading a Movie or a TV Series, by clicking this icon, you will be able to watch it immediately in streaming! 40 | It will be opened by your OS default video software (be careful that it can read all the video format). 41 | Of course there is the limitation that you can't skip minutes of video if it is not already downloaded. 42 | 43 | Here a little video that show how it works 44 | 45 | ![Streaming video](asset/ReproduceStreaming.mp4) 46 | 47 | **Get Torrent File** 48 | 49 | ![Download torrent icon](asset/DownloadTorrentFile.png) 50 | 51 | While you are downloading a torrent, you are also able to download the torrent of the current file. 52 | 53 | **Sharing is caring** 54 | 55 | ![Share icon](asset/Share.png) 56 | 57 | Yes, you can share what are you downloading with your friend, parents, enemies or whoever you want! By just clicking 58 | that icon, in your clipboard will be copied 59 | a link to send to your friend. 60 | 61 | Then they just need to click the link, and then they will start to download your same torrent via browser!* 62 | 63 | \* The speed in accessing and downloading the file is proportional to how much seed the torrent has. 64 | 65 | **Magnet Link** 66 | 67 | ![Magnet icon](asset/Magnet.png) 68 | 69 | By clicking this icon, in your clipboard will be saved the magnet link of the current torrent. 70 | 71 | ### Exploring Settings 72 | 73 | In the settings you will be able to perform different useful actions! 74 | 75 | **General Tab** 76 | 77 | ![General](asset/General.png) 78 | 79 | In this section you will be able to set the path of the generic downloaded torrents 80 | 81 | And also you will be able to set the speed of the Download and the Upload. 82 | 83 | **Category Tab** 84 | 85 | ![Category](asset/Category.png) 86 | 87 | You want to add or remove a category from the **Explore** menu in base of your need? With CrawFish is possible! You have 88 | just to follow some steps: 89 | 90 | In the Settings there is the tab **Category** where you can add a label in the **Explore** menu in the main page as a 91 | fast research section. 92 | 93 | When you are in **Category** section, clic **Add** and will appear a Generic Category, then expand it by clicking on it. 94 | 95 | Edit the fields by pressing the pencil icon or delete the created category by clicking the trashcan icon. 96 | 97 | In the fields of the **Category** you have these possibilities: 98 | 99 | - Choose a specific path where to download the torrents![Category path pic](asset/CategoryPath.png) 100 | - The name of the label that will appear in the **Explore** main menu![Category label pic](asset/CategoryLabel.png) 101 | - You can include some tag for a faster research (ex. by writing "Eng" it will search for all the torrents that include 102 | the Eng language on the name)![Category tag pic](asset/CategoryTag.png) 103 | - Have the possibility to put a default research for that 104 | category![Category indexer](asset/CategoryIndexer.png) ![Category indexer open](asset/CategoryIndexerOpen.png) 105 | 106 | When everything is set, click on the save icon that appeared on the right instead of the pencil. Then in the Explore 107 | menu you will have your custom category. 108 | 109 | Here a GIF that resume the operations ![Create category gif](asset/CreateCategory.gif) 110 | 111 | **Theme** 112 | 113 | ![Theme pic](asset/Theme.png) 114 | 115 | Also you are able to activate/disable the dark theme 116 | 117 | ### Note 118 | 119 | > There is a way to support this project? 120 | 121 | **Yes!** Simply use the software, open issue if there is improvement/bug fixing that can be done and say thanks! It is 122 | enough 123 | 124 | Issue can be sent here. [Issue board](https://github.com/drakonkat/webtorrent-express-api/issues) 125 | 126 | ## License 127 | 128 | MIT. Copyright (c) [Drakonkat](https://gitlab.com/tndsite/quix-player/-/blob/master/LICENSE). 129 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const createError = require('http-errors'); 2 | const express = require('express'); 3 | const path = require('path'); 4 | const cors = require('cors'); 5 | const cookieParser = require('cookie-parser'); 6 | const logger = require('morgan'); 7 | const timeout = require('connect-timeout') 8 | const rfs = require("rotating-file-stream"); 9 | const compression = require('compression'); 10 | const indexRouter = require('./routes/index'); 11 | const configRouter = require('./routes/config'); 12 | const torrentRouter = require('./routes/torrent'); 13 | const categoryRouter = require('./routes/category'); 14 | const indexerRouter = require('./routes/indexer'); 15 | const streamRouter = require('./routes/stream'); 16 | const fileRouter = require('./routes/files'); 17 | const ConfigStorage = require("./routes/classes/ConfigStorage"); 18 | const SearxFetcher = require("./routes/classes/SearxFetcher"); 19 | const wss = require("./websocket/server"); 20 | const app = express(); 21 | 22 | // Setup storage part 23 | var storage; 24 | if (!storage) { 25 | storage = new ConfigStorage(); 26 | } 27 | app.locals.storage = storage; 28 | var searx; 29 | if (!searx) { 30 | searx = new SearxFetcher(); 31 | } 32 | app.locals.searx = searx; 33 | 34 | // Api configuration log and similiar 35 | const pad = num => (num > 9 ? "" : "0") + num; 36 | const generator = (time, index) => { 37 | if (!time) return "request.log"; 38 | 39 | let month = time.getFullYear() + "" + pad(time.getMonth() + 1); 40 | let day = pad(time.getDate()); 41 | 42 | return `${month}/${day}-file-${index}.log`; 43 | }; 44 | const stream = rfs.createStream(generator, { 45 | size: "20M", // rotate every 10 MegaBytes written 46 | compress: "gzip", // compress rotated files 47 | maxFiles: 10 48 | }); 49 | logger.token('bearer', function (req, res) { 50 | return req.headers.authorization || "No auth"; 51 | }) 52 | logger.token('body', function (req, res) { 53 | return JSON.stringify(req.body); 54 | }) 55 | app.use(logger('[:date[clf]] :response-time ms :remote-addr ":method :url" :status ":user-agent" :bearer', { 56 | stream 57 | })); 58 | app.use(logger('[:date[clf]] :response-time ms :remote-addr ":method :url" :status ":user-agent" :bearer')); 59 | app.use(compression()) 60 | app.use(logger('dev')); 61 | app.use(cors()); 62 | app.use(timeout('40s')) 63 | 64 | 65 | app.engine('pug', require('pug').__express) 66 | // app.set('views', path.join(__dirname, 'views')); 67 | app.set('view engine', 'pug'); 68 | app.use(express.json()); 69 | app.use(express.urlencoded({extended: false})); 70 | app.use(cookieParser()); 71 | app.use(express.static(path.join(__dirname, '/public'))); 72 | 73 | 74 | app.use('/', indexRouter); 75 | app.use('/config', configRouter); 76 | app.use('/torrent', torrentRouter); 77 | app.use('//torrent', torrentRouter); 78 | app.use('/stream', streamRouter); 79 | app.use('/category', categoryRouter); 80 | app.use('/indexer', indexerRouter); 81 | app.use('/file', fileRouter); 82 | 83 | // catch 404 and forward to error handler 84 | app.use(function (req, res, next) { 85 | next(createError(404)); 86 | }); 87 | 88 | // error handler 89 | app.use(function (err, req, res, next) { 90 | // set locals, only providing error in development 91 | res.locals.message = err.message; 92 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 93 | 94 | // render the error page 95 | res.status(err.status || 500); 96 | res.json('error'); 97 | }); 98 | 99 | 100 | module.exports = app; 101 | 102 | -------------------------------------------------------------------------------- /asset/AddedTorrent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/AddedTorrent.png -------------------------------------------------------------------------------- /asset/Category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/Category.png -------------------------------------------------------------------------------- /asset/CategoryIndexer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/CategoryIndexer.png -------------------------------------------------------------------------------- /asset/CategoryIndexerOpen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/CategoryIndexerOpen.png -------------------------------------------------------------------------------- /asset/CategoryLabel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/CategoryLabel.png -------------------------------------------------------------------------------- /asset/CategoryOpen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/CategoryOpen.png -------------------------------------------------------------------------------- /asset/CategoryPath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/CategoryPath.png -------------------------------------------------------------------------------- /asset/CategoryTag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/CategoryTag.png -------------------------------------------------------------------------------- /asset/CreateCategory.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/CreateCategory.gif -------------------------------------------------------------------------------- /asset/Custom_Search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/Custom_Search.png -------------------------------------------------------------------------------- /asset/DownloadTorrentFile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/DownloadTorrentFile.png -------------------------------------------------------------------------------- /asset/Downloading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/Downloading.png -------------------------------------------------------------------------------- /asset/FitGirl_Research.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/FitGirl_Research.png -------------------------------------------------------------------------------- /asset/General.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/General.png -------------------------------------------------------------------------------- /asset/Home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/Home.png -------------------------------------------------------------------------------- /asset/Magnet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/Magnet.png -------------------------------------------------------------------------------- /asset/Reproduce.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/Reproduce.png -------------------------------------------------------------------------------- /asset/ReproduceStreaming.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/ReproduceStreaming.mp4 -------------------------------------------------------------------------------- /asset/Share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/Share.png -------------------------------------------------------------------------------- /asset/ShareIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/ShareIcon.png -------------------------------------------------------------------------------- /asset/Theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/Theme.png -------------------------------------------------------------------------------- /asset/logo-nobackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/asset/logo-nobackground.png -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var start = require('../start'); 8 | var http = require('http'); 9 | var open = require('open'); 10 | start(3000) 11 | -------------------------------------------------------------------------------- /bin/www-prod: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var start = require('../start'); 8 | var http = require('http'); 9 | var open = require('open'); 10 | process.env.NODE_ENV = "production" 11 | 12 | start(3000) 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crawfish", 3 | "version": "1.7.19", 4 | "private": false, 5 | "author": "drakonkat", 6 | "description": "Innovative torrent client", 7 | "repository": "https://github.com/drakonkat/crawfish", 8 | "keywords": [ 9 | "torrent", 10 | "docker", 11 | "express", 12 | "stream" 13 | ], 14 | "scripts": { 15 | "start": "npx nodemon --ignore website/ --ignore config.json ./bin/www", 16 | "start-electron": "electron .", 17 | "swagger-autogen": "node swagger.js", 18 | "prod": "node ./bin/www-prod", 19 | "bundle": "electron-builder", 20 | "bundle-linux": "npm run build --prefix website/crawfish-official && electron-builder -l -p always", 21 | "bundle-win": "npm run build --prefix website/crawfish-official && electron-builder -w -p always", 22 | "bundle-mac": "npm run build --prefix website/crawfish-official && electron-builder -m -p always", 23 | "bundleg": "npm run build --prefix website/crawfish-official && electron-builder" 24 | }, 25 | "build": { 26 | "generateUpdatesFilesForAllChannels": true, 27 | "appId": "com.tnd.crawfish", 28 | "productName": "CrawFish", 29 | "win": { 30 | "target": "nsis", 31 | "publish": [ 32 | "github" 33 | ], 34 | "requestedExecutionLevel": "requireAdministrator" 35 | }, 36 | "linux": { 37 | "target": "AppImage", 38 | "publish": [ 39 | "github" 40 | ] 41 | }, 42 | "nsis": { 43 | "oneClick": false, 44 | "allowToChangeInstallationDirectory": true, 45 | "installerIcon": "./public/crawfish-official/favicon.ico", 46 | "uninstallerIcon": "./public/crawfish-official/favicon.ico" 47 | }, 48 | "icon": "public/crawfish-official/logo512.png", 49 | "copyright": "Copyright 2020-2022 TND" 50 | }, 51 | "main": "public/electron.js", 52 | "dependencies": { 53 | "axios": "^0.27.2", 54 | "cheerio": "^1.0.0-rc.12", 55 | "compression": "^1.7.4", 56 | "connect-timeout": "^1.9.0", 57 | "cookie-parser": "~1.4.6", 58 | "cors": "^2.8.5", 59 | "debug": "~2.6.9", 60 | "downloads-folder": "^3.0.3", 61 | "electron-is-dev": "^2.0.0", 62 | "electron-log": "^4.4.8", 63 | "electron-unhandled": "^4.0.1", 64 | "electron-updater": "^5.2.1", 65 | "express": "~4.18.1", 66 | "fast-xml-parser": "^4.0.10", 67 | "form-data": "^4.0.0", 68 | "http-errors": "~1.8.1", 69 | "mime-types": "^2.1.35", 70 | "moment": "^2.29.4", 71 | "morgan": "~1.10.0", 72 | "node-schedule": "^2.1.0", 73 | "octokit": "^2.0.7", 74 | "open": "^8.4.0", 75 | "pouchdb": "^7.3.0", 76 | "pug": "^3.0.2", 77 | "qs": "^6.11.0", 78 | "rotating-file-stream": "^3.0.4", 79 | "swagger-ui-express": "^4.5.0", 80 | "webtorrent": "^1.8.16", 81 | "webtorrent-hybrid": "^4.1.3", 82 | "wrtc": "^0.4.7", 83 | "ws": "^8.9.0" 84 | }, 85 | "devDependencies": { 86 | "electron": "^20.2.0", 87 | "electron-builder": "^23.3.3", 88 | "nodemon": "^2.0.20", 89 | "swagger-autogen": "^2.22.0" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /public/electron.js: -------------------------------------------------------------------------------- 1 | const electron = require("electron"); 2 | const app = electron.app; 3 | const safeStorage = electron.safeStorage; 4 | const Menu = electron.Menu; 5 | const MenuItem = electron.MenuItem; 6 | const Dialog = electron.dialog; 7 | const BrowserWindow = electron.BrowserWindow; 8 | const path = require("path"); 9 | const isDev = require("electron-is-dev"); 10 | const cp = require("child_process"); 11 | const {autoUpdater} = require("electron-updater"); 12 | const {writeFileSyncRecursive} = require("../routes/classes/utility"); 13 | const fs = require('fs'); 14 | const os = require('os') 15 | const log = require('electron-log'); 16 | const package = require("./../package.json") 17 | const unhandled = require('electron-unhandled'); 18 | const {Octokit} = require("octokit"); 19 | autoUpdater.autoDownload = false; 20 | const octokit = new Octokit({ 21 | auth: process.env.GH_TOKEN 22 | }) 23 | 24 | 25 | const userDataPath = path.join(os.homedir(), "Crawfish"); 26 | log.transports.file.level = 'info'; 27 | log.transports.file.resolvePath = () => userDataPath + "/crawfish.log"; 28 | console.log = log.log; 29 | Object.assign(console, log.functions); 30 | const updateNotification = { 31 | title: "Crawfish downloaded", 32 | body: "A new version is ready to be installed, when you close the software this will be automatically updated to the new version!" 33 | } 34 | 35 | let mainWindow; 36 | const createWindow = () => { 37 | //Setup menu 38 | let menu = Menu.getApplicationMenu(); // get default menu 39 | let items = [] 40 | menu.items.forEach((item) => { 41 | console.log("Menu voice: ", item.role) 42 | if (item.role === "filemenu") { 43 | items.push(item) 44 | } 45 | }) 46 | Menu.setApplicationMenu(Menu.buildFromTemplate(items)); 47 | Menu.getApplicationMenu().append(new MenuItem({ 48 | label: 'Update option', 49 | submenu: [ 50 | { 51 | label: "Search for beta update", 52 | click: () => { 53 | autoUpdater.channel = "beta"; 54 | autoUpdater.checkForUpdates().then((r) => { 55 | console.log("Response updates: ", r && r.versionInfo.version !== package.version, r) 56 | if (r && r.versionInfo.version !== package.version) { 57 | let output = Dialog.showMessageBoxSync({ 58 | title: "Beta available", 59 | message: "Do you want to update the solution to beta version (It will have new feature, but even new bug)?", 60 | type: "question", 61 | buttons: ["Yes, update at the next start!", "No, Thanks!"] 62 | }) 63 | switch (output) { 64 | case 0: 65 | autoUpdater.channel = "beta"; 66 | autoUpdater.checkForUpdatesAndNotify(updateNotification); 67 | break; 68 | default: 69 | } 70 | } 71 | }).catch(console.error) 72 | } 73 | }, { 74 | label: "Search for stable update", 75 | click: () => { 76 | autoUpdater.channel = "latest"; 77 | autoUpdater.checkForUpdates().then((r) => { 78 | console.log("Response updates stable: ", r && r.versionInfo.version !== package.version, r) 79 | if (r && !r.versionInfo.version.includes("beta") && r.versionInfo.version !== package.version) { 80 | let output = Dialog.showMessageBoxSync({ 81 | title: "Stable available", 82 | message: "Do you want to update the solution to stable version?", 83 | type: "question", 84 | buttons: ["Yes, update at the next start!", "No, Thanks!"] 85 | }) 86 | switch (output) { 87 | case 0: 88 | autoUpdater.channel = "latest"; 89 | autoUpdater.checkForUpdatesAndNotify(updateNotification); 90 | break; 91 | default: 92 | } 93 | } 94 | }).catch(console.error) 95 | } 96 | }, 97 | ] 98 | })) 99 | unhandled({ 100 | showDialog: true, 101 | logger: console.error, 102 | reportButton: (error) => { 103 | octokit.request('POST /repos/drakonkat/crawfish/issues', { 104 | owner: 'drakonkat', 105 | repo: 'crawfish', 106 | title: 'Error launching electron app', 107 | body: `\`\`\`\n${error.stack}\n\`\`\`\n\n---\n\n`, 108 | assignees: [ 109 | 'drakonkat' 110 | ], 111 | milestone: null, 112 | labels: [ 113 | 'automatic-bug-report' 114 | ] 115 | }).catch(console.error) 116 | } 117 | }); 118 | 119 | let title = "CrawFish - " + package.version; 120 | let port = 3000; 121 | mainWindow = new BrowserWindow({ 122 | width: 1280, 123 | height: 720, 124 | title: title, 125 | webPreferences: {} 126 | }); 127 | mainWindow.on('page-title-updated', (evt) => { 128 | evt.preventDefault(); 129 | }); 130 | // mainWindow.setMenuBarVisibility(false) 131 | 132 | 133 | /** 134 | * Controllo di versione TODO Better handling 135 | */ 136 | let subprocess; 137 | // let pathConfig = "/home/mm/Crawfish/config_db/LOCK" 138 | let pathConfig = path.join(userDataPath, "config_db", "LOCK") + ""; 139 | if (!fs.existsSync(pathConfig)) { 140 | writeFileSyncRecursive(pathConfig) 141 | } 142 | if (isDev) { 143 | subprocess = cp.fork( 144 | "bin/www" 145 | ); 146 | subprocess.on('message', result => { 147 | if (result) { 148 | let {message} = result 149 | switch (message) { 150 | case "READY": 151 | mainWindow.loadURL( 152 | "http://localhost:" + port + "/crawfish-official/index.html" 153 | ); 154 | mainWindow.webContents.openDevTools(); 155 | mainWindow.on("closed", () => { 156 | try { 157 | subprocess.kill('SIGHUP'); 158 | } catch (e) { 159 | console.log("Exception closing process, probably already closed by Operating system") 160 | } 161 | return (mainWindow = null) 162 | }); 163 | break; 164 | case "PORT": 165 | if (result.data) { 166 | port = result.data; 167 | } 168 | break; 169 | default: 170 | console.error("Error in server process " + message) 171 | 172 | } 173 | } 174 | 175 | }); 176 | } else { 177 | subprocess = cp.fork( 178 | `${path.join(__dirname, "../bin/www")}` 179 | ); 180 | subprocess.on('message', result => { 181 | if (result) { 182 | let {message} = result 183 | switch (message) { 184 | case "READY": 185 | mainWindow.loadURL( 186 | "http://localhost:" + port + "/crawfish-official/index.html" 187 | ); 188 | mainWindow.on("closed", () => { 189 | try { 190 | subprocess.kill('SIGHUP'); 191 | } catch (e) { 192 | console.log("Exception closing process, probably already closed by Operating system") 193 | } 194 | return (mainWindow = null) 195 | }); 196 | autoUpdater.channel = package.version.includes("beta") ? "beta" : "latest"; 197 | autoUpdater.checkForUpdates().then((r) => { 198 | console.log("Response updates: " + package.version.includes("beta") ? "beta" : "latest", r && r.versionInfo.version !== package.version, r) 199 | if (r && r.versionInfo.version !== package.version) { 200 | let output = Dialog.showMessageBoxSync({ 201 | title: "New update available", 202 | message: "Is ok to update :) Click yes to proceed", 203 | type: "question", 204 | buttons: ["Yes, update at the next start!", "No, update can break everything!"] 205 | }) 206 | switch (output) { 207 | case 0: 208 | autoUpdater.checkForUpdatesAndNotify(updateNotification).then(r => console.log("Update check: ", r)); 209 | break; 210 | default: 211 | } 212 | } 213 | 214 | }).catch(console.error); 215 | break; 216 | case "PORT": 217 | if (result.data) { 218 | port = result.data; 219 | } 220 | break; 221 | default: 222 | console.error("Error in server process " + message) 223 | 224 | } 225 | } 226 | }); 227 | } 228 | } 229 | 230 | app.on("ready", createWindow) 231 | app.on("window-all-closed", () => { 232 | process.platform !== "darwin" && app.quit() 233 | }) 234 | app.on("activate", () => { 235 | mainWindow === null && createWindow() 236 | }) 237 | 238 | 239 | -------------------------------------------------------------------------------- /routes/category.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const {GAMES, MOVIES, TVSHOW, GENERIC, FITGIRL} = require("./classes/type"); 3 | const path = require("path"); 4 | 5 | const router = express.Router(); 6 | 7 | 8 | const KEY_VARIABLE_CATEGORY = "KEY_VARIABLE_CATEGORY" 9 | const DEFAULT_CATEGORY = (downloadPath) => [ 10 | { 11 | id: 1, 12 | type: GAMES, 13 | label: "Games", 14 | path: path.join(downloadPath, "games"), 15 | tooltip: "A curated list of repacked games", 16 | tag: "", 17 | defaultSearch: "repack", 18 | }, 19 | { 20 | id: 2, 21 | type: MOVIES, 22 | label: "Movie", 23 | path: path.join(downloadPath, "movies"), 24 | tooltip: "Explore a list of movies", 25 | tag: undefined, 26 | defaultSearch: "2022", 27 | }, 28 | { 29 | id: 3, 30 | type: MOVIES, 31 | label: "Movie 1080p", 32 | path: path.join(downloadPath, "movies_HD"), 33 | tooltip: "Explore a list of movies (filtered in 1080p)", 34 | tag: "1080", 35 | defaultSearch: "2022", 36 | }, 37 | { 38 | id: 4, 39 | type: TVSHOW, 40 | label: "Tv", 41 | path: path.join(downloadPath, "tvshow"), 42 | tag: undefined, 43 | defaultSearch: "2022", 44 | }, 45 | { 46 | id: 5, 47 | type: GENERIC, 48 | label: "General", 49 | path: path.join(downloadPath, "miscellaneous"), 50 | tag: undefined, 51 | defaultSearch: "2022", 52 | }, 53 | { 54 | id: 6, 55 | type: FITGIRL, 56 | label: "FitGirl", 57 | path: path.join(downloadPath, "games"), 58 | } 59 | 60 | ] 61 | 62 | const getCategory = (req) => { 63 | let output = req.app.locals.storage.getVariable(KEY_VARIABLE_CATEGORY) 64 | if (output) { 65 | return output 66 | } else { 67 | return DEFAULT_CATEGORY(req.app.locals.storage.configuration.downloadPath); 68 | } 69 | } 70 | 71 | 72 | router.get('/', async (req, res, next) => { 73 | /* 74 | #swagger.tags = ['Category'] 75 | #swagger.summary = "Get all the category that identify the default path for that category" 76 | #swagger.responses[200] = { 77 | description: "List of the available category", 78 | schema: [{ 79 | $id: 1 80 | $type: MOVIES 81 | $label: "Movies" 82 | $defaultSearch: "2022" 83 | $path: "./" 84 | $tag: "en,1080p" 85 | $Tooltip: "Here you can find interesting movies" 86 | }] 87 | } 88 | } 89 | */ 90 | let output = getCategory(req) 91 | res.status(200).json(output) 92 | }); 93 | router.post('/', async (req, res, next) => { 94 | /* 95 | #swagger.tags = ['Category'] 96 | #swagger.summary = "Create a category" 97 | #swagger.parameters['category'] = { 98 | in: 'body', 99 | description: 'Create a category', 100 | schema: { 101 | $id: 1 102 | $type: MOVIES 103 | $label: "Movies" 104 | $defaultSearch: "2022" 105 | $path: "./" 106 | $tag: "en,1080p" 107 | $Tooltip: "Here you can find interesting movies" 108 | } 109 | } 110 | #swagger.responses[200] = { 111 | description: "If the operation gone fine, it will return the id", 112 | schema: true 113 | } 114 | */ 115 | 116 | let input = { 117 | id: 0, 118 | type: req.body.type, 119 | label: req.body.label, 120 | defaultSearch: req.body.type.defaultSearch, 121 | path: req.body.path || req.app.locals.storage.configuration.downloadPath, 122 | tag: req.body.tag, 123 | tooltip: req.body.tooltip 124 | } 125 | let categories = getCategory(req); 126 | if (categories.length > 0) { 127 | let isPresent = true 128 | while (isPresent) { 129 | input.id++ 130 | isPresent = categories.map(x => x.id).includes(input.id); 131 | } 132 | } 133 | categories.push(input); 134 | await req.app.locals.storage.setVariable(KEY_VARIABLE_CATEGORY, categories); 135 | res.status(200).json(input.id) 136 | }); 137 | router.patch('/', async (req, res, next) => { 138 | /* 139 | #swagger.tags = ['Category'] 140 | #swagger.summary = "Restore the default category" 141 | #swagger.responses[200] = { 142 | description: "If the operation gone fine, it will return the id", 143 | schema: true 144 | } 145 | */ 146 | await req.app.locals.storage.setVariable(KEY_VARIABLE_CATEGORY, DEFAULT_CATEGORY(req.app.locals.storage.configuration.downloadPath)); 147 | res.status(200).json(true) 148 | }); 149 | 150 | router.put('/:categoryId', async (req, res, next) => { 151 | /* 152 | #swagger.tags = ['Category'] 153 | #swagger.summary = "Edit a category, based on the id of the path" 154 | #swagger.parameters['category'] = { 155 | in: 'body', 156 | description: 'Edit a category', 157 | schema: { 158 | $id: 1 159 | $type: MOVIES 160 | $label: "Movies" 161 | $defaultSearch: "2022" 162 | $path: "./" 163 | $tag: "en,1080p" 164 | $Tooltip: "Here you can find interesting movies" 165 | } 166 | } 167 | #swagger.responses[200] = { 168 | description: "If the operation gone fine, true", 169 | schema: true 170 | } 171 | */ 172 | let categoryId = req.params.categoryId 173 | 174 | let input = { 175 | id: parseInt(categoryId), 176 | type: req.body.type, 177 | label: req.body.label, 178 | defaultSearch: req.body.defaultSearch, 179 | path: req.body.path, 180 | tag: req.body.tag, 181 | tooltip: req.body.tooltip 182 | } 183 | let categories = getCategory(req); 184 | if (categories.length > 0) { 185 | let index = categories.findIndex(x => x.id == categoryId) 186 | console.log("SAVED DATA:", categoryId, index) 187 | if (index !== -1) { 188 | categories[index] = input; 189 | await req.app.locals.storage.setVariable(KEY_VARIABLE_CATEGORY, categories); 190 | let output = req.app.locals.storage.getVariable(KEY_VARIABLE_CATEGORY) 191 | console.log("RETRIEVED DATA:", output) 192 | res.status(200).json(true) 193 | } else { 194 | res.status(200).json(false) 195 | } 196 | } else { 197 | res.status(200).json(false) 198 | } 199 | 200 | 201 | }); 202 | 203 | router.delete('/:categoryId', async (req, res, next) => { 204 | /* 205 | #swagger.tags = ['Category'] 206 | #swagger.summary = "Remove a category, based on the id in the path param" 207 | #swagger.responses[200] = { 208 | description: "If the operation gone fine return true, otherwise false", 209 | schema: true 210 | } 211 | */ 212 | let categoryId = req.params.categoryId 213 | let categories = getCategory(req); 214 | if (categories.length > 0) { 215 | let index = categories.findIndex(x => x.id == categoryId) 216 | if (index !== -1) { 217 | categories.splice(index, 1); 218 | await req.app.locals.storage.setVariable(KEY_VARIABLE_CATEGORY, categories); 219 | res.status(200).json(true) 220 | 221 | } else { 222 | res.status(200).json(false) 223 | 224 | } 225 | } else { 226 | res.status(200).json(false) 227 | 228 | } 229 | }); 230 | 231 | module.exports = router; 232 | -------------------------------------------------------------------------------- /routes/classes/SearxFetcher.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | 3 | 4 | class SearxFetcher { 5 | configuration = { 6 | instances: [], 7 | ready: false 8 | } 9 | 10 | 11 | constructor() { 12 | axios.get("https://searx.space/data/instances.json").then(res => { 13 | console.log("Founded searx resource", true) 14 | let array = []; 15 | for (let instancesKey in res.data.instances) { 16 | let instance = res.data.instances[instancesKey]; 17 | if (instance.http.grade === "A+" && instance.tls.grade === "A+" && instance.html.grade === "V" && instance.timing && !["https://search.ononoki.org/", "https://searx.tiekoetter.com/"].includes(instancesKey)) { 18 | array.push({ 19 | ...instance, 20 | url: instancesKey 21 | }) 22 | } 23 | } 24 | this.configuration.instances = array.sort((a, b) => { 25 | if (a && a.timing && a.timing.search && a.timing.search.all && b && b.timing && b.timing.search && b.timing.search.all) { 26 | if (a.timing.search.all.median < b.timing.search.all.median) { 27 | return -1; 28 | } 29 | if (a.timing.search.all.median > b.timing.search.all.median) { 30 | return 1; 31 | } 32 | return 0; 33 | } 34 | }); 35 | this.reconfigureFetcher().catch(error => { 36 | console.error("Error configuring fetcher: ", error && error.message) 37 | }); 38 | }) 39 | } 40 | 41 | 42 | reconfigureFetcher = async () => { 43 | for (let i in this.configuration.instances) { 44 | let instance = this.configuration.instances[i] 45 | let instancesKey = instance.url; 46 | try { 47 | await axios.get(instancesKey + "?q=2022&category_files=on&format=json"); 48 | this.configuration.usedInstance = instance 49 | this.configuration.usedInstance.host = instancesKey 50 | this.configuration.ready = true; 51 | console.log("Founded source: ", instancesKey) 52 | break; 53 | } catch (e) { 54 | console.error("Error checking resource " + instancesKey + ": ", false) 55 | } 56 | 57 | } 58 | } 59 | 60 | search = async (q = "2022") => { 61 | let promise = new Promise((resolve, reject) => { 62 | let wait = () => { 63 | if (this.configuration.ready) { 64 | return resolve(); 65 | } else { 66 | console.log("Still waiting: ") 67 | setTimeout(wait, 2000); 68 | } 69 | } 70 | wait(); 71 | }); 72 | await promise; 73 | let res = await axios.get(this.configuration.usedInstance.host + "?q=" + q + "&category_files=on&format=json&engines=nyaa,yggtorrent,torrentz,solidtorrents"); 74 | return res.data.results || [] 75 | } 76 | 77 | } 78 | 79 | module.exports = SearxFetcher; 80 | -------------------------------------------------------------------------------- /routes/classes/indexers.js: -------------------------------------------------------------------------------- 1 | const cheerio = require("cheerio"); 2 | const axios = require("axios"); 3 | const {parseTorznabResult} = require("./utility"); 4 | 5 | 6 | const API_KEY = "uyxwnibswpogk8vmjyle9diqb6m7o82u"; 7 | const categories = { 8 | _1337x: { 9 | name: "1337x", 10 | tvShow: "5000,5030,5040,5070,5080,100005,100006,100007,100009,100041,100071,100074,100075", 11 | movies: "2000,2010,2030,2040,2045,2060,2070,100001,100002,100003,100004,100042,100054,100055,100066,100070,100073,100076", 12 | games: "4050,100010,100011,100012,100013,100014,100015,100016,100017,100043,100044,100045,100046,100067,100072,100077,100082", 13 | music: "100022,100023,100024,100025,100026,100027,100053,100058,100059,100060,100068,100069", 14 | xxx: "6000,6010,6060,100048,100049,100050,100051,100067", 15 | book: "3030,7000,7020,7030,100036,100039,100052" 16 | }, 17 | nyaasi: { 18 | name: "nyaasi", 19 | anime: "5000,5070,125996,134634,140679" 20 | }, 21 | rarbg: { 22 | name: "rarbg" 23 | } 24 | } 25 | 26 | 27 | const crawlFitGirl = async (q) => { 28 | let games = []; 29 | let link = "https://fitgirl-repacks.site" 30 | if (q) { 31 | link = "https://fitgirl-repacks.site/?s=" + q 32 | } 33 | let res = await axios.get(link) 34 | let $ = cheerio.load(res.data) 35 | if (q) { 36 | let promises = []; 37 | $('.category-lossless-repack').each(async (x, elem) => { 38 | let linkDetailArticle; 39 | $(elem).find('a').each((y, elem2) => { 40 | if ($(elem2).text().includes("Continue reading")) { 41 | linkDetailArticle = $(elem2).attr("href"); 42 | } 43 | }) 44 | if (linkDetailArticle) { 45 | promises.push((async () => { 46 | console.log(x + " finished. Link: " + linkDetailArticle) 47 | let res = await axios.get(linkDetailArticle) 48 | let $ = cheerio.load(res.data) 49 | let gameName = $('.entry-title').text() 50 | 51 | let description = $('.su-spoiler-content').text() 52 | let magnets = []; 53 | $('a').each((y, elem2) => { 54 | if ($(elem2).text().includes("magnet")) { 55 | magnets.push($(elem2).attr("href")); 56 | } 57 | }) 58 | let originalSize = null; 59 | let repackSize = null; 60 | $('p').each((y, elem2) => { 61 | let texts = $(elem2).text().split("\n"); 62 | let os = parameterToFind(texts, "Original Size: ") 63 | let rs = parameterToFind(texts, "Repack Size: ") 64 | if (os || rs) { 65 | originalSize = os; 66 | repackSize = rs; 67 | } 68 | }) 69 | 70 | if (magnets.length > 0) { 71 | games.splice(x, 0, { 72 | name: gameName, 73 | description, 74 | originalSize, 75 | repackSize, 76 | magnets 77 | }) 78 | } 79 | })()) 80 | } 81 | }); 82 | await Promise.all(promises) 83 | return games; 84 | } else { 85 | $('.category-lossless-repack').each((x, elem) => { 86 | let magnets = []; 87 | $(elem).find('a').each((y, elem2) => { 88 | if ($(elem2).text().includes("magnet")) { 89 | magnets.push($(elem2).attr("href")); 90 | } 91 | }) 92 | let originalSize = null; 93 | let repackSize = null; 94 | $(elem).find('p').each((y, elem2) => { 95 | let texts = $(elem2).text().split("\n"); 96 | let os = parameterToFind(texts, "Original Size: ") 97 | let rs = parameterToFind(texts, "Repack Size: ") 98 | if (os || rs) { 99 | originalSize = os; 100 | repackSize = rs; 101 | } 102 | }) 103 | 104 | let gameName = $(elem).find('h1,.entry-title').text() 105 | let description = $(elem).find('.su-spoiler-content').text() 106 | if (magnets.length > -1) { 107 | games.push({ 108 | name: gameName, 109 | description, 110 | originalSize, 111 | repackSize, 112 | magnets 113 | }) 114 | } 115 | }); 116 | return games; 117 | } 118 | } 119 | 120 | const crawlMovies1337x = async (q) => { 121 | let cat = categories._1337x.movies 122 | let result = await axios.get("https://jackett-racknerd.tnl.one/api/v2.0/indexers/1337x/results/torznab/?apikey=" + API_KEY + "&t=movie&q=" + q + "&attrs=poster,magneturl,language,infohash,leechers&cat=" + cat) 123 | return parseTorznabResult(result.data); 124 | } 125 | 126 | 127 | const crawlTvShow1337x = async (q) => { 128 | let cat = categories._1337x.tvShow 129 | let result = await axios.get("https://jackett-racknerd.tnl.one/api/v2.0/indexers/1337x/results/torznab/?apikey=" + API_KEY + "&t=tvsearch&q=" + q + "&attrs=poster,magneturl,language,infohash,leechers&cat=" + cat) 130 | return parseTorznabResult(result.data); 131 | } 132 | 133 | const crawlGames1337x = async (q) => { 134 | let cat = categories._1337x.games 135 | let result = await axios.get("https://jackett-racknerd.tnl.one/api/v2.0/indexers/1337x/results/torznab/?apikey=" + API_KEY + "&t=tvsearch&q=" + q + "&attrs=poster,magneturl,language,infohash,leechers&cat=" + cat) 136 | return parseTorznabResult(result.data); 137 | } 138 | 139 | const crawlMusic1337x = async (q) => { 140 | let cat = categories._1337x.music 141 | let result = await axios.get("https://jackett-racknerd.tnl.one/api/v2.0/indexers/1337x/results/torznab/?apikey=" + API_KEY + "&t=tvsearch&q=" + q + "&attrs=poster,magneturl,language,infohash,leechers&cat=" + cat) 142 | return parseTorznabResult(result.data); 143 | } 144 | 145 | const jackettCrawl = async (name, cat, q) => { 146 | let result 147 | if (cat) { 148 | result = await axios.get("https://jackett-racknerd.tnl.one/api/v2.0/indexers/" + name + "/results/torznab/?apikey=" + API_KEY + "&q=" + q + "&attrs=poster,magneturl,language,infohash,leechers&cat=" + cat) 149 | } else { 150 | result = await axios.get("https://jackett-racknerd.tnl.one/api/v2.0/indexers/" + name + "/results/torznab/?apikey=" + API_KEY + "&q=" + q + "&attrs=poster,magneturl,language,infohash,leechers"); 151 | } 152 | return parseTorznabResult(result.data); 153 | } 154 | 155 | const parameterToFind = (texts, q) => { 156 | for (let text of texts) { 157 | if (text.includes(q)) { 158 | return text.substring(text.indexOf(q) + q.length, text.length); 159 | } 160 | } 161 | } 162 | 163 | module.exports = { 164 | crawlFitGirl, 165 | crawlMovies1337x, 166 | crawlTvShow1337x, 167 | crawlGames1337x, 168 | crawlMusic1337x, 169 | categories, 170 | jackettCrawl 171 | } 172 | -------------------------------------------------------------------------------- /routes/classes/type.js: -------------------------------------------------------------------------------- 1 | const GENERIC = ("GENERIC") 2 | const _1337x = ("_1337x") 3 | const NYAASI = ("nyaasi") 4 | const SEARX = ("SEARX") 5 | const GAMES = ("GAMES") 6 | const MUSIC = ("MUSIC") 7 | const MOVIES = ("MOVIES") 8 | const TVSHOW = ("TVSHOW") 9 | const FITGIRL = ("FITGIRL") 10 | const XXX = ("XXX") 11 | const BOOK = ("BOOK") 12 | const ANIME = ("ANIME") 13 | const RARBG = ("RARBG") 14 | 15 | 16 | module.exports = {GAMES, MOVIES, MUSIC, TVSHOW, FITGIRL, GENERIC, _1337x, SEARX, BOOK, XXX, ANIME, NYAASI, RARBG} 17 | -------------------------------------------------------------------------------- /routes/classes/utility.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const {XMLParser, XMLBuilder, XMLValidator} = require('fast-xml-parser'); 3 | const mime = require('mime-types') 4 | 5 | const mapTorrent = (x) => { 6 | let progress; 7 | let lengthFile = x.files.reduce((total, file) => { 8 | return total + file.length; 9 | }, 0); 10 | if (x.files && x.files.length > 0) { 11 | progress = x.files.reduce((total, file) => { 12 | if (file.paused) { 13 | return total + file.length; 14 | } else { 15 | return total + (file.progress * file.length); 16 | } 17 | }, 0) / lengthFile 18 | } else { 19 | progress = x.progress 20 | } 21 | return { 22 | size: x.files && x.files.reduce((total, file) => { 23 | return total + file.length; 24 | }, 0), 25 | name: x.name, 26 | infoHash: x.infoHash, 27 | magnet: x.magnetURI || x.magnet, 28 | downloaded: x.downloaded, 29 | uploaded: x.uploaded, 30 | downloadSpeed: x.downloadSpeed, 31 | uploadSpeed: x.uploadSpeed, 32 | progress: progress, 33 | ratio: x.ratio, 34 | path: x.path, 35 | done: x.done, 36 | length: x.length, 37 | paused: x.paused, 38 | timeRemaining: x.timeRemaining, 39 | received: x.received, 40 | files: x.files && x.files.map(y => { 41 | return { 42 | name: y.name, 43 | length: y.length, 44 | path: y.path, 45 | paused: y.paused || false, 46 | progress: y.progress, 47 | streamable: supportedFormats.includes(getExtension(y.name)), 48 | done: y.progress >= 1, 49 | mime: mime.lookup(y.name) 50 | } 51 | }) 52 | } 53 | } 54 | const TORRENTS_KEY = "torrent"; 55 | const getExtension = (fileName) => { 56 | return fileName.substring(fileName.lastIndexOf('.') + 1); 57 | }; 58 | const supportedFormats = ["mp4", "webm", "m4v", "jpg", "gif", "png", "m4a", "mp3", "wav"] 59 | const simpleHash = (id, filename) => { 60 | return id + " - " + filename; 61 | }; 62 | 63 | 64 | function writeFileSyncRecursive(filename, content = "", charset) { 65 | // -- normalize path separator to '/' instead of path.sep, 66 | // -- as / works in node for Windows as well, and mixed \\ and / can appear in the path 67 | let filepath = filename.replace(/\\/g, '/'); 68 | 69 | // -- preparation to allow absolute paths as well 70 | let root = ''; 71 | if (filepath[0] === '/') { 72 | root = '/'; 73 | filepath = filepath.slice(1); 74 | } else if (filepath[1] === ':') { 75 | root = filepath.slice(0, 3); // c:\ 76 | filepath = filepath.slice(3); 77 | } 78 | 79 | // -- create folders all the way down 80 | const folders = filepath.split('/').slice(0, -1); // remove last item, file 81 | folders.reduce( 82 | (acc, folder) => { 83 | const folderPath = acc + folder + '/'; 84 | if (!fs.existsSync(folderPath)) { 85 | fs.mkdirSync(folderPath); 86 | } 87 | return folderPath 88 | }, 89 | root // first 'acc', important 90 | ); 91 | 92 | // -- write file 93 | fs.writeFileSync(root + filepath, content, charset); 94 | return; 95 | } 96 | 97 | const parseTorznabResult = (data) => { 98 | const xmlParser = new XMLParser({ 99 | ignoreAttributes: false, 100 | attributeNamePrefix: "", 101 | // attributesGroupName: "group_", 102 | parseAttributeValue: true, 103 | removeNSPrefix: true 104 | }); 105 | let result = xmlParser.parse(data); 106 | let channel = result.rss && result.rss.channel ? result.rss.channel : result.feed; 107 | if (Array.isArray(channel)) { 108 | channel = channel[0]; 109 | } 110 | 111 | 112 | let items = channel.item; 113 | if (items && !Array.isArray(items)) { 114 | items = [items]; 115 | } else if (!items) { 116 | items = [] 117 | } 118 | 119 | 120 | for (let i = 0; i < items.length; i++) { 121 | let val = items[i]; 122 | for (let elem of val.attr) { 123 | if (val[elem.name] && !Array.isArray(val[elem.name])) { 124 | val[elem.name] = [val[elem.name], elem.value] 125 | } else if (val[elem.name] && Array.isArray(val[elem.name])) { 126 | val[elem.name].push(elem.value); 127 | } else { 128 | val[elem.name] = elem.value 129 | } 130 | } 131 | items[i] = val; 132 | } 133 | delete items.attr; 134 | return items; 135 | }; 136 | 137 | 138 | const stringToDate = (string) => { 139 | if (string instanceof Date) { 140 | return string; 141 | } else if (string) { 142 | let d = new Date(); 143 | let [hours, minutes] = string.split(':'); 144 | console.log("Check converting: ", string, hours, minutes) 145 | d.setHours(hours); 146 | d.setMinutes(minutes); 147 | console.log("Check converting2: ", d.toLocaleTimeString()) 148 | return d; 149 | } else { 150 | return null; 151 | } 152 | } 153 | 154 | async function deselectFileFromTorrent(temp, db, fileName = "") { 155 | let t = mapTorrent(temp); 156 | let foundedTorrent; 157 | try { 158 | foundedTorrent = await db.get(TORRENTS_KEY + t.infoHash); 159 | } catch (e) { 160 | console.warn("TORRENT NOT EXISTING BEFORE") 161 | } 162 | if (foundedTorrent) { 163 | foundedTorrent = { 164 | ...t, 165 | files: t.files.map(f => { 166 | if (f.name === fileName || foundedTorrent.files.find(x => f.name === x.name).paused) { 167 | f.paused = true; 168 | } 169 | return f 170 | }), 171 | _rev: foundedTorrent._rev, 172 | _id: TORRENTS_KEY + t.infoHash 173 | }; 174 | db.put(foundedTorrent) 175 | } else { 176 | await db.put({ 177 | ...t, 178 | files: t.files.map(f => { 179 | if (f.name === fileName) { 180 | f.paused = true; 181 | } 182 | return f 183 | }), 184 | _id: TORRENTS_KEY + t.infoHash 185 | }) 186 | } 187 | 188 | temp.deselect(0, temp.pieces.length - 1, false) 189 | for (let i = 0; i < temp.files.length; i++) { 190 | let f = temp.files[i] 191 | let fStored = foundedTorrent.files[i] 192 | if (!fStored.paused) { 193 | f.select() 194 | } else { 195 | f.deselect() 196 | } 197 | } 198 | } 199 | 200 | 201 | async function selectFileFromTorrent(temp, db, fileName = "") { 202 | let t = mapTorrent(temp); 203 | let foundedTorrent; 204 | try { 205 | foundedTorrent = await db.get(TORRENTS_KEY + t.infoHash); 206 | } catch (e) { 207 | console.warn("TORRENT NOT EXISTING BEFORE") 208 | } 209 | if (foundedTorrent) { 210 | foundedTorrent = { 211 | ...t, 212 | files: t.files.map(f => { 213 | if (foundedTorrent.files.find(x => f.name === x.name).paused) { 214 | f.paused = true; 215 | } 216 | if (f.name === fileName) { 217 | f.paused = false; 218 | } 219 | return f 220 | }), 221 | _rev: foundedTorrent._rev, 222 | _id: TORRENTS_KEY + t.infoHash 223 | }; 224 | db.put(foundedTorrent) 225 | } else { 226 | await db.put({ 227 | ...t, 228 | files: t.files.map(f => { 229 | if (f.name === fileName) { 230 | f.paused = false; 231 | } 232 | return f 233 | }), 234 | _id: TORRENTS_KEY + t.infoHash 235 | }) 236 | } 237 | 238 | temp.deselect(0, temp.pieces.length - 1, false) 239 | for (let i = 0; i < temp.files.length; i++) { 240 | let f = temp.files[i] 241 | let fStored = foundedTorrent.files[i] 242 | if (!fStored.paused) { 243 | f.select() 244 | } else { 245 | f.deselect() 246 | } 247 | } 248 | } 249 | 250 | module.exports = { 251 | mapTorrent, 252 | TORRENTS_KEY, 253 | getExtension, 254 | supportedFormats, 255 | simpleHash, 256 | writeFileSyncRecursive, 257 | parseTorznabResult, 258 | stringToDate, 259 | deselectFileFromTorrent, 260 | selectFileFromTorrent 261 | } 262 | -------------------------------------------------------------------------------- /routes/config.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const {stringToDate} = require("./classes/utility"); 3 | const moment = require("moment"); 4 | const router = express.Router(); 5 | 6 | 7 | router.post('/edit', async (req, res, next) => { 8 | /* 9 | #swagger.tags = ['Config'] 10 | #swagger.summary = "Modify the configuration about the torrent client" 11 | #swagger.parameters['config'] = { 12 | in: 'body', 13 | description: 'Configuration of the client', 14 | schema: { 15 | path: './' 16 | downloadLimit: '8000' 17 | uploadLimit: '8000' 18 | } 19 | } 20 | #swagger.responses[200] = { 21 | description: "Output of the operation", 22 | schema: true 23 | } 24 | */ 25 | let { 26 | downloadPath, 27 | download, 28 | upload, 29 | alternativeTimeStart, 30 | alternativeTimeEnd, 31 | alternativeDownload, 32 | alternativeUpload, 33 | } = req.body; 34 | 35 | await req.app.locals.storage.setDownload(downloadPath); 36 | await req.app.locals.storage.setSpeedConf({ 37 | alternativeTimeStart: stringToDate(alternativeTimeStart), 38 | alternativeTimeEnd: stringToDate(alternativeTimeEnd), 39 | alternativeDownload, 40 | alternativeUpload, 41 | download, 42 | upload 43 | }); 44 | { 45 | let { 46 | alternativeTimeStart, 47 | alternativeTimeEnd 48 | } = req.app.locals.storage.configuration.speed; 49 | res.status(200).json({ 50 | actualDownload: req.app.locals.storage.liveData.client.downloadSpeed, 51 | actualUpload: req.app.locals.storage.liveData.client.uploadSpeed, 52 | actualRatio: req.app.locals.storage.liveData.client.ratio, 53 | downloadSpeed: req.app.locals.storage.configuration.opts.downloadLimit, 54 | downloadPath: req.app.locals.storage.configuration.downloadPath, 55 | uploadSpeed: req.app.locals.storage.configuration.opts.uploadLimit, 56 | ...req.app.locals.storage.configuration.speed, 57 | alternativeTimeStart: alternativeTimeStart ? moment(alternativeTimeStart).format("HH:mm") : null, 58 | alternativeTimeEnd: alternativeTimeEnd ? moment(alternativeTimeEnd).format("HH:mm") : null 59 | }) 60 | } 61 | }); 62 | router.get('/', async (req, res, next) => { 63 | /* 64 | #swagger.tags = ['Config'] 65 | #swagger.summary = "Return the configuration of the torrent" 66 | #swagger.responses[200] = { 67 | description: "The configuration", 68 | schema: true 69 | } 70 | */ 71 | let { 72 | 73 | alternativeTimeStart, 74 | alternativeTimeEnd 75 | } = req.app.locals.storage.configuration.speed; 76 | res.status(200).json({ 77 | actualDownload: req.app.locals.storage.liveData.client.downloadSpeed, 78 | actualUpload: req.app.locals.storage.liveData.client.uploadSpeed, 79 | actualRatio: req.app.locals.storage.liveData.client.ratio, 80 | downloadSpeed: req.app.locals.storage.configuration.opts.downloadLimit, 81 | downloadPath: req.app.locals.storage.configuration.downloadPath, 82 | uploadSpeed: req.app.locals.storage.configuration.opts.uploadLimit, 83 | ...req.app.locals.storage.configuration.speed, 84 | alternativeTimeStart: alternativeTimeStart ? moment(alternativeTimeStart).format("HH:mm") : null, 85 | alternativeTimeEnd: alternativeTimeEnd ? moment(alternativeTimeEnd).format("HH:mm") : null 86 | }) 87 | }); 88 | 89 | module.exports = router; 90 | -------------------------------------------------------------------------------- /routes/files.js: -------------------------------------------------------------------------------- 1 | const open = require('open'); 2 | const express = require('express'); 3 | const {getExtension, mapTorrent, simpleHash, supportedFormats} = require("./classes/utility"); 4 | const {crawlFitGirl, crawlMovies1337x, crawlTvShow1337x} = require("./classes/indexers"); 5 | const path = require("path"); 6 | 7 | 8 | const router = express.Router(); 9 | 10 | router.get('/list', async (req, res, next) => { 11 | /* 12 | #swagger.tags = ['Files'] 13 | #swagger.summary = "Return the list of the file contained in the torrent" 14 | #swagger.responses[200] = { 15 | description: "Configuration data", 16 | schema: [{ 17 | done: true, 18 | 19 | streamable: true, 20 | name: true, 21 | id:"asdkjasndlas - Nome" 22 | }] 23 | } 24 | */ 25 | try { 26 | let torrents = req.app.locals.storage.liveData.client.torrents.map(mapTorrent); 27 | let oldTorrent = await req.app.locals.storage.getAllTorrent(); 28 | torrents.push(...oldTorrent.filter(x => !torrents.map(y => y.infoHash).includes(x.infoHash))) 29 | let files = []; 30 | torrents.forEach((t) => { 31 | if (t && t.files) { 32 | t.files.forEach((f) => { 33 | files.push({ 34 | done: f.progress >= 1, 35 | streamable: supportedFormats.includes(getExtension(f.name)), 36 | name: f.name, 37 | name: f.name, 38 | id: simpleHash(t.infoHash, f.name), 39 | torrentMagnet: t.magnet 40 | }) 41 | }) 42 | } 43 | }) 44 | res.status(200).json(files) 45 | } catch (e) { 46 | console.error(e) 47 | } 48 | }); 49 | 50 | router.get('/open', async (req, res, next) => { 51 | /* 52 | #swagger.tags = ['files'] 53 | #swagger.summary = "Open the file in the local system" 54 | #swagger.responses[200] = { 55 | description: "Open the file in the localsystem and use the id = require( the file to open it as queryparam named 'fileid'" 56 | */ 57 | try { 58 | let opened = false; 59 | let torrents = req.app.locals.storage.liveData.client.torrents.map(mapTorrent); 60 | let oldTorrent = await req.app.locals.storage.getAllTorrent(); 61 | torrents.push(...oldTorrent.filter(x => !torrents.map(y => y.infoHash).includes(x.infoHash))) 62 | torrents.forEach((t) => { 63 | if (!opened && t && t.files) { 64 | t.files.forEach((f) => { 65 | if (!opened && req.query.fileid === simpleHash(t.infoHash, f.name)) { 66 | open(f.path); 67 | opened = true; 68 | } 69 | }) 70 | } 71 | }) 72 | res.status(200).json(opened) 73 | } catch (e) { 74 | console.error(e) 75 | } 76 | }); 77 | router.get('/openFolder', async (req, res, next) => { 78 | /* 79 | #swagger.tags = ['files'] 80 | #swagger.summary = "Open the folder where the file is" 81 | #swagger.responses[200] = { 82 | description: "Open the folder where the file is in the localsystem" 83 | */ 84 | try { 85 | let opened = false; 86 | let torrents = req.app.locals.storage.liveData.client.torrents.map(mapTorrent); 87 | let oldTorrent = await req.app.locals.storage.getAllTorrent(); 88 | torrents.push(...oldTorrent.filter(x => !torrents.map(y => y.infoHash).includes(x.infoHash))) 89 | for (const t of torrents) { 90 | if (!opened && t && t.files && t.infoHash === req.query.torrentId) { 91 | let f = t.files[0] 92 | await open(path.dirname(f.path), {wait: true}); 93 | opened = true; 94 | } 95 | } 96 | res.status(200).json(opened) 97 | } catch (e) { 98 | console.error(e) 99 | } 100 | }); 101 | 102 | 103 | router.get('/stream/:filename', async (req, res, next) => { 104 | /* 105 | #swagger.tags = ['files'] 106 | #swagger.summary = "Open the file in the local system" 107 | #swagger.responses[200] = { 108 | description: "Open the file in the localsystem and use the id = require( the file to open it as queryparam named 'fileid'" 109 | */ 110 | try { 111 | let opened = false; 112 | let torrents = req.app.locals.storage.liveData.client.torrents.map(mapTorrent); 113 | let oldTorrent = await req.app.locals.storage.getAllTorrent(); 114 | torrents.push(...oldTorrent.filter(x => !torrents.map(y => y.infoHash).includes(x.infoHash))) 115 | torrents.forEach((t) => { 116 | if (!opened && t && t.files) { 117 | t.files.forEach((f) => { 118 | if (!opened && req.query.fileid === simpleHash(t.infoHash, f.name)) { 119 | res.sendFile(f.path); 120 | opened = true; 121 | } 122 | }) 123 | } 124 | }) 125 | } catch (e) { 126 | console.error(e) 127 | } 128 | }); 129 | 130 | 131 | router.get('/search', async (req, res, next) => { 132 | /* 133 | #swagger.tags = ['Files'] 134 | #swagger.summary = "Return a search indexed torrent, based on searx" 135 | #swagger.responses[200] = { 136 | description: "Configuration data", 137 | schema: [{ 138 | "url": "https://xxx.xxx/description.php?id=56842669", 139 | "title": "Texas Chainsaw Massacre (2022) [720p] [WEBRip]", 140 | "seed": "91", 141 | "leech": "31", 142 | "magnetlink": "magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp://tracker.leechers-paradise.org:6969&tr=udp://tracker.coppersurfer.tk:6969&tr=udp://tracker.opentrackr.org:1337&tr=udp://explodie.org:6969&tr=udp://tracker.empire-js.us:1337&tr=wss://tracker.btorrent.xyz&tr=wss://tracker.openwebtorrent.com&ws=https://webtorrent.io/torrents/&xs=https://webtorrent.io/torrents/sintel.torrent", 143 | "template": "torrent.html", 144 | "publishedDate": "Feb 19, 2022", 145 | "filesize": 799469148, 146 | "engine": "xxx", 147 | "parsed_url": [ 148 | "https", 149 | "xxx.xxx", 150 | "/description.php", 151 | "", 152 | "id=56842669", 153 | "" 154 | ], 155 | "engines": [ 156 | "xxx" 157 | ], 158 | "positions": [ 159 | 7 160 | ], 161 | "score": 0.14285714285714285, 162 | "category": "videos", 163 | "pretty_url": "https://xxx.xxx/description.php?id=56842669", 164 | "pubdate": "2022-02-19 03:35:55" 165 | }] 166 | } 167 | */ 168 | try { 169 | let results = await req.app.locals.searx.search(req && req.query && req.query.q); 170 | res.status(200).json(results) 171 | } catch (e) { 172 | console.error(e) 173 | } 174 | }); 175 | 176 | 177 | router.get('/movie', async (req, res, next) => { 178 | /* 179 | #swagger.tags = ['Files'] 180 | #swagger.summary = "Return a result fetched from public 1337x instances" 181 | #swagger.responses[200] = { 182 | description: "List of result", 183 | schema: [] 184 | */ 185 | try { 186 | try { 187 | let results = await crawlMovies1337x(req && req.query && req.query.q); 188 | res.status(200).json(results) 189 | } catch (e) { 190 | console.error(e) 191 | let results = await req.app.locals.searx.search(req && req.query && req.query.q); 192 | res.status(200).json(results) 193 | } 194 | } catch (e) { 195 | console.error(e) 196 | } 197 | }); 198 | 199 | router.get('/tvshow', async (req, res, next) => { 200 | /* 201 | #swagger.tags = ['Files'] 202 | #swagger.summary = "Return a result fetched from public 1337x instances" 203 | #swagger.responses[200] = { 204 | description: "List of result", 205 | schema: [] 206 | */ 207 | try { 208 | try { 209 | let results = await crawlTvShow1337x(req && req.query && req.query.q); 210 | res.status(200).json(results) 211 | } catch (e) { 212 | console.error(e) 213 | let results = await req.app.locals.searx.search(req && req.query && req.query.q); 214 | res.status(200).json(results) 215 | } 216 | } catch (e) { 217 | console.error(e) 218 | } 219 | }); 220 | 221 | 222 | router.get('/games/:source/', async (req, res, next) => { 223 | /* 224 | #swagger.tags = ['Files'] 225 | #swagger.summary = "Indexed search of games parsed = require( games website" 226 | #swagger.responses[200] = { 227 | description: "Configuration data", 228 | schema: [{ 229 | "name": "Cyberpunk 2077", 230 | "description": "91", 231 | "originalSize": "42.2 GB", 232 | "repackSize": " = require( 17.2 GB [Selective Download]", 233 | "magnet": [] 234 | } 235 | */ 236 | try { 237 | let source = req.params.source 238 | let q = req && req.query && req.query.q 239 | let results; 240 | switch (source) { 241 | case "FITGIRL": 242 | default: 243 | results = await crawlFitGirl(q) 244 | break; 245 | } 246 | res.status(200).json(results) 247 | } catch (e) { 248 | console.error(e) 249 | } 250 | }); 251 | 252 | module.exports = router; 253 | 254 | 255 | 256 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const swaggerUi = require('swagger-ui-express'); 3 | const swaggerDocument = require('./../swagger-output.json'); 4 | const moment = require("moment/moment"); 5 | 6 | const router = express.Router(); 7 | router.use('/api-docs', swaggerUi.serve); 8 | router.get('/api-docs', swaggerUi.setup(swaggerDocument)); 9 | router.get('/health-check', async (req, res, next) => { 10 | /* 11 | #swagger.tags = ['Index'] 12 | #swagger.summary = "Return true if the server is operative" 13 | #swagger.responses[200] = { 14 | description: "The status of the server", 15 | schema: true 16 | } 17 | */ 18 | res.status(200).json(true) 19 | }); 20 | 21 | 22 | module.exports = router; 23 | -------------------------------------------------------------------------------- /routes/indexer.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { 3 | crawlFitGirl, 4 | crawlTvShow1337x, 5 | crawlMovies1337x, 6 | crawlGames1337x, 7 | jackettCrawl, 8 | categories 9 | } = require("./classes/indexers"); 10 | const { 11 | MOVIES, 12 | GAMES, 13 | TVSHOW, 14 | FITGIRL, 15 | GENERIC, 16 | MUSIC, 17 | SEARX, 18 | _1337x, 19 | BOOK, 20 | ANIME, 21 | XXX, 22 | NYAASI, RARBG 23 | } = require("./classes/type"); 24 | const router = express.Router(); 25 | 26 | const filterIndexing = (elem) => { 27 | return elem.magnet; 28 | } 29 | 30 | const parseIndexing = (elem) => { 31 | return { 32 | name: elem.title || elem.name, 33 | description: elem.description || elem.guid || elem.url, 34 | seeders: elem.seeders || elem.seed, 35 | peers: elem.peers || elem.leech, 36 | size: elem.size || elem.filesize || elem.originalSize, 37 | repackSize: elem.repackSize, 38 | magnet: elem.magnet || elem.magneturl || elem.magnetlink || elem.link || (elem.magnets && elem.magnets[0]), 39 | magnets: elem.magnets 40 | } 41 | } 42 | 43 | router.get('/:source', async (req, res, next) => { 44 | /* 45 | #swagger.tags = ['indexer'] 46 | #swagger.summary = "Based on the source will make a research on the defined indexers then remap it to a standard format (Not all the value can be populated)" 47 | #swagger.responses[200] = { 48 | description: "Configuration data", 49 | schema: [{ 50 | "name": "Cyberpunk 2077", 51 | "description": "An intresting DRM-free game yeah", 52 | "seeders": "91", 53 | "peers": "910", 54 | "size": "42.2 GB", 55 | "repackSize": " = require( 17.2 GB [Selective Download]", 56 | "magnet": "magnet:...", 57 | "magnets": ["magnet:...","magnet:..."] 58 | } 59 | */ 60 | try { 61 | let source = req.params.source 62 | let q = req && req.query && req.query.q 63 | let results; 64 | switch (source) { 65 | case RARBG: 66 | results = (await jackettCrawl(categories.rarbg.name, null, q)); 67 | break; 68 | case ANIME: 69 | results = (await jackettCrawl(categories.nyaasi.name, categories.nyaasi.anime, q)); 70 | break; 71 | case BOOK: 72 | results = (await jackettCrawl(categories._1337x.name, categories._1337x.book, q)); 73 | break; 74 | case XXX: 75 | results = (await jackettCrawl(categories._1337x.name, categories._1337x.xxx, q)); 76 | break; 77 | case NYAASI: 78 | results = (await jackettCrawl(categories.nyaasi.name, null, q)); 79 | break; 80 | case _1337x: 81 | case GENERIC: 82 | results = (await jackettCrawl(categories._1337x.name, null, q)); 83 | break; 84 | case SEARX: 85 | results = (await req.app.locals.searx.search(req && req.query && req.query.q)); 86 | break; 87 | case MUSIC: 88 | results = (await jackettCrawl(categories._1337x.name, categories._1337x.music, q)); 89 | break; 90 | case MOVIES: 91 | results = (await jackettCrawl(categories._1337x.name, categories._1337x.movies, q)); 92 | break; 93 | case TVSHOW: 94 | results = (await jackettCrawl(categories._1337x.name, categories._1337x.tvShow, q)); 95 | break; 96 | case GAMES: 97 | results = (await jackettCrawl(categories._1337x.name, categories._1337x.games, q)); 98 | break; 99 | case FITGIRL: 100 | default: 101 | results = (await crawlFitGirl(q)) 102 | break; 103 | } 104 | res.status(200).json(results.map(parseIndexing).filter(filterIndexing)) 105 | } catch (e) { 106 | console.error(e) 107 | } 108 | }); 109 | router.get('/', (req, res, next) => { 110 | /* 111 | #swagger.tags = ['indexer'] 112 | #swagger.summary = "Return the list of the supported indexer" 113 | #swagger.responses[200] = { 114 | description: "Indexer list", 115 | schema: ["MOVIES","GAMES","TVSHOW"] 116 | */ 117 | try { 118 | res.status(200).json([MOVIES, GAMES, TVSHOW, FITGIRL, GENERIC, MUSIC, SEARX, _1337x, BOOK, ANIME, XXX, NYAASI, RARBG]) 119 | } catch (e) { 120 | console.error(e) 121 | } 122 | }); 123 | 124 | module.exports = router; 125 | -------------------------------------------------------------------------------- /routes/stream.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const FormData = require('form-data') 3 | const fs = require("fs") 4 | const express = require('express') 5 | 6 | const router = express.Router(); 7 | const VARIABLE_CONF_STREAM = "configurationStream" 8 | 9 | 10 | router.post('/upload', async (req, res, next) => { 11 | /* 12 | #swagger.tags = ['Stream'] 13 | #swagger.summary = "Upload to a remote a single file" 14 | #swagger.parameters['torrent'] = { 15 | in: 'body', 16 | description: 'File to load', 17 | schema: { 18 | $magnet: 'magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent' 19 | $fileName: 'xxx.mp4' 20 | } 21 | } 22 | #swagger.responses[200] = { 23 | description: "Result of the operation", 24 | schema: { 25 | "files": [ 26 | { 27 | "name": "xxx.mp4", 28 | "size": 11111, 29 | "url": "https://domain.com/mcurg8n382uy", 30 | "deleteUrl": "https://domain.com/mcurg8n382uy?killcode=agdt0meepz" 31 | } 32 | ] 33 | } 34 | } 35 | */ 36 | try { 37 | let streamConf = JSON.parse(req.app.locals.storage.getVariable(VARIABLE_CONF_STREAM) || "{}"); 38 | let token = streamConf.uptobox && streamConf.uptobox.token && streamConf.uptobox.token; 39 | 40 | if (!token || !streamConf.uploadEnabled) { 41 | if (streamConf.uploadEnabled) { 42 | res.status(405).json({ 43 | message: "Missing auth token for uptobox" 44 | }); 45 | } else { 46 | res.status(405).json({ 47 | message: "Upload disabled" 48 | }); 49 | } 50 | 51 | } else { 52 | let torrent = req.app.locals.storage.liveData.client.get(req.body.magnet); 53 | if (!torrent) { 54 | let oldTorrent = await req.app.locals.storage.getAllTorrent(); 55 | torrent = oldTorrent.find(x => x.magnet == req.body.magnet) 56 | } 57 | let file = torrent.files.find(x => x.name == req.body.fileName) 58 | if (!file) { 59 | res.status(404) 60 | } else { 61 | let responseUpload = await axios({ 62 | method: 'GET', 63 | url: 'https://uptobox.com/api/upload?token=' + token, 64 | }) 65 | let data = new FormData(); 66 | data.append('token', token); 67 | data.append('file', fs.createReadStream(file.path)); 68 | // data.append('file', fs.createReadStream("./Downloads/a.mp4")); 69 | 70 | let config = { 71 | method: "POST", 72 | url: "https:" + responseUpload.data.data.uploadLink, 73 | maxContentLength: 100000000, 74 | maxBodyLength: 1000000000, 75 | timeout: 0, 76 | headers: { 77 | ...data.getHeaders() 78 | }, 79 | data: data 80 | }; 81 | 82 | let uploadResponse = await axios(config); 83 | res.status(200).json(uploadResponse.data); 84 | } 85 | } 86 | } catch (e) { 87 | console.error(e) 88 | } 89 | }); 90 | 91 | 92 | router.post('/check-existing', async (req, res, next) => { 93 | /* 94 | #swagger.tags = ['Stream'] 95 | #swagger.summary = "Upload to a remote a single file" 96 | #swagger.parameters['torrent'] = { 97 | in: 'body', 98 | description: 'File to load', 99 | schema: { 100 | $magnet: 'magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent' 101 | $fileName: 'xxx.mp4' 102 | } 103 | } 104 | #swagger.responses[200] = { 105 | description: "Result of the operation", 106 | schema: { 107 | "files": [ 108 | { 109 | "name": "xxx.mp4", 110 | "size": 11111, 111 | "url": "https://domain.com/mcurg8n382uy", 112 | "deleteUrl": "https://domain.com/mcurg8n382uy?killcode=agdt0meepz" 113 | } 114 | ] 115 | } 116 | } 117 | */ 118 | try { 119 | let streamConf = JSON.parse(req.app.locals.storage.getVariable(VARIABLE_CONF_STREAM) || "{}"); 120 | let token = streamConf.uptobox && streamConf.uptobox.token && streamConf.uptobox.token; 121 | 122 | if (!token || !streamConf.uploadEnabled) { 123 | if (streamConf.uploadEnabled) { 124 | res.status(405).json({ 125 | message: "Missing auth token for uptobox" 126 | }); 127 | } else { 128 | res.status(405).json({ 129 | message: "Upload disabled" 130 | }); 131 | } 132 | 133 | } else { 134 | let torrent = req.app.locals.storage.liveData.client.get(req.body.magnet); 135 | if (!torrent) { 136 | let oldTorrent = await req.app.locals.storage.getAllTorrent(); 137 | torrent = oldTorrent.find(x => x.magnet == req.body.magnet) 138 | } 139 | let file = torrent.files.find(x => x.name == req.body.fileName) 140 | if (!file) { 141 | res.status(404) 142 | } else { 143 | let responseSearch = await axios({ 144 | method: "GET", 145 | url: "https://uptobox.com/api/user/files?token=" + token + "&path=//&limit=1&offset=0&searchField=file_name&search=" + file.name, 146 | }) 147 | 148 | res.status(200).json(responseSearch.data.data.files.map(x => { 149 | return { 150 | name: x.file_name, 151 | size: x.file_size, 152 | url: "https://domain.com/" + x.file_code 153 | } 154 | 155 | })); 156 | } 157 | } 158 | } catch (e) { 159 | console.error(e) 160 | } 161 | }); 162 | 163 | 164 | router.get('/config', async (req, res, next) => { 165 | /* 166 | #swagger.tags = ['Stream'] 167 | #swagger.summary = "Retrieve the configuration about streaming" 168 | #swagger.responses[200] = { 169 | description: "Configuration data", 170 | schema: { 171 | $uptobox: {token: 'xxxxxxx'} 172 | } 173 | } 174 | */ 175 | try { 176 | let streamConf = JSON.parse(req.app.locals.storage.getVariable(VARIABLE_CONF_STREAM) || "{}"); 177 | res.status(200).json(streamConf) 178 | } catch (e) { 179 | console.error(e) 180 | } 181 | }); 182 | 183 | 184 | router.post('/config', async (req, res, next) => { 185 | /* 186 | #swagger.tags = ['Stream'] 187 | #swagger.summary = "Update the configuration about streaming platform" 188 | #swagger.parameters['conf'] = { 189 | in: 'body', 190 | description: 'Data about configuration (Only uptobox available now', 191 | schema: { 192 | $uploadEnabled: false, 193 | $uptobox: {token: 'xxxxxxx'} 194 | } 195 | } 196 | #swagger.responses[200] = { 197 | description: "Configuration data", 198 | schema: { 199 | $uptobox: {token: 'xxxxxxx'} 200 | } 201 | } 202 | */ 203 | try { 204 | let streamConf = JSON.parse(req.app.locals.storage.getVariable(VARIABLE_CONF_STREAM) || "{}"); 205 | let upToBoxToken = req.body.uptobox && req.body.uptobox.token; 206 | streamConf = { 207 | uploadEnabled: req.body.uploadEnabled, 208 | uptobox: {token: upToBoxToken} 209 | } 210 | req.app.locals.storage.setVariable(VARIABLE_CONF_STREAM, JSON.stringify(streamConf)); 211 | res.status(200).json(streamConf) 212 | } catch (e) { 213 | console.error(e) 214 | } 215 | }); 216 | 217 | module.exports = router; 218 | -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | var app = require('./app'); 2 | var http = require('http'); 3 | var open = require('open'); 4 | var schedule = require('node-schedule'); 5 | const {wss} = require("./websocket/server"); 6 | 7 | function start(port = 3000) { 8 | 9 | process.env.NODE_ENV = "development" 10 | 11 | 12 | process.on('SIGINT', function () { 13 | schedule.gracefulShutdown() 14 | .then(() => process.exit(0)) 15 | }) 16 | 17 | process.on('uncaughtException', function (err) { 18 | try { 19 | console.error('*** uncaughtException:', err); 20 | if (process.send) { 21 | // Say my process is ready 22 | process.send({message: "ERROR: " + err.message, data: err}); 23 | } 24 | } catch (err) { 25 | 26 | } 27 | }); 28 | /** 29 | * Get port from environment and store in Express. 30 | */ 31 | port = normalizePort(port || process.env.PORT || '3000'); 32 | if (process.send) { 33 | // Say my process is ready 34 | process.send({message: "PORT", data: port}); 35 | } 36 | app.set('port', port); 37 | 38 | 39 | /** 40 | * Create HTTP server. 41 | */ 42 | 43 | var server = http.createServer(app); 44 | 45 | /** 46 | * Listen on provided port, on all network interfaces. 47 | */ 48 | 49 | server.listen(port, async () => { 50 | console.log('Express server stared! Mode: ', process.env.NODE_ENV); 51 | app.locals.storage.setServer(server) 52 | }); 53 | 54 | server.on('error', onError); 55 | server.on('listening', onListening); 56 | 57 | /** 58 | * Normalize a port into a number, string, or false. 59 | */ 60 | 61 | function normalizePort(val) { 62 | var port = parseInt(val, 10); 63 | 64 | if (isNaN(port)) { 65 | // named pipe 66 | return val; 67 | } 68 | 69 | if (port >= 0) { 70 | // port number 71 | return port; 72 | } 73 | 74 | return false; 75 | } 76 | 77 | /** 78 | * Event listener for HTTP server "error" event. 79 | */ 80 | 81 | function onError(error) { 82 | console.error("Error in main process", error.code) 83 | if (error.syscall !== 'listen') { 84 | throw error; 85 | } 86 | 87 | var bind = typeof port === 'string' 88 | ? 'Pipe ' + port 89 | : 'Port ' + port; 90 | 91 | // handle specific listen errors with friendly messages 92 | switch (error.code) { 93 | case 'EACCES': 94 | console.error(bind + ' requires elevated privileges'); 95 | process.exit(1); 96 | break; 97 | case 'EADDRINUSE': 98 | console.error(bind + ' is already in use. Change to: ' + (port + 1)); 99 | if (port < 65535) { 100 | start(port + 1) 101 | } 102 | break; 103 | default: 104 | throw error; 105 | } 106 | } 107 | 108 | /** 109 | * Event listener for HTTP server "listening" event. 110 | */ 111 | 112 | function onListening() { 113 | var addr = server.address(); 114 | var bind = typeof addr === 'string' 115 | ? 'pipe ' + addr 116 | : 'port ' + addr.port; 117 | } 118 | 119 | 120 | } 121 | 122 | module.exports = start; 123 | -------------------------------------------------------------------------------- /swagger.js: -------------------------------------------------------------------------------- 1 | const swaggerAutogen = require('swagger-autogen')() 2 | 3 | const outputFile = './swagger-output.json' 4 | const endpointsFiles = ['./app.js'] 5 | 6 | swaggerAutogen(outputFile, endpointsFiles) 7 | -------------------------------------------------------------------------------- /views/error.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p Welcome to #{title} 6 | -------------------------------------------------------------------------------- /views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content 8 | -------------------------------------------------------------------------------- /website/crawfish-official/.env: -------------------------------------------------------------------------------- 1 | PORT=3001 2 | SKIP_PREFLIGHT_CHECK=true 3 | FAST_REFRESH=false 4 | BROWSER=brave 5 | REACT_APP_BASE_PATH=/ 6 | REACT_APP_CUSTOM_API_PORT=3000 7 | 8 | 9 | -------------------------------------------------------------------------------- /website/crawfish-official/.env.production: -------------------------------------------------------------------------------- 1 | REACT_APP_BASE_PATH=/ 2 | BUILD_PATH='./../../public/crawfish-official' 3 | -------------------------------------------------------------------------------- /website/crawfish-official/.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 | .idea 25 | -------------------------------------------------------------------------------- /website/crawfish-official/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 TND 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /website/crawfish-official/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crawfish-official", 3 | "version": "1.0.0", 4 | "homepage": ".", 5 | "private": true, 6 | "dependencies": { 7 | "@emotion/react": "^11.10.4", 8 | "@emotion/styled": "^11.10.4", 9 | "@mui/icons-material": "^5.10.6", 10 | "@mui/material": "^5.10.8", 11 | "@mui/utils": "^5.10.6", 12 | "@testing-library/jest-dom": "^5.16.5", 13 | "@testing-library/react": "^13.4.0", 14 | "@testing-library/user-event": "^14.4.3", 15 | "axios": "^0.27.2", 16 | "mobx": "^6.6.2", 17 | "mobx-react": "^7.5.3", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "react-scripts": "4.0.3", 21 | "web-vitals": "^3.0.1", 22 | "webtorrent": "^1.8.6", 23 | "ws": "^8.8.1" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject" 30 | }, 31 | "eslintConfig": { 32 | "extends": [ 33 | "react-app", 34 | "react-app/jest" 35 | ] 36 | }, 37 | "browser": { 38 | "crypto": false 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | }, 52 | "devDependencies": { 53 | "cross-env": "^7.0.3" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /website/crawfish-official/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/website/crawfish-official/public/favicon.ico -------------------------------------------------------------------------------- /website/crawfish-official/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 19 | 20 | 29 | CrawFish 30 | 31 | 32 | 33 |
34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /website/crawfish-official/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/website/crawfish-official/public/logo192.png -------------------------------------------------------------------------------- /website/crawfish-official/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/website/crawfish-official/public/logo512.png -------------------------------------------------------------------------------- /website/crawfish-official/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "CrawFish", 3 | "name": "A torrent tool on QuiX technology", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": "/", 22 | "display": "fullscreen", 23 | "theme_color": "#000000", 24 | "background_color": "#121212" 25 | } 26 | -------------------------------------------------------------------------------- /website/crawfish-official/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /website/crawfish-official/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /website/crawfish-official/src/App.js: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import TorrentManager from "./screen/TorrentManager"; 3 | import {useAppStore} from "./MobxContext/AppContext"; 4 | 5 | function App() { 6 | let store = useAppStore(); 7 | 8 | return ( 9 | 10 | ); 11 | } 12 | 13 | export default App; 14 | -------------------------------------------------------------------------------- /website/crawfish-official/src/App.test.js: -------------------------------------------------------------------------------- 1 | import {render, screen} from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /website/crawfish-official/src/MobxContext/AppContext.js: -------------------------------------------------------------------------------- 1 | import {createContext, useContext} from "react"; 2 | import {createAppStore} from "./appStore"; 3 | import {useLocalObservable} from "mobx-react"; 4 | 5 | const AppContext = createContext(null) 6 | 7 | export const AppProvider = ({children}) => { 8 | const appStore = useLocalObservable(createAppStore) 9 | 10 | return 11 | {children} 12 | 13 | } 14 | 15 | export const useAppStore = () => useContext(AppContext) 16 | 17 | -------------------------------------------------------------------------------- /website/crawfish-official/src/MobxContext/appStore.js: -------------------------------------------------------------------------------- 1 | import {createTheme} from "@mui/material"; 2 | 3 | const defaultTheme = createTheme(); 4 | const options = { 5 | typography: { 6 | fontSize: 12, 7 | }, 8 | palette: { 9 | mode: 'dark', 10 | background: { 11 | default: "#303030", 12 | paper: "#424242" 13 | } 14 | }, 15 | components: { 16 | MuiTab: { 17 | root: { 18 | padding: "0px" 19 | } 20 | }, 21 | MuiLinearProgress: { 22 | styleOverrides: { 23 | root: { 24 | borderRadius: "10px" 25 | } 26 | } 27 | }, 28 | MuiListItemButton: { 29 | styleOverrides: { 30 | root: { 31 | borderRadius: "20px", 32 | paddingLeft: "10px", 33 | paddingRight: "10px", 34 | } 35 | } 36 | }, 37 | MuiTableContainer: { 38 | styleOverrides: { 39 | root: { 40 | height: "100%", 41 | }, 42 | }, 43 | }, 44 | MuiContainer: { 45 | styleOverrides: { 46 | root: { 47 | overflow:"hidden", 48 | paddingLeft: "0px", 49 | paddingRight: "0px", 50 | height: "100%", 51 | [defaultTheme.breakpoints.up('xs')]: { 52 | paddingLeft: "0px", 53 | paddingRight: "0px", 54 | paddingTop: "5px", 55 | } 56 | }, 57 | }, 58 | }, 59 | }, 60 | }; 61 | export const createAppStore = () => { 62 | return { 63 | loading: { 64 | get: true, 65 | set(data) { 66 | if (data !== this.get) { 67 | this.get = data; 68 | } 69 | }, 70 | }, 71 | error: { 72 | get: false, 73 | set(data) { 74 | if (data !== this.get) { 75 | this.get = data; 76 | } 77 | }, 78 | }, 79 | conf: 80 | { 81 | get: {}, 82 | set(data) { 83 | if (data !== this.get) { 84 | this.get = data; 85 | } 86 | }, 87 | }, 88 | status: 89 | { 90 | get: [], 91 | set(data) { 92 | if (JSON.stringify(data) !== JSON.stringify(this.get)) { 93 | this.get = data; 94 | } 95 | }, 96 | }, 97 | theme: { 98 | get: createTheme(options), 99 | options: options, 100 | getDefaultTheme() { 101 | return createTheme(options) 102 | }, 103 | set(themeOptions) { 104 | this.get = createTheme(themeOptions) 105 | } 106 | 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /website/crawfish-official/src/asset/logo-nobackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/website/crawfish-official/src/asset/logo-nobackground.png -------------------------------------------------------------------------------- /website/crawfish-official/src/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | width: 100%; 4 | } 5 | 6 | div.root { 7 | height: 100%; 8 | width: 100%; 9 | } 10 | 11 | body { 12 | height: 100%; 13 | width: 100%; 14 | margin: 0; 15 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 16 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 17 | sans-serif; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | } 21 | 22 | code { 23 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 24 | monospace; 25 | } 26 | -------------------------------------------------------------------------------- /website/crawfish-official/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './index.css'; 3 | import App from './App'; 4 | import {createRoot} from 'react-dom/client'; 5 | import {AppProvider} from "./MobxContext/AppContext"; 6 | 7 | 8 | const container = document.getElementById('root'); 9 | const root = createRoot(container); 10 | 11 | 12 | root.render(); 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /website/crawfish-official/src/library/WebTorrentGuiV2.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {observer} from "mobx-react"; 3 | import {WebTorrentHelper} from "./WebTorrentHelper"; 4 | import {Button, Divider, InputAdornment, LinearProgress, Snackbar, Stack, TextField, Tooltip} from "@mui/material"; 5 | import {AddCircle, Delete, DeleteForever, PauseCircle, PlayCircle, Search} from "@mui/icons-material"; 6 | 7 | import {Menu} from "./components/Menu"; 8 | import AddTorrent from "./components/AddTorrent"; 9 | 10 | import TorrentClientTable from "./components/TorrentClientTable"; 11 | import FilesTable from "./components/FilesTable"; 12 | import {grey} from "@mui/material/colors"; 13 | import {CLIENT, CLIENT_DOWNLOAD, CLIENT_SEEDING, GAMES, MOVIES, SETTINGS, TVSHOW} from "./types"; 14 | import {SettingsPage} from "./components/SettingsPage"; 15 | import SpeedMeter from "./components/SpeedMeter"; 16 | import TorrentTableRow from "./components/TorrentTableRow"; 17 | 18 | class WebTorrentGuiV2 extends Component { 19 | 20 | state = { 21 | selectedTorrent: [], 22 | filterTorrent: () => { 23 | return true 24 | }, 25 | showAddTorrent: false, 26 | enabledView: CLIENT, 27 | search: "", 28 | severity: "success", 29 | snackbar: false, 30 | snackbarMessage: "Copied to clipboard", 31 | defaultMenu: [], 32 | order:'asc', 33 | orderBy:'name' 34 | } 35 | 36 | componentDidMount() { 37 | let {host, port, baseUrl, store} = this.props 38 | let {loading} = store; 39 | this.setState({client: new WebTorrentHelper(baseUrl ? {baseUrl} : {baseUrl: host + ":" + port}, store)}, async () => { 40 | await this.refreshCategory(); 41 | loading.set(false); 42 | // this.setState({ 43 | // enabledView: SETTINGS 44 | // }) 45 | }) 46 | } 47 | 48 | componentWillUnmount() { 49 | clearInterval(this.interval) 50 | } 51 | 52 | refreshCategory = async () => { 53 | try { 54 | let {client} = this.state 55 | let res = await client.getCategory(); 56 | this.setState({defaultMenu: res.data}) 57 | 58 | } catch (e) { 59 | console.error(e) 60 | } 61 | } 62 | 63 | removeAll = () => { 64 | try { 65 | let {client, selectedTorrent} = this.state; 66 | let {store} = this.props; 67 | let {status} = store; 68 | let torrents = status.get; 69 | torrents.forEach(x => { 70 | if (selectedTorrent == null || selectedTorrent.length < 1 || selectedTorrent.includes(x.infoHash)) { 71 | client.removeTorrent({magnet: x.magnet}) 72 | } 73 | }) 74 | } catch (e) { 75 | console.error(e) 76 | } 77 | } 78 | 79 | destroyAll = () => { 80 | try { 81 | let {client, selectedTorrent} = this.state; 82 | let {store} = this.props; 83 | let {status} = store; 84 | let torrents = status.get; 85 | torrents.forEach(x => { 86 | if (selectedTorrent == null || selectedTorrent.length < 1 || selectedTorrent.includes(x.infoHash)) { 87 | client.destroyTorrent({magnet: x.magnet}) 88 | } 89 | }) 90 | } catch (e) { 91 | console.error(e) 92 | } 93 | } 94 | 95 | resumeAll = () => { 96 | try { 97 | let {client, selectedTorrent} = this.state; 98 | let {store} = this.props; 99 | let {status} = store; 100 | let torrents = status.get; 101 | torrents.forEach(x => { 102 | if (selectedTorrent == null || selectedTorrent.length < 1 || selectedTorrent.includes(x.infoHash)) { 103 | client.addTorrent({magnet: x.magnet, path: x.path}) 104 | } 105 | }) 106 | } catch (e) { 107 | console.error(e) 108 | } 109 | } 110 | pauseAll = () => { 111 | try { 112 | let {client, selectedTorrent} = this.state; 113 | let {store} = this.props; 114 | let {status} = store; 115 | let torrents = status.get; 116 | torrents.forEach(x => { 117 | if (selectedTorrent == null || selectedTorrent.length < 1 || selectedTorrent.includes(x.infoHash)) { 118 | client.pauseTorrent({magnet: x.magnet}) 119 | } 120 | }) 121 | } catch (e) { 122 | console.error(e) 123 | } 124 | } 125 | 126 | descendingComparator(a, b, orderBy) { 127 | if (b[orderBy] < a[orderBy]) { 128 | return -1; 129 | } 130 | if (b[orderBy] > a[orderBy]) { 131 | return 1; 132 | } 133 | return 0; 134 | } 135 | 136 | getComparator(order, orderBy) { 137 | return order === 'desc' 138 | ? (a, b) => this.descendingComparator(a, b, orderBy) 139 | : (a, b) => -this.descendingComparator(a, b, orderBy); 140 | } 141 | 142 | openSnackbar = () => { 143 | this.setState({snackbar: true}) 144 | } 145 | handleRequestSort = (event, property) => { 146 | let {order, orderBy} = this.state; 147 | const isAsc = orderBy === property && order === 'asc'; 148 | this.setState({ 149 | order:isAsc ? 'desc' : 'asc', 150 | orderBy:property 151 | }) 152 | }; 153 | renderBody = () => { 154 | let {enabledView, enabledCategory, filterTorrent, client, search, selectedTorrent, order, orderBy} = this.state; 155 | let {remote, store} = this.props; 156 | let {status} = store; 157 | let torrents = status.get.filter(filterTorrent); 158 | 159 | switch (enabledView) { 160 | case SETTINGS: 161 | return ; 166 | case CLIENT: 167 | case CLIENT_DOWNLOAD: 168 | case CLIENT_SEEDING: 169 | return { 173 | if (event.target.checked) { 174 | this.setState({selectedTorrent: torrents.map(x => x.infoHash)}) 175 | } else { 176 | this.setState({selectedTorrent: []}) 177 | } 178 | }} 179 | order={order} 180 | orderBy={orderBy} 181 | onRequestSort={this.handleRequestSort} 182 | torrents={torrents.sort(this.getComparator(order, orderBy))} 183 | predicate={x => selectedTorrent.includes(x.infoHash)} 184 | callbackfn={(torrent, index) => { 185 | return 193 | }}/> 194 | case TVSHOW: 195 | case GAMES: 196 | case MOVIES: 197 | default: 198 | return 206 | 207 | } 208 | } 209 | 210 | 211 | render() { 212 | let { 213 | client, 214 | showAddTorrent, 215 | enabledView, 216 | search, 217 | severity, 218 | snackbar, 219 | snackbarMessage, 220 | defaultMenu, 221 | enabledCategory 222 | } = this.state; 223 | let {logo, store} = this.props; 224 | let {loading} = store; 225 | let disabledToolbar = enabledView === SETTINGS; 226 | if (loading.get) { 227 | return 231 | 232 | 233 | } 234 | return ( 235 | 239 | { 243 | this.setState({snackbar: false}) 244 | }} 245 | severity={severity} 246 | message={snackbarMessage} 247 | key={"snackabr"} 248 | autoHideDuration={5000} 249 | /> 250 | 253 | { 259 | let categoryIndex = defaultMenu.findIndex(x => x.id === id); 260 | if (categoryIndex !== -1) { 261 | this.setState({ 262 | enabledView, 263 | enabledCategory: defaultMenu[categoryIndex] 264 | }) 265 | } else { 266 | this.setState({ 267 | enabledView, 268 | enabledCategory: undefined 269 | }) 270 | } 271 | }} 272 | onChange={this.darkLightMode} 273 | filterDownload={() => { 274 | this.setState({ 275 | filterTorrent: (x) => { 276 | return x.paused === false && x.progress !== 1; 277 | }, 278 | enabledView: CLIENT_DOWNLOAD 279 | }) 280 | }} 281 | filterSeeding={() => { 282 | this.setState({ 283 | filterTorrent: (x) => { 284 | return x.paused === false && x.progress >= 1; 285 | }, 286 | enabledView: CLIENT_SEEDING 287 | }) 288 | }} 289 | filterHome={() => { 290 | this.setState({ 291 | filterTorrent: () => { 292 | return true; 293 | }, 294 | enabledView: CLIENT 295 | }) 296 | }} 297 | /> 298 | 299 | 300 | 302 | 307 | 310 | 313 | 314 | 317 | 318 | 319 | 322 | 323 | {/*TODO Enable when sorting is working */} 324 | {/**/} 325 | {/**/} 326 | {/**/} 327 | {![CLIENT, CLIENT_DOWNLOAD, CLIENT_SEEDING, SETTINGS].includes(enabledView) && <> 328 | 330 | 335 | 336 | 337 | ) 338 | }} 339 | value={search} 340 | onChange={(e) => { 341 | let text = e.target.value; 342 | this.setState({search: text}) 343 | }} 344 | />} 345 | {client && } 349 | 350 | 351 | {this.renderBody()} 352 | 353 | 354 | { 357 | client.addTorrent({magnet, path}) 358 | .catch(console.error) 359 | this.setState({showAddTorrent: false}) 360 | }} 361 | onClose={() => { 362 | this.setState({showAddTorrent: false}) 363 | }} 364 | /> 365 | 366 | ); 367 | } 368 | 369 | 370 | isRowSelected = (infoHash) => { 371 | let {selectedTorrent} = this.state; 372 | return selectedTorrent.includes(infoHash); 373 | } 374 | onChangeRowSelection = (infoHash) => { 375 | let {selectedTorrent} = this.state; 376 | if (this.isRowSelected(infoHash)) { 377 | this.setState({ 378 | selectedTorrent: selectedTorrent.filter(x => x !== infoHash) 379 | }) 380 | } else { 381 | selectedTorrent.push(infoHash) 382 | this.setState({ 383 | selectedTorrent 384 | }) 385 | } 386 | } 387 | 388 | darkLightMode = (e, checked) => { 389 | let {store} = this.props; 390 | let {theme} = store; 391 | let mode = checked ? "dark" : "light"; 392 | let background = { 393 | default: "#303030", 394 | paper: "#424242" 395 | } 396 | if (mode === "light") { 397 | background = { 398 | default: grey[300], 399 | paper: grey[200] 400 | } 401 | } 402 | theme.set({ 403 | ...theme.options, 404 | palette: { 405 | ...theme.options.palette, 406 | mode, 407 | background 408 | } 409 | }) 410 | } 411 | } 412 | 413 | export default observer(WebTorrentGuiV2); 414 | -------------------------------------------------------------------------------- /website/crawfish-official/src/library/WebTorrentHelper.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | 4 | 5 | 6 | export class WebTorrentHelper { 7 | 8 | config = { 9 | baseUrl: "" 10 | } 11 | ws 12 | 13 | constructor(config, store) { 14 | this.config = config; 15 | let {conf, status} = store; 16 | let wssBasePath; 17 | if (this.config.baseUrl.includes("https://")) { 18 | wssBasePath = this.config.baseUrl.replace("https://", "wss://") 19 | wssBasePath = wssBasePath.replace(":3000", "") 20 | this.config.baseUrl = this.config.baseUrl.replace(":3000", "") 21 | } else { 22 | wssBasePath = this.config.baseUrl.replace("http://", "ws://") 23 | } 24 | this.refreshWs(wssBasePath, conf, status); 25 | this.axios = axios.create({ 26 | baseURL: this.config.baseUrl, 27 | timeout: 120000, 28 | headers: {'X-Custom-Header': 'foobar'} 29 | }); 30 | } 31 | 32 | refreshWs(wssBasePath, conf, status) { 33 | this.ws = new WebSocket(wssBasePath + 'wss'); 34 | this.ws.onopen = () => { 35 | console.info("Connection opened with the client") 36 | this.checkStatusWs() 37 | this.getConfWs() 38 | }; 39 | this.ws.onclose = (closeEvent, arg) => { 40 | console.debug("Disconnected from ws: ", closeEvent, arg, this.ws.readyState !== WebSocket.OPEN, this.ws.readyState) 41 | this.refreshWs(wssBasePath, conf, status) 42 | } 43 | this.ws.onmessage = (event) => { 44 | let data = JSON.parse(event.data); 45 | 46 | switch (data.key) { 47 | case "CONF": 48 | conf.set(data.value) 49 | setTimeout(this.getConfWs, 500) 50 | break; 51 | case "STATUS": 52 | status.set(data.value) 53 | setTimeout(this.checkStatusWs, 600) 54 | break; 55 | default: 56 | break; 57 | } 58 | }; 59 | } 60 | 61 | addTorrent = (data) => { 62 | return this.axios({ 63 | method: "post", 64 | url: "/torrent/add", 65 | headers: { 66 | 'Content-Type': 'application/json' 67 | }, 68 | data: data 69 | }); 70 | } 71 | pauseTorrent = (data) => { 72 | return this.axios({ 73 | method: "post", 74 | url: "/torrent/pause", 75 | headers: { 76 | 'Content-Type': 'application/json' 77 | }, 78 | data: data 79 | }); 80 | } 81 | deselectTorrent = (data) => { 82 | return this.axios({ 83 | method: "post", 84 | url: "/torrent/deselect", 85 | headers: { 86 | 'Content-Type': 'application/json' 87 | }, 88 | data: data 89 | }); 90 | } 91 | selectTorrent = (data) => { 92 | return this.axios({ 93 | method: "post", 94 | url: "/torrent/select", 95 | headers: { 96 | 'Content-Type': 'application/json' 97 | }, 98 | data: data 99 | }); 100 | } 101 | removeTorrent = (data) => { 102 | return this.axios({ 103 | method: "post", 104 | url: "/torrent/remove", 105 | headers: { 106 | 'Content-Type': 'application/json' 107 | }, 108 | data: data 109 | }); 110 | } 111 | destroyTorrent = (data) => { 112 | return this.axios({ 113 | method: "post", 114 | url: "/torrent/destroy", 115 | headers: { 116 | 'Content-Type': 'application/json' 117 | }, 118 | data: data 119 | }); 120 | } 121 | checkStatus = () => { 122 | return this.axios({ 123 | method: "get", 124 | url: "/torrent/check-status", 125 | headers: { 126 | 'Content-Type': 'application/json' 127 | } 128 | }); 129 | } 130 | checkStatusWs = () => { 131 | this.ws.send("STATUS") 132 | } 133 | getConf = () => { 134 | return this.axios({ 135 | method: "get", 136 | url: "/config/", 137 | headers: { 138 | 'Content-Type': 'application/json' 139 | } 140 | }); 141 | } 142 | getConfWs = () => { 143 | this.ws.send("CONF") 144 | } 145 | saveConf = (data) => { 146 | return this.axios({ 147 | method: "post", 148 | url: "/config/edit", 149 | headers: { 150 | 'Content-Type': 'application/json' 151 | }, 152 | data: data 153 | }); 154 | } 155 | listFiles = () => { 156 | return this.axios({ 157 | method: "get", 158 | url: "/file/list", 159 | headers: { 160 | 'Content-Type': 'application/json' 161 | } 162 | }); 163 | } 164 | fileOpen = (id) => { 165 | return this.axios({ 166 | method: "get", 167 | url: "/file/open?fileid=" + id, 168 | headers: { 169 | 'Content-Type': 'application/json' 170 | } 171 | }); 172 | } 173 | folderOpen = (id) => { 174 | return this.axios({ 175 | method: "get", 176 | url: "/file/openFolder?torrentId=" + id, 177 | headers: { 178 | 'Content-Type': 'application/json' 179 | } 180 | }); 181 | } 182 | fileStream = (id) => { 183 | return this.axios({ 184 | method: "get", 185 | url: "/file/stream?fileid=" + id, 186 | headers: { 187 | 'Content-Type': 'application/json' 188 | } 189 | }); 190 | } 191 | fileStreamLink = (id, fileName, remote) => { 192 | let url = "file/stream/" + fileName + "?fileid=" + id; 193 | if (!remote || this.config.baseUrl.includes("http")) { 194 | return this.config.baseUrl + "/" + url; 195 | } else { 196 | let protocol = window.location.protocol; 197 | let domain = window.location.hostname; 198 | let port = window.location.port; 199 | return `${protocol}//${domain}${port ? (":" + port) : ""}` + url 200 | } 201 | } 202 | getTorrentFile = (id, fileName, remote) => { 203 | let url = "torrent/get-file/" + fileName + "?torrentId=" + id; 204 | if (!remote || this.config.baseUrl.includes("http")) { 205 | return this.config.baseUrl + "/" + url; 206 | } else { 207 | let protocol = window.location.protocol; 208 | let domain = window.location.hostname; 209 | let port = window.location.port; 210 | return `${protocol}//${domain}${port ? (":" + port) : ""}` + url 211 | } 212 | } 213 | search = (q) => { 214 | if (!q) { 215 | q = "2022"; 216 | } 217 | return this.axios({ 218 | method: "get", 219 | url: "/file/search?q=" + q, 220 | headers: { 221 | 'Content-Type': 'application/json' 222 | } 223 | }); 224 | } 225 | searchMovie = (q) => { 226 | if (!q) { 227 | q = "2022"; 228 | } 229 | return this.axios({ 230 | method: "get", 231 | url: "/file/movie?q=" + q, 232 | headers: { 233 | 'Content-Type': 'application/json' 234 | } 235 | }); 236 | } 237 | searchTv = (q) => { 238 | if (!q) { 239 | q = "2022"; 240 | } 241 | return this.axios({ 242 | method: "get", 243 | url: "/file/tvshow?q=" + q, 244 | headers: { 245 | 'Content-Type': 'application/json' 246 | } 247 | }); 248 | } 249 | searchGames = (q) => { 250 | return this.axios({ 251 | method: "get", 252 | url: "/file/games/fitgirl/?q=" + q, 253 | headers: { 254 | 'Content-Type': 'application/json' 255 | } 256 | }); 257 | } 258 | 259 | 260 | getIndexer = () => { 261 | return this.axios({ 262 | method: "get", 263 | url: "/indexer", 264 | headers: { 265 | 'Content-Type': 'application/json' 266 | } 267 | }); 268 | } 269 | getCategory = () => { 270 | return this.axios({ 271 | method: "get", 272 | url: "/category", 273 | headers: { 274 | 'Content-Type': 'application/json' 275 | } 276 | }); 277 | } 278 | 279 | addCategory = (data) => { 280 | return this.axios({ 281 | method: "post", 282 | url: "/category", 283 | headers: { 284 | 'Content-Type': 'application/json' 285 | }, 286 | data 287 | }); 288 | } 289 | 290 | restoreCategory = () => { 291 | return this.axios({ 292 | method: "patch", 293 | url: "/category", 294 | headers: { 295 | 'Content-Type': 'application/json' 296 | } 297 | }); 298 | } 299 | 300 | editCategory = (id, data) => { 301 | return this.axios({ 302 | method: "PUT", 303 | url: "/category/" + id, 304 | headers: { 305 | 'Content-Type': 'application/json' 306 | }, 307 | data 308 | }); 309 | } 310 | 311 | deleteCategory = (id) => { 312 | return this.axios({ 313 | method: "DELETE", 314 | url: "/category/" + id, 315 | headers: { 316 | 'Content-Type': 'application/json' 317 | } 318 | }); 319 | } 320 | 321 | searchIndexer = (type, q = " ") => { 322 | return this.axios({ 323 | method: "get", 324 | url: "/indexer/" + type + "?q=" + q, 325 | headers: { 326 | 'Content-Type': 'application/json' 327 | } 328 | }); 329 | } 330 | } 331 | 332 | -------------------------------------------------------------------------------- /website/crawfish-official/src/library/components/AddTorrent.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { 3 | Button, 4 | Chip, 5 | Dialog, 6 | DialogActions, 7 | DialogContent, 8 | DialogContentText, 9 | DialogTitle, 10 | Stack, 11 | TextField, 12 | Typography 13 | } from "@mui/material"; 14 | import {Save} from "@mui/icons-material"; 15 | 16 | class AddTorrent extends Component { 17 | state = { 18 | path: this.props.defaultPath 19 | } 20 | 21 | render() { 22 | let {open, onSubmit, onClose} = this.props; 23 | let {path, magnet, files} = this.state; 24 | return ( 25 | { 26 | onClose() 27 | }}> 28 | Add a torrent 29 | 30 | 31 | Here you can add a magnet address, or upload a file 32 | 33 | 34 | 35 | Magnet 36 | 37 | {files ? { 39 | this.setState({files: null}) 40 | }} 41 | /> 42 | : 43 | { 49 | this.setState({magnet: e.target.value}) 50 | }} 51 | // InputProps={{ 52 | // endAdornment: ( 53 | // 54 | // 56 | // 57 | // { 62 | // this.setState({files: e.target.files, magnet: null}) 63 | // }} 64 | // /> 65 | // 66 | // 67 | // ) 68 | // }} 69 | />} 70 |
71 | 72 |
73 |
74 | 75 | 78 | 88 | 89 |
90 | ); 91 | } 92 | } 93 | 94 | AddTorrent.defaultProps = { 95 | open: false, 96 | onSubmit: () => { 97 | }, 98 | onClose: () => { 99 | }, 100 | defaultPath: null 101 | } 102 | export default AddTorrent; 103 | -------------------------------------------------------------------------------- /website/crawfish-official/src/library/components/FileElement.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {IconButton, ListItemButton, Stack, Tooltip, Typography} from "@mui/material"; 3 | import {OpenInNew, PauseCircle, PlayCircleOutline} from "@mui/icons-material"; 4 | import {LinearProgressWithLabel} from "./LinearProgressWithLabel"; 5 | import {humanFileSize} from "../utils"; 6 | 7 | function FileElement(props) { 8 | let {client, file, remote, torrentMagnet, torrent} = props; 9 | if (!remote) { 10 | remote = false; 11 | } 12 | let onClick = () => { 13 | if (file.streamable && remote) { 14 | window.open("https://tndsite.gitlab.io/quix-player/?magnet=" + torrentMagnet, "_blank") 15 | // window.open("https://btorrent.xyz/view#" + torrentMagnet, "_blank"); 16 | } else if (remote) { 17 | let a = document.createElement("a"); 18 | a.href = client.fileStreamLink(file.id, file.name, remote); 19 | a.download = file.name; 20 | a.click(); 21 | } else { 22 | client.fileOpen(file.id); 23 | } 24 | } 25 | let color = "primary"; 26 | 27 | if (file.paused) { 28 | color = "warning"; 29 | } else if (file.progress === 1) { 30 | color = "success"; 31 | } 32 | return ( 33 | 35 | 38 | {/**/} 42 | 43 | {/**/} 44 | {file.name} 45 | 46 | 47 | {humanFileSize(file.length)} 48 | 49 | 51 | {!torrent.paused && { 52 | if (file.paused) { 53 | client.selectTorrent({ 54 | magnet: torrentMagnet, 55 | fileName: file.name 56 | }) 57 | } else { 58 | client.deselectTorrent({ 59 | magnet: torrentMagnet, 60 | fileName: file.name 61 | }) 62 | } 63 | }}> 64 | {file.paused ? : 65 | } 66 | } 67 | 68 | 69 | 70 | ) 71 | ; 72 | } 73 | 74 | export default FileElement; 75 | -------------------------------------------------------------------------------- /website/crawfish-official/src/library/components/FilesTable.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import {Alert, IconButton, LinearProgress, Snackbar, Stack, Tooltip, Typography} from "@mui/material"; 3 | import {Attachment, CloudDownload, Download, Upload} from "@mui/icons-material"; 4 | import {humanFileSize} from "../utils"; 5 | 6 | 7 | class FilesTable extends Component { 8 | state = { 9 | loading: true, 10 | files: [], 11 | snackbar: false, 12 | snackbarMessage: "Adding torrent..." 13 | } 14 | 15 | 16 | componentDidMount() { 17 | let search = this.props 18 | this.refreshStatus(search); 19 | } 20 | 21 | componentDidUpdate(prevProps, prevState, snapshot) { 22 | if (prevProps.search !== this.props.search) { 23 | if (this.timeoutId) { 24 | clearTimeout(this.timeoutId) 25 | } 26 | this.timeoutId = setTimeout(this.refreshStatus, 1000); 27 | } 28 | } 29 | 30 | refreshStatus = async () => { 31 | try { 32 | let {client, search, torrents, category, searchApi} = this.props 33 | this.setState({loading: true}) 34 | let elaboratedSearch = [] 35 | if (!search || search === " " || search === "") { 36 | elaboratedSearch.push(category.defaultSearch) 37 | } else { 38 | elaboratedSearch.push(search) 39 | } 40 | if (category && category.tag) { 41 | elaboratedSearch.push(...category.tag.split(",")) 42 | } 43 | let res = await searchApi(category.type, elaboratedSearch.join(" ")); 44 | this.setState({ 45 | files: res.data.map((file, index) => { 46 | let disabled = torrents.some(t => { 47 | return t.name.includes(file.title) 48 | }) 49 | let color = "success"; 50 | if(!file.seeders || file.seeders < 3){ 51 | color = "error"; 52 | } 53 | 54 | return ( 66 | 74 | {file.name} 75 | {(file.seeders || file.peers) && 81 | 82 | Seed: {file.seeders} 83 | 84 | Leech: {file.peers} 85 | } 86 | 92 | 93 | {humanFileSize(file.size)} 94 | {file.repackSize && repacked {humanFileSize(file.repackSize)}} 96 | 97 | 98 | 99 | { 103 | this.setState({ 104 | snackbar: true, 105 | snackbatMessage: "Adding torrent..." 106 | }, () => { 107 | client.addTorrent({magnet: file.magnet, path: category.path}) 108 | .then((res) => { 109 | this.setState({ 110 | snackbar: true, 111 | snackbarMessage: "Added torrent " + file.name 112 | }, () => { 113 | setTimeout(() => { 114 | this.setState(p => { 115 | return { 116 | snackbar: false, 117 | snackbarMessage: "Adding torrent..." 118 | } 119 | }) 120 | }, 2000) 121 | }) 122 | }) 123 | .catch((e) => { 124 | this.setState({ 125 | snackbar: true, 126 | snackbarMessage: "Error adding torrent: " + e.message 127 | }, () => { 128 | setTimeout(() => { 129 | this.setState(p => { 130 | return { 131 | snackbar: false, 132 | snackbarMessage: "Adding torrent..." 133 | } 134 | }) 135 | }, 2000) 136 | }) 137 | }) 138 | }) 139 | }} 140 | > 141 | 142 | 143 | 144 | ) 145 | }), loading: false 146 | }) 147 | } catch (e) { 148 | console.error(e) 149 | } finally { 150 | if (this.state.loading) { 151 | this.setState({loading: false}) 152 | } 153 | } 154 | 155 | } 156 | 157 | render() { 158 | let {files, loading, snackbar, snackbarMessage} = this.state 159 | return 161 | { 165 | this.setState({snackbar: false}) 166 | }} 167 | key={"snackabr"} 168 | autoHideDuration={null} 169 | > 170 | 171 | {snackbarMessage} 172 | 173 | 174 | {loading && } 175 | {!loading && files} 176 | ; 177 | } 178 | } 179 | 180 | FilesTable.defaultProps = { 181 | navigateBack: () => { 182 | console.log("NOT IMPLEMENTED navigateBack") 183 | }, 184 | torrents: [], 185 | client: {}, 186 | search: null 187 | }; 188 | 189 | export default FilesTable 190 | -------------------------------------------------------------------------------- /website/crawfish-official/src/library/components/LinearProgressWithLabel.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import LinearProgress from '@mui/material/LinearProgress'; 3 | import Typography from '@mui/material/Typography'; 4 | import Box from '@mui/material/Box'; 5 | 6 | export function LinearProgressWithLabel(props) { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | {`${Math.round( 14 | props.value, 15 | )}%`} 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /website/crawfish-official/src/library/components/Menu.js: -------------------------------------------------------------------------------- 1 | import { 2 | Divider, 3 | List, 4 | ListItem, 5 | ListItemAvatar, 6 | ListItemButton, 7 | ListItemText, 8 | Stack, 9 | Switch, 10 | Tooltip 11 | } from "@mui/material"; 12 | import { 13 | DarkMode, 14 | Download, 15 | Home, 16 | LibraryMusic, 17 | ManageSearch, 18 | Movie, 19 | Settings, 20 | Tv, 21 | Upload, 22 | VideogameAsset 23 | } from "@mui/icons-material"; 24 | import React from "react"; 25 | import {CLIENT, CLIENT_DOWNLOAD, CLIENT_SEEDING, GAMES, MOVIES, MUSIC, SETTINGS, TVSHOW} from "../types"; 26 | 27 | 28 | Menu.defaultProps = { 29 | defaultMenu: [] 30 | } 31 | 32 | export function Menu(props) { 33 | let {logo, defaultMenu, changeView, enabledView, category} = props; 34 | return 40 | 41 | 42 | {logo && {"logo"}/} 43 | 44 | 45 | 48 | 49 | { 52 | props.filterHome() 53 | changeView(CLIENT, null) 54 | }} 55 | > 56 | 57 | 58 | 59 | 60 | 61 | 64 | 65 | { 68 | props.filterDownload() 69 | changeView(CLIENT_DOWNLOAD, null) 70 | }} 71 | 72 | > 73 | 74 | 75 | 76 | 77 | 78 | 81 | 82 | { 85 | props.filterSeeding() 86 | changeView(CLIENT_SEEDING, null) 87 | }} 88 | 89 | > 90 | 91 | 92 | 93 | 94 | 95 | 98 | 99 | 100 | 101 | 102 | 103 | 106 | 107 | {defaultMenu.map((x, index) => { 108 | let {tooltip, type, label, id} = x 109 | let icon; 110 | switch (type) { 111 | case MOVIES: 112 | icon = ; 113 | break; 114 | case GAMES: 115 | icon = ; 116 | break; 117 | case TVSHOW: 118 | icon = ; 119 | break; 120 | case MUSIC: 121 | icon = ; 122 | break; 123 | default: 124 | icon = 125 | break; 126 | } 127 | return 129 | { 131 | changeView(type, id) 132 | }} 133 | selected={category && category.id === id} 134 | > 135 | 136 | 137 | {icon} 138 | 139 | 140 | 143 | 144 | 145 | })} 146 | 147 | 148 | 149 | 150 | 151 | 154 | 155 | { 158 | changeView(SETTINGS) 159 | }}> 160 | 161 | 162 | 163 | 164 | 165 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 178 | 179 | 180 | 181 | 182 | ; 183 | } 184 | -------------------------------------------------------------------------------- /website/crawfish-official/src/library/components/SettingsPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Box, Stack, Tab, Tabs, Typography} from "@mui/material"; 3 | import {Lock} from "@mui/icons-material"; 4 | import General from "./SettingsSections/General"; 5 | import Category from "./SettingsSections/Category"; 6 | 7 | function TabPanel(props) { 8 | const {children, value, index, ...other} = props; 9 | 10 | return ( 11 | 24 | ); 25 | } 26 | 27 | function a11yProps(index) { 28 | return { 29 | id: `tab-${index}`, 30 | 'aria-controls': `tabpanel-${index}`, 31 | }; 32 | } 33 | 34 | export function SettingsPage(props) { 35 | 36 | const [value, setValue] = React.useState(0); 37 | 38 | const handleChange = (event, newValue) => { 39 | setValue(newValue); 40 | }; 41 | 42 | 43 | return 45 | 47 | 48 | 49 | } label="Indexer" {...a11yProps(2)} /> 50 | 51 | 52 | 55 | 56 | 57 | 60 | 61 | 62 | Item Three 63 | 64 | 65 | } 66 | ; 67 | -------------------------------------------------------------------------------- /website/crawfish-official/src/library/components/SettingsSections/Category.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import { 3 | Accordion, 4 | AccordionDetails, 5 | AccordionSummary, 6 | Button, 7 | Collapse, 8 | Grid, 9 | IconButton, 10 | LinearProgress, 11 | MenuItem, 12 | Select, 13 | Stack, 14 | TextField 15 | } from "@mui/material"; 16 | import {Add, Cancel, DeleteForever, ExpandMore, ModeEdit, Save, SettingsBackupRestore} from "@mui/icons-material"; 17 | import Typography from "@mui/material/Typography"; 18 | import ConfirmationButton from "../SupportComponent/ConfirmationButton"; 19 | 20 | function TextIcon(props) { 21 | return ( 22 | {props.icon} 23 | 24 | // 25 | // {/**/} 28 | // 29 | ) 30 | 31 | } 32 | 33 | 34 | function CategoryItem(props) { 35 | let [category, setCategory] = useState(props.category); 36 | let [edit, setEdit] = useState(false); 37 | return ( 38 | 39 | } 41 | aria-controls="panel1a-content" 42 | id="panel1a-header" 43 | > 44 | {category.label} 45 | {category.type} 46 | 47 | 48 | 49 | 50 | 51 | Path: 52 | setCategory({...category, path: e.target.value})} 60 | /> 61 | 62 | 63 | Menu label: 64 | setCategory({...category, label: e.target.value})} 72 | /> 73 | 74 | 75 | Tag (included in the search): 76 | setCategory({...category, tag: e.target.value})} 84 | /> 85 | 86 | 87 | Default search: 88 | setCategory({...category, defaultSearch: e.target.value})} 95 | /> 96 | 97 | 98 | Indexer: 99 | 107 | 108 | 109 | 110 | {!edit && { 111 | setEdit(true) 112 | } 113 | } color={"primary"}/>} text={"Edit"}/>} 114 | {!edit && { 115 | if (category.id) { 116 | await props.client.deleteCategory(category.id) 117 | } 118 | props.refreshCategory(); 119 | }} icon={} text={"Delete"}/>} 120 | {edit && { 121 | if (category.id) { 122 | await props.client.editCategory(category.id, category) 123 | } else { 124 | await props.client.addCategory(category) 125 | } 126 | setEdit(!edit) 127 | props.refreshCategory(); 128 | }} icon={} text={"Save"}/>} 129 | {edit && { 130 | setEdit(!edit); 131 | setCategory(props.category) 132 | }} icon={} text={"Cancel"}/>} 133 | 134 | 135 | 136 | 137 | ); 138 | } 139 | 140 | function Category(props) { 141 | let [loading, setLoading] = useState(true); 142 | let [categories, setCategories] = useState([]); 143 | let [indexers, setIndexers] = useState([]); 144 | const refreshStatus = async () => { 145 | setLoading(true) 146 | try { 147 | let {client} = props 148 | let res = await client.getCategory(); 149 | setCategories(res.data) 150 | await props.refreshCategory(); 151 | } catch (e) { 152 | console.error(e) 153 | } 154 | try { 155 | let {client} = props 156 | let res = await client.getIndexer(); 157 | setIndexers(res.data) 158 | } catch (e) { 159 | console.error(e) 160 | } 161 | setLoading(false) 162 | } 163 | useEffect(() => { 164 | refreshStatus().then(() => { 165 | setLoading(false) 166 | }) 167 | // eslint-disable-next-line react-hooks/exhaustive-deps 168 | }, []) 169 | if (loading) { 170 | return 171 | } 172 | return ( 178 | 194 | {categories.map((category, index) => ( 195 | 196 | 198 | 199 | )).reverse()} 200 | { 202 | setLoading(true) 203 | try { 204 | await props.client.restoreCategory() 205 | await refreshStatus() 206 | } catch (e) { 207 | console.error("Error creating category") 208 | } finally { 209 | setLoading(false) 210 | } 211 | }} 212 | dialogContent={"Clicking 'Ok' all the categories will be restored to default"} 213 | startIcon={} variant={"outlined"} fullWidth>Restore default 214 | ); 215 | } 216 | 217 | export default Category; 218 | -------------------------------------------------------------------------------- /website/crawfish-official/src/library/components/SettingsSections/General.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import { 3 | Button, 4 | Checkbox, 5 | FormControlLabel, 6 | Grid, 7 | LinearProgress, 8 | Stack, 9 | Switch, 10 | TextField, 11 | Typography 12 | } from "@mui/material"; 13 | import {humanFileSize} from "../../utils"; 14 | import {Save} from "@mui/icons-material"; 15 | import {observer} from "mobx-react"; 16 | import {useAppStore} from "../../../MobxContext/AppContext"; 17 | 18 | function General(props) { 19 | let store = useAppStore(); 20 | let [loading, setLoading] = useState(true); 21 | let [configuration, setConfiguration] = useState({}); 22 | let [defaultConfiguration, setDefaultConfiguration] = useState({}); 23 | 24 | 25 | const refreshStatus = async (res) => { 26 | let {conf} = store; 27 | let data; 28 | if (!res || !res.data) { 29 | data = conf.get 30 | } else { 31 | data = res.data 32 | } 33 | data = { 34 | ...data, 35 | alternativeTimeStart: data.alternativeTimeStart ? data.alternativeTimeStart.slice(0, 5) : null, 36 | alternativeTimeEnd: data.alternativeTimeEnd ? data.alternativeTimeEnd.slice(0, 5) : null, 37 | } 38 | setConfiguration(data) 39 | setDefaultConfiguration(data) 40 | } 41 | useEffect(() => { 42 | refreshStatus().then(() => { 43 | setLoading(false) 44 | }) 45 | // eslint-disable-next-line react-hooks/exhaustive-deps 46 | }, []) 47 | 48 | let { 49 | downloadPath, 50 | download, 51 | upload, 52 | alternativeTimeStart, 53 | alternativeTimeEnd, 54 | alternativeDownload, 55 | alternativeUpload, 56 | } = configuration 57 | if (loading) { 58 | return 59 | } 60 | return ( 66 | 67 | 68 | Download path 69 | setConfiguration({...configuration, downloadPath: e.target.value})} 76 | /> 77 | 78 | 79 | Download speed (Kb/s) 80 | 0 ? download / 1000 : download} 85 | helperText={ { 87 | if (e.target.checked) { 88 | setConfiguration({...configuration, download: -1}) 89 | } else { 90 | setConfiguration({...configuration, download: 8000000}) 91 | } 92 | }} 93 | checked={download === -1} 94 | />} 95 | label={"Check for unlimited (" + humanFileSize(download, true) + "/s)"} 96 | labelPlacement="end" 97 | />} 98 | // helperText={download == -1 ? "unlimited" : humanFileSize(download) + "/s"} 99 | onChange={(e) => { 100 | setConfiguration({...configuration, download: Math.max(-1, e.target.value * 1000)}) 101 | }} 102 | 103 | /> 104 | 105 | 106 | Upload speed (Bytes/s -1 means unlimited) 107 | 0 ? upload / 1000 : upload} 112 | helperText={ { 114 | if (e.target.checked) { 115 | setConfiguration({...configuration, upload: -1}) 116 | } else { 117 | setConfiguration({...configuration, upload: 8000000}) 118 | } 119 | }} 120 | checked={upload === -1} 121 | />} 122 | label={"Check for unlimited (" + humanFileSize(upload, true) + "/s)"} 123 | labelPlacement="end" 124 | />} 125 | onChange={(e) => setConfiguration({ 126 | ...configuration, upload: Math.max(-1, e.target.value * 1000) 127 | })} 128 | /> 129 | 130 | 131 | Alternative speed limit 132 | { 136 | if (e.target.checked) { 137 | let tempStart = new Date(); 138 | tempStart.setMinutes(0); 139 | let tempEnd = new Date(); 140 | tempEnd.setMinutes(0); 141 | tempEnd.setHours(Math.min(tempStart.getHours() + 1, 23)); 142 | setConfiguration({ 143 | ...configuration, 144 | alternativeTimeStart: tempStart.toLocaleTimeString().slice(0, 5), 145 | alternativeTimeEnd: tempEnd.toLocaleTimeString().slice(0, 5), 146 | alternativeUpload: upload, 147 | alternativeDownload: download 148 | }) 149 | } else { 150 | setConfiguration({ 151 | ...configuration, 152 | alternativeTimeStart: null, 153 | alternativeTimeEnd: null, 154 | alternativeUpload: null, 155 | alternativeDownload: null 156 | }) 157 | } 158 | }}/>} 159 | label="Enabled" 160 | /> 161 | 162 | {!!alternativeTimeStart && <> 163 | 164 | Alternative start time 165 | setConfiguration({ 171 | ...configuration, alternativeTimeStart: e.target.value 172 | })} 173 | /> 174 | 175 | 176 | Alternative end time 177 | setConfiguration({ 183 | ...configuration, alternativeTimeEnd: e.target.value 184 | })} 185 | /> 186 | 187 | 188 | Alternative download speed (Kb/s) 189 | 0 ? alternativeDownload / 1000 : alternativeDownload} 194 | helperText={ { 196 | if (e.target.checked) { 197 | setConfiguration({...configuration, alternativeDownload: -1}) 198 | } else { 199 | setConfiguration({...configuration, alternativeDownload: 8000000}) 200 | } 201 | }} 202 | checked={alternativeDownload === -1} 203 | />} 204 | label={"Check for unlimited (" + humanFileSize(alternativeDownload, true) + "/s)"} 205 | labelPlacement="end" 206 | />} 207 | onChange={(e) => { 208 | setConfiguration({ 209 | ...configuration, 210 | alternativeDownload: Math.max(-1, e.target.value * 1000) 211 | }) 212 | }} 213 | 214 | /> 215 | 216 | 217 | Alternative upload speed (Bytes/s -1 means 218 | unlimited) 219 | 0 ? alternativeUpload / 1000 : alternativeUpload} 224 | helperText={ { 226 | if (e.target.checked) { 227 | setConfiguration({...configuration, upload: -1}) 228 | } else { 229 | setConfiguration({...configuration, upload: 8000000}) 230 | } 231 | }} 232 | checked={alternativeUpload === -1} 233 | />} 234 | label={"Check for unlimited (" + humanFileSize(alternativeUpload, true) + "/s)"} 235 | labelPlacement="end" 236 | />} 237 | onChange={(e) => setConfiguration({ 238 | ...configuration, alternativeUpload: Math.max(-1, e.target.value * 1000) 239 | })} 240 | /> 241 | 242 | } 243 | 244 | 245 | 251 | {/**/} 252 | 253 | ); 254 | } 255 | 256 | 257 | // function SpeedButton(props) { 258 | // let {ws, registerFunction} = props.client; 259 | // useEffect(() => { 260 | // registerFunction("CONF", (data) => { 261 | // console.log("Data received: ", data) 262 | // console.timeEnd() 263 | // }) 264 | // // eslint-disable-next-line react-hooks/exhaustive-deps 265 | // }, []) 266 | // 267 | // return ( 268 | // 272 | // ); 273 | // } 274 | 275 | 276 | export default observer(General); 277 | -------------------------------------------------------------------------------- /website/crawfish-official/src/library/components/SpeedMeter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Download, Upload} from "@mui/icons-material"; 3 | import {humanFileSize} from "../utils"; 4 | import {Tooltip, Typography} from "@mui/material"; 5 | import {observer} from "mobx-react"; 6 | 7 | function SpeedMeter(props) { 8 | let {store} = props; 9 | let {conf} = store; 10 | 11 | let { 12 | actualDownload, 13 | actualUpload, 14 | downloadSpeed, 15 | uploadSpeed 16 | } = conf.get 17 | 18 | let colorDownload; 19 | let tooltipDownload; 20 | 21 | if (downloadSpeed < 0) { 22 | colorDownload = "success" 23 | tooltipDownload = "Speed not limited! (You can limit from settings page)"; 24 | } else if (actualDownload >= (downloadSpeed * 0.90)) { 25 | colorDownload = "error" 26 | tooltipDownload = "Speed at his limit! (Raise the limit from settings page)"; 27 | } else if (actualDownload >= (downloadSpeed * 0.75)) { 28 | colorDownload = "warning" 29 | tooltipDownload = "Speed near to the limit! (Raise the limit from settings page)"; 30 | } else { 31 | colorDownload = "success" 32 | tooltipDownload = "Speed not at his limit, everything is fine"; 33 | } 34 | let colorUpload; 35 | let tooltipUpload; 36 | if (uploadSpeed < 0) { 37 | colorUpload = "success" 38 | tooltipUpload = "Unlimited speed, thanks! (You can limit from settings page, but remember sharing is caring!)"; 39 | } else if (actualUpload >= (uploadSpeed * 0.90)) { 40 | colorUpload = "error" 41 | tooltipUpload = "Speed at his limit! (Raise the limit from settings page)"; 42 | } else if (actualUpload >= (uploadSpeed * 0.75)) { 43 | colorUpload = "warning" 44 | tooltipUpload = "Speed near to the limit! (Raise the limit from settings page)"; 45 | } else { 46 | colorUpload = "success" 47 | tooltipUpload = "Speed not at his limit, thanks for sharing bandwidth!"; 48 | } 49 | 50 | return ( 51 | 55 | 56 | 59 | 60 | {humanFileSize(actualDownload) + "/s"} {humanFileSize(actualUpload) + "/s"} 61 | 62 | 65 | 66 | 67 | ); 68 | } 69 | 70 | export default observer(SpeedMeter); 71 | -------------------------------------------------------------------------------- /website/crawfish-official/src/library/components/SupportComponent/ConfirmationButton.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import Dialog from '@mui/material/Dialog'; 4 | import DialogActions from '@mui/material/DialogActions'; 5 | import DialogContent from '@mui/material/DialogContent'; 6 | import DialogContentText from '@mui/material/DialogContentText'; 7 | import DialogTitle from '@mui/material/DialogTitle'; 8 | import Slide from '@mui/material/Slide'; 9 | 10 | const Transition = React.forwardRef(function Transition(props, ref) { 11 | return ; 12 | }); 13 | 14 | function ConfirmationButton(props) { 15 | const [open, setOpen] = React.useState(false); 16 | 17 | let {children, onClick, dialogTitle, dialogContent} = props; 18 | let buttonProps = { 19 | ...props, 20 | onClick: undefined, 21 | dialogContent: undefined, 22 | dialogTitle: undefined, 23 | children: undefined 24 | } 25 | const handleClickOpen = () => { 26 | setOpen(true); 27 | }; 28 | 29 | const handleClose = () => { 30 | setOpen(false); 31 | }; 32 | 33 | return ( 34 | <> 35 | 38 | 45 | {dialogTitle} 46 | {dialogContent && 47 | 48 | {dialogContent} 49 | 50 | } 51 | 52 | 53 | 59 | 60 | 61 | 62 | ); 63 | } 64 | 65 | 66 | ConfirmationButton.defaultProps = { 67 | onClick: () => { 68 | alert("Not implemented yet") 69 | }, 70 | children: "Not implemented", 71 | dialogTitle: "Are you sure?" 72 | }; 73 | 74 | export default ConfirmationButton; 75 | -------------------------------------------------------------------------------- /website/crawfish-official/src/library/components/TorrentClientTable.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Box, 4 | Checkbox, 5 | Paper, 6 | Table, 7 | TableBody, 8 | TableCell, 9 | TableContainer, 10 | TableHead, 11 | TableRow, 12 | TableSortLabel, 13 | Typography 14 | } from "@mui/material"; 15 | import {visuallyHidden} from '@mui/utils'; 16 | 17 | 18 | const headCells = [ 19 | { 20 | id: 'name', 21 | label: 'Name', 22 | align: 'left', 23 | sortable:true 24 | }, 25 | { 26 | id: 'progress', 27 | label: 'Progress', 28 | sortable:true 29 | }, { 30 | id: 'timeRemaining', 31 | label: 'Time left', 32 | align: 'align', 33 | sortable: true 34 | }, { 35 | id: 'size', 36 | label: 'Size', 37 | align: 'left', 38 | sortable: true 39 | }, { 40 | id: 'actions', 41 | label: 'Actions', 42 | sortable: false 43 | } 44 | 45 | ]; 46 | 47 | function TorrentClientTable(props) { 48 | let {onClick, torrents, predicate, callbackfn, onRequestSort, orderBy, order} = props 49 | const createSortHandler = (property) => (event) => { 50 | onRequestSort(event, property); 51 | }; 52 | return 53 | 54 | 55 | 56 | 57 | 62 | 63 | {headCells.map((cell) => { 64 | if (cell.sortable) { 65 | return 66 | 71 | 72 | {cell.label} 73 | 74 | {orderBy === cell.id ? ( 75 | 76 | {order === 'desc' ? 'sorted descending' : 'sorted ascending'} 77 | 78 | ) : null} 79 | 80 | 81 | } else { 82 | return 83 | 84 | {cell.label} 85 | 86 | 87 | } 88 | })} 89 | 90 | 91 | 92 | {torrents && torrents.map(callbackfn)} 93 | 94 |
95 |
; 96 | } 97 | 98 | TorrentClientTable.defaultProps = { 99 | onClick: () => { 100 | }, 101 | torrents: [], 102 | predicate: () => { 103 | 104 | }, 105 | callbackfn: () => { 106 | }, 107 | onRequestSort: () => { 108 | console.log("Sorting not implemented") 109 | } 110 | }; 111 | 112 | export default TorrentClientTable 113 | -------------------------------------------------------------------------------- /website/crawfish-official/src/library/components/TorrentTableRow.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import { 3 | Checkbox, 4 | Collapse, 5 | IconButton, 6 | List, 7 | ListItemAvatar, 8 | ListItemButton, 9 | ListItemText, 10 | Stack, 11 | TableCell, 12 | TableRow, 13 | Tooltip, 14 | Typography 15 | } from "@mui/material"; 16 | import { 17 | ContentCopy, 18 | Delete, 19 | DeleteForever, 20 | DownloadForOffline, 21 | FolderOpen, 22 | KeyboardArrowDown, KeyboardArrowUp, 23 | Link, 24 | LiveTvOutlined, 25 | OndemandVideo, 26 | PauseCircle, 27 | PlayCircleOutline 28 | } from "@mui/icons-material"; 29 | import {copyToClipboard, humanFileSize, toTime} from "./../utils"; 30 | import {LinearProgressWithLabel} from "./../components/LinearProgressWithLabel"; 31 | import FileElement from "./../components/FileElement"; 32 | 33 | 34 | function TorrentTableRow(props) { 35 | let {openSnackbar, remote, client, torrent, index, isRowSelected, onChangeRowSelection} = props; 36 | 37 | const [rowOpened, setRowOpened] = useState(false) 38 | 39 | let videoFiles = []; 40 | torrent.files.forEach(f => { 41 | if (f.mime && f.mime.includes("video")) { 42 | videoFiles.push(f) 43 | } 44 | }) 45 | let state; 46 | let color = "primary"; 47 | if (torrent.paused) { 48 | state = "paused"; 49 | color = "warning"; 50 | } else if (torrent.progress === 1) { 51 | state = "completed"; 52 | color = "success"; 53 | } else if (torrent.timeRemaining > 0) { 54 | state = toTime(torrent.timeRemaining) 55 | } else { 56 | state = "--:--" 57 | } 58 | return (<> 59 | 60 | 61 | onChangeRowSelection(torrent.infoHash)} 65 | /> 66 | 67 | 68 | {torrent.name} 69 | 70 | 71 | 73 | 74 | 75 | 76 | {state} 77 | 78 | 79 | 80 | 81 | {humanFileSize(torrent.size)} 82 | 83 | 84 | 85 | {videoFiles.length > 0 && 86 | 90 | {videoFiles.map((file, index) => { 91 | return { 93 | if (remote) { 94 | let a = document.createElement("a"); 95 | a.href = client.fileStreamLink(file.id, file.name, remote); 96 | a.download = file.name; 97 | a.click(); 98 | } else { 99 | client.fileOpen(file.id); 100 | } 101 | } 102 | } 103 | > 104 | 105 | 107 | {`${Math.round( 108 | file.progress * 100, 109 | )}%`} 110 | 111 | 112 | 113 | 116 | 117 | })} 118 | }> 119 | { 120 | if (videoFiles.length === 1) { 121 | let file = videoFiles[0] 122 | if (remote) { 123 | let a = document.createElement("a"); 124 | a.href = client.fileStreamLink(file.id, file.name, remote); 125 | a.download = file.name; 126 | a.click(); 127 | } else { 128 | client.fileOpen(file.id); 129 | } 130 | } 131 | }}> 132 | 133 | 134 | } 135 | {!remote && 136 | { 137 | client.folderOpen(torrent.infoHash); 138 | }}> 139 | 140 | 141 | } 142 | 143 | { 144 | let a = document.createElement("a"); 145 | a.href = client.getTorrentFile(torrent.infoHash, torrent.name + ".torrent", remote); 146 | a.download = torrent.name + ".torrent"; 147 | a.click(); 148 | }}> 149 | 150 | 151 | 152 | 153 | { 154 | copyToClipboard("https://tndsite.gitlab.io/quix-player/?magnet=" + torrent.infoHash, openSnackbar) 155 | }}> 156 | 157 | 158 | 159 | 160 | { 161 | if (torrent.paused) { 162 | client.addTorrent({magnet: torrent.magnet, path: torrent.path}) 163 | } else { 164 | client.pauseTorrent({magnet: torrent.magnet}) 165 | } 166 | }}> 167 | {torrent.paused ? : 168 | } 169 | 170 | 171 | 172 | { 173 | client.removeTorrent({magnet: torrent.magnet}) 174 | }}> 175 | 176 | 177 | 178 | 179 | { 180 | client.destroyTorrent({magnet: torrent.magnet}) 181 | }}> 182 | 183 | 184 | 185 | 186 | { 187 | copyToClipboard(torrent.magnet, openSnackbar) 188 | }}> 189 | 190 | 191 | 192 | 193 | { 194 | setRowOpened(!rowOpened); 195 | }}> 196 | {rowOpened ? : } 197 | 198 | 199 | 200 | 201 | 202 | 205 | 206 | {torrent.files.map(f => { 207 | return 214 | })} 215 | 216 | 217 | 218 | ); 219 | } 220 | 221 | 222 | export default TorrentTableRow; 223 | -------------------------------------------------------------------------------- /website/crawfish-official/src/library/types.js: -------------------------------------------------------------------------------- 1 | export const GAMES = ("GAMES") 2 | export const MUSIC = ("MUSIC") 3 | export const MOVIES = ("MOVIES") 4 | export const TVSHOW = ("TVSHOW") 5 | export const CLIENT = ("CLIENT") 6 | export const CLIENT_DOWNLOAD = ("CLIENT_DOWNLOAD") 7 | export const CLIENT_SEEDING = ("CLIENT_SEEDING") 8 | export const SETTINGS = ("SETTINGS") 9 | -------------------------------------------------------------------------------- /website/crawfish-official/src/library/utils.js: -------------------------------------------------------------------------------- 1 | export function humanFileSize(bytes, si = false, dp = 1) { 2 | if (isNaN(bytes)) { 3 | return bytes; 4 | } 5 | const thresh = si ? 1000 : 1024; 6 | 7 | if (Math.abs(bytes) < thresh) { 8 | return bytes + ' B'; 9 | } 10 | 11 | const units = si 12 | ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 13 | : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; 14 | let u = -1; 15 | const r = 10 ** dp; 16 | 17 | do { 18 | bytes /= thresh; 19 | ++u; 20 | } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1); 21 | 22 | 23 | return round(bytes.toFixed(dp)) + ' ' + units[u]; 24 | } 25 | 26 | export function round(input) { 27 | return Math.round(input * 100) / 100 28 | } 29 | 30 | export function toTime(input) { 31 | let date = new Date(0); 32 | date.setSeconds(input / 1000); // specify value for SECONDS here 33 | return date.toISOString().substr(11, 8); 34 | 35 | } 36 | 37 | export const getDevice = () => { 38 | return /Android|webOS|iPhone|iPad|iPod|BlackBerry|BB|PlayBook|IEMobile|Windows Phone|Kindle|Silk|Opera Mini/i.test(navigator.userAgent) 39 | } 40 | 41 | export const copyToClipboard = (text, fb) => { 42 | if (getDevice()) { 43 | let dummy = document.createElement("textarea"); 44 | document.body.appendChild(dummy); 45 | dummy.value = text; 46 | dummy.select(); 47 | document.execCommand("copy"); 48 | document.body.removeChild(dummy); 49 | } else { 50 | navigator.clipboard.writeText(text) 51 | } 52 | if (fb) { 53 | fb() 54 | } 55 | 56 | } 57 | 58 | -------------------------------------------------------------------------------- /website/crawfish-official/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /website/crawfish-official/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({getCLS, getFID, getFCP, getLCP, getTTFB}) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /website/crawfish-official/src/screen/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Alert} from "@mui/material"; 3 | 4 | export default class ErrorBoundary extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = {hasError: false}; 8 | } 9 | 10 | static getDerivedStateFromError(error) { 11 | // Update state so the next render will show the fallback UI. 12 | return {hasError: true}; 13 | } 14 | 15 | componentDidCatch(error, errorInfo) { 16 | // You can also log the error to an error reporting service 17 | console.error("Error in boundary", error, errorInfo); 18 | } 19 | 20 | render() { 21 | if (this.state.hasError) { 22 | // You can render any custom fallback UI 23 | return Something get wrong, try to restart; 24 | } 25 | 26 | return this.props.children; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /website/crawfish-official/src/screen/TorrentManager.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {Container, CssBaseline, Stack, ThemeProvider, Typography} from "@mui/material"; 3 | import logo from "../asset/default-nomargin.svg" 4 | import WebTorrentGuiV2 from "../library/WebTorrentGuiV2"; 5 | import {observer} from "mobx-react"; 6 | import ErrorBoundary from "./ErrorBoundary"; 7 | 8 | 9 | function TorrentManager(props) { 10 | let {store} = props; 11 | let {theme} = store; 12 | const [path, setPath] = useState(null); 13 | useEffect(() => { 14 | (async () => { 15 | let port = process.env.REACT_APP_CUSTOM_API_PORT || new URLSearchParams(window.location.search).get("port") || window.location.port; 16 | let protocol = window.location.protocol; 17 | let domain = window.location.hostname; 18 | let path = `${protocol}//${domain}${port ? (":" + port) : ""}` + process.env.REACT_APP_BASE_PATH 19 | console.info("Setting at startup api path: ", path) 20 | setPath(path); 21 | })() 22 | }, []) 23 | console.debug("Using path: ", path) 24 | return ( 25 | 26 | 27 | 28 | 29 | 31 | {path ? : 34 | Something got wrong attach the request.log file and open an issue on github 35 | } 36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | 43 | export default observer(TorrentManager); 44 | -------------------------------------------------------------------------------- /website/crawfish-official/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /websocket/server.js: -------------------------------------------------------------------------------- 1 | const {WebSocketServer} = require('ws'); 2 | 3 | const wss = (server) => { 4 | return new WebSocketServer({ 5 | server: server, 6 | path: "/wss", 7 | perMessageDeflate: { 8 | zlibDeflateOptions: { 9 | // See zlib defaults. 10 | chunkSize: 1024, 11 | memLevel: 7, 12 | level: 3 13 | }, 14 | zlibInflateOptions: { 15 | chunkSize: 10 * 1024 16 | }, 17 | // Other options settable: 18 | clientNoContextTakeover: true, // Defaults to negotiated value. 19 | serverNoContextTakeover: true, // Defaults to negotiated value. 20 | serverMaxWindowBits: 10, // Defaults to negotiated value. 21 | // Below options specified as default values. 22 | concurrencyLimit: 10, // Limits zlib concurrency for perf. 23 | threshold: 1024 // Size (in bytes) below which messages 24 | // should not be compressed if context takeover is disabled. 25 | } 26 | }) 27 | }; 28 | 29 | 30 | module.exports = wss; 31 | -------------------------------------------------------------------------------- /websocket/wss-conf.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakonkat/Crawfish/ab1e85b5dcfac5a1ee6decf1e1bf161cae787be7/websocket/wss-conf.js --------------------------------------------------------------------------------