@ -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 |
@ -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 |
@ -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.* |
@ -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]; | |||
} | |||
} |
@ -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; | |||
} |
@ -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; | |||
} |
@ -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; | |||
} |
@ -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; | |||
} |
@ -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()); | |||
} | |||
}); | |||
}); | |||
} | |||
} |
@ -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[]; | |||
} |
@ -0,0 +1,6 @@ | |||
export * from "./MovieInfo"; | |||
export * from "./MovieQuota"; | |||
export * from "./MovieTicket"; | |||
export * from "./MovieTorrent"; | |||
export * from "./RegisterToken"; | |||
export * from "./User"; |
@ -0,0 +1,7 @@ | |||
import Application from "./Application"; | |||
// Create a new application instance | |||
let app = new Application(); | |||
// Start the application | |||
app.start(); |
@ -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(); | |||
} | |||
} |
@ -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); | |||
} | |||
} |
@ -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() { | |||
} | |||
} |
@ -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); | |||
} | |||
} |
@ -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; | |||
} | |||
} |
@ -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; | |||
} | |||
} |
@ -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, | |||
} |
@ -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};` | |||
} | |||
} |
@ -0,0 +1,5 @@ | |||
import TorrentGalaxy from "./providers/torrentgalaxy"; | |||
export { | |||
TorrentGalaxy | |||
} |
@ -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; | |||
} |
@ -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[]>; | |||
} |
@ -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 | |||
)); | |||
} | |||
} |
@ -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; | |||
} |
@ -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; | |||
} |
@ -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); | |||
} | |||
} |
@ -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; | |||
} | |||
@ -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. */ | |||
} | |||
} |