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

xxczaki / discord-bot / 21551166906

31 Jan 2026 09:24PM UTC coverage: 93.705% (-0.5%) from 94.206%
21551166906

push

github

xxczaki
revamp caching system

968 of 1085 branches covered (89.22%)

Branch coverage included in aggregate %.

124 of 139 new or added lines in 8 files covered. (89.21%)

2128 of 2219 relevant lines covered (95.9%)

11.91 hits per line

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

83.82
/src/utils/OpusCacheManager.ts
1
import { existsSync, mkdirSync } from 'node:fs';
2
import { readdir, unlink } from 'node:fs/promises';
3
import { join } from 'node:path';
4
import Fuse from 'fuse.js';
5
import { transliterate } from 'transliteration';
6
import getEnvironmentVariable from './getEnvironmentVariable';
7
import logger from './logger';
8
import reportError from './reportError';
9
import sanitizeForFilename from './sanitizeForFilename';
10

11
export interface CacheEntry {
12
        filename: string;
13
        title: string;
14
        author: string;
15
        durationSeconds: number | null;
16
}
17

18
interface ParsedCacheFilename {
19
        title: string;
20
        author: string;
21
        durationSeconds: number | null;
22
}
23

24
interface VerifyMatchOptions {
25
        entry: string;
26
        title: string;
27
        author: string;
28
}
29

30
export interface TrackMetadata {
31
        title: string;
32
        author: string;
33
        durationMS: number;
34
}
35

36
const FUSE_THRESHOLD = 0.6;
2✔
37
const DURATION_TOLERANCE_SECONDS = 5;
2✔
38
const MAX_TITLE_LENGTH = 100;
2✔
39
const MAX_AUTHOR_LENGTH = 50;
2✔
40

41
const STOP_WORDS = new Set([
2✔
42
        'the',
43
        'and',
44
        'for',
45
        'from',
46
        'with',
47
        'version',
48
        'remaster',
49
        'remastered',
50
        'single',
51
        'topic',
52
        'live',
53
        'official',
54
        'audio',
55
        'video',
56
        'edit',
57
        'radio',
58
]);
59

60
export class OpusCacheManager {
61
        static #instance: OpusCacheManager;
62
        static #directoryPath: string | null = null;
2✔
63

64
        #entries: CacheEntry[] = [];
31✔
65
        #fuse: Fuse<CacheEntry> | null = null;
31✔
66

67
        constructor(private cacheDirectory: string) {}
31✔
68

69
        static getDirectoryPath(): string {
70
                if (OpusCacheManager.#directoryPath) {
2!
NEW
71
                        return OpusCacheManager.#directoryPath;
×
72
                }
73

74
                if (getEnvironmentVariable('NODE_ENV') !== 'development') {
2!
75
                        OpusCacheManager.#directoryPath = '/opus-cache';
2✔
76
                        return OpusCacheManager.#directoryPath;
2✔
77
                }
78

NEW
79
                const directory = join(import.meta.dirname, 'opus-cache');
×
80

NEW
81
                if (!existsSync(directory)) {
×
NEW
82
                        mkdirSync(directory);
×
NEW
83
                        logger.info(`Initialized a development-only Opus cache at ${directory}.`);
×
84
                }
85

NEW
86
                OpusCacheManager.#directoryPath = directory;
×
NEW
87
                return OpusCacheManager.#directoryPath;
×
88
        }
89

90
        static initialize(cacheDirectory?: string): OpusCacheManager {
91
                if (!OpusCacheManager.#instance) {
1!
92
                        const directory = cacheDirectory ?? OpusCacheManager.getDirectoryPath();
1✔
93
                        OpusCacheManager.#instance = new OpusCacheManager(directory);
1✔
94
                }
95

96
                return OpusCacheManager.#instance;
1✔
97
        }
98

99
        static getInstance(): OpusCacheManager {
NEW
100
                if (!OpusCacheManager.#instance) {
×
NEW
101
                        throw new Error('OpusCacheManager not initialized');
×
102
                }
103

NEW
104
                return OpusCacheManager.#instance;
×
105
        }
106

107
        async scan(): Promise<void> {
108
                try {
14✔
109
                        const files = await readdir(this.cacheDirectory);
14✔
110
                        const opusFiles = files.filter((file) => file.endsWith('.opus'));
14✔
111

112
                        for (const filename of opusFiles) {
14✔
113
                                const parsed = this.#parseFilename(filename);
11✔
114

115
                                if (parsed) {
11!
116
                                        this.#entries.push({
11✔
117
                                                filename,
118
                                                title: parsed.title,
119
                                                author: parsed.author,
120
                                                durationSeconds: parsed.durationSeconds,
121
                                        });
122
                                }
123
                        }
124

125
                        this.#rebuildFuseIndex();
14✔
126

127
                        logger.info(
14✔
128
                                { entryCount: this.#entries.length },
129
                                'Opus cache index initialized',
130
                        );
131
                } catch (error) {
NEW
132
                        logger.error(error, 'Failed to initialize opus cache index');
×
133
                }
134
        }
