mirror of
https://github.com/sakaljurgis/kodi-api-server.git
synced 2026-07-08 20:37:41 +00:00
migrations; torr search, dl, seed, delete; files scan, delete; configs
This commit is contained in:
+8
-1
@@ -3,8 +3,15 @@ STATIC_SERVE_FOLDER="data/static"
|
|||||||
LOG_FILE_REQUESTS="data/static/static_log.txt"
|
LOG_FILE_REQUESTS="data/static/static_log.txt"
|
||||||
DB_PATH="./data/db/db.db"
|
DB_PATH="./data/db/db.db"
|
||||||
RECENT_SEARCHES_FOLDER="./data/recent_searches"
|
RECENT_SEARCHES_FOLDER="./data/recent_searches"
|
||||||
|
RECENT_SEARCHES_LIMIT=20
|
||||||
VIDEO_FILES_FOLDERS='[
|
VIDEO_FILES_FOLDERS='[
|
||||||
"/home/user/videos",
|
"/home/user/videos",
|
||||||
"/mnt/hdd/videos"
|
"/mnt/hdd/videos"
|
||||||
]'
|
]'
|
||||||
VIDEO_FILES_EXT='["*.mkv", "*.mp4", "*.avi"]'
|
VIDEO_FILES_EXT='[".mkv", ".mp4", ".avi"]'
|
||||||
|
PATTERNS_TO_IGNORE='["AOE1/"]' #file path patterns to ignore from video files scan
|
||||||
|
LINKOMANIJA_USERNAME=usern
|
||||||
|
LINKOMANIJA_PASSWORD=password
|
||||||
|
LINKOMANIJA_PASSKEY=some123long321passkey
|
||||||
|
TORRENT_DOWNLOAD_DIR='/mnt/hdd/videos'
|
||||||
|
TRANSMISSION_CLIENT_HOST=192.168.1.133
|
||||||
|
|||||||
@@ -5,36 +5,44 @@ export class CreateFilesTable1672729641013 implements MigrationInterface {
|
|||||||
await queryRunner.query(
|
await queryRunner.query(
|
||||||
`create table files
|
`create table files
|
||||||
(
|
(
|
||||||
id INTEGER
|
id INTEGER
|
||||||
primary key autoincrement,
|
primary key autoincrement,
|
||||||
path VARCHAR(512),
|
path VARCHAR(512),
|
||||||
deleted TINYINT,
|
relative_path TEXT,
|
||||||
size INTEGER,
|
deleted TINYINT,
|
||||||
file_name VARCHAR(512),
|
size INTEGER,
|
||||||
infos TEXT,
|
file_name VARCHAR(512),
|
||||||
info TEXT,
|
infos TEXT,
|
||||||
title_id INTEGER,
|
info TEXT,
|
||||||
season INTEGER default NULL,
|
title_id INTEGER,
|
||||||
duration REAL,
|
season INTEGER default NULL,
|
||||||
transmission INTEGER default NULL,
|
duration REAL,
|
||||||
lm INTEGER default NULL
|
transmission_id INTEGER default NULL,
|
||||||
|
linkomanija INTEGER default NULL,
|
||||||
|
progress REAL,
|
||||||
|
stream_provider TEXT default 'fs' not null
|
||||||
);`,
|
);`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await queryRunner.query(
|
await queryRunner.query(
|
||||||
`create unique index path
|
`create unique index files_path
|
||||||
on files (path);`,
|
on files (path);`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await queryRunner.query(
|
await queryRunner.query(
|
||||||
`create index season
|
`create index files_season
|
||||||
on files (season);`,
|
on files (season);`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await queryRunner.query(
|
await queryRunner.query(
|
||||||
`create index title_id
|
`create index files_title_id
|
||||||
on files (title_id);`,
|
on files (title_id);`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`create index files_relative_path
|
||||||
|
on files (relative_path);`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ export class CreateLrtCategoriesTable1672730402752
|
|||||||
await queryRunner.query(
|
await queryRunner.query(
|
||||||
`create table lrt_categories
|
`create table lrt_categories
|
||||||
(
|
(
|
||||||
category_url TEXT,
|
category_url VARCHAR(256),
|
||||||
category_id TEXT
|
category_id INTEGER,
|
||||||
|
title VARCHAR(128),
|
||||||
|
last_access TIMESTAMP,
|
||||||
|
thumb VARCHAR(256)
|
||||||
);`,
|
);`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -16,6 +19,11 @@ export class CreateLrtCategoriesTable1672730402752
|
|||||||
`create index lrt_categories_category_url_index
|
`create index lrt_categories_category_url_index
|
||||||
on lrt_categories (category_url);`,
|
on lrt_categories (category_url);`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`create index lrt_categories_category_id
|
||||||
|
on lrt_categories (category_id);`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
||||||
|
|
||||||
export class AddStreamProviderColumnToFilesTable1672820632908
|
|
||||||
implements MigrationInterface
|
|
||||||
{
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(
|
|
||||||
`alter table files
|
|
||||||
add stream_provider TEXT default 'fs' not null;`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(
|
|
||||||
`alter table files
|
|
||||||
drop column stream_provider;`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
||||||
|
|
||||||
export class AddRelativePathColumnToFilesTable1672990196772
|
|
||||||
implements MigrationInterface
|
|
||||||
{
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(
|
|
||||||
`alter table files
|
|
||||||
add relative_path TEXT;`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(
|
|
||||||
`alter table files
|
|
||||||
drop column relative_path;`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
||||||
|
|
||||||
export class RenameLinkomanijaColumnInFilesTable1673010469305
|
|
||||||
implements MigrationInterface
|
|
||||||
{
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE files RENAME COLUMN lm to linkomanija;`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE files RENAME COLUMN linkomanija to lm;`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
||||||
|
|
||||||
export class AddWebtorrentColumnsToFilesTable1673010885172
|
|
||||||
implements MigrationInterface
|
|
||||||
{
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(
|
|
||||||
`alter table files
|
|
||||||
add webtorrent INTEGER;`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(
|
|
||||||
`alter table files
|
|
||||||
drop column webtorrent;`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class CreateTorrentsTableAndRelateToFiles1673603746273
|
||||||
|
implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`alter table files
|
||||||
|
add torrent_id INTEGER;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`create unique index file_torrent_id
|
||||||
|
on files (torrent_id);`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`create table torrents
|
||||||
|
(
|
||||||
|
id INTEGER primary key,
|
||||||
|
magnet VARCHAR(512),
|
||||||
|
name VARCHAR(256),
|
||||||
|
path VARCHAR(512),
|
||||||
|
size INTEGER,
|
||||||
|
stopped TINYINT,
|
||||||
|
info_hash VARCHAR(64),
|
||||||
|
progress REAL,
|
||||||
|
transmission_id INTEGER,
|
||||||
|
info TEXT
|
||||||
|
);`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`create unique index torrent_magnet
|
||||||
|
on torrents (magnet);`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`create unique index torrent_transmission_id
|
||||||
|
on torrents (transmission_id);`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`alter table files
|
||||||
|
drop column torrent_id;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(`DROP TABLE torrents`);
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+3210
-35
File diff suppressed because it is too large
Load Diff
+8
-3
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": false,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prebuild": "rimraf dist",
|
"prebuild": "rimraf dist",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"@nestjs/platform-express": "^9.0.0",
|
"@nestjs/platform-express": "^9.0.0",
|
||||||
"@nestjs/serve-static": "^3.0.0",
|
"@nestjs/serve-static": "^3.0.0",
|
||||||
"@nestjs/typeorm": "^9.0.1",
|
"@nestjs/typeorm": "^9.0.1",
|
||||||
"@types/fluent-ffmpeg": "^2.1.20",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
@@ -42,16 +42,21 @@
|
|||||||
"sqlite": "^4.1.2",
|
"sqlite": "^4.1.2",
|
||||||
"sqlite3": "^5.1.2",
|
"sqlite3": "^5.1.2",
|
||||||
"torrent-name-parser": "^0.6.5",
|
"torrent-name-parser": "^0.6.5",
|
||||||
"typeorm": "^0.3.11"
|
"transmission": "^0.4.10",
|
||||||
|
"typeorm": "^0.3.11",
|
||||||
|
"webtorrent": "^1.9.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^9.0.0",
|
"@nestjs/cli": "^9.0.0",
|
||||||
"@nestjs/schematics": "^9.0.0",
|
"@nestjs/schematics": "^9.0.0",
|
||||||
"@nestjs/testing": "^9.0.0",
|
"@nestjs/testing": "^9.0.0",
|
||||||
|
"@types/cheerio": "^0.22.31",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
|
"@types/fluent-ffmpeg": "^2.1.20",
|
||||||
"@types/jest": "28.1.8",
|
"@types/jest": "28.1.8",
|
||||||
"@types/node": "^16.0.0",
|
"@types/node": "^16.0.0",
|
||||||
"@types/supertest": "^2.0.11",
|
"@types/supertest": "^2.0.11",
|
||||||
|
"@types/webtorrent": "^0.109.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { Controller, Get, Head, Param, Req, Res } from '@nestjs/common';
|
|||||||
import ApiResponse from '../Dto/api-response.dto';
|
import ApiResponse from '../Dto/api-response.dto';
|
||||||
import { AllFilesService } from './all-files.service';
|
import { AllFilesService } from './all-files.service';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { TitleTypeEnum } from '../../VideoFiles/Enum/title-type.enum';
|
import { TitleTypeEnum } from '../../Shared/Enum/title-type.enum';
|
||||||
|
import { KodiApiResponse } from '../Dto/kodi-api-response.type';
|
||||||
|
import NotificationResponse from '../Dto/notification-response.dto';
|
||||||
|
|
||||||
@Controller('api/all')
|
@Controller('api/all')
|
||||||
export class AllFilesController {
|
export class AllFilesController {
|
||||||
@@ -23,6 +25,16 @@ export class AllFilesController {
|
|||||||
return this.allFilesService.getListOfTitles(TitleTypeEnum.movie);
|
return this.allFilesService.getListOfTitles(TitleTypeEnum.movie);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('scan')
|
||||||
|
scanNewFiles(): KodiApiResponse {
|
||||||
|
return this.allFilesService.scanNewFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('delete/:fileId')
|
||||||
|
async deleteFile(@Param('fileId') fileId: string): Promise<KodiApiResponse> {
|
||||||
|
return this.allFilesService.deleteFile(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('play/:fileId')
|
@Get('play/:fileId')
|
||||||
@Head('play/:fileId')
|
@Head('play/:fileId')
|
||||||
play(
|
play(
|
||||||
@@ -37,7 +49,7 @@ export class AllFilesController {
|
|||||||
getTitle(
|
getTitle(
|
||||||
@Param('titleId') titleId: string, //actually a number, how to cast?
|
@Param('titleId') titleId: string, //actually a number, how to cast?
|
||||||
@Param('seasonId') seasonId: string, //actually a number, how to cast?
|
@Param('seasonId') seasonId: string, //actually a number, how to cast?
|
||||||
): Promise<ApiResponse> {
|
): Promise<KodiApiResponse> {
|
||||||
return this.allFilesService.loadTitle(
|
return this.allFilesService.loadTitle(
|
||||||
parseInt(titleId),
|
parseInt(titleId),
|
||||||
parseInt(seasonId),
|
parseInt(seasonId),
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ import { VideoFilesModule } from '../../VideoFiles/video-files.module';
|
|||||||
imports: [StreamerModule, VideoFilesModule],
|
imports: [StreamerModule, VideoFilesModule],
|
||||||
controllers: [AllFilesController],
|
controllers: [AllFilesController],
|
||||||
providers: [AllFilesService, KodiApiResponseFactory],
|
providers: [AllFilesService, KodiApiResponseFactory],
|
||||||
|
exports: [AllFilesService],
|
||||||
})
|
})
|
||||||
export class AllFilesModule {}
|
export class AllFilesModule {}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import { KodiApiResponseFactory } from '../kodi-api-response.factory';
|
|||||||
import { StreamerFacade } from '../../Streamer/streamer.facade';
|
import { StreamerFacade } from '../../Streamer/streamer.facade';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { VideoFilesFacade } from '../../VideoFiles/video-files.facade';
|
import { VideoFilesFacade } from '../../VideoFiles/video-files.facade';
|
||||||
import { TitleTypeEnum } from '../../VideoFiles/Enum/title-type.enum';
|
import { TitleTypeEnum } from '../../Shared/Enum/title-type.enum';
|
||||||
import { FileEntity } from '../../VideoFiles/Entity/file.entity';
|
import { FileEntity } from '../../Shared/Entity/file.entity';
|
||||||
import { TitleEntity } from '../../VideoFiles/Entity/title.entity';
|
import { TitleEntity } from '../../Shared/Entity/title.entity';
|
||||||
|
import { KodiApiResponse } from '../Dto/kodi-api-response.type';
|
||||||
|
import NotificationResponse from '../Dto/notification-response.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AllFilesService {
|
export class AllFilesService {
|
||||||
@@ -17,19 +19,17 @@ export class AllFilesService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
getMenu(): ApiResponse {
|
getMenu(): ApiResponse {
|
||||||
const apiResponse = this.koiApiResponseFactory.createApiResponse();
|
return this.koiApiResponseFactory
|
||||||
apiResponse
|
.createApiResponse()
|
||||||
.createItem()
|
.addNavigationItems(
|
||||||
.setLabel('Movies')
|
[
|
||||||
.setPath('all/movie')
|
['Movies', 'movie'],
|
||||||
.setToFolder();
|
['Shows', 'show'],
|
||||||
apiResponse
|
['Scan for new Files', 'scan'],
|
||||||
.createItem()
|
],
|
||||||
.setLabel('Shows')
|
'all/',
|
||||||
.setPath('all/show')
|
)
|
||||||
.setToFolder();
|
.setNoSort();
|
||||||
|
|
||||||
return apiResponse;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getListOfTitles(type: TitleTypeEnum): Promise<ApiResponse> {
|
async getListOfTitles(type: TitleTypeEnum): Promise<ApiResponse> {
|
||||||
@@ -50,16 +50,16 @@ export class AllFilesService {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadTitle(titleId: number, seasonId: number): Promise<ApiResponse> {
|
async loadTitle(titleId: number, seasonId: number): Promise<KodiApiResponse> {
|
||||||
const titleEntity = titleId
|
const titleEntity = titleId
|
||||||
? await this.videoFilesFacade.getTitleWithFiles(titleId)
|
? await this.videoFilesFacade.getTitleWithFiles(titleId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (titleEntity === null) {
|
if (titleEntity === null) {
|
||||||
//todo - alert instead of full api response
|
return this.koiApiResponseFactory.createNotificationResponse(
|
||||||
return this.koiApiResponseFactory
|
'Not found' + (titleId ? ' ' + titleId : ''),
|
||||||
.createApiResponse()
|
false,
|
||||||
.setTitle('Not found' + (titleId ? ' ' + titleId : ''));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (titleEntity.type === TitleTypeEnum.movie) {
|
if (titleEntity.type === TitleTypeEnum.movie) {
|
||||||
@@ -78,21 +78,33 @@ export class AllFilesService {
|
|||||||
return this.buildSeasonsResponse(titleEntity);
|
return this.buildSeasonsResponse(titleEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildFilesResponse(
|
buildFilesResponse(
|
||||||
title: string,
|
title: string,
|
||||||
fileEntities: Array<FileEntity>,
|
fileEntities: Array<FileEntity>,
|
||||||
|
torrentContext = false,
|
||||||
): ApiResponse {
|
): ApiResponse {
|
||||||
const apiResponse = this.koiApiResponseFactory.createApiResponse();
|
const apiResponse = this.koiApiResponseFactory.createApiResponse();
|
||||||
apiResponse.setTitle(title);
|
apiResponse.setTitle(title);
|
||||||
for (const fileEntity of fileEntities) {
|
for (const fileEntity of fileEntities) {
|
||||||
apiResponse
|
const item = apiResponse
|
||||||
.createItem()
|
.createItem()
|
||||||
.setLabel(fileEntity.fileName)
|
.setLabel(fileEntity.fileName)
|
||||||
.setPath('/api?path=all/play/' + fileEntity.id)
|
.setPath('/api?path=all/play/' + fileEntity.id)
|
||||||
.setToPlayable()
|
.setToPlayable()
|
||||||
.setPlot('Size ' + fileEntity.size)
|
.setPlot(this.buildPlot(fileEntity))
|
||||||
.setSize(fileEntity.size)
|
.setSize(fileEntity.size)
|
||||||
.setDuration(fileEntity.duration);
|
.setDuration(fileEntity.duration);
|
||||||
|
|
||||||
|
if (!torrentContext) {
|
||||||
|
if (fileEntity.torrentId) {
|
||||||
|
item.addContextMenu(
|
||||||
|
'Show torrent',
|
||||||
|
'torr/torrent-context/' + fileEntity.torrentId,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
item.addContextMenu('Delete file', 'all/delete/' + fileEntity.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiResponse;
|
return apiResponse;
|
||||||
@@ -102,7 +114,6 @@ export class AllFilesService {
|
|||||||
const apiResponse = this.koiApiResponseFactory.createApiResponse();
|
const apiResponse = this.koiApiResponseFactory.createApiResponse();
|
||||||
apiResponse.setTitle(titleEntity.title);
|
apiResponse.setTitle(titleEntity.title);
|
||||||
|
|
||||||
//todo - add num files count
|
|
||||||
const seasons = [];
|
const seasons = [];
|
||||||
const filesCount = {};
|
const filesCount = {};
|
||||||
for (const fileEntity of titleEntity.files) {
|
for (const fileEntity of titleEntity.files) {
|
||||||
@@ -144,4 +155,60 @@ export class AllFilesService {
|
|||||||
|
|
||||||
return this.streamerFacade.streamVideoFile(request, response, fileEntity);
|
return this.streamerFacade.streamVideoFile(request, response, fileEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getHRFileSize(size: number) {
|
||||||
|
// convert to human readable format.
|
||||||
|
const i: number = Math.floor(Math.log(size) / Math.log(1024));
|
||||||
|
const iPow: number = Math.pow(1024, i);
|
||||||
|
const sizePow: number = size / iPow;
|
||||||
|
|
||||||
|
return sizePow.toFixed(2) + ' ' + ['B', 'KB', 'MB', 'GB', 'TB'][i];
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPlot(fileEntity: FileEntity): string {
|
||||||
|
const plot: string[] = [];
|
||||||
|
if (fileEntity.size) {
|
||||||
|
plot.push('Size ' + this.getHRFileSize(fileEntity.size));
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrTorrProps: string[] = [];
|
||||||
|
if (fileEntity.linkomanija) {
|
||||||
|
arrTorrProps.push('LM');
|
||||||
|
}
|
||||||
|
if (fileEntity.transmissionId) {
|
||||||
|
arrTorrProps.push('transm');
|
||||||
|
}
|
||||||
|
if (fileEntity.torrentId) {
|
||||||
|
arrTorrProps.push('wt');
|
||||||
|
}
|
||||||
|
const torrProps = arrTorrProps.join(' + ');
|
||||||
|
if (torrProps) {
|
||||||
|
plot.push(torrProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileEntity.progress) {
|
||||||
|
const progress = Math.round(fileEntity.progress * 1000) / 10;
|
||||||
|
const stopped = fileEntity.torrent.stopped ? ' Stopped' : '';
|
||||||
|
plot.push(`${progress}%${stopped}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return plot.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
public scanNewFiles(): NotificationResponse {
|
||||||
|
this.videoFilesFacade.updateFsVideoFiles();
|
||||||
|
return this.koiApiResponseFactory.createNotificationResponse(
|
||||||
|
'Scanning for new files and deleting old.',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile(fileId: string) {
|
||||||
|
const id = parseInt(fileId);
|
||||||
|
const result = await this.videoFilesFacade.deleteFile(id);
|
||||||
|
return new NotificationResponse(
|
||||||
|
result ? 'successfully deleted' : 'delete failed',
|
||||||
|
result,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import ResponseItem from './response-item.dto';
|
import ResponseItem from './response-item.dto';
|
||||||
|
import { NavigationItem } from '../../Shared/Dto/navigation-item.dto';
|
||||||
|
|
||||||
export default class ApiResponse {
|
export default class ApiResponse {
|
||||||
items: Array<ResponseItem> = [];
|
items: Array<ResponseItem> = [];
|
||||||
content = 'videos';
|
content = 'videos';
|
||||||
updateList = true;
|
updateList = true;
|
||||||
|
selectList: boolean | undefined;
|
||||||
category: string | undefined;
|
category: string | undefined;
|
||||||
play: string | undefined;
|
play: string | undefined;
|
||||||
msgBoxOK: string | undefined;
|
msgBoxOK: string | undefined;
|
||||||
@@ -40,4 +42,25 @@ export default class ApiResponse {
|
|||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setShowAsSelectList() {
|
||||||
|
this.updateList = false;
|
||||||
|
this.selectList = true;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addNavigationItems(navigationItems: NavigationItem[], pathPrefix = '') {
|
||||||
|
for (const [label, path, searchFor] of navigationItems) {
|
||||||
|
const item = this.createItem()
|
||||||
|
.setLabel(label)
|
||||||
|
.setToFolder()
|
||||||
|
.setPath(pathPrefix + path ?? '');
|
||||||
|
if (searchFor !== undefined) {
|
||||||
|
item.setActionSearch(searchFor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import ApiResponse from './api-response.dto';
|
||||||
|
import NotificationResponse from './notification-response.dto';
|
||||||
|
|
||||||
|
export type KodiApiResponse = ApiResponse | NotificationResponse;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export default class NotificationResponse {
|
export default class NotificationResponse {
|
||||||
notification: string;
|
readonly notification: string;
|
||||||
refreshContainer: boolean;
|
readonly refreshContainer: boolean;
|
||||||
|
readonly updateList = false;
|
||||||
|
|
||||||
constructor(message: string, refreshContainer = true) {
|
constructor(message: string, refreshContainer = true) {
|
||||||
this.notification = message;
|
this.notification = message;
|
||||||
|
|||||||
@@ -4,10 +4,17 @@ import { firstValueFrom } from 'rxjs';
|
|||||||
import { SearchResponseInterface } from '../Interface/search-response.interface';
|
import { SearchResponseInterface } from '../Interface/search-response.interface';
|
||||||
import { SearchResponseDto } from '../Dto/search-response.dto';
|
import { SearchResponseDto } from '../Dto/search-response.dto';
|
||||||
import { SearchResponseItemDto } from '../Dto/search-response-item.dto';
|
import { SearchResponseItemDto } from '../Dto/search-response-item.dto';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { LrtCategory } from '../Entity/lrt-category.entity';
|
||||||
|
import { IsNull, Not, Repository } from 'typeorm';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LrtApiCategoryClient {
|
export class LrtApiCategoryClient {
|
||||||
constructor(private readonly httpService: HttpService) {}
|
constructor(
|
||||||
|
private readonly httpService: HttpService,
|
||||||
|
@InjectRepository(LrtCategory)
|
||||||
|
private lrtCategoriesRepository: Repository<LrtCategory>,
|
||||||
|
) {}
|
||||||
|
|
||||||
async getCategory(catId: string): Promise<SearchResponseDto> {
|
async getCategory(catId: string): Promise<SearchResponseDto> {
|
||||||
const url = 'https://www.lrt.lt/api/search?type=3&category_id=' + catId;
|
const url = 'https://www.lrt.lt/api/search?type=3&category_id=' + catId;
|
||||||
@@ -36,6 +43,23 @@ export class LrtApiCategoryClient {
|
|||||||
responseDto.addItem(itemDto);
|
responseDto.addItem(itemDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update last access
|
||||||
|
this.lrtCategoriesRepository
|
||||||
|
.update({ categoryId: catId }, { lastAccess: Date.now() })
|
||||||
|
.then((r) => {
|
||||||
|
if (r.affected === 0) {
|
||||||
|
//todo - search-update category info
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return responseDto;
|
return responseDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRecentCategories(count: number): Promise<LrtCategory[]> {
|
||||||
|
return this.lrtCategoriesRepository.find({
|
||||||
|
order: { lastAccess: 'DESC' },
|
||||||
|
take: count,
|
||||||
|
where: { lastAccess: Not(IsNull()) },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ export class LrtApiSearchClient {
|
|||||||
|
|
||||||
async searchCategories(query: string): Promise<SearchResponseDto> {
|
async searchCategories(query: string): Promise<SearchResponseDto> {
|
||||||
query = query === 'viskas' ? '' : query;
|
query = query === 'viskas' ? '' : query;
|
||||||
const url = 'https://www.lrt.lt/api/search?type=3&tema=' + query;
|
//const url = 'https://www.lrt.lt/api/search?type=3&tema=' + query;
|
||||||
|
const url = 'https://www.lrt.lt/api/search?type=3&order=desc&tema=' + query;
|
||||||
//https://www.lrt.lt/api/search?page=1&count=44&order=desc
|
//https://www.lrt.lt/api/search?page=1&count=44&order=desc
|
||||||
//https://www.lrt.lt/api/search?get_terms=1
|
//https://www.lrt.lt/api/search?get_terms=1
|
||||||
const resp = await firstValueFrom(this.httpService.get(url));
|
const resp = await firstValueFrom(this.httpService.get(url));
|
||||||
@@ -43,6 +44,18 @@ export class LrtApiSearchClient {
|
|||||||
searchItem.img_path_prefix +
|
searchItem.img_path_prefix +
|
||||||
'282x158' +
|
'282x158' +
|
||||||
searchItem.img_path_postfix;
|
searchItem.img_path_postfix;
|
||||||
|
let categoryId = await this.findCategoryIdInDb(searchItem.category_url);
|
||||||
|
|
||||||
|
if (categoryId === null) {
|
||||||
|
categoryId = await this.findCategoryIdInLrt(searchItem.category_url);
|
||||||
|
this.saveCategory(
|
||||||
|
searchItem.category_url,
|
||||||
|
categoryId,
|
||||||
|
searchItem.category_title,
|
||||||
|
itemDto.thumb,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
itemDto.categoryId = await this.findCategoryId(searchItem.category_url);
|
itemDto.categoryId = await this.findCategoryId(searchItem.category_url);
|
||||||
|
|
||||||
loadedCategories.add(searchItem.category_title);
|
loadedCategories.add(searchItem.category_title);
|
||||||
@@ -60,10 +73,17 @@ export class LrtApiSearchClient {
|
|||||||
return this.findCategoryIdInLrt(categoryUrl);
|
return this.findCategoryIdInLrt(categoryUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private saveCategory(categoryUrl: string, catId: string) {
|
private saveCategory(
|
||||||
|
categoryUrl: string,
|
||||||
|
catId: string,
|
||||||
|
catTitle: string,
|
||||||
|
thumb: string,
|
||||||
|
) {
|
||||||
const cat = new LrtCategory();
|
const cat = new LrtCategory();
|
||||||
cat.categoryUrl = categoryUrl;
|
cat.categoryUrl = categoryUrl;
|
||||||
cat.categoryId = catId;
|
cat.categoryId = catId;
|
||||||
|
cat.title = catTitle;
|
||||||
|
cat.thumb = thumb;
|
||||||
this.lrtCategoriesRepository.save(cat).then();
|
this.lrtCategoriesRepository.save(cat).then();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,9 +111,7 @@ export class LrtApiSearchClient {
|
|||||||
const interim = body.indexOf('"', start);
|
const interim = body.indexOf('"', start);
|
||||||
const end = body.indexOf('"', interim + 1);
|
const end = body.indexOf('"', interim + 1);
|
||||||
if (end > interim) {
|
if (end > interim) {
|
||||||
const catId = body.substring(interim + 1, end);
|
return body.substring(interim + 1, end);
|
||||||
this.saveCategory(categoryUrl, catId);
|
|
||||||
return catId;
|
|
||||||
} else {
|
} else {
|
||||||
throw 'unexpected response body 1';
|
throw 'unexpected response body 1';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,13 @@ export class LrtCategory {
|
|||||||
|
|
||||||
@Column({ name: 'category_id' })
|
@Column({ name: 'category_id' })
|
||||||
categoryId: string;
|
categoryId: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@Column({ name: 'last_access' })
|
||||||
|
lastAccess: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
thumb: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { LrtApiSearchClient } from './Client/lrt-api-search.client';
|
|||||||
import { SearchResponseDto } from './Dto/search-response.dto';
|
import { SearchResponseDto } from './Dto/search-response.dto';
|
||||||
import { LrtApiCategoryClient } from './Client/lrt-api-category.client';
|
import { LrtApiCategoryClient } from './Client/lrt-api-category.client';
|
||||||
import { LrtApiPlaylistClient } from './Client/lrt-api-playlist.client';
|
import { LrtApiPlaylistClient } from './Client/lrt-api-playlist.client';
|
||||||
|
import { LrtCategory } from './Entity/lrt-category.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LrtApiClient {
|
export class LrtApiClient {
|
||||||
@@ -23,4 +24,8 @@ export class LrtApiClient {
|
|||||||
getPlaylist(mediaUrl: string): Promise<string> {
|
getPlaylist(mediaUrl: string): Promise<string> {
|
||||||
return this.playlistClient.getPlaylistUrl(mediaUrl);
|
return this.playlistClient.getPlaylistUrl(mediaUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRecentCategories(count: number): Promise<LrtCategory[]> {
|
||||||
|
return this.categoryClient.getRecentCategories(count);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class LrtController {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
getLrtMenu(): ApiResponse {
|
getLrtMenu(): Promise<ApiResponse> {
|
||||||
return this.lrtService.getMainMenu();
|
return this.lrtService.getMainMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { LrtController } from './lrt.controller';
|
|||||||
import { LrtService } from './lrt.service';
|
import { LrtService } from './lrt.service';
|
||||||
import { KodiApiResponseFactory } from '../kodi-api-response.factory';
|
import { KodiApiResponseFactory } from '../kodi-api-response.factory';
|
||||||
import { LrtApiClientModule } from './LrtApiClient/lrt-api-client.module';
|
import { LrtApiClientModule } from './LrtApiClient/lrt-api-client.module';
|
||||||
import { RecentSearchesService } from '../RecentSearches/recent-searches.service';
|
import { RecentSearchesModule } from '../RecentSearches/recent-searches.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [LrtApiClientModule],
|
imports: [LrtApiClientModule, RecentSearchesModule],
|
||||||
controllers: [LrtController],
|
controllers: [LrtController],
|
||||||
providers: [KodiApiResponseFactory, LrtService, RecentSearchesService],
|
providers: [KodiApiResponseFactory, LrtService],
|
||||||
})
|
})
|
||||||
export class LrtModule {}
|
export class LrtModule {}
|
||||||
|
|||||||
@@ -12,34 +12,34 @@ export class LrtService {
|
|||||||
private readonly recentSearchesService: RecentSearchesService,
|
private readonly recentSearchesService: RecentSearchesService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
getMainMenu(): ApiResponse {
|
getMainMenu(): Promise<ApiResponse> {
|
||||||
const apiResponse = this.kodiApiResponseFactory.createApiResponse();
|
const apiResponse = this.kodiApiResponseFactory.createApiResponse();
|
||||||
apiResponse.setNoSort().setTitle('LRT.lt');
|
apiResponse.setNoSort().setTitle('LRT.lt');
|
||||||
this.createMainMenu(apiResponse);
|
|
||||||
|
|
||||||
return apiResponse;
|
return this.createMainMenu(apiResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
private createMainMenu(apiResponse: ApiResponse): void {
|
private async createMainMenu(apiResponse: ApiResponse): Promise<ApiResponse> {
|
||||||
//todo - refactor to 5 most recent searches + viskas?
|
apiResponse.addNavigationItems(
|
||||||
const items = {
|
[
|
||||||
'lrt/recent': 'neseniai ieskoti',
|
['paieška', 'search', ''],
|
||||||
'lrt/tema/vaikams': 'vaikams',
|
['neseniai ieskoti', 'recent'],
|
||||||
'lrt/tema/sportas': 'sportas',
|
['viskas', 'tema/viskas'],
|
||||||
'lrt/tema/kultura': 'kultura',
|
],
|
||||||
'lrt/tema/muzika': 'muzika',
|
'lrt/',
|
||||||
'lrt/tema/viskas': 'viskas',
|
);
|
||||||
'lrt/tema/tv-laidos': 'tv-laidos',
|
|
||||||
};
|
|
||||||
apiResponse
|
|
||||||
.createItem()
|
|
||||||
.setPath('lrt/search')
|
|
||||||
.setActionSearch()
|
|
||||||
.setLabel('paieška');
|
|
||||||
|
|
||||||
for (const [path, label] of Object.entries(items)) {
|
const recentCategories = await this.lrtApiClient.getRecentCategories(7);
|
||||||
apiResponse.createItem().setLabel(label).setToFolder().setPath(path);
|
for (const category of recentCategories) {
|
||||||
|
apiResponse
|
||||||
|
.createItem()
|
||||||
|
.setPath('/lrt/cat/' + category.categoryId)
|
||||||
|
.setThumb(category.thumb)
|
||||||
|
.setLabel(category.title)
|
||||||
|
.setToFolder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return apiResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchCategories(query: string): Promise<ApiResponse> {
|
async searchCategories(query: string): Promise<ApiResponse> {
|
||||||
|
|||||||
@@ -4,16 +4,21 @@ import { configService, ConfigService } from '../../config/config.service';
|
|||||||
import { KodiApiResponseFactory } from '../kodi-api-response.factory';
|
import { KodiApiResponseFactory } from '../kodi-api-response.factory';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { readFile, writeFile } from 'fs/promises';
|
import { readFile, writeFile } from 'fs/promises';
|
||||||
|
import { RecentSearchesConfig } from '../../config/recent-searches.config';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RecentSearchesService {
|
export class RecentSearchesService {
|
||||||
private readonly configService: ConfigService;
|
private readonly config: RecentSearchesConfig;
|
||||||
|
|
||||||
constructor(private readonly kodiApiResponseFactory: KodiApiResponseFactory) {
|
constructor(private readonly kodiApiResponseFactory: KodiApiResponseFactory) {
|
||||||
this.configService = configService;
|
this.config = configService.getRecentSearchesConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRecentSearches(module: string, path: string): Promise<ApiResponse> {
|
async getRecentSearches(module: string, path: string): Promise<ApiResponse> {
|
||||||
const data = await this.getRecentSearchesData(module);
|
const data = await this.getRecentSearchesData(module);
|
||||||
const response = this.kodiApiResponseFactory.createApiResponse();
|
const response = this.kodiApiResponseFactory
|
||||||
|
.createApiResponse()
|
||||||
|
.setNoSort();
|
||||||
for (const recentSearch of data) {
|
for (const recentSearch of data) {
|
||||||
response
|
response
|
||||||
.createItem()
|
.createItem()
|
||||||
@@ -28,14 +33,19 @@ export class RecentSearchesService {
|
|||||||
async addRecentSearch(module: string, term: string): Promise<void> {
|
async addRecentSearch(module: string, term: string): Promise<void> {
|
||||||
const data = await this.getRecentSearchesData(module);
|
const data = await this.getRecentSearchesData(module);
|
||||||
const index = data.indexOf(term);
|
const index = data.indexOf(term);
|
||||||
if (index > -1) {
|
if (index === 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
if (index > 0) {
|
||||||
data.splice(index, 1);
|
data.splice(index, 1);
|
||||||
}
|
}
|
||||||
data.push(term);
|
data.unshift(term);
|
||||||
const rawData = JSON.stringify(data);
|
|
||||||
const filePath = this.getFilePath(module);
|
|
||||||
|
|
||||||
await writeFile(filePath, rawData);
|
const rawData = JSON.stringify(
|
||||||
|
data.slice(0, this.config.getRecentSearchesLimit()),
|
||||||
|
);
|
||||||
|
const filePath = this.getFilePath(module);
|
||||||
|
writeFile(filePath, rawData).then();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getRecentSearchesData(module: string): Promise<Array<string>> {
|
private async getRecentSearchesData(module: string): Promise<Array<string>> {
|
||||||
@@ -46,9 +56,6 @@ export class RecentSearchesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getFilePath(module: string): string {
|
private getFilePath(module: string): string {
|
||||||
return join(
|
return join(this.config.getRecentSearchesFolder(), module + '.json');
|
||||||
this.configService.getPaths().getRecentSearchesFolder(),
|
|
||||||
module + '.json',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||||
|
import ApiResponse from '../Dto/api-response.dto';
|
||||||
|
import { TorrentService } from './torrent.service';
|
||||||
|
|
||||||
|
@Controller('api/torr')
|
||||||
|
export class TorrentController {
|
||||||
|
constructor(private readonly torrService: TorrentService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async getMenu(): Promise<ApiResponse> {
|
||||||
|
return this.torrService.getMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('torrs')
|
||||||
|
async getTorrents(): Promise<ApiResponse> {
|
||||||
|
return this.torrService.getTorrents();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('torr/:id')
|
||||||
|
async getTorrFiles(@Param('id') id: string): Promise<ApiResponse> {
|
||||||
|
return this.torrService.getTorrentFiles(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('torrent/:id')
|
||||||
|
async getOneTorrentAsList(@Param('id') id: string) {
|
||||||
|
return this.torrService.getOneTorrentAsList(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('torrent-context/:id')
|
||||||
|
async getOneTorrentAsListFromContextMenu(@Param('id') id: string) {
|
||||||
|
return this.torrService.getOneTorrentAsListFromContextMenu(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('search/:provider')
|
||||||
|
getSearch(
|
||||||
|
@Query('search') query: string,
|
||||||
|
@Param('provider') provider: string,
|
||||||
|
) {
|
||||||
|
return this.torrService.search(query, provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('recent/:provider')
|
||||||
|
getRecent(@Param('provider') provider: string) {
|
||||||
|
return this.torrService.getRecentSearches(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('add/:torrId')
|
||||||
|
add(@Param('torrId') torrId: string) {
|
||||||
|
return this.torrService.add(torrId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('custom-menu/:provider')
|
||||||
|
async getCustomMenu(@Param() params: string[]) {
|
||||||
|
return this.torrService.getCustomMenu(params['provider'], '');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('custom-menu/:provider/*')
|
||||||
|
async getCustomMenuAction(@Param() params: string[]) {
|
||||||
|
return this.torrService.getCustomMenu(params['provider'], params[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('delete/:id')
|
||||||
|
deleteTorrent(@Param('id') id: string) {
|
||||||
|
return this.torrService.deleteTorrent(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('resume/:id')
|
||||||
|
async resumeTorrent(@Param('id') id: string) {
|
||||||
|
return this.torrService.resumeTorrent(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('stop/:id')
|
||||||
|
async stopTorrent(@Param('id') id: string) {
|
||||||
|
return this.torrService.stopTorrent(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('stop-transmission/:id')
|
||||||
|
async stopTransmissionTorrent(@Param('id') id: string) {
|
||||||
|
return this.torrService.stopTransmissionTorrent(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('add-transmission/:id')
|
||||||
|
async addTransmissionTorrent(@Param('id') id: string) {
|
||||||
|
return this.torrService.addTransmissionTorrent(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TorrentService } from './torrent.service';
|
||||||
|
import { TorrentController } from './torrent.controller';
|
||||||
|
import { KodiApiResponseFactory } from '../kodi-api-response.factory';
|
||||||
|
import { SearchClientModule } from '../../Torrent/SearchClient/search-client.module';
|
||||||
|
import { RecentSearchesModule } from '../RecentSearches/recent-searches.module';
|
||||||
|
import { TorrentDownloadClientModule } from '../../Torrent/DownloadClient/torrent-download-client.module';
|
||||||
|
import { AllFilesModule } from '../AllFiles/all-files.module';
|
||||||
|
import { TorrentSeedClientModule } from '../../Torrent/SeedClient/torrent-seed-client.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
SearchClientModule,
|
||||||
|
RecentSearchesModule,
|
||||||
|
TorrentDownloadClientModule,
|
||||||
|
AllFilesModule,
|
||||||
|
TorrentSeedClientModule,
|
||||||
|
],
|
||||||
|
controllers: [TorrentController],
|
||||||
|
providers: [TorrentService, KodiApiResponseFactory],
|
||||||
|
})
|
||||||
|
export class TorrentModule {}
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { KodiApiResponseFactory } from '../kodi-api-response.factory';
|
||||||
|
import ApiResponse from '../Dto/api-response.dto';
|
||||||
|
import { SearchClientFacade } from '../../Torrent/SearchClient/search-client.facade';
|
||||||
|
import { TorrentSearchResult } from '../../Torrent/Dto/torrent-search-result.class';
|
||||||
|
import { RecentSearchesService } from '../RecentSearches/recent-searches.service';
|
||||||
|
import { TorrentSearchClientResponse } from '../../Torrent/Dto/torrent-search-client-response.dto';
|
||||||
|
import { TorrentDownloadClient } from '../../Torrent/DownloadClient/torrent-download.client';
|
||||||
|
import { AllFilesService } from '../AllFiles/all-files.service';
|
||||||
|
import NotificationResponse from '../Dto/notification-response.dto';
|
||||||
|
import { TorrentEntity } from '../../Shared/Entity/torrent.entity';
|
||||||
|
import { TorrentSeedClient } from '../../Torrent/SeedClient/torrent-seed.client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TorrentService {
|
||||||
|
private torrentsCache: Record<string, TorrentSearchResult> = {}; //no need for all that info, magnet is enough
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly koiApiResponseFactory: KodiApiResponseFactory,
|
||||||
|
private readonly torrSearchClient: SearchClientFacade,
|
||||||
|
private readonly recentSearchesService: RecentSearchesService,
|
||||||
|
private readonly torrDownloadClient: TorrentDownloadClient,
|
||||||
|
private readonly torrSeedClient: TorrentSeedClient,
|
||||||
|
private readonly allFilesService: AllFilesService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getMenu(): Promise<ApiResponse> {
|
||||||
|
const apiResponse = this.koiApiResponseFactory
|
||||||
|
.createApiResponse()
|
||||||
|
.setNoSort();
|
||||||
|
const searchClientNames = this.torrSearchClient.getSearchClientNames();
|
||||||
|
apiResponse.addNavigationItems([['Torrs', 'torrs']], 'torr/');
|
||||||
|
apiResponse.setTitle('Torrent service');
|
||||||
|
for (const clientName of searchClientNames) {
|
||||||
|
apiResponse.addNavigationItems(
|
||||||
|
[
|
||||||
|
[clientName + ' Search', 'search/' + clientName, ''],
|
||||||
|
[clientName + ' Neseniai ieškoti', 'recent/' + clientName],
|
||||||
|
],
|
||||||
|
'torr/',
|
||||||
|
);
|
||||||
|
const customMenu = await this.torrSearchClient.getCustomMenu(clientName);
|
||||||
|
apiResponse.addNavigationItems(
|
||||||
|
customMenu.navigationItems,
|
||||||
|
'torr/custom-menu/' + clientName + '/',
|
||||||
|
);
|
||||||
|
this.addTorrentSearchResultsToApiResponse(
|
||||||
|
apiResponse,
|
||||||
|
customMenu.searchResults,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomMenu(provider: string, path: string) {
|
||||||
|
const apiResponse = this.koiApiResponseFactory.createApiResponse();
|
||||||
|
apiResponse.setNoSort();
|
||||||
|
const customMenu: TorrentSearchClientResponse | false =
|
||||||
|
await this.torrSearchClient
|
||||||
|
.getCustomMenu(provider, path)
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
if (customMenu === false) {
|
||||||
|
return this.koiApiResponseFactory.createNotificationResponse(
|
||||||
|
'Error while searching',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
apiResponse.setTitle(customMenu.title);
|
||||||
|
apiResponse.addNavigationItems(
|
||||||
|
customMenu.navigationItems,
|
||||||
|
'torr/custom-menu/' + provider + '/',
|
||||||
|
);
|
||||||
|
this.addTorrentSearchResultsToApiResponse(
|
||||||
|
apiResponse,
|
||||||
|
customMenu.searchResults,
|
||||||
|
);
|
||||||
|
|
||||||
|
return apiResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(
|
||||||
|
query: string,
|
||||||
|
provider: string,
|
||||||
|
): Promise<ApiResponse | NotificationResponse> {
|
||||||
|
this.recentSearchesService.addRecentSearch('torr', query).then(/*ignore*/);
|
||||||
|
const searchResults: TorrentSearchClientResponse | false =
|
||||||
|
await this.torrSearchClient.search(query, provider).catch(() => false);
|
||||||
|
|
||||||
|
if (searchResults === false) {
|
||||||
|
return this.koiApiResponseFactory.createNotificationResponse(
|
||||||
|
'Error while searching',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiResponse = this.koiApiResponseFactory.createApiResponse();
|
||||||
|
apiResponse.setNoSort();
|
||||||
|
this.addTorrentSearchResultsToApiResponse(
|
||||||
|
apiResponse,
|
||||||
|
searchResults.searchResults,
|
||||||
|
);
|
||||||
|
|
||||||
|
return apiResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRecentSearches(provider: string): Promise<ApiResponse> {
|
||||||
|
return this.recentSearchesService.getRecentSearches(
|
||||||
|
'torr',
|
||||||
|
'torr/search/' + provider,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async add(torrId: string): Promise<ApiResponse | NotificationResponse> {
|
||||||
|
const torrSearchData = this.torrentsCache[torrId];
|
||||||
|
if (torrSearchData === undefined) {
|
||||||
|
throw new NotFoundException('torrent search data not found');
|
||||||
|
}
|
||||||
|
const fileEntities = await this.torrDownloadClient
|
||||||
|
.addTorrent(torrSearchData.magnet)
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
|
if (fileEntities === null) {
|
||||||
|
return this.koiApiResponseFactory.createNotificationResponse(
|
||||||
|
'error adding torrent',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.allFilesService.buildFilesResponse(
|
||||||
|
torrSearchData.name,
|
||||||
|
fileEntities,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addTorrentSearchResultsToApiResponse(
|
||||||
|
apiResponse: ApiResponse,
|
||||||
|
searchResults: TorrentSearchResult[],
|
||||||
|
): void {
|
||||||
|
for (const searchResult of searchResults) {
|
||||||
|
this.torrentsCache[searchResult.id] = searchResult;
|
||||||
|
apiResponse
|
||||||
|
.createItem()
|
||||||
|
.setDate(searchResult.date)
|
||||||
|
.setLabel(searchResult.name)
|
||||||
|
.setToFolder()
|
||||||
|
.setPath('torr/add/' + searchResult.id)
|
||||||
|
.setPlot(
|
||||||
|
[
|
||||||
|
searchResult.date,
|
||||||
|
searchResult.size,
|
||||||
|
'S' + searchResult.seeders + ' L' + searchResult.leechers,
|
||||||
|
searchResult.info,
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTorrents(): Promise<ApiResponse> {
|
||||||
|
const torrentEntities = await this.torrDownloadClient.getTorrents();
|
||||||
|
const apiResponse = this.koiApiResponseFactory.createApiResponse();
|
||||||
|
apiResponse.setTitle('Torrents');
|
||||||
|
|
||||||
|
for (const torrentEntity of torrentEntities) {
|
||||||
|
this.addTorrentToApiResponse(torrentEntity, apiResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
private addTorrentToApiResponse(
|
||||||
|
torrentEntity: TorrentEntity,
|
||||||
|
apiResponse: ApiResponse,
|
||||||
|
): void {
|
||||||
|
const firstFileEntity = torrentEntity.files[0];
|
||||||
|
if (!firstFileEntity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstFileEntity.progress = torrentEntity.progress;
|
||||||
|
firstFileEntity.size = torrentEntity.size;
|
||||||
|
const item = apiResponse
|
||||||
|
.createItem()
|
||||||
|
.setPlot(this.buildPlot(torrentEntity))
|
||||||
|
.setLabel(torrentEntity.name)
|
||||||
|
.setPath('/torr/torr/' + torrentEntity.id)
|
||||||
|
.setToFolder();
|
||||||
|
|
||||||
|
if (torrentEntity.stopped && torrentEntity.progress !== 1) {
|
||||||
|
item.addContextMenu('resume wt', '/torr/resume/' + torrentEntity.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!torrentEntity.stopped) {
|
||||||
|
item.addContextMenu('stop wt', '/torr/stop/' + torrentEntity.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (torrentEntity.transmissionId) {
|
||||||
|
item.addContextMenu(
|
||||||
|
'stop transmission',
|
||||||
|
'/torr/stop-transmission/' + torrentEntity.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!torrentEntity.transmissionId && torrentEntity.progress === 1) {
|
||||||
|
item.addContextMenu(
|
||||||
|
'add transmission',
|
||||||
|
'/torr/add-transmission/' + torrentEntity.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
item.addContextMenu('delete', '/torr/delete/' + torrentEntity.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPlot(torrentEntity: TorrentEntity): string {
|
||||||
|
const plot: string[] = [];
|
||||||
|
if (torrentEntity.size) {
|
||||||
|
plot.push(
|
||||||
|
'Size ' + this.allFilesService.getHRFileSize(torrentEntity.size),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrTorrProps: string[] = [];
|
||||||
|
if (torrentEntity.linkomanija) {
|
||||||
|
arrTorrProps.push('LM');
|
||||||
|
}
|
||||||
|
if (torrentEntity.transmissionId) {
|
||||||
|
arrTorrProps.push('transm');
|
||||||
|
}
|
||||||
|
|
||||||
|
arrTorrProps.push('wt');
|
||||||
|
|
||||||
|
const torrProps = arrTorrProps.join(' + ');
|
||||||
|
if (torrProps) {
|
||||||
|
plot.push(torrProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (torrentEntity.progress) {
|
||||||
|
const progress = Math.round(torrentEntity.progress * 1000) / 10;
|
||||||
|
const stopped = torrentEntity.stopped ? ' Stopped' : '';
|
||||||
|
plot.push(`${progress}%${stopped}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return plot.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
getOneTorrentAsListFromContextMenu(torrentId: string) {
|
||||||
|
//by default context menu does not update container (view), we need to force it
|
||||||
|
return {
|
||||||
|
forceUpdate: {
|
||||||
|
urlParams: {
|
||||||
|
action: 'query',
|
||||||
|
path: 'torr/torrent/' + torrentId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOneTorrentAsList(torrentId: string): Promise<ApiResponse> {
|
||||||
|
const id = parseInt(torrentId);
|
||||||
|
const torrentEntity = await this.torrDownloadClient.getTorrentById(id);
|
||||||
|
const apiResponse = this.koiApiResponseFactory.createApiResponse();
|
||||||
|
if (torrentEntity) {
|
||||||
|
this.addTorrentToApiResponse(torrentEntity, apiResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTorrentFiles(id: string): Promise<ApiResponse> {
|
||||||
|
const torrentEntity = await this.torrDownloadClient.getTorrentById(
|
||||||
|
parseInt(id),
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.allFilesService.buildFilesResponse(
|
||||||
|
torrentEntity.name,
|
||||||
|
torrentEntity.files,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopTorrent(id: string) {
|
||||||
|
const torrId = parseInt(id);
|
||||||
|
try {
|
||||||
|
await this.torrDownloadClient.stopTorrent(torrId);
|
||||||
|
} catch (e) {
|
||||||
|
return this.koiApiResponseFactory.createNotificationResponse(
|
||||||
|
'error stopping',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.koiApiResponseFactory.createNotificationResponse('stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
async resumeTorrent(id: string) {
|
||||||
|
const torrId = parseInt(id);
|
||||||
|
try {
|
||||||
|
await this.torrDownloadClient.resumeTorrent(torrId);
|
||||||
|
} catch (e) {
|
||||||
|
return this.koiApiResponseFactory.createNotificationResponse(
|
||||||
|
'error resuming',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.koiApiResponseFactory.createNotificationResponse('resumed');
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopTransmissionTorrent(id: string) {
|
||||||
|
const torrId = parseInt(id);
|
||||||
|
const result = await this.torrSeedClient
|
||||||
|
.removeTorrent(torrId)
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
return new NotificationResponse(
|
||||||
|
result ? 'torrent removed from transmission' : 'removal failed',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addTransmissionTorrent(id: string) {
|
||||||
|
const torrId = parseInt(id);
|
||||||
|
const result = await this.torrSeedClient
|
||||||
|
.addTorrentById(torrId)
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
return new NotificationResponse(
|
||||||
|
result ? 'torrent added to transmission' : 'adding failed',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTorrent(id: string) {
|
||||||
|
const torrId = parseInt(id);
|
||||||
|
const result = await this.torrSeedClient
|
||||||
|
.removeTorrent(torrId, true)
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
return new NotificationResponse(
|
||||||
|
result ? 'torrent deleted' : 'deleting from wt not implemented',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,12 @@ import { join } from 'path';
|
|||||||
|
|
||||||
export class KodiApiUrlRewriteMiddleware implements NestMiddleware {
|
export class KodiApiUrlRewriteMiddleware implements NestMiddleware {
|
||||||
async use(req: Request, res: Response, next: NextFunction) {
|
async use(req: Request, res: Response, next: NextFunction) {
|
||||||
let path = '';
|
|
||||||
if (req.query && req.query.path) {
|
if (req.query && req.query.path) {
|
||||||
|
let path = '';
|
||||||
path = req.query.path as string;
|
path = req.query.path as string;
|
||||||
|
const query = req.url.split('?').pop();
|
||||||
|
req.url = join('/api/', path) + (query ? '?' + query : '');
|
||||||
}
|
}
|
||||||
const query = req.url.split('?').pop();
|
|
||||||
req.url = join('/api/', path) + (query ? '?' + query : '');
|
|
||||||
|
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,9 @@ export class KodiApiController {
|
|||||||
getMainMenu() {
|
getMainMenu() {
|
||||||
return this.kodiApiService.getMainMenu();
|
return this.kodiApiService.getMainMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('test')
|
||||||
|
getCurrentTest() {
|
||||||
|
return this.kodiApiService.getCurrentTest();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import { LrtModule } from './LRT/lrt.module';
|
|||||||
import { KodiApiService } from './kodi-api.service';
|
import { KodiApiService } from './kodi-api.service';
|
||||||
import { KodiApiResponseFactory } from './kodi-api-response.factory';
|
import { KodiApiResponseFactory } from './kodi-api-response.factory';
|
||||||
import { AllFilesModule } from './AllFiles/all-files.module';
|
import { AllFilesModule } from './AllFiles/all-files.module';
|
||||||
|
import { TorrentModule } from './Torrent/torrent.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [LrtModule, AllFilesModule],
|
imports: [LrtModule, AllFilesModule, TorrentModule],
|
||||||
controllers: [KodiApiController],
|
controllers: [KodiApiController],
|
||||||
providers: [KodiApiService, KodiApiResponseFactory],
|
providers: [KodiApiService, KodiApiResponseFactory],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,23 +9,27 @@ export class KodiApiService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
getMainMenu(): ApiResponse {
|
getMainMenu(): ApiResponse {
|
||||||
const apiResponse = this.kodiApiResponseFactory.createApiResponse();
|
return this.kodiApiResponseFactory
|
||||||
|
.createApiResponse()
|
||||||
apiResponse.setTitle('Menu');
|
.setTitle('Menu')
|
||||||
this.createMainMenu(apiResponse);
|
.addNavigationItems([
|
||||||
|
['All Files', 'all'],
|
||||||
return apiResponse;
|
['Torr', 'torr'],
|
||||||
|
['LRT.lt', 'lrt'],
|
||||||
|
//['Test', 'test'],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private createMainMenu(apiResponse: ApiResponse): void {
|
getCurrentTest() {
|
||||||
const items = {
|
return this.kodiApiResponseFactory
|
||||||
all: 'All files',
|
.createApiResponse()
|
||||||
torr: 'Torr',
|
.setTitle('Menu')
|
||||||
lrt: 'LRT.lt',
|
.addNavigationItems([
|
||||||
};
|
['select 1', 'all'],
|
||||||
|
['select 2', 'lrt'],
|
||||||
for (const [path, label] of Object.entries(items)) {
|
['select 3', 'torr'],
|
||||||
apiResponse.createItem().setLabel(label).setToFolder().setPath(path);
|
['select 4 search', 'test', ''],
|
||||||
}
|
])
|
||||||
|
.setShowAsSelectList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export class RequestLogMiddleware implements NestMiddleware {
|
|||||||
async use(req: Request, res: Response, next: NextFunction) {
|
async use(req: Request, res: Response, next: NextFunction) {
|
||||||
const strIP = this.extractIp(req);
|
const strIP = this.extractIp(req);
|
||||||
this.log(strIP + ' ' + req.method + ' ' + decodeURI(req.url));
|
this.log(strIP + ' ' + req.method + ' ' + decodeURI(req.url));
|
||||||
|
console.log(decodeURI(req.url));
|
||||||
|
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export type NavigationItem = [label: string, path?: string, searchFor?: string];
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
|
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||||
import { TitleEntity } from './title.entity';
|
import { TitleEntity } from './title.entity';
|
||||||
import { StreamProviderEnum } from '../../Streamer/ReadStreamProvider/stream-provider.enum';
|
import { StreamProviderEnum } from '../Enum/stream-provider.enum';
|
||||||
|
import { TorrentEntity } from './torrent.entity';
|
||||||
|
|
||||||
@Entity('files')
|
@Entity('files')
|
||||||
export class FileEntity {
|
export class FileEntity {
|
||||||
@@ -44,13 +45,19 @@ export class FileEntity {
|
|||||||
@Column({ name: 'relative_path' })
|
@Column({ name: 'relative_path' })
|
||||||
relativePath: string;
|
relativePath: string;
|
||||||
|
|
||||||
//todo - consider if these 3 below should be better organized
|
@Column({ name: 'transmission_id' })
|
||||||
@Column()
|
transmissionId: number;
|
||||||
transmission: boolean;
|
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
linkomanija: boolean;
|
linkomanija: boolean;
|
||||||
|
|
||||||
@Column()
|
@Column({ name: 'torrent_id' })
|
||||||
webtorrent: boolean;
|
torrentId: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => TorrentEntity, (torrent) => torrent.files)
|
||||||
|
@JoinColumn({ name: 'torrent_id' })
|
||||||
|
torrent: TorrentEntity;
|
||||||
|
|
||||||
|
@Column({ type: 'float' })
|
||||||
|
progress: number;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Column, Entity, OneToMany, PrimaryColumn } from 'typeorm';
|
||||||
|
import { FileEntity } from './file.entity';
|
||||||
|
|
||||||
|
@Entity('torrents')
|
||||||
|
export class TorrentEntity {
|
||||||
|
@PrimaryColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
magnet: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
path: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
stopped: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'info_hash' })
|
||||||
|
infoHash: string;
|
||||||
|
|
||||||
|
@Column({ type: 'float' })
|
||||||
|
progress: number;
|
||||||
|
|
||||||
|
@OneToMany(() => FileEntity, (file) => file.torrent)
|
||||||
|
files: FileEntity[];
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
size: number;
|
||||||
|
|
||||||
|
@Column({ name: 'transmission_id' })
|
||||||
|
transmissionId: number;
|
||||||
|
|
||||||
|
//todo - consider changing to a must-seed property for other private trackers (trl, etc)
|
||||||
|
@Column()
|
||||||
|
linkomanija: boolean;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
info: string;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FileEntity } from '../../../VideoFiles/Entity/file.entity';
|
import { FileEntity } from '../../../Shared/Entity/file.entity';
|
||||||
import { ReadStreamCreatable } from '../../Interface/read-stream-creatable.interface';
|
import { ReadStreamCreatable } from '../../Interface/read-stream-creatable.interface';
|
||||||
import { MimeService } from '../../Mime/mime.service';
|
import { MimeService } from '../../Mime/mime.service';
|
||||||
import { Stats } from 'fs';
|
import { Stats } from 'fs';
|
||||||
@@ -6,13 +6,17 @@ import { stat } from 'fs/promises';
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { ReadStreamCreatableFile } from './read-stream-creatable-file.dto';
|
import { ReadStreamCreatableFile } from './read-stream-creatable-file.dto';
|
||||||
import { ReadStreamCreatableProviderInterface } from '../read-stream-creatable-provider.interface';
|
import { ReadStreamCreatableProviderInterface } from '../read-stream-creatable-provider.interface';
|
||||||
import { StreamProviderEnum } from '../stream-provider.enum';
|
import { StreamProviderEnum } from '../../../Shared/Enum/stream-provider.enum';
|
||||||
|
import { VideoFilesFacade } from '../../../VideoFiles/video-files.facade';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FsReadStreamCreatableProvider
|
export class FsReadStreamCreatableProvider
|
||||||
implements ReadStreamCreatableProviderInterface
|
implements ReadStreamCreatableProviderInterface
|
||||||
{
|
{
|
||||||
constructor(private readonly mimeService: MimeService) {}
|
constructor(
|
||||||
|
private readonly mimeService: MimeService,
|
||||||
|
private readonly videoFilesService: VideoFilesFacade,
|
||||||
|
) {}
|
||||||
|
|
||||||
supports(fileEntity: FileEntity): boolean {
|
supports(fileEntity: FileEntity): boolean {
|
||||||
return fileEntity.streamProvider === StreamProviderEnum.fs;
|
return fileEntity.streamProvider === StreamProviderEnum.fs;
|
||||||
@@ -24,7 +28,7 @@ export class FsReadStreamCreatableProvider
|
|||||||
const filePath = fileEntity.path;
|
const filePath = fileEntity.path;
|
||||||
const stats: Stats | false = await stat(filePath).catch(() => false);
|
const stats: Stats | false = await stat(filePath).catch(() => false);
|
||||||
if (stats === false) {
|
if (stats === false) {
|
||||||
//todo - set the file to deleted, since no longer available. Events?
|
this.videoFilesService.updateFsVideoFiles();
|
||||||
throw new NotFoundException('file not found');
|
throw new NotFoundException('file not found');
|
||||||
}
|
}
|
||||||
const mimeType = this.mimeService.getMime(filePath);
|
const mimeType = this.mimeService.getMime(filePath);
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { ReadStream } from 'fs';
|
||||||
|
import { ReadStreamOptions } from '../../Interface/read-stream-options.interface';
|
||||||
|
import { ReadStreamCreatable } from '../../Interface/read-stream-creatable.interface';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { TorrentFile } from 'webtorrent';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ReadStreamCreatableTorrentFile implements ReadStreamCreatable {
|
||||||
|
constructor(
|
||||||
|
private readonly torrentFile: TorrentFile,
|
||||||
|
readonly length: number,
|
||||||
|
readonly mimeType: string,
|
||||||
|
) {
|
||||||
|
this.torrentFile = torrentFile;
|
||||||
|
this.length = length;
|
||||||
|
this.mimeType = mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
createReadStream(options: ReadStreamOptions): ReadStream {
|
||||||
|
return this.torrentFile.createReadStream(
|
||||||
|
options as { start: number; end: number },
|
||||||
|
) as ReadStream;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { FileEntity } from '../../../Shared/Entity/file.entity';
|
||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { MimeService } from '../../Mime/mime.service';
|
||||||
|
import { ReadStreamCreatableProviderInterface } from '../read-stream-creatable-provider.interface';
|
||||||
|
import { TorrentDownloadClient } from '../../../Torrent/DownloadClient/torrent-download.client';
|
||||||
|
import { StreamProviderEnum } from '../../../Shared/Enum/stream-provider.enum';
|
||||||
|
import { ReadStreamCreatable } from '../../Interface/read-stream-creatable.interface';
|
||||||
|
import { ReadStreamCreatableTorrentFile } from './read-stream-creatable-torrent-file.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WtReadStreamCreatableProvider
|
||||||
|
implements ReadStreamCreatableProviderInterface
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
private readonly mimeService: MimeService,
|
||||||
|
private readonly torrDlClient: TorrentDownloadClient,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
supports(fileEntity: FileEntity): boolean {
|
||||||
|
return fileEntity.streamProvider === StreamProviderEnum.wt;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getReadStreamCreatable(
|
||||||
|
fileEntity: FileEntity,
|
||||||
|
): Promise<ReadStreamCreatable> {
|
||||||
|
if (!fileEntity.torrent) {
|
||||||
|
throw new NotFoundException('torrent not found');
|
||||||
|
}
|
||||||
|
const torrentFile = await this.torrDlClient.getTorrentFile(fileEntity);
|
||||||
|
if (torrentFile === null) {
|
||||||
|
//todo - rescan files, probably moved to fs provider. Events?
|
||||||
|
throw new NotFoundException('torrent file not found');
|
||||||
|
}
|
||||||
|
const mimeType = this.mimeService.getMime(fileEntity.relativePath);
|
||||||
|
|
||||||
|
return new ReadStreamCreatableTorrentFile(
|
||||||
|
torrentFile,
|
||||||
|
torrentFile.length,
|
||||||
|
mimeType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FileEntity } from '../../VideoFiles/Entity/file.entity';
|
import { FileEntity } from '../../Shared/Entity/file.entity';
|
||||||
import { ReadStreamCreatable } from '../Interface/read-stream-creatable.interface';
|
import { ReadStreamCreatable } from '../Interface/read-stream-creatable.interface';
|
||||||
|
|
||||||
export interface ReadStreamCreatableProviderInterface {
|
export interface ReadStreamCreatableProviderInterface {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ReadStreamCreatable } from '../Interface/read-stream-creatable.interface';
|
import { ReadStreamCreatable } from '../Interface/read-stream-creatable.interface';
|
||||||
import { FileEntity } from '../../VideoFiles/Entity/file.entity';
|
import { FileEntity } from '../../Shared/Entity/file.entity';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { ReadStreamCreatableProviderInterface } from './read-stream-creatable-provider.interface';
|
import { ReadStreamCreatableProviderInterface } from './read-stream-creatable-provider.interface';
|
||||||
|
|
||||||
@@ -17,5 +17,6 @@ export class ReadStreamCreatableProvider {
|
|||||||
return provider.getReadStreamCreatable(fileEntity);
|
return provider.getReadStreamCreatable(fileEntity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return Promise.reject('no stream provider');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,5 +17,8 @@ export class ResponseSenderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readable.pipe(response);
|
readable.pipe(response);
|
||||||
|
readable.on('error', (e) => {
|
||||||
|
//todo - figure out the Error: Writable stream closed prematurely
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { FileEntity } from '../VideoFiles/Entity/file.entity';
|
import { FileEntity } from '../Shared/Entity/file.entity';
|
||||||
import { ReadStreamCreatableProvider } from './ReadStreamProvider/read-stream-creatable.provider';
|
import { ReadStreamCreatableProvider } from './ReadStreamProvider/read-stream-creatable.provider';
|
||||||
import { StreamerService } from './StreamerService/streamer.service';
|
import { StreamerService } from './StreamerService/streamer.service';
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,12 @@ import {
|
|||||||
ReadStreamCreatableProviders,
|
ReadStreamCreatableProviders,
|
||||||
} from './ReadStreamProvider/read-stream-creatable.provider';
|
} from './ReadStreamProvider/read-stream-creatable.provider';
|
||||||
import { FsReadStreamCreatableProvider } from './ReadStreamProvider/FileSystem/fs-read-stream-creatable.provider';
|
import { FsReadStreamCreatableProvider } from './ReadStreamProvider/FileSystem/fs-read-stream-creatable.provider';
|
||||||
|
import { WtReadStreamCreatableProvider } from './ReadStreamProvider/Webtorrent/wt-read-stream-creatable.provider';
|
||||||
|
import { TorrentDownloadClientModule } from '../Torrent/DownloadClient/torrent-download-client.module';
|
||||||
|
import { VideoFilesModule } from '../VideoFiles/video-files.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [TorrentDownloadClientModule, VideoFilesModule],
|
||||||
providers: [
|
providers: [
|
||||||
MimeService,
|
MimeService,
|
||||||
RequestRangeService,
|
RequestRangeService,
|
||||||
@@ -22,10 +25,11 @@ import { FsReadStreamCreatableProvider } from './ReadStreamProvider/FileSystem/f
|
|||||||
StreamerFacade,
|
StreamerFacade,
|
||||||
ReadStreamCreatableProvider,
|
ReadStreamCreatableProvider,
|
||||||
FsReadStreamCreatableProvider,
|
FsReadStreamCreatableProvider,
|
||||||
|
WtReadStreamCreatableProvider,
|
||||||
{
|
{
|
||||||
provide: ReadStreamCreatableProviders,
|
provide: ReadStreamCreatableProviders,
|
||||||
useFactory: (...providers) => providers,
|
useFactory: (...providers) => providers,
|
||||||
inject: [FsReadStreamCreatableProvider],
|
inject: [FsReadStreamCreatableProvider, WtReadStreamCreatableProvider],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
exports: [StreamerFacade],
|
exports: [StreamerFacade],
|
||||||
|
|||||||
@@ -0,0 +1,299 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import * as WebTorrent from 'webtorrent';
|
||||||
|
import { Instance, Torrent, TorrentFile, TorrentOptions } from 'webtorrent';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { TorrentEntity } from '../../../Shared/Entity/torrent.entity';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { configService } from '../../../config/config.service';
|
||||||
|
import { extname, join } from 'path';
|
||||||
|
import { FileEntity } from '../../../Shared/Entity/file.entity';
|
||||||
|
import { StreamProviderEnum } from '../../../Shared/Enum/stream-provider.enum';
|
||||||
|
import { VideoFilesFacade } from '../../../VideoFiles/video-files.facade';
|
||||||
|
import { TorrentSeedClient } from '../../SeedClient/torrent-seed.client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WebtorrentClient {
|
||||||
|
private client: Instance;
|
||||||
|
private readonly torrentOptions: TorrentOptions;
|
||||||
|
private readonly configService = configService.getVideoFilesConfig();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(TorrentEntity)
|
||||||
|
private readonly torrentRepository: Repository<TorrentEntity>,
|
||||||
|
private readonly videoFilesFacade: VideoFilesFacade,
|
||||||
|
@InjectRepository(FileEntity)
|
||||||
|
private readonly filesRepository: Repository<FileEntity>,
|
||||||
|
private readonly seedClient: TorrentSeedClient,
|
||||||
|
) {
|
||||||
|
this.client = new WebTorrent();
|
||||||
|
|
||||||
|
this.torrentOptions = {};
|
||||||
|
this.torrentOptions.path = this.configService.getTorrentDownloadDir();
|
||||||
|
this.torrentOptions.getAnnounceOpts = function() {
|
||||||
|
const that = this as Torrent;
|
||||||
|
|
||||||
|
return {
|
||||||
|
uploaded: 0, //this.uploaded,
|
||||||
|
downloaded: 0, //this.downloaded,
|
||||||
|
left: that.length, //Math.max(this.length - this.downloaded, 0)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
this.resumeNotStopped();
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeNotStopped(): void {
|
||||||
|
this.torrentRepository
|
||||||
|
.find({
|
||||||
|
where: { stopped: false },
|
||||||
|
})
|
||||||
|
.then((torrentEntities) => {
|
||||||
|
for (const torrentEntity of torrentEntities) {
|
||||||
|
this.addTorrent(torrentEntity.magnet).then();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async addTorrent(magnet: string): Promise<FileEntity[]> {
|
||||||
|
let torrentEntity = await this.torrentRepository
|
||||||
|
.findOne({
|
||||||
|
where: { magnet: magnet },
|
||||||
|
relations: { files: { torrent: true } },
|
||||||
|
})
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
|
if (torrentEntity && torrentEntity.infoHash) {
|
||||||
|
//if stopped and complete - return immediately
|
||||||
|
if (torrentEntity.stopped && torrentEntity.progress === 1) {
|
||||||
|
return torrentEntity.files;
|
||||||
|
}
|
||||||
|
//if already in wt - return files, if not continue as always
|
||||||
|
const torrent = this.client.get(torrentEntity.infoHash);
|
||||||
|
if (torrent) {
|
||||||
|
return torrentEntity.files;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (torrentEntity === null) {
|
||||||
|
torrentEntity = new TorrentEntity();
|
||||||
|
torrentEntity.magnet = magnet;
|
||||||
|
|
||||||
|
await this.torrentRepository.save(torrentEntity);
|
||||||
|
torrentEntity = await this.torrentRepository
|
||||||
|
.findOneByOrFail({ magnet: magnet })
|
||||||
|
.catch(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const torrent = await this.addTorrentReturnOnReady(torrentEntity);
|
||||||
|
|
||||||
|
this.mapTorrentToEntity(torrentEntity, torrent);
|
||||||
|
|
||||||
|
await this.torrentRepository.save(torrentEntity);
|
||||||
|
|
||||||
|
if (torrent.done && torrentEntity.files) {
|
||||||
|
//in case we added already downloaded one
|
||||||
|
this.registerDone(torrent);
|
||||||
|
return torrentEntity.files;
|
||||||
|
}
|
||||||
|
|
||||||
|
const partialFileEntities = this.buildPartialVideoFileEntities(
|
||||||
|
torrentEntity,
|
||||||
|
torrent,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileEntities = await this.videoFilesFacade.addVideoFiles(
|
||||||
|
partialFileEntities,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.registerProgressSave(torrent);
|
||||||
|
this.registerDone(torrent);
|
||||||
|
|
||||||
|
return fileEntities;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async addTorrentReturnOnReady(
|
||||||
|
torrentEntity: TorrentEntity,
|
||||||
|
): Promise<Torrent> {
|
||||||
|
let torrent = null;
|
||||||
|
if (torrentEntity.infoHash) {
|
||||||
|
torrent = this.client.get(torrentEntity.infoHash) as Torrent | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!torrent) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const torrent = this.client.add(
|
||||||
|
torrentEntity.magnet,
|
||||||
|
this.torrentOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
torrent.on('ready', () => {
|
||||||
|
resolve(torrent);
|
||||||
|
});
|
||||||
|
|
||||||
|
torrent.on('error', () => {
|
||||||
|
reject('torrent error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(torrent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerProgressSave(torrent: Torrent) {
|
||||||
|
const notOftenThan = 1000 * 5;
|
||||||
|
let lastUpdateTime = 0;
|
||||||
|
|
||||||
|
torrent.on('download', async () => {
|
||||||
|
if (lastUpdateTime < Date.now() - notOftenThan) {
|
||||||
|
await this.torrentRepository.update(
|
||||||
|
{ infoHash: torrent.infoHash },
|
||||||
|
{ progress: torrent.progress },
|
||||||
|
);
|
||||||
|
for (const file of torrent.files) {
|
||||||
|
await this.filesRepository.update(
|
||||||
|
{ relativePath: file.path },
|
||||||
|
{ progress: file.progress },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lastUpdateTime = Date.now();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerDone(torrent: Torrent) {
|
||||||
|
if (torrent.done) {
|
||||||
|
this.actWhenDone(torrent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
torrent.on('done', () => {
|
||||||
|
this.actWhenDone(torrent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private actWhenDone(torrent: Torrent) {
|
||||||
|
this.torrentRepository
|
||||||
|
.update({ infoHash: torrent.infoHash }, { progress: 1, stopped: true })
|
||||||
|
.then(async () => {
|
||||||
|
const torrentEntity = await this.torrentRepository.findOne({
|
||||||
|
where: { infoHash: torrent.infoHash },
|
||||||
|
relations: { files: { torrent: true } },
|
||||||
|
});
|
||||||
|
|
||||||
|
//update files to progress 1
|
||||||
|
await this.filesRepository.update(
|
||||||
|
{ torrentId: torrentEntity.id },
|
||||||
|
{ progress: 1 },
|
||||||
|
);
|
||||||
|
|
||||||
|
return torrentEntity;
|
||||||
|
})
|
||||||
|
.then(async (torrentEntity) => {
|
||||||
|
//move file to seeding
|
||||||
|
if (torrentEntity.magnet.toLowerCase().includes('linkomanija')) {
|
||||||
|
const torrentInfo = await this.seedClient
|
||||||
|
.addTorrent(torrentEntity)
|
||||||
|
.catch(() => null);
|
||||||
|
if (torrentInfo) {
|
||||||
|
torrentEntity.transmissionId = parseInt(torrentInfo.id);
|
||||||
|
await this.torrentRepository.save(torrentEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return torrentEntity;
|
||||||
|
})
|
||||||
|
.then(async (torrentEntity) => {
|
||||||
|
//update files info with expanders and move to fs provider
|
||||||
|
const filePaths: string[] = [];
|
||||||
|
for (const file of torrentEntity.files) {
|
||||||
|
filePaths.push(file.path);
|
||||||
|
}
|
||||||
|
this.videoFilesFacade.updateEntitiesByPath(filePaths).then();
|
||||||
|
torrent.destroy();
|
||||||
|
|
||||||
|
return torrentEntity;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapTorrentToEntity(torrentEntity: TorrentEntity, torrent: Torrent) {
|
||||||
|
torrentEntity.path = torrent.path;
|
||||||
|
torrentEntity.infoHash = torrent.infoHash;
|
||||||
|
torrentEntity.name = torrent.name;
|
||||||
|
torrentEntity.stopped = torrent.progress === 1;
|
||||||
|
torrentEntity.progress = torrent.progress;
|
||||||
|
torrentEntity.size = torrent.length;
|
||||||
|
torrentEntity.linkomanija = torrentEntity.magnet
|
||||||
|
.toLowerCase()
|
||||||
|
.includes('linkomanija');
|
||||||
|
|
||||||
|
//map all files (include not video files, will be needed for deletion)
|
||||||
|
torrentEntity.info = JSON.stringify({
|
||||||
|
files: torrent.files.map((file) => {
|
||||||
|
return { path: join(torrent.path, file.path) };
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildPartialVideoFileEntities(
|
||||||
|
torrentEntity: TorrentEntity,
|
||||||
|
torrent: Torrent,
|
||||||
|
): Partial<FileEntity>[] {
|
||||||
|
return torrent.files.reduce(
|
||||||
|
(filtered: Partial<FileEntity>[], file: TorrentFile) => {
|
||||||
|
if (
|
||||||
|
this.configService
|
||||||
|
.getVideoFilesExt()
|
||||||
|
.indexOf(extname(file.name).toLowerCase()) > -1
|
||||||
|
) {
|
||||||
|
const partialFileEntity: Partial<FileEntity> = {
|
||||||
|
path: join(torrent.path, file.path),
|
||||||
|
relativePath: file.path,
|
||||||
|
torrent: torrentEntity,
|
||||||
|
size: file.length,
|
||||||
|
streamProvider: StreamProviderEnum.wt,
|
||||||
|
fileName: file.name,
|
||||||
|
linkomanija: torrentEntity.magnet
|
||||||
|
.toLowerCase()
|
||||||
|
.includes('linkomanija'),
|
||||||
|
};
|
||||||
|
filtered.push(partialFileEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTorrentByInfoHash(torrentInfoHash: string): Torrent | void {
|
||||||
|
return this.client.get(torrentInfoHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopTorrent(torrentId: number): Promise<void> {
|
||||||
|
const torrentEntity = await this.torrentRepository.findOne({
|
||||||
|
where: { id: torrentId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!torrentEntity || !torrentEntity.infoHash) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const torrent = await this.getTorrentByInfoHash(torrentEntity.infoHash);
|
||||||
|
|
||||||
|
if (!torrent || torrent.done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
torrent.destroy({ destroyStore: false }, (e) => {
|
||||||
|
if (e) {
|
||||||
|
return reject(e);
|
||||||
|
}
|
||||||
|
torrentEntity.stopped = true;
|
||||||
|
this.torrentRepository.save(torrentEntity);
|
||||||
|
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TorrentDownloadClient } from './torrent-download.client';
|
||||||
|
import { VideoFilesModule } from '../../VideoFiles/video-files.module';
|
||||||
|
import { WebtorrentClient } from './WebtorrentClient/webtorrent.client';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { TorrentEntity } from '../../Shared/Entity/torrent.entity';
|
||||||
|
import { FileEntity } from '../../Shared/Entity/file.entity';
|
||||||
|
import { TorrentSeedClientModule } from '../SeedClient/torrent-seed-client.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
VideoFilesModule,
|
||||||
|
TypeOrmModule.forFeature([TorrentEntity, FileEntity]),
|
||||||
|
TorrentSeedClientModule,
|
||||||
|
],
|
||||||
|
providers: [WebtorrentClient, TorrentDownloadClient],
|
||||||
|
exports: [TorrentDownloadClient],
|
||||||
|
})
|
||||||
|
export class TorrentDownloadClientModule {}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { WebtorrentClient } from './WebtorrentClient/webtorrent.client';
|
||||||
|
import { FileEntity } from '../../Shared/Entity/file.entity';
|
||||||
|
import { TorrentFile } from 'webtorrent';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { TorrentEntity } from '../../Shared/Entity/torrent.entity';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TorrentDownloadClient {
|
||||||
|
constructor(
|
||||||
|
private readonly webtorrentClient: WebtorrentClient,
|
||||||
|
@InjectRepository(TorrentEntity)
|
||||||
|
private readonly torrentRepository: Repository<TorrentEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
addTorrent(magnet: string): Promise<FileEntity[]> {
|
||||||
|
return this.webtorrentClient.addTorrent(magnet);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTorrentFile(fileEntity: FileEntity): Promise<TorrentFile | null> {
|
||||||
|
const torrentInfoHash = fileEntity.torrent.infoHash;
|
||||||
|
let torrent = this.webtorrentClient.getTorrentByInfoHash(torrentInfoHash);
|
||||||
|
if (!torrent) {
|
||||||
|
await this.webtorrentClient.addTorrent(fileEntity.torrent.magnet);
|
||||||
|
}
|
||||||
|
|
||||||
|
torrent = this.webtorrentClient.getTorrentByInfoHash(torrentInfoHash);
|
||||||
|
if (!torrent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of torrent.files) {
|
||||||
|
if (file.path === fileEntity.relativePath) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTorrents(): Promise<TorrentEntity[]> {
|
||||||
|
return this.torrentRepository.find({
|
||||||
|
relations: { files: { torrent: true } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getTorrentById(id: number): Promise<TorrentEntity> {
|
||||||
|
return this.torrentRepository.findOne({
|
||||||
|
where: { id: id },
|
||||||
|
relations: { files: { torrent: true } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stopTorrent(torrentId: number): Promise<void> {
|
||||||
|
return this.webtorrentClient.stopTorrent(torrentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async resumeTorrent(torrentId: number): Promise<FileEntity[]> {
|
||||||
|
const torrentEntity = await this.torrentRepository.findOne({
|
||||||
|
where: { id: torrentId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!torrentEntity || !torrentEntity.magnet) {
|
||||||
|
return Promise.reject('torrent not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.addTorrent(torrentEntity.magnet);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { NavigationItem } from '../../Shared/Dto/navigation-item.dto';
|
||||||
|
import { TorrentSearchResult } from './torrent-search-result.class';
|
||||||
|
|
||||||
|
export class TorrentSearchClientResponse {
|
||||||
|
title = '';
|
||||||
|
navigationItems: NavigationItem[] = [];
|
||||||
|
searchResults: TorrentSearchResult[] = [];
|
||||||
|
//todo pagination
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export class TorrentSearchResult {
|
||||||
|
id: string;
|
||||||
|
magnet: string;
|
||||||
|
name: string;
|
||||||
|
size: string;
|
||||||
|
seeders: string;
|
||||||
|
leechers: string;
|
||||||
|
date: string;
|
||||||
|
info: string; //anything that does not fit to above
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { SearchClientInterface } from '../Interface/search-client.interface';
|
||||||
|
import { LinkomanijaClient } from '../LinkomanijaClient/linkomanija.client';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { TorrentSearchClientResponse } from '../../Dto/torrent-search-client-response.dto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bridge is needed as it seems you cannot inject providers from other modules using factory in module level
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class LinkomanijaClientBridge implements SearchClientInterface {
|
||||||
|
clientName: string;
|
||||||
|
|
||||||
|
constructor(private readonly client: LinkomanijaClient) {
|
||||||
|
this.clientName = client.clientName;
|
||||||
|
}
|
||||||
|
|
||||||
|
search(query: string) {
|
||||||
|
return this.client.search(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
customMenu(path: string): Promise<TorrentSearchClientResponse> {
|
||||||
|
return this.client.customMenu(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TorrentSearchResult } from '../../Dto/torrent-search-result.class';
|
||||||
|
import { TorrentSearchClientResponse } from '../../Dto/torrent-search-client-response.dto';
|
||||||
|
|
||||||
|
export interface SearchClientInterface {
|
||||||
|
clientName: string;
|
||||||
|
|
||||||
|
search(query: string): Promise<TorrentSearchResult[]>;
|
||||||
|
|
||||||
|
customMenu(path: string): Promise<TorrentSearchClientResponse>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { Agent, RequestOptions, request, get } from 'https';
|
||||||
|
import {
|
||||||
|
configService,
|
||||||
|
ConfigService,
|
||||||
|
} from '../../../../config/config.service';
|
||||||
|
import { IncomingMessage } from 'http';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LinkomanijaHttpClient {
|
||||||
|
private readonly configService: ConfigService;
|
||||||
|
private readonly requestOptions: RequestOptions;
|
||||||
|
private consecutiveLoginFails: number;
|
||||||
|
private lastLoginFailTimeStamp = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.requestOptions = {
|
||||||
|
headers: {},
|
||||||
|
agent: new Agent({ keepAlive: true }),
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
hostname: 'www.linkomanija.net',
|
||||||
|
port: 443,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.configService = configService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async search(query: string): Promise<string> {
|
||||||
|
const c = ['c29', 'c52', 'c30', 'c60', 'c53', 'c61', 'c28', 'c62'];
|
||||||
|
const path = `/browse.php?${c.join('=1&')}=1&incldead=0&search=${query}`;
|
||||||
|
|
||||||
|
return this.getPage(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getTop(category: string, time: string): Promise<string> {
|
||||||
|
const path = `/browse.php?top=${category}&time=${time}`;
|
||||||
|
|
||||||
|
return this.getPage(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPage(path: string): Promise<string> {
|
||||||
|
const options = {
|
||||||
|
...this.requestOptions,
|
||||||
|
path: encodeURI(path),
|
||||||
|
method: 'POST',
|
||||||
|
};
|
||||||
|
|
||||||
|
const response: IncomingMessage | false = await this.httpGet(options).catch(
|
||||||
|
() => false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response === false) {
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.parseAndSetCookie(response);
|
||||||
|
|
||||||
|
if (
|
||||||
|
response.statusCode === 302 &&
|
||||||
|
response.headers.location.includes('login.php')
|
||||||
|
) {
|
||||||
|
//need to log in
|
||||||
|
const loggedIn = await this.login().catch(() => false);
|
||||||
|
if (loggedIn || this.checkLoginSafeToAttempt()) {
|
||||||
|
return this.getPage(path);
|
||||||
|
}
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let rawData = '';
|
||||||
|
response.on('data', (chunk) => (rawData += chunk));
|
||||||
|
response.on('end', () => {
|
||||||
|
resolve(rawData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private httpGet(options: RequestOptions): Promise<IncomingMessage> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = get(options, (res) => {
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
req.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private login(): Promise<boolean> {
|
||||||
|
if (!this.checkLoginSafeToAttempt()) {
|
||||||
|
console.log('LM too many login attempts reached');
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
...this.requestOptions,
|
||||||
|
path: '/takelogin.php',
|
||||||
|
method: 'POST',
|
||||||
|
};
|
||||||
|
|
||||||
|
const userName = process.env['LINKOMANIJA_USERNAME'];
|
||||||
|
const password = process.env['LINKOMANIJA_PASSWORD'];
|
||||||
|
const postData = `username=${userName}&password=${password}&commit=Prisijungti`;
|
||||||
|
|
||||||
|
options.headers = JSON.parse(JSON.stringify(options.headers));
|
||||||
|
options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||||
|
options.headers['Content-Length'] = Buffer.byteLength(postData);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = request(options, (res) => {
|
||||||
|
this.parseAndSetCookie(res);
|
||||||
|
|
||||||
|
if (
|
||||||
|
res.statusCode === 302 &&
|
||||||
|
!res.headers.location.includes('login.php')
|
||||||
|
) {
|
||||||
|
this.consecutiveLoginFails = 0;
|
||||||
|
resolve(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.increaseLoginFailures();
|
||||||
|
reject();
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (e) => {
|
||||||
|
this.increaseLoginFailures();
|
||||||
|
reject();
|
||||||
|
});
|
||||||
|
|
||||||
|
req.write(postData);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseAndSetCookie(response: IncomingMessage): void {
|
||||||
|
const cookieToSet = response.headers['set-cookie'];
|
||||||
|
if (cookieToSet) {
|
||||||
|
this.requestOptions.headers['cookie'] = cookieToSet[0].split(';')[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private increaseLoginFailures(): void {
|
||||||
|
this.consecutiveLoginFails++;
|
||||||
|
this.lastLoginFailTimeStamp = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkLoginSafeToAttempt(): boolean {
|
||||||
|
return (
|
||||||
|
this.consecutiveLoginFails <= 5 ||
|
||||||
|
this.lastLoginFailTimeStamp < Date.now() - 1000 * 60 * 5
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
import * as url from 'url';
|
||||||
|
import { TorrentSearchResult } from '../../../Dto/torrent-search-result.class';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LinkomanijaResponseParser {
|
||||||
|
parseRaw(rawData: string): TorrentSearchResult[] {
|
||||||
|
const $ = cheerio.load(rawData);
|
||||||
|
const torrents = [];
|
||||||
|
|
||||||
|
$('body').find('br').replaceWith('\n');
|
||||||
|
|
||||||
|
$('table').each(function (iTable, elTable) {
|
||||||
|
$(elTable)
|
||||||
|
.find('a.index')
|
||||||
|
.each(function (iA, elA) {
|
||||||
|
const torrent = new TorrentSearchResult();
|
||||||
|
const arrTorDetails = $(elA.parent.parent).find('td');
|
||||||
|
|
||||||
|
let sNameAndDesc = $(arrTorDetails[1]).text();
|
||||||
|
sNameAndDesc = sNameAndDesc.split('\t').join('');
|
||||||
|
const arrNameAndDesc = sNameAndDesc
|
||||||
|
.split('\n')
|
||||||
|
.filter(function (val) {
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
|
||||||
|
torrent.name = arrNameAndDesc[0] ? arrNameAndDesc[0].trim() : '';
|
||||||
|
torrent.info = arrNameAndDesc[1] ? arrNameAndDesc[1].trim() : '';
|
||||||
|
torrent.date = $(arrTorDetails[4]).text().split('\n')[0];
|
||||||
|
torrent.size = $(arrTorDetails[5]).text().split('\n').join(' ');
|
||||||
|
torrent.seeders = $(arrTorDetails[7]).text().split('\n').join(' ');
|
||||||
|
torrent.leechers = $(arrTorDetails[8]).text().split('\n').join(' ');
|
||||||
|
|
||||||
|
if ('attribs' in elA) {
|
||||||
|
const { id, name } = url.parse(elA.attribs['href'], true).query;
|
||||||
|
torrent.id = id as string;
|
||||||
|
|
||||||
|
const relDlUrl = encodeURI(
|
||||||
|
'/download.php?id=' +
|
||||||
|
id +
|
||||||
|
'&passkey=' +
|
||||||
|
process.env['LINKOMANIJA_PASSKEY'] +
|
||||||
|
'&name=' +
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
|
||||||
|
torrent.magnet = 'https://www.linkomanija.net' + relDlUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrImgs = $(arrTorDetails).find('img');
|
||||||
|
|
||||||
|
if (
|
||||||
|
arrImgs &&
|
||||||
|
arrImgs[0] &&
|
||||||
|
'attribs' in arrImgs[0] &&
|
||||||
|
arrImgs[0].attribs &&
|
||||||
|
arrImgs[0].attribs.alt
|
||||||
|
) {
|
||||||
|
torrent.info =
|
||||||
|
arrImgs[0].attribs.alt +
|
||||||
|
(torrent.info ? ': ' + torrent.info : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
torrents.push(torrent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return torrents;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { LinkomanijaHttpClient } from './HttpClient/linkomanija-http.client';
|
||||||
|
import { LinkomanijaResponseParser } from './ResponseParser/linkomanija-response.parser';
|
||||||
|
import { TorrentSearchResult } from '../../Dto/torrent-search-result.class';
|
||||||
|
import { SearchClientInterface } from '../Interface/search-client.interface';
|
||||||
|
import { NavigationItem } from '../../../Shared/Dto/navigation-item.dto';
|
||||||
|
import { TorrentSearchClientResponse } from '../../Dto/torrent-search-client-response.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LinkomanijaClient implements SearchClientInterface {
|
||||||
|
clientName = 'LM';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly lmHttpClient: LinkomanijaHttpClient,
|
||||||
|
private readonly lmResponseParser: LinkomanijaResponseParser,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async search(query: string): Promise<TorrentSearchResult[]> {
|
||||||
|
const rawData = await this.lmHttpClient.search(query).catch((e) => e);
|
||||||
|
|
||||||
|
return this.lmResponseParser.parseRaw(rawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTop(category: string, time: string): Promise<TorrentSearchResult[]> {
|
||||||
|
const rawData = await this.lmHttpClient
|
||||||
|
.getTop(category, time)
|
||||||
|
.catch((e) => e);
|
||||||
|
|
||||||
|
return this.lmResponseParser.parseRaw(rawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async customMenu(path: string): Promise<TorrentSearchClientResponse> {
|
||||||
|
const [service, category, time] = path.split('/');
|
||||||
|
const customMenuResponse = new TorrentSearchClientResponse();
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
customMenuResponse.navigationItems.push(['LM Top', 'top']);
|
||||||
|
return Promise.resolve(customMenuResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
const categories: NavigationItem[] = [
|
||||||
|
['Movies LT', 'top/53'],
|
||||||
|
['Movies LT HD', 'top/61'],
|
||||||
|
['Movies', 'top/29'],
|
||||||
|
['Movies HD', 'top/52'],
|
||||||
|
['TV LT', 'top/28'],
|
||||||
|
['TV LT HD', 'top/62'],
|
||||||
|
['TV', 'top/30'],
|
||||||
|
['TV HD', 'top/60'],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!category) {
|
||||||
|
customMenuResponse.navigationItems = categories;
|
||||||
|
customMenuResponse.title = 'LM Top';
|
||||||
|
return Promise.resolve(customMenuResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
const times: NavigationItem[] = [
|
||||||
|
['Savaitės', 'top/' + category + '/7'],
|
||||||
|
['2 Savaičių', 'top/' + category + '/14'],
|
||||||
|
['Mėnesio', 'top/' + category + '/30'],
|
||||||
|
['3 Mėnesių', 'top/' + category + '/90'],
|
||||||
|
['Metų', 'top/' + category + '/365'],
|
||||||
|
['Visų laikų', 'top/' + category + '/0'],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!time) {
|
||||||
|
customMenuResponse.navigationItems = times;
|
||||||
|
customMenuResponse.title =
|
||||||
|
'LM Top, ' + categories.find((value) => value[1] == path)[0];
|
||||||
|
return Promise.resolve(customMenuResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
customMenuResponse.title =
|
||||||
|
'LM Top, ' +
|
||||||
|
categories.find((value) => value[1] == 'top/' + category)[0] +
|
||||||
|
', ' +
|
||||||
|
times.find((value) => value[1] == path)[0];
|
||||||
|
customMenuResponse.searchResults = await this.getTop(category, time);
|
||||||
|
|
||||||
|
return customMenuResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { LinkomanijaClient } from './linkomanija.client';
|
||||||
|
import { LinkomanijaHttpClient } from './HttpClient/linkomanija-http.client';
|
||||||
|
import { LinkomanijaResponseParser } from './ResponseParser/linkomanija-response.parser';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [],
|
||||||
|
providers: [
|
||||||
|
LinkomanijaClient,
|
||||||
|
LinkomanijaHttpClient,
|
||||||
|
LinkomanijaResponseParser,
|
||||||
|
],
|
||||||
|
exports: [LinkomanijaClient],
|
||||||
|
})
|
||||||
|
export class LinkomanijaModule {}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { SearchClientInterface } from './Interface/search-client.interface';
|
||||||
|
import { TorrentSearchClientResponse } from '../Dto/torrent-search-client-response.dto';
|
||||||
|
|
||||||
|
export const SearchClients = 'SearchClients';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SearchClientFacade {
|
||||||
|
constructor(
|
||||||
|
@Inject(SearchClients)
|
||||||
|
private readonly searchClients: SearchClientInterface[],
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getSearchClientNames(): string[] {
|
||||||
|
const clients = [];
|
||||||
|
for (const searchClient of this.searchClients) {
|
||||||
|
clients.push(searchClient.clientName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
//todo - page
|
||||||
|
async search(
|
||||||
|
query: string,
|
||||||
|
clientName: string,
|
||||||
|
): Promise<TorrentSearchClientResponse> {
|
||||||
|
for (const searchClient of this.searchClients) {
|
||||||
|
if (searchClient.clientName.toLowerCase() === clientName.toLowerCase()) {
|
||||||
|
const response = new TorrentSearchClientResponse();
|
||||||
|
response.title = searchClient.clientName + ' search for: ' + query;
|
||||||
|
response.searchResults = await searchClient.search(query);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject('client not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
getCustomMenu(
|
||||||
|
clientName: string,
|
||||||
|
path = '',
|
||||||
|
): Promise<TorrentSearchClientResponse> {
|
||||||
|
for (const searchClient of this.searchClients) {
|
||||||
|
if (searchClient.clientName.toLowerCase() === clientName.toLowerCase()) {
|
||||||
|
return searchClient.customMenu(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject('client not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { LinkomanijaModule } from './LinkomanijaClient/linkomanija.module';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SearchClientFacade, SearchClients } from './search-client.facade';
|
||||||
|
import { LinkomanijaClientBridge } from './ClientBridge/linkomanija-client.bridge';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [LinkomanijaModule],
|
||||||
|
providers: [
|
||||||
|
SearchClientFacade,
|
||||||
|
LinkomanijaClientBridge,
|
||||||
|
{
|
||||||
|
provide: SearchClients,
|
||||||
|
useFactory: (...clients) => clients,
|
||||||
|
inject: [LinkomanijaClientBridge],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exports: [SearchClientFacade],
|
||||||
|
})
|
||||||
|
export class SearchClientModule {}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export class TorrentInfo {
|
||||||
|
id: string;
|
||||||
|
linkomanija: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import * as Transmission from 'transmission';
|
||||||
|
import { VideoFilesConfig } from '../../../config/video-files.config';
|
||||||
|
import { configService } from '../../../config/config.service';
|
||||||
|
import { TorrentEntity } from '../../../Shared/Entity/torrent.entity';
|
||||||
|
import { TorrentInfo } from '../Dto/torrent-info.dto';
|
||||||
|
import { FileEntity } from '../../../Shared/Entity/file.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TransmissionClient {
|
||||||
|
private transmission: Transmission;
|
||||||
|
private config: VideoFilesConfig = configService.getVideoFilesConfig();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.transmission = new Transmission(this.config.getTransmissionOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
addTorrent(torrentEntity: TorrentEntity): Promise<TorrentInfo> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.transmission.addUrl(
|
||||||
|
torrentEntity.magnet,
|
||||||
|
{ 'download-dir': torrentEntity.path },
|
||||||
|
(err, arg) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const torrentInfo = new TorrentInfo();
|
||||||
|
torrentInfo.id = arg.id;
|
||||||
|
torrentInfo.linkomanija = torrentEntity.magnet
|
||||||
|
.toLowerCase()
|
||||||
|
.includes('linkomanija');
|
||||||
|
|
||||||
|
return resolve(torrentInfo);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findTorrentInfoByFile(fileEntity: FileEntity): Promise<TorrentInfo> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.transmission.get((err, result) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const torr of result.torrents) {
|
||||||
|
for (const file of torr.files) {
|
||||||
|
if (fileEntity.path === torr.downloadDir + '/' + file.name) {
|
||||||
|
const torrentInfo = new TorrentInfo();
|
||||||
|
torrentInfo.id = torr.id;
|
||||||
|
torrentInfo.linkomanija = torr.magnetLink
|
||||||
|
.toLowerCase()
|
||||||
|
.includes('linkomanija');
|
||||||
|
|
||||||
|
return resolve(torrentInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reject('file not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTorrent(torrentEntity: TorrentEntity, removeData = false) {
|
||||||
|
if (torrentEntity.transmissionId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.transmission.remove(
|
||||||
|
[torrentEntity.transmissionId],
|
||||||
|
removeData,
|
||||||
|
(e, r) => {
|
||||||
|
if (e) {
|
||||||
|
return reject(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(r);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TorrentSeedClient } from './torrent-seed.client';
|
||||||
|
import { TransmissionClient } from './Transmission/transmission.client';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { TorrentEntity } from '../../Shared/Entity/torrent.entity';
|
||||||
|
import { FileEntity } from '../../Shared/Entity/file.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([TorrentEntity, FileEntity])],
|
||||||
|
providers: [TorrentSeedClient, TransmissionClient],
|
||||||
|
exports: [TorrentSeedClient],
|
||||||
|
})
|
||||||
|
export class TorrentSeedClientModule {}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { TorrentEntity } from '../../Shared/Entity/torrent.entity';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { TransmissionClient } from './Transmission/transmission.client';
|
||||||
|
import { TorrentInfo } from './Dto/torrent-info.dto';
|
||||||
|
import { FileEntity } from '../../Shared/Entity/file.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TorrentSeedClient {
|
||||||
|
constructor(
|
||||||
|
private readonly transmissionClient: TransmissionClient,
|
||||||
|
@InjectRepository(TorrentEntity)
|
||||||
|
private readonly torrentRepository: Repository<TorrentEntity>,
|
||||||
|
@InjectRepository(FileEntity)
|
||||||
|
private readonly filesRepository: Repository<FileEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
addTorrent(torrentEntity: TorrentEntity): Promise<TorrentInfo> {
|
||||||
|
return this.transmissionClient.addTorrent(torrentEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addTorrentById(torrId: number): Promise<TorrentInfo> {
|
||||||
|
const torrentEntity = await this.torrentRepository.findOne({
|
||||||
|
where: { id: torrId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!torrentEntity) {
|
||||||
|
return Promise.reject('torrent not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const torrInfo = await this.addTorrent(torrentEntity).catch(() => null);
|
||||||
|
|
||||||
|
if (torrInfo) {
|
||||||
|
await this.torrentRepository.update(
|
||||||
|
{ id: torrentEntity.id },
|
||||||
|
{ transmissionId: torrInfo.id, linkomanija: torrInfo.linkomanija },
|
||||||
|
);
|
||||||
|
await this.filesRepository.update(
|
||||||
|
{ torrentId: torrentEntity.id },
|
||||||
|
{ transmissionId: torrInfo.id, linkomanija: torrInfo.linkomanija },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return torrInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
findTorrentInfoByFile(fileEntity: FileEntity): Promise<TorrentInfo | null> {
|
||||||
|
return this.transmissionClient.findTorrentInfoByFile(fileEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeTorrent(torrId: number, removeData = false) {
|
||||||
|
const torrentEntity = await this.torrentRepository.findOne({
|
||||||
|
where: { id: torrId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!torrentEntity) {
|
||||||
|
return Promise.reject('torrent not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.transmissionClient
|
||||||
|
.removeTorrent(torrentEntity, removeData)
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
|
if (result && !removeData) {
|
||||||
|
await this.torrentRepository.update(
|
||||||
|
{ id: torrentEntity.id },
|
||||||
|
{ transmissionId: null },
|
||||||
|
);
|
||||||
|
await this.filesRepository.update(
|
||||||
|
{ torrentId: torrentEntity.id },
|
||||||
|
{ transmissionId: null },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result && removeData) {
|
||||||
|
await this.torrentRepository.delete({
|
||||||
|
id: torrentEntity.id,
|
||||||
|
});
|
||||||
|
await this.filesRepository.update(
|
||||||
|
{ torrentId: torrentEntity.id },
|
||||||
|
{ transmissionId: null, torrentId: null, deleted: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get } from '@nestjs/common';
|
||||||
import { VideoFilesFacade } from '../VideoFiles/video-files.facade';
|
|
||||||
|
|
||||||
@Controller('try')
|
@Controller('try')
|
||||||
export class TryController {
|
export class TryController {
|
||||||
constructor(readonly videoFilesFacade: VideoFilesFacade) {}
|
|
||||||
|
|
||||||
@Get('')
|
@Get('')
|
||||||
main() {
|
main() {
|
||||||
this.videoFilesFacade.updateFsVideoFiles();
|
return { hello: 'world' };
|
||||||
return { doing: 'something' };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TryController } from './try.cotroller';
|
import { TryController } from './try.cotroller';
|
||||||
import { VideoFilesModule } from '../VideoFiles/video-files.module';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Playground module for playing around
|
* Playground module for playing around
|
||||||
*/
|
*/
|
||||||
@Module({
|
@Module({
|
||||||
imports: [VideoFilesModule],
|
imports: [],
|
||||||
controllers: [TryController],
|
controllers: [TryController],
|
||||||
providers: [],
|
providers: [],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FileEntityExpanderInterface } from './file-entity-expander.interface';
|
import { FileEntityExpanderInterface } from './file-entity-expander.interface';
|
||||||
import { FileEntity } from '../../Entity/file.entity';
|
import { FileEntity } from '../../../Shared/Entity/file.entity';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { FileEntityExpanderInterface } from './file-entity-expander.interface';
|
import { FileEntityExpanderInterface } from './file-entity-expander.interface';
|
||||||
import { FileEntity } from '../../Entity/file.entity';
|
import { FileEntity } from '../../../Shared/Entity/file.entity';
|
||||||
import * as ffmpeg from 'fluent-ffmpeg';
|
import * as ffmpeg from 'fluent-ffmpeg';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { StreamProviderEnum } from '../../../Shared/Enum/stream-provider.enum';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FileEntityDurationExpander implements FileEntityExpanderInterface {
|
export class FileEntityDurationExpander implements FileEntityExpanderInterface {
|
||||||
expand(fileEntity: FileEntity): Promise<FileEntity> {
|
expand(fileEntity: FileEntity): Promise<FileEntity> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
if (fileEntity.streamProvider !== StreamProviderEnum.fs) {
|
||||||
|
return resolve(fileEntity);
|
||||||
|
}
|
||||||
ffmpeg.ffprobe(fileEntity.path, (err, data) => {
|
ffmpeg.ffprobe(fileEntity.path, (err, data) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
//no error handling!
|
//no error handling!
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FileEntity } from '../../Entity/file.entity';
|
import { FileEntity } from '../../../Shared/Entity/file.entity';
|
||||||
|
|
||||||
export interface FileEntityExpanderInterface {
|
export interface FileEntityExpanderInterface {
|
||||||
expand(fileEntity: FileEntity): Promise<FileEntity>;
|
expand(fileEntity: FileEntity): Promise<FileEntity>;
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { FileEntityExpanderInterface } from './file-entity-expander.interface';
|
import { FileEntityExpanderInterface } from './file-entity-expander.interface';
|
||||||
import { FileEntity } from '../../Entity/file.entity';
|
import { FileEntity } from '../../../Shared/Entity/file.entity';
|
||||||
import { Stats } from 'fs';
|
import { Stats } from 'fs';
|
||||||
import { stat } from 'fs/promises';
|
import { stat } from 'fs/promises';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { StreamProviderEnum } from '../../../Shared/Enum/stream-provider.enum';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FileEntitySizeExpander implements FileEntityExpanderInterface {
|
export class FileEntitySizeExpander implements FileEntityExpanderInterface {
|
||||||
async expand(fileEntity: FileEntity): Promise<FileEntity> {
|
async expand(fileEntity: FileEntity): Promise<FileEntity> {
|
||||||
|
if (fileEntity.streamProvider !== StreamProviderEnum.fs) {
|
||||||
|
return Promise.resolve(fileEntity);
|
||||||
|
}
|
||||||
const stats: Stats | false = await stat(fileEntity.path).catch(() => false);
|
const stats: Stats | false = await stat(fileEntity.path).catch(() => false);
|
||||||
|
|
||||||
if (stats !== false) {
|
if (stats !== false) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import * as tnp from 'torrent-name-parser';
|
import * as tnp from 'torrent-name-parser';
|
||||||
import { FileEntityExpanderInterface } from './file-entity-expander.interface';
|
import { FileEntityExpanderInterface } from './file-entity-expander.interface';
|
||||||
import { FileEntity } from 'src/VideoFiles/Entity/file.entity';
|
import { FileEntity } from 'src/Shared/Entity/file.entity';
|
||||||
import { VideoFilesUpdateRepository } from '../video-files-update.repository';
|
import { VideoFilesUpdateRepository } from '../video-files-update.repository';
|
||||||
import { TitleTypeEnum } from '../../Enum/title-type.enum';
|
import { TitleTypeEnum } from '../../../Shared/Enum/title-type.enum';
|
||||||
|
|
||||||
//todo - refactor this copy-paste, add types, ect.
|
//todo - refactor this copy-paste, add types, ect.
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { FileEntityExpanderInterface } from './file-entity-expander.interface';
|
||||||
|
import { FileEntity } from '../../../Shared/Entity/file.entity';
|
||||||
|
import { TorrentSeedClient } from '../../../Torrent/SeedClient/torrent-seed.client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FileEntityTransmissionExpander
|
||||||
|
implements FileEntityExpanderInterface
|
||||||
|
{
|
||||||
|
constructor(private readonly torrentSeedClient: TorrentSeedClient) {}
|
||||||
|
|
||||||
|
async expand(fileEntity: FileEntity): Promise<FileEntity> {
|
||||||
|
const torrentInfo = await this.torrentSeedClient
|
||||||
|
.findTorrentInfoByFile(fileEntity)
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
|
if (torrentInfo === null) {
|
||||||
|
return Promise.resolve(fileEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
fileEntity.transmissionId = torrentInfo.id;
|
||||||
|
fileEntity.linkomanija = torrentInfo.linkomanija;
|
||||||
|
|
||||||
|
return fileEntity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FileEntityExpanderInterface } from './file-entity-expander.interface';
|
import { FileEntityExpanderInterface } from './file-entity-expander.interface';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { FileEntity } from '../../Entity/file.entity';
|
import { FileEntity } from '../../../Shared/Entity/file.entity';
|
||||||
|
|
||||||
export const FileEntityExpanders = 'FileEntityExpanders';
|
export const FileEntityExpanders = 'FileEntityExpanders';
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ export const FileEntityExpanders = 'FileEntityExpanders';
|
|||||||
export class FileEntityExpander implements FileEntityExpanderInterface {
|
export class FileEntityExpander implements FileEntityExpanderInterface {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(FileEntityExpanders)
|
@Inject(FileEntityExpanders)
|
||||||
private expanders: Array<FileEntityExpanderInterface>,
|
private expanders: FileEntityExpanderInterface[],
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async expand(fileEntity: FileEntity): Promise<FileEntity> {
|
async expand(fileEntity: FileEntity): Promise<FileEntity> {
|
||||||
@@ -18,11 +18,8 @@ export class FileEntityExpander implements FileEntityExpanderInterface {
|
|||||||
const expanderPromise = expander.expand(fileEntity);
|
const expanderPromise = expander.expand(fileEntity);
|
||||||
expanderPromises.push(expanderPromise);
|
expanderPromises.push(expanderPromise);
|
||||||
}
|
}
|
||||||
|
await Promise.all(expanderPromises);
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return fileEntity;
|
||||||
Promise.all(expanderPromises).then(() => {
|
|
||||||
resolve(fileEntity);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class VideoFilesScannerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const findOptions = [path, '-name'].concat(
|
const findOptions = [path, '-name'].concat(
|
||||||
this.config.getVideoFilesExt().join(' -o -name ').split(' '),
|
('*' + this.config.getVideoFilesExt().join(' -o -name *')).split(' '),
|
||||||
);
|
);
|
||||||
|
|
||||||
const objCommand = spawn('find', findOptions);
|
const objCommand = spawn('find', findOptions);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Module, Provider } from '@nestjs/common';
|
import { Module, Provider } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { TitleEntity } from '../Entity/title.entity';
|
import { TitleEntity } from '../../Shared/Entity/title.entity';
|
||||||
import { FileEntity } from '../Entity/file.entity';
|
import { FileEntity } from '../../Shared/Entity/file.entity';
|
||||||
import {
|
import {
|
||||||
FileEntityExpander,
|
FileEntityExpander,
|
||||||
FileEntityExpanders,
|
FileEntityExpanders,
|
||||||
@@ -13,6 +13,8 @@ import { VideoFilesUpdateRepository } from './video-files-update.repository';
|
|||||||
import { FileEntitySizeExpander } from './Expander/file-entity-size.expander';
|
import { FileEntitySizeExpander } from './Expander/file-entity-size.expander';
|
||||||
import { FileEntityDefaultsExpander } from './Expander/file-entity-defaults.expander';
|
import { FileEntityDefaultsExpander } from './Expander/file-entity-defaults.expander';
|
||||||
import { FileEntityDurationExpander } from './Expander/file-entity-duration.expander';
|
import { FileEntityDurationExpander } from './Expander/file-entity-duration.expander';
|
||||||
|
import { FileEntityTransmissionExpander } from './Expander/file-entity-transmission.expander';
|
||||||
|
import { TorrentSeedClientModule } from '../../Torrent/SeedClient/torrent-seed-client.module';
|
||||||
|
|
||||||
const fileEntityExpanders: Provider<any>[] = [
|
const fileEntityExpanders: Provider<any>[] = [
|
||||||
FileEntityExpander,
|
FileEntityExpander,
|
||||||
@@ -20,22 +22,26 @@ const fileEntityExpanders: Provider<any>[] = [
|
|||||||
FileEntitySizeExpander,
|
FileEntitySizeExpander,
|
||||||
FileEntityTitleInfoExpander,
|
FileEntityTitleInfoExpander,
|
||||||
FileEntityDurationExpander,
|
FileEntityDurationExpander,
|
||||||
|
FileEntityTransmissionExpander,
|
||||||
{
|
{
|
||||||
provide: FileEntityExpanders,
|
provide: FileEntityExpanders,
|
||||||
useFactory: (...expanders) => expanders,
|
useFactory: (...expanders) => expanders,
|
||||||
inject: [
|
inject: [
|
||||||
//expanders run in parallel, make sure they don't rely on each other's result
|
//expanders run in parallel, make sure they don't rely on each other's result
|
||||||
//todo - linkomanija, webtorrent, transmission expanders
|
|
||||||
FileEntityDefaultsExpander,
|
FileEntityDefaultsExpander,
|
||||||
FileEntitySizeExpander,
|
FileEntitySizeExpander,
|
||||||
FileEntityTitleInfoExpander,
|
FileEntityTitleInfoExpander,
|
||||||
FileEntityDurationExpander,
|
FileEntityDurationExpander,
|
||||||
|
FileEntityTransmissionExpander,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([TitleEntity, FileEntity])],
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([TitleEntity, FileEntity]),
|
||||||
|
TorrentSeedClientModule,
|
||||||
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [
|
providers: [
|
||||||
VideoFilesUpdateService,
|
VideoFilesUpdateService,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { FileEntity } from '../Entity/file.entity';
|
import { FileEntity } from '../../Shared/Entity/file.entity';
|
||||||
import { In, Not, Repository, UpdateResult } from 'typeorm';
|
import { In, Not, Repository, UpdateResult } from 'typeorm';
|
||||||
import { StreamProviderEnum } from '../../Streamer/ReadStreamProvider/stream-provider.enum';
|
import { StreamProviderEnum } from '../../Shared/Enum/stream-provider.enum';
|
||||||
import { TitleEntity } from '../Entity/title.entity';
|
import { TitleEntity } from '../../Shared/Entity/title.entity';
|
||||||
import { TitleTypeEnum } from '../Enum/title-type.enum';
|
import { TitleTypeEnum } from '../../Shared/Enum/title-type.enum';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VideoFilesUpdateRepository {
|
export class VideoFilesUpdateRepository {
|
||||||
@@ -78,6 +78,7 @@ export class VideoFilesUpdateRepository {
|
|||||||
): Promise<FileEntity> {
|
): Promise<FileEntity> {
|
||||||
let fileEntity = await this.fileRepository.findOne({
|
let fileEntity = await this.fileRepository.findOne({
|
||||||
where: { path: path },
|
where: { path: path },
|
||||||
|
relations: { torrent: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (fileEntity === null) {
|
if (fileEntity === null) {
|
||||||
@@ -87,6 +88,7 @@ export class VideoFilesUpdateRepository {
|
|||||||
await this.fileRepository.save(fileEntity);
|
await this.fileRepository.save(fileEntity);
|
||||||
fileEntity = await this.fileRepository.findOne({
|
fileEntity = await this.fileRepository.findOne({
|
||||||
where: { path: path },
|
where: { path: path },
|
||||||
|
relations: { torrent: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,4 +99,11 @@ export class VideoFilesUpdateRepository {
|
|||||||
public saveFiles(fileEntities: FileEntity[]) {
|
public saveFiles(fileEntities: FileEntity[]) {
|
||||||
return this.fileRepository.save(fileEntities);
|
return this.fileRepository.save(fileEntities);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public findFilesByPaths(paths: string[]): Promise<FileEntity[]> {
|
||||||
|
return this.fileRepository.find({
|
||||||
|
where: { path: In(paths) },
|
||||||
|
relations: { torrent: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,16 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { VideoFilesScannerService } from './Scanner/video-files-scanner.service';
|
import { VideoFilesScannerService } from './Scanner/video-files-scanner.service';
|
||||||
import { VideoFilesUpdateRepository } from './video-files-update.repository';
|
import { VideoFilesUpdateRepository } from './video-files-update.repository';
|
||||||
import { FileEntityExpander } from './Expander/file-entity.expander';
|
import { FileEntityExpander } from './Expander/file-entity.expander';
|
||||||
import { FileEntity } from '../Entity/file.entity';
|
import { FileEntity } from '../../Shared/Entity/file.entity';
|
||||||
import { StreamProviderEnum } from '../../Streamer/ReadStreamProvider/stream-provider.enum';
|
import { StreamProviderEnum } from '../../Shared/Enum/stream-provider.enum';
|
||||||
|
import { configService } from '../../config/config.service';
|
||||||
|
import { VideoFilesConfig } from '../../config/video-files.config';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VideoFilesUpdateService {
|
export class VideoFilesUpdateService {
|
||||||
|
private readonly config: VideoFilesConfig =
|
||||||
|
configService.getVideoFilesConfig();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly scanner: VideoFilesScannerService,
|
private readonly scanner: VideoFilesScannerService,
|
||||||
private readonly repository: VideoFilesUpdateRepository,
|
private readonly repository: VideoFilesUpdateRepository,
|
||||||
@@ -20,20 +25,30 @@ export class VideoFilesUpdateService {
|
|||||||
|
|
||||||
const filesInDb = await this.repository.getMatchingPaths(filesInFs);
|
const filesInDb = await this.repository.getMatchingPaths(filesInFs);
|
||||||
const filesToAddToDb = filesInFs.filter(
|
const filesToAddToDb = filesInFs.filter(
|
||||||
(filePath) => !filesInDb.includes(filePath),
|
(filePath) =>
|
||||||
|
!filesInDb.includes(filePath) && !this.isPathToIgnore(filePath),
|
||||||
);
|
);
|
||||||
|
|
||||||
const entitiesToSave = [];
|
const entitiesToSave = [];
|
||||||
|
|
||||||
for (const fileToAddToDb of filesToAddToDb) {
|
for (const fileToAddToDb of filesToAddToDb) {
|
||||||
if (this.checkPathToIgnore(fileToAddToDb)) {
|
const fileEntity = await this.repository.findOrCreateFile(
|
||||||
|
fileToAddToDb,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
//check if download is complete
|
||||||
|
if (
|
||||||
|
fileEntity.streamProvider === StreamProviderEnum.wt &&
|
||||||
|
fileEntity.torrent &&
|
||||||
|
fileEntity.torrent.stopped === false
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
fileEntity.relativePath = filesToRelativePaths[fileToAddToDb];
|
||||||
|
fileEntity.streamProvider = StreamProviderEnum.fs;
|
||||||
|
|
||||||
const fileEntity = await this.buildFileEntity(
|
await this.expander.expand(fileEntity);
|
||||||
fileToAddToDb,
|
|
||||||
filesToRelativePaths[fileToAddToDb],
|
|
||||||
);
|
|
||||||
|
|
||||||
entitiesToSave.push(fileEntity);
|
entitiesToSave.push(fileEntity);
|
||||||
}
|
}
|
||||||
@@ -41,19 +56,63 @@ export class VideoFilesUpdateService {
|
|||||||
await this.repository.saveFiles(entitiesToSave);
|
await this.repository.saveFiles(entitiesToSave);
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkPathToIgnore(filePath: string): boolean {
|
private isPathToIgnore(filePath: string): boolean {
|
||||||
//todo - move to separate provider, include in settings
|
for (const pathPattern of this.config.getPathPatternsToIgnore()) {
|
||||||
return filePath.indexOf('AOE1') > -1;
|
if (filePath.indexOf(pathPattern) > -1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildFileEntity(
|
async updateEntitiesByPath(filePaths: string[]) {
|
||||||
path: string,
|
const fileEntitiesToUpdate = [];
|
||||||
relativePath: string,
|
const fileEntities = await this.repository.findFilesByPaths(filePaths);
|
||||||
): Promise<FileEntity> {
|
|
||||||
const fileEntity = await this.repository.findOrCreateFile(path, false);
|
|
||||||
fileEntity.relativePath = relativePath;
|
|
||||||
fileEntity.streamProvider = StreamProviderEnum.fs;
|
|
||||||
|
|
||||||
return await this.expander.expand(fileEntity);
|
for (const fileEntity of fileEntities) {
|
||||||
|
if (!fileEntity.relativePath) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
fileEntity.streamProvider === StreamProviderEnum.wt &&
|
||||||
|
fileEntity.torrent &&
|
||||||
|
fileEntity.torrent.stopped === false
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fileEntity.streamProvider = StreamProviderEnum.fs;
|
||||||
|
|
||||||
|
await this.expander.expand(fileEntity);
|
||||||
|
fileEntitiesToUpdate.push(fileEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.repository.saveFiles(fileEntitiesToUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async buildAndSaveEntitiesFromPartial(
|
||||||
|
partialFileEntities: Partial<FileEntity>[],
|
||||||
|
): Promise<FileEntity[]> {
|
||||||
|
const entitiesToSave: FileEntity[] = [];
|
||||||
|
for (const partialFileEntity of partialFileEntities) {
|
||||||
|
let fileEntity = await this.repository.findOrCreateFile(
|
||||||
|
partialFileEntity.path,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
fileEntity = { ...fileEntity, ...partialFileEntity };
|
||||||
|
|
||||||
|
await this.expander.expand(fileEntity);
|
||||||
|
entitiesToSave.push(fileEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.repository.saveFiles(entitiesToSave);
|
||||||
|
|
||||||
|
return this.repository.findFilesByPaths(
|
||||||
|
entitiesToSave.map((fileEntity) => {
|
||||||
|
return fileEntity.path;
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { TitleEntity } from './Entity/title.entity';
|
import { TitleEntity } from '../Shared/Entity/title.entity';
|
||||||
import { TitleTypeEnum } from './Enum/title-type.enum';
|
import { TitleTypeEnum } from '../Shared/Enum/title-type.enum';
|
||||||
import { FileEntity } from './Entity/file.entity';
|
import { FileEntity } from '../Shared/Entity/file.entity';
|
||||||
import { VideoFilesUpdateService } from './Update/video-files-update.service';
|
import { VideoFilesUpdateService } from './Update/video-files-update.service';
|
||||||
|
import NotificationResponse from '../KodiApi/Dto/notification-response.dto';
|
||||||
|
import { Stats } from 'fs';
|
||||||
|
import { stat, unlink } from 'fs/promises';
|
||||||
|
|
||||||
//todo - refactor provider as facade
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VideoFilesFacade {
|
export class VideoFilesFacade {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -29,7 +31,7 @@ export class VideoFilesFacade {
|
|||||||
async getTitleWithFiles(titleId: number): Promise<TitleEntity | null> {
|
async getTitleWithFiles(titleId: number): Promise<TitleEntity | null> {
|
||||||
return this.titleRepository
|
return this.titleRepository
|
||||||
.findOne({
|
.findOne({
|
||||||
relations: { files: true },
|
relations: { files: { torrent: true } },
|
||||||
where: { id: titleId, files: { deleted: false } },
|
where: { id: titleId, files: { deleted: false } },
|
||||||
})
|
})
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
@@ -37,11 +39,57 @@ export class VideoFilesFacade {
|
|||||||
|
|
||||||
async getFile(fileId: number): Promise<FileEntity | null> {
|
async getFile(fileId: number): Promise<FileEntity | null> {
|
||||||
return this.fileRepository
|
return this.fileRepository
|
||||||
.findOne({ where: { id: fileId } })
|
.findOne({ where: { id: fileId }, relations: { torrent: true } })
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan for new files and remove deleted.
|
||||||
|
* Once all hooks are working correctly, this one shouldn't be needed anymore
|
||||||
|
*/
|
||||||
updateFsVideoFiles(): void {
|
updateFsVideoFiles(): void {
|
||||||
this.videoFilesUpdateService.updateFsVideoFiles().then();
|
this.videoFilesUpdateService.updateFsVideoFiles().then();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addVideoFiles(
|
||||||
|
partialFileEntities: Partial<FileEntity>[],
|
||||||
|
): Promise<FileEntity[]> {
|
||||||
|
return this.videoFilesUpdateService.buildAndSaveEntitiesFromPartial(
|
||||||
|
partialFileEntities,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if entity with provided path exists and fully downloaded, it will
|
||||||
|
* - update entity properties, like size, duration, etc.
|
||||||
|
* - move to fs provider
|
||||||
|
* @param filePaths
|
||||||
|
*/
|
||||||
|
updateEntitiesByPath(filePaths: string[]) {
|
||||||
|
return this.videoFilesUpdateService.updateEntitiesByPath(filePaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile(fileId: number): Promise<boolean> {
|
||||||
|
const fileEntity = await this.fileRepository.findOne({
|
||||||
|
where: { id: fileId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fileEntity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileEntity.deleted = true;
|
||||||
|
await this.fileRepository.save(fileEntity);
|
||||||
|
|
||||||
|
const stats: Stats | false = await stat(fileEntity.path).catch(() => false);
|
||||||
|
|
||||||
|
if (stats === false) {
|
||||||
|
//file already deleted
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return unlink(fileEntity.path)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { TitleEntity } from './Entity/title.entity';
|
import { TitleEntity } from '../Shared/Entity/title.entity';
|
||||||
import { FileEntity } from './Entity/file.entity';
|
import { FileEntity } from '../Shared/Entity/file.entity';
|
||||||
import { VideoFilesFacade } from './video-files.facade';
|
import { VideoFilesFacade } from './video-files.facade';
|
||||||
import { VideoFilesUpdateModule } from './Update/video-files-update.module';
|
import { VideoFilesUpdateModule } from './Update/video-files-update.module';
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
|||||||
import { TypeOrmConfig } from './type-orm.config';
|
import { TypeOrmConfig } from './type-orm.config';
|
||||||
import { PathsConfig } from './paths.config';
|
import { PathsConfig } from './paths.config';
|
||||||
import { VideoFilesConfig } from './video-files.config';
|
import { VideoFilesConfig } from './video-files.config';
|
||||||
|
import { RecentSearchesConfig } from './recent-searches.config';
|
||||||
|
|
||||||
env.config();
|
env.config();
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ export class ConfigService {
|
|||||||
private typeOrmConfig: TypeOrmConfig = new TypeOrmConfig(this);
|
private typeOrmConfig: TypeOrmConfig = new TypeOrmConfig(this);
|
||||||
private pathsConfig: PathsConfig = new PathsConfig(this);
|
private pathsConfig: PathsConfig = new PathsConfig(this);
|
||||||
private videoFilesConfig: VideoFilesConfig = new VideoFilesConfig(this);
|
private videoFilesConfig: VideoFilesConfig = new VideoFilesConfig(this);
|
||||||
|
private recentSearches: RecentSearchesConfig = new RecentSearchesConfig(this);
|
||||||
|
|
||||||
public getEnv(key: string): any {
|
public getEnv(key: string): any {
|
||||||
return process.env[key];
|
return process.env[key];
|
||||||
@@ -32,6 +34,10 @@ export class ConfigService {
|
|||||||
public getVideoFilesConfig(): VideoFilesConfig {
|
public getVideoFilesConfig(): VideoFilesConfig {
|
||||||
return this.videoFilesConfig;
|
return this.videoFilesConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getRecentSearchesConfig(): RecentSearchesConfig {
|
||||||
|
return this.recentSearches;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const configService = new ConfigService();
|
export const configService = new ConfigService();
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ export class PathsConfig {
|
|||||||
return this.getPathByEnvKey('STATIC_SERVE_FOLDER');
|
return this.getPathByEnvKey('STATIC_SERVE_FOLDER');
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRecentSearchesFolder(): string {
|
|
||||||
return this.getPathByEnvKey('RECENT_SEARCHES_FOLDER');
|
|
||||||
}
|
|
||||||
|
|
||||||
public getPathByEnvKey(key: string) {
|
public getPathByEnvKey(key: string) {
|
||||||
const relPath = this.configService.getEnv(key);
|
const relPath = this.configService.getEnv(key);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { ConfigService } from './config.service';
|
||||||
|
|
||||||
|
export class RecentSearchesConfig {
|
||||||
|
constructor(private readonly configService: ConfigService) {
|
||||||
|
this.configService = configService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRecentSearchesFolder(): string {
|
||||||
|
return this.configService
|
||||||
|
.getPaths()
|
||||||
|
.getPathByEnvKey('RECENT_SEARCHES_FOLDER');
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRecentSearchesLimit(): number {
|
||||||
|
return parseInt(this.configService.getEnv('RECENT_SEARCHES_LIMIT'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import { LrtCategory } from '../KodiApi/LRT/LrtApiClient/Entity/lrt-category.entity';
|
import { LrtCategory } from '../KodiApi/LRT/LrtApiClient/Entity/lrt-category.entity';
|
||||||
import { ConfigService } from './config.service';
|
import { ConfigService } from './config.service';
|
||||||
import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type';
|
import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type';
|
||||||
import { TitleEntity } from '../VideoFiles/Entity/title.entity';
|
import { TitleEntity } from '../Shared/Entity/title.entity';
|
||||||
import { FileEntity } from '../VideoFiles/Entity/file.entity';
|
import { FileEntity } from '../Shared/Entity/file.entity';
|
||||||
|
import { TorrentEntity } from '../Shared/Entity/torrent.entity';
|
||||||
|
|
||||||
export class TypeOrmConfig {
|
export class TypeOrmConfig {
|
||||||
constructor(configService: ConfigService) {
|
constructor(configService: ConfigService) {
|
||||||
this.type = 'sqlite';
|
this.type = 'sqlite';
|
||||||
this.database = configService.getEnv('DB_PATH');
|
this.database = configService.getEnv('DB_PATH');
|
||||||
this.entities = [LrtCategory, TitleEntity, FileEntity];
|
this.entities = [LrtCategory, TitleEntity, FileEntity, TorrentEntity];
|
||||||
}
|
}
|
||||||
type: 'sqlite';
|
type: 'sqlite';
|
||||||
database: string;
|
database: string;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ConfigService } from './config.service';
|
import { ConfigService } from './config.service';
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
export class VideoFilesConfig {
|
export class VideoFilesConfig {
|
||||||
constructor(private readonly configService: ConfigService) {
|
constructor(private readonly configService: ConfigService) {
|
||||||
@@ -13,4 +12,25 @@ export class VideoFilesConfig {
|
|||||||
public getVideoFilesExt(): string[] {
|
public getVideoFilesExt(): string[] {
|
||||||
return JSON.parse(this.configService.getEnv('VIDEO_FILES_EXT'));
|
return JSON.parse(this.configService.getEnv('VIDEO_FILES_EXT'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getPathPatternsToIgnore(): string[] {
|
||||||
|
return JSON.parse(this.configService.getEnv('PATTERNS_TO_IGNORE'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTorrentDownloadDir(): string {
|
||||||
|
return this.configService
|
||||||
|
.getPaths()
|
||||||
|
.getPathByEnvKey('TORRENT_DOWNLOAD_DIR');
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTransmissionOptions() {
|
||||||
|
return {
|
||||||
|
host: this.configService.getEnv('TRANSMISSION_CLIENT_HOST'), // # default 'localhost'
|
||||||
|
//port: 9091, // # default 9091
|
||||||
|
//username: "username", // # default blank
|
||||||
|
//password: "password", // # default blank
|
||||||
|
//ssl: true, //# default false use https
|
||||||
|
//url: "/my/other/url" // # default '/transmission/rpc'
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { HttpExceptionFilter } from './Exception/http-exception.filter';
|
|||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
app.useGlobalFilters(new HttpExceptionFilter());
|
app.useGlobalFilters(new HttpExceptionFilter());
|
||||||
|
app.getHttpAdapter().getInstance().set('etag', false);
|
||||||
await app.listen(configService.getPort());
|
await app.listen(configService.getPort());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user