refactor streamer to use stream providers (e.g. to plugin web torrent)

This commit is contained in:
Jurgis Sakalauskas
2023-01-04 14:26:05 +02:00
parent 8c900a774e
commit 3c9ea2fb39
13 changed files with 123 additions and 56 deletions
@@ -0,0 +1,19 @@
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;`,
);
}
}
+3 -3
View File
@@ -136,12 +136,12 @@ export class AllFilesService {
if (isNaN(id)) { if (isNaN(id)) {
throw new NotFoundException(`id #${fileId} not correct`); 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`); throw new NotFoundException(`id #${fileId} not found`);
} }
return this.streamerFacade.streamFile(request, response, filePath); return this.streamerFacade.streamVideoFile(request, response, fileEntity);
} }
} }
@@ -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<ReadStreamCreatable> {
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);
}
}
@@ -1,7 +1,9 @@
import { ReadStreamCreatable } from '../Interface/read-stream-creatable.interface';
import { createReadStream, ReadStream } from 'fs'; 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 { export class ReadStreamCreatableFile implements ReadStreamCreatable {
constructor( constructor(
private readonly path: string, private readonly path: string,
@@ -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<ReadStreamCreatable>;
}
@@ -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<ReadStreamCreatableProviderInterface>,
) {}
getReadStreamCreatable(fileEntity: FileEntity): Promise<ReadStreamCreatable> {
for (const provider of this.providers) {
if (provider.supports(fileEntity)) {
return provider.getReadStreamCreatable(fileEntity);
}
}
}
}
@@ -0,0 +1,4 @@
export enum StreamProviderEnum {
fs = 'fs', //filesystem
wt = 'wt', //web torrent
}
@@ -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<void> {
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);
}
}
@@ -1,6 +1,5 @@
import { ReadStreamCreatable } from '../Interface/read-stream-creatable.interface'; import { ReadStreamCreatable } from '../Interface/read-stream-creatable.interface';
import { RequestRangeService } from '../RequestRange/request-range.service'; import { RequestRangeService } from '../RequestRange/request-range.service';
import { MimeService } from '../Mime/mime.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { ResponseHeadersService } from '../ResponseHeadersService/response-headers.service'; import { ResponseHeadersService } from '../ResponseHeadersService/response-headers.service';
@@ -10,7 +9,6 @@ import RequestRangeBytes from '../RequestRange/request-range-bytes.class';
@Injectable() @Injectable()
export class StreamerService { export class StreamerService {
constructor( constructor(
private readonly mimeService: MimeService,
private readonly requestRangeService: RequestRangeService, private readonly requestRangeService: RequestRangeService,
private readonly responseHeadersService: ResponseHeadersService, private readonly responseHeadersService: ResponseHeadersService,
private readonly responseSender: ResponseSenderService, private readonly responseSender: ResponseSenderService,
+13 -5
View File
@@ -1,16 +1,24 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Request, Response } from 'express'; 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() @Injectable()
export class StreamerFacade { export class StreamerFacade {
constructor(private readonly fileStreamerService: FileStreamerService) {} constructor(
private readonly streamerService: StreamerService,
private readonly readStreamCreatableProvider: ReadStreamCreatableProvider,
) {}
async streamFile( async streamVideoFile(
request: Request, request: Request,
response: Response, response: Response,
filePath: string, fileEntity: FileEntity,
): Promise<void> { ): Promise<void> {
return this.fileStreamerService.streamFile(request, response, filePath); const readStreamCreatable =
await this.readStreamCreatableProvider.getReadStreamCreatable(fileEntity);
return this.streamerService.stream(request, response, readStreamCreatable);
} }
} }
+12 -2
View File
@@ -3,9 +3,13 @@ import { StreamerFacade } from './streamer.facade';
import { MimeService } from './Mime/mime.service'; import { MimeService } from './Mime/mime.service';
import { RequestRangeService } from './RequestRange/request-range.service'; import { RequestRangeService } from './RequestRange/request-range.service';
import { StreamerService } from './StreamerService/streamer.service'; import { StreamerService } from './StreamerService/streamer.service';
import { FileStreamerService } from './StreamerService/file-streamer.service';
import { ResponseHeadersService } from './ResponseHeadersService/response-headers.service'; import { ResponseHeadersService } from './ResponseHeadersService/response-headers.service';
import { ResponseSenderService } from './ResponseSender/response-sender.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({ @Module({
imports: [], imports: [],
@@ -14,9 +18,15 @@ import { ResponseSenderService } from './ResponseSender/response-sender.service'
RequestRangeService, RequestRangeService,
ResponseHeadersService, ResponseHeadersService,
ResponseSenderService, ResponseSenderService,
FileStreamerService,
StreamerService, StreamerService,
StreamerFacade, StreamerFacade,
ReadStreamCreatableProvider,
FsReadStreamCreatableProvider,
{
provide: ReadStreamCreatableProviders,
useFactory: (...providers) => providers,
inject: [FsReadStreamCreatableProvider],
},
], ],
exports: [StreamerFacade], exports: [StreamerFacade],
}) })
+4
View File
@@ -1,5 +1,6 @@
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';
@Entity('files') @Entity('files')
export class FileEntity { export class FileEntity {
@@ -36,4 +37,7 @@ export class FileEntity {
@Column() @Column()
duration: number; duration: number;
@Column({ name: 'stream_provider' })
streamProvider: StreamProviderEnum;
} }
+2 -8
View File
@@ -32,15 +32,9 @@ export class VideoFilesProvider {
.catch(() => null); .catch(() => null);
} }
async getFilePath(fileId: number): Promise<string | null> { async getFile(fileId: number): Promise<FileEntity | null> {
const entity = await this.fileRepository return this.fileRepository
.findOne({ where: { id: fileId } }) .findOne({ where: { id: fileId } })
.catch(() => null); .catch(() => null);
if (entity === null) {
return null;
}
return entity.path;
} }
} }