@media-engine/core 0.1.0 → 1.0.0

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 (121) hide show
  1. package/README.md +40 -255
  2. package/README.ru.md +80 -0
  3. package/dist/cache/memory.d.ts +5 -0
  4. package/dist/cache/memory.js +43 -3
  5. package/dist/cache/memory.js.map +1 -1
  6. package/dist/cache/types.d.ts +2 -0
  7. package/dist/details/types.d.ts +6 -0
  8. package/dist/engine/availability.d.ts +6 -0
  9. package/dist/engine/availability.js +137 -0
  10. package/dist/engine/availability.js.map +1 -0
  11. package/dist/engine/circuit-breaker.d.ts +37 -0
  12. package/dist/engine/circuit-breaker.js +174 -0
  13. package/dist/engine/circuit-breaker.js.map +1 -0
  14. package/dist/engine/concurrency-limiter.d.ts +13 -0
  15. package/dist/engine/concurrency-limiter.js +109 -0
  16. package/dist/engine/concurrency-limiter.js.map +1 -0
  17. package/dist/engine/engine.d.ts +15 -5
  18. package/dist/engine/engine.js +510 -848
  19. package/dist/engine/engine.js.map +1 -1
  20. package/dist/engine/in-flight.d.ts +15 -0
  21. package/dist/engine/in-flight.js +80 -0
  22. package/dist/engine/in-flight.js.map +1 -0
  23. package/dist/engine/operation.d.ts +7 -0
  24. package/dist/engine/operation.js +49 -0
  25. package/dist/engine/operation.js.map +1 -0
  26. package/dist/engine/provider-calls.d.ts +52 -0
  27. package/dist/engine/provider-calls.js +262 -0
  28. package/dist/engine/provider-calls.js.map +1 -0
  29. package/dist/engine/query.d.ts +26 -0
  30. package/dist/engine/query.js +430 -0
  31. package/dist/engine/query.js.map +1 -0
  32. package/dist/engine/response-meta.d.ts +15 -0
  33. package/dist/engine/response-meta.js +33 -0
  34. package/dist/engine/response-meta.js.map +1 -0
  35. package/dist/engine/runtime.d.ts +5 -0
  36. package/dist/engine/runtime.js +48 -0
  37. package/dist/engine/runtime.js.map +1 -0
  38. package/dist/engine/search-discovery.d.ts +3 -0
  39. package/dist/engine/search-discovery.js +35 -0
  40. package/dist/engine/search-discovery.js.map +1 -0
  41. package/dist/engine/search-enrichment-shared.d.ts +23 -0
  42. package/dist/engine/search-enrichment-shared.js +68 -0
  43. package/dist/engine/search-enrichment-shared.js.map +1 -0
  44. package/dist/engine/search-enrichment.d.ts +42 -0
  45. package/dist/engine/search-enrichment.js +150 -0
  46. package/dist/engine/search-enrichment.js.map +1 -0
  47. package/dist/engine/search-identity-snapshot.d.ts +15 -0
  48. package/dist/engine/search-identity-snapshot.js +101 -0
  49. package/dist/engine/search-identity-snapshot.js.map +1 -0
  50. package/dist/engine/search-outcomes.d.ts +32 -0
  51. package/dist/engine/search-outcomes.js +118 -0
  52. package/dist/engine/search-outcomes.js.map +1 -0
  53. package/dist/engine/search-poster-enrichment.d.ts +31 -0
  54. package/dist/engine/search-poster-enrichment.js +93 -0
  55. package/dist/engine/search-poster-enrichment.js.map +1 -0
  56. package/dist/engine/search-result-freeze.d.ts +13 -0
  57. package/dist/engine/search-result-freeze.js +155 -0
  58. package/dist/engine/search-result-freeze.js.map +1 -0
  59. package/dist/engine/stale-fallback.d.ts +11 -0
  60. package/dist/engine/stale-fallback.js +60 -0
  61. package/dist/engine/stale-fallback.js.map +1 -0
  62. package/dist/engine/timeout-budget.d.ts +7 -0
  63. package/dist/engine/timeout-budget.js +31 -0
  64. package/dist/engine/timeout-budget.js.map +1 -0
  65. package/dist/engine/torrents.d.ts +22 -0
  66. package/dist/engine/torrents.js +188 -0
  67. package/dist/engine/torrents.js.map +1 -0
  68. package/dist/engine/types.d.ts +35 -0
  69. package/dist/errors/types.d.ts +1 -1
  70. package/dist/errors/types.js.map +1 -1
  71. package/dist/index.d.ts +2 -1
  72. package/dist/index.js +1 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/media/types.d.ts +1 -1
  75. package/dist/merge/details-identity.d.ts +5 -0
  76. package/dist/merge/details-identity.js +63 -0
  77. package/dist/merge/details-identity.js.map +1 -0
  78. package/dist/merge/diversity.d.ts +15 -0
  79. package/dist/merge/diversity.js +52 -0
  80. package/dist/merge/diversity.js.map +1 -0
  81. package/dist/merge/fields.d.ts +25 -0
  82. package/dist/merge/fields.js +364 -0
  83. package/dist/merge/fields.js.map +1 -0
  84. package/dist/merge/grouping.d.ts +3 -0
  85. package/dist/merge/grouping.js +143 -0
  86. package/dist/merge/grouping.js.map +1 -0
  87. package/dist/merge/identity.d.ts +5 -0
  88. package/dist/merge/identity.js +21 -0
  89. package/dist/merge/identity.js.map +1 -0
  90. package/dist/merge/internal.d.ts +22 -0
  91. package/dist/merge/internal.js +15 -0
  92. package/dist/merge/internal.js.map +1 -0
  93. package/dist/merge/media-type.d.ts +5 -0
  94. package/dist/merge/media-type.js +15 -0
  95. package/dist/merge/media-type.js.map +1 -0
  96. package/dist/merge/priority.d.ts +9 -0
  97. package/dist/merge/priority.js +63 -0
  98. package/dist/merge/priority.js.map +1 -0
  99. package/dist/merge/scoring.d.ts +15 -0
  100. package/dist/merge/scoring.js +364 -0
  101. package/dist/merge/scoring.js.map +1 -0
  102. package/dist/merge/strategy.js +66 -955
  103. package/dist/merge/strategy.js.map +1 -1
  104. package/dist/merge/title.d.ts +4 -0
  105. package/dist/merge/title.js +22 -0
  106. package/dist/merge/title.js.map +1 -0
  107. package/dist/providers/registry.d.ts +4 -2
  108. package/dist/providers/registry.js +11 -2
  109. package/dist/providers/registry.js.map +1 -1
  110. package/dist/providers/types.d.ts +4 -0
  111. package/dist/response/types.d.ts +21 -0
  112. package/dist/search/types.d.ts +34 -0
  113. package/dist/testing/providers.js +2 -0
  114. package/dist/testing/providers.js.map +1 -1
  115. package/dist/torrent/index.d.ts +1 -0
  116. package/dist/torrent/index.js +2 -0
  117. package/dist/torrent/index.js.map +1 -0
  118. package/dist/torrent/types.d.ts +132 -0
  119. package/dist/torrent/types.js +2 -0
  120. package/dist/torrent/types.js.map +1 -0
  121. package/package.json +13 -4
