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

xxczaki / discord-bot / 21689294588

04 Feb 2026 09:35PM UTC coverage: 93.613% (-0.09%) from 93.701%
21689294588

push

github

xxczaki
fix

972 of 1091 branches covered (89.09%)

Branch coverage included in aggregate %.

2135 of 2228 relevant lines covered (95.83%)

11.88 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!
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

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

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

86
                OpusCacheManager.#directoryPath = directory;
×
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 {
100
                if (!OpusCacheManager.#instance) {
×
101
                        OpusCacheManager.#instance = new OpusCacheManager(
×
102
                                OpusCacheManager.getDirectoryPath(),
103
                        );
104
                }
105

106
                return OpusCacheManager.#instance;
×
107
        }
108

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

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

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

127
                        this.#rebuildFuseIndex();
14✔
128

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

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

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

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

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

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

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

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

176
                        const durationDiff = Math.abs(entry.durationSeconds - durationSeconds);
6✔
177

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

183
                return null;
3✔
184
        }
185

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

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

197
                this.#rebuildFuseIndex();
3✔
198
        }
199

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

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

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

216
                const filePath = this.getFilePath(filename);
4✔
217

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

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

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

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

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

249
                return `${title}_${author}_${durationSeconds}.opus`;
7✔
250
        }
251

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

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

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

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

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

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

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

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

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

296
                if (parts.length < 2) {
11!
297
                        return null;
×
298
                }
299

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

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

310
                        if (!combinedText) {
9!
311
                                return null;
×
312
                        }
313

314
                        return { title: combinedText, author: '', durationSeconds };
9✔
315
                }
316

317
                const combinedText = parts.join(' ');
2✔
318

319
                if (!combinedText) {
2!
320
                        return null;
×
321
                }
322

323
                return { title: combinedText, author: '', durationSeconds: null };
2✔
324
        }
325
}
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