Browse Source

Seeker successfully adds movies to the torrent client

master
David Ludwig 4 years ago
parent
commit
00dfa2e644
32 changed files with 2912 additions and 7 deletions
  1. +14
    -0
      .env
  2. +14
    -0
      .env.example
  3. +117
    -0
      .gitignore
  4. +1
    -1
      nodemon.json
  5. +9
    -1
      package.json
  6. +104
    -0
      src/Application.ts
  7. +26
    -0
      src/database/entities/MovieInfo.ts
  8. +11
    -0
      src/database/entities/MovieQuota.ts
  9. +42
    -0
      src/database/entities/MovieTicket.ts
  10. +15
    -0
      src/database/entities/MovieTorrent.ts
  11. +39
    -0
      src/database/entities/RegisterToken.ts
  12. +32
    -0
      src/database/entities/User.ts
  13. +6
    -0
      src/database/entities/index.ts
  14. +7
    -0
      src/index.ts
  15. +49
    -0
      src/services/Database.ts
  16. +85
    -0
      src/services/IpcInterface.ts
  17. +75
    -0
      src/services/MovieSearch.ts
  18. +58
    -0
      src/services/Service.ts
  19. +97
    -0
      src/services/Supervisor.ts
  20. +221
    -0
      src/services/TorrentClientIpc.ts
  21. +13
    -0
      src/services/index.ts
  22. +63
    -0
      src/torrents/Torrent.ts
  23. +5
    -0
      src/torrents/index.ts
  24. +153
    -0
      src/torrents/parsing.ts
  25. +25
    -0
      src/torrents/providers/Provider.ts
  26. +30
    -0
      src/torrents/providers/torrentgalaxy/index.ts
  27. +135
    -0
      src/torrents/providers/torrentgalaxy/search.ts
  28. +58
    -0
      src/torrents/ranking.ts
  29. +82
    -0
      src/torrents/util.ts
  30. +350
    -0
      src/typings/node-ipc/index.d.ts
  31. +71
    -0
      tsconfig.json
  32. +905
    -5
      yarn.lock

+ 14
- 0
.env View File

@ -0,0 +1,14 @@
# Seeker socket path
IPC_SOCKET_PATH = /tmp/seeker.sock
# Database -----------------------------------------------------------------------------------------
DB_TYPE = mysql
DB_HOST = database
DB_PORT = 3306
DB_USER = root
DB_PASSWORD_FILE = /run/secrets/mysql_root_password
DB_DATABASE = autoplex_request
# Torrent client IPC socket path
TORRENT_CLIENT_IPC_SOCKET = /tmp/torrent_client.sock

+ 14
- 0
.env.example View File

@ -0,0 +1,14 @@
# Seeker socket path
IPC_SOCKET_PATH = /tmp/seeker.sock
# Database -----------------------------------------------------------------------------------------
DB_TYPE = mysql
DB_HOST = database
DB_PORT = 3306
DB_USER = root
DB_PASSWORD_FILE = /run/secrets/mysql_root_password
DB_DATABASE = autoplex_request
# Torrent client IPC socket path
TORRENT_CLIENT_IPC_SOCKET = /tmp/torrent_client.sock

+ 117
- 0
.gitignore View File

@ -0,0 +1,117 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
.data/
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

+ 1
- 1
nodemon.json View File

@ -2,7 +2,7 @@
"watch": ["src"],
"ext": "ts,json",
"ignore": ["src/**/*.spec.ts"],
"exec": "node --inspect=0.0.0.0:9230 -r tsconfig-paths/register -r ts-node/register src/index.ts",
"exec": "node --inspect=0.0.0.0:9230 -r ts-node/register src/index.ts",
"events": {
"start": "clear"
}


+ 9
- 1
package.json View File

@ -12,10 +12,18 @@
"start:dev": "nodemon"
},
"devDependencies": {
"@types/node": "^14.14.41",
"@types/xml2js": "^0.4.8",
"nodemon": "^2.0.7",
"rimraf": "^3.0.2",
"ts-node": "^9.1.1",
"typescript": "^4.2.4"
},
"dependencies": {
"typeorm": "^0.2.32"
"cheerio": "^1.0.0-rc.6",
"mysql": "^2.18.1",
"node-ipc": "^9.1.4",
"typeorm": "^0.2.32",
"xml2js": "^0.4.23"
}
}

+ 104
- 0
src/Application.ts View File

@ -0,0 +1,104 @@
import services from "./services";
import Service from "./services/Service";
import assert from "assert";
interface ServiceMap {
[name: string]: Service
}
/**
* The main application class
*/
export default class Application
{
private static __instance: Application;
/**
* All available services
*/
protected services: ServiceMap = {};
/**
* Return the current application instance
*/
public static instance() {
return this.__instance;
}
/**
* Create a new application instance
*/
public constructor() {
Application.__instance = this;
for (let ServiceClass of Object.values(services)) {
this.installService(ServiceClass);
}
}
/**
* Install a service into the application
*/
protected installService(ServiceClass: new (app: Application) => Service) {
let service = new ServiceClass(this);
this.services[service.name] = service;
}
/**
* Boot the application and all of the services
*/
protected async boot() {
let services = Object.values(this.services);
return Promise.all(services.map(service => service.boot()));
}
/**
* Initialize the application if necessary
*/
protected async initialize() {
}
/**
* Shutdown the application
*/
protected shutdown() {
let services = Object.values(this.services);
return Promise.all(services.map(service => service.shutdown()));
}
/**
* Start the application
*/
public async start() {
await this.boot();
await this.initialize();
for (let service of Object.values(this.services)) {
service.start();
}
}
/**
* Quit the application
*/
public async quit(code: number = 0) {
await this.shutdown();
process.exit(code);
}
// Access --------------------------------------------------------------------------------------
/**
* Get all available services
*/
public serviceList() {
return Object.keys(this.services);
}
/**
* Get an application service instance
*/
public service<T extends Service>(serviceName: string) {
assert(serviceName in this.services);
return <T>this.services[serviceName];
}
}

+ 26
- 0
src/database/entities/MovieInfo.ts View File

@ -0,0 +1,26 @@
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";
@Entity()
export class MovieInfo extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column({ unique: true })
tmdbId!: number;
@Column({ type: "text", nullable: true })
overview!: string | null;
@Column({ type: "int", nullable: true })
runtime!: number | null;
@Column({ type: "char", length: 10, nullable: true })
releaseDate!: string | null;
@Column({ type: "varchar", length: 32, nullable: true })
backdropPath!: string | null;
@Column({ type: "varchar", length: 32, nullable: true })
posterPath!: string | null;
}

