ziplayer 0.2.7-dev.0 → 0.2.7-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/AI-Guide.md +407 -756
  2. package/README.md +275 -10
  3. package/dist/extensions/BaseExtension.d.ts +1 -0
  4. package/dist/extensions/BaseExtension.d.ts.map +1 -1
  5. package/dist/extensions/BaseExtension.js.map +1 -1
  6. package/dist/extensions/index.d.ts +38 -3
  7. package/dist/extensions/index.d.ts.map +1 -1
  8. package/dist/extensions/index.js +259 -41
  9. package/dist/extensions/index.js.map +1 -1
  10. package/dist/persistence/PersistenceManager.d.ts +95 -0
  11. package/dist/persistence/PersistenceManager.d.ts.map +1 -0
  12. package/dist/persistence/PersistenceManager.js +968 -0
  13. package/dist/persistence/PersistenceManager.js.map +1 -0
  14. package/dist/plugins/BasePlugin.js +1 -1
  15. package/dist/plugins/BasePlugin.js.map +1 -1
  16. package/dist/plugins/index.d.ts +19 -4
  17. package/dist/plugins/index.d.ts.map +1 -1
  18. package/dist/plugins/index.js +204 -113
  19. package/dist/plugins/index.js.map +1 -1
  20. package/dist/structures/FilterManager.js +3 -3
  21. package/dist/structures/FilterManager.js.map +1 -1
  22. package/dist/structures/Player.d.ts +65 -14
  23. package/dist/structures/Player.d.ts.map +1 -1
  24. package/dist/structures/Player.js +330 -88
  25. package/dist/structures/Player.js.map +1 -1
  26. package/dist/structures/PlayerManager.d.ts +127 -91
  27. package/dist/structures/PlayerManager.d.ts.map +1 -1
  28. package/dist/structures/PlayerManager.js +437 -124
  29. package/dist/structures/PlayerManager.js.map +1 -1
  30. package/dist/structures/Queue.d.ts +136 -31
  31. package/dist/structures/Queue.d.ts.map +1 -1
  32. package/dist/structures/Queue.js +265 -46
  33. package/dist/structures/Queue.js.map +1 -1
  34. package/dist/types/index.d.ts +46 -6
  35. package/dist/types/index.d.ts.map +1 -1
  36. package/dist/types/index.js +1 -0
  37. package/dist/types/index.js.map +1 -1
  38. package/dist/types/persistence.d.ts +74 -0
  39. package/dist/types/persistence.d.ts.map +1 -0
  40. package/dist/types/persistence.js +3 -0
  41. package/dist/types/persistence.js.map +1 -0
  42. package/package.json +3 -2
  43. package/src/extensions/BaseExtension.ts +1 -0
  44. package/src/extensions/index.ts +320 -37
  45. package/src/persistence/PersistenceManager.ts +1073 -0
  46. package/src/plugins/BasePlugin.ts +1 -1
  47. package/src/plugins/index.ts +248 -133
  48. package/src/structures/FilterManager.ts +3 -3
  49. package/src/structures/Player.ts +358 -94
  50. package/src/structures/PlayerManager.ts +535 -129
  51. package/src/structures/Queue.ts +300 -55
  52. package/src/types/index.ts +52 -10
  53. package/src/types/persistence.ts +83 -0
  54. package/src/types/plugin.ts +1 -1
