@ -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. */ | |||||
} | |||||
} |