package/README.md CHANGED
@@ -1,295 +1,80 @@
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
25
+ const search = await media.search({ title: "Interstellar" });
26
+ const details = await media.getDetails({ imdb: "tt0816692" });
66
27
 
67
- A provider is a small object that implements the `MediaProvider` contract:
68
-
69
- ```ts
70
- import type { MediaProvider } from "@media-engine/core";
71
-
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
+ Details lookup requires a namespaced external ID through `ids` or a shortcut such as `imdb`. The plain `id` field is deprecated because provider-native IDs are not globally unique.
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
+ The engine also has `getAvailability()` for optional streaming providers and a separate `discoverTorrents()` contract for torrent discovery providers.
97
35
 
98
- Applications can bound slower optional providers independently while keeping a global upper limit:
36
+ ## What comes from core
99
37
 
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
- ```
38
+ - `MediaEngine`;
39
+ - search, details, media, streaming, and torrent-discovery types;
40
+ - metadata, streaming, and torrent provider contracts;
41
+ - merge and cache interfaces;
42
+ - `MemoryCache`;
43
+ - normalized errors and provider failure metadata;
44
+ - mock providers and fixtures for tests.
110
45
 
111
- The effective timeout is the smaller of `timeoutMs` and the matching `providerTimeouts` value.
46
+ 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`. Search failures and debug timings include an optional execution `phase`; repeated failures from one provider are represented once in the public failure list. Mandatory retryable primary/fallback degradation is not stored in the normal cache.
112
47
 
113
- ## Streaming Availability Contract
48
+ Title search distinguishes primary discovery from slower fallback identity sources through optional `capabilities.search.titleDiscovery`. Custom providers are primary by default. Supported multi-word typos are broadened through primary providers when no exact title exists, even if weak fuzzy noise is present. Fallback providers run when the remaining result is empty, a multi-word query has no exact identity, or exact-title identities conflict. External-ID search still calls every compatible provider immediately.
114
49
 
115
- Streaming availability is separate from metadata search and details. Configure streaming providers through `streamingProviders` and call `getAvailability` with a media or episode identity.
50
+ With a cache configured, the first healthy mandatory discovery whose top candidate has a strong external ID stores a separate identity snapshot for 30 minutes without refreshing it inside that window. Equivalent cache misses with another `limit` share up to 20 confirmed candidates and retain their known order even when a successful upstream response drifts. Stabilization adds `SEARCH_IDENTITY_SNAPSHOT_STABILIZED`. A retryably degraded partial search uses `SEARCH_IDENTITY_SNAPSHOT_FALLBACK` while retaining `meta.providers.failed`, `meta.cached: false`, and the no-cache policy. Neither path accepts conflicting strong IDs; non-retryable degradation does not use the snapshot, and a weak top candidate without a strong ID cannot establish one. Debug mode exposes restored/reordered counters. A first cold degraded request has no snapshot to recover from.
116
51
 
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
- };
52
+ Providers can separately set `capabilities.searchEnrichment: false` to stay out of best-effort search-card ID/poster work. This keeps short optional enrichment deadlines from consuming the reliability budget of a mandatory fallback identity source.
142
53
 
143
- const media = new MediaEngine({
144
- providers: [createMockProvider()],
145
- streamingProviders: [streamingProvider],
146
- });
54
+ Optional search ID/poster enrichment failures do not discard the base results. They produce bounded `meta.warnings`, remain cacheable with those warnings, and expose attempted/skipped/succeeded/failed counters plus phase-aware timings when debug mode is enabled. One planner limits enrichment to the bounded top discovery window, at most six additional calls, at most two calls per provider, and 1.5 seconds total. It skips providers that cannot improve a missing field and reuses matching ID-search plus cached or in-flight details outcomes for poster selection.
147
55
 
148
- const availability = await media.getAvailability({
149
- type: "movie",
150
- imdb: "tt0816692",
151
- });
152
- ```
56
+ Mandatory discovery and eligible snapshot recovery freeze result identity, score, and order before optional enrichment. Matching enrichment may add presentation fields, non-conflicting external IDs, and source attribution, including aliases that make an unresolved candidate relevant. It never adds provider candidates as new results, changes `id`, `type`, `title`, `originalTitle`, or `year`, recalculates a score, or reranks the response. Conflicting added IDs retain the discovery value and emit `EXTERNAL_ID_CONFLICT`.
153
57
 
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.
58
+ Mandatory ranking favors close multi-word title completions and external IDs that support reliable cross-catalog follow-up. Popular anime catalog identities remain competitive when their audience is established; small audience counters and ratings without vote counts do not receive full ranking weight.
155
59
 
