@media-engine/providers 0.1.1 → 1.1.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 (259) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +85 -25
  3. package/README.ru.md +90 -26
  4. package/dist/aniliberty-streaming/client.d.ts +33 -0
  5. package/dist/aniliberty-streaming/client.js +188 -0
  6. package/dist/aniliberty-streaming/client.js.map +1 -0
  7. package/dist/aniliberty-streaming/config.d.ts +24 -0
  8. package/dist/aniliberty-streaming/config.js +61 -0
  9. package/dist/aniliberty-streaming/config.js.map +1 -0
  10. package/dist/aniliberty-streaming/index.d.ts +4 -0
  11. package/dist/aniliberty-streaming/index.js +45 -0
  12. package/dist/aniliberty-streaming/index.js.map +1 -0
  13. package/dist/aniliberty-streaming/mapping.d.ts +3 -0
  14. package/dist/aniliberty-streaming/mapping.js +87 -0
  15. package/dist/aniliberty-streaming/mapping.js.map +1 -0
  16. package/dist/aniliberty-streaming/matching.d.ts +4 -0
  17. package/dist/aniliberty-streaming/matching.js +44 -0
  18. package/dist/aniliberty-streaming/matching.js.map +1 -0
  19. package/dist/anilist/graphql.d.ts +2 -0
  20. package/dist/anilist/graphql.js +135 -0
  21. package/dist/anilist/graphql.js.map +1 -0
  22. package/dist/anilist/index.js +8 -6
  23. package/dist/anilist/index.js.map +1 -1
  24. package/dist/bitsearch-torrent/client.d.ts +19 -0
  25. package/dist/bitsearch-torrent/client.js +181 -0
  26. package/dist/bitsearch-torrent/client.js.map +1 -0
  27. package/dist/bitsearch-torrent/config.d.ts +26 -0
  28. package/dist/bitsearch-torrent/config.js +74 -0
  29. package/dist/bitsearch-torrent/config.js.map +1 -0
  30. package/dist/bitsearch-torrent/index.d.ts +4 -0
  31. package/dist/bitsearch-torrent/index.js +35 -0
  32. package/dist/bitsearch-torrent/index.js.map +1 -0
  33. package/dist/bitsearch-torrent/mapping.d.ts +3 -0
  34. package/dist/bitsearch-torrent/mapping.js +53 -0
  35. package/dist/bitsearch-torrent/mapping.js.map +1 -0
  36. package/dist/bitsearch-torrent/matching.d.ts +3 -0
  37. package/dist/bitsearch-torrent/matching.js +13 -0
  38. package/dist/bitsearch-torrent/matching.js.map +1 -0
  39. package/dist/bitsearch-torrent/quota.d.ts +9 -0
  40. package/dist/bitsearch-torrent/quota.js +53 -0
  41. package/dist/bitsearch-torrent/quota.js.map +1 -0
  42. package/dist/cinemeta/index.js +37 -12
  43. package/dist/cinemeta/index.js.map +1 -1
  44. package/dist/ddbb-streaming/client.d.ts +24 -0
  45. package/dist/ddbb-streaming/client.js +98 -0
  46. package/dist/ddbb-streaming/client.js.map +1 -0
  47. package/dist/ddbb-streaming/config.d.ts +31 -0
  48. package/dist/ddbb-streaming/config.js +67 -0
  49. package/dist/ddbb-streaming/config.js.map +1 -0
  50. package/dist/ddbb-streaming/index.d.ts +4 -0
  51. package/dist/ddbb-streaming/index.js +53 -0
  52. package/dist/ddbb-streaming/index.js.map +1 -0
  53. package/dist/ddbb-streaming/mapping.d.ts +8 -0
  54. package/dist/ddbb-streaming/mapping.js +164 -0
  55. package/dist/ddbb-streaming/mapping.js.map +1 -0
  56. package/dist/ddbb-streaming/validation.d.ts +3 -0
  57. package/dist/ddbb-streaming/validation.js +84 -0
  58. package/dist/ddbb-streaming/validation.js.map +1 -0
  59. package/dist/experimental-streaming/index.js +55 -0
  60. package/dist/experimental-streaming/index.js.map +1 -1
  61. package/dist/filmix-streaming/client.d.ts +36 -0
  62. package/dist/filmix-streaming/client.js +241 -0
  63. package/dist/filmix-streaming/client.js.map +1 -0
  64. package/dist/filmix-streaming/config.d.ts +42 -0
  65. package/dist/filmix-streaming/config.js +93 -0
  66. package/dist/filmix-streaming/config.js.map +1 -0
  67. package/dist/filmix-streaming/index.d.ts +4 -0
  68. package/dist/filmix-streaming/index.js +48 -0
  69. package/dist/filmix-streaming/index.js.map +1 -0
  70. package/dist/filmix-streaming/mapping.d.ts +3 -0
  71. package/dist/filmix-streaming/mapping.js +109 -0
  72. package/dist/filmix-streaming/mapping.js.map +1 -0
  73. package/dist/filmix-streaming/matching.d.ts +4 -0
  74. package/dist/filmix-streaming/matching.js +41 -0
  75. package/dist/filmix-streaming/matching.js.map +1 -0
  76. package/dist/flixhq-streaming/client.d.ts +12 -0
  77. package/dist/flixhq-streaming/client.js +103 -0
  78. package/dist/flixhq-streaming/client.js.map +1 -0
  79. package/dist/flixhq-streaming/index.d.ts +2 -2
  80. package/dist/flixhq-streaming/index.js +58 -104
  81. package/dist/flixhq-streaming/index.js.map +1 -1
  82. package/dist/imdb-dataset/index.d.ts +20 -5
  83. package/dist/imdb-dataset/index.js +82 -184
  84. package/dist/imdb-dataset/index.js.map +1 -1
  85. package/dist/imdb-dataset/memory-storage.d.ts +7 -0
  86. package/dist/imdb-dataset/memory-storage.js +168 -0
  87. package/dist/imdb-dataset/memory-storage.js.map +1 -0
  88. package/dist/imdb-dataset/sqlite-import.d.ts +24 -0
  89. package/dist/imdb-dataset/sqlite-import.js +301 -0
  90. package/dist/imdb-dataset/sqlite-import.js.map +1 -0
  91. package/dist/imdb-dataset/sqlite-runtime.d.ts +3 -0
  92. package/dist/imdb-dataset/sqlite-runtime.js +11 -0
  93. package/dist/imdb-dataset/sqlite-runtime.js.map +1 -0
  94. package/dist/imdb-dataset/sqlite-schema.d.ts +6 -0
  95. package/dist/imdb-dataset/sqlite-schema.js +88 -0
  96. package/dist/imdb-dataset/sqlite-schema.js.map +1 -0
  97. package/dist/imdb-dataset/sqlite-storage.d.ts +12 -0
  98. package/dist/imdb-dataset/sqlite-storage.js +208 -0
  99. package/dist/imdb-dataset/sqlite-storage.js.map +1 -0
  100. package/dist/imdb-dataset/storage.d.ts +33 -0
  101. package/dist/imdb-dataset/storage.js +2 -0
  102. package/dist/imdb-dataset/storage.js.map +1 -0
  103. package/dist/index.d.ts +11 -0
  104. package/dist/index.js +11 -0
  105. package/dist/index.js.map +1 -1
  106. package/dist/jacred-torrent/client.d.ts +24 -0
  107. package/dist/jacred-torrent/client.js +254 -0
  108. package/dist/jacred-torrent/client.js.map +1 -0
  109. package/dist/jacred-torrent/config.d.ts +24 -0
  110. package/dist/jacred-torrent/config.js +70 -0
  111. package/dist/jacred-torrent/config.js.map +1 -0
  112. package/dist/jacred-torrent/index.d.ts +4 -0
  113. package/dist/jacred-torrent/index.js +45 -0
  114. package/dist/jacred-torrent/index.js.map +1 -0
  115. package/dist/jacred-torrent/mapping.d.ts +3 -0
  116. package/dist/jacred-torrent/mapping.js +129 -0
  117. package/dist/jacred-torrent/mapping.js.map +1 -0
  118. package/dist/jacred-torrent/matching.d.ts +3 -0
  119. package/dist/jacred-torrent/matching.js +19 -0
  120. package/dist/jacred-torrent/matching.js.map +1 -0
  121. package/dist/kinobd/index.js +5 -6
  122. package/dist/kinobd/index.js.map +1 -1
  123. package/dist/kinobd-streaming/availability.js +52 -29
  124. package/dist/kinobd-streaming/availability.js.map +1 -1
  125. package/dist/kinobd-streaming/candidates.d.ts +3 -2
  126. package/dist/kinobd-streaming/candidates.js +89 -37
  127. package/dist/kinobd-streaming/candidates.js.map +1 -1
  128. package/dist/kinobd-streaming/config.d.ts +15 -0
  129. package/dist/kinobd-streaming/config.js +21 -17
  130. package/dist/kinobd-streaming/config.js.map +1 -1
  131. package/dist/kinobd-streaming/index.d.ts +1 -1
  132. package/dist/kinobd-streaming/index.js.map +1 -1
  133. package/dist/kinobd-streaming/players.d.ts +2 -1
  134. package/dist/kinobd-streaming/players.js +7 -7
  135. package/dist/kinobd-streaming/players.js.map +1 -1
  136. package/dist/kinobd-streaming/request-budget.d.ts +15 -0
  137. package/dist/kinobd-streaming/request-budget.js +61 -0
  138. package/dist/kinobd-streaming/request-budget.js.map +1 -0
  139. package/dist/kinobd-streaming/validation.d.ts +5 -3
  140. package/dist/kinobd-streaming/validation.js +78 -49
  141. package/dist/kinobd-streaming/validation.js.map +1 -1
  142. package/dist/magnetz-torrent/client.d.ts +16 -0
  143. package/dist/magnetz-torrent/client.js +235 -0
  144. package/dist/magnetz-torrent/client.js.map +1 -0
  145. package/dist/magnetz-torrent/config.d.ts +27 -0
  146. package/dist/magnetz-torrent/config.js +73 -0
  147. package/dist/magnetz-torrent/config.js.map +1 -0
  148. package/dist/magnetz-torrent/index.d.ts +4 -0
  149. package/dist/magnetz-torrent/index.js +35 -0
  150. package/dist/magnetz-torrent/index.js.map +1 -0
  151. package/dist/magnetz-torrent/mapping.d.ts +3 -0
  152. package/dist/magnetz-torrent/mapping.js +52 -0
  153. package/dist/magnetz-torrent/mapping.js.map +1 -0
  154. package/dist/magnetz-torrent/matching.d.ts +3 -0
  155. package/dist/magnetz-torrent/matching.js +5 -0
  156. package/dist/magnetz-torrent/matching.js.map +1 -0
  157. package/dist/magnetz-torrent/request-gate.d.ts +9 -0
  158. package/dist/magnetz-torrent/request-gate.js +48 -0
  159. package/dist/magnetz-torrent/request-gate.js.map +1 -0
  160. package/dist/package-version.d.ts +2 -0
  161. package/dist/package-version.js +5 -0
  162. package/dist/package-version.js.map +1 -0
  163. package/dist/rutube-streaming/client.d.ts +18 -0
  164. package/dist/rutube-streaming/client.js +106 -0
  165. package/dist/rutube-streaming/client.js.map +1 -0
  166. package/dist/rutube-streaming/config.d.ts +26 -0
  167. package/dist/rutube-streaming/config.js +57 -0
  168. package/dist/rutube-streaming/config.js.map +1 -0
  169. package/dist/rutube-streaming/index.d.ts +4 -0
  170. package/dist/rutube-streaming/index.js +38 -0
  171. package/dist/rutube-streaming/index.js.map +1 -0
  172. package/dist/rutube-streaming/mapping.d.ts +4 -0
  173. package/dist/rutube-streaming/mapping.js +32 -0
  174. package/dist/rutube-streaming/mapping.js.map +1 -0
  175. package/dist/rutube-streaming/matching.d.ts +2 -0
  176. package/dist/rutube-streaming/matching.js +37 -0
  177. package/dist/rutube-streaming/matching.js.map +1 -0
  178. package/dist/shared/abort.d.ts +1 -0
  179. package/dist/shared/abort.js +22 -0
  180. package/dist/shared/abort.js.map +1 -1
  181. package/dist/shared/http.d.ts +8 -2
  182. package/dist/shared/http.js +37 -67
  183. package/dist/shared/http.js.map +1 -1
  184. package/dist/shared/index.d.ts +1 -0
  185. package/dist/shared/index.js +1 -0
  186. package/dist/shared/index.js.map +1 -1
  187. package/dist/shared/mapping.js +3 -1
  188. package/dist/shared/mapping.js.map +1 -1
  189. package/dist/shared/output-url.d.ts +2 -0
  190. package/dist/shared/output-url.js +50 -0
  191. package/dist/shared/output-url.js.map +1 -0
  192. package/dist/shared/rate-limit.js +2 -18
  193. package/dist/shared/rate-limit.js.map +1 -1
  194. package/dist/shared/response-body.d.ts +6 -0
  195. package/dist/shared/response-body.js +109 -0
  196. package/dist/shared/response-body.js.map +1 -0
  197. package/dist/shared/safe-fetch.d.ts +16 -0
  198. package/dist/shared/safe-fetch.js +300 -0
  199. package/dist/shared/safe-fetch.js.map +1 -0
  200. package/dist/shared/torrent-release-matching.d.ts +7 -0
  201. package/dist/shared/torrent-release-matching.js +143 -0
  202. package/dist/shared/torrent-release-matching.js.map +1 -0
  203. package/dist/shared/torrent-release-metadata.d.ts +4 -0
  204. package/dist/shared/torrent-release-metadata.js +95 -0
  205. package/dist/shared/torrent-release-metadata.js.map +1 -0
  206. package/dist/shikimori/index.js +4 -7
  207. package/dist/shikimori/index.js.map +1 -1
  208. package/dist/tvmaze/index.d.ts +11 -0
  209. package/dist/tvmaze/index.js +274 -0
  210. package/dist/tvmaze/index.js.map +1 -0
  211. package/dist/veoveo-streaming/client.d.ts +28 -0
  212. package/dist/veoveo-streaming/client.js +212 -0
  213. package/dist/veoveo-streaming/client.js.map +1 -0
  214. package/dist/veoveo-streaming/config.d.ts +32 -0
  215. package/dist/veoveo-streaming/config.js +67 -0
  216. package/dist/veoveo-streaming/config.js.map +1 -0
  217. package/dist/veoveo-streaming/index.d.ts +4 -0
  218. package/dist/veoveo-streaming/index.js +46 -0
  219. package/dist/veoveo-streaming/index.js.map +1 -0
  220. package/dist/veoveo-streaming/mapping.d.ts +3 -0
  221. package/dist/veoveo-streaming/mapping.js +121 -0
  222. package/dist/veoveo-streaming/mapping.js.map +1 -0
  223. package/dist/videohub-streaming/client.d.ts +33 -0
  224. package/dist/videohub-streaming/client.js +208 -0
  225. package/dist/videohub-streaming/client.js.map +1 -0
  226. package/dist/videohub-streaming/config.d.ts +34 -0
  227. package/dist/videohub-streaming/config.js +73 -0
  228. package/dist/videohub-streaming/config.js.map +1 -0
  229. package/dist/videohub-streaming/index.d.ts +4 -0
  230. package/dist/videohub-streaming/index.js +53 -0
  231. package/dist/videohub-streaming/index.js.map +1 -0
  232. package/dist/videohub-streaming/mapping.d.ts +3 -0
  233. package/dist/videohub-streaming/mapping.js +84 -0
  234. package/dist/videohub-streaming/mapping.js.map +1 -0
  235. package/dist/wikidata/cache.d.ts +11 -0
  236. package/dist/wikidata/cache.js +30 -0
  237. package/dist/wikidata/cache.js.map +1 -0
  238. package/dist/wikidata/candidates.d.ts +12 -0
  239. package/dist/wikidata/candidates.js +67 -0
  240. package/dist/wikidata/candidates.js.map +1 -0
  241. package/dist/wikidata/client.d.ts +40 -0
  242. package/dist/wikidata/client.js +224 -0
  243. package/dist/wikidata/client.js.map +1 -0
  244. package/dist/wikidata/index.d.ts +3 -0
  245. package/dist/wikidata/index.js +30 -104
  246. package/dist/wikidata/index.js.map +1 -1
  247. package/dist/yts-torrent/client.d.ts +25 -0
  248. package/dist/yts-torrent/client.js +163 -0
  249. package/dist/yts-torrent/client.js.map +1 -0
  250. package/dist/yts-torrent/config.d.ts +22 -0
  251. package/dist/yts-torrent/config.js +57 -0
  252. package/dist/yts-torrent/config.js.map +1 -0
  253. package/dist/yts-torrent/index.d.ts +4 -0
  254. package/dist/yts-torrent/index.js +42 -0
  255. package/dist/yts-torrent/index.js.map +1 -0
  256. package/dist/yts-torrent/mapping.d.ts +4 -0
  257. package/dist/yts-torrent/mapping.js +107 -0
  258. package/dist/yts-torrent/mapping.js.map +1 -0
  259. package/package.json +23 -5