@@ -3,7 +3,7 @@ import { SourcePlugin, Track, SearchResult, StreamInfo } from "../types";
3
3
  export abstract class BasePlugin implements SourcePlugin {
4
4
  abstract name: string;
5
5
  abstract version: string;
6
- priority?: number = 0;
6
+ priority?: number = 0; // Higher = run first
7
7
 
8
8
  abstract canHandle(query: string): boolean;
9
9
  abstract search(query: string, requestedBy: string): Promise<SearchResult>;
@@ -6,6 +6,8 @@ import type { Player } from "../structures/Player";
6
6
 
7
7
  type PluginManagerOptions = {
8
8
  extractorTimeout: number | undefined;
9
+ maxFallbackAttempts?: number;
10
+ enableCache?: boolean;
9
11
  };
10
12
 
11
13
  export { BasePlugin } from "./BasePlugin";
@@ -28,84 +30,73 @@ function levenshtein(a: string, b: string): number {
28
30
 
29
31
  function similarity(a: string, b: string): number {
30
32
  if (!a || !b) return 0;
31
-
32
33
  const dist = levenshtein(a, b);
33
34
  const maxLen = Math.max(a.length, b.length);
34
-
35
- return 1 - dist / maxLen; // 0 → 1
35
+ return 1 - dist / maxLen;
36
36
  }
37
37
 
38
38
  function normalize(str: string): string {
39
39
  return str
40
40
  .toLowerCase()
41
- .replace(/\(.*?\)|\[.*?\]/g, "") // remove (remix), [lyrics]
41
+ .replace(/\(.*?\)|\[.*?\]/g, "")
42
42
  .replace(/[^a-z0-9\s]/g, "")
43
43
  .replace(/\s+/g, " ")
44
44
  .trim();
45
45
  }
46
46
 
47
47
  const MUSIC_KEYWORDS = ["official", "mv", "audio", "lyrics", "remix", "cover", "ft", "feat", "prod", "music video"];
48
-
49
48
  const NON_MUSIC_KEYWORDS = ["reaction", "review", "podcast", "interview", "vlog", "live stream", "news", "tiktok"];
50
49
 
51
50
  function detectContentType(title: string): number {
52
51
  const t = title.toLowerCase();
53
-
54
52
  let score = 0;
55
-
56
- for (const k of MUSIC_KEYWORDS) {
57
- if (t.includes(k)) score += 2;
58
- }
59
-
60
- for (const k of NON_MUSIC_KEYWORDS) {
61
- if (t.includes(k)) score -= 3;
62
- }
63
-
53
+ for (const k of MUSIC_KEYWORDS) if (t.includes(k)) score += 2;
54
+ for (const k of NON_MUSIC_KEYWORDS) if (t.includes(k)) score -= 3;
64
55
  return score;
65
56
  }
66
57
 
67
58
  function tokenOverlap(a: string, b: string): number {
68
59
  const setA = new Set(a.split(" "));
69
60
  const setB = new Set(b.split(" "));
70
-
71
61
  let match = 0;
72
- for (const word of setA) {
73
- if (setB.has(word)) match++;
74
- }
75
-
62
+ for (const word of setA) if (setB.has(word)) match++;
76
63
  return match / Math.max(setA.size, setB.size);
77
64
  }
78
65
 
79
66
  function scoreTrack(base: Track, candidate: Track): number {
80
67
  const titleA = normalize(base.title);
81
68
  const titleB = normalize(candidate.title);
82
-
83
69
  let score = 0;
84
-
85
- // ===== FUZZY =====
86
- const sim = similarity(titleA, titleB); // 0 → 1
87
- score += sim * 50;
88
-
89
- // ===== TOKEN MATCH =====
70
+ score += similarity(titleA, titleB) * 50;
90
71
  score += tokenOverlap(titleA, titleB) * 30;
91
-
92
- // ===== CONTENT TYPE =====
93
72
  score += detectContentType(candidate.title);
94
-
95
73
  return score;
96
74
  }
97
75
 
98
- // Plugin factory
76
+ // Cache entry for stream results
77
+ interface StreamCacheEntry {
78
+ streamInfo: StreamInfo;
79
+ timestamp: number;
80
+ expiresAt: number;
81
+ }
82
+
99
83
  export class PluginManager {
100
84
  private options: PluginManagerOptions;
101
85
  private player: Player;
102
86
  private manager: PlayerManager;
103
87
  private plugins: Map<string, BasePlugin> = new Map();
88
+ private streamCache: Map<string, StreamCacheEntry> = new Map();
89
+ private readonly STREAM_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
90
+ private pendingStreams: Map<string, Promise<StreamInfo | null>> = new Map(); // Dedupe in-flight requests
104
91
 
105
92
  constructor(player: Player, manager: PlayerManager, options: PluginManagerOptions) {
106
93
  this.player = player;
107
94
  this.manager = manager;
108
- this.options = options;
95
+ this.options = {
96
+ maxFallbackAttempts: 3,
97
+ enableCache: true,
98
+ ...options,
99
+ };
109
100
  }
110
101
 
111
102
  debug(message?: any, ...optionalParams: any[]): void {
@@ -115,11 +106,18 @@ export class PluginManager {
115
106
  }
116
107
 
117
108
  register(plugin: BasePlugin): void {
109
+ if (this.plugins.has(plugin.name)) {
110
+ this.debug(`Overwriting existing plugin: ${plugin.name}`);
111
+ }
112
+ plugin.priority ??= 0;
118
113
  this.plugins.set(plugin.name, plugin);
114
+ this.debug(`Registered plugin: ${plugin.name} (priority: ${plugin.priority})`);
119
115
  }
120
116
 
121
117
  unregister(name: string): boolean {
122
- return this.plugins.delete(name);
118
+ const removed = this.plugins.delete(name);
119
+ if (removed) this.debug(`Unregistered plugin: ${name}`);
120
+ return removed;
123
121
  }
124
122
 
125
123
  get(name: string): BasePlugin | undefined {
@@ -127,162 +125,279 @@ export class PluginManager {
127
125
  }
128
126
 
129
127
  getAll(): BasePlugin[] {
130
- return Array.from(this.plugins.values());
128
+ return Array.from(this.plugins.values()).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
131
129
  }
132
130
 
133
131
  findPlugin(query: string): BasePlugin | undefined {
134
- return this.getAll().find((plugin) => plugin.canHandle(query));
132
+ // First try exact match by source
133
+ for (const plugin of this.getAll()) {
134
+ if (plugin.name && query.toLowerCase().includes(plugin.name.toLowerCase())) {
135
+ return plugin;
136
+ }
137
+ }
138
+
139
+ // Then try canHandle
140
+ return this.getAll().find((plugin) => plugin.canHandle?.(query) ?? false);
135
141
  }
136
142
 
137
143
  clear(): void {
138
144
  this.plugins.clear();
145
+ this.streamCache.clear();
146
+ this.pendingStreams.clear();
139
147
  }
140
148
 
141
- async getStream(track: Track): Promise<StreamInfo | null> {
142
- const timeoutMs = this.options.extractorTimeout ?? 50000;
143
- const primary = this.get(track.source) || this.findPlugin(track.url);
144
- if (!primary) {
145
- this.debug(`No plugin found for track: ${track.title}`);
146
- return null;
149
+ private getStreamCacheKey(track: Track): string {
150
+ return `${track.source}:${track.url}:${track.id || track.title}`;
151
+ }
152
+
153
+ private getCachedStream(track: Track): StreamInfo | null {
154
+ if (!this.options.enableCache) return null;
155
+
156
+ const key = this.getStreamCacheKey(track);
157
+ const cached = this.streamCache.get(key);
158
+
159
+ if (cached && Date.now() < cached.expiresAt) {
160
+ this.debug(`[Cache] Hit for track: ${track.title}`);
161
+ return cached.streamInfo;
162
+ }
163
+
164
+ if (cached) {
165
+ this.debug(`[Cache] Expired for track: ${track.title}`);
166
+ this.streamCache.delete(key);
147
167
  }
168
+
169
+ return null;
170
+ }
171
+
172
+ private setCachedStream(track: Track, streamInfo: StreamInfo): void {
173
+ if (!this.options.enableCache) return;
174
+
175
+ const key = this.getStreamCacheKey(track);
176
+ this.streamCache.set(key, {
177
+ streamInfo,
178
+ timestamp: Date.now(),
179
+ expiresAt: Date.now() + this.STREAM_CACHE_TTL,
180
+ });
181
+ this.debug(`[Cache] Stored for track: ${track.title}`);
182
+ }
183
+
184
+ private async getStreamWithDedupe(track: Track, primary: BasePlugin): Promise<StreamInfo | null> {
185
+ const key = this.getStreamCacheKey(track);
186
+
187
+ // Check if there's already an in-flight request
188
+ if (this.pendingStreams.has(key)) {
189
+ this.debug(`[Dedupe] Waiting for existing request: ${track.title}`);
190
+ return this.pendingStreams.get(key)!;
191
+ }
192
+
193
+ // Create new request
194
+ const promise = this.getStreamInternal(track, primary);
195
+ this.pendingStreams.set(key, promise);
196
+
148
197
  try {
198
+ const result = await promise;
199
+ return result;
200
+ } finally {
201
+ this.pendingStreams.delete(key);
202
+ }
203
+ }
204
+
205
+ private async getStreamInternal(track: Track, primary: BasePlugin): Promise<StreamInfo | null> {
206
+ const timeoutMs = this.options.extractorTimeout ?? 50000;
207
+
208
+ // Check cache first
209
+ const cached = this.getCachedStream(track);
210
+ if (cached) return cached;
211
+
212
+ // Try primary plugin first
213
+ try {
214
+ this.debug(`[Primary] Trying ${primary.name} for track: ${track.title}`);
149
215
  const controller = new AbortController();
150
- const result = await withTimeout(primary.getStream(track, controller.signal), timeoutMs, "Primary timeout");
151
- if (result?.stream) return result;
152
- throw new Error("Primary failed");
153
- } catch {
154
- this.debug("Primary failed → fallback parallel");
216
+ const result = await withTimeout(
217
+ primary.getStream(track, controller.signal),
218
+ timeoutMs,
219
+ `Primary timeout: ${primary.name}`,
220
+ );
221
+
222
+ if (result?.stream) {
223
+ this.debug(`[Primary] Success via ${primary.name}`);
224
+ this.setCachedStream(track, result);
225
+ return result;
226
+ }
227
+ throw new Error("Primary plugin returned no stream");
228
+ } catch (error) {
229
+ this.debug(`[Primary] Failed: ${primary.name}`, error);
155
230
  }
156
231
 
157
- // ===== FALLBACK PARALLEL =====
158
- const plugins = this.getAll()
159
- .filter((p) => p !== primary)
160
- .map((p) => {
161
- p.priority ??= 0;
162
- return p;
163
- })
164
- .sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
165
-
166
- // group by priority
167
- const groups = new Map<number, BasePlugin[]>();
168
- for (const p of plugins) {
169
- if (!groups.has(p.priority ?? 0)) groups.set(p.priority ?? 0, []);
170
- groups.get(p.priority ?? 0)!.push(p);
232
+ // Fallback to other plugins
233
+ const fallbackPlugins = this.getAll()
234
+ .filter((p) => p !== primary && p.name !== primary.name)
235
+ .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
236
+
237
+ if (fallbackPlugins.length === 0) {
238
+ this.debug(`[Fallback] No fallback plugins available`);
239
+ return null;
171
240
  }
172
- for (const [priority, group] of groups) {
173
- this.debug(`Running group priority=${priority}`);
174
- const controller = new AbortController();
241
+
242
+ this.debug(`[Fallback] Trying ${fallbackPlugins.length} plugins sequentially`);
243
+
244
+ // Try plugins sequentially to avoid overwhelming sources
245
+ let attempt = 0;
246
+ for (const plugin of fallbackPlugins) {
247
+ attempt++;
248
+ if (attempt > (this.options.maxFallbackAttempts ?? 3)) {
249
+ this.debug(`[Fallback] Max attempts (${this.options.maxFallbackAttempts}) reached`);
250
+ break;
251
+ }
252
+
175
253
  try {
176
- const promises = group.map((p) => {
177
- const run = async () => {
178
- try {
179
- let result: StreamInfo | null = null;
180
-
181
- if (p.getStream) {
182
- try {
183
- result = await withTimeout(p.getStream(track, controller.signal), timeoutMs, `Timeout ${p.name}`);
184
- } catch (err) {
185
- // getStream thất bại → log rồi thử getFallback
186
- this.debug(`getStream failed for ${p.name}, trying getFallback`, err);
187
- }
188
-
189
- if (result?.stream) {
190
- this.debug(`Success via ${p.name}`);
191
- controller.abort();
192
- return result;
193
- }
194
- }
195
-
196
- if (p.getFallback) {
197
- result = await withTimeout(p.getFallback(track, controller.signal), timeoutMs, `Fallback timeout ${p.name}`);
198
- if (result?.stream) {
199
- this.debug(`Fallback via ${p.name}`);
200
- controller.abort();
201
- return result;
202
- }
203
- }
204
-
205
- throw new Error("No stream");
206
- } catch (err) {
207
- if (controller.signal.aborted) throw new Error("Aborted");
208
- this.debug(`Failed ${p.name}`, err);
209
- throw err;
210
- }
211
- };
212
- return run();
213
- });
214
-
215
- const result = await Promise.any(promises);
216
- if (result?.stream) return result;
217
- } catch {
218
- this.debug(`Priority group ${priority} failed`);
219
- controller.abort();
254
+ this.debug(`[Fallback] Attempt ${attempt}/${fallbackPlugins.length}: ${plugin.name}`);
255
+ const controller = new AbortController();
256
+
257
+ let result: StreamInfo | null = null;
258
+
259
+ // Try getStream first
260
+ if (plugin.getStream) {
261
+ result = await withTimeout(plugin.getStream(track, controller.signal), timeoutMs, `Timeout: ${plugin.name}`);
262
+ }
263
+
264
+ // Try fallback method if getStream failed
265
+ if (!result?.stream && plugin.getFallback) {
266
+ this.debug(`[Fallback] Trying fallback method for ${plugin.name}`);
267
+ result = await withTimeout(plugin.getFallback(track, controller.signal), timeoutMs, `Fallback timeout: ${plugin.name}`);
268
+ }
269
+
270
+ if (result?.stream) {
271
+ this.debug(`[Fallback] Success via ${plugin.name}`);
272
+ this.setCachedStream(track, result);
273
+ return result;
274
+ }
275
+ } catch (error) {
276
+ this.debug(`[Fallback] Failed: ${plugin.name}`, error);
220
277
  }
221
278
  }
222
279
 
223
- throw new Error(`All plugins failed for track: ${track.title}`);
280
+ this.debug(`[Fallback] All plugins failed for track: ${track.title}`);
281
+ return null;
282
+ }
283
+
284
+ async getStream(track: Track): Promise<StreamInfo | null> {
285
+ if (!track) {
286
+ this.debug(`[getStream] No track provided`);
287
+ return null;
288
+ }
289
+
290
+ // Find the most appropriate plugin
291
+ let primary = this.get(track.source);
292
+ if (!primary) {
293
+ primary = this.findPlugin(track.url);
294
+ }
295
+ if (!primary) {
296
+ this.debug(`[getStream] No plugin found for track: ${track.title} (source: ${track.source})`);
297
+ return null;
298
+ }
299
+
300
+ return this.getStreamWithDedupe(track, primary);
224
301
  }
225
302
 
226
303
  /**
227
304
  * Get related tracks for a given track
228
305
  * @param {Track} track Track to find related tracks for
229
- * @returns {Track[]} Related tracks or empty array
230
- * @example
231
- * const related = await player.getRelatedTracks(track);
232
- * console.log(`Found ${related.length} related tracks`);
306
+ * @returns {Promise<Track[]>} Related tracks or empty array
233
307
  */
234
308
  async getRelatedTracks(track: Track): Promise<Track[]> {
235
309
  if (!track) return [];
236
310
 
237
311
  const timeoutMs = this.options.extractorTimeout ?? 15000;
238
312
  const limit = 20;
313
+ const minSimilarityScore = 10; // Minimum score to consider
239
314
 
240
- const allPlugins = this.getAll()
315
+ const relatedPlugins = this.getAll()
241
316
  .filter((p) => typeof p.getRelatedTracks === "function")
242
- .sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
317
+ .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
318
+
319
+ if (relatedPlugins.length === 0) {
320
+ this.debug(`[RelatedTracks] No plugins support related tracks`);
321
+ return [];
322
+ }
243
323
 
244
324
  const history = this.player.queue.previousTracks;
325
+ const historyUrls = new Set(history.map((t) => t.url));
326
+ const currentTrackUrl = track.url;
245
327
 
246
328
  const results: Track[] = [];
247
329
 
248
- // ===== TRY ALL PLUGINS (NOT JUST FIRST SUCCESS) =====
249
- await Promise.allSettled(
250
- allPlugins.map(async (p) => {
251
- try {
252
- this.debug(`[RelatedTracks] Querying ${p.name}`);
253
-
254
- const related = await withTimeout(p.getRelatedTracks!(track, { limit, history }), timeoutMs, `Timeout ${p.name}`);
255
-
256
- if (Array.isArray(related)) {
257
- results.push(...related);
330
+ // Try plugins in parallel but with limit
331
+ const batchSize = 3;
332
+ for (let i = 0; i < relatedPlugins.length; i += batchSize) {
333
+ const batch = relatedPlugins.slice(i, i + batchSize);
334
+ const batchResults = await Promise.allSettled(
335
+ batch.map(async (plugin) => {
336
+ try {
337
+ this.debug(`[RelatedTracks] Querying ${plugin.name}`);
338
+ const related = await withTimeout(
339
+ plugin.getRelatedTracks!(track, { limit, history }),
340
+ timeoutMs,
341
+ `Timeout ${plugin.name}`,
342
+ );
343
+ return Array.isArray(related) ? related : [];
344
+ } catch (err) {
345
+ this.debug(`[RelatedTracks] ${plugin.name} failed`, err);
346
+ return [];
258
347
  }
259
- } catch (err) {
260
- this.debug(`[RelatedTracks] ${p.name} failed`, err);
348
+ }),
349
+ );
350
+
351
+ for (const result of batchResults) {
352
+ if (result.status === "fulfilled") {
353
+ results.push(...result.value);
261
354
  }
262
- }),
263
- );
355
+ }
356
+ }
264
357
 
265
358
  if (results.length === 0) {
266
- this.debug(`[RelatedTracks] No results`);
359
+ this.debug(`[RelatedTracks] No results from any plugin`);
267
360
  return [];
268
361
  }
269
362
 
270
- // ===== DEDUPE =====
363
+ // Deduplicate by URL
271
364
  const unique = new Map<string, Track>();
272
365
  for (const t of results) {
273
- if (!unique.has(t.url)) {
366
+ if (!unique.has(t.url) && t.url !== currentTrackUrl && !historyUrls.has(t.url)) {
274
367
  unique.set(t.url, t);
275
368
  }
276
369
  }
277
370
 
278
- // ===== SCORE + SORT =====
371
+ // Score and sort
279
372
  const ranked = Array.from(unique.values())
280
373
  .map((t) => ({ track: t, score: scoreTrack(track, t) }))
374
+ .filter((item) => item.score >= minSimilarityScore)
281
375
  .sort((a, b) => b.score - a.score)
282
376
  .slice(0, limit)
283
377
  .map((x) => x.track);
284
378
 
285
- this.debug(`[RelatedTracks] Final ${ranked.length} tracks`);
379
+ this.debug(`[RelatedTracks] Found ${ranked.length} related tracks (filtered from ${results.length})`);
286
380
  return ranked;
287
381
  }
382
+
383
+ /**
384
+ * Clear stream cache
385
+ */
386
+ clearStreamCache(): void {
387
+ const size = this.streamCache.size;
388
+ this.streamCache.clear();
389
+ this.debug(`[Cache] Cleared ${size} stream cache entries`);
390
+ }
391
+
392
+ /**
393
+ * Get plugin statistics
394
+ */
395
+ getStats(): object {
396
+ return {
397
+ totalPlugins: this.plugins.size,
398
+ pluginNames: Array.from(this.plugins.keys()),
399
+ cacheSize: this.streamCache.size,
400
+ pendingRequests: this.pendingStreams.size,
401
+ };
402
+ }
288
403
  }
@@ -156,7 +156,7 @@ export class FilterManager {
156
156
 
157
157
  this.activeFilters.push(audioFilter);
158
158
  this.debug(`[FilterManager] Applied filter: ${audioFilter.name} - ${audioFilter.description}`);
159
- return await this.player.refeshPlayerResource();
159
+ return await this.player.refreshPlayerResource();
160
160
  }
161
161
 
162
162
  /**
@@ -195,7 +195,7 @@ export class FilterManager {
195
195
  }
196
196
  const removed = this.activeFilters.splice(index, 1)[0];
197
197
  this.debug(`[FilterManager] Removed filter: ${removed.name}`);
198
- return await this.player.refeshPlayerResource();
198
+ return await this.player.refreshPlayerResource();
199
199
  }
200
200
 
201
201
  /**
@@ -209,7 +209,7 @@ export class FilterManager {
209
209
  const count = this.activeFilters.length;
210
210
  this.activeFilters = [];
211
211
  this.debug(`[FilterManager] Cleared ${count} filters`);
212
- return await this.player.refeshPlayerResource();
212
+ return await this.player.refreshPlayerResource();
213
213
  }
214
214
 
215
215
  /**