135

136
        #rebuildFuseIndex(): void {
137
                this.#fuse = new Fuse(this.#entries, {
18✔
138
                        keys: [
139
                                { name: 'title', weight: 0.7 },
140
                                { name: 'author', weight: 0.3 },
141
                        ],
142
                        threshold: FUSE_THRESHOLD,
143
                        includeScore: true,
144
                });
145
        }
146

147
        findMatch(
148
                title: string,
149
                author: string,
150
                durationSeconds: number,
151
        ): CacheEntry | null {
152
                if (!this.#fuse || this.#entries.length === 0) {
10✔
153
                        return null;
1✔
154
                }
155

156
                const searchQuery = this.#normalizeForSearch(`${title} ${author}`);
9✔
157
                const results = this.#fuse.search(searchQuery);
9✔
158

159
                for (const result of results) {
9✔
160
                        const entry = result.item;
9✔
161

162
                        if (!this.#verifyMatch({ entry: entry.title, title, author })) {
9✔
163
                                continue;
1✔
164
                        }
165

166
                        if (entry.durationSeconds === null && durationSeconds === 0) {
8✔
167
                                return entry;
1✔
168
                        }
169

170
                        if (entry.durationSeconds === null) {
7✔
171
                                continue;
1✔
172
                        }
173

174
                        const durationDiff = Math.abs(entry.durationSeconds - durationSeconds);
6✔
175

176
                        if (durationDiff <= DURATION_TOLERANCE_SECONDS) {
6✔
177
                                return entry;
5✔
178
                        }
179
                }
180

181
                return null;
3✔
182
        }
183

184
        addEntry(entry: CacheEntry): void {
185
                const existingIndex = this.#entries.findIndex(
3✔
186
                        (existing) => existing.filename === entry.filename,
1✔
187
                );
188

189
                if (existingIndex !== -1) {
3✔
190
                        this.#entries[existingIndex] = entry;
1✔
191
                } else {
192
                        this.#entries.push(entry);
2✔
193
                }
194

195
                this.#rebuildFuseIndex();
3✔
196
        }
197

198
        removeEntry(filename: string): void {
199
                const index = this.#entries.findIndex(
2✔
200
                        (entry) => entry.filename === filename,
1✔
201
                );
202

203
                if (index !== -1) {
2✔
204
                        this.#entries.splice(index, 1);
1✔
205
                        this.#rebuildFuseIndex();
1✔
206
                }
207
        }
208