+ 11
- 0
src/database/entities/MovieQuota.ts View File

@ -0,0 +1,11 @@
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class MovieQuota extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column({ default: 5 })
moviesPerWeek!: number;
}

+ 42
- 0
src/database/entities/MovieTicket.ts View File

@ -0,0 +1,42 @@
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne, OneToMany, OneToOne, JoinColumn, CreateDateColumn } from "typeorm";
import { MovieInfo } from "./MovieInfo";
import { MovieTorrent } from "./MovieTorrent";
import { User } from "./User";
@Entity()
export class MovieTicket extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: "varchar", length: 27, nullable: true })
imdbId!: string | null;
@Column({ type: "varchar" })
title!: string;
@Column({ type: "year", nullable: true })
year!: number | null;
@CreateDateColumn()
createdAt!: Date;
@Column({ default: false })
isFulfilled!: boolean;
@Column({ default: false })
isCanceled!: boolean;
@Column({ default: false })
isStale!: boolean;
@ManyToOne(() => User, user => user.movieTickets)
user!: User;
@OneToMany(() => MovieTorrent, torrent => torrent.movieTicket)
torrents!: MovieTorrent[];
@OneToOne(() => MovieInfo, { nullable: true })
@JoinColumn()
info!: MovieInfo | null;
}

+ 15
- 0
src/database/entities/MovieTorrent.ts View File

@ -0,0 +1,15 @@
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne } from "typeorm";
import { MovieTicket } from "./MovieTicket";
@Entity()
export class MovieTorrent extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column()
infoHash!: string;
@ManyToOne(() => MovieTicket, ticket => ticket.torrents)
movieTicket!: MovieTicket;
}

+ 39
- 0
src/database/entities/RegisterToken.ts View File

@ -0,0 +1,39 @@
import { randomBytes } from "crypto";
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";
@Entity()
export class RegisterToken extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column()
token!: string
/**
* Check if the provided token is valid
*/
public static async isValid(token: string) {
if (typeof token !== "string") {
return false;
}
return Boolean(token) && await RegisterToken.count({token}) > 0;
}
/**
* Create a new registration token and insert it into the database
*/
public static generate() {
return new Promise<RegisterToken>((resolve, reject) => {
randomBytes(48, async (err, result) => {
if (err) {
reject(err);
} else {
let token = new RegisterToken();
token.token = result.toString("hex");
resolve(await token.save());
}
});
});
}
}

+ 32
- 0
src/database/entities/User.ts View File

@ -0,0 +1,32 @@
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, OneToMany, OneToOne, JoinColumn, CreateDateColumn } from "typeorm";
import { MovieTicket } from "./MovieTicket";
import { MovieQuota } from "./MovieQuota";
@Entity()
export class User extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column()
isAdmin!: boolean;
@Column({ length: 50 })
name!: string;
@Column({ length: 255 })
email!: string;
@Column({ type: "char", length: 60 })
password!: string;
@CreateDateColumn()
createdAt!: Date;
@OneToOne(() => MovieQuota, { nullable: true })
@JoinColumn()
quota!: MovieQuota;
@OneToMany(() => User, user => user.movieTickets)
movieTickets!: MovieTicket[];
}

+ 6
- 0
src/database/entities/index.ts View File

@ -0,0 +1,6 @@
export * from "./MovieInfo";
export * from "./MovieQuota";
export * from "./MovieTicket";
export * from "./MovieTorrent";
export * from "./RegisterToken";
export * from "./User";

+ 7
- 0
src/index.ts View File

@ -0,0 +1,7 @@
import Application from "./Application";
// Create a new application instance
let app = new Application();
// Start the application
app.start();

+ 49
- 0
src/services/Database.ts View File

@ -0,0 +1,49 @@
import { Connection, createConnection } from "typeorm";
import * as entities from "../database/entities";
import Service from "./Service";
import { readFile } from "fs/promises";
import Application from "../Application";
export default class Database extends Service
{
/**
* The active database connection
*/
protected connection!: Connection;
/**
* Create a new database instance
*/
public constructor(app: Application) {
super("Database", app);
}
/**
* Boot the database service
*/
public async boot() {
// Fetch the database password from the secret file
let password = (await readFile(<string>process.env["DB_PASSWORD_FILE"])).toString().trim();
// Create the database connection
await createConnection({
type : <"mysql" | "mariadb">process.env["DB_TYPE"],
host : process.env["DB_HOST"],
port : parseInt(<string>process.env["DB_PORT"]),
username : process.env["DB_USER"],
password : password,
database : process.env["DB_DATABASE"],
// synchronize: process.env["NODE_ENV"] != "production",
synchronize: false,
entities : Object.values(entities),
migrations : ["src/migrations/*.ts"]
});
}
/**
* Shutdown the database service
*/
public async shutdown() {
await this.connection.close();
}
}

+ 85
- 0
src/services/IpcInterface.ts View File

@ -0,0 +1,85 @@
import ipc from "node-ipc";
import type { Server } from "node-ipc";
import { Socket } from "net";
import Service from "./Service";
import Application from "../Application";
import MovieSearch from "./MovieSearch";
import Supervisor from "./Supervisor";
import { MovieTicket } from "../database/entities";
export default class IpcInterface extends Service
{
/**
* Quick reference to the IPC server
*/
protected server!: Server;
/**
* Create a new IPC interface
*/
public constructor(app: Application) {
super("IPC", app);
ipc.config.id = "seeker";
ipc.config.retry = 1500;
ipc.config.silent = true;
}
/**
* Boot the IPC interface
*/
public boot() {
return new Promise<void>((resolve, reject) => {
console.log("Serving:", process.env["IPC_SOCKET_PATH"]);
ipc.serve(<string>process.env["IPC_SOCKET_PATH"], () => {
this.server = ipc.server;
this.installEventHandlers(this.server);
resolve();
});
ipc.server.start();
});
}
public async shutdown() {
if (this.server) {
this.server.stop();
}
}
/**
* Install the the event handlers
*/
protected installEventHandlers(server: Server) {
this.addEventHandler(server, "search_movie", this.searchMovie);
}
/**
* Handle a specific event
*/
protected addEventHandler(server: Server, method: string, handle: (...args: any[]) => Promise<any>) {
server.on(method, async (message: any, socket: Socket) => {
try {
let response = await handle.apply(this, [message]);
this.server.emit(socket, method, { response });
} catch (error) {
console.log("Error:", method, error);
this.server.emit(socket, method, {
response: undefined,
error
});
}
});
}
// Interface Methods ---------------------------------------------------------------------------
/**
* Invoked when a new Movie ticket has been created
*/
protected async searchMovie(ticketId: number) {
let movie = await MovieTicket.findOne(ticketId);
if (movie === undefined) {
return null;
}
this.app.service<Supervisor>("Supervisor").searchMovie(movie);
}
}

