implement all lrt client functions

This commit is contained in:
Jurgis Sakalauskas
2022-12-08 12:39:41 +02:00
parent 9d00d549b6
commit d00601b52c
15 changed files with 307 additions and 121 deletions
+14 -3
View File
@@ -3,6 +3,7 @@ import ContextMenu from './context-menu.dto';
export default class ResponseItem { export default class ResponseItem {
isFolder = false; isFolder = false;
isPlayable = true; isPlayable = true;
IsPlayable = true; //backwards compatability
mediatype: string; mediatype: string;
action: 'query' | 'search'; action: 'query' | 'search';
label: string; label: string;
@@ -12,6 +13,7 @@ export default class ResponseItem {
duration: number; duration: number;
thumb: string; thumb: string;
contextMenus: Array<ContextMenu>; contextMenus: Array<ContextMenu>;
date: string;
constructor() { constructor() {
this.setToPlayable(); this.setToPlayable();
@@ -20,6 +22,7 @@ export default class ResponseItem {
setToFolder() { setToFolder() {
this.isFolder = true; this.isFolder = true;
delete this.isPlayable; delete this.isPlayable;
delete this.IsPlayable;
delete this.mediatype; delete this.mediatype;
delete this.action; delete this.action;
@@ -29,6 +32,7 @@ export default class ResponseItem {
setToPlayable(withQueryAction = false) { setToPlayable(withQueryAction = false) {
this.isFolder = false; this.isFolder = false;
this.isPlayable = true; this.isPlayable = true;
this.IsPlayable = true;
this.mediatype = 'video'; this.mediatype = 'video';
// if path provided is not to a video, but additional api request is needed // if path provided is not to a video, but additional api request is needed
@@ -43,6 +47,7 @@ export default class ResponseItem {
setActionSearch() { setActionSearch() {
this.action = 'search'; this.action = 'search';
delete this.isPlayable; delete this.isPlayable;
delete this.IsPlayable;
delete this.mediatype; delete this.mediatype;
this.isFolder = false; this.isFolder = false;
@@ -60,13 +65,13 @@ export default class ResponseItem {
return this; return this;
} }
setSize(size) { setSize(size: number) {
this.size = size; this.size = size;
return this; return this;
} }
setPlot(plot) { setPlot(plot: string) {
this.plot = plot; this.plot = plot;
return this; return this;
@@ -78,12 +83,18 @@ export default class ResponseItem {
return this; return this;
} }
setThumb(thumb) { setThumb(thumb: string) {
this.thumb = thumb; this.thumb = thumb;
return this; return this;
} }
setDate(date: string) {
this.date = date;
return this;
}
addContextMenu(name, path) { addContextMenu(name, path) {
if (!this.contextMenus) { if (!this.contextMenus) {
this.contextMenus = []; this.contextMenus = [];
@@ -0,0 +1,41 @@
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
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';
@Injectable()
export class LrtApiCategoryClient {
constructor(private readonly httpService: HttpService) {}
async getCategory(catId: string): Promise<SearchResponseDto> {
const url = 'https://www.lrt.lt/api/search?type=3&category_id=' + catId;
const resp = await firstValueFrom(this.httpService.get(url));
const searchResponse: SearchResponseInterface = resp.data;
//todo - move to mapper
const responseDto = new SearchResponseDto(
parseInt(searchResponse.page),
parseInt(searchResponse.total_found),
//todo - consider calculating total pages?
);
for (const searchItem of searchResponse.items) {
const itemDto = new SearchResponseItemDto();
itemDto.label = searchItem.title;
itemDto.thumb =
'https://www.lrt.lt' +
searchItem.img_path_prefix +
'282x158' +
searchItem.img_path_postfix;
itemDto.categoryId = catId;
itemDto.date = searchItem.item_date;
itemDto.url = searchItem.url;
responseDto.addItem(itemDto);
}
return responseDto;
}
}
@@ -0,0 +1,18 @@
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
import { MediaInfoResponseInterface } from '../Interface/media-info-response.interface';
@Injectable()
export class LrtApiPlaylistClient {
constructor(private readonly httpService: HttpService) {}
async getPlaylistUrl(mediaUrl: string): Promise<string> {
const url =
'https://www.lrt.lt/servisai/stream_url/vod/media_info/?url=/' + mediaUrl;
const resp = await firstValueFrom(this.httpService.get(url));
const mediaInfoResponse: MediaInfoResponseInterface = resp.data;
return mediaInfoResponse.playlist_item.file;
}
}
@@ -0,0 +1,66 @@
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
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';
@Injectable()
export class LrtApiSearchClient {
constructor(private readonly httpService: HttpService) {}
async searchCategories(query: string): Promise<SearchResponseDto> {
query = query === 'viskas' ? '' : query;
const url = 'https://www.lrt.lt/api/search?type=3&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));
const searchResponse: SearchResponseInterface = resp.data;
const cats = [];
//todo - move to mapper
const responseDto = new SearchResponseDto(
parseInt(searchResponse.page),
parseInt(searchResponse.total_found),
);
for (const searchItem of searchResponse.items) {
if (cats.indexOf(searchItem.category_title) > -1) {
continue;
}
const itemDto = new SearchResponseItemDto();
itemDto.label = searchItem.category_title;
itemDto.thumb =
'https://www.lrt.lt' +
searchItem.img_path_prefix +
'282x158' +
searchItem.img_path_postfix;
itemDto.categoryId = await this.findCategoryId(searchItem.category_url);
cats.push(searchItem.category_title);
responseDto.addItem(itemDto);
}
return responseDto;
}
private async findCategoryId(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;
const start = body.indexOf('data.category_id');
if (start > -1) {
const interim = body.indexOf('"', start);
const end = body.indexOf('"', interim + 1);
if (end > interim) {
return body.substring(interim + 1, end);
} else {
throw 'unexpected response body 1';
}
} else {
throw 'unexpected response body 2';
}
}
}
@@ -0,0 +1,7 @@
export class SearchResponseItemDto {
label: string;
thumb: string;
categoryId: string;
date: string;
url: string;
}
@@ -0,0 +1,15 @@
import { SearchResponseItemDto } from './search-response-item.dto';
export class SearchResponseDto {
readonly items: Array<SearchResponseItemDto> = [];
constructor(readonly page: number, readonly total: number) {
this.page = page;
this.total = total;
}
addItem(item: SearchResponseItemDto) {
this.items.push(item);
return this;
}
}
@@ -0,0 +1,24 @@
export class MediaInfoResponseInterface {
id: number;
title: string;
type: number;
content: string;
category_id: number;
heritage: any;
date: string;
offset: number;
tags: Array<{
slug: string;
first: 1;
last: number;
name: string;
}>;
url: string;
full_url: string;
playlist_item: {
file: string;
title: string;
image: string;
mediaid: number;
};
}
@@ -1,4 +1,4 @@
interface SearchResponseItemInterface { export interface SearchResponseItemInterface {
photo_horizontal: number; photo_horizontal: number;
category_img_path_postfix: string; category_img_path_postfix: string;
age_restriction: string; age_restriction: string;
@@ -29,4 +29,5 @@ interface SearchResponseItemInterface {
n18: number; n18: number;
subtitle: string; subtitle: string;
photo_count: number; photo_count: number;
category_id: string;
} }
@@ -1,10 +1,13 @@
interface SearchResponseInterface { import { SearchResponseItemInterface } from './search-response-item.interface';
export interface SearchResponseInterface {
q: string; q: string;
meta: { meta: {
time: number; time: string;
total: number; total: string;
total_found: number; total_found: string;
}; };
total_found: string | number; page: string;
total_found: string;
items: Array<SearchResponseItemInterface>; items: Array<SearchResponseItemInterface>;
} }
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { LrtApiSearchClient } from './Client/lrt-api-search.client';
import { LrtApiClient } from './lrt-api.client';
import { LrtApiCategoryClient } from './Client/lrt-api-category.client';
import { LrtApiPlaylistClient } from './Client/lrt-api-playlist.client';
@Module({
imports: [
HttpModule.register({
withCredentials: true,
}),
],
providers: [
LrtApiSearchClient,
LrtApiCategoryClient,
LrtApiPlaylistClient,
LrtApiClient,
],
exports: [LrtApiClient],
})
export class LrtApiClientModule {}
@@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';
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';
@Injectable()
export class LrtApiClient {
constructor(
private readonly searchClient: LrtApiSearchClient,
private readonly categoryClient: LrtApiCategoryClient,
private readonly playlistClient: LrtApiPlaylistClient,
) {}
searchCategories(query: string): Promise<SearchResponseDto> {
return this.searchClient.searchCategories(query);
}
getCategory(catId: string): Promise<SearchResponseDto> {
return this.categoryClient.getCategory(catId);
}
getPlaylist(mediaUrl: string): Promise<string> {
return this.playlistClient.getPlaylistUrl(mediaUrl);
}
}
-62
View File
@@ -1,62 +0,0 @@
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
@Injectable()
export class LrtApiClient {
constructor(private readonly httpService: HttpService) {}
async getSearch(topic: string): Promise<any> {
//todo return api response
topic = topic === 'viskas' ? topic : '';
const url = 'https://www.lrt.lt/api/search?type=3&tema=' + topic;
const resp = await firstValueFrom(this.httpService.get(url));
const data: SearchResponseInterface = resp.data;
const items: Array<any> = data.items;
const cats = [];
const returnItems = [];
if (items) {
for (const i in items) {
const item = items[i];
if (cats.indexOf(item.category_title) > -1) {
continue;
}
//todo refactor to api response item
const newItem = {
label: item.category_title,
isFolder: true,
thumb:
'https://www.lrt.lt' +
item.img_path_prefix +
'282x158' +
item.img_path_postfix,
categoryId: await this.findCategoryId(item.category_url),
};
cats.push(item.category_title);
returnItems.push(newItem);
}
}
return returnItems;
}
private async findCategoryId(categoryUrl: string): Promise<string> {
const url = 'https://www.lrt.lt' + categoryUrl;
const resp = await firstValueFrom(this.httpService.get(url));
const body: string = resp.data;
const start = body.indexOf('data.category_id');
if (start > -1) {
const interim = body.indexOf('"', start);
const end = body.indexOf('"', interim + 1);
if (end > interim) {
return body.substring(interim + 1, end);
} else {
throw 'unexpected response body 1';
}
} else {
throw 'unexpected response body 2';
}
}
}
+13 -41
View File
@@ -1,64 +1,36 @@
import { Controller, Get, Param, Query, Req } from '@nestjs/common'; import { Controller, Get, Param, Query, Req } from '@nestjs/common';
import { Request } from 'express'; import { Request } from 'express';
import { LrtService } from './lrt.service'; import { LrtService } from './lrt.service';
import { LrtApiClient } from './lrt-api.client'; import ApiResponse from '../Dto/api-response.dto';
@Controller('api/lrt') @Controller('api/lrt')
export class LrtController { export class LrtController {
constructor( constructor(private readonly lrtService: LrtService) {}
private readonly lrtService: LrtService,
private readonly lrtApiClient: LrtApiClient,
) {}
@Get() @Get()
getLrtMenu() { getLrtMenu(): ApiResponse {
return this.lrtService.getMainMenu(); return this.lrtService.getMainMenu();
} }
@Get('search') @Get('search')
getSearch(@Req() request: Request, @Query('search') search: string) { getSearch(@Query('search') search: string): Promise<ApiResponse> {
return { return this.lrtService.searchCategories(search);
mod: 'lrt',
path: request.url,
search: search ?? 'no',
};
}
@Get('recent')
async getRecent(@Req() request: Request) {
return {
mod: 'lrt',
path: request.url,
recent: true,
};
} }
@Get('cat/:id') @Get('cat/:id')
async getCategory(@Req() request: Request, @Param('id') id: number) { async getCategory(@Param('id') id: string): Promise<ApiResponse> {
return { return this.lrtService.getCategory(id);
mod: 'lrt - category',
path: request.url,
category: id,
};
} }
@Get('tema/:topic') @Get('tema/:topic')
async getTema(@Req() request: Request, @Param('topic') topic: string) { async getTema(@Param('topic') topic: string): Promise<ApiResponse> {
const resp = await this.lrtApiClient.getSearch(topic); return this.lrtService.searchCategories(topic);
return {
mod: 'lrt - category',
path: request.url,
topic: resp,
};
} }
@Get('play/*') @Get('play/*')
getPlay(@Req() request: Request, @Param() params: string[]) { async getPlay(@Req() request: Request, @Param() params: string[]) {
const playPath = params[0]; const mediaUrl = params[0];
return {
mod: 'lrt - play', return this.lrtService.getPlayableItem(mediaUrl);
path: request.url,
playPath: playPath,
};
} }
} }
+3 -8
View File
@@ -2,16 +2,11 @@ import { Module } from '@nestjs/common';
import { LrtController } from './lrt.controller'; import { LrtController } from './lrt.controller';
import { LrtService } from './lrt.service'; import { LrtService } from './lrt.service';
import { KodiApiResponseFactory } from '../kodi-api-response.factory'; import { KodiApiResponseFactory } from '../kodi-api-response.factory';
import { HttpModule } from '@nestjs/axios'; import { LrtApiClientModule } from './LrtApiClient/lrt-api-client.module';
import { LrtApiClient } from './lrt-api.client';
@Module({ @Module({
imports: [ imports: [LrtApiClientModule],
HttpModule.register({
withCredentials: true,
}),
],
controllers: [LrtController], controllers: [LrtController],
providers: [KodiApiResponseFactory, LrtService, LrtApiClient], providers: [KodiApiResponseFactory, LrtService],
}) })
export class LrtModule {} export class LrtModule {}
+48 -1
View File
@@ -1,12 +1,15 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import ApiResponse from '../Dto/api-response.dto'; import ApiResponse from '../Dto/api-response.dto';
import { KodiApiResponseFactory } from '../kodi-api-response.factory'; import { KodiApiResponseFactory } from '../kodi-api-response.factory';
import { LrtApiClient } from './LrtApiClient/lrt-api.client';
@Injectable() @Injectable()
export class LrtService { export class LrtService {
constructor( constructor(
private readonly kodiApiResponseFactory: KodiApiResponseFactory, private readonly kodiApiResponseFactory: KodiApiResponseFactory,
private readonly lrtApiClient: LrtApiClient,
) {} ) {}
getMainMenu(): ApiResponse { getMainMenu(): ApiResponse {
const apiResponse = this.kodiApiResponseFactory.createApiResponse(); const apiResponse = this.kodiApiResponseFactory.createApiResponse();
apiResponse.setNoSort().setTitle('LRT.lt'); apiResponse.setNoSort().setTitle('LRT.lt');
@@ -17,7 +20,6 @@ export class LrtService {
private createMainMenu(apiResponse: ApiResponse): void { private createMainMenu(apiResponse: ApiResponse): void {
const items = { const items = {
'lrt/recent': 'neseniai ieškoti',
'lrt/tema/vaikams': 'vaikams', 'lrt/tema/vaikams': 'vaikams',
'lrt/tema/sportas': 'sportas', 'lrt/tema/sportas': 'sportas',
'lrt/tema/kultura': 'kultura', 'lrt/tema/kultura': 'kultura',
@@ -35,4 +37,49 @@ export class LrtService {
apiResponse.createItem().setLabel(label).setToFolder().setPath(path); apiResponse.createItem().setLabel(label).setToFolder().setPath(path);
} }
} }
async searchCategories(query: string): Promise<ApiResponse> {
const searchResponseDto = await this.lrtApiClient.searchCategories(query);
const apiResponse = this.kodiApiResponseFactory.createApiResponse();
apiResponse.setTitle('LRT ' + query);
for (const itemDto of searchResponseDto.items) {
apiResponse
.createItem()
.setLabel(itemDto.label)
.setThumb(itemDto.thumb)
.setPath('lrt/cat/' + itemDto.categoryId)
.setToFolder();
}
return apiResponse;
}
async getCategory(catId: string): Promise<ApiResponse> {
const searchResponseDto = await this.lrtApiClient.getCategory(catId);
const apiResponse = this.kodiApiResponseFactory.createApiResponse();
apiResponse.setTitle('LRT.lt');
for (const itemDto of searchResponseDto.items) {
apiResponse
.createItem()
.setLabel(itemDto.label + ' ' + itemDto.date)
.setDate(itemDto.date)
.setThumb(itemDto.thumb)
.setPath('lrt/play' + itemDto.url)
.setToPlayable(true)
.setPlot(itemDto.date);
}
return apiResponse;
}
async getPlayableItem(mediaUrl: string): Promise<ApiResponse> {
const playableUrl = await this.lrtApiClient.getPlaylist(mediaUrl);
const apiResponse = this.kodiApiResponseFactory.createApiResponse();
apiResponse.setTitle('LRT.lt');
apiResponse.setToPlayable(playableUrl);
return apiResponse;
}
} }