From a45d800f8e33d9d595b358e62b3c6d476c846fa4 Mon Sep 17 00:00:00 2001 From: Raghu Saxena Date: Thu, 8 Sep 2022 20:21:47 +0800 Subject: [PATCH 1/8] 2.0.0 alpha 1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 30645e5..56a95ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "qbit-race", - "version": "1.0.0", + "version": "2.0.0-alpha.1", "description": "Qbit utilities for racing", "main": "./bin/index.js", "type": "module", From 71c85aabe93b571435bb4f2eed11fdfe1d7bb331 Mon Sep 17 00:00:00 2001 From: Raghu Saxena Date: Thu, 8 Sep 2022 20:24:55 +0800 Subject: [PATCH 2/8] npm ignore --- .npmignore | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .npmignore diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..0db1fd8 --- /dev/null +++ b/.npmignore @@ -0,0 +1,11 @@ +__mocks__/ +__tests__/ +.github/ +src/ +tests/ +.gitignore +.npmignore +sample.env +sample.settings.js +settings.d.ts +tsconfig.json \ No newline at end of file From 22390033d1998c1d497e9a0e1b4c39ac48df423d Mon Sep 17 00:00:00 2001 From: Raghu Saxena Date: Thu, 8 Sep 2022 20:36:21 +0800 Subject: [PATCH 3/8] Remove old stuffs :gem: :raised_hands: --- .npmignore | 6 +- build/add_torrent.js | 149 --------------------------- build/check_qbit.js | 49 --------- build/config.js | 9 +- build/discord/messages.js | 70 ------------- build/helpers/post_race_resume.js | 68 ------------ build/helpers/pre_race.js | 85 --------------- build/helpers/resume.js | 32 ------ package.json | 2 +- sample.settings.js | 17 --- settings.d.ts | 21 ---- src/add_torrent.ts | 166 ------------------------------ src/check_qbit.ts | 57 ---------- src/config.ts | 9 +- src/discord/messages.ts | 80 +------------- src/helpers/post_race_resume.ts | 77 -------------- src/helpers/pre_race.ts | 105 ------------------- src/helpers/resume.ts | 43 -------- src/utils/config.ts | 2 +- 19 files changed, 16 insertions(+), 1031 deletions(-) delete mode 100644 build/add_torrent.js delete mode 100644 build/check_qbit.js delete mode 100644 build/helpers/post_race_resume.js delete mode 100644 build/helpers/pre_race.js delete mode 100644 build/helpers/resume.js delete mode 100644 sample.settings.js delete mode 100644 settings.d.ts delete mode 100644 src/add_torrent.ts delete mode 100644 src/check_qbit.ts delete mode 100644 src/helpers/post_race_resume.ts delete mode 100644 src/helpers/pre_race.ts delete mode 100644 src/helpers/resume.ts diff --git a/.npmignore b/.npmignore index 0db1fd8..848c2b7 100644 --- a/.npmignore +++ b/.npmignore @@ -8,4 +8,8 @@ tests/ sample.env sample.settings.js settings.d.ts -tsconfig.json \ No newline at end of file +settings.js +.env +tsconfig.json +logs/ +node_modules/ \ No newline at end of file diff --git a/build/add_torrent.js b/build/add_torrent.js deleted file mode 100644 index 287a126..0000000 --- a/build/add_torrent.js +++ /dev/null @@ -1,149 +0,0 @@ -/** - * @deprecated - * - * TODO: Completely remove in v2 - */ -import { login } from './qbittorrent/auth.js'; -import { addTags, addTorrent, deleteTorrents, getTorrentInfo, getTorrents, getTrackers, reannounce } from './qbittorrent/api.js'; -import { sleep } from './helpers/utilities.js'; -import { logger } from './helpers/logger.js'; -import { preRaceCheck } from './helpers/pre_race.js'; -import { SETTINGS } from '../settings.js'; -import { sendMessage } from './discord/api.js'; -import { addMessage } from './discord/messages.js'; -import { resume } from './helpers/resume.js'; -import { getTorrentMetainfo } from './helpers/torrent.js'; -import * as fs from 'fs'; -export const add_torrent = async (args) => { - // Arguments: - // previous: [infohash] [torrentname] [tracker] [path] - // new: [path] - // const infohash = args[0].toLowerCase(); - // const torrentName = args[1]; - // const tracker = args[2]; - // const path = args[3]; - //TODO: Check if first param is infohash or path? Or no bw compatibility?... - // Assume none at first. - const path = args[0]; - let category = undefined; - // We need to read the torrent file and get the metainfo - let torrentFile; - let metainfo; - try { - torrentFile = fs.readFileSync(path); - metainfo = getTorrentMetainfo(torrentFile); - } - catch (e) { - logger.error(`Failed to read/parse torrent from file`); - throw new Error("TORRENT_READ_FAIL"); - } - //Check if `--category` is present. Then the next argument is the category to set. - for (let index = 1; index < args.length; index++) { - if (args[index] === '--category') { - //The next one should be the actual category - if (index + 1 < args.length) { - category = args[index + 1]; - } - else { - //They fucked up. Let them know, but dont error out (act as if not set). - logger.error('--category set but the category is missing.'); - break; - } - } - } - let t1 = Date.now(); - try { - await login(); - } - catch (error) { - logger.error(`Failed to login. Exiting...`); - process.exit(1); - } - let t2 = Date.now(); - logger.info(`Login completed in ${((t2 - t1) / 1000).toFixed(2)} seconds.`); - logger.info('Performing Pre Race check...'); - const okay = await preRaceCheck(); - if (okay === false) { - logger.info(`Conditions not met. Skipping ${metainfo.name}`); - process.exit(0); //it is a soft exit. - } - logger.info(`Adding torrent ${metainfo.name}`); - try { - await addTorrent(torrentFile, category); - } - catch (error) { - process.exit(1); - } - //Wait for torrent to register in Qbit, initial announce. - await sleep(5000); - //Now we get the torrent's trackers, which will let us set the tags. - let tags = []; - try { - let trackers = await getTrackers(metainfo.hash); - trackers.splice(0, 3); - tags = trackers.map(({ url }) => new URL(url).hostname); - logger.info(`Adding ${tags.length} tags.`); - await addTags([{ hash: metainfo.hash }], tags); - } - catch (error) { - logger.error(`Failed to add tags. Error code ${error}`); - } - //We also want to get the size of the torrent, for the notification. - let torrent; - try { - torrent = await getTorrentInfo(metainfo.hash); - } - catch (error) { - process.exit(1); - } - //Now we move anto reannounce - logger.info("Starting reannounce check..."); - let attempts = 0; - let announceOK = false; - while (attempts < SETTINGS.REANNOUNCE_LIMIT) { - logger.info(`Attempt #${attempts + 1}: Querying tracker status...`); - try { - let trackers = await getTrackers(metainfo.hash); - trackers.splice(0, 3); - let working = trackers.some(tracker => tracker.status === 2); - if (!working) { - //We need to reannounce - logger.info('Need to reannounce. Sending request and sleeping...'); - await reannounce(metainfo.hash); - await sleep(SETTINGS.REANNOUNCE_INTERVAL); - attempts++; - } - else { - announceOK = true; - logger.info('Tracker is OK. Exiting...'); - break; - } - } - catch (error) { - logger.error('Caught an error. Exiting...'); - process.exit(1); - } - } - //We got here but failed reannounce failed. Delete it. - if (announceOK === false) { - logger.info(`Did not get an OK from tracker even after ${SETTINGS.REANNOUNCE_LIMIT} attempts. Deleting...`); - await deleteTorrents([{ hash: metainfo.hash }]); - // Resume any that were paused - logger.info('Going to resume any paused torrents...'); - const torrents = await getTorrents(); - resume(torrents); - } - else { - //Send message to discord (if enabled) - const { enabled } = SETTINGS.DISCORD_NOTIFICATIONS || { enabled: false }; - if (enabled === true) { - try { - await sendMessage(addMessage(torrent.name, tags, torrent.size, attempts)); - } - catch (error) { - process.exit(1); - } - } - } -}; -//# sourceMappingURL=add_torrent.js.map \ No newline at end of file diff --git a/build/check_qbit.js b/build/check_qbit.js deleted file mode 100644 index 0d6f725..0000000 --- a/build/check_qbit.js +++ /dev/null @@ -1,49 +0,0 @@ -import { QBIT_HOST, QBIT_PORT } from './config.js'; -import { login } from './qbittorrent/auth.js'; -import { logger } from './helpers/logger.js'; -import { sendMessage } from './discord/api.js'; -import { SETTINGS } from '../settings.js'; -import { addMessage } from './discord/messages.js'; -export const check_qbit = async () => { - let t1 = Date.now(); - // Check settings - if (!Array.isArray(SETTINGS.PAUSE_SKIP_CATEGORIES)) { - logger.error(`Failed to validate settings! PAUSE_SKIP_CATEGORIES is missing. Please check sample.settings.js and copy changes to settings.js`); - process.exit(1); - } - if (!Array.isArray(SETTINGS.PAUSE_SKIP_TAGS)) { - logger.error(`Failed to validate settings! PAUSE_SKIP_TAGS is missing. Please check sample.settings.js and copy changes to settings.js`); - process.exit(1); - } - try { - await login(); - } - catch (errorCode) { - if (errorCode === 999) { - logger.error(`FAILED! qBittorrent API is not listening at http://${QBIT_HOST}:${QBIT_PORT}`); - } - else { - logger.error(`Failed with error code ${errorCode}. Check username / password. Exiting...`); - } - process.exit(1); - } - let t2 = Date.now(); - logger.info(`Login completed in ${((t2 - t1) / 1000).toFixed(2)} seconds.`); - const { enabled, botUsername, botAvatar } = SETTINGS.DISCORD_NOTIFICATIONS || { enabled: false }; - if (enabled === true) { - try { - // await sendMessage({ - // content: 'qbit-race validation test', - // username: botUsername, - // avatar_url: botAvatar - // }); - await sendMessage(addMessage('Ubuntu 20.04 LTS', ['ubuntu.com', 'linux.com'], 1024 * 1024 * 1024 * 3.412, 1)); - } - catch (error) { - logger.error('Failed to validate discord webhook. Either disable discord notifications or fix the webhook.'); - process.exit(1); - } - } - logger.info(`SUCCESS!`); -}; -//# sourceMappingURL=check_qbit.js.map \ No newline at end of file diff --git a/build/config.js b/build/config.js index a7139a5..52abd1d 100644 --- a/build/config.js +++ b/build/config.js @@ -2,7 +2,6 @@ import * as dotenv from 'dotenv'; import path, { dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); -import { SETTINGS } from '../settings.js'; import { logger } from './helpers/logger.js'; let COOKIE = ''; let LOGFILE = 'default.log'; @@ -35,10 +34,10 @@ if (!QBIT_PASSWORD) { console.log("Please define QBIT_PASSWORD in your .env file."); process.exit(1); } -if (!DISCORD_WEBHOOK && SETTINGS.DISCORD_NOTIFICATIONS.enabled === true) { - console.log("Please define DISCORD_WEBHOOK in your .env file. (Discord notifications are enabled but a webhook is not defined)\n"); - process.exit(1); -} +// if (!DISCORD_WEBHOOK && SETTINGS.DISCORD_NOTIFICATIONS.enabled === true){ +// console.log("Please define DISCORD_WEBHOOK in your .env file. (Discord notifications are enabled but a webhook is not defined)\n"); +// process.exit(1); +// } const setCookie = (cookie) => { COOKIE = cookie; logger.info(`Updated COOKIE!`); diff --git a/build/discord/messages.js b/build/discord/messages.js index 3f48658..ad2ad29 100644 --- a/build/discord/messages.js +++ b/build/discord/messages.js @@ -1,75 +1,5 @@ //Prepare message JSONs for different requirements import { humanFileSize } from '../helpers/utilities.js'; -import { SETTINGS } from '../../settings.js'; -const { botUsername, botAvatar } = SETTINGS.DISCORD_NOTIFICATIONS || { botUsername: 'qBittorrent', botAvatar: '' }; -export const addMessage = (torrentName, trackers, size, reannounceCount) => { - const humanSize = humanFileSize(size, false, 2); - const body = { - content: `Added ${torrentName} (${humanSize})`, - username: botUsername, - avatar_url: botAvatar, - embeds: [ - { - title: torrentName, - description: 'Added to qBittorrent', - thumbnail: { - url: botAvatar - }, - fields: [ - { - name: trackers.length === 1 ? 'Tracker' : 'Trackers', - value: trackers.join('\n') - }, - { - name: 'Size', - value: humanSize - }, - { - name: 'Reannounce Count', - value: reannounceCount.toString() - } - ] - } - ] - }; - return body; -}; -export const completeMessage = (torrentName, trackers, size, ratio) => { - let trackersMessage = trackers.join('\n'); - if (trackersMessage === '') { - trackersMessage = 'No trackers set!'; - } - const humanSize = humanFileSize(size, false, 2); - const body = { - content: `Completed ${torrentName}! (Ratio: ${ratio.toFixed(2)})`, - username: botUsername, - avatar_url: botAvatar, - embeds: [ - { - title: torrentName, - description: 'Completed download', - thumbnail: { - url: botAvatar - }, - fields: [ - { - name: 'Ratio', - value: ratio.toFixed(2).toString() - }, - { - name: trackers.length === 1 ? 'Tracker' : 'Trackers', - value: trackersMessage - }, - { - name: 'Size', - value: humanSize - } - ] - } - ] - }; - return body; -}; export const buildMessageBody = (discordSettings, partialBody) => { return { ...partialBody, diff --git a/build/helpers/post_race_resume.js b/build/helpers/post_race_resume.js deleted file mode 100644 index 3fd8012..0000000 --- a/build/helpers/post_race_resume.js +++ /dev/null @@ -1,68 +0,0 @@ -import { SETTINGS } from '../../settings.js'; -import { sendMessage } from '../discord/api.js'; -import { completeMessage } from '../discord/messages.js'; -import { getTorrentInfo, getTorrents, setCategory } from "../qbittorrent/api.js"; -import { login } from "../qbittorrent/auth.js"; -import { logger } from "./logger.js"; -import { resume } from './resume.js'; -/** - * postRaceResume runs after a race completes and resumes torrents. - * - * It first calls getTorrents to get a list, and then checks to see if any are still downloading. - * It also checks if any are stalled / in reannounce loop. - * If nothing is pending, it will resume all torrents. - */ -export const postRaceResume = async (infohash, tracker) => { - let torrents; - try { - await login(); - } - catch (error) { - console.log(`Failed to login. Exiting...`); - process.exit(1); - } - logger.info(`Login completed!`); - // Check if this torrent's category is in the rename list - // In case they are on old settings, skip it - if (SETTINGS.CATEGORY_FINISH_CHANGE !== undefined) { - const torrentInfo = await getTorrentInfo(infohash); - const newCategoryName = SETTINGS.CATEGORY_FINISH_CHANGE[torrentInfo.category]; - // Check if there was a rule for it or not - if (newCategoryName !== undefined) { - try { - await setCategory(infohash, newCategoryName); - logger.info(`Changed category for ${infohash} from ${torrentInfo.category} to ${newCategoryName}`); - } - catch (error) { - logger.error(`Failed to change category for ${infohash} from ${torrentInfo.category} to ${newCategoryName}`); - } - } - } - logger.info(`Getting torrent list`); - try { - torrents = await getTorrents(); - } - catch (error) { - logger.error(`Failed to get torrents from qBittorrent`); - process.exit(1); - } - //Get the stats for this torrent and send to discord - const { enabled } = SETTINGS.DISCORD_NOTIFICATIONS || { enabled: false }; - if (enabled === true) { - let torrent = torrents.find(t => t.hash === infohash); - if (torrent === undefined) { - logger.error(`Unable to find completed torrent (${infohash}) in array. Exiting...`); - process.exit(1); - } - try { - logger.info('Sending notification to deluge...'); - await sendMessage(completeMessage(torrent.name, torrent.tags.split(','), torrent.size, torrent.ratio)); - } - catch (error) { - logger.error('Failed to send notification to Discord'); - } - } - // handle the resume part - await resume(torrents); -}; -//# sourceMappingURL=post_race_resume.js.map \ No newline at end of file diff --git a/build/helpers/pre_race.js b/build/helpers/pre_race.js deleted file mode 100644 index 8fb1161..0000000 --- a/build/helpers/pre_race.js +++ /dev/null @@ -1,85 +0,0 @@ -import { getTorrents, pauseTorrents } from '../qbittorrent/api.js'; -import { logger } from './logger.js'; -import { SEEDING_STATES } from './constants.js'; -import { SETTINGS } from '../../settings.js'; -export const preRaceCheck = () => { - return new Promise(async (resolve, reject) => { - logger.info(`Getting torrent list`); - let torrents; - try { - torrents = await getTorrents(); - } - catch (error) { - logger.error(`Failed to get torrents from qBittorrent`); - process.exit(1); - } - if (SETTINGS.CONCURRENT_RACES !== -1) { - const downloading = torrents.filter(torrent => torrent.state === 'downloading'); - if (downloading.length > SETTINGS.CONCURRENT_RACES) { - resolve(false); - return; - } - logger.info(`Currently downloading ${downloading.length} torrents.`); - //e.g. 5000 * 30 = 150000ms. Now - this number, is how old the oldest - //reannouncing torrent can be. Older than that it is a stalled torrent (seeder abandoned), and we will not count it. - const reannounceYoungest = Date.now() - (SETTINGS.REANNOUNCE_INTERVAL * SETTINGS.REANNOUNCE_LIMIT); - const stalledDL = torrents.filter(torrent => { - if (torrent.state === 'stalledDL') { - if (SETTINGS.COUNT_STALLED_DOWNLOADS === true) { - return true; //We dont care about ratio in this case. Stalled is stalled. - } - if ((torrent.added_on * 1000) > reannounceYoungest) { - console.log('This is still in reannounce phase', torrent); - return true; //This is younger (timestamp is more) than limit. So it is probably still contacting tracker. - } - return false; - } - else { - return false; - } - }); - logger.info(`Currently there are ${stalledDL.length} stalled torrents, waiting for race to start`); - if ((downloading.length + stalledDL.length) >= SETTINGS.CONCURRENT_RACES) { - resolve(false); - return; - } - } - //If we got here, downloading is not a problem. Let's pause those whuch we can - if (SETTINGS.PAUSE_RATIO !== -1) { - const uploading = torrents.filter(torrents => SEEDING_STATES.some(state => state === torrents.state)); - logger.info(`Currently seeding ${uploading.length} torrents.`); - const toPause = uploading.filter(torrent => { - //If the ratio is bellow threshold, we will not pause it anyway. - if (torrent.ratio < SETTINGS.PAUSE_RATIO) { - logger.info(`Ratio for ${torrent.name} is below threshold. Not pausing.`); - return false; - } - //Ratio is pausable. Check category next - if (SETTINGS.PAUSE_SKIP_CATEGORIES.includes(torrent.category)) { - logger.info(`Category ${torrent.category} for ${torrent.name} is in the skip list. Will not pause!`); - return false; - } - //Finally check the tags for any overlap - //If for some tag in skip list, torrent tags includes it, then skip - const torrentTags = torrent.tags.split(','); - if (SETTINGS.PAUSE_SKIP_TAGS.some(skipTag => torrentTags.includes(skipTag))) { - logger.info(`Torrent ${torrent.name} has tags (${torrent.tags}) in common with skip tags. Will not pause!`); - return false; - } - return true; - }); - // Do not pause categories / tags that we have set to skip - logger.info(`Going to pause ${toPause.length} torrents.`); - try { - await pauseTorrents(toPause); - } - catch (error) { - logger.error(`Failed to pause torrents`); - process.exit(1); - } - } - //We are now ready to race! - resolve(true); - }); -}; -//# sourceMappingURL=pre_race.js.map \ No newline at end of file diff --git a/build/helpers/resume.js b/build/helpers/resume.js deleted file mode 100644 index 7e5e3f0..0000000 --- a/build/helpers/resume.js +++ /dev/null @@ -1,32 +0,0 @@ -import { SETTINGS } from '../../settings.js'; -import { resumeTorrents } from "../qbittorrent/api.js"; -import { logger } from "./logger.js"; -export const resume = async (torrents) => { - const reannounceYoungest = Date.now() - (SETTINGS.REANNOUNCE_INTERVAL * SETTINGS.REANNOUNCE_LIMIT); - for (let x = 0; x < torrents.length; x++) { - const torrent = torrents[x]; - if (torrent.state === 'downloading') { - logger.info(`We are still downloading, not resuming rest.`); - return; //We do not want to resume, since something else is downloading. - } - if (torrent.state === 'stalledDL' && (torrent.added_on * 1000) > reannounceYoungest) { - logger.info(`There is a torrent in reannounce phase, not resuming rest.`); - return; //This torrent is also still in the reannounce phase - } - } - const paused = torrents.filter(torrent => torrent.state === 'pausedUP'); - if (paused.length === 0) { - logger.info(`No downloading, nothing to resume either.`); - return; - } - logger.info(`No downloading torrents. Resuming ${paused.length} torrents...`); - try { - await resumeTorrents(paused); - } - catch (error) { - logger.error(`Failed to resume torrents.`); - process.exit(1); - } - logger.info(`Resumed all torrents. Exiting...`); -}; -//# sourceMappingURL=resume.js.map \ No newline at end of file diff --git a/package.json b/package.json index 56a95ff..9fca65b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "qbit-race", - "version": "2.0.0-alpha.1", + "version": "2.0.0-alpha.2", "description": "Qbit utilities for racing", "main": "./bin/index.js", "type": "module", diff --git a/sample.settings.js b/sample.settings.js deleted file mode 100644 index e920bd1..0000000 --- a/sample.settings.js +++ /dev/null @@ -1,17 +0,0 @@ -export const SETTINGS = { - REANNOUNCE_INTERVAL: 5000, //Time to wait between reannounces, milliseconds. - REANNOUNCE_LIMIT: 30, //How many times to try to reannounce - PAUSE_RATIO: 1, //Seeding Torrents with ratio greater than or equal to this will be paused when a torrent is loaded for racing. Set to -1 to disable pause (worse performance) - PAUSE_SKIP_TAGS: ["tracker.linux.org", "some_other_tag"], //Case sensitive list of tags, to skip when pausing torrents before race - PAUSE_SKIP_CATEGORIES: ["permaseeding", "some_other_category"], //Case sensitive list of categories, to skip when pausing torrents before race - CONCURRENT_RACES: 1, //If these many races (downloads in qbit) are going on, then the incoming torrent will be skipped - COUNT_STALLED_DOWNLOADS: false, //These are cases when the seeder abandons. Sets whether to count them while checking CONCURRENT_RACES. Advisable to set to false, because if CONCURRENT_RACES is 1, and a seeder abandons, this will skip all future races even though box is idle - DISCORD_NOTIFICATIONS: { - enabled: false, - botUsername: 'qBittorrent', - botAvatar: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/New_qBittorrent_Logo.svg/600px-New_qBittorrent_Logo.svg.png' - }, - CATEGORY_FINISH_CHANGE: { - 'OLD CAT': 'NEW CAT', - } -} diff --git a/settings.d.ts b/settings.d.ts deleted file mode 100644 index 380f143..0000000 --- a/settings.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -interface DISCORD_SETTINGS { - enabled: boolean; - botUsername: string; - botAvatar: string; -} - -interface CATEGORY_FINISH_RULES { - [originalCatrogry: string]: string, -} - -export const SETTINGS: { - REANNOUNCE_INTERVAL: number; - REANNOUNCE_LIMIT: number; - PAUSE_RATIO: number; - PAUSE_SKIP_TAGS: string[]; - PAUSE_SKIP_CATEGORIES: string[]; - CONCURRENT_RACES: number; - COUNT_STALLED_DOWNLOADS: boolean; - DISCORD_NOTIFICATIONS: DISCORD_SETTINGS; - CATEGORY_FINISH_CHANGE: CATEGORY_FINISH_RULES; -} \ No newline at end of file diff --git a/src/add_torrent.ts b/src/add_torrent.ts deleted file mode 100644 index a8a3b15..0000000 --- a/src/add_torrent.ts +++ /dev/null @@ -1,166 +0,0 @@ -/** - * @deprecated - * - * TODO: Completely remove in v2 - */ -import { login } from './qbittorrent/auth.js'; -import { addTags, addTorrent, deleteTorrents, getTorrentInfo, getTorrents, getTrackers, reannounce } from './qbittorrent/api.js' -import { sleep } from './helpers/utilities.js'; -import { logger } from './helpers/logger.js'; -import { preRaceCheck } from './helpers/pre_race.js'; -import { SETTINGS } from '../settings.js'; -import { sendMessage } from './discord/api.js'; -import { addMessage } from './discord/messages.js'; -import { torrentFromApi } from './interfaces.js'; -import { resume } from './helpers/resume.js'; -import { getTorrentMetainfo, torrentMetainfo, TorrentMetainfoV2 } from './helpers/torrent.js'; -import * as fs from 'fs'; - -export const add_torrent = async (args: string[]) => { - - // Arguments: - // previous: [infohash] [torrentname] [tracker] [path] - // new: [path] - - // const infohash = args[0].toLowerCase(); - // const torrentName = args[1]; - // const tracker = args[2]; - // const path = args[3]; - - //TODO: Check if first param is infohash or path? Or no bw compatibility?... - // Assume none at first. - const path = args[0]; - - let category: string | undefined = undefined; - - // We need to read the torrent file and get the metainfo - let torrentFile: Buffer; - let metainfo: TorrentMetainfoV2; - - try { - torrentFile = fs.readFileSync(path); - metainfo = getTorrentMetainfo(torrentFile); - } catch (e){ - logger.error(`Failed to read/parse torrent from file`); - throw new Error("TORRENT_READ_FAIL"); - } - - //Check if `--category` is present. Then the next argument is the category to set. - for (let index = 1; index < args.length; index++){ - if (args[index] === '--category'){ - //The next one should be the actual category - if (index + 1 < args.length){ - category = args[index + 1]; - } else { - //They fucked up. Let them know, but dont error out (act as if not set). - logger.error('--category set but the category is missing.'); - break; - } - } - } - - let t1 = Date.now(); - - try { - await login(); - } catch (error) { - logger.error(`Failed to login. Exiting...`); - process.exit(1); - } - - let t2 = Date.now(); - logger.info(`Login completed in ${((t2 - t1) / 1000).toFixed(2)} seconds.`); - logger.info('Performing Pre Race check...'); - const okay = await preRaceCheck(); - - if (okay === false){ - logger.info(`Conditions not met. Skipping ${metainfo.name}`); - process.exit(0); //it is a soft exit. - } - - logger.info(`Adding torrent ${metainfo.name}`); - - - try { - await addTorrent(torrentFile, category); - } catch (error) { - process.exit(1); - } - - //Wait for torrent to register in Qbit, initial announce. - await sleep(5000); - - //Now we get the torrent's trackers, which will let us set the tags. - let tags: string[] = [] - - try { - let trackers: any[] = await getTrackers(metainfo.hash); - trackers.splice(0, 3); - tags = trackers.map(({ url }) => new URL(url).hostname); - logger.info(`Adding ${tags.length} tags.`); - await addTags([{ hash: metainfo.hash }], tags); - } catch (error) { - logger.error(`Failed to add tags. Error code ${error}`); - } - - //We also want to get the size of the torrent, for the notification. - let torrent: torrentFromApi; - - try { - torrent = await getTorrentInfo(metainfo.hash); - } catch (error){ - process.exit(1); - } - - //Now we move anto reannounce - logger.info("Starting reannounce check..."); - let attempts = 0; - let announceOK = false; - - while (attempts < SETTINGS.REANNOUNCE_LIMIT) { - - logger.info(`Attempt #${attempts + 1}: Querying tracker status...`); - - try { - let trackers: any[] = await getTrackers(metainfo.hash); - trackers.splice(0, 3); - let working = trackers.some(tracker => tracker.status === 2); - - if (!working) { - //We need to reannounce - logger.info('Need to reannounce. Sending request and sleeping...'); - await reannounce(metainfo.hash); - await sleep(SETTINGS.REANNOUNCE_INTERVAL); - attempts++; - } else { - announceOK = true; - logger.info('Tracker is OK. Exiting...'); - break; - } - } catch (error) { - logger.error('Caught an error. Exiting...'); - process.exit(1); - } - } - - //We got here but failed reannounce failed. Delete it. - if (announceOK === false){ - logger.info(`Did not get an OK from tracker even after ${SETTINGS.REANNOUNCE_LIMIT} attempts. Deleting...`); - await deleteTorrents([{ hash: metainfo.hash }]); - - // Resume any that were paused - logger.info('Going to resume any paused torrents...'); - const torrents = await getTorrents(); - resume(torrents); - } else { - //Send message to discord (if enabled) - const { enabled } = SETTINGS.DISCORD_NOTIFICATIONS || { enabled: false } - if (enabled === true) { - try { - await sendMessage(addMessage(torrent.name, tags, torrent.size, attempts)) - } catch (error){ - process.exit(1); - } - } - } -} \ No newline at end of file diff --git a/src/check_qbit.ts b/src/check_qbit.ts deleted file mode 100644 index dcae298..0000000 --- a/src/check_qbit.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { QBIT_HOST, QBIT_PORT } from './config.js'; -import { login } from './qbittorrent/auth.js'; -import { logger } from './helpers/logger.js'; -import { sendMessage } from './discord/api.js'; -import { SETTINGS } from '../settings.js'; -import { addMessage } from './discord/messages.js'; - -export const check_qbit = async () => { - - let t1 = Date.now(); - - // Check settings - - if (!Array.isArray(SETTINGS.PAUSE_SKIP_CATEGORIES)){ - logger.error(`Failed to validate settings! PAUSE_SKIP_CATEGORIES is missing. Please check sample.settings.js and copy changes to settings.js`); - process.exit(1); - } - - if (!Array.isArray(SETTINGS.PAUSE_SKIP_TAGS)){ - logger.error(`Failed to validate settings! PAUSE_SKIP_TAGS is missing. Please check sample.settings.js and copy changes to settings.js`); - process.exit(1); - } - - try { - await login(); - } catch (errorCode) { - - if (errorCode === 999){ - logger.error(`FAILED! qBittorrent API is not listening at http://${QBIT_HOST}:${QBIT_PORT}`); - } else { - logger.error(`Failed with error code ${errorCode}. Check username / password. Exiting...`); - } - - process.exit(1); - } - - let t2 = Date.now(); - logger.info(`Login completed in ${((t2 - t1) / 1000).toFixed(2)} seconds.`); - - const { enabled, botUsername, botAvatar } = SETTINGS.DISCORD_NOTIFICATIONS || { enabled: false } - if (enabled === true){ - try { - // await sendMessage({ - // content: 'qbit-race validation test', - // username: botUsername, - // avatar_url: botAvatar - // }); - await sendMessage(addMessage('Ubuntu 20.04 LTS', ['ubuntu.com', 'linux.com'], 1024 * 1024 * 1024 * 3.412, 1)); - } catch (error){ - logger.error('Failed to validate discord webhook. Either disable discord notifications or fix the webhook.'); - process.exit(1); - } - } - - - logger.info(`SUCCESS!`); -} \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index d4c4acb..dd520ec 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,7 +4,6 @@ import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); -import { SETTINGS } from '../settings.js'; import { logger } from './helpers/logger.js'; let COOKIE = ''; @@ -46,10 +45,10 @@ if (!QBIT_PASSWORD){ process.exit(1); } -if (!DISCORD_WEBHOOK && SETTINGS.DISCORD_NOTIFICATIONS.enabled === true){ - console.log("Please define DISCORD_WEBHOOK in your .env file. (Discord notifications are enabled but a webhook is not defined)\n"); - process.exit(1); -} +// if (!DISCORD_WEBHOOK && SETTINGS.DISCORD_NOTIFICATIONS.enabled === true){ +// console.log("Please define DISCORD_WEBHOOK in your .env file. (Discord notifications are enabled but a webhook is not defined)\n"); +// process.exit(1); +// } const setCookie = (cookie: string) => { COOKIE = cookie; diff --git a/src/discord/messages.ts b/src/discord/messages.ts index 2667968..ef5a3d3 100644 --- a/src/discord/messages.ts +++ b/src/discord/messages.ts @@ -1,85 +1,7 @@ //Prepare message JSONs for different requirements import { humanFileSize } from '../helpers/utilities.js'; -import { DISCORD_SETTINGS, SETTINGS } from '../../settings.js'; import { QbittorrentTorrent } from '../qbittorrent/api.js'; - -const { botUsername, botAvatar } = SETTINGS.DISCORD_NOTIFICATIONS || { botUsername: 'qBittorrent', botAvatar: '' } - - -export const addMessage = (torrentName: string, trackers: string[], size: number, reannounceCount: number) => { - - const humanSize = humanFileSize(size, false, 2); - const body = { - content: `Added ${torrentName} (${humanSize})`, - username: botUsername, - avatar_url: botAvatar, - embeds: [ - { - title: torrentName, - description: 'Added to qBittorrent', - thumbnail: { - url: botAvatar - }, - fields: [ - { - name: trackers.length === 1 ? 'Tracker' : 'Trackers', - value: trackers.join('\n') - }, - { - name: 'Size', - value: humanSize - }, - { - name: 'Reannounce Count', - value: reannounceCount.toString() - } - ] - } - ] - }; - - return body; -} - -export const completeMessage = (torrentName: string, trackers: string[], size: number, ratio: number) => { - - let trackersMessage = trackers.join('\n'); - if (trackersMessage === ''){ - trackersMessage = 'No trackers set!'; - } - - const humanSize = humanFileSize(size, false, 2); - const body = { - content: `Completed ${torrentName}! (Ratio: ${ratio.toFixed(2)})`, - username: botUsername, - avatar_url: botAvatar, - embeds: [ - { - title: torrentName, - description: 'Completed download', - thumbnail: { - url: botAvatar - }, - fields: [ - { - name: 'Ratio', - value: ratio.toFixed(2).toString() - }, - { - name: trackers.length === 1 ? 'Tracker' : 'Trackers', - value: trackersMessage - }, - { - name: 'Size', - value: humanSize - } - ] - } - ] - }; - - return body; -} +import { DISCORD_SETTINGS } from '../utils/config.js'; type EmbedField = { name: string; diff --git a/src/helpers/post_race_resume.ts b/src/helpers/post_race_resume.ts deleted file mode 100644 index 03aff6a..0000000 --- a/src/helpers/post_race_resume.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { SETTINGS } from '../../settings.js'; -import { sendMessage } from '../discord/api.js'; -import { completeMessage } from '../discord/messages.js'; -import { torrentFromApi } from '../interfaces.js'; -import { getTorrentInfo, getTorrents, setCategory } from "../qbittorrent/api.js"; -import { login } from "../qbittorrent/auth.js"; -import { logger } from "./logger.js"; -import { resume } from './resume.js'; - -/** - * postRaceResume runs after a race completes and resumes torrents. - * - * It first calls getTorrents to get a list, and then checks to see if any are still downloading. - * It also checks if any are stalled / in reannounce loop. - * If nothing is pending, it will resume all torrents. - */ -export const postRaceResume = async (infohash: string, tracker: string) => { - - let torrents: torrentFromApi[]; - - try { - await login(); - } catch (error) { - console.log(`Failed to login. Exiting...`); - process.exit(1); - } - - logger.info(`Login completed!`); - - // Check if this torrent's category is in the rename list - // In case they are on old settings, skip it - if (SETTINGS.CATEGORY_FINISH_CHANGE !== undefined){ - const torrentInfo = await getTorrentInfo(infohash); - const newCategoryName = SETTINGS.CATEGORY_FINISH_CHANGE[torrentInfo.category]; - - // Check if there was a rule for it or not - if (newCategoryName !== undefined){ - try { - await setCategory(infohash, newCategoryName); - logger.info(`Changed category for ${infohash} from ${torrentInfo.category} to ${newCategoryName}`); - } catch (error) { - logger.error(`Failed to change category for ${infohash} from ${torrentInfo.category} to ${newCategoryName}`); - } - } - } - - logger.info(`Getting torrent list`); - - try { - torrents = await getTorrents(); - } catch (error) { - logger.error(`Failed to get torrents from qBittorrent`); - process.exit(1); - } - - //Get the stats for this torrent and send to discord - const { enabled } = SETTINGS.DISCORD_NOTIFICATIONS || { enabled: false } - - if (enabled === true){ - let torrent = torrents.find(t => t.hash === infohash); - - if (torrent === undefined){ - logger.error(`Unable to find completed torrent (${infohash}) in array. Exiting...`) - process.exit(1); - } - - try { - logger.info('Sending notification to deluge...'); - await sendMessage(completeMessage(torrent.name, torrent.tags.split(','), torrent.size, torrent.ratio)); - } catch (error){ - logger.error('Failed to send notification to Discord'); - } - } - - // handle the resume part - await resume(torrents); -} \ No newline at end of file diff --git a/src/helpers/pre_race.ts b/src/helpers/pre_race.ts deleted file mode 100644 index 1c00220..0000000 --- a/src/helpers/pre_race.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { getTorrents, pauseTorrents } from '../qbittorrent/api.js' -import { logger } from './logger.js'; -import { SEEDING_STATES } from './constants.js'; -import { SETTINGS } from '../../settings.js'; -import { torrentFromApi } from '../interfaces.js'; - -export const preRaceCheck = () => { - return new Promise( async (resolve, reject) => { - logger.info(`Getting torrent list`); - let torrents: torrentFromApi[]; - - try { - torrents = await getTorrents(); - } catch (error){ - logger.error(`Failed to get torrents from qBittorrent`); - process.exit(1); - } - - if (SETTINGS.CONCURRENT_RACES !== -1){ - const downloading = torrents.filter(torrent => torrent.state === 'downloading'); - - if (downloading.length > SETTINGS.CONCURRENT_RACES){ - resolve(false); - return; - } - - logger.info(`Currently downloading ${downloading.length} torrents.`); - - //e.g. 5000 * 30 = 150000ms. Now - this number, is how old the oldest - //reannouncing torrent can be. Older than that it is a stalled torrent (seeder abandoned), and we will not count it. - const reannounceYoungest = Date.now() - (SETTINGS.REANNOUNCE_INTERVAL * SETTINGS.REANNOUNCE_LIMIT); - - const stalledDL = torrents.filter(torrent => { - - if (torrent.state === 'stalledDL') { - if (SETTINGS.COUNT_STALLED_DOWNLOADS === true){ - return true; //We dont care about ratio in this case. Stalled is stalled. - } - - if ((torrent.added_on * 1000) > reannounceYoungest){ - console.log('This is still in reannounce phase', torrent); - return true; //This is younger (timestamp is more) than limit. So it is probably still contacting tracker. - } - - return false; - } else { - return false; - } - }); - - logger.info(`Currently there are ${stalledDL.length} stalled torrents, waiting for race to start`); - - if ((downloading.length + stalledDL.length) >= SETTINGS.CONCURRENT_RACES){ - resolve(false); - return; - } - - } - - //If we got here, downloading is not a problem. Let's pause those whuch we can - if (SETTINGS.PAUSE_RATIO !== -1){ - const uploading = torrents.filter(torrents => SEEDING_STATES.some(state => state === torrents.state)); - logger.info(`Currently seeding ${uploading.length} torrents.`); - - const toPause = uploading.filter(torrent => { - - //If the ratio is bellow threshold, we will not pause it anyway. - if (torrent.ratio < SETTINGS.PAUSE_RATIO){ - logger.info(`Ratio for ${torrent.name} is below threshold. Not pausing.`); - return false; - } - - //Ratio is pausable. Check category next - if (SETTINGS.PAUSE_SKIP_CATEGORIES.includes(torrent.category)){ - logger.info(`Category ${torrent.category} for ${torrent.name} is in the skip list. Will not pause!`); - return false; - } - - //Finally check the tags for any overlap - //If for some tag in skip list, torrent tags includes it, then skip - const torrentTags = torrent.tags.split(','); - - if (SETTINGS.PAUSE_SKIP_TAGS.some(skipTag => torrentTags.includes(skipTag))){ - logger.info(`Torrent ${torrent.name} has tags (${torrent.tags}) in common with skip tags. Will not pause!`); - return false; - } - - return true; - }) - - // Do not pause categories / tags that we have set to skip - logger.info(`Going to pause ${toPause.length} torrents.`); - - try { - await pauseTorrents(toPause); - } catch (error){ - logger.error(`Failed to pause torrents`); - process.exit(1); - } - } - - //We are now ready to race! - resolve(true); - }) -} \ No newline at end of file diff --git a/src/helpers/resume.ts b/src/helpers/resume.ts deleted file mode 100644 index 4725883..0000000 --- a/src/helpers/resume.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { SETTINGS } from '../../settings.js'; -import { torrentFromApi } from '../interfaces.js'; -import { resumeTorrents } from "../qbittorrent/api.js"; -import { logger } from "./logger.js"; - -export const resume = async (torrents: torrentFromApi[]) => { - - const reannounceYoungest = Date.now() - (SETTINGS.REANNOUNCE_INTERVAL * SETTINGS.REANNOUNCE_LIMIT); - - for (let x = 0; x < torrents.length; x++){ - - const torrent = torrents[x]; - - if (torrent.state === 'downloading'){ - logger.info(`We are still downloading, not resuming rest.`); - return; //We do not want to resume, since something else is downloading. - } - - if (torrent.state === 'stalledDL' && (torrent.added_on * 1000) > reannounceYoungest){ - logger.info(`There is a torrent in reannounce phase, not resuming rest.`) - return; //This torrent is also still in the reannounce phase - } - - } - - const paused = torrents.filter(torrent => torrent.state === 'pausedUP'); - - if (paused.length === 0){ - logger.info(`No downloading, nothing to resume either.`); - return; - } - - logger.info(`No downloading torrents. Resuming ${paused.length} torrents...`); - - try { - await resumeTorrents(paused); - } catch (error) { - logger.error(`Failed to resume torrents.`); - process.exit(1); - } - - logger.info(`Resumed all torrents. Exiting...`); -} \ No newline at end of file diff --git a/src/utils/config.ts b/src/utils/config.ts index 41fbe67..8fdebd8 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,4 +1,4 @@ -type DISCORD_SETTINGS = { +export type DISCORD_SETTINGS = { /** * Controls whether the webhook is enabled or not */ From ba7c3862cad880473b4e3e073650c96d357eda34 Mon Sep 17 00:00:00 2001 From: Raghu Saxena Date: Thu, 8 Sep 2022 20:37:10 +0800 Subject: [PATCH 4/8] fix gtihub workfflow --- .github/workflows/node.js.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index bd955be..3d6ea5d 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -21,8 +21,6 @@ jobs: uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - - run: cp sample.settings.js settings.js - - run: cp sample.env .env - run: npm install - run: npm run test - run: npm run build \ No newline at end of file From 65ee380327312cab21c40d3a331b168830c7c5f8 Mon Sep 17 00:00:00 2001 From: Raghu Saxena Date: Thu, 8 Sep 2022 20:38:22 +0800 Subject: [PATCH 5/8] Dont exit --- src/config.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/config.ts b/src/config.ts index dd520ec..f75359e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -27,22 +27,22 @@ export const PROM_PORT = process.env.PROM_PORT || '9999'; if (!QBIT_HOST){ console.log("Please define QBIT_HOST in your .env file."); - process.exit(1); + // process.exit(1); } if (!QBIT_PORT){ console.log("Please define QBIT_PORT in your .env file."); - process.exit(1); + // process.exit(1); } if (!QBIT_USERNAME){ console.log("Please define QBIT_USERNAME in your .env file."); - process.exit(1); + // process.exit(1); } if (!QBIT_PASSWORD){ console.log("Please define QBIT_PASSWORD in your .env file."); - process.exit(1); + // process.exit(1); } // if (!DISCORD_WEBHOOK && SETTINGS.DISCORD_NOTIFICATIONS.enabled === true){ From 9f7710af342e202eb8f6d86296a12167f65f9eb6 Mon Sep 17 00:00:00 2001 From: Raghu Saxena Date: Thu, 8 Sep 2022 20:40:44 +0800 Subject: [PATCH 6/8] build before publish :ded: --- build/config.js | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build/config.js b/build/config.js index 52abd1d..e3d9450 100644 --- a/build/config.js +++ b/build/config.js @@ -20,19 +20,19 @@ export const PROM_IP = process.env.PROM_IP || '127.0.0.1'; export const PROM_PORT = process.env.PROM_PORT || '9999'; if (!QBIT_HOST) { console.log("Please define QBIT_HOST in your .env file."); - process.exit(1); + // process.exit(1); } if (!QBIT_PORT) { console.log("Please define QBIT_PORT in your .env file."); - process.exit(1); + // process.exit(1); } if (!QBIT_USERNAME) { console.log("Please define QBIT_USERNAME in your .env file."); - process.exit(1); + // process.exit(1); } if (!QBIT_PASSWORD) { console.log("Please define QBIT_PASSWORD in your .env file."); - process.exit(1); + // process.exit(1); } // if (!DISCORD_WEBHOOK && SETTINGS.DISCORD_NOTIFICATIONS.enabled === true){ // console.log("Please define DISCORD_WEBHOOK in your .env file. (Discord notifications are enabled but a webhook is not defined)\n"); diff --git a/package.json b/package.json index 9fca65b..88d60a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "qbit-race", - "version": "2.0.0-alpha.2", + "version": "2.0.0-alpha.3", "description": "Qbit utilities for racing", "main": "./bin/index.js", "type": "module", From e2ee56220483744fbcaea12584b01c61b115fcc1 Mon Sep 17 00:00:00 2001 From: Raghu Saxena Date: Thu, 8 Sep 2022 21:15:41 +0800 Subject: [PATCH 7/8] actually call add fn --- bin/index.mjs | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/index.mjs b/bin/index.mjs index 7d24012..8ae89e8 100755 --- a/bin/index.mjs +++ b/bin/index.mjs @@ -8,6 +8,7 @@ import { buildTorrentAddedBody } from '../build/discord/messages.js' import { getLoggerV3 } from '../build/utils/logger.js' import { tagErroredTorrents } from '../build/racing/tag.js' import { postRaceResumeV2 } from '../build/racing/completed.js' +import { addTorrentToRace } from '../build/racing/add.js'; const logger = getLoggerV3(); logger.info(`Starting...`); @@ -59,6 +60,8 @@ program.command('completed').description('Run post race procedure on complete of program.command('add').description('Add a new torrent').requiredOption('-p, --path ', 'The path to the torrent file (can be in /tmp)').option('-c, --category ', 'Category to set in qBittorrent').action(async(options) => { logger.debug(`Going to add torrent from ${options.path}, and set category ${options.category}`); + + await addTorrentToRace(api, config, options.path, options.category); }) program.parse(); diff --git a/package.json b/package.json index 88d60a8..40cd589 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "qbit-race", - "version": "2.0.0-alpha.3", + "version": "2.0.0-alpha.4", "description": "Qbit utilities for racing", "main": "./bin/index.js", "type": "module", From 5c9aed6a6296b19c3e8bbf7d0da38490852336e7 Mon Sep 17 00:00:00 2001 From: Raghu Saxena Date: Thu, 8 Sep 2022 21:52:05 +0800 Subject: [PATCH 8/8] Drop Node v14 :flushed: --- .github/workflows/node.js.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 3d6ea5d..2842cbe 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [14.x, 16.x, 18.x] + node-version: [16.x, 18.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: