@media-engine/core 0.1.0 → 0.1.1

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 (75) hide show
  1. package/README.md +27 -268
  2. package/README.ru.md +54 -0
  3. package/dist/cache/memory.d.ts +5 -0
  4. package/dist/cache/memory.js +40 -2
  5. package/dist/cache/memory.js.map +1 -1
  6. package/dist/cache/types.d.ts +2 -0
  7. package/dist/engine/availability.d.ts +5 -0
  8. package/dist/engine/availability.js +129 -0
  9. package/dist/engine/availability.js.map +1 -0
  10. package/dist/engine/circuit-breaker.d.ts +36 -0
  11. package/dist/engine/circuit-breaker.js +163 -0
  12. package/dist/engine/circuit-breaker.js.map +1 -0
  13. package/dist/engine/concurrency-limiter.d.ts +13 -0
  14. package/dist/engine/concurrency-limiter.js +109 -0
  15. package/dist/engine/concurrency-limiter.js.map +1 -0
  16. package/dist/engine/engine.d.ts +7 -2
  17. package/dist/engine/engine.js +335 -848
  18. package/dist/engine/engine.js.map +1 -1
  19. package/dist/engine/in-flight.d.ts +4 -0
  20. package/dist/engine/in-flight.js +25 -0
  21. package/dist/engine/in-flight.js.map +1 -0
  22. package/dist/engine/provider-calls.d.ts +42 -0
  23. package/dist/engine/provider-calls.js +212 -0
  24. package/dist/engine/provider-calls.js.map +1 -0
  25. package/dist/engine/query.d.ts +21 -0
  26. package/dist/engine/query.js +241 -0
  27. package/dist/engine/query.js.map +1 -0
  28. package/dist/engine/response-meta.d.ts +13 -0
  29. package/dist/engine/response-meta.js +26 -0
  30. package/dist/engine/response-meta.js.map +1 -0
  31. package/dist/engine/runtime.d.ts +3 -0
  32. package/dist/engine/runtime.js +29 -0
  33. package/dist/engine/runtime.js.map +1 -0
  34. package/dist/engine/search-enrichment.d.ts +28 -0
  35. package/dist/engine/search-enrichment.js +90 -0
  36. package/dist/engine/search-enrichment.js.map +1 -0
  37. package/dist/engine/stale-fallback.d.ts +11 -0
  38. package/dist/engine/stale-fallback.js +60 -0
  39. package/dist/engine/stale-fallback.js.map +1 -0
  40. package/dist/engine/timeout-budget.d.ts +7 -0
  41. package/dist/engine/timeout-budget.js +31 -0
  42. package/dist/engine/timeout-budget.js.map +1 -0
  43. package/dist/engine/types.d.ts +30 -0
  44. package/dist/merge/details-identity.d.ts +5 -0
  45. package/dist/merge/details-identity.js +63 -0
  46. package/dist/merge/details-identity.js.map +1 -0
  47. package/dist/merge/fields.d.ts +25 -0
  48. package/dist/merge/fields.js +364 -0
  49. package/dist/merge/fields.js.map +1 -0
  50. package/dist/merge/grouping.d.ts +3 -0
  51. package/dist/merge/grouping.js +143 -0
  52. package/dist/merge/grouping.js.map +1 -0
  53. package/dist/merge/identity.d.ts +5 -0
  54. package/dist/merge/identity.js +21 -0
  55. package/dist/merge/identity.js.map +1 -0
  56. package/dist/merge/internal.d.ts +22 -0
  57. package/dist/merge/internal.js +15 -0
  58. package/dist/merge/internal.js.map +1 -0
  59. package/dist/merge/media-type.d.ts +5 -0
  60. package/dist/merge/media-type.js +15 -0
  61. package/dist/merge/media-type.js.map +1 -0
  62. package/dist/merge/priority.d.ts +9 -0
  63. package/dist/merge/priority.js +61 -0
  64. package/dist/merge/priority.js.map +1 -0
  65. package/dist/merge/scoring.d.ts +5 -0
  66. package/dist/merge/scoring.js +260 -0
  67. package/dist/merge/scoring.js.map +1 -0
  68. package/dist/merge/strategy.js +14 -943
  69. package/dist/merge/strategy.js.map +1 -1
  70. package/dist/merge/title.d.ts +4 -0
  71. package/dist/merge/title.js +22 -0
  72. package/dist/merge/title.js.map +1 -0
  73. package/dist/providers/types.d.ts +1 -0
  74. package/dist/response/types.d.ts +1 -0
  75. package/package.json +7 -3
