mirror of
https://github.com/sakaljurgis/kodi-api-server.git
synced 2026-07-09 04:47:41 +00:00
migrations; torr search, dl, seed, delete; files scan, delete; configs
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -9,5 +9,6 @@ import { VideoFilesModule } from '../../VideoFiles/video-files.module';
|
||||
imports: [StreamerModule, VideoFilesModule],
|
||||
controllers: [AllFilesController],
|
||||
providers: [AllFilesService, KodiApiResponseFactory],
|
||||
exports: [AllFilesService],
|
||||
})
|
||||
export class AllFilesModule {}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export class LrtController {
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
getLrtMenu(): ApiResponse {
|
||||
getLrtMenu(): Promise<ApiResponse> {
|
||||
return this.lrtService.getMainMenu();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -8,4 +8,9 @@ export class KodiApiController {
|
||||
getMainMenu() {
|
||||
return this.kodiApiService.getMainMenu();
|
||||
}
|
||||
|
||||
@Get('test')
|
||||
getCurrentTest() {
|
||||
return this.kodiApiService.getCurrentTest();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user