+ 75
- 0
src/services/MovieSearch.ts View File

@ -0,0 +1,75 @@
import Application from "../Application";
import { MovieTicket } from "../database/entities";
import * as providerClasses from "../torrents";
import Provider, { MediaType } from "../torrents/providers/Provider";
import Torrent from "../torrents/Torrent";
import { rankTorrents } from "../torrents/ranking";
import Service from "./Service";
export default class MovieSearch extends Service
{
/**
* Available providers that support movies
*/
protected providers!: Provider[];
/**
* Create a new instance of the movie search service
*/
public constructor(app: Application) {
super("Movie Search", app);
}
/**
* Boot the movie search service
*/
public async boot() {
let providers = Object.values(providerClasses);
this.providers = providers.filter(provider => provider.PROVIDES & MediaType.Movies)
.map(ProviderClass => new ProviderClass());
}
/**
* Shutdown the service
*/
public async shutdown() {
}
/**
* Search for a movie
*/
public async searchMovie(movie: MovieTicket) {
// Search by IMDb
let torrents = await this.searchImdb(movie);
if (torrents.length == 0) {
return null;
}
// Determine the preferred torrents
let preferredTorrents = rankTorrents(torrents);
if (preferredTorrents.length == 0) {
preferredTorrents = torrents;
}
// Return the selected torrent
this.log("Found movie torrent for", movie.title);
return await preferredTorrents[0].downloadLink();
}
/**
* Search for a movie by its IMDb ID
*/
protected async searchImdb(movie: MovieTicket): Promise<Torrent[]> {
if (movie.imdbId == null) {
return [];
}
let results = await Promise.all(this.providers.map(provider => provider.searchMovie(movie)));
return (<Torrent[]>[]).concat(...results);
}
/**
* Pick the best torrent from the list
*/
protected pickBestTorrent() {
}
}

+ 58
- 0
src/services/Service.ts View File

@ -0,0 +1,58 @@
import Application from "../Application";
export default abstract class Service
{
/**
* The name of the service
*/
public readonly name: string;
/**
* The application instance
*/
protected readonly app: Application;
/**
* Enable/disable logging for this service
*/
public logging: boolean = true;
/**
* Create a new service
*/
public constructor(name: string, app: Application) {
this.app = app;
this.name = name;
}
// Required Service Implementation -------------------------------------------------------------
/**
* Boot the service
*/
public abstract boot(): Promise<void>;
/**
* Shut the application down
*/
public abstract shutdown(): Promise<void>;
// Miscellaneous ------------------------------------------------------------------------------
/**
* Indicate the application is ready
*/
public start() {
// no-op
};
/**
* Service-specific logging
*/
public log(...args: any[]) {
if (!this.logging) {
return;
}
console.log(`[${this.name}]:`, ...args);
}
}

+ 97
- 0
src/services/Supervisor.ts View File

@ -0,0 +1,97 @@
import Application from "../Application";
import { MovieTicket, MovieTorrent } from "../database/entities";
import MovieSearch from "./MovieSearch";
import Service from "./Service";
import TorrentClientIpc, { TorrentClientConnectionError } from "./TorrentClientIpc";
export default class Supervisor extends Service
{
/**
* Keep a list of pending torrent links to add
*/
protected pendingTorrentsToAdd: string[];
/**
* The movie search service instance
*/
protected movieSearch!: MovieSearch;
/**
* The torrent client IPC service instance
*/
protected torrentClient!: TorrentClientIpc;
/**
* Create a new supervisor service instance
*/
public constructor(app: Application) {
super("Supervisor", app);
this.pendingTorrentsToAdd = [];
}
/**
* Boot the supervisor service
*/
public async boot() {}
/**
* All services are booted and ready
*/
public start() {
this.movieSearch = this.app.service<MovieSearch>("Movie Search");
this.torrentClient = this.app.service<TorrentClientIpc>("Torrent Client IPC");
this.searchMovies();
}
/**
* Shutdown the supervisor service
*/
public async shutdown() {}
// Tasks ---------------------------------------------------------------------------------------
/**
* @TODO Performing a promise-all instead of waiting between each movie may be much faster
* Search available movies in the database
*/
public async searchMovies() {
let movies = await MovieTicket.find({where: {isFulfilled: false}, relations: [ "torrents" ]});
for (let movie of movies) {
// Skip already-resolved non-stale torrents
if (movie.torrents.length > 0 && !movie.isStale) {
this.log("Skipping already satisfied ticket")
continue;
}
await this.searchMovie(movie);
}
}
/**
* Search for a movie and add it to the torrent client
*/
public async searchMovie(movie: MovieTicket) {
// Search for a movie torrent
let link = await this.movieSearch.searchMovie(movie);
if (link === null) {
return false;
}
this.log("Found a torrent for:", movie.title, link);
// Send the link to the client
let infoHash: string;
try {
infoHash = await this.torrentClient.add(link);
} catch(e) {
if (e instanceof TorrentClientConnectionError) {
this.log("Failed to add torrent to client... Added to pending");
this.pendingTorrentsToAdd.push(link);
}
return false;
}
// Store a reference to this torrent in the database
let torrent = new MovieTorrent();
torrent.infoHash = infoHash;
torrent.movieTicket = movie;
await torrent.save();
return true;
}
}

+ 221
- 0
src/services/TorrentClientIpc.ts View File

