• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Nikorag / iplayarr / 14473087310

15 Apr 2025 03:16PM UTC coverage: 33.286% (+10.4%) from 22.916%
14473087310

push

github

web-flow
Merge pull request #104 from Nikorag/0425_tests

120 of 500 branches covered (24.0%)

Branch coverage included in aggregate %.

160 of 215 new or added lines in 9 files covered. (74.42%)

1 existing line in 1 file now uncovered.

576 of 1591 relevant lines covered (36.2%)

1.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

40.16
/src/service/searchService.ts
1
import axios, { AxiosResponse } from 'axios';
1✔
2

3
import { IplayarrParameter } from '../types/IplayarrParameters';
1✔
4
import { IPlayerDetails } from '../types/IPlayerDetails';
5
import { IPlayerSearchResult, VideoType } from '../types/IPlayerSearchResult';
1✔
6
import { IPlayerNewSearchResponse } from '../types/responses/iplayer/IPlayerNewSearchResponse';
7
import { IPlayerChilrenResponse } from '../types/responses/IPlayerMetadataResponse';
8
import { Synonym } from '../types/Synonym';
9
import { createNZBName, getQualityProfile, removeLastFourDigitNumber, splitArrayIntoChunks } from '../utils/Utils';
1✔
10
import configService from './configService';
1✔
11
import episodeCacheService from './episodeCacheService';
1✔
12
import iplayerService from './iplayerService';
1✔
13
import RedisCacheService from './redisCacheService';
1✔
14
import synonymService from './synonymService';
1✔
15

16
interface SearchTerm {
17
    term: string,
18
    synonym?: Synonym
19
}
20