156
- ## Search Response Example
60
+ The built-in strategy keeps the first result and every score unchanged, but may interleave a similarly ranked candidate inside the top ten after two results from the same normalized matched-title/media-type family. The alternative must be within `0.03` score and `0.05` title relevance, so weak noise is not promoted merely for variety. Debug mode adds optional per-result `ranking` evidence with the formula, match/title evidence, weighted signal contributions, and raw-score/diversity/final positions; normal responses omit it.
157
61
 
158
- ```ts
159
- import { MediaEngine, createSuccessProvider } from "@media-engine/core";
62
+ `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`.
160
63
 
161
- const media = new MediaEngine({
162
- providers: [createSuccessProvider()],
163
- });
64
+ Public search, details, availability, and torrent-discovery inputs are canonicalized before provider selection and cache/coalescing keys are built: strings and IDs are trimmed, language is lowercased, top-level ID shortcuts become `ids`, torrent `alternativeTitles` are trimmed and deduplicated, and provider filters are trimmed, deduplicated, and sorted. Known IMDb/numeric ID formats and bounded field lengths and alias counts are validated. Search and torrent discovery with `limit: 0` return empty uncached responses without provider or cache work.
164
65
 
165
- const response = await media.search({
166
- title: "Interstellar",
167
- });
168
- ```
66
+ `MemoryCache` accepts only non-negative safe-integer TTL values. Omit `defaultTtlMs` and per-entry `ttlMs` for entries without expiration; negative values are not a no-expiry sentinel. Stale TTL values follow the same numeric validation.
169
67
 