@ -0,0 +1,221 @@
import ipc from "node-ipc";
import { Socket } from "net";
import Application from "../Application";
import Service from "./Service";
interface IResponse {
response?: any,
error?: string | Error
}
export interface ITorrent {
name: string,
infoHash: string,
progress: number,
state: TorrentState
}
export interface ISerializedFile {
path : string;
size : number;
downloaded: number;
progress : number;
selected : boolean;
}
export interface ISerializedTorrent {
name : string;
infoHash : string;
downloaded : number;
uploaded : number;
ratio : number;
size : number;
downloadSpeed: number;
uploadSpeed : number;
numPeers : number;
progress : number;
path : string;
state : TorrentState;
files : ISerializedFile[];
}
export enum TorrentState {
Ready = 0x1,
Paused = 0x2,
Done = 0x4
}
/**
* A custom error type for torrent client connection errors
*/
export class TorrentClientConnectionError extends Error {
constructor(...args: any[]) {
super(...args);
Object.setPrototypeOf(this, TorrentClientConnectionError.prototype);
}
}
/**
* The torrent client IPC service
*/
export default class TorrentClientIpc extends Service
{
/**
* Indicate if there is an active connection to the IPC
*/
private __isConnected: boolean;
/**
* The active IPC socket
*/
protected socket!: Socket;
/**
* Create a new IPC client for the torrent client
*/
constructor(app: Application) {
super("Torrent Client IPC", app);
ipc.config.id = "torrent_webui";
ipc.config.retry = 1500;
ipc.config.silent = true;
this.__isConnected = false;
}
/**
* Boot the torrent client IPC service
*/
public boot() {
return new Promise<void>((resolve, reject) => {
ipc.connectTo("torrent_client", process.env["TORRENT_CLIENT_IPC_SOCKET"], () => {
this.socket = ipc.of["torrent_client"];
this.installSocketEventHandlers(this.socket);
this.installSocketMessageHandlers(this.socket);
resolve();
});
});
}
/**
* Shutdown the service
*/
public async shutdown() {
}
/**
* Install the event handlers for the IPC socket
*/
protected installSocketEventHandlers(socket: Socket) {
socket.on("connect", () => this.onConnect());
socket.on("error", (error: any) => this.onError(error));
socket.on("disconnect", () => this.onDisconnect());
socket.on("destroy", () => this.onDestroy());
}
protected installSocketMessageHandlers(socket: Socket) {
}
// Socket Event Handlers -----------------------------------------------------------------------
protected onConnect() {
this.log("IPC: Connection established");
this.__isConnected = true;
}
protected onError(error: string | Error) {
if (this.__isConnected) {
this.log("IPC: Error occurred:", error);
}
}
protected onDisconnect() {
if (this.__isConnected) {
this.log("IPC: Disconnected");
}
this.__isConnected = false;
}
protected onDestroy() {
this.log("IPC: Destroyed");
}
// Methods -------------------------------------------------------------------------------------
/**
* Perform a general request to the torrent client
*/
protected async request(method: string, message?: any) {
return new Promise<IResponse>((resolve, reject) => {
if (!this.isConnected) {
reject(new TorrentClientConnectionError("Not connected to torrent client"));
return;
}
let respond = (response: any) => {
clearTimeout(timeout);
resolve(response);
}
// Include timeout mechanism in the off chance something breaks
let timeout = setTimeout(() => {
this.socket.off(method, respond);
reject(new TorrentClientConnectionError("Torrent client IPC request timeout"));
}, 1000);
this.socket.once(method, respond);
this.socket.emit(method, message);
});
}
/**
* Add a torrent to the client
* @param torrent Magnet URI or file buffer
*/
public async add(torrent: string | Buffer) {
let response = await this.request("add", torrent);
if (response.error) {
throw new Error("Failed to add torrent");
}
return <string>response.response;
}
/**
* Remove a torrent from the client
* @param torrent Torrent info hash
*/
public async remove(torrent: string) {
let response = await this.request("remove", torrent);
if (response.error) {
throw new Error("Failed to remove torrent");
}
}
/**
* Get a list of all torrents in the client
*/
public async list() {
let response = await this.request("list");
if (response.error) {
console.error(response.error);
throw new Error("Failed to obtain torrent list");
}
return <ITorrent[]>response.response;
}
/**
* Get full details of each of the provided torrents
* @param torrentIds Array of torrent info hashes
*/
public async details(...torrentIds: string[]) {
let response = await this.request("details", torrentIds);
if (response.error) {
console.error(response.error);
throw new Error("Failed to retrieve torrent details");
}
return <ISerializedTorrent[]>response.response;
}
// Accessors -----------------------------------------------------------------------------------
get isConnected() {
return this.__isConnected;
}
}

+ 13
- 0
src/services/index.ts View File

@ -0,0 +1,13 @@
import Database from "./Database";
import IpcInterface from "./IpcInterface";
import MovieSearch from "./MovieSearch";
import Supervisor from "./Supervisor";
import TorrentClientIpc from "./TorrentClientIpc";
export default {
Database,
IpcInterface,
MovieSearch,
Supervisor,
TorrentClientIpc,
}

+ 63
- 0
src/torrents/Torrent.ts View File

@ -0,0 +1,63 @@
import { MovieTicket } from "../database/entities";
import { ITorrentMetaInfo, parseMovieTorrentName } from "./parsing";
export default class Torrent
{
/**
* The name of the torrent
*/
public readonly name: string;
/**
* The size of the torrent in bytes (if available)
*/
public readonly size: number | null;
/**
* The number of seeders (if available)
*/
public readonly seeders: number;
/**
* Download link (if available)
*/
protected readonly link: string | null;
/**
* Metadata of the torrent
*/
public readonly metadata: ITorrentMetaInfo;
/**
* Create a new Torrent instance
*
* @param name The name of the torrent
* @param size The size of the torrent in bytes (if available)
* @param seeders The number of seeders (if available)
* @param link The number of seeders (if available)
*/
public constructor(movie: MovieTicket, name: string, size?: number, seeders?: number, link?: string) {
this.name = name.trim();
this.size = size ?? null;
this.seeders = seeders ?? 1;
this.link = link ?? null;
this.metadata = parseMovieTorrentName(name, movie.title ?? "", movie.year ?? undefined);
}
/**
* Return a link to download (magnet or .torrent)
*/
public async downloadLink() {
if (this.link === null) {
throw Error("Magnet link does not exist");
}
return this.link;
}
/**
* Serialize this torrent into a string
*/
public toString() {
return `Name: ${this.name}; Size: ${this.size}; Seeders: ${this.seeders};`
}
}

+ 5
- 0
src/torrents/index.ts View File

@ -0,0 +1,5 @@
import TorrentGalaxy from "./providers/torrentgalaxy";
export {
TorrentGalaxy
}

+ 153
- 0
src/torrents/parsing.ts View File

