@media-engine/core 0.1.1 → 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.
- package/README.md +32 -6
- package/README.ru.md +32 -6
- package/dist/cache/memory.js +10 -8
- package/dist/cache/memory.js.map +1 -1
- package/dist/details/types.d.ts +6 -0
- package/dist/engine/availability.d.ts +1 -0
- package/dist/engine/availability.js +8 -0
- package/dist/engine/availability.js.map +1 -1
- package/dist/engine/circuit-breaker.d.ts +1 -0
- package/dist/engine/circuit-breaker.js +11 -0
- package/dist/engine/circuit-breaker.js.map +1 -1
- package/dist/engine/concurrency-limiter.js +2 -2
- package/dist/engine/concurrency-limiter.js.map +1 -1
- package/dist/engine/engine.d.ts +9 -4
- package/dist/engine/engine.js +277 -102
- package/dist/engine/engine.js.map +1 -1
- package/dist/engine/in-flight.d.ts +12 -1
- package/dist/engine/in-flight.js +69 -14
- package/dist/engine/in-flight.js.map +1 -1
- package/dist/engine/operation.d.ts +7 -0
- package/dist/engine/operation.js +49 -0
- package/dist/engine/operation.js.map +1 -0
- package/dist/engine/provider-calls.d.ts +10 -0
- package/dist/engine/provider-calls.js +62 -12
- package/dist/engine/provider-calls.js.map +1 -1
- package/dist/engine/query.d.ts +5 -0
- package/dist/engine/query.js +223 -34
- package/dist/engine/query.js.map +1 -1
- package/dist/engine/response-meta.d.ts +3 -1
- package/dist/engine/response-meta.js +8 -1
- package/dist/engine/response-meta.js.map +1 -1
- package/dist/engine/runtime.d.ts +2 -0
- package/dist/engine/runtime.js +19 -0
- package/dist/engine/runtime.js.map +1 -1
- package/dist/engine/search-discovery.d.ts +3 -0
- package/dist/engine/search-discovery.js +35 -0
- package/dist/engine/search-discovery.js.map +1 -0
- package/dist/engine/search-enrichment-shared.d.ts +23 -0
- package/dist/engine/search-enrichment-shared.js +68 -0
- package/dist/engine/search-enrichment-shared.js.map +1 -0
- package/dist/engine/search-enrichment.d.ts +21 -7
- package/dist/engine/search-enrichment.js +135 -75
- package/dist/engine/search-enrichment.js.map +1 -1
- package/dist/engine/search-identity-snapshot.d.ts +15 -0
- package/dist/engine/search-identity-snapshot.js +101 -0
- package/dist/engine/search-identity-snapshot.js.map +1 -0
- package/dist/engine/search-outcomes.d.ts +32 -0
- package/dist/engine/search-outcomes.js +118 -0
- package/dist/engine/search-outcomes.js.map +1 -0
- package/dist/engine/search-poster-enrichment.d.ts +31 -0
- package/dist/engine/search-poster-enrichment.js +93 -0
- package/dist/engine/search-poster-enrichment.js.map +1 -0
- package/dist/engine/search-result-freeze.d.ts +13 -0
- package/dist/engine/search-result-freeze.js +155 -0
- package/dist/engine/search-result-freeze.js.map +1 -0
- package/dist/engine/torrents.d.ts +22 -0
- package/dist/engine/torrents.js +188 -0
- package/dist/engine/torrents.js.map +1 -0
- package/dist/engine/types.d.ts +6 -1
- package/dist/errors/types.d.ts +1 -1
- package/dist/errors/types.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/media/types.d.ts +1 -1
- package/dist/merge/diversity.d.ts +15 -0
- package/dist/merge/diversity.js +52 -0
- package/dist/merge/diversity.js.map +1 -0
- package/dist/merge/priority.js +3 -1
- package/dist/merge/priority.js.map +1 -1
- package/dist/merge/scoring.d.ts +10 -0
- package/dist/merge/scoring.js +132 -28
- package/dist/merge/scoring.js.map +1 -1
- package/dist/merge/strategy.js +53 -13
- package/dist/merge/strategy.js.map +1 -1
- package/dist/providers/registry.d.ts +4 -2
- package/dist/providers/registry.js +11 -2
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/types.d.ts +3 -0
- package/dist/response/types.d.ts +20 -0
- package/dist/search/types.d.ts +34 -0
- package/dist/testing/providers.js +2 -0
- package/dist/testing/providers.js.map +1 -1
- package/dist/torrent/index.d.ts +1 -0
- package/dist/torrent/index.js +2 -0
- package/dist/torrent/index.js.map +1 -0
- package/dist/torrent/types.d.ts +132 -0
- package/dist/torrent/types.js +2 -0
- package/dist/torrent/types.js.map +1 -0
- package/package.json +8 -3
|
@@ -1,28 +1,42 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { DetailsQuery } from "../details/index.js";
|
|
2
|
+
import type { ExternalIds, MediaDetails } from "../media/index.js";
|
|
2
3
|
import type { MergeStrategy } from "../merge/index.js";
|
|
3
4
|
import type { ProviderRegistry } from "../providers/index.js";
|
|
4
5
|
import type { MediaSearchResult } from "../search/index.js";
|
|
5
6
|
import type { ProviderCircuitBreaker } from "./circuit-breaker.js";
|
|
6
7
|
import type { ProviderConcurrencyLimiter } from "./concurrency-limiter.js";
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
import { type SearchPosterEnrichment } from "./search-poster-enrichment.js";
|
|
9
|
+
import type { SearchIdEnrichment } from "./search-result-freeze.js";
|
|
10
|
+
interface SearchEnrichmentPlannerInput {
|
|
11
|
+
results: MediaSearchResult[];
|
|
12
|
+
publicLimit: number | undefined;
|
|
9
13
|
language: string | undefined;
|
|
10
14
|
excludedProviders: ReadonlySet<string>;
|
|
11
15
|
registry: ProviderRegistry;
|
|
12
16
|
mergeStrategy: MergeStrategy;
|
|
13
17
|
debug: boolean;
|
|
18
|
+
signal?: AbortSignal;
|
|
14
19
|
circuitBreaker?: ProviderCircuitBreaker | undefined;
|
|
15
20
|
concurrencyLimiter?: ProviderConcurrencyLimiter | undefined;
|
|
16
21
|
getProviderTimeoutMs(providerName: string): number | undefined;
|
|
22
|
+
loadReusableDetails(query: DetailsQuery, signal: AbortSignal | undefined, maxWaitMs: number): Promise<MediaDetails | undefined>;
|
|
17
23
|
}
|
|
18
|
-
|
|
24
|
+
interface SearchEnrichmentPlanResult {
|
|
25
|
+
idEnrichments: SearchIdEnrichment[];
|
|
26
|
+
skippedId: number;
|
|
27
|
+
detailsEnrichments: SearchDetailsEnrichment[];
|
|
28
|
+
posterEnrichments: SearchPosterEnrichment[];
|
|
29
|
+
skippedPoster: number;
|
|
30
|
+
}
|
|
31
|
+
interface SearchDetailsEnrichment {
|
|
19
32
|
ids: ExternalIds | undefined;
|
|
20
|
-
|
|
33
|
+
details: MediaDetails;
|
|
21
34
|
}
|
|
35
|
+
export declare function executeSearchEnrichmentPlan(input: SearchEnrichmentPlannerInput): Promise<SearchEnrichmentPlanResult>;
|
|
22
36
|
export declare function needsSearchEnrichment(item: {
|
|
23
37
|
ratings?: unknown[];
|
|
24
38
|
description?: string;
|
|
25
39
|
poster?: unknown;
|
|
26
40
|
}): boolean;
|
|
27
|
-
export declare function
|
|
28
|
-
export
|
|
41
|
+
export declare function applySearchDetailsEnrichments(results: MediaSearchResult[], enrichments: SearchDetailsEnrichment[]): MediaSearchResult[];
|
|
42
|
+
export {};
|
|
@@ -1,90 +1,150 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { callTimedProviderSearch } from "./provider-calls.js";
|
|
2
|
+
import { hasExternalIds } from "./query.js";
|
|
3
|
+
import { createSearchDetailsQuery, hasSharedExternalId, SearchEnrichmentCallBudget, supportsSearchEnrichment, supportsSearchEnrichmentFeature, } from "./search-enrichment-shared.js";
|
|
4
|
+
import { loadSearchPoster } from "./search-poster-enrichment.js";
|
|
5
|
+
const SEARCH_ENRICHMENT_MAX_CANDIDATES = 6;
|
|
6
|
+
const SEARCH_POSTER_MAX_CANDIDATES = 3;
|
|
7
|
+
// Runs one bounded plan for ID and canonical-poster enrichment of the visible top results.
|
|
8
|
+
// Выполняет единый bounded plan для ID и canonical-poster enrichment видимых top results.
|
|
9
|
+
export async function executeSearchEnrichmentPlan(input) {
|
|
10
|
+
const candidateLimit = Math.min(SEARCH_ENRICHMENT_MAX_CANDIDATES, input.publicLimit ?? SEARCH_ENRICHMENT_MAX_CANDIDATES);
|
|
11
|
+
const candidates = input.results.slice(0, candidateLimit);
|
|
12
|
+
const posterCandidateLimit = Math.min(SEARCH_POSTER_MAX_CANDIDATES, candidateLimit);
|
|
13
|
+
const budget = new SearchEnrichmentCallBudget(input.getProviderTimeoutMs);
|
|
14
|
+
const idEnrichments = [];
|
|
15
|
+
const detailsEnrichments = [];
|
|
16
|
+
const posterEnrichments = [];
|
|
17
|
+
let skippedId = 0;
|
|
18
|
+
for (const [index, result] of candidates.entries()) {
|
|
19
|
+
const hasIds = hasExternalIds(result.item.ids);
|
|
20
|
+
const query = hasIds ? createSearchDetailsQuery(result, input.language) : undefined;
|
|
21
|
+
const reusableDetails = query
|
|
22
|
+
? await input.loadReusableDetails(query, input.signal, budget.getRemainingWallTimeMs())
|
|
23
|
+
: undefined;
|
|
24
|
+
const planningResult = reusableDetails
|
|
25
|
+
? applyReusableDetailsToResult(result, reusableDetails)
|
|
26
|
+
: result;
|
|
27
|
+
const idEnrichment = planIdEnrichment(input, planningResult, budget);
|
|
28
|
+
if (reusableDetails) {
|
|
29
|
+
detailsEnrichments.push({ ids: result.item.ids, details: reusableDetails });
|
|
30
|
+
}
|
|
31
|
+
if (needsSearchEnrichment(result.item) && hasIds) {
|
|
32
|
+
if (idEnrichment) {
|
|
33
|
+
idEnrichments.push({ ids: result.item.ids, outcome: idEnrichment.outcome });
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
skippedId += 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (index >= posterCandidateLimit || !query) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
posterEnrichments.push(loadSearchPoster({
|
|
43
|
+
result,
|
|
44
|
+
language: input.language,
|
|
45
|
+
excludedProviders: input.excludedProviders,
|
|
46
|
+
registry: input.registry,
|
|
47
|
+
mergeStrategy: input.mergeStrategy,
|
|
48
|
+
debug: input.debug,
|
|
49
|
+
signal: input.signal,
|
|
50
|
+
circuitBreaker: input.circuitBreaker,
|
|
51
|
+
concurrencyLimiter: input.concurrencyLimiter,
|
|
52
|
+
budget,
|
|
53
|
+
reusableDetails,
|
|
54
|
+
idEnrichment,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
const [resolvedIdEnrichments, resolvedPosterEnrichments] = await Promise.all([
|
|
58
|
+
Promise.all(idEnrichments.map(async (enrichment) => ({
|
|
59
|
+
ids: enrichment.ids,
|
|
60
|
+
outcome: await enrichment.outcome,
|
|
61
|
+
}))),
|
|
62
|
+
Promise.all(posterEnrichments),
|
|
63
|
+
]);
|
|
64
|
+
return {
|
|
65
|
+
idEnrichments: resolvedIdEnrichments,
|
|
66
|
+
skippedId,
|
|
67
|
+
detailsEnrichments,
|
|
68
|
+
posterEnrichments: resolvedPosterEnrichments,
|
|
69
|
+
skippedPoster: resolvedPosterEnrichments.reduce((total, enrichment) => total + enrichment.skipped, 0),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// Enriches compact catalog hits only when a provider advertises a useful missing field.
|
|
73
|
+
// Обогащает compact catalog hits, только если provider заявляет полезное отсутствующее поле.
|
|
6
74
|
export function needsSearchEnrichment(item) {
|
|
7
75
|
return !item.ratings?.length || !item.description?.trim() || !item.poster;
|
|
8
76
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export async function loadSearchPoster(input) {
|
|
12
|
-
if (!hasExternalIds(input.result.item.ids)) {
|
|
77
|
+
function planIdEnrichment(input, result, budget) {
|
|
78
|
+
if (!needsSearchEnrichment(result.item) || !hasExternalIds(result.item.ids)) {
|
|
13
79
|
return undefined;
|
|
14
80
|
}
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
ids: input.result.item.ids,
|
|
18
|
-
language: input.language,
|
|
19
|
-
};
|
|
20
|
-
const searchProviders = new Set(input.result.sources.map((source) => source.provider));
|
|
81
|
+
const existingProviders = new Set(result.sources.map((source) => source.provider));
|
|
82
|
+
const enrichmentType = result.item.type === "anime" ? undefined : result.item.type;
|
|
21
83
|
const providers = input.registry
|
|
22
|
-
.
|
|
23
|
-
.filter((
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
concurrencyLimiter: input.concurrencyLimiter,
|
|
37
|
-
timeoutMs: providerTimeoutMs === undefined
|
|
38
|
-
? SEARCH_DETAILS_POSTER_ENRICHMENT_TIMEOUT_MS
|
|
39
|
-
: Math.min(providerTimeoutMs, SEARCH_DETAILS_POSTER_ENRICHMENT_TIMEOUT_MS),
|
|
40
|
-
});
|
|
41
|
-
}));
|
|
42
|
-
const loadedResults = new Map(outcomes.flatMap((outcome) => outcome.failure || !outcome.result ? [] : [[outcome.provider, outcome.result]]));
|
|
43
|
-
const providerResults = providers.flatMap((provider) => {
|
|
44
|
-
if (provider.name === reusablePosterProvider?.name) {
|
|
45
|
-
return [
|
|
46
|
-
{
|
|
47
|
-
provider: reusablePosterProvider.name,
|
|
48
|
-
details: searchItemAsDetails(input.result.item),
|
|
49
|
-
},
|
|
50
|
-
];
|
|
84
|
+
.selectSearchProviders({ ids: result.item.ids, type: enrichmentType })
|
|
85
|
+
.filter((candidate) => supportsSearchEnrichment(candidate) && !existingProviders.has(candidate.name))
|
|
86
|
+
.map((candidate, index) => ({
|
|
87
|
+
candidate,
|
|
88
|
+
index,
|
|
89
|
+
score: getMissingFieldImprovementScore(candidate, result.item),
|
|
90
|
+
}))
|
|
91
|
+
.filter(({ score }) => score > 0)
|
|
92
|
+
.sort((left, right) => right.score - left.score || left.index - right.index)
|
|
93
|
+
.map(({ candidate }) => candidate);
|
|
94
|
+
for (const provider of providers) {
|
|
95
|
+
const timeoutMs = budget.reserve(provider.name);
|
|
96
|
+
if (timeoutMs === undefined) {
|
|
97
|
+
continue;
|
|
51
98
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
99
|
+
return {
|
|
100
|
+
provider,
|
|
101
|
+
outcome: callTimedProviderSearch(provider, {
|
|
102
|
+
ids: result.item.ids,
|
|
103
|
+
type: enrichmentType,
|
|
104
|
+
limit: 1,
|
|
105
|
+
language: input.language,
|
|
106
|
+
}, {
|
|
107
|
+
debug: input.debug,
|
|
108
|
+
language: input.language,
|
|
109
|
+
signal: input.signal,
|
|
110
|
+
timeoutMs,
|
|
111
|
+
circuitBreaker: input.circuitBreaker,
|
|
112
|
+
concurrencyLimiter: input.concurrencyLimiter,
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
61
117
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
118
|
+
function getMissingFieldImprovementScore(provider, item) {
|
|
119
|
+
let score = 0;
|
|
120
|
+
if (!item.description?.trim()) {
|
|
121
|
+
score += 1;
|
|
122
|
+
}
|
|
123
|
+
if (!item.poster && supportsSearchEnrichmentFeature(provider, "posters")) {
|
|
124
|
+
score += 1;
|
|
125
|
+
}
|
|
126
|
+
if (!item.ratings?.length && supportsSearchEnrichmentFeature(provider, "ratings")) {
|
|
127
|
+
score += 1;
|
|
72
128
|
}
|
|
129
|
+
return score;
|
|
73
130
|
}
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
export function
|
|
131
|
+
// Reuses compact fields from a matching cached/in-flight details operation.
|
|
132
|
+
// Переиспользует compact-поля совпадающей cached/in-flight details operation.
|
|
133
|
+
export function applySearchDetailsEnrichments(results, enrichments) {
|
|
77
134
|
return results.map((result) => {
|
|
78
|
-
const
|
|
79
|
-
return
|
|
135
|
+
const details = enrichments.find((enrichment) => hasSharedExternalId(result.item.ids, enrichment.ids))?.details;
|
|
136
|
+
return details ? applyReusableDetailsToResult(result, details) : result;
|
|
80
137
|
});
|
|
81
138
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
139
|
+
function applyReusableDetailsToResult(result, details) {
|
|
140
|
+
return {
|
|
141
|
+
...result,
|
|
142
|
+
item: {
|
|
143
|
+
...result.item,
|
|
144
|
+
description: result.item.description?.trim() ? result.item.description : details.description,
|
|
145
|
+
poster: details.poster ?? result.item.poster,
|
|
146
|
+
ratings: result.item.ratings?.length ? result.item.ratings : details.ratings,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
89
149
|
}
|
|
90
150
|
//# sourceMappingURL=search-enrichment.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-enrichment.js","sourceRoot":"","sources":["../../src/engine/search-enrichment.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"search-enrichment.js","sourceRoot":"","sources":["../../src/engine/search-enrichment.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CacheSetOptions } from "../cache/index.js";
|
|
2
|
+
import type { SearchIdentitySnapshotDebugMeta } from "../response/index.js";
|
|
3
|
+
import type { MediaSearchResult } from "../search/index.js";
|
|
4
|
+
export interface SearchIdentitySnapshot {
|
|
5
|
+
version: 2;
|
|
6
|
+
results: MediaSearchResult[];
|
|
7
|
+
}
|
|
8
|
+
export interface SearchIdentitySnapshotRecovery {
|
|
9
|
+
results: MediaSearchResult[];
|
|
10
|
+
debug?: SearchIdentitySnapshotDebugMeta;
|
|
11
|
+
}
|
|
12
|
+
export declare const SEARCH_IDENTITY_SNAPSHOT_CACHE_OPTIONS: CacheSetOptions;
|
|
13
|
+
export declare function createSearchIdentitySnapshot(results: MediaSearchResult[]): SearchIdentitySnapshot | undefined;
|
|
14
|
+
export declare function recoverSearchIdentitySnapshot(currentResults: MediaSearchResult[], snapshot: SearchIdentitySnapshot | undefined): SearchIdentitySnapshotRecovery;
|
|
15
|
+
export declare function isUsableSearchIdentitySnapshot(value: unknown): value is SearchIdentitySnapshot;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { hasSharedStrongId, hasStrongIdConflict } from "../merge/identity.js";
|
|
2
|
+
import { STRONG_ID_KEYS } from "../merge/internal.js";
|
|
3
|
+
import { normalizeTitle, titleCandidates } from "../merge/title.js";
|
|
4
|
+
const SEARCH_IDENTITY_SNAPSHOT_TTL_MS = 30 * 60_000;
|
|
5
|
+
const SEARCH_IDENTITY_SNAPSHOT_MAX_RESULTS = 20;
|
|
6
|
+
export const SEARCH_IDENTITY_SNAPSHOT_CACHE_OPTIONS = {
|
|
7
|
+
ttlMs: SEARCH_IDENTITY_SNAPSHOT_TTL_MS,
|
|
8
|
+
staleTtlMs: 0,
|
|
9
|
+
};
|
|
10
|
+
// Keeps only a bounded prior known-good candidate list, not response metadata or cache state.
|
|
11
|
+
// Хранит только ограниченный список ранее подтвержденных кандидатов без response metadata.
|
|
12
|
+
export function createSearchIdentitySnapshot(results) {
|
|
13
|
+
const topResult = results[0];
|
|
14
|
+
if (!topResult || !STRONG_ID_KEYS.some((key) => Boolean(topResult.item.ids?.[key]))) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
version: 2,
|
|
19
|
+
results: structuredClone(results.slice(0, SEARCH_IDENTITY_SNAPSHOT_MAX_RESULTS)),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
// Restores known identities and order without treating conflicting strong IDs as one candidate.
|
|
23
|
+
// Восстанавливает identity и порядок, не объединяя кандидатов с конфликтующими strong ID.
|
|
24
|
+
export function recoverSearchIdentitySnapshot(currentResults, snapshot) {
|
|
25
|
+
if (!isUsableSearchIdentitySnapshot(snapshot)) {
|
|
26
|
+
return { results: currentResults };
|
|
27
|
+
}
|
|
28
|
+
const recovered = [];
|
|
29
|
+
const usedCurrentIndexes = new Set();
|
|
30
|
+
let restored = 0;
|
|
31
|
+
let reordered = 0;
|
|
32
|
+
for (const snapshotResult of snapshot.results) {
|
|
33
|
+
const currentIndex = currentResults.findIndex((current, index) => !usedCurrentIndexes.has(index) && isSameSearchIdentity(current.item, snapshotResult.item));
|
|
34
|
+
if (currentIndex !== -1) {
|
|
35
|
+
usedCurrentIndexes.add(currentIndex);
|
|
36
|
+
recovered.push(currentResults[currentIndex]);
|
|
37
|
+
if (currentIndex !== recovered.length - 1) {
|
|
38
|
+
reordered += 1;
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (currentResults.some((current) => hasConflictingSearchIdentity(current.item, snapshotResult.item))) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
recovered.push(structuredClone(snapshotResult));
|
|
46
|
+
restored += 1;
|
|
47
|
+
}
|
|
48
|
+
currentResults.forEach((result, index) => {
|
|
49
|
+
if (!usedCurrentIndexes.has(index)) {
|
|
50
|
+
recovered.push(result);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
if (restored === 0 && reordered === 0) {
|
|
54
|
+
return { results: currentResults };
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
results: recovered,
|
|
58
|
+
debug: {
|
|
59
|
+
applied: true,
|
|
60
|
+
restored,
|
|
61
|
+
reordered,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function isUsableSearchIdentitySnapshot(value) {
|
|
66
|
+
if (!value || typeof value !== "object") {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
const snapshot = value;
|
|
70
|
+
return snapshot.version === 2 && Array.isArray(snapshot.results) && snapshot.results.length > 0;
|
|
71
|
+
}
|
|
72
|
+
function isSameSearchIdentity(left, right) {
|
|
73
|
+
if (left.type !== right.type || hasStrongIdConflict(left.ids, right.ids)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (hasSharedStrongId(left.ids, right.ids)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (left.id === right.id &&
|
|
80
|
+
hasSharedNormalizedTitle(left, right) &&
|
|
81
|
+
(left.year === undefined || right.year === undefined || left.year === right.year)) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return hasSameTitleYearIdentity(left, right);
|
|
85
|
+
}
|
|
86
|
+
function hasConflictingSearchIdentity(left, right) {
|
|
87
|
+
return (left.type === right.type &&
|
|
88
|
+
hasStrongIdConflict(left.ids, right.ids) &&
|
|
89
|
+
hasSameTitleYearIdentity(left, right));
|
|
90
|
+
}
|
|
91
|
+
function hasSameTitleYearIdentity(left, right) {
|
|
92
|
+
if (left.year === undefined || left.year !== right.year) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return hasSharedNormalizedTitle(left, right);
|
|
96
|
+
}
|
|
97
|
+
function hasSharedNormalizedTitle(left, right) {
|
|
98
|
+
const leftTitles = new Set(titleCandidates(left).map(normalizeTitle).filter(Boolean));
|
|
99
|
+
return titleCandidates(right).some((title) => leftTitles.has(normalizeTitle(title)));
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=search-identity-snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-identity-snapshot.js","sourceRoot":"","sources":["../../src/engine/search-identity-snapshot.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ProviderSearchResult } from "../providers/index.js";
|
|
2
|
+
import type { EngineWarning, ProviderFailure, ProviderTimingMeta, SearchEnrichmentDebugMeta } from "../response/index.js";
|
|
3
|
+
import type { ProviderDetailsCallOutcome, ProviderSearchCallOutcome } from "./provider-calls.js";
|
|
4
|
+
type MandatorySearchPhase = "primary" | "retry" | "fallback" | "provider_fallback";
|
|
5
|
+
export declare class SearchOutcomeAccumulator {
|
|
6
|
+
readonly successful: string[];
|
|
7
|
+
readonly failed: ProviderFailure[];
|
|
8
|
+
readonly results: ProviderSearchResult[];
|
|
9
|
+
readonly timings: ProviderTimingMeta[];
|
|
10
|
+
readonly warnings: EngineWarning[];
|
|
11
|
+
private readonly retryableMandatoryFailures;
|
|
12
|
+
private readonly enrichment;
|
|
13
|
+
constructor(input: {
|
|
14
|
+
successful: string[];
|
|
15
|
+
failed: ProviderFailure[];
|
|
16
|
+
results: ProviderSearchResult[];
|
|
17
|
+
timings: ProviderTimingMeta[];
|
|
18
|
+
warnings: EngineWarning[];
|
|
19
|
+
});
|
|
20
|
+
appendMandatory(outcomes: ProviderSearchCallOutcome[], phase: MandatorySearchPhase, options?: {
|
|
21
|
+
deduplicateResults?: boolean;
|
|
22
|
+
}): void;
|
|
23
|
+
appendIdEnrichment(outcomes: ProviderSearchCallOutcome[], skipped: number): void;
|
|
24
|
+
observePosterEnrichment(outcomes: ProviderDetailsCallOutcome[], skipped: number): void;
|
|
25
|
+
appendEnrichmentWarnings(): void;
|
|
26
|
+
hasRetryableMandatoryFailure(): boolean;
|
|
27
|
+
getEnrichmentDebugMeta(): SearchEnrichmentDebugMeta;
|
|
28
|
+
private observeEnrichment;
|
|
29
|
+
private upsertFailure;
|
|
30
|
+
private removeFailure;
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { appendUniqueSearchResults } from "./query.js";
|
|
2
|
+
const ID_ENRICHMENT_WARNING = "SEARCH_ID_ENRICHMENT_FAILED";
|
|
3
|
+
const POSTER_ENRICHMENT_WARNING = "SEARCH_POSTER_ENRICHMENT_FAILED";
|
|
4
|
+
// Owns phase-aware search execution state and keeps public provider summaries deduplicated.
|
|
5
|
+
// Хранит phase-aware состояние search и не допускает дублей в публичной сводке провайдеров.
|
|
6
|
+
export class SearchOutcomeAccumulator {
|
|
7
|
+
successful;
|
|
8
|
+
failed;
|
|
9
|
+
results;
|
|
10
|
+
timings;
|
|
11
|
+
warnings;
|
|
12
|
+
retryableMandatoryFailures = new Set();
|
|
13
|
+
enrichment = {
|
|
14
|
+
id: createCounters(),
|
|
15
|
+
poster: createCounters(),
|
|
16
|
+
};
|
|
17
|
+
constructor(input) {
|
|
18
|
+
this.successful = input.successful;
|
|
19
|
+
this.failed = input.failed;
|
|
20
|
+
this.results = input.results;
|
|
21
|
+
this.timings = input.timings;
|
|
22
|
+
this.warnings = input.warnings;
|
|
23
|
+
}
|
|
24
|
+
// Adds mandatory provider work and applies retry recovery to the public failure summary.
|
|
25
|
+
// Добавляет обязательную provider-работу и учитывает успешное retry-восстановление.
|
|
26
|
+
appendMandatory(outcomes, phase, options = {}) {
|
|
27
|
+
for (const outcome of outcomes) {
|
|
28
|
+
this.timings.push(withPhase(outcome.timing, phase));
|
|
29
|
+
if (outcome.failure) {
|
|
30
|
+
this.upsertFailure(outcome.failure, phase);
|
|
31
|
+
if (outcome.failure.retryable) {
|
|
32
|
+
this.retryableMandatoryFailures.add(outcome.provider);
|
|
33
|
+
}
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (phase === "retry") {
|
|
37
|
+
this.removeFailure(outcome.provider);
|
|
38
|
+
this.retryableMandatoryFailures.delete(outcome.provider);
|
|
39
|
+
}
|
|
40
|
+
if (!this.successful.includes(outcome.provider)) {
|
|
41
|
+
this.successful.push(outcome.provider);
|
|
42
|
+
}
|
|
43
|
+
if (options.deduplicateResults) {
|
|
44
|
+
appendUniqueSearchResults(this.results, outcome.results);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
this.results.push(...outcome.results);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Records optional ID enrichment without adding its candidates to mandatory discovery state.
|
|
52
|
+
// Учитывает optional ID enrichment без добавления кандидатов в mandatory discovery state.
|
|
53
|
+
appendIdEnrichment(outcomes, skipped) {
|
|
54
|
+
this.observeEnrichment(outcomes, "id_enrichment", this.enrichment.id, skipped);
|
|
55
|
+
}
|
|
56
|
+
// Records optional poster-enrichment calls without promoting them to provider failures.
|
|
57
|
+
// Учитывает optional poster enrichment без добавления его ошибок в provider failures.
|
|
58
|
+
observePosterEnrichment(outcomes, skipped) {
|
|
59
|
+
this.observeEnrichment(outcomes, "poster_enrichment", this.enrichment.poster, skipped);
|
|
60
|
+
}
|
|
61
|
+
// Adds at most one aggregate warning per optional enrichment phase.
|
|
62
|
+
// Добавляет не более одного агрегированного warning на каждую optional enrichment-фазу.
|
|
63
|
+
appendEnrichmentWarnings() {
|
|
64
|
+
appendEnrichmentWarning(this.warnings, ID_ENRICHMENT_WARNING, "ID", this.enrichment.id.failed);
|
|
65
|
+
appendEnrichmentWarning(this.warnings, POSTER_ENRICHMENT_WARNING, "poster", this.enrichment.poster.failed);
|
|
66
|
+
}
|
|
67
|
+
hasRetryableMandatoryFailure() {
|
|
68
|
+
return this.retryableMandatoryFailures.size > 0;
|
|
69
|
+
}
|
|
70
|
+
getEnrichmentDebugMeta() {
|
|
71
|
+
return structuredClone(this.enrichment);
|
|
72
|
+
}
|
|
73
|
+
observeEnrichment(outcomes, phase, counters, skipped) {
|
|
74
|
+
counters.skipped += skipped;
|
|
75
|
+
for (const outcome of outcomes) {
|
|
76
|
+
counters.attempted += 1;
|
|
77
|
+
this.timings.push(withPhase(outcome.timing, phase));
|
|
78
|
+
if (outcome.failure) {
|
|
79
|
+
counters.failed += 1;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
counters.succeeded += 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
upsertFailure(failure, phase) {
|
|
87
|
+
const phaseFailure = { ...failure, phase };
|
|
88
|
+
const index = this.failed.findIndex((existing) => existing.provider === failure.provider);
|
|
89
|
+
if (index === -1) {
|
|
90
|
+
this.failed.push(phaseFailure);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
this.failed[index] = phaseFailure;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
removeFailure(provider) {
|
|
97
|
+
const index = this.failed.findIndex((failure) => failure.provider === provider);
|
|
98
|
+
if (index !== -1) {
|
|
99
|
+
this.failed.splice(index, 1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function createCounters() {
|
|
104
|
+
return { attempted: 0, skipped: 0, succeeded: 0, failed: 0 };
|
|
105
|
+
}
|
|
106
|
+
function withPhase(timing, phase) {
|
|
107
|
+
return { ...timing, phase };
|
|
108
|
+
}
|
|
109
|
+
function appendEnrichmentWarning(warnings, code, label, failures) {
|
|
110
|
+
if (failures === 0) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
warnings.push({
|
|
114
|
+
code,
|
|
115
|
+
message: `Optional search ${label} enrichment failed for ${failures} provider ${failures === 1 ? "call" : "calls"}.`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=search-outcomes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-outcomes.js","sourceRoot":"","sources":["../../src/engine/search-outcomes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ExternalIds, Image, MediaDetails } from "../media/index.js";
|
|
2
|
+
import type { MergeStrategy } from "../merge/index.js";
|
|
3
|
+
import type { ProviderRegistry } from "../providers/index.js";
|
|
4
|
+
import type { MediaSearchResult } from "../search/index.js";
|
|
5
|
+
import type { ProviderCircuitBreaker } from "./circuit-breaker.js";
|
|
6
|
+
import type { ProviderConcurrencyLimiter } from "./concurrency-limiter.js";
|
|
7
|
+
import { type ProviderDetailsCallOutcome } from "./provider-calls.js";
|
|
8
|
+
import { type PlannedIdEnrichment, type SearchEnrichmentCallBudget } from "./search-enrichment-shared.js";
|
|
9
|
+
export interface SearchPosterEnrichment {
|
|
10
|
+
ids: ExternalIds | undefined;
|
|
11
|
+
poster: Image | undefined;
|
|
12
|
+
outcomes: ProviderDetailsCallOutcome[];
|
|
13
|
+
skipped: number;
|
|
14
|
+
}
|
|
15
|
+
interface SearchPosterLookupInput {
|
|
16
|
+
result: MediaSearchResult;
|
|
17
|
+
language: string | undefined;
|
|
18
|
+
excludedProviders: ReadonlySet<string>;
|
|
19
|
+
registry: ProviderRegistry;
|
|
20
|
+
mergeStrategy: MergeStrategy;
|
|
21
|
+
debug: boolean;
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
circuitBreaker?: ProviderCircuitBreaker | undefined;
|
|
24
|
+
concurrencyLimiter?: ProviderConcurrencyLimiter | undefined;
|
|
25
|
+
budget: SearchEnrichmentCallBudget;
|
|
26
|
+
reusableDetails?: MediaDetails;
|
|
27
|
+
idEnrichment?: PlannedIdEnrichment;
|
|
28
|
+
}
|
|
29
|
+
export declare function loadSearchPoster(input: SearchPosterLookupInput): Promise<SearchPosterEnrichment>;
|
|
30
|
+
export declare function applySearchPosterEnrichments(results: MediaSearchResult[], enrichments: SearchPosterEnrichment[]): MediaSearchResult[];
|
|
31
|
+
export {};
|