@@ -0,0 +1,109 @@
1
+ import { normalizeProviderOutputUrl } from "../shared/index.js";
2
+ import { resolveFilmixQualityLink, } from "./client.js";
3
+ const QUALITY_ORDER = [720, 480];
4
+ const BLOCKED_TRANSLATION = /^(?:заблокировано|недоступно)(?:\s|$)/iu;
5
+ const RESTRICTION_VIDEO = /\/abuse_(?:\[[0-9,]+\]|%s|\d+)\.mp4$/iu;
6
+ export function mapFilmixAvailability(provider, post, query, sourceUrl, now, linkTtlMs, maxQuality = 480) {
7
+ const expiresAt = new Date(now + linkTtlMs).toISOString();
8
+ const options = post.type === "movie"
9
+ ? mapMovieOptions(provider, post, sourceUrl, expiresAt, maxQuality)
10
+ : mapEpisodeOptions(provider, post, query, sourceUrl, expiresAt, maxQuality);
11
+ if (options.length === 0)
12
+ return null;
13
+ const episodes = post.type === "series"
14
+ ? [
15
+ {
16
+ seasonNumber: query.seasonNumber,
17
+ episodeNumber: query.episodeNumber,
18
+ options,
19
+ },
20
+ ]
21
+ : undefined;
22
+ return {
23
+ query,
24
+ item: {
25
+ type: post.type,
26
+ title: post.title,
27
+ ...(post.originalTitle ? { originalTitle: post.originalTitle } : {}),
28
+ year: post.year,
29
+ },
30
+ ...(episodes ? { episodes } : {}),
31
+ options,
32
+ sourceProviders: [{ provider, ...(sourceUrl ? { url: sourceUrl } : {}) }],
33
+ checkedAt: new Date(now).toISOString(),
34
+ };
35
+ }
36
+ function mapMovieOptions(provider, post, sourceUrl, expiresAt, maxQuality) {
37
+ return uniqueOptions(post.movies.flatMap((stream, index) => {
38
+ return createOptions(provider, post.id, stream, index, sourceUrl, expiresAt, maxQuality);
39
+ }));
40
+ }
41
+ function mapEpisodeOptions(provider, post, query, sourceUrl, expiresAt, maxQuality) {
42
+ const season = post.seasons.filter((entry) => entry.number === query.seasonNumber);
43
+ if (season.length !== 1)
44
+ return [];
45
+ return uniqueOptions(season[0].translations.flatMap((translation, index) => {
46
+ const episodes = translation.episodes.filter((episode) => episode.number === query.episodeNumber);
47
+ if (episodes.length !== 1)
48
+ return [];
49
+ return createOptions(provider, post.id, episodes[0], index, sourceUrl, expiresAt, maxQuality, {
50
+ seasonNumber: season[0].number,
51
+ episodeNumber: episodes[0].number,
52
+ });
53
+ }));
54
+ }
55
+ function createOptions(provider, postId, stream, translationIndex, sourceUrl, expiresAt, maxQuality, episode) {
56
+ if (isRestrictedStream(stream))
57
+ return [];
58
+ const episodeKey = episode ? `:${episode.seasonNumber}:${episode.episodeNumber}` : "";
59
+ return QUALITY_ORDER.flatMap((quality) => {
60
+ if (quality > maxQuality || !stream.qualities.includes(quality))
61
+ return [];
62
+ const resolved = resolveFilmixQualityLink(stream.link, quality);
63
+ const url = normalizeProviderOutputUrl(resolved);
64
+ if (!url || new URL(url).pathname.toLowerCase().endsWith(".mp4") === false)
65
+ return [];
66
+ return [
67
+ {
68
+ id: `${provider}:${postId}${episodeKey}:${translationIndex}:${quality}`,
69
+ provider,
70
+ player: {
71
+ kind: "mp4",
72
+ label: "Filmix",
73
+ providerPlayerId: `${postId}${episodeKey}:${translationIndex}`,
74
+ },
75
+ translation: {
76
+ title: stream.translation,
77
+ type: "unknown",
78
+ },
79
+ quality: { label: `${quality}p`, height: quality },
80
+ ...(episode ? { episode } : {}),
81
+ access: { url },
82
+ availability: "available",
83
+ expiresAt,
84
+ ...(sourceUrl ? { sourceUrl } : {}),
85
+ },
86
+ ];
87
+ });
88
+ }
89
+ function isRestrictedStream(stream) {
90
+ if (BLOCKED_TRANSLATION.test(stream.translation.trim()))
91
+ return true;
92
+ try {
93
+ return RESTRICTION_VIDEO.test(new URL(stream.link).pathname);
94
+ }
95
+ catch {
96
+ return true;
97
+ }
98
+ }
99
+ function uniqueOptions(options) {
100
+ const seen = new Set();
101
+ return options.filter((option) => {
102
+ const key = `${option.translation?.title ?? ""}\u0000${option.access.url}`;
103
+ if (seen.has(key))
104
+ return false;
105
+ seen.add(key);
106
+ return true;
107
+ });
108
+ }
109
+ //# sourceMappingURL=mapping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mapping.js","sourceRoot":"","sources":["../../src/filmix-streaming/mapping.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { MediaAvailability } from "@media-engine/core";
2
+ import type { FilmixPost, FilmixPostSummary } from "./client.js";
3
+ export declare function selectFilmixPost(posts: FilmixPostSummary[], query: MediaAvailability["query"]): FilmixPostSummary | undefined;
4
+ export declare function matchesSelectedFilmixPost(post: FilmixPost, selected: FilmixPostSummary): boolean;
@@ -0,0 +1,41 @@
1
+ import { normalizeProviderSearchText } from "../shared/mapping.js";
2
+ export function selectFilmixPost(posts, query) {
3
+ const title = query.title?.trim();
4
+ if (!title || !Number.isInteger(query.year))
5
+ return undefined;
6
+ const normalizedTitle = normalizeProviderSearchText(title);
7
+ if (!normalizedTitle)
8
+ return undefined;
9
+ const sameIdentity = uniqueById(posts.filter((post) => post.year === query.year && post.type === query.type));
10
+ const exact = sameIdentity.filter((post) => hasExactTitle(post, normalizedTitle));
11
+ if (exact.length === 1)
12
+ return exact[0];
13
+ if (exact.length > 1)
14
+ return undefined;
15
+ // Filmix search can return a localized title that differs from the catalog title.
16
+ // A fallback is safe only when search itself yields one year/type identity.
17
+ return sameIdentity.length === 1 ? sameIdentity[0] : undefined;
18
+ }
19
+ export function matchesSelectedFilmixPost(post, selected) {
20
+ if (post.id !== selected.id ||
21
+ post.year !== selected.year ||
22
+ post.section !== selected.section ||
23
+ post.type !== selected.type) {
24
+ return false;
25
+ }
26
+ const selectedTitles = normalizedTitles(selected);
27
+ return normalizedTitles(post).some((title) => selectedTitles.includes(title));
28
+ }
29
+ function hasExactTitle(post, normalizedTitle) {
30
+ return normalizedTitles(post).includes(normalizedTitle);
31
+ }
32
+ function normalizedTitles(post) {
33
+ return [post.title, post.originalTitle]
34
+ .filter((title) => Boolean(title))
35
+ .map(normalizeProviderSearchText)
36
+ .filter(Boolean);
37
+ }
38
+ function uniqueById(posts) {
39
+ return [...new Map(posts.map((post) => [post.id, post])).values()];
40
+ }
41
+ //# sourceMappingURL=matching.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matching.js","sourceRoot":"","sources":["../../src/filmix-streaming/matching.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AAGnE,MAAM,UAAU,gBAAgB,CAC9B,KAA0B,EAC1B,KAAiC;IAEjC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC;IAClC,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IAE9D,MAAM,eAAe,GAAG,2BAA2B,CAAC,KAAK,CAAC,CAAC;IAC3D,IAAI,CAAC,eAAe;QAAE,OAAO,SAAS,CAAC;IAEvC,MAAM,YAAY,GAAG,UAAU,CAC7B,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC,CAC7E,CAAC;IACF,MAAM,KAAK,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC;IAElF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;IACxC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IAEvC,kFAAkF;IAClF,4EAA4E;IAC5E,OAAO,YAAY,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,IAAgB,EAAE,QAA2B;IACrF,IACE,IAAI,CAAC,EAAE,KAAK,QAAQ,CAAC,EAAE;QACvB,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,IAAI;QAC3B,IAAI,CAAC,OAAO,KAAK,QAAQ,CAAC,OAAO;QACjC,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,IAAI,EAC3B,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,cAAc,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAClD,OAAO,gBAAgB,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,cAAc,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;AAChF,CAAC;AAED,SAAS,aAAa,CAAC,IAAuB,EAAE,eAAuB;IACrE,OAAO,gBAAgB,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAuB;IAC/C,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,aAAa,CAAC;SACpC,MAAM,CAAC,CAAC,KAAK,EAAmB,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;SAClD,GAAG,CAAC,2BAA2B,CAAC;SAChC,MAAM,CAAC,OAAO,CAAC,CAAC;AACrB,CAAC;AAED,SAAS,UAAU,CAAC,KAA0B;IAC5C,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;AACrE,CAAC"}
@@ -0,0 +1,12 @@
1
+ import { type ProviderContext } from "@media-engine/core";
2
+ import { type ProviderFetch, type ProviderRateLimitGate } from "../shared/index.js";
3
+ export interface FlixHqHttpConfig {
4
+ baseUrl: string;
5
+ name: string;
6
+ fetch?: ProviderFetch;
7
+ rateLimitGate: ProviderRateLimitGate;
8
+ maxHtmlBytes: number;
9
+ userAgent: string;
10
+ }
11
+ export declare function fetchFlixHqText(config: FlixHqHttpConfig, initialUrl: URL, context: ProviderContext, init?: RequestInit): Promise<string>;
12
+ export declare function normalizeFlixHqNavigationUrl(value: string | null | undefined, baseUrl: string | undefined): string | undefined;
@@ -0,0 +1,103 @@
1
+ import { ProviderError } from "@media-engine/core";
2
+ import { deferProviderRateLimitFromResponse, mapProviderHttpError, mapHttpResponseToProviderError, normalizePublicHttpUrl, } from "../shared/index.js";
3
+ import { readBoundedResponseText } from "../shared/response-body.js";
4
+ const MAX_NAVIGATION_REDIRECTS = 3;
5
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
6
+ // Reads only configured-origin FlixHQ pages; redirects are manual and origin-checked per hop.
7
+ // Читает только страницы configured origin FlixHQ с ручной проверкой каждого redirect hop.
8
+ export async function fetchFlixHqText(config, initialUrl, context, init = {}) {
9
+ const fetchImpl = config.fetch ?? fetch;
10
+ let url = assertSameOrigin(config, initialUrl);
11
+ const visited = new Set();
12
+ try {
13
+ for (let redirects = 0;; redirects += 1) {
14
+ if (visited.has(url.href)) {
15
+ throw createNavigationError(config.name, "redirect loop");
16
+ }
17
+ visited.add(url.href);
18
+ await config.rateLimitGate.wait(context.signal);
19
+ const response = await fetchImpl(url, {
20
+ ...init,
21
+ headers: {
22
+ ...createHeaders(config),
23
+ ...Object.fromEntries(new Headers(init.headers).entries()),
24
+ },
25
+ redirect: "manual",
26
+ signal: context.signal,
27
+ });
28
+ deferProviderRateLimitFromResponse(config.rateLimitGate, response);
29
+ const location = REDIRECT_STATUSES.has(response.status)
30
+ ? response.headers.get("location")
31
+ : null;
32
+ if (location) {
33
+ await cancelResponse(response);
34
+ if (redirects >= MAX_NAVIGATION_REDIRECTS) {
35
+ throw createNavigationError(config.name, "redirect limit exceeded");
36
+ }
37
+ url = assertSameOrigin(config, new URL(location, url));
38
+ continue;
39
+ }
40
+ if (!response.ok) {
41
+ throw mapHttpResponseToProviderError(config.name, response);
42
+ }
43
+ return await readBoundedResponseText(config.name, response, config.maxHtmlBytes, {
44
+ signal: context.signal,
45
+ });
46
+ }
47
+ }
48
+ catch (error) {
49
+ throw mapProviderHttpError(config.name, error);
50
+ }
51
+ }
52
+ export function normalizeFlixHqNavigationUrl(value, baseUrl) {
53
+ if (!value) {
54
+ return undefined;
55
+ }
56
+ try {
57
+ if (!baseUrl) {
58
+ return normalizePublicHttpUrl(value);
59
+ }
60
+ const url = baseUrl ? new URL(value, baseUrl) : new URL(value);
61
+ if ((url.protocol !== "http:" && url.protocol !== "https:") ||
62
+ url.username !== "" ||
63
+ url.password !== "" ||
64
+ (baseUrl && url.origin !== new URL(baseUrl).origin)) {
65
+ return undefined;
66
+ }
67
+ return url.href;
68
+ }
69
+ catch {
70
+ return undefined;
71
+ }
72
+ }
73
+ async function cancelResponse(response) {
74
+ try {
75
+ await response.body?.cancel();
76
+ }
77
+ catch {
78
+ // Redirect cleanup must not hide the navigation policy result.
79
+ }
80
+ }
81
+ function assertSameOrigin(config, url) {
82
+ if (normalizeFlixHqNavigationUrl(url.href, config.baseUrl) !== url.href) {
83
+ throw createNavigationError(config.name, "cross-origin navigation");
84
+ }
85
+ return url;
86
+ }
87
+ function createHeaders(config) {
88
+ return {
89
+ Accept: "text/html,application/json;q=0.9,*/*;q=0.8",
90
+ "User-Agent": config.userAgent,
91
+ Referer: `${config.baseUrl}/`,
92
+ "X-Requested-With": "XMLHttpRequest",
93
+ };
94
+ }
95
+ function createNavigationError(provider, reason) {
96
+ return new ProviderError({
97
+ provider,
98
+ code: "PROVIDER_INVALID_RESPONSE",
99
+ message: `Provider "${provider}" returned unsafe FlixHQ navigation (${reason}).`,
100
+ retryable: false,
101
+ });
102
+ }
103
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/flixhq-streaming/client.ts"],"names":[],"mappings":"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"}
@@ -25,6 +25,6 @@ interface EpisodeCandidate extends StreamEpisodeRef {
25
25
  }
