From d00601b52cda1d9b00db97f3d563986b13a35dcf Mon Sep 17 00:00:00 2001 From: Jurgis Sakalauskas Date: Thu, 8 Dec 2022 12:39:41 +0200 Subject: [PATCH] implement all lrt client functions --- src/KodiApi/Dto/response-item.dto.ts | 17 ++++- .../Client/lrt-api-category.client.ts | 41 ++++++++++++ .../Client/lrt-api-playlist.client.ts | 18 +++++ .../Client/lrt-api-search.client.ts | 66 +++++++++++++++++++ .../Dto/search-response-item.dto.ts | 7 ++ .../LrtApiClient/Dto/search-response.dto.ts | 15 +++++ .../media-info-response.interface.ts | 24 +++++++ .../search-response-item.interface.ts | 3 +- .../Interface/search-response.interface.ts | 13 ++-- .../LRT/LrtApiClient/lrt-api-client.module.ts | 22 +++++++ .../LRT/LrtApiClient/lrt-api.client.ts | 26 ++++++++ src/KodiApi/LRT/lrt-api.client.ts | 62 ----------------- src/KodiApi/LRT/lrt.controller.ts | 54 ++++----------- src/KodiApi/LRT/lrt.module.ts | 11 +--- src/KodiApi/LRT/lrt.service.ts | 49 +++++++++++++- 15 files changed, 307 insertions(+), 121 deletions(-) create mode 100644 src/KodiApi/LRT/LrtApiClient/Client/lrt-api-category.client.ts create mode 100644 src/KodiApi/LRT/LrtApiClient/Client/lrt-api-playlist.client.ts create mode 100644 src/KodiApi/LRT/LrtApiClient/Client/lrt-api-search.client.ts create mode 100644 src/KodiApi/LRT/LrtApiClient/Dto/search-response-item.dto.ts create mode 100644 src/KodiApi/LRT/LrtApiClient/Dto/search-response.dto.ts create mode 100644 src/KodiApi/LRT/LrtApiClient/Interface/media-info-response.interface.ts create mode 100644 src/KodiApi/LRT/LrtApiClient/lrt-api-client.module.ts create mode 100644 src/KodiApi/LRT/LrtApiClient/lrt-api.client.ts delete mode 100644 src/KodiApi/LRT/lrt-api.client.ts diff --git a/src/KodiApi/Dto/response-item.dto.ts b/src/KodiApi/Dto/response-item.dto.ts index ea4bdc2..23d54f5 100644 --- a/src/KodiApi/Dto/response-item.dto.ts +++ b/src/KodiApi/Dto/response-item.dto.ts @@ -3,6 +3,7 @@ import ContextMenu from './context-menu.dto'; export default class ResponseItem { isFolder = false; isPlayable = true; + IsPlayable = true; //backwards compatability mediatype: string; action: 'query' | 'search'; label: string; @@ -12,6 +13,7 @@ export default class ResponseItem { duration: number; thumb: string; contextMenus: Array; + date: string; constructor() { this.setToPlayable(); @@ -20,6 +22,7 @@ export default class ResponseItem { setToFolder() { this.isFolder = true; delete this.isPlayable; + delete this.IsPlayable; delete this.mediatype; delete this.action; @@ -29,6 +32,7 @@ export default class ResponseItem { setToPlayable(withQueryAction = false) { this.isFolder = false; this.isPlayable = true; + this.IsPlayable = true; this.mediatype = 'video'; // if path provided is not to a video, but additional api request is needed @@ -43,6 +47,7 @@ export default class ResponseItem { setActionSearch() { this.action = 'search'; delete this.isPlayable; + delete this.IsPlayable; delete this.mediatype; this.isFolder = false; @@ -60,13 +65,13 @@ export default class ResponseItem { return this; } - setSize(size) { + setSize(size: number) { this.size = size; return this; } - setPlot(plot) { + setPlot(plot: string) { this.plot = plot; return this; @@ -78,12 +83,18 @@ export default class ResponseItem { return this; } - setThumb(thumb) { + setThumb(thumb: string) { this.thumb = thumb; return this; } + setDate(date: string) { + this.date = date; + + return this; + } + addContextMenu(name, path) { if (!this.contextMenus) { this.contextMenus = []; diff --git a/src/KodiApi/LRT/LrtApiClient/Client/lrt-api-category.client.ts b/src/KodiApi/LRT/LrtApiClient/Client/lrt-api-category.client.ts new file mode 100644 index 0000000..54d87e4 --- /dev/null +++ b/src/KodiApi/LRT/LrtApiClient/Client/lrt-api-category.client.ts @@ -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 { + 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; + } +} diff --git a/src/KodiApi/LRT/LrtApiClient/Client/lrt-api-playlist.client.ts b/src/KodiApi/LRT/LrtApiClient/Client/lrt-api-playlist.client.ts new file mode 100644 index 0000000..ef62dfb --- /dev/null +++ b/src/KodiApi/LRT/LrtApiClient/Client/lrt-api-playlist.client.ts @@ -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 { + 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; + } +} diff --git a/src/KodiApi/LRT/LrtApiClient/Client/lrt-api-search.client.ts b/src/KodiApi/LRT/LrtApiClient/Client/lrt-api-search.client.ts new file mode 100644 index 0000000..012b555 --- /dev/null +++ b/src/KodiApi/LRT/LrtApiClient/Client/lrt-api-search.client.ts @@ -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 { + 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 { + //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'; + } + } +} diff --git a/src/KodiApi/LRT/LrtApiClient/Dto/search-response-item.dto.ts b/src/KodiApi/LRT/LrtApiClient/Dto/search-response-item.dto.ts new file mode 100644 index 0000000..6ee9b93 --- /dev/null +++ b/src/KodiApi/LRT/LrtApiClient/Dto/search-response-item.dto.ts @@ -0,0 +1,7 @@ +export class SearchResponseItemDto { + label: string; + thumb: string; + categoryId: string; + date: string; + url: string; +} diff --git a/src/KodiApi/LRT/LrtApiClient/Dto/search-response.dto.ts b/src/KodiApi/LRT/LrtApiClient/Dto/search-response.dto.ts new file mode 100644 index 0000000..265dfa0 --- /dev/null +++ b/src/KodiApi/LRT/LrtApiClient/Dto/search-response.dto.ts @@ -0,0 +1,15 @@ +import { SearchResponseItemDto } from './search-response-item.dto'; + +export class SearchResponseDto { + readonly items: Array = []; + constructor(readonly page: number, readonly total: number) { + this.page = page; + this.total = total; + } + + addItem(item: SearchResponseItemDto) { + this.items.push(item); + + return this; + } +} diff --git a/src/KodiApi/LRT/LrtApiClient/Interface/media-info-response.interface.ts b/src/KodiApi/LRT/LrtApiClient/Interface/media-info-response.interface.ts new file mode 100644 index 0000000..9141a9e --- /dev/null +++ b/src/KodiApi/LRT/LrtApiClient/Interface/media-info-response.interface.ts @@ -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; + }; +} diff --git a/src/KodiApi/LRT/LrtApiClient/Interface/search-response-item.interface.ts b/src/KodiApi/LRT/LrtApiClient/Interface/search-response-item.interface.ts index 61fcbf9..38d5b2c 100644 --- a/src/KodiApi/LRT/LrtApiClient/Interface/search-response-item.interface.ts +++ b/src/KodiApi/LRT/LrtApiClient/Interface/search-response-item.interface.ts @@ -1,4 +1,4 @@ -interface SearchResponseItemInterface { +export interface SearchResponseItemInterface { photo_horizontal: number; category_img_path_postfix: string; age_restriction: string; @@ -29,4 +29,5 @@ interface SearchResponseItemInterface { n18: number; subtitle: string; photo_count: number; + category_id: string; } diff --git a/src/KodiApi/LRT/LrtApiClient/Interface/search-response.interface.ts b/src/KodiApi/LRT/LrtApiClient/Interface/search-response.interface.ts index e731778..fc75f5d 100644 --- a/src/KodiApi/LRT/LrtApiClient/Interface/search-response.interface.ts +++ b/src/KodiApi/LRT/LrtApiClient/Interface/search-response.interface.ts @@ -1,10 +1,13 @@ -interface SearchResponseInterface { +import { SearchResponseItemInterface } from './search-response-item.interface'; + +export interface SearchResponseInterface { q: string; meta: { - time: number; - total: number; - total_found: number; + time: string; + total: string; + total_found: string; }; - total_found: string | number; + page: string; + total_found: string; items: Array; } diff --git a/src/KodiApi/LRT/LrtApiClient/lrt-api-client.module.ts b/src/KodiApi/LRT/LrtApiClient/lrt-api-client.module.ts new file mode 100644 index 0000000..3fc1300 --- /dev/null +++ b/src/KodiApi/LRT/LrtApiClient/lrt-api-client.module.ts @@ -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 {} diff --git a/src/KodiApi/LRT/LrtApiClient/lrt-api.client.ts b/src/KodiApi/LRT/LrtApiClient/lrt-api.client.ts new file mode 100644 index 0000000..dd2229f --- /dev/null +++ b/src/KodiApi/LRT/LrtApiClient/lrt-api.client.ts @@ -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 { + return this.searchClient.searchCategories(query); + } + + getCategory(catId: string): Promise { + return this.categoryClient.getCategory(catId); + } + + getPlaylist(mediaUrl: string): Promise { + return this.playlistClient.getPlaylistUrl(mediaUrl); + } +} diff --git a/src/KodiApi/LRT/lrt-api.client.ts b/src/KodiApi/LRT/lrt-api.client.ts deleted file mode 100644 index 3402d7e..0000000 --- a/src/KodiApi/LRT/lrt-api.client.ts +++ /dev/null @@ -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 { - //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 = 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 { - 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'; - } - } -} diff --git a/src/KodiApi/LRT/lrt.controller.ts b/src/KodiApi/LRT/lrt.controller.ts index a307c3b..7a34bdd 100644 --- a/src/KodiApi/LRT/lrt.controller.ts +++ b/src/KodiApi/LRT/lrt.controller.ts @@ -1,64 +1,36 @@ import { Controller, Get, Param, Query, Req } from '@nestjs/common'; import { Request } from 'express'; import { LrtService } from './lrt.service'; -import { LrtApiClient } from './lrt-api.client'; +import ApiResponse from '../Dto/api-response.dto'; @Controller('api/lrt') export class LrtController { - constructor( - private readonly lrtService: LrtService, - private readonly lrtApiClient: LrtApiClient, - ) {} + constructor(private readonly lrtService: LrtService) {} @Get() - getLrtMenu() { + getLrtMenu(): ApiResponse { return this.lrtService.getMainMenu(); } @Get('search') - getSearch(@Req() request: Request, @Query('search') search: string) { - return { - mod: 'lrt', - path: request.url, - search: search ?? 'no', - }; - } - - @Get('recent') - async getRecent(@Req() request: Request) { - return { - mod: 'lrt', - path: request.url, - recent: true, - }; + getSearch(@Query('search') search: string): Promise { + return this.lrtService.searchCategories(search); } @Get('cat/:id') - async getCategory(@Req() request: Request, @Param('id') id: number) { - return { - mod: 'lrt - category', - path: request.url, - category: id, - }; + async getCategory(@Param('id') id: string): Promise { + return this.lrtService.getCategory(id); } @Get('tema/:topic') - async getTema(@Req() request: Request, @Param('topic') topic: string) { - const resp = await this.lrtApiClient.getSearch(topic); - return { - mod: 'lrt - category', - path: request.url, - topic: resp, - }; + async getTema(@Param('topic') topic: string): Promise { + return this.lrtService.searchCategories(topic); } @Get('play/*') - getPlay(@Req() request: Request, @Param() params: string[]) { - const playPath = params[0]; - return { - mod: 'lrt - play', - path: request.url, - playPath: playPath, - }; + async getPlay(@Req() request: Request, @Param() params: string[]) { + const mediaUrl = params[0]; + + return this.lrtService.getPlayableItem(mediaUrl); } } diff --git a/src/KodiApi/LRT/lrt.module.ts b/src/KodiApi/LRT/lrt.module.ts index 86d65e1..d191ad9 100644 --- a/src/KodiApi/LRT/lrt.module.ts +++ b/src/KodiApi/LRT/lrt.module.ts @@ -2,16 +2,11 @@ import { Module } from '@nestjs/common'; import { LrtController } from './lrt.controller'; import { LrtService } from './lrt.service'; import { KodiApiResponseFactory } from '../kodi-api-response.factory'; -import { HttpModule } from '@nestjs/axios'; -import { LrtApiClient } from './lrt-api.client'; +import { LrtApiClientModule } from './LrtApiClient/lrt-api-client.module'; @Module({ - imports: [ - HttpModule.register({ - withCredentials: true, - }), - ], + imports: [LrtApiClientModule], controllers: [LrtController], - providers: [KodiApiResponseFactory, LrtService, LrtApiClient], + providers: [KodiApiResponseFactory, LrtService], }) export class LrtModule {} diff --git a/src/KodiApi/LRT/lrt.service.ts b/src/KodiApi/LRT/lrt.service.ts index 1ca1500..fce28cb 100644 --- a/src/KodiApi/LRT/lrt.service.ts +++ b/src/KodiApi/LRT/lrt.service.ts @@ -1,12 +1,15 @@ import { Injectable } from '@nestjs/common'; import ApiResponse from '../Dto/api-response.dto'; import { KodiApiResponseFactory } from '../kodi-api-response.factory'; +import { LrtApiClient } from './LrtApiClient/lrt-api.client'; @Injectable() export class LrtService { constructor( private readonly kodiApiResponseFactory: KodiApiResponseFactory, + private readonly lrtApiClient: LrtApiClient, ) {} + getMainMenu(): ApiResponse { const apiResponse = this.kodiApiResponseFactory.createApiResponse(); apiResponse.setNoSort().setTitle('LRT.lt'); @@ -17,7 +20,6 @@ export class LrtService { private createMainMenu(apiResponse: ApiResponse): void { const items = { - 'lrt/recent': 'neseniai ieškoti', 'lrt/tema/vaikams': 'vaikams', 'lrt/tema/sportas': 'sportas', 'lrt/tema/kultura': 'kultura', @@ -35,4 +37,49 @@ export class LrtService { apiResponse.createItem().setLabel(label).setToFolder().setPath(path); } } + + async searchCategories(query: string): Promise { + 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 { + 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 { + const playableUrl = await this.lrtApiClient.getPlaylist(mediaUrl); + const apiResponse = this.kodiApiResponseFactory.createApiResponse(); + apiResponse.setTitle('LRT.lt'); + apiResponse.setToPlayable(playableUrl); + + return apiResponse; + } }