add migrations (package, config, migrations), add exceptions filter for kodi, start VideoFiles module

This commit is contained in:
Jurgis Sakalauskas
2023-01-03 10:58:33 +02:00
parent 2fb78b61c4
commit 9cf70326af
27 changed files with 295 additions and 69 deletions
+29
View File
@@ -0,0 +1,29 @@
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
} from '@nestjs/common';
import { Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
//const request = ctx.getRequest<Request>();
const status = exception.getStatus();
// 200 response is bad practice,
// but currently it is the only way KODi will show what happened
response.status(200).json({
// original response:
response: exception.getResponse(),
// for KODI:
notification: status + ': ' + exception.message,
refreshContainer: false,
updateList: false,
// msgBoxOK: status + ': ' + exception.message,
});
}
}
+1 -1
View File
@@ -1,8 +1,8 @@
import { Controller, Get, Head, Param, Req, Res } from '@nestjs/common';
import ApiResponse from '../Dto/api-response.dto';
import { AllFilesService } from './all-files.service';
import { TitleTypeEnum } from './Enum/title-type.enum';
import { Request, Response } from 'express';
import { TitleTypeEnum } from '../../VideoFiles/Enum/title-type.enum';
@Controller('api/all')
export class AllFilesController {
+2 -7
View File
@@ -2,16 +2,11 @@ import { Module } from '@nestjs/common';
import { AllFilesController } from './all-files.controller';
import { AllFilesService } from './all-files.service';
import { KodiApiResponseFactory } from '../kodi-api-response.factory';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TitleEntity } from './Entity/title.entity';
import { FileEntity } from './Entity/file.entity';
import { StreamerModule } from '../../Streamer/streamer.module';
import { VideoFilesModule } from '../../VideoFiles/video-files.module';
@Module({
imports: [
TypeOrmModule.forFeature([TitleEntity, FileEntity]),
StreamerModule,
],
imports: [StreamerModule, VideoFilesModule],
controllers: [AllFilesController],
providers: [AllFilesService, KodiApiResponseFactory],
})
+23 -26
View File
@@ -1,23 +1,19 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import ApiResponse from '../Dto/api-response.dto';
import { KodiApiResponseFactory } from '../kodi-api-response.factory';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { TitleEntity } from './Entity/title.entity';
import { FileEntity } from './Entity/file.entity';
import { TitleTypeEnum } from './Enum/title-type.enum';
import { StreamerFacade } from '../../Streamer/streamer.facade';
import { Request, Response } from 'express';
import { VideoFilesProvider } from '../../VideoFiles/video-files.provider';
import { TitleTypeEnum } from '../../VideoFiles/Enum/title-type.enum';
import { FileEntity } from '../../VideoFiles/Entity/file.entity';
import { TitleEntity } from '../../VideoFiles/Entity/title.entity';
@Injectable()
export class AllFilesService {
constructor(
private readonly koiApiResponseFactory: KodiApiResponseFactory,
@InjectRepository(TitleEntity)
private titleRepository: Repository<TitleEntity>,
@InjectRepository(FileEntity)
private fileRepository: Repository<FileEntity>,
private readonly streamerFacade: StreamerFacade,
private readonly videoTitlesProvider: VideoFilesProvider,
) {}
getMenu(): ApiResponse {
@@ -37,13 +33,7 @@ export class AllFilesService {
}
async getListOfTitles(type: TitleTypeEnum): Promise<ApiResponse> {
const entities = await this.titleRepository
.createQueryBuilder()
.setFindOptions({ relations: { files: true } })
.where('type = :type', { type: type })
.andWhere('deleted = :deleted', { deleted: false })
.getMany();
const entities = await this.videoTitlesProvider.getListOfTitles(type);
const response = this.koiApiResponseFactory.createApiResponse();
response.setTitle('All ' + type + 's');
entities.forEach((entity) => {
@@ -51,7 +41,9 @@ export class AllFilesService {
.createItem()
.setLabel(entity.title)
.setToFolder()
.setPlot(entity.files.length + ' files')
.setPlot(
entity.files.length + (entity.files.length > 1 ? ' files' : ' file'),
)
.setPath('all/' + entity.id);
});
@@ -59,15 +51,15 @@ export class AllFilesService {
}
async loadTitle(titleId: number, seasonId: number): Promise<ApiResponse> {
const titleEntity = await this.titleRepository.findOne({
relations: { files: true },
where: { id: titleId },
});
const titleEntity = titleId
? await this.videoTitlesProvider.getTitleWithFiles(titleId)
: null;
if (titleEntity === null) {
//todo - alert instead of full api response
return this.koiApiResponseFactory
.createApiResponse()
.setTitle('Not found ' + titleId);
.setTitle('Not found' + (titleId ? ' ' + titleId : ''));
}
if (titleEntity.type === TitleTypeEnum.movie) {
@@ -112,10 +104,13 @@ export class AllFilesService {
//todo - add num files count
const seasons = [];
const filesCount = {};
for (const fileEntity of titleEntity.files) {
if (seasons.indexOf(fileEntity.season) === -1) {
seasons.push(fileEntity.season);
filesCount[fileEntity.season] = 0;
}
filesCount[fileEntity.season]++;
}
for (const season of seasons) {
@@ -123,6 +118,9 @@ export class AllFilesService {
.createItem()
.setLabel(season + ' Sezonas')
.setToFolder()
.setPlot(
filesCount[season] + (filesCount[season] > 1 ? ' files' : ' file'),
)
.setPath(`all/${titleEntity.id}/${season}`);
}
@@ -138,13 +136,12 @@ export class AllFilesService {
if (isNaN(id)) {
throw new NotFoundException(`id #${fileId} not correct`);
}
const filePath = await this.videoTitlesProvider.getFilePath(id);
const entity = await this.fileRepository.findOne({ where: { id: id } });
if (entity === null) {
if (filePath === null) {
throw new NotFoundException(`id #${fileId} not found`);
}
return this.streamerFacade.streamFile(request, response, entity.path);
return this.streamerFacade.streamFile(request, response, filePath);
}
}
+4 -4
View File
@@ -4,10 +4,10 @@ export default class ApiResponse {
items: Array<ResponseItem> = [];
content = 'videos';
updateList = true;
category: string;
play: string;
msgBoxOK: string;
nosort: boolean;
category: string | undefined;
play: string | undefined;
msgBoxOK: string | undefined;
nosort: boolean | undefined;
setTitle(title) {
this.category = title;
@@ -25,6 +25,7 @@ export class LrtApiSearchClient {
const searchResponse: SearchResponseInterface = resp.data;
const cats = [];
const loadedCategories = new Set();
//todo - move to mapper
const responseDto = new SearchResponseDto(
@@ -36,6 +37,9 @@ export class LrtApiSearchClient {
if (cats.indexOf(searchItem.category_title) > -1) {
continue;
}
if (loadedCategories.has(searchItem.category_title)) {
continue;
}
const itemDto = new SearchResponseItemDto();
itemDto.label = searchItem.category_title;
itemDto.thumb =
@@ -46,6 +50,7 @@ export class LrtApiSearchClient {
itemDto.categoryId = await this.findCategoryId(searchItem.category_url);
cats.push(searchItem.category_title);
loadedCategories.add(searchItem.category_title);
responseDto.addItem(itemDto);
}
@@ -83,7 +88,6 @@ export class LrtApiSearchClient {
}
private async findCategoryIdInLrt(categoryUrl: string): Promise<string> {
//todo - cache to database
const url = 'https://www.lrt.lt' + categoryUrl;
const resp = await firstValueFrom(this.httpService.get(url));
const body: string = resp.data;
@@ -1,6 +1,6 @@
import { Column, Entity, PrimaryColumn } from 'typeorm';
@Entity()
@Entity('lrt_categories')
export class LrtCategory {
@PrimaryColumn({ name: 'category_url' })
categoryUrl: string;
+19
View File
@@ -0,0 +1,19 @@
import { StaticService } from './static.service';
describe('StaticService', () => {
let staticService: StaticService;
beforeEach(() => {
staticService = new StaticService();
});
describe('provideDirIndex', () => {
it('should return file/dir list of dir', async () => {
jest.spyOn(staticService, 'provideDirIndex');
process.env.STATIC_SERVE_FOLDER = 'test/Static/_test-data/StaticServe';
const result = await staticService.provideDirIndex('');
expect(result).toHaveLength(2);
});
});
});
+2 -4
View File
@@ -1,11 +1,9 @@
import { Controller, Get } from '@nestjs/common';
import { EntityManager } from 'typeorm';
@Controller('try')
export class TryController {
constructor(private orm: EntityManager) {}
@Get('')
async main() {
return await this.orm.query('select * from aliases');
main() {
return process.env;
}
}
-1
View File
@@ -1,5 +1,4 @@
import { Module } from '@nestjs/common';
import { TryController } from './try.cotroller';
/**
@@ -15,9 +15,6 @@ export class FileEntity {
@Column()
size: number;
@Column({ name: 'relative_path' })
relativePath: string;
@Column({ name: 'file_name' })
fileName: string;
+13
View File
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TitleEntity } from './Entity/title.entity';
import { FileEntity } from './Entity/file.entity';
import { VideoFilesProvider } from './video-files.provider';
@Module({
imports: [TypeOrmModule.forFeature([TitleEntity, FileEntity])],
controllers: [],
providers: [VideoFilesProvider],
exports: [VideoFilesProvider],
})
export class VideoFilesModule {}
+46
View File
@@ -0,0 +1,46 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { TitleEntity } from './Entity/title.entity';
import { TitleTypeEnum } from './Enum/title-type.enum';
import { FileEntity } from './Entity/file.entity';
@Injectable()
export class VideoFilesProvider {
constructor(
@InjectRepository(TitleEntity)
private readonly titleRepository: Repository<TitleEntity>,
@InjectRepository(FileEntity)
private readonly fileRepository: Repository<FileEntity>,
) {}
async getListOfTitles(type: TitleTypeEnum): Promise<TitleEntity[]> {
return this.titleRepository
.createQueryBuilder()
.setFindOptions({ relations: { files: true } })
.where('type = :type', { type: type })
.andWhere('deleted = :deleted', { deleted: false })
.getMany();
}
async getTitleWithFiles(titleId: number): Promise<TitleEntity | null> {
return this.titleRepository
.findOne({
relations: { files: true },
where: { id: titleId },
})
.catch(() => null);
}
async getFilePath(fileId: number): Promise<string | null> {
const entity = await this.fileRepository
.findOne({ where: { id: fileId } })
.catch(() => null);
if (entity === null) {
return null;
}
return entity.path;
}
}
+19 -13
View File
@@ -7,27 +7,33 @@ export class PathsConfig {
}
public getStaticRequestsLogPath(): string {
return join(
this.getRootPath(),
this.configService.getEnv('LOG_FILE_REQUESTS'),
);
return this.getPathByEnvKey('LOG_FILE_REQUESTS');
}
public getStaticFolder(): string {
return join(
this.getRootPath(),
this.configService.getEnv('STATIC_SERVE_FOLDER'),
);
return this.getPathByEnvKey('STATIC_SERVE_FOLDER');
}
public getRecentSearchesFolder(): string {
return join(
this.getRootPath(),
this.configService.getEnv('RECENT_SEARCHES_FOLDER'),
);
return this.getPathByEnvKey('RECENT_SEARCHES_FOLDER');
}
private getPathByEnvKey(key: string) {
const relPath = this.configService.getEnv(key);
return this.getPath(relPath);
}
private getPath(relPath: string) {
if (relPath[0] === '/') {
//this is an absolute path
return relPath;
}
return join(this.getRootPath(), relPath);
}
private getRootPath(): string {
return join(__dirname, '../..');
return this.configService.getEnv('PWD');
}
}
+11
View File
@@ -0,0 +1,11 @@
import { DataSource, DataSourceOptions } from 'typeorm';
import { configService } from './config.service';
/**
* config for typeorm cli
*/
export default new DataSource(<DataSourceOptions>{
type: configService.getTypeOrmConfig().type,
database: configService.getTypeOrmConfig().database,
migrations: ['migrations/*{.ts,.js}'],
});
+2 -2
View File
@@ -1,8 +1,8 @@
import { LrtCategory } from '../KodiApi/LRT/LrtApiClient/Entity/lrt-category.entity';
import { TitleEntity } from '../KodiApi/AllFiles/Entity/title.entity';
import { FileEntity } from '../KodiApi/AllFiles/Entity/file.entity';
import { ConfigService } from './config.service';
import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type';
import { TitleEntity } from '../VideoFiles/Entity/title.entity';
import { FileEntity } from '../VideoFiles/Entity/file.entity';
export class TypeOrmConfig {
constructor(configService: ConfigService) {
+2
View File
@@ -1,9 +1,11 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { configService } from './config/config.service';
import { HttpExceptionFilter } from './Exception/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(configService.getPort());
}