mirror of
https://github.com/sakaljurgis/kodi-api-server.git
synced 2026-07-09 04:47:41 +00:00
start with streamer
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { extname } from 'path';
|
||||
|
||||
@Injectable()
|
||||
export class MimeService {
|
||||
private mimeNames = {
|
||||
'.css': 'text/css',
|
||||
'.html': 'text/html',
|
||||
'.js': 'application/javascript',
|
||||
'.mp3': 'audio/mpeg',
|
||||
'.mp4': 'video/mp4',
|
||||
'.ogg': 'application/ogg',
|
||||
'.ogv': 'video/ogg',
|
||||
'.oga': 'audio/ogg',
|
||||
'.txt': 'text/plain',
|
||||
'.wav': 'audio/x-wav',
|
||||
'.webm': 'video/webm',
|
||||
'.mkv': 'video/x-matroska',
|
||||
'.m3u': 'application/vnd.apple.mpegurl',
|
||||
'.ts': 'video/MP2T',
|
||||
'.json': 'application/json',
|
||||
'.avi': 'video/x-msvideo',
|
||||
};
|
||||
|
||||
getMime(filePath: string): string {
|
||||
return this.getMimeNameFromExt(extname(filePath));
|
||||
}
|
||||
|
||||
private getMimeNameFromExt(ext): string {
|
||||
return this.mimeNames[ext.toLowerCase()] ?? 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export default class RequestRangeBytes {
|
||||
readonly end: number;
|
||||
readonly start: number;
|
||||
|
||||
constructor(start: number, end: number) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
createParamDecorator,
|
||||
ExecutionContext,
|
||||
HttpException,
|
||||
} from '@nestjs/common';
|
||||
import RequestRangeBytes from './request-range-bytes.class';
|
||||
|
||||
export const RequestRange = createParamDecorator(
|
||||
(data: string, ctx: ExecutionContext): RequestRangeBytes | null => {
|
||||
const headers = ctx.switchToHttp().getRequest().headers;
|
||||
|
||||
if (headers['range']) {
|
||||
throw new HttpException('not implemented', 501);
|
||||
}
|
||||
|
||||
return new RequestRangeBytes(null, null);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import RequestRangeBytes from './request-range-bytes.class';
|
||||
import { Request } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class RequestRangeService {
|
||||
getRangeFromHeaders(request: Request, fileSize: number): RequestRangeBytes {
|
||||
const range = request.headers['range'];
|
||||
if (!range || range.length === 0) {
|
||||
return new RequestRangeBytes(0, fileSize - 1);
|
||||
}
|
||||
|
||||
const rangeArray = range.split(/bytes=([0-9]*)-([0-9]*)/);
|
||||
|
||||
const start = parseInt(rangeArray[1]);
|
||||
const end = parseInt(rangeArray[2]);
|
||||
|
||||
if (isNaN(start) && isNaN(end)) {
|
||||
return new RequestRangeBytes(0, fileSize - 1);
|
||||
}
|
||||
|
||||
if (!isNaN(start) && isNaN(end)) {
|
||||
return new RequestRangeBytes(start, fileSize - 1);
|
||||
}
|
||||
|
||||
if (isNaN(start) && !isNaN(end)) {
|
||||
return new RequestRangeBytes(fileSize - end, fileSize - 1);
|
||||
}
|
||||
|
||||
return new RequestRangeBytes(start, end);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { StreamerService } from './streamer.service';
|
||||
|
||||
@Injectable()
|
||||
export class StreamerFacade {
|
||||
constructor(private readonly streamerService: StreamerService) {}
|
||||
|
||||
streamFile(request: Request, response: Response, filePath: string): void {
|
||||
this.streamerService.streamFile(request, response, filePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [
|
||||
MimeService,
|
||||
RequestRangeService,
|
||||
StreamerService,
|
||||
StreamerFacade,
|
||||
],
|
||||
exports: [StreamerFacade],
|
||||
})
|
||||
export class StreamerModule {}
|
||||
@@ -0,0 +1,110 @@
|
||||
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) {
|
||||
throw new NotFoundException('file not found');
|
||||
}
|
||||
|
||||
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