170
- Shape:
68
+ `search`, `getDetails`, `getAvailability`, and `discoverTorrents` accept optional `{ signal }` operation options. Identical requests still share one provider operation, but each caller has an independent subscription: aborting one caller does not affect the others, while the shared provider signal is aborted once no active subscribers remain. Fully cancelled work is not cached, and client cancellation is not counted as an upstream circuit-breaker failure.
171
69
 
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
- ```
70
+ A streaming provider that resolves `null` is recorded as a successful no-result lookup. The engine reports an all-failed error only when every selected streaming provider actually failed. Discovered player options with an uncertain validation result remain visible with `availability: "unknown"`; the response includes `STREAM_VALIDATION_DEGRADED` and is retried instead of being stored in the normal availability cache.
280
71
 
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.
72
+ The constructor also accepts streaming and torrent 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.
282
73
 
283
- ## Testing Utilities
74
+ Torrent discovery remains independent from streaming availability. A `TorrentProvider` returns normalized candidates with source attribution and an explicit `magnet`, `torrent_file`, or `external` handoff. Optional provider `catalog` metadata supports regional/international UI grouping, and a candidate may retain a concrete meta-indexer `catalogSource`; neither field guarantees a release audio language. Core does not open that handoff, download torrent metadata, join a swarm, select files, store media, proxy traffic, or transcode video. No concrete torrent provider is bundled in core.
284
75
 
285
- The core package exports deterministic helpers for tests and examples:
76
+ 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 four main operations without repeating every field.
286
77
 
287
- - `createMockProvider`;
288
- - `createSuccessProvider`;
289
- - `createFailingProvider`;
290
- - `createTimeoutProvider`;
291
- - `sampleMovie`;
292
- - `sampleSeries`;
293
- - `sampleAnime`.
78
+ ## License
294
79
 
295
- These helpers do not call real APIs and do not require API keys.
80
+ MIT
package/README.ru.md ADDED
@@ -0,0 +1,80 @@
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
+ Для загрузки деталей нужен внешний ID с указанием источника — через `ids` или сокращение вроде `imdb`. Обычное поле `id` устарело, потому что внутренние ID разных провайдеров не образуют общее пространство имён.
33
+
34
+ Для опциональных стриминговых провайдеров у движка также есть `getAvailability()`, а для torrent discovery — отдельный контракт `discoverTorrents()`.
35
+
36
+ ## Что экспортирует core
37
+
38
+ - `MediaEngine`;
39
+ - типы поиска, деталей, медиа, стриминга и torrent discovery;
40
+ - контракты metadata-, streaming- и torrent-провайдеров;
41
+ - интерфейсы объединения и кеша;
42
+ - `MemoryCache`;
43
+ - нормализованные ошибки и сведения о сбоях провайдеров;
44
+ - mock-провайдеры и примеры данных для тестов.
45
+
46
+ Провайдеры запускаются параллельно. Если один источник не сработал, а другой вернул данные, полезный ответ сохранится, а ошибка попадёт в `meta.providers.failed`. Search-ошибки и debug timings содержат опциональную фазу выполнения `phase`; повторные ошибки одного провайдера представлены одной записью в публичном списке. Обязательная retryable-деградация primary/fallback не записывается в normal cache.
47
+
48
+ Title search разделяет основные discovery-источники и более медленные fallback identity-источники через опциональный `capabilities.search.titleDiscovery`. Пользовательские провайдеры по умолчанию считаются primary. Поддерживаемые multi-word опечатки расширяются через primary-провайдеры при отсутствии точного title, даже если присутствует weak fuzzy noise. Fallback-провайдеры запускаются при пустом результате, отсутствии точной identity у multi-word запроса или конфликтующих точных title-identity. Поиск по внешнему ID по-прежнему сразу вызывает все совместимые провайдеры.
49
+
50
+ При настроенном cache первая здоровая mandatory discovery, у top-кандидата которой есть strong external ID, отдельно сохраняет identity snapshot на 30 минут без продления этого окна. Эквивалентные cache misses с другим `limit` используют до 20 общих подтвержденных кандидатов и сохраняют их известный порядок, даже если успешный upstream-ответ меняется. Такая стабилизация добавляет `SEARCH_IDENTITY_SNAPSHOT_STABILIZED`. Retryable-деградированный частичный поиск использует `SEARCH_IDENTITY_SNAPSHOT_FALLBACK`, сохраняя `meta.providers.failed`, `meta.cached: false` и запрет обычного кеширования. Оба пути не принимают конфликтующий strong ID; при non-retryable-деградации snapshot не применяется, а слабый top-кандидат без strong ID не может создать snapshot. Debug-режим показывает счетчики restored/reordered. У первого холодного деградированного запроса snapshot для восстановления ещё нет.
51
+
52
+ Отдельный `capabilities.searchEnrichment: false` исключает провайдер из best-effort ID/poster enrichment поисковых карточек. Так короткий optional enrichment deadline не расходует reliability budget обязательного fallback identity-источника.
53
+
54
+ Ошибки опционального ID/poster enrichment не удаляют базовые результаты. Они формируют ограниченные `meta.warnings`, могут кэшироваться вместе с предупреждениями, а в debug-режиме предоставляют счетчики attempted/skipped/succeeded/failed и phase-aware timings. Единый planner ограничивает enrichment ограниченным top discovery window, максимум шестью дополнительными вызовами, двумя вызовами одного провайдера и общим временем 1,5 секунды. Он пропускает провайдеры, неспособные улучшить отсутствующее поле, и переиспользует совпадающий ID-search, а также cached или in-flight details для выбора poster.
55
+
56
+ Mandatory discovery и подходящее snapshot recovery фиксируют identity, score и порядок результатов до optional enrichment. Совпавшее enrichment может добавить presentation-поля, непротиворечивые external IDs и source attribution, включая aliases, которые делают ранее unresolved candidate релевантным. Оно не добавляет provider candidates как новые результаты, не меняет `id`, `type`, `title`, `originalTitle` или `year`, не пересчитывает score и не переставляет ответ. При конфликте добавляемого ID сохраняется discovery-значение и формируется `EXTERNAL_ID_CONFLICT`.
57
+
58
+ Mandatory ranking предпочитает близкие по длине multi-word title completions и external IDs, пригодные для надёжного cross-catalog follow-up. Популярные anime catalog identities остаются конкурентными при подтверждённой аудитории; малые audience counters и ratings без vote count не получают полный ranking weight.
59
+
60
+ Встроенная стратегия сохраняет первый результат и все score, но внутри top-10 может поднять сопоставимый кандидат после двух результатов одной normalized matched-title/media-type family. Альтернатива должна отличаться не более чем на `0.03` по score и `0.05` по title relevance, поэтому слабый шум не продвигается только ради разнообразия. В debug-режиме результат получает опциональный `ranking` с formula, match/title evidence, взвешенными signal contributions и позициями raw score/diversity/final; в обычном ответе этого поля нет.
61
+
62
+ `MemoryCache` может сохранять метаданные в отдельном ограниченном stale-окне. `MediaEngine` использует их только для поиска и деталей, когда все выбранные провайдеры завершились с retryable-ошибками; устаревшие streaming-ссылки никогда не возвращаются. В таком ответе `meta.cached` и `meta.stale` равны `true`.
63
+
64
+ Публичные search, details, availability и torrent-discovery запросы приводятся к canonical-виду до выбора провайдеров и построения cache/coalescing keys: строки и ID обрезаются, язык переводится в нижний регистр, top-level ID shortcuts переносятся в `ids`, torrent `alternativeTitles` обрезаются и дедуплицируются, а provider filters обрезаются, дедуплицируются и сортируются. Известные форматы IMDb/numeric ID, длины полей и количество aliases валидируются. Search и torrent discovery с `limit: 0` возвращают пустые некешированные ответы без обращений к провайдерам или cache.
65
+
66
+ `MemoryCache` принимает для TTL только неотрицательные safe integer значения. Для записей без срока истечения не задавайте `defaultTtlMs` и per-entry `ttlMs`; отрицательные значения не являются no-expiry sentinel. Для stale TTL действует та же числовая валидация.
67
+
68
+ `search`, `getDetails`, `getAvailability` и `discoverTorrents` принимают опциональные operation options `{ signal }`. Одинаковые запросы по-прежнему делят одну provider operation, но у каждого caller отдельная подписка: отмена одного caller не влияет на остальных, а общий provider signal отменяется, когда активных подписчиков не осталось. Полностью отмененная работа не кешируется, а client cancellation не считается upstream-сбоем circuit breaker.
69
+
70
+ Если streaming-провайдер возвращает `null`, это считается успешным запросом без результата. Ошибка all-failed возникает, только когда действительно завершились ошибкой все выбранные streaming-провайдеры. Найденные плееры с неопределённым результатом проверки остаются в ответе с `availability: "unknown"`; engine добавляет `STREAM_VALIDATION_DEGRADED` и повторяет lookup вместо записи такого ответа в обычный availability-кеш.
71
+
72
+ Конструктор также принимает streaming- и torrent-провайдеры, кеш, общий и индивидуальные тайм-ауты, собственную стратегию объединения и debug-режим. По умолчанию одновременно выполняются не более двух операций каждого провайдера, а отменяемая очередь ограничена 100 элементами; `providerConcurrency` позволяет настроить отдельные лимиты или отключить gate. Ожидание в очереди входит в существующий тайм-аут провайдера. Сам core никогда не импортирует пакеты с конкретными провайдерами.
73
+
74
+ Torrent discovery не смешивается со streaming availability. `TorrentProvider` возвращает нормализованные кандидаты с source attribution и явным handoff типа `magnet`, `torrent_file` или `external`. Опциональные provider `catalog` metadata позволяют UI группировать региональные и международные каталоги, а candidate может сохранять конкретный `catalogSource` meta-indexer; ни одно из этих полей не гарантирует язык аудиодорожки релиза. Core не открывает handoff, не загружает torrent metadata, не подключается к swarm, не выбирает файлы, не хранит media, не проксирует трафик и не транскодирует видео. Сам core не содержит конкретного torrent-провайдера.
75
+
76
+ Точные типы доступны через exports пакета. В коротком [описании публичного API](https://github.com/Yaneart/media-engine/blob/main/docs/public-api.md) разобраны четыре основные операции без перечисления каждого поля.
77
+
78
+ ## Лицензия
79
+
80
+ 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,18 @@ 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
+ validateTtl("defaultTtlMs", options.defaultTtlMs);
15
+ validateTtl("defaultStaleTtlMs", options.defaultStaleTtlMs);
13
16
  this.now = options.now ?? Date.now;
14
17
  this.defaultTtlMs = options.defaultTtlMs;
18
+ this.defaultStaleTtlMs = options.defaultStaleTtlMs;
15
19
  this.maxEntries = options.maxEntries;
16
20
  }
17
21
  // Reads a cached value and removes it when it has expired.
@@ -22,6 +26,23 @@ export class MemoryCache {
22
26
  return undefined;
23
27
  }
24
28
  if (this.isExpired(entry)) {
29
+ this.deleteIfBeyondStaleWindow(key, entry);
30
+ return undefined;
31
+ }
32
+ if (this.maxEntries !== undefined) {
33
+ this.entries.delete(key);
34
+ this.entries.set(key, entry);
35
+ }
36
+ return cloneCacheValue(entry.value);
37
+ }
38
+ // Reads a value only after normal TTL expiration and before its stale window closes.
39
+ // Читает значение только после обычного TTL и до завершения stale-окна.
40
+ getStale(key) {
41
+ const entry = this.entries.get(key);
42
+ if (!entry || !this.isExpired(entry)) {
43
+ return undefined;
44
+ }
45
+ if (this.isBeyondStaleWindow(entry)) {
25
46
  this.entries.delete(key);
26
47
  return undefined;
27
48
  }
@@ -35,9 +56,15 @@ export class MemoryCache {
35
56
  // Сохраняет значение с опциональным TTL в миллисекундах.
36
57
  set(key, value, options = {}) {
37
58
  const ttlMs = options.ttlMs ?? this.defaultTtlMs;
38
- const expiresAt = ttlMs === undefined || ttlMs < 0 ? undefined : this.now() + ttlMs;
59
+ const staleTtlMs = options.staleTtlMs ?? this.defaultStaleTtlMs;
60
+ validateTtl("ttlMs", ttlMs);
61
+ validateTtl("staleTtlMs", staleTtlMs);
62
+ const expiresAt = ttlMs === undefined ? undefined : this.now() + ttlMs;
63
+ const staleUntil = expiresAt === undefined || staleTtlMs === undefined || staleTtlMs <= 0
64
+ ? undefined
65
+ : expiresAt + staleTtlMs;
39
66
  this.entries.delete(key);
40
- this.entries.set(key, { value: cloneCacheValue(value), expiresAt });
67
+ this.entries.set(key, { value: cloneCacheValue(value), expiresAt, staleUntil });
41
68
  this.evictOverflow();
42
69
  }
43
70
  // Removes one cache entry by key.
@@ -55,6 +82,14 @@ export class MemoryCache {
55
82
  isExpired(entry) {
56
83
  return entry.expiresAt !== undefined && entry.expiresAt <= this.now();
57
84
  }
85
+ isBeyondStaleWindow(entry) {
86
+ return entry.staleUntil === undefined || entry.staleUntil <= this.now();
87
+ }
88
+ deleteIfBeyondStaleWindow(key, entry) {
89
+ if (this.isBeyondStaleWindow(entry)) {
90
+ this.entries.delete(key);
91
+ }
92
+ }
58
93
  // Evicts expired entries first, then least-recently-used entries until the bound is met.
59
94
  // Сначала удаляет истекшие записи, затем давно не использованные до соблюдения лимита.
60
95
  evictOverflow() {
@@ -62,7 +97,7 @@ export class MemoryCache {
62
97
  return;
63
98
  }
64
99
  for (const [key, entry] of this.entries) {
65
- if (this.isExpired(entry)) {
100
+ if (this.isExpired(entry) && this.isBeyondStaleWindow(entry)) {
66
101
  this.entries.delete(key);
67
102
  }
68
103
  }
@@ -80,4 +115,9 @@ export class MemoryCache {
80
115
  function cloneCacheValue(value) {
81
116
  return structuredClone(value);
82
117
  }
118
+ function validateTtl(name, value) {
119
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
120
+ throw new TypeError(`MemoryCache ${name} must be a non-negative safe integer.`);
121
+ }
122
+ }
83
123
  //# sourceMappingURL=memory.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/cache/memory.ts"],"names":[],"mappings":"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"}
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;
@@ -1,6 +1,12 @@
1
1
  import type { ExternalIds, MediaDetails, MediaType } from "../media/index.js";
2
2
  import type { ResponseMeta } from "../response/index.js";
3
3
  export interface DetailsQuery {
4
+ /**
5
+ * @deprecated Provider-native IDs do not share a global namespace. Use `ids`
6
+ * or a named external ID shortcut such as `imdb` or `kinopoisk`.
7
+ * RU: внутренние ID провайдеров не имеют общего namespace; используйте `ids`
8
+ * или именованное сокращение внешнего ID.
9
+ */
4
10
  id?: string;
5
11
  ids?: ExternalIds;
6
12
  imdb?: string;
@@ -0,0 +1,6 @@
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;
6
+ export declare function hasUnknownStreamValidation(availability: MediaAvailability): boolean;