mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 17:36:15 -08:00
fix(downloads): harden offline metadata resolution
This commit is contained in:
1 parent
e7467788ed
commit
7357120612
7 files changed
+376
-101
No files matched your search
@@ -0,0 +1,82 @@
|
||||
import type {
|
||||
DownloadMetadataSnapshot,
|
||||
StalkerVodInfo,
|
||||
XtreamSerieInfo,
|
||||
XtreamVodInfo,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
|
||||
export function movieSeed(snapshot: DownloadMetadataSnapshot): XtreamVodInfo {
|
||||
const cast = snapshot.cast?.map(({ name }) => name).join(', ') ?? '';
|
||||
return {
|
||||
kinopoisk_url: '',
|
||||
tmdb_id: snapshot.tmdbId ?? 0,
|
||||
name: snapshot.title,
|
||||
o_name: snapshot.originalTitle ?? '',
|
||||
cover_big: snapshot.posterUrl ?? '',
|
||||
movie_image: snapshot.posterUrl ?? '',
|
||||
releasedate: snapshot.releaseDate ?? '',
|
||||
episode_run_time: snapshot.durationMinutes ?? 0,
|
||||
youtube_trailer: '',
|
||||
director: snapshot.creators?.map(({ name }) => name).join(', ') ?? '',
|
||||
actors: cast,
|
||||
cast,
|
||||
description: snapshot.plot ?? '',
|
||||
plot: snapshot.plot ?? '',
|
||||
age: '',
|
||||
mpaa_rating: '',
|
||||
rating_count_kinopoisk: 0,
|
||||
country: '',
|
||||
genre: snapshot.genres?.join(', ') ?? '',
|
||||
backdrop_path: snapshot.backdropUrl ? [snapshot.backdropUrl] : [],
|
||||
duration_secs: (snapshot.durationMinutes ?? 0) * 60,
|
||||
duration: '',
|
||||
video: [],
|
||||
audio: [],
|
||||
bitrate: 0,
|
||||
rating: snapshot.rating ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function seriesSeed(
|
||||
snapshot: DownloadMetadataSnapshot
|
||||
): XtreamSerieInfo {
|
||||
return {
|
||||
name: snapshot.title,
|
||||
cover: snapshot.posterUrl ?? '',
|
||||
plot: snapshot.plot ?? '',
|
||||
cast: snapshot.cast?.map(({ name }) => name).join(', ') ?? '',
|
||||
director: snapshot.creators?.map(({ name }) => name).join(', ') ?? '',
|
||||
genre: snapshot.genres?.join(', ') ?? '',
|
||||
releaseDate: snapshot.releaseDate ?? '',
|
||||
last_modified: '',
|
||||
rating: snapshot.rating === undefined ? '' : String(snapshot.rating),
|
||||
rating_5based: 0,
|
||||
backdrop_path: snapshot.backdropUrl ? [snapshot.backdropUrl] : [],
|
||||
youtube_trailer: '',
|
||||
episode_run_time:
|
||||
snapshot.durationMinutes === undefined
|
||||
? ''
|
||||
: String(snapshot.durationMinutes),
|
||||
category_id: snapshot.providerCategoryId ?? '',
|
||||
tmdb_id: snapshot.tmdbId,
|
||||
};
|
||||
}
|
||||
|
||||
export function stalkerSeed(
|
||||
snapshot: DownloadMetadataSnapshot
|
||||
): StalkerVodInfo {
|
||||
return {
|
||||
movie_image: snapshot.posterUrl ?? '',
|
||||
description: snapshot.plot ?? '',
|
||||
name: snapshot.title,
|
||||
o_name: snapshot.originalTitle,
|
||||
actors: snapshot.cast?.map(({ name }) => name).join(', ') ?? '',
|
||||
director: snapshot.creators?.map(({ name }) => name).join(', ') ?? '',
|
||||
releasedate: snapshot.releaseDate ?? '',
|
||||
genre: snapshot.genres?.join(', ') ?? '',
|
||||
rating_imdb:
|
||||
snapshot.rating === undefined ? '' : String(snapshot.rating),
|
||||
rating_kinopoisk: '',
|
||||
tmdb_id: snapshot.tmdbId,
|
||||
};
|
||||
}
|
||||
@@ -126,6 +126,48 @@ describe('download metadata mapper', () => {
|
||||
expect(mapped).not.toHaveProperty('cmd');
|
||||
});
|
||||
|
||||
it('normalizes nested Stalker series identity and legacy editorial fallbacks', () => {
|
||||
const fallback: DownloadMetadataSnapshot = {
|
||||
version: 1,
|
||||
language: 'en',
|
||||
mediaKind: 'series',
|
||||
title: 'Downloaded episode title',
|
||||
episode: {
|
||||
title: 'Local episode',
|
||||
seasonNumber: 2,
|
||||
episodeNumber: 4,
|
||||
},
|
||||
};
|
||||
|
||||
const mapped = mapProviderToDownloadSnapshot({
|
||||
source: 'stalker',
|
||||
language: 'en',
|
||||
mediaKind: 'series',
|
||||
fallback,
|
||||
provider: {
|
||||
title: 'Root episode title',
|
||||
screenshot_uri:
|
||||
'https://images.example.test/stills/legacy-series.jpg',
|
||||
genres_str: 'Drama, Mystery',
|
||||
year: '2024',
|
||||
info: {
|
||||
name: 'Nested parent series title',
|
||||
o_name: 'Original parent title',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mapped.title).toBe('Nested parent series title');
|
||||
expect(mapped.originalTitle).toBe('Original parent title');
|
||||
expect(mapped.posterUrl).toBe(
|
||||
'https://images.example.test/stills/legacy-series.jpg'
|
||||
);
|
||||
expect(mapped.genres).toEqual(['Drama', 'Mystery']);
|
||||
expect(mapped.releaseDate).toBe('2024');
|
||||
expect(mapped.year).toBe(2024);
|
||||
expect(mapped.episode).toEqual(fallback.episode);
|
||||
});
|
||||
|
||||
it('bounds mapped people and genres to the persisted DTO limits', () => {
|
||||
const mapped = mapProviderToDownloadSnapshot({
|
||||
source: 'xtream',
|
||||
|
||||
@@ -12,10 +12,12 @@ import {
|
||||
import type {
|
||||
DownloadMetadataPerson,
|
||||
DownloadMetadataSnapshot,
|
||||
StalkerVodInfo,
|
||||
XtreamSerieInfo,
|
||||
XtreamVodInfo,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
movieSeed,
|
||||
seriesSeed,
|
||||
stalkerSeed,
|
||||
} from './download-metadata-tmdb-seeds';
|
||||
|
||||
export type DownloadMetadataProviderSource = 'xtream' | 'stalker';
|
||||
|
||||
@@ -128,6 +130,7 @@ function durationMinutes(
|
||||
}
|
||||
|
||||
export function mapProviderToDownloadSnapshot({
|
||||
source,
|
||||
language,
|
||||
mediaKind,
|
||||
fallback,
|
||||
@@ -137,16 +140,36 @@ export function mapProviderToDownloadSnapshot({
|
||||
const info = record(root['info']);
|
||||
const editorial = [info, root];
|
||||
const identity = [root, info];
|
||||
const title = string(first(identity, ['title', 'name'])) ?? fallback.title;
|
||||
const title =
|
||||
(source === 'stalker'
|
||||
? (string(info?.['name']) ??
|
||||
string(info?.['o_name']) ??
|
||||
string(root['o_name']) ??
|
||||
string(root['name']) ??
|
||||
string(root['title']))
|
||||
: string(first(identity, ['title', 'name']))) ?? fallback.title;
|
||||
const originalTitle =
|
||||
string(first(editorial, ['o_name', 'originalTitle'])) ??
|
||||
fallback.originalTitle;
|
||||
const plot =
|
||||
string(first(editorial, ['plot', 'description'])) ?? fallback.plot;
|
||||
const releaseDate =
|
||||
string(first(editorial, ['releaseDate', 'releasedate'])) ??
|
||||
fallback.releaseDate;
|
||||
const providerGenres = strings(first(editorial, ['genre', 'genres']));
|
||||
string(
|
||||
first(
|
||||
editorial,
|
||||
source === 'stalker'
|
||||
? ['releaseDate', 'releasedate', 'year']
|
||||
: ['releaseDate', 'releasedate']
|
||||
)
|
||||
) ?? fallback.releaseDate;
|
||||
const providerGenres = strings(
|
||||
first(
|
||||
editorial,
|
||||
source === 'stalker'
|
||||
? ['genre', 'genres', 'genres_str']
|
||||
: ['genre', 'genres']
|
||||
)
|
||||
);
|
||||
const providerCast = people(
|
||||
first(editorial, ['tmdb_cast', 'actors', 'cast'])
|
||||
);
|
||||
@@ -155,14 +178,19 @@ export function mapProviderToDownloadSnapshot({
|
||||
);
|
||||
const posterUrl =
|
||||
string(
|
||||
first(editorial, [
|
||||
'movie_image',
|
||||
'cover_big',
|
||||
'cover',
|
||||
'poster_url',
|
||||
'posterUrl',
|
||||
'logo',
|
||||
])
|
||||
source === 'stalker'
|
||||
? (info?.['movie_image'] ??
|
||||
root['cover'] ??
|
||||
root['screenshot_uri'] ??
|
||||
root['logo'])
|
||||
: first(editorial, [
|
||||
'movie_image',
|
||||
'cover_big',
|
||||
'cover',
|
||||
'poster_url',
|
||||
'posterUrl',
|
||||
'logo',
|
||||
])
|
||||
) ?? fallback.posterUrl;
|
||||
const backdropUrl =
|
||||
backdrop(
|
||||
@@ -213,78 +241,6 @@ export function mapProviderToDownloadSnapshot({
|
||||
});
|
||||
}
|
||||
|
||||
function movieSeed(snapshot: DownloadMetadataSnapshot): XtreamVodInfo {
|
||||
const cast = snapshot.cast?.map(({ name }) => name).join(', ') ?? '';
|
||||
return {
|
||||
kinopoisk_url: '',
|
||||
tmdb_id: snapshot.tmdbId ?? 0,
|
||||
name: snapshot.title,
|
||||
o_name: snapshot.originalTitle ?? '',
|
||||
cover_big: snapshot.posterUrl ?? '',
|
||||
movie_image: snapshot.posterUrl ?? '',
|
||||
releasedate: snapshot.releaseDate ?? '',
|
||||
episode_run_time: snapshot.durationMinutes ?? 0,
|
||||
youtube_trailer: '',
|
||||
director: snapshot.creators?.map(({ name }) => name).join(', ') ?? '',
|
||||
actors: cast,
|
||||
cast,
|
||||
description: snapshot.plot ?? '',
|
||||
plot: snapshot.plot ?? '',
|
||||
age: '',
|
||||
mpaa_rating: '',
|
||||
rating_count_kinopoisk: 0,
|
||||
country: '',
|
||||
genre: snapshot.genres?.join(', ') ?? '',
|
||||
backdrop_path: snapshot.backdropUrl ? [snapshot.backdropUrl] : [],
|
||||
duration_secs: (snapshot.durationMinutes ?? 0) * 60,
|
||||
duration: '',
|
||||
video: [],
|
||||
audio: [],
|
||||
bitrate: 0,
|
||||
rating: snapshot.rating ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
function seriesSeed(snapshot: DownloadMetadataSnapshot): XtreamSerieInfo {
|
||||
return {
|
||||
name: snapshot.title,
|
||||
cover: snapshot.posterUrl ?? '',
|
||||
plot: snapshot.plot ?? '',
|
||||
cast: snapshot.cast?.map(({ name }) => name).join(', ') ?? '',
|
||||
director: snapshot.creators?.map(({ name }) => name).join(', ') ?? '',
|
||||
genre: snapshot.genres?.join(', ') ?? '',
|
||||
releaseDate: snapshot.releaseDate ?? '',
|
||||
last_modified: '',
|
||||
rating: snapshot.rating === undefined ? '' : String(snapshot.rating),
|
||||
rating_5based: 0,
|
||||
backdrop_path: snapshot.backdropUrl ? [snapshot.backdropUrl] : [],
|
||||
youtube_trailer: '',
|
||||
episode_run_time:
|
||||
snapshot.durationMinutes === undefined
|
||||
? ''
|
||||
: String(snapshot.durationMinutes),
|
||||
category_id: snapshot.providerCategoryId ?? '',
|
||||
tmdb_id: snapshot.tmdbId,
|
||||
};
|
||||
}
|
||||
|
||||
function stalkerSeed(snapshot: DownloadMetadataSnapshot): StalkerVodInfo {
|
||||
return {
|
||||
movie_image: snapshot.posterUrl ?? '',
|
||||
description: snapshot.plot ?? '',
|
||||
name: snapshot.title,
|
||||
o_name: snapshot.originalTitle,
|
||||
actors: snapshot.cast?.map(({ name }) => name).join(', ') ?? '',
|
||||
director: snapshot.creators?.map(({ name }) => name).join(', ') ?? '',
|
||||
releasedate: snapshot.releaseDate ?? '',
|
||||
genre: snapshot.genres?.join(', ') ?? '',
|
||||
rating_imdb:
|
||||
snapshot.rating === undefined ? '' : String(snapshot.rating),
|
||||
rating_kinopoisk: '',
|
||||
tmdb_id: snapshot.tmdbId,
|
||||
};
|
||||
}
|
||||
|
||||
export function mergeSnapshotWithTmdb(
|
||||
snapshot: DownloadMetadataSnapshot,
|
||||
details: TmdbMovieDetails | TmdbTvDetails,
|
||||
|
||||
+76
-1
@@ -148,6 +148,7 @@ describe('DownloadOfflineMetadataService', () => {
|
||||
let downloads: DownloadsFake;
|
||||
let playlists: PlaylistsFake;
|
||||
let tmdb: TmdbFake;
|
||||
let currentLanguage: string;
|
||||
|
||||
beforeEach(() => {
|
||||
db = {
|
||||
@@ -165,6 +166,7 @@ describe('DownloadOfflineMetadataService', () => {
|
||||
enrichMovie: jest.fn().mockResolvedValue(null),
|
||||
enrichTv: jest.fn().mockResolvedValue(null),
|
||||
};
|
||||
currentLanguage = 'en';
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -175,7 +177,7 @@ describe('DownloadOfflineMetadataService', () => {
|
||||
{ provide: TmdbEnrichmentService, useValue: tmdb },
|
||||
{
|
||||
provide: SettingsStore,
|
||||
useValue: { language: () => 'en' },
|
||||
useValue: { language: () => currentLanguage },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -330,6 +332,79 @@ describe('DownloadOfflineMetadataService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps Stalker merge precedence when recent lookup fails', async () => {
|
||||
playlists.getPlaylistById.mockReturnValue(of(STALKER_PLAYLIST));
|
||||
playlists.getPortalRecentlyViewed.mockReturnValue(
|
||||
throwError(() => new Error('recent unavailable'))
|
||||
);
|
||||
tmdb.isEnabled.mockReturnValue(true);
|
||||
tmdb.enrichMovie.mockResolvedValue({
|
||||
id: 603,
|
||||
overview: 'Localized TMDB plot',
|
||||
vote_average: 7.2,
|
||||
vote_count: 100,
|
||||
});
|
||||
const local = snapshot({ plot: undefined, rating: 9.1 });
|
||||
|
||||
const resolved = await service.resolve(movieDetail({}, local));
|
||||
|
||||
expect(resolved.plot).toBe('Localized TMDB plot');
|
||||
expect(resolved.rating).toBe(9.1);
|
||||
expect(db.getContentByXtreamId).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('labels a provider-missing successful TMDB refresh with the current language', async () => {
|
||||
currentLanguage = 'de';
|
||||
tmdb.isEnabled.mockReturnValue(true);
|
||||
tmdb.enrichMovie.mockResolvedValue({
|
||||
id: 603,
|
||||
overview: 'Lokalisierte TMDB-Beschreibung',
|
||||
});
|
||||
const local = snapshot({ language: 'en' });
|
||||
|
||||
const resolved = await service.resolve(movieDetail({}, local));
|
||||
|
||||
expect(db.getContentByXtreamId).toHaveBeenCalledWith(41, PLAYLIST_ID);
|
||||
expect(resolved.language).toBe('de');
|
||||
expect(downloads.updateMetadata).toHaveBeenCalledWith(
|
||||
17,
|
||||
expect.objectContaining({ language: 'de' })
|
||||
);
|
||||
});
|
||||
|
||||
it('does not relabel a mismatched snapshot when no refresh succeeds', async () => {
|
||||
currentLanguage = 'de';
|
||||
const local = snapshot({ language: 'en' });
|
||||
|
||||
await expect(service.resolve(movieDetail({}, local))).resolves.toEqual(
|
||||
local
|
||||
);
|
||||
expect(downloads.updateMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('omits credential-bearing provider artwork before persistence', async () => {
|
||||
db.getContentByXtreamId.mockResolvedValue({
|
||||
id: 1,
|
||||
category_id: 12,
|
||||
title: 'Recovered provider title',
|
||||
rating: '7',
|
||||
added: '0',
|
||||
poster_url:
|
||||
'https://images.example.test/authorization/value/poster.jpg',
|
||||
xtream_id: 41,
|
||||
type: 'movie',
|
||||
});
|
||||
const local = snapshot({ plot: undefined, posterUrl: undefined });
|
||||
|
||||
const resolved = await service.resolve(movieDetail({}, local));
|
||||
|
||||
expect(resolved.posterUrl).toBeUndefined();
|
||||
expect(downloads.updateMetadata).toHaveBeenCalledWith(
|
||||
17,
|
||||
expect.not.objectContaining({ posterUrl: expect.anything() })
|
||||
);
|
||||
});
|
||||
|
||||
it('persists one materially changed successful merge', async () => {
|
||||
tmdb.isEnabled.mockReturnValue(true);
|
||||
tmdb.enrichMovie.mockResolvedValue(tmdbMovie);
|
||||
|
||||
+27
-5
@@ -209,7 +209,11 @@ export class DownloadOfflineMetadataService {
|
||||
resolved = local;
|
||||
}
|
||||
}
|
||||
resolved = await this.enrichWithTmdb(resolved, context.source);
|
||||
resolved = await this.enrichWithTmdb(
|
||||
resolved,
|
||||
context.source,
|
||||
language
|
||||
);
|
||||
|
||||
if (materiallyEqual(local, resolved)) {
|
||||
return local;
|
||||
@@ -238,15 +242,21 @@ export class DownloadOfflineMetadataService {
|
||||
detail: DownloadOfflineDetail
|
||||
): Promise<ProviderContext> {
|
||||
const item = representative(detail);
|
||||
let source: DownloadMetadataProviderSource;
|
||||
try {
|
||||
const playlist = await firstValueFrom(
|
||||
this.playlists.getPlaylistById(item.playlistId)
|
||||
);
|
||||
const source: DownloadMetadataProviderSource =
|
||||
source =
|
||||
playlist?.portalUrl && playlist.macAddress
|
||||
? 'stalker'
|
||||
: 'xtream';
|
||||
if (source === 'stalker') {
|
||||
} catch {
|
||||
return { source: 'xtream' };
|
||||
}
|
||||
|
||||
if (source === 'stalker') {
|
||||
try {
|
||||
const recent = await firstValueFrom(
|
||||
this.playlists.getPortalRecentlyViewed(item.playlistId)
|
||||
);
|
||||
@@ -254,7 +264,12 @@ export class DownloadOfflineMetadataService {
|
||||
source,
|
||||
provider: matchingRecent(recent, targetId(detail)),
|
||||
};
|
||||
} catch {
|
||||
return { source };
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
source,
|
||||
provider: await this.db.getContentByXtreamId(
|
||||
@@ -269,7 +284,8 @@ export class DownloadOfflineMetadataService {
|
||||
|
||||
private async enrichWithTmdb(
|
||||
snapshot: DownloadMetadataSnapshot,
|
||||
source: DownloadMetadataProviderSource
|
||||
source: DownloadMetadataProviderSource,
|
||||
language: string
|
||||
): Promise<DownloadMetadataSnapshot> {
|
||||
try {
|
||||
if (!this.tmdb.isEnabled()) {
|
||||
@@ -286,7 +302,13 @@ export class DownloadOfflineMetadataService {
|
||||
? await this.tmdb.enrichMovie(query)
|
||||
: await this.tmdb.enrichTv(query);
|
||||
return details
|
||||
? mergeSnapshotWithTmdb(snapshot, details, source)
|
||||
? mergeSnapshotWithTmdb(
|
||||
snapshot.language === language
|
||||
? snapshot
|
||||
: { ...snapshot, language },
|
||||
details,
|
||||
source
|
||||
)
|
||||
: snapshot;
|
||||
} catch {
|
||||
return snapshot;
|
||||
|
||||
@@ -127,4 +127,77 @@ describe('download metadata snapshot factories', () => {
|
||||
'https://images.example.test/posters/secret-garden.jpg'
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'accesskey',
|
||||
'accesstoken',
|
||||
'apikey',
|
||||
'auth',
|
||||
'authentication',
|
||||
'authorization',
|
||||
'cookie',
|
||||
'credential',
|
||||
'credentials',
|
||||
'devicemac',
|
||||
'key',
|
||||
'mac',
|
||||
'macaddress',
|
||||
'oauth',
|
||||
'password',
|
||||
'passwd',
|
||||
'privatekey',
|
||||
'refreshtoken',
|
||||
'secret',
|
||||
'secretkey',
|
||||
'session',
|
||||
'sig',
|
||||
'signature',
|
||||
'signingkey',
|
||||
'token',
|
||||
])('drops artwork containing backend credential path alias %s', (alias) => {
|
||||
const snapshot = createMovieDownloadSnapshot({
|
||||
language: 'en',
|
||||
title: 'Unsafe artwork',
|
||||
posterUrl: `https://images.example.test/${alias}/value/poster.jpg`,
|
||||
});
|
||||
|
||||
expect(snapshot.posterUrl).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
'authentication',
|
||||
'authorization',
|
||||
'cookie',
|
||||
'credential',
|
||||
'macaddress',
|
||||
'oauth',
|
||||
'password',
|
||||
'passwd',
|
||||
'secret',
|
||||
'session',
|
||||
'signature',
|
||||
'token',
|
||||
'auth',
|
||||
'key',
|
||||
'mac',
|
||||
'sig',
|
||||
'proxy-auth',
|
||||
'device-mac',
|
||||
'access-key',
|
||||
'api-key',
|
||||
'private-key',
|
||||
'secret-key',
|
||||
'signing-key',
|
||||
])(
|
||||
'drops artwork containing backend credential query alias %s',
|
||||
(alias) => {
|
||||
const snapshot = createMovieDownloadSnapshot({
|
||||
language: 'en',
|
||||
title: 'Unsafe artwork',
|
||||
posterUrl: `https://images.example.test/poster.jpg?${alias}=value`,
|
||||
});
|
||||
|
||||
expect(snapshot.posterUrl).toBeUndefined();
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -24,31 +24,44 @@ const IMAGE_PATH_HINTS = new Set([
|
||||
'still',
|
||||
'stills',
|
||||
]);
|
||||
// Keep these aliases synchronized with the canonical Electron validator in
|
||||
// apps/electron-backend/src/app/events/database/download-artwork-url.ts.
|
||||
const CREDENTIAL_PATH_KEYS = new Set([
|
||||
'accesskey',
|
||||
'accesstoken',
|
||||
'apikey',
|
||||
'auth',
|
||||
'authentication',
|
||||
'authorization',
|
||||
'cookie',
|
||||
'credential',
|
||||
'credentials',
|
||||
'devicemac',
|
||||
'key',
|
||||
'mac',
|
||||
'macaddress',
|
||||
'oauth',
|
||||
'password',
|
||||
'passwd',
|
||||
'privatekey',
|
||||
'refreshtoken',
|
||||
'secret',
|
||||
'secretkey',
|
||||
'session',
|
||||
'sig',
|
||||
'signature',
|
||||
'signingkey',
|
||||
'token',
|
||||
]);
|
||||
const CREDENTIAL_QUERY_TERMS = [
|
||||
'auth',
|
||||
'authentication',
|
||||
'authorization',
|
||||
'cookie',
|
||||
'credential',
|
||||
'key',
|
||||
'mac',
|
||||
'macaddress',
|
||||
'oauth',
|
||||
'password',
|
||||
'passwd',
|
||||
'secret',
|
||||
'session',
|
||||
'signature',
|
||||
@@ -94,6 +107,19 @@ function normalizedToken(value: string): string {
|
||||
return value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
}
|
||||
|
||||
function isCredentialQueryKey(key: string): boolean {
|
||||
const token = normalizedToken(key);
|
||||
return (
|
||||
['auth', 'key', 'mac', 'sig'].includes(token) ||
|
||||
token.endsWith('auth') ||
|
||||
token.endsWith('mac') ||
|
||||
CREDENTIAL_QUERY_TERMS.some((term) => token.includes(term)) ||
|
||||
['accesskey', 'apikey', 'privatekey', 'secretkey', 'signingkey'].some(
|
||||
(term) => token.includes(term)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function credentialPath(pathname: string): boolean {
|
||||
return pathname
|
||||
.split('/')
|
||||
@@ -150,14 +176,13 @@ function artworkUrl(value: string | undefined): string | undefined {
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
let credentialQuery = false;
|
||||
let hasCredentialQuery = false;
|
||||
url.searchParams.forEach((_value, key) => {
|
||||
const token = normalizedToken(key);
|
||||
if (CREDENTIAL_QUERY_TERMS.some((term) => token.includes(term))) {
|
||||
credentialQuery = true;
|
||||
if (isCredentialQueryKey(key)) {
|
||||
hasCredentialQuery = true;
|
||||
}
|
||||
});
|
||||
if (credentialQuery) {
|
||||
if (hasCredentialQuery) {
|
||||
return undefined;
|
||||
}
|
||||
return normalized;
|
||||
|
||||
Reference in new issue
Block a user