• 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.74
/src/service/iplayerService.ts
1
import { ChildProcess, spawn } from 'child_process';
5✔
2
import fs from 'fs';
5✔
3
import path from 'path';
5✔
4

5
import { nativeSeriesRegex, timestampFile } from '../constants/iPlayarrConstants';
5✔
6
import { DownloadDetails } from '../types/DownloadDetails';
7
import { IplayarrParameter } from '../types/IplayarrParameters';
5✔
8
import { IPlayerDetails } from '../types/IPlayerDetails';
9
import { IPlayerSearchResult } from '../types/IPlayerSearchResult';
10
import { Synonym } from '../types/Synonym';
11
import { getPotentialRoman, getQualityProfile } from '../utils/Utils';
5✔
12
import configService from './configService';
5✔
13
import episodeCacheService from './episodeCacheService';
5✔
14
import getIplayerExecutableService from './getIplayerExecutableService';
5✔
15
import loggingService from './loggingService';
5✔
16
import queueService from './queueService';
5✔
17

18
const iplayerService = {
5✔
19
    createPidDirectory: async (pid : string): Promise<void> => {
20
        const downloadDir : string = await configService.getParameter(IplayarrParameter.DOWNLOAD_DIR) as string;
2✔
21
        fs.mkdirSync(`${downloadDir}/${pid}`, { recursive: true });
2✔
22
        fs.writeFileSync(`${downloadDir}/${pid}/${timestampFile}`, '');
2✔
23
    },
24

25
    download: async (pid: string): Promise<ChildProcess> => {
26
        const {exec, args} = await getIplayerExecutableService.getAllDownloadParameters(pid);
1✔
27

28
        await iplayerService.createPidDirectory(pid);
1✔
29
        loggingService.debug(`Executing get_iplayer with args: ${args.join(' ')}`);
1✔
30
        const downloadProcess = spawn(exec, args);
1✔
31

32
        downloadProcess.stdout.on('data', (data) => {
1✔
33
            if (queueService.getFromQueue(pid)) {
×
NEW
34
                getIplayerExecutableService.logProgress(pid, data);
×
NEW
35
                const downloadDetails : DownloadDetails | undefined = getIplayerExecutableService.parseProgress(pid, data);
×
NEW
36
                if (downloadDetails){
×
NEW
37
                    queueService.updateQueue(pid, downloadDetails);
×
38
                }
39
            }
40
        });
41

42
        downloadProcess.on('close', (code) => getIplayerExecutableService.processCompletedDownload(pid, code));
1✔
43

44
        return downloadProcess;
1✔
45
    },
46

47
    refreshCache: async () => {
48
        const {exec, args} = await getIplayerExecutableService.getIPlayerExec();
1✔
49

50
        //Refresh the cache
51
        loggingService.debug(`Executing get_iplayer with args: ${[...args].join(' ')} --cache-rebuild`);
1✔
52
        const refreshService = spawn(exec as string, [...args, '--cache-rebuild'], { shell: true });
1✔
53

54
        refreshService.stdout.on('data', (data) => {
1✔
55
            loggingService.debug(data.toString());
×
56
        });
57

58
        refreshService.stderr.on('data', (data) => {
1✔
59
            loggingService.error(data.toString());
×
60
        });
61

62
        //Delete failed jobs
63
        iplayerService.cleanupFailedDownloads();
1✔
64
    },
65

66
    cleanupFailedDownloads: async() : Promise<void> => {
NEW
67
        const downloadDir = await configService.getParameter(IplayarrParameter.DOWNLOAD_DIR) as string;
×
68
        const threeHoursAgo: number = Date.now() - 3 * 60 * 60 * 1000;
×
69
        fs.readdir(downloadDir, { withFileTypes: true }, (err, entries) => {
×
70
            if (err) {
×
71
                console.error('Error reading directory:', err);
×
72
                return;
×
73
            }
74

75
            entries.forEach(entry => {
×
76
                if (!entry.isDirectory()) return;
×
77

78
                const dirPath: string = path.join(downloadDir, entry.name);
×
79
                const filePath: string = path.join(dirPath, timestampFile);
×
80

81
                fs.stat(filePath, (err, stats) => {
×
82
                    if (err) {
×
83
                        // Ignore missing files
84
                        if (err.code !== 'ENOENT') console.error(`Error checking ${filePath}:`, err);
×
85
                        return;
×
86
                    }
87

88
                    if (stats.mtimeMs < threeHoursAgo) {
×
89
                        fs.rm(dirPath, { recursive: true, force: true }, (err) => {
×
90
                            if (err) {
×
91
                                loggingService.error(`Error deleting ${dirPath}:`, err);
×
92
                            } else {
93
                                loggingService.log(`Deleted old directory: ${dirPath}`);
×
94
                            }
95
                        });
96
                    }
97
                });
98
            });
99
        });
100
    },
101

102
    details: async (pids: string[]): Promise<IPlayerDetails[]> => {
103
        return await Promise.all(pids.map((pid) => iplayerService.episodeDetails(pid)));
×
104
    },
105

106
    episodeDetails: async (pid: string): Promise<IPlayerDetails> => {
107
        const { programme } = await episodeCacheService.getMetadata(pid);
1✔
108
        const runtime = programme.versions ? (programme.versions[0].duration / 60) : 0;
1!
109
        const category = programme.categories ? programme.categories[0].title : '';
1!
110

111
        //Get the series number, we'll override with a series name "Series X" to avoid christmas specials
112
        const seriesName: string | undefined = programme.parent?.programme?.type == 'series' ? programme.parent?.programme?.title : undefined;
1!
113
        const seriesMatch = seriesName?.match(nativeSeriesRegex);
1✔
114

115
        const series = seriesMatch ? getPotentialRoman(seriesMatch[1]) : programme.parent?.programme?.position;
1!
116
        const episode = programme.position ?? (series ? programme.parent?.programme?.aggregated_episode_count : undefined);
1!
117
        return {
1✔
118
            pid,
119
            title: programme.display_title?.title ?? programme.title,
1!
120
            episode,
121
            series,
122
            channel: programme.ownership?.service?.title,
123
            category,
124
            description: programme.medium_synopsis,
125
            runtime,
126
            firstBroadcast: programme.first_broadcast_date,
127
            link: `https://www.bbc.co.uk/programmes/${pid}`,
128
            thumbnail: programme.image ? `https://ichef.bbci.co.uk/images/ic/1920x1080/${programme.image.pid}.jpg` : undefined
1!
129
        }
130
    },
131

132
    performSearch: async (term: string, synonym?: Synonym): Promise<IPlayerSearchResult[]> => {
133
        const { sizeFactor } = await getQualityProfile();
×
134
        return new Promise(async (resolve, reject) => {
×
135
            const results: IPlayerSearchResult[] = []
×
NEW
136
            const {exec, args} = await getIplayerExecutableService.getSearchParameters(term, synonym);
×
137

NEW
138
            loggingService.debug(`Executing get_iplayer with args: ${args.join(' ')}`);
×
NEW
139
            const searchProcess = spawn(exec as string, args, { shell: true });
×
140

141
            searchProcess.stdout.on('data', (data) => {
×
142
                loggingService.debug(data.toString().trim());
×
NEW
143
                const chunkResults : IPlayerSearchResult[] = getIplayerExecutableService.parseResults(term, data, sizeFactor);
×
NEW
144
                chunkResults.forEach((chunk) => results.push(chunk));
×
145
            });
146

147
            searchProcess.stderr.on('data', (data) => {
×
148
                loggingService.error(data.toString().trim());
×
149
            });
150

151
            searchProcess.on('close', async (code) => {
×
152
                if (code === 0) {
×
NEW
153
                    const processedResults : IPlayerSearchResult[] = await getIplayerExecutableService.processCompletedSearch(results, synonym);
×
154
                    
NEW
155
                    resolve(processedResults);
×
156
                } else {
157
                    reject(new Error(`Process exited with code ${code}`));
×
158
                }
159
            });
160
        });
161
    }
162
}
163

164
export default iplayerService;
5✔
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