mirror of
https://github.com/sakaljurgis/kodi-api-server.git
synced 2026-07-08 20:37:41 +00:00
implement all lrt client functions
This commit is contained in:
@@ -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<ContextMenu>;
|
||||
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 = [];
|
||||
|
||||
@@ -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;
|
||||
category_img_path_postfix: string;
|
||||
age_restriction: string;
|
||||
@@ -29,4 +29,5 @@ interface SearchResponseItemInterface {
|
||||
n18: number;
|
||||
subtitle: string;
|
||||
photo_count: number;
|
||||
category_id: string;
|
||||
}
|
||||
|
||||
@@ -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<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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ApiResponse> {
|
||||
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<ApiResponse> {
|
||||
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<ApiResponse> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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<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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user