21
export class SearchService {
1✔
22
    searchCache: RedisCacheService<IPlayerSearchResult[]> = new RedisCacheService('search_cache', 300);
1✔
23

24
    async search(inputTerm: string, season?: number, episode?: number): Promise<IPlayerSearchResult[]> {
25
        const nativeSearchEnabled = await configService.getParameter(IplayarrParameter.NATIVE_SEARCH);
5✔
26
        const { term, synonym } = await this.#getTerm(inputTerm, season);
5✔
27

28
        let results: IPlayerSearchResult[] | undefined = await this.searchCache.get(term);
5✔
29
        if (!results) {
5✔
30
            const service = (term == '*' || nativeSearchEnabled != 'true') ? iplayerService : this;
3✔
31
            results = await service.performSearch(term, synonym);
3✔
32
            this.searchCache.set(term, results as IPlayerSearchResult[]);
3✔
33
        } else {
34
            //Fix the results which are stored as string
35
            results.forEach(result => {
2✔
36
                result.pubDate = new Date((result.pubDate as unknown as string));
4✔
37
            });
38
        }
39

40
        const filteredResults = await this.#filterForSeasonAndEpisode(results as IPlayerSearchResult[], season, episode);
5✔
41

42
        if (nativeSearchEnabled == 'false') {
5✔
43
            const episodeCache: IPlayerSearchResult[] = await episodeCacheService.searchEpisodeCache(inputTerm);
3✔
44
            for (const cachedEpisode of episodeCache) {
3✔
NEW
45
                if (cachedEpisode) {
×
NEW
46
                    const exists = filteredResults.some(({ pid }) => pid == cachedEpisode.pid);
×
NEW
47
                    const validSeason = season ? cachedEpisode.series == season : true;
×
NEW
48
                    const validEpisode = episode ? cachedEpisode.episode == episode : true;
×
NEW
49
                    if (!exists && validSeason && validEpisode) {
×
NEW
50
                        filteredResults.push({ ...cachedEpisode, pubDate: cachedEpisode.pubDate ? new Date(cachedEpisode.pubDate) : undefined });
×
51
                    }
52
                }
53
            }
54
        }
55

56
        return filteredResults.filter(({ pubDate }) => !pubDate || pubDate < new Date());
5✔
57
    }
58

59
    async performSearch(term: string, synonym?: Synonym): Promise<IPlayerSearchResult[]> {
NEW
60
        const { sizeFactor } = await getQualityProfile();
×
NEW
61
        const url = `https://ibl.api.bbc.co.uk/ibl/v1/new-search?q=${encodeURIComponent(term)}`;
×
NEW
62
        const response: AxiosResponse<IPlayerNewSearchResponse> = await axios.get(url);
×
NEW
63
        if (response.status == 200) {
×
NEW
64
            const { new_search: { results } } = response.data;
×
NEW
65
            const brandPids: Set<string> = new Set();
×
NEW
66
            let infos: IPlayerDetails[] = [];
×
67

68
            //Only get the first brand from iplayer, if we 
NEW
69
            if (results.length > 0) {
×
NEW
70
                const { id } = results[0];
×
NEW
71
                const brandPid = await episodeCacheService.findBrandForPid(id);
×
NEW
72
                if (brandPid) {
×
NEW
73
                    brandPids.add(brandPid);
×
74
                } else {
NEW
75
                    const pidInfos = await iplayerService.details([id]);
×
NEW
76
                    infos = [...infos, ...pidInfos];
×
77
                }
78
            }
79

NEW
80
            for (const brandPid of brandPids) {
×
NEW
81
                const { data: { children: seriesList } }: { data: IPlayerChilrenResponse } = await axios.get(`https://www.bbc.co.uk/programmes/${encodeURIComponent(brandPid)}/children.json?limit=100`);
×
NEW
82
                const episodes = (await Promise.all(seriesList.programmes.filter(({ type, title }) => type == 'series' && !title.toLocaleLowerCase().includes('special')).map(({ pid }) => episodeCacheService.getSeriesEpisodes(pid)))).flat();
×
83

NEW
84
                const chunks = splitArrayIntoChunks(episodes, 5);
×
NEW
85
                const chunkInfos = await chunks.reduce(async (accPromise, chunk) => {
×
NEW
86
                    const acc = await accPromise; // Ensure previous results are awaited
×
NEW
87
                    const results: IPlayerDetails[] = await iplayerService.details(chunk);
×
NEW
88
                    return [...acc, ...results];
×
89
                }, Promise.resolve([])); // Initialize accumulator as a resolved Promise
90

NEW
91
                infos = [...infos, ...chunkInfos];
×
92
            }
93

NEW
94
            const synonymName = synonym ? (synonym.filenameOverride || synonym.from).replaceAll(/[^a-zA-Z0-9\s.]/g, '').replaceAll(' ', '.') : undefined;
×
NEW
95
            return await Promise.all(infos.map((info: IPlayerDetails) => this.#createSearchResult(info.title, info, sizeFactor, synonymName)));
×
96

97
        } else {
NEW
98
            return [];
×
99
        }
100
    }
101

102
    async #getTerm(inputTerm: string, season?: number): Promise<SearchTerm> {
1✔
103
        const term = !season ? removeLastFourDigitNumber(inputTerm) : inputTerm;
5✔
104
        const synonym = await synonymService.getSynonym(inputTerm);
5✔
105
        return {
5✔
106
            term: synonym ? synonym.target : term,
5✔
107
            synonym
108
        }
109
    }
110

111
    async #filterForSeasonAndEpisode(results: IPlayerSearchResult[], season?: number, episode?: number) {
112
        return results.filter((result) => {
5✔
113
            return ((!season || result.series == season) && (!episode || result.episode == episode))
7✔
114
        })
115
    }
116

117
    async #createSearchResult(term: string, details: IPlayerDetails, sizeFactor: number, synonymName: string | undefined): Promise<IPlayerSearchResult> {
NEW
118
        const size: number | undefined = details.runtime ? (details.runtime * 60) * sizeFactor : undefined;
×
119

NEW
120
        const type: VideoType = details.episode && details.series ? VideoType.TV : VideoType.MOVIE;
×
NEW
121
        const nzbName = await createNZBName(type, {
×
122
            title: details.title.replaceAll(' ', '.'),
123
            season: details.series ? details.series.toString().padStart(2, '0') : undefined,
×
124
            episode: details.episode ? details.episode.toString().padStart(2, '0') : undefined,
×
125
            synonym: synonymName
126
        });
127

NEW
128
        return {
×
129
            number: 0,
130
            title: details.title,
131
            channel: details.channel || '',
×
132
            pid: details.pid,
133
            request: {
134
                term,
135
                line: term
136
            },
137
            episode: details.episode,
138
            pubDate: details.firstBroadcast ? new Date(details.firstBroadcast) : undefined,
×
139
            series: details.series,
140
            type,
141
            size,
142
            nzbName
143
        }
144
    }
145

146
    removeFromSearchCache(term: string) {
NEW
147
        this.searchCache.del(term);
×
148
    }
149
}
150

151
export default new SearchService();
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc