mirror of
https://github.com/sakaljurgis/kodi-api-server.git
synced 2026-07-08 20:37:41 +00:00
streamer service: split to separate files
This commit is contained in:
@@ -29,8 +29,8 @@ export class AllFilesController {
|
||||
@Param('fileId') fileId: string,
|
||||
@Req() request: Request,
|
||||
@Res() response: Response,
|
||||
) {
|
||||
this.allFilesService.play(fileId, request, response);
|
||||
): Promise<void> {
|
||||
return this.allFilesService.play(fileId, request, response);
|
||||
}
|
||||
|
||||
@Get(':titleId/:seasonId?')
|
||||
|
||||
@@ -135,17 +135,16 @@ export class AllFilesService {
|
||||
response: Response,
|
||||
): Promise<void> {
|
||||
const id = parseInt(fileId);
|
||||
if (isNaN(id)) {
|
||||
throw new NotFoundException(`id #${fileId} not correct`);
|
||||
}
|
||||
|
||||
const entity = await this.fileRepository.findOne({ where: { id: id } });
|
||||
|
||||
if (entity === null) {
|
||||
response
|
||||
.status(404)
|
||||
.send('entity not found id: ' + fileId)
|
||||
.end();
|
||||
|
||||
return;
|
||||
throw new NotFoundException(`id #${fileId} not found`);
|
||||
}
|
||||
|
||||
this.streamerFacade.streamFile(request, response, entity.path);
|
||||
return this.streamerFacade.streamFile(request, response, entity.path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ReadStreamCreatable } from '../Interface/read-stream-creatable.interface';
|
||||
import { createReadStream, ReadStream } from 'fs';
|
||||
import { ReadStreamOptions } from '../Interface/read-stream-options.interface';
|
||||
|
||||
export class ReadStreamCreatableFile implements ReadStreamCreatable {
|
||||
constructor(
|
||||
private readonly path: string,
|
||||
readonly length: number,
|
||||
readonly mimeType: string,
|
||||
) {
|
||||
this.path = path;
|
||||
this.length = length;
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
createReadStream(options: ReadStreamOptions): ReadStream {
|
||||
return createReadStream(this.path, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ReadStream } from 'fs';
|
||||
import { ReadStreamOptions } from './read-stream-options.interface';
|
||||
|
||||
export interface ReadStreamCreatable {
|
||||
readonly mimeType: string;
|
||||
readonly length: number;
|
||||
createReadStream(options: ReadStreamOptions): ReadStream;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export class ReadStreamOptions {
|
||||
start?: number;
|
||||
end?: number;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
export default class RequestRangeBytes {
|
||||
readonly end: number;
|
||||
import { ReadStreamOptions } from '../Interface/read-stream-options.interface';
|
||||
|
||||
export default class RequestRangeBytes implements ReadStreamOptions {
|
||||
readonly start: number;
|
||||
readonly end: number;
|
||||
|
||||
constructor(start: number, end: number) {
|
||||
this.start = start;
|
||||
|
||||
@@ -29,4 +29,12 @@ export class RequestRangeService {
|
||||
|
||||
return new RequestRangeBytes(start, end);
|
||||
}
|
||||
|
||||
isRangeSatisfiable(requestRange: RequestRangeBytes, size: number): boolean {
|
||||
return (
|
||||
requestRange.start < size &&
|
||||
requestRange.end < size &&
|
||||
requestRange.end >= requestRange.start
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import RequestRangeBytes from '../RequestRange/request-range-bytes.class';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class ResponseHeadersService {
|
||||
createResponseHeaders(
|
||||
mimeType: string,
|
||||
length: number,
|
||||
requestRange: RequestRangeBytes | null,
|
||||
): Record<string, string> {
|
||||
const responseHeaders = {};
|
||||
responseHeaders['Content-Type'] = mimeType;
|
||||
responseHeaders['Accept-Ranges'] = 'bytes';
|
||||
responseHeaders['Cache-Control'] = 'no-cache';
|
||||
responseHeaders['Content-Length'] = length;
|
||||
|
||||
if (requestRange) {
|
||||
responseHeaders[
|
||||
'Content-Range'
|
||||
] = `bytes ${requestRange.start}-${requestRange.end}/${length}`;
|
||||
|
||||
responseHeaders['Content-Length'] =
|
||||
requestRange.end - requestRange.start + 1;
|
||||
}
|
||||
|
||||
return responseHeaders;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Response } from 'express';
|
||||
import { ReadStream } from 'fs';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class ResponseSenderService {
|
||||
send(
|
||||
response: Response,
|
||||
status: number,
|
||||
headers: Record<string, string>,
|
||||
readable: ReadStream | null = null,
|
||||
): void {
|
||||
response.writeHead(status, headers);
|
||||
if (!readable) {
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
readable.pipe(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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';
|
||||
import { ResponseSenderService } from '../ResponseSender/response-sender.service';
|
||||
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,
|
||||
) {}
|
||||
stream(
|
||||
req: Request,
|
||||
res: Response,
|
||||
streamCreatable: ReadStreamCreatable,
|
||||
): void {
|
||||
const size = streamCreatable.length;
|
||||
const requestRange = this.getRange(req, size);
|
||||
|
||||
if (!this.requestRangeService.isRangeSatisfiable(requestRange, size)) {
|
||||
return this.handleUnsatisfiable(size, res);
|
||||
}
|
||||
|
||||
const responseHeaders = this.responseHeadersService.createResponseHeaders(
|
||||
streamCreatable.mimeType,
|
||||
size,
|
||||
requestRange,
|
||||
);
|
||||
|
||||
const status = req.headers.range ? 206 : 200;
|
||||
const readable =
|
||||
req.method === 'HEAD'
|
||||
? null
|
||||
: streamCreatable.createReadStream(requestRange);
|
||||
|
||||
return this.responseSender.send(res, status, responseHeaders, readable);
|
||||
}
|
||||
|
||||
private getRange(req: Request, size: number): RequestRangeBytes {
|
||||
return this.requestRangeService.getRangeFromHeaders(req, size);
|
||||
}
|
||||
|
||||
private handleUnsatisfiable(size: number, res: Response) {
|
||||
const responseHeaders = { 'Content-Range': 'bytes */' + size };
|
||||
this.responseSender.send(res, 416, responseHeaders, null);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { StreamerService } from './streamer.service';
|
||||
import { FileStreamerService } from './StreamerService/file-streamer.service';
|
||||
|
||||
@Injectable()
|
||||
export class StreamerFacade {
|
||||
constructor(private readonly streamerService: StreamerService) {}
|
||||
constructor(private readonly fileStreamerService: FileStreamerService) {}
|
||||
|
||||
streamFile(request: Request, response: Response, filePath: string): void {
|
||||
this.streamerService.streamFile(request, response, filePath);
|
||||
async streamFile(
|
||||
request: Request,
|
||||
response: Response,
|
||||
filePath: string,
|
||||
): Promise<void> {
|
||||
return this.fileStreamerService.streamFile(request, response, filePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,19 @@ import { Module } from '@nestjs/common';
|
||||
import { StreamerFacade } from './streamer.facade';
|
||||
import { MimeService } from './Mime/mime.service';
|
||||
import { RequestRangeService } from './RequestRange/request-range.service';
|
||||
import { StreamerService } from './streamer.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';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [
|
||||
MimeService,
|
||||
RequestRangeService,
|
||||
ResponseHeadersService,
|
||||
ResponseSenderService,
|
||||
FileStreamerService,
|
||||
StreamerService,
|
||||
StreamerFacade,
|
||||
],
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { MimeService } from './Mime/mime.service';
|
||||
import { RequestRangeService } from './RequestRange/request-range.service';
|
||||
import { stat } from 'fs/promises';
|
||||
import { createReadStream, ReadStream, Stats } from 'fs';
|
||||
import RequestRangeBytes from './RequestRange/request-range-bytes.class';
|
||||
|
||||
@Injectable()
|
||||
export class StreamerService {
|
||||
constructor(
|
||||
readonly mimeService: MimeService,
|
||||
readonly requestRangeService: RequestRangeService,
|
||||
) {}
|
||||
|
||||
async streamFile(
|
||||
request: Request,
|
||||
response: Response,
|
||||
filePath: string,
|
||||
): Promise<void> {
|
||||
const stats: Stats | false = await stat(filePath).catch(() => false);
|
||||
if (stats === false) {
|
||||
this.sendResponse(response, 404, {});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const requestRange = this.requestRangeService.getRangeFromHeaders(
|
||||
request,
|
||||
stats.size,
|
||||
);
|
||||
console.log(requestRange, request.method);
|
||||
|
||||
if (!this.isRangeSatisfiable(requestRange, stats.size)) {
|
||||
const responseHeaders = { 'Content-Range': 'bytes */' + stats.size };
|
||||
this.sendResponse(response, 416, responseHeaders, null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const responseHeaders = this.createResponseHeaders(
|
||||
filePath,
|
||||
stats.size,
|
||||
requestRange,
|
||||
);
|
||||
|
||||
if (request.method == 'HEAD') {
|
||||
this.sendResponse(
|
||||
response,
|
||||
request.headers.range ? 206 : 200,
|
||||
responseHeaders,
|
||||
null,
|
||||
);
|
||||
} else {
|
||||
this.sendResponse(
|
||||
response,
|
||||
request.headers.range ? 206 : 200,
|
||||
responseHeaders,
|
||||
createReadStream(filePath, requestRange),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private createResponseHeaders(
|
||||
filePath: string,
|
||||
contentSize: number,
|
||||
requestRange: RequestRangeBytes | null,
|
||||
): Record<string, string> {
|
||||
const responseHeaders = {};
|
||||
responseHeaders['Content-Type'] = this.mimeService.getMime(filePath);
|
||||
responseHeaders['Accept-Ranges'] = 'bytes';
|
||||
responseHeaders['Cache-Control'] = 'no-cache';
|
||||
responseHeaders['Content-Length'] = contentSize;
|
||||
|
||||
if (requestRange) {
|
||||
responseHeaders[
|
||||
'Content-Range'
|
||||
] = `bytes ${requestRange.start}-${requestRange.end}/${contentSize}`;
|
||||
|
||||
responseHeaders['Content-Length'] =
|
||||
requestRange.end - requestRange.start + 1;
|
||||
}
|
||||
|
||||
return responseHeaders;
|
||||
}
|
||||
|
||||
private sendResponse(
|
||||
response: Response,
|
||||
status: number,
|
||||
headers: Record<string, string>,
|
||||
readable: ReadStream | null = null,
|
||||
): void {
|
||||
response.writeHead(status, headers);
|
||||
if (!readable) {
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
readable.pipe(response);
|
||||
}
|
||||
|
||||
private isRangeSatisfiable(
|
||||
requestRange: RequestRangeBytes,
|
||||
fileSize: number,
|
||||
): boolean {
|
||||
return (
|
||||
requestRange.start < fileSize &&
|
||||
requestRange.end < fileSize &&
|
||||
requestRange.end >= requestRange.start
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user