migrations; torr search, dl, seed, delete; files scan, delete; configs

This commit is contained in:
Jurgis Sakalauskas
2023-02-03 17:41:40 +02:00
parent 8d354027fc
commit daaeda539b
84 changed files with 5456 additions and 301 deletions
+14 -2
View File
@@ -2,7 +2,9 @@ import { Controller, Get, Head, Param, Req, Res } from '@nestjs/common';
import ApiResponse from '../Dto/api-response.dto';
import { AllFilesService } from './all-files.service';
import { Request, Response } from 'express';
import { TitleTypeEnum } from '../../VideoFiles/Enum/title-type.enum';
import { TitleTypeEnum } from '../../Shared/Enum/title-type.enum';
import { KodiApiResponse } from '../Dto/kodi-api-response.type';
import NotificationResponse from '../Dto/notification-response.dto';
@Controller('api/all')
export class AllFilesController {
@@ -23,6 +25,16 @@ export class AllFilesController {
return this.allFilesService.getListOfTitles(TitleTypeEnum.movie);
}
@Get('scan')
scanNewFiles(): KodiApiResponse {
return this.allFilesService.scanNewFiles();
}
@Get('delete/:fileId')
async deleteFile(@Param('fileId') fileId: string): Promise<KodiApiResponse> {
return this.allFilesService.deleteFile(fileId);
}
@Get('play/:fileId')
@Head('play/:fileId')
play(
@@ -37,7 +49,7 @@ export class AllFilesController {
getTitle(
@Param('titleId') titleId: string, //actually a number, how to cast?
@Param('seasonId') seasonId: string, //actually a number, how to cast?
): Promise<ApiResponse> {
): Promise<KodiApiResponse> {
return this.allFilesService.loadTitle(
parseInt(titleId),
parseInt(seasonId),
+1
View File
@@ -9,5 +9,6 @@ import { VideoFilesModule } from '../../VideoFiles/video-files.module';
imports: [StreamerModule, VideoFilesModule],
controllers: [AllFilesController],
providers: [AllFilesService, KodiApiResponseFactory],
exports: [AllFilesService],
})
export class AllFilesModule {}
+92 -25
View File
@@ -4,9 +4,11 @@ import { KodiApiResponseFactory } from '../kodi-api-response.factory';
import { StreamerFacade } from '../../Streamer/streamer.facade';
import { Request, Response } from 'express';
import { VideoFilesFacade } from '../../VideoFiles/video-files.facade';
import { TitleTypeEnum } from '../../VideoFiles/Enum/title-type.enum';
import { FileEntity } from '../../VideoFiles/Entity/file.entity';
import { TitleEntity } from '../../VideoFiles/Entity/title.entity';
import { TitleTypeEnum } from '../../Shared/Enum/title-type.enum';
import { FileEntity } from '../../Shared/Entity/file.entity';
import { TitleEntity } from '../../Shared/Entity/title.entity';
import { KodiApiResponse } from '../Dto/kodi-api-response.type';
import NotificationResponse from '../Dto/notification-response.dto';
@Injectable()
export class AllFilesService {
@@ -17,19 +19,17 @@ export class AllFilesService {
) {}
getMenu(): ApiResponse {
const apiResponse = this.koiApiResponseFactory.createApiResponse();
apiResponse
.createItem()
.setLabel('Movies')
.setPath('all/movie')
.setToFolder();
apiResponse
.createItem()
.setLabel('Shows')
.setPath('all/show')
.setToFolder();
return apiResponse;
return this.koiApiResponseFactory
.createApiResponse()
.addNavigationItems(
[
['Movies', 'movie'],
['Shows', 'show'],
['Scan for new Files', 'scan'],
],
'all/',
)
.setNoSort();
}
async getListOfTitles(type: TitleTypeEnum): Promise<ApiResponse> {
@@ -50,16 +50,16 @@ export class AllFilesService {
return response;
}
async loadTitle(titleId: number, seasonId: number): Promise<ApiResponse> {
async loadTitle(titleId: number, seasonId: number): Promise<KodiApiResponse> {
const titleEntity = titleId
? await this.videoFilesFacade.getTitleWithFiles(titleId)
: null;
if (titleEntity === null) {
//todo - alert instead of full api response
return this.koiApiResponseFactory
.createApiResponse()
.setTitle('Not found' + (titleId ? ' ' + titleId : ''));
return this.koiApiResponseFactory.createNotificationResponse(
'Not found' + (titleId ? ' ' + titleId : ''),
false,
);
}
if (titleEntity.type === TitleTypeEnum.movie) {
@@ -78,21 +78,33 @@ export class AllFilesService {
return this.buildSeasonsResponse(titleEntity);
}
private buildFilesResponse(
buildFilesResponse(
title: string,
fileEntities: Array<FileEntity>,
torrentContext = false,
): ApiResponse {
const apiResponse = this.koiApiResponseFactory.createApiResponse();
apiResponse.setTitle(title);
for (const fileEntity of fileEntities) {
apiResponse
const item = apiResponse
.createItem()
.setLabel(fileEntity.fileName)
.setPath('/api?path=all/play/' + fileEntity.id)
.setToPlayable()
.setPlot('Size ' + fileEntity.size)
.setPlot(this.buildPlot(fileEntity))
.setSize(fileEntity.size)
.setDuration(fileEntity.duration);
if (!torrentContext) {
if (fileEntity.torrentId) {
item.addContextMenu(
'Show torrent',
'torr/torrent-context/' + fileEntity.torrentId,
);
} else {
item.addContextMenu('Delete file', 'all/delete/' + fileEntity.id);
}
}
}
return apiResponse;
@@ -102,7 +114,6 @@ export class AllFilesService {
const apiResponse = this.koiApiResponseFactory.createApiResponse();
apiResponse.setTitle(titleEntity.title);
//todo - add num files count
const seasons = [];
const filesCount = {};
for (const fileEntity of titleEntity.files) {
@@ -144,4 +155,60 @@ export class AllFilesService {
return this.streamerFacade.streamVideoFile(request, response, fileEntity);
}
getHRFileSize(size: number) {
// convert to human readable format.
const i: number = Math.floor(Math.log(size) / Math.log(1024));
const iPow: number = Math.pow(1024, i);
const sizePow: number = size / iPow;
return sizePow.toFixed(2) + ' ' + ['B', 'KB', 'MB', 'GB', 'TB'][i];
}
buildPlot(fileEntity: FileEntity): string {
const plot: string[] = [];
if (fileEntity.size) {
plot.push('Size ' + this.getHRFileSize(fileEntity.size));
}
const arrTorrProps: string[] = [];
if (fileEntity.linkomanija) {
arrTorrProps.push('LM');
}
if (fileEntity.transmissionId) {
arrTorrProps.push('transm');
}
if (fileEntity.torrentId) {
arrTorrProps.push('wt');
}
const torrProps = arrTorrProps.join(' + ');
if (torrProps) {
plot.push(torrProps);
}
if (fileEntity.progress) {
const progress = Math.round(fileEntity.progress * 1000) / 10;
const stopped = fileEntity.torrent.stopped ? ' Stopped' : '';
plot.push(`${progress}%${stopped}`);
}
return plot.join('\n');
}
public scanNewFiles(): NotificationResponse {
this.videoFilesFacade.updateFsVideoFiles();
return this.koiApiResponseFactory.createNotificationResponse(
'Scanning for new files and deleting old.',
false,
);
}
async deleteFile(fileId: string) {
const id = parseInt(fileId);
const result = await this.videoFilesFacade.deleteFile(id);
return new NotificationResponse(
result ? 'successfully deleted' : 'delete failed',
result,
);
}
}
+23
View File
@@ -1,9 +1,11 @@
import ResponseItem from './response-item.dto';
import { NavigationItem } from '../../Shared/Dto/navigation-item.dto';
export default class ApiResponse {
items: Array<ResponseItem> = [];
content = 'videos';
updateList = true;
selectList: boolean | undefined;
category: string | undefined;
play: string | undefined;
msgBoxOK: string | undefined;
@@ -40,4 +42,25 @@ export default class ApiResponse {
return this;
}
setShowAsSelectList() {
this.updateList = false;
this.selectList = true;
return this;
}
addNavigationItems(navigationItems: NavigationItem[], pathPrefix = '') {
for (const [label, path, searchFor] of navigationItems) {
const item = this.createItem()
.setLabel(label)
.setToFolder()
.setPath(pathPrefix + path ?? '');
if (searchFor !== undefined) {
item.setActionSearch(searchFor);
}
}
return this;
}
}
@@ -0,0 +1,4 @@
import ApiResponse from './api-response.dto';
import NotificationResponse from './notification-response.dto';
export type KodiApiResponse = ApiResponse | NotificationResponse;
+3 -2
View File
@@ -1,6 +1,7 @@
export default class NotificationResponse {
notification: string;
refreshContainer: boolean;
readonly notification: string;
readonly refreshContainer: boolean;
readonly updateList = false;
constructor(message: string, refreshContainer = true) {
this.notification = message;
@@ -4,10 +4,17 @@ import { firstValueFrom } from 'rxjs';
import { SearchResponseInterface } from '../Interface/search-response.interface';
import { SearchResponseDto } from '../Dto/search-response.dto';
import { SearchResponseItemDto } from '../Dto/search-response-item.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { LrtCategory } from '../Entity/lrt-category.entity';
import { IsNull, Not, Repository } from 'typeorm';
@Injectable()
export class LrtApiCategoryClient {
constructor(private readonly httpService: HttpService) {}
constructor(
private readonly httpService: HttpService,
@InjectRepository(LrtCategory)
private lrtCategoriesRepository: Repository<LrtCategory>,
) {}
async getCategory(catId: string): Promise<SearchResponseDto> {
const url = 'https://www.lrt.lt/api/search?type=3&category_id=' + catId;
@@ -36,6 +43,23 @@ export class LrtApiCategoryClient {
responseDto.addItem(itemDto);
}
//update last access
this.lrtCategoriesRepository
.update({ categoryId: catId }, { lastAccess: Date.now() })
.then((r) => {
if (r.affected === 0) {
//todo - search-update category info
}
});
return responseDto;
}
getRecentCategories(count: number): Promise<LrtCategory[]> {
return this.lrtCategoriesRepository.find({
order: { lastAccess: 'DESC' },
take: count,
where: { lastAccess: Not(IsNull()) },
});
}
}
@@ -18,7 +18,8 @@ export class LrtApiSearchClient {
async searchCategories(query: string): Promise<SearchResponseDto> {
query = query === 'viskas' ? '' : query;
const url = 'https://www.lrt.lt/api/search?type=3&tema=' + query;
//const url = 'https://www.lrt.lt/api/search?type=3&tema=' + query;
const url = 'https://www.lrt.lt/api/search?type=3&order=desc&tema=' + query;
//https://www.lrt.lt/api/search?page=1&count=44&order=desc
//https://www.lrt.lt/api/search?get_terms=1
const resp = await firstValueFrom(this.httpService.get(url));
@@ -43,6 +44,18 @@ export class LrtApiSearchClient {
searchItem.img_path_prefix +
'282x158' +
searchItem.img_path_postfix;
let categoryId = await this.findCategoryIdInDb(searchItem.category_url);
if (categoryId === null) {
categoryId = await this.findCategoryIdInLrt(searchItem.category_url);
this.saveCategory(
searchItem.category_url,
categoryId,
searchItem.category_title,
itemDto.thumb,
);
}
itemDto.categoryId = await this.findCategoryId(searchItem.category_url);
loadedCategories.add(searchItem.category_title);
@@ -60,10 +73,17 @@ export class LrtApiSearchClient {
return this.findCategoryIdInLrt(categoryUrl);
}
private saveCategory(categoryUrl: string, catId: string) {
private saveCategory(
categoryUrl: string,
catId: string,
catTitle: string,
thumb: string,
) {
const cat = new LrtCategory();
cat.categoryUrl = categoryUrl;
cat.categoryId = catId;
cat.title = catTitle;
cat.thumb = thumb;
this.lrtCategoriesRepository.save(cat).then();
}
@@ -91,9 +111,7 @@ export class LrtApiSearchClient {
const interim = body.indexOf('"', start);
const end = body.indexOf('"', interim + 1);
if (end > interim) {
const catId = body.substring(interim + 1, end);
this.saveCategory(categoryUrl, catId);
return catId;
return body.substring(interim + 1, end);
} else {
throw 'unexpected response body 1';
}
@@ -7,4 +7,13 @@ export class LrtCategory {
@Column({ name: 'category_id' })
categoryId: string;
@Column()
title: string;
@Column({ name: 'last_access' })
lastAccess: number;
@Column()
thumb: string;
}
@@ -3,6 +3,7 @@ import { LrtApiSearchClient } from './Client/lrt-api-search.client';
import { SearchResponseDto } from './Dto/search-response.dto';
import { LrtApiCategoryClient } from './Client/lrt-api-category.client';
import { LrtApiPlaylistClient } from './Client/lrt-api-playlist.client';
import { LrtCategory } from './Entity/lrt-category.entity';
@Injectable()
export class LrtApiClient {
@@ -23,4 +24,8 @@ export class LrtApiClient {
getPlaylist(mediaUrl: string): Promise<string> {
return this.playlistClient.getPlaylistUrl(mediaUrl);
}
getRecentCategories(count: number): Promise<LrtCategory[]> {
return this.categoryClient.getRecentCategories(count);
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ export class LrtController {
) {}
@Get()
getLrtMenu(): ApiResponse {
getLrtMenu(): Promise<ApiResponse> {
return this.lrtService.getMainMenu();
}
+3 -3
View File
@@ -3,11 +3,11 @@ import { LrtController } from './lrt.controller';
import { LrtService } from './lrt.service';
import { KodiApiResponseFactory } from '../kodi-api-response.factory';
import { LrtApiClientModule } from './LrtApiClient/lrt-api-client.module';
import { RecentSearchesService } from '../RecentSearches/recent-searches.service';
import { RecentSearchesModule } from '../RecentSearches/recent-searches.module';
@Module({
imports: [LrtApiClientModule],
imports: [LrtApiClientModule, RecentSearchesModule],
controllers: [LrtController],
providers: [KodiApiResponseFactory, LrtService, RecentSearchesService],
providers: [KodiApiResponseFactory, LrtService],
})
export class LrtModule {}
+21 -21
View File
@@ -12,34 +12,34 @@ export class LrtService {
private readonly recentSearchesService: RecentSearchesService,
) {}
getMainMenu(): ApiResponse {
getMainMenu(): Promise<ApiResponse> {
const apiResponse = this.kodiApiResponseFactory.createApiResponse();
apiResponse.setNoSort().setTitle('LRT.lt');
this.createMainMenu(apiResponse);
return apiResponse;
return this.createMainMenu(apiResponse);
}
private createMainMenu(apiResponse: ApiResponse): void {
//todo - refactor to 5 most recent searches + viskas?
const items = {
'lrt/recent': 'neseniai ieskoti',
'lrt/tema/vaikams': 'vaikams',
'lrt/tema/sportas': 'sportas',
'lrt/tema/kultura': 'kultura',
'lrt/tema/muzika': 'muzika',
'lrt/tema/viskas': 'viskas',
'lrt/tema/tv-laidos': 'tv-laidos',
};
apiResponse
.createItem()
.setPath('lrt/search')
.setActionSearch()
.setLabel('paieška');
private async createMainMenu(apiResponse: ApiResponse): Promise<ApiResponse> {
apiResponse.addNavigationItems(
[
['paieška', 'search', ''],
['neseniai ieskoti', 'recent'],
['viskas', 'tema/viskas'],
],
'lrt/',
);
for (const [path, label] of Object.entries(items)) {
apiResponse.createItem().setLabel(label).setToFolder().setPath(path);
const recentCategories = await this.lrtApiClient.getRecentCategories(7);
for (const category of recentCategories) {
apiResponse
.createItem()
.setPath('/lrt/cat/' + category.categoryId)
.setThumb(category.thumb)
.setLabel(category.title)
.setToFolder();
}
return apiResponse;
}
async searchCategories(query: string): Promise<ApiResponse> {
@@ -4,16 +4,21 @@ import { configService, ConfigService } from '../../config/config.service';
import { KodiApiResponseFactory } from '../kodi-api-response.factory';
import { join } from 'path';
import { readFile, writeFile } from 'fs/promises';
import { RecentSearchesConfig } from '../../config/recent-searches.config';
@Injectable()
export class RecentSearchesService {
private readonly configService: ConfigService;
private readonly config: RecentSearchesConfig;
constructor(private readonly kodiApiResponseFactory: KodiApiResponseFactory) {
this.configService = configService;
this.config = configService.getRecentSearchesConfig();
}
async getRecentSearches(module: string, path: string): Promise<ApiResponse> {
const data = await this.getRecentSearchesData(module);
const response = this.kodiApiResponseFactory.createApiResponse();
const response = this.kodiApiResponseFactory
.createApiResponse()
.setNoSort();
for (const recentSearch of data) {
response
.createItem()
@@ -28,14 +33,19 @@ export class RecentSearchesService {
async addRecentSearch(module: string, term: string): Promise<void> {
const data = await this.getRecentSearchesData(module);
const index = data.indexOf(term);
if (index > -1) {
if (index === 0) {
return Promise.resolve();
}
if (index > 0) {
data.splice(index, 1);
}
data.push(term);
const rawData = JSON.stringify(data);
const filePath = this.getFilePath(module);
data.unshift(term);
await writeFile(filePath, rawData);
const rawData = JSON.stringify(
data.slice(0, this.config.getRecentSearchesLimit()),
);
const filePath = this.getFilePath(module);
writeFile(filePath, rawData).then();
}
private async getRecentSearchesData(module: string): Promise<Array<string>> {
@@ -46,9 +56,6 @@ export class RecentSearchesService {
}
private getFilePath(module: string): string {
return join(
this.configService.getPaths().getRecentSearchesFolder(),
module + '.json',
);
return join(this.config.getRecentSearchesFolder(), module + '.json');
}
}
+86
View File
@@ -0,0 +1,86 @@
import { Controller, Get, Param, Query } from '@nestjs/common';
import ApiResponse from '../Dto/api-response.dto';
import { TorrentService } from './torrent.service';
@Controller('api/torr')
export class TorrentController {
constructor(private readonly torrService: TorrentService) {}
@Get()
async getMenu(): Promise<ApiResponse> {
return this.torrService.getMenu();
}
@Get('torrs')
async getTorrents(): Promise<ApiResponse> {
return this.torrService.getTorrents();
}
@Get('torr/:id')
async getTorrFiles(@Param('id') id: string): Promise<ApiResponse> {
return this.torrService.getTorrentFiles(id);
}
@Get('torrent/:id')
async getOneTorrentAsList(@Param('id') id: string) {
return this.torrService.getOneTorrentAsList(id);
}
@Get('torrent-context/:id')
async getOneTorrentAsListFromContextMenu(@Param('id') id: string) {
return this.torrService.getOneTorrentAsListFromContextMenu(id);
}
@Get('search/:provider')
getSearch(
@Query('search') query: string,
@Param('provider') provider: string,
) {
return this.torrService.search(query, provider);
}
@Get('recent/:provider')
getRecent(@Param('provider') provider: string) {
return this.torrService.getRecentSearches(provider);
}
@Get('add/:torrId')
add(@Param('torrId') torrId: string) {
return this.torrService.add(torrId);
}
@Get('custom-menu/:provider')
async getCustomMenu(@Param() params: string[]) {
return this.torrService.getCustomMenu(params['provider'], '');
}
@Get('custom-menu/:provider/*')
async getCustomMenuAction(@Param() params: string[]) {
return this.torrService.getCustomMenu(params['provider'], params[0]);
}
@Get('delete/:id')
deleteTorrent(@Param('id') id: string) {
return this.torrService.deleteTorrent(id);
}
@Get('resume/:id')
async resumeTorrent(@Param('id') id: string) {
return this.torrService.resumeTorrent(id);
}
@Get('stop/:id')
async stopTorrent(@Param('id') id: string) {
return this.torrService.stopTorrent(id);
}
@Get('stop-transmission/:id')
async stopTransmissionTorrent(@Param('id') id: string) {
return this.torrService.stopTransmissionTorrent(id);
}
@Get('add-transmission/:id')
async addTransmissionTorrent(@Param('id') id: string) {
return this.torrService.addTransmissionTorrent(id);
}
}
+22
View File
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { TorrentService } from './torrent.service';
import { TorrentController } from './torrent.controller';
import { KodiApiResponseFactory } from '../kodi-api-response.factory';
import { SearchClientModule } from '../../Torrent/SearchClient/search-client.module';
import { RecentSearchesModule } from '../RecentSearches/recent-searches.module';
import { TorrentDownloadClientModule } from '../../Torrent/DownloadClient/torrent-download-client.module';
import { AllFilesModule } from '../AllFiles/all-files.module';
import { TorrentSeedClientModule } from '../../Torrent/SeedClient/torrent-seed-client.module';
@Module({
imports: [
SearchClientModule,
RecentSearchesModule,
TorrentDownloadClientModule,
AllFilesModule,
TorrentSeedClientModule,
],
controllers: [TorrentController],
providers: [TorrentService, KodiApiResponseFactory],
})
export class TorrentModule {}
+344
View File
@@ -0,0 +1,344 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { KodiApiResponseFactory } from '../kodi-api-response.factory';
import ApiResponse from '../Dto/api-response.dto';
import { SearchClientFacade } from '../../Torrent/SearchClient/search-client.facade';
import { TorrentSearchResult } from '../../Torrent/Dto/torrent-search-result.class';
import { RecentSearchesService } from '../RecentSearches/recent-searches.service';
import { TorrentSearchClientResponse } from '../../Torrent/Dto/torrent-search-client-response.dto';
import { TorrentDownloadClient } from '../../Torrent/DownloadClient/torrent-download.client';
import { AllFilesService } from '../AllFiles/all-files.service';
import NotificationResponse from '../Dto/notification-response.dto';
import { TorrentEntity } from '../../Shared/Entity/torrent.entity';
import { TorrentSeedClient } from '../../Torrent/SeedClient/torrent-seed.client';
@Injectable()
export class TorrentService {
private torrentsCache: Record<string, TorrentSearchResult> = {}; //no need for all that info, magnet is enough
constructor(
private readonly koiApiResponseFactory: KodiApiResponseFactory,
private readonly torrSearchClient: SearchClientFacade,
private readonly recentSearchesService: RecentSearchesService,
private readonly torrDownloadClient: TorrentDownloadClient,
private readonly torrSeedClient: TorrentSeedClient,
private readonly allFilesService: AllFilesService,
) {}
async getMenu(): Promise<ApiResponse> {
const apiResponse = this.koiApiResponseFactory
.createApiResponse()
.setNoSort();
const searchClientNames = this.torrSearchClient.getSearchClientNames();
apiResponse.addNavigationItems([['Torrs', 'torrs']], 'torr/');
apiResponse.setTitle('Torrent service');
for (const clientName of searchClientNames) {
apiResponse.addNavigationItems(
[
[clientName + ' Search', 'search/' + clientName, ''],
[clientName + ' Neseniai ieškoti', 'recent/' + clientName],
],
'torr/',
);
const customMenu = await this.torrSearchClient.getCustomMenu(clientName);
apiResponse.addNavigationItems(
customMenu.navigationItems,
'torr/custom-menu/' + clientName + '/',
);
this.addTorrentSearchResultsToApiResponse(
apiResponse,
customMenu.searchResults,
);
}
return apiResponse;
}
async getCustomMenu(provider: string, path: string) {
const apiResponse = this.koiApiResponseFactory.createApiResponse();
apiResponse.setNoSort();
const customMenu: TorrentSearchClientResponse | false =
await this.torrSearchClient
.getCustomMenu(provider, path)
.catch(() => false);
if (customMenu === false) {
return this.koiApiResponseFactory.createNotificationResponse(
'Error while searching',
false,
);
}
apiResponse.setTitle(customMenu.title);
apiResponse.addNavigationItems(
customMenu.navigationItems,
'torr/custom-menu/' + provider + '/',
);
this.addTorrentSearchResultsToApiResponse(
apiResponse,
customMenu.searchResults,
);
return apiResponse;
}
async search(
query: string,
provider: string,
): Promise<ApiResponse | NotificationResponse> {
this.recentSearchesService.addRecentSearch('torr', query).then(/*ignore*/);
const searchResults: TorrentSearchClientResponse | false =
await this.torrSearchClient.search(query, provider).catch(() => false);
if (searchResults === false) {
return this.koiApiResponseFactory.createNotificationResponse(
'Error while searching',
false,
);
}
const apiResponse = this.koiApiResponseFactory.createApiResponse();
apiResponse.setNoSort();
this.addTorrentSearchResultsToApiResponse(
apiResponse,
searchResults.searchResults,
);
return apiResponse;
}
async getRecentSearches(provider: string): Promise<ApiResponse> {
return this.recentSearchesService.getRecentSearches(
'torr',
'torr/search/' + provider,
);
}
async add(torrId: string): Promise<ApiResponse | NotificationResponse> {
const torrSearchData = this.torrentsCache[torrId];
if (torrSearchData === undefined) {
throw new NotFoundException('torrent search data not found');
}
const fileEntities = await this.torrDownloadClient
.addTorrent(torrSearchData.magnet)
.catch(() => null);
if (fileEntities === null) {
return this.koiApiResponseFactory.createNotificationResponse(
'error adding torrent',
false,
);
}
return this.allFilesService.buildFilesResponse(
torrSearchData.name,
fileEntities,
true,
);
}
private addTorrentSearchResultsToApiResponse(
apiResponse: ApiResponse,
searchResults: TorrentSearchResult[],
): void {
for (const searchResult of searchResults) {
this.torrentsCache[searchResult.id] = searchResult;
apiResponse
.createItem()
.setDate(searchResult.date)
.setLabel(searchResult.name)
.setToFolder()
.setPath('torr/add/' + searchResult.id)
.setPlot(
[
searchResult.date,
searchResult.size,
'S' + searchResult.seeders + ' L' + searchResult.leechers,
searchResult.info,
].join('\n'),
);
}
}
async getTorrents(): Promise<ApiResponse> {
const torrentEntities = await this.torrDownloadClient.getTorrents();
const apiResponse = this.koiApiResponseFactory.createApiResponse();
apiResponse.setTitle('Torrents');
for (const torrentEntity of torrentEntities) {
this.addTorrentToApiResponse(torrentEntity, apiResponse);
}
return apiResponse;
}
private addTorrentToApiResponse(
torrentEntity: TorrentEntity,
apiResponse: ApiResponse,
): void {
const firstFileEntity = torrentEntity.files[0];
if (!firstFileEntity) {
return;
}
firstFileEntity.progress = torrentEntity.progress;
firstFileEntity.size = torrentEntity.size;
const item = apiResponse
.createItem()
.setPlot(this.buildPlot(torrentEntity))
.setLabel(torrentEntity.name)
.setPath('/torr/torr/' + torrentEntity.id)
.setToFolder();
if (torrentEntity.stopped && torrentEntity.progress !== 1) {
item.addContextMenu('resume wt', '/torr/resume/' + torrentEntity.id);
}
if (!torrentEntity.stopped) {
item.addContextMenu('stop wt', '/torr/stop/' + torrentEntity.id);
}
if (torrentEntity.transmissionId) {
item.addContextMenu(
'stop transmission',
'/torr/stop-transmission/' + torrentEntity.id,
);
}
if (!torrentEntity.transmissionId && torrentEntity.progress === 1) {
item.addContextMenu(
'add transmission',
'/torr/add-transmission/' + torrentEntity.id,
);
}
item.addContextMenu('delete', '/torr/delete/' + torrentEntity.id);
}
buildPlot(torrentEntity: TorrentEntity): string {
const plot: string[] = [];
if (torrentEntity.size) {
plot.push(
'Size ' + this.allFilesService.getHRFileSize(torrentEntity.size),
);
}
const arrTorrProps: string[] = [];
if (torrentEntity.linkomanija) {
arrTorrProps.push('LM');
}
if (torrentEntity.transmissionId) {
arrTorrProps.push('transm');
}
arrTorrProps.push('wt');
const torrProps = arrTorrProps.join(' + ');
if (torrProps) {
plot.push(torrProps);
}
if (torrentEntity.progress) {
const progress = Math.round(torrentEntity.progress * 1000) / 10;
const stopped = torrentEntity.stopped ? ' Stopped' : '';
plot.push(`${progress}%${stopped}`);
}
return plot.join('\n');
}
getOneTorrentAsListFromContextMenu(torrentId: string) {
//by default context menu does not update container (view), we need to force it
return {
forceUpdate: {
urlParams: {
action: 'query',
path: 'torr/torrent/' + torrentId,
},
},
};
}
async getOneTorrentAsList(torrentId: string): Promise<ApiResponse> {
const id = parseInt(torrentId);
const torrentEntity = await this.torrDownloadClient.getTorrentById(id);
const apiResponse = this.koiApiResponseFactory.createApiResponse();
if (torrentEntity) {
this.addTorrentToApiResponse(torrentEntity, apiResponse);
}
return apiResponse;
}
async getTorrentFiles(id: string): Promise<ApiResponse> {
const torrentEntity = await this.torrDownloadClient.getTorrentById(
parseInt(id),
);
return this.allFilesService.buildFilesResponse(
torrentEntity.name,
torrentEntity.files,
true,
);
}
async stopTorrent(id: string) {
const torrId = parseInt(id);
try {
await this.torrDownloadClient.stopTorrent(torrId);
} catch (e) {
return this.koiApiResponseFactory.createNotificationResponse(
'error stopping',
);
}
return this.koiApiResponseFactory.createNotificationResponse('stopped');
}
async resumeTorrent(id: string) {
const torrId = parseInt(id);
try {
await this.torrDownloadClient.resumeTorrent(torrId);
} catch (e) {
return this.koiApiResponseFactory.createNotificationResponse(
'error resuming',
);
}
return this.koiApiResponseFactory.createNotificationResponse('resumed');
}
async stopTransmissionTorrent(id: string) {
const torrId = parseInt(id);
const result = await this.torrSeedClient
.removeTorrent(torrId)
.catch(() => false);
return new NotificationResponse(
result ? 'torrent removed from transmission' : 'removal failed',
true,
);
}
async addTransmissionTorrent(id: string) {
const torrId = parseInt(id);
const result = await this.torrSeedClient
.addTorrentById(torrId)
.catch(() => false);
return new NotificationResponse(
result ? 'torrent added to transmission' : 'adding failed',
true,
);
}
async deleteTorrent(id: string) {
const torrId = parseInt(id);
const result = await this.torrSeedClient
.removeTorrent(torrId, true)
.catch(() => false);
return new NotificationResponse(
result ? 'torrent deleted' : 'deleting from wt not implemented',
true,
);
}
}
@@ -5,12 +5,12 @@ import { join } from 'path';
export class KodiApiUrlRewriteMiddleware implements NestMiddleware {
async use(req: Request, res: Response, next: NextFunction) {
let path = '';
if (req.query && req.query.path) {
let path = '';
path = req.query.path as string;
const query = req.url.split('?').pop();
req.url = join('/api/', path) + (query ? '?' + query : '');
}
const query = req.url.split('?').pop();
req.url = join('/api/', path) + (query ? '?' + query : '');
next();
}
+5
View File
@@ -8,4 +8,9 @@ export class KodiApiController {
getMainMenu() {
return this.kodiApiService.getMainMenu();
}
@Get('test')
getCurrentTest() {
return this.kodiApiService.getCurrentTest();
}
}
+2 -1
View File
@@ -10,9 +10,10 @@ import { LrtModule } from './LRT/lrt.module';
import { KodiApiService } from './kodi-api.service';
import { KodiApiResponseFactory } from './kodi-api-response.factory';
import { AllFilesModule } from './AllFiles/all-files.module';
import { TorrentModule } from './Torrent/torrent.module';
@Module({
imports: [LrtModule, AllFilesModule],
imports: [LrtModule, AllFilesModule, TorrentModule],
controllers: [KodiApiController],
providers: [KodiApiService, KodiApiResponseFactory],
})
+20 -16
View File
@@ -9,23 +9,27 @@ export class KodiApiService {
) {}
getMainMenu(): ApiResponse {
const apiResponse = this.kodiApiResponseFactory.createApiResponse();
apiResponse.setTitle('Menu');
this.createMainMenu(apiResponse);
return apiResponse;
return this.kodiApiResponseFactory
.createApiResponse()
.setTitle('Menu')
.addNavigationItems([
['All Files', 'all'],
['Torr', 'torr'],
['LRT.lt', 'lrt'],
//['Test', 'test'],
]);
}
private createMainMenu(apiResponse: ApiResponse): void {
const items = {
all: 'All files',
torr: 'Torr',
lrt: 'LRT.lt',
};
for (const [path, label] of Object.entries(items)) {
apiResponse.createItem().setLabel(label).setToFolder().setPath(path);
}
getCurrentTest() {
return this.kodiApiResponseFactory
.createApiResponse()
.setTitle('Menu')
.addNavigationItems([
['select 1', 'all'],
['select 2', 'lrt'],
['select 3', 'torr'],
['select 4 search', 'test', ''],
])
.setShowAsSelectList();
}
}