├── .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 |  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 |  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 |  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 |  46 | 47 | **Get Torrent File** 48 | 49 |  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 |  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 |  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 |  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 |  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 100 | - The name of the label that will appear in the **Explore** main menu 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) 103 | - Have the possibility to put a default research for that 104 | category  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  110 | 111 | **Theme** 112 | 113 |  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 |