@ -0,0 +1,153 @@
/**
* Video quality from lowest to highest
*/
export enum Resolution {
HD4k,
HD1080,
HD720,
SD384,
SD480,
SD360,
Unknown
}
// https://en.wikipedia.org/wiki/Pirated_movie_release_types#DVD_and_VOD_ripping
// https://en.wikipedia.org/wiki/Standard_(warez)#cite_note-txd2k9-13
/**
* Types of releases from lowest quality to highest
*/
export enum ReleaseType {
BluRay,
WebDl,
WebRip,
WebCap,
HDRip,
DVDR,
DVDRip,
Unknown, // Unknown is better than cam tbh
HDCAM,
CAM
}
export enum VideoCodec {
XviD,
x264,
x265,
}
export enum VideoCodecFlag {
REMUX,
HDR,
HEVC
}
export enum AudioCodec {
AC3,
DD51,
AAC71,
Atmos71,
TenBit
}
export interface ITorrentMetaInfo {
containsOtherLanguage: boolean,
resolution: Resolution,
releaseType: ReleaseType,
}
/**
* Determine meta-info from a torrent name
*/
export function parseMovieTorrentName(torrentName: string, title: string = "", year?: number) {
// Split the meta info after the year if possible to make parsing more reliable
let split = torrentName.split(new RegExp(`${year}|\\(${year}\\)`));
let metaInfo = split[split.length - 1];
title = split.length > 1 ? "" : title; // No need to check title in parsing if split correctly
return <ITorrentMetaInfo>{
containsOtherLanguage: determineIfContainsOtherLanguages(torrentName, title),
resolution: determineResolution(metaInfo, title),
releaseType: determineReleaseType(metaInfo, title),
}
}
/**
* Examine the torrent name for language indicators
*/
function determineIfContainsOtherLanguages(torrentName: string, title: string) {
let matches = torrentName.match(/\b(?:Hindi|Telugu|Ita|Italian|Spanish|Latino|Russian|Arabic|Dual|Multi)\b/gi);
for (let match of matches ?? []) {
if (title.indexOf(match) == -1) {
return true;
}
}
return false;
}
/**
* Interpret the resolution string as an enum value
*/
function resolutionFromString(resolution: string) {
switch(resolution.toUpperCase()) {
case "4K":
case "UHD":
case "2160":
return Resolution.HD4k;
case "1080":
return Resolution.HD1080;
case "720":
return Resolution.HD720;
case "480":
return Resolution.SD480;
case "384":
return Resolution.SD384;
case "360":
return Resolution.SD360;
default:
return Resolution.Unknown;
}
}
/**
* Determine the video resolution of the torrent
*/
function determineResolution(torrentName: string, title: string) {
let matches = torrentName.match(/\b(?:2160|1080|720|480|384|360)p?|UltraHD|UHD|4K\b/gi);
if (matches == null) {
return Resolution.Unknown;
}
let resolution = matches[matches.length - 1];
// Make sure what was matched is not part of the title...
if (matches.length == 1 && title.indexOf(resolution) != -1) {
return Resolution.Unknown;
}
return resolutionFromString(resolution.replace(/p$/i, ""));
}
/**
* Determine the release type of the torrent
*/
function determineReleaseType(torrentName: string, title: string) {
let releaseTypeRegex: {[type: string]: RegExp} = {
[ReleaseType.BluRay]: /\b(?:BR|Blu-Ray|BluRay|BDRip|BRRip|BDMV|BDR|BD25|BD50|BD5|BD9)\b/i,
[ReleaseType.WebDl] : /\b(?:WEB.?DL|WEB-DLRip)\b/i,
[ReleaseType.WebRip]: /\b(?:WEB.?Rip|WEB)\b/i,
[ReleaseType.WebCap]: /\bWEB.?Cap\b/i,
[ReleaseType.HDRip] : /\b(?:HC|HD.?Rip)\b/i,
[ReleaseType.DVDR] : /\bDVD.?R|DVD-Full|Full-Rip|DVD.?5|DVD.?9\b/i,
[ReleaseType.DVDRip]: /\bDVD.?Rip|DVD.?Mux/i,
[ReleaseType.HDCAM] : /\b(?:TRUE|HD)CAM\b/i,
[ReleaseType.CAM] : /\bCAM.?Rip\b/i,
};
let matches: RegExpMatchArray | null;
for (let type in releaseTypeRegex) {
matches = torrentName.match(releaseTypeRegex[type]);
if (!matches) {
continue;
}
if (matches.length == 1 || title.indexOf(matches[matches.length - 1]) == -1) {
return <ReleaseType>parseInt(type);
}
}
return ReleaseType.Unknown;
}

+ 25
- 0
src/torrents/providers/Provider.ts View File

@ -0,0 +1,25 @@
import { MovieTicket } from "../../database/entities";
import Torrent from "../Torrent";
/**
* Media type flags
*/
export enum MediaType {
None = 0x0,
Movies = 0x1,
TvShows = 0x2
}
export default abstract class Provider
{
/**
* Indicate what media types the provider supports
*/
public static readonly PROVIDES: MediaType = MediaType.None;
/**
* Search for movies
*/
public abstract searchMovie(movie: MovieTicket): Promise<Torrent[]>;
}

+ 30
- 0
src/torrents/providers/torrentgalaxy/index.ts View File

@ -0,0 +1,30 @@
import { MovieTicket } from "../../../database/entities";
import Provider, { MediaType } from "../Provider";
import Torrent from "../../Torrent";
import { search, Sort } from "./search";
export default class TorrentGalaxy extends Provider
{
/**
* Indicate that this provider provides movies
*/
public static readonly PROVIDES = MediaType.Movies;
/**
* Search for a movie
*/
public async searchMovie(movie: MovieTicket) {
if (movie.imdbId === null) {
return [];
}
let torrents = await search(movie.imdbId, undefined, Sort.Seeders);
return torrents.torrents.map(torrent => new Torrent(
movie,
torrent.name,
torrent.size,
torrent.seeders,
torrent.magnet
));
}
}

+ 135
- 0
src/torrents/providers/torrentgalaxy/search.ts View File