26
26
  export declare function flixHqStreamingProvider(options?: FlixHqStreamingProviderOptions): StreamingProvider;
27
27
  export declare function parseSubtitleInfo(value: string): SubtitleTrack[];
28
- export declare function parseSearchCandidates(html: string): SearchCandidate[];
29
- export declare function parseEpisodes(html: string): EpisodeCandidate[];
28
+ export declare function parseSearchCandidates(html: string, baseUrl?: string): SearchCandidate[];
29
+ export declare function parseEpisodes(html: string, baseUrl?: string): EpisodeCandidate[];
30
30
  export {};
@@ -1,5 +1,9 @@
1
- import { deferProviderRateLimitFromResponse, mapProviderHttpError, mapHttpResponseToProviderError, normalizePublicHttpUrl, ProviderRateLimitGate, } from "../shared/index.js";
1
+ import { MEDIA_ENGINE_DEFAULT_USER_AGENT } from "../package-version.js";
2
+ import { mapProviderHttpError, normalizeProviderOutputUrl, ProviderRateLimitGate, } from "../shared/index.js";
2
3
  import { rethrowIfProviderAborted } from "../shared/abort.js";
4
+ import { readBoundedResponseText } from "../shared/response-body.js";
5
+ import { createHardenedProviderFetch } from "../shared/safe-fetch.js";
6
+ import { fetchFlixHqText, normalizeFlixHqNavigationUrl } from "./client.js";
3
7
  const DEFAULT_BASE_URL = "https://flixhq.one";
