mirror of
https://github.com/sakaljurgis/kodi-api-server.git
synced 2026-07-08 20:37:41 +00:00
move aliases to file storage
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
export interface DataStorageServiceInterface {
|
||||
get<T>(key: string, fallback: T): Promise<T>;
|
||||
set(key: string, data: unknown): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JsonFileDataStorageService } from './json-file-data-storage.service';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [],
|
||||
providers: [JsonFileDataStorageService],
|
||||
exports: [JsonFileDataStorageService],
|
||||
})
|
||||
export class DataStorageModule {}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { join } from 'path';
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import { configService, ConfigService } from '../config/config.service';
|
||||
import { fileExistsSync } from 'tsconfig-paths/lib/filesystem';
|
||||
import { DataStorageServiceInterface } from './data-storage-service.interface';
|
||||
|
||||
/**
|
||||
* This service is used to store data in json files.
|
||||
* It is cached in memory to save on readFile calls.
|
||||
* Use set(cache = false) to skip caching for large files.
|
||||
*/
|
||||
@Injectable()
|
||||
export class JsonFileDataStorageService implements DataStorageServiceInterface {
|
||||
private readonly data: Record<string, unknown> = {};
|
||||
private readonly config: ConfigService;
|
||||
constructor() {
|
||||
this.config = configService;
|
||||
}
|
||||
|
||||
async get<T>(key: string, fallback: T): Promise<T> {
|
||||
if (this.data[key]) {
|
||||
return this.data[key] as T;
|
||||
}
|
||||
|
||||
const filePath = this.getFilePath(key);
|
||||
if (fileExistsSync(filePath)) {
|
||||
try {
|
||||
const rawData = await readFile(filePath);
|
||||
const data = JSON.parse(rawData.toString());
|
||||
this.data[key] = data;
|
||||
|
||||
return data as T;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
async set(key: string, data: unknown, cache = true): Promise<void> {
|
||||
if (cache) {
|
||||
this.data[key] = data;
|
||||
}
|
||||
|
||||
const filePath = this.getFilePath(key);
|
||||
await writeFile(filePath, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
private getFilePath(key: string): string {
|
||||
return join(this.config.getPaths().getDbFolderPath(), key + '.json');
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ export class KodiApiInterfaceController {
|
||||
@Get()
|
||||
@Header('content-type', 'text/html')
|
||||
async main(@Res() res: Response) {
|
||||
//todo - make the views directory global so no need to edit nest-cli.json every time
|
||||
const readable = createReadStream(join(__dirname, './View/if.html'));
|
||||
readable.pipe(res);
|
||||
}
|
||||
|
||||
@@ -4,13 +4,17 @@ import { FileEntityExpanderInterface } from './file-entity-expander.interface';
|
||||
import { FileEntity } from 'src/Shared/Entity/file.entity';
|
||||
import { VideoFilesUpdateRepository } from '../video-files-update.repository';
|
||||
import { TitleTypeEnum } from '../../../Shared/Enum/title-type.enum';
|
||||
import { JsonFileDataStorageService } from '../../../DataStorage/json-file-data-storage.service';
|
||||
|
||||
//todo - refactor this copy-paste, add types, ect.
|
||||
@Injectable()
|
||||
export class FileEntityTitleInfoExpander
|
||||
implements FileEntityExpanderInterface
|
||||
{
|
||||
constructor(private readonly repository: VideoFilesUpdateRepository) {
|
||||
constructor(
|
||||
private readonly repository: VideoFilesUpdateRepository,
|
||||
private readonly jsonFileDataStorageService: JsonFileDataStorageService,
|
||||
) {
|
||||
tnp.configure({ sample: /sample/i }, { sample: 'boolean' });
|
||||
tnp.configure({ year: /([\[\(]?((?:19[0-9][0-9]|20[0-2][0-9]))[\]\)]?)/ });
|
||||
tnp.configure({ container: /MKV|AVI|MP4|mkv|avi|mp4/i });
|
||||
@@ -23,7 +27,7 @@ export class FileEntityTitleInfoExpander
|
||||
fileEntity.infos = JSON.stringify(titleInfo.infos);
|
||||
fileEntity.season = titleInfo.info.season;
|
||||
fileEntity.title = await this.repository.findOrCreateTitle(
|
||||
titleInfo.info.title,
|
||||
await this.checkForAliases(titleInfo.info.title),
|
||||
titleInfo.info.season ? TitleTypeEnum.show : TitleTypeEnum.movie,
|
||||
);
|
||||
|
||||
@@ -94,7 +98,6 @@ export class FileEntityTitleInfoExpander
|
||||
}
|
||||
|
||||
combined.title = this.removePartNumberFromTitle(combined.title);
|
||||
combined.title = this.checkForAliases(combined.title);
|
||||
return combined;
|
||||
}
|
||||
|
||||
@@ -111,23 +114,17 @@ export class FileEntityTitleInfoExpander
|
||||
return /^\d+$/.test(str);
|
||||
}
|
||||
|
||||
private checkForAliases(title) {
|
||||
//todo - db or external file
|
||||
const aliases = [
|
||||
['spider man', 'Spider Man'],
|
||||
['spiderman', 'Spider Man'],
|
||||
['spider-man', 'Spider Man'],
|
||||
['madagascar', 'Madagascar'],
|
||||
['ice age', 'Ice Age'],
|
||||
['hotel transylvania', 'Hotel Transylvania'],
|
||||
['despicable me', 'Despicable Me'],
|
||||
['the matrix', 'The Matrix'],
|
||||
['frozen', 'Frozen'],
|
||||
['garfield', 'Garfield'],
|
||||
['harry potter', 'Harry Potter'],
|
||||
['fifty shades', 'Fifty Shades'],
|
||||
['lilo', 'Lilo and Stitch'],
|
||||
];
|
||||
/**
|
||||
* Check if title matches any of the aliases and return the alias if it does
|
||||
* @param title
|
||||
* @private
|
||||
*/
|
||||
private async checkForAliases(title) {
|
||||
type Alias = [string, string];
|
||||
const aliases: Alias[] = await this.jsonFileDataStorageService.get<Alias[]>(
|
||||
'aliases',
|
||||
[],
|
||||
);
|
||||
|
||||
for (let i = 0; i < aliases.length; i++) {
|
||||
const [pattern, alias] = aliases[i];
|
||||
|
||||
@@ -11,7 +11,7 @@ export class VideoFilesScannerService {
|
||||
this.config = configService.getVideoFilesConfig();
|
||||
}
|
||||
|
||||
public async scanNewFiles(): Promise<
|
||||
public async scanFsForVideoFiles(): Promise<
|
||||
[paths: string[], pathToRelPath: Record<string, string>]
|
||||
> {
|
||||
let filesInFs = [];
|
||||
|
||||
@@ -15,6 +15,7 @@ import { FileEntityDefaultsExpander } from './Expander/file-entity-defaults.expa
|
||||
import { FileEntityDurationExpander } from './Expander/file-entity-duration.expander';
|
||||
import { FileEntityTransmissionExpander } from './Expander/file-entity-transmission.expander';
|
||||
import { TorrentSeedClientModule } from '../../Torrent/SeedClient/torrent-seed-client.module';
|
||||
import { DataStorageModule } from '../../DataStorage/data-storage.module';
|
||||
|
||||
const fileEntityExpanders: Provider<any>[] = [
|
||||
FileEntityExpander,
|
||||
@@ -41,6 +42,7 @@ const fileEntityExpanders: Provider<any>[] = [
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([TitleEntity, FileEntity]),
|
||||
TorrentSeedClientModule,
|
||||
DataStorageModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [
|
||||
|
||||
@@ -18,17 +18,27 @@ export class VideoFilesUpdateService {
|
||||
private readonly expander: FileEntityExpander,
|
||||
) {}
|
||||
|
||||
async updateFsVideoFiles(): Promise<void> {
|
||||
const [filesInFs, filesToRelativePaths] = await this.scanner.scanNewFiles();
|
||||
/**
|
||||
* Scan for new files and remove deleted.
|
||||
* Refresh is used to force a refresh of all files
|
||||
* i.e. existing files db will be parsed on new rules (e.g. new aliases)
|
||||
* @param refresh
|
||||
*/
|
||||
async updateFsVideoFiles(refresh = false): Promise<void> {
|
||||
const [filesInFs, filesToRelativePaths] =
|
||||
await this.scanner.scanFsForVideoFiles();
|
||||
|
||||
await this.repository.markRemovedFilesAsDeleted(filesInFs);
|
||||
|
||||
const filesInDb = await this.repository.getMatchingPaths(filesInFs);
|
||||
const filesToAddToDb = filesInFs.filter(
|
||||
(filePath) =>
|
||||
!filesInDb.includes(filePath) && !this.isPathToIgnore(filePath),
|
||||
);
|
||||
let filesToAddToDb: string[] = filesInFs;
|
||||
|
||||
if (!refresh) {
|
||||
const filesInDb = await this.repository.getMatchingPaths(filesInFs);
|
||||
filesToAddToDb = filesInFs.filter(
|
||||
(filePath) =>
|
||||
!filesInDb.includes(filePath) && !this.isPathToIgnore(filePath),
|
||||
);
|
||||
}
|
||||
const entitiesToSave = [];
|
||||
|
||||
for (const fileToAddToDb of filesToAddToDb) {
|
||||
@@ -38,6 +48,7 @@ export class VideoFilesUpdateService {
|
||||
);
|
||||
|
||||
//check if download is complete
|
||||
//todo - figure out if this makes sense
|
||||
if (
|
||||
fileEntity.streamProvider === StreamProviderEnum.wt &&
|
||||
fileEntity.torrent &&
|
||||
|
||||
@@ -8,8 +8,8 @@ import { RecentSearchesConfig } from './recent-searches.config';
|
||||
env.config();
|
||||
|
||||
export class ConfigService {
|
||||
private typeOrmConfig: TypeOrmConfig = new TypeOrmConfig(this);
|
||||
private pathsConfig: PathsConfig = new PathsConfig(this);
|
||||
private typeOrmConfig: TypeOrmConfig = new TypeOrmConfig(this);
|
||||
private videoFilesConfig: VideoFilesConfig = new VideoFilesConfig(this);
|
||||
private recentSearches: RecentSearchesConfig = new RecentSearchesConfig(this);
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ export class PathsConfig {
|
||||
return this.getPath(relPath);
|
||||
}
|
||||
|
||||
public getDbFolderPath(): string {
|
||||
return '/srv/data/db';
|
||||
}
|
||||
|
||||
private getPath(relPath: string) {
|
||||
if (relPath[0] === '/') {
|
||||
//this is an absolute path
|
||||
|
||||
@@ -9,7 +9,10 @@ import { join } from 'path';
|
||||
export class TypeOrmConfig {
|
||||
constructor(configService: ConfigService) {
|
||||
this.type = 'sqlite';
|
||||
this.database = join('/srv/data/db', configService.getEnv('DB_FILENAME'));
|
||||
this.database = join(
|
||||
configService.getPaths().getDbFolderPath(),
|
||||
configService.getEnv('DB_FILENAME'),
|
||||
);
|
||||
this.entities = [LrtCategory, TitleEntity, FileEntity, TorrentEntity];
|
||||
}
|
||||
type: 'sqlite';
|
||||
|
||||
Reference in New Issue
Block a user