• 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

90.63
/src/service/getIplayerExecutableService.ts
1
import fs from 'fs';
4✔
2
import path from 'path';
4✔
3

4
import { listFormat, progressRegex } from '../constants/iPlayarrConstants';
4✔
5
import { DownloadDetails } from '../types/DownloadDetails';
6
import { GetIPlayerExecutable } from '../types/GetIplayer/GetIPlayerExecutable';
7
import { IplayarrParameter } from '../types/IplayarrParameters';
4✔
8
import { IPlayerSearchResult, VideoType } from '../types/IPlayerSearchResult';
4✔
9
import { LogLine, LogLineLevel } from '../types/LogLine';
4✔
10
import { QueueEntry } from '../types/QueueEntry';
11
import { Synonym } from '../types/Synonym';
12
import { createNZBName, extractSeriesNumber } from '../utils/Utils';
4✔
13
import configService from './configService';
4✔
14
import historyService from './historyService';
4✔
15
import loggingService from './loggingService';
4✔
16
import queueService from './queueService';
4✔
17
import socketService from './socketService';
4✔
18

19
export class GetIplayerExecutableService {
4✔
20
    async getIPlayerExec(): Promise<GetIPlayerExecutable> {
21
        const fullExec: string = await configService.getParameter(IplayarrParameter.GET_IPLAYER_EXEC) as string;
5✔
22
        const args: RegExpMatchArray = fullExec.match(/(?:[^\s"]+|"[^"]*")+/g) as RegExpMatchArray;
5✔
23

24
        const exec: string = args.shift() as string;
5✔
25

26
        const cacheLocation = process.env.CACHE_LOCATION;
5✔
27
        if (cacheLocation) {
5✔
28
            args.push('--profile-dir');
1✔
29
            args.push(`"${cacheLocation}"`);
1✔
30
        }
31

32
        return {
5✔
33
            exec,
34
            args
35
        }
36
    }
37

38
    async #getQualityParam(): Promise<string> {
4✔
39
        const videoQuality = await configService.getParameter(IplayarrParameter.VIDEO_QUALITY) as string;
1✔
40
        return `--tv-quality=${videoQuality}`;
1✔
41
    }
42

43
    async getAllDownloadParameters(pid: string): Promise<GetIPlayerExecutable> {
44
        const downloadDir: string = await configService.getParameter(IplayarrParameter.DOWNLOAD_DIR) as string;
1✔
45

46
        const { exec, args } = await this.getIPlayerExec();
1✔
47
        const additionalParamsString: string = await configService.getParameter(IplayarrParameter.ADDITIONAL_IPLAYER_DOWNLOAD_PARAMS) as string;
1✔
48
        const additionalParams: string[] = additionalParamsString ? additionalParamsString.split(' ') : [];
1!
49

50
        const allArgs: string[] = [...args, ...additionalParams, await this.#getQualityParam(), '--output', `${downloadDir}/${pid}`, '--overwrite', '--force', '--log-progress', `--pid=${pid}`];
1✔
51

52
        return {
1✔
53
            exec,
54
            args: allArgs
55
        }
56
    }
57

58
    async getSearchParameters(term: string, synonym?: Synonym) {
59
        const { exec, args } = await this.getIPlayerExec();
2✔
60
        const exemptionArgs: string[] = [];
2✔
61
        if (synonym && synonym.exemptions) {
2✔
62
            const exemptions = synonym.exemptions.split(',');
1✔
63
            for (const exemption of exemptions) {
1✔
64
                exemptionArgs.push('--exclude');
2✔
65
                exemptionArgs.push(`"${exemption}"`);
2✔
66
            }
67
        }
68
        if (term == '*') {
2✔
69
            const rssHours: string = (await configService.getParameter(IplayarrParameter.RSS_FEED_HOURS)) as string;
1✔
70
            (args as RegExpMatchArray).push('--available-since');
1✔
71
            (args as RegExpMatchArray).push(rssHours);
1✔
72
        }
73
        const allArgs = [...args, '--listformat', `"${listFormat}"`, ...exemptionArgs, `"${term}"`];
2✔
74

75
        return {
2✔
76
            exec,
77
            args: allArgs
78
        }
79
    }
80

81
    logProgress(pid: string, data : any) {
82
        console.log(data.toString());
1✔
83
        const logLine: LogLine = { level: LogLineLevel.INFO, id: pid, message: data.toString(), timestamp: new Date() }
1✔
84
        socketService.emit('log', logLine);
1✔
85
    }
86

87
    parseProgress(pid: string, data: any): DownloadDetails | undefined {
88
        const lines: string[] = data.toString().split('\n');
2✔
89
        const progressLines: string[] = lines.filter((l) => progressRegex.exec(l));
2✔
90
        if (progressLines.length > 0) {
2✔
91
            const progressLine: string = progressLines.pop() as string;
1✔
92
            const match = progressRegex.exec(progressLine);
1✔
93
            if (match) {
1✔
94

95
                // eslint-disable-next-line @typescript-eslint/no-unused-vars
96
                const [_, progress, size, speed, eta] = match;
1✔
97
                const percentFactor = (100 - parseFloat(progress)) / 100;
1✔
98
                const sizeLeft = parseFloat(size) * percentFactor;
1✔
99

100
                const deltaDetails: Partial<DownloadDetails> = {
1✔
101
                    uuid: pid,
102
                    progress: parseFloat(progress),
103
                    size: parseFloat(size),
104
                    speed: parseFloat(speed),
105
                    eta,
106
                    sizeLeft
107
                }
108

109
                return deltaDetails;
1✔
110
            }
111
        }
112
        return;
1✔
113
    }
114

115
    async processCompletedDownload(pid: string, code: number | null): Promise<void> {
116
        const [downloadDir, completeDir] = await configService.getParameters(IplayarrParameter.DOWNLOAD_DIR, IplayarrParameter.COMPLETE_DIR) as string[];
2✔
117
        if (code === 0) {
2✔
118
            const queueItem: QueueEntry | undefined = queueService.getFromQueue(pid);
2✔
119
            if (queueItem) {
2✔
120
                try {
2✔
121
                    const uuidPath = path.join(downloadDir, pid);
2✔
122
                    loggingService.debug(pid, `Looking for MP4 files in ${uuidPath}`);
2✔
123
                    const files = fs.readdirSync(uuidPath);
2✔
124
                    const mp4File = files.find(file => file.endsWith('.mp4'));
2✔
125

126
                    if (mp4File) {
2✔
127
                        const oldPath = path.join(uuidPath, mp4File);
1✔
128
                        loggingService.debug(pid, `Found MP4 file ${oldPath}`);
1✔
129
                        const newPath = path.join(completeDir, `${queueItem?.nzbName}.mp4`);
1✔
130
                        loggingService.debug(pid, `Moving ${oldPath} to ${newPath}`);
1✔
131

132
                        fs.copyFileSync(oldPath, newPath);
1✔
133
                    }
134

135
                    // Delete the uuid directory and file after moving it
136
                    loggingService.debug(pid, `Deleting old directory ${uuidPath}`);
2✔
137
                    fs.rmSync(uuidPath, { recursive: true, force: true });
2✔
138

139
                    await historyService.addHistory(queueItem);
2✔
140
                } catch (err) {
NEW
141
                    loggingService.error(err);
×
142
                }
143
            }
144
        }
145
        queueService.removeFromQueue(pid);
2✔
146
    }
147

148
    parseResults(term: string, data: any, sizeFactor: number): IPlayerSearchResult[] {
149
        const results: IPlayerSearchResult[] = [];
1✔
150
        const lines: string[] = data.toString().split('\n');
1✔
151
        for (const line of lines) {
1✔
152
            if (line.startsWith('RESULT|:|')) {
1✔
153
                // eslint-disable-next-line @typescript-eslint/no-unused-vars
154
                const [_, pid, rawTitle, seriesStr, episodeStr, number, channel, durationStr, onlineFrom] = line.split('|:|');
1✔
155
                const episode: number | undefined = (episodeStr == '' ? undefined : parseInt(episodeStr));
1!
156
                const [title, series] = (seriesStr == '' ? [rawTitle, undefined] : extractSeriesNumber(rawTitle, seriesStr))
1!
157
                const type: VideoType = episode && series ? VideoType.TV : VideoType.MOVIE;
1!
158
                const size: number | undefined = durationStr ? parseInt(durationStr) * sizeFactor : undefined;
1!
159
                results.push({
1✔
160
                    pid,
161
                    title,
162
                    channel,
163
                    number: parseInt(number),
164
                    request: { term, line },
165
                    episode,
166
                    series,
167
                    type,
168
                    size,
169
                    pubDate: onlineFrom ? new Date(onlineFrom) : undefined
1!
170
                });
171
            }
172
        }
173
        return results;
1✔
174
    }
175

176
    async processCompletedSearch(results : IPlayerSearchResult[], synonym? : Synonym) : Promise<IPlayerSearchResult[]> {
177
        for (const result of results) {
1✔
178
            const synonymName = synonym ? (synonym.filenameOverride || synonym.from).replaceAll(/[^a-zA-Z0-9\s.]/g, '').replaceAll(' ', '.') : undefined;
1!
179

180
            const nzbName = await createNZBName(result.type, {
1✔
181
                title: result.title.replaceAll(' ', '.'),
182
                season: result.series ? result.series.toString().padStart(2, '0') : undefined,
1!
183
                episode: result.episode ? result.episode.toString().padStart(2, '0') : undefined,
1!
184
                synonym: synonymName
185
            });
186
            result.nzbName = nzbName;
1✔
187
        }
188
        return results;
1✔
189
    }
190
}
191

192
export default new GetIplayerExecutableService();
4✔
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