4
8
  const DEFAULT_PROVIDER_NAME = "flixhq-streaming";
5
9
  const DEFAULT_MAX_HTML_BYTES = 1024 * 1024;
@@ -22,6 +26,7 @@ export function flixHqStreamingProvider(options = {}) {
22
26
  };
23
27
  }
24
28
  function createConfig(options) {
29
+ const name = normalizeProviderName(options.name ?? DEFAULT_PROVIDER_NAME);
25
30
  const maxHtmlBytes = options.maxHtmlBytes ?? DEFAULT_MAX_HTML_BYTES;
26
31
  const playerLimit = options.playerLimit ?? DEFAULT_PLAYER_LIMIT;
27
32
  const playerValidationConcurrency = options.playerValidationConcurrency ?? DEFAULT_PLAYER_VALIDATION_CONCURRENCY;
@@ -48,8 +53,9 @@ function createConfig(options) {
48
53
  }
49
54
  return {
50
55
  baseUrl: normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL),
51
- name: normalizeProviderName(options.name ?? DEFAULT_PROVIDER_NAME),
56
+ name,
52
57
  fetch: options.fetch,
58
+ externalFetch: options.fetch ?? createHardenedProviderFetch({ provider: name }),
53
59
  rateLimitGate: new ProviderRateLimitGate(),
54
60
  maxHtmlBytes,
55
61
  playerLimit,
@@ -57,7 +63,7 @@ function createConfig(options) {
57
63
  playerValidationTimeoutMs,
58
64
  playerValidationMaxBytes,
59
65
  subtitleInfoMaxBytes,
60
- userAgent: options.userAgent?.trim() || "MediaEngine/0.0 (+https://github.com/Yaneart/media-engine)",
66
+ userAgent: options.userAgent?.trim() || MEDIA_ENGINE_DEFAULT_USER_AGENT,
61
67
  };
62
68
  }
