mirror of
https://github.com/sakaljurgis/kodi-api-server.git
synced 2026-07-08 20:37:41 +00:00
refactor streamer to use stream providers (e.g. to plugin web torrent)
This commit is contained in:
@@ -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;`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-2
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user