From 3c9ea2fb398d2ab795b31fd4518fb6c199e0ba89 Mon Sep 17 00:00:00 2001 From: Jurgis Sakalauskas Date: Wed, 4 Jan 2023 14:26:05 +0200 Subject: [PATCH] refactor streamer to use stream providers (e.g. to plugin web torrent) --- ...908-AddStreamProviderColumnToFilesTable.ts | 19 +++++++++++ src/KodiApi/AllFiles/all-files.service.ts | 6 ++-- .../fs-read-stream-creatable.provider.ts | 34 +++++++++++++++++++ .../read-stream-creatable-file.dto.ts | 6 ++-- ...ead-stream-creatable-provider.interface.ts | 7 ++++ .../read-stream-creatable.provider.ts | 21 ++++++++++++ .../stream-provider.enum.ts | 4 +++ .../StreamerService/file-streamer.service.ts | 34 ------------------- .../StreamerService/streamer.service.ts | 2 -- src/Streamer/streamer.facade.ts | 18 +++++++--- src/Streamer/streamer.module.ts | 14 ++++++-- src/VideoFiles/Entity/file.entity.ts | 4 +++ src/VideoFiles/video-files.provider.ts | 10 ++---- 13 files changed, 123 insertions(+), 56 deletions(-) create mode 100644 migrations/1672820632908-AddStreamProviderColumnToFilesTable.ts create mode 100644 src/Streamer/ReadStreamProvider/FileSystem/fs-read-stream-creatable.provider.ts rename src/Streamer/{Dto => ReadStreamProvider/FileSystem}/read-stream-creatable-file.dto.ts (65%) create mode 100644 src/Streamer/ReadStreamProvider/read-stream-creatable-provider.interface.ts create mode 100644 src/Streamer/ReadStreamProvider/read-stream-creatable.provider.ts create mode 100644 src/Streamer/ReadStreamProvider/stream-provider.enum.ts delete mode 100644 src/Streamer/StreamerService/file-streamer.service.ts diff --git a/migrations/1672820632908-AddStreamProviderColumnToFilesTable.ts b/migrations/1672820632908-AddStreamProviderColumnToFilesTable.ts new file mode 100644 index 0000000..d35e173 --- /dev/null +++ b/migrations/1672820632908-AddStreamProviderColumnToFilesTable.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddStreamProviderColumnToFilesTable1672820632908 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `alter table files + add stream_provider TEXT default 'fs' not null;`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `alter table files + drop column stream_provider;`, + ); + } +} diff --git a/src/KodiApi/AllFiles/all-files.service.ts b/src/KodiApi/AllFiles/all-files.service.ts index a38138c..482178e 100644 --- a/src/KodiApi/AllFiles/all-files.service.ts +++ b/src/KodiApi/AllFiles/all-files.service.ts @@ -136,12 +136,12 @@ export class AllFilesService { if (isNaN(id)) { throw new NotFoundException(`id #${fileId} not correct`); } - const filePath = await this.videoTitlesProvider.getFilePath(id); + const fileEntity = await this.videoTitlesProvider.getFile(id); - if (filePath === null) { + if (fileEntity === null) { throw new NotFoundException(`id #${fileId} not found`); } - return this.streamerFacade.streamFile(request, response, filePath); + return this.streamerFacade.streamVideoFile(request, response, fileEntity); } } diff --git a/src/Streamer/ReadStreamProvider/FileSystem/fs-read-stream-creatable.provider.ts b/src/Streamer/ReadStreamProvider/FileSystem/fs-read-stream-creatable.provider.ts new file mode 100644 index 0000000..cfc92b7 --- /dev/null +++ b/src/Streamer/ReadStreamProvider/FileSystem/fs-read-stream-creatable.provider.ts @@ -0,0 +1,34 @@ +import { FileEntity } from '../../../VideoFiles/Entity/file.entity'; +import { ReadStreamCreatable } from '../../Interface/read-stream-creatable.interface'; +import { MimeService } from '../../Mime/mime.service'; +import { Stats } from 'fs'; +import { stat } from 'fs/promises'; +import { Injectable, NotFoundException } from '@nestjs/common'; +import { ReadStreamCreatableFile } from './read-stream-creatable-file.dto'; +import { ReadStreamCreatableProviderInterface } from '../read-stream-creatable-provider.interface'; +import { StreamProviderEnum } from '../stream-provider.enum'; + +@Injectable() +export class FsReadStreamCreatableProvider + implements ReadStreamCreatableProviderInterface +{ + constructor(private readonly mimeService: MimeService) {} + + supports(fileEntity: FileEntity): boolean { + return fileEntity.streamProvider === StreamProviderEnum.fs; + } + + async getReadStreamCreatable( + fileEntity: FileEntity, + ): Promise { + const filePath = fileEntity.path; + const stats: Stats | false = await stat(filePath).catch(() => false); + if (stats === false) { + //todo - set the file to deleted, since no longer available. Events? + throw new NotFoundException('file not found'); + } + const mimeType = this.mimeService.getMime(filePath); + + return new ReadStreamCreatableFile(filePath, stats.size, mimeType); + } +} diff --git a/src/Streamer/Dto/read-stream-creatable-file.dto.ts b/src/Streamer/ReadStreamProvider/FileSystem/read-stream-creatable-file.dto.ts similarity index 65% rename from src/Streamer/Dto/read-stream-creatable-file.dto.ts rename to src/Streamer/ReadStreamProvider/FileSystem/read-stream-creatable-file.dto.ts index d9ae651..4877d12 100644 --- a/src/Streamer/Dto/read-stream-creatable-file.dto.ts +++ b/src/Streamer/ReadStreamProvider/FileSystem/read-stream-creatable-file.dto.ts @@ -1,7 +1,9 @@ -import { ReadStreamCreatable } from '../Interface/read-stream-creatable.interface'; import { createReadStream, ReadStream } from 'fs'; -import { ReadStreamOptions } from '../Interface/read-stream-options.interface'; +import { ReadStreamOptions } from '../../Interface/read-stream-options.interface'; +import { ReadStreamCreatable } from '../../Interface/read-stream-creatable.interface'; +import { Injectable } from '@nestjs/common'; +@Injectable() export class ReadStreamCreatableFile implements ReadStreamCreatable { constructor( private readonly path: string, diff --git a/src/Streamer/ReadStreamProvider/read-stream-creatable-provider.interface.ts b/src/Streamer/ReadStreamProvider/read-stream-creatable-provider.interface.ts new file mode 100644 index 0000000..c1ca70b --- /dev/null +++ b/src/Streamer/ReadStreamProvider/read-stream-creatable-provider.interface.ts @@ -0,0 +1,7 @@ +import { FileEntity } from '../../VideoFiles/Entity/file.entity'; +import { ReadStreamCreatable } from '../Interface/read-stream-creatable.interface'; + +export interface ReadStreamCreatableProviderInterface { + supports(fileEntity: FileEntity): boolean; + getReadStreamCreatable(fileEntity: FileEntity): Promise; +} diff --git a/src/Streamer/ReadStreamProvider/read-stream-creatable.provider.ts b/src/Streamer/ReadStreamProvider/read-stream-creatable.provider.ts new file mode 100644 index 0000000..b259c82 --- /dev/null +++ b/src/Streamer/ReadStreamProvider/read-stream-creatable.provider.ts @@ -0,0 +1,21 @@ +import { ReadStreamCreatable } from '../Interface/read-stream-creatable.interface'; +import { FileEntity } from '../../VideoFiles/Entity/file.entity'; +import { Inject, Injectable } from '@nestjs/common'; +import { ReadStreamCreatableProviderInterface } from './read-stream-creatable-provider.interface'; + +export const ReadStreamCreatableProviders = 'ReadStreamCreatableProviders'; + +@Injectable() +export class ReadStreamCreatableProvider { + constructor( + @Inject(ReadStreamCreatableProviders) + private providers: Array, + ) {} + getReadStreamCreatable(fileEntity: FileEntity): Promise { + for (const provider of this.providers) { + if (provider.supports(fileEntity)) { + return provider.getReadStreamCreatable(fileEntity); + } + } + } +} diff --git a/src/Streamer/ReadStreamProvider/stream-provider.enum.ts b/src/Streamer/ReadStreamProvider/stream-provider.enum.ts new file mode 100644 index 0000000..c24b09a --- /dev/null +++ b/src/Streamer/ReadStreamProvider/stream-provider.enum.ts @@ -0,0 +1,4 @@ +export enum StreamProviderEnum { + fs = 'fs', //filesystem + wt = 'wt', //web torrent +} diff --git a/src/Streamer/StreamerService/file-streamer.service.ts b/src/Streamer/StreamerService/file-streamer.service.ts deleted file mode 100644 index f122e4e..0000000 --- a/src/Streamer/StreamerService/file-streamer.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Request, Response } from 'express'; -import { Stats } from 'fs'; -import { stat } from 'fs/promises'; -import { Injectable, NotFoundException } from '@nestjs/common'; -import { ReadStreamCreatableFile } from '../Dto/read-stream-creatable-file.dto'; -import { StreamerService } from './streamer.service'; -import { MimeService } from '../Mime/mime.service'; - -@Injectable() -export class FileStreamerService { - constructor( - private readonly streamerService: StreamerService, - private readonly mimeService: MimeService, - ) {} - async streamFile( - request: Request, - response: Response, - filePath: string, - ): Promise { - const stats: Stats | false = await stat(filePath).catch(() => false); - if (stats === false) { - //todo - set the file to deleted, since no longer available. Events? - throw new NotFoundException('file not found'); - } - const mimeType = this.mimeService.getMime(filePath); - const streamCreatable = new ReadStreamCreatableFile( - filePath, - stats.size, - mimeType, - ); - - return this.streamerService.stream(request, response, streamCreatable); - } -} diff --git a/src/Streamer/StreamerService/streamer.service.ts b/src/Streamer/StreamerService/streamer.service.ts index b666e29..d6ee316 100644 --- a/src/Streamer/StreamerService/streamer.service.ts +++ b/src/Streamer/StreamerService/streamer.service.ts @@ -1,6 +1,5 @@ import { ReadStreamCreatable } from '../Interface/read-stream-creatable.interface'; import { RequestRangeService } from '../RequestRange/request-range.service'; -import { MimeService } from '../Mime/mime.service'; import { Injectable } from '@nestjs/common'; import { Request, Response } from 'express'; import { ResponseHeadersService } from '../ResponseHeadersService/response-headers.service'; @@ -10,7 +9,6 @@ import RequestRangeBytes from '../RequestRange/request-range-bytes.class'; @Injectable() export class StreamerService { constructor( - private readonly mimeService: MimeService, private readonly requestRangeService: RequestRangeService, private readonly responseHeadersService: ResponseHeadersService, private readonly responseSender: ResponseSenderService, diff --git a/src/Streamer/streamer.facade.ts b/src/Streamer/streamer.facade.ts index 39726f7..f0fbddc 100644 --- a/src/Streamer/streamer.facade.ts +++ b/src/Streamer/streamer.facade.ts @@ -1,16 +1,24 @@ import { Injectable } from '@nestjs/common'; import { Request, Response } from 'express'; -import { FileStreamerService } from './StreamerService/file-streamer.service'; +import { FileEntity } from '../VideoFiles/Entity/file.entity'; +import { ReadStreamCreatableProvider } from './ReadStreamProvider/read-stream-creatable.provider'; +import { StreamerService } from './StreamerService/streamer.service'; @Injectable() export class StreamerFacade { - constructor(private readonly fileStreamerService: FileStreamerService) {} + constructor( + private readonly streamerService: StreamerService, + private readonly readStreamCreatableProvider: ReadStreamCreatableProvider, + ) {} - async streamFile( + async streamVideoFile( request: Request, response: Response, - filePath: string, + fileEntity: FileEntity, ): Promise { - return this.fileStreamerService.streamFile(request, response, filePath); + const readStreamCreatable = + await this.readStreamCreatableProvider.getReadStreamCreatable(fileEntity); + + return this.streamerService.stream(request, response, readStreamCreatable); } } diff --git a/src/Streamer/streamer.module.ts b/src/Streamer/streamer.module.ts index f7fd620..7f6ba02 100644 --- a/src/Streamer/streamer.module.ts +++ b/src/Streamer/streamer.module.ts @@ -3,9 +3,13 @@ import { StreamerFacade } from './streamer.facade'; import { MimeService } from './Mime/mime.service'; import { RequestRangeService } from './RequestRange/request-range.service'; import { StreamerService } from './StreamerService/streamer.service'; -import { FileStreamerService } from './StreamerService/file-streamer.service'; import { ResponseHeadersService } from './ResponseHeadersService/response-headers.service'; import { ResponseSenderService } from './ResponseSender/response-sender.service'; +import { + ReadStreamCreatableProvider, + ReadStreamCreatableProviders, +} from './ReadStreamProvider/read-stream-creatable.provider'; +import { FsReadStreamCreatableProvider } from './ReadStreamProvider/FileSystem/fs-read-stream-creatable.provider'; @Module({ imports: [], @@ -14,9 +18,15 @@ import { ResponseSenderService } from './ResponseSender/response-sender.service' RequestRangeService, ResponseHeadersService, ResponseSenderService, - FileStreamerService, StreamerService, StreamerFacade, + ReadStreamCreatableProvider, + FsReadStreamCreatableProvider, + { + provide: ReadStreamCreatableProviders, + useFactory: (...providers) => providers, + inject: [FsReadStreamCreatableProvider], + }, ], exports: [StreamerFacade], }) diff --git a/src/VideoFiles/Entity/file.entity.ts b/src/VideoFiles/Entity/file.entity.ts index 25af9b0..0194340 100644 --- a/src/VideoFiles/Entity/file.entity.ts +++ b/src/VideoFiles/Entity/file.entity.ts @@ -1,5 +1,6 @@ import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; import { TitleEntity } from './title.entity'; +import { StreamProviderEnum } from '../../Streamer/ReadStreamProvider/stream-provider.enum'; @Entity('files') export class FileEntity { @@ -36,4 +37,7 @@ export class FileEntity { @Column() duration: number; + + @Column({ name: 'stream_provider' }) + streamProvider: StreamProviderEnum; } diff --git a/src/VideoFiles/video-files.provider.ts b/src/VideoFiles/video-files.provider.ts index e7b3741..58bbeb5 100644 --- a/src/VideoFiles/video-files.provider.ts +++ b/src/VideoFiles/video-files.provider.ts @@ -32,15 +32,9 @@ export class VideoFilesProvider { .catch(() => null); } - async getFilePath(fileId: number): Promise { - const entity = await this.fileRepository + async getFile(fileId: number): Promise { + return this.fileRepository .findOne({ where: { id: fileId } }) .catch(() => null); - - if (entity === null) { - return null; - } - - return entity.path; } }