63
69
  function createCapabilities() {
@@ -93,21 +99,21 @@ async function getFlixHqAvailability(config, query, context) {
93
99
  }
94
100
  const searchUrl = new URL("/search", config.baseUrl);
95
101
  searchUrl.searchParams.set("keyword", query.title.trim());
96
- const searchHtml = await fetchText(config, searchUrl, context);
97
- const candidate = selectBestCandidate(parseSearchCandidates(searchHtml), query);
102
+ const searchHtml = await fetchFlixHqText(config, searchUrl, context);
103
+ const candidate = selectBestCandidate(parseSearchCandidates(searchHtml, config.baseUrl), query);
98
104
  if (!candidate) {
99
105
  return emptyAvailability(query);
100
106
  }
101
- const mediaHtml = await fetchText(config, new URL(candidate.mediaUrl), context);
107
+ const mediaHtml = await fetchFlixHqText(config, new URL(candidate.mediaUrl), context);
102
108
  const episode = candidate.type === "movie"
103
109
  ? { url: candidate.mediaUrl, title: candidate.title }
104
- : selectEpisode(parseEpisodes(mediaHtml), query.seasonNumber ?? 1, query.episodeNumber ?? 1);
110
+ : selectEpisode(parseEpisodes(mediaHtml, config.baseUrl), query.seasonNumber ?? 1, query.episodeNumber ?? 1);
105
111
  if (!episode) {
106
112
  return emptyAvailability(query);
107
113
  }
108
114
  const playerHtml = episode.url === candidate.mediaUrl
109
115
  ? mediaHtml
110
- : await fetchText(config, new URL(episode.url), context);
116
+ : await fetchFlixHqText(config, new URL(episode.url), context);
111
117
  const token = findPlayerToken(playerHtml, candidate.type);
112
118
  if (!token) {
113
119
  return emptyAvailability(query);
@@ -129,19 +135,29 @@ async function getFlixHqAvailability(config, query, context) {
129
135
  },
130
136
  options,
131
137
  episodes: [{ ...episode, options }],
132
- sourceProviders: [{ provider: config.name, url: candidate.mediaUrl, ids: query.ids }],
138
+ sourceProviders: [
139
+ {
140
+ provider: config.name,
141
+ url: normalizeProviderOutputUrl(candidate.mediaUrl),
142
+ ids: query.ids,
143
+ },
144
+ ],
133
145
  checkedAt: new Date().toISOString(),
134
146
  };
135
147
  }