@ -0,0 +1,135 @@
import cheerio from "cheerio";
import { request, convertToBytes } from "../../util";
const BASE_URL = "https://torrentgalaxy.mx/torrents.php?search=";
export enum LanguageId {
AllLanguages = 0,
English = 1,
French = 2,
German = 3,
Italian = 4,
Japanese = 5,
Spanish = 6,
Russian = 7,
Hindi = 8,
OtherMultiple = 9,
Korean = 10,
Danish = 11,
Norwegian = 12,
Dutch = 13,
Chinese = 14,
Portuguese = 15,
Bengali = 16,
Polish = 17,
Turkish = 18,
Telugu = 19,
Urdu = 20,
Arabic = 21,
Swedish = 22,
Romanian = 23,
Thai = 24
}
export enum Language {
AllLanguages ="AllLanguages",
English ="English",
French ="French",
German ="German",
Italian ="Italian",
Japanese ="Japanese",
Spanish ="Spanish",
Russian ="Russian",
Hindi ="Hindi",
OtherMultiple ="OtherMultiple",
Korean ="Korean",
Danish ="Danish",
Norwegian ="Norwegian",
Dutch ="Dutch",
Chinese ="Chinese",
Portuguese ="Portuguese",
Bengali ="Bengali",
Polish ="Polish",
Turkish ="Turkish",
Telugu ="Telugu",
Urdu ="Urdu",
Arabic ="Arabic",
Swedish ="Swedish",
Romanian ="Romanian",
Thai ="Thai"
}
export enum Category {
Documentaries = 9,
MoviesHD = 42,
MoviesSD = 1,
Movies4K = 3,
MoviesPacks = 4,
TVEpisodesHD = 41,
TVEPisodesSD = 5,
TVPacks = 6,
TVSports = 7
}
export enum Sort {
Date = "id",
Name = "name",
Size = "size",
Seeders = "seeders"
}
export enum SortOrder {
Asc = "asc",
Desc = "desc",
}
interface ITorrentGalaxyTorrent {
category: number,
language: Language,
name : string,
magnet : string,
size : number,
seeders : number,
leechers: number
}
interface ITorrentGalaxyResults {
torrents: ITorrentGalaxyTorrent[],
total_results: number
}
function scrapeRow($: cheerio.Root, row: cheerio.Cheerio): ITorrentGalaxyTorrent {
let children = row.children();
let category = <string>$(children[0]).find("a").attr("href")?.split("cat=")[1];
let language = <Language>$(children[2]).find("img[title]").attr("title");
let name = $(children[3]).text();
let magnet = <string>$(children[4]).find("a[href^='magnet']").first().attr("href");
let [size, unit] = $(children[7]).text().split(" ");
let [seeders, leechers] = $(children[10]).text().slice(1, -1).split('/').map(v => parseInt(v));
return {
category: parseInt(category),
size: convertToBytes(parseFloat(size), unit),
language, name, magnet, seeders, leechers
}
}
function scrapeResults(response: string): ITorrentGalaxyResults {
let torrents: ITorrentGalaxyTorrent[] = [];
let $ = cheerio.load(response);
$(".tgxtable .tgxtablerow").each((_, elem) => {
torrents.push(scrapeRow($, $(elem)));
});
return {
torrents,
total_results: parseInt($("#filterbox2 > span").text())
};
}
/**
* Supports IMDb links too
*/
export async function search(query: string, language: LanguageId = LanguageId.AllLanguages, sort: Sort = Sort.Date, order: SortOrder = SortOrder.Desc) {
let res = await request(`${BASE_URL}${encodeURI(query)}&lang=${language}&sort=${sort}&order=${order}`);
let results = scrapeResults(res);
return results;
}

+ 58
- 0
src/torrents/ranking.ts View File

@ -0,0 +1,58 @@
import { Resolution } from "./parsing";
import Torrent from "./Torrent";
/**
* Rank a list of torrents from best to worst to download
*/
export function rankTorrents(torrents: Torrent[]) {
torrents.sort(sortCompare);
let preferred = torrents.filter(selectPreferredTorrents);
return preferred;
}
/**
* Filter out unwanted torrents
*/
function selectPreferredTorrents(torrent: Torrent) {
if (torrent.seeders == 0 || torrent.metadata.containsOtherLanguage) {
return false;
}
if (torrent.metadata.resolution == Resolution.HD4k) {
return torrent.size != null && torrent.size < 15*1024*1024*1024; // 15GB
}
return true;
}
/**
* A comparator for ranking torrents
*
* @param a Left side
* @param b Right side
*/
function sortCompare(a: Torrent, b: Torrent) {
// Languages
let languageCmp = <any>a.metadata.containsOtherLanguage - <any>b.metadata.containsOtherLanguage;
if (languageCmp !== 0) {
return languageCmp;
}
// Resolution
let resolutionCmp = a.metadata.resolution - b.metadata.resolution;
if (resolutionCmp !== 0) {
return resolutionCmp;
}
// If one has only a few seeds, don't worry about the other info. Prioritize seed count
if (a.seeders < 5 || b.seeders < 5) {
let seedersCmp = b.seeders - a.seeders;
if (seedersCmp != 0) {
return seedersCmp;
}
}
// Sort by the file size
let fileSizeCmp = (a.size ?? 0) - (b.size ?? 0);
if (fileSizeCmp !== 0) {
return fileSizeCmp;
}
return 0;
}

+ 82
- 0
src/torrents/util.ts View File

@ -0,0 +1,82 @@
import { parseString } from "xml2js";
import https from "https";
/**
* Perform an RSS/XML request
*/
export function rssRequest<T = any>(url: string) {
return new Promise<T>((resolve, reject) => {
https.get(url, { headers: { "User-Agent": "Node", "Accept": "application/rss+xml" } }, (response) => {
if (response.statusCode !== 200) {
reject("Status error: " + response.statusCode);
return;
}
response.setEncoding("utf-8");
let body = "";
response.on("data", (chunk) => body += chunk);
response.on("end", () => parseString(body, (err, result) => {
if (err) {
reject(err);
return;
}
resolve(result);
}));
});
});
}
/**
* Perform a generic GET request
*/
export function jsonRequest<T = any>(url: string) {
return new Promise<T>((resolve, reject) => {
https.get(url, { headers: { "User-Agent": "Node", "Accept": "*/*" } }, (response) => {
if (response.statusCode !== 200) {
reject("Status error: " + response.statusCode);
return;
}
response.setEncoding("utf-8");
let body = "";
response.on("data", (chunk) => body += chunk);
response.on("end", () => resolve(JSON.parse(body)));
});
});
}
/**
* Perform a generic GET request
*/
export function request(url: string, timeout: number = 10000) {
return new Promise<string>((resolve, reject) => {
https.get(url, { headers: { "User-Agent": "Node", "Accept": "*/*" }, timeout }, (response) => {
if (response.statusCode !== 200) {
reject("Status error: " + response.statusCode);
return;
}
response.setEncoding("utf-8");
let body = "";
response.on("data", (chunk) => body += chunk);
response.on("end", () => resolve(body));
}).on("timeout", () => reject("timeout"));
});
}
export function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export function convertToBytes(size: number, unit: string, throwUnknownUnit: boolean = true) {
switch(unit.toUpperCase()) {
case "GB":
return Math.ceil(size*1024*1024*1024);
case "MB":
return Math.ceil(size*1024*1024);
case "KB":
return Math.ceil(size*1024*1024);
default:
if (throwUnknownUnit) {
throw new Error("Unknown unit provided");
}
return Math.ceil(size);
}
}

+ 350
- 0
src/typings/node-ipc/index.d.ts View File

