@ -1,22 +0,0 @@ | |||||
import { BaseEntity, Column, CreateDateColumn, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm"; | |||||
import { DiscordRequest } from "./DiscordRequest"; | |||||
import { User } from "./User"; | |||||
@Entity() | |||||
export class DiscordAccount extends BaseEntity | |||||
{ | |||||
@PrimaryGeneratedColumn() | |||||
id!: number; | |||||
@Column({ length: 20 }) | |||||
discordId!: string; | |||||
@ManyToOne(() => User, user => user.discordAccounts) | |||||
user!: User; | |||||
@OneToMany(() => DiscordRequest, request => request.account) | |||||
requests!: DiscordRequest[]; | |||||
@CreateDateColumn() | |||||
createdAt!: Date; | |||||
} |
@ -1,17 +0,0 @@ | |||||
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from "typeorm"; | |||||
@Entity() | |||||
export class DiscordChannel extends BaseEntity | |||||
{ | |||||
@PrimaryGeneratedColumn() | |||||
id!: number; | |||||
@Column({ length: 20 }) | |||||
channelId!: string; | |||||
@Column({ length: 20 }) | |||||
messageId!: string; | |||||
@CreateDateColumn() | |||||
createdAt!: Date; | |||||
} |
@ -1,31 +0,0 @@ | |||||
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from "typeorm"; | |||||
import { DiscordAccount } from "./DiscordAccount"; | |||||
import { User } from "./User"; | |||||
@Entity() | |||||
export class DiscordLinkRequest extends BaseEntity | |||||
{ | |||||
@PrimaryGeneratedColumn() | |||||
id!: number; | |||||
@Column() | |||||
discordId!: string; | |||||
@Column() | |||||
token!: string; | |||||
@CreateDateColumn() | |||||
createdAt!: Date; | |||||
/** | |||||
* Fulfill a link request for the given user | |||||
*/ | |||||
public async linkUser(user: User) { | |||||
let account = new DiscordAccount(); | |||||
account.discordId = this.discordId; | |||||
account.user = user; | |||||
let awaitSave = account.save(); | |||||
await DiscordLinkRequest.delete({ id: this.id }); | |||||
return await awaitSave; | |||||
} | |||||
} |
@ -1,26 +0,0 @@ | |||||
import { BaseEntity, Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn } from "typeorm"; | |||||
import { DiscordAccount } from "./DiscordAccount"; | |||||
import { MovieTicket } from "./MovieTicket"; | |||||
@Entity() | |||||
export class DiscordRequest extends BaseEntity | |||||
{ | |||||
@PrimaryGeneratedColumn() | |||||
id!: number; | |||||
@Column({ length: 20 }) | |||||
channelId!: string; | |||||
@Column({ length: 20 }) | |||||
messageId!: string; | |||||
@CreateDateColumn() | |||||
createdAt!: Date; | |||||
@ManyToOne(() => DiscordAccount, account => account.requests) | |||||
account!: DiscordAccount; | |||||
@OneToOne(() => MovieTicket) | |||||
@JoinColumn() | |||||
ticket!: MovieTicket; | |||||
} |
@ -1,23 +0,0 @@ | |||||
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm"; | |||||
@Entity() | |||||
export class MovieInfo extends BaseEntity | |||||
{ | |||||
@PrimaryGeneratedColumn() | |||||
id!: 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; | |||||
} |
@ -1,11 +0,0 @@ | |||||
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm"; | |||||
@Entity() | |||||
export class MovieQuota extends BaseEntity | |||||
{ | |||||
@PrimaryGeneratedColumn() | |||||
id!: number; | |||||
@Column({ default: 5 }) | |||||
moviesPerWeek!: number; | |||||
} |
@ -1,93 +0,0 @@ | |||||
import { IApiMovieDetails } from "@common/api_schema"; | |||||
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne, OneToMany, OneToOne, JoinColumn, CreateDateColumn, Not, IsNull } 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: "int", nullable: true }) | |||||
tmdbId!: number | null; | |||||
@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; | |||||
/** | |||||
* @TODO This needs to check for fulfilled tickets too, but not there yet | |||||
* Fetch all active ticket ID's | |||||
*/ | |||||
public static async activeTicketMap() { | |||||
let tickets = await MovieTicket.find({ where: { tmdbId: Not(IsNull()), isCanceled: false } }); | |||||
let result: {[tmdbId: number]: number} = {}; | |||||
for (let ticket of tickets) { | |||||
result[<number>ticket.tmdbId] = ticket.id; | |||||
} | |||||
return result; | |||||
} | |||||
/** | |||||
* Insert a request via IMDb movie details | |||||
*/ | |||||
public static async requestImdb(user: User, imdbId: string, title: string, year: number) { | |||||
let ticket = new MovieTicket(); | |||||
ticket.imdbId = imdbId; | |||||
ticket.title = title; | |||||
ticket.year = year; | |||||
ticket.user = user; | |||||
return await ticket.save(); | |||||
} | |||||
/** | |||||
* Insert a rquest via TMDb movie details | |||||
*/ | |||||
public static async requestTmdb(user: User, movie: IApiMovieDetails) { | |||||
let info = new MovieInfo(); | |||||
info.overview = movie.overview; | |||||
info.posterPath = movie.posterPath; | |||||
info.backdropPath = movie.backdropPath; | |||||
info.releaseDate = movie.releaseDate; | |||||
info.runtime = movie.runtime; | |||||
await info.save(); | |||||
let ticket = new MovieTicket(); | |||||
ticket.tmdbId = movie.tmdbId; | |||||
ticket.imdbId = movie.imdbId; | |||||
ticket.title = movie.title; | |||||
ticket.year = movie.releaseDate ? parseInt(movie.releaseDate.slice(0, 4)) : null; | |||||
ticket.user = user; | |||||
ticket.info = info; | |||||
return await ticket.save(); | |||||
} | |||||
} |
@ -1,18 +0,0 @@ | |||||
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne } from "typeorm"; | |||||
import { MovieTicket } from "./MovieTicket"; | |||||
@Entity() | |||||
export class MovieTorrent extends BaseEntity | |||||
{ | |||||
@PrimaryGeneratedColumn() | |||||
id!: number; | |||||
@Column() | |||||
infoHash!: string; | |||||
@Column() | |||||
diskName!: string; | |||||
@ManyToOne(() => MovieTicket, ticket => ticket.torrents) | |||||
movieTicket!: MovieTicket; | |||||
} |
@ -1,50 +0,0 @@ | |||||
import Plex from "@lib/plex"; | |||||
import { BaseEntity, Column, Entity, In, PrimaryColumn } from "typeorm"; | |||||
@Entity() | |||||
export class PlexMovie extends BaseEntity | |||||
{ | |||||
@PrimaryColumn({ length: 27 }) | |||||
imdbId!: string; | |||||
@Column({ type: "int", nullable: true, unique: true }) | |||||
tmdbId!: number|null; | |||||
@Column() | |||||
plexKey!: number; | |||||
/** | |||||
* Check if a movie is on Plex given its TMDb ID | |||||
*/ | |||||
public static async findPlexKey(tmdbId: number|string) { | |||||
return (await PlexMovie.findOne({ where: { tmdbId } }))?.plexKey ?? null; | |||||
} | |||||
/** | |||||
* Get the set of IMDb IDs stored in the library | |||||
*/ | |||||
public static async imdbSet() { | |||||
let rows = await PlexMovie.createQueryBuilder("plex_movie") | |||||
.select("imdbId") | |||||
.getRawMany(); | |||||
return new Set(rows.map(row => <string>row.imdbId)); | |||||
} | |||||
/** | |||||
* Insert a set of IMDb IDs into the database | |||||
*/ | |||||
public static async insertMovies(movies: { imdbId: string, plexKey: number }[]) { | |||||
await PlexMovie.createQueryBuilder() | |||||
.insert() | |||||
.into("plex_movie") | |||||
.values([...movies].map(({imdbId, plexKey}) => ({ imdbId, tmdbId: null, plexKey }))) | |||||
.execute(); | |||||
} | |||||
/** | |||||
* Remove the given set of IMDb IDs from the library | |||||
*/ | |||||
public static async removeImdbSet(imdbIds: Set<string>) { | |||||
await PlexMovie.delete({ imdbId: In([...imdbIds]) }); | |||||
} | |||||
} |
@ -1,32 +0,0 @@ | |||||
import { generateToken } from "@autoplex/utils"; | |||||
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 async generate() { | |||||
let token = new RegisterToken(); | |||||
token.token = await generateToken(); | |||||
return await token.save(); | |||||
} | |||||
} |
@ -1,113 +0,0 @@ | |||||
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, OneToMany, OneToOne, JoinColumn, CreateDateColumn, MoreThanOrEqual } from "typeorm"; | |||||
import bcrypt from "bcrypt"; | |||||
import { MovieTicket } from "./MovieTicket"; | |||||
import { MovieQuota } from "./MovieQuota"; | |||||
import { IApiMovie } from "@common/api_schema"; | |||||
import { DiscordAccount } from "./DiscordAccount"; | |||||
@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 | null; | |||||
@OneToMany(() => User, user => user.movieTickets) | |||||
movieTickets!: MovieTicket[]; | |||||
@OneToMany(() => DiscordAccount, account => account.user) | |||||
discordAccounts!: DiscordAccount[]; | |||||
/** | |||||
* Authenticate a user and return an auth token upon success | |||||
*/ | |||||
public static async authenticate(email: string, password: string) { | |||||
let user = <User>await User.findOne({ email }); | |||||
if (user === undefined || !(await bcrypt.compare(password, user.password))) { | |||||
return null; | |||||
} | |||||
return user; | |||||
} | |||||
/** | |||||
* Create a new user | |||||
*/ | |||||
public static async createUser(name: string, email: string, password: string, quota: number|null = 5) { | |||||
let user = new User(); | |||||
user.isAdmin = false; | |||||
user.name = name; | |||||
user.email = email; | |||||
user.password = await bcrypt.hash(password, 8); | |||||
// Create a quota if necessary | |||||
if (quota !== null) { | |||||
user.quota = new MovieQuota; | |||||
user.quota.moviesPerWeek = quota; | |||||
await user.quota.save(); | |||||
} | |||||
return await user.save(); | |||||
} | |||||
/** | |||||
* Determine the user's available quota | |||||
*/ | |||||
public async availableQuota() { | |||||
let quota = await this.fetchQuota(); | |||||
if (quota === null) { | |||||
return null; | |||||
} | |||||
let oneWeekAgo = new Date(Date.now() - 1000*60*60*24*7); | |||||
let numTicketsThisWeek = await MovieTicket.count({ | |||||
user: this, | |||||
createdAt: MoreThanOrEqual(oneWeekAgo), | |||||
isCanceled: false | |||||
}); | |||||
return quota.moviesPerWeek - numTicketsThisWeek; | |||||
} | |||||
/** | |||||
* Get the user's quota, fetching it if undefined | |||||
*/ | |||||
public async fetchQuota() { | |||||
if (this.quota !== undefined) { | |||||
return this.quota; | |||||
} | |||||
let user = <User>await User.findOne(this.id, { relations: ["quota"] }); | |||||
return user.quota; | |||||
} | |||||
/** | |||||
* Fetch active movie tickets for this user | |||||
*/ | |||||
public async activeMovieTickets() { | |||||
let tickets = await MovieTicket.find({ | |||||
where: { user: this, isCanceled: false, isFulfilled: false }, | |||||
relations: ["info"] | |||||
}); | |||||
return tickets.map(ticket => <IApiMovie>({ | |||||
plexLink : null, | |||||
posterPath : ticket.info?.posterPath, | |||||
releaseDate: ticket.info?.releaseDate, | |||||
ticketId : ticket.id, | |||||
title : ticket.title, | |||||
tmdbId : ticket.tmdbId | |||||
})); | |||||
} | |||||
} |
@ -1,11 +0,0 @@ | |||||
export * from "./DiscordAccount"; | |||||
export * from "./DiscordChannel"; | |||||
export * from "./DiscordLinkRequest"; | |||||
export * from "./DiscordRequest"; | |||||
export * from "./MovieInfo"; | |||||
export * from "./MovieQuota"; | |||||
export * from "./MovieTicket"; | |||||
export * from "./MovieTorrent"; | |||||
export * from "./PlexMovie"; | |||||
export * from "./RegisterToken"; | |||||
export * from "./User"; |
@ -1,22 +0,0 @@ | |||||
import * as entities from "./entities"; | |||||
import { readFile } from "fs/promises"; | |||||
import { createConnection } from "typeorm"; | |||||
export default async function connectToDatabase() { | |||||
// 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: true, // Seems stable enough for my liking | |||||
entities : Object.values(entities), | |||||
migrations : ["src/migrations/*.ts"] | |||||
}); | |||||
} |