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,
|
@Param('fileId') fileId: string,
|
||||||
@Req() request: Request,
|
@Req() request: Request,
|
||||||
@Res() response: Response,
|
@Res() response: Response,
|
||||||
) {
|
): Promise<void> {
|
||||||
this.allFilesService.play(fileId, request, response);
|
return this.allFilesService.play(fileId, request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':titleId/:seasonId?')
|
@Get(':titleId/:seasonId?')
|
||||||
|
|||||||
@@ -135,17 +135,16 @@ export class AllFilesService {
|
|||||||
response: Response,
|
response: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const id = parseInt(fileId);
|
const id = parseInt(fileId);
|
||||||
|
if (isNaN(id)) {
|
||||||
|
throw new NotFoundException(`id #${fileId} not correct`);
|
||||||
|
}
|
||||||
|
|
||||||
const entity = await this.fileRepository.findOne({ where: { id: id } });
|
const entity = await this.fileRepository.findOne({ where: { id: id } });
|
||||||
|
|
||||||
if (entity === null) {
|
if (entity === null) {
|
||||||
response
|
throw new NotFoundException(`id #${fileId} not found`);
|
||||||
.status(404)
|
|
||||||
.send('entity not found id: ' + fileId)
|
|
||||||
.end();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
import { ReadStreamOptions } from '../Interface/read-stream-options.interface';
|
||||||
readonly end: number;
|
|
||||||
|
export default class RequestRangeBytes implements ReadStreamOptions {
|
||||||
readonly start: number;
|
readonly start: number;
|
||||||
|
readonly end: number;
|
||||||
|
|
||||||
constructor(start: number, end: number) {
|
constructor(start: number, end: number) {
|
||||||
this.start = start;
|
this.start = start;
|
||||||
|
|||||||
@@ -29,4 +29,12 @@ export class RequestRangeService {
|
|||||||
|
|
||||||
return new RequestRangeBytes(start, end);
|
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 { Injectable } from '@nestjs/common';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { StreamerService } from './streamer.service';
|
import { FileStreamerService } from './StreamerService/file-streamer.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StreamerFacade {
|
export class StreamerFacade {
|
||||||
constructor(private readonly streamerService: StreamerService) {}
|
constructor(private readonly fileStreamerService: FileStreamerService) {}
|
||||||
|
|
||||||
streamFile(request: Request, response: Response, filePath: string): void {
|
async streamFile(
|
||||||
this.streamerService.streamFile(request, response, filePath);
|
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 { 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 './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({
|
@Module({
|
||||||
imports: [],
|
imports: [],
|
||||||
providers: [
|
providers: [
|
||||||
MimeService,
|
MimeService,
|
||||||
RequestRangeService,
|
RequestRangeService,
|
||||||
|
ResponseHeadersService,
|
||||||
|
ResponseSenderService,
|
||||||
|
FileStreamerService,
|
||||||
StreamerService,
|
StreamerService,
|
||||||
StreamerFacade,
|
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