@ -0,0 +1,350 @@
/// <reference types="node"/>
declare module "node-ipc" {
import { Socket } from "net";
declare const NodeIPC: NodeIPC.NodeIPC;
declare namespace NodeIPC {
interface NodeIPC extends IPC
{}
interface IPC {
/**
* Set these variables in the ipc.config scope to overwrite or set default values
*/
config: Config;
/**
* https://www.npmjs.com/package/node-ipc#log
*/
log(...args: any[]): void;
/**
* https://www.npmjs.com/package/node-ipc#connectto
* Used for connecting as a client to local Unix Sockets and Windows Sockets.
* This is the fastest way for processes on the same machine to communicate
* because it bypasses the network card which TCP and UDP must both use.
* @param id is the string id of the socket being connected to.
* The socket with this id is added to the ipc.of object when created.
* @param path is the path of the Unix Domain Socket File, if the System is Windows,
* this will automatically be converted to an appropriate pipe with the same information as the Unix Domain Socket File.
* If not set this will default to ipc.config.socketRoot+ipc.config.appspace+id
* @param callback this is the function to execute when the socket has been created
*/
connectTo(id: string, path?: string, callback?: () => void): void;
/**
* https://www.npmjs.com/package/node-ipc#connectto
* Used for connecting as a client to local Unix Sockets and Windows Sockets.
* This is the fastest way for processes on the same machine to communicate
* because it bypasses the network card which TCP and UDP must both use.
* @param id is the string id of the socket being connected to.
* The socket with this id is added to the ipc.of object when created.
* @param callback this is the function to execute when the socket has been created
*/
connectTo(id: string, callback?: () => void): void;
/**
* https://www.npmjs.com/package/node-ipc#connecttonet
* Used to connect as a client to a TCP or TLS socket via the network card.
* This can be local or remote, if local, it is recommended that you use the Unix
* and Windows Socket Implementaion of connectTo instead as it is much faster since it avoids the network card altogether.
* For TLS and SSL Sockets see the node-ipc TLS and SSL docs.
* They have a few additional requirements, and things to know about and so have their own doc.
* @param id is the string id of the socket being connected to. For TCP & TLS sockets,
* this id is added to the ipc.of object when the socket is created with a reference to the socket
* @param host is the host on which the TCP or TLS socket resides.
* This will default to ipc.config.networkHost if not specified
* @param port the port on which the TCP or TLS socket resides
* @param callback this is the function to execute when the socket has been created
*/
connectToNet(id: string, host?: string, port?: number, callback?: () => void): void;
/**
* https://www.npmjs.com/package/node-ipc#connecttonet
* Used to connect as a client to a TCP or TLS socket via the network card.
* This can be local or remote, if local, it is recommended that you use the Unix
* and Windows Socket Implementaion of connectTo instead as it is much faster since it avoids the network card altogether.
* For TLS and SSL Sockets see the node-ipc TLS and SSL docs.
* They have a few additional requirements, and things to know about and so have their own doc.
* @param id is the string id of the socket being connected to. For TCP & TLS sockets,
* this id is added to the ipc.of object when the socket is created with a reference to the socket
* @param callback this is the function to execute when the socket has been created
*/
connectToNet(id: string, callback?: () => void): void;
/**
* https://www.npmjs.com/package/node-ipc#connecttonet
* Used to connect as a client to a TCP or TLS socket via the network card.
* This can be local or remote, if local, it is recommended that you use the Unix
* and Windows Socket Implementaion of connectTo instead as it is much faster since it avoids the network card altogether.
* For TLS and SSL Sockets see the node-ipc TLS and SSL docs.
* They have a few additional requirements, and things to know about and so have their own doc.
* @param id is the string id of the socket being connected to.
* For TCP & TLS sockets, this id is added to the ipc.of object when the socket is created with a reference to the socket
* @param host is the host on which the TCP or TLS socket resides. This will default to ipc.config.networkHost if not specified
* @param port the port on which the TCP or TLS socket resides
* @param callback this is the function to execute when the socket has been created
*/
connectToNet(id: string, hostOrPort: number | string, callback?: () => void): void;
/**
* https://www.npmjs.com/package/node-ipc#disconnect
* Used to disconnect a client from a Unix, Windows, TCP or TLS socket.
* The socket and its refrence will be removed from memory and the ipc.of scope.
* This can be local or remote. UDP clients do not maintain connections and so there are no Clients and this method has no value to them
* @param id is the string id of the socket from which to disconnect
*/
disconnect(id: string): void;
/**
* https://www.npmjs.com/package/node-ipc#serve
* Used to create local Unix Socket Server or Windows Socket Server to which Clients can bind.
* The server can emit events to specific Client Sockets, or broadcast events to all known Client Sockets
* @param path This is the path of the Unix Domain Socket File, if the System is Windows,
* this will automatically be converted to an appropriate pipe with the same information as the Unix Domain Socket File.
* If not set this will default to ipc.config.socketRoot+ipc.config.appspace+id
* @param callback This is a function to be called after the Server has started.
* This can also be done by binding an event to the start event like ipc.server.on('start',function(){});
*/
serve(path: string, callback?: () => void): void;
/**
* https://www.npmjs.com/package/node-ipc#serve
* Used to create local Unix Socket Server or Windows Socket Server to which Clients can bind.
* The server can emit events to specific Client Sockets, or broadcast events to all known Client Sockets
* @param callback This is a function to be called after the Server has started.
* This can also be done by binding an event to the start event like ipc.server.on('start',function(){});
*/
serve(callback?: () => void): void;
/**
* https://www.npmjs.com/package/node-ipc#serve
* Used to create local Unix Socket Server or Windows Socket Server to which Clients can bind.
* The server can emit events to specific Client Sockets, or broadcast events to all known Client Sockets
*/
serve(callback: null): void;
/**
* https://www.npmjs.com/package/node-ipc#servenet
* @param host If not specified this defaults to the first address in os.networkInterfaces().
* For TCP, TLS & UDP servers this is most likely going to be 127.0.0.1 or ::1
* @param port The port on which the TCP, UDP, or TLS Socket server will be bound, this defaults to 8000 if not specified
* @param UDPType If set this will create the server as a UDP socket. 'udp4' or 'udp6' are valid values.
* This defaults to not being set. When using udp6 make sure to specify a valid IPv6 host, like ::1
* @param callback Function to be called when the server is created
*/
serveNet(host?: string, port?: number, UDPType?: "udp4" | "udp6", callback?: () => void): void;
/**
* https://www.npmjs.com/package/node-ipc#servenet
* @param UDPType If set this will create the server as a UDP socket. 'udp4' or 'udp6' are valid values.
* This defaults to not being set. When using udp6 make sure to specify a valid IPv6 host, like ::1
* @param callback Function to be called when the server is created
*/
serveNet(UDPType: "udp4" | "udp6", callback?: () => void): void;
/**
* https://www.npmjs.com/package/node-ipc#servenet
* @param callback Function to be called when the server is created
* @param port The port on which the TCP, UDP, or TLS Socket server will be bound, this defaults to 8000 if not specified
*/
serveNet(callbackOrPort: EmptyCallback | number): void;
/**
* https://www.npmjs.com/package/node-ipc#servenet
* @param host If not specified this defaults to the first address in os.networkInterfaces().
* For TCP, TLS & UDP servers this is most likely going to be 127.0.0.1 or ::1
* @param port The port on which the TCP, UDP, or TLS Socket server will be bound, this defaults to 8000 if not specified
* @param callback Function to be called when the server is created
*/
serveNet(host: string, port: number, callback?: () => void): void;
/**
* This is where socket connection refrences will be stored when connecting to them as a client via the ipc.connectTo
* or iupc.connectToNet. They will be stored based on the ID used to create them, eg : ipc.of.mySocket
*/
of: any;
/**
* This is a refrence to the server created by ipc.serve or ipc.serveNet
*/
server: Server;
}
type EmptyCallback = () => void;
interface Client {
/**
* triggered when a JSON message is received. The event name will be the type string from your message
* and the param will be the data object from your message eg : { type:'myEvent',data:{a:1}}
*/
on(event: string, callback: (message: any, socket: Socket) => void): Client;
/**
* triggered when an error has occured
*/
on(event: "error", callback: (err: any) => void): Client;
/**
* connect - triggered when socket connected
* disconnect - triggered by client when socket has disconnected from server
* destroy - triggered when socket has been totally destroyed, no further auto retries will happen and all references are gone
*/
on(event: "connect" | "disconnect" | "destroy", callback: () => void): Client;
/**
* triggered by server when a client socket has disconnected
*/
on(event: "socket.disconnected", callback: (socket: Socket, destroyedSocketID: string) => void): Client;
/**
* triggered when ipc.config.rawBuffer is true and a message is received
*/
on(event: "data", callback: (buffer: Buffer) => void): Client;
emit(event: string, value?: any): Client;
/**
* Unbind subscribed events
*/
off(event: string, handler: any): Client;
}
interface Server extends Client {
/**
* start serving need top call serve or serveNet first to set up the server
*/
start(): void;
/**
* close the server and stop serving
*/
stop(): void;
emit(value: any): Client;
emit(event: string, value: any): Client;
emit(socket: Socket | SocketConfig, event: string, value?: any): Server;
emit(socketConfig: Socket | SocketConfig, value?: any): Server;
}
interface SocketConfig {
address?: string;
port?: number;
}
interface Config {
/**
* Default: 'app.'
* Used for Unix Socket (Unix Domain Socket) namespacing.
* If not set specifically, the Unix Domain Socket will combine the socketRoot, appspace,
* and id to form the Unix Socket Path for creation or binding.
* This is available incase you have many apps running on your system, you may have several sockets with the same id,
* but if you change the appspace, you will still have app specic unique sockets
*/
appspace: string;
/**
* Default: '/tmp/'
* The directory in which to create or bind to a Unix Socket
*/
socketRoot: string;
/**
* Default: os.hostname()
* The id of this socket or service
*/
id: string;
/**
* Default: 'localhost'
* The local or remote host on which TCP, TLS or UDP Sockets should connect
* Should resolve to 127.0.0.1 or ::1 see the table below related to this
*/
networkHost: string;
/**
* Default: 8000
* The default port on which TCP, TLS, or UDP sockets should connect
*/
networkPort: number;
/**
* Default: 'utf8'
* the default encoding for data sent on sockets. Mostly used if rawBuffer is set to true.
* Valid values are : ascii utf8 utf16le ucs2 base64 hex
*/
encoding: "ascii" | "utf8" | "utf16le" | "ucs2" | "base64" | "hex";
/**
* Default: false
* If true, data will be sent and received as a raw node Buffer NOT an Object as JSON.
* This is great for Binary or hex IPC, and communicating with other processes in languages like C and C++
*/
rawBuffer: boolean;
/**
* Default: false
* Synchronous requests. Clients will not send new requests until the server answers
*/
sync: boolean;
/**
* Default: false
* Turn on/off logging default is false which means logging is on
*/
silent: boolean;
/**
* Default: true
* Turn on/off util.inspect colors for ipc.log
*/
logInColor: boolean;
/**
* Default: 5
* Set the depth for util.inspect during ipc.log
*/
logDepth: number;
/**
* Default: console.log
* The function which receives the output from ipc.log; should take a single string argument
*/
logger(msg: string): void;
/**
* Default: 100
* This is the max number of connections allowed to a socket. It is currently only being set on Unix Sockets.
* Other Socket types are using the system defaults
*/
maxConnections: number;
/**
* Default: 500
* This is the time in milliseconds a client will wait before trying to reconnect to a server if the connection is lost.
* This does not effect UDP sockets since they do not have a client server relationship like Unix Sockets and TCP Sockets
*/
retry: number;
/* */
/**
* Default: false
* if set, it represents the maximum number of retries after each disconnect before giving up
* and completely killing a specific connection
*/
maxRetries: boolean | number;
/**
* Default: false
* Defaults to false meaning clients will continue to retry to connect to servers indefinitely at the retry interval.
* If set to any number the client will stop retrying when that number is exceeded after each disconnect.
* If set to true in real time it will immediately stop trying to connect regardless of maxRetries.
* If set to 0, the client will NOT try to reconnect
*/
stopRetrying: boolean;
/**
* Default: true
* Defaults to true meaning that the module will take care of deleting the IPC socket prior to startup.
* If you use node-ipc in a clustered environment where there will be multiple listeners on the same socket,
* you must set this to false and then take care of deleting the socket in your own code.
*/
unlink: boolean;
/**
* Primarily used when specifying which interface a client should connect through.
* see the socket.connect documentation in the node.js api https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener
*/
interfaces: {
/**
* Default: false
*/
localAddress?: boolean;
/**
* Default: false
*/
localPort?: boolean;
/**
* Default: false
*/
family?: boolean;
/**
* Default: false
*/
hints?: boolean;
/**
* Default: false
*/
lookup?: boolean;
};
tls: {
rejectUnauthorized?: boolean;
public?: string;
private?: string;
};
}
}
export = NodeIPC
// declare const RootIPC: NodeIPC.IPC & { IPC: new () => NodeIPC.IPC };
// export = RootIPC;
}

+ 71
- 0
tsconfig.json View File

@ -0,0 +1,71 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./build", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": ["./src/typings"], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
"sourceRoot": "./src", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}

+ 905
- 5
yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save