136
148
  async function filterUnavailablePlayers(config, options, context) {
137
149
  const results = await mapWithConcurrency(options, config.playerValidationConcurrency, async (option) => ({
138
150
  option,
139
- unavailable: await isPlayerUnavailable(config, option, context),
151
+ outcome: await validatePlayer(config, option, context),
140
152
  }));
141
- return results.filter((result) => !result.unavailable).map((result) => result.option);
142
- }
143
- async function isPlayerUnavailable(config, option, context) {
144
- const fetchImpl = config.fetch ?? fetch;
153
+ return results
154
+ .filter((result) => result.outcome !== "unavailable")
155
+ .map((result) => result.outcome === "unknown"
156
+ ? { ...result.option, availability: "unknown" }
157
+ : result.option);
158
+ }
159
+ async function validatePlayer(config, option, context) {
160
+ const fetchImpl = config.externalFetch;
145
161
  const controller = new AbortController();
146
162
  const timeout = setTimeout(() => controller.abort(), config.playerValidationTimeoutMs);
147
163
  const signal = context.signal
@@ -163,22 +179,25 @@ async function isPlayerUnavailable(config, option, context) {
163
179
  },
164
180
  signal,
165
181
  });
166
- if (response.status === 404 || response.status === 410 || response.status >= 500) {
167
- return true;
182
+ if (response.status === 404 || response.status === 410) {
183
+ await response.body?.cancel();
184
+ return "unavailable";
185
+ }
186
+ if (!response.ok && response.status !== 206) {
187
+ await response.body?.cancel();
188
+ return "unknown";
168
189
  }
169
- if (!response.ok && response.status !== 206)
170
- return false;
171
190
  if (isDirect) {
172
191
  await response.body?.cancel();
173
- return false;
192
+ return "available";
174
193
  }
175
- const html = await readBoundedResponseText(response, config.playerValidationMaxBytes);
176
- return hasUnavailableMarker(html);
194
+ const html = await readBoundedResponseText(config.name, response, config.playerValidationMaxBytes, { signal, overflow: "truncate" });
195
+ return hasUnavailableMarker(html) ? "unavailable" : "available";
177
196
  }
