From 204fe5165812b51a6470f6b47a94bb4a986526ba Mon Sep 17 00:00:00 2001 From: iPromKnight Date: Mon, 5 Feb 2024 01:51:22 +0000 Subject: [PATCH] Rewritten repository in typescript --- src/node/consumer/package-lock.json | 70 +++- src/node/consumer/package.json | 11 +- src/node/consumer/src/index.js | 4 +- src/node/consumer/src/jobs/processTorrents.js | 2 +- src/node/consumer/src/lib/cache.js | 2 +- src/node/consumer/src/lib/config.js | 2 +- src/node/consumer/src/lib/ingestedTorrent.js | 2 +- .../consumer/src/lib/{logger.js => logger.ts} | 0 src/node/consumer/src/lib/repository.js | 382 ------------------ src/node/consumer/src/lib/torrentEntries.js | 4 +- src/node/consumer/src/lib/torrentFiles.js | 2 +- src/node/consumer/src/lib/trackerService.js | 2 +- .../src/repository/database_repository.ts | 259 ++++++++++++ .../interfaces/content_attributes.ts | 11 + .../repository/interfaces/file_attributes.ts | 19 + .../interfaces/ingested_page_attributes.ts | 6 + .../interfaces/ingested_torrent_attributes.ts | 16 + .../interfaces/provider_attributes.ts | 10 + .../interfaces/skip_torrent_attributes.ts | 8 + .../interfaces/subtitle_attributes.ts | 12 + .../interfaces/torrent_attributes.ts | 26 ++ .../consumer/src/repository/models/content.ts | 22 + .../consumer/src/repository/models/file.ts | 60 +++ .../src/repository/models/ingestedPage.ts | 16 + .../src/repository/models/ingestedTorrent.ts | 40 ++ .../src/repository/models/provider.ts | 15 + .../src/repository/models/skipTorrent.ts | 10 + .../src/repository/models/subtitle.ts | 39 ++ .../consumer/src/repository/models/torrent.ts | 56 +++ .../consumer/{jsconfig.json => tsconfig.json} | 9 +- 30 files changed, 700 insertions(+), 417 deletions(-) rename src/node/consumer/src/lib/{logger.js => logger.ts} (100%) delete mode 100644 src/node/consumer/src/lib/repository.js create mode 100644 src/node/consumer/src/repository/database_repository.ts create mode 100644 src/node/consumer/src/repository/interfaces/content_attributes.ts create mode 100644 src/node/consumer/src/repository/interfaces/file_attributes.ts create mode 100644 src/node/consumer/src/repository/interfaces/ingested_page_attributes.ts create mode 100644 src/node/consumer/src/repository/interfaces/ingested_torrent_attributes.ts create mode 100644 src/node/consumer/src/repository/interfaces/provider_attributes.ts create mode 100644 src/node/consumer/src/repository/interfaces/skip_torrent_attributes.ts create mode 100644 src/node/consumer/src/repository/interfaces/subtitle_attributes.ts create mode 100644 src/node/consumer/src/repository/interfaces/torrent_attributes.ts create mode 100644 src/node/consumer/src/repository/models/content.ts create mode 100644 src/node/consumer/src/repository/models/file.ts create mode 100644 src/node/consumer/src/repository/models/ingestedPage.ts create mode 100644 src/node/consumer/src/repository/models/ingestedTorrent.ts create mode 100644 src/node/consumer/src/repository/models/provider.ts create mode 100644 src/node/consumer/src/repository/models/skipTorrent.ts create mode 100644 src/node/consumer/src/repository/models/subtitle.ts create mode 100644 src/node/consumer/src/repository/models/torrent.ts rename src/node/consumer/{jsconfig.json => tsconfig.json} (64%) diff --git a/src/node/consumer/package-lock.json b/src/node/consumer/package-lock.json index f6408a3..9a34802 100644 --- a/src/node/consumer/package-lock.json +++ b/src/node/consumer/package-lock.json @@ -14,24 +14,25 @@ "axios": "^1.6.1", "bottleneck": "^2.19.5", "cache-manager": "^5.4.0", - "dotenv": "^16.4.1", "google-sr": "^3.2.1", "jaro-winkler": "^0.2.8", "magnet-uri": "^6.2.0", "moment": "^2.30.1", - "mongodb": "^6.3.0", "name-to-imdb": "^3.0.4", "parse-torrent-title": "https://github.com/TheBeastLT/parse-torrent-title.git#022408972c2a040f846331a912a6a8487746a654", "pg": "^8.11.3", "pg-hstore": "^2.3.4", "pino": "^8.18.0", - "sequelize": "^6.31.1", + "reflect-metadata": "^0.2.1", + "sequelize": "^6.36.0", + "sequelize-typescript": "^2.1.6", "torrent-stream": "^1.2.1", "user-agents": "^1.0.1444" }, "devDependencies": { - "@types/node": "^20.11.6", + "@types/node": "^20.11.16", "@types/stremio-addon-sdk": "^1.6.10", + "@types/validator": "^13.11.8", "esbuild": "^0.20.0", "eslint": "^8.56.0", "eslint-plugin-import": "^2.29.1", @@ -581,7 +582,8 @@ }, "node_modules/@types/node": { "version": "20.11.16", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz", + "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==", "dependencies": { "undici-types": "~5.26.4" } @@ -597,7 +599,8 @@ }, "node_modules/@types/validator": { "version": "13.11.8", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.8.tgz", + "integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ==" }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", @@ -1409,17 +1412,6 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/dotenv": { - "version": "16.4.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", - "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" - } - }, "node_modules/dottie": { "version": "2.0.6", "license": "MIT" @@ -3776,6 +3768,11 @@ "node": ">= 12.13.0" } }, + "node_modules/reflect-metadata": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", + "integrity": "sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", "dev": true, @@ -3965,13 +3962,14 @@ }, "node_modules/sequelize": { "version": "6.36.0", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.36.0.tgz", + "integrity": "sha512-PqOa11EHwA/zLmGDU4aynbsavbHJUlgRvFuC/2cA4LhOuV6NHKcQ0IXB+hNdFrGT3rULmvc4kdIwnfCNsrECMQ==", "funding": [ { "type": "opencollective", "url": "https://opencollective.com/sequelize" } ], - "license": "MIT", "dependencies": { "@types/debug": "^4.1.8", "@types/validator": "^13.7.17", @@ -4030,6 +4028,42 @@ "node": ">= 10.0.0" } }, + "node_modules/sequelize-typescript": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/sequelize-typescript/-/sequelize-typescript-2.1.6.tgz", + "integrity": "sha512-Vc2N++3en346RsbGjL3h7tgAl2Y7V+2liYTAOZ8XL0KTw3ahFHsyAUzOwct51n+g70I1TOUDgs06Oh6+XGcFkQ==", + "dependencies": { + "glob": "7.2.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "@types/validator": "*", + "reflect-metadata": "*", + "sequelize": ">=6.20.1" + } + }, + "node_modules/sequelize-typescript/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sequelize/node_modules/semver": { "version": "7.5.4", "license": "ISC", diff --git a/src/node/consumer/package.json b/src/node/consumer/package.json index 9678cfc..2639881 100644 --- a/src/node/consumer/package.json +++ b/src/node/consumer/package.json @@ -24,18 +24,21 @@ "pg": "^8.11.3", "pg-hstore": "^2.3.4", "pino": "^8.18.0", - "sequelize": "^6.31.1", + "reflect-metadata": "^0.2.1", + "sequelize": "^6.36.0", + "sequelize-typescript": "^2.1.6", "torrent-stream": "^1.2.1", "user-agents": "^1.0.1444" }, "devDependencies": { - "@types/node": "^20.11.6", + "@types/node": "^20.11.16", "@types/stremio-addon-sdk": "^1.6.10", + "@types/validator": "^13.11.8", "esbuild": "^0.20.0", "eslint": "^8.56.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-import-helpers": "^1.3.1", - "tsx": "^4.7.0", - "pino-pretty": "^10.3.1" + "pino-pretty": "^10.3.1", + "tsx": "^4.7.0" } } diff --git a/src/node/consumer/src/index.js b/src/node/consumer/src/index.js index ac1dd83..e3ac0b7 100644 --- a/src/node/consumer/src/index.js +++ b/src/node/consumer/src/index.js @@ -1,9 +1,9 @@ import { listenToQueue } from './jobs/processTorrents.js'; -import { connect } from './lib/repository.js'; +import { repository } from "./repository/database_repository"; import { getTrackers } from "./lib/trackerService.js"; (async () => { await getTrackers(); - await connect(); + await repository.connect(); await listenToQueue(); })(); \ No newline at end of file diff --git a/src/node/consumer/src/jobs/processTorrents.js b/src/node/consumer/src/jobs/processTorrents.js index 23d9701..1dd50b2 100644 --- a/src/node/consumer/src/jobs/processTorrents.js +++ b/src/node/consumer/src/jobs/processTorrents.js @@ -1,7 +1,7 @@ import amqp from 'amqplib' import { rabbitConfig, jobConfig } from '../lib/config.js' import { processTorrentRecord } from "../lib/ingestedTorrent.js"; -import {logger} from "../lib/logger.js"; +import {logger} from "../lib/logger"; const assertQueueOptions = { durable: true } const consumeQueueOptions = { noAck: false } diff --git a/src/node/consumer/src/lib/cache.js b/src/node/consumer/src/lib/cache.js index 7b9be26..a7c1128 100644 --- a/src/node/consumer/src/lib/cache.js +++ b/src/node/consumer/src/lib/cache.js @@ -1,7 +1,7 @@ import { createCache, memoryStore} from 'cache-manager'; import { mongoDbStore } from '@tirke/node-cache-manager-mongodb' import { cacheConfig } from './config.js'; -import { logger } from './logger.js'; +import { logger } from './logger'; import { CacheType } from "./types.js"; const GLOBAL_KEY_PREFIX = 'knightcrawler-consumer'; diff --git a/src/node/consumer/src/lib/config.js b/src/node/consumer/src/lib/config.js index a63f093..69769ea 100644 --- a/src/node/consumer/src/lib/config.js +++ b/src/node/consumer/src/lib/config.js @@ -1,4 +1,4 @@ -export const rabbitConfig = { +export const rabbitConfig = { URI: process.env.RABBIT_URI || 'amqp://localhost', QUEUE_NAME: process.env.QUEUE_NAME || 'test-queue' } diff --git a/src/node/consumer/src/lib/ingestedTorrent.js b/src/node/consumer/src/lib/ingestedTorrent.js index 4948173..fe8705d 100644 --- a/src/node/consumer/src/lib/ingestedTorrent.js +++ b/src/node/consumer/src/lib/ingestedTorrent.js @@ -1,7 +1,7 @@ import { createTorrentEntry, checkAndUpdateTorrent } from './torrentEntries.js'; import {getTrackers} from "./trackerService.js"; import { TorrentType } from './types.js'; -import {logger} from "./logger.js"; +import {logger} from "./logger"; export async function processTorrentRecord(torrent) { const {category} = torrent; diff --git a/src/node/consumer/src/lib/logger.js b/src/node/consumer/src/lib/logger.ts similarity index 100% rename from src/node/consumer/src/lib/logger.js rename to src/node/consumer/src/lib/logger.ts diff --git a/src/node/consumer/src/lib/repository.js b/src/node/consumer/src/lib/repository.js deleted file mode 100644 index 34c2b8c..0000000 --- a/src/node/consumer/src/lib/repository.js +++ /dev/null @@ -1,382 +0,0 @@ -import moment from 'moment'; -import { Sequelize, Op, DataTypes, fn, col, literal } from 'sequelize'; -import { databaseConfig } from './config.js'; -import { logger } from "./logger.js"; -import * as Promises from './promises.js'; - -const database = new Sequelize( - databaseConfig.POSTGRES_URI, - { - logging: false - } -); - -const Provider = database.define('provider', { - name: { type: DataTypes.STRING(32), primaryKey: true }, - lastScraped: { type: DataTypes.DATE }, - lastScrapedId: { type: DataTypes.STRING(128) } -}); - -const IngestedTorrent = database.define('ingested_torrent', { - id: { type: DataTypes.BIGINT, autoIncrement: true, primaryKey: true }, - name: DataTypes.STRING, - source: DataTypes.STRING, - category: DataTypes.STRING, - info_hash: DataTypes.STRING, - size: DataTypes.STRING, - seeders: DataTypes.INTEGER, - leechers: DataTypes.INTEGER, - imdb: DataTypes.STRING, - processed: { - type: DataTypes.BOOLEAN, - defaultValue: false - } -}, - { - indexes: [ - { - unique: true, - fields: ['source', 'info_hash'] - } - ] - }) - -/* eslint-disable no-unused-vars */ -const IngestedPage = database.define('ingested_page', { - id: { type: DataTypes.BIGINT, autoIncrement: true, primaryKey: true }, - url: { type: DataTypes.STRING, allowNull: false }, -}, - { - indexes: [ - { - unique: true, - fields: ['url'] - } - ] - }) -/* eslint-enable no-unused-vars */ - -const Torrent = database.define('torrent', - { - infoHash: { type: DataTypes.STRING(64), primaryKey: true }, - provider: { type: DataTypes.STRING(32), allowNull: false }, - torrentId: { type: DataTypes.STRING(512) }, - title: { type: DataTypes.STRING(512), allowNull: false }, - size: { type: DataTypes.BIGINT }, - type: { type: DataTypes.STRING(16), allowNull: false }, - uploadDate: { type: DataTypes.DATE, allowNull: false }, - seeders: { type: DataTypes.SMALLINT }, - trackers: { type: DataTypes.STRING(8000) }, - languages: { type: DataTypes.STRING(4096) }, - resolution: { type: DataTypes.STRING(16) }, - reviewed: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }, - opened: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false } - } -); - -const File = database.define('file', - { - id: { type: DataTypes.BIGINT, autoIncrement: true, primaryKey: true }, - infoHash: { - type: DataTypes.STRING(64), - allowNull: false, - references: { model: Torrent, key: 'infoHash' }, - onDelete: 'CASCADE' - }, - fileIndex: { type: DataTypes.INTEGER }, - title: { type: DataTypes.STRING(512), allowNull: false }, - size: { type: DataTypes.BIGINT }, - imdbId: { type: DataTypes.STRING(32) }, - imdbSeason: { type: DataTypes.INTEGER }, - imdbEpisode: { type: DataTypes.INTEGER }, - kitsuId: { type: DataTypes.INTEGER }, - kitsuEpisode: { type: DataTypes.INTEGER } - }, - { - indexes: [ - { - unique: true, - name: 'files_unique_file_constraint', - fields: [ - col('infoHash'), - fn('COALESCE', (col('fileIndex')), -1), - fn('COALESCE', (col('imdbId')), 'null'), - fn('COALESCE', (col('imdbSeason')), -1), - fn('COALESCE', (col('imdbEpisode')), -1), - fn('COALESCE', (col('kitsuId')), -1), - fn('COALESCE', (col('kitsuEpisode')), -1) - ] - }, - { unique: false, fields: ['imdbId', 'imdbSeason', 'imdbEpisode'] }, - { unique: false, fields: ['kitsuId', 'kitsuEpisode'] } - ] - } -); - -const Subtitle = database.define('subtitle', - { - infoHash: { - type: DataTypes.STRING(64), - allowNull: false, - references: { model: Torrent, key: 'infoHash' }, - onDelete: 'CASCADE' - }, - fileIndex: { - type: DataTypes.INTEGER, - allowNull: false - }, - fileId: { - type: DataTypes.BIGINT, - allowNull: true, - references: { model: File, key: 'id' }, - onDelete: 'SET NULL' - }, - title: { type: DataTypes.STRING(512), allowNull: false }, - }, - { - timestamps: false, - indexes: [ - { - unique: true, - name: 'subtitles_unique_subtitle_constraint', - fields: [ - col('infoHash'), - col('fileIndex'), - fn('COALESCE', (col('fileId')), -1) - ] - }, - { unique: false, fields: ['fileId'] } - ] - } -); - -const Content = database.define('content', - { - infoHash: { - type: DataTypes.STRING(64), - primaryKey: true, - allowNull: false, - references: { model: Torrent, key: 'infoHash' }, - onDelete: 'CASCADE' - }, - fileIndex: { - type: DataTypes.INTEGER, - primaryKey: true, - allowNull: false - }, - path: { type: DataTypes.STRING(512), allowNull: false }, - size: { type: DataTypes.BIGINT }, - }, - { - timestamps: false, - } -); - -const SkipTorrent = database.define('skip_torrent', { - infoHash: { type: DataTypes.STRING(64), primaryKey: true }, -}); - -Torrent.hasMany(File, { foreignKey: 'infoHash', constraints: false }); -File.belongsTo(Torrent, { foreignKey: 'infoHash', constraints: false }); -Torrent.hasMany(Content, { foreignKey: 'infoHash', constraints: false }); -Content.belongsTo(Torrent, { foreignKey: 'infoHash', constraints: false }); -File.hasMany(Subtitle, { foreignKey: 'fileId', constraints: false }); -Subtitle.belongsTo(File, { foreignKey: 'fileId', constraints: false }); - -export function connect() { - return database.sync({ alter: databaseConfig.AUTO_CREATE_AND_APPLY_MIGRATIONS }) - .catch(error => { - console.error('Failed syncing database: ', error.message); - throw error; - }); - // I'm not convinced this code is needed. If anyone can see where it leads, or what it does, please inform me. - // For now, I'm commenting it out. I don't think we ever reach this at the moment anyway as the previous ENABLE_SYNC - // was always on. - // return Promise.resolve(); -} - -export function getProvider(provider) { - return Provider.findOrCreate({ where: { name: { [Op.eq]: provider.name } }, defaults: provider }) - .then((result) => result[0]) - .catch(() => provider); -} - -export function getTorrent(torrent) { - const where = torrent.infoHash - ? { infoHash: torrent.infoHash } - : { provider: torrent.provider, torrentId: torrent.torrentId } - return Torrent.findOne({ where: where }); -} - -export function getTorrentsBasedOnTitle(titleQuery, type) { - return getTorrentsBasedOnQuery({ title: { [Op.regexp]: `${titleQuery}` }, type: type }); -} - -export function getTorrentsBasedOnQuery(where) { - return Torrent.findAll({ where: where }); -} - -export function getFilesBasedOnQuery(where) { - return File.findAll({ where: where }); -} - -export function getUnprocessedIngestedTorrents() { - return IngestedTorrent.findAll({ - where: { - processed: false, - category: { - [Op.or]: ['tv', 'movies'] - } - }, - - }); -} - -export function setIngestedTorrentsProcessed(ingestedTorrents) { - return Promises.sequence(ingestedTorrents - .map(ingestedTorrent => () => { - ingestedTorrent.processed = true; - return ingestedTorrent.save(); - })); -} - -export function getTorrentsWithoutSize() { - return Torrent.findAll({ - where: literal( - 'exists (select 1 from files where files."infoHash" = torrent."infoHash" and files.size = 300000000)'), - order: [ - ['seeders', 'DESC'] - ] - }); -} - -export function getUpdateSeedersTorrents(limit = 50) { - const until = moment().subtract(7, 'days').format('YYYY-MM-DD'); - return Torrent.findAll({ - where: literal(`torrent."updatedAt" < '${until}'`), - limit: limit, - order: [ - ['seeders', 'DESC'], - ['updatedAt', 'ASC'] - ] - }); -} - -export function getUpdateSeedersNewTorrents(limit = 50) { - const lastUpdate = moment().subtract(12, 'hours').format('YYYY-MM-DD'); - const createdAfter = moment().subtract(4, 'days').format('YYYY-MM-DD'); - return Torrent.findAll({ - where: literal(`torrent."updatedAt" < '${lastUpdate}' AND torrent."createdAt" > '${createdAfter}'`), - limit: limit, - order: [ - ['seeders', 'ASC'], - ['updatedAt', 'ASC'] - ] - }); -} - -export function getNoContentsTorrents() { - return Torrent.findAll({ - where: { opened: false, seeders: { [Op.gte]: 1 } }, - limit: 500, - order: [[fn('RANDOM')]] - }); -} - -export function createTorrent(torrent) { - return Torrent.upsert(torrent) - .then(() => createContents(torrent.infoHash, torrent.contents)) - .then(() => createSubtitles(torrent.infoHash, torrent.subtitles)); -} - -export function setTorrentSeeders(torrent, seeders) { - const where = torrent.infoHash - ? { infoHash: torrent.infoHash } - : { provider: torrent.provider, torrentId: torrent.torrentId } - return Torrent.update( - { seeders: seeders }, - { where: where } - ); -} - -export function deleteTorrent(torrent) { - return Torrent.destroy({ where: { infoHash: torrent.infoHash } }) -} - -export function createFile(file) { - if (file.id) { - return (file.dataValues ? file.save() : File.upsert(file)) - .then(() => upsertSubtitles(file, file.subtitles)); - } - if (file.subtitles && file.subtitles.length) { - file.subtitles = file.subtitles.map(subtitle => ({ infoHash: file.infoHash, title: subtitle.path, ...subtitle })); - } - return File.create(file, { include: [Subtitle], ignoreDuplicates: true }); -} - -export function getFiles(torrent) { - return File.findAll({ where: { infoHash: torrent.infoHash } }); -} - -export function getFilesBasedOnTitle(titleQuery) { - return File.findAll({ where: { title: { [Op.regexp]: `${titleQuery}` } } }); -} - -export function deleteFile(file) { - return File.destroy({ where: { id: file.id } }) -} - -export function createSubtitles(infoHash, subtitles) { - if (subtitles && subtitles.length) { - return Subtitle.bulkCreate(subtitles.map(subtitle => ({ infoHash, title: subtitle.path, ...subtitle }))); - } - return Promise.resolve(); -} - -export function upsertSubtitles(file, subtitles) { - if (file.id && subtitles && subtitles.length) { - return Promises.sequence(subtitles - .map(subtitle => { - subtitle.fileId = file.id; - subtitle.infoHash = subtitle.infoHash || file.infoHash; - subtitle.title = subtitle.title || subtitle.path; - return subtitle; - }) - .map(subtitle => () => subtitle.dataValues ? subtitle.save() : Subtitle.create(subtitle))); - } - return Promise.resolve(); -} - -export function getSubtitles(torrent) { - return Subtitle.findAll({ where: { infoHash: torrent.infoHash } }); -} - -export function getUnassignedSubtitles() { - return Subtitle.findAll({ where: { fileId: null } }); -} - -export function createContents(infoHash, contents) { - if (contents && contents.length) { - return Content.bulkCreate(contents.map(content => ({ infoHash, ...content })), { ignoreDuplicates: true }) - .then(() => Torrent.update({ opened: true }, { where: { infoHash: infoHash }, silent: true })); - } - return Promise.resolve(); -} - -export function getContents(torrent) { - return Content.findAll({ where: { infoHash: torrent.infoHash } }); -} - -export function getSkipTorrent(torrent) { - return SkipTorrent.findByPk(torrent.infoHash) - .then((result) => { - if (!result) { - throw new Error(`torrent not found: ${torrent.infoHash}`); - } - return result.dataValues; - }) -} - -export function createSkipTorrent(torrent) { - return SkipTorrent.upsert({ infoHash: torrent.infoHash }); -} diff --git a/src/node/consumer/src/lib/torrentEntries.js b/src/node/consumer/src/lib/torrentEntries.js index 998b92e..9fb18a2 100644 --- a/src/node/consumer/src/lib/torrentEntries.js +++ b/src/node/consumer/src/lib/torrentEntries.js @@ -2,11 +2,11 @@ import { parse } from 'parse-torrent-title'; import { getImdbId, getKitsuId } from './metadata.js'; import { isPackTorrent } from './parseHelper.js'; import * as Promises from './promises.js'; -import * as repository from './repository.js'; +import { repository } from '../repository/database_repository'; import { parseTorrentFiles } from './torrentFiles.js'; import { assignSubtitles } from './torrentSubtitles.js'; import { TorrentType } from './types.js'; -import {logger} from "./logger.js"; +import {logger} from "./logger"; export async function createTorrentEntry(torrent, overwrite = false) { const titleInfo = parse(torrent.title); diff --git a/src/node/consumer/src/lib/torrentFiles.js b/src/node/consumer/src/lib/torrentFiles.js index 0ac9ea9..68913f9 100644 --- a/src/node/consumer/src/lib/torrentFiles.js +++ b/src/node/consumer/src/lib/torrentFiles.js @@ -9,7 +9,7 @@ import { parseSeriesVideos, isPackTorrent } from './parseHelper.js'; import * as Promises from './promises.js'; import {torrentFiles} from "./torrent.js"; import { TorrentType } from './types.js'; -import {logger} from "./logger.js"; +import {logger} from "./logger"; const MIN_SIZE = 5 * 1024 * 1024; // 5 MB const imdb_limiter = new Bottleneck({ maxConcurrent: metadataConfig.IMDB_CONCURRENT, minTime: metadataConfig.IMDB_INTERVAL_MS }); diff --git a/src/node/consumer/src/lib/trackerService.js b/src/node/consumer/src/lib/trackerService.js index 1af9b13..736630d 100644 --- a/src/node/consumer/src/lib/trackerService.js +++ b/src/node/consumer/src/lib/trackerService.js @@ -1,7 +1,7 @@ import axios from 'axios'; import {cacheTrackers} from "./cache.js"; import { trackerConfig } from './config.js'; -import {logger} from "./logger.js"; +import {logger} from "./logger"; const downloadTrackers = async () => { const response = await axios.get(trackerConfig.TRACKERS_URL); diff --git a/src/node/consumer/src/repository/database_repository.ts b/src/node/consumer/src/repository/database_repository.ts new file mode 100644 index 0000000..7aabd04 --- /dev/null +++ b/src/node/consumer/src/repository/database_repository.ts @@ -0,0 +1,259 @@ +import moment from 'moment'; +import {literal, Op, WhereOptions} from "sequelize"; +import {Model, Sequelize} from 'sequelize-typescript'; +import {databaseConfig} from '../lib/config'; +import * as Promises from '../lib/promises.js'; +import {Provider} from "./models/provider"; +import {File} from "./models/file"; +import {Torrent} from "./models/torrent"; +import {IngestedTorrent} from "./models/ingestedTorrent"; +import {Subtitle} from "./models/subtitle"; +import {Content} from "./models/content"; +import {SkipTorrent} from "./models/skipTorrent"; +import {FileAttributes} from "./interfaces/file_attributes"; +import {TorrentAttributes} from "./interfaces/torrent_attributes"; +import {IngestedPage} from "./models/ingestedPage"; +import {logger} from "../lib/logger"; + +class DatabaseRepository { + private readonly database: Sequelize; + + private models = [ + Torrent, + Provider, + File, + Subtitle, + Content, + SkipTorrent, + IngestedTorrent, + IngestedPage]; + + constructor() { + this.database = this.createDatabase(); + } + + public async connect(): Promise { + try { + await this.database.authenticate(); + logger.info('Database connection has been established successfully.'); + await this.database.sync({alter: true}); + } catch (error) { + logger.error('Failed syncing database: ', error); + throw error; + } + } + + public async getProvider(provider: Provider): Promise { + try { + const [result] = await Provider.findOrCreate({ where: { name: { [Op.eq]: provider.name } }, defaults: provider }); + return result; + } catch { + return provider as Provider; + } + } + + public async getTorrent(torrent: Torrent): Promise { + const where = torrent.infoHash + ? { infoHash: torrent.infoHash } + : { provider: torrent.provider, torrentId: torrent.torrentId }; + return await Torrent.findOne({ where }); + } + + public async getTorrentsBasedOnTitle(titleQuery: string, type: string): Promise { + return this.getTorrentsBasedOnQuery({ title: { [Op.regexp]: `${titleQuery}` }, type }); + } + + public async getTorrentsBasedOnQuery(where: WhereOptions): Promise { + return await Torrent.findAll({ where }); + } + + public async getFilesBasedOnQuery(where: WhereOptions): Promise { + return await File.findAll({ where }); + } + + public async getUnprocessedIngestedTorrents(): Promise { + return await IngestedTorrent.findAll({ + where: { + processed: false, + category: { + [Op.or]: ['tv', 'movies'] + } + }, + }); + } + + public async setIngestedTorrentsProcessed(ingestedTorrents: IngestedTorrent[]): Promise { + await Promises.sequence(ingestedTorrents + .map(ingestedTorrent => async () => { + ingestedTorrent.processed = true; + await ingestedTorrent.save(); + })); + } + + public async getTorrentsWithoutSize(): Promise { + return await Torrent.findAll({ + where: literal( + 'exists (select 1 from files where files."infoHash" = torrent."infoHash" and files.size = 300000000)'), + order: [ + ['seeders', 'DESC'] + ] + }); + } + + public async getUpdateSeedersTorrents(limit = 50): Promise { + const until = moment().subtract(7, 'days').format('YYYY-MM-DD'); + return await Torrent.findAll({ + where: literal(`torrent."updatedAt" < '${until}'`), + limit: limit, + order: [ + ['seeders', 'DESC'], + ['updatedAt', 'ASC'] + ] + }); + } + + public async getUpdateSeedersNewTorrents(limit = 50): Promise { + const lastUpdate = moment().subtract(12, 'hours').format('YYYY-MM-DD'); + const createdAfter = moment().subtract(4, 'days').format('YYYY-MM-DD'); + return await Torrent.findAll({ + where: literal(`torrent."updatedAt" < '${lastUpdate}' AND torrent."createdAt" > '${createdAfter}'`), + limit: limit, + order: [ + ['seeders', 'ASC'], + ['updatedAt', 'ASC'] + ] + }); + } + + public async getNoContentsTorrents(): Promise { + return await Torrent.findAll({ + where: { opened: false, seeders: { [Op.gte]: 1 } }, + limit: 500, + order: literal('random()') + }); + } + + public async createTorrent(torrent: Torrent): Promise { + await Torrent.upsert(torrent); + await this.createContents(torrent.infoHash, torrent.contents); + await this.createSubtitles(torrent.infoHash, torrent.subtitles); + } + + public async setTorrentSeeders(torrent: TorrentAttributes, seeders: number): Promise<[number]> { + const where = torrent.infoHash + ? { infoHash: torrent.infoHash } + : { provider: torrent.provider, torrentId: torrent.torrentId }; + + return await Torrent.update( + { seeders: seeders }, + { where: where } + ); + } + + public async deleteTorrent(torrent: TorrentAttributes): Promise { + return await Torrent.destroy({ where: { infoHash: torrent.infoHash } }); + } + + public async createFile(file: File): Promise { + if (file.id) { + if (file.dataValues) { + await file.save(); + } else { + await File.upsert(file); + } + await this.upsertSubtitles(file, file.subtitles); + } else { + if (file.subtitles && file.subtitles.length) { + file.subtitles = file.subtitles.map(subtitle => { + subtitle.title = subtitle.path; + return subtitle; + }); + } + await File.create(file, { include: [Subtitle], ignoreDuplicates: true }); + } + } + + public async getFiles(torrent: Torrent): Promise { + return File.findAll({ where: { infoHash: torrent.infoHash } }); + } + + public async getFilesBasedOnTitle(titleQuery: string): Promise { + return File.findAll({ where: { title: { [Op.regexp]: `${titleQuery}` } } }); + } + + public async deleteFile(file: File): Promise { + return File.destroy({ where: { id: file.id } }); + } + + public async createSubtitles(infoHash: string, subtitles: Subtitle[]): Promise[]> { + if (subtitles && subtitles.length) { + return Subtitle.bulkCreate(subtitles.map(subtitle => ({ infoHash, title: subtitle.path, ...subtitle }))); + } + return Promise.resolve(); + } + + public async upsertSubtitles(file: File, subtitles: Subtitle[]): Promise { + if (file.id && subtitles && subtitles.length) { + await Promises.sequence(subtitles + .map(subtitle => { + subtitle.fileId = file.id; + subtitle.infoHash = subtitle.infoHash || file.infoHash; + subtitle.title = subtitle.title || subtitle.path; + return subtitle; + }) + .map(subtitle => async () => { + if (subtitle.dataValues) { + await subtitle.save(); + } else { + await Subtitle.create(subtitle); + } + })); + } + } + + public async getSubtitles(torrent: Torrent): Promise { + return Subtitle.findAll({ where: { infoHash: torrent.infoHash } }); + } + + public async getUnassignedSubtitles(): Promise { + return Subtitle.findAll({ where: { fileId: null } }); + } + + public async createContents(infoHash: string, contents: Content[]): Promise { + if (contents && contents.length) { + await Content.bulkCreate(contents.map(content => ({ infoHash, ...content })), { ignoreDuplicates: true }); + await Torrent.update({ opened: true }, { where: { infoHash: infoHash }, silent: true }); + } + } + + public async getContents(torrent: Torrent): Promise { + return Content.findAll({ where: { infoHash: torrent.infoHash } }); + } + + public async getSkipTorrent(torrent: Torrent): Promise { + const result = await SkipTorrent.findByPk(torrent.infoHash); + if (!result) { + throw new Error(`torrent not found: ${torrent.infoHash}`); + } + return result.dataValues as SkipTorrent; + } + + public async createSkipTorrent(torrent: Torrent): Promise<[SkipTorrent, boolean]> { + return SkipTorrent.upsert({ infoHash: torrent.infoHash }); + } + + private createDatabase(): Sequelize { + const newDatabase = new Sequelize( + databaseConfig.POSTGRES_URI, + { + logging: false + } + ); + + newDatabase.addModels(this.models); + + return newDatabase; + } +} + +export const repository = new DatabaseRepository(); diff --git a/src/node/consumer/src/repository/interfaces/content_attributes.ts b/src/node/consumer/src/repository/interfaces/content_attributes.ts new file mode 100644 index 0000000..c7d8e85 --- /dev/null +++ b/src/node/consumer/src/repository/interfaces/content_attributes.ts @@ -0,0 +1,11 @@ +import {Optional} from "sequelize"; + +export interface ContentAttributes { + infoHash: string; + fileIndex: number; + path: string; + size: number; +} + +export interface ContentCreationAttributes extends Optional { +} \ No newline at end of file diff --git a/src/node/consumer/src/repository/interfaces/file_attributes.ts b/src/node/consumer/src/repository/interfaces/file_attributes.ts new file mode 100644 index 0000000..4cb68ea --- /dev/null +++ b/src/node/consumer/src/repository/interfaces/file_attributes.ts @@ -0,0 +1,19 @@ +import {Optional} from "sequelize"; +import {SubtitleAttributes} from "./subtitle_attributes"; + +export interface FileAttributes { + id?: number; + infoHash: string; + fileIndex: number; + title: string; + size: number; + imdbId: string; + imdbSeason: number; + imdbEpisode: number; + kitsuId: number; + kitsuEpisode: number; + subtitles?: SubtitleAttributes[]; +} + +export interface FileCreationAttributes extends Optional { +} \ No newline at end of file diff --git a/src/node/consumer/src/repository/interfaces/ingested_page_attributes.ts b/src/node/consumer/src/repository/interfaces/ingested_page_attributes.ts new file mode 100644 index 0000000..21bf9a6 --- /dev/null +++ b/src/node/consumer/src/repository/interfaces/ingested_page_attributes.ts @@ -0,0 +1,6 @@ +export interface IngestedPageAttributes { + url: string; +} + +export interface IngestedPageCreationAttributes extends IngestedPageAttributes { +} \ No newline at end of file diff --git a/src/node/consumer/src/repository/interfaces/ingested_torrent_attributes.ts b/src/node/consumer/src/repository/interfaces/ingested_torrent_attributes.ts new file mode 100644 index 0000000..9dbe491 --- /dev/null +++ b/src/node/consumer/src/repository/interfaces/ingested_torrent_attributes.ts @@ -0,0 +1,16 @@ +import {Optional} from "sequelize"; + +export interface IngestedTorrentAttributes { + name: string; + source: string; + category: string; + info_hash: string; + size: string; + seeders: number; + leechers: number; + imdb: string; + processed: boolean; +} + +export interface IngestedTorrentCreationAttributes extends Optional { +} \ No newline at end of file diff --git a/src/node/consumer/src/repository/interfaces/provider_attributes.ts b/src/node/consumer/src/repository/interfaces/provider_attributes.ts new file mode 100644 index 0000000..c175caa --- /dev/null +++ b/src/node/consumer/src/repository/interfaces/provider_attributes.ts @@ -0,0 +1,10 @@ +import {Optional} from "sequelize"; + +export interface ProviderAttributes { + name: string; + lastScraped: Date; + lastScrapedId: string; +} + +export interface ProviderCreationAttributes extends Optional { +} \ No newline at end of file diff --git a/src/node/consumer/src/repository/interfaces/skip_torrent_attributes.ts b/src/node/consumer/src/repository/interfaces/skip_torrent_attributes.ts new file mode 100644 index 0000000..9b843da --- /dev/null +++ b/src/node/consumer/src/repository/interfaces/skip_torrent_attributes.ts @@ -0,0 +1,8 @@ +import {Optional} from "sequelize"; + +export interface SkipTorrentAttributes { + infoHash: string; +} + +export interface SkipTorrentCreationAttributes extends Optional { +} \ No newline at end of file diff --git a/src/node/consumer/src/repository/interfaces/subtitle_attributes.ts b/src/node/consumer/src/repository/interfaces/subtitle_attributes.ts new file mode 100644 index 0000000..6e4a28b --- /dev/null +++ b/src/node/consumer/src/repository/interfaces/subtitle_attributes.ts @@ -0,0 +1,12 @@ +import {Optional} from "sequelize"; + +export interface SubtitleAttributes { + infoHash: string; + fileIndex: number; + fileId?: number; + title: string; + path: string; +} + +export interface SubtitleCreationAttributes extends Optional { +} \ No newline at end of file diff --git a/src/node/consumer/src/repository/interfaces/torrent_attributes.ts b/src/node/consumer/src/repository/interfaces/torrent_attributes.ts new file mode 100644 index 0000000..0fcef0b --- /dev/null +++ b/src/node/consumer/src/repository/interfaces/torrent_attributes.ts @@ -0,0 +1,26 @@ +import {Optional} from "sequelize"; +import {ContentAttributes} from "./content_attributes"; +import {SubtitleAttributes} from "./subtitle_attributes"; +import {FileAttributes} from "./file_attributes"; + +export interface TorrentAttributes { + infoHash: string; + provider: string; + torrentId: string; + title: string; + size: number; + type: string; + uploadDate: Date; + seeders: number; + trackers: string; + languages: string; + resolution: string; + reviewed: boolean; + opened: boolean; + contents: ContentAttributes[]; + files: FileAttributes[]; + subtitles?: SubtitleAttributes[]; +} + +export interface TorrentCreationAttributes extends Optional { +} \ No newline at end of file diff --git a/src/node/consumer/src/repository/models/content.ts b/src/node/consumer/src/repository/models/content.ts new file mode 100644 index 0000000..7e70d7e --- /dev/null +++ b/src/node/consumer/src/repository/models/content.ts @@ -0,0 +1,22 @@ +import {Table, Column, Model, HasMany, DataType, BelongsTo, ForeignKey} from 'sequelize-typescript'; +import {ContentAttributes, ContentCreationAttributes} from "../interfaces/content_attributes"; +import {Torrent} from "./torrent"; + +@Table({modelName: 'content', timestamps: false}) +export class Content extends Model { + @Column({ type: DataType.STRING(64), primaryKey: true, allowNull: false, onDelete: 'CASCADE' }) + @ForeignKey(() => Torrent) + declare infoHash: string; + + @Column({ type: DataType.INTEGER, primaryKey: true, allowNull: false }) + declare fileIndex: number; + + @Column({ type: DataType.STRING(512), allowNull: false }) + declare path: string; + + @Column({ type: DataType.BIGINT }) + declare size: number; + + @BelongsTo(() => Torrent, { constraints: false, foreignKey: 'infoHash' }) + torrent: Torrent; +} \ No newline at end of file diff --git a/src/node/consumer/src/repository/models/file.ts b/src/node/consumer/src/repository/models/file.ts new file mode 100644 index 0000000..2383a78 --- /dev/null +++ b/src/node/consumer/src/repository/models/file.ts @@ -0,0 +1,60 @@ +import {Table, Column, Model, HasMany, DataType, BelongsTo, ForeignKey} from 'sequelize-typescript'; +import {FileAttributes, FileCreationAttributes} from "../interfaces/file_attributes"; +import {Torrent} from "./torrent"; +import {Subtitle} from "./subtitle"; +import {SubtitleAttributes} from "../interfaces/subtitle_attributes"; + +const indexes = [ + { + unique: true, + name: 'files_unique_file_constraint', + fields: [ + 'infoHash', + 'fileIndex', + 'imdbId', + 'imdbSeason', + 'imdbEpisode', + 'kitsuId', + 'kitsuEpisode' + ] + }, + { unique: false, fields: ['imdbId', 'imdbSeason', 'imdbEpisode'] }, + { unique: false, fields: ['kitsuId', 'kitsuEpisode'] } +]; + +@Table({modelName: 'file', timestamps: true, indexes: indexes }) +export class File extends Model { + @Column({ type: DataType.STRING(64), allowNull: false, onDelete: 'CASCADE' }) + @ForeignKey(() => Torrent) + declare infoHash: string; + + @Column({ type: DataType.INTEGER}) + declare fileIndex: number; + + @Column({ type: DataType.STRING(512), allowNull: false }) + declare title: string; + + @Column({ type: DataType.BIGINT }) + declare size: number; + + @Column({ type: DataType.STRING(32) }) + declare imdbId: string; + + @Column({ type: DataType.INTEGER }) + declare imdbSeason: number; + + @Column({ type: DataType.INTEGER }) + declare imdbEpisode: number; + + @Column({ type: DataType.INTEGER }) + declare kitsuId: number; + + @Column({ type: DataType.INTEGER }) + declare kitsuEpisode: number; + + @HasMany(() => Subtitle, { constraints: false, foreignKey: 'fileId'}) + declare subtitles?: Subtitle[]; + + @BelongsTo(() => Torrent, { constraints: false, foreignKey: 'infoHash' }) + torrent: Torrent; +} \ No newline at end of file diff --git a/src/node/consumer/src/repository/models/ingestedPage.ts b/src/node/consumer/src/repository/models/ingestedPage.ts new file mode 100644 index 0000000..6504344 --- /dev/null +++ b/src/node/consumer/src/repository/models/ingestedPage.ts @@ -0,0 +1,16 @@ +import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript'; +import {IngestedPageAttributes, IngestedPageCreationAttributes} from "../interfaces/ingested_page_attributes"; + +const indexes = [ + { + unique: true, + name: 'ingested_page_unique_url_constraint', + fields: ['url'] + } +]; + +@Table({modelName: 'ingested_page', timestamps: true, indexes: indexes}) +export class IngestedPage extends Model { + @Column({ type: DataType.STRING(512), allowNull: false }) + declare url: string; +} \ No newline at end of file diff --git a/src/node/consumer/src/repository/models/ingestedTorrent.ts b/src/node/consumer/src/repository/models/ingestedTorrent.ts new file mode 100644 index 0000000..5a21a4a --- /dev/null +++ b/src/node/consumer/src/repository/models/ingestedTorrent.ts @@ -0,0 +1,40 @@ +import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript'; +import {IngestedTorrentAttributes, IngestedTorrentCreationAttributes} from "../interfaces/ingested_torrent_attributes"; + +const indexes = [ + { + unique: true, + name: 'ingested_torrent_unique_source_info_hash_constraint', + fields: ['source', 'info_hash'] + } +]; + +@Table({modelName: 'ingested_torrent', timestamps: true, indexes: indexes}) +export class IngestedTorrent extends Model { + @Column({ type: DataType.STRING(512) }) + declare name: string; + + @Column({ type: DataType.STRING(512) }) + declare source: string; + + @Column({ type: DataType.STRING(32) }) + declare category: string; + + @Column({ type: DataType.STRING(64) }) + declare info_hash: string; + + @Column({ type: DataType.STRING(32) }) + declare size: string; + + @Column({ type: DataType.INTEGER }) + declare seeders: number; + + @Column({ type: DataType.INTEGER }) + declare leechers: number; + + @Column({ type: DataType.STRING(32) }) + declare imdb: string; + + @Column({ type: DataType.BOOLEAN, defaultValue: false }) + declare processed: boolean; +} \ No newline at end of file diff --git a/src/node/consumer/src/repository/models/provider.ts b/src/node/consumer/src/repository/models/provider.ts new file mode 100644 index 0000000..26a8bfc --- /dev/null +++ b/src/node/consumer/src/repository/models/provider.ts @@ -0,0 +1,15 @@ +import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript'; +import {ProviderAttributes, ProviderCreationAttributes} from "../interfaces/provider_attributes"; + +@Table({modelName: 'provider', timestamps: false}) +export class Provider extends Model { + + @Column({ type: DataType.STRING(32), primaryKey: true }) + declare name: string; + + @Column({ type: DataType.DATE }) + declare lastScraped: Date; + + @Column({ type: DataType.STRING(128) }) + declare lastScrapedId: string; +} \ No newline at end of file diff --git a/src/node/consumer/src/repository/models/skipTorrent.ts b/src/node/consumer/src/repository/models/skipTorrent.ts new file mode 100644 index 0000000..101ce68 --- /dev/null +++ b/src/node/consumer/src/repository/models/skipTorrent.ts @@ -0,0 +1,10 @@ +import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript'; +import {SkipTorrentAttributes, SkipTorrentCreationAttributes} from "../interfaces/skip_torrent_attributes"; + + +@Table({modelName: 'skip_torrent', timestamps: false}) +export class SkipTorrent extends Model { + + @Column({ type: DataType.STRING(64), primaryKey: true }) + declare infoHash: string; +} \ No newline at end of file diff --git a/src/node/consumer/src/repository/models/subtitle.ts b/src/node/consumer/src/repository/models/subtitle.ts new file mode 100644 index 0000000..0a4157c --- /dev/null +++ b/src/node/consumer/src/repository/models/subtitle.ts @@ -0,0 +1,39 @@ +import {Table, Column, Model, HasMany, DataType, BelongsTo, ForeignKey} from 'sequelize-typescript'; +import {SubtitleAttributes, SubtitleCreationAttributes} from "../interfaces/subtitle_attributes"; +import {File} from "./file"; +import {Torrent} from "./torrent"; + +const indexes = [ + { + unique: true, + name: 'subtitles_unique_subtitle_constraint', + fields: [ + 'infoHash', + 'fileIndex', + 'fileId' + ] + }, + { unique: false, fields: ['fileId'] } +]; + +@Table({modelName: 'subtitle', timestamps: false, indexes: indexes}) +export class Subtitle extends Model { + + @Column({ type: DataType.STRING(64), allowNull: false, onDelete: 'CASCADE' }) + declare infoHash: string; + + @Column({ type: DataType.INTEGER, allowNull: false }) + declare fileIndex: number; + + @Column({ type: DataType.BIGINT, allowNull: true, onDelete: 'SET NULL' }) + @ForeignKey(() => File) + declare fileId?: number | null; + + @Column({ type: DataType.STRING(512), allowNull: false }) + declare title: string; + + @BelongsTo(() => File, { constraints: false, foreignKey: 'fileId' }) + file: File; + + path: string; +} \ No newline at end of file diff --git a/src/node/consumer/src/repository/models/torrent.ts b/src/node/consumer/src/repository/models/torrent.ts new file mode 100644 index 0000000..b6b7a80 --- /dev/null +++ b/src/node/consumer/src/repository/models/torrent.ts @@ -0,0 +1,56 @@ +import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript'; +import {TorrentAttributes, TorrentCreationAttributes} from "../interfaces/torrent_attributes"; +import {Content} from "./content"; +import {File} from "./file"; +import {Subtitle} from "./subtitle"; + +@Table({modelName: 'torrent', timestamps: true}) + +export class Torrent extends Model { + @Column({type: DataType.STRING(64), primaryKey: true}) + declare infoHash: string; + + @Column({type: DataType.STRING(32), allowNull: false}) + declare provider: string; + + @Column({type: DataType.STRING(512)}) + declare torrentId: string; + + @Column({type: DataType.STRING(512), allowNull: false}) + declare title: string; + + @Column({type: DataType.BIGINT}) + declare size: number; + + @Column({type: DataType.STRING(16), allowNull: false}) + declare type: string; + + @Column({type: DataType.DATE, allowNull: false}) + declare uploadDate: Date; + + @Column({type: DataType.SMALLINT}) + declare seeders: number; + + @Column({type: DataType.STRING(8000)}) + declare trackers: string; + + @Column({type: DataType.STRING(4096)}) + declare languages: string; + + @Column({type: DataType.STRING(16)}) + declare resolution: string; + + @Column({type: DataType.BOOLEAN, allowNull: false, defaultValue: false}) + declare reviewed: boolean; + + @Column({type: DataType.BOOLEAN, allowNull: false, defaultValue: false}) + declare opened: boolean; + + @HasMany(() => Content, { foreignKey: 'infoHash', constraints: false }) + contents?: Content[]; + + @HasMany(() => File, { foreignKey: 'infoHash', constraints: false }) + files?: File[]; + + subtitles?: Subtitle[]; +} \ No newline at end of file diff --git a/src/node/consumer/jsconfig.json b/src/node/consumer/tsconfig.json similarity index 64% rename from src/node/consumer/jsconfig.json rename to src/node/consumer/tsconfig.json index 6b1c407..c6f83d1 100644 --- a/src/node/consumer/jsconfig.json +++ b/src/node/consumer/tsconfig.json @@ -3,7 +3,7 @@ "baseUrl": "./src", "checkJs": true, "isolatedModules": true, - "lib": ["es6"], + "lib": ["ESNext"], "module": "ESNext", "moduleResolution": "node", "outDir": "./dist", @@ -13,9 +13,12 @@ "rootDir": "./src", "skipLibCheck": true, "sourceMap": true, - "target": "ES6", + "target": "ESNext", "types": ["node"], - "typeRoots": ["node_modules/@types", "src/@types"] + "typeRoots": ["node_modules/@types", "src/@types"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "allowSyntheticDefaultImports": true }, "exclude": ["node_modules"] } \ No newline at end of file