package/README.md CHANGED
@@ -1,295 +1,54 @@
1
1
  # @media-engine/core
2
2
 
3
- Core TypeScript package for Media Engine.
3
+ **English** | [Русский](https://github.com/Yaneart/media-engine/blob/main/packages/core/README.ru.md)
4
4
 
5
- It owns the framework-independent engine, public media types, provider contracts, streaming provider contracts, provider registry, merge strategy, cache interface, error model, and testing utilities.
5
+ This is the part of Media Engine that does the thinking: it chooses providers, runs them, merges their answers, caches results, and turns failures into a predictable shape.
6
6
 
7
- Core does not import concrete providers and does not read API keys from the environment. Providers are passed in from the outside.
7
+ It does not contain any real data sources. For those, install `@media-engine/providers` too.
8
8
 
9
9
  ## Install
10
10
 
11
11
  ```bash
12
- npm install @media-engine/core
12
+ npm install @media-engine/core @media-engine/providers
13
13
  ```
14
14
 
15
- ## Basic Usage
15
+ ## Basic use
16
16
 
17
17
  ```ts
18
- import { MediaEngine, createMockProvider } from "@media-engine/core";
18
+ import { MediaEngine } from "@media-engine/core";
19
+ import { cinemetaProvider, kinobdProvider } from "@media-engine/providers";
19
20
 
20
21
  const media = new MediaEngine({
21
- providers: [createMockProvider()],
22
+ providers: [kinobdProvider(), cinemetaProvider()],
22
23
  });
23
24
 
24
- const response = await media.search({
25
- title: "Interstellar",
26
- });
27
-
28
- console.log(response.results[0]?.item.title);
29
- console.log(response.meta.providers.successful);
30
- ```
31
-
32
- ## Usage With Mock Provider
33
-
34
- ```ts
35
- import {
36
- MediaEngine,
37
- createDetailsResult,
38
- createMockProvider,
39
- createSearchResult,
40
- sampleMovie,
41
- } from "@media-engine/core";
42
-
43
- const mockProvider = createMockProvider({
44
- name: "local-fixture",
45
- searchResults: [createSearchResult("local-fixture", sampleMovie)],
46
- detailsResult: createDetailsResult("local-fixture", sampleMovie),
47
- });
48
-
49
- const media = new MediaEngine({
50
- providers: [mockProvider],
51
- });
52
-
53
- const search = await media.search({
54
- imdb: "tt0816692",
55
- });
56
-
57
- const details = await media.getDetails({
58
- imdb: "tt0816692",
59
- });
60
-
61
- console.log(search.results.length);
62
- console.log(details.details?.title);
63
- ```
64
-
65
- ## Provider Contract Overview
66
-
67
- A provider is a small object that implements the `MediaProvider` contract:
68
-
69
- ```ts
70
- import type { MediaProvider } from "@media-engine/core";
25
+ const search = await media.search({ title: "Interstellar" });
26
+ const details = await media.getDetails({ imdb: "tt0816692" });
71
27
 
72
- export const provider: MediaProvider = {
73
- name: "example",
74
- kind: "metadata",
75
- capabilities: {
76
- mediaTypes: ["movie", "series", "anime"],
77
- search: {
78
- byTitle: true,
79
- byExternalIds: ["imdb", "tmdb"],
80
- },
81
- details: {
82
- byExternalIds: ["imdb", "tmdb"],
83
- },
84
- },
85
- async search(query, context) {
86
- return [];
87
- },
88
- async getDetails(query, context) {
89
- return null;
90
- },
91
- };
28
+ console.log(search.results[0]?.item);
29
+ console.log(details.details);
92
30
  ```
93
31
 
94
- `capabilities` tell the engine when a provider can be selected. `search` returns normalized provider search results. `getDetails` is optional; providers without it are skipped by `MediaEngine.getDetails`.
32
+ The engine also has `getAvailability()` for optional streaming providers.
95
33
 
96
- Provider methods receive a `context` with `signal`, `timeoutMs`, `debug`, and `language`. Providers should respect `context.signal` when they perform slow work.
34
+ ## What comes from core
97
35
 
98
- Applications can bound slower optional providers independently while keeping a global upper limit:
99
-
100
- ```ts
101
- const media = new MediaEngine({
102
- providers,
103
- timeoutMs: 5_000,
104
- providerTimeouts: {
105
- cinemeta: 2_500,
106
- wikidata: 2_500,
107
- },
108
- });
109
- ```
110
-
111
- The effective timeout is the smaller of `timeoutMs` and the matching `providerTimeouts` value.
112
-
113
- ## Streaming Availability Contract
114
-
115
- Streaming availability is separate from metadata search and details. Configure streaming providers through `streamingProviders` and call `getAvailability` with a media or episode identity.
116
-
117
- ```ts
118
- import { MediaEngine, createMockProvider } from "@media-engine/core";
119
- import type { StreamingProvider } from "@media-engine/core";
120
-
121
- const streamingProvider: StreamingProvider = {
122
- name: "example-streaming",
123
- kind: "streaming",
124
- capabilities: {
125
- mediaTypes: ["movie", "series", "anime"],
126
- lookup: {
127
- byTitle: true,
128
- byExternalIds: ["imdb"],
129
- byEpisode: true,
130
- },
131
- features: ["embed", "translations", "qualities"],
132
- },
133
- async getAvailability(query) {
134
- return {
135
- query,
136
- options: [],
137
- sourceProviders: [{ provider: "example-streaming" }],
138
- checkedAt: new Date().toISOString(),
139
- };
140
- },
141
- };
142
-
143
- const media = new MediaEngine({
144
- providers: [createMockProvider()],
145
- streamingProviders: [streamingProvider],
146
- });
147
-
148
- const availability = await media.getAvailability({
149
- type: "movie",
150
- imdb: "tt0816692",
151
- });
152
- ```
153
-
154
- Core does not decide whether a third-party player source is appropriate for a product. Concrete streaming providers must document their own source rules and expose only safe access URLs.
155
-
156
- ## Search Response Example
157
-
158
- ```ts
159
- import { MediaEngine, createSuccessProvider } from "@media-engine/core";
36
+ - `MediaEngine`;
37
+ - search, details, media, and streaming types;
38
+ - metadata and streaming provider contracts;
39
+ - merge and cache interfaces;
40
+ - `MemoryCache`;
41
+ - normalized errors and provider failure metadata;
42
+ - mock providers and fixtures for tests.
160
43
 
161
- const media = new MediaEngine({
162
- providers: [createSuccessProvider()],
163
- });
164
-
165
- const response = await media.search({
166
- title: "Interstellar",
167
- });
168
- ```
169
-
170
- Shape:
171
-
172
- ```ts
173
- {
174
- query: {
175
- title: "Interstellar",
176
- },
177
- results: [
178
- {
179
- item: {
180
- id: "sample-movie-interstellar",
181
- type: "movie",
182
- title: "Interstellar",
183
- year: 2014,
184
- },
185
- score: 0.5,
186
- sources: [
187
- {
188
- provider: "success-provider",
189
- },
190
- ],
191
- },
192
- ],
193
- meta: {
194
- providers: {
195
- requested: ["success-provider"],
196
- successful: ["success-provider"],
197
- failed: [],
198
- },
199
- cached: false,
200
- tookMs: 1,
201
- },
202
- }
203
- ```
204
-
205
- `score`, `tookMs`, and optional fields can vary with the provider result and merge strategy.
206
-
207
- ## Details Response Example
208
-
209
- ```ts
210
- import { MediaEngine, createSuccessProvider } from "@media-engine/core";
211
-
212
- const media = new MediaEngine({
213
- providers: [createSuccessProvider()],
214
- });
215
-
216
- const response = await media.getDetails({
217
- imdb: "tt0816692",
218
- });
219
- ```
220
-
221
- Shape:
222
-
223
- ```ts
224
- {
225
- query: {
226
- imdb: "tt0816692",
227
- ids: {
228
- imdb: "tt0816692",
229
- },
230
- },
231
- details: {
232
- id: "sample-movie-interstellar",
233
- type: "movie",
234
- title: "Interstellar",
235
- year: 2014,
236
- },
237
- meta: {
238
- providers: {
239
- requested: ["success-provider"],
240
- successful: ["success-provider"],
241
- failed: [],
242
- },
243
- cached: false,
244
- tookMs: 1,
245
- },
246
- }
247
- ```
248
-
249
- If selected providers return no details, `details` is `null`.
250
-
251
- ## Availability Response Example
252
-
253
- ```ts
254
- import { MediaEngine, createSuccessProvider } from "@media-engine/core";
255
-
256
- const media = new MediaEngine({
257
- providers: [createSuccessProvider()],
258
- });
259
-
260
- const response = await media.getAvailability({
261
- type: "movie",
262
- imdb: "tt0816692",
263
- });
264
- ```
265
-
266
- When no streaming providers are configured, `options` is empty and `sourceProviders` is empty. Provider failures are exposed through response metadata when configured providers fail.
267
-
268
- ## Bounded Memory Cache
269
-
270
- ```ts
271
- import { MediaEngine, MemoryCache } from "@media-engine/core";
272
-
273
- const media = new MediaEngine({
274
- cache: new MemoryCache({
275
- defaultTtlMs: 5 * 60_000,
276
- maxEntries: 500,
277
- }),
278
- });
279
- ```
44
+ Provider calls run concurrently. If one source fails and another succeeds, the response keeps the useful data and lists the failure in `meta.providers.failed`.
280
45
 
281
- `defaultTtlMs` applies when a cache write does not specify its own TTL. `maxEntries` bounds memory usage and evicts the least-recently-used entry when full.
46
+ `MemoryCache` can retain metadata for a separate bounded stale window. `MediaEngine` uses it only for search and details when every selected provider fails retryably; stale streaming links are never returned. Such responses set `meta.cached` and `meta.stale` to `true`.
282
47
 
283
- ## Testing Utilities
48
+ The constructor also accepts streaming providers, a cache, global and per-provider timeouts, a custom merge strategy, and debug mode. Provider calls are bounded to two concurrent operations per provider by default, with a cancellable queue of 100; `providerConcurrency` can tune per-provider limits or disable the gate. Queue waiting remains inside the existing provider timeout. Core never imports concrete provider packages itself.
284
49
 
285
- The core package exports deterministic helpers for tests and examples:
50
+ Exact types are available from the package exports. The short [public API guide](https://github.com/Yaneart/media-engine/blob/main/docs/public-api.md) explains the three main operations without repeating every field.
286
51
 
287
- - `createMockProvider`;
288
- - `createSuccessProvider`;
289
- - `createFailingProvider`;
290
- - `createTimeoutProvider`;
291
- - `sampleMovie`;
292
- - `sampleSeries`;
293
- - `sampleAnime`.
52
+ ## License
294
53
 
295
- These helpers do not call real APIs and do not require API keys.
54
+ MIT
package/README.ru.md ADDED
@@ -0,0 +1,54 @@
1
+ # @media-engine/core
2
+
3
+ [English](https://github.com/Yaneart/media-engine/blob/main/packages/core/README.md) | **Русский**
4
+
5
+ Это часть Media Engine, которая отвечает за основную логику: выбирает провайдеры, запускает их, объединяет ответы, кеширует результат и приводит ошибки к предсказуемому виду.
6
+
7
+ Самих источников данных в core нет. Для них установите ещё и `@media-engine/providers`.
8
+
9
+ ## Установка
10
+
11
+ ```bash
12
+ npm install @media-engine/core @media-engine/providers
13
+ ```
14
+
15
+ ## Простой пример
16
+
17
+ ```ts
18
+ import { MediaEngine } from "@media-engine/core";
19
+ import { cinemetaProvider, kinobdProvider } from "@media-engine/providers";
20
+
21
+ const media = new MediaEngine({
22
+ providers: [kinobdProvider(), cinemetaProvider()],
23
+ });
24
+
25
+ const search = await media.search({ title: "Интерстеллар" });
26
+ const details = await media.getDetails({ imdb: "tt0816692" });
27
+
28
+ console.log(search.results[0]?.item);
29
+ console.log(details.details);
30
+ ```
31
+
32
+ Для опциональных стриминговых провайдеров у движка также есть `getAvailability()`.
33
+
34
+ ## Что экспортирует core
35
+
36
+ - `MediaEngine`;
37
+ - типы поиска, деталей, медиа и стриминга;
38
+ - контракты metadata- и streaming-провайдеров;
39
+ - интерфейсы объединения и кеша;
40
+ - `MemoryCache`;
41
+ - нормализованные ошибки и сведения о сбоях провайдеров;
42
+ - mock-провайдеры и примеры данных для тестов.
43
+
44
+ Провайдеры запускаются параллельно. Если один источник не сработал, а другой вернул данные, полезный ответ сохранится, а ошибка попадёт в `meta.providers.failed`.
45
+
46
+ `MemoryCache` может сохранять метаданные в отдельном ограниченном stale-окне. `MediaEngine` использует их только для поиска и деталей, когда все выбранные провайдеры завершились с retryable-ошибками; устаревшие streaming-ссылки никогда не возвращаются. В таком ответе `meta.cached` и `meta.stale` равны `true`.
47
+
48
+ Конструктор также принимает стриминговые провайдеры, кеш, общий и индивидуальные тайм-ауты, собственную стратегию объединения и debug-режим. По умолчанию одновременно выполняются не более двух операций каждого провайдера, а отменяемая очередь ограничена 100 элементами; `providerConcurrency` позволяет настроить отдельные лимиты или отключить gate. Ожидание в очереди входит в существующий тайм-аут провайдера. Сам core никогда не импортирует пакеты с конкретными провайдерами.
49
+
50
+ Точные типы доступны через exports пакета. В коротком [описании публичного API](https://github.com/Yaneart/media-engine/blob/main/docs/public-api.md) разобраны три основные операции без перечисления каждого поля.
51
+
52
+ ## Лицензия
53
+
54
+ MIT
@@ -2,18 +2,23 @@ import type { Cache, CacheSetOptions } from "./types.js";
2
2
  export interface MemoryCacheOptions {
3
3
  now?: () => number;
4
4
  defaultTtlMs?: number;
5
+ defaultStaleTtlMs?: number;
5
6
  maxEntries?: number;
6
7
  }
7
8
  export declare class MemoryCache implements Cache {
8
9
  private readonly entries;
9
10
  private readonly now;
10
11
  private readonly defaultTtlMs?;
12
+ private readonly defaultStaleTtlMs?;
11
13
  private readonly maxEntries?;
12
14
  constructor(options?: MemoryCacheOptions);
13
15
  get<T>(key: string): T | undefined;
16
+ getStale<T>(key: string): T | undefined;
14
17
  set<T>(key: string, value: T, options?: CacheSetOptions): void;
15
18
  delete(key: string): void;
16
19
  clear(): void;
17
20
  private isExpired;
21
+ private isBeyondStaleWindow;
22
+ private deleteIfBeyondStaleWindow;
18
23
  private evictOverflow;
19
24
  }
@@ -4,14 +4,20 @@ export class MemoryCache {
4
4
  entries = new Map();
5
5
  now;
6
6
  defaultTtlMs;
7
+ defaultStaleTtlMs;
7
8
  maxEntries;
8
9
  constructor(options = {}) {
9
10
  if (options.maxEntries !== undefined &&
10
11
  (!Number.isInteger(options.maxEntries) || options.maxEntries <= 0)) {
11
12
  throw new TypeError("MemoryCache maxEntries must be a positive integer.");
12
13
  }
14
+ if (options.defaultStaleTtlMs !== undefined &&
15
+ (!Number.isInteger(options.defaultStaleTtlMs) || options.defaultStaleTtlMs < 0)) {
16
+ throw new TypeError("MemoryCache defaultStaleTtlMs must be a non-negative integer.");
17
+ }
13
18
  this.now = options.now ?? Date.now;
14
19
  this.defaultTtlMs = options.defaultTtlMs;
20
+ this.defaultStaleTtlMs = options.defaultStaleTtlMs;
15
21
  this.maxEntries = options.maxEntries;
16
22
  }
17
23
  // Reads a cached value and removes it when it has expired.
@@ -22,6 +28,23 @@ export class MemoryCache {
22
28
  return undefined;
23
29
  }
24
30
  if (this.isExpired(entry)) {
31
+ this.deleteIfBeyondStaleWindow(key, entry);
32
+ return undefined;
33
+ }
34
+ if (this.maxEntries !== undefined) {
35
+ this.entries.delete(key);
36
+ this.entries.set(key, entry);
37
+ }
38
+ return cloneCacheValue(entry.value);
39
+ }
40
+ // Reads a value only after normal TTL expiration and before its stale window closes.
41
+ // Читает значение только после обычного TTL и до завершения stale-окна.
42
+ getStale(key) {
43
+ const entry = this.entries.get(key);
44
+ if (!entry || !this.isExpired(entry)) {
45
+ return undefined;
46
+ }
47
+ if (this.isBeyondStaleWindow(entry)) {
25
48
  this.entries.delete(key);
26
49
  return undefined;
27
50
  }
@@ -35,9 +58,16 @@ export class MemoryCache {
35
58
  // Сохраняет значение с опциональным TTL в миллисекундах.
36
59
  set(key, value, options = {}) {
37
60
  const ttlMs = options.ttlMs ?? this.defaultTtlMs;
61
+ const staleTtlMs = options.staleTtlMs ?? this.defaultStaleTtlMs;
62
+ if (staleTtlMs !== undefined && (!Number.isInteger(staleTtlMs) || staleTtlMs < 0)) {
63
+ throw new TypeError("MemoryCache staleTtlMs must be a non-negative integer.");
64
+ }
38
65
  const expiresAt = ttlMs === undefined || ttlMs < 0 ? undefined : this.now() + ttlMs;
66
+ const staleUntil = expiresAt === undefined || staleTtlMs === undefined || staleTtlMs <= 0
67
+ ? undefined
68
+ : expiresAt + staleTtlMs;
39
69
  this.entries.delete(key);
40
- this.entries.set(key, { value: cloneCacheValue(value), expiresAt });
70
+ this.entries.set(key, { value: cloneCacheValue(value), expiresAt, staleUntil });
41
71
  this.evictOverflow();
42
72
  }
43
73
  // Removes one cache entry by key.
@@ -55,6 +85,14 @@ export class MemoryCache {
55
85
  isExpired(entry) {
56
86
  return entry.expiresAt !== undefined && entry.expiresAt <= this.now();
57
87
  }
88
+ isBeyondStaleWindow(entry) {
89
+ return entry.staleUntil === undefined || entry.staleUntil <= this.now();
90
+ }
91
+ deleteIfBeyondStaleWindow(key, entry) {
92
+ if (this.isBeyondStaleWindow(entry)) {
93
+ this.entries.delete(key);
94
+ }
95
+ }
58
96
  // Evicts expired entries first, then least-recently-used entries until the bound is met.
59
97
  // Сначала удаляет истекшие записи, затем давно не использованные до соблюдения лимита.
60
98
  evictOverflow() {
@@ -62,7 +100,7 @@ export class MemoryCache {
62
100
  return;
63
101
  }
64
102
  for (const [key, entry] of this.entries) {
65
- if (this.isExpired(entry)) {
103
+ if (this.isExpired(entry) && this.isBeyondStaleWindow(entry)) {
66
104
  this.entries.delete(key);
67
105
  }
68
106
  }
@@ -1 +1 @@
1
- {"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/cache/memory.ts"],"names":[],"mappings":"AAiBA,gEAAgE;AAChE,oEAAoE;AACpE,MAAM,OAAO,WAAW;IACL,OAAO,GAAG,IAAI,GAAG,EAAqC,CAAC;IACvD,GAAG,CAAe;IAClB,YAAY,CAAU;IACtB,UAAU,CAAU;IAErC,YAAY,UAA8B,EAAE;QAC1C,IACE,OAAO,CAAC,UAAU,KAAK,SAAS;YAChC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,OAAO,CAAC,UAAU,IAAI,CAAC,CAAC,EAClE,CAAC;YACD,MAAM,IAAI,SAAS,CAAC,oDAAoD,CAAC,CAAC;QAC5E,CAAC;QAED,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;QACnC,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;QACzC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IACvC,CAAC;IAED,2DAA2D;IAC3D,2DAA2D;IAC3D,GAAG,CAAI,GAAW;QAChB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAEpC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,IAAI,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACzB,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;YAClC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACzB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC/B,CAAC;QAED,OAAO,eAAe,CAAC,KAAK,CAAC,KAAU,CAAC,CAAC;IAC3C,CAAC;IAED,uDAAuD;IACvD,yDAAyD;IACzD,GAAG,CAAI,GAAW,EAAE,KAAQ,EAAE,UAA2B,EAAE;QACzD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,YAAY,CAAC;QACjD,MAAM,SAAS,GAAG,KAAK,KAAK,SAAS,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;QAEpF,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,eAAe,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,aAAa,EAAE,CAAC;IACvB,CAAC;IAED,kCAAkC;IAClC,sCAAsC;IACtC,MAAM,CAAC,GAAW;QAChB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAED,6BAA6B;IAC7B,4BAA4B;IAC5B,KAAK;QACH,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IAED,iEAAiE;IACjE,8DAA8D;IACtD,SAAS,CAAC,KAAgC;QAChD,OAAO,KAAK,CAAC,SAAS,KAAK,SAAS,IAAI,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IACxE,CAAC;IAED,yFAAyF;IACzF,uFAAuF;IAC/E,aAAa;QACnB,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YAC1E,OAAO;QACT,CAAC;QAED,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACxC,IAAI,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC1B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;YAC3C,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAA2B,CAAC;YAEzE,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;gBAC5B,MAAM;YACR,CAAC;YAED,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;CACF;AAED,2EAA2E;AAC3E,8EAA8E;AAC9E,SAAS,eAAe,CAAI,KAAQ;IAClC,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC"}
1
+ {"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/cache/memory.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,10 @@
1
1
  export interface CacheSetOptions {
2
2
  ttlMs?: number;
3
+ staleTtlMs?: number;
3
4
  }
4
5
  export interface Cache {
5
6
  get<T>(key: string): Promise<T | undefined> | T | undefined;
7
+ getStale?<T>(key: string): Promise<T | undefined> | T | undefined;
6
8
  set<T>(key: string, value: T, options?: CacheSetOptions): Promise<void> | void;
7
9
  delete(key: string): Promise<void> | void;
8
10
  clear(): Promise<void> | void;
@@ -0,0 +1,5 @@
1
+ import type { CacheSetOptions } from "../cache/index.js";
2
+ import type { MediaAvailability, StreamQuery, StreamingProvider } from "../streaming/index.js";
3
+ export declare function selectStreamingProviders(providers: StreamingProvider[], query: StreamQuery): StreamingProvider[];
4
+ export declare function mergeAvailabilityResults(query: StreamQuery, results: MediaAvailability[]): MediaAvailability;
5
+ export declare function createAvailabilityCacheOptions(availability: MediaAvailability): CacheSetOptions | undefined;
@@ -0,0 +1,129 @@
1
+ import { sortObject } from "./query.js";
2
+ const EXPIRING_AVAILABILITY_CACHE_SAFETY_MS = 1_000;
3
+ // Selects streaming providers that can answer the normalized stream query.
4
+ // Выбирает streaming-провайдеры, которые могут ответить на нормализованный stream query.
5
+ export function selectStreamingProviders(providers, query) {
6
+ return providers.filter((provider) => {
7
+ if (query.providers && !query.providers.includes(provider.name)) {
8
+ return false;
9
+ }
10
+ if (!provider.capabilities.mediaTypes.includes(query.type)) {
11
+ return false;
12
+ }
13
+ if (hasEpisodeQuery(query) && !provider.capabilities.lookup.byEpisode) {
14
+ return false;
15
+ }
16
+ return (Boolean(query.title && provider.capabilities.lookup.byTitle) ||
17
+ hasSupportedExternalId(query.ids, provider.capabilities.lookup.byExternalIds));
18
+ });
19
+ }
20
+ // Merges availability results without hiding provider attribution.
21
+ // Объединяет availability-результаты, не скрывая атрибуцию провайдеров.
22
+ export function mergeAvailabilityResults(query, results) {
23
+ return {
24
+ query,
25
+ item: results.find((result) => result.item)?.item,
26
+ episodes: mergeEpisodeAvailability(results),
27
+ options: uniqueBy(results.flatMap((result) => result.options), (option) => `${option.provider}:${option.id}`),
28
+ sourceProviders: uniqueBy(results.flatMap((result) => result.sourceProviders), (source) => createStreamingSourceKey(source)),
29
+ checkedAt: new Date().toISOString(),
30
+ };
31
+ }
32
+ // Keeps cached direct links from outliving the earliest advertised expiration.
33
+ // Не позволяет кешированным прямым ссылкам пережить ближайший заявленный срок действия.
34
+ export function createAvailabilityCacheOptions(availability) {
35
+ const expiresAtValues = [
36
+ ...availability.options,
37
+ ...(availability.episodes?.flatMap((episode) => episode.options) ?? []),
38
+ ]
39
+ .map((option) => option.expiresAt)
40
+ .filter((value) => value !== undefined)
41
+ .map((value) => Date.parse(value))
42
+ .filter(Number.isFinite);
43
+ if (expiresAtValues.length === 0) {
44
+ return { staleTtlMs: 0 };
45
+ }
46
+ const earliestExpiration = Math.min(...expiresAtValues);
47
+ return {
48
+ ttlMs: Math.max(0, earliestExpiration - Date.now() - EXPIRING_AVAILABILITY_CACHE_SAFETY_MS),
49
+ staleTtlMs: 0,
50
+ };
51
+ }
52
+ // Merges episode-level availability blocks by episode identity.
53
+ // Объединяет episode-level availability блоки по идентичности эпизода.
54
+ function mergeEpisodeAvailability(results) {
55
+ const episodesByKey = new Map();
56
+ for (const episode of results.flatMap((result) => [
57
+ ...(result.episodes ?? []),
58
+ ...createEpisodeAvailabilityFromOptions(result.options),
59
+ ])) {
60
+ const key = createEpisodeKey(episode);
61
+ const existing = episodesByKey.get(key);
62
+ if (!existing) {
63
+ episodesByKey.set(key, {
64
+ seasonNumber: episode.seasonNumber,
65
+ episodeNumber: episode.episodeNumber,
66
+ absoluteEpisodeNumber: episode.absoluteEpisodeNumber,
67
+ title: episode.title,
68
+ options: uniqueBy(episode.options, (option) => `${option.provider}:${option.id}`),
69
+ });
70
+ continue;
71
+ }
72
+ existing.options = uniqueBy([...existing.options, ...episode.options], (option) => `${option.provider}:${option.id}`);
73
+ existing.title ??= episode.title;
74
+ }
75
+ return episodesByKey.size > 0 ? [...episodesByKey.values()] : undefined;
76
+ }
77
+ // Creates episode blocks from top-level options that carry episode identity.
78
+ // Создает episode blocks из top-level options, которые содержат идентичность эпизода.
79
+ function createEpisodeAvailabilityFromOptions(options) {
80
+ return options
81
+ .filter((option) => option.episode)
82
+ .map((option) => ({
83
+ seasonNumber: option.episode?.seasonNumber,
84
+ episodeNumber: option.episode?.episodeNumber,
85
+ absoluteEpisodeNumber: option.episode?.absoluteEpisodeNumber,
86
+ options: [option],
87
+ }));
88
+ }
89
+ // Checks whether query ids overlap provider-supported external ID sources.
90
+ // Проверяет, пересекаются ли query ids с поддерживаемыми провайдером источниками ID.
91
+ function hasSupportedExternalId(ids, supportedSources) {
92
+ return Boolean(ids && supportedSources.some((source) => Boolean(ids[source])));
93
+ }
94
+ // Checks whether query targets a concrete episode.
95
+ // Проверяет, нацелен ли query на конкретный эпизод.
96
+ function hasEpisodeQuery(query) {
97
+ return (query.seasonNumber !== undefined ||
98
+ query.episodeNumber !== undefined ||
99
+ query.absoluteEpisodeNumber !== undefined);
100
+ }
101
+ // Creates a stable identity for an episode availability block.
102
+ // Создает стабильную идентичность для блока доступности эпизода.
103
+ function createEpisodeKey(episode) {
104
+ return [
105
+ episode.seasonNumber ?? "",
106
+ episode.episodeNumber ?? "",
107
+ episode.absoluteEpisodeNumber ?? "",
108
+ ].join(":");
109
+ }
110
+ // Creates a stable identity for provider source attribution.
111
+ // Создает стабильную идентичность для атрибуции источника провайдера.
112
+ function createStreamingSourceKey(source) {
113
+ return `${source.provider}:${source.url ?? ""}:${JSON.stringify(sortObject(source.ids ?? {}))}`;
114
+ }
115
+ // Keeps the first value for each derived key.
116
+ // Оставляет первое значение для каждого вычисленного ключа.
117
+ function uniqueBy(values, getKey) {
118
+ const seen = new Set();
119
+ const unique = [];
120
+ for (const value of values) {
121
+ const key = getKey(value);
122
+ if (!seen.has(key)) {
123
+ seen.add(key);
124
+ unique.push(value);
125
+ }
126
+ }
127
+ return unique;
128
+ }
129
+ //# sourceMappingURL=availability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"availability.js","sourceRoot":"","sources":["../../src/engine/availability.ts"],"names":[],"mappings":"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"}