178
197
  catch (error) {
179
198
  rethrowIfProviderAborted(context, error);
180
199
  // A validation timeout or transient network failure must not hide a discovered player.
181
- return false;
200
+ return "unknown";
182
201
  }
183
202
  finally {
184
203
  clearTimeout(timeout);
@@ -189,7 +208,7 @@ async function enrichPlayerOption(config, option, context) {
189
208
  if (!subtitleInfoUrl)
190
209
  return option;
191
210
  try {
192
- const response = await (config.fetch ?? fetch)(subtitleInfoUrl, {
211
+ const response = await config.externalFetch(subtitleInfoUrl, {
193
212
  headers: {
194
213
  Accept: "application/json",
195
214
  "User-Agent": config.userAgent,
@@ -199,12 +218,9 @@ async function enrichPlayerOption(config, option, context) {
199
218
  });
200
219
  if (!response.ok)
201
220
  return option;
202
- const declaredLength = Number(response.headers.get("content-length"));
203
- if (Number.isFinite(declaredLength) && declaredLength > config.subtitleInfoMaxBytes) {
204
- await response.body?.cancel();
205
- return option;
206
- }
207
- const text = await readBoundedResponseText(response, config.subtitleInfoMaxBytes);
221
+ const text = await readBoundedResponseText(config.name, response, config.subtitleInfoMaxBytes, {
222
+ signal: context.signal,
223
+ });
208
224
  const subtitles = parseSubtitleInfo(text);
209
225
  return subtitles.length > 0 ? { ...option, subtitles } : option;
210
226
  }
@@ -227,36 +243,6 @@ async function mapWithConcurrency(values, concurrency, mapper) {
227
243
  await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, async () => worker()));
228
244
  return results;
229
245
  }
230
- async function readBoundedResponseText(response, maxBytes) {
231
- if (!response.body)
232
- return "";
233
- const reader = response.body.getReader();
234
- const chunks = [];
235
- let totalBytes = 0;
236
- try {
237
- while (totalBytes <= maxBytes) {
238
- const { done, value } = await reader.read();
239
- if (done)
240
- break;
241
- totalBytes += value.byteLength;
242
- if (totalBytes > maxBytes) {
243
- await reader.cancel();
244
- break;
245
- }
246
- chunks.push(value);
247
- }
248
- }
249
- finally {
250
- reader.releaseLock();
251
- }
252
- const body = new Uint8Array(chunks.reduce((size, chunk) => size + chunk.byteLength, 0));
253
- let offset = 0;
254
- for (const chunk of chunks) {
255
- body.set(chunk, offset);
256
- offset += chunk.byteLength;
257
- }
258
- return new TextDecoder().decode(body);
259
- }
260
246
  function hasUnavailableMarker(html) {
261
247
  const normalized = normalizeTitle(html);
262
248
  return (normalized.includes("video not found") ||
@@ -268,10 +254,9 @@ function hasUnavailableMarker(html) {
268
254
  async function fetchPlayers(config, token, type, context) {
269
255
  const body = new URLSearchParams();
270
256
  body.set(type === "movie" ? "players" : "players_show", token);
271
- const responseText = await fetchText(config, new URL("/ajax/ajax.php", config.baseUrl), context, {
257
+ const responseText = await fetchFlixHqText(config, new URL("/ajax/ajax.php", config.baseUrl), context, {
272
258
  method: "POST",
273
259
  headers: {
274
- ...createHeaders(config),
275
260
  "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
276
261
  },
277
262
  body: body.toString(),
@@ -293,6 +278,8 @@ function mapPlayer(config, player, episode, index) {
293
278
  const kind = inferPlayerKind(url);
294
279
  const quality = inferQuality(label, url);
295
280
  const expiresAt = inferExpiresAt(url);
281
+ const referer = normalizeProviderOutputUrl(`${config.baseUrl}/`);
282
+ const sourceUrl = normalizeProviderOutputUrl(episode.url);
296
283
  return {
297
284
  id: `${config.name}:${episode.seasonNumber ?? 0}:${episode.episodeNumber ?? 0}:${index + 1}`,
298
285
  provider: config.name,
@@ -307,13 +294,12 @@ function mapPlayer(config, player, episode, index) {
307
294
  },
308
295
  access: {
309
296
  url,
310
- referer: `${config.baseUrl}/`,
311
- headers: { Referer: `${config.baseUrl}/` },
297
+ ...(referer ? { referer, headers: { Referer: referer } } : {}),
312
298
  },
313
299
  availability: "available",
314
300
  ...(quality ? { quality } : {}),
315
301
  ...(expiresAt ? { expiresAt } : {}),
316
- sourceUrl: episode.url,
302
+ sourceUrl,
317
303
  };
318
304
  }
319
305
  export function parseSubtitleInfo(value) {
@@ -348,11 +334,11 @@ export function parseSubtitleInfo(value) {
348
334
  }
349
335
  return tracks;
350
336
  }
351
- export function parseSearchCandidates(html) {
337
+ export function parseSearchCandidates(html, baseUrl) {
352
338
  const candidates = [];
353
339
  const seen = new Set();
354
340
  for (const anchor of parseAnchors(html)) {
355
- const href = normalizeHttpUrl(getAttribute(anchor.attributes, "href"));
341
+ const href = normalizeFlixHqNavigationUrl(getAttribute(anchor.attributes, "href"), baseUrl);
356
342
  const type = href?.includes("/watch-movie/")
357
343
  ? "movie"
358
344
  : href?.includes("/watch-series/")
@@ -370,9 +356,9 @@ export function parseSearchCandidates(html) {
370
356
  }
371
357
  return candidates;
372
358
  }
373
- export function parseEpisodes(html) {
359
+ export function parseEpisodes(html, baseUrl) {
374
360
  return parseAnchors(html).flatMap((anchor) => {
375
- const href = normalizeHttpUrl(getAttribute(anchor.attributes, "href"));
361
+ const href = normalizeFlixHqNavigationUrl(getAttribute(anchor.attributes, "href"), baseUrl);
376
362
  const match = href?.match(/\/s(\d+)-e(\d+)\/?$/i);
377
363
  if (!href || !match)
378
364
  return [];
@@ -430,41 +416,6 @@ function getAttribute(attributes, name) {
430
416
  const expression = new RegExp(`(?:^|\\s)${escapedName}\\s*=\\s*(["'])(.*?)\\1`, "i");
431
417
  return attributes.match(expression)?.[2];
432
418
  }
433
- async function fetchText(config, url, context, init = {}) {
434
- const fetchImpl = config.fetch ?? fetch;
435
- try {
436
- await config.rateLimitGate.wait(context.signal);
437
- const response = await fetchImpl(url, {
438
- ...init,
439
- headers: init.headers ?? createHeaders(config),
440
- signal: context.signal,
441
- });
442
- deferProviderRateLimitFromResponse(config.rateLimitGate, response);
443
- if (!response.ok) {
444
- throw mapHttpResponseToProviderError(config.name, response);
445
- }
446
- const declaredLength = Number(response.headers.get("content-length"));
447
- if (Number.isFinite(declaredLength) && declaredLength > config.maxHtmlBytes) {
448
- throw new Error(`Provider "${config.name}" returned an oversized response.`);
449
- }
450
- const body = await response.arrayBuffer();
451
- if (body.byteLength > config.maxHtmlBytes) {
452
- throw new Error(`Provider "${config.name}" returned an oversized response.`);
453
- }
454
- return new TextDecoder().decode(body);
455
- }
456
- catch (error) {
457
- throw mapProviderHttpError(config.name, error);
458
- }
459
- }
460
- function createHeaders(config) {
461
- return {
462
- Accept: "text/html,application/json;q=0.9,*/*;q=0.8",
463
- "User-Agent": config.userAgent,
464
- Referer: `${config.baseUrl}/`,
465
- "X-Requested-With": "XMLHttpRequest",
466
- };
467
- }
468
419
  function isPlayerResponse(value) {
469
420
  return typeof value === "object" && value !== null;
470
421
  }
@@ -473,6 +424,9 @@ function normalizeBaseUrl(value) {
473
424
  if (url.protocol !== "https:" && url.protocol !== "http:") {
474
425
  throw new TypeError("FlixHQ streaming baseUrl must use HTTP or HTTPS.");
475
426
  }
427
+ if (url.username !== "" || url.password !== "") {
428
+ throw new TypeError("FlixHQ streaming baseUrl must not include credentials.");
429
+ }
476
430
  return url.href.replace(/\/$/, "");
477
431
  }
478
432
  function normalizeProviderName(value) {
@@ -482,7 +436,7 @@ function normalizeProviderName(value) {
482
436
  return name;
483
437
  }
484
438
  function normalizeHttpUrl(value) {
485
- return normalizePublicHttpUrl(value);
439
+ return normalizeProviderOutputUrl(value);
486
440
  }
487
441
  function findSubtitleInfoUrl(playerUrl) {
488
442
  try {