209
        async deleteEntry(filename: string | undefined): Promise<void> {
210
                if (!filename) {
6✔
211
                        return;
2✔
212
                }
213

214
                const filePath = this.getFilePath(filename);
4✔
215

216
                try {
4✔
217
                        await unlink(filePath);
4✔
218
                        this.removeEntry(filename);
1✔
219
                } catch (error) {
220
                        if (error instanceof Error && error.message.includes('ENOENT')) {
3✔
221
                                return;
1✔
222
                        }
223

224
                        reportError(error, 'Failed to delete Opus cache entry');
2✔
225
                }
226
        }
227

228
        getFilePath(filename: string): string {
229
                return join(this.cacheDirectory, filename);
5✔
230
        }
231

232
        generateFilename(metadata: TrackMetadata): string {
233
                const title = sanitizeForFilename(
8✔
234
                        metadata.title || 'unknown_title',
9✔
235
                        MAX_TITLE_LENGTH,
236
                );
237
                const author = sanitizeForFilename(
8✔
238
                        metadata.author || 'unknown_artist',
9✔
239
                        MAX_AUTHOR_LENGTH,
240
                );
241
                const durationSeconds = Math.round(metadata.durationMS / 1000);
8✔
242

243
                if (durationSeconds === 0) {
8✔
244
                        return `${title}_${author}.opus`;
1✔
245
                }
246

247
                return `${title}_${author}_${durationSeconds}.opus`;
7✔
248
        }
249

250
        get entryCount(): number {
251
                return this.#entries.length;
5✔
252
        }
253

254
        get directory(): string {
255
                return this.cacheDirectory;
1✔
256
        }
257

258
        #normalizeForSearch(input: string): string {
259
                return transliterate(input).toLowerCase();
36✔
260
        }
261

262
        #getSignificantWords(text: string): string[] {
263
                return this.#normalizeForSearch(text)
18✔
264
                        .split(/[\s\-_()]+/)
265
                        .filter((word) => word.length >= 3 && !STOP_WORDS.has(word));
33✔
266
        }
267

268
        #verifyMatch({ entry, title, author }: VerifyMatchOptions): boolean {
269
                const entryNormalized = this.#normalizeForSearch(entry);
9✔
270
                const titleWords = this.#getSignificantWords(title);
9✔
271
                const authorWords = this.#getSignificantWords(author);
9✔
272

273
                const titleMatches = titleWords.filter((word) =>
9✔
274
                        entryNormalized.includes(word),
18✔
275
                ).length;
276
                const authorMatches = authorWords.filter((word) =>
9✔
277
                        entryNormalized.includes(word),
9✔
278
                ).length;
279

280
                return (
9✔
281
                        titleMatches >= Math.ceil(titleWords.length / 2) &&
17✔
282
                        authorMatches === authorWords.length
283
                );
284
        }
285

286
        #parseFilename(filename: string): ParsedCacheFilename | null {
287
                if (!filename.endsWith('.opus')) {
11!
NEW
288
                        return null;
×
289
                }
290

291
                const nameWithoutExtension = filename.slice(0, -5);
11✔
292
                const parts = nameWithoutExtension.split('_');
11✔
293

294
                if (parts.length < 2) {
11!
NEW
295
                        return null;
×
296
                }
297

298
                const lastPart = parts.at(-1);
11✔
299
                const durationSeconds = lastPart
11!
300
                        ? Number.parseInt(lastPart, 10)
301
                        : Number.NaN;
302
                const hasDuration = !Number.isNaN(durationSeconds);
11✔
303

304
                if (hasDuration) {
11✔
305
                        const textParts = parts.slice(0, -1);
9✔
306
                        const combinedText = textParts.join(' ');
9✔
307

308
                        if (!combinedText) {
9!
NEW
309
                                return null;
×
310
                        }
311

312
                        return { title: combinedText, author: '', durationSeconds };
9✔
313
                }
314

315
                const combinedText = parts.join(' ');
2✔
316

317
                if (!combinedText) {
2!
NEW
318
                        return null;
×
319
                }
320

321
                return { title: combinedText, author: '', durationSeconds: null };